mirror of
https://github.com/telemt/telemt.git
synced 2026-10-29 20:17:16 +03:00
53f131e34d
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
299 lines
11 KiB
Rust
299 lines
11 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use crate::daemon::{self, DaemonOptions};
|
|
|
|
/// Parses daemon-related options from CLI arguments.
|
|
pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
|
|
let mut opts = DaemonOptions::default();
|
|
let mut i = 0;
|
|
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--daemon" | "-d" => {
|
|
opts.daemonize = true;
|
|
}
|
|
"--foreground" | "-f" => {
|
|
opts.foreground = true;
|
|
}
|
|
"--strict-runtime-paths" => {
|
|
opts.strict_runtime_paths = true;
|
|
}
|
|
"--pid-file" => {
|
|
i += 1;
|
|
if i < args.len() {
|
|
opts.pid_file = Some(PathBuf::from(&args[i]));
|
|
}
|
|
}
|
|
s if s.starts_with("--pid-file=") => {
|
|
opts.pid_file = Some(PathBuf::from(s.trim_start_matches("--pid-file=")));
|
|
}
|
|
"--run-as-user" => {
|
|
i += 1;
|
|
if i < args.len() {
|
|
opts.user = Some(args[i].clone());
|
|
}
|
|
}
|
|
s if s.starts_with("--run-as-user=") => {
|
|
opts.user = Some(s.trim_start_matches("--run-as-user=").to_string());
|
|
}
|
|
"--run-as-group" => {
|
|
i += 1;
|
|
if i < args.len() {
|
|
opts.group = Some(args[i].clone());
|
|
}
|
|
}
|
|
s if s.starts_with("--run-as-group=") => {
|
|
opts.group = Some(s.trim_start_matches("--run-as-group=").to_string());
|
|
}
|
|
"--working-dir" => {
|
|
i += 1;
|
|
if i < args.len() {
|
|
opts.working_dir = Some(PathBuf::from(&args[i]));
|
|
}
|
|
}
|
|
s if s.starts_with("--working-dir=") => {
|
|
opts.working_dir = Some(PathBuf::from(s.trim_start_matches("--working-dir=")));
|
|
}
|
|
_ => {}
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
opts
|
|
}
|
|
|
|
/// Sends SIGTERM and waits briefly for graceful PID-file cleanup.
|
|
pub(super) fn stop(pid_file: &Path, strict_runtime_paths: bool) -> i32 {
|
|
use nix::sys::signal::Signal;
|
|
|
|
println!("Stopping telemt daemon...");
|
|
|
|
match daemon::signal_pid_file(pid_file, Signal::SIGTERM, strict_runtime_paths) {
|
|
Ok(()) => {
|
|
println!("Stop signal sent successfully");
|
|
|
|
// Wait for process to exit for up to ten seconds.
|
|
for _ in 0..20 {
|
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
if let daemon::DaemonStatus::NotRunning =
|
|
daemon::check_status(pid_file, strict_runtime_paths)
|
|
{
|
|
println!("Daemon stopped");
|
|
return 0;
|
|
}
|
|
}
|
|
println!("Daemon may still be shutting down");
|
|
0
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to stop daemon: {}", e);
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sends SIGHUP to trigger configuration reload.
|
|
pub(super) fn reload(pid_file: &Path, strict_runtime_paths: bool) -> i32 {
|
|
use nix::sys::signal::Signal;
|
|
|
|
println!("Reloading telemt configuration...");
|
|
|
|
match daemon::signal_pid_file(pid_file, Signal::SIGHUP, strict_runtime_paths) {
|
|
Ok(()) => {
|
|
println!("Reload signal sent successfully");
|
|
0
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to reload daemon: {}", e);
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reports daemon status without mutating PID lifecycle state.
|
|
pub(super) fn status(pid_file: &Path, strict_runtime_paths: bool) -> i32 {
|
|
match daemon::check_status(pid_file, strict_runtime_paths) {
|
|
daemon::DaemonStatus::Running(pid) => {
|
|
println!("telemt is running (pid {})", pid);
|
|
0
|
|
}
|
|
daemon::DaemonStatus::Stale(pid) => {
|
|
println!("telemt is not running (stale pid file, was pid {})", pid);
|
|
1
|
|
}
|
|
daemon::DaemonStatus::NotRunning => {
|
|
println!("telemt is not running");
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::fs;
|
|
use std::os::unix::fs::{PermissionsExt, symlink};
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use super::*;
|
|
|
|
const CONTROL_PID: &str = "TELEMT_RUNTIME_PATH_TEST_PID";
|
|
const CONTROL_READY: &str = "TELEMT_RUNTIME_PATH_TEST_READY";
|
|
const CONTROL_RELOADED: &str = "TELEMT_RUNTIME_PATH_TEST_RELOADED";
|
|
|
|
struct ControlChild(Child);
|
|
|
|
impl Drop for ControlChild {
|
|
fn drop(&mut self) {
|
|
let _ = self.0.kill();
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
fn wait_for_file(path: &Path) -> bool {
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while Instant::now() < deadline {
|
|
if path.exists() {
|
|
return true;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
false
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_path_policy_defaults_to_compatibility_for_all_commands() {
|
|
for command in ["run", "start", "stop", "reload", "status"] {
|
|
let args = vec![command.to_string(), "config.toml".to_string()];
|
|
assert!(
|
|
!crate::cli::parse_command(&args)
|
|
.daemon_opts
|
|
.strict_runtime_paths
|
|
);
|
|
assert!(!parse_daemon_args(&args).strict_runtime_paths);
|
|
|
|
for position in [1, args.len()] {
|
|
let mut strict_args = args.clone();
|
|
strict_args.insert(position, "--strict-runtime-paths".to_string());
|
|
let parsed = crate::cli::parse_command(&strict_args);
|
|
assert!(parsed.daemon_opts.strict_runtime_paths);
|
|
assert!(parse_daemon_args(&strict_args).strict_runtime_paths);
|
|
assert_eq!(parsed.config_path, "config.toml");
|
|
}
|
|
}
|
|
let args = vec![
|
|
"--strict-runtime-paths".to_string(),
|
|
"config.toml".to_string(),
|
|
];
|
|
let parsed = crate::cli::parse_command(&args);
|
|
assert_eq!(parsed.subcommand, crate::cli::Subcommand::Run);
|
|
assert!(parsed.daemon_opts.strict_runtime_paths);
|
|
assert_eq!(parsed.config_path, "config.toml");
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn daemon_control_subprocess() {
|
|
let Some(pid_path) = std::env::var_os(CONTROL_PID) else {
|
|
return;
|
|
};
|
|
let ready = PathBuf::from(std::env::var_os(CONTROL_READY).unwrap());
|
|
let reloaded = PathBuf::from(std::env::var_os(CONTROL_RELOADED).unwrap());
|
|
let mut terminate =
|
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).unwrap();
|
|
let mut reload =
|
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).unwrap();
|
|
let mut owner = daemon::PidFile::new(PathBuf::from(pid_path), false);
|
|
owner.acquire().unwrap();
|
|
fs::write(&ready, b"ready").unwrap();
|
|
let deadline = tokio::time::sleep(Duration::from_secs(15));
|
|
tokio::pin!(deadline);
|
|
loop {
|
|
tokio::select! {
|
|
_ = terminate.recv() => break,
|
|
_ = reload.recv() => fs::write(&reloaded, b"reloaded").unwrap(),
|
|
_ = &mut deadline => panic!("daemon control subprocess timed out"),
|
|
}
|
|
}
|
|
owner.release().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn control_commands_follow_runtime_path_policy() {
|
|
for (mode, linked_parent, trusted_parent) in [
|
|
(0o777, false, false),
|
|
(0o777, true, false),
|
|
(0o755, false, true),
|
|
] {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let real = root.path().join("run");
|
|
let linked = root.path().join("linked");
|
|
fs::create_dir(&real).unwrap();
|
|
fs::set_permissions(&real, fs::Permissions::from_mode(mode)).unwrap();
|
|
symlink(&real, &linked).unwrap();
|
|
let pid_path = if linked_parent { &linked } else { &real }.join("telemt.pid");
|
|
let ready = root.path().join("ready");
|
|
let reloaded = root.path().join("reloaded");
|
|
let mut child = ControlChild(
|
|
Command::new(std::env::current_exe().unwrap())
|
|
.args([
|
|
"--exact",
|
|
"cli::daemon_commands::tests::daemon_control_subprocess",
|
|
"--nocapture",
|
|
])
|
|
.env(CONTROL_PID, &pid_path)
|
|
.env(CONTROL_READY, &ready)
|
|
.env(CONTROL_RELOADED, &reloaded)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.unwrap(),
|
|
);
|
|
assert!(
|
|
wait_for_file(&ready),
|
|
"daemon control subprocess did not become ready"
|
|
);
|
|
let command = |name: &str, strict: bool| {
|
|
let mut args = vec![
|
|
name.to_string(),
|
|
"--pid-file".to_string(),
|
|
pid_path.to_str().unwrap().to_string(),
|
|
];
|
|
if strict {
|
|
args.push("--strict-runtime-paths".to_string());
|
|
}
|
|
crate::cli::execute_subcommand(&crate::cli::parse_command(&args))
|
|
};
|
|
|
|
assert_eq!(command("status", false), Some(0));
|
|
if trusted_parent {
|
|
assert_eq!(command("status", true), Some(0));
|
|
assert_eq!(command("reload", true), Some(0));
|
|
assert!(wait_for_file(&reloaded));
|
|
assert_eq!(command("stop", true), Some(0));
|
|
} else {
|
|
assert_eq!(command("status", true), Some(1));
|
|
assert_eq!(command("reload", true), Some(1));
|
|
assert!(!reloaded.exists());
|
|
assert_eq!(command("stop", true), Some(1));
|
|
assert_eq!(command("status", false), Some(0));
|
|
assert_eq!(command("reload", false), Some(0));
|
|
assert!(wait_for_file(&reloaded));
|
|
assert_eq!(command("stop", false), Some(0));
|
|
}
|
|
assert!(child.0.wait().unwrap().success());
|
|
assert!(!pid_path.exists());
|
|
assert_eq!(command("status", false), Some(1));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn status_does_not_remove_stale_pid_file() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let pid_file = directory.path().join("telemt.pid");
|
|
fs::write(&pid_file, b"2000000000\n").unwrap();
|
|
|
|
assert_eq!(status(&pid_file, false), 1);
|
|
assert!(pid_file.exists());
|
|
}
|
|
}
|