Add daemon CLI commands

- Introduce option `--background` for `daemon start` to send the new
  process into the background.
- Implement a `ProcessDescr` which identifies a Husk process. This pays
  attention to the possibility of a rollover of the process ids.
- Implement `daemon status`.
- Implement `daemon stop` which sends `SIG_INT` to a running Husk
  process.
This commit is contained in:
Malte Meiboom
2026-06-30 12:17:07 +02:00
parent 847bdb1294
commit ee6b467139
11 changed files with 395 additions and 28 deletions
Generated
+159 -17
View File
@@ -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"
+4
View File
@@ -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"] }
+15 -1
View File
@@ -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)]
+6
View File
@@ -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}")]
+28 -4
View File
@@ -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(())
+1
View File
@@ -1,2 +1,3 @@
pub mod crypto;
pub mod escape;
pub mod process_descr;
+138
View File
@@ -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<Self> {
let location_path = PathBuf::from(location.clone());
if location_path.is_file() {
let mut file = File::open(location_path)?;
let mut buf: Vec<u8> = 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<Pid> {
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())
}
}
}
+13 -2
View File
@@ -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<Vec<String>>,
pub subject_replacement: Option<String>,
pub pid_file: Option<String>,
logging: Option<toml::Value>,
}
@@ -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<Mutex<HuskConfig>>;
@@ -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);
+2 -3
View File
@@ -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(())
}
+27 -1
View File
@@ -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 {
+2
View File
@@ -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]