From 7340c13142b84e1709c765a6a88018ac7177d55a Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Sat, 23 Jul 2022 11:49:34 +0200 Subject: [PATCH] daemon: implement daemonize for UNIX platforms --- Cargo.lock | 1 + Cargo.toml | 3 +++ src/config.rs | 10 ++++++- src/daemonize.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 23 +++++++++++++++- 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/daemonize.rs diff --git a/Cargo.lock b/Cargo.lock index 7ef83ecf..4bce1fe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,6 +237,7 @@ dependencies = [ "dirs", "fern", "jsonrpc", + "libc", "log", "miniscript", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index d9035e07..cb24543c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,6 @@ rusqlite = { version = "0.28", features = ["bundled", "unlock_notify"] } # To talk to bitcoind jsonrpc = "0.12" + +# Used for daemonization +libc = "0.2" diff --git a/src/config.rs b/src/config.rs index 0620582d..425d559f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,6 +45,11 @@ fn default_poll_interval() -> Duration { Duration::from_secs(30) } +#[cfg(unix)] +fn default_daemon() -> bool { + false +} + /// Everything we need to know for talking to bitcoind serenely #[derive(Debug, Clone, Deserialize, Serialize)] pub struct BitcoindConfig { @@ -69,7 +74,9 @@ pub struct Config { /// An optional custom data directory pub data_dir: Option, /// Whether to daemonize the process - pub daemon: Option, + #[cfg(unix)] + #[serde(default = "default_daemon")] + pub daemon: bool, /// What messages to log #[serde( deserialize_with = "deserialize_fromstr", @@ -244,6 +251,7 @@ mod tests { "#.trim_start().replace(" ", ""); let parsed = toml::from_str::(&toml_str).expect("Deserializing toml_str"); let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml"); + #[cfg(unix)] // On non-UNIX there is no 'daemon' member. assert_eq!(toml_str, serialized); // Invalid desc checksum diff --git a/src/daemonize.rs b/src/daemonize.rs new file mode 100644 index 00000000..0ce62afe --- /dev/null +++ b/src/daemonize.rs @@ -0,0 +1,69 @@ +use std::env::set_current_dir; +use std::fs::File; +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::os::unix::io::AsRawFd; +use std::path::Path; + +// This code was highly inspired from Frank Denis (@jedisct1) 'daemonize-simple' crate, +// available at https://github.com/jedisct1/rust-daemonize-simple/blob/master/src/unix.rs . +// MIT licensed according to https://github.com/jedisct1/rust-daemonize-simple/blob/master/Cargo.toml +pub unsafe fn daemonize( + chdir: &Path, + pid_file: &Path, + log_file: &Path, +) -> Result<(), &'static str> { + match libc::fork() { + -1 => return Err("fork() failed"), + 0 => {} + _ => { + libc::_exit(0); + } + } + libc::setsid(); + match libc::fork() { + -1 => return Err("Second fork() failed"), + 0 => {} + _ => { + libc::_exit(0); + } + }; + + let fd = OpenOptions::new() + .read(true) + .open("/dev/null") + .map_err(|_| "Unable to open the stdin file")?; + if libc::dup2(fd.as_raw_fd(), 0) == -1 { + return Err("dup2(stdin) failed"); + } + let fd = OpenOptions::new() + .create(true) + .append(true) + .open(log_file) + .map_err(|_| "Unable to open the stdout file")?; + if libc::dup2(fd.as_raw_fd(), 1) == -1 { + return Err("dup2(stdout) failed"); + } + let fd = OpenOptions::new() + .create(true) + .append(true) + .open(log_file) + .map_err(|_| "Unable to open the stderr file")?; + if libc::dup2(fd.as_raw_fd(), 2) == -1 { + return Err("dup2(stderr) failed"); + } + + let pid = match libc::getpid() { + -1 => return Err("getpid() failed"), + pid => pid, + }; + let pid_str = format!("{}", pid); + File::create(pid_file) + .map_err(|_| "Creating the PID file failed")? + .write_all(pid_str.as_bytes()) + .map_err(|_| "Writing to the PID file failed")?; + + set_current_dir(chdir).map_err(|_| "chdir() failed")?; + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index fd0b65c9..62420db0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ mod bitcoin; pub mod config; +#[cfg(unix)] +mod daemonize; mod database; use crate::{ @@ -50,6 +52,8 @@ pub enum StartupError { DatadirCreation(path::PathBuf, io::Error), Database(SqliteDbError), Bitcoind(BitcoindError), + #[cfg(unix)] + Daemonization(&'static str), } impl fmt::Display for StartupError { @@ -66,6 +70,8 @@ impl fmt::Display for StartupError { ), Self::Database(e) => write!(f, "Error initializing database: '{}'.", e), Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e), + #[cfg(unix)] + Self::Daemonization(e) => write!(f, "Error when daemonizing: '{}'.", e), } } } @@ -170,6 +176,20 @@ impl DaemonHandle { bitcoind.with_retry_limit(None); log::info!("Connection to bitcoind established and checked."); + // If we are on a UNIX system and they told us to daemonize, do it now. + // NOTE: it's safe to daemonize now, as we don't carry any open DB connection + // https://www.sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_ + #[cfg(unix)] + if config.daemon { + log::info!("Daemonizing"); + let log_file = data_dir.as_path().join("log"); + let pid_file = data_dir.as_path().join("revaultd.pid"); + unsafe { + daemonize::daemonize(&data_dir, &log_file, &pid_file) + .map_err(StartupError::Daemonization)?; + } + } + Ok(Self {}) } @@ -369,7 +389,8 @@ mod tests { let config = Config { bitcoind_config, data_dir: Some(data_dir.clone()), - daemon: None, + #[cfg(unix)] + daemon: false, log_level: log::LevelFilter::Debug, main_descriptor: desc, };