objectiveai_cli/command/kill_helpers.rs
1//! Shared kill-by-lock-owner logic for the `{api,db,mcp,viewer} kill`
2//! commands.
3//!
4//! A server's identity on disk is the lockfile it holds: the api at
5//! `<dir>/bin/locks` key `api` (machine-wide, one lock), and db /
6//! mcp / viewer at `<dir>/state/<state>/locks` keyed `db` / `mcp` /
7//! `viewer` (per state). Killing a server means reading the live
8//! owner PIDs of that lockfile via [`objectiveai_sdk::lockfile::owners`]
9//! and terminating them — for db, killing the supervisor takes the
10//! postmaster with it (job object / `PR_SET_PDEATHSIG`).
11
12use std::path::PathBuf;
13
14use crate::context::Context;
15use crate::error::Error;
16
17/// Read the owner PIDs of `(locks_dir, key)` and kill each. Returns
18/// the count actually terminated. A lockfile with no live owner
19/// yields zero — idempotent.
20pub async fn kill_lock_owners(locks_dir: PathBuf, key: &str) -> Result<usize, Error> {
21 let pids = objectiveai_sdk::lockfile::owners(&locks_dir, key)
22 .await
23 .map_err(|e| Error::Spawn(format!("read lock owners for {key}"), e))?;
24 let mut killed = 0;
25 for pid in pids {
26 killed += crate::spawn::kill_pid(pid);
27 }
28 Ok(killed)
29}
30
31/// Kill the per-state lockfile owners for `key` across the scope:
32/// `--state` hits only the current `OBJECTIVEAI_STATE`; `--global`
33/// fans out across every `<dir>/state/<name>/locks` concurrently and
34/// sums the kills.
35pub async fn kill_per_state(
36 ctx: &Context,
37 scope: objectiveai_sdk::cli::command::SetScope,
38 key: &'static str,
39) -> Result<usize, Error> {
40 match scope {
41 objectiveai_sdk::cli::command::SetScope::State => {
42 let locks_dir = ctx.filesystem.state_dir().join("locks");
43 kill_lock_owners(locks_dir, key).await
44 }
45 objectiveai_sdk::cli::command::SetScope::Global => {
46 let states_root = ctx.filesystem.dir().join("state");
47 let mut dirs: Vec<PathBuf> = Vec::new();
48 match tokio::fs::read_dir(&states_root).await {
49 Ok(mut rd) => {
50 while let Some(entry) = rd
51 .next_entry()
52 .await
53 .map_err(|e| Error::Spawn("list states".to_string(), e))?
54 {
55 if entry
56 .file_type()
57 .await
58 .map(|t| t.is_dir())
59 .unwrap_or(false)
60 {
61 dirs.push(entry.path().join("locks"));
62 }
63 }
64 }
65 // No state dir at all → nothing to kill.
66 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
67 Err(e) => return Err(Error::Spawn("open state dir".to_string(), e)),
68 }
69 let results = futures::future::join_all(
70 dirs.into_iter().map(|d| kill_lock_owners(d, key)),
71 )
72 .await;
73 let mut total = 0;
74 for r in results {
75 total += r?;
76 }
77 Ok(total)
78 }
79 }
80}