@@ -302,17 +302,17 @@ impl App {
302302 // Persist the removing marker so crash recovery can resume
303303 let work_dir = self . work_dir ( id) ;
304304 if let Err ( err) = work_dir. set_removing ( ) {
305- warn ! ( "Failed to write .removing marker for {id}: {err:?}" ) ;
305+ warn ! ( "failed to write .removing marker for {id}: {err:?}" ) ;
306306 }
307307
308308 // Clean up port forwarding immediately
309309 self . cleanup_port_forward ( id) . await ;
310310
311- // Spawn background cleanup coroutine
311+ // User-initiated removal always deletes the workdir
312312 let app = self . clone ( ) ;
313313 let id = id. to_string ( ) ;
314314 tokio:: spawn ( async move {
315- if let Err ( err) = app. finish_remove_vm ( & id) . await {
315+ if let Err ( err) = app. finish_remove_vm ( & id, true ) . await {
316316 error ! ( "Background cleanup failed for {id}: {err:?}" ) ;
317317 }
318318 } ) ;
@@ -321,8 +321,10 @@ impl App {
321321 }
322322
323323 /// Background cleanup: stop supervisor process, wait for it to exit,
324- /// remove from supervisor, delete workdir, and free CID.
325- async fn finish_remove_vm ( & self , id : & str ) -> Result < ( ) > {
324+ /// remove from supervisor, optionally delete workdir, and free CID.
325+ ///
326+ /// `delete_workdir`: true for user-initiated removal, false for orphan cleanup.
327+ async fn finish_remove_vm ( & self , id : & str , delete_workdir : bool ) -> Result < ( ) > {
326328 // Stop the supervisor process (idempotent if already stopped)
327329 if let Err ( err) = self . supervisor . stop ( id) . await {
328330 debug ! ( "supervisor.stop({id}) during removal: {err:?}" ) ;
@@ -361,12 +363,20 @@ impl App {
361363 }
362364 }
363365
364- // Delete the workdir (may already be gone, e.g. manual deletion before reload)
366+ // Only delete the workdir for user-initiated removal or if .removing marker exists.
367+ // Orphaned supervisor processes without the marker keep their data intact.
365368 let vm_path = self . work_dir ( id) ;
366- if vm_path. path ( ) . exists ( ) {
367- if let Err ( err) = fs:: remove_dir_all ( & vm_path) {
368- error ! ( "Failed to remove VM directory for {id}: {err:?}" ) ;
369+ if delete_workdir || vm_path. is_removing ( ) {
370+ if vm_path. path ( ) . exists ( ) {
371+ if let Err ( err) = fs:: remove_dir_all ( & vm_path) {
372+ error ! ( "failed to remove VM directory for {id}: {err:?}" ) ;
373+ }
369374 }
375+ } else if vm_path. path ( ) . exists ( ) {
376+ info ! (
377+ "VM {id} workdir preserved (orphan cleanup): {}" ,
378+ vm_path. path( ) . display( )
379+ ) ;
370380 }
371381
372382 // Free CID and remove from memory (last step)
@@ -381,7 +391,8 @@ impl App {
381391 Ok ( ( ) )
382392 }
383393
384- /// Spawn a background task to clean up a VM (stop + remove from supervisor + delete workdir).
394+ /// Spawn a background task to clean up a VM (stop + remove from supervisor).
395+ /// Workdir deletion is based on the `.removing` marker (only present for user-initiated removal).
385396 /// Returns false if a cleanup task is already running for this VM.
386397 fn spawn_finish_remove ( & self , id : & str ) -> bool {
387398 {
@@ -394,12 +405,13 @@ impl App {
394405 vm. state . removing = true ;
395406 }
396407 // If VM is not in memory (e.g. orphaned supervisor process), no entry to guard
397- // but we still need to clean up the supervisor process and workdir .
408+ // but we still need to clean up the supervisor process.
398409 }
399410 let app = self . clone ( ) ;
400411 let id = id. to_string ( ) ;
401412 tokio:: spawn ( async move {
402- if let Err ( err) = app. finish_remove_vm ( & id) . await {
413+ // Don't pass delete_workdir=true; rely on .removing marker check inside
414+ if let Err ( err) = app. finish_remove_vm ( & id, false ) . await {
403415 error ! ( "Background cleanup failed for {id}: {err:?}" ) ;
404416 }
405417 } ) ;
0 commit comments