Skip to main content

rbnx/cmd/
shutdown.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2// `rbnx shutdown` — tear down a stack previously brought up by `rbnx boot`.
3//
4// Reads `<manifest-dir>/rbnx-boot/state.json` (written incrementally by
5// boot at every successful spawn), kills each component's process group,
6// and sweeps any docker container boot recorded as a driver host. After
7// teardown the state file is deleted so a stale state can't confuse the
8// next `rbnx shutdown`.
9
10use anyhow::{Context, Result};
11use robonix_cli::output;
12use std::path::PathBuf;
13
14use super::teardown;
15
16pub async fn execute(file: PathBuf) -> Result<()> {
17    let manifest_path = file.canonicalize().with_context(|| {
18        format!(
19            "manifest not found: {} (rbnx shutdown defaults to ./robonix_manifest.yaml; \
20             pass -f to point elsewhere)",
21            file.display()
22        )
23    })?;
24    let manifest_dir = manifest_path
25        .parent()
26        .context("manifest has no parent directory")?
27        .to_path_buf();
28
29    let state_path = teardown::state_path(&manifest_dir);
30    if !state_path.exists() {
31        anyhow::bail!(
32            "no boot state at {} — has `rbnx boot` been run from this manifest? \
33             (state is written when boot starts spawning components and removed on shutdown.)",
34            state_path.display()
35        );
36    }
37
38    let state = teardown::read_state(&state_path)?;
39    output::action(
40        "Shutting down",
41        &format!(
42            "{} ({} component(s))",
43            state.manifest_path,
44            state.components.len()
45        ),
46    );
47
48    teardown::teardown(&state.components).await;
49
50    // Best-effort: also signal the boot process itself (which is probably
51    // already dead because we just killed all its children, but if the
52    // user ran `rbnx boot` in the foreground and `rbnx shutdown` from
53    // another shell, this lets boot exit its signal-wait loop cleanly).
54    if state.boot_pid != 0 && state.boot_pid != std::process::id() {
55        let pid = nix::unistd::Pid::from_raw(state.boot_pid as i32);
56        let _ = nix::sys::signal::kill(pid, nix::sys::signal::Signal::SIGTERM);
57    }
58
59    let _ = std::fs::remove_file(&state_path);
60    output::success(&format!("torn down — removed {}", state_path.display()));
61    Ok(())
62}