Skip to content

Commit 92cf3a2

Browse files
committed
Merge #21007: bitcoind: Add -daemonwait option to wait for initialization
e017a91 bitcoind: Add -daemonwait option to wait for initialization (Wladimir J. van der Laan) c3e6fde shutdown: Use RAII TokenPipe in shutdown (Wladimir J. van der Laan) 612f746 util: Add RAII TokenPipe (Wladimir J. van der Laan) Pull request description: This adds a `-daemonwait` flag that does the same as `-daemon` except that it, from a user perspective, backgrounds the process only after initialization is complete. This is similar to the behaviour of some other software such as c-lightning. This can be useful when the process launching bitcoind wants to guarantee that either the RPC server is running, or that initialization failed, before continuing. The exit code indicates the initialization result. The use of the libc function `daemon()` is replaced by a custom implementation which is inspired by the [glibc implementation](https://github.com/lattera/glibc/blob/master/misc/daemon.c#L44), but which also creates a pipe from the child to the parent process for communication. An additional advantage of having our own `daemon()` implementation is that no MACOS-specific pragmas are needed anymore to silence a deprecation warning. TODO: - [x] Factor out `token_read` and `token_write` to an utility, and use them in `shutdown.cpp` as well—this is exactly the same kind of communication mechanism. - [x] RAII-ify pipe endpoints. - [x] Improve granularity of the `configure.ac` checks. This currently still checks for the function `daemon()` which makes no sense as it's not used. It should check for individual functions such as `fork()` and `setsid()` etc—the former being required, the second optional. - [-] ~~Signal propagation during initialization: if say, pressing Ctrl-C during `-daemonwait` it would be good to pass this SIGINT on to the child process instead of detaching the parent process and letting the child run free.~~ This is not necessary, see bitcoin/bitcoin#21007 (comment). Future: - Consider if it makes sense to use this in the RPC tests (there would be no more need for "is RPC ready" polling loops). I think this is out of scope for this PR. ACKs for top commit: jonatack: Tested ACK e017a91 checked change since previous review is move-only Tree-SHA512: 53369b8ca2247e4cf3af8cb2cfd5b3399e8e0e3296423d64be987004758162a7ddc1287b01a92d7692328edcb2da4cf05d279b1b4ef61a665b71440ab6a6dbe2
2 parents e828fc8 + e017a91 commit 92cf3a2

File tree

9 files changed

+382
-57
lines changed

9 files changed

+382
-57
lines changed

build_msvc/bitcoin_config.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@
9292
don't. */
9393
#define HAVE_DECL_BSWAP_64 0
9494

95-
/* Define to 1 if you have the declaration of `daemon', and to 0 if you don't.
95+
/* Define to 1 if you have the declaration of `fork', and to 0 if you don't.
9696
*/
97-
#define HAVE_DECL_DAEMON 0
97+
#define HAVE_DECL_FORK 0
9898

9999
/* Define to 1 if you have the declaration of `htobe16', and to 0 if you
100100
don't. */
@@ -132,6 +132,10 @@
132132
don't. */
133133
#define HAVE_DECL_LE64TOH 0
134134

135+
/* Define to 1 if you have the declaration of `setsid', and to 0 if you don't.
136+
*/
137+
#define HAVE_DECL_SETSID 0
138+
135139
/* Define to 1 if you have the declaration of `strerror_r', and to 0 if you
136140
don't. */
137141
#define HAVE_DECL_STRERROR_R 0

configure.ac

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -922,8 +922,9 @@ AC_CHECK_DECLS([getifaddrs, freeifaddrs],,,
922922
)
923923
AC_CHECK_DECLS([strnlen])
924924

925-
dnl Check for daemon(3), unrelated to --with-daemon (although used by it)
926-
AC_CHECK_DECLS([daemon])
925+
dnl These are used for daemonization in bitcoind
926+
AC_CHECK_DECLS([fork])
927+
AC_CHECK_DECLS([setsid])
927928

928929
AC_CHECK_DECLS([pipe2])
929930

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ BITCOIN_CORE_H = \
253253
util/system.h \
254254
util/threadnames.h \
255255
util/time.h \
256+
util/tokenpipe.h \
256257
util/trace.h \
257258
util/translation.h \
258259
util/ui_change_type.h \
@@ -584,6 +585,7 @@ libbitcoin_util_a_SOURCES = \
584585
util/strencodings.cpp \
585586
util/string.cpp \
586587
util/time.cpp \
588+
util/tokenpipe.cpp \
587589
$(BITCOIN_CORE_H)
588590

589591
if USE_LIBEVENT

src/bitcoind.cpp

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <util/strencodings.h>
2121
#include <util/system.h>
2222
#include <util/threadnames.h>
23+
#include <util/tokenpipe.h>
2324
#include <util/translation.h>
2425
#include <util/url.h>
2526

@@ -28,6 +29,79 @@
2829
const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
2930
UrlDecodeFn* const URL_DECODE = urlDecode;
3031

32+
#if HAVE_DECL_FORK
33+
34+
/** Custom implementation of daemon(). This implements the same order of operations as glibc.
35+
* Opens a pipe to the child process to be able to wait for an event to occur.
36+
*
37+
* @returns 0 if successful, and in child process.
38+
* >0 if successful, and in parent process.
39+
* -1 in case of error (in parent process).
40+
*
41+
* In case of success, endpoint will be one end of a pipe from the child to parent process,
42+
* which can be used with TokenWrite (in the child) or TokenRead (in the parent).
43+
*/
44+
int fork_daemon(bool nochdir, bool noclose, TokenPipeEnd& endpoint)
45+
{
46+
// communication pipe with child process
47+
std::optional<TokenPipe> umbilical = TokenPipe::Make();
48+
if (!umbilical) {
49+
return -1; // pipe or pipe2 failed.
50+
}
51+
52+
int pid = fork();
53+
if (pid < 0) {
54+
return -1; // fork failed.
55+
}
56+
if (pid != 0) {
57+
// Parent process gets read end, closes write end.
58+
endpoint = umbilical->TakeReadEnd();
59+
umbilical->TakeWriteEnd().Close();
60+
61+
int status = endpoint.TokenRead();
62+
if (status != 0) { // Something went wrong while setting up child process.
63+
endpoint.Close();
64+
return -1;
65+
}
66+
67+
return pid;
68+
}
69+
// Child process gets write end, closes read end.
70+
endpoint = umbilical->TakeWriteEnd();
71+
umbilical->TakeReadEnd().Close();
72+
73+
#if HAVE_DECL_SETSID
74+
if (setsid() < 0) {
75+
exit(1); // setsid failed.
76+
}
77+
#endif
78+
79+
if (!nochdir) {
80+
if (chdir("/") != 0) {
81+
exit(1); // chdir failed.
82+
}
83+
}
84+
if (!noclose) {
85+
// Open /dev/null, and clone it into STDIN, STDOUT and STDERR to detach
86+
// from terminal.
87+
int fd = open("/dev/null", O_RDWR);
88+
if (fd >= 0) {
89+
bool err = dup2(fd, STDIN_FILENO) < 0 || dup2(fd, STDOUT_FILENO) < 0 || dup2(fd, STDERR_FILENO) < 0;
90+
// Don't close if fd<=2 to try to handle the case where the program was invoked without any file descriptors open.
91+
if (fd > 2) close(fd);
92+
if (err) {
93+
exit(1); // dup2 failed.
94+
}
95+
} else {
96+
exit(1); // open /dev/null failed.
97+
}
98+
}
99+
endpoint.TokenWrite(0); // Success
100+
return 0;
101+
}
102+
103+
#endif
104+
31105
static bool AppInit(int argc, char* argv[])
32106
{
33107
NodeContext node;
@@ -59,6 +133,14 @@ static bool AppInit(int argc, char* argv[])
59133
return true;
60134
}
61135

136+
#if HAVE_DECL_FORK
137+
// Communication with parent after daemonizing. This is used for signalling in the following ways:
138+
// - a boolean token is sent when the initialization process (all the Init* functions) have finished to indicate
139+
// that the parent process can quit, and whether it was successful/unsuccessful.
140+
// - an unexpected shutdown of the child process creates an unexpected end of stream at the parent
141+
// end, which is interpreted as failure to start.
142+
TokenPipeEnd daemon_ep;
143+
#endif
62144
util::Ref context{node};
63145
try
64146
{
@@ -105,24 +187,34 @@ static bool AppInit(int argc, char* argv[])
105187
// InitError will have been called with detailed error, which ends up on console
106188
return false;
107189
}
108-
if (args.GetBoolArg("-daemon", false)) {
109-
#if HAVE_DECL_DAEMON
110-
#if defined(MAC_OSX)
111-
#pragma GCC diagnostic push
112-
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
113-
#endif
190+
if (args.GetBoolArg("-daemon", DEFAULT_DAEMON) || args.GetBoolArg("-daemonwait", DEFAULT_DAEMONWAIT)) {
191+
#if HAVE_DECL_FORK
114192
tfm::format(std::cout, PACKAGE_NAME " starting\n");
115193

116194
// Daemonize
117-
if (daemon(1, 0)) { // don't chdir (1), do close FDs (0)
118-
return InitError(Untranslated(strprintf("daemon() failed: %s\n", strerror(errno))));
195+
switch (fork_daemon(1, 0, daemon_ep)) { // don't chdir (1), do close FDs (0)
196+
case 0: // Child: continue.
197+
// If -daemonwait is not enabled, immediately send a success token the parent.
198+
if (!args.GetBoolArg("-daemonwait", DEFAULT_DAEMONWAIT)) {
199+
daemon_ep.TokenWrite(1);
200+
daemon_ep.Close();
201+
}
202+
break;
203+
case -1: // Error happened.
204+
return InitError(Untranslated(strprintf("fork_daemon() failed: %s\n", strerror(errno))));
205+
default: { // Parent: wait and exit.
206+
int token = daemon_ep.TokenRead();
207+
if (token) { // Success
208+
exit(EXIT_SUCCESS);
209+
} else { // fRet = false or token read error (premature exit).
210+
tfm::format(std::cerr, "Error during initializaton - check debug.log for details\n");
211+
exit(EXIT_FAILURE);
212+
}
213+
}
119214
}
120-
#if defined(MAC_OSX)
121-
#pragma GCC diagnostic pop
122-
#endif
123215
#else
124216
return InitError(Untranslated("-daemon is not supported on this operating system\n"));
125-
#endif // HAVE_DECL_DAEMON
217+
#endif // HAVE_DECL_FORK
126218
}
127219
// Lock data directory after daemonization
128220
if (!AppInitLockDataDirectory())
@@ -138,6 +230,13 @@ static bool AppInit(int argc, char* argv[])
138230
PrintExceptionContinue(nullptr, "AppInit()");
139231
}
140232

233+
#if HAVE_DECL_FORK
234+
if (daemon_ep.IsOpen()) {
235+
// Signal initialization status to parent, then close pipe.
236+
daemon_ep.TokenWrite(fRet);
237+
daemon_ep.Close();
238+
}
239+
#endif
141240
if (fRet) {
142241
WaitForShutdown();
143242
}

src/init.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,9 @@ void SetupServerArgs(NodeContext& node)
580580
argsman.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);
581581
argsman.AddArg("-server", "Accept command line and JSON-RPC commands", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
582582

583-
#if HAVE_DECL_DAEMON
584-
argsman.AddArg("-daemon", "Run in the background as a daemon and accept commands", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
583+
#if HAVE_DECL_FORK
584+
argsman.AddArg("-daemon", strprintf("Run in the background as a daemon and accept commands (default: %d)", DEFAULT_DAEMON), ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
585+
argsman.AddArg("-daemonwait", strprintf("Wait for initialization to be finished before exiting. This implies -daemon (default: %d)", DEFAULT_DAEMONWAIT), ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
585586
#else
586587
hidden_args.emplace_back("-daemon");
587588
#endif

src/init.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
#include <memory>
1010
#include <string>
1111

12+
//! Default value for -daemon option
13+
static constexpr bool DEFAULT_DAEMON = false;
14+
//! Default value for -daemonwait option
15+
static constexpr bool DEFAULT_DAEMONWAIT = false;
16+
1217
class ArgsManager;
1318
struct NodeContext;
1419
namespace interfaces {

src/shutdown.cpp

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55

66
#include <shutdown.h>
77

8+
#include <logging.h>
9+
#include <util/tokenpipe.h>
10+
811
#include <config/bitcoin-config.h>
912

1013
#include <assert.h>
1114
#include <atomic>
1215
#ifdef WIN32
1316
#include <condition_variable>
14-
#else
15-
#include <errno.h>
16-
#include <fcntl.h>
17-
#include <unistd.h>
1817
#endif
1918

2019
static std::atomic<bool> fRequestShutdown(false);
@@ -24,25 +23,18 @@ std::mutex g_shutdown_mutex;
2423
std::condition_variable g_shutdown_cv;
2524
#else
2625
/** On UNIX-like operating systems use the self-pipe trick.
27-
* Index 0 will be the read end of the pipe, index 1 the write end.
2826
*/
29-
static int g_shutdown_pipe[2] = {-1, -1};
27+
static TokenPipeEnd g_shutdown_r;
28+
static TokenPipeEnd g_shutdown_w;
3029
#endif
3130

3231
bool InitShutdownState()
3332
{
3433
#ifndef WIN32
35-
#if HAVE_O_CLOEXEC && HAVE_DECL_PIPE2
36-
// If we can, make sure that the file descriptors are closed on exec()
37-
// to prevent interference.
38-
if (pipe2(g_shutdown_pipe, O_CLOEXEC) != 0) {
39-
return false;
40-
}
41-
#else
42-
if (pipe(g_shutdown_pipe) != 0) {
43-
return false;
44-
}
45-
#endif
34+
std::optional<TokenPipe> pipe = TokenPipe::Make();
35+
if (!pipe) return false;
36+
g_shutdown_r = pipe->TakeReadEnd();
37+
g_shutdown_w = pipe->TakeWriteEnd();
4638
#endif
4739
return true;
4840
}
@@ -59,17 +51,10 @@ void StartShutdown()
5951
// case of a reentrant signal.
6052
if (!fRequestShutdown.exchange(true)) {
6153
// Write an arbitrary byte to the write end of the shutdown pipe.
62-
const char token = 'x';
63-
while (true) {
64-
int result = write(g_shutdown_pipe[1], &token, 1);
65-
if (result < 0) {
66-
// Failure. It's possible that the write was interrupted by another signal.
67-
// Other errors are unexpected here.
68-
assert(errno == EINTR);
69-
} else {
70-
assert(result == 1);
71-
break;
72-
}
54+
int res = g_shutdown_w.TokenWrite('x');
55+
if (res != 0) {
56+
LogPrintf("Sending shutdown token failed\n");
57+
assert(0);
7358
}
7459
}
7560
#endif
@@ -96,17 +81,10 @@ void WaitForShutdown()
9681
std::unique_lock<std::mutex> lk(g_shutdown_mutex);
9782
g_shutdown_cv.wait(lk, [] { return fRequestShutdown.load(); });
9883
#else
99-
char token;
100-
while (true) {
101-
int result = read(g_shutdown_pipe[0], &token, 1);
102-
if (result < 0) {
103-
// Failure. Check if the read was interrupted by a signal.
104-
// Other errors are unexpected here.
105-
assert(errno == EINTR);
106-
} else {
107-
assert(result == 1);
108-
break;
109-
}
84+
int res = g_shutdown_r.TokenRead();
85+
if (res != 'x') {
86+
LogPrintf("Reading shutdown token failed\n");
87+
assert(0);
11088
}
11189
#endif
11290
}

0 commit comments

Comments
 (0)