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
+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]