use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Match {
pub pid: u32,
pub command: String,
}
pub fn matching(pattern: &str) -> Vec<Match> {
let listed = crate::git_backend::command_without_repo_scope("pgrep")
.args(["-fl", pattern])
.output();
let Ok(listed) = listed else {
return Vec::new();
};
let text = String::from_utf8_lossy(&listed.stdout).into_owned();
parse_matches(&text, &lineage())
}
pub fn parse_matches(text: &str, ours: &BTreeSet<u32>) -> Vec<Match> {
text.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}
let (pid, command) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
let pid: u32 = pid.trim().parse().ok()?;
if ours.contains(&pid) {
return None;
}
Some(Match {
pid,
command: command.trim().to_string(),
})
})
.collect()
}
pub fn lineage() -> BTreeSet<u32> {
let mut found = BTreeSet::new();
let mut pid = std::process::id();
for _ in 0..32 {
if !found.insert(pid) {
break;
}
let Some(parent) = parent_of(pid) else { break };
if parent <= 1 {
break;
}
pid = parent;
}
found
}
fn parent_of(pid: u32) -> Option<u32> {
let output = crate::git_backend::command_without_repo_scope("ps")
.args(["-o", "ppid=", "-p", &pid.to_string()])
.output()
.ok()?;
String::from_utf8_lossy(&output.stdout).trim().parse().ok()
}
pub fn signal(pids: &[u32], signal: &str) -> Vec<Signalled> {
let known = Signal::named(signal);
pids.iter()
.map(|pid| Signalled {
pid: *pid,
outcome: match &known {
Ok(signal) => signal_one(*pid, *signal),
Err(refusal) => SignalOutcome::Failed(refusal.clone()),
},
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Signal {
Term,
Kill,
Int,
}
impl Signal {
fn named(name: &str) -> Result<Self, String> {
match name {
"TERM" => Ok(Signal::Term),
"KILL" => Ok(Signal::Kill),
"INT" => Ok(Signal::Int),
other => Err(format!("unknown signal {other}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signalled {
pub pid: u32,
pub outcome: SignalOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignalOutcome {
Sent,
Gone,
NotPermitted,
Failed(String),
}
impl SignalOutcome {
pub fn is_failure(&self) -> bool {
matches!(self, SignalOutcome::NotPermitted | SignalOutcome::Failed(_))
}
}
#[cfg(unix)]
fn signal_one(pid: u32, signal: Signal) -> SignalOutcome {
let number = match signal {
Signal::Term => libc::SIGTERM,
Signal::Kill => libc::SIGKILL,
Signal::Int => libc::SIGINT,
};
let sent = unsafe { libc::kill(pid as libc::pid_t, number) };
if sent == 0 {
return SignalOutcome::Sent;
}
let error = std::io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ESRCH) => SignalOutcome::Gone,
Some(libc::EPERM) => SignalOutcome::NotPermitted,
_ => SignalOutcome::Failed(error.to_string()),
}
}
#[cfg(windows)]
fn signal_one(pid: u32, _signal: Signal) -> SignalOutcome {
const NO_SUCH_PROCESS: i32 = 128;
let status = crate::git_backend::command_without_repo_scope("taskkill")
.args(["/PID", &pid.to_string(), "/T", "/F"])
.output();
match status {
Ok(out) if out.status.success() => SignalOutcome::Sent,
Ok(out) if out.status.code() == Some(NO_SUCH_PROCESS) => SignalOutcome::Gone,
Ok(out) => SignalOutcome::Failed(
String::from_utf8_lossy(&out.stderr).trim().to_string(),
),
Err(error) => SignalOutcome::Failed(error.to_string()),
}
}
#[cfg(unix)]
pub fn is_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
#[cfg(windows)]
pub fn is_alive(pid: u32) -> bool {
crate::git_backend::command_without_repo_scope("tasklist")
.args(["/FI", &format!("PID eq {pid}")])
.output()
.map(|out| String::from_utf8_lossy(&out.stdout).contains(&pid.to_string()))
.unwrap_or(false)
}
pub const STOP_GRACE: std::time::Duration = std::time::Duration::from_secs(20);
pub fn descendants_in(table: &[(u32, u32)], root: u32) -> Vec<u32> {
let mut found: Vec<u32> = Vec::new();
let mut seen: BTreeSet<u32> = BTreeSet::from([root]);
let mut level = vec![root];
while !level.is_empty() {
let next: Vec<u32> = table
.iter()
.filter(|(pid, parent)| level.contains(parent) && seen.insert(*pid))
.map(|(pid, _)| *pid)
.collect();
found.extend(&next);
level = next;
}
found.reverse();
found
}
#[cfg(unix)]
pub fn descendants() -> Result<Vec<u32>, String> {
let output = crate::git_backend::command_without_repo_scope("ps")
.args(["-axo", "pid=,ppid="])
.output()
.map_err(|e| format!("running ps: {e}"))?;
if !output.status.success() {
return Err(format!("ps failed: {}", String::from_utf8_lossy(&output.stderr).trim()));
}
let table: Vec<(u32, u32)> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let mut fields = line.split_whitespace();
Some((fields.next()?.parse().ok()?, fields.next()?.parse().ok()?))
})
.collect();
Ok(descendants_in(&table, std::process::id()))
}
#[cfg(unix)]
fn end_descendants() {
let started = match descendants() {
Ok(started) => started,
Err(error) => {
crate::errln!("wsb: cannot list what this process started ({error}); nothing was ended");
return;
}
};
for pid in &started {
crate::platform::end_politely(*pid as i32);
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
while std::time::Instant::now() < deadline && started.iter().any(|pid| is_alive(*pid)) {
std::thread::sleep(std::time::Duration::from_millis(100));
}
for pid in started.iter().filter(|pid| is_alive(**pid)) {
crate::platform::end_hard(*pid as i32);
}
}
pub fn end_when_stopped(flag: std::sync::Arc<std::sync::atomic::AtomicBool>, grace: std::time::Duration) {
static WATCHING: std::sync::Once = std::sync::Once::new();
WATCHING.call_once(|| {
std::thread::spawn(move || {
while !flag.load(std::sync::atomic::Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(200));
}
std::thread::sleep(grace);
crate::errln!(
"wsb: asked to stop {} s ago and still running; ending what it started and exiting",
grace.as_secs()
);
#[cfg(unix)]
end_descendants();
#[cfg(windows)]
crate::platform::end_group(std::process::id(), false);
std::process::exit(STOPPED_EXIT_CODE);
});
});
}
pub const STOPPED_EXIT_CODE: i32 = 130;
pub fn wait_passing_stop(
child: &mut std::process::Child,
flag: &std::sync::atomic::AtomicBool,
grace: std::time::Duration,
) -> std::io::Result<std::process::ExitStatus> {
let mut asked: Option<std::time::Instant> = None;
let mut ended_hard = false;
loop {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
if flag.load(std::sync::atomic::Ordering::SeqCst) {
match asked {
None => {
crate::platform::end_politely(child.id() as i32);
asked = Some(std::time::Instant::now());
}
Some(at) if !ended_hard && at.elapsed() > grace + grace / 2 => {
crate::platform::end_hard(child.id() as i32);
ended_hard = true;
}
Some(_) => {}
}
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
pub fn any_running(pattern: &str) -> bool {
!matching(pattern).is_empty()
}
pub fn describe(matches: &[Match]) -> String {
matches
.iter()
.map(|found| format!("{} {}", found.pid, found.command))
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
#[path = "process_tests.rs"]
mod tests;