@@ -48,6 +48,30 @@ pub(crate) mod registry;
4848mod vm_info;
4949mod workdir;
5050
51+ fn signal_pidfd ( pid : u32 , signal : libc:: c_int ) -> std:: io:: Result < ( ) > {
52+ // SAFETY: pidfd syscalls receive scalar arguments and a null siginfo pointer.
53+ let fd = unsafe { libc:: syscall ( libc:: SYS_pidfd_open , pid, 0 ) } ;
54+ if fd < 0 {
55+ return Err ( std:: io:: Error :: last_os_error ( ) ) ;
56+ }
57+ let result = unsafe {
58+ libc:: syscall (
59+ libc:: SYS_pidfd_send_signal ,
60+ fd,
61+ signal,
62+ std:: ptr:: null :: < libc:: siginfo_t > ( ) ,
63+ 0 ,
64+ )
65+ } ;
66+ // SAFETY: fd was returned by pidfd_open and is owned by this function.
67+ unsafe { libc:: close ( fd as libc:: c_int ) } ;
68+ if result < 0 {
69+ Err ( std:: io:: Error :: last_os_error ( ) )
70+ } else {
71+ Ok ( ( ) )
72+ }
73+ }
74+
5175#[ derive( Deserialize , Serialize , Debug , Clone ) ]
5276pub struct PortMapping {
5377 pub address : IpAddr ,
@@ -255,26 +279,6 @@ pub struct App {
255279 pub ( crate ) pull_status : Arc < Mutex < std:: collections:: HashMap < String , PullStatus > > > ,
256280}
257281
258- #[ derive( Debug , PartialEq , Eq ) ]
259- enum DependencyAction {
260- KeepRunning ,
261- StopDependents ,
262- StopVm ,
263- Finished ,
264- }
265-
266- fn dependency_action ( main_running : bool , dependents_running : & [ bool ] ) -> DependencyAction {
267- if dependents_running. is_empty ( ) {
268- DependencyAction :: Finished
269- } else if !main_running {
270- DependencyAction :: StopDependents
271- } else if dependents_running. iter ( ) . any ( |running| !running) {
272- DependencyAction :: StopVm
273- } else {
274- DependencyAction :: KeepRunning
275- }
276- }
277-
278282impl App {
279283 pub ( crate ) fn lock ( & self ) -> MutexGuard < ' _ , AppState > {
280284 self . state . lock ( ) . or_panic ( "mutex poisoned" )
@@ -298,7 +302,6 @@ impl App {
298302 cid_pool,
299303 vms : HashMap :: new ( ) ,
300304 active_forwards : HashMap :: new ( ) ,
301- dependency_monitors : HashSet :: new ( ) ,
302305 } ) ) ,
303306 config : Arc :: new ( config) ,
304307 forward_service : Arc :: new ( tokio:: sync:: Mutex :: new ( ForwardService :: new ( ) ) ) ,
@@ -397,7 +400,6 @@ impl App {
397400 vm_state. config . clone ( )
398401 } ;
399402 if !is_running {
400- self . wait_for_dependent_processes_stopped ( id) . await ?;
401403 let work_dir = self . work_dir ( id) ;
402404 for path in [ work_dir. serial_pty ( ) , work_dir. qmp_socket ( ) ] {
403405 if path. symlink_metadata ( ) . is_ok ( ) {
@@ -419,10 +421,8 @@ impl App {
419421 let vm_state = state. get_mut ( id) . context ( "VM not found" ) ?;
420422 vm_state. state . runtime_networks = runtime_networks;
421423 }
422- let has_dependent_processes = processes. len ( ) > 1 ;
423424 for process in processes {
424425 if let Err ( err) = self . supervisor . deploy ( & process) . await {
425- self . stop_dependent_processes ( id) . await ;
426426 if let Err ( clear_err) = work_dir. clear_runtime_networks ( ) {
427427 warn ! (
428428 id,
@@ -436,9 +436,6 @@ impl App {
436436 . with_context ( || format ! ( "failed to start process {}" , process. id) ) ;
437437 }
438438 }
439- if has_dependent_processes {
440- self . spawn_dependency_monitor ( id) ;
441- }
442439
443440 let mut state = self . lock ( ) ;
444441 let vm_state = state. get_mut ( id) . context ( "VM not found" ) ?;
@@ -457,103 +454,36 @@ impl App {
457454 pub async fn stop_vm ( & self , id : & str ) -> Result < ( ) > {
458455 self . set_started ( id, false ) ?;
459456 self . cleanup_port_forward ( id) . await ;
460- let result = self . supervisor . stop ( id) . await ;
461- self . stop_dependent_processes ( id) . await ;
462- result
463- }
464-
465- async fn stop_dependent_processes ( & self , id : & str ) {
466- for process in self . supervisor . list ( ) . await . unwrap_or_default ( ) {
467- let note =
468- serde_json:: from_str :: < ProcessAnnotation > ( & process. config . note ) . unwrap_or_default ( ) ;
469- if note. live_for . as_deref ( ) == Some ( id) {
470- if let Err ( err) = self . supervisor . stop ( & process. config . id ) . await {
471- debug ! (
472- process_id = process. config. id,
473- "failed to stop dependent process: {err:?}"
474- ) ;
475- }
476- }
477- }
478- }
479-
480- async fn wait_for_dependent_processes_stopped ( & self , id : & str ) -> Result < ( ) > {
481- for _ in 0 ..50 {
482- let running = self . supervisor . list ( ) . await ?. into_iter ( ) . any ( |process| {
483- let note = serde_json:: from_str :: < ProcessAnnotation > ( & process. config . note )
484- . unwrap_or_default ( ) ;
485- note. live_for . as_deref ( ) == Some ( id) && process. state . status . is_running ( )
486- } ) ;
487- if !running {
488- return Ok ( ( ) ) ;
489- }
490- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) . await ;
491- }
492- bail ! ( "timed out waiting for dependent processes of VM {id} to stop" )
457+ self . stop_vm_process ( id) . await ?;
458+ Ok ( ( ) )
493459 }
494460
495- /// Keep auxiliary processes fail-closed with their owning VM. If QEMU
496- /// exits, all dependents are stopped. If a required dependent exits first,
497- /// QEMU is stopped rather than continuing with a broken emulated device.
498- fn spawn_dependency_monitor ( & self , id : & str ) {
499- {
500- let mut state = self . lock ( ) ;
501- if !state. dependency_monitors . insert ( id. to_string ( ) ) {
502- return ;
461+ async fn stop_vm_process ( & self , id : & str ) -> Result < ( ) > {
462+ let Some ( info) = self . supervisor . info ( id) . await ? else {
463+ return Ok ( ( ) ) ;
464+ } ;
465+ if info. state . status . is_running ( ) {
466+ let pid = info. state . pid . context ( "running VM launcher has no PID" ) ?;
467+ if let Err ( error) = signal_pidfd ( pid, libc:: SIGTERM ) {
468+ warn ! ( id, %pid, %error, "failed to signal VM launcher gracefully; forcing shutdown" ) ;
469+ return self . supervisor . stop ( id) . await ;
503470 }
504- }
505- let app = self . clone ( ) ;
506- let id = id. to_string ( ) ;
507- tokio:: spawn ( async move {
508- loop {
509- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 250 ) ) . await ;
510- let processes = match app. supervisor . list ( ) . await {
511- Ok ( processes) => processes,
512- Err ( err) => {
513- warn ! ( vm_id = id, "failed to inspect dependent processes: {err:?}" ) ;
514- continue ;
515- }
516- } ;
517- let main_running = processes
518- . iter ( )
519- . find ( |process| process. config . id == id)
520- . is_some_and ( |process| process. state . status . is_running ( ) ) ;
521- let dependents = processes
522- . iter ( )
523- . filter ( |process| {
524- serde_json:: from_str :: < ProcessAnnotation > ( & process. config . note )
525- . unwrap_or_default ( )
526- . live_for
527- . as_deref ( )
528- == Some ( id. as_str ( ) )
529- } )
530- . collect :: < Vec < _ > > ( ) ;
531- let dependent_states = dependents
532- . iter ( )
533- . map ( |process| process. state . status . is_running ( ) )
534- . collect :: < Vec < _ > > ( ) ;
535- match dependency_action ( main_running, & dependent_states) {
536- DependencyAction :: KeepRunning => continue ,
537- DependencyAction :: Finished => break ,
538- DependencyAction :: StopDependents => {
539- app. stop_dependent_processes ( & id) . await ;
540- break ;
541- }
542- DependencyAction :: StopVm => {
543- error ! ( vm_id = id, "required VM process exited; stopping VM" ) ;
544- if let Err ( err) = app. supervisor . stop ( & id) . await {
545- debug ! (
546- vm_id = id,
547- "failed to stop VM after dependent exit: {err:?}"
548- ) ;
549- }
550- app. stop_dependent_processes ( & id) . await ;
551- break ;
552- }
471+ for _ in 0 ..150 {
472+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) . await ;
473+ let running = self
474+ . supervisor
475+ . info ( id)
476+ . await ?
477+ . is_some_and ( |info| info. state . status . is_running ( ) ) ;
478+ if !running {
479+ // Synchronize Supervisor's `started` flag after the launcher
480+ // completed its graceful child cleanup.
481+ return self . supervisor . stop ( id) . await ;
553482 }
554483 }
555- app. lock ( ) . dependency_monitors . remove ( & id) ;
556- } ) ;
484+ warn ! ( id, "VM launcher did not stop gracefully; forcing shutdown" ) ;
485+ }
486+ self . supervisor . stop ( id) . await
557487 }
558488
559489 pub async fn remove_vm ( & self , id : & str ) -> Result < ( ) > {
@@ -594,10 +524,9 @@ impl App {
594524 /// `delete_workdir`: true for user-initiated removal, false for orphan cleanup.
595525 async fn finish_remove_vm ( & self , id : & str , delete_workdir : bool ) -> Result < ( ) > {
596526 // Stop the supervisor process (idempotent if already stopped)
597- if let Err ( err) = self . supervisor . stop ( id) . await {
598- debug ! ( "supervisor. stop({id}) during removal: {err:?}" ) ;
527+ if let Err ( err) = self . stop_vm_process ( id) . await {
528+ debug ! ( "graceful VM stop during removal failed : {err:?}" ) ;
599529 }
600- self . stop_dependent_processes ( id) . await ;
601530
602531 // Poll until the process is no longer running, then remove it.
603532 // Some VMs take a long time to stop (e.g. 2+ hours), so we wait indefinitely.
@@ -632,31 +561,6 @@ impl App {
632561 }
633562 }
634563
635- // Auxiliary processes (currently swtpm) are owned by the VM through
636- // ProcessAnnotation::live_for. Remove their stopped supervisor entries
637- // before deleting the VM workdir.
638- for process in self . supervisor . list ( ) . await . unwrap_or_default ( ) {
639- let note =
640- serde_json:: from_str :: < ProcessAnnotation > ( & process. config . note ) . unwrap_or_default ( ) ;
641- if note. live_for . as_deref ( ) != Some ( id) {
642- continue ;
643- }
644- for _ in 0 ..50 {
645- match self . supervisor . info ( & process. config . id ) . await {
646- Ok ( Some ( info) ) if info. state . status . is_running ( ) => {
647- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) . await ;
648- }
649- _ => break ,
650- }
651- }
652- if let Err ( err) = self . supervisor . remove ( & process. config . id ) . await {
653- warn ! (
654- process_id = process. config. id,
655- "failed to remove dependent process: {err:?}"
656- ) ;
657- }
658- }
659-
660564 // Only delete the workdir for user-initiated removal or if .removing marker exists.
661565 // Orphaned supervisor processes without the marker keep their data intact.
662566 let vm_path = self . work_dir ( id) ;
@@ -896,24 +800,16 @@ impl App {
896800
897801 // Clean up orphaned supervisor processes (in supervisor but not loaded as VMs)
898802 let loaded_vm_ids: HashSet < String > = self . lock ( ) . vms . keys ( ) . cloned ( ) . collect ( ) ;
899- for ( note, process) in & running_vms {
900- let owner = note. live_for . as_deref ( ) . unwrap_or ( & process. config . id ) ;
901- if !loaded_vm_ids. contains ( owner) {
803+ for ( _, process) in & running_vms {
804+ if !loaded_vm_ids. contains ( & process. config . id ) {
902805 info ! (
903806 "Cleaning up orphaned supervisor process: {}" ,
904807 process. config. id
905808 ) ;
906- self . spawn_finish_remove ( owner ) ;
809+ self . spawn_finish_remove ( & process . config . id ) ;
907810 }
908811 }
909812
910- // Re-establish lifecycle monitoring after a VMM restart. The monitor
911- // also reconciles a QEMU or dependent process that exited while VMM was
912- // unavailable.
913- for id in self . lock ( ) . vms . keys ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) {
914- self . spawn_dependency_monitor ( & id) ;
915- }
916-
917813 // Restore port forwarding for running bridge-mode VMs with persisted guest IPs
918814 let vm_ids: Vec < String > = self . lock ( ) . vms . keys ( ) . cloned ( ) . collect ( ) ;
919815 for id in vm_ids {
@@ -1705,24 +1601,6 @@ mod tests {
17051601 hex:: encode ( vec ! [ byte; len] )
17061602 }
17071603
1708- #[ test]
1709- fn dependency_lifecycle_is_fail_closed ( ) {
1710- assert_eq ! (
1711- dependency_action( true , & [ true ] ) ,
1712- DependencyAction :: KeepRunning
1713- ) ;
1714- assert_eq ! (
1715- dependency_action( false , & [ true ] ) ,
1716- DependencyAction :: StopDependents
1717- ) ;
1718- assert_eq ! ( dependency_action( true , & [ false ] ) , DependencyAction :: StopVm ) ;
1719- assert_eq ! (
1720- dependency_action( false , & [ false ] ) ,
1721- DependencyAction :: StopDependents
1722- ) ;
1723- assert_eq ! ( dependency_action( true , & [ ] ) , DependencyAction :: Finished ) ;
1724- }
1725-
17261604 #[ test]
17271605 fn gpu_config_has_gpus_only_when_resolved_gpu_list_is_non_empty ( ) {
17281606 assert ! ( !GpuConfig :: default ( ) . has_gpus( ) ) ;
@@ -2324,8 +2202,6 @@ pub(crate) struct AppState {
23242202 vms : HashMap < String , VmState > ,
23252203 /// Tracks active port forwarding rules per VM ID (bridge mode only).
23262204 active_forwards : HashMap < String , Vec < ForwardRule > > ,
2327- /// VM IDs with an active auxiliary-process lifecycle monitor.
2328- dependency_monitors : HashSet < String > ,
23292205}
23302206
23312207impl AppState {
0 commit comments