1use 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 raw_manifest = std::fs::read_to_string(&manifest_path)
34 .with_context(|| format!("read {}", manifest_path.display()))?;
35 let manifest_value: serde_yaml::Value = serde_yaml::from_str(&raw_manifest)
36 .with_context(|| format!("parse {}", manifest_path.display()))?;
37 super::deploy::prepare_manifest(manifest_value, None)
38 .context("apply top-level deploy env for shutdown hooks")?;
39
40 let state_path = teardown::state_path(&manifest_dir);
41 if !state_path.exists() {
42 anyhow::bail!(
43 "no boot state at {} — has `rbnx boot` been run from this manifest? \
44 (state is written when boot starts spawning components and removed on shutdown.)",
45 state_path.display()
46 );
47 }
48
49 let state = teardown::read_state(&state_path)?;
50 output::action(
51 "Shutting down",
52 &format!(
53 "{} ({} component(s))",
54 state.manifest_path,
55 state.components.len()
56 ),
57 );
58
59 let boot_id = (!state.boot_id.is_empty()).then_some(state.boot_id.as_str());
60 let complete =
61 teardown::teardown(Some(&state.atlas_endpoint), &state.components, boot_id).await;
62 if !complete {
63 anyhow::bail!(
64 "shutdown refused one or more stale/mismatched process groups; preserving {}",
65 state_path.display()
66 );
67 }
68
69 let boot_identity_matches = state.boot_start_time_ticks.is_none_or(|expected| {
74 robonix_cli::launch::proc_start_time_ticks(state.boot_pid) == Some(expected)
75 });
76 if state.boot_pid != 0 && state.boot_pid != std::process::id() && boot_identity_matches {
77 let pid = nix::unistd::Pid::from_raw(state.boot_pid as i32);
78 let _ = nix::sys::signal::kill(pid, nix::sys::signal::Signal::SIGTERM);
79 }
80
81 let _ = std::fs::remove_file(&state_path);
82 output::success(&format!("torn down — removed {}", state_path.display()));
83 Ok(())
84}