@@ -10,20 +10,22 @@ use dstack_kms_rpc::kms_client::KmsClient;
1010use dstack_types:: shared_filenames:: {
1111 APP_COMPOSE , ENCRYPTED_ENV , INSTANCE_INFO , SYS_CONFIG , USER_CONFIG ,
1212} ;
13- use dstack_vmm_rpc:: { self as pb, GpuInfo , StatusRequest , StatusResponse , VmConfiguration } ;
13+ use dstack_vmm_rpc:: {
14+ self as pb, GpuInfo , ReloadVmsResponse , StatusRequest , StatusResponse , VmConfiguration ,
15+ } ;
1416use fs_err as fs;
1517use guest_api:: client:: DefaultClient as GuestClient ;
1618use id_pool:: IdPool ;
1719use ra_rpc:: client:: RaClient ;
1820use serde:: { Deserialize , Serialize } ;
1921use serde_json:: json;
20- use std:: collections:: { BTreeSet , HashMap , VecDeque } ;
22+ use std:: collections:: { BTreeSet , HashMap , HashSet , VecDeque } ;
2123use std:: net:: IpAddr ;
2224use std:: path:: { Path , PathBuf } ;
2325use std:: sync:: { Arc , Mutex , MutexGuard } ;
2426use std:: time:: SystemTime ;
2527use supervisor_client:: SupervisorClient ;
26- use tracing:: { error, info} ;
28+ use tracing:: { error, info, warn } ;
2729
2830pub use image:: { Image , ImageInfo } ;
2931pub use qemu:: { VmConfig , VmWorkDir } ;
@@ -326,6 +328,199 @@ impl App {
326328 Ok ( ( ) )
327329 }
328330
331+ /// Reload VMs directory and sync with memory state while preserving statistics
332+ pub async fn reload_vms_sync ( & self ) -> Result < ReloadVmsResponse > {
333+ let vm_path = self . vm_dir ( ) ;
334+ let mut loaded = 0u32 ;
335+ let mut updated = 0u32 ;
336+ let mut removed = 0u32 ;
337+
338+ // Get running VMs to preserve CIDs and process info
339+ let running_vms = self . supervisor . list ( ) . await . context ( "Failed to list VMs" ) ?;
340+ let running_vms_map: HashMap < String , _ > = running_vms
341+ . into_iter ( )
342+ . map ( |p| ( p. config . id . clone ( ) , p) )
343+ . collect ( ) ;
344+ let occupied_cids = running_vms_map
345+ . iter ( )
346+ . filter ( |( _, p) | {
347+ serde_json:: from_str :: < ProcessAnnotation > ( & p. config . note )
348+ . unwrap_or_default ( )
349+ . is_cvm ( )
350+ } )
351+ . map ( |( id, p) | ( id. clone ( ) , p. config . cid . unwrap ( ) ) )
352+ . collect :: < HashMap < _ , _ > > ( ) ;
353+
354+ // Update CID pool with running VMs
355+ {
356+ let mut state = self . lock ( ) ;
357+ // First clear the pool and re-occupy running VM CIDs
358+ state. cid_pool . clear ( ) ;
359+ for cid in occupied_cids. values ( ) {
360+ state. cid_pool . occupy ( * cid) ?;
361+ }
362+ }
363+
364+ // Get VM IDs from filesystem
365+ let mut fs_vm_ids = HashSet :: new ( ) ;
366+ if vm_path. exists ( ) {
367+ for entry in fs:: read_dir ( & vm_path) . context ( "Failed to read VM directory" ) ? {
368+ let entry = entry. context ( "Failed to read directory entry" ) ?;
369+ let vm_dir_path = entry. path ( ) ;
370+ if vm_dir_path. is_dir ( ) {
371+ // Try to get VM ID from directory name or manifest
372+ if let Some ( vm_id) = vm_dir_path. file_name ( ) . and_then ( |n| n. to_str ( ) ) {
373+ fs_vm_ids. insert ( vm_id. to_string ( ) ) ;
374+ }
375+ }
376+ }
377+ }
378+
379+ // Get VM IDs currently in memory and their CIDs
380+ let ( memory_vm_ids, existing_cids) : ( HashSet < String > , HashSet < u32 > ) = {
381+ let state = self . lock ( ) ;
382+ (
383+ state. vms . keys ( ) . cloned ( ) . collect ( ) ,
384+ state. vms . values ( ) . map ( |vm| vm. config . cid ) . collect ( ) ,
385+ )
386+ } ;
387+
388+ // Remove VMs that no longer exist in filesystem
389+ let to_remove: Vec < String > = memory_vm_ids. difference ( & fs_vm_ids) . cloned ( ) . collect ( ) ;
390+ if !to_remove. is_empty ( ) {
391+ for vm_id in & to_remove {
392+ // Stop the VM process first if it's running
393+ if running_vms_map. contains_key ( vm_id) {
394+ if let Err ( err) = self . supervisor . stop ( vm_id) . await {
395+ warn ! ( "Failed to stop VM process {vm_id}: {err:?}" ) ;
396+ }
397+ }
398+
399+ // Remove from memory and free CID
400+ let mut state = self . lock ( ) ;
401+ if let Some ( vm) = state. vms . remove ( vm_id) {
402+ state. cid_pool . free ( vm. config . cid ) ;
403+ removed += 1 ;
404+ info ! ( "Removed VM {vm_id} from memory (directory no longer exists)" ) ;
405+ }
406+ }
407+ }
408+
409+ // Load or update VMs from filesystem
410+ if vm_path. exists ( ) {
411+ for entry in fs:: read_dir ( vm_path) . context ( "Failed to read VM directory" ) ? {
412+ let entry = entry. context ( "Failed to read directory entry" ) ?;
413+ let vm_path = entry. path ( ) ;
414+ if vm_path. is_dir ( ) {
415+ match self . load_or_update_vm ( & vm_path, & occupied_cids, true ) . await {
416+ Ok ( is_new) => {
417+ if is_new {
418+ loaded += 1 ;
419+ } else {
420+ updated += 1 ;
421+ }
422+ }
423+ Err ( err) => {
424+ error ! ( "Failed to load or update VM: {err:?}" ) ;
425+ }
426+ }
427+ }
428+ }
429+ }
430+
431+ // Clean up any orphaned CIDs that aren't being used
432+ {
433+ let mut state = self . lock ( ) ;
434+ let used_cids: HashSet < u32 > = state. vms . values ( ) . map ( |vm| vm. config . cid ) . collect ( ) ;
435+ let orphaned_cids: Vec < u32 > = existing_cids. difference ( & used_cids) . cloned ( ) . collect ( ) ;
436+ for cid in orphaned_cids {
437+ state. cid_pool . free ( cid) ;
438+ info ! ( "Released orphaned CID {cid}" ) ;
439+ }
440+ }
441+
442+ Ok ( ReloadVmsResponse {
443+ loaded,
444+ updated,
445+ removed,
446+ } )
447+ }
448+
449+ /// Load or update a VM, preserving existing statistics
450+ async fn load_or_update_vm (
451+ & self ,
452+ work_dir : impl AsRef < Path > ,
453+ cids_assigned : & HashMap < String , u32 > ,
454+ auto_start : bool ,
455+ ) -> Result < bool > {
456+ let vm_work_dir = VmWorkDir :: new ( work_dir. as_ref ( ) ) ;
457+ let manifest = vm_work_dir. manifest ( ) . context ( "Failed to read manifest" ) ?;
458+ if manifest. image . len ( ) > 64
459+ || manifest. image . contains ( ".." )
460+ || !manifest
461+ . image
462+ . chars ( )
463+ . all ( |c| c. is_alphanumeric ( ) || c == '_' || c == '-' || c == '.' )
464+ {
465+ bail ! ( "Invalid image name" ) ;
466+ }
467+ let image_path = self . config . image_path . join ( & manifest. image ) ;
468+ let image = Image :: load ( & image_path) . context ( "Failed to load image" ) ?;
469+ let vm_id = manifest. id . clone ( ) ;
470+ let app_compose = vm_work_dir
471+ . app_compose ( )
472+ . context ( "Failed to read compose file" ) ?;
473+
474+ let mut is_new = false ;
475+ {
476+ let mut states = self . lock ( ) ;
477+
478+ // For existing VMs, keep their current CID
479+ // For new VMs, try to use assigned CID or allocate a new one
480+ let cid = if let Some ( existing_vm) = states. get ( & vm_id) {
481+ // Keep existing CID
482+ existing_vm. config . cid
483+ } else if let Some ( assigned_cid) = cids_assigned. get ( & vm_id) {
484+ // Use assigned CID from running processes
485+ * assigned_cid
486+ } else {
487+ // Allocate new CID only for truly new VMs
488+ states. cid_pool . allocate ( ) . context ( "CID pool exhausted" ) ?
489+ } ;
490+
491+ let vm_config = VmConfig {
492+ manifest,
493+ image,
494+ cid,
495+ workdir : vm_work_dir. path ( ) . to_path_buf ( ) ,
496+ gateway_enabled : app_compose. gateway_enabled ( ) ,
497+ } ;
498+
499+ match states. get_mut ( & vm_id) {
500+ Some ( vm) => {
501+ // Update existing VM but preserve statistics and CID
502+ let old_state = vm. state . clone ( ) ;
503+ vm. config = vm_config. into ( ) ;
504+ vm. state = old_state; // Preserve the existing state with statistics
505+ }
506+ None => {
507+ // This is a new VM, need to occupy its CID if it wasn't allocated
508+ if !cids_assigned. contains_key ( & vm_id) {
509+ states. cid_pool . occupy ( cid) ?;
510+ }
511+ states. add ( VmState :: new ( vm_config) ) ;
512+ is_new = true ;
513+ }
514+ }
515+ } ;
516+
517+ if auto_start && vm_work_dir. started ( ) . unwrap_or_default ( ) {
518+ self . start_vm ( & vm_id) . await ?;
519+ }
520+
521+ Ok ( is_new)
522+ }
523+
329524 pub async fn list_vms ( & self , request : StatusRequest ) -> Result < StatusResponse > {
330525 let vms = self
331526 . supervisor
0 commit comments