on main help sign in
fix: handled edge network issue when doing provisioning
commitb433d3ef93a2d1b4523f806c656286114f0fcc49 authorrobcholz <84130577+robcholz@users.noreply.github.com> dateMon 9 Feb 2026, 01:16 (8 months ago) parentecfce7a
2 files changed+151−6
src/provision.sh+46−2 src/vm.rs+105−4
src/provision.sh+46−2
@@ -1,12 +1,55 @@
11#!/bin/bash
2−set -eux
2+set -eEux
3+
4+trap 'echo "[vibebox][error] provisioning failed"; echo "VIBEBOX_PROVISION_FAILED"; systemctl poweroff || true; exit 1' ERR
5+
6+# Wait for network + DNS before apt-get to avoid early boot flakiness.
7+wait_for_network() {
8+ echo "[vibebox] waiting for network/DNS readiness"
9+ local deadline=$((SECONDS + 60))
10+ while [ "$SECONDS" -lt "$deadline" ]; do
11+ local has_route=0
12+ if ip -4 route show default >/dev/null 2>&1; then
13+ has_route=1
14+ elif ip -6 route show default >/dev/null 2>&1; then
15+ has_route=1
16+ fi
17+ if [ "$has_route" -eq 1 ]; then
18+ if getent hosts deb.debian.org >/dev/null 2>&1; then
19+ return 0
20+ fi
21+ fi
22+ sleep 1
23+ done
24+ echo "[vibebox][warn] network/DNS still not ready after 60s; continuing" >&2
25+ echo "[vibebox][warn] /etc/resolv.conf:" >&2
26+ cat /etc/resolv.conf >&2 || true
27+ ip -br addr >&2 || true
28+ ip route >&2 || true
29+ ip -6 route >&2 || true
30+ return 0
31+}
32+
33+apt_update_with_retries() {
34+ local attempt=1
35+ while [ "$attempt" -le 5 ]; do
36+ if apt-get update; then
37+ return 0
38+ fi
39+ echo "[vibebox][warn] apt-get update failed (attempt ${attempt}/5); retrying..." >&2
40+ attempt=$((attempt + 1))
41+ sleep 2
42+ done
43+ return 1
44+}
345
446# Don't wait too long for slow mirrors.
547echo 'Acquire::http::Timeout "2";' | tee /etc/apt/apt.conf.d/99timeout
648echo 'Acquire::https::Timeout "2";' | tee -a /etc/apt/apt.conf.d/99timeout
749echo 'Acquire::Retries "2";' | tee -a /etc/apt/apt.conf.d/99timeout
850
9−apt-get update
51+wait_for_network
52+apt_update_with_retries
1053apt-get install -y --no-install-recommends \
1154 build-essential \
1255 pkg-config \
@@ -55,4 +98,5 @@ sleep 100 # sleep here so that we don't see the login screen flash up before the
5598EOF
5699
57100# Done provisioning, power off the VM
101+echo "VIBEBOX_PROVISION_OK"
58102systemctl poweroff
src/vm.rs+105−4
@@ -40,6 +40,7 @@ const DEFAULT_RAM_MB: u64 = 2048;
4040const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
4141const START_TIMEOUT: Duration = Duration::from_secs(60);
4242const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
43+const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
4344
4445struct StatusFile {
4546 path: PathBuf,
@@ -76,7 +77,15 @@ const BASE_DISK_RAW_NAME: &str = "disk.raw";
7677
7778#[derive(Clone)]
7879pub(crate) enum LoginAction {
79− Expect { text: String, timeout: Duration },
80+ Expect {
81+ text: String,
82+ timeout: Duration,
83+ },
84+ ExpectEither {
85+ success: String,
86+ failure: String,
87+ timeout: Duration,
88+ },
8089 Send(String),
8190}
8291use LoginAction::*;
@@ -359,6 +368,12 @@ enum WaitResult {
359368 Found,
360369}
361370
371+#[derive(PartialEq, Eq)]
372+enum WaitAnyResult {
373+ Timeout,
374+ Found(usize),
375+}
376+
362377pub enum VmInput {
363378 Bytes(Vec<u8>),
364379 Shutdown,
@@ -366,6 +381,7 @@ pub enum VmInput {
366381
367382enum VmOutput {
368383 LoginActionTimeout { action: String, timeout: Duration },
384+ LoginActionFailed { action: String, reason: String },
369385}
370386
371387#[derive(Default)]
@@ -402,6 +418,41 @@ impl OutputMonitor {
402418 WaitResult::Found
403419 }
404420 }
421+
422+ fn wait_for_any(&self, needles: &[&str], timeout: Duration) -> WaitAnyResult {
423+ let mut found: Option<usize> = None;
424+ let (_unused, timeout_result) = self
425+ .condvar
426+ .wait_timeout_while(self.buffer.lock().unwrap(), timeout, |buf| {
427+ if let Some((pos, idx, len)) = find_any(buf, needles) {
428+ *buf = buf[(pos + len)..].to_string();
429+ found = Some(idx);
430+ false
431+ } else {
432+ true
433+ }
434+ })
435+ .unwrap();
436+
437+ if timeout_result.timed_out() {
438+ WaitAnyResult::Timeout
439+ } else {
440+ WaitAnyResult::Found(found.unwrap_or(0))
441+ }
442+ }
443+}
444+
445+fn find_any(buf: &str, needles: &[&str]) -> Option<(usize, usize, usize)> {
446+ let mut best: Option<(usize, usize, usize)> = None; // (pos, idx, len)
447+ for (idx, needle) in needles.iter().enumerate() {
448+ if let Some(pos) = buf.find(needle) {
449+ let candidate = (pos, idx, needle.len());
450+ if best.is_none_or(|b| candidate.0 < b.0) {
451+ best = Some(candidate);
452+ }
453+ }
454+ }
455+ best
405456}
406457
407458#[derive(Debug)]
@@ -546,14 +597,25 @@ fn ensure_default_image(
546597 fs::copy(base_raw, default_raw)?;
547598
548599 let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
549− run_vm(
600+ let provision_actions = [
601+ Send(provision_command),
602+ ExpectEither {
603+ success: "VIBEBOX_PROVISION_OK".to_string(),
604+ failure: "VIBEBOX_PROVISION_FAILED".to_string(),
605+ timeout: PROVISION_EXPECT_TIMEOUT,
606+ },
607+ ];
608+ if let Err(err) = run_vm(
550609 default_raw,
551− &[Send(provision_command)],
610+ &provision_actions,
552611 directory_shares,
553612 DEFAULT_CPU_COUNT,
554613 DEFAULT_RAM_BYTES,
555614 None,
556− )?;
615+ ) {
616+ let _ = fs::remove_file(default_raw);
617+ return Err(err);
618+ }
557619
558620 Ok(())
559621}
@@ -1033,6 +1095,27 @@ fn spawn_login_actions_thread(
10331095 return;
10341096 }
10351097 }
1098+ ExpectEither {
1099+ success,
1100+ failure,
1101+ timeout,
1102+ } => match output_monitor.wait_for_any(&[&success, &failure], timeout) {
1103+ WaitAnyResult::Found(0) => {}
1104+ WaitAnyResult::Found(_) => {
1105+ let _ = vm_output_tx.send(VmOutput::LoginActionFailed {
1106+ action: format!("expect '{}'", success),
1107+ reason: format!("saw failure marker '{}'", failure),
1108+ });
1109+ return;
1110+ }
1111+ WaitAnyResult::Timeout => {
1112+ let _ = vm_output_tx.send(VmOutput::LoginActionTimeout {
1113+ action: format!("expect '{}'", success),
1114+ timeout,
1115+ });
1116+ return;
1117+ }
1118+ },
10361119 Send(mut text) => {
10371120 text.push('\n'); // Type the newline so the command is actually submitted.
10381121 input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
@@ -1192,6 +1275,24 @@ where
11921275 }
11931276 break;
11941277 }
1278+ Ok(VmOutput::LoginActionFailed { action, reason }) => {
1279+ exit_result = Err(format!(
1280+ "Login action ({}) failed: {}; shutting down.",
1281+ action, reason
1282+ )
1283+ .into());
1284+ unsafe {
1285+ if vm.canRequestStop() {
1286+ if let Err(err) = vm.requestStopWithError() {
1287+ tracing::error!(error = ?err, "failed to request VM stop");
1288+ }
1289+ } else if vm.canStop() {
1290+ let handler = RcBlock::new(|_error: *mut NSError| {});
1291+ vm.stopWithCompletionHandler(&handler);
1292+ }
1293+ }
1294+ break;
1295+ }
11951296 Err(mpsc::TryRecvError::Empty) => {}
11961297 Err(mpsc::TryRecvError::Disconnected) => {}
11971298 }