wallswitch 0.63.2

randomly selects wallpapers for multiple monitors
Documentation
//! Process management and single-instance serialization using PID tracking.
//!
//! This module ensures that only a single instance of the application runs
//! concurrently. It records the active process ID (PID) to a dedicated PID file
//! on startup, terminating any existing instance found before proceeding.

use crate::{Config, WallSwitchError, WallSwitchResult, get_config_path};
use std::{fs, path::Path, path::PathBuf, process, thread::sleep, time::Duration};
use sysinfo::{Pid, ProcessesToUpdate, System};

/// Scans the system for a running instance recorded in the PID file and terminates it.
///
/// If a PID file exists and contains a valid PID of a running `wallswitch` process,
/// that process is terminated. Finally, the PID file is updated with the current process's PID.
///
/// # Errors
///
/// Returns a [`WallSwitchError`] if directory creation or
/// writing to the PID file fails.
pub fn kill_other_instances(config: &Config) -> WallSwitchResult<()> {
    if config.dry_run {
        if config.verbose {
            println!("[DRY-RUN] Skipping single-instance locking and termination.");
        }
        return Ok(());
    }

    let pid_path = get_pid_path()?;
    let current_pid = process::id();

    // 1. Try to read the previous PID and terminate its process if active
    if let Some(old_pid_u32) = read_pid_file(&pid_path)?
    // Early filter: Prevent self-termination and unnecessary system queries
    && old_pid_u32 != current_pid
    {
        let mut sys = System::new();
        let old_pid = Pid::from_u32(old_pid_u32);

        // Query only the specific process to see if it is still running
        sys.refresh_processes(ProcessesToUpdate::Some(&[old_pid]), true);

        if let Some(process) = sys.process(old_pid) {
            // Safety check: Verify process name to avoid killing an unrelated process
            let process_name = process.name().to_string_lossy().to_lowercase();
            if process_name.contains("wallswitch") {
                if config.verbose {
                    println!("PID file: {}", pid_path.display());
                    println!(
                        "Terminating previous instance found in PID file (PID: {})...\n",
                        old_pid_u32
                    );
                }
                process.kill();

                // Give the OS a short window to release resources and sockets
                sleep(Duration::from_millis(100));
            } else if config.verbose {
                println!(
                    "PID {} is active but process name {:?} does not match. Skipping termination.",
                    old_pid_u32, process_name
                );
            }
        }
    }

    // 2. Ensure parent directories exist and write the current PID to the PID file
    if let Some(parent) = pid_path.parent() {
        fs::create_dir_all(parent).map_err(|io_error| WallSwitchError::IOError {
            path: parent.to_path_buf(),
            io_error,
        })?;
    }

    fs::write(&pid_path, current_pid.to_string()).map_err(|io_error| WallSwitchError::IOError {
        path: pid_path.clone(),
        io_error,
    })?;

    Ok(())
}

/// Reads the PID file and returns the stored PID if it exists and is valid.
///
/// If the file does not exist, returns `Ok(None)`. If the file contains invalid
/// or corrupted data, it returns `Ok(None)` to allow self-healing (overwriting the file).
fn read_pid_file(path: &Path) -> WallSwitchResult<Option<u32>> {
    if !path.exists() {
        return Ok(None);
    }

    let content = fs::read_to_string(path).map_err(|io_error| WallSwitchError::IOError {
        path: path.to_path_buf(),
        io_error,
    })?;

    // Parse the PID; if corrupted, treat as None to allow self-healing overwrites later
    let pid = content.trim().parse::<u32>().ok();
    Ok(pid)
}

/// Resolves the absolute path to the application PID file.
fn get_pid_path() -> WallSwitchResult<PathBuf> {
    let mut path = get_config_path()?;
    path.set_file_name("wallswitch.pid");
    Ok(path)
}

//----------------------------------------------------------------------------//
//                                   Tests                                    //
//----------------------------------------------------------------------------//

#[cfg(test)]
mod tests_pids {
    use super::*;

    #[test]
    fn test_read_pid_file_missing() {
        let temp_dir = std::env::temp_dir().join("wallswitch_test_missing_err");
        let non_existent = temp_dir.join("non_existent.pid");

        let result = read_pid_file(&non_existent);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), None);
    }

    #[test]
    fn test_read_pid_file_valid() {
        let temp_dir = std::env::temp_dir().join("wallswitch_test_valid_err");
        fs::create_dir_all(&temp_dir).unwrap();
        let pid_file = temp_dir.join("test.pid");

        fs::write(&pid_file, "12345").unwrap();

        let result = read_pid_file(&pid_file);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), Some(12345));

        let _ = fs::remove_dir_all(temp_dir);
    }

    #[test]
    fn test_read_pid_file_invalid_content() {
        let temp_dir = std::env::temp_dir().join("wallswitch_test_invalid_err");
        fs::create_dir_all(&temp_dir).unwrap();
        let pid_file = temp_dir.join("invalid.pid");

        fs::write(&pid_file, "not_a_number").unwrap();

        let result = read_pid_file(&pid_file);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), None);

        let _ = fs::remove_dir_all(temp_dir);
    }

    #[test]
    fn test_kill_other_instances_creates_valid_pid_file() {
        // Mock the configuration using isolated temporary paths
        let temp_dir = std::env::temp_dir().join("wallswitch_test_integration");
        fs::create_dir_all(&temp_dir).unwrap();

        // Temporary test configuration
        let config = Config {
            dry_run: false,
            verbose: false,
            // Add any other necessary fields of your Config struct here if needed...
            ..Default::default()
        };

        // Execute the main function of the module
        let result = kill_other_instances(&config);
        assert!(result.is_ok());

        // Retrieve the path that should have been created
        let pid_path = get_pid_path().unwrap();
        assert!(pid_path.exists());

        // The file must contain the PID of the current test process
        let written_pid = fs::read_to_string(&pid_path).unwrap();
        let parsed: u32 = written_pid.trim().parse().unwrap();
        assert_eq!(parsed, std::process::id());

        let _ = fs::remove_dir_all(temp_dir);
    }
}