@@ -1,5 +1,5 @@
1+use crate::instance::STATUS_FILE_NAME;
1use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
2use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
2−use anyhow::{Context, Error, Result, bail};
5 io::{self, Write},
5 io::{self, Write},
@@ -23,7 +23,6 @@ use std::{
25use block2::RcBlock;
25use block2::RcBlock;
26−use bytesize::ByteSize;
27use dispatch2::DispatchQueue;
26use dispatch2::DispatchQueue;
28use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
27use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
29use objc2_foundation::*;
28use objc2_foundation::*;
@@ -35,19 +34,49 @@ const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
35const SHARED_DIRECTORIES_TAG: &str = "shared";
34const SHARED_DIRECTORIES_TAG: &str = "shared";
36pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
35pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
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;
38const START_TIMEOUT: Duration = Duration::from_secs(60);
41const START_TIMEOUT: Duration = Duration::from_secs(60);
39const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
42const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
40const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
43const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
42−const ERROR_REPORT_SCRIPT: &str = include_str!("error_report.sh");
45+struct StatusFile {
51+ fn new(path: PathBuf) -> Self {
54+ cleared: AtomicBool::new(false),
58+ fn update(&self, message: &str) {
59+ let _ = fs::write(&self.path, message);
63+impl Drop for StatusFile {
65+ if !self.cleared.load(Ordering::SeqCst) {
66+ let _ = fs::remove_file(&self.path);
67+ self.cleared.store(true, Ordering::SeqCst);
43const PROVISION_SCRIPT: &str = include_str!("provision.sh");
71const PROVISION_SCRIPT: &str = include_str!("provision.sh");
72+const PROVISION_SCRIPT_NAME: &str = "provision.sh";
44const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
73const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
45const DEFAULT_RAW_NAME: &str = "default.raw";
74const DEFAULT_RAW_NAME: &str = "default.raw";
46const INSTANCE_RAW_NAME: &str = "instance.raw";
75const INSTANCE_RAW_NAME: &str = "instance.raw";
47const BASE_DISK_RAW_NAME: &str = "disk.raw";
76const BASE_DISK_RAW_NAME: &str = "disk.raw";
49#[derive(Clone)]
78#[derive(Clone)]
50−pub enum LoginAction {
79+pub(crate) enum LoginAction {
52 text: String,
81 text: String,
53 timeout: Duration,
82 timeout: Duration,
@@ -59,20 +88,23 @@ pub enum LoginAction {
60 Send(String),
89 Send(String),
62−use crate::config::BoxConfig;
63use LoginAction::*;
91use LoginAction::*;
65#[derive(Clone)]
93#[derive(Clone)]
66−pub struct DirectoryShare {
94+pub(crate) struct DirectoryShare {
67 host: PathBuf,
95 host: PathBuf,
68 guest: PathBuf,
96 guest: PathBuf,
69 read_only: bool,
97 read_only: bool,
72impl DirectoryShare {
100impl DirectoryShare {
73− pub fn new(host: PathBuf, mut guest: PathBuf, read_only: bool) -> Result<Self> {
103+ mut guest: PathBuf,
105+ ) -> Result<Self, Box<dyn std::error::Error>> {
74 if !host.exists() {
106 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());
77 if !guest.is_absolute() {
109 if !guest.is_absolute() {
78 guest = PathBuf::from("/root").join(guest);
110 guest = PathBuf::from("/root").join(guest);
@@ -84,10 +116,10 @@ impl DirectoryShare {
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>> {
88 let parts: Vec<&str> = spec.split(':').collect();
120 let parts: Vec<&str> = spec.split(':').collect();
89 if parts.len() < 2 || parts.len() > 3 {
121 if parts.len() < 2 || parts.len() > 3 {
90− bail!(format!("invalid mount spec: {spec}"));
122+ return Err(format!("Invalid mount spec: {spec}").into());
92 let host = expand_tilde_path(parts[0]);
124 let host = expand_tilde_path(parts[0]);
93 let guest = PathBuf::from(parts[1]);
125 let guest = PathBuf::from(parts[1]);
@@ -96,10 +128,11 @@ impl DirectoryShare {
96 "read-only" => true,
128 "read-only" => true,
97 "read-write" => false,
129 "read-write" => false,
99− bail!(format!(
131+ return Err(format!(
100 "Invalid mount mode '{}'; expected read-only or read-write",
132 "Invalid mount mode '{}'; expected read-only or read-write",
@@ -143,28 +176,19 @@ pub struct VmArg {
143 pub mounts: Vec<String>,
176 pub mounts: Vec<String>,
146−type StatusEmitter<'a> = dyn Fn(&str) + std::marker::Send + Sync + 'a;
148−fn emit_status(status: Option<&StatusEmitter<'_>>, message: &str) {
149− if let Some(status) = status {
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>>
156 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
181 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
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())
161−pub fn run_with_args_and_extras<F>(
186+pub(crate) fn run_with_args_and_extras<F>(
162 args: VmArg,
187 args: VmArg,
163 io_handler: F,
188 io_handler: F,
164 extra_login_actions: Vec<LoginAction>,
189 extra_login_actions: Vec<LoginAction>,
165 extra_directory_shares: Vec<DirectoryShare>,
190 extra_directory_shares: Vec<DirectoryShare>,
166− status: Option<&StatusEmitter<'_>>,
191+) -> Result<(), Box<dyn std::error::Error>>
169 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
193 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
@@ -173,7 +197,7 @@ where
173 let project_root = env::current_dir()?;
197 let project_root = env::current_dir()?;
174 let project_name = project_root
198 let project_name = project_root
175 .file_name()
199 .file_name()
176− .with_context(|| "Project directory has no name")?
200+ .ok_or("Project directory has no name")?
177 .to_string_lossy()
201 .to_string_lossy()
178 .into_owned();
202 .into_owned();
@@ -185,10 +209,8 @@ where
185 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
209 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
187 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
211 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
188− fs::create_dir_all(&instance_dir)?;
212+ let status_file = StatusFile::new(instance_dir.join(STATUS_FILE_NAME));
189− emit_status(status, "preparing VM image...");
213+ status_file.update("preparing VM image...");
190− tracing::info!("preparing VM image...");
191− let provision_log = instance_dir.join("provision.log");
193 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
215 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
194 let base_compressed = cache_dir.join(basename_compressed);
216 let base_compressed = cache_dir.join(basename_compressed);
@@ -212,14 +234,13 @@ where
212 &base_compressed,
234 &base_compressed,
213 &default_raw,
235 &default_raw,
214 std::slice::from_ref(&mise_directory_share),
236 std::slice::from_ref(&mise_directory_share),
215− Some(&provision_log),
237+ Some(&status_file),
218 let _ = ensure_instance_disk(
239 let _ = ensure_instance_disk(
219 &instance_raw,
240 &instance_raw,
220 &default_raw,
241 &default_raw,
221− ByteSize(args.disk_bytes),
242+ args.disk_bytes,
222− status,
243+ Some(&status_file),
224 let base_size = fs::metadata(&default_raw)?.len();
245 let base_size = fs::metadata(&default_raw)?.len();
225 let instance_size = fs::metadata(&instance_raw)?.len();
246 let instance_size = fs::metadata(&instance_raw)?.len();
@@ -249,7 +270,7 @@ where
251 if needs_resize {
272 if needs_resize {
252− let resize_cmd = script_command_from_content("resize_disk.sh", RESIZE_DISK_SCRIPT)?;
273+ let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
253 login_actions.push(Send(resize_cmd));
274 login_actions.push(Send(resize_cmd));
@@ -265,29 +286,25 @@ where
265 &directory_shares[..],
286 &directory_shares[..],
266 args.cpu_count,
287 args.cpu_count,
267 args.ram_bytes,
288 args.ram_bytes,
268− status,
289+ Some(&status_file),
269 io_handler,
290 io_handler,
273−pub fn script_command_from_content(label: &str, script: &str) -> Result<String> {
294+pub(crate) fn script_command_from_content(
297+) -> Result<String, Box<dyn std::error::Error>> {
274 let marker = "VIBE_SCRIPT_EOF";
298 let marker = "VIBE_SCRIPT_EOF";
275 let guest_dir = "/tmp/vibe-scripts";
299 let guest_dir = "/tmp/vibe-scripts";
276 let guest_path = format!("{guest_dir}/{label}.sh");
300 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,
281− let wrapped_script = ERROR_REPORT_SCRIPT
282− .replace("__LABEL__", label)
283− .replace("__SCRIPT_BODY__", script_body);
284 let command = format!(
301 let command = format!(
285− "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{wrapped_script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
302+ "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
287 if script.contains(marker) {
304 if script.contains(marker) {
288− bail!(format!(
305+ return Err(
289− "Script '{label}' contains marker '{marker}', cannot safely upload"
306+ format!("Script '{label}' contains marker '{marker}', cannot safely upload").into(),
292 Ok(command)
309 Ok(command)
@@ -484,16 +501,18 @@ impl IoControl {
484fn ensure_base_image(
501fn ensure_base_image(
485 base_raw: &Path,
502 base_raw: &Path,
486 base_compressed: &Path,
503 base_compressed: &Path,
487− status: Option<&StatusEmitter<'_>>,
504+ status: Option<&StatusFile>,
488−) -> Result<()> {
505+) -> Result<(), Box<dyn std::error::Error>> {
489 if base_raw.exists() {
506 if base_raw.exists() {
490 return Ok(());
507 return Ok(());
493 if !base_compressed.exists()
510 if !base_compressed.exists()
494− || fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
511+ || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
513+ if let Some(status) = status {
496− emit_status(status, "downloading base image...");
514+ status.update("downloading base image...");
497 tracing::info!("downloading base image");
516 tracing::info!("downloading base image");
498 let status = Command::new("curl")
517 let status = Command::new("curl")
@@ -508,14 +527,15 @@ fn ensure_base_image(
509 .status()?;
528 .status()?;
510 if !status.success() {
529 if !status.success() {
511− bail!("failed to download base image");
530+ return Err("Failed to download base image".into());
515 // Check SHA
534 // Check SHA
517− emit_status(status, "verifying base image...");
536+ if let Some(status) = status {
518− tracing::info!("verifying base image...");
537+ status.update("verifying base image...");
519 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
539 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
521 let mut child = Command::new("/usr/bin/shasum")
541 let mut child = Command::new("/usr/bin/shasum")
@@ -533,25 +553,25 @@ fn ensure_base_image(
534 let status = child.wait().expect("failed to wait on child");
554 let status = child.wait().expect("failed to wait on child");
535 if !status.success() {
555 if !status.success() {
537− "SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}"
556+ return Err(format!("SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}").into());
560+ if let Some(status) = status {
542− emit_status(status, "decompressing base image...");
561+ status.update("decompressing base image...");
543− tracing::info!("decompressing base image...");
563+ tracing::info!("decompressing base image");
544 let status = Command::new("tar")
564 let status = Command::new("tar")
547 &base_compressed.to_string_lossy(),
567 &base_compressed.to_string_lossy(),
548 BASE_DISK_RAW_NAME,
568 BASE_DISK_RAW_NAME,
550− .stdout(fs::File::create(base_raw)?)
570+ .stdout(std::fs::File::create(base_raw)?)
551 .status()?;
571 .status()?;
553 if !status.success() {
573 if !status.success() {
554− bail!("Failed to decompress base image");
574+ return Err("Failed to decompress base image".into());
@@ -562,20 +582,21 @@ fn ensure_default_image(
562 base_compressed: &Path,
582 base_compressed: &Path,
563 default_raw: &Path,
583 default_raw: &Path,
564 directory_shares: &[DirectoryShare],
584 directory_shares: &[DirectoryShare],
565− provision_log: Option<&Path>,
566− status: Option<&StatusEmitter<'_>>,
585+ status: Option<&StatusFile>,
567−) -> Result<()> {
586+) -> Result<(), Box<dyn std::error::Error>> {
568 if default_raw.exists() {
587 if default_raw.exists() {
569 return Ok(());
588 return Ok(());
572 ensure_base_image(base_raw, base_compressed, status)?;
591 ensure_base_image(base_raw, base_compressed, status)?;
593+ if let Some(status) = status {
574− emit_status(status, "configuring base image...");
594+ status.update("configuring base image...");
575− tracing::info!("configuring base image...");
596+ tracing::info!("configuring base image");
576 fs::copy(base_raw, default_raw)?;
597 fs::copy(base_raw, default_raw)?;
578− let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
599+ let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
579 let provision_actions = [
600 let provision_actions = [
580 Send(provision_command),
601 Send(provision_command),
581 ExpectEither {
602 ExpectEither {
@@ -584,31 +605,14 @@ fn ensure_default_image(
584 timeout: PROVISION_EXPECT_TIMEOUT,
605 timeout: PROVISION_EXPECT_TIMEOUT,
587− let provision_result = if let Some(log_path) = provision_log {
588− let log_path = log_path.to_path_buf();
593− BoxConfig::default().cpu_count,
594− BoxConfig::default().ram_size.as_u64(),
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)
605− BoxConfig::default().cpu_count,
606− BoxConfig::default().ram_size.as_u64(),
611− if let Err(err) = provision_result {
608+ if let Err(err) = run_vm(
612 let _ = fs::remove_file(default_raw);
616 let _ = fs::remove_file(default_raw);
613 return Err(err);
617 return Err(err);
@@ -619,16 +623,18 @@ fn ensure_default_image(
619fn ensure_instance_disk(
623fn ensure_instance_disk(
620 instance_raw: &Path,
624 instance_raw: &Path,
621 template_raw: &Path,
625 template_raw: &Path,
622− target_bytes: ByteSize,
626+ target_bytes: u64,
623− status: Option<&StatusEmitter<'_>>,
627+ status: Option<&StatusFile>,
624−) -> Result<bool> {
628+) -> Result<bool, Box<dyn std::error::Error>> {
625 if instance_raw.exists() {
629 if instance_raw.exists() {
626− let current_size = ByteSize(fs::metadata(instance_raw)?.len());
630+ let current_size = fs::metadata(instance_raw)?.len();
627 if current_size != target_bytes {
631 if current_size != target_bytes {
628− let current_gb = current_size;
632+ let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
629− let target_gb = target_bytes;
633+ let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
630 tracing::warn!(
634 tracing::warn!(
635+ current_bytes = current_size,
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.",
637+ "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.",
632 current_gb,
638 current_gb,
633 target_gb
639 target_gb
@@ -636,28 +642,30 @@ fn ensure_instance_disk(
636 return Ok(false);
642 return Ok(false);
639− let template_size = ByteSize(fs::metadata(template_raw)?.len());
645+ let template_size = fs::metadata(template_raw)?.len();
640 if target_bytes < template_size {
646 if target_bytes < template_size {
641− bail!(format!(
647+ return Err(format!(
642− "Requested disk size {} is smaller than base image size {}",
648+ "Requested disk size {} bytes is smaller than base image size {} bytes",
643 target_bytes, template_size
649 target_bytes, template_size
646 let target_size = target_bytes;
653 let target_size = target_bytes;
647 let needs_resize = target_size > template_size;
654 let needs_resize = target_size > template_size;
649− emit_status(status, "creating instance disk...");
656+ if let Some(status) = status {
650− tracing::info!("creating instance disk...");
657+ status.update("creating instance disk...");
651 tracing::info!(path = %template_raw.display(), "creating instance disk");
659 tracing::info!(path = %template_raw.display(), "creating instance disk");
652− fs::create_dir_all(instance_raw.parent().unwrap())?;
660+ std::fs::create_dir_all(instance_raw.parent().unwrap())?;
653 if target_size == template_size {
661 if target_size == template_size {
654 fs::copy(template_raw, instance_raw)?;
662 fs::copy(template_raw, instance_raw)?;
655 return Ok(needs_resize);
663 return Ok(needs_resize);
658− let mut dst = fs::File::create(instance_raw)?;
666+ let mut dst = std::fs::File::create(instance_raw)?;
659− dst.set_len(target_size.as_u64())?;
667+ dst.set_len(target_size)?;
660− let mut src = fs::File::open(template_raw)?;
668+ let mut src = std::fs::File::open(template_raw)?;
661 std::io::copy(&mut src, &mut dst)?;
669 std::io::copy(&mut src, &mut dst)?;
662 Ok(needs_resize)
670 Ok(needs_resize)
@@ -675,17 +683,18 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
675 (read_stream.into(), write_stream.into())
683 (read_stream.into(), write_stream.into())
678−pub fn spawn_vm_io_with_hooks<
686+pub fn spawn_vm_io_with_hooks<F, G>(
679− F: FnMut(&str) -> bool + std::marker::Send + 'static,
680− G: FnMut(&[u8]) + std::marker::Send + 'static,
682 output_monitor: Arc<OutputMonitor>,
687 output_monitor: Arc<OutputMonitor>,
683 vm_output_fd: OwnedFd,
688 vm_output_fd: OwnedFd,
684 vm_input_fd: OwnedFd,
689 vm_input_fd: OwnedFd,
685 io_control: Arc<IoControl>,
690 io_control: Arc<IoControl>,
686 mut on_line: F,
691 mut on_line: F,
687 mut on_output: G,
692 mut on_output: G,
688−) -> IoContext {
693+) -> IoContext
695+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
696+ G: FnMut(&[u8]) + ::std::marker::Send + 'static,
689 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
698 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
691 // raw_guard is set when we've put the user's terminal into raw mode because we've attached stdin/stdout to the VM.
700 // 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 +842,14 @@ pub fn spawn_vm_io_with_hooks<
833 PollResult::Spurious => continue,
842 PollResult::Spurious => continue,
834 PollResult::Ready(bytes) => {
843 PollResult::Ready(bytes) => {
835 if io_control.forward_output() {
844 if io_control.forward_output() {
836− // enable raw mode if we haven't already
845+ // enable raw mode, if we haven't already
837 if raw_guard.lock().unwrap().is_none()
846 if raw_guard.lock().unwrap().is_none()
838 && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
847 && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
840 *raw_guard.lock().unwrap() = Some(guard);
849 *raw_guard.lock().unwrap() = Some(guard);
843− let mut stdout = io::stdout().lock();
852+ let mut stdout = std::io::stdout().lock();
844 if stdout.write_all(bytes).is_err() {
853 if stdout.write_all(bytes).is_err() {
@@ -856,7 +865,7 @@ pub fn spawn_vm_io_with_hooks<
857 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
866 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
858 let mux_thread = thread::spawn(move || {
867 let mux_thread = thread::spawn(move || {
859− let mut vm_writer = fs::File::from(vm_input_fd);
868+ let mut vm_writer = std::fs::File::from(vm_input_fd);
861 match input_rx.recv() {
870 match input_rx.recv() {
862 Ok(VmInput::Bytes(data)) => {
871 Ok(VmInput::Bytes(data)) => {
@@ -886,7 +895,7 @@ pub fn spawn_vm_io_with_line_handler<F>(
886 on_line: F,
895 on_line: F,
887) -> IoContext
896) -> IoContext
889− F: FnMut(&str) -> bool + std::marker::Send + 'static,
898+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
891 spawn_vm_io_with_hooks(
900 spawn_vm_io_with_hooks(
892 output_monitor,
901 output_monitor,
@@ -906,36 +915,6 @@ pub fn spawn_vm_io(
906 spawn_vm_io_with_line_handler(output_monitor, vm_output_fd, vm_input_fd, |_| false)
915 spawn_vm_io_with_line_handler(output_monitor, vm_output_fd, vm_input_fd, |_| false)
909−fn spawn_vm_io_with_log(
910− output_monitor: Arc<OutputMonitor>,
911− vm_output_fd: OwnedFd,
912− vm_input_fd: OwnedFd,
915− let log_file = fs::OpenOptions::new()
921− .map(|file| Arc::new(Mutex::new(file)));
923− spawn_vm_io_with_hooks(
930− if let Some(log) = &log_file
931− && let Ok(mut file) = log.lock()
933− let _ = file.write_all(bytes);
939impl IoContext {
918impl IoContext {
940 pub fn shutdown(self) {
919 pub fn shutdown(self) {
941 let _ = self.input_tx.send(VmInput::Shutdown);
920 let _ = self.input_tx.send(VmInput::Shutdown);
@@ -953,7 +932,7 @@ fn create_vm_configuration(
953 vm_writes_to_fd: OwnedFd,
932 vm_writes_to_fd: OwnedFd,
954 cpu_count: usize,
933 cpu_count: usize,
955 ram_bytes: u64,
934 ram_bytes: u64,
956−) -> Result<Retained<VZVirtualMachineConfiguration>> {
935+) -> Result<Retained<VZVirtualMachineConfiguration>, Box<dyn std::error::Error>> {
958 let platform =
937 let platform =
959 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
938 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
@@ -987,7 +966,7 @@ fn create_vm_configuration(
988 VZDiskImageCachingMode::Automatic,
967 VZDiskImageCachingMode::Automatic,
989 VZDiskImageSynchronizationMode::Full,
968 VZDiskImageSynchronizationMode::Full,
992 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
971 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
993 VZVirtioBlockDeviceConfiguration::alloc(),
972 VZVirtioBlockDeviceConfiguration::alloc(),
@@ -1074,7 +1053,7 @@ fn create_vm_configuration(
1074 // Validate
1053 // Validate
1075 config.validateWithError().map_err(|e| {
1054 config.validateWithError().map_err(|e| {
1076 io::Error::other(format!(
1055 io::Error::other(format!(
1077− "invalid VM configuration: {:?}",
1056+ "Invalid VM configuration: {:?}",
1078 e.localizedDescription()
1057 e.localizedDescription()
@@ -1083,9 +1062,9 @@ fn create_vm_configuration(
1086−fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
1065+fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std::error::Error>> {
1087 unsafe {
1066 unsafe {
1088− let temp_dir = env::temp_dir();
1067+ let temp_dir = std::env::temp_dir();
1089 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
1068 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
1090 let url = nsurl_from_path(&temp_path)?;
1069 let url = nsurl_from_path(&temp_path)?;
1091 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
1070 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
@@ -1101,8 +1080,8 @@ fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
1101fn spawn_login_actions_thread(
1080fn spawn_login_actions_thread(
1102 login_actions: Vec<LoginAction>,
1081 login_actions: Vec<LoginAction>,
1103 output_monitor: Arc<OutputMonitor>,
1082 output_monitor: Arc<OutputMonitor>,
1104− input_tx: Sender<VmInput>,
1083+ input_tx: mpsc::Sender<VmInput>,
1105− vm_output_tx: Sender<VmOutput>,
1084+ vm_output_tx: mpsc::Sender<VmOutput>,
1106) -> thread::JoinHandle<()> {
1085) -> thread::JoinHandle<()> {
1107 thread::spawn(move || {
1086 thread::spawn(move || {
1108 for a in login_actions {
1087 for a in login_actions {
@@ -1152,9 +1131,9 @@ fn run_vm_with_io<F>(
1152 directory_shares: &[DirectoryShare],
1131 directory_shares: &[DirectoryShare],
1153 cpu_count: usize,
1132 cpu_count: usize,
1154 ram_bytes: u64,
1133 ram_bytes: u64,
1155− status: Option<&StatusEmitter<'_>>,
1134+ status: Option<&StatusFile>,
1156 io_handler: F,
1135 io_handler: F,
1157−) -> Result<()>
1136+) -> Result<(), Box<dyn std::error::Error>>
1159 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
1138 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
@@ -1201,22 +1180,23 @@ where
1202 match rx.try_recv() {
1181 match rx.try_recv() {
1203 Ok(result) => {
1182 Ok(result) => {
1204− result.map_err(|e| Error::msg(format!("Failed to start VM: {}", e)))?;
1183+ result.map_err(|e| format!("Failed to start VM: {}", e))?;
1207 Err(mpsc::TryRecvError::Empty) => continue,
1186 Err(mpsc::TryRecvError::Empty) => continue,
1208 Err(mpsc::TryRecvError::Disconnected) => {
1187 Err(mpsc::TryRecvError::Disconnected) => {
1209− bail!("VM start channel disconnected");
1188+ return Err("VM start channel disconnected".into());
1214 if Instant::now() >= start_deadline {
1193 if Instant::now() >= start_deadline {
1215− bail!("Timed out waiting for VM to start");
1194+ return Err("Timed out waiting for VM to start".into());
1218− emit_status(status, "vm booting... go vibecoder!");
1197+ if let Some(status) = status {
1219− tracing::info!("vm booting... go vibecoder!");
1198+ status.update("vm booting... go vibecoder!");
1220 tracing::info!("vm booting");
1200 tracing::info!("vm booting");
1222 let output_monitor = Arc::new(OutputMonitor::default());
1202 let output_monitor = Arc::new(OutputMonitor::default());
@@ -1262,7 +1242,7 @@ where
1264 let mut last_state = None;
1244 let mut last_state = None;
1265− let mut exit_result: Result<(), String> = Ok(());
1245+ let mut exit_result = Ok(());
1267 unsafe {
1247 unsafe {
1268 NSRunLoop::mainRunLoop().runMode_beforeDate(
1248 NSRunLoop::mainRunLoop().runMode_beforeDate(
@@ -1281,7 +1261,8 @@ where
1281 exit_result = Err(format!(
1261 exit_result = Err(format!(
1282 "Login action ({}) timed out after {:?}; shutting down.",
1262 "Login action ({}) timed out after {:?}; shutting down.",
1283 action, timeout
1263 action, timeout
1285 unsafe {
1266 unsafe {
1286 if vm.canRequestStop() {
1267 if vm.canRequestStop() {
1287 if let Err(err) = vm.requestStopWithError() {
1268 if let Err(err) = vm.requestStopWithError() {
@@ -1298,7 +1279,8 @@ where
1298 exit_result = Err(format!(
1279 exit_result = Err(format!(
1299 "Login action ({}) failed: {}; shutting down.",
1280 "Login action ({}) failed: {}; shutting down.",
1300 action, reason
1281 action, reason
1302 unsafe {
1284 unsafe {
1303 if vm.canRequestStop() {
1285 if vm.canRequestStop() {
1304 if let Err(err) = vm.requestStopWithError() {
1286 if let Err(err) = vm.requestStopWithError() {
@@ -1314,7 +1296,7 @@ where
1314 Err(mpsc::TryRecvError::Empty) => {}
1296 Err(mpsc::TryRecvError::Empty) => {}
1315 Err(mpsc::TryRecvError::Disconnected) => {}
1297 Err(mpsc::TryRecvError::Disconnected) => {}
1317− if state != VZVirtualMachineState::Running {
1299+ if state != objc2_virtualization::VZVirtualMachineState::Running {
1318 //eprintln!("VM stopped with state: {:?}", state);
1300 //eprintln!("VM stopped with state: {:?}", state);
@@ -1324,7 +1306,7 @@ where
1325 io_ctx.shutdown();
1307 io_ctx.shutdown();
1327− exit_result.map_err(Error::msg)
1309+ exit_result
1330fn run_vm(
1312fn run_vm(
@@ -1333,8 +1315,8 @@ fn run_vm(
1333 directory_shares: &[DirectoryShare],
1315 directory_shares: &[DirectoryShare],
1334 cpu_count: usize,
1316 cpu_count: usize,
1335 ram_bytes: u64,
1317 ram_bytes: u64,
1336− status: Option<&StatusEmitter<'_>>,
1318+ status: Option<&StatusFile>,
1337−) -> Result<()> {
1319+) -> Result<(), Box<dyn std::error::Error>> {
1338 run_vm_with_io(
1320 run_vm_with_io(
1339 disk_path,
1321 disk_path,
1340 login_actions,
1322 login_actions,
@@ -1346,7 +1328,7 @@ fn run_vm(
1349−fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1331+fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
1350 let abs_path = if path.is_absolute() {
1332 let abs_path = if path.is_absolute() {
1351 path.to_path_buf()
1333 path.to_path_buf()
1352 } else {
1334 } else {
@@ -1355,7 +1337,7 @@ fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1355 let ns_path = NSString::from_str(
1337 let ns_path = NSString::from_str(
1356 abs_path
1338 abs_path
1357 .to_str()
1339 .to_str()
1358− .with_context(|| "non-UTF8 path encountered while building NSURL")?,
1340+ .ok_or("Non-UTF8 path encountered while building NSURL")?,
1360 Ok(NSURL::fileURLWithPath(&ns_path))
1342 Ok(NSURL::fileURLWithPath(&ns_path))
@@ -1370,7 +1352,7 @@ fn enable_raw_mode(fd: i32) -> io::Result<RawModeGuard> {
1370 let original = attributes;
1352 let original = attributes;
1372 // Disable translation of carriage return to newline on input
1354 // Disable translation of carriage return to newline on input
1373− attributes.c_iflag &= !libc::ICRNL;
1355+ attributes.c_iflag &= !(libc::ICRNL);
1374 // Disable canonical mode (line buffering), echo, and signal generation
1356 // Disable canonical mode (line buffering), echo, and signal generation
1375 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
1357 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
1376 attributes.c_cc[libc::VMIN] = 0;
1358 attributes.c_cc[libc::VMIN] = 0;
@@ -1398,10 +1380,10 @@ impl Drop for RawModeGuard {
1399// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
1381// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
1400pub fn ensure_signed() {
1382pub fn ensure_signed() {
1401− if env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1383+ if std::env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1404− let exe = env::current_exe().expect("failed to get current exe path");
1386+ let exe = std::env::current_exe().expect("failed to get current exe path");
1405 let exe_str = exe.to_str().expect("exe path not valid utf-8");
1387 let exe_str = exe.to_str().expect("exe path not valid utf-8");
1407 let has_required_entitlements = {
1389 let has_required_entitlements = {
@@ -1423,8 +1405,8 @@ pub fn ensure_signed() {
1425 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1407 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1426− let entitlements_path = env::temp_dir().join("entitlements.plist");
1408+ let entitlements_path = std::env::temp_dir().join("entitlements.plist");
1427− fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1409+ std::fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1429 let output = Command::new("codesign")
1411 let output = Command::new("codesign")
@@ -1437,7 +1419,7 @@ pub fn ensure_signed() {
1438 .output();
1420 .output();
1440− let _ = fs::remove_file(&entitlements_path);
1422+ let _ = std::fs::remove_file(&entitlements_path);
1442 match output {
1424 match output {
1443 Ok(o) if o.status.success() => {
1425 Ok(o) if o.status.success() => {
@@ -1445,7 +1427,7 @@ pub fn ensure_signed() {
1445 if !stderr.trim().is_empty() {
1427 if !stderr.trim().is_empty() {
1446 tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
1428 tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
1448− let err = Command::new(&exe).args(env::args_os().skip(1)).exec();
1430+ let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
1449 tracing::error!(error = %err, "failed to re-exec after signing");
1431 tracing::error!(error = %err, "failed to re-exec after signing");
1450 std::process::exit(1);
1432 std::process::exit(1);