ptools 0.2.23

Utilities for inspecting Linux processes
//
//   Copyright (c) 2026 Basil Crow
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
//

use std::process;
use std::thread;
use std::time::Duration;

use nix::sys::signal::Signal;
use nix::sys::signal::{self};
use nix::unistd::Pid;
use ptools::model::stat::ProcState;

/// Poll until the process reaches stopped state or no longer exists.
/// Uses exponential backoff starting at 10ms, capped at 100ms.
fn verify_stopped(pid: u64) -> bool {
    let mut backoff = Duration::from_millis(10);
    let cap = Duration::from_millis(100);
    let mut warned_d = false;
    loop {
        let handle = ptools::proc::ProcHandle::from_pid(pid);
        match handle.state() {
            Ok(ProcState::Stopped) => return true,
            Ok(ProcState::Tracing) => {
                eprintln!("pstop: process {pid} is stopped under a debugger, not by us");
                return false;
            }
            Err(_) => {
                eprintln!("pstop: process {pid} has exited");
                return false;
            }
            Ok(ProcState::Waiting) => {
                if !warned_d {
                    eprintln!(
                        "pstop: process {pid} is in uninterruptible sleep; \
                         SIGSTOP is pending but may take time to take effect"
                    );
                    warned_d = true;
                }
                thread::sleep(backoff);
                backoff = (backoff * 2).min(cap);
            }
            Ok(_) => {
                thread::sleep(backoff);
                backoff = (backoff * 2).min(cap);
            }
        }
    }
}

fn stop_process(pid: u64) -> bool {
    let nix_pid = Pid::from_raw(pid as i32);
    if let Err(e) = signal::kill(nix_pid, Signal::SIGSTOP) {
        eprintln!("pstop: cannot stop {pid}: {e}");
        return false;
    }
    verify_stopped(pid)
}

struct Args {
    pid: Vec<u64>,
}

fn print_usage() {
    eprintln!("Usage: pstop PID...");
    eprintln!("Stop processes with SIGSTOP. A /proc/pid path may also be used.");
    eprintln!();
    eprintln!("Options:");
    eprintln!("  -h, --help       Print help");
    eprintln!("  -V, --version    Print version");
}

fn parse_args() -> Args {
    use lexopt::prelude::*;

    let mut args = Args { pid: Vec::new() };
    let mut parser = lexopt::Parser::from_env();

    while let Some(arg) = parser.next().unwrap_or_else(|e| {
        eprintln!("pstop: {e}");
        process::exit(2);
    }) {
        match arg {
            Short('h') | Long("help") => {
                print_usage();
                process::exit(0);
            }
            Short('V') | Long("version") => {
                println!("pstop {}", env!("CARGO_PKG_VERSION"));
                process::exit(0);
            }
            Value(val) => {
                let s = val.to_string_lossy();
                match ptools::proc::parse_pid_arg(&s) {
                    Ok(ptools::proc::PidArg::Pid(pid)) => args.pid.push(pid),
                    Ok(ptools::proc::PidArg::Skip) => {}
                    Err(msg) => {
                        eprintln!("pstop: {msg}");
                        process::exit(2);
                    }
                }
            }
            _ => {
                eprintln!("pstop: unexpected argument: {arg:?}");
                process::exit(2);
            }
        }
    }

    if args.pid.is_empty() {
        eprintln!("pstop: at least one PID required");
        process::exit(2);
    }
    args
}

fn main() {
    ptools::reset_sigpipe();
    let args = parse_args();
    let mut failed = false;

    for &pid in &args.pid {
        if !stop_process(pid) {
            failed = true;
        }
    }

    if failed {
        process::exit(1);
    }
}