use std::path::Path;
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, Signal, System};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
pub fn running_pids(exe_name: &str) -> Vec<u32> {
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing(),
);
sys.processes()
.values()
.filter(|p| matches_exe(p.name().to_string_lossy().as_ref(), exe_name))
.map(|p| p.pid().as_u32())
.collect()
}
pub fn ensure_not_running(exe_name: &str) -> Result<(), crate::error::Error> {
let pids = running_pids(exe_name);
if pids.is_empty() {
Ok(())
} else {
Err(crate::error::Error::AlreadyRunning {
name: exe_name.to_string(),
pids,
})
}
}
pub fn kill_by_name(exe_name: &str) -> usize {
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing(),
);
let mut count = 0usize;
for process in sys.processes().values() {
if matches_exe(process.name().to_string_lossy().as_ref(), exe_name) {
let _ = process.kill_with(Signal::Term).or_else(|| Some(process.kill()));
count += 1;
}
}
count
}
pub async fn spawn_and_wait_for_listening(
binary_path: &Path,
address: &str,
port: u16,
) -> Result<String, crate::error::Error> {
let name = binary_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| binary_path.display().to_string());
let mut cmd = Command::new(binary_path);
cmd.env("ADDRESS", address)
.env("PORT", port.to_string())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0008);
}
let mut child = cmd
.spawn()
.map_err(|e| crate::error::Error::Spawn(name.clone(), e))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| crate::error::Error::SpawnNoListeningLine { name: name.clone() })?;
let mut reader = BufReader::new(stderr).lines();
let mut listening_line: Option<String> = None;
loop {
tokio::select! {
line = reader.next_line() => {
match line {
Ok(Some(line)) => {
if line.to_ascii_lowercase().contains("listening") {
listening_line = Some(line);
break;
}
}
Ok(None) => break, Err(_) => break,
}
}
status = child.wait() => {
let _ = status;
break;
}
}
}
let Some(line) = listening_line else {
return Err(crate::error::Error::SpawnNoListeningLine { name });
};
tokio::spawn(async move {
while let Ok(Some(_)) = reader.next_line().await {}
});
drop(child);
Ok(line)
}
fn matches_exe(observed: &str, target: &str) -> bool {
let trim = |s: &str| {
s.strip_suffix(".exe")
.or_else(|| s.strip_suffix(".EXE"))
.unwrap_or(s)
.to_ascii_lowercase()
};
trim(observed) == trim(target)
}