use crate::{Config, WallSwitchError, WallSwitchResult};
use std::{
io::{Write, stdout},
process::{Command, Output, Stdio},
thread::sleep,
time::Duration,
};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
pub struct DaemonConfig {
pub cmd_name: &'static str,
pub pre_spawn_hook: Option<fn() -> WallSwitchResult<()>>,
}
pub struct DaemonManager;
impl DaemonManager {
pub fn ensure_running(config: &Config, daemon: &DaemonConfig) -> WallSwitchResult<()> {
if is_process_running(daemon.cmd_name) {
return Ok(());
}
if config.dry_run {
println!(
"[DRY-RUN] {name} is down; would perform clean start.",
name = daemon.cmd_name
);
let cmd = Command::new(daemon.cmd_name);
println!("[DRY-RUN] Would execute: {:?}", cmd);
return Ok(());
}
if config.verbose {
println!(
"{name} is down. Performing clean start...",
name = daemon.cmd_name
);
}
terminate_processes_by_name(daemon.cmd_name);
if let Some(hook) = daemon.pre_spawn_hook {
hook()?;
}
let mut cmd = Command::new(daemon.cmd_name);
cmd.stdout(Stdio::null()).stderr(Stdio::null());
let name = daemon.cmd_name.to_string();
cmd.spawn()
.map_err(|e| WallSwitchError::DaemonError(name, e.to_string()))?;
wait_for_process_ready(daemon.cmd_name, config)?;
Ok(())
}
}
fn get_refreshed_system() -> System {
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true, ProcessRefreshKind::nothing().with_exe(UpdateKind::Always),
);
sys
}
fn find_processes_by_name<'a>(
sys: &'a System,
name: &'a str,
) -> impl Iterator<Item = &'a sysinfo::Process> {
sys.processes().values().filter(move |process| {
process.exe().is_some_and(|path| {
path.file_name()
.is_some_and(|n| n.to_string_lossy().eq_ignore_ascii_case(name))
})
})
}
pub fn is_process_running(process_name: &str) -> bool {
let sys = get_refreshed_system();
find_processes_by_name(&sys, process_name).any(|_| true)
}
pub fn terminate_processes_by_name(name: &str) {
if name.trim().is_empty() {
return;
}
let sys = get_refreshed_system();
let targets: Vec<_> = find_processes_by_name(&sys, name).collect();
for process in targets {
let _ = process.kill();
}
}
pub fn wait_for_process_ready(name: &str, config: &Config) -> WallSwitchResult<()> {
let max_wait = 5.0;
let step = 0.2;
let mut elapsed = 0.0;
while elapsed < max_wait {
sleep(Duration::from_secs_f32(step));
if is_process_running(name) {
if config.verbose {
println!("\n{name} successfully initialized.");
}
return Ok(());
}
if config.verbose {
print!("\rWait to initialize {name}. Time: {elapsed:0.1}/{max_wait:0.1}s");
let _ = stdout().flush();
}
elapsed += step;
}
if config.verbose {
println!();
}
Err(WallSwitchError::UnableToFind(format!(
"{name} daemon failed to respond after initialization."
)))
}
pub trait CommandExt {
fn run_with_config(&mut self, config: &Config, context: &str) -> WallSwitchResult<Output>;
}
impl CommandExt for Command {
fn run_with_config(&mut self, config: &Config, context: &str) -> WallSwitchResult<Output> {
let output = self.output().map_err(|e| {
eprintln!("Failed to execute command: {:?}", self.get_program());
WallSwitchError::Io(e)
})?;
let program = self.get_program();
let arguments: Vec<_> = self.get_args().collect();
let is_success = output.status.success();
if !is_success || config.verbose {
println!("\nprogram: {program:?}");
println!("arguments: {arguments:#?}");
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
println!("stdout:'{}'\n", stdout.trim());
}
}
if !is_success {
let stderr = String::from_utf8_lossy(&output.stderr);
let status = output.status;
eprintln!("{context} status: {status}");
eprintln!("{context} stderr: {stderr}");
return Err(WallSwitchError::CommandFailed {
program: format!("{program:?}"),
status: status.to_string(),
stderr: stderr.to_string(),
});
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
#[test]
fn test_is_process_running_with_invalid_name() {
assert!(!is_process_running("non_existent_process_xyz_123"));
}
#[test]
fn test_command_ext_success() {
let config = Config {
dry_run: false,
verbose: false,
..Default::default()
};
#[cfg(target_os = "windows")]
let mut cmd = Command::new("cmd");
#[cfg(target_os = "windows")]
cmd.args(["/C", "echo hello"]);
#[cfg(not(target_os = "windows"))]
let mut cmd = Command::new("echo");
#[cfg(not(target_os = "windows"))]
cmd.arg("hello");
let result = cmd.run_with_config(&config, "Test Context");
assert!(result.is_ok());
}
#[test]
fn test_command_ext_failure() {
let config = Config {
dry_run: false,
verbose: false,
..Default::default()
};
let mut cmd = Command::new("non_existent_binary_for_test");
let result = cmd.run_with_config(&config, "Test Fail Context");
assert!(result.is_err());
}
}