@@ -1,7 +1,7 @@
1−use 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},
@@ -15,7 +15,6 @@ use std::{
15 process::{Command, Stdio},
15 process::{Command, Stdio},
17 Arc, Condvar, Mutex,
17 Arc, Condvar, Mutex,
18− atomic::{AtomicBool, Ordering},
19 mpsc::{self, Receiver, Sender},
18 mpsc::{self, Receiver, Sender},
@@ -23,8 +22,8 @@ use std::{
25use block2::RcBlock;
24use block2::RcBlock;
26−use bytesize::ByteSize;
27use dispatch2::DispatchQueue;
25use dispatch2::DispatchQueue;
26+use lexopt::prelude::*;
28use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
27use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
29use objc2_foundation::*;
28use objc2_foundation::*;
30use objc2_virtualization::*;
29use objc2_virtualization::*;
@@ -33,46 +32,39 @@ const DEBIAN_COMPRESSED_DISK_URL: &str = "https://cloud.debian.org/images/cloud/
33const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
32const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
34const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
33const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
35const SHARED_DIRECTORIES_TAG: &str = "shared";
34const SHARED_DIRECTORIES_TAG: &str = "shared";
36−pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
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;
38const START_TIMEOUT: Duration = Duration::from_secs(60);
40const START_TIMEOUT: Duration = Duration::from_secs(60);
41+const DEFAULT_EXPECT_TIMEOUT: Duration = Duration::from_secs(30);
39const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
42const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
40−const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
42−const ERROR_REPORT_SCRIPT: &str = include_str!("error_report.sh");
43const PROVISION_SCRIPT: &str = include_str!("provision.sh");
43const PROVISION_SCRIPT: &str = include_str!("provision.sh");
44−const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
45−const DEFAULT_RAW_NAME: &str = "default.raw";
46−const INSTANCE_RAW_NAME: &str = "instance.raw";
47−const BASE_DISK_RAW_NAME: &str = "disk.raw";
49#[derive(Clone)]
45#[derive(Clone)]
50−pub enum LoginAction {
46+enum LoginAction {
58− timeout: Duration,
47+ Expect { text: String, timeout: Duration },
60 Send(String),
48 Send(String),
49+ Script { path: PathBuf, index: usize },
62−use crate::config::BoxConfig;
63use LoginAction::*;
51use LoginAction::*;
65#[derive(Clone)]
53#[derive(Clone)]
66−pub struct DirectoryShare {
54+struct DirectoryShare {
67 host: PathBuf,
55 host: PathBuf,
68 guest: PathBuf,
56 guest: PathBuf,
69 read_only: bool,
57 read_only: bool,
72impl DirectoryShare {
60impl DirectoryShare {
73− pub fn new(host: PathBuf, mut guest: PathBuf, read_only: bool) -> Result<Self> {
63+ mut guest: PathBuf,
65+ ) -> Result<Self, Box<dyn std::error::Error>> {
74 if !host.exists() {
66 if !host.exists() {
75− bail!(format!("host path does not exist: {}", host.display()));
67+ return Err(format!("Host path does not exist: {}", host.display()).into());
77 if !guest.is_absolute() {
69 if !guest.is_absolute() {
78 guest = PathBuf::from("/root").join(guest);
70 guest = PathBuf::from("/root").join(guest);
@@ -84,22 +76,23 @@ impl DirectoryShare {
87− pub fn from_mount_spec(spec: &str) -> Result<Self> {
79+ fn from_mount_spec(spec: &str) -> Result<Self, Box<dyn std::error::Error>> {
88 let parts: Vec<&str> = spec.split(':').collect();
80 let parts: Vec<&str> = spec.split(':').collect();
89 if parts.len() < 2 || parts.len() > 3 {
81 if parts.len() < 2 || parts.len() > 3 {
90− bail!(format!("invalid mount spec: {spec}"));
82+ return Err(format!("Invalid mount spec: {spec}").into());
92− let host = expand_tilde_path(parts[0]);
84+ let host = PathBuf::from(parts[0]);
93 let guest = PathBuf::from(parts[1]);
85 let guest = PathBuf::from(parts[1]);
94 let read_only = if parts.len() == 3 {
86 let read_only = if parts.len() == 3 {
95 match parts[2] {
87 match parts[2] {
96 "read-only" => true,
88 "read-only" => true,
97 "read-write" => false,
89 "read-write" => false,
99− bail!(format!(
91+ return Err(format!(
100 "Invalid mount mode '{}'; expected read-only or read-write",
92 "Invalid mount mode '{}'; expected read-only or read-write",
@@ -122,58 +115,47 @@ impl DirectoryShare {
125−fn expand_tilde_path(value: &str) -> PathBuf {
118+fn main() -> Result<(), Box<dyn std::error::Error>> {
126− if let Some(stripped) = value.strip_prefix("~/") {
119+ let args = parse_cli()?;
127− if let Ok(home) = env::var("HOME") {
128− return PathBuf::from(home).join(stripped);
130− } else if value == "~"
131− && let Ok(home) = env::var("HOME")
133− return PathBuf::from(home);
135− PathBuf::from(value)
138−pub struct VmArg {
121+ if args.version {
139− pub cpu_count: usize,
122+ println!("Vibe");
140− pub ram_bytes: u64,
123+ println!("https://github.com/lynaghk/vibe/");
141− pub disk_bytes: u64,
124+ println!("Git SHA: {}", env!("GIT_SHA"));
142− pub no_default_mounts: bool,
125+ std::process::exit(0);
143− 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<()>
128+ if args.help {
156− F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
130+ "Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
158− run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new(), None)
132+vibe [OPTIONS] [disk-image.raw]
136+ --help Print this help message.
137+ --version Print the version (commit SHA).
138+ --no-default-mounts Disable all default mounts.
139+ --mount host-path:guest-path[:read-only | :read-write] Mount `host-path` inside VM at `guest-path`.
140+ Defaults to read-write.
141+ Errors if host-path does not exist.
142+ --cpus <count> Number of virtual CPUs (default {DEFAULT_CPU_COUNT}).
143+ --ram <megabytes> RAM size in megabytes (default {DEFAULT_RAM_MB}).
144+ --script <path/to/script.sh> Run script in VM.
145+ --send <some-command> Type `some-command` followed by newline into the VM.
146+ --expect <string> [timeout-seconds] Wait for `string` to appear in console output before executing next `--script` or `--send`.
147+ If `string` does not appear within timeout (default 30 seconds), shutdown VM with error.
150+ std::process::exit(0);
161−pub fn run_with_args_and_extras<F>(
164− extra_login_actions: Vec<LoginAction>,
165− extra_directory_shares: Vec<DirectoryShare>,
166− status: Option<&StatusEmitter<'_>>,
169− F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
171 ensure_signed();
153 ensure_signed();
173 let project_root = env::current_dir()?;
155 let project_root = env::current_dir()?;
174 let project_name = project_root
156 let project_name = project_root
175 .file_name()
157 .file_name()
176− .with_context(|| "Project directory has no name")?
158+ .ok_or("Project directory has no name")?
177 .to_string_lossy()
159 .to_string_lossy()
178 .into_owned();
160 .into_owned();
@@ -181,14 +163,10 @@ where
181 let cache_home = env::var("XDG_CACHE_HOME")
163 let cache_home = env::var("XDG_CACHE_HOME")
182 .map(PathBuf::from)
164 .map(PathBuf::from)
183 .unwrap_or_else(|_| home.join(".cache"));
165 .unwrap_or_else(|_| home.join(".cache"));
184− let cache_dir = cache_home.join(GLOBAL_CACHE_DIR_NAME);
166+ let cache_dir = cache_home.join("vibe");
185 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
167 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
187− let instance_dir = project_root.join(INSTANCE_DIR_NAME);
169+ let instance_dir = project_root.join(".vibe");
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");
193 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
171 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
194 let base_compressed = cache_dir.join(basename_compressed);
172 let base_compressed = cache_dir.join(basename_compressed);
@@ -197,8 +175,8 @@ where
197 basename_compressed.trim_end_matches(".tar.xz")
175 basename_compressed.trim_end_matches(".tar.xz")
200− let default_raw = cache_dir.join(DEFAULT_RAW_NAME);
178+ let default_raw = cache_dir.join("default.raw");
201− let instance_raw = instance_dir.join(INSTANCE_RAW_NAME);
179+ let instance_raw = instance_dir.join("instance.raw");
203 // Prepare system-wide directories
181 // Prepare system-wide directories
204 fs::create_dir_all(&cache_dir)?;
182 fs::create_dir_all(&cache_dir)?;
@@ -207,31 +185,28 @@ where
207 let mise_directory_share =
185 let mise_directory_share =
208 DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
186 DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
188+ let disk_path = if let Some(path) = args.disk {
190+ return Err(format!("Disk image does not exist: {}", path.display()).into());
210− ensure_default_image(
194+ ensure_default_image(
211− &base_raw,
195+ &base_raw,
212− &base_compressed,
196+ &base_compressed,
213− &default_raw,
197+ &default_raw,
214− std::slice::from_ref(&mise_directory_share),
198+ std::slice::from_ref(&mise_directory_share),
215− Some(&provision_log),
218− let _ = ensure_instance_disk(
200+ ensure_instance_disk(&instance_raw, &default_raw)?;
220− &default_raw,
202+ instance_raw
221− ByteSize(args.disk_bytes),
203+ };
224− let base_size = fs::metadata(&default_raw)?.len();
225− let instance_size = fs::metadata(&instance_raw)?.len();
226− let needs_resize = instance_size > base_size;
227− let disk_path = instance_raw;
229 let mut login_actions = Vec::new();
205 let mut login_actions = Vec::new();
230 let mut directory_shares = Vec::new();
206 let mut directory_shares = Vec::new();
232 if !args.no_default_mounts {
208 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())));
209+ login_actions.push(Send(format!("cd {project_name}")));
236 // 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.
211 // 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.
237 // I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
212 // I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
@@ -239,55 +214,170 @@ where
239 login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
214 login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
217+ directory_shares.push(
218+ DirectoryShare::new(
220+ PathBuf::from("/root/").join(project_name),
223+ .expect("Project directory must exist"),
242 directory_shares.push(mise_directory_share);
226 directory_shares.push(mise_directory_share);
228+ // Add default shares, if they exist
230+ DirectoryShare::new(home.join(".m2"), "/root/.m2".into(), false),
231+ DirectoryShare::new(
232+ home.join(".cargo/registry"),
233+ "/root/.cargo/registry".into(),
236+ DirectoryShare::new(home.join(".codex"), "/root/.codex".into(), false),
237+ DirectoryShare::new(home.join(".claude"), "/root/.claude".into(), false),
245− directory_shares.extend(extra_directory_shares);
242+ directory_shares.push(share)
247 for spec in &args.mounts {
246 for spec in &args.mounts {
248 directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
247 directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
252− let resize_cmd = script_command_from_content("resize_disk.sh", RESIZE_DISK_SCRIPT)?;
253− login_actions.push(Send(resize_cmd));
256 if let Some(motd_action) = motd_login_action(&directory_shares) {
250 if let Some(motd_action) = motd_login_action(&directory_shares) {
257 login_actions.push(motd_action);
251 login_actions.push(motd_action);
254+ // Any user-provided login actions must come after our system ones
260− login_actions.extend(extra_login_actions);
255+ login_actions.extend(args.login_actions);
262− run_vm_with_io(
257+ run_vm(
263 &disk_path,
258 &disk_path,
264 &login_actions,
259 &login_actions,
265 &directory_shares[..],
260 &directory_shares[..],
266 args.cpu_count,
261 args.cpu_count,
267 args.ram_bytes,
262 args.ram_bytes,
267+ disk: Option<PathBuf>,
270+ no_default_mounts: bool,
271+ mounts: Vec<String>,
272+ login_actions: Vec<LoginAction>,
277+fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
278+ fn os_to_string(value: OsString, flag: &str) -> Result<String, Box<dyn std::error::Error>> {
281+ .map_err(|_| format!("{flag} expects valid UTF-8").into())
284+ let mut parser = lexopt::Parser::from_env();
285+ let mut disk = None;
286+ let mut version = false;
287+ let mut help = false;
288+ let mut no_default_mounts = false;
289+ let mut mounts = Vec::new();
290+ let mut login_actions = Vec::new();
291+ let mut script_index = 0;
292+ let mut cpu_count = DEFAULT_CPU_COUNT;
293+ let mut ram_bytes = DEFAULT_RAM_BYTES;
295+ while let Some(arg) = parser.next()? {
297+ Long("version") => version = true,
298+ Long("help") | Short('h') => help = true,
299+ Long("no-default-mounts") => no_default_mounts = true,
301+ let value = os_to_string(parser.value()?, "--cpus")?.parse()?;
303+ return Err("--cpus must be >= 1".into());
308+ let value: u64 = os_to_string(parser.value()?, "--ram")?.parse()?;
310+ return Err("--ram must be >= 1".into());
312+ ram_bytes = value * BYTES_PER_MB;
315+ mounts.push(os_to_string(parser.value()?, "--mount")?);
318+ login_actions.push(Script {
319+ path: os_to_string(parser.value()?, "--script")?.into(),
320+ index: script_index,
325+ login_actions.push(Send(os_to_string(parser.value()?, "--send")?));
328+ let text = os_to_string(parser.value()?, "--expect")?;
329+ let timeout = match parser.optional_value() {
330+ Some(value) => Duration::from_secs(os_to_string(value, "--expect")?.parse()?),
331+ None => DEFAULT_EXPECT_TIMEOUT,
333+ login_actions.push(Expect { text, timeout });
337+ return Err("Only one disk path may be provided".into());
339+ disk = Some(PathBuf::from(value));
341+ _ => return Err(arg.unexpected().into()),
357+fn script_command_from_path(
360+) -> Result<String, Box<dyn std::error::Error>> {
361+ let script = fs::read_to_string(path)
362+ .map_err(|err| format!("Failed to read script {}: {err}", path.display()))?;
363+ let label = format!("{}_{}", index, path.file_name().unwrap().display());
273−pub fn script_command_from_content(label: &str, script: &str) -> Result<String> {
364+ script_command_from_content(&label, &script)
367+fn script_command_from_content(
370+) -> Result<String, Box<dyn std::error::Error>> {
274 let marker = "VIBE_SCRIPT_EOF";
371 let marker = "VIBE_SCRIPT_EOF";
275 let guest_dir = "/tmp/vibe-scripts";
372 let guest_dir = "/tmp/vibe-scripts";
276 let guest_path = format!("{guest_dir}/{label}.sh");
373 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!(
374 let command = format!(
285− "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{wrapped_script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
375+ "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
287 if script.contains(marker) {
377 if script.contains(marker) {
288− bail!(format!(
378+ return Err(
289− "Script '{label}' contains marker '{marker}', cannot safely upload"
379+ format!("Script '{label}' contains marker '{marker}', cannot safely upload").into(),
292 Ok(command)
382 Ok(command)
@@ -321,6 +411,18 @@ fn motd_login_action(directory_shares: &[DirectoryShare]) -> Option<LoginAction>
323 let mut output = String::new();
413 let mut output = String::new();
416+░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░
417+░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
418+ ░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
419+ ░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓██████▓▒░
420+ ░▒▓█▓▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
421+ ░▒▓█▓▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
422+ ░▒▓██▓▒░ ░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░
324 output.push_str(&format!(
426 output.push_str(&format!(
325 "{host_header:<host_width$} {guest_header:<guest_width$} {mode_header}\n",
427 "{host_header:<host_width$} {guest_header:<guest_width$} {mode_header}\n",
326 host_width = host_width
428 host_width = host_width
@@ -351,12 +453,6 @@ enum WaitResult {
354−#[derive(PartialEq, Eq)]
360pub enum VmInput {
456pub enum VmInput {
361 Bytes(Vec<u8>),
457 Bytes(Vec<u8>),
362 Shutdown,
458 Shutdown,
@@ -364,7 +460,6 @@ pub enum VmInput {
365enum VmOutput {
461enum VmOutput {
366 LoginActionTimeout { action: String, timeout: Duration },
462 LoginActionTimeout { action: String, timeout: Duration },
367− LoginActionFailed { action: String, reason: String },
370#[derive(Default)]
465#[derive(Default)]
@@ -401,100 +496,20 @@ impl OutputMonitor {
401 WaitResult::Found
496 WaitResult::Found
405− fn wait_for_any(&self, needles: &[&str], timeout: Duration) -> WaitAnyResult {
406− let mut found: Option<usize> = None;
407− let (_unused, timeout_result) = self
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();
420− if timeout_result.timed_out() {
421− WaitAnyResult::Timeout
423− WaitAnyResult::Found(found.unwrap_or(0))
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);
442−pub struct IoControl {
443− forward_input: AtomicBool,
444− forward_output: AtomicBool,
445− restore_terminal: AtomicBool,
449− pub fn new() -> Arc<Self> {
451− forward_input: AtomicBool::new(true),
452− forward_output: AtomicBool::new(true),
453− restore_terminal: AtomicBool::new(false),
457− pub fn set_forward_input(&self, enabled: bool) {
458− self.forward_input.store(enabled, Ordering::SeqCst);
461− pub fn set_forward_output(&self, enabled: bool) {
462− self.forward_output.store(enabled, Ordering::SeqCst);
465− pub fn request_terminal_restore(&self) {
466− self.restore_terminal.store(true, Ordering::SeqCst);
469− fn forward_input(&self) -> bool {
470− self.forward_input.load(Ordering::SeqCst)
473− fn forward_output(&self) -> bool {
474− self.forward_output.load(Ordering::SeqCst)
477− fn take_restore_terminal(&self) -> bool {
478− self.restore_terminal
479− .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
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+) -> Result<(), Box<dyn std::error::Error>> {
489 if base_raw.exists() {
505 if base_raw.exists() {
490 return Ok(());
506 return Ok(());
493 if !base_compressed.exists()
509 if !base_compressed.exists()
494− || fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
510+ || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
496− emit_status(status, "downloading base image...");
512+ println!("Downloading base image...");
497− tracing::info!("downloading base image");
498 let status = Command::new("curl")
513 let status = Command::new("curl")
500 "--continue-at",
515 "--continue-at",
@@ -508,14 +523,12 @@ fn ensure_base_image(
509 .status()?;
524 .status()?;
510 if !status.success() {
525 if !status.success() {
511− bail!("failed to download base image");
526+ return Err("Failed to download base image".into());
515 // Check SHA
530 // Check SHA
517− emit_status(status, "verifying base image...");
518− tracing::info!("verifying base image...");
519 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
532 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
521 let mut child = Command::new("/usr/bin/shasum")
534 let mut child = Command::new("/usr/bin/shasum")
@@ -533,25 +546,18 @@ fn ensure_base_image(
534 let status = child.wait().expect("failed to wait on child");
547 let status = child.wait().expect("failed to wait on child");
535 if !status.success() {
548 if !status.success() {
537− "SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}"
549+ return Err(format!("SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}").into());
542− emit_status(status, "decompressing base image...");
553+ println!("Decompressing base image...");
543− tracing::info!("decompressing base image...");
544 let status = Command::new("tar")
554 let status = Command::new("tar")
547− &base_compressed.to_string_lossy(),
555+ .args(["-xOf", &base_compressed.to_string_lossy(), "disk.raw"])
550− .stdout(fs::File::create(base_raw)?)
556+ .stdout(std::fs::File::create(base_raw)?)
551 .status()?;
557 .status()?;
553 if !status.success() {
559 if !status.success() {
554− bail!("Failed to decompress base image");
560+ return Err("Failed to decompress base image".into());
@@ -562,56 +568,24 @@ fn ensure_default_image(
562 base_compressed: &Path,
568 base_compressed: &Path,
563 default_raw: &Path,
569 default_raw: &Path,
564 directory_shares: &[DirectoryShare],
570 directory_shares: &[DirectoryShare],
565− provision_log: Option<&Path>,
571+) -> Result<(), Box<dyn std::error::Error>> {
566− status: Option<&StatusEmitter<'_>>,
568 if default_raw.exists() {
572 if default_raw.exists() {
569 return Ok(());
573 return Ok(());
572− ensure_base_image(base_raw, base_compressed, status)?;
576+ ensure_base_image(base_raw, base_compressed)?;
574− emit_status(status, "configuring base image...");
578+ println!("Configuring base image...");
575− tracing::info!("configuring base image...");
576 fs::copy(base_raw, default_raw)?;
579 fs::copy(base_raw, default_raw)?;
578 let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
581 let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
579− let provision_actions = [
582+ run_vm(
580− Send(provision_command),
584+ &[Send(provision_command)],
582− success: "VIBEBOX_PROVISION_OK".to_string(),
583− failure: "VIBEBOX_PROVISION_FAILED".to_string(),
584− timeout: PROVISION_EXPECT_TIMEOUT,
587− let provision_result = if let Some(log_path) = provision_log {
588− let log_path = log_path.to_path_buf();
592− directory_shares,
585+ directory_shares,
593− BoxConfig::default().cpu_count,
586+ DEFAULT_CPU_COUNT,
594− BoxConfig::default().ram_size.as_u64(),
587+ DEFAULT_RAM_BYTES,
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 {
612− let _ = fs::remove_file(default_raw);
@@ -619,47 +593,15 @@ fn ensure_default_image(
619fn ensure_instance_disk(
593fn ensure_instance_disk(
620 instance_raw: &Path,
594 instance_raw: &Path,
621 template_raw: &Path,
595 template_raw: &Path,
622− target_bytes: ByteSize,
596+) -> Result<(), Box<dyn std::error::Error>> {
623− status: Option<&StatusEmitter<'_>>,
625 if instance_raw.exists() {
597 if instance_raw.exists() {
626− let current_size = ByteSize(fs::metadata(instance_raw)?.len());
627− if current_size != target_bytes {
628− let current_gb = current_size;
629− let target_gb = target_bytes;
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.",
636− return Ok(false);
598+ return Ok(());
639− let template_size = ByteSize(fs::metadata(template_raw)?.len());
640− if target_bytes < template_size {
642− "Requested disk size {} is smaller than base image size {}",
643− target_bytes, template_size
646− let target_size = target_bytes;
647− let needs_resize = target_size > template_size;
649− emit_status(status, "creating instance disk...");
650− tracing::info!("creating instance disk...");
651− tracing::info!(path = %template_raw.display(), "creating instance disk");
652− fs::create_dir_all(instance_raw.parent().unwrap())?;
653− if target_size == template_size {
654− fs::copy(template_raw, instance_raw)?;
655− return Ok(needs_resize);
658− let mut dst = fs::File::create(instance_raw)?;
601+ println!("Creating instance disk from {}...", template_raw.display());
659− dst.set_len(target_size.as_u64())?;
602+ std::fs::create_dir_all(instance_raw.parent().unwrap())?;
660− let mut src = fs::File::open(template_raw)?;
603+ fs::copy(template_raw, instance_raw)?;
661− std::io::copy(&mut src, &mut dst)?;
604+ Ok(())
665pub struct IoContext {
607pub struct IoContext {
@@ -675,16 +617,10 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
675 (read_stream.into(), write_stream.into())
617 (read_stream.into(), write_stream.into())
678−pub fn spawn_vm_io_with_hooks<
620+pub fn spawn_vm_io(
679− F: FnMut(&str) -> bool + std::marker::Send + 'static,
680− G: FnMut(&[u8]) + std::marker::Send + 'static,
682 output_monitor: Arc<OutputMonitor>,
621 output_monitor: Arc<OutputMonitor>,
683 vm_output_fd: OwnedFd,
622 vm_output_fd: OwnedFd,
684 vm_input_fd: OwnedFd,
623 vm_input_fd: OwnedFd,
685− io_control: Arc<IoControl>,
688) -> IoContext {
624) -> IoContext {
689 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
625 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
@@ -700,12 +636,7 @@ pub fn spawn_vm_io_with_hooks<
703− fn poll_with_wakeup<'a>(
639+ fn poll_with_wakeup<'a>(main_fd: RawFd, wakeup_fd: RawFd, buf: &'a mut [u8]) -> PollResult<'a> {
708− ) -> PollResult<'a> {
709 let mut fds = [
640 let mut fds = [
710 libc::pollfd {
641 libc::pollfd {
711 fd: main_fd,
642 fd: main_fd,
@@ -719,12 +650,8 @@ pub fn spawn_vm_io_with_hooks<
722− let ret = unsafe { libc::poll(fds.as_mut_ptr(), 2, timeout_ms) };
653+ let ret = unsafe { libc::poll(fds.as_mut_ptr(), 2, -1) };
724− PollResult::Spurious
727− } else if fds[1].revents & libc::POLLIN != 0 {
654+ if ret <= 0 || fds[1].revents & libc::POLLIN != 0 {
728 PollResult::Shutdown
655 PollResult::Shutdown
729 } else if fds[0].revents & libc::POLLIN != 0 {
656 } else if fds[0].revents & libc::POLLIN != 0 {
730 let n = unsafe { libc::read(main_fd, buf.as_mut_ptr() as *mut _, buf.len()) };
657 let n = unsafe { libc::read(main_fd, buf.as_mut_ptr() as *mut _, buf.len()) };
@@ -740,68 +667,23 @@ pub fn spawn_vm_io_with_hooks<
743− fn poll_wakeup_only(wakeup_fd: RawFd, timeout_ms: i32) -> bool {
744− let mut fds = [libc::pollfd {
746− events: libc::POLLIN,
750− let ret = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
751− ret > 0 && fds[0].revents & libc::POLLIN != 0
754 // Copies from stdin to the VM; also polls wakeup_read to exit the thread when it's time to shutdown.
670 // Copies from stdin to the VM; also polls wakeup_read to exit the thread when it's time to shutdown.
755 let stdin_thread = thread::spawn({
671 let stdin_thread = thread::spawn({
756 let input_tx = input_tx.clone();
672 let input_tx = input_tx.clone();
757 let raw_guard = raw_guard.clone();
673 let raw_guard = raw_guard.clone();
758 let wakeup_read = wakeup_read.try_clone().unwrap();
674 let wakeup_read = wakeup_read.try_clone().unwrap();
759− let io_control = io_control.clone();
761 move || {
676 move || {
762 let mut buf = [0u8; 1024];
677 let mut buf = [0u8; 1024];
763− let mut pending_command: Vec<u8> = Vec::new();
764− let mut command_mode = false;
766− if !io_control.forward_input() {
767− if poll_wakeup_only(wakeup_read.as_raw_fd(), 100) {
773− match poll_with_wakeup(libc::STDIN_FILENO, wakeup_read.as_raw_fd(), &mut buf, -1) {
679+ match poll_with_wakeup(libc::STDIN_FILENO, wakeup_read.as_raw_fd(), &mut buf) {
774 PollResult::Shutdown | PollResult::Error => break,
680 PollResult::Shutdown | PollResult::Error => break,
775 PollResult::Spurious => continue,
681 PollResult::Spurious => continue,
776 PollResult::Ready(bytes) => {
682 PollResult::Ready(bytes) => {
777− let mut send_buf: Vec<u8> = Vec::new();
779− if pending_command.is_empty() && !command_mode && b == b':' {
780− command_mode = true;
784− pending_command.push(b);
789− if b == b'\n' && command_mode {
790− let line = String::from_utf8_lossy(&pending_command);
791− let trimmed = line.trim_end_matches(&['\r', '\n'][..]);
792− let consumed = on_line(trimmed);
794− send_buf.extend_from_slice(&pending_command);
796− pending_command.clear();
797− command_mode = false;
800 if raw_guard.lock().unwrap().is_none() {
683 if raw_guard.lock().unwrap().is_none() {
801 continue;
684 continue;
803− if !send_buf.is_empty() && input_tx.send(VmInput::Bytes(send_buf)).is_err()
686+ if input_tx.send(VmInput::Bytes(bytes.to_vec())).is_err() {
@@ -814,40 +696,28 @@ pub fn spawn_vm_io_with_hooks<
814 let stdout_thread = thread::spawn({
696 let stdout_thread = thread::spawn({
815 let raw_guard = raw_guard.clone();
697 let raw_guard = raw_guard.clone();
816 let wakeup_read = wakeup_read.try_clone().unwrap();
698 let wakeup_read = wakeup_read.try_clone().unwrap();
817− let io_control = io_control.clone();
819 move || {
700 move || {
701+ let mut stdout = std::io::stdout().lock();
820 let mut buf = [0u8; 1024];
702 let mut buf = [0u8; 1024];
822− if io_control.take_restore_terminal() {
823− let mut guard = raw_guard.lock().unwrap();
826− match poll_with_wakeup(
704+ match poll_with_wakeup(vm_output_fd.as_raw_fd(), wakeup_read.as_raw_fd(), &mut buf)
827− vm_output_fd.as_raw_fd(),
828− wakeup_read.as_raw_fd(),
832 PollResult::Shutdown | PollResult::Error => break,
706 PollResult::Shutdown | PollResult::Error => break,
833 PollResult::Spurious => continue,
707 PollResult::Spurious => continue,
834 PollResult::Ready(bytes) => {
708 PollResult::Ready(bytes) => {
835− if io_control.forward_output() {
836− // enable raw mode if we haven't already
709+ // enable raw mode, if we haven't already
837− if raw_guard.lock().unwrap().is_none()
710+ if raw_guard.lock().unwrap().is_none()
838− && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
711+ && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
840− *raw_guard.lock().unwrap() = Some(guard);
713+ *raw_guard.lock().unwrap() = Some(guard);
843− let mut stdout = io::stdout().lock();
844− if stdout.write_all(bytes).is_err() {
716+ if stdout.write_all(bytes).is_err() {
847− let _ = stdout.flush();
719+ let _ = stdout.flush();
849 output_monitor.push(bytes);
720 output_monitor.push(bytes);
@@ -856,7 +726,7 @@ pub fn spawn_vm_io_with_hooks<
857 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
727 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
858 let mux_thread = thread::spawn(move || {
728 let mux_thread = thread::spawn(move || {
859− let mut vm_writer = fs::File::from(vm_input_fd);
729+ let mut vm_writer = std::fs::File::from(vm_input_fd);
861 match input_rx.recv() {
731 match input_rx.recv() {
862 Ok(VmInput::Bytes(data)) => {
732 Ok(VmInput::Bytes(data)) => {
@@ -879,63 +749,6 @@ pub fn spawn_vm_io_with_hooks<
882−pub fn spawn_vm_io_with_line_handler<F>(
883− output_monitor: Arc<OutputMonitor>,
884− vm_output_fd: OwnedFd,
885− vm_input_fd: OwnedFd,
889− F: FnMut(&str) -> bool + std::marker::Send + 'static,
891− spawn_vm_io_with_hooks(
902− output_monitor: Arc<OutputMonitor>,
903− vm_output_fd: OwnedFd,
904− vm_input_fd: OwnedFd,
906− 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 {
752impl IoContext {
940 pub fn shutdown(self) {
753 pub fn shutdown(self) {
941 let _ = self.input_tx.send(VmInput::Shutdown);
754 let _ = self.input_tx.send(VmInput::Shutdown);
@@ -953,7 +766,7 @@ fn create_vm_configuration(
953 vm_writes_to_fd: OwnedFd,
766 vm_writes_to_fd: OwnedFd,
954 cpu_count: usize,
767 cpu_count: usize,
955 ram_bytes: u64,
768 ram_bytes: u64,
956−) -> Result<Retained<VZVirtualMachineConfiguration>> {
769+) -> Result<Retained<VZVirtualMachineConfiguration>, Box<dyn std::error::Error>> {
958 let platform =
771 let platform =
959 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
772 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
@@ -987,7 +800,7 @@ fn create_vm_configuration(
988 VZDiskImageCachingMode::Automatic,
801 VZDiskImageCachingMode::Automatic,
989 VZDiskImageSynchronizationMode::Full,
802 VZDiskImageSynchronizationMode::Full,
992 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
805 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
993 VZVirtioBlockDeviceConfiguration::alloc(),
806 VZVirtioBlockDeviceConfiguration::alloc(),
@@ -1074,7 +887,7 @@ fn create_vm_configuration(
1074 // Validate
887 // Validate
1075 config.validateWithError().map_err(|e| {
888 config.validateWithError().map_err(|e| {
1076 io::Error::other(format!(
889 io::Error::other(format!(
1077− "invalid VM configuration: {:?}",
890+ "Invalid VM configuration: {:?}",
1078 e.localizedDescription()
891 e.localizedDescription()
@@ -1083,9 +896,9 @@ fn create_vm_configuration(
1086−fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
899+fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std::error::Error>> {
1087 unsafe {
900 unsafe {
1088− let temp_dir = env::temp_dir();
901+ let temp_dir = std::env::temp_dir();
1089 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
902 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
1090 let url = nsurl_from_path(&temp_path)?;
903 let url = nsurl_from_path(&temp_path)?;
1091 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
904 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
@@ -1101,8 +914,8 @@ fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
1101fn spawn_login_actions_thread(
914fn spawn_login_actions_thread(
1102 login_actions: Vec<LoginAction>,
915 login_actions: Vec<LoginAction>,
1103 output_monitor: Arc<OutputMonitor>,
916 output_monitor: Arc<OutputMonitor>,
1104− input_tx: Sender<VmInput>,
917+ input_tx: mpsc::Sender<VmInput>,
1105− vm_output_tx: Sender<VmOutput>,
918+ vm_output_tx: mpsc::Sender<VmOutput>,
1106) -> thread::JoinHandle<()> {
919) -> thread::JoinHandle<()> {
1107 thread::spawn(move || {
920 thread::spawn(move || {
1108 for a in login_actions {
921 for a in login_actions {
@@ -1116,48 +929,34 @@ fn spawn_login_actions_thread(
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),
1132− WaitAnyResult::Timeout => {
1133− let _ = vm_output_tx.send(VmOutput::LoginActionTimeout {
1134− action: format!("expect '{}'", success),
1140 Send(mut text) => {
932 Send(mut text) => {
1141 text.push('\n'); // Type the newline so the command is actually submitted.
933 text.push('\n'); // Type the newline so the command is actually submitted.
1142 input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
934 input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
936+ Script { path, index } => {
937+ let command = match script_command_from_path(&path, index) {
938+ Ok(command) => command,
944+ let mut text = command;
946+ input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
1149−fn run_vm_with_io<F>(
953+fn run_vm(
1150 disk_path: &Path,
954 disk_path: &Path,
1151 login_actions: &[LoginAction],
955 login_actions: &[LoginAction],
1152 directory_shares: &[DirectoryShare],
956 directory_shares: &[DirectoryShare],
1153 cpu_count: usize,
957 cpu_count: usize,
1154 ram_bytes: u64,
958 ram_bytes: u64,
1155− status: Option<&StatusEmitter<'_>>,
959+) -> Result<(), Box<dyn std::error::Error>> {
1159− F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
1161 let (vm_reads_from, we_write_to) = create_pipe();
960 let (vm_reads_from, we_write_to) = create_pipe();
1162 let (we_read_from, vm_writes_to) = create_pipe();
961 let (we_read_from, vm_writes_to) = create_pipe();
@@ -1201,26 +1000,24 @@ where
1202 match rx.try_recv() {
1001 match rx.try_recv() {
1203 Ok(result) => {
1002 Ok(result) => {
1204− result.map_err(|e| Error::msg(format!("Failed to start VM: {}", e)))?;
1003+ result.map_err(|e| format!("Failed to start VM: {}", e))?;
1207 Err(mpsc::TryRecvError::Empty) => continue,
1006 Err(mpsc::TryRecvError::Empty) => continue,
1208 Err(mpsc::TryRecvError::Disconnected) => {
1007 Err(mpsc::TryRecvError::Disconnected) => {
1209− bail!("VM start channel disconnected");
1008+ return Err("VM start channel disconnected".into());
1214 if Instant::now() >= start_deadline {
1013 if Instant::now() >= start_deadline {
1215− bail!("Timed out waiting for VM to start");
1014+ return Err("Timed out waiting for VM to start".into());
1218− emit_status(status, "vm booting... go vibecoder!");
1017+ println!("VM booting...");
1219− tracing::info!("vm booting... go vibecoder!");
1220− tracing::info!("vm booting");
1222 let output_monitor = Arc::new(OutputMonitor::default());
1019 let output_monitor = Arc::new(OutputMonitor::default());
1223− let io_ctx = io_handler(output_monitor.clone(), we_read_from, we_write_to);
1020+ let io_ctx = spawn_vm_io(output_monitor.clone(), we_read_from, we_write_to);
1225 let mut all_login_actions = vec![
1022 let mut all_login_actions = vec![
1226 Expect {
1023 Expect {
@@ -1262,7 +1059,7 @@ where
1264 let mut last_state = None;
1061 let mut last_state = None;
1265− let mut exit_result: Result<(), String> = Ok(());
1062+ let mut exit_result = Ok(());
1267 unsafe {
1064 unsafe {
1268 NSRunLoop::mainRunLoop().runMode_beforeDate(
1065 NSRunLoop::mainRunLoop().runMode_beforeDate(
@@ -1281,28 +1078,12 @@ where
1281 exit_result = Err(format!(
1078 exit_result = Err(format!(
1282 "Login action ({}) timed out after {:?}; shutting down.",
1079 "Login action ({}) timed out after {:?}; shutting down.",
1283 action, timeout
1080 action, timeout
1286− if vm.canRequestStop() {
1287− if let Err(err) = vm.requestStopWithError() {
1288− tracing::error!(error = ?err, "failed to request VM stop");
1290− } else if vm.canStop() {
1291− let handler = RcBlock::new(|_error: *mut NSError| {});
1292− vm.stopWithCompletionHandler(&handler);
1297− Ok(VmOutput::LoginActionFailed { action, reason }) => {
1298− exit_result = Err(format!(
1299− "Login action ({}) failed: {}; shutting down.",
1302 unsafe {
1083 unsafe {
1303 if vm.canRequestStop() {
1084 if vm.canRequestStop() {
1304 if let Err(err) = vm.requestStopWithError() {
1085 if let Err(err) = vm.requestStopWithError() {
1305− tracing::error!(error = ?err, "failed to request VM stop");
1086+ eprintln!("Failed to request VM stop: {:?}", err);
1307 } else if vm.canStop() {
1088 } else if vm.canStop() {
1308 let handler = RcBlock::new(|_error: *mut NSError| {});
1089 let handler = RcBlock::new(|_error: *mut NSError| {});
@@ -1314,7 +1095,7 @@ where
1314 Err(mpsc::TryRecvError::Empty) => {}
1095 Err(mpsc::TryRecvError::Empty) => {}
1315 Err(mpsc::TryRecvError::Disconnected) => {}
1096 Err(mpsc::TryRecvError::Disconnected) => {}
1317− if state != VZVirtualMachineState::Running {
1098+ if state != objc2_virtualization::VZVirtualMachineState::Running {
1318 //eprintln!("VM stopped with state: {:?}", state);
1099 //eprintln!("VM stopped with state: {:?}", state);
@@ -1324,29 +1105,10 @@ where
1325 io_ctx.shutdown();
1106 io_ctx.shutdown();
1327− exit_result.map_err(Error::msg)
1108+ exit_result
1332− login_actions: &[LoginAction],
1333− directory_shares: &[DirectoryShare],
1336− status: Option<&StatusEmitter<'_>>,
1349−fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1111+fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
1350 let abs_path = if path.is_absolute() {
1112 let abs_path = if path.is_absolute() {
1351 path.to_path_buf()
1113 path.to_path_buf()
1352 } else {
1114 } else {
@@ -1355,7 +1117,7 @@ fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1355 let ns_path = NSString::from_str(
1117 let ns_path = NSString::from_str(
1356 abs_path
1118 abs_path
1357 .to_str()
1119 .to_str()
1358− .with_context(|| "non-UTF8 path encountered while building NSURL")?,
1120+ .ok_or("Non-UTF8 path encountered while building NSURL")?,
1360 Ok(NSURL::fileURLWithPath(&ns_path))
1122 Ok(NSURL::fileURLWithPath(&ns_path))
@@ -1370,7 +1132,7 @@ fn enable_raw_mode(fd: i32) -> io::Result<RawModeGuard> {
1370 let original = attributes;
1132 let original = attributes;
1372 // Disable translation of carriage return to newline on input
1134 // Disable translation of carriage return to newline on input
1373− attributes.c_iflag &= !libc::ICRNL;
1135+ attributes.c_iflag &= !(libc::ICRNL);
1374 // Disable canonical mode (line buffering), echo, and signal generation
1136 // Disable canonical mode (line buffering), echo, and signal generation
1375 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
1137 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
1376 attributes.c_cc[libc::VMIN] = 0;
1138 attributes.c_cc[libc::VMIN] = 0;
@@ -1398,10 +1160,7 @@ impl Drop for RawModeGuard {
1399// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
1161// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
1400pub fn ensure_signed() {
1162pub fn ensure_signed() {
1401− if env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1404− let exe = env::current_exe().expect("failed to get current exe path");
1163+ 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");
1164 let exe_str = exe.to_str().expect("exe path not valid utf-8");
1407 let has_required_entitlements = {
1166 let has_required_entitlements = {
@@ -1423,10 +1182,10 @@ pub fn ensure_signed() {
1425 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1184 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1426− let entitlements_path = env::temp_dir().join("entitlements.plist");
1185+ let entitlements_path = std::env::temp_dir().join("entitlements.plist");
1427− fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1186+ std::fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1429− let output = Command::new("codesign")
1188+ let status = Command::new("codesign")
1431 "--sign",
1190 "--sign",
@@ -1435,27 +1194,22 @@ pub fn ensure_signed() {
1435 entitlements_path.to_str().unwrap(),
1194 entitlements_path.to_str().unwrap(),
1436 exe_str,
1195 exe_str,
1438− .output();
1197+ .status();
1440− let _ = fs::remove_file(&entitlements_path);
1199+ let _ = std::fs::remove_file(&entitlements_path);
1442− match output {
1201+ match status {
1443− Ok(o) if o.status.success() => {
1202+ Ok(s) if s.success() => {
1444− let stderr = String::from_utf8_lossy(&o.stderr);
1445− if !stderr.trim().is_empty() {
1446− tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
1448− let err = Command::new(&exe).args(env::args_os().skip(1)).exec();
1203+ let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
1449− tracing::error!(error = %err, "failed to re-exec after signing");
1204+ eprintln!("failed to re-exec after signing: {err}");
1450 std::process::exit(1);
1205 std::process::exit(1);
1452− Ok(o) => {
1207+ Ok(s) => {
1453− let stderr = String::from_utf8_lossy(&o.stderr);
1208+ eprintln!("codesign failed with status: {s}");
1454− tracing::error!(status = %o.status, codesign_stderr = %stderr.trim(), "codesign failed");
1455 std::process::exit(1);
1209 std::process::exit(1);
1457 Err(e) => {
1211 Err(e) => {
1458− tracing::error!(error = %e, "failed to run codesign");
1212+ eprintln!("failed to run codesign: {e}");
1459 std::process::exit(1);
1213 std::process::exit(1);