@@ -292,17 +292,17 @@ impl App {
292292 // Persist the removing marker so crash recovery can resume
293293 let work_dir = self . work_dir ( id) ;
294294 if let Err ( err) = work_dir. set_removing ( ) {
295- warn ! ( "Failed to write .removing marker for {id}: {err:?}" ) ;
295+ warn ! ( "failed to write .removing marker for {id}: {err:?}" ) ;
296296 }
297297
298298 // Clean up port forwarding immediately
299299 self . cleanup_port_forward ( id) . await ;
300300
301- // Spawn background cleanup coroutine
301+ // User-initiated removal always deletes the workdir
302302 let app = self . clone ( ) ;
303303 let id = id. to_string ( ) ;
304304 tokio:: spawn ( async move {
305- if let Err ( err) = app. finish_remove_vm ( & id) . await {
305+ if let Err ( err) = app. finish_remove_vm ( & id, true ) . await {
306306 error ! ( "Background cleanup failed for {id}: {err:?}" ) ;
307307 }
308308 } ) ;
@@ -311,8 +311,10 @@ impl App {
311311 }
312312
313313 /// Background cleanup: stop supervisor process, wait for it to exit,
314- /// remove from supervisor, delete workdir, and free CID.
315- async fn finish_remove_vm ( & self , id : & str ) -> Result < ( ) > {
314+ /// remove from supervisor, optionally delete workdir, and free CID.
315+ ///
316+ /// `delete_workdir`: true for user-initiated removal, false for orphan cleanup.
317+ async fn finish_remove_vm ( & self , id : & str , delete_workdir : bool ) -> Result < ( ) > {
316318 // Stop the supervisor process (idempotent if already stopped)
317319 if let Err ( err) = self . supervisor . stop ( id) . await {
318320 debug ! ( "supervisor.stop({id}) during removal: {err:?}" ) ;
@@ -351,12 +353,17 @@ impl App {
351353 }
352354 }
353355
354- // Delete the workdir (may already be gone, e.g. manual deletion before reload)
356+ // Only delete the workdir for user-initiated removal or if .removing marker exists.
357+ // Orphaned supervisor processes without the marker keep their data intact.
355358 let vm_path = self . work_dir ( id) ;
356- if vm_path. path ( ) . exists ( ) {
357- if let Err ( err) = fs:: remove_dir_all ( & vm_path) {
358- error ! ( "Failed to remove VM directory for {id}: {err:?}" ) ;
359+ if delete_workdir || vm_path. is_removing ( ) {
360+ if vm_path. path ( ) . exists ( ) {
361+ if let Err ( err) = fs:: remove_dir_all ( & vm_path) {
362+ error ! ( "failed to remove VM directory for {id}: {err:?}" ) ;
363+ }
359364 }
365+ } else if vm_path. path ( ) . exists ( ) {
366+ info ! ( "VM {id} workdir preserved (orphan cleanup): {}" , vm_path. path( ) . display( ) ) ;
360367 }
361368
362369 // Free CID and remove from memory (last step)
@@ -371,7 +378,8 @@ impl App {
371378 Ok ( ( ) )
372379 }
373380
374- /// Spawn a background task to clean up a VM (stop + remove from supervisor + delete workdir).
381+ /// Spawn a background task to clean up a VM (stop + remove from supervisor).
382+ /// Workdir deletion is based on the `.removing` marker (only present for user-initiated removal).
375383 /// Returns false if a cleanup task is already running for this VM.
376384 fn spawn_finish_remove ( & self , id : & str ) -> bool {
377385 {
@@ -384,12 +392,13 @@ impl App {
384392 vm. state . removing = true ;
385393 }
386394 // If VM is not in memory (e.g. orphaned supervisor process), no entry to guard
387- // but we still need to clean up the supervisor process and workdir .
395+ // but we still need to clean up the supervisor process.
388396 }
389397 let app = self . clone ( ) ;
390398 let id = id. to_string ( ) ;
391399 tokio:: spawn ( async move {
392- if let Err ( err) = app. finish_remove_vm ( & id) . await {
400+ // Don't pass delete_workdir=true; rely on .removing marker check inside
401+ if let Err ( err) = app. finish_remove_vm ( & id, false ) . await {
393402 error ! ( "Background cleanup failed for {id}: {err:?}" ) ;
394403 }
395404 } ) ;
0 commit comments