diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -4,3 +4,11 @@ This release includes the following features and fixes: - Using addresses in createmultisig is now deprectated. Use -deprecatedrpc=createmultisig to get the old behavior. + - `-includeconf=` can be used to include additional configuration files. + Only works inside the `bitcoin.conf` file, not inside included files or from + command-line. Multiple files may be included. Can be disabled from command- + line via `-noincludeconf`. Note that multi-argument commands like + `-includeconf` will override preceding `-noincludeconf`, i.e. + noincludeconf=1 + includeconf=relative.conf + as bitcoin.conf will still include `relative.conf`. \ No newline at end of file diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -150,7 +150,7 @@ return EXIT_FAILURE; } try { - gArgs.ReadConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)); + gArgs.ReadConfigFiles(); } catch (const std::exception &e) { fprintf(stderr, "Error reading configuration file: %s\n", e.what()); return EXIT_FAILURE; diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp --- a/src/bitcoind.cpp +++ b/src/bitcoind.cpp @@ -102,7 +102,7 @@ return false; } try { - gArgs.ReadConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)); + gArgs.ReadConfigFiles(); } catch (const std::exception &e) { fprintf(stderr, "Error reading configuration file: %s\n", e.what()); return false; diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -387,6 +387,10 @@ "our mempool min fee (default: %d)", DEFAULT_FEEFILTER)); } + strUsage += HelpMessageOpt( + "-includeconf=", + _("Specify additional configuration file, relative to the -datadir " + "path (only useable from configuration file, not command line)")); strUsage += HelpMessageOpt( "-finalizationdelay=", strprintf("Set the minimum amount of time to wait between a " diff --git a/src/interfaces/node.h b/src/interfaces/node.h --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -52,7 +52,7 @@ virtual bool softSetBoolArg(const std::string &arg, bool value) = 0; //! Load settings from configuration file. - virtual void readConfigFile(const std::string &conf_path) = 0; + virtual void readConfigFiles() = 0; //! Choose network parameters. virtual void selectParams(const std::string &network) = 0; diff --git a/src/interfaces/node.cpp b/src/interfaces/node.cpp --- a/src/interfaces/node.cpp +++ b/src/interfaces/node.cpp @@ -54,9 +54,7 @@ void parseParameters(int argc, const char *const argv[]) override { gArgs.ParseParameters(argc, argv); } - void readConfigFile(const std::string &conf_path) override { - gArgs.ReadConfigFile(conf_path); - } + void readConfigFiles() override { gArgs.ReadConfigFiles(); } bool softSetArg(const std::string &arg, const std::string &value) override { return gArgs.SoftSetArg(arg, value); diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -669,7 +669,7 @@ return EXIT_FAILURE; } try { - node->readConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)); + node->readConfigFiles(); } catch (const std::exception &e) { QMessageBox::critical( 0, QObject::tr(PACKAGE_NAME), diff --git a/src/util.h b/src/util.h --- a/src/util.h +++ b/src/util.h @@ -126,7 +126,7 @@ void SelectConfigNetwork(const std::string &network); void ParseParameters(int argc, const char *const argv[]); - void ReadConfigFile(const std::string &confPath); + void ReadConfigFiles(); /** * Log warnings for options in m_section_only_args when they are specified diff --git a/src/util.cpp b/src/util.cpp --- a/src/util.cpp +++ b/src/util.cpp @@ -470,6 +470,20 @@ m_override_args[key].push_back(val); } } + + // we do not allow -includeconf from command line, so we clear it here + auto it = m_override_args.find("-includeconf"); + if (it != m_override_args.end()) { + if (it->second.size() > 0) { + for (const auto &ic : it->second) { + fprintf(stderr, + "warning: -includeconf cannot be used from " + "commandline; ignoring -includeconf=%s\n", + ic.c_str()); + } + m_override_args.erase(it); + } + } } std::vector ArgsManager::GetArgs(const std::string &strArg) const { @@ -783,17 +797,50 @@ } } -void ArgsManager::ReadConfigFile(const std::string &confPath) { +void ArgsManager::ReadConfigFiles() { { LOCK(cs_args); m_config_args.clear(); } + const std::string confPath = GetArg("-conf", BITCOIN_CONF_FILENAME); fs::ifstream stream(GetConfigFile(confPath)); // ok to not have a config file if (stream.good()) { ReadConfigStream(stream); + // if there is an -includeconf in the override args, but it is empty, + // that means the user passed '-noincludeconf' on the command line, in + // which case we should not include anything + bool emptyIncludeConf; + { + LOCK(cs_args); + emptyIncludeConf = m_override_args.count("-includeconf") == 0; + } + if (emptyIncludeConf) { + std::vector includeconf(GetArgs("-includeconf")); + { + // We haven't set m_network yet (that happens in + // SelectParams()), so manually check for network.includeconf + // args. + std::vector includeconf_net(GetArgs( + std::string("-") + GetChainName() + ".includeconf")); + includeconf.insert(includeconf.end(), includeconf_net.begin(), + includeconf_net.end()); + } + + for (const std::string &to_include : includeconf) { + fs::ifstream include_config(GetConfigFile(to_include)); + if (include_config.good()) { + ReadConfigStream(include_config); + LogPrintf("Included configuration file %s\n", + to_include.c_str()); + } else { + fprintf(stderr, "Failed to include configuration file %s\n", + to_include.c_str()); + } + } + } } // If datadir is changed in .conf file: diff --git a/test/functional/feature_includeconf.py b/test/functional/feature_includeconf.py new file mode 100755 --- /dev/null +++ b/test/functional/feature_includeconf.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Tests the includeconf argument + +Verify that: + +1. adding includeconf to the configuration file causes the includeconf + file to be loaded in the correct order. +2. includeconf cannot be used as a command line argument. +3. includeconf cannot be used recursively (ie includeconf can only + be used from the base config file). +4. multiple includeconf arguments can be specified in the main config + file. +""" +import os +import tempfile + +from test_framework.test_framework import BitcoinTestFramework, assert_equal + + +class IncludeConfTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 1 + + def setup_chain(self): + super().setup_chain() + # Create additional config files + # - tmpdir/node0/relative.conf + with open(os.path.join(self.options.tmpdir, "node0", "relative.conf"), "w", encoding="utf8") as f: + f.write("uacomment=relative\n") + # - tmpdir/node0/relative2.conf + with open(os.path.join(self.options.tmpdir, "node0", "relative2.conf"), "w", encoding="utf8") as f: + f.write("uacomment=relative2\n") + with open(os.path.join(self.options.tmpdir, "node0", "bitcoin.conf"), "a", encoding='utf8') as f: + f.write("uacomment=main\nincludeconf=relative.conf\n") + + def run_test(self): + self.log.info( + "-includeconf works from config file. subversion should end with 'main; relative)/'") + + subversion = self.nodes[0].getnetworkinfo()["subversion"] + assert subversion.endswith("main; relative)/") + + self.log.info( + "-includeconf cannot be used as command-line arg. subversion should still end with 'main; relative)/'") + self.stop_node(0) + with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: + self.start_node( + 0, extra_args=["-includeconf=relative2.conf"], stderr=log_stderr) + + subversion = self.nodes[0].getnetworkinfo()["subversion"] + assert subversion.endswith("main; relative)/") + log_stderr.seek(0) + stderr = log_stderr.read().decode('utf-8').strip() + assert_equal( + stderr, 'warning: -includeconf cannot be used from commandline; ignoring -includeconf=relative2.conf') + + self.log.info( + "-includeconf cannot be used recursively. subversion should end with 'main; relative)/'") + with open(os.path.join(self.options.tmpdir, "node0", "relative.conf"), "a", encoding="utf8") as f: + f.write("includeconf=relative2.conf\n") + + self.restart_node(0) + + subversion = self.nodes[0].getnetworkinfo()["subversion"] + assert subversion.endswith("main; relative)/") + + self.log.info( + "multiple -includeconf args can be used from the base config file. subversion should end with 'main; relative; relative2)/'") + with open(os.path.join(self.options.tmpdir, "node0", "relative.conf"), "w", encoding="utf8") as f: + f.write("uacomment=relative\n") + + with open(os.path.join(self.options.tmpdir, "node0", "bitcoin.conf"), "a", encoding='utf8') as f: + f.write("includeconf=relative2.conf\n") + + self.restart_node(0) + + subversion = self.nodes[0].getnetworkinfo()["subversion"] + assert subversion.endswith("main; relative; relative2)/") + + +if __name__ == '__main__': + IncludeConfTest().main() diff --git a/test/functional/timing.json b/test/functional/timing.json --- a/test/functional/timing.json +++ b/test/functional/timing.json @@ -103,6 +103,10 @@ "name": "feature_dersig.py", "time": 3 }, + { + "name": "feature_includeconf.py", + "time": 11 + }, { "name": "feature_logging.py", "time": 17