diff --git a/src/util/system.h b/src/util/system.h --- a/src/util/system.h +++ b/src/util/system.h @@ -94,6 +94,9 @@ #ifdef WIN32 fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true); #endif +#ifndef WIN32 +std::string ShellEscape(const std::string &arg); +#endif #if defined(HAVE_SYSTEM) void runCommand(const std::string &strCommand); #endif diff --git a/src/util/system.cpp b/src/util/system.cpp --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -64,6 +64,8 @@ #include #endif +#include + // Application startup time (used for uptime calculation) const int64_t nStartupTime = GetTime(); @@ -1263,6 +1265,14 @@ } #endif +#ifndef WIN32 +std::string ShellEscape(const std::string &arg) { + std::string escaped = arg; + boost::replace_all(escaped, "'", "'\"'\"'"); + return "'" + escaped + "'"; +} +#endif + #if defined(HAVE_SYSTEM) void runCommand(const std::string &strCommand) { if (strCommand.empty()) { diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -128,8 +128,11 @@ OptionsCategory::WALLET); #if defined(HAVE_SYSTEM) gArgs.AddArg("-walletnotify=", - "Execute command when a wallet transaction changes (%s in cmd " - "is replaced by TxID)", + "Execute command when a wallet transaction changes. %s in cmd " + "is replaced by TxID and %w is replaced by wallet name. %w is " + "not currently implemented on windows. On systems where %w is " + "supported, it should NOT be quoted because this would break " + "shell escaping used to invoke the command.", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); #endif gArgs.AddArg( diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -907,6 +907,15 @@ if (!strCmd.empty()) { boost::replace_all(strCmd, "%s", wtxIn.GetId().GetHex()); +#ifndef WIN32 + // Substituting the wallet name isn't currently supported on windows + // because windows shell escaping has not been implemented yet: + // https://github.com/bitcoin/bitcoin/pull/13339#issuecomment-537384875 + // A few ways it could be implemented in the future are described in: + // https://github.com/bitcoin/bitcoin/pull/13339#issuecomment-461288094 + boost::replace_all(strCmd, "%w", ShellEscape(GetName())); +#endif + std::thread t(runCommand, strCmd); // Thread runs free. t.detach(); diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -16,6 +16,16 @@ FORK_WARNING_MESSAGE = "Warning: Large-work fork detected, forking after block {}" +# Linux allow all characters other than \x00 +# Windows disallow control characters (0-31) and /\?%:|"<> +FILE_CHAR_START = 32 if os.name == 'nt' else 1 +FILE_CHAR_END = 128 +FILE_CHAR_BLACKLIST = '/\\?%*:|"<>' if os.name == 'nt' else '/' + + +def notify_outputname(walletname, txid): + return txid if os.name == 'nt' else '{}_{}'.format(walletname, txid) + class NotificationsTest(BitcoinTestFramework): def set_test_params(self): @@ -23,6 +33,10 @@ self.setup_clean_chain = True def setup_network(self): + self.wallet = ''.join( + chr(i) for i in range( + FILE_CHAR_START, + FILE_CHAR_END) if chr(i) not in FILE_CHAR_BLACKLIST) self.alertnotify_dir = os.path.join(self.options.tmpdir, "alertnotify") self.blocknotify_dir = os.path.join(self.options.tmpdir, "blocknotify") self.walletnotify_dir = os.path.join( @@ -37,7 +51,8 @@ "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s'))], ["-blockversion=211", "-rescan", - "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, '%s'))]] + "-wallet={}".format(self.wallet), + "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s')))]] super().setup_network() def run_test(self): @@ -71,8 +86,8 @@ timeout=10) # directory content should equal the generated transaction hashes - txids_rpc = list( - map(lambda t: t['txid'], self.nodes[1].listtransactions("*", block_count))) + txids_rpc = list(map(lambda t: notify_outputname( + self.wallet, t['txid']), self.nodes[1].listtransactions("*", block_count))) assert_equal( sorted(txids_rpc), sorted( os.listdir( @@ -93,8 +108,8 @@ timeout=10) # directory content should equal the generated transaction hashes - txids_rpc = list( - map(lambda t: t['txid'], self.nodes[1].listtransactions("*", block_count))) + txids_rpc = list(map(lambda t: notify_outputname( + self.wallet, t['txid']), self.nodes[1].listtransactions("*", block_count))) assert_equal( sorted(txids_rpc), sorted( os.listdir(