1#![expect(missing_docs)]
8#![cfg_attr(not(test), forbid(unsafe_code))]
9
10mod cli_args;
11mod crash_dump;
12mod kvp;
13mod meshworker;
14mod serial_io;
15mod storage_builder;
16mod tracing_init;
17mod ttrpc;
18
19pub use cli_args::Options;
22use console_relay::ConsoleLaunchOptions;
23
24use crate::cli_args::SecureBootTemplateCli;
25use anyhow::Context;
26use anyhow::bail;
27use chipset_resources::battery::HostBatteryUpdate;
28use clap::CommandFactory;
29use clap::FromArgMatches;
30use clap::Parser;
31use cli_args::DiskCliKind;
32use cli_args::EfiDiagnosticsLogLevelCli;
33use cli_args::EndpointConfigCli;
34use cli_args::NicConfigCli;
35use cli_args::ProvisionVmgs;
36use cli_args::SerialConfigCli;
37use cli_args::UefiConsoleModeCli;
38use cli_args::VirtioBusCli;
39use cli_args::VmgsCli;
40use crash_dump::spawn_dump_handler;
41use disk_backend_resources::DelayDiskHandle;
42use disk_backend_resources::DiskLayerDescription;
43use disk_backend_resources::layer::DiskLayerHandle;
44use disk_backend_resources::layer::RamDiskLayerHandle;
45use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
46use disk_backend_resources::layer::SqliteDiskLayerHandle;
47use floppy_resources::FloppyDiskConfig;
48use framebuffer::FRAMEBUFFER_SIZE;
49use framebuffer::FramebufferAccess;
50use futures::AsyncReadExt;
51use futures::AsyncWrite;
52use futures::AsyncWriteExt;
53use futures::FutureExt;
54use futures::StreamExt;
55use futures::executor::block_on;
56use futures::io::AllowStdIo;
57use futures_concurrency::stream::Merge;
58use gdma_resources::GdmaDeviceHandle;
59use gdma_resources::VportDefinition;
60use get_resources::ged::GuestServicingFlags;
61use guid::Guid;
62use input_core::MultiplexedInputHandle;
63use inspect::InspectMut;
64use inspect::InspectionBuilder;
65use io::Read;
66use mesh::CancelContext;
67use mesh::CellUpdater;
68use mesh::error::RemoteError;
69use mesh::rpc::Rpc;
70use mesh::rpc::RpcError;
71use mesh::rpc::RpcSend;
72use mesh_worker::WorkerEvent;
73use mesh_worker::WorkerHandle;
74use meshworker::VmmMesh;
75use net_backend_resources::mac_address::MacAddress;
76use nvme_resources::NamespaceDefinition;
77use nvme_resources::NvmeControllerRequest;
78use openvmm_defs::config::Config;
79use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64;
80use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2;
81use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86;
82use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86_WITH_VTL2;
83use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
84use openvmm_defs::config::DEFAULT_PCIE_ECAM_BASE;
85use openvmm_defs::config::DeviceVtl;
86use openvmm_defs::config::EfiDiagnosticsLogLevelType;
87use openvmm_defs::config::HypervisorConfig;
88use openvmm_defs::config::LateMapVtl0MemoryPolicy;
89use openvmm_defs::config::LoadMode;
90use openvmm_defs::config::MemoryConfig;
91use openvmm_defs::config::PcieDeviceConfig;
92use openvmm_defs::config::PcieRootComplexConfig;
93use openvmm_defs::config::PcieRootPortConfig;
94use openvmm_defs::config::PcieSwitchConfig;
95use openvmm_defs::config::ProcessorTopologyConfig;
96use openvmm_defs::config::SerialInformation;
97use openvmm_defs::config::VirtioBus;
98use openvmm_defs::config::VmbusConfig;
99use openvmm_defs::config::VpciDeviceConfig;
100use openvmm_defs::config::Vtl2BaseAddressType;
101use openvmm_defs::config::Vtl2Config;
102use openvmm_defs::rpc::PulseSaveRestoreError;
103use openvmm_defs::rpc::VmRpc;
104use openvmm_defs::worker::VM_WORKER;
105use openvmm_defs::worker::VmWorkerParameters;
106use openvmm_helpers::disk::create_disk_type;
107use openvmm_helpers::disk::open_disk_type;
108use pal_async::DefaultDriver;
109use pal_async::DefaultPool;
110use pal_async::pipe::PolledPipe;
111use pal_async::socket::PolledSocket;
112use pal_async::task::Spawn;
113use pal_async::task::Task;
114use pal_async::timer::PolledTimer;
115use scsidisk_resources::SimpleScsiDiskHandle;
116use scsidisk_resources::SimpleScsiDvdHandle;
117use serial_16550_resources::ComPort;
118use serial_core::resources::DisconnectedSerialBackendHandle;
119use serial_io::SerialIo;
120use sparse_mmap::alloc_shared_memory;
121use std::cell::RefCell;
122use std::fmt::Write as _;
123use std::future::pending;
124use std::io;
125#[cfg(unix)]
126use std::io::IsTerminal;
127use std::io::Write;
128use std::net::TcpListener;
129use std::path::Path;
130use std::path::PathBuf;
131use std::pin::pin;
132use std::sync::Arc;
133use std::thread;
134use std::time::Duration;
135use std::time::Instant;
136use storvsp_resources::ScsiControllerRequest;
137use storvsp_resources::ScsiDeviceAndPath;
138use storvsp_resources::ScsiPath;
139use tpm_resources::TpmDeviceHandle;
140use tpm_resources::TpmRegisterLayout;
141use tracing_helpers::AnyhowValueExt;
142use uidevices_resources::SynthKeyboardHandle;
143use uidevices_resources::SynthMouseHandle;
144use uidevices_resources::SynthVideoHandle;
145use video_core::SharedFramebufferHandle;
146use virtio_resources::VirtioPciDeviceHandle;
147use vm_manifest_builder::BaseChipsetType;
148use vm_manifest_builder::MachineArch;
149use vm_manifest_builder::VmChipsetResult;
150use vm_manifest_builder::VmManifestBuilder;
151use vm_resource::IntoResource;
152use vm_resource::Resource;
153use vm_resource::kind::DiskHandleKind;
154use vm_resource::kind::DiskLayerHandleKind;
155use vm_resource::kind::NetEndpointHandleKind;
156use vm_resource::kind::VirtioDeviceHandle;
157use vm_resource::kind::VmbusDeviceHandleKind;
158use vmbus_serial_resources::VmbusSerialDeviceHandle;
159use vmbus_serial_resources::VmbusSerialPort;
160use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
161use vmgs_resources::GuestStateEncryptionPolicy;
162use vmgs_resources::VmgsDisk;
163use vmgs_resources::VmgsFileHandle;
164use vmgs_resources::VmgsResource;
165use vmotherboard::ChipsetDeviceHandle;
166use vnc_worker_defs::VncParameters;
167
168pub fn openvmm_main() {
169 #[cfg(unix)]
172 let orig_termios = io::stderr().is_terminal().then(term::get_termios);
173
174 let exit_code = match do_main() {
175 Ok(_) => 0,
176 Err(err) => {
177 eprintln!("fatal error: {:?}", err);
178 1
179 }
180 };
181
182 #[cfg(unix)]
184 if let Some(orig_termios) = orig_termios {
185 term::set_termios(orig_termios);
186 }
187
188 let _ = io::stdout().flush();
194 pal::process::terminate(exit_code);
195}
196
197#[derive(Default)]
198struct VmResources {
199 console_in: Option<Box<dyn AsyncWrite + Send + Unpin>>,
200 framebuffer_access: Option<FramebufferAccess>,
201 shutdown_ic: Option<mesh::Sender<hyperv_ic_resources::shutdown::ShutdownRpc>>,
202 kvp_ic: Option<mesh::Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
203 scsi_rpc: Option<mesh::Sender<ScsiControllerRequest>>,
204 nvme_vtl2_rpc: Option<mesh::Sender<NvmeControllerRequest>>,
205 ged_rpc: Option<mesh::Sender<get_resources::ged::GuestEmulationRequest>>,
206 vtl2_settings: Option<vtl2_settings_proto::Vtl2Settings>,
207 #[cfg(windows)]
208 switch_ports: Vec<vmswitch::kernel::SwitchPort>,
209}
210
211impl VmResources {
212 async fn modify_vtl2_settings(
217 &mut self,
218 f: impl FnOnce(&mut vtl2_settings_proto::Vtl2Settings),
219 ) -> anyhow::Result<()> {
220 let mut settings_copy = self
221 .vtl2_settings
222 .clone()
223 .context("vtl2 settings not configured")?;
224
225 f(&mut settings_copy);
226
227 let ged_rpc = self.ged_rpc.as_ref().context("no GED configured")?;
228
229 ged_rpc
230 .call_failable(
231 get_resources::ged::GuestEmulationRequest::ModifyVtl2Settings,
232 prost::Message::encode_to_vec(&settings_copy),
233 )
234 .await?;
235
236 self.vtl2_settings = Some(settings_copy);
238 Ok(())
239 }
240
241 async fn add_vtl0_scsi_disk(
246 &mut self,
247 controller_guid: Guid,
248 lun: u32,
249 device_type: vtl2_settings_proto::physical_device::DeviceType,
250 device_path: Guid,
251 sub_device_path: u32,
252 ) -> anyhow::Result<()> {
253 let mut not_found = false;
254 self.modify_vtl2_settings(|settings| {
255 let dynamic = settings.dynamic.get_or_insert_with(Default::default);
256
257 let scsi_controller = dynamic.storage_controllers.iter_mut().find(|c| {
259 c.instance_id == controller_guid.to_string()
260 && c.protocol
261 == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
262 });
263
264 let Some(scsi_controller) = scsi_controller else {
265 not_found = true;
266 return;
267 };
268
269 scsi_controller.luns.push(vtl2_settings_proto::Lun {
271 location: lun,
272 device_id: Guid::new_random().to_string(),
273 vendor_id: "OpenVMM".to_string(),
274 product_id: "Disk".to_string(),
275 product_revision_level: "1.0".to_string(),
276 serial_number: "0".to_string(),
277 model_number: "1".to_string(),
278 physical_devices: Some(vtl2_settings_proto::PhysicalDevices {
279 r#type: vtl2_settings_proto::physical_devices::BackingType::Single.into(),
280 device: Some(vtl2_settings_proto::PhysicalDevice {
281 device_type: device_type.into(),
282 device_path: device_path.to_string(),
283 sub_device_path,
284 }),
285 devices: Vec::new(),
286 }),
287 is_dvd: false,
288 ..Default::default()
289 });
290 })
291 .await?;
292
293 if not_found {
294 anyhow::bail!("SCSI controller {} not found", controller_guid);
295 }
296 Ok(())
297 }
298
299 async fn remove_vtl0_scsi_disk(
303 &mut self,
304 controller_guid: Guid,
305 lun: u32,
306 ) -> anyhow::Result<()> {
307 self.modify_vtl2_settings(|settings| {
308 let dynamic = settings.dynamic.as_mut();
309 if let Some(dynamic) = dynamic {
310 if let Some(scsi_controller) = dynamic.storage_controllers.iter_mut().find(|c| {
312 c.instance_id == controller_guid.to_string()
313 && c.protocol
314 == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
315 }) {
316 scsi_controller.luns.retain(|l| l.location != lun);
318 }
319 }
320 })
321 .await
322 }
323
324 async fn remove_vtl0_scsi_disk_by_nvme_nsid(
328 &mut self,
329 controller_guid: Guid,
330 nvme_controller_guid: Guid,
331 nsid: u32,
332 ) -> anyhow::Result<Option<u32>> {
333 let mut removed_lun = None;
334 self.modify_vtl2_settings(|settings| {
335 let dynamic = settings.dynamic.as_mut();
336 if let Some(dynamic) = dynamic {
337 if let Some(scsi_controller) = dynamic.storage_controllers.iter_mut().find(|c| {
339 c.instance_id == controller_guid.to_string()
340 && c.protocol
341 == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
342 }) {
343 let nvme_controller_str = nvme_controller_guid.to_string();
345 scsi_controller.luns.retain(|l| {
346 let dominated_by_nsid = l.physical_devices.as_ref().is_some_and(|pd| {
347 pd.device.as_ref().is_some_and(|d| {
348 d.device_type
349 == vtl2_settings_proto::physical_device::DeviceType::Nvme as i32
350 && d.device_path == nvme_controller_str
351 && d.sub_device_path == nsid
352 })
353 });
354 if dominated_by_nsid {
355 removed_lun = Some(l.location);
356 false } else {
358 true }
360 });
361 }
362 }
363 })
364 .await?;
365 Ok(removed_lun)
366 }
367}
368
369struct ConsoleState<'a> {
370 device: &'a str,
371 input: Box<dyn AsyncWrite + Unpin + Send>,
372}
373
374fn build_switch_list(all_switches: &[cli_args::GenericPcieSwitchCli]) -> Vec<PcieSwitchConfig> {
379 all_switches
380 .iter()
381 .map(|switch_cli| PcieSwitchConfig {
382 name: switch_cli.name.clone(),
383 num_downstream_ports: switch_cli.num_downstream_ports,
384 parent_port: switch_cli.port_name.clone(),
385 hotplug: switch_cli.hotplug,
386 })
387 .collect()
388}
389
390async fn vm_config_from_command_line(
391 spawner: impl Spawn,
392 mesh: &VmmMesh,
393 opt: &Options,
394) -> anyhow::Result<(Config, VmResources)> {
395 let (_, serial_driver) = DefaultPool::spawn_on_thread("serial");
396 serial_driver.spawn("leak", pending::<()>()).detach();
398
399 let openhcl_vtl = if opt.vtl2 {
400 DeviceVtl::Vtl2
401 } else {
402 DeviceVtl::Vtl0
403 };
404
405 let console_state: RefCell<Option<ConsoleState<'_>>> = RefCell::new(None);
406 let setup_serial = |name: &str, cli_cfg, device| -> anyhow::Result<_> {
407 Ok(match cli_cfg {
408 SerialConfigCli::Console => {
409 if let Some(console_state) = console_state.borrow().as_ref() {
410 bail!("console already set by {}", console_state.device);
411 }
412 let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
413 let (serial_read, serial_write) = AsyncReadExt::split(serial);
414 *console_state.borrow_mut() = Some(ConsoleState {
415 device,
416 input: Box::new(serial_write),
417 });
418 thread::Builder::new()
419 .name(name.to_owned())
420 .spawn(move || {
421 let _ = block_on(futures::io::copy(
422 serial_read,
423 &mut AllowStdIo::new(term::raw_stdout()),
424 ));
425 })
426 .unwrap();
427 Some(config)
428 }
429 SerialConfigCli::Stderr => {
430 let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
431 thread::Builder::new()
432 .name(name.to_owned())
433 .spawn(move || {
434 let _ = block_on(futures::io::copy(
435 serial,
436 &mut AllowStdIo::new(term::raw_stderr()),
437 ));
438 })
439 .unwrap();
440 Some(config)
441 }
442 SerialConfigCli::File(path) => {
443 let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
444 let file = fs_err::File::create(path).context("failed to create file")?;
445
446 thread::Builder::new()
447 .name(name.to_owned())
448 .spawn(move || {
449 let _ = block_on(futures::io::copy(serial, &mut AllowStdIo::new(file)));
450 })
451 .unwrap();
452 Some(config)
453 }
454 SerialConfigCli::None => None,
455 SerialConfigCli::Pipe(path) => {
456 Some(serial_io::bind_serial(&path).context("failed to bind serial")?)
457 }
458 SerialConfigCli::Tcp(addr) => {
459 Some(serial_io::bind_tcp_serial(&addr).context("failed to bind serial")?)
460 }
461 SerialConfigCli::NewConsole(app, window_title) => {
462 let path = console_relay::random_console_path();
463 let config =
464 serial_io::bind_serial(&path).context("failed to bind console serial")?;
465 let window_title =
466 window_title.unwrap_or_else(|| name.to_uppercase() + " [OpenVMM]");
467
468 console_relay::launch_console(
469 app.or_else(openvmm_terminal_app).as_deref(),
470 &path,
471 ConsoleLaunchOptions {
472 window_title: Some(window_title),
473 },
474 )
475 .context("failed to launch console")?;
476
477 Some(config)
478 }
479 })
480 };
481
482 let setup_serial_virtio = |name, cli_cfg, device| -> anyhow::Result<_> {
484 Ok(match cli_cfg {
485 SerialConfigCli::Console => {
486 if console_state.borrow().is_some() {
487 bail!("console already set");
488 }
489 let mut io = SerialIo::new().context("creating serial IO")?;
490 io.spawn_copy_out(name, term::raw_stdout());
491 *console_state.borrow_mut() = Some(ConsoleState {
492 device,
493 input: Box::new(PolledPipe::new(&serial_driver, io.input.unwrap())?),
494 });
495 Some(io.config)
496 }
497 SerialConfigCli::Stderr => {
498 let mut io = SerialIo::new().context("creating serial IO")?;
499 io.spawn_copy_out(name, term::raw_stderr());
500 io.config.input = None;
503 Some(io.config)
504 }
505 SerialConfigCli::File(path) => {
506 let mut io = SerialIo::new().context("creating serial IO")?;
507 let file = fs_err::File::create(path).context("failed to create file")?;
508 io.spawn_copy_out(name, file);
509 io.config.input = None;
512 Some(io.config)
513 }
514 SerialConfigCli::None => None,
515 SerialConfigCli::Pipe(path) => {
516 let mut io = SerialIo::new().context("creating serial IO")?;
517 io.spawn_copy_listener(serial_driver.clone(), name, &path)
518 .with_context(|| format!("listening on pipe {}", path.display()))?
519 .detach();
520 Some(io.config)
521 }
522 SerialConfigCli::Tcp(_addr) => anyhow::bail!("TCP virtio serial not supported"),
523 SerialConfigCli::NewConsole(app, window_title) => {
524 let path = console_relay::random_console_path();
525
526 let mut io = SerialIo::new().context("creating serial IO")?;
527 io.spawn_copy_listener(serial_driver.clone(), name, &path)
528 .with_context(|| format!("listening on pipe {}", path.display()))?
529 .detach();
530
531 let window_title =
532 window_title.unwrap_or_else(|| name.to_uppercase() + " [OpenVMM]");
533
534 console_relay::launch_console(
535 app.or_else(openvmm_terminal_app).as_deref(),
536 &path,
537 ConsoleLaunchOptions {
538 window_title: Some(window_title),
539 },
540 )
541 .context("failed to launch console")?;
542 Some(io.config)
543 }
544 })
545 };
546
547 let virtio_console = opt.virtio_console || opt.virtio_console_pci;
548 let mut vmbus_devices = Vec::new();
549
550 let serial0_cfg = setup_serial(
551 "com1",
552 opt.com1.clone().unwrap_or({
553 if !virtio_console {
554 SerialConfigCli::Console
555 } else {
556 SerialConfigCli::None
557 }
558 }),
559 if cfg!(guest_arch = "x86_64") {
560 "ttyS0"
561 } else {
562 "ttyAMA0"
563 },
564 )?;
565 let serial1_cfg = setup_serial(
566 "com2",
567 opt.com2.clone().unwrap_or(SerialConfigCli::None),
568 if cfg!(guest_arch = "x86_64") {
569 "ttyS1"
570 } else {
571 "ttyAMA1"
572 },
573 )?;
574 let serial2_cfg = setup_serial(
575 "com3",
576 opt.com3.clone().unwrap_or(SerialConfigCli::None),
577 if cfg!(guest_arch = "x86_64") {
578 "ttyS2"
579 } else {
580 "ttyAMA2"
581 },
582 )?;
583 let serial3_cfg = setup_serial(
584 "com4",
585 opt.com4.clone().unwrap_or(SerialConfigCli::None),
586 if cfg!(guest_arch = "x86_64") {
587 "ttyS3"
588 } else {
589 "ttyAMA3"
590 },
591 )?;
592 let virtio_serial_cfg = setup_serial_virtio(
593 "virtio_serial",
594 opt.virtio_serial.clone().unwrap_or({
595 if virtio_console {
596 SerialConfigCli::Console
597 } else {
598 SerialConfigCli::None
599 }
600 }),
601 if opt.virtio_console_pci {
602 "hvc1"
603 } else {
604 "hvc0"
605 },
606 )?;
607 let with_vmbus_com1_serial = if let Some(vmbus_com1_cfg) = setup_serial(
608 "vmbus_com1",
609 opt.vmbus_com1_serial
610 .clone()
611 .unwrap_or(SerialConfigCli::None),
612 "vmbus_com1",
613 )? {
614 vmbus_devices.push((
615 openhcl_vtl,
616 VmbusSerialDeviceHandle {
617 port: VmbusSerialPort::Com1,
618 backend: vmbus_com1_cfg,
619 }
620 .into_resource(),
621 ));
622 true
623 } else {
624 false
625 };
626 let with_vmbus_com2_serial = if let Some(vmbus_com2_cfg) = setup_serial(
627 "vmbus_com2",
628 opt.vmbus_com2_serial
629 .clone()
630 .unwrap_or(SerialConfigCli::None),
631 "vmbus_com2",
632 )? {
633 vmbus_devices.push((
634 openhcl_vtl,
635 VmbusSerialDeviceHandle {
636 port: VmbusSerialPort::Com2,
637 backend: vmbus_com2_cfg,
638 }
639 .into_resource(),
640 ));
641 true
642 } else {
643 false
644 };
645 let debugcon_cfg = setup_serial(
646 "debugcon",
647 opt.debugcon
648 .clone()
649 .map(|cfg| cfg.serial)
650 .unwrap_or(SerialConfigCli::None),
651 "debugcon",
652 )?;
653
654 let mut resources = VmResources::default();
655 let mut console_str = "";
656 if let Some(ConsoleState { device, input }) = console_state.into_inner() {
657 resources.console_in = Some(input);
658 console_str = device;
659 }
660
661 if opt.shared_memory {
662 tracing::warn!("--shared-memory/-M flag has no effect and will be removed");
663 }
664
665 const MAX_PROCESSOR_COUNT: u32 = 1024;
666
667 if opt.processors == 0 || opt.processors > MAX_PROCESSOR_COUNT {
668 bail!("invalid proc count: {}", opt.processors);
669 }
670
671 if opt.scsi_sub_channels > (MAX_PROCESSOR_COUNT - 1) as u16 {
674 bail!(
675 "invalid SCSI sub-channel count: requested {}, max {}",
676 opt.scsi_sub_channels,
677 MAX_PROCESSOR_COUNT - 1
678 );
679 }
680
681 let with_get = opt.get || (opt.vtl2 && !opt.no_get);
682
683 let mut storage = storage_builder::StorageBuilder::new(with_get.then_some(openhcl_vtl));
684 for &cli_args::DiskCli {
685 vtl,
686 ref kind,
687 read_only,
688 is_dvd,
689 underhill,
690 ref pcie_port,
691 } in &opt.disk
692 {
693 if pcie_port.is_some() {
694 anyhow::bail!("`--disk` is incompatible with PCIe");
695 }
696
697 storage.add(
698 vtl,
699 underhill,
700 storage_builder::DiskLocation::Scsi(None),
701 kind,
702 is_dvd,
703 read_only,
704 )?;
705 }
706
707 for &cli_args::IdeDiskCli {
708 ref kind,
709 read_only,
710 channel,
711 device,
712 is_dvd,
713 } in &opt.ide
714 {
715 storage.add(
716 DeviceVtl::Vtl0,
717 None,
718 storage_builder::DiskLocation::Ide(channel, device),
719 kind,
720 is_dvd,
721 read_only,
722 )?;
723 }
724
725 for &cli_args::DiskCli {
726 vtl,
727 ref kind,
728 read_only,
729 is_dvd,
730 underhill,
731 ref pcie_port,
732 } in &opt.nvme
733 {
734 storage.add(
735 vtl,
736 underhill,
737 storage_builder::DiskLocation::Nvme(None, pcie_port.clone()),
738 kind,
739 is_dvd,
740 read_only,
741 )?;
742 }
743
744 let floppy_disks: Vec<_> = opt
745 .floppy
746 .iter()
747 .map(|disk| -> anyhow::Result<_> {
748 let &cli_args::FloppyDiskCli {
749 ref kind,
750 read_only,
751 } = disk;
752 Ok(FloppyDiskConfig {
753 disk_type: disk_open(kind, read_only)?,
754 read_only,
755 })
756 })
757 .collect::<Result<Vec<_>, _>>()?;
758
759 let mut mana_nics = [(); 3].map(|()| None);
760 let mut underhill_nics = Vec::new();
761 let mut vpci_devices = Vec::new();
762
763 let mut nic_index = 0;
764 for cli_cfg in &opt.net {
765 let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
766 if cli_cfg.underhill {
767 if !opt.no_alias_map {
768 anyhow::bail!("must specify --no-alias-map to offer NICs to VTL2");
769 }
770 let mana = mana_nics[openhcl_vtl as usize].get_or_insert_with(|| {
771 let vpci_instance_id = Guid::new_random();
772 underhill_nics.push(vtl2_settings_proto::NicDeviceLegacy {
773 instance_id: vpci_instance_id.to_string(),
774 subordinate_instance_id: None,
775 max_sub_channels: None,
776 });
777 (vpci_instance_id, GdmaDeviceHandle { vports: Vec::new() })
778 });
779 mana.1.vports.push(VportDefinition {
780 mac_address: vport.mac_address,
781 endpoint: vport.endpoint,
782 });
783 } else {
784 vmbus_devices.push(vport.into_netvsp_handle());
785 }
786 }
787
788 if opt.nic {
789 let nic_config = parse_endpoint(
790 &NicConfigCli {
791 vtl: DeviceVtl::Vtl0,
792 endpoint: EndpointConfigCli::Consomme { cidr: None },
793 max_queues: None,
794 underhill: false,
795 },
796 &mut nic_index,
797 &mut resources,
798 )?;
799 vmbus_devices.push(nic_config.into_netvsp_handle());
800 }
801
802 if opt.mcr {
803 tracing::info!("Instantiating MCR controller");
804
805 const MCR_INSTANCE_ID: Guid = guid::guid!("07effd8f-7501-426c-a947-d8345f39113d");
807
808 vpci_devices.push(VpciDeviceConfig {
809 vtl: DeviceVtl::Vtl0,
810 instance_id: MCR_INSTANCE_ID,
811 resource: mcr_resources::McrControllerHandle {
812 instance_id: MCR_INSTANCE_ID,
813 }
814 .into_resource(),
815 });
816 }
817
818 let mut pcie_devices = Vec::new();
821 for (index, cli_cfg) in opt.pcie_remote.iter().enumerate() {
822 tracing::info!(
823 port_name = %cli_cfg.port_name,
824 socket_path = ?cli_cfg.socket_path,
825 "instantiating PCIe remote device"
826 );
827
828 const PCIE_REMOTE_BASE_INSTANCE_ID: Guid =
830 guid::guid!("28ed784d-c059-429f-9d9a-46bea02562c0");
831 let instance_id = Guid {
832 data1: index as u32,
833 ..PCIE_REMOTE_BASE_INSTANCE_ID
834 };
835
836 pcie_devices.push(PcieDeviceConfig {
837 port_name: cli_cfg.port_name.clone(),
838 resource: pcie_remote_resources::PcieRemoteHandle {
839 instance_id,
840 socket_path: cli_cfg.socket_path.clone(),
841 hu: cli_cfg.hu,
842 controller: cli_cfg.controller,
843 }
844 .into_resource(),
845 });
846 }
847
848 #[cfg(windows)]
849 let mut kernel_vmnics = Vec::new();
850 #[cfg(windows)]
851 for (index, switch_id) in opt.kernel_vmnic.iter().enumerate() {
852 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
854 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
855
856 const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-435d-11ee-9f59-00155d5016fc");
858 let instance_id = Guid {
859 data1: index as u32,
860 ..BASE_INSTANCE_ID
861 };
862
863 let switch_id = if switch_id == "default" {
864 DEFAULT_SWITCH
865 } else {
866 switch_id
867 };
868 let (port_id, port) = new_switch_port(switch_id)?;
869 resources.switch_ports.push(port);
870
871 kernel_vmnics.push(openvmm_defs::config::KernelVmNicConfig {
872 instance_id,
873 mac_address: mac_address.into(),
874 switch_port_id: port_id,
875 });
876 }
877
878 for vport in &opt.mana {
879 let vport = parse_endpoint(vport, &mut nic_index, &mut resources)?;
880 mana_nics[vport.vtl as usize]
881 .get_or_insert_with(|| (Guid::new_random(), GdmaDeviceHandle { vports: Vec::new() }))
882 .1
883 .vports
884 .push(VportDefinition {
885 mac_address: vport.mac_address,
886 endpoint: vport.endpoint,
887 });
888 }
889
890 vpci_devices.extend(mana_nics.into_iter().enumerate().filter_map(|(vtl, nic)| {
891 nic.map(|(instance_id, handle)| VpciDeviceConfig {
892 vtl: match vtl {
893 0 => DeviceVtl::Vtl0,
894 1 => DeviceVtl::Vtl1,
895 2 => DeviceVtl::Vtl2,
896 _ => unreachable!(),
897 },
898 instance_id,
899 resource: handle.into_resource(),
900 })
901 }));
902
903 let pcie_switches = build_switch_list(&opt.pcie_switch);
904
905 let pcie_root_complexes = opt
906 .pcie_root_complex
907 .iter()
908 .enumerate()
909 .map(|(i, cli)| {
910 let ports = opt
911 .pcie_root_port
912 .iter()
913 .filter(|port_cli| port_cli.root_complex_name == cli.name)
914 .map(|port_cli| PcieRootPortConfig {
915 name: port_cli.name.clone(),
916 hotplug: port_cli.hotplug,
917 })
918 .collect();
919
920 PcieRootComplexConfig {
921 index: i as u32,
922 name: cli.name.clone(),
923 segment: cli.segment,
924 start_bus: cli.start_bus,
925 end_bus: cli.end_bus,
926 low_mmio_size: cli.low_mmio,
927 high_mmio_size: cli.high_mmio,
928 ports,
929 }
930 })
931 .collect();
932
933 #[cfg(windows)]
934 let vpci_resources: Vec<_> = opt
935 .device
936 .iter()
937 .map(|path| -> anyhow::Result<_> {
938 Ok(virt_whp::device::DeviceHandle(
939 whp::VpciResource::new(
940 None,
941 Default::default(),
942 &whp::VpciResourceDescriptor::Sriov(path, 0, 0),
943 )
944 .with_context(|| format!("opening PCI device {}", path))?,
945 ))
946 })
947 .collect::<Result<_, _>>()?;
948
949 #[cfg(windows)]
951 let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
952 Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
953 } else {
954 None
955 };
956
957 let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc || opt.pcat {
958 let vram = alloc_shared_memory(FRAMEBUFFER_SIZE)?;
959 let (fb, fba) =
960 framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
961 resources.framebuffer_access = Some(fba);
962 Some(fb)
963 } else {
964 None
965 };
966
967 let is_arm = cfg!(guest_arch = "aarch64");
968 let is_x86 = cfg!(guest_arch = "x86_64");
969
970 let load_mode;
971 let with_hv;
972
973 let any_serial_configured = serial0_cfg.is_some()
974 || serial1_cfg.is_some()
975 || serial2_cfg.is_some()
976 || serial3_cfg.is_some();
977
978 let has_com3 = serial2_cfg.is_some();
979
980 let mut chipset = VmManifestBuilder::new(
981 if opt.igvm.is_some() {
982 BaseChipsetType::HclHost
983 } else if opt.pcat {
984 BaseChipsetType::HypervGen1
985 } else if opt.uefi {
986 BaseChipsetType::HypervGen2Uefi
987 } else if opt.hv {
988 BaseChipsetType::HyperVGen2LinuxDirect
989 } else {
990 BaseChipsetType::UnenlightenedLinuxDirect
991 },
992 if is_x86 {
993 MachineArch::X86_64
994 } else {
995 MachineArch::Aarch64
996 },
997 );
998
999 if framebuffer.is_some() {
1000 chipset = chipset.with_framebuffer();
1001 }
1002 if opt.guest_watchdog {
1003 chipset = chipset.with_guest_watchdog();
1004 }
1005 if any_serial_configured {
1006 chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
1007 }
1008 if opt.battery {
1009 let (tx, rx) = mesh::channel();
1010 tx.send(HostBatteryUpdate::default_present());
1011 chipset = chipset.with_battery(rx);
1012 }
1013 if let Some(cfg) = &opt.debugcon {
1014 chipset = chipset.with_debugcon(
1015 debugcon_cfg.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
1016 cfg.port,
1017 );
1018 }
1019
1020 let bios_guid = Guid::new_random();
1022
1023 let VmChipsetResult {
1024 chipset,
1025 mut chipset_devices,
1026 } = chipset
1027 .build()
1028 .context("failed to build chipset configuration")?;
1029
1030 if let Some(path) = &opt.igvm {
1031 let file = fs_err::File::open(path)
1032 .context("failed to open igvm file")?
1033 .into();
1034 let cmdline = opt.cmdline.join(" ");
1035 with_hv = true;
1036
1037 load_mode = LoadMode::Igvm {
1038 file,
1039 cmdline,
1040 vtl2_base_address: opt.igvm_vtl2_relocation_type,
1041 com_serial: has_com3.then(|| SerialInformation {
1042 io_port: ComPort::Com3.io_port(),
1043 irq: ComPort::Com3.irq().into(),
1044 }),
1045 };
1046 } else if opt.pcat {
1047 if !is_x86 {
1049 anyhow::bail!("pcat not supported on this architecture");
1050 }
1051 with_hv = true;
1052
1053 let firmware = openvmm_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
1054 load_mode = LoadMode::Pcat {
1055 firmware,
1056 boot_order: opt
1057 .pcat_boot_order
1058 .map(|x| x.0)
1059 .unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
1060 };
1061 } else if opt.uefi {
1062 use openvmm_defs::config::UefiConsoleMode;
1063
1064 with_hv = true;
1065
1066 let firmware = fs_err::File::open(
1067 (opt.uefi_firmware.0)
1068 .as_ref()
1069 .context("must provide uefi firmware when booting with uefi")?,
1070 )
1071 .context("failed to open uefi firmware")?;
1072
1073 load_mode = LoadMode::Uefi {
1076 firmware: firmware.into(),
1077 enable_debugging: opt.uefi_debug,
1078 enable_memory_protections: opt.uefi_enable_memory_protections,
1079 disable_frontpage: opt.disable_frontpage,
1080 enable_tpm: opt.tpm,
1081 enable_battery: opt.battery,
1082 enable_serial: any_serial_configured,
1083 enable_vpci_boot: false,
1084 uefi_console_mode: opt.uefi_console_mode.map(|m| match m {
1085 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1086 UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
1087 UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
1088 UefiConsoleModeCli::None => UefiConsoleMode::None,
1089 }),
1090 default_boot_always_attempt: opt.default_boot_always_attempt,
1091 bios_guid,
1092 azi_hsm_enabled: opt.azi_hsm_enabled,
1093 };
1094 } else {
1095 let mut cmdline = "panic=-1 debug".to_string();
1097
1098 with_hv = opt.hv;
1099 if with_hv {
1100 cmdline += " pci=off";
1101 }
1102
1103 if !console_str.is_empty() {
1104 let _ = write!(&mut cmdline, " console={}", console_str);
1105 }
1106 if opt.gfx {
1107 cmdline += " console=tty";
1108 }
1109 for extra in &opt.cmdline {
1110 let _ = write!(&mut cmdline, " {}", extra);
1111 }
1112
1113 let kernel = fs_err::File::open(
1114 (opt.kernel.0)
1115 .as_ref()
1116 .context("must provide kernel when booting with linux direct")?,
1117 )
1118 .context("failed to open kernel")?;
1119 let initrd = (opt.initrd.0)
1120 .as_ref()
1121 .map(fs_err::File::open)
1122 .transpose()
1123 .context("failed to open initrd")?;
1124
1125 let custom_dsdt = match &opt.custom_dsdt {
1126 Some(path) => {
1127 let mut v = Vec::new();
1128 fs_err::File::open(path)
1129 .context("failed to open custom dsdt")?
1130 .read_to_end(&mut v)
1131 .context("failed to read custom dsdt")?;
1132 Some(v)
1133 }
1134 None => None,
1135 };
1136
1137 load_mode = LoadMode::Linux {
1138 kernel: kernel.into(),
1139 initrd: initrd.map(Into::into),
1140 cmdline,
1141 custom_dsdt,
1142 enable_serial: any_serial_configured,
1143 };
1144 }
1145
1146 let mut vmgs = Some(if let Some(VmgsCli { kind, provision }) = &opt.vmgs {
1147 let disk = VmgsDisk {
1148 disk: disk_open(kind, false).context("failed to open vmgs disk")?,
1149 encryption_policy: if opt.test_gsp_by_id {
1150 GuestStateEncryptionPolicy::GspById(true)
1151 } else {
1152 GuestStateEncryptionPolicy::None(true)
1153 },
1154 };
1155 match provision {
1156 ProvisionVmgs::OnEmpty => VmgsResource::Disk(disk),
1157 ProvisionVmgs::OnFailure => VmgsResource::ReprovisionOnFailure(disk),
1158 ProvisionVmgs::True => VmgsResource::Reprovision(disk),
1159 }
1160 } else {
1161 VmgsResource::Ephemeral
1162 });
1163
1164 if with_get && with_hv {
1165 let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
1166 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
1167 fixed: Some(Default::default()),
1168 dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
1169 storage_controllers: storage.build_underhill(),
1170 nic_devices: underhill_nics,
1171 }),
1172 namespace_settings: Vec::default(),
1173 };
1174
1175 resources.vtl2_settings = Some(vtl2_settings.clone());
1177
1178 let (send, guest_request_recv) = mesh::channel();
1179 resources.ged_rpc = Some(send);
1180
1181 let vmgs = vmgs.take().unwrap();
1182
1183 vmbus_devices.extend([
1184 (
1185 openhcl_vtl,
1186 get_resources::gel::GuestEmulationLogHandle.into_resource(),
1187 ),
1188 (
1189 openhcl_vtl,
1190 get_resources::ged::GuestEmulationDeviceHandle {
1191 firmware: if opt.pcat {
1192 get_resources::ged::GuestFirmwareConfig::Pcat {
1193 boot_order: opt
1194 .pcat_boot_order
1195 .map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
1196 .map(|x| match x {
1197 openvmm_defs::config::PcatBootDevice::Floppy => {
1198 get_resources::ged::PcatBootDevice::Floppy
1199 }
1200 openvmm_defs::config::PcatBootDevice::HardDrive => {
1201 get_resources::ged::PcatBootDevice::HardDrive
1202 }
1203 openvmm_defs::config::PcatBootDevice::Optical => {
1204 get_resources::ged::PcatBootDevice::Optical
1205 }
1206 openvmm_defs::config::PcatBootDevice::Network => {
1207 get_resources::ged::PcatBootDevice::Network
1208 }
1209 }),
1210 }
1211 } else {
1212 use get_resources::ged::UefiConsoleMode;
1213
1214 get_resources::ged::GuestFirmwareConfig::Uefi {
1215 enable_vpci_boot: storage.has_vtl0_nvme(),
1216 firmware_debug: opt.uefi_debug,
1217 disable_frontpage: opt.disable_frontpage,
1218 console_mode: match opt.uefi_console_mode.unwrap_or(UefiConsoleModeCli::Default) {
1219 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1220 UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
1221 UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
1222 UefiConsoleModeCli::None => UefiConsoleMode::None,
1223 },
1224 default_boot_always_attempt: opt.default_boot_always_attempt,
1225 }
1226 },
1227 com1: with_vmbus_com1_serial,
1228 com2: with_vmbus_com2_serial,
1229 serial_tx_only: opt.serial_tx_only,
1230 vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
1231 vmbus_redirection: opt.vmbus_redirect,
1232 vmgs,
1233 framebuffer: opt
1234 .vtl2_gfx
1235 .then(|| SharedFramebufferHandle.into_resource()),
1236 guest_request_recv,
1237 enable_tpm: opt.tpm,
1238 firmware_event_send: None,
1239 secure_boot_enabled: opt.secure_boot,
1240 secure_boot_template: match opt.secure_boot_template {
1241 Some(SecureBootTemplateCli::Windows) => {
1242 get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1243 },
1244 Some(SecureBootTemplateCli::UefiCa) => {
1245 get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1246 }
1247 None => {
1248 get_resources::ged::GuestSecureBootTemplateType::None
1249 },
1250 },
1251 enable_battery: opt.battery,
1252 no_persistent_secrets: true,
1253 igvm_attest_test_config: None,
1254 test_gsp_by_id: opt.test_gsp_by_id,
1255 efi_diagnostics_log_level: {
1256 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1257 EfiDiagnosticsLogLevelCli::Default => get_resources::ged::EfiDiagnosticsLogLevelType::Default,
1258 EfiDiagnosticsLogLevelCli::Info => get_resources::ged::EfiDiagnosticsLogLevelType::Info,
1259 EfiDiagnosticsLogLevelCli::Full => get_resources::ged::EfiDiagnosticsLogLevelType::Full,
1260 }
1261 },
1262 hv_sint_enabled: false,
1263 azi_hsm_enabled: opt.azi_hsm_enabled,
1264 }
1265 .into_resource(),
1266 ),
1267 ]);
1268 }
1269
1270 if opt.tpm && !opt.vtl2 {
1271 let register_layout = if cfg!(guest_arch = "x86_64") {
1272 TpmRegisterLayout::IoPort
1273 } else {
1274 TpmRegisterLayout::Mmio
1275 };
1276
1277 let (ppi_store, nvram_store) = if opt.vmgs.is_some() {
1278 (
1279 VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1280 VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1281 )
1282 } else {
1283 (
1284 EphemeralNonVolatileStoreHandle.into_resource(),
1285 EphemeralNonVolatileStoreHandle.into_resource(),
1286 )
1287 };
1288
1289 chipset_devices.push(ChipsetDeviceHandle {
1290 name: "tpm".to_string(),
1291 resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1292 device: TpmDeviceHandle {
1293 ppi_store,
1294 nvram_store,
1295 refresh_tpm_seeds: false,
1296 ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1297 register_layout,
1298 guest_secret_key: None,
1299 logger: None,
1300 is_confidential_vm: false,
1301 bios_guid,
1302 }
1303 .into_resource(),
1304 worker_host: mesh.make_host("tpm", None).await?,
1305 }
1306 .into_resource(),
1307 });
1308 }
1309
1310 let custom_uefi_vars = {
1311 use firmware_uefi_custom_vars::CustomVars;
1312
1313 let base_vars = match opt.secure_boot_template {
1316 Some(template) => {
1317 if is_x86 {
1318 match template {
1319 SecureBootTemplateCli::Windows => {
1320 hyperv_secure_boot_templates::x64::microsoft_windows()
1321 }
1322 SecureBootTemplateCli::UefiCa => {
1323 hyperv_secure_boot_templates::x64::microsoft_uefi_ca()
1324 }
1325 }
1326 } else if is_arm {
1327 match template {
1328 SecureBootTemplateCli::Windows => {
1329 hyperv_secure_boot_templates::aarch64::microsoft_windows()
1330 }
1331 SecureBootTemplateCli::UefiCa => {
1332 hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca()
1333 }
1334 }
1335 } else {
1336 anyhow::bail!("no secure boot template for current guest_arch")
1337 }
1338 }
1339 None => CustomVars::default(),
1340 };
1341
1342 let custom_uefi_json_data = match &opt.custom_uefi_json {
1345 Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?),
1346 None => None,
1347 };
1348
1349 match custom_uefi_json_data {
1351 Some(data) => {
1352 let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?;
1353 base_vars.apply_delta(delta)?
1354 }
1355 None => base_vars,
1356 }
1357 };
1358
1359 let vga_firmware = if opt.pcat {
1360 Some(openvmm_pcat_locator::find_svga_bios(
1361 opt.vga_firmware.as_deref(),
1362 )?)
1363 } else {
1364 None
1365 };
1366
1367 if opt.gfx {
1368 vmbus_devices.extend([
1369 (
1370 DeviceVtl::Vtl0,
1371 SynthVideoHandle {
1372 framebuffer: SharedFramebufferHandle.into_resource(),
1373 }
1374 .into_resource(),
1375 ),
1376 (
1377 DeviceVtl::Vtl0,
1378 SynthKeyboardHandle {
1379 source: MultiplexedInputHandle {
1380 elevation: 1,
1382 }
1383 .into_resource(),
1384 }
1385 .into_resource(),
1386 ),
1387 (
1388 DeviceVtl::Vtl0,
1389 SynthMouseHandle {
1390 source: MultiplexedInputHandle {
1391 elevation: 1,
1393 }
1394 .into_resource(),
1395 }
1396 .into_resource(),
1397 ),
1398 ]);
1399 }
1400
1401 let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
1402 if let Some(path) = path {
1403 cleanup_socket(path.as_ref());
1404 let listener = unix_socket::UnixListener::bind(path)
1405 .with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
1406 Ok(Some(listener))
1407 } else {
1408 Ok(None)
1409 }
1410 };
1411
1412 let vtl0_vsock_listener = vsock_listener(opt.vsock_path.as_deref())?;
1413 let vtl2_vsock_listener = vsock_listener(opt.vtl2_vsock_path.as_deref())?;
1414
1415 let mmio_gaps = if opt.vtl2
1418 && !matches!(
1419 opt.igvm_vtl2_relocation_type,
1420 Vtl2BaseAddressType::Vtl2Allocate { .. },
1421 ) {
1422 if is_x86 {
1423 DEFAULT_MMIO_GAPS_X86_WITH_VTL2.into()
1424 } else {
1425 DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2.into()
1426 }
1427 } else if is_x86 {
1428 DEFAULT_MMIO_GAPS_X86.into()
1429 } else {
1430 DEFAULT_MMIO_GAPS_AARCH64.into()
1431 };
1432
1433 if let Some(path) = &opt.openhcl_dump_path {
1434 let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
1435 task.detach();
1436 vmbus_devices.push((openhcl_vtl, resource));
1437 }
1438
1439 #[cfg(guest_arch = "aarch64")]
1440 let topology_arch = openvmm_defs::config::ArchTopologyConfig::Aarch64(
1441 openvmm_defs::config::Aarch64TopologyConfig {
1442 gic_config: None,
1444 pmu_gsiv: openvmm_defs::config::PmuGsivConfig::Platform,
1445 },
1446 );
1447 #[cfg(guest_arch = "x86_64")]
1448 let topology_arch =
1449 openvmm_defs::config::ArchTopologyConfig::X86(openvmm_defs::config::X86TopologyConfig {
1450 apic_id_offset: opt.apic_id_offset,
1451 x2apic: opt.x2apic,
1452 });
1453
1454 let with_isolation = if let Some(isolation) = &opt.isolation {
1455 if !opt.vtl2 {
1457 anyhow::bail!("isolation is only currently supported with vtl2");
1458 }
1459
1460 if !opt.no_alias_map {
1462 anyhow::bail!("alias map not supported with isolation");
1463 }
1464
1465 match isolation {
1466 cli_args::IsolationCli::Vbs => Some(openvmm_defs::config::IsolationType::Vbs),
1467 }
1468 } else {
1469 None
1470 };
1471
1472 if with_hv {
1473 let (shutdown_send, shutdown_recv) = mesh::channel();
1474 resources.shutdown_ic = Some(shutdown_send);
1475 let (kvp_send, kvp_recv) = mesh::channel();
1476 resources.kvp_ic = Some(kvp_send);
1477 vmbus_devices.extend(
1478 [
1479 hyperv_ic_resources::shutdown::ShutdownIcHandle {
1480 recv: shutdown_recv,
1481 }
1482 .into_resource(),
1483 hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_recv }.into_resource(),
1484 hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
1485 ]
1486 .map(|r| (DeviceVtl::Vtl0, r)),
1487 );
1488 }
1489
1490 if let Some(hive_path) = &opt.imc {
1491 let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
1492 vmbus_devices.push((
1493 DeviceVtl::Vtl0,
1494 vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
1495 ));
1496 }
1497
1498 let mut virtio_devices = Vec::new();
1499 let mut add_virtio_device = |bus, resource: Resource<VirtioDeviceHandle>| {
1500 let bus = match bus {
1501 VirtioBusCli::Auto => {
1502 if with_hv && (cfg!(windows) || cfg!(target_os = "macos")) {
1505 None
1506 } else {
1507 Some(VirtioBus::Pci)
1508 }
1509 }
1510 VirtioBusCli::Mmio => Some(VirtioBus::Mmio),
1511 VirtioBusCli::Pci => Some(VirtioBus::Pci),
1512 VirtioBusCli::Vpci => None,
1513 };
1514 if let Some(bus) = bus {
1515 virtio_devices.push((bus, resource));
1516 } else {
1517 vpci_devices.push(VpciDeviceConfig {
1518 vtl: DeviceVtl::Vtl0,
1519 instance_id: Guid::new_random(),
1520 resource: VirtioPciDeviceHandle(resource).into_resource(),
1521 });
1522 }
1523 };
1524
1525 for cli_cfg in &opt.virtio_net {
1526 if cli_cfg.underhill {
1527 anyhow::bail!("use --net uh:[...] to add underhill NICs")
1528 }
1529 let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
1530 add_virtio_device(
1531 VirtioBusCli::Auto,
1532 virtio_resources::net::VirtioNetHandle {
1533 max_queues: vport.max_queues,
1534 mac_address: vport.mac_address,
1535 endpoint: vport.endpoint,
1536 }
1537 .into_resource(),
1538 );
1539 }
1540
1541 for args in &opt.virtio_fs {
1542 add_virtio_device(
1543 opt.virtio_fs_bus,
1544 virtio_resources::fs::VirtioFsHandle {
1545 tag: args.tag.clone(),
1546 fs: virtio_resources::fs::VirtioFsBackend::HostFs {
1547 root_path: args.path.clone(),
1548 mount_options: args.options.clone(),
1549 },
1550 }
1551 .into_resource(),
1552 );
1553 }
1554
1555 for args in &opt.virtio_fs_shmem {
1556 add_virtio_device(
1557 opt.virtio_fs_bus,
1558 virtio_resources::fs::VirtioFsHandle {
1559 tag: args.tag.clone(),
1560 fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
1561 root_path: args.path.clone(),
1562 },
1563 }
1564 .into_resource(),
1565 );
1566 }
1567
1568 for args in &opt.virtio_9p {
1569 add_virtio_device(
1570 VirtioBusCli::Auto,
1571 virtio_resources::p9::VirtioPlan9Handle {
1572 tag: args.tag.clone(),
1573 root_path: args.path.clone(),
1574 debug: opt.virtio_9p_debug,
1575 }
1576 .into_resource(),
1577 );
1578 }
1579
1580 if let Some(path) = &opt.virtio_pmem {
1581 add_virtio_device(
1582 VirtioBusCli::Auto,
1583 virtio_resources::pmem::VirtioPmemHandle { path: path.clone() }.into_resource(),
1584 );
1585 }
1586
1587 let mut cfg = Config {
1588 chipset,
1589 load_mode,
1590 floppy_disks,
1591 pcie_root_complexes,
1592 pcie_devices,
1593 pcie_switches,
1594 vpci_devices,
1595 ide_disks: Vec::new(),
1596 memory: MemoryConfig {
1597 mem_size: opt.memory,
1598 mmio_gaps,
1599 prefetch_memory: opt.prefetch,
1600 pcie_ecam_base: DEFAULT_PCIE_ECAM_BASE,
1601 },
1602 processor_topology: ProcessorTopologyConfig {
1603 proc_count: opt.processors,
1604 vps_per_socket: opt.vps_per_socket,
1605 enable_smt: match opt.smt {
1606 cli_args::SmtConfigCli::Auto => None,
1607 cli_args::SmtConfigCli::Force => Some(true),
1608 cli_args::SmtConfigCli::Off => Some(false),
1609 },
1610 arch: Some(topology_arch),
1611 },
1612 hypervisor: HypervisorConfig {
1613 with_hv,
1614 with_vtl2: opt.vtl2.then_some(Vtl2Config {
1615 vtl0_alias_map: !opt.no_alias_map,
1616 late_map_vtl0_memory: match opt.late_map_vtl0_policy {
1617 cli_args::Vtl0LateMapPolicyCli::Off => None,
1618 cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
1619 cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
1620 cli_args::Vtl0LateMapPolicyCli::Exception => {
1621 Some(LateMapVtl0MemoryPolicy::InjectException)
1622 }
1623 },
1624 }),
1625 with_isolation,
1626 user_mode_hv_enlightenments: opt.no_enlightenments,
1627 user_mode_apic: opt.user_mode_apic,
1628 },
1629 #[cfg(windows)]
1630 kernel_vmnics,
1631 input: mesh::Receiver::new(),
1632 framebuffer,
1633 vga_firmware,
1634 vtl2_gfx: opt.vtl2_gfx,
1635 virtio_console_pci: opt.virtio_console_pci,
1636 virtio_serial: virtio_serial_cfg,
1637 virtio_devices,
1638 vmbus: with_hv.then_some(VmbusConfig {
1639 vsock_listener: vtl0_vsock_listener,
1640 vsock_path: opt.vsock_path.clone(),
1641 vtl2_redirect: opt.vmbus_redirect,
1642 vmbus_max_version: opt.vmbus_max_version,
1643 #[cfg(windows)]
1644 vmbusproxy_handle,
1645 }),
1646 vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
1647 vsock_listener: vtl2_vsock_listener,
1648 vsock_path: opt.vtl2_vsock_path.clone(),
1649 ..Default::default()
1650 }),
1651 vmbus_devices,
1652 chipset_devices,
1653 #[cfg(windows)]
1654 vpci_resources,
1655 vmgs,
1656 secure_boot_enabled: opt.secure_boot,
1657 custom_uefi_vars,
1658 firmware_event_send: None,
1659 debugger_rpc: None,
1660 generation_id_recv: None,
1661 rtc_delta_milliseconds: 0,
1662 automatic_guest_reset: !opt.halt_on_reset,
1663 efi_diagnostics_log_level: {
1664 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1665 EfiDiagnosticsLogLevelCli::Default => EfiDiagnosticsLogLevelType::Default,
1666 EfiDiagnosticsLogLevelCli::Info => EfiDiagnosticsLogLevelType::Info,
1667 EfiDiagnosticsLogLevelCli::Full => EfiDiagnosticsLogLevelType::Full,
1668 }
1669 },
1670 };
1671
1672 storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
1673 Ok((cfg, resources))
1674}
1675
1676fn openvmm_terminal_app() -> Option<PathBuf> {
1678 std::env::var_os("OPENVMM_TERM")
1679 .or_else(|| std::env::var_os("HVLITE_TERM"))
1680 .map(Into::into)
1681}
1682
1683fn cleanup_socket(path: &Path) {
1685 #[cfg(windows)]
1686 let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
1687 #[cfg(not(windows))]
1688 let is_socket = path
1689 .metadata()
1690 .is_ok_and(|meta| std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type()));
1691
1692 if is_socket {
1693 let _ = std::fs::remove_file(path);
1694 }
1695}
1696
1697#[cfg(windows)]
1698const DEFAULT_SWITCH: &str = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444";
1699
1700#[cfg(windows)]
1701fn new_switch_port(
1702 switch_id: &str,
1703) -> anyhow::Result<(
1704 openvmm_defs::config::SwitchPortId,
1705 vmswitch::kernel::SwitchPort,
1706)> {
1707 let id = vmswitch::kernel::SwitchPortId {
1708 switch: switch_id.parse().context("invalid switch id")?,
1709 port: Guid::new_random(),
1710 };
1711 let _ = vmswitch::hcn::Network::open(&id.switch)
1712 .with_context(|| format!("could not find switch {}", id.switch))?;
1713
1714 let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
1715
1716 let id = openvmm_defs::config::SwitchPortId {
1717 switch: id.switch,
1718 port: id.port,
1719 };
1720 Ok((id, port))
1721}
1722
1723fn parse_endpoint(
1724 cli_cfg: &NicConfigCli,
1725 index: &mut usize,
1726 resources: &mut VmResources,
1727) -> anyhow::Result<NicConfig> {
1728 let _ = resources;
1729 let endpoint = match &cli_cfg.endpoint {
1730 EndpointConfigCli::Consomme { cidr } => {
1731 net_backend_resources::consomme::ConsommeHandle { cidr: cidr.clone() }.into_resource()
1732 }
1733 EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
1734 EndpointConfigCli::Dio { id } => {
1735 #[cfg(windows)]
1736 {
1737 let (port_id, port) = new_switch_port(id.as_deref().unwrap_or(DEFAULT_SWITCH))?;
1738 resources.switch_ports.push(port);
1739 net_backend_resources::dio::WindowsDirectIoHandle {
1740 switch_port_id: net_backend_resources::dio::SwitchPortId {
1741 switch: port_id.switch,
1742 port: port_id.port,
1743 },
1744 }
1745 .into_resource()
1746 }
1747
1748 #[cfg(not(windows))]
1749 {
1750 let _ = id;
1751 bail!("cannot use dio on non-windows platforms")
1752 }
1753 }
1754 EndpointConfigCli::Tap { name } => {
1755 net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
1756 }
1757 };
1758
1759 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
1761 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
1762
1763 const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-da43-11ed-936a-00155d6db52f");
1765 let instance_id = Guid {
1766 data1: *index as u32,
1767 ..BASE_INSTANCE_ID
1768 };
1769 *index += 1;
1770
1771 Ok(NicConfig {
1772 vtl: cli_cfg.vtl,
1773 instance_id,
1774 endpoint,
1775 mac_address: mac_address.into(),
1776 max_queues: cli_cfg.max_queues,
1777 })
1778}
1779
1780#[derive(Debug)]
1781struct NicConfig {
1782 vtl: DeviceVtl,
1783 instance_id: Guid,
1784 mac_address: MacAddress,
1785 endpoint: Resource<NetEndpointHandleKind>,
1786 max_queues: Option<u16>,
1787}
1788
1789impl NicConfig {
1790 fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
1791 (
1792 self.vtl,
1793 netvsp_resources::NetvspHandle {
1794 instance_id: self.instance_id,
1795 mac_address: self.mac_address,
1796 endpoint: self.endpoint,
1797 max_queues: self.max_queues,
1798 }
1799 .into_resource(),
1800 )
1801 }
1802}
1803
1804enum LayerOrDisk {
1805 Layer(DiskLayerDescription),
1806 Disk(Resource<DiskHandleKind>),
1807}
1808
1809fn disk_open(disk_cli: &DiskCliKind, read_only: bool) -> anyhow::Result<Resource<DiskHandleKind>> {
1810 let mut layers = Vec::new();
1811 disk_open_inner(disk_cli, read_only, &mut layers)?;
1812 if layers.len() == 1 && matches!(layers[0], LayerOrDisk::Disk(_)) {
1813 let LayerOrDisk::Disk(disk) = layers.pop().unwrap() else {
1814 unreachable!()
1815 };
1816 Ok(disk)
1817 } else {
1818 Ok(Resource::new(disk_backend_resources::LayeredDiskHandle {
1819 layers: layers
1820 .into_iter()
1821 .map(|layer| match layer {
1822 LayerOrDisk::Layer(layer) => layer,
1823 LayerOrDisk::Disk(disk) => DiskLayerDescription {
1824 layer: DiskLayerHandle(disk).into_resource(),
1825 read_cache: false,
1826 write_through: false,
1827 },
1828 })
1829 .collect(),
1830 }))
1831 }
1832}
1833
1834fn disk_open_inner(
1835 disk_cli: &DiskCliKind,
1836 read_only: bool,
1837 layers: &mut Vec<LayerOrDisk>,
1838) -> anyhow::Result<()> {
1839 fn layer<T: IntoResource<DiskLayerHandleKind>>(layer: T) -> LayerOrDisk {
1840 LayerOrDisk::Layer(layer.into_resource().into())
1841 }
1842 fn disk<T: IntoResource<DiskHandleKind>>(disk: T) -> LayerOrDisk {
1843 LayerOrDisk::Disk(disk.into_resource())
1844 }
1845 match disk_cli {
1846 &DiskCliKind::Memory(len) => {
1847 layers.push(layer(RamDiskLayerHandle { len: Some(len) }));
1848 }
1849 DiskCliKind::File {
1850 path,
1851 create_with_len,
1852 } => layers.push(LayerOrDisk::Disk(if let Some(size) = create_with_len {
1853 create_disk_type(path, *size)
1854 .with_context(|| format!("failed to create {}", path.display()))?
1855 } else {
1856 open_disk_type(path, read_only)
1857 .with_context(|| format!("failed to open {}", path.display()))?
1858 })),
1859 DiskCliKind::Blob { kind, url } => {
1860 layers.push(disk(disk_backend_resources::BlobDiskHandle {
1861 url: url.to_owned(),
1862 format: match kind {
1863 cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
1864 cli_args::BlobKind::Vhd1 => disk_backend_resources::BlobDiskFormat::FixedVhd1,
1865 },
1866 }))
1867 }
1868 DiskCliKind::MemoryDiff(inner) => {
1869 layers.push(layer(RamDiskLayerHandle { len: None }));
1870 disk_open_inner(inner, true, layers)?;
1871 }
1872 DiskCliKind::PersistentReservationsWrapper(inner) => layers.push(disk(
1873 disk_backend_resources::DiskWithReservationsHandle(disk_open(inner, read_only)?),
1874 )),
1875 DiskCliKind::DelayDiskWrapper {
1876 delay_ms,
1877 disk: inner,
1878 } => layers.push(disk(DelayDiskHandle {
1879 delay: CellUpdater::new(Duration::from_millis(*delay_ms)).cell(),
1880 disk: disk_open(inner, read_only)?,
1881 })),
1882 DiskCliKind::Crypt {
1883 disk: inner,
1884 cipher,
1885 key_file,
1886 } => layers.push(disk(disk_crypt_resources::DiskCryptHandle {
1887 disk: disk_open(inner, read_only)?,
1888 cipher: match cipher {
1889 cli_args::DiskCipher::XtsAes256 => disk_crypt_resources::Cipher::XtsAes256,
1890 },
1891 key: fs_err::read(key_file).context("failed to read key file")?,
1892 })),
1893 DiskCliKind::Sqlite {
1894 path,
1895 create_with_len,
1896 } => {
1897 match (create_with_len.is_some(), path.exists()) {
1902 (true, true) => anyhow::bail!(
1903 "cannot create new sqlite disk at {} - file already exists",
1904 path.display()
1905 ),
1906 (false, false) => anyhow::bail!(
1907 "cannot open sqlite disk at {} - file not found",
1908 path.display()
1909 ),
1910 _ => {}
1911 }
1912
1913 layers.push(layer(SqliteDiskLayerHandle {
1914 dbhd_path: path.display().to_string(),
1915 format_dbhd: create_with_len.map(|len| {
1916 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1917 logically_read_only: false,
1918 len: Some(len),
1919 }
1920 }),
1921 }));
1922 }
1923 DiskCliKind::SqliteDiff { path, create, disk } => {
1924 match (create, path.exists()) {
1929 (true, true) => anyhow::bail!(
1930 "cannot create new sqlite disk at {} - file already exists",
1931 path.display()
1932 ),
1933 (false, false) => anyhow::bail!(
1934 "cannot open sqlite disk at {} - file not found",
1935 path.display()
1936 ),
1937 _ => {}
1938 }
1939
1940 layers.push(layer(SqliteDiskLayerHandle {
1941 dbhd_path: path.display().to_string(),
1942 format_dbhd: create.then_some(
1943 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1944 logically_read_only: false,
1945 len: None,
1946 },
1947 ),
1948 }));
1949 disk_open_inner(disk, true, layers)?;
1950 }
1951 DiskCliKind::AutoCacheSqlite {
1952 cache_path,
1953 key,
1954 disk,
1955 } => {
1956 layers.push(LayerOrDisk::Layer(DiskLayerDescription {
1957 read_cache: true,
1958 write_through: false,
1959 layer: SqliteAutoCacheDiskLayerHandle {
1960 cache_path: cache_path.clone(),
1961 cache_key: key.clone(),
1962 }
1963 .into_resource(),
1964 }));
1965 disk_open_inner(disk, read_only, layers)?;
1966 }
1967 }
1968 Ok(())
1969}
1970
1971fn do_main() -> anyhow::Result<()> {
1972 #[cfg(windows)]
1973 pal::windows::disable_hard_error_dialog();
1974
1975 tracing_init::enable_tracing()?;
1976
1977 meshworker::run_vmm_mesh_host()?;
1981
1982 let opt = Options::parse();
1983 if let Some(path) = &opt.write_saved_state_proto {
1984 mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
1985 .write_to_path(path)
1986 .context("failed to write protobuf descriptors")?;
1987 return Ok(());
1988 }
1989
1990 if let Some(path) = opt.relay_console_path {
1991 let console_title = opt.relay_console_title.unwrap_or_default();
1992 return console_relay::relay_console(&path, console_title.as_str());
1993 }
1994
1995 #[cfg(any(feature = "grpc", feature = "ttrpc"))]
1996 if let Some(path) = opt.ttrpc.as_ref().or(opt.grpc.as_ref()) {
1997 return block_on(async {
1998 let _ = std::fs::remove_file(path);
1999 let listener =
2000 unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
2001
2002 let transport = if opt.ttrpc.is_some() {
2003 ttrpc::RpcTransport::Ttrpc
2004 } else {
2005 ttrpc::RpcTransport::Grpc
2006 };
2007
2008 let mut handle =
2010 mesh_worker::launch_local_worker::<ttrpc::TtrpcWorker>(ttrpc::Parameters {
2011 listener,
2012 transport,
2013 })
2014 .await?;
2015
2016 tracing::info!(%transport, path = %path.display(), "listening");
2017
2018 pal::close_stdout().context("failed to close stdout")?;
2020
2021 handle.join().await?;
2022
2023 Ok(())
2024 });
2025 }
2026
2027 DefaultPool::run_with(async |driver| {
2028 let mesh = VmmMesh::new(&driver, opt.single_process)?;
2029 let result = run_control(&driver, &mesh, opt).await;
2030 mesh.shutdown().await;
2031 result
2032 })
2033}
2034
2035fn maybe_with_radix_u64(s: &str) -> Result<u64, String> {
2036 let (radix, prefix_len) = if s.starts_with("0x") || s.starts_with("0X") {
2037 (16, 2)
2038 } else if s.starts_with("0o") || s.starts_with("0O") {
2039 (8, 2)
2040 } else if s.starts_with("0b") || s.starts_with("0B") {
2041 (2, 2)
2042 } else {
2043 (10, 0)
2044 };
2045
2046 u64::from_str_radix(&s[prefix_len..], radix).map_err(|e| format!("{e}"))
2047}
2048
2049#[derive(Parser)]
2050#[clap(
2051 name = "openvmm",
2052 disable_help_flag = true,
2053 disable_version_flag = true,
2054 no_binary_name = true,
2055 help_template("{subcommands}")
2056)]
2057enum InteractiveCommand {
2058 #[clap(visible_alias = "R")]
2062 Restart,
2063
2064 #[clap(visible_alias = "n")]
2066 Nmi,
2067
2068 #[clap(visible_alias = "p")]
2070 Pause,
2071
2072 #[clap(visible_alias = "r")]
2074 Resume,
2075
2076 #[clap(visible_alias = "psr")]
2078 PulseSaveRestore,
2079
2080 #[clap(visible_alias = "spsr")]
2082 SchedulePulseSaveRestore {
2083 interval: Option<u64>,
2086 },
2087
2088 #[clap(visible_alias = "d")]
2090 AddDisk {
2091 #[clap(long = "ro")]
2092 read_only: bool,
2093 #[clap(long = "dvd")]
2094 is_dvd: bool,
2095 #[clap(long, default_value_t)]
2096 target: u8,
2097 #[clap(long, default_value_t)]
2098 path: u8,
2099 #[clap(long, default_value_t)]
2100 lun: u8,
2101 #[clap(long)]
2102 ram: Option<u64>,
2103 file_path: Option<PathBuf>,
2104 },
2105
2106 #[clap(visible_alias = "D")]
2108 RmDisk {
2109 #[clap(long)]
2110 target: u8,
2111 #[clap(long)]
2112 path: u8,
2113 #[clap(long)]
2114 lun: u8,
2115 },
2116
2117 #[clap(subcommand)]
2119 Vtl2Settings(Vtl2SettingsCommand),
2120
2121 AddNvmeNs {
2123 #[clap(long = "ro")]
2124 read_only: bool,
2125 #[clap(long)]
2127 nsid: u32,
2128 #[clap(long)]
2130 ram: Option<u64>,
2131 file_path: Option<PathBuf>,
2133 #[clap(long)]
2136 vtl0_lun: Option<u32>,
2137 },
2138
2139 RmNvmeNs {
2141 #[clap(long)]
2143 nsid: u32,
2144 #[clap(long)]
2146 vtl0: bool,
2147 },
2148
2149 #[clap(visible_alias = "x")]
2151 Inspect {
2152 #[clap(short, long)]
2154 recursive: bool,
2155 #[clap(short, long, requires("recursive"))]
2157 limit: Option<usize>,
2158 #[clap(short = 'v', long)]
2160 paravisor: bool,
2161 element: Option<String>,
2163 #[clap(short, long, conflicts_with("recursive"))]
2165 update: Option<String>,
2166 },
2167
2168 #[clap(visible_alias = "V")]
2170 RestartVnc,
2171
2172 #[clap(visible_alias = "v")]
2174 Hvsock {
2175 #[clap(short, long)]
2177 term: Option<PathBuf>,
2178 port: u32,
2180 },
2181
2182 #[clap(visible_alias = "q")]
2184 Quit,
2185
2186 #[clap(visible_alias = "i")]
2191 Input { data: Vec<String> },
2192
2193 #[clap(visible_alias = "I")]
2197 InputMode,
2198
2199 Reset,
2201
2202 Shutdown {
2204 #[clap(long, short = 'r')]
2206 reboot: bool,
2207 #[clap(long, short = 'h', conflicts_with = "reboot")]
2209 hibernate: bool,
2210 #[clap(long, short = 'f')]
2212 force: bool,
2213 },
2214
2215 #[clap(visible_alias = "ch")]
2218 ClearHalt,
2219
2220 ServiceVtl2 {
2222 #[clap(long, short = 'u')]
2225 user_mode_only: bool,
2226 #[clap(long, conflicts_with("user_mode_only"))]
2229 igvm: Option<PathBuf>,
2230 #[clap(long, short = 'n', default_missing_value = "true")]
2233 nvme_keepalive: bool,
2234 #[clap(long)]
2237 mana_keepalive: bool,
2238 },
2239
2240 ReadMemory {
2242 #[clap(value_parser=maybe_with_radix_u64)]
2244 gpa: u64,
2245 #[clap(value_parser=maybe_with_radix_u64)]
2247 size: u64,
2248 #[clap(long, short = 'f')]
2251 file: Option<PathBuf>,
2252 },
2253
2254 WriteMemory {
2256 #[clap(value_parser=maybe_with_radix_u64)]
2258 gpa: u64,
2259 hex: Option<String>,
2262 #[clap(long, short = 'f')]
2264 file: Option<PathBuf>,
2265 },
2266
2267 Panic,
2269
2270 Kvp(kvp::KvpCommand),
2272}
2273
2274#[derive(clap::Subcommand)]
2276enum Vtl2SettingsCommand {
2277 Show,
2279
2280 AddScsiDisk {
2284 #[clap(long)]
2287 controller: Option<String>,
2288 #[clap(long)]
2290 lun: u32,
2291 #[clap(
2293 long,
2294 conflicts_with = "backing_scsi_lun",
2295 required_unless_present = "backing_scsi_lun"
2296 )]
2297 backing_nvme_nsid: Option<u32>,
2298 #[clap(
2300 long,
2301 conflicts_with = "backing_nvme_nsid",
2302 required_unless_present = "backing_nvme_nsid"
2303 )]
2304 backing_scsi_lun: Option<u32>,
2305 },
2306
2307 RmScsiDisk {
2309 #[clap(long)]
2312 controller: Option<String>,
2313 #[clap(long)]
2315 lun: u32,
2316 },
2317}
2318
2319struct CommandParser {
2320 app: clap::Command,
2321}
2322
2323impl CommandParser {
2324 fn new() -> Self {
2325 let mut app = InteractiveCommand::command();
2327 for sc in app.get_subcommands_mut() {
2328 *sc = sc
2329 .clone()
2330 .help_template("{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}");
2331 }
2332 Self { app }
2333 }
2334
2335 fn parse(&mut self, line: &str) -> clap::error::Result<InteractiveCommand> {
2336 let args = shell_words::split(line)
2337 .map_err(|err| self.app.error(clap::error::ErrorKind::ValueValidation, err))?;
2338 let matches = self.app.try_get_matches_from_mut(args)?;
2339 InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(&mut self.app))
2340 }
2341}
2342
2343fn new_hvsock_service_id(port: u32) -> Guid {
2344 Guid {
2347 data1: port,
2348 .."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
2349 }
2350}
2351
2352async fn run_control(driver: &DefaultDriver, mesh: &VmmMesh, opt: Options) -> anyhow::Result<()> {
2353 let (mut vm_config, mut resources) = vm_config_from_command_line(driver, mesh, &opt).await?;
2354
2355 let mut vnc_worker = None;
2356 if opt.gfx || opt.vnc {
2357 let listener = TcpListener::bind(format!("127.0.0.1:{}", opt.vnc_port))
2358 .with_context(|| format!("binding to VNC port {}", opt.vnc_port))?;
2359
2360 let input_send = vm_config.input.sender();
2361 let framebuffer = resources
2362 .framebuffer_access
2363 .take()
2364 .expect("synth video enabled");
2365
2366 let vnc_host = mesh
2367 .make_host("vnc", None)
2368 .await
2369 .context("spawning vnc process failed")?;
2370
2371 vnc_worker = Some(
2372 vnc_host
2373 .launch_worker(
2374 vnc_worker_defs::VNC_WORKER_TCP,
2375 VncParameters {
2376 listener,
2377 framebuffer,
2378 input_send,
2379 },
2380 )
2381 .await?,
2382 )
2383 }
2384
2385 let gdb_worker = if let Some(port) = opt.gdb {
2387 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
2388 .with_context(|| format!("binding to gdb port {}", port))?;
2389
2390 let (req_tx, req_rx) = mesh::channel();
2391 vm_config.debugger_rpc = Some(req_rx);
2392
2393 let gdb_host = mesh
2394 .make_host("gdb", None)
2395 .await
2396 .context("spawning gdbstub process failed")?;
2397
2398 Some(
2399 gdb_host
2400 .launch_worker(
2401 debug_worker_defs::DEBUGGER_WORKER,
2402 debug_worker_defs::DebuggerParameters {
2403 listener,
2404 req_chan: req_tx,
2405 vp_count: vm_config.processor_topology.proc_count,
2406 target_arch: if cfg!(guest_arch = "x86_64") {
2407 debug_worker_defs::TargetArch::X86_64
2408 } else {
2409 debug_worker_defs::TargetArch::Aarch64
2410 },
2411 },
2412 )
2413 .await
2414 .context("failed to launch gdbstub worker")?,
2415 )
2416 } else {
2417 None
2418 };
2419
2420 let (vm_rpc, rpc_recv) = mesh::channel();
2422 let (notify_send, notify_recv) = mesh::channel();
2423 let mut vm_worker = {
2424 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2425
2426 let params = VmWorkerParameters {
2427 hypervisor: opt.hypervisor,
2428 cfg: vm_config,
2429 saved_state: None,
2430 rpc: rpc_recv,
2431 notify: notify_send,
2432 };
2433 vm_host
2434 .launch_worker(VM_WORKER, params)
2435 .await
2436 .context("failed to launch vm worker")?
2437 };
2438
2439 if !opt.paused {
2440 vm_rpc.call(VmRpc::Resume, ()).await?;
2441 }
2442
2443 let paravisor_diag = Arc::new(diag_client::DiagClient::from_dialer(
2444 driver.clone(),
2445 DiagDialer {
2446 driver: driver.clone(),
2447 vm_rpc: vm_rpc.clone(),
2448 openhcl_vtl: if opt.vtl2 {
2449 DeviceVtl::Vtl2
2450 } else {
2451 DeviceVtl::Vtl0
2452 },
2453 },
2454 ));
2455
2456 let mut diag_inspector = DiagInspector::new(driver.clone(), paravisor_diag.clone());
2457
2458 let (console_command_send, console_command_recv) = mesh::channel();
2459 let (inspect_completion_engine_send, inspect_completion_engine_recv) = mesh::channel();
2460
2461 let mut console_in = resources.console_in.take();
2462 thread::Builder::new()
2463 .name("stdio-thread".to_string())
2464 .spawn(move || {
2465 #[cfg(unix)]
2467 if io::stderr().is_terminal() {
2468 term::revert_terminal_on_panic()
2469 }
2470
2471 let mut rl = rustyline::Editor::<
2472 interactive_console::OpenvmmRustylineEditor,
2473 rustyline::history::FileHistory,
2474 >::with_config(
2475 rustyline::Config::builder()
2476 .completion_type(rustyline::CompletionType::List)
2477 .build(),
2478 )
2479 .unwrap();
2480
2481 rl.set_helper(Some(interactive_console::OpenvmmRustylineEditor {
2482 openvmm_inspect_req: Arc::new(inspect_completion_engine_send),
2483 }));
2484
2485 let history_file = {
2486 const HISTORY_FILE: &str = ".openvmm_history";
2487
2488 let history_folder = None
2491 .or_else(dirs::state_dir)
2492 .or_else(dirs::data_local_dir)
2493 .map(|path| path.join("openvmm"));
2494
2495 if let Some(history_folder) = history_folder {
2496 if let Err(err) = std::fs::create_dir_all(&history_folder) {
2497 tracing::warn!(
2498 error = &err as &dyn std::error::Error,
2499 "could not create directory: {}",
2500 history_folder.display()
2501 )
2502 }
2503
2504 Some(history_folder.join(HISTORY_FILE))
2505 } else {
2506 None
2507 }
2508 };
2509
2510 if let Some(history_file) = &history_file {
2511 tracing::info!("restoring history from {}", history_file.display());
2512 if rl.load_history(history_file).is_err() {
2513 tracing::info!("could not find existing {}", history_file.display());
2514 }
2515 }
2516
2517 rl.bind_sequence(
2519 rustyline::KeyEvent::new('\x08', rustyline::Modifiers::CTRL),
2520 rustyline::Cmd::Kill(rustyline::Movement::BackwardWord(1, rustyline::Word::Emacs)),
2521 );
2522
2523 let mut parser = CommandParser::new();
2524
2525 let mut stdin = io::stdin();
2526 loop {
2527 term::set_raw_console(true).expect("failed to set raw console mode");
2529
2530 if let Some(input) = console_in.as_mut() {
2531 let mut buf = [0; 32];
2532 loop {
2533 let n = stdin.read(&mut buf).unwrap();
2534 let mut b = &buf[..n];
2535 let stop = if let Some(ctrlq) = b.iter().position(|x| *x == 0x11) {
2536 b = &b[..ctrlq];
2537 true
2538 } else {
2539 false
2540 };
2541 block_on(input.as_mut().write_all(b)).expect("BUGBUG");
2542 if stop {
2543 break;
2544 }
2545 }
2546 }
2547
2548 term::set_raw_console(false).expect("failed to set raw console mode");
2549
2550 loop {
2551 let line = rl.readline("openvmm> ");
2552 if line.is_err() {
2553 break;
2554 }
2555 let line = line.unwrap();
2556 let trimmed = line.trim();
2557 if trimmed.is_empty() {
2558 continue;
2559 }
2560 if let Err(err) = rl.add_history_entry(&line) {
2561 tracing::warn!(
2562 err = &err as &dyn std::error::Error,
2563 "error adding to .openvmm_history"
2564 )
2565 }
2566
2567 match parser.parse(trimmed) {
2568 Ok(cmd) => match cmd {
2569 InteractiveCommand::Input { data } => {
2570 let mut data = data.join(" ");
2571 data.push('\n');
2572 if let Some(input) = console_in.as_mut() {
2573 block_on(input.write_all(data.as_bytes())).expect("BUGBUG");
2574 }
2575 }
2576 InteractiveCommand::InputMode => break,
2577 cmd => {
2578 let (processing_done_send, processing_done_recv) =
2580 mesh::oneshot::<()>();
2581 console_command_send.send((cmd, processing_done_send));
2582 let _ = block_on(processing_done_recv);
2583 }
2584 },
2585 Err(err) => {
2586 err.print().unwrap();
2587 }
2588 }
2589
2590 if let Some(history_file) = &history_file {
2591 rl.append_history(history_file).unwrap();
2592 }
2593 }
2594 }
2595 })
2596 .unwrap();
2597
2598 let mut state_change_task = None::<Task<Result<StateChange, RpcError>>>;
2599 let mut pulse_save_restore_interval: Option<Duration> = None;
2600 let mut pending_shutdown = None;
2601
2602 enum StateChange {
2603 Pause(bool),
2604 Resume(bool),
2605 Reset(Result<(), RemoteError>),
2606 PulseSaveRestore(Result<(), PulseSaveRestoreError>),
2607 ServiceVtl2(anyhow::Result<Duration>),
2608 }
2609
2610 enum Event {
2611 Command((InteractiveCommand, mesh::OneshotSender<()>)),
2612 InspectRequestFromCompletionEngine(
2613 (InspectTarget, String, mesh::OneshotSender<inspect::Node>),
2614 ),
2615 Quit,
2616 Halt(vmm_core_defs::HaltReason),
2617 PulseSaveRestore,
2618 Worker(WorkerEvent),
2619 VncWorker(WorkerEvent),
2620 StateChange(Result<StateChange, RpcError>),
2621 ShutdownResult(Result<hyperv_ic_resources::shutdown::ShutdownResult, RpcError>),
2622 }
2623
2624 let mut console_command_recv = console_command_recv
2625 .map(Event::Command)
2626 .chain(futures::stream::repeat_with(|| Event::Quit));
2627
2628 let mut notify_recv = notify_recv.map(Event::Halt);
2629
2630 let mut inspect_completion_engine_recv =
2631 inspect_completion_engine_recv.map(Event::InspectRequestFromCompletionEngine);
2632
2633 let mut quit = false;
2634 loop {
2635 let event = {
2636 let pulse_save_restore = pin!(async {
2637 match pulse_save_restore_interval {
2638 Some(wait) => {
2639 PolledTimer::new(driver).sleep(wait).await;
2640 Event::PulseSaveRestore
2641 }
2642 None => pending().await,
2643 }
2644 });
2645
2646 let vm = (&mut vm_worker).map(Event::Worker);
2647 let vnc = futures::stream::iter(vnc_worker.as_mut())
2648 .flatten()
2649 .map(Event::VncWorker);
2650 let change = futures::stream::iter(state_change_task.as_mut().map(|x| x.into_stream()))
2651 .flatten()
2652 .map(Event::StateChange);
2653 let shutdown = pin!(async {
2654 if let Some(s) = &mut pending_shutdown {
2655 Event::ShutdownResult(s.await)
2656 } else {
2657 pending().await
2658 }
2659 });
2660
2661 (
2662 &mut console_command_recv,
2663 &mut inspect_completion_engine_recv,
2664 &mut notify_recv,
2665 pulse_save_restore.into_stream(),
2666 vm,
2667 vnc,
2668 change,
2669 shutdown.into_stream(),
2670 )
2671 .merge()
2672 .next()
2673 .await
2674 .unwrap()
2675 };
2676
2677 let (cmd, _processing_done_send) = match event {
2678 Event::Command(message) => message,
2679 Event::InspectRequestFromCompletionEngine((vtl, path, res)) => {
2680 let mut inspection =
2681 InspectionBuilder::new(&path)
2682 .depth(Some(1))
2683 .inspect(inspect_obj(
2684 vtl,
2685 mesh,
2686 &vm_worker,
2687 vnc_worker.as_ref(),
2688 gdb_worker.as_ref(),
2689 &mut diag_inspector,
2690 ));
2691 let _ = CancelContext::new()
2692 .with_timeout(Duration::from_secs(1))
2693 .until_cancelled(inspection.resolve())
2694 .await;
2695
2696 let node = inspection.results();
2697 res.send(node);
2698 continue;
2699 }
2700 Event::Quit => break,
2701 Event::Halt(reason) => {
2702 tracing::info!(?reason, "guest halted");
2703 continue;
2704 }
2705 Event::PulseSaveRestore => {
2706 vm_rpc.call(VmRpc::PulseSaveRestore, ()).await??;
2707 continue;
2708 }
2709 Event::Worker(event) => {
2710 match event {
2711 WorkerEvent::Stopped => {
2712 if quit {
2713 tracing::info!("vm stopped");
2714 } else {
2715 tracing::error!("vm worker unexpectedly stopped");
2716 }
2717 break;
2718 }
2719 WorkerEvent::Failed(err) => {
2720 tracing::error!(error = &err as &dyn std::error::Error, "vm worker failed");
2721 break;
2722 }
2723 WorkerEvent::RestartFailed(err) => {
2724 tracing::error!(
2725 error = &err as &dyn std::error::Error,
2726 "vm worker restart failed"
2727 );
2728 }
2729 WorkerEvent::Started => {
2730 tracing::info!("vm worker restarted");
2731 }
2732 }
2733 continue;
2734 }
2735 Event::VncWorker(event) => {
2736 match event {
2737 WorkerEvent::Stopped => tracing::error!("vnc unexpectedly stopped"),
2738 WorkerEvent::Failed(err) => {
2739 tracing::error!(
2740 error = &err as &dyn std::error::Error,
2741 "vnc worker failed"
2742 );
2743 }
2744 WorkerEvent::RestartFailed(err) => {
2745 tracing::error!(
2746 error = &err as &dyn std::error::Error,
2747 "vnc worker restart failed"
2748 );
2749 }
2750 WorkerEvent::Started => {
2751 tracing::info!("vnc worker restarted");
2752 }
2753 }
2754 continue;
2755 }
2756 Event::StateChange(r) => {
2757 match r {
2758 Ok(sc) => match sc {
2759 StateChange::Pause(success) => {
2760 if success {
2761 tracing::info!("pause complete");
2762 } else {
2763 tracing::warn!("already paused");
2764 }
2765 }
2766 StateChange::Resume(success) => {
2767 if success {
2768 tracing::info!("resumed complete");
2769 } else {
2770 tracing::warn!("already running");
2771 }
2772 }
2773 StateChange::Reset(r) => match r {
2774 Ok(()) => tracing::info!("reset complete"),
2775 Err(err) => tracing::error!(
2776 error = &err as &dyn std::error::Error,
2777 "reset failed"
2778 ),
2779 },
2780 StateChange::PulseSaveRestore(r) => match r {
2781 Ok(()) => tracing::info!("pulse save/restore complete"),
2782 Err(err) => tracing::error!(
2783 error = &err as &dyn std::error::Error,
2784 "pulse save/restore failed"
2785 ),
2786 },
2787 StateChange::ServiceVtl2(r) => match r {
2788 Ok(dur) => {
2789 tracing::info!(
2790 duration = dur.as_millis() as i64,
2791 "vtl2 servicing complete"
2792 )
2793 }
2794 Err(err) => tracing::error!(
2795 error = err.as_ref() as &dyn std::error::Error,
2796 "vtl2 servicing failed"
2797 ),
2798 },
2799 },
2800 Err(err) => {
2801 tracing::error!(
2802 error = &err as &dyn std::error::Error,
2803 "communication failure during state change"
2804 );
2805 }
2806 }
2807 state_change_task = None;
2808 continue;
2809 }
2810 Event::ShutdownResult(r) => {
2811 match r {
2812 Ok(r) => match r {
2813 hyperv_ic_resources::shutdown::ShutdownResult::Ok => {
2814 tracing::info!("shutdown initiated");
2815 }
2816 hyperv_ic_resources::shutdown::ShutdownResult::NotReady => {
2817 tracing::error!("shutdown ic not ready");
2818 }
2819 hyperv_ic_resources::shutdown::ShutdownResult::AlreadyInProgress => {
2820 tracing::error!("shutdown already in progress");
2821 }
2822 hyperv_ic_resources::shutdown::ShutdownResult::Failed(hr) => {
2823 tracing::error!("shutdown failed with error code {hr:#x}");
2824 }
2825 },
2826 Err(err) => {
2827 tracing::error!(
2828 error = &err as &dyn std::error::Error,
2829 "communication failure during shutdown"
2830 );
2831 }
2832 }
2833 pending_shutdown = None;
2834 continue;
2835 }
2836 };
2837
2838 fn inspect_obj<'a>(
2839 target: InspectTarget,
2840 mesh: &'a VmmMesh,
2841 vm_worker: &'a WorkerHandle,
2842 vnc_worker: Option<&'a WorkerHandle>,
2843 gdb_worker: Option<&'a WorkerHandle>,
2844 diag_inspector: &'a mut DiagInspector,
2845 ) -> impl 'a + InspectMut {
2846 inspect::adhoc_mut(move |req| match target {
2847 InspectTarget::Host => {
2848 let mut resp = req.respond();
2849 resp.field("mesh", mesh)
2850 .field("vm", vm_worker)
2851 .field("vnc", vnc_worker)
2852 .field("gdb", gdb_worker);
2853 }
2854 InspectTarget::Paravisor => {
2855 diag_inspector.inspect_mut(req);
2856 }
2857 })
2858 }
2859
2860 fn state_change<U: 'static + Send>(
2861 driver: impl Spawn,
2862 vm_rpc: &mesh::Sender<VmRpc>,
2863 state_change_task: &mut Option<Task<Result<StateChange, RpcError>>>,
2864 f: impl FnOnce(Rpc<(), U>) -> VmRpc,
2865 g: impl FnOnce(U) -> StateChange + 'static + Send,
2866 ) {
2867 if state_change_task.is_some() {
2868 tracing::error!("state change already in progress");
2869 } else {
2870 let rpc = vm_rpc.call(f, ());
2871 *state_change_task =
2872 Some(driver.spawn("state-change", async move { Ok(g(rpc.await?)) }));
2873 }
2874 }
2875
2876 match cmd {
2877 InteractiveCommand::Panic => {
2878 panic!("injected panic")
2879 }
2880 InteractiveCommand::Restart => {
2881 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2883
2884 vm_worker.restart(&vm_host);
2885 }
2886 InteractiveCommand::Pause => {
2887 state_change(
2888 driver,
2889 &vm_rpc,
2890 &mut state_change_task,
2891 VmRpc::Pause,
2892 StateChange::Pause,
2893 );
2894 }
2895 InteractiveCommand::Resume => {
2896 state_change(
2897 driver,
2898 &vm_rpc,
2899 &mut state_change_task,
2900 VmRpc::Resume,
2901 StateChange::Resume,
2902 );
2903 }
2904 InteractiveCommand::Reset => {
2905 state_change(
2906 driver,
2907 &vm_rpc,
2908 &mut state_change_task,
2909 VmRpc::Reset,
2910 StateChange::Reset,
2911 );
2912 }
2913 InteractiveCommand::PulseSaveRestore => {
2914 state_change(
2915 driver,
2916 &vm_rpc,
2917 &mut state_change_task,
2918 VmRpc::PulseSaveRestore,
2919 StateChange::PulseSaveRestore,
2920 );
2921 }
2922 InteractiveCommand::SchedulePulseSaveRestore { interval } => {
2923 pulse_save_restore_interval = match interval {
2924 Some(seconds) if seconds != 0 => Some(Duration::from_secs(seconds)),
2925 _ => {
2926 None
2928 }
2929 }
2930 }
2931 InteractiveCommand::Shutdown {
2932 reboot,
2933 hibernate,
2934 force,
2935 } => {
2936 if pending_shutdown.is_some() {
2937 println!("shutdown already in progress");
2938 } else if let Some(ic) = &resources.shutdown_ic {
2939 let params = hyperv_ic_resources::shutdown::ShutdownParams {
2940 shutdown_type: if hibernate {
2941 hyperv_ic_resources::shutdown::ShutdownType::Hibernate
2942 } else if reboot {
2943 hyperv_ic_resources::shutdown::ShutdownType::Reboot
2944 } else {
2945 hyperv_ic_resources::shutdown::ShutdownType::PowerOff
2946 },
2947 force,
2948 };
2949 pending_shutdown =
2950 Some(ic.call(hyperv_ic_resources::shutdown::ShutdownRpc::Shutdown, params));
2951 } else {
2952 println!("no shutdown ic configured");
2953 }
2954 }
2955 InteractiveCommand::Nmi => {
2956 let _ = vm_rpc.call(VmRpc::Nmi, 0).await;
2957 }
2958 InteractiveCommand::ClearHalt => {
2959 vm_rpc.call(VmRpc::ClearHalt, ()).await.ok();
2960 }
2961 InteractiveCommand::AddDisk {
2962 read_only,
2963 target,
2964 path,
2965 lun,
2966 ram,
2967 file_path,
2968 is_dvd,
2969 } => {
2970 let action = async {
2971 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2972 let disk_type = match ram {
2973 None => {
2974 let path = file_path.context("no filename passed")?;
2975 open_disk_type(path.as_ref(), read_only)
2976 .with_context(|| format!("failed to open {}", path.display()))?
2977 }
2978 Some(size) => {
2979 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
2980 RamDiskLayerHandle { len: Some(size) },
2981 ))
2982 }
2983 };
2984
2985 let device = if is_dvd {
2986 SimpleScsiDvdHandle {
2987 media: Some(disk_type),
2988 requests: None,
2989 }
2990 .into_resource()
2991 } else {
2992 SimpleScsiDiskHandle {
2993 disk: disk_type,
2994 read_only,
2995 parameters: Default::default(),
2996 }
2997 .into_resource()
2998 };
2999
3000 let cfg = ScsiDeviceAndPath {
3001 path: ScsiPath { path, target, lun },
3002 device,
3003 };
3004
3005 scsi.call_failable(ScsiControllerRequest::AddDevice, cfg)
3006 .await?;
3007
3008 anyhow::Result::<_>::Ok(())
3009 };
3010
3011 if let Err(error) = action.await {
3012 tracing::error!(error = error.as_error(), "error adding disk")
3013 }
3014 }
3015 InteractiveCommand::RmDisk { target, path, lun } => {
3016 let action = async {
3017 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
3018 scsi.call_failable(
3019 ScsiControllerRequest::RemoveDevice,
3020 ScsiPath { target, path, lun },
3021 )
3022 .await?;
3023 anyhow::Ok(())
3024 };
3025
3026 if let Err(error) = action.await {
3027 tracing::error!(error = error.as_error(), "error removing disk")
3028 }
3029 }
3030 InteractiveCommand::Vtl2Settings(cmd) => {
3031 if resources.vtl2_settings.is_none() {
3032 eprintln!("error: no VTL2 settings (not running with VTL2?)");
3033 continue;
3034 }
3035 let action = async {
3036 match cmd {
3037 Vtl2SettingsCommand::Show => {
3038 let settings = resources.vtl2_settings.as_ref().unwrap();
3039 println!("{:#?}", settings);
3040 }
3041 Vtl2SettingsCommand::AddScsiDisk {
3042 controller,
3043 lun,
3044 backing_nvme_nsid,
3045 backing_scsi_lun,
3046 } => {
3047 let (device_type, device_path, sub_device_path) = match (
3049 backing_nvme_nsid,
3050 backing_scsi_lun,
3051 ) {
3052 (Some(nsid), None) => (
3053 vtl2_settings_proto::physical_device::DeviceType::Nvme,
3054 storage_builder::NVME_VTL2_INSTANCE_ID,
3055 nsid,
3056 ),
3057 (None, Some(scsi_lun)) => (
3058 vtl2_settings_proto::physical_device::DeviceType::Vscsi,
3059 storage_builder::SCSI_VTL2_INSTANCE_ID,
3060 scsi_lun,
3061 ),
3062 (Some(_), Some(_)) => {
3063 anyhow::bail!(
3064 "can't specify both --backing-nvme-nsid and --backing-scsi-lun"
3065 );
3066 }
3067 (None, None) => {
3068 anyhow::bail!(
3069 "must specify either --backing-nvme-nsid or --backing-scsi-lun"
3070 );
3071 }
3072 };
3073
3074 let controller_guid = controller
3076 .map(|s| s.parse())
3077 .transpose()
3078 .context("invalid controller GUID")?
3079 .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3080
3081 resources
3082 .add_vtl0_scsi_disk(
3083 controller_guid,
3084 lun,
3085 device_type,
3086 device_path,
3087 sub_device_path,
3088 )
3089 .await?;
3090
3091 let backing_desc = if backing_nvme_nsid.is_some() {
3092 format!("nvme_nsid={}", sub_device_path)
3093 } else {
3094 format!("scsi_lun={}", sub_device_path)
3095 };
3096 println!(
3097 "Added VTL0 SCSI disk: controller={}, lun={}, backing={}",
3098 controller_guid, lun, backing_desc
3099 );
3100 }
3101 Vtl2SettingsCommand::RmScsiDisk { controller, lun } => {
3102 let controller_guid = controller
3104 .map(|s| s.parse())
3105 .transpose()
3106 .context("invalid controller GUID")?
3107 .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3108
3109 resources
3110 .remove_vtl0_scsi_disk(controller_guid, lun)
3111 .await?;
3112
3113 println!(
3114 "Removed VTL0 SCSI disk: controller={}, lun={}",
3115 controller_guid, lun
3116 );
3117 }
3118 }
3119 anyhow::Ok(())
3120 };
3121
3122 if let Err(error) = action.await {
3123 eprintln!("error: {}", error);
3124 }
3125 }
3126 InteractiveCommand::AddNvmeNs {
3127 read_only,
3128 nsid,
3129 ram,
3130 file_path,
3131 vtl0_lun,
3132 } => {
3133 if resources.vtl2_settings.is_none() {
3134 eprintln!("error: add-nvme-ns requires --vtl2 mode");
3135 continue;
3136 }
3137 let action = async {
3138 let nvme = resources
3139 .nvme_vtl2_rpc
3140 .as_ref()
3141 .context("no vtl2 nvme controller")?;
3142 let disk_type = match (ram, file_path) {
3143 (None, Some(path)) => open_disk_type(path.as_ref(), read_only)
3144 .with_context(|| format!("failed to open {}", path.display()))?,
3145 (Some(size), None) => {
3146 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
3147 RamDiskLayerHandle { len: Some(size) },
3148 ))
3149 }
3150 (None, None) => {
3151 anyhow::bail!("must specify either file path or --ram");
3152 }
3153 (Some(_), Some(_)) => {
3154 anyhow::bail!("cannot specify both file path and --ram");
3155 }
3156 };
3157
3158 let ns = NamespaceDefinition {
3159 nsid,
3160 read_only,
3161 disk: disk_type,
3162 };
3163
3164 nvme.call_failable(NvmeControllerRequest::AddNamespace, ns)
3165 .await?;
3166 println!("Added namespace {}", nsid);
3167
3168 if let Some(lun) = vtl0_lun {
3170 resources
3171 .add_vtl0_scsi_disk(
3172 storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3173 lun,
3174 vtl2_settings_proto::physical_device::DeviceType::Nvme,
3175 storage_builder::NVME_VTL2_INSTANCE_ID,
3176 nsid,
3177 )
3178 .await?;
3179 println!("Exposed namespace {} to VTL0 as SCSI lun={}", nsid, lun);
3180 }
3181
3182 Ok(())
3183 };
3184
3185 if let Err(error) = action.await {
3186 eprintln!("error adding nvme namespace: {}", error);
3187 }
3188 }
3189 InteractiveCommand::RmNvmeNs { nsid, vtl0 } => {
3190 if resources.vtl2_settings.is_none() {
3191 eprintln!("error: rm-nvme-ns requires --vtl2 mode");
3192 continue;
3193 }
3194 let action = async {
3195 if vtl0 {
3197 let removed_lun = resources
3198 .remove_vtl0_scsi_disk_by_nvme_nsid(
3199 storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3200 storage_builder::NVME_VTL2_INSTANCE_ID,
3201 nsid,
3202 )
3203 .await?;
3204 if let Some(lun) = removed_lun {
3205 println!("Removed VTL0 SCSI lun={}", lun);
3206 } else {
3207 println!("No VTL0 SCSI disk found backed by NVMe nsid={}", nsid);
3208 }
3209 }
3210
3211 let nvme = resources
3212 .nvme_vtl2_rpc
3213 .as_ref()
3214 .context("no vtl2 nvme controller")?;
3215 nvme.call_failable(NvmeControllerRequest::RemoveNamespace, nsid)
3216 .await?;
3217 println!("Removed NVMe namespace {}", nsid);
3218 anyhow::Ok(())
3219 };
3220
3221 if let Err(error) = action.await {
3222 eprintln!("error removing nvme namespace: {}", error);
3223 }
3224 }
3225 InteractiveCommand::Inspect {
3226 recursive,
3227 limit,
3228 paravisor,
3229 element,
3230 update,
3231 } => {
3232 let obj = inspect_obj(
3233 if paravisor {
3234 InspectTarget::Paravisor
3235 } else {
3236 InspectTarget::Host
3237 },
3238 mesh,
3239 &vm_worker,
3240 vnc_worker.as_ref(),
3241 gdb_worker.as_ref(),
3242 &mut diag_inspector,
3243 );
3244
3245 if let Some(value) = update {
3246 let Some(element) = element else {
3247 anyhow::bail!("must provide element for update")
3248 };
3249
3250 let value = async {
3251 let update = inspect::update(&element, &value, obj);
3252 let value = CancelContext::new()
3253 .with_timeout(Duration::from_secs(1))
3254 .until_cancelled(update)
3255 .await??;
3256 anyhow::Ok(value)
3257 }
3258 .await;
3259 match value {
3260 Ok(node) => println!("{:#}", node),
3261 Err(err) => println!("error: {:#}", err),
3262 }
3263 } else {
3264 let element = element.unwrap_or_default();
3265 let depth = if recursive { limit } else { Some(0) };
3266 let node = async {
3267 let mut inspection =
3268 InspectionBuilder::new(&element).depth(depth).inspect(obj);
3269 let _ = CancelContext::new()
3270 .with_timeout(Duration::from_secs(1))
3271 .until_cancelled(inspection.resolve())
3272 .await;
3273 inspection.results()
3274 }
3275 .await;
3276
3277 println!("{:#}", node);
3278 }
3279 }
3280 InteractiveCommand::RestartVnc => {
3281 if let Some(vnc) = &mut vnc_worker {
3282 let action = async {
3283 let vnc_host = mesh
3284 .make_host("vnc", None)
3285 .await
3286 .context("spawning vnc process failed")?;
3287
3288 vnc.restart(&vnc_host);
3289 anyhow::Result::<_>::Ok(())
3290 };
3291
3292 if let Err(error) = action.await {
3293 eprintln!("error: {}", error);
3294 }
3295 } else {
3296 eprintln!("ERROR: no VNC server running");
3297 }
3298 }
3299 InteractiveCommand::Hvsock { term, port } => {
3300 let vm_rpc = &vm_rpc;
3301 let action = async || {
3302 let service_id = new_hvsock_service_id(port);
3303 let socket = vm_rpc
3304 .call_failable(
3305 VmRpc::ConnectHvsock,
3306 (
3307 CancelContext::new().with_timeout(Duration::from_secs(2)),
3308 service_id,
3309 DeviceVtl::Vtl0,
3310 ),
3311 )
3312 .await?;
3313 let socket = PolledSocket::new(driver, socket)?;
3314 let mut console = console_relay::Console::new(
3315 driver.clone(),
3316 term.or_else(openvmm_terminal_app).as_deref(),
3317 Some(ConsoleLaunchOptions {
3318 window_title: Some(format!("HVSock{} [OpenVMM]", port)),
3319 }),
3320 )?;
3321 driver
3322 .spawn("console-relay", async move { console.relay(socket).await })
3323 .detach();
3324 anyhow::Result::<_>::Ok(())
3325 };
3326
3327 if let Err(error) = (action)().await {
3328 eprintln!("error: {}", error);
3329 }
3330 }
3331 InteractiveCommand::ServiceVtl2 {
3332 user_mode_only,
3333 igvm,
3334 mana_keepalive,
3335 nvme_keepalive,
3336 } => {
3337 let paravisor_diag = paravisor_diag.clone();
3338 let vm_rpc = vm_rpc.clone();
3339 let igvm = igvm.or_else(|| opt.igvm.clone());
3340 let ged_rpc = resources.ged_rpc.clone();
3341 let r = async move {
3342 let start;
3343 if user_mode_only {
3344 start = Instant::now();
3345 paravisor_diag.restart().await?;
3346 } else {
3347 let path = igvm.context("no igvm file loaded")?;
3348 let file = fs_err::File::open(path)?;
3349 start = Instant::now();
3350 openvmm_helpers::underhill::save_underhill(
3351 &vm_rpc,
3352 ged_rpc.as_ref().context("no GED")?,
3353 GuestServicingFlags {
3354 nvme_keepalive,
3355 mana_keepalive,
3356 },
3357 file.into(),
3358 )
3359 .await?;
3360 openvmm_helpers::underhill::restore_underhill(
3361 &vm_rpc,
3362 ged_rpc.as_ref().context("no GED")?,
3363 )
3364 .await?;
3365 }
3366 let end = Instant::now();
3367 Ok(end - start)
3368 }
3369 .map(|r| Ok(StateChange::ServiceVtl2(r)));
3370 if state_change_task.is_some() {
3371 tracing::error!("state change already in progress");
3372 } else {
3373 state_change_task = Some(driver.spawn("state-change", r));
3374 }
3375 }
3376 InteractiveCommand::Quit => {
3377 tracing::info!("quitting");
3378 resources.scsi_rpc = None;
3381 resources.nvme_vtl2_rpc = None;
3382
3383 vm_worker.stop();
3384 quit = true;
3385 }
3386 InteractiveCommand::ReadMemory { gpa, size, file } => {
3387 let size = size as usize;
3388 let data = vm_rpc.call(VmRpc::ReadMemory, (gpa, size)).await?;
3389
3390 match data {
3391 Ok(bytes) => {
3392 if let Some(file) = file {
3393 if let Err(err) = fs_err::write(file, bytes) {
3394 eprintln!("error: {err:?}");
3395 }
3396 } else {
3397 let width = 16;
3398 let show_ascii = true;
3399
3400 let mut dump = String::new();
3401 for (i, chunk) in bytes.chunks(width).enumerate() {
3402 let hex_part: Vec<String> =
3403 chunk.iter().map(|byte| format!("{:02x}", byte)).collect();
3404 let hex_line = hex_part.join(" ");
3405
3406 if show_ascii {
3407 let ascii_part: String = chunk
3408 .iter()
3409 .map(|&byte| {
3410 if byte.is_ascii_graphic() || byte == b' ' {
3411 byte as char
3412 } else {
3413 '.'
3414 }
3415 })
3416 .collect();
3417 dump.push_str(&format!(
3418 "{:04x}: {:<width$} {}\n",
3419 i * width,
3420 hex_line,
3421 ascii_part,
3422 width = width * 3 - 1
3423 ));
3424 } else {
3425 dump.push_str(&format!("{:04x}: {}\n", i * width, hex_line));
3426 }
3427 }
3428
3429 println!("{dump}");
3430 }
3431 }
3432 Err(err) => {
3433 eprintln!("error: {err:?}");
3434 }
3435 }
3436 }
3437 InteractiveCommand::WriteMemory { gpa, hex, file } => {
3438 if hex.is_some() == file.is_some() {
3439 eprintln!("error: either path to the file or the hex string must be specified");
3440 continue;
3441 }
3442
3443 let data = if let Some(file) = file {
3444 let data = fs_err::read(file);
3445 match data {
3446 Ok(data) => data,
3447 Err(err) => {
3448 eprintln!("error: {err:?}");
3449 continue;
3450 }
3451 }
3452 } else if let Some(hex) = hex {
3453 if hex.len() & 1 != 0 {
3454 eprintln!(
3455 "error: expected even number of hex digits (2 hex digits per byte)"
3456 );
3457 continue;
3458 }
3459 let data: Result<Vec<u8>, String> = (0..hex.len())
3460 .step_by(2)
3461 .map(|i| {
3462 u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
3463 format!("invalid hex character at position {}: {}", i, e)
3464 })
3465 })
3466 .collect();
3467
3468 match data {
3469 Ok(data) => data,
3470 Err(err) => {
3471 eprintln!("error: {err}");
3472 continue;
3473 }
3474 }
3475 } else {
3476 unreachable!();
3477 };
3478
3479 if data.is_empty() {
3480 eprintln!("error: no data to write");
3481 continue;
3482 }
3483
3484 if let Err(err) = vm_rpc.call(VmRpc::WriteMemory, (gpa, data)).await? {
3485 eprintln!("error: {err:?}");
3486 }
3487 }
3488 InteractiveCommand::Kvp(command) => {
3489 let Some(kvp) = &resources.kvp_ic else {
3490 eprintln!("error: no kvp ic configured");
3491 continue;
3492 };
3493 if let Err(err) = kvp::handle_kvp(kvp, command).await {
3494 eprintln!("error: {err:#}");
3495 }
3496 }
3497 InteractiveCommand::Input { .. } | InteractiveCommand::InputMode => unreachable!(),
3498 }
3499 }
3500
3501 vm_worker.stop();
3502 vm_worker.join().await?;
3503 Ok(())
3504}
3505
3506struct DiagDialer {
3507 driver: DefaultDriver,
3508 vm_rpc: mesh::Sender<VmRpc>,
3509 openhcl_vtl: DeviceVtl,
3510}
3511
3512impl mesh_rpc::client::Dial for DiagDialer {
3513 type Stream = PolledSocket<unix_socket::UnixStream>;
3514
3515 async fn dial(&mut self) -> io::Result<Self::Stream> {
3516 let service_id = new_hvsock_service_id(1);
3517 let socket = self
3518 .vm_rpc
3519 .call_failable(
3520 VmRpc::ConnectHvsock,
3521 (
3522 CancelContext::new().with_timeout(Duration::from_secs(2)),
3523 service_id,
3524 self.openhcl_vtl,
3525 ),
3526 )
3527 .await
3528 .map_err(io::Error::other)?;
3529
3530 PolledSocket::new(&self.driver, socket)
3531 }
3532}
3533
3534pub struct DiagInspector(DiagInspectorInner);
3541
3542enum DiagInspectorInner {
3543 NotStarted(DefaultDriver, Arc<diag_client::DiagClient>),
3544 Started {
3545 send: mesh::Sender<inspect::Deferred>,
3546 _task: Task<()>,
3547 },
3548 Invalid,
3549}
3550
3551impl DiagInspector {
3552 pub fn new(driver: DefaultDriver, diag_client: Arc<diag_client::DiagClient>) -> Self {
3553 Self(DiagInspectorInner::NotStarted(driver, diag_client))
3554 }
3555
3556 fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
3557 loop {
3558 match self.0 {
3559 DiagInspectorInner::NotStarted { .. } => {
3560 let DiagInspectorInner::NotStarted(driver, client) =
3561 std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
3562 else {
3563 unreachable!()
3564 };
3565 let (send, recv) = mesh::channel();
3566 let task = driver.clone().spawn("diag-inspect", async move {
3567 Self::run(&client, recv).await
3568 });
3569
3570 self.0 = DiagInspectorInner::Started { send, _task: task };
3571 }
3572 DiagInspectorInner::Started { ref send, .. } => break send,
3573 DiagInspectorInner::Invalid => unreachable!(),
3574 }
3575 }
3576 }
3577
3578 async fn run(
3579 diag_client: &diag_client::DiagClient,
3580 mut recv: mesh::Receiver<inspect::Deferred>,
3581 ) {
3582 while let Some(deferred) = recv.next().await {
3583 let info = deferred.external_request();
3584 let result = match info.request_type {
3585 inspect::ExternalRequestType::Inspect { depth } => {
3586 if depth == 0 {
3587 Ok(inspect::Node::Unevaluated)
3588 } else {
3589 diag_client
3591 .inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
3592 .await
3593 }
3594 }
3595 inspect::ExternalRequestType::Update { value } => {
3596 (diag_client.update(info.path, value).await).map(inspect::Node::Value)
3597 }
3598 };
3599 deferred.complete_external(
3600 result.unwrap_or_else(|err| {
3601 inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
3602 }),
3603 inspect::SensitivityLevel::Unspecified,
3604 )
3605 }
3606 }
3607}
3608
3609impl InspectMut for DiagInspector {
3610 fn inspect_mut(&mut self, req: inspect::Request<'_>) {
3611 self.start().send(req.defer());
3612 }
3613}
3614
3615enum InspectTarget {
3616 Host,
3617 Paravisor,
3618}
3619
3620mod interactive_console {
3621 use super::InteractiveCommand;
3622 use rustyline::Helper;
3623 use rustyline::Highlighter;
3624 use rustyline::Hinter;
3625 use rustyline::Validator;
3626
3627 #[derive(Helper, Highlighter, Hinter, Validator)]
3628 pub(crate) struct OpenvmmRustylineEditor {
3629 pub openvmm_inspect_req: std::sync::Arc<
3630 mesh::Sender<(
3631 super::InspectTarget,
3632 String,
3633 mesh::OneshotSender<inspect::Node>,
3634 )>,
3635 >,
3636 }
3637
3638 impl rustyline::completion::Completer for OpenvmmRustylineEditor {
3639 type Candidate = String;
3640
3641 fn complete(
3642 &self,
3643 line: &str,
3644 pos: usize,
3645 _ctx: &rustyline::Context<'_>,
3646 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
3647 let Ok(cmd) = shell_words::split(line) else {
3648 return Ok((0, Vec::with_capacity(0)));
3649 };
3650
3651 let completions = futures::executor::block_on(
3652 clap_dyn_complete::Complete {
3653 cmd,
3654 raw: Some(line.into()),
3655 position: Some(pos),
3656 }
3657 .generate_completions::<InteractiveCommand>(None, self),
3658 );
3659
3660 let pos_from_end = {
3661 let line = line.chars().take(pos).collect::<String>();
3662
3663 let trailing_ws = line.len() - line.trim_end().len();
3664
3665 if trailing_ws > 0 {
3666 line.len() - trailing_ws + 1 } else {
3668 let last_word = shell_words::split(&line)
3669 .unwrap_or_default()
3670 .last()
3671 .cloned()
3672 .unwrap_or_default();
3673
3674 line.len() - last_word.len()
3675 }
3676 };
3677
3678 Ok((pos_from_end, completions))
3679 }
3680 }
3681
3682 impl clap_dyn_complete::CustomCompleterFactory for &OpenvmmRustylineEditor {
3683 type CustomCompleter = OpenvmmComplete;
3684 async fn build(&self, _ctx: &clap_dyn_complete::RootCtx<'_>) -> Self::CustomCompleter {
3685 OpenvmmComplete {
3686 openvmm_inspect_req: self.openvmm_inspect_req.clone(),
3687 }
3688 }
3689 }
3690
3691 pub struct OpenvmmComplete {
3692 openvmm_inspect_req: std::sync::Arc<
3693 mesh::Sender<(
3694 super::InspectTarget,
3695 String,
3696 mesh::OneshotSender<inspect::Node>,
3697 )>,
3698 >,
3699 }
3700
3701 impl clap_dyn_complete::CustomCompleter for OpenvmmComplete {
3702 async fn complete(
3703 &self,
3704 ctx: &clap_dyn_complete::RootCtx<'_>,
3705 subcommand_path: &[&str],
3706 arg_id: &str,
3707 ) -> Vec<String> {
3708 match (subcommand_path, arg_id) {
3709 (["openvmm", "inspect"], "element") => {
3710 let on_error = vec!["failed/to/connect".into()];
3711
3712 let (parent_path, to_complete) = (ctx.to_complete)
3713 .rsplit_once('/')
3714 .unwrap_or(("", ctx.to_complete));
3715
3716 let node = {
3717 let paravisor = {
3718 let raw_arg = ctx
3719 .matches
3720 .subcommand()
3721 .unwrap()
3722 .1
3723 .get_one::<String>("paravisor")
3724 .map(|x| x.as_str())
3725 .unwrap_or_default();
3726 raw_arg == "true"
3727 };
3728
3729 let (tx, rx) = mesh::oneshot();
3730 self.openvmm_inspect_req.send((
3731 if paravisor {
3732 super::InspectTarget::Paravisor
3733 } else {
3734 super::InspectTarget::Host
3735 },
3736 parent_path.to_owned(),
3737 tx,
3738 ));
3739 let Ok(node) = rx.await else {
3740 return on_error;
3741 };
3742
3743 node
3744 };
3745
3746 let mut completions = Vec::new();
3747
3748 if let inspect::Node::Dir(dir) = node {
3749 for entry in dir {
3750 if entry.name.starts_with(to_complete) {
3751 if parent_path.is_empty() {
3752 completions.push(format!("{}/", entry.name))
3753 } else {
3754 completions.push(format!(
3755 "{}/{}{}",
3756 parent_path,
3757 entry.name,
3758 if matches!(entry.node, inspect::Node::Dir(..)) {
3759 "/"
3760 } else {
3761 ""
3762 }
3763 ))
3764 }
3765 }
3766 }
3767 } else {
3768 return on_error;
3769 }
3770
3771 completions
3772 }
3773 _ => Vec::new(),
3774 }
3775 }
3776 }
3777}