@@ -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 }