daemon: implement daemonize for UNIX platforms

This commit is contained in:
Antoine Poinsot 2022-07-23 11:49:34 +02:00
parent c095346e17
commit 7340c13142
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
5 changed files with 104 additions and 2 deletions

1
Cargo.lock generated
View File

@ -237,6 +237,7 @@ dependencies = [
"dirs",
"fern",
"jsonrpc",
"libc",
"log",
"miniscript",
"rusqlite",

View File

@ -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"

View File

@ -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<PathBuf>,
/// Whether to daemonize the process
pub daemon: Option<bool>,
#[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::<Config>(&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

69
src/daemonize.rs Normal file
View File

@ -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(())
}

View File

@ -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,
};