mirror of
https://github.com/telemt/telemt.git
synced 2026-10-29 03:57:15 +03:00
Runtime Paths Checks as opt-in #935
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
+165
-8
@@ -15,6 +15,9 @@ pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
|
||||
"--foreground" | "-f" => {
|
||||
opts.foreground = true;
|
||||
}
|
||||
"--strict-runtime-paths" => {
|
||||
opts.strict_runtime_paths = true;
|
||||
}
|
||||
"--pid-file" => {
|
||||
i += 1;
|
||||
if i < args.len() {
|
||||
@@ -60,19 +63,21 @@ pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
|
||||
}
|
||||
|
||||
/// Sends SIGTERM and waits briefly for graceful PID-file cleanup.
|
||||
pub(super) fn stop(pid_file: &Path) -> i32 {
|
||||
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) {
|
||||
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) {
|
||||
if let daemon::DaemonStatus::NotRunning =
|
||||
daemon::check_status(pid_file, strict_runtime_paths)
|
||||
{
|
||||
println!("Daemon stopped");
|
||||
return 0;
|
||||
}
|
||||
@@ -88,12 +93,12 @@ pub(super) fn stop(pid_file: &Path) -> i32 {
|
||||
}
|
||||
|
||||
/// Sends SIGHUP to trigger configuration reload.
|
||||
pub(super) fn reload(pid_file: &Path) -> i32 {
|
||||
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) {
|
||||
match daemon::signal_pid_file(pid_file, Signal::SIGHUP, strict_runtime_paths) {
|
||||
Ok(()) => {
|
||||
println!("Reload signal sent successfully");
|
||||
0
|
||||
@@ -106,8 +111,8 @@ pub(super) fn reload(pid_file: &Path) -> i32 {
|
||||
}
|
||||
|
||||
/// Reports daemon status without mutating PID lifecycle state.
|
||||
pub(super) fn status(pid_file: &Path) -> i32 {
|
||||
match daemon::check_status(pid_file) {
|
||||
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
|
||||
@@ -126,16 +131,168 @@ pub(super) fn status(pid_file: &Path) -> i32 {
|
||||
#[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), 1);
|
||||
assert_eq!(status(&pid_file, false), 1);
|
||||
assert!(pid_file.exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user