objectiveai_sdk/process.rs
1//! Process-management helpers that pair with [`crate::lockfile`]'s owner
2//! lookups: given a pid (e.g. from [`crate::lockfile::owners`] /
3//! [`crate::lockfile::owners_in_tree`]), ask the OS to terminate it —
4//! plus the repo-wide [`no_window`] spawn hygiene.
5
6/// Windows `CREATE_NO_WINDOW`: spawn the child without a console
7/// window. Our services are windowless (daemon, laboratory host, db
8/// supervisor, the viewer shell), so every console-subsystem child
9/// they spawn — podman, postgres, plugins, tools, runners, the cli
10/// re-execing itself — otherwise allocates and FLASHES its own
11/// console at the user. Applied at every runtime spawn site in the
12/// repository; no-op off Windows.
13#[cfg(any(
14 feature = "http",
15 feature = "mcp",
16 feature = "cli-executor",
17 feature = "cli-listener",
18 feature = "lockfile",
19 feature = "subprocess-reaper"
20))]
21pub fn no_window(command: &mut tokio::process::Command) {
22 #[cfg(windows)]
23 {
24 use std::os::windows::process::CommandExt;
25 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
26 command.creation_flags(CREATE_NO_WINDOW);
27 }
28 #[cfg(not(windows))]
29 {
30 let _ = command;
31 }
32}
33
34
35/// Send a terminate to `pid` — Unix `SIGTERM`, Windows `TerminateProcess`.
36/// Returns 1 if a live process with that pid was signalled, 0 otherwise
37/// (no such process, or the signal/open failed). Idempotent and
38/// best-effort: killing an already-dead pid is a 0, not an error.
39///
40/// Gated on the two features that carry the nix / windows-sys deps —
41/// the module itself exists for every subprocess-spawning feature
42/// (see the `no_window` gate).
43#[cfg(all(unix, any(feature = "lockfile", feature = "subprocess-reaper")))]
44pub fn kill_pid(pid: u32) -> usize {
45 // SAFETY: `kill(2)` with a valid signal number is sound — it only
46 // checks the target's existence and posts a signal, touching no
47 // memory. `SIGTERM` to a nonexistent pid returns -1 (ESRCH), which
48 // we map to 0.
49 let rc = unsafe { nix::libc::kill(pid as nix::libc::pid_t, nix::libc::SIGTERM) };
50 if rc == 0 { 1 } else { 0 }
51}
52
53#[cfg(all(windows, any(feature = "lockfile", feature = "subprocess-reaper")))]
54pub fn kill_pid(pid: u32) -> usize {
55 use windows_sys::Win32::Foundation::CloseHandle;
56 use windows_sys::Win32::System::Threading::{
57 OpenProcess, PROCESS_TERMINATE, TerminateProcess,
58 };
59 // SAFETY: `OpenProcess` returns null on failure (no such pid, or no
60 // rights) — we check before use. `TerminateProcess` and `CloseHandle`
61 // operate only on the handle we just opened; the handle is closed on
62 // every path.
63 unsafe {
64 let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
65 if handle.is_null() {
66 return 0;
67 }
68 let ok = TerminateProcess(handle, 1);
69 CloseHandle(handle);
70 if ok != 0 { 1 } else { 0 }
71 }
72}