on main help sign in
fix: enforce shutdown timeout when vm input never becomes ready
commitecfce7acf70239ea1427b5655bbb78ea8d43b101 authorrobcholz <84130577+robcholz@users.noreply.github.com> dateMon 9 Feb 2026, 00:59 (8 months ago) parent7065144
2 files changed+90−11
docs/tasks.md+1 src/vm_manager.rs+89−11
docs/tasks.md+1
@@ -64,3 +64,4 @@
64643. [ ] Redirect vm output to log.
65654. [ ] Redirect vm output to vibebox starting it.
66665. [ ] use anyhow to sync api.
67+6. [ ] add support for ipv6.
src/vm_manager.rs+89−11
@@ -30,8 +30,22 @@ use crate::{
3030
3131const VM_MANAGER_LOCK_NAME: &str = "vm.lock";
3232const VM_MANAGER_LOG_NAME: &str = "vm_manager.log";
33+const SHUTDOWN_RETRY_MS: u64 = 500;
34+#[cfg(test)]
35+const HARD_SHUTDOWN_TIMEOUT_MS: u64 = 1_000;
36+#[cfg(not(test))]
3337const HARD_SHUTDOWN_TIMEOUT_MS: u64 = 12_000;
3438
39+#[cfg(not(test))]
40+fn force_exit(_reason: &str) -> ! {
41+ std::process::exit(1);
42+}
43+
44+#[cfg(test)]
45+fn force_exit(reason: &str) -> ! {
46+ panic!("{reason}");
47+}
48+
3549pub fn ensure_manager(
3650 raw_args: &[std::ffi::OsString],
3751 auto_shutdown_ms: u64,
@@ -782,9 +796,14 @@ fn manager_event_loop(
782796 let hard_timeout = Duration::from_millis(HARD_SHUTDOWN_TIMEOUT_MS);
783797
784798 loop {
785− let timeout = match shutdown_deadline {
786− Some(deadline) => deadline.saturating_duration_since(Instant::now()),
787− None => Duration::from_secs(1),
799+ let timeout = match (shutdown_deadline, hard_deadline) {
800+ (Some(shutdown), Some(hard)) => {
801+ let next = if shutdown <= hard { shutdown } else { hard };
802+ next.saturating_duration_since(Instant::now())
803+ }
804+ (Some(shutdown), None) => shutdown.saturating_duration_since(Instant::now()),
805+ (None, Some(hard)) => hard.saturating_duration_since(Instant::now()),
806+ (None, None) => Duration::from_secs(1),
788807 };
789808
790809 match event_rx.recv_timeout(timeout) {
@@ -824,6 +843,9 @@ fn manager_event_loop(
824843 && Instant::now() >= deadline
825844 && !shutdown_sent
826845 {
846+ if hard_deadline.is_none() {
847+ hard_deadline = Some(Instant::now() + hard_timeout);
848+ }
827849 let mut sent = false;
828850 if let Some(tx) = vm_input_tx.lock().unwrap().clone() {
829851 if tx
@@ -843,19 +865,26 @@ fn manager_event_loop(
843865 shutdown_deadline = None;
844866 hard_deadline = Some(Instant::now() + hard_timeout);
845867 } else {
846− shutdown_deadline = Some(Instant::now() + Duration::from_millis(500));
868+ shutdown_deadline =
869+ Some(Instant::now() + Duration::from_millis(SHUTDOWN_RETRY_MS));
847870 }
848871 }
849872 if ref_count == 0
850− && shutdown_sent
851873 && let Some(deadline) = hard_deadline
852874 && Instant::now() >= deadline
853875 {
854− tracing::warn!(
855− timeout_ms = HARD_SHUTDOWN_TIMEOUT_MS,
856− "force exiting: VM did not stop after shutdown timeout"
857− );
858− std::process::exit(1);
876+ if shutdown_sent {
877+ tracing::warn!(
878+ timeout_ms = HARD_SHUTDOWN_TIMEOUT_MS,
879+ "force exiting: VM did not stop after shutdown timeout"
880+ );
881+ } else {
882+ tracing::warn!(
883+ timeout_ms = HARD_SHUTDOWN_TIMEOUT_MS,
884+ "force exiting: VM input not ready after shutdown timeout"
885+ );
886+ }
887+ force_exit("vm manager forced exit");
859888 }
860889 }
861890 Err(mpsc::RecvTimeoutError::Disconnected) => break,
@@ -868,7 +897,7 @@ fn manager_event_loop(
868897#[cfg(test)]
869898mod tests {
870899 use super::*;
871− use std::{sync::mpsc, time::Duration};
900+ use std::{sync::mpsc, thread, time::Duration};
872901
873902 #[test]
874903 fn manager_powers_off_after_grace_when_no_refs() {
@@ -901,4 +930,53 @@ mod tests {
901930 let _ = event_tx.send(ManagerEvent::VmExited(None));
902931 let _ = manager_thread.join();
903932 }
933+
934+ #[test]
935+ fn manager_force_exits_when_vm_input_never_ready() {
936+ let (event_tx, event_rx) = mpsc::channel::<ManagerEvent>();
937+ let vm_input_tx = Arc::new(Mutex::new(None));
938+
939+ let manager_thread = thread::spawn(move || {
940+ let _ = manager_event_loop(event_rx, vm_input_tx, 10);
941+ });
942+
943+ event_tx.send(ManagerEvent::Inc(None)).unwrap();
944+ event_tx.send(ManagerEvent::Dec(None)).unwrap();
945+
946+ let join_result = manager_thread.join();
947+ assert!(
948+ join_result.is_err(),
949+ "expected manager to force-exit when vm input never becomes ready"
950+ );
951+ }
952+
953+ #[test]
954+ fn manager_sends_shutdown_after_vm_input_becomes_ready() {
955+ let (event_tx, event_rx) = mpsc::channel::<ManagerEvent>();
956+ let (vm_tx, vm_rx) = mpsc::channel::<VmInput>();
957+ let vm_input_tx = Arc::new(Mutex::new(None));
958+ let vm_input_for_thread = vm_input_tx.clone();
959+
960+ let manager_thread = thread::spawn(move || {
961+ manager_event_loop(event_rx, vm_input_for_thread, 10).expect("event loop");
962+ });
963+
964+ event_tx.send(ManagerEvent::Inc(None)).unwrap();
965+ event_tx.send(ManagerEvent::Dec(None)).unwrap();
966+
967+ thread::sleep(Duration::from_millis(100));
968+ *vm_input_tx.lock().unwrap() = Some(vm_tx);
969+
970+ let msg = vm_rx
971+ .recv_timeout(Duration::from_secs(2))
972+ .expect("poweroff");
973+ match msg {
974+ VmInput::Bytes(data) => {
975+ assert_eq!(data, b"systemctl poweroff\n");
976+ }
977+ _ => panic!("unexpected vm input"),
978+ }
979+ let _ = event_tx.send(ManagerEvent::VmExited(None));
980+ let _ = manager_thread.join();
981+ }
904982}