use crate::{
AtomicWriteExt as _, Config, Environment, WallSwitchError, WallSwitchResult, get_config_path,
};
use std::{
fs,
path::{Path, PathBuf},
process, thread,
time::Duration,
};
use sysinfo::{Pid, ProcessesToUpdate, System};
pub fn kill_other_instances(config: &Config, env: &Environment) -> WallSwitchResult<()> {
if config.dry_run {
if config.verbose {
println!("[DRY-RUN] Skipping single-instance locking and termination.");
}
return Ok(());
}
let app_name = env.get_pkg_name();
let pid_path = get_pid_path(app_name, env)?;
let current_pid = process::id();
if let Some(old_pid) = read_pid_file(&pid_path)?
&& old_pid != current_pid
{
handle_existing_instance(&pid_path, old_pid, app_name, config)?;
}
ensure_parent_dir_exists(&pid_path)?;
atomic_write_pid_file(&pid_path, current_pid)?;
Ok(())
}
fn handle_existing_instance(
pid_path: &Path,
old_pid_u32: u32,
app_name: &str,
config: &Config,
) -> WallSwitchResult<()> {
let mut sys = System::new();
let old_pid = Pid::from_u32(old_pid_u32);
sys.refresh_processes(ProcessesToUpdate::Some(&[old_pid]), true);
if let Some(process) = sys.process(old_pid) {
let process_name = process.name().to_string_lossy().to_lowercase();
let target_name_lower = app_name.to_lowercase();
if process_name.contains(&target_name_lower) {
if config.verbose {
println!("PID file: {}", pid_path.display());
println!(
"Terminating previous instance found in PID file (PID: {})...\n",
old_pid_u32
);
}
process.kill();
thread::sleep(Duration::from_millis(200));
} else if config.verbose {
println!(
"PID {} is active but process '{}' does not match '{}'. Skipping.",
old_pid_u32, process_name, app_name
);
}
}
Ok(())
}
fn ensure_parent_dir_exists(path: &Path) -> WallSwitchResult<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| WallSwitchError::IOError {
path: parent.to_path_buf(),
io_error: e,
})?;
}
Ok(())
}
fn atomic_write_pid_file(path: &Path, pid: u32) -> WallSwitchResult<()> {
path.atomic_write(|temp_path| {
fs::write(temp_path, pid.to_string()).map_err(|e| WallSwitchError::IOError {
path: temp_path.to_path_buf(),
io_error: e,
})
})
}
fn read_pid_file(path: &Path) -> WallSwitchResult<Option<u32>> {
if !path.exists() || !path.metadata()?.is_file() {
return Ok(None);
}
let content = fs::read_to_string(path).map_err(|e| WallSwitchError::IOError {
path: path.to_path_buf(),
io_error: e,
})?;
Ok(content.trim().parse::<u32>().ok())
}
fn get_pid_path(app_name: &str, env: &Environment) -> WallSwitchResult<PathBuf> {
let mut path = get_config_path(env)?;
let pid_filename = format!("{}.pid", app_name);
path.set_file_name(pid_filename);
Ok(path)
}
#[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");
fs::create_dir_all(&temp_dir).unwrap();
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);
let _ = fs::remove_dir_all(temp_dir);
}
#[test]
fn test_read_pid_file_valid() {
let temp_dir = std::env::temp_dir().join("wallswitch_test_valid");
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");
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_environment_pkg_name_integration() {
let env = Environment::new();
assert!(env.is_ok());
let app_name = env.unwrap().get_pkg_name().to_string();
assert!(!app_name.is_empty());
}
}