3434DEFAULT_CONFIG_PATH = os .path .expanduser ("~/.dstack-vmm/config.json" )
3535DEFAULT_KMS_WHITELIST_PATH = os .path .expanduser ("~/.dstack-vmm/kms-whitelist.json" )
3636
37- # VMM discovery directory
38- DISCOVERY_DIR = "/run/dstack-vmm"
37+
38+ # VMM discovery directories
39+ # Each user's instances are in $XDG_RUNTIME_DIR/dstack-vmm (typically /run/user/<uid>/dstack-vmm).
40+ # CLI scans all users' directories so operators can see every instance on the host.
41+ def _get_discovery_dirs () -> List [Tuple [str , Optional [str ]]]:
42+ """Return list of (discovery_dir, username) tuples."""
43+ import pwd
44+
45+ dirs = []
46+ run_user = "/run/user"
47+ if os .path .isdir (run_user ):
48+ try :
49+ for uid_str in os .listdir (run_user ):
50+ candidate = os .path .join (run_user , uid_str , "dstack-vmm" )
51+ if os .path .isdir (candidate ):
52+ try :
53+ username = pwd .getpwuid (int (uid_str )).pw_name
54+ except (KeyError , ValueError ):
55+ username = f"uid:{ uid_str } "
56+ dirs .append ((candidate , username ))
57+ except PermissionError :
58+ pass
59+ return dirs
3960
4061
4162def load_config () -> Dict [str , Any ]:
@@ -56,31 +77,29 @@ def load_config() -> Dict[str, Any]:
5677
5778
5879def discover_vmm_instances () -> List [Dict [str , Any ]]:
59- """Discover all running VMM instances from the discovery directory .
80+ """Discover all running VMM instances across all users on the host .
6081
6182 Returns:
6283 List of VMM instance info dicts, sorted by started_at.
6384
6485 """
6586 instances = []
66- if not os .path .isdir (DISCOVERY_DIR ):
67- return instances
68-
69- for fname in os .listdir (DISCOVERY_DIR ):
70- if not fname .endswith (".json" ):
71- continue
72- fpath = os .path .join (DISCOVERY_DIR , fname )
73- try :
74- with open (fpath , "r" ) as f :
75- info = json .load (f )
76- # Check if process is still alive
77- pid = info .get ("pid" )
78- if pid and not os .path .exists (f"/proc/{ pid } " ):
79- # Stale file, skip
87+ for discovery_dir , username in _get_discovery_dirs ():
88+ for fname in os .listdir (discovery_dir ):
89+ if not fname .endswith (".json" ):
90+ continue
91+ fpath = os .path .join (discovery_dir , fname )
92+ try :
93+ with open (fpath , "r" ) as f :
94+ info = json .load (f )
95+ pid = info .get ("pid" )
96+ if pid and not os .path .exists (f"/proc/{ pid } " ):
97+ continue
98+ if username :
99+ info ["user" ] = username
100+ instances .append (info )
101+ except (json .JSONDecodeError , FileNotFoundError , PermissionError ):
80102 continue
81- instances .append (info )
82- except (json .JSONDecodeError , FileNotFoundError , PermissionError ):
83- continue
84103
85104 instances .sort (key = lambda x : x .get ("started_at" , 0 ))
86105 return instances
@@ -163,7 +182,9 @@ def cmd_ls_vmm(args):
163182
164183 if not instances :
165184 print ("No running VMM instances found." )
166- print (f" (discovery directory: { DISCOVERY_DIR } )" )
185+ print (
186+ f" (scanned: { ', ' .join (d for d , _ in _get_discovery_dirs ()) or '/run/user/*/dstack-vmm' } )"
187+ )
167188 return
168189
169190 if getattr (args , "json" , False ):
@@ -172,18 +193,19 @@ def cmd_ls_vmm(args):
172193
173194 # Table output
174195
175- fmt = " {active} {id:<12s} {pid:<8s} {node:<12s} {address:<24s} {workdir}"
196+ fmt = " {active} {id:<12s} {pid:<8s} {user:<10s} { node:<12s} {address:<24s} {workdir}"
176197 print (
177198 fmt .format (
178199 active = "" ,
179200 id = "ID" ,
180201 pid = "PID" ,
202+ user = "USER" ,
181203 node = "NAME" ,
182204 address = "ADDRESS" ,
183205 workdir = "WORKING DIR" ,
184206 )
185207 )
186- print (" " + "-" * 90 )
208+ print (" " + "-" * 100 )
187209
188210 for inst in instances :
189211 short_id = inst ["id" ][:8 ]
@@ -196,6 +218,7 @@ def cmd_ls_vmm(args):
196218 active = is_active ,
197219 id = short_id ,
198220 pid = str (inst .get ("pid" , "?" )),
221+ user = inst .get ("user" , "?" )[:10 ],
199222 node = node_name [:12 ],
200223 address = address [:24 ],
201224 workdir = inst .get ("working_dir" , "?" ),
@@ -1517,21 +1540,47 @@ def main():
15171540
15181541 subparsers = parser .add_subparsers (dest = "command" , help = "Commands" )
15191542
1520- # VMM discovery commands
1521- ls_vmm_parser = subparsers .add_parser (
1522- "ls- vmm" , help = "List all running VMM instances on this host "
1543+ # VMM discovery commands (vmm ls / vmm switch)
1544+ vmm_parser = subparsers .add_parser (
1545+ "vmm" , help = "VMM instance management (ls, switch) "
15231546 )
1524- ls_vmm_parser .add_argument (
1547+ vmm_subparsers = vmm_parser .add_subparsers (
1548+ dest = "vmm_command" , help = "VMM sub-commands"
1549+ )
1550+
1551+ vmm_ls_parser = vmm_subparsers .add_parser (
1552+ "ls" , help = "List all running VMM instances on this host"
1553+ )
1554+ vmm_ls_parser .add_argument (
15251555 "--json" , action = "store_true" , help = "Output in JSON format"
15261556 )
15271557
1528- switch_vmm_parser = subparsers .add_parser (
1529- "switch-vmm " , help = "Switch active VMM instance"
1558+ vmm_switch_parser = vmm_subparsers .add_parser (
1559+ "switch" , help = "Switch active VMM instance"
15301560 )
1531- switch_vmm_parser .add_argument (
1561+ vmm_switch_parser .add_argument (
15321562 "vmm_id" , help = "VMM instance ID (prefix match supported)"
15331563 )
15341564
1565+ # Register nested subcommands for top-level help display
1566+ _nested_commands = {
1567+ "vmm ls" : "List all running VMM instances on this host" ,
1568+ "vmm switch" : "Switch active VMM instance" ,
1569+ }
1570+
1571+ # Patch parser's format_help to show nested subcommands
1572+ _orig_format_help = parser .format_help
1573+
1574+ def _patched_format_help ():
1575+ text = _orig_format_help ()
1576+ # Append nested subcommands after the subparser listing
1577+ extra = "\n nested commands:\n "
1578+ for cmd , desc in _nested_commands .items ():
1579+ extra += f" { cmd :<24s} { desc } \n "
1580+ return text + extra
1581+
1582+ parser .format_help = _patched_format_help
1583+
15351584 # List command
15361585 lsvm_parser = subparsers .add_parser ("lsvm" , help = "List VMs" )
15371586 lsvm_parser .add_argument (
@@ -1879,11 +1928,13 @@ def main():
18791928 args = parser .parse_args ()
18801929
18811930 # Handle discovery commands before creating CLI (they don't need a connection)
1882- if args .command == "ls-vmm" :
1883- cmd_ls_vmm (args )
1884- return
1885- elif args .command == "switch-vmm" :
1886- cmd_switch_vmm (args )
1931+ if args .command == "vmm" :
1932+ if args .vmm_command == "ls" :
1933+ cmd_ls_vmm (args )
1934+ elif args .vmm_command == "switch" :
1935+ cmd_switch_vmm (args )
1936+ else :
1937+ vmm_parser .print_help ()
18871938 return
18881939
18891940 # Resolve the URL with auto-discovery
0 commit comments