@@ -1,5 +1,5 @@
1+use crate::instance::STATUS_FILE_NAME;
12use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
2−use anyhow::{Context, Error, Result, bail};
33use std::{
44 env, fs,
55 io::{self, Write},
@@ -23,7 +23,6 @@ use std::{
2323};
2424
2525use block2::RcBlock;
26−use bytesize::ByteSize;
2726use dispatch2::DispatchQueue;
2827use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
2928use objc2_foundation::*;
@@ -33,46 +32,75 @@ const DEBIAN_COMPRESSED_DISK_URL: &str = "https://cloud.debian.org/images/cloud/
3332const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
3433const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
3534const SHARED_DIRECTORIES_TAG: &str = "shared";
36−pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
3735
36+const BYTES_PER_MB: u64 = 1024 * 1024;
37+const DEFAULT_CPU_COUNT: usize = 2;
38+const DEFAULT_RAM_MB: u64 = 2048;
39+const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
3840const START_TIMEOUT: Duration = Duration::from_secs(60);
3941const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
40−const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
4142
42−const ERROR_REPORT_SCRIPT: &str = include_str!("error_report.sh");
43+struct StatusFile {
44+ path: PathBuf,
45+ cleared: AtomicBool,
46+}
47+
48+impl StatusFile {
49+ fn new(path: PathBuf) -> Self {
50+ Self {
51+ path,
52+ cleared: AtomicBool::new(false),
53+ }
54+ }
55+
56+ fn update(&self, message: &str) {
57+ let _ = fs::write(&self.path, message);
58+ }
59+
60+ fn clear(&self) {
61+ if !self.cleared.swap(true, Ordering::SeqCst) {
62+ let _ = fs::remove_file(&self.path);
63+ }
64+ }
65+}
66+
67+impl Drop for StatusFile {
68+ fn drop(&mut self) {
69+ if !self.cleared.load(Ordering::SeqCst) {
70+ let _ = fs::remove_file(&self.path);
71+ self.cleared.store(true, Ordering::SeqCst);
72+ }
73+ }
74+}
4375const PROVISION_SCRIPT: &str = include_str!("provision.sh");
76+const PROVISION_SCRIPT_NAME: &str = "provision.sh";
4477const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
4578const DEFAULT_RAW_NAME: &str = "default.raw";
4679const INSTANCE_RAW_NAME: &str = "instance.raw";
4780const BASE_DISK_RAW_NAME: &str = "disk.raw";
4881
4982#[derive(Clone)]
50−pub enum LoginAction {
51− Expect {
52− text: String,
53− timeout: Duration,
54− },
55− ExpectEither {
56− success: String,
57− failure: String,
58− timeout: Duration,
59− },
83+pub(crate) enum LoginAction {
84+ Expect { text: String, timeout: Duration },
6085 Send(String),
6186}
62−use crate::config::BoxConfig;
6387use LoginAction::*;
6488
6589#[derive(Clone)]
66−pub struct DirectoryShare {
90+pub(crate) struct DirectoryShare {
6791 host: PathBuf,
6892 guest: PathBuf,
6993 read_only: bool,
7094}
7195
7296impl DirectoryShare {
73− pub fn new(host: PathBuf, mut guest: PathBuf, read_only: bool) -> Result<Self> {
97+ pub(crate) fn new(
98+ host: PathBuf,
99+ mut guest: PathBuf,
100+ read_only: bool,
101+ ) -> Result<Self, Box<dyn std::error::Error>> {
74102 if !host.exists() {
75− bail!(format!("host path does not exist: {}", host.display()));
103+ return Err(format!("Host path does not exist: {}", host.display()).into());
76104 }
77105 if !guest.is_absolute() {
78106 guest = PathBuf::from("/root").join(guest);
@@ -84,10 +112,10 @@ impl DirectoryShare {
84112 })
85113 }
86114
87− pub fn from_mount_spec(spec: &str) -> Result<Self> {
115+ pub(crate) fn from_mount_spec(spec: &str) -> Result<Self, Box<dyn std::error::Error>> {
88116 let parts: Vec<&str> = spec.split(':').collect();
89117 if parts.len() < 2 || parts.len() > 3 {
90− bail!(format!("invalid mount spec: {spec}"));
118+ return Err(format!("Invalid mount spec: {spec}").into());
91119 }
92120 let host = expand_tilde_path(parts[0]);
93121 let guest = PathBuf::from(parts[1]);
@@ -96,10 +124,11 @@ impl DirectoryShare {
96124 "read-only" => true,
97125 "read-write" => false,
98126 _ => {
99− bail!(format!(
127+ return Err(format!(
100128 "Invalid mount mode '{}'; expected read-only or read-write",
101129 parts[2]
102− ));
130+ )
131+ .into());
103132 }
104133 }
105134 } else {
@@ -143,28 +172,19 @@ pub struct VmArg {
143172 pub mounts: Vec<String>,
144173}
145174
146−type StatusEmitter<'a> = dyn Fn(&str) + std::marker::Send + Sync + 'a;
147−
148−fn emit_status(status: Option<&StatusEmitter<'_>>, message: &str) {
149− if let Some(status) = status {
150− status(message);
151− }
152−}
153−
154−pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<()>
175+pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
155176where
156177 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
157178{
158− run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new(), None)
179+ run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new())
159180}
160181
161−pub fn run_with_args_and_extras<F>(
182+pub(crate) fn run_with_args_and_extras<F>(
162183 args: VmArg,
163184 io_handler: F,
164185 extra_login_actions: Vec<LoginAction>,
165186 extra_directory_shares: Vec<DirectoryShare>,
166− status: Option<&StatusEmitter<'_>>,
167−) -> Result<()>
187+) -> Result<(), Box<dyn std::error::Error>>
168188where
169189 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
170190{
@@ -173,7 +193,7 @@ where
173193 let project_root = env::current_dir()?;
174194 let project_name = project_root
175195 .file_name()
176− .with_context(|| "Project directory has no name")?
196+ .ok_or("Project directory has no name")?
177197 .to_string_lossy()
178198 .into_owned();
179199
@@ -185,10 +205,8 @@ where
185205 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
186206
187207 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
188− fs::create_dir_all(&instance_dir)?;
189− emit_status(status, "preparing VM image...");
190− tracing::info!("preparing VM image...");
191− let provision_log = instance_dir.join("provision.log");
208+ let status_file = StatusFile::new(instance_dir.join(STATUS_FILE_NAME));
209+ status_file.update("preparing VM image...");
192210
193211 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
194212 let base_compressed = cache_dir.join(basename_compressed);
@@ -212,14 +230,13 @@ where
212230 &base_compressed,
213231 &default_raw,
214232 std::slice::from_ref(&mise_directory_share),
215− Some(&provision_log),
216− status,
233+ Some(&status_file),
217234 )?;
218235 let _ = ensure_instance_disk(
219236 &instance_raw,
220237 &default_raw,
221− ByteSize(args.disk_bytes),
222− status,
238+ args.disk_bytes,
239+ Some(&status_file),
223240 )?;
224241 let base_size = fs::metadata(&default_raw)?.len();
225242 let instance_size = fs::metadata(&instance_raw)?.len();
@@ -230,8 +247,7 @@ where
230247 let mut directory_shares = Vec::new();
231248
232249 if !args.no_default_mounts {
233− let project_guest_dir = PathBuf::from(PROJECT_GUEST_BASE).join(project_name);
234− login_actions.push(Send(format!("cd {}", project_guest_dir.display())));
250+ login_actions.push(Send(format!("cd {project_name}")));
235251
236252 // discourage read/write of .git folder from within the VM. note that this isn't secure, since the VM runs as root and could unmount this.
237253 // I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
@@ -239,6 +255,15 @@ where
239255 login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
240256 }
241257
258+ directory_shares.push(
259+ DirectoryShare::new(
260+ project_root,
261+ PathBuf::from("/root/").join(project_name),
262+ false,
263+ )
264+ .expect("Project directory must exist"),
265+ );
266+
242267 directory_shares.push(mise_directory_share);
243268 }
244269
@@ -249,7 +274,7 @@ where
249274 }
250275
251276 if needs_resize {
252− let resize_cmd = script_command_from_content("resize_disk.sh", RESIZE_DISK_SCRIPT)?;
277+ let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
253278 login_actions.push(Send(resize_cmd));
254279 }
255280
@@ -265,29 +290,25 @@ where
265290 &directory_shares[..],
266291 args.cpu_count,
267292 args.ram_bytes,
268− status,
293+ Some(&status_file),
269294 io_handler,
270295 )
271296}
272297
273−pub fn script_command_from_content(label: &str, script: &str) -> Result<String> {
298+pub(crate) fn script_command_from_content(
299+ label: &str,
300+ script: &str,
301+) -> Result<String, Box<dyn std::error::Error>> {
274302 let marker = "VIBE_SCRIPT_EOF";
275303 let guest_dir = "/tmp/vibe-scripts";
276304 let guest_path = format!("{guest_dir}/{label}.sh");
277− let script_body = match script.split_once('\n') {
278− Some((first, rest)) if first.starts_with("#!") => rest,
279− _ => script,
280− };
281− let wrapped_script = ERROR_REPORT_SCRIPT
282− .replace("__LABEL__", label)
283− .replace("__SCRIPT_BODY__", script_body);
284305 let command = format!(
285− "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{wrapped_script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
306+ "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
286307 );
287308 if script.contains(marker) {
288− bail!(format!(
289− "Script '{label}' contains marker '{marker}', cannot safely upload"
290− ));
309+ return Err(
310+ format!("Script '{label}' contains marker '{marker}', cannot safely upload").into(),
311+ );
291312 }
292313 Ok(command)
293314}
@@ -351,12 +372,6 @@ enum WaitResult {
351372 Found,
352373}
353374
354−#[derive(PartialEq, Eq)]
355−enum WaitAnyResult {
356− Timeout,
357− Found(usize),
358−}
359−
360375pub enum VmInput {
361376 Bytes(Vec<u8>),
362377 Shutdown,
@@ -364,7 +379,6 @@ pub enum VmInput {
364379
365380enum VmOutput {
366381 LoginActionTimeout { action: String, timeout: Duration },
367− LoginActionFailed { action: String, reason: String },
368382}
369383
370384#[derive(Default)]
@@ -401,41 +415,6 @@ impl OutputMonitor {
401415 WaitResult::Found
402416 }
403417 }
404−
405− fn wait_for_any(&self, needles: &[&str], timeout: Duration) -> WaitAnyResult {
406− let mut found: Option<usize> = None;
407− let (_unused, timeout_result) = self
408− .condvar
409− .wait_timeout_while(self.buffer.lock().unwrap(), timeout, |buf| {
410− if let Some((pos, idx, len)) = find_any(buf, needles) {
411− *buf = buf[(pos + len)..].to_string();
412− found = Some(idx);
413− false
414− } else {
415− true
416− }
417− })
418− .unwrap();
419−
420− if timeout_result.timed_out() {
421− WaitAnyResult::Timeout
422− } else {
423− WaitAnyResult::Found(found.unwrap_or(0))
424− }
425− }
426−}
427−
428−fn find_any(buf: &str, needles: &[&str]) -> Option<(usize, usize, usize)> {
429− let mut best: Option<(usize, usize, usize)> = None; // (pos, idx, len)
430− for (idx, needle) in needles.iter().enumerate() {
431− if let Some(pos) = buf.find(needle) {
432− let candidate = (pos, idx, needle.len());
433− if best.is_none_or(|b| candidate.0 < b.0) {
434− best = Some(candidate);
435− }
436− }
437− }
438− best
439418}
440419
441420#[derive(Debug)]
@@ -484,16 +463,18 @@ impl IoControl {
484463fn ensure_base_image(
485464 base_raw: &Path,
486465 base_compressed: &Path,
487− status: Option<&StatusEmitter<'_>>,
488−) -> Result<()> {
466+ status: Option<&StatusFile>,
467+) -> Result<(), Box<dyn std::error::Error>> {
489468 if base_raw.exists() {
490469 return Ok(());
491470 }
492471
493472 if !base_compressed.exists()
494− || fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
473+ || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
495474 {
496− emit_status(status, "downloading base image...");
475+ if let Some(status) = status {
476+ status.update("downloading base image...");
477+ }
497478 tracing::info!("downloading base image");
498479 let status = Command::new("curl")
499480 .args([
@@ -508,14 +489,15 @@ fn ensure_base_image(
508489 ])
509490 .status()?;
510491 if !status.success() {
511− bail!("failed to download base image");
492+ return Err("Failed to download base image".into());
512493 }
513494 }
514495
515496 // Check SHA
516497 {
517− emit_status(status, "verifying base image...");
518− tracing::info!("verifying base image...");
498+ if let Some(status) = status {
499+ status.update("verifying base image...");
500+ }
519501 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
520502
521503 let mut child = Command::new("/usr/bin/shasum")
@@ -533,25 +515,25 @@ fn ensure_base_image(
533515
534516 let status = child.wait().expect("failed to wait on child");
535517 if !status.success() {
536− bail!(format!(
537− "SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}"
538− ));
518+ return Err(format!("SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}").into());
539519 }
540520 }
541521
542− emit_status(status, "decompressing base image...");
543− tracing::info!("decompressing base image...");
522+ if let Some(status) = status {
523+ status.update("decompressing base image...");
524+ }
525+ tracing::info!("decompressing base image");
544526 let status = Command::new("tar")
545527 .args([
546528 "-xOf",
547529 &base_compressed.to_string_lossy(),
548530 BASE_DISK_RAW_NAME,
549531 ])
550− .stdout(fs::File::create(base_raw)?)
532+ .stdout(std::fs::File::create(base_raw)?)
551533 .status()?;
552534
553535 if !status.success() {
554− bail!("Failed to decompress base image");
536+ return Err("Failed to decompress base image".into());
555537 }
556538
557539 Ok(())
@@ -562,56 +544,29 @@ fn ensure_default_image(
562544 base_compressed: &Path,
563545 default_raw: &Path,
564546 directory_shares: &[DirectoryShare],
565− provision_log: Option<&Path>,
566− status: Option<&StatusEmitter<'_>>,
567−) -> Result<()> {
547+ status: Option<&StatusFile>,
548+) -> Result<(), Box<dyn std::error::Error>> {
568549 if default_raw.exists() {
569550 return Ok(());
570551 }
571552
572553 ensure_base_image(base_raw, base_compressed, status)?;
573554
574− emit_status(status, "configuring base image...");
575− tracing::info!("configuring base image...");
555+ if let Some(status) = status {
556+ status.update("configuring base image...");
557+ }
558+ tracing::info!("configuring base image");
576559 fs::copy(base_raw, default_raw)?;
577560
578− let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
579− let provision_actions = [
580− Send(provision_command),
581− ExpectEither {
582− success: "VIBEBOX_PROVISION_OK".to_string(),
583− failure: "VIBEBOX_PROVISION_FAILED".to_string(),
584− timeout: PROVISION_EXPECT_TIMEOUT,
585− },
586− ];
587− let provision_result = if let Some(log_path) = provision_log {
588− let log_path = log_path.to_path_buf();
589− run_vm_with_io(
590− default_raw,
591− &provision_actions,
592− directory_shares,
593− BoxConfig::default().cpu_count,
594− BoxConfig::default().ram_size.as_u64(),
595− status,
596− move |output_monitor, vm_output_fd, vm_input_fd| {
597− spawn_vm_io_with_log(output_monitor, vm_output_fd, vm_input_fd, log_path)
598− },
599− )
600− } else {
601− run_vm(
602− default_raw,
603− &provision_actions,
604− directory_shares,
605− BoxConfig::default().cpu_count,
606− BoxConfig::default().ram_size.as_u64(),
607− status,
608− )
609− };
610−
611− if let Err(err) = provision_result {
612− let _ = fs::remove_file(default_raw);
613− return Err(err);
614− }
561+ let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
562+ run_vm(
563+ default_raw,
564+ &[Send(provision_command)],
565+ directory_shares,
566+ DEFAULT_CPU_COUNT,
567+ DEFAULT_RAM_BYTES,
568+ None,
569+ )?;
615570
616571 Ok(())
617572}
@@ -619,16 +574,18 @@ fn ensure_default_image(
619574fn ensure_instance_disk(
620575 instance_raw: &Path,
621576 template_raw: &Path,
622− target_bytes: ByteSize,
623− status: Option<&StatusEmitter<'_>>,
624−) -> Result<bool> {
577+ target_bytes: u64,
578+ status: Option<&StatusFile>,
579+) -> Result<bool, Box<dyn std::error::Error>> {
625580 if instance_raw.exists() {
626− let current_size = ByteSize(fs::metadata(instance_raw)?.len());
581+ let current_size = fs::metadata(instance_raw)?.len();
627582 if current_size != target_bytes {
628− let current_gb = current_size;
629− let target_gb = target_bytes;
583+ let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
584+ let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
630585 tracing::warn!(
631− "instance disk size does not match config (current {}, config {}); disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using existing disk.",
586+ current_bytes = current_size,
587+ target_bytes,
588+ "instance disk size does not match config (current {:.2} GB, config {:.2} GB); disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using existing disk.",
632589 current_gb,
633590 target_gb
634591 );
@@ -636,28 +593,30 @@ fn ensure_instance_disk(
636593 return Ok(false);
637594 }
638595
639− let template_size = ByteSize(fs::metadata(template_raw)?.len());
596+ let template_size = fs::metadata(template_raw)?.len();
640597 if target_bytes < template_size {
641− bail!(format!(
642− "Requested disk size {} is smaller than base image size {}",
598+ return Err(format!(
599+ "Requested disk size {} bytes is smaller than base image size {} bytes",
643600 target_bytes, template_size
644− ));
601+ )
602+ .into());
645603 }
646604 let target_size = target_bytes;
647605 let needs_resize = target_size > template_size;
648606
649− emit_status(status, "creating instance disk...");
650− tracing::info!("creating instance disk...");
607+ if let Some(status) = status {
608+ status.update("creating instance disk...");
609+ }
651610 tracing::info!(path = %template_raw.display(), "creating instance disk");
652− fs::create_dir_all(instance_raw.parent().unwrap())?;
611+ std::fs::create_dir_all(instance_raw.parent().unwrap())?;
653612 if target_size == template_size {
654613 fs::copy(template_raw, instance_raw)?;
655614 return Ok(needs_resize);
656615 }
657616
658− let mut dst = fs::File::create(instance_raw)?;
659− dst.set_len(target_size.as_u64())?;
660− let mut src = fs::File::open(template_raw)?;
617+ let mut dst = std::fs::File::create(instance_raw)?;
618+ dst.set_len(target_size)?;
619+ let mut src = std::fs::File::open(template_raw)?;
661620 std::io::copy(&mut src, &mut dst)?;
662621 Ok(needs_resize)
663622}
@@ -675,17 +634,18 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
675634 (read_stream.into(), write_stream.into())
676635}
677636
678−pub fn spawn_vm_io_with_hooks<
679− F: FnMut(&str) -> bool + std::marker::Send + 'static,
680− G: FnMut(&[u8]) + std::marker::Send + 'static,
681−>(
637+pub fn spawn_vm_io_with_hooks<F, G>(
682638 output_monitor: Arc<OutputMonitor>,
683639 vm_output_fd: OwnedFd,
684640 vm_input_fd: OwnedFd,
685641 io_control: Arc<IoControl>,
686642 mut on_line: F,
687643 mut on_output: G,
688−) -> IoContext {
644+) -> IoContext
645+where
646+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
647+ G: FnMut(&[u8]) + ::std::marker::Send + 'static,
648+{
689649 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
690650
691651 // raw_guard is set when we've put the user's terminal into raw mode because we've attached stdin/stdout to the VM.
@@ -833,14 +793,14 @@ pub fn spawn_vm_io_with_hooks<
833793 PollResult::Spurious => continue,
834794 PollResult::Ready(bytes) => {
835795 if io_control.forward_output() {
836− // enable raw mode if we haven't already
796+ // enable raw mode, if we haven't already
837797 if raw_guard.lock().unwrap().is_none()
838798 && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
839799 {
840800 *raw_guard.lock().unwrap() = Some(guard);
841801 }
842802
843− let mut stdout = io::stdout().lock();
803+ let mut stdout = std::io::stdout().lock();
844804 if stdout.write_all(bytes).is_err() {
845805 break;
846806 }
@@ -856,7 +816,7 @@ pub fn spawn_vm_io_with_hooks<
856816
857817 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
858818 let mux_thread = thread::spawn(move || {
859− let mut vm_writer = fs::File::from(vm_input_fd);
819+ let mut vm_writer = std::fs::File::from(vm_input_fd);
860820 loop {
861821 match input_rx.recv() {
862822 Ok(VmInput::Bytes(data)) => {
@@ -886,7 +846,7 @@ pub fn spawn_vm_io_with_line_handler<F>(
886846 on_line: F,
887847) -> IoContext
888848where
889− F: FnMut(&str) -> bool + std::marker::Send + 'static,
849+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
890850{
891851 spawn_vm_io_with_hooks(
892852 output_monitor,
@@ -906,36 +866,6 @@ pub fn spawn_vm_io(
906866 spawn_vm_io_with_line_handler(output_monitor, vm_output_fd, vm_input_fd, |_| false)
907867}
908868
909−fn spawn_vm_io_with_log(
910− output_monitor: Arc<OutputMonitor>,
911− vm_output_fd: OwnedFd,
912− vm_input_fd: OwnedFd,
913− log_path: PathBuf,
914−) -> IoContext {
915− let log_file = fs::OpenOptions::new()
916− .create(true)
917− .write(true)
918− .truncate(true)
919− .open(&log_path)
920− .ok()
921− .map(|file| Arc::new(Mutex::new(file)));
922−
923− spawn_vm_io_with_hooks(
924− output_monitor,
925− vm_output_fd,
926− vm_input_fd,
927− IoControl::new(),
928− |_| false,
929− move |bytes| {
930− if let Some(log) = &log_file
931− && let Ok(mut file) = log.lock()
932− {
933− let _ = file.write_all(bytes);
934− }
935− },
936− )
937−}
938−
939869impl IoContext {
940870 pub fn shutdown(self) {
941871 let _ = self.input_tx.send(VmInput::Shutdown);
@@ -953,7 +883,7 @@ fn create_vm_configuration(
953883 vm_writes_to_fd: OwnedFd,
954884 cpu_count: usize,
955885 ram_bytes: u64,
956−) -> Result<Retained<VZVirtualMachineConfiguration>> {
886+) -> Result<Retained<VZVirtualMachineConfiguration>, Box<dyn std::error::Error>> {
957887 unsafe {
958888 let platform =
959889 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
@@ -987,7 +917,7 @@ fn create_vm_configuration(
987917 false,
988918 VZDiskImageCachingMode::Automatic,
989919 VZDiskImageSynchronizationMode::Full,
990− )?;
920+ ).unwrap();
991921
992922 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
993923 VZVirtioBlockDeviceConfiguration::alloc(),
@@ -1074,7 +1004,7 @@ fn create_vm_configuration(
10741004 // Validate
10751005 config.validateWithError().map_err(|e| {
10761006 io::Error::other(format!(
1077− "invalid VM configuration: {:?}",
1007+ "Invalid VM configuration: {:?}",
10781008 e.localizedDescription()
10791009 ))
10801010 })?;
@@ -1083,9 +1013,9 @@ fn create_vm_configuration(
10831013 }
10841014}
10851015
1086−fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
1016+fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std::error::Error>> {
10871017 unsafe {
1088− let temp_dir = env::temp_dir();
1018+ let temp_dir = std::env::temp_dir();
10891019 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
10901020 let url = nsurl_from_path(&temp_path)?;
10911021 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
@@ -1101,8 +1031,8 @@ fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
11011031fn spawn_login_actions_thread(
11021032 login_actions: Vec<LoginAction>,
11031033 output_monitor: Arc<OutputMonitor>,
1104− input_tx: Sender<VmInput>,
1105− vm_output_tx: Sender<VmOutput>,
1034+ input_tx: mpsc::Sender<VmInput>,
1035+ vm_output_tx: mpsc::Sender<VmOutput>,
11061036) -> thread::JoinHandle<()> {
11071037 thread::spawn(move || {
11081038 for a in login_actions {
@@ -1116,27 +1046,6 @@ fn spawn_login_actions_thread(
11161046 return;
11171047 }
11181048 }
1119− ExpectEither {
1120− success,
1121− failure,
1122− timeout,
1123− } => match output_monitor.wait_for_any(&[&success, &failure], timeout) {
1124− WaitAnyResult::Found(0) => {}
1125− WaitAnyResult::Found(_) => {
1126− let _ = vm_output_tx.send(VmOutput::LoginActionFailed {
1127− action: format!("expect '{}'", success),
1128− reason: format!("saw failure marker '{}'", failure),
1129− });
1130− return;
1131− }
1132− WaitAnyResult::Timeout => {
1133− let _ = vm_output_tx.send(VmOutput::LoginActionTimeout {
1134− action: format!("expect '{}'", success),
1135− timeout,
1136− });
1137− return;
1138− }
1139− },
11401049 Send(mut text) => {
11411050 text.push('\n'); // Type the newline so the command is actually submitted.
11421051 input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
@@ -1152,9 +1061,9 @@ fn run_vm_with_io<F>(
11521061 directory_shares: &[DirectoryShare],
11531062 cpu_count: usize,
11541063 ram_bytes: u64,
1155− status: Option<&StatusEmitter<'_>>,
1064+ status: Option<&StatusFile>,
11561065 io_handler: F,
1157−) -> Result<()>
1066+) -> Result<(), Box<dyn std::error::Error>>
11581067where
11591068 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
11601069{
@@ -1201,22 +1110,24 @@ where
12011110
12021111 match rx.try_recv() {
12031112 Ok(result) => {
1204− result.map_err(|e| Error::msg(format!("Failed to start VM: {}", e)))?;
1113+ result.map_err(|e| format!("Failed to start VM: {}", e))?;
12051114 break;
12061115 }
12071116 Err(mpsc::TryRecvError::Empty) => continue,
12081117 Err(mpsc::TryRecvError::Disconnected) => {
1209− bail!("VM start channel disconnected");
1118+ return Err("VM start channel disconnected".into());
12101119 }
12111120 }
12121121 }
12131122
12141123 if Instant::now() >= start_deadline {
1215− bail!("Timed out waiting for VM to start");
1124+ return Err("Timed out waiting for VM to start".into());
12161125 }
12171126
1218− emit_status(status, "vm booting... go vibecoder!");
1219− tracing::info!("vm booting... go vibecoder!");
1127+ if let Some(status) = status {
1128+ status.update("vm booting... go vibecoder!");
1129+ status.clear();
1130+ }
12201131 tracing::info!("vm booting");
12211132
12221133 let output_monitor = Arc::new(OutputMonitor::default());
@@ -1262,7 +1173,7 @@ where
12621173 );
12631174
12641175 let mut last_state = None;
1265− let mut exit_result: Result<(), String> = Ok(());
1176+ let mut exit_result = Ok(());
12661177 loop {
12671178 unsafe {
12681179 NSRunLoop::mainRunLoop().runMode_beforeDate(
@@ -1281,24 +1192,8 @@ where
12811192 exit_result = Err(format!(
12821193 "Login action ({}) timed out after {:?}; shutting down.",
12831194 action, timeout
1284− ));
1285− unsafe {
1286− if vm.canRequestStop() {
1287− if let Err(err) = vm.requestStopWithError() {
1288− tracing::error!(error = ?err, "failed to request VM stop");
1289− }
1290− } else if vm.canStop() {
1291− let handler = RcBlock::new(|_error: *mut NSError| {});
1292− vm.stopWithCompletionHandler(&handler);
1293− }
1294− }
1295− break;
1296− }
1297− Ok(VmOutput::LoginActionFailed { action, reason }) => {
1298− exit_result = Err(format!(
1299− "Login action ({}) failed: {}; shutting down.",
1300− action, reason
1301− ));
1195+ )
1196+ .into());
13021197 unsafe {
13031198 if vm.canRequestStop() {
13041199 if let Err(err) = vm.requestStopWithError() {
@@ -1314,7 +1209,7 @@ where
13141209 Err(mpsc::TryRecvError::Empty) => {}
13151210 Err(mpsc::TryRecvError::Disconnected) => {}
13161211 }
1317− if state != VZVirtualMachineState::Running {
1212+ if state != objc2_virtualization::VZVirtualMachineState::Running {
13181213 //eprintln!("VM stopped with state: {:?}", state);
13191214 break;
13201215 }
@@ -1324,7 +1219,7 @@ where
13241219
13251220 io_ctx.shutdown();
13261221
1327− exit_result.map_err(Error::msg)
1222+ exit_result
13281223}
13291224
13301225fn run_vm(
@@ -1333,8 +1228,8 @@ fn run_vm(
13331228 directory_shares: &[DirectoryShare],
13341229 cpu_count: usize,
13351230 ram_bytes: u64,
1336− status: Option<&StatusEmitter<'_>>,
1337−) -> Result<()> {
1231+ status: Option<&StatusFile>,
1232+) -> Result<(), Box<dyn std::error::Error>> {
13381233 run_vm_with_io(
13391234 disk_path,
13401235 login_actions,
@@ -1346,7 +1241,7 @@ fn run_vm(
13461241 )
13471242}
13481243
1349−fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1244+fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
13501245 let abs_path = if path.is_absolute() {
13511246 path.to_path_buf()
13521247 } else {
@@ -1355,7 +1250,7 @@ fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
13551250 let ns_path = NSString::from_str(
13561251 abs_path
13571252 .to_str()
1358− .with_context(|| "non-UTF8 path encountered while building NSURL")?,
1253+ .ok_or("Non-UTF8 path encountered while building NSURL")?,
13591254 );
13601255 Ok(NSURL::fileURLWithPath(&ns_path))
13611256}
@@ -1370,7 +1265,7 @@ fn enable_raw_mode(fd: i32) -> io::Result<RawModeGuard> {
13701265 let original = attributes;
13711266
13721267 // Disable translation of carriage return to newline on input
1373− attributes.c_iflag &= !libc::ICRNL;
1268+ attributes.c_iflag &= !(libc::ICRNL);
13741269 // Disable canonical mode (line buffering), echo, and signal generation
13751270 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
13761271 attributes.c_cc[libc::VMIN] = 0;
@@ -1398,10 +1293,10 @@ impl Drop for RawModeGuard {
13981293
13991294// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
14001295pub fn ensure_signed() {
1401− if env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1296+ if std::env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
14021297 return;
14031298 }
1404− let exe = env::current_exe().expect("failed to get current exe path");
1299+ let exe = std::env::current_exe().expect("failed to get current exe path");
14051300 let exe_str = exe.to_str().expect("exe path not valid utf-8");
14061301
14071302 let has_required_entitlements = {
@@ -1423,8 +1318,8 @@ pub fn ensure_signed() {
14231318 }
14241319
14251320 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1426− let entitlements_path = env::temp_dir().join("entitlements.plist");
1427− fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1321+ let entitlements_path = std::env::temp_dir().join("entitlements.plist");
1322+ std::fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
14281323
14291324 let output = Command::new("codesign")
14301325 .args([
@@ -1437,7 +1332,7 @@ pub fn ensure_signed() {
14371332 ])
14381333 .output();
14391334
1440− let _ = fs::remove_file(&entitlements_path);
1335+ let _ = std::fs::remove_file(&entitlements_path);
14411336
14421337 match output {
14431338 Ok(o) if o.status.success() => {
@@ -1445,7 +1340,7 @@ pub fn ensure_signed() {
14451340 if !stderr.trim().is_empty() {
14461341 tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
14471342 }
1448− let err = Command::new(&exe).args(env::args_os().skip(1)).exec();
1343+ let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
14491344 tracing::error!(error = %err, "failed to re-exec after signing");
14501345 std::process::exit(1);
14511346 }