@@ -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::*;
@@ -35,19 +34,49 @@ const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
3534const SHARED_DIRECTORIES_TAG: &str = "shared";
3635pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
3736
37+const BYTES_PER_MB: u64 = 1024 * 1024;
38+const DEFAULT_CPU_COUNT: usize = 2;
39+const DEFAULT_RAM_MB: u64 = 2048;
40+const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
3841const START_TIMEOUT: Duration = Duration::from_secs(60);
3942const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
4043const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
4144
42−const ERROR_REPORT_SCRIPT: &str = include_str!("error_report.sh");
45+struct StatusFile {
46+ path: PathBuf,
47+ cleared: AtomicBool,
48+}
49+
50+impl StatusFile {
51+ fn new(path: PathBuf) -> Self {
52+ Self {
53+ path,
54+ cleared: AtomicBool::new(false),
55+ }
56+ }
57+
58+ fn update(&self, message: &str) {
59+ let _ = fs::write(&self.path, message);
60+ }
61+}
62+
63+impl Drop for StatusFile {
64+ fn drop(&mut self) {
65+ if !self.cleared.load(Ordering::SeqCst) {
66+ let _ = fs::remove_file(&self.path);
67+ self.cleared.store(true, Ordering::SeqCst);
68+ }
69+ }
70+}
4371const PROVISION_SCRIPT: &str = include_str!("provision.sh");
72+const PROVISION_SCRIPT_NAME: &str = "provision.sh";
4473const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
4574const DEFAULT_RAW_NAME: &str = "default.raw";
4675const INSTANCE_RAW_NAME: &str = "instance.raw";
4776const BASE_DISK_RAW_NAME: &str = "disk.raw";
4877
4978#[derive(Clone)]
50−pub enum LoginAction {
79+pub(crate) enum LoginAction {
5180 Expect {
5281 text: String,
5382 timeout: Duration,
@@ -59,20 +88,23 @@ pub enum LoginAction {
5988 },
6089 Send(String),
6190}
62−use crate::config::BoxConfig;
6391use LoginAction::*;
6492
6593#[derive(Clone)]
66−pub struct DirectoryShare {
94+pub(crate) struct DirectoryShare {
6795 host: PathBuf,
6896 guest: PathBuf,
6997 read_only: bool,
7098}
7199
72100impl DirectoryShare {
73− pub fn new(host: PathBuf, mut guest: PathBuf, read_only: bool) -> Result<Self> {
101+ pub(crate) fn new(
102+ host: PathBuf,
103+ mut guest: PathBuf,
104+ read_only: bool,
105+ ) -> Result<Self, Box<dyn std::error::Error>> {
74106 if !host.exists() {
75− bail!(format!("host path does not exist: {}", host.display()));
107+ return Err(format!("Host path does not exist: {}", host.display()).into());
76108 }
77109 if !guest.is_absolute() {
78110 guest = PathBuf::from("/root").join(guest);
@@ -84,10 +116,10 @@ impl DirectoryShare {
84116 })
85117 }
86118
87− pub fn from_mount_spec(spec: &str) -> Result<Self> {
119+ pub(crate) fn from_mount_spec(spec: &str) -> Result<Self, Box<dyn std::error::Error>> {
88120 let parts: Vec<&str> = spec.split(':').collect();
89121 if parts.len() < 2 || parts.len() > 3 {
90− bail!(format!("invalid mount spec: {spec}"));
122+ return Err(format!("Invalid mount spec: {spec}").into());
91123 }
92124 let host = expand_tilde_path(parts[0]);
93125 let guest = PathBuf::from(parts[1]);
@@ -96,10 +128,11 @@ impl DirectoryShare {
96128 "read-only" => true,
97129 "read-write" => false,
98130 _ => {
99− bail!(format!(
131+ return Err(format!(
100132 "Invalid mount mode '{}'; expected read-only or read-write",
101133 parts[2]
102− ));
134+ )
135+ .into());
103136 }
104137 }
105138 } else {
@@ -143,28 +176,19 @@ pub struct VmArg {
143176 pub mounts: Vec<String>,
144177}
145178
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<()>
179+pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
155180where
156181 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
157182{
158− run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new(), None)
183+ run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new())
159184}
160185
161−pub fn run_with_args_and_extras<F>(
186+pub(crate) fn run_with_args_and_extras<F>(
162187 args: VmArg,
163188 io_handler: F,
164189 extra_login_actions: Vec<LoginAction>,
165190 extra_directory_shares: Vec<DirectoryShare>,
166− status: Option<&StatusEmitter<'_>>,
167−) -> Result<()>
191+) -> Result<(), Box<dyn std::error::Error>>
168192where
169193 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
170194{
@@ -173,7 +197,7 @@ where
173197 let project_root = env::current_dir()?;
174198 let project_name = project_root
175199 .file_name()
176− .with_context(|| "Project directory has no name")?
200+ .ok_or("Project directory has no name")?
177201 .to_string_lossy()
178202 .into_owned();
179203
@@ -186,8 +210,8 @@ where
186210
187211 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
188212 fs::create_dir_all(&instance_dir)?;
189− emit_status(status, "preparing VM image...");
190− tracing::info!("preparing VM image...");
213+ let status_file = StatusFile::new(instance_dir.join(STATUS_FILE_NAME));
214+ status_file.update("preparing VM image...");
191215 let provision_log = instance_dir.join("provision.log");
192216
193217 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
@@ -212,14 +236,14 @@ where
212236 &base_compressed,
213237 &default_raw,
214238 std::slice::from_ref(&mise_directory_share),
239+ Some(&status_file),
215240 Some(&provision_log),
216− status,
217241 )?;
218242 let _ = ensure_instance_disk(
219243 &instance_raw,
220244 &default_raw,
221− ByteSize(args.disk_bytes),
222− status,
245+ args.disk_bytes,
246+ Some(&status_file),
223247 )?;
224248 let base_size = fs::metadata(&default_raw)?.len();
225249 let instance_size = fs::metadata(&instance_raw)?.len();
@@ -249,7 +273,7 @@ where
249273 }
250274
251275 if needs_resize {
252− let resize_cmd = script_command_from_content("resize_disk.sh", RESIZE_DISK_SCRIPT)?;
276+ let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
253277 login_actions.push(Send(resize_cmd));
254278 }
255279
@@ -265,29 +289,25 @@ where
265289 &directory_shares[..],
266290 args.cpu_count,
267291 args.ram_bytes,
268− status,
292+ Some(&status_file),
269293 io_handler,
270294 )
271295}
272296
273−pub fn script_command_from_content(label: &str, script: &str) -> Result<String> {
297+pub(crate) fn script_command_from_content(
298+ label: &str,
299+ script: &str,
300+) -> Result<String, Box<dyn std::error::Error>> {
274301 let marker = "VIBE_SCRIPT_EOF";
275302 let guest_dir = "/tmp/vibe-scripts";
276303 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);
284304 let command = format!(
285− "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{wrapped_script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
305+ "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
286306 );
287307 if script.contains(marker) {
288− bail!(format!(
289− "Script '{label}' contains marker '{marker}', cannot safely upload"
290− ));
308+ return Err(
309+ format!("Script '{label}' contains marker '{marker}', cannot safely upload").into(),
310+ );
291311 }
292312 Ok(command)
293313}
@@ -484,16 +504,18 @@ impl IoControl {
484504fn ensure_base_image(
485505 base_raw: &Path,
486506 base_compressed: &Path,
487− status: Option<&StatusEmitter<'_>>,
488−) -> Result<()> {
507+ status: Option<&StatusFile>,
508+) -> Result<(), Box<dyn std::error::Error>> {
489509 if base_raw.exists() {
490510 return Ok(());
491511 }
492512
493513 if !base_compressed.exists()
494− || fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
514+ || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
495515 {
496− emit_status(status, "downloading base image...");
516+ if let Some(status) = status {
517+ status.update("downloading base image...");
518+ }
497519 tracing::info!("downloading base image");
498520 let status = Command::new("curl")
499521 .args([
@@ -508,14 +530,15 @@ fn ensure_base_image(
508530 ])
509531 .status()?;
510532 if !status.success() {
511− bail!("failed to download base image");
533+ return Err("Failed to download base image".into());
512534 }
513535 }
514536
515537 // Check SHA
516538 {
517− emit_status(status, "verifying base image...");
518− tracing::info!("verifying base image...");
539+ if let Some(status) = status {
540+ status.update("verifying base image...");
541+ }
519542 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
520543
521544 let mut child = Command::new("/usr/bin/shasum")
@@ -533,25 +556,25 @@ fn ensure_base_image(
533556
534557 let status = child.wait().expect("failed to wait on child");
535558 if !status.success() {
536− bail!(format!(
537− "SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}"
538− ));
559+ return Err(format!("SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}").into());
539560 }
540561 }
541562
542− emit_status(status, "decompressing base image...");
543− tracing::info!("decompressing base image...");
563+ if let Some(status) = status {
564+ status.update("decompressing base image...");
565+ }
566+ tracing::info!("decompressing base image");
544567 let status = Command::new("tar")
545568 .args([
546569 "-xOf",
547570 &base_compressed.to_string_lossy(),
548571 BASE_DISK_RAW_NAME,
549572 ])
550− .stdout(fs::File::create(base_raw)?)
573+ .stdout(std::fs::File::create(base_raw)?)
551574 .status()?;
552575
553576 if !status.success() {
554− bail!("Failed to decompress base image");
577+ return Err("Failed to decompress base image".into());
555578 }
556579
557580 Ok(())
@@ -562,20 +585,22 @@ fn ensure_default_image(
562585 base_compressed: &Path,
563586 default_raw: &Path,
564587 directory_shares: &[DirectoryShare],
588+ status: Option<&StatusFile>,
565589 provision_log: Option<&Path>,
566− status: Option<&StatusEmitter<'_>>,
567−) -> Result<()> {
590+) -> Result<(), Box<dyn std::error::Error>> {
568591 if default_raw.exists() {
569592 return Ok(());
570593 }
571594
572595 ensure_base_image(base_raw, base_compressed, status)?;
573596
574− emit_status(status, "configuring base image...");
575− tracing::info!("configuring base image...");
597+ if let Some(status) = status {
598+ status.update("configuring base image...");
599+ }
600+ tracing::info!("configuring base image");
576601 fs::copy(base_raw, default_raw)?;
577602
578− let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
603+ let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
579604 let provision_actions = [
580605 Send(provision_command),
581606 ExpectEither {
@@ -590,9 +615,9 @@ fn ensure_default_image(
590615 default_raw,
591616 &provision_actions,
592617 directory_shares,
593− BoxConfig::default().cpu_count,
594− BoxConfig::default().ram_size.as_u64(),
595− status,
618+ DEFAULT_CPU_COUNT,
619+ DEFAULT_RAM_BYTES,
620+ None,
596621 move |output_monitor, vm_output_fd, vm_input_fd| {
597622 spawn_vm_io_with_log(output_monitor, vm_output_fd, vm_input_fd, log_path)
598623 },
@@ -602,9 +627,9 @@ fn ensure_default_image(
602627 default_raw,
603628 &provision_actions,
604629 directory_shares,
605− BoxConfig::default().cpu_count,
606− BoxConfig::default().ram_size.as_u64(),
607− status,
630+ DEFAULT_CPU_COUNT,
631+ DEFAULT_RAM_BYTES,
632+ None,
608633 )
609634 };
610635
@@ -619,16 +644,18 @@ fn ensure_default_image(
619644fn ensure_instance_disk(
620645 instance_raw: &Path,
621646 template_raw: &Path,
622− target_bytes: ByteSize,
623− status: Option<&StatusEmitter<'_>>,
624−) -> Result<bool> {
647+ target_bytes: u64,
648+ status: Option<&StatusFile>,
649+) -> Result<bool, Box<dyn std::error::Error>> {
625650 if instance_raw.exists() {
626− let current_size = ByteSize(fs::metadata(instance_raw)?.len());
651+ let current_size = fs::metadata(instance_raw)?.len();
627652 if current_size != target_bytes {
628− let current_gb = current_size;
629− let target_gb = target_bytes;
653+ let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
654+ let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
630655 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.",
656+ current_bytes = current_size,
657+ target_bytes,
658+ "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.",
632659 current_gb,
633660 target_gb
634661 );
@@ -636,28 +663,30 @@ fn ensure_instance_disk(
636663 return Ok(false);
637664 }
638665
639− let template_size = ByteSize(fs::metadata(template_raw)?.len());
666+ let template_size = fs::metadata(template_raw)?.len();
640667 if target_bytes < template_size {
641− bail!(format!(
642− "Requested disk size {} is smaller than base image size {}",
668+ return Err(format!(
669+ "Requested disk size {} bytes is smaller than base image size {} bytes",
643670 target_bytes, template_size
644− ));
671+ )
672+ .into());
645673 }
646674 let target_size = target_bytes;
647675 let needs_resize = target_size > template_size;
648676
649− emit_status(status, "creating instance disk...");
650− tracing::info!("creating instance disk...");
677+ if let Some(status) = status {
678+ status.update("creating instance disk...");
679+ }
651680 tracing::info!(path = %template_raw.display(), "creating instance disk");
652− fs::create_dir_all(instance_raw.parent().unwrap())?;
681+ std::fs::create_dir_all(instance_raw.parent().unwrap())?;
653682 if target_size == template_size {
654683 fs::copy(template_raw, instance_raw)?;
655684 return Ok(needs_resize);
656685 }
657686
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)?;
687+ let mut dst = std::fs::File::create(instance_raw)?;
688+ dst.set_len(target_size)?;
689+ let mut src = std::fs::File::open(template_raw)?;
661690 std::io::copy(&mut src, &mut dst)?;
662691 Ok(needs_resize)
663692}
@@ -675,17 +704,18 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
675704 (read_stream.into(), write_stream.into())
676705}
677706
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−>(
707+pub fn spawn_vm_io_with_hooks<F, G>(
682708 output_monitor: Arc<OutputMonitor>,
683709 vm_output_fd: OwnedFd,
684710 vm_input_fd: OwnedFd,
685711 io_control: Arc<IoControl>,
686712 mut on_line: F,
687713 mut on_output: G,
688−) -> IoContext {
714+) -> IoContext
715+where
716+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
717+ G: FnMut(&[u8]) + ::std::marker::Send + 'static,
718+{
689719 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
690720
691721 // 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 +863,14 @@ pub fn spawn_vm_io_with_hooks<
833863 PollResult::Spurious => continue,
834864 PollResult::Ready(bytes) => {
835865 if io_control.forward_output() {
836− // enable raw mode if we haven't already
866+ // enable raw mode, if we haven't already
837867 if raw_guard.lock().unwrap().is_none()
838868 && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
839869 {
840870 *raw_guard.lock().unwrap() = Some(guard);
841871 }
842872
843− let mut stdout = io::stdout().lock();
873+ let mut stdout = std::io::stdout().lock();
844874 if stdout.write_all(bytes).is_err() {
845875 break;
846876 }
@@ -856,7 +886,7 @@ pub fn spawn_vm_io_with_hooks<
856886
857887 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
858888 let mux_thread = thread::spawn(move || {
859− let mut vm_writer = fs::File::from(vm_input_fd);
889+ let mut vm_writer = std::fs::File::from(vm_input_fd);
860890 loop {
861891 match input_rx.recv() {
862892 Ok(VmInput::Bytes(data)) => {
@@ -886,7 +916,7 @@ pub fn spawn_vm_io_with_line_handler<F>(
886916 on_line: F,
887917) -> IoContext
888918where
889− F: FnMut(&str) -> bool + std::marker::Send + 'static,
919+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
890920{
891921 spawn_vm_io_with_hooks(
892922 output_monitor,
@@ -953,7 +983,7 @@ fn create_vm_configuration(
953983 vm_writes_to_fd: OwnedFd,
954984 cpu_count: usize,
955985 ram_bytes: u64,
956−) -> Result<Retained<VZVirtualMachineConfiguration>> {
986+) -> Result<Retained<VZVirtualMachineConfiguration>, Box<dyn std::error::Error>> {
957987 unsafe {
958988 let platform =
959989 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
@@ -987,7 +1017,7 @@ fn create_vm_configuration(
9871017 false,
9881018 VZDiskImageCachingMode::Automatic,
9891019 VZDiskImageSynchronizationMode::Full,
990− )?;
1020+ ).unwrap();
9911021
9921022 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
9931023 VZVirtioBlockDeviceConfiguration::alloc(),
@@ -1074,7 +1104,7 @@ fn create_vm_configuration(
10741104 // Validate
10751105 config.validateWithError().map_err(|e| {
10761106 io::Error::other(format!(
1077− "invalid VM configuration: {:?}",
1107+ "Invalid VM configuration: {:?}",
10781108 e.localizedDescription()
10791109 ))
10801110 })?;
@@ -1083,9 +1113,9 @@ fn create_vm_configuration(
10831113 }
10841114}
10851115
1086−fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
1116+fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std::error::Error>> {
10871117 unsafe {
1088− let temp_dir = env::temp_dir();
1118+ let temp_dir = std::env::temp_dir();
10891119 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
10901120 let url = nsurl_from_path(&temp_path)?;
10911121 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
@@ -1101,8 +1131,8 @@ fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
11011131fn spawn_login_actions_thread(
11021132 login_actions: Vec<LoginAction>,
11031133 output_monitor: Arc<OutputMonitor>,
1104− input_tx: Sender<VmInput>,
1105− vm_output_tx: Sender<VmOutput>,
1134+ input_tx: mpsc::Sender<VmInput>,
1135+ vm_output_tx: mpsc::Sender<VmOutput>,
11061136) -> thread::JoinHandle<()> {
11071137 thread::spawn(move || {
11081138 for a in login_actions {
@@ -1152,9 +1182,9 @@ fn run_vm_with_io<F>(
11521182 directory_shares: &[DirectoryShare],
11531183 cpu_count: usize,
11541184 ram_bytes: u64,
1155− status: Option<&StatusEmitter<'_>>,
1185+ status: Option<&StatusFile>,
11561186 io_handler: F,
1157−) -> Result<()>
1187+) -> Result<(), Box<dyn std::error::Error>>
11581188where
11591189 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
11601190{
@@ -1201,22 +1231,23 @@ where
12011231
12021232 match rx.try_recv() {
12031233 Ok(result) => {
1204− result.map_err(|e| Error::msg(format!("Failed to start VM: {}", e)))?;
1234+ result.map_err(|e| format!("Failed to start VM: {}", e))?;
12051235 break;
12061236 }
12071237 Err(mpsc::TryRecvError::Empty) => continue,
12081238 Err(mpsc::TryRecvError::Disconnected) => {
1209− bail!("VM start channel disconnected");
1239+ return Err("VM start channel disconnected".into());
12101240 }
12111241 }
12121242 }
12131243
12141244 if Instant::now() >= start_deadline {
1215− bail!("Timed out waiting for VM to start");
1245+ return Err("Timed out waiting for VM to start".into());
12161246 }
12171247
1218− emit_status(status, "vm booting... go vibecoder!");
1219− tracing::info!("vm booting... go vibecoder!");
1248+ if let Some(status) = status {
1249+ status.update("vm booting... go vibecoder!");
1250+ }
12201251 tracing::info!("vm booting");
12211252
12221253 let output_monitor = Arc::new(OutputMonitor::default());
@@ -1262,7 +1293,7 @@ where
12621293 );
12631294
12641295 let mut last_state = None;
1265− let mut exit_result: Result<(), String> = Ok(());
1296+ let mut exit_result = Ok(());
12661297 loop {
12671298 unsafe {
12681299 NSRunLoop::mainRunLoop().runMode_beforeDate(
@@ -1281,7 +1312,8 @@ where
12811312 exit_result = Err(format!(
12821313 "Login action ({}) timed out after {:?}; shutting down.",
12831314 action, timeout
1284− ));
1315+ )
1316+ .into());
12851317 unsafe {
12861318 if vm.canRequestStop() {
12871319 if let Err(err) = vm.requestStopWithError() {
@@ -1298,7 +1330,8 @@ where
12981330 exit_result = Err(format!(
12991331 "Login action ({}) failed: {}; shutting down.",
13001332 action, reason
1301− ));
1333+ )
1334+ .into());
13021335 unsafe {
13031336 if vm.canRequestStop() {
13041337 if let Err(err) = vm.requestStopWithError() {
@@ -1314,7 +1347,7 @@ where
13141347 Err(mpsc::TryRecvError::Empty) => {}
13151348 Err(mpsc::TryRecvError::Disconnected) => {}
13161349 }
1317− if state != VZVirtualMachineState::Running {
1350+ if state != objc2_virtualization::VZVirtualMachineState::Running {
13181351 //eprintln!("VM stopped with state: {:?}", state);
13191352 break;
13201353 }
@@ -1324,7 +1357,7 @@ where
13241357
13251358 io_ctx.shutdown();
13261359
1327− exit_result.map_err(Error::msg)
1360+ exit_result
13281361}
13291362
13301363fn run_vm(
@@ -1333,8 +1366,8 @@ fn run_vm(
13331366 directory_shares: &[DirectoryShare],
13341367 cpu_count: usize,
13351368 ram_bytes: u64,
1336− status: Option<&StatusEmitter<'_>>,
1337−) -> Result<()> {
1369+ status: Option<&StatusFile>,
1370+) -> Result<(), Box<dyn std::error::Error>> {
13381371 run_vm_with_io(
13391372 disk_path,
13401373 login_actions,
@@ -1346,7 +1379,7 @@ fn run_vm(
13461379 )
13471380}
13481381
1349−fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1382+fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
13501383 let abs_path = if path.is_absolute() {
13511384 path.to_path_buf()
13521385 } else {
@@ -1355,7 +1388,7 @@ fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
13551388 let ns_path = NSString::from_str(
13561389 abs_path
13571390 .to_str()
1358− .with_context(|| "non-UTF8 path encountered while building NSURL")?,
1391+ .ok_or("Non-UTF8 path encountered while building NSURL")?,
13591392 );
13601393 Ok(NSURL::fileURLWithPath(&ns_path))
13611394}
@@ -1370,7 +1403,7 @@ fn enable_raw_mode(fd: i32) -> io::Result<RawModeGuard> {
13701403 let original = attributes;
13711404
13721405 // Disable translation of carriage return to newline on input
1373− attributes.c_iflag &= !libc::ICRNL;
1406+ attributes.c_iflag &= !(libc::ICRNL);
13741407 // Disable canonical mode (line buffering), echo, and signal generation
13751408 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
13761409 attributes.c_cc[libc::VMIN] = 0;
@@ -1398,10 +1431,10 @@ impl Drop for RawModeGuard {
13981431
13991432// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
14001433pub fn ensure_signed() {
1401− if env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1434+ if std::env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
14021435 return;
14031436 }
1404− let exe = env::current_exe().expect("failed to get current exe path");
1437+ let exe = std::env::current_exe().expect("failed to get current exe path");
14051438 let exe_str = exe.to_str().expect("exe path not valid utf-8");
14061439
14071440 let has_required_entitlements = {
@@ -1423,8 +1456,8 @@ pub fn ensure_signed() {
14231456 }
14241457
14251458 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");
1459+ let entitlements_path = std::env::temp_dir().join("entitlements.plist");
1460+ std::fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
14281461
14291462 let output = Command::new("codesign")
14301463 .args([
@@ -1437,7 +1470,7 @@ pub fn ensure_signed() {
14371470 ])
14381471 .output();
14391472
1440− let _ = fs::remove_file(&entitlements_path);
1473+ let _ = std::fs::remove_file(&entitlements_path);
14411474
14421475 match output {
14431476 Ok(o) if o.status.success() => {
@@ -1445,7 +1478,7 @@ pub fn ensure_signed() {
14451478 if !stderr.trim().is_empty() {
14461479 tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
14471480 }
1448− let err = Command::new(&exe).args(env::args_os().skip(1)).exec();
1481+ let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
14491482 tracing::error!(error = %err, "failed to re-exec after signing");
14501483 std::process::exit(1);
14511484 }