Skip to content

Commit 1d1f8bb

Browse files
author
MarcoFalke
committed
Merge #16115: On bitcoind startup, write config args to debug.log
b951b09 on startup, write config options to debug.log (Larry Ruane) Pull request description: When a developer is examining `debug.log` after something goes wrong, it's often useful to know the exact options the failing instance of `bitcoind` was started with. Sometimes the `debug.log` file is all that's available for the analysis. This PR logs the `bitcoin.conf` entries and command-line arguments to `debug.log` on startup. ACKs for top commit: MarcoFalke: ACK b951b09 🐪 jonatack: ACK b951b09 reviewed diff, re-code review, built, ran tests, launched bitcoind and reviewed debug log output, verified value of `str` debug log in the added unit test. Tree-SHA512: bbca4fb3d49f99261758302bde0b8b67300ccc72e7380b01f1f66a146ae8a008a045df0ca5ca9664caff034d0ee38ea7ef38a50f38374525608c07ba52790358
2 parents 3b69310 + b951b09 commit 1d1f8bb

File tree

6 files changed

+113
-6
lines changed

6 files changed

+113
-6
lines changed

src/init.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ void SetupServerArgs()
436436
gArgs.AddArg("-timeout=<n>", strprintf("Specify connection timeout in milliseconds (minimum: 1, default: %d)", DEFAULT_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
437437
gArgs.AddArg("-peertimeout=<n>", strprintf("Specify p2p connection timeout in seconds. This option determines the amount of time a peer may be inactive before the connection to it is dropped. (minimum: 1, default: %d)", DEFAULT_PEER_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CONNECTION);
438438
gArgs.AddArg("-torcontrol=<ip>:<port>", strprintf("Tor control port to use if onion listening enabled (default: %s)", DEFAULT_TOR_CONTROL), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
439-
gArgs.AddArg("-torpassword=<pass>", "Tor control port password (default: empty)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
439+
gArgs.AddArg("-torpassword=<pass>", "Tor control port password (default: empty)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::CONNECTION);
440440
gArgs.AddArg("-asmap=<file>", "Specify asn mapping used for bucketing of the peers. Path should be relative to the -datadir path.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
441441
#ifdef USE_UPNP
442442
#if USE_UPNP
@@ -537,15 +537,15 @@ void SetupServerArgs()
537537

538538
gArgs.AddArg("-rest", strprintf("Accept public REST requests (default: %u)", DEFAULT_REST_ENABLE), ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
539539
gArgs.AddArg("-rpcallowip=<ip>", "Allow JSON-RPC connections from specified source. Valid for <ip> are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This option can be specified multiple times", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
540-
gArgs.AddArg("-rpcauth=<userpw>", "Username and HMAC-SHA-256 hashed password for JSON-RPC connections. The field <userpw> comes in the format: <USERNAME>:<SALT>$<HASH>. A canonical python script is included in share/rpcauth. The client then connects normally using the rpcuser=<USERNAME>/rpcpassword=<PASSWORD> pair of arguments. This option can be specified multiple times", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
541-
gArgs.AddArg("-rpcbind=<addr>[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC);
540+
gArgs.AddArg("-rpcauth=<userpw>", "Username and HMAC-SHA-256 hashed password for JSON-RPC connections. The field <userpw> comes in the format: <USERNAME>:<SALT>$<HASH>. A canonical python script is included in share/rpcauth. The client then connects normally using the rpcuser=<USERNAME>/rpcpassword=<PASSWORD> pair of arguments. This option can be specified multiple times", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
541+
gArgs.AddArg("-rpcbind=<addr>[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
542542
gArgs.AddArg("-rpccookiefile=<loc>", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
543-
gArgs.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
543+
gArgs.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
544544
gArgs.AddArg("-rpcport=<port>", strprintf("Listen for JSON-RPC connections on <port> (default: %u, testnet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC);
545545
gArgs.AddArg("-rpcserialversion", strprintf("Sets the serialization of raw transaction or block hex returned in non-verbose mode, non-segwit(0) or segwit(1) (default: %d)", DEFAULT_RPC_SERIALIZE_VERSION), ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
546546
gArgs.AddArg("-rpcservertimeout=<n>", strprintf("Timeout during HTTP requests (default: %d)", DEFAULT_HTTP_SERVER_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
547547
gArgs.AddArg("-rpcthreads=<n>", strprintf("Set the number of threads to service RPC calls (default: %d)", DEFAULT_HTTP_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
548-
gArgs.AddArg("-rpcuser=<user>", "Username for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
548+
gArgs.AddArg("-rpcuser=<user>", "Username for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
549549
gArgs.AddArg("-rpcwhitelist=<whitelist>", "Set a whitelist to filter incoming RPC calls for a specific user. The field <whitelist> comes in the format: <USERNAME>:<rpc 1>,<rpc 2>,...,<rpc n>. If multiple whitelists are set for a given user, they are set-intersected. See -rpcwhitelistdefault documentation for information on default whitelist behavior.", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
550550
gArgs.AddArg("-rpcwhitelistdefault", "Sets default behavior for rpc whitelisting. Unless rpcwhitelistdefault is set to 0, if any -rpcwhitelist is set, the rpc server acts as if all rpc users are subject to empty-unless-otherwise-specified whitelists. If rpcwhitelistdefault is set to 1 and no -rpcwhitelist is set, rpc server acts as if all rpc users are subject to empty whitelists.", ArgsManager::ALLOW_BOOL, OptionsCategory::RPC);
551551
gArgs.AddArg("-rpcworkqueue=<n>", strprintf("Set the depth of the work queue to service RPC calls (default: %d)", DEFAULT_HTTP_WORKQUEUE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
@@ -1230,6 +1230,9 @@ bool AppInitMain(NodeContext& node)
12301230
LogPrintf("Config file: %s (not found, skipping)\n", config_file_path.string());
12311231
}
12321232

1233+
// Log the config arguments to debug.log
1234+
gArgs.LogArgs();
1235+
12331236
LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD);
12341237

12351238
// Warn about relative -datadir path.

src/test/getarg_tests.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,32 @@ BOOST_AUTO_TEST_CASE(boolargno)
183183
BOOST_CHECK(gArgs.GetBoolArg("-foo", false));
184184
}
185185

186+
BOOST_AUTO_TEST_CASE(logargs)
187+
{
188+
const auto okaylog_bool = std::make_pair("-okaylog-bool", ArgsManager::ALLOW_BOOL);
189+
const auto okaylog_negbool = std::make_pair("-okaylog-negbool", ArgsManager::ALLOW_BOOL);
190+
const auto okaylog = std::make_pair("-okaylog", ArgsManager::ALLOW_ANY);
191+
const auto dontlog = std::make_pair("-dontlog", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE);
192+
SetupArgs({okaylog_bool, okaylog_negbool, okaylog, dontlog});
193+
ResetArgs("-okaylog-bool -nookaylog-negbool -okaylog=public -dontlog=private");
194+
195+
// Everything logged to debug.log will also append to str
196+
std::string str;
197+
auto print_connection = LogInstance().PushBackCallback(
198+
[&str](const std::string& s) {
199+
str += s;
200+
});
201+
202+
// Log the arguments
203+
gArgs.LogArgs();
204+
205+
LogInstance().DeleteCallback(print_connection);
206+
// Check that what should appear does, and what shouldn't doesn't.
207+
BOOST_CHECK(str.find("Command-line arg: okaylog-bool=\"\"") != std::string::npos);
208+
BOOST_CHECK(str.find("Command-line arg: okaylog-negbool=false") != std::string::npos);
209+
BOOST_CHECK(str.find("Command-line arg: okaylog=\"public\"") != std::string::npos);
210+
BOOST_CHECK(str.find("dontlog=****") != std::string::npos);
211+
BOOST_CHECK(str.find("private") == std::string::npos);
212+
}
213+
186214
BOOST_AUTO_TEST_SUITE_END()

src/util/system.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,32 @@ std::vector<util::SettingsValue> ArgsManager::GetSettingsList(const std::string&
864864
return util::GetSettingsList(m_settings, m_network, SettingName(arg), !UseDefaultSection(arg));
865865
}
866866

867+
void ArgsManager::logArgsPrefix(
868+
const std::string& prefix,
869+
const std::string& section,
870+
const std::map<std::string, std::vector<util::SettingsValue>>& args) const
871+
{
872+
std::string section_str = section.empty() ? "" : "[" + section + "] ";
873+
for (const auto& arg : args) {
874+
for (const auto& value : arg.second) {
875+
Optional<unsigned int> flags = GetArgFlags('-' + arg.first);
876+
if (flags) {
877+
std::string value_str = (*flags & SENSITIVE) ? "****" : value.write();
878+
LogPrintf("%s %s%s=%s\n", prefix, section_str, arg.first, value_str);
879+
}
880+
}
881+
}
882+
}
883+
884+
void ArgsManager::LogArgs() const
885+
{
886+
LOCK(cs_args);
887+
for (const auto& section : m_settings.ro_config) {
888+
logArgsPrefix("Config file arg:", section.first, section.second);
889+
}
890+
logArgsPrefix("Command-line arg:", "", m_settings.command_line_options);
891+
}
892+
867893
bool RenameOver(fs::path src, fs::path dest)
868894
{
869895
#ifdef WIN32

src/util/system.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ class ArgsManager
145145
* between mainnet and regtest/testnet won't cause problems due to these
146146
* parameters by accident. */
147147
NETWORK_ONLY = 0x200,
148+
// This argument's value is sensitive (such as a password).
149+
SENSITIVE = 0x400,
148150
};
149151

150152
protected:
@@ -318,6 +320,19 @@ class ArgsManager
318320
* Return nullopt for unknown arg.
319321
*/
320322
Optional<unsigned int> GetArgFlags(const std::string& name) const;
323+
324+
/**
325+
* Log the config file options and the command line arguments,
326+
* useful for troubleshooting.
327+
*/
328+
void LogArgs() const;
329+
330+
private:
331+
// Helper function for LogArgs().
332+
void logArgsPrefix(
333+
const std::string& prefix,
334+
const std::string& section,
335+
const std::map<std::string, std::vector<util::SettingsValue>>& args) const;
321336
};
322337

323338
extern ArgsManager gArgs;

test/functional/feature_config_args.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,40 @@ def test_log_buffer(self):
8383
self.start_node(0, extra_args=['-noconnect=0'])
8484
self.stop_node(0)
8585

86+
def test_args_log(self):
87+
self.log.info('Test config args logging')
88+
with self.nodes[0].assert_debug_log(
89+
expected_msgs=[
90+
'Command-line arg: addnode="some.node"',
91+
'Command-line arg: rpcauth=****',
92+
'Command-line arg: rpcbind=****',
93+
'Command-line arg: rpcpassword=****',
94+
'Command-line arg: rpcuser=****',
95+
'Command-line arg: torpassword=****',
96+
'Config file arg: regtest="1"',
97+
'Config file arg: [regtest] server="1"',
98+
],
99+
unexpected_msgs=[
100+
'alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0',
101+
'127.1.1.1',
102+
'secret-rpcuser',
103+
'secret-torpassword',
104+
]):
105+
self.start_node(0, extra_args=[
106+
'-addnode=some.node',
107+
'-rpcauth=alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0',
108+
'-rpcbind=127.1.1.1',
109+
'-rpcpassword=',
110+
'-rpcuser=secret-rpcuser',
111+
'-torpassword=secret-torpassword',
112+
])
113+
self.stop_node(0)
114+
86115
def run_test(self):
87116
self.stop_node(0)
88117

89118
self.test_log_buffer()
119+
self.test_args_log()
90120

91121
self.test_config_file_parser()
92122

test/functional/test_framework/test_node.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT):
298298
wait_until(self.is_node_stopped, timeout=timeout)
299299

300300
@contextlib.contextmanager
301-
def assert_debug_log(self, expected_msgs, timeout=2):
301+
def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2):
302+
if unexpected_msgs is None:
303+
unexpected_msgs = []
302304
time_end = time.time() + timeout
303305
debug_log = os.path.join(self.datadir, self.chain, 'debug.log')
304306
with open(debug_log, encoding='utf-8') as dl:
@@ -313,6 +315,9 @@ def assert_debug_log(self, expected_msgs, timeout=2):
313315
dl.seek(prev_size)
314316
log = dl.read()
315317
print_log = " - " + "\n - ".join(log.splitlines())
318+
for unexpected_msg in unexpected_msgs:
319+
if re.search(re.escape(unexpected_msg), log, flags=re.MULTILINE):
320+
self._raise_assertion_error('Unexpected message "{}" partially matches log:\n\n{}\n\n'.format(unexpected_msg, print_log))
316321
for expected_msg in expected_msgs:
317322
if re.search(re.escape(expected_msg), log, flags=re.MULTILINE) is None:
318323
found = False

0 commit comments

Comments
 (0)