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:
+15
-1
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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,2 +1,3 @@
|
||||
pub mod crypto;
|
||||
pub mod escape;
|
||||
pub mod process_descr;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user