#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(unix)]
mod unix;
#[cfg(target_os = "windows")]
mod windows;
use std::{
collections::HashSet,
ffi::OsStr,
path::{
Path,
PathBuf,
},
};
pub struct ProcessInfo {
pub id: usize,
pub image: PathBuf,
pub tcp_server_ports: HashSet<u16>,
}
#[cfg(target_os = "linux")]
use linux::close_all_files_except;
#[cfg(target_os = "linux")]
use linux::list_processes_internal;
#[cfg(target_os = "macos")]
use macos::close_all_files_except;
#[cfg(target_os = "macos")]
use macos::list_processes_internal;
#[cfg(unix)]
use unix::kill_internal;
#[cfg(unix)]
use unix::start_detached_internal;
#[cfg(target_os = "windows")]
use windows::kill_internal;
#[cfg(target_os = "windows")]
use windows::list_processes_internal;
#[cfg(target_os = "windows")]
use windows::start_detached_internal;
pub fn list_processes() -> impl Iterator<Item = ProcessInfo> {
list_processes_internal()
}
pub fn start_detached<P, A, S>(
path: P,
args: A,
) -> usize
where
P: AsRef<Path>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
start_detached_internal(path, args)
}
pub fn kill(pid: usize) {
kill_internal(pid)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
convert::TryFrom as _,
env::current_exe,
ffi::OsString,
fs::{
create_dir,
read_to_string,
remove_dir_all,
File,
},
io::{
BufRead,
BufReader,
},
net::{
Ipv4Addr,
TcpListener,
},
path::Path,
thread::sleep,
time::Duration,
};
struct TestArea {
path: PathBuf,
}
impl TestArea {
fn path(&self) -> &Path {
&self.path
}
fn new() -> Self {
let path = [
current_exe().unwrap().parent().unwrap(),
Path::new(&uuid::Uuid::new_v4().to_string()),
]
.iter()
.collect();
let _ = create_dir(&path);
Self {
path,
}
}
}
impl Drop for TestArea {
fn drop(&mut self) {
let _ = remove_dir_all(&self.path);
}
}
#[test]
fn detached() {
let test_area = TestArea::new();
let mock_subprocess = PathBuf::from(
String::from_utf8_lossy(
&std::process::Command::new("cargo")
.args(&["run", "--bin", "mock_subprocess", "--", "where"])
.output()
.unwrap()
.stdout,
)
.to_string(),
);
let args = vec![
OsString::from("detached"),
test_area.path().as_os_str().to_owned(),
OsString::from("abc"),
OsString::from("def ghi"),
];
let reported_pid = start_detached(mock_subprocess, &args);
assert_ne!(0, reported_pid);
sleep(Duration::from_millis(250));
let pid = read_to_string(
[test_area.path(), Path::new("pid")].iter().collect::<PathBuf>(),
)
.unwrap();
let pid = pid.trim().parse::<usize>().unwrap();
assert_eq!(pid, reported_pid);
let lines = BufReader::new(
File::open(
[test_area.path(), Path::new("args")]
.iter()
.collect::<PathBuf>(),
)
.unwrap(),
)
.lines()
.map(Result::unwrap)
.map(OsString::from)
.collect::<Vec<_>>();
assert_eq!(args, lines);
#[cfg(unix)]
{
let handles = read_to_string(
[test_area.path(), Path::new("handles")]
.iter()
.collect::<PathBuf>(),
)
.unwrap();
assert_eq!(0, handles.len(), "Handles: {}", handles);
}
drop(test_area);
}
#[test]
fn find_self_by_image_path() {
let mut processes = list_processes();
let self_path = current_exe().unwrap().canonicalize().unwrap();
let self_id = usize::try_from(std::process::id()).unwrap();
assert!(processes.any(|process| {
process.image == self_path && process.id == self_id
}));
}
#[test]
fn find_self_by_tcp_server_port() {
let tcp = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap();
let port = tcp.local_addr().unwrap().port();
let mut processes = list_processes();
let self_id = usize::try_from(std::process::id()).unwrap();
assert!(processes.any(|process| {
process.tcp_server_ports.contains(&port) && process.id == self_id
}));
}
}