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