Skip to main content

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}