1use anyhow::{Context, Result};
17use robonix_cli::launch::{PackageRuntimeRecord, shutdown_package_runtime_checked};
18use serde::{Deserialize, Serialize};
19use std::path::{Path, PathBuf};
20use std::time::Duration;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct BootState {
24 pub manifest_path: String,
25 pub boot_pid: u32,
26 #[serde(default)]
27 pub boot_start_time_ticks: Option<u64>,
28 #[serde(default)]
29 pub boot_id: String,
30 pub started_at_ms: u64,
31 pub atlas_endpoint: String,
32 pub components: Vec<ComponentRecord>,
33}
34
35pub type ComponentRecord = PackageRuntimeRecord;
38
39pub fn state_path(manifest_dir: &Path) -> PathBuf {
40 manifest_dir.join("rbnx-boot").join("state.json")
41}
42
43pub fn write_state(path: &Path, state: &BootState) -> Result<()> {
44 if let Some(parent) = path.parent() {
45 std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
46 }
47 let text = serde_json::to_string_pretty(state)?;
48 let temp = path.with_extension(format!("json.{}.tmp", std::process::id()));
49 std::fs::write(&temp, text).with_context(|| format!("write {}", temp.display()))?;
50 std::fs::rename(&temp, path)
51 .with_context(|| format!("replace {} with {}", path.display(), temp.display()))?;
52 Ok(())
53}
54
55pub fn read_state(path: &Path) -> Result<BootState> {
56 let raw = std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
57 let state: BootState =
58 serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
59 Ok(state)
60}
61
62pub async fn teardown(
65 atlas_endpoint: Option<&str>,
66 components: &[ComponentRecord],
67 boot_id: Option<&str>,
68) -> bool {
69 let mut complete = true;
70 for c in components.iter().rev() {
72 robonix_cli::output::sub_step(&format!(
73 "[shutdown] {} stopping (pid={}, pgid={})",
74 c.name, c.pid, c.pgid
75 ));
76 complete &=
77 shutdown_package_runtime_checked(atlas_endpoint, c, Duration::from_secs(30), boot_id)
78 .await;
79 }
80 complete
81}