rbnx/cmd/
boot_watchdog.rs1use anyhow::{Context, Result};
10use std::path::{Path, PathBuf};
11use std::process::Stdio;
12use std::time::Duration;
13
14use super::teardown;
15
16pub fn spawn(
17 state_path: &Path,
18 boot_pid: u32,
19 boot_start_time_ticks: Option<u64>,
20 boot_id: &str,
21) -> Result<()> {
22 use std::os::unix::process::CommandExt;
23
24 let exe = std::env::current_exe().context("resolve rbnx binary for boot watchdog")?;
25 let mut command = std::process::Command::new(exe);
26 command
27 .arg("__watch-boot")
28 .arg("--state")
29 .arg(state_path)
30 .arg("--boot-pid")
31 .arg(boot_pid.to_string())
32 .arg("--boot-id")
33 .arg(boot_id)
34 .stdin(Stdio::null())
35 .stdout(Stdio::null())
36 .stderr(Stdio::null());
37 if let Some(ticks) = boot_start_time_ticks {
38 command
39 .arg("--boot-start-time-ticks")
40 .arg(ticks.to_string());
41 }
42 unsafe {
45 command.pre_exec(|| {
46 nix::unistd::setsid()
47 .map(|_| ())
48 .map_err(std::io::Error::other)
49 });
50 }
51 command.spawn().context("spawn detached boot watchdog")?;
52 Ok(())
53}
54
55pub async fn execute(
56 state_path: PathBuf,
57 boot_pid: u32,
58 boot_start_time_ticks: Option<u64>,
59 boot_id: String,
60) -> Result<()> {
61 loop {
62 if !state_path.exists() {
63 return Ok(());
64 }
65 let alive = watched_boot_is_alive(boot_pid, boot_start_time_ticks, &boot_id);
66 if !alive {
67 break;
68 }
69 tokio::time::sleep(Duration::from_millis(250)).await;
70 }
71
72 let state = teardown::read_state(&state_path)?;
73 if !matches_watched_boot(&state, boot_pid, boot_start_time_ticks, &boot_id) {
74 return Ok(());
75 }
76 let state_boot_id = (!state.boot_id.is_empty()).then_some(state.boot_id.as_str());
77 let complete = teardown::teardown(
78 Some(&state.atlas_endpoint),
79 &state.components,
80 state_boot_id,
81 )
82 .await;
83 if !complete {
84 return Ok(());
85 }
86
87 if teardown::read_state(&state_path)
90 .ok()
91 .is_some_and(|current| {
92 matches_watched_boot(¤t, boot_pid, boot_start_time_ticks, &boot_id)
93 })
94 {
95 let _ = std::fs::remove_file(&state_path);
96 }
97 Ok(())
98}
99
100fn matches_watched_boot(
101 state: &teardown::BootState,
102 boot_pid: u32,
103 boot_start_time_ticks: Option<u64>,
104 boot_id: &str,
105) -> bool {
106 state.boot_pid == boot_pid
107 && state.boot_id == boot_id
108 && match (boot_start_time_ticks, state.boot_start_time_ticks) {
109 (Some(expected), Some(actual)) => expected == actual,
110 (Some(_), None) => false,
111 (None, _) => true,
112 }
113}
114
115fn watched_boot_is_alive(boot_pid: u32, boot_start_time_ticks: Option<u64>, boot_id: &str) -> bool {
116 match boot_start_time_ticks {
117 Some(expected) => robonix_cli::launch::proc_start_time_ticks(boot_pid) == Some(expected),
118 None => robonix_cli::launch::process_has_boot_id(boot_pid, boot_id),
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 #[ignore]
128 fn boot_identity_fixture_process() {
129 std::thread::sleep(Duration::from_secs(30));
130 }
131
132 #[test]
133 fn missing_start_ticks_fall_back_to_exact_live_boot_id() {
134 let boot_id = format!("watchdog-live-parent-{}", std::process::id());
135 let mut child = std::process::Command::new(std::env::current_exe().unwrap())
136 .args([
137 "--exact",
138 "cmd::boot_watchdog::tests::boot_identity_fixture_process",
139 "--ignored",
140 "--test-threads=1",
141 ])
142 .env("RBNX_BOOT_ID", &boot_id)
143 .spawn()
144 .unwrap();
145
146 let deadline = std::time::Instant::now() + Duration::from_secs(2);
147 let mut matched = false;
148 while std::time::Instant::now() < deadline {
149 if watched_boot_is_alive(child.id(), None, &boot_id) {
150 matched = true;
151 break;
152 }
153 std::thread::sleep(Duration::from_millis(10));
154 }
155 assert!(
156 matched,
157 "live boot parent must survive the no-procfs fallback"
158 );
159 assert!(!watched_boot_is_alive(
160 child.id(),
161 None,
162 "different-boot-id"
163 ));
164
165 let _ = child.kill();
166 let _ = child.wait();
167 }
168}