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
);
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());
if config.dry_run {
println!("[DRY-RUN] Would execute: {:?}", cmd);
} else {
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 find_processes_by_name<'a>(
sys: &'a mut 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() == name)
})
})
}
pub fn is_process_running(process_name: &str) -> bool {
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing().with_exe(UpdateKind::Always),
);
find_processes_by_name(&mut sys, process_name).any(|_| true)
}
pub fn terminate_processes_by_name(name: &str) {
if name.trim().is_empty() {
return;
}
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true, ProcessRefreshKind::nothing().with_exe(UpdateKind::Always),
);
let targets: Vec<_> = find_processes_by_name(&mut 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.1;
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();
if !output.status.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 !output.status.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)
}
}