running-process-py 3.4.1

PyO3 bindings for running-process
Documentation
use std::thread;
use std::time::{Duration, Instant};

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use sysinfo::{Signal, System};

use crate::helpers::{descendant_pids, system_pid, to_py_err};
use crate::registry::{process_created_at, same_process_identity, DetachedLaunchEntry};

pub(crate) fn kill_process_tree_impl(pid: u32, timeout_seconds: f64) {
    let mut system = System::new();
    system.refresh_processes();
    let pid = system_pid(pid);
    let Some(_) = system.process(pid) else {
        return;
    };

    let mut kill_order = descendant_pids(&system, pid);
    kill_order.reverse();
    kill_order.push(pid);

    for target in &kill_order {
        if let Some(process) = system.process(*target) {
            if !process.kill_with(Signal::Kill).unwrap_or(false) {
                process.kill();
            }
        }
    }

    let deadline = Instant::now()
        .checked_add(Duration::from_secs_f64(timeout_seconds.max(0.0)))
        .unwrap_or_else(Instant::now);
    loop {
        system.refresh_processes();
        if kill_order
            .iter()
            .all(|target| system.process(*target).is_none())
        {
            break;
        }
        if Instant::now() >= deadline {
            break;
        }
        thread::sleep(Duration::from_millis(25));
    }
}

#[pyfunction]
pub(crate) fn native_get_process_tree_info(pid: u32) -> String {
    let mut system = System::new();
    system.refresh_processes();
    let pid = system_pid(pid);
    let Some(process) = system.process(pid) else {
        return format!("Could not get process info for PID {}", pid.as_u32());
    };

    let mut info = vec![
        format!("Process {} ({})", pid.as_u32(), process.name()),
        format!("Status: {:?}", process.status()),
    ];
    let children = descendant_pids(&system, pid);
    if !children.is_empty() {
        info.push("Child processes:".to_string());
        for child_pid in children {
            if let Some(child) = system.process(child_pid) {
                info.push(format!("  Child {} ({})", child_pid.as_u32(), child.name()));
            }
        }
    }
    info.join("\n")
}

#[pyfunction]
#[pyo3(signature = (pid, timeout_seconds=3.0))]
pub(crate) fn native_kill_process_tree(pid: u32, timeout_seconds: f64) {
    kill_process_tree_impl(pid, timeout_seconds);
}

#[pyfunction]
pub(crate) fn native_process_created_at(pid: u32) -> Option<f64> {
    process_created_at(pid)
}

#[pyfunction]
#[pyo3(signature = (pid, created_at, tolerance_seconds=1.0))]
pub(crate) fn native_is_same_process(pid: u32, created_at: f64, tolerance_seconds: f64) -> bool {
    same_process_identity(pid, created_at, tolerance_seconds)
}

#[pyfunction]
#[pyo3(signature = (command, cwd=None, env=None, originator=None))]
pub(crate) fn native_launch_detached(
    py: Python<'_>,
    command: String,
    cwd: Option<String>,
    env: Option<Bound<'_, PyDict>>,
    originator: Option<String>,
) -> PyResult<DetachedLaunchEntry> {
    let command = command.trim().to_string();
    if command.is_empty() {
        return Err(PyValueError::new_err("command must not be empty"));
    }

    let env_pairs = env
        .map(|mapping| {
            mapping
                .iter()
                .map(|(key, value)| Ok((key.extract::<String>()?, value.extract::<String>()?)))
                .collect::<PyResult<Vec<(String, String)>>>()
        })
        .transpose()?
        .unwrap_or_default();

    let spawned = py
        .allow_threads(move || {
            let mut request = running_process_client::SpawnCommandRequest::shell(command);
            if let Some(cwd) = cwd {
                request = request.with_cwd(cwd);
            }
            for (key, value) in env_pairs {
                request = request.with_env(key, value);
            }
            if let Some(originator) = originator {
                request = request.with_originator(originator);
            }

            let mut client = running_process_client::connect_or_start(None)?;
            client.spawn_command(&request)
        })
        .map_err(to_py_err)?;

    Ok((
        spawned.pid,
        spawned.created_at,
        spawned.command,
        spawned.cwd,
        spawned.originator,
        spawned.containment,
    ))
}