@@ -1,3 +1,4 @@
1+use crate::instance::STATUS_FILE_NAME;
12use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
23use std::{
34 env, fs,
@@ -38,6 +39,39 @@ const DEFAULT_RAM_MB: u64 = 2048;
3839const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
3940const START_TIMEOUT: Duration = Duration::from_secs(60);
4041const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
42+
43+struct StatusFile {
44+ path: PathBuf,
45+ cleared: AtomicBool,
46+}
47+
48+impl StatusFile {
49+ fn new(path: PathBuf) -> Self {
50+ Self {
51+ path,
52+ cleared: AtomicBool::new(false),
53+ }
54+ }
55+
56+ fn update(&self, message: &str) {
57+ let _ = fs::write(&self.path, message);
58+ }
59+
60+ fn clear(&self) {
61+ if !self.cleared.swap(true, Ordering::SeqCst) {
62+ let _ = fs::remove_file(&self.path);
63+ }
64+ }
65+}
66+
67+impl Drop for StatusFile {
68+ fn drop(&mut self) {
69+ if !self.cleared.load(Ordering::SeqCst) {
70+ let _ = fs::remove_file(&self.path);
71+ self.cleared.store(true, Ordering::SeqCst);
72+ }
73+ }
74+}
4175const PROVISION_SCRIPT: &str = include_str!("provision.sh");
4276const PROVISION_SCRIPT_NAME: &str = "provision.sh";
4377const DEFAULT_RAW_NAME: &str = "default.raw";
@@ -170,6 +204,8 @@ where
170204 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
171205
172206 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
207+ let status_file = StatusFile::new(instance_dir.join(STATUS_FILE_NAME));
208+ status_file.update("preparing VM image...");
173209
174210 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
175211 let base_compressed = cache_dir.join(basename_compressed);
@@ -193,8 +229,9 @@ where
193229 &base_compressed,
194230 &default_raw,
195231 std::slice::from_ref(&mise_directory_share),
232+ Some(&status_file),
196233 )?;
197− ensure_instance_disk(&instance_raw, &default_raw)?;
234+ ensure_instance_disk(&instance_raw, &default_raw, Some(&status_file))?;
198235 let disk_path = instance_raw;
199236
200237 let mut login_actions = Vec::new();
@@ -239,6 +276,7 @@ where
239276 &directory_shares[..],
240277 args.cpu_count,
241278 args.ram_bytes,
279+ Some(&status_file),
242280 io_handler,
243281 )
244282}
@@ -421,6 +459,7 @@ impl IoControl {
421459fn ensure_base_image(
422460 base_raw: &Path,
423461 base_compressed: &Path,
462+ status: Option<&StatusFile>,
424463) -> Result<(), Box<dyn std::error::Error>> {
425464 if base_raw.exists() {
426465 return Ok(());
@@ -429,6 +468,9 @@ fn ensure_base_image(
429468 if !base_compressed.exists()
430469 || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
431470 {
471+ if let Some(status) = status {
472+ status.update("downloading base image...");
473+ }
432474 tracing::info!("downloading base image");
433475 let status = Command::new("curl")
434476 .args([
@@ -449,6 +491,9 @@ fn ensure_base_image(
449491
450492 // Check SHA
451493 {
494+ if let Some(status) = status {
495+ status.update("verifying base image...");
496+ }
452497 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
453498
454499 let mut child = Command::new("/usr/bin/shasum")
@@ -470,6 +515,9 @@ fn ensure_base_image(
470515 }
471516 }
472517
518+ if let Some(status) = status {
519+ status.update("decompressing base image...");
520+ }
473521 tracing::info!("decompressing base image");
474522 let status = Command::new("tar")
475523 .args([
@@ -492,13 +540,17 @@ fn ensure_default_image(
492540 base_compressed: &Path,
493541 default_raw: &Path,
494542 directory_shares: &[DirectoryShare],
543+ status: Option<&StatusFile>,
495544) -> Result<(), Box<dyn std::error::Error>> {
496545 if default_raw.exists() {
497546 return Ok(());
498547 }
499548
500− ensure_base_image(base_raw, base_compressed)?;
549+ ensure_base_image(base_raw, base_compressed, status)?;
501550
551+ if let Some(status) = status {
552+ status.update("configuring base image...");
553+ }
502554 tracing::info!("configuring base image");
503555 fs::copy(base_raw, default_raw)?;
504556
@@ -509,6 +561,7 @@ fn ensure_default_image(
509561 directory_shares,
510562 DEFAULT_CPU_COUNT,
511563 DEFAULT_RAM_BYTES,
564+ None,
512565 )?;
513566
514567 Ok(())
@@ -517,11 +570,15 @@ fn ensure_default_image(
517570fn ensure_instance_disk(
518571 instance_raw: &Path,
519572 template_raw: &Path,
573+ status: Option<&StatusFile>,
520574) -> Result<(), Box<dyn std::error::Error>> {
521575 if instance_raw.exists() {
522576 return Ok(());
523577 }
524578
579+ if let Some(status) = status {
580+ status.update("creating instance disk...");
581+ }
525582 tracing::info!(path = %template_raw.display(), "creating instance disk");
526583 std::fs::create_dir_all(instance_raw.parent().unwrap())?;
527584 fs::copy(template_raw, instance_raw)?;
@@ -980,6 +1037,7 @@ fn run_vm_with_io<F>(
9801037 directory_shares: &[DirectoryShare],
9811038 cpu_count: usize,
9821039 ram_bytes: u64,
1040+ status: Option<&StatusFile>,
9831041 io_handler: F,
9841042) -> Result<(), Box<dyn std::error::Error>>
9851043where
@@ -1042,6 +1100,10 @@ where
10421100 return Err("Timed out waiting for VM to start".into());
10431101 }
10441102
1103+ if let Some(status) = status {
1104+ status.update("vm booting... go vibecoder!");
1105+ status.clear();
1106+ }
10451107 tracing::info!("vm booting");
10461108
10471109 let output_monitor = Arc::new(OutputMonitor::default());
@@ -1142,6 +1204,7 @@ fn run_vm(
11421204 directory_shares: &[DirectoryShare],
11431205 cpu_count: usize,
11441206 ram_bytes: u64,
1207+ status: Option<&StatusFile>,
11451208) -> Result<(), Box<dyn std::error::Error>> {
11461209 run_vm_with_io(
11471210 disk_path,
@@ -1149,6 +1212,7 @@ fn run_vm(
11491212 directory_shares,
11501213 cpu_count,
11511214 ram_bytes,
1215+ status,
11521216 spawn_vm_io,
11531217 )
11541218}