diff --git a/Cargo.lock b/Cargo.lock index a3c5442..2c5796e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,7 +356,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -551,6 +551,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "daemonix" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b747381562a10fd2104e9333ad72fe5158d79de81b64426fc9e229c6d3fb38" +dependencies = [ + "libc", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -975,7 +984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1205,6 +1214,7 @@ dependencies = [ "bytes", "clap", "clap_complete", + "daemonix", "fastrand", "indymilter", "log", @@ -1217,6 +1227,7 @@ dependencies = [ "sequoia-wot", "serde", "serde_derive", + "sysinfo", "tempfile", "thiserror 2.0.18", "tokio", @@ -1312,7 +1323,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1467,7 +1478,7 @@ dependencies = [ "socket2", "widestring", "windows-registry", - "windows-result", + "windows-result 0.4.1", "windows-sys 0.61.2", ] @@ -1638,7 +1649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1861,6 +1872,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1880,6 +1900,25 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1987,7 +2026,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -2602,7 +2641,7 @@ dependencies = [ "sequoia-keystore-gpg-agent", "sequoia-keystore-softkeys", "sequoia-openpgp", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-util", ] @@ -2619,7 +2658,7 @@ dependencies = [ "log", "sequoia-openpgp", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", ] @@ -2980,6 +3019,20 @@ dependencies = [ "syn", ] +[[package]] +name = "sysinfo" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -3549,6 +3602,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3557,9 +3645,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -3584,21 +3683,46 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3607,7 +3731,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3616,7 +3749,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3652,7 +3785,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3686,6 +3819,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 64465d3..1838236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,11 +28,15 @@ sequoia-openpgp = "2.3.0" sequoia-wot = "0.15.0" serde = "1.0.228" serde_derive = "1.0.228" +sysinfo = "0.36.1" tempfile = "3.27.0" thiserror = "2.0.17" tokio = { version = "1.52.3", features = [ "tokio-macros", "rt-multi-thread", "signal" ] } toml = "0.9.8" +[target.'cfg(unix)'.dependencies] +daemonix = "0.1.0" + [build-dependencies] anyhow = "1.0.100" clap = { version = "4", features = ["derive"] } diff --git a/src/cli/cli_args.rs b/src/cli/cli_args.rs index b38e40a..08a46d3 100644 --- a/src/cli/cli_args.rs +++ b/src/cli/cli_args.rs @@ -53,11 +53,25 @@ pub struct DaemonCommand { } #[derive(Debug, Subcommand)] pub enum DaemonSubcommand { - Start, + Start(DaemonStartCommand), Stop, Status } +#[derive(Parser, Debug)] +#[clap( + name = "daemon", + about = "Start Husk", +)] +pub struct DaemonStartCommand { + #[clap( + long = "background", + help = "Start Husk as a background process.", + default_value_t = false, + )] + background: bool, +} + // Introducer subcommands #[derive(Parser, Debug)] diff --git a/src/commands.rs b/src/commands.rs index ebe6038..4d8cb2c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,6 +13,12 @@ pub mod signkey; #[derive(thiserror::Error, Debug)] pub enum CommandError { + #[error("Daemon already running, pid: {0}")] + DaemonAlreadyRunning(String), + #[error("Invalid pid file path: {0}")] + InvalidPidFilePath(String), + #[error("Cannot identify process")] + CannotIdentifyProcess, #[error("Certificate not found: {0}")] CertNotFound(Fingerprint), #[error("Certificate not usable: {0}")] diff --git a/src/commands/daemon.rs b/src/commands/daemon.rs index 3405484..4784bf8 100644 --- a/src/commands/daemon.rs +++ b/src/commands/daemon.rs @@ -1,20 +1,44 @@ use anyhow::Result; use crate::cli::cli_args::{DaemonCommand, DaemonSubcommand}; +use crate::common::process_descr::ProcessDescr; use crate::config::HuskConfigContainer; +use crate::config::HuskConfig; use crate::Daemon; pub async fn dispatch(cmd: DaemonCommand, config: HuskConfigContainer) -> Result<()> { + let c: HuskConfig = config.clone().into(); + match cmd.subcommand { - DaemonSubcommand::Start => { - Daemon::run(config).await?; + DaemonSubcommand::Start(_sub_cmd) => { + // Write the pid file. + let pid_file_name = c.get_pid_file(); + ProcessDescr::create(pid_file_name.clone())?; + + let result_code = Daemon::run(config).await; + + // and remove it when the daemon terminated. + let _ = ProcessDescr::remove(pid_file_name); + return result_code; }, DaemonSubcommand::Stop => { - println!("stop"); + let process_descr = ProcessDescr::load(c.get_pid_file())?; + if process_descr.send_sig_interrupt().is_ok() { + let _ = ProcessDescr::remove(process_descr.file_location); + } }, DaemonSubcommand::Status => { - println!("status"); + if let Ok(process_descr) = ProcessDescr::load(c.get_pid_file()) { + if process_descr.is_running() { + println!("Running, with pid: {}", process_descr.pid); + } else { + println!("Not running, but stale pid file detected: {}", + c.get_pid_file()); + } + } else { + println!("Not running"); + } } } Ok(()) diff --git a/src/common/mod.rs b/src/common/mod.rs index b390873..d58916f 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,2 +1,3 @@ pub mod crypto; pub mod escape; +pub mod process_descr; diff --git a/src/common/process_descr.rs b/src/common/process_descr.rs new file mode 100644 index 0000000..5a0c2fa --- /dev/null +++ b/src/common/process_descr.rs @@ -0,0 +1,138 @@ +//! pid file handling + +use std::io::Read; +use std::io::Write; +use std::fs::DirBuilder; +use std::path::PathBuf; +use std::fs::File; +use std::fs::remove_file; + +use anyhow::Result; +use sysinfo::System; +use sysinfo::RefreshKind; +use sysinfo::Signal; +use sysinfo::Pid; +use sysinfo::ProcessRefreshKind; +use serde::Serialize; +use serde::Deserialize; + +use crate::commands::CommandError; + +/// The pid file contains to process id, the process name and the time +/// the process started. These are used to identify a Husk process. +#[derive(Serialize, Deserialize)] +pub struct ProcessDescr { + pub pid: usize, + pub name: String, + pub started: u64, + pub file_location: String, +} + +impl ProcessDescr { + + /// Create a file at `location` and populate it with the pid, the + /// process name and starting time of the current process. + pub fn create(location: String) -> Result<()> { + let location_path = PathBuf::from(location.clone()); + + let system = System::new_all(); + let pid = std::process::id(); + + if let Some(process) = system.process(Pid::from(pid as usize)) { + + let started = process.start_time(); + let name = process.name().to_string_lossy().to_string(); + + if location_path.exists() { + return Err(CommandError::DaemonAlreadyRunning(pid.to_string()).into()); + } + + // create directory + if let Some(path) = location_path.parent() { + DirBuilder::new() + .recursive(true) + .create(path)?; + } else { + return Err(CommandError::InvalidPidFilePath(location).into()); + } + + let mut descr_file = File::create(&location_path)?; + let content = ProcessDescr { + pid: pid as usize, + name: name, + started: started, + file_location: location, + }; + descr_file.write_all(toml::to_string(&content)?.as_bytes())?; + + Ok(()) + } else { + Err(CommandError::InvalidPidFilePath(location).into()) + } + } + + /// Remove a pid file + pub fn remove(location: String) -> Result<()> { + let location_path = PathBuf::from(location.clone()); + if location_path.is_file() { + remove_file(location_path) + .map_err(|e| e.into()) + } else { + Err(CommandError::InvalidPidFilePath(location).into()) + } + } + + /// Load a ProcessDescr object from `location`. + pub fn load(location: String) -> Result { + let location_path = PathBuf::from(location.clone()); + if location_path.is_file() { + let mut file = File::open(location_path)?; + let mut buf: Vec = Vec::new(); + let _ = file.read_to_end(&mut buf)?; + let buf = String::from_utf8_lossy(buf.as_slice()); + let process_descr: ProcessDescr = toml::from_str(buf.to_string().as_str())?; + + Ok(process_descr) + } else { + Err(CommandError::InvalidPidFilePath(location).into()) + } + } + + /// Return true if the process described is running. + pub fn is_running(&self) -> bool { + self.get_process().is_ok() + } + + /// Get the process identified by the `ProcessDescr`. + pub fn get_process(&self) -> Result { + let system = System::new_with_specifics( + RefreshKind::nothing() + .with_processes(ProcessRefreshKind::everything()) + ); + + if let Some(process) = system.process(Pid::from(self.pid)) { + let process_name = process.name().to_string_lossy().to_string(); + if process_name == self.name + && process.start_time() == self.started { + return Ok(process.pid()); + } + } + Err(CommandError::CannotIdentifyProcess.into()) + } + + /// Send SIG_INT to the process identified by the `ProcessDescr`. + pub fn send_sig_interrupt(&self) -> Result<()> { + let system = System::new_with_specifics( + RefreshKind::nothing() + .with_processes(ProcessRefreshKind::everything()) + ); + let pid = self.get_process()?; + + if let Some(process) = system.process(pid) { + process.kill_with(Signal::Interrupt); + Ok(()) + } else { + Err(CommandError::CannotIdentifyProcess.into()) + } + } +} diff --git a/src/config.rs b/src/config.rs index 1c9afac..3096469 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,9 @@ use toml; use log4rs; use crate::types::errors::HuskError; -use crate::types::defaults::{CONSOLE_LOGGING, SUBJECT_REPLACEMENT}; +use crate::types::defaults::CONSOLE_LOGGING; +use crate::types::defaults::PID_FILE_LOCATION; +use crate::types::defaults::SUBJECT_REPLACEMENT; #[derive(Deserialize, Debug, Clone)] pub struct HuskConfig { @@ -17,6 +19,7 @@ pub struct HuskConfig { pub sequoia_home: String, pub keyservers: Option>, pub subject_replacement: Option, + pub pid_file: Option, logging: Option, } @@ -76,6 +79,13 @@ impl HuskConfig { SUBJECT_REPLACEMENT.to_string() } } + pub fn get_pid_file(&self) -> String { + if let Some(pid_file) = &self.pid_file { + pid_file.clone() + } else { + PID_FILE_LOCATION.to_string() + } + } } pub type HuskConfigContainer = Arc>; @@ -101,7 +111,8 @@ mod tests { sequoia_home: "".into(), keyservers: None, subject_replacement: None, - logging: None + logging: None, + pid_file: None, }; assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT); diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 9cab8db..5d7ec09 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -53,9 +53,8 @@ impl Daemon { let config = Default::default(); - indymilter::run(listener, callbacks, config, signal::ctrl_c()) - .await - .expect("milter execution failed"); + let _ = indymilter::run(listener, callbacks, config, signal::ctrl_c()) + .await; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 046a9f9..f7f2985 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,9 @@ use std::sync::{Arc, Mutex}; use clap::Parser; use types::defaults; +#[cfg(unix)] +use daemonix::Daemonize; + pub mod cli; use cli::cli_args::CliArgs; pub mod commands; @@ -17,8 +20,31 @@ pub mod daemon; use daemon::Daemon; pub mod common; +pub fn main() -> anyhow::Result<()> { + + let mut daemonize = 0; + // XXX keep this in sync with cli/cli_args.rs + for arg in std::env::args() { + if arg == "daemon" && daemonize == 0 { daemonize = 1; } + if arg == "start" && daemonize == 1 { daemonize = 2; } + if arg == "--background" && daemonize == 2 { daemonize = 3; } + } + + #[cfg(unix)] + if daemonize == 3 { + Daemonize::new().start()?; + eprintln!("pid: {}", std::process::id()); + } + + async_main().map_err(|e| { + log::error!("exiting: {}", e); + e + }) + +} + #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn async_main() -> anyhow::Result<()> { let args = CliArgs::parse(); let config_file = match &args.config { diff --git a/src/types/defaults.rs b/src/types/defaults.rs index e9b4d8f..8f5d0d2 100644 --- a/src/types/defaults.rs +++ b/src/types/defaults.rs @@ -6,6 +6,8 @@ pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml"; /// and replaced by `SUBJECT_REPLACEMENT` (unless configured otherwise). pub const SUBJECT_REPLACEMENT: &str = "..."; +pub const PID_FILE_LOCATION: &str = "/run/husk/pid"; + /// Default logging configuration. pub const CONSOLE_LOGGING: &str = " [appenders.console]