wallswitch 0.62.6

randomly selects wallpapers for multiple monitors
Documentation
use crate::{Config, WallSwitchError, WallSwitchResult};
use std::{
    io::{Write, stdout},
    process::{Command, Output, Stdio},
    thread::sleep,
    time::Duration,
};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};

/// Configuration for a background daemon to ensure consistent lifecycle management.
pub struct DaemonConfig {
    /// The name of the process to check (e.g., "aww-daemon").
    pub cmd_name: &'static str,
    /// Optional hook to run before spawning the daemon (e.g., cleaning sockets).
    pub pre_spawn_hook: Option<fn() -> WallSwitchResult<()>>,
}

/// A generic coordinator to handle background daemon processes.
///
/// This helper abstracts process state management, including automatic termination
/// of existing instances, pre-spawn hooks, and polling feedback to ensure
/// the daemon is ready before the application continues.
pub struct DaemonManager;

impl DaemonManager {
    /// Ensures that a daemon is running.
    ///
    /// If the process is not running, this method will:
    /// 1. Terminate any existing processes with the same name using `sysinfo`.
    /// 2. Execute any optional `pre_spawn_hook`.
    /// 3. Spawn the daemon.
    /// 4. Poll the system until the process is active or a timeout is reached.
    ///
    /// # Errors
    /// Returns a [`WallSwitchError`] if the daemon fails to spawn, the pre-spawn
    /// hook fails, or the daemon fails to initialize within the timeout period.
    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
            );
        }

        // 1. Robust Termination: Use sysinfo to kill existing processes by name.
        terminate_processes_by_name(daemon.cmd_name);

        // 2. Pre-spawn Hook: Execute custom logic (e.g., socket cleanup for 'aww').
        if let Some(hook) = daemon.pre_spawn_hook {
            hook()?;
        }

        // 3. Spawning
        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()))?;
        }

        // 4. Polling: Wait for the process to appear in the system table.
        wait_for_process_ready(daemon.cmd_name, config)?;

        Ok(())
    }
}

/// Internal helper to get an iterator of processes matching a name.
/// This is the "Single Source of Truth" for finding processes.
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)
        })
    })
}

/// Checks if a process with the specified name is currently running.
/// O(n) complexity, but short-circuits on the first match (fast).
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),
    );

    // .any() is idiomatic and performs short-circuiting.
    find_processes_by_name(&mut sys, process_name).any(|_| true)
}

/// Terminates all processes that match the provided name.
///
/// This is a robust, cross-platform implementation that:
/// 1. Validates the input name to prevent accidental broad-spectrum kills.
/// 2. Uses a precise refresh to minimize CPU overhead.
/// 3. Identifies processes by their executable name.
/// 4. Safely attempts to kill each instance, ignoring processes where
///    permissions are denied (best-effort approach).
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, // remove_dead_processes
        ProcessRefreshKind::nothing().with_exe(UpdateKind::Always),
    );

    // Here we explicitly collect into a Vec because we need to
    // own the references or iterate safely while potentially mutating state.
    let targets: Vec<_> = find_processes_by_name(&mut sys, name).collect();

    for process in targets {
        let _ = process.kill();
    }
}

/// Polls the system until the specified process is detected or timeout is reached.
///
/// This provides a dynamic wait window, replacing fixed sleep durations to
/// make the application as responsive as possible.
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.",
    )))
}

/// Extension trait for `std::process::Command` to unify command execution logic.
pub trait CommandExt {
    /// Executes the command with integrated dry-run, verbosity, and error handling.
    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)
    }
}