wallswitch 0.63.1

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

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

/// Scans the system for a running instance recorded in the lockfile and terminates it.
///
/// If a lockfile exists, this function reads the stored PID, verifies with the OS
/// if the process is still alive, and terminates it if running. Finally, it overwrites
/// the lockfile with the current process's PID to acquire the execution lock.
///
/// # Errors
///
/// Returns a [`WallSwitchError`](crate::WallSwitchError) if directory creation or
/// writing to the lockfile 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 lock_path = get_lock_path()?;
    let current_pid = process::id();
    let mut sys = System::new();

    // 1. Check if a lockfile exists and try to terminate the previous instance
    if lock_path.exists()
        && let Ok(content) = fs::read_to_string(&lock_path)
        && let Ok(old_pid_u32) = content.trim().parse::<u32>()
    {
        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) {
            // Prevent self-termination (safety check)
            if old_pid_u32 != current_pid {
                if config.verbose {
                    println!(
                        "Terminating previous instance found in lockfile (PID: {})...\n",
                        old_pid_u32
                    );
                }
                process.kill();

                // Give the OS a short window to release resources and clean up sockets
                sleep(Duration::from_millis(100));
            }
        }
    }

    // 2. Ensure parent directories exist and write the current PID to the lockfile
    if let Some(parent) = lock_path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&lock_path, current_pid.to_string())?;

    Ok(())
}

/// Resolves the absolute path to the application lockfile.
///
/// Typically points to `~/.config/wallswitch/wallswitch.lock`.
fn get_lock_path() -> WallSwitchResult<PathBuf> {
    let mut path = get_config_path()?;
    path.set_file_name("wallswitch.lock");
    Ok(path)
}

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

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

    #[test]
    fn test_lock_path_resolves_correctly() {
        let path_result = get_lock_path();
        assert!(path_result.is_ok());

        let path = path_result.unwrap();
        assert!(path.to_string_lossy().ends_with("wallswitch.lock"));
    }

    #[test]
    fn test_lockfile_write_and_parse() {
        let temp_dir = std::env::temp_dir().join("wallswitch_test_pids");
        fs::create_dir_all(&temp_dir).unwrap();
        let test_lock_file = temp_dir.join("test_wallswitch.lock");

        let current_pid = std::process::id();
        fs::write(&test_lock_file, current_pid.to_string()).unwrap();

        // Verify that the written PID matches the parsed output
        let content = fs::read_to_string(&test_lock_file).unwrap();
        let parsed_pid: u32 = content.trim().parse().unwrap();
        assert_eq!(parsed_pid, current_pid);

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

    #[test]
    fn test_lockfile_invalid_content_handling() {
        let temp_dir = std::env::temp_dir().join("wallswitch_test_pids_invalid");
        fs::create_dir_all(&temp_dir).unwrap();
        let test_lock_file = temp_dir.join("invalid_test_wallswitch.lock");

        // Write corrupted alphanumeric data to simulate a damaged lockfile
        fs::write(&test_lock_file, "corrupt_data_abc_123").unwrap();

        let content = fs::read_to_string(&test_lock_file).unwrap();
        let parsed_result = content.trim().parse::<u32>();

        // Parsing non-numeric values must result in an error
        assert!(parsed_result.is_err());

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

    #[test]
    fn test_sysinfo_query_of_self_pid() {
        let mut sys = System::new();
        let current_pid = Pid::from_u32(std::process::id());

        // Refresh specifically the current process PID to verify sysinfo behavior
        sys.refresh_processes(ProcessesToUpdate::Some(&[current_pid]), true);

        // The current running process must always exist in the operating system process list
        let current_process = sys.process(current_pid);
        assert!(current_process.is_some());
    }
}