@@ -1,5 +1,5 @@
1−use crate::instance::STATUS_FILE_NAME;
21use 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,6 +23,7 @@ use std::{
2323};
2424
2525use block2::RcBlock;
26+use bytesize::ByteSize;
2627use dispatch2::DispatchQueue;
2728use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
2829use objc2_foundation::*;
@@ -34,49 +35,19 @@ const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
3435const SHARED_DIRECTORIES_TAG: &str = "shared";
3536pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
3637
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;
4138const START_TIMEOUT: Duration = Duration::from_secs(60);
4239const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
4340const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
4441
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−}
42+const ERROR_REPORT_SCRIPT: &str = include_str!("error_report.sh");
7143const PROVISION_SCRIPT: &str = include_str!("provision.sh");
72−const PROVISION_SCRIPT_NAME: &str = "provision.sh";
7344const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
7445const DEFAULT_RAW_NAME: &str = "default.raw";
7546const INSTANCE_RAW_NAME: &str = "instance.raw";
7647const BASE_DISK_RAW_NAME: &str = "disk.raw";
7748
7849#[derive(Clone)]
79−pub(crate) enum LoginAction {
50+pub enum LoginAction {
8051 Expect {
8152 text: String,
8253 timeout: Duration,
@@ -88,23 +59,20 @@ pub(crate) enum LoginAction {
8859 },
8960 Send(String),
9061}
62+use crate::config::BoxConfig;
9163use LoginAction::*;
9264
9365#[derive(Clone)]
94−pub(crate) struct DirectoryShare {
66+pub struct DirectoryShare {
9567 host: PathBuf,
9668 guest: PathBuf,
9769 read_only: bool,
9870}
9971
10072impl DirectoryShare {
101− pub(crate) fn new(
102− host: PathBuf,
103− mut guest: PathBuf,
104− read_only: bool,
105− ) -> Result<Self, Box<dyn std::error::Error>> {
73+ pub fn new(host: PathBuf, mut guest: PathBuf, read_only: bool) -> Result<Self> {
10674 if !host.exists() {
107− return Err(format!("Host path does not exist: {}", host.display()).into());
75+ bail!(format!("host path does not exist: {}", host.display()));
10876 }
10977 if !guest.is_absolute() {
11078 guest = PathBuf::from("/root").join(guest);
@@ -116,10 +84,10 @@ impl DirectoryShare {
11684 })
11785 }
11886
119− pub(crate) fn from_mount_spec(spec: &str) -> Result<Self, Box<dyn std::error::Error>> {
87+ pub fn from_mount_spec(spec: &str) -> Result<Self> {
12088 let parts: Vec<&str> = spec.split(':').collect();
12189 if parts.len() < 2 || parts.len() > 3 {
122− return Err(format!("Invalid mount spec: {spec}").into());
90+ bail!(format!("invalid mount spec: {spec}"));
12391 }
12492 let host = expand_tilde_path(parts[0]);
12593 let guest = PathBuf::from(parts[1]);
@@ -128,11 +96,10 @@ impl DirectoryShare {
12896 "read-only" => true,
12997 "read-write" => false,
13098 _ => {
131− return Err(format!(
99+ bail!(format!(
132100 "Invalid mount mode '{}'; expected read-only or read-write",
133101 parts[2]
134− )
135− .into());
102+ ));
136103 }
137104 }
138105 } else {
@@ -176,19 +143,28 @@ pub struct VmArg {
176143 pub mounts: Vec<String>,
177144}
178145
179−pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
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<()>
180155where
181156 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
182157{
183− run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new())
158+ run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new(), None)
184159}
185160
186−pub(crate) fn run_with_args_and_extras<F>(
161+pub fn run_with_args_and_extras<F>(
187162 args: VmArg,
188163 io_handler: F,
189164 extra_login_actions: Vec<LoginAction>,
190165 extra_directory_shares: Vec<DirectoryShare>,
191−) -> Result<(), Box<dyn std::error::Error>>
166+ status: Option<&StatusEmitter<'_>>,
167+) -> Result<()>
192168where
193169 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
194170{
@@ -197,7 +173,7 @@ where
197173 let project_root = env::current_dir()?;
198174 let project_name = project_root
199175 .file_name()
200− .ok_or("Project directory has no name")?
176+ .with_context(|| "Project directory has no name")?
201177 .to_string_lossy()
202178 .into_owned();
203179
@@ -210,8 +186,8 @@ where
210186
211187 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
212188 fs::create_dir_all(&instance_dir)?;
213− let status_file = StatusFile::new(instance_dir.join(STATUS_FILE_NAME));
214− status_file.update("preparing VM image...");
189+ emit_status(status, "preparing VM image...");
190+ tracing::info!("preparing VM image...");
215191 let provision_log = instance_dir.join("provision.log");
216192
217193 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
@@ -236,14 +212,14 @@ where
236212 &base_compressed,
237213 &default_raw,
238214 std::slice::from_ref(&mise_directory_share),
239− Some(&status_file),
240215 Some(&provision_log),
216+ status,
241217 )?;
242218 let _ = ensure_instance_disk(
243219 &instance_raw,
244220 &default_raw,
245− args.disk_bytes,
246− Some(&status_file),
221+ ByteSize(args.disk_bytes),
222+ status,
247223 )?;
248224 let base_size = fs::metadata(&default_raw)?.len();
249225 let instance_size = fs::metadata(&instance_raw)?.len();
@@ -273,7 +249,7 @@ where
273249 }
274250
275251 if needs_resize {
276− let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
252+ let resize_cmd = script_command_from_content("resize_disk.sh", RESIZE_DISK_SCRIPT)?;
277253 login_actions.push(Send(resize_cmd));
278254 }
279255
@@ -289,25 +265,29 @@ where
289265 &directory_shares[..],
290266 args.cpu_count,
291267 args.ram_bytes,
292− Some(&status_file),
268+ status,
293269 io_handler,
294270 )
295271}
296272
297−pub(crate) fn script_command_from_content(
298− label: &str,
299− script: &str,
300−) -> Result<String, Box<dyn std::error::Error>> {
273+pub fn script_command_from_content(label: &str, script: &str) -> Result<String> {
301274 let marker = "VIBE_SCRIPT_EOF";
302275 let guest_dir = "/tmp/vibe-scripts";
303276 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);
304284 let command = format!(
305− "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
285+ "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{wrapped_script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
306286 );
307287 if script.contains(marker) {
308− return Err(
309− format!("Script '{label}' contains marker '{marker}', cannot safely upload").into(),
310− );
288+ bail!(format!(
289+ "Script '{label}' contains marker '{marker}', cannot safely upload"
290+ ));
311291 }
312292 Ok(command)
313293}
@@ -504,18 +484,16 @@ impl IoControl {
504484fn ensure_base_image(
505485 base_raw: &Path,
506486 base_compressed: &Path,
507− status: Option<&StatusFile>,
508−) -> Result<(), Box<dyn std::error::Error>> {
487+ status: Option<&StatusEmitter<'_>>,
488+) -> Result<()> {
509489 if base_raw.exists() {
510490 return Ok(());
511491 }
512492
513493 if !base_compressed.exists()
514− || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
494+ || fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
515495 {
516− if let Some(status) = status {
517− status.update("downloading base image...");
518− }
496+ emit_status(status, "downloading base image...");
519497 tracing::info!("downloading base image");
520498 let status = Command::new("curl")
521499 .args([
@@ -530,15 +508,14 @@ fn ensure_base_image(
530508 ])
531509 .status()?;
532510 if !status.success() {
533− return Err("Failed to download base image".into());
511+ bail!("failed to download base image");
534512 }
535513 }
536514
537515 // Check SHA
538516 {
539− if let Some(status) = status {
540− status.update("verifying base image...");
541− }
517+ emit_status(status, "verifying base image...");
518+ tracing::info!("verifying base image...");
542519 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
543520
544521 let mut child = Command::new("/usr/bin/shasum")
@@ -556,25 +533,25 @@ fn ensure_base_image(
556533
557534 let status = child.wait().expect("failed to wait on child");
558535 if !status.success() {
559− return Err(format!("SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}").into());
536+ bail!(format!(
537+ "SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}"
538+ ));
560539 }
561540 }
562541
563− if let Some(status) = status {
564− status.update("decompressing base image...");
565− }
566− tracing::info!("decompressing base image");
542+ emit_status(status, "decompressing base image...");
543+ tracing::info!("decompressing base image...");
567544 let status = Command::new("tar")
568545 .args([
569546 "-xOf",
570547 &base_compressed.to_string_lossy(),
571548 BASE_DISK_RAW_NAME,
572549 ])
573− .stdout(std::fs::File::create(base_raw)?)
550+ .stdout(fs::File::create(base_raw)?)
574551 .status()?;
575552
576553 if !status.success() {
577− return Err("Failed to decompress base image".into());
554+ bail!("Failed to decompress base image");
578555 }
579556
580557 Ok(())
@@ -585,22 +562,20 @@ fn ensure_default_image(
585562 base_compressed: &Path,
586563 default_raw: &Path,
587564 directory_shares: &[DirectoryShare],
588− status: Option<&StatusFile>,
589565 provision_log: Option<&Path>,
590−) -> Result<(), Box<dyn std::error::Error>> {
566+ status: Option<&StatusEmitter<'_>>,
567+) -> Result<()> {
591568 if default_raw.exists() {
592569 return Ok(());
593570 }
594571
595572 ensure_base_image(base_raw, base_compressed, status)?;
596573
597− if let Some(status) = status {
598− status.update("configuring base image...");
599− }
600− tracing::info!("configuring base image");
574+ emit_status(status, "configuring base image...");
575+ tracing::info!("configuring base image...");
601576 fs::copy(base_raw, default_raw)?;
602577
603− let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
578+ let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
604579 let provision_actions = [
605580 Send(provision_command),
606581 ExpectEither {
@@ -615,9 +590,9 @@ fn ensure_default_image(
615590 default_raw,
616591 &provision_actions,
617592 directory_shares,
618− DEFAULT_CPU_COUNT,
619− DEFAULT_RAM_BYTES,
620− None,
593+ BoxConfig::default().cpu_count,
594+ BoxConfig::default().ram_size.as_u64(),
595+ status,
621596 move |output_monitor, vm_output_fd, vm_input_fd| {
622597 spawn_vm_io_with_log(output_monitor, vm_output_fd, vm_input_fd, log_path)
623598 },
@@ -627,9 +602,9 @@ fn ensure_default_image(
627602 default_raw,
628603 &provision_actions,
629604 directory_shares,
630− DEFAULT_CPU_COUNT,
631− DEFAULT_RAM_BYTES,
632− None,
605+ BoxConfig::default().cpu_count,
606+ BoxConfig::default().ram_size.as_u64(),
607+ status,
633608 )
634609 };
635610
@@ -644,18 +619,16 @@ fn ensure_default_image(
644619fn ensure_instance_disk(
645620 instance_raw: &Path,
646621 template_raw: &Path,
647− target_bytes: u64,
648− status: Option<&StatusFile>,
649−) -> Result<bool, Box<dyn std::error::Error>> {
622+ target_bytes: ByteSize,
623+ status: Option<&StatusEmitter<'_>>,
624+) -> Result<bool> {
650625 if instance_raw.exists() {
651− let current_size = fs::metadata(instance_raw)?.len();
626+ let current_size = ByteSize(fs::metadata(instance_raw)?.len());
652627 if current_size != 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);
628+ let current_gb = current_size;
629+ let target_gb = target_bytes;
655630 tracing::warn!(
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.",
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.",
659632 current_gb,
660633 target_gb
661634 );
@@ -663,30 +636,28 @@ fn ensure_instance_disk(
663636 return Ok(false);
664637 }
665638
666− let template_size = fs::metadata(template_raw)?.len();
639+ let template_size = ByteSize(fs::metadata(template_raw)?.len());
667640 if target_bytes < template_size {
668− return Err(format!(
669− "Requested disk size {} bytes is smaller than base image size {} bytes",
641+ bail!(format!(
642+ "Requested disk size {} is smaller than base image size {}",
670643 target_bytes, template_size
671− )
672− .into());
644+ ));
673645 }
674646 let target_size = target_bytes;
675647 let needs_resize = target_size > template_size;
676648
677− if let Some(status) = status {
678− status.update("creating instance disk...");
679− }
649+ emit_status(status, "creating instance disk...");
650+ tracing::info!("creating instance disk...");
680651 tracing::info!(path = %template_raw.display(), "creating instance disk");
681− std::fs::create_dir_all(instance_raw.parent().unwrap())?;
652+ fs::create_dir_all(instance_raw.parent().unwrap())?;
682653 if target_size == template_size {
683654 fs::copy(template_raw, instance_raw)?;
684655 return Ok(needs_resize);
685656 }
686657
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)?;
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)?;
690661 std::io::copy(&mut src, &mut dst)?;
691662 Ok(needs_resize)
692663}
@@ -704,18 +675,17 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
704675 (read_stream.into(), write_stream.into())
705676}
706677
707−pub fn spawn_vm_io_with_hooks<F, G>(
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+>(
708682 output_monitor: Arc<OutputMonitor>,
709683 vm_output_fd: OwnedFd,
710684 vm_input_fd: OwnedFd,
711685 io_control: Arc<IoControl>,
712686 mut on_line: F,
713687 mut on_output: G,
714−) -> IoContext
715−where
716− F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
717− G: FnMut(&[u8]) + ::std::marker::Send + 'static,
718−{
688+) -> IoContext {
719689 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
720690
721691 // raw_guard is set when we've put the user's terminal into raw mode because we've attached stdin/stdout to the VM.
@@ -863,14 +833,14 @@ where
863833 PollResult::Spurious => continue,
864834 PollResult::Ready(bytes) => {
865835 if io_control.forward_output() {
866− // enable raw mode, if we haven't already
836+ // enable raw mode if we haven't already
867837 if raw_guard.lock().unwrap().is_none()
868838 && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
869839 {
870840 *raw_guard.lock().unwrap() = Some(guard);
871841 }
872842
873− let mut stdout = std::io::stdout().lock();
843+ let mut stdout = io::stdout().lock();
874844 if stdout.write_all(bytes).is_err() {
875845 break;
876846 }
@@ -886,7 +856,7 @@ where
886856
887857 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
888858 let mux_thread = thread::spawn(move || {
889− let mut vm_writer = std::fs::File::from(vm_input_fd);
859+ let mut vm_writer = fs::File::from(vm_input_fd);
890860 loop {
891861 match input_rx.recv() {
892862 Ok(VmInput::Bytes(data)) => {
@@ -916,7 +886,7 @@ pub fn spawn_vm_io_with_line_handler<F>(
916886 on_line: F,
917887) -> IoContext
918888where
919− F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
889+ F: FnMut(&str) -> bool + std::marker::Send + 'static,
920890{
921891 spawn_vm_io_with_hooks(
922892 output_monitor,
@@ -983,7 +953,7 @@ fn create_vm_configuration(
983953 vm_writes_to_fd: OwnedFd,
984954 cpu_count: usize,
985955 ram_bytes: u64,
986−) -> Result<Retained<VZVirtualMachineConfiguration>, Box<dyn std::error::Error>> {
956+) -> Result<Retained<VZVirtualMachineConfiguration>> {
987957 unsafe {
988958 let platform =
989959 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
@@ -1017,7 +987,7 @@ fn create_vm_configuration(
1017987 false,
1018988 VZDiskImageCachingMode::Automatic,
1019989 VZDiskImageSynchronizationMode::Full,
1020− ).unwrap();
990+ )?;
1021991
1022992 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
1023993 VZVirtioBlockDeviceConfiguration::alloc(),
@@ -1104,7 +1074,7 @@ fn create_vm_configuration(
11041074 // Validate
11051075 config.validateWithError().map_err(|e| {
11061076 io::Error::other(format!(
1107− "Invalid VM configuration: {:?}",
1077+ "invalid VM configuration: {:?}",
11081078 e.localizedDescription()
11091079 ))
11101080 })?;
@@ -1113,9 +1083,9 @@ fn create_vm_configuration(
11131083 }
11141084}
11151085
1116−fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std::error::Error>> {
1086+fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
11171087 unsafe {
1118− let temp_dir = std::env::temp_dir();
1088+ let temp_dir = env::temp_dir();
11191089 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
11201090 let url = nsurl_from_path(&temp_path)?;
11211091 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
@@ -1131,8 +1101,8 @@ fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std
11311101fn spawn_login_actions_thread(
11321102 login_actions: Vec<LoginAction>,
11331103 output_monitor: Arc<OutputMonitor>,
1134− input_tx: mpsc::Sender<VmInput>,
1135− vm_output_tx: mpsc::Sender<VmOutput>,
1104+ input_tx: Sender<VmInput>,
1105+ vm_output_tx: Sender<VmOutput>,
11361106) -> thread::JoinHandle<()> {
11371107 thread::spawn(move || {
11381108 for a in login_actions {
@@ -1182,9 +1152,9 @@ fn run_vm_with_io<F>(
11821152 directory_shares: &[DirectoryShare],
11831153 cpu_count: usize,
11841154 ram_bytes: u64,
1185− status: Option<&StatusFile>,
1155+ status: Option<&StatusEmitter<'_>>,
11861156 io_handler: F,
1187−) -> Result<(), Box<dyn std::error::Error>>
1157+) -> Result<()>
11881158where
11891159 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
11901160{
@@ -1231,23 +1201,22 @@ where
12311201
12321202 match rx.try_recv() {
12331203 Ok(result) => {
1234− result.map_err(|e| format!("Failed to start VM: {}", e))?;
1204+ result.map_err(|e| Error::msg(format!("Failed to start VM: {}", e)))?;
12351205 break;
12361206 }
12371207 Err(mpsc::TryRecvError::Empty) => continue,
12381208 Err(mpsc::TryRecvError::Disconnected) => {
1239− return Err("VM start channel disconnected".into());
1209+ bail!("VM start channel disconnected");
12401210 }
12411211 }
12421212 }
12431213
12441214 if Instant::now() >= start_deadline {
1245− return Err("Timed out waiting for VM to start".into());
1215+ bail!("Timed out waiting for VM to start");
12461216 }
12471217
1248− if let Some(status) = status {
1249− status.update("vm booting... go vibecoder!");
1250− }
1218+ emit_status(status, "vm booting... go vibecoder!");
1219+ tracing::info!("vm booting... go vibecoder!");
12511220 tracing::info!("vm booting");
12521221
12531222 let output_monitor = Arc::new(OutputMonitor::default());
@@ -1293,7 +1262,7 @@ where
12931262 );
12941263
12951264 let mut last_state = None;
1296− let mut exit_result = Ok(());
1265+ let mut exit_result: Result<(), String> = Ok(());
12971266 loop {
12981267 unsafe {
12991268 NSRunLoop::mainRunLoop().runMode_beforeDate(
@@ -1312,8 +1281,7 @@ where
13121281 exit_result = Err(format!(
13131282 "Login action ({}) timed out after {:?}; shutting down.",
13141283 action, timeout
1315− )
1316− .into());
1284+ ));
13171285 unsafe {
13181286 if vm.canRequestStop() {
13191287 if let Err(err) = vm.requestStopWithError() {
@@ -1330,8 +1298,7 @@ where
13301298 exit_result = Err(format!(
13311299 "Login action ({}) failed: {}; shutting down.",
13321300 action, reason
1333− )
1334− .into());
1301+ ));
13351302 unsafe {
13361303 if vm.canRequestStop() {
13371304 if let Err(err) = vm.requestStopWithError() {
@@ -1347,7 +1314,7 @@ where
13471314 Err(mpsc::TryRecvError::Empty) => {}
13481315 Err(mpsc::TryRecvError::Disconnected) => {}
13491316 }
1350− if state != objc2_virtualization::VZVirtualMachineState::Running {
1317+ if state != VZVirtualMachineState::Running {
13511318 //eprintln!("VM stopped with state: {:?}", state);
13521319 break;
13531320 }
@@ -1357,7 +1324,7 @@ where
13571324
13581325 io_ctx.shutdown();
13591326
1360− exit_result
1327+ exit_result.map_err(Error::msg)
13611328}
13621329
13631330fn run_vm(
@@ -1366,8 +1333,8 @@ fn run_vm(
13661333 directory_shares: &[DirectoryShare],
13671334 cpu_count: usize,
13681335 ram_bytes: u64,
1369− status: Option<&StatusFile>,
1370−) -> Result<(), Box<dyn std::error::Error>> {
1336+ status: Option<&StatusEmitter<'_>>,
1337+) -> Result<()> {
13711338 run_vm_with_io(
13721339 disk_path,
13731340 login_actions,
@@ -1379,7 +1346,7 @@ fn run_vm(
13791346 )
13801347}
13811348
1382−fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
1349+fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
13831350 let abs_path = if path.is_absolute() {
13841351 path.to_path_buf()
13851352 } else {
@@ -1388,7 +1355,7 @@ fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::E
13881355 let ns_path = NSString::from_str(
13891356 abs_path
13901357 .to_str()
1391− .ok_or("Non-UTF8 path encountered while building NSURL")?,
1358+ .with_context(|| "non-UTF8 path encountered while building NSURL")?,
13921359 );
13931360 Ok(NSURL::fileURLWithPath(&ns_path))
13941361}
@@ -1403,7 +1370,7 @@ fn enable_raw_mode(fd: i32) -> io::Result<RawModeGuard> {
14031370 let original = attributes;
14041371
14051372 // Disable translation of carriage return to newline on input
1406− attributes.c_iflag &= !(libc::ICRNL);
1373+ attributes.c_iflag &= !libc::ICRNL;
14071374 // Disable canonical mode (line buffering), echo, and signal generation
14081375 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
14091376 attributes.c_cc[libc::VMIN] = 0;
@@ -1431,10 +1398,10 @@ impl Drop for RawModeGuard {
14311398
14321399// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
14331400pub fn ensure_signed() {
1434− if std::env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1401+ if env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
14351402 return;
14361403 }
1437− let exe = std::env::current_exe().expect("failed to get current exe path");
1404+ let exe = env::current_exe().expect("failed to get current exe path");
14381405 let exe_str = exe.to_str().expect("exe path not valid utf-8");
14391406
14401407 let has_required_entitlements = {
@@ -1456,8 +1423,8 @@ pub fn ensure_signed() {
14561423 }
14571424
14581425 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1459− let entitlements_path = std::env::temp_dir().join("entitlements.plist");
1460− std::fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1426+ let entitlements_path = env::temp_dir().join("entitlements.plist");
1427+ fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
14611428
14621429 let output = Command::new("codesign")
14631430 .args([
@@ -1470,7 +1437,7 @@ pub fn ensure_signed() {
14701437 ])
14711438 .output();
14721439
1473− let _ = std::fs::remove_file(&entitlements_path);
1440+ let _ = fs::remove_file(&entitlements_path);
14741441
14751442 match output {
14761443 Ok(o) if o.status.success() => {
@@ -1478,7 +1445,7 @@ pub fn ensure_signed() {
14781445 if !stderr.trim().is_empty() {
14791446 tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
14801447 }
1481− let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
1448+ let err = Command::new(&exe).args(env::args_os().skip(1)).exec();
14821449 tracing::error!(error = %err, "failed to re-exec after signing");
14831450 std::process::exit(1);
14841451 }