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