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 hvlite_defs::config::Config;
63use hvlite_defs::config::DEFAULT_MMIO_GAPS_AARCH64;
64use hvlite_defs::config::DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2;
65use hvlite_defs::config::DEFAULT_MMIO_GAPS_X86;
66use hvlite_defs::config::DEFAULT_MMIO_GAPS_X86_WITH_VTL2;
67use hvlite_defs::config::DEFAULT_PCAT_BOOT_ORDER;
68use hvlite_defs::config::DEFAULT_PCIE_ECAM_BASE;
69use hvlite_defs::config::DeviceVtl;
70use hvlite_defs::config::EfiDiagnosticsLogLevelType;
71use hvlite_defs::config::HypervisorConfig;
72use hvlite_defs::config::LateMapVtl0MemoryPolicy;
73use hvlite_defs::config::LoadMode;
74use hvlite_defs::config::MemoryConfig;
75use hvlite_defs::config::PcieRootComplexConfig;
76use hvlite_defs::config::PcieRootPortConfig;
77use hvlite_defs::config::PcieSwitchConfig;
78use hvlite_defs::config::ProcessorTopologyConfig;
79use hvlite_defs::config::SerialInformation;
80use hvlite_defs::config::VirtioBus;
81use hvlite_defs::config::VmbusConfig;
82use hvlite_defs::config::VpciDeviceConfig;
83use hvlite_defs::config::Vtl2BaseAddressType;
84use hvlite_defs::config::Vtl2Config;
85use hvlite_defs::rpc::PulseSaveRestoreError;
86use hvlite_defs::rpc::VmRpc;
87use hvlite_defs::worker::VM_WORKER;
88use hvlite_defs::worker::VmWorkerParameters;
89use hvlite_helpers::disk::create_disk_type;
90use hvlite_helpers::disk::open_disk_type;
91use input_core::MultiplexedInputHandle;
92use inspect::InspectMut;
93use inspect::InspectionBuilder;
94use io::Read;
95use mesh::CancelContext;
96use mesh::CellUpdater;
97use mesh::error::RemoteError;
98use mesh::rpc::Rpc;
99use mesh::rpc::RpcError;
100use mesh::rpc::RpcSend;
101use mesh_worker::WorkerEvent;
102use mesh_worker::WorkerHandle;
103use mesh_worker::launch_local_worker;
104use meshworker::VmmMesh;
105use net_backend_resources::mac_address::MacAddress;
106use pal_async::DefaultDriver;
107use pal_async::DefaultPool;
108use pal_async::pipe::PolledPipe;
109use pal_async::socket::PolledSocket;
110use pal_async::task::Spawn;
111use pal_async::task::Task;
112use pal_async::timer::PolledTimer;
113use scsidisk_resources::SimpleScsiDiskHandle;
114use scsidisk_resources::SimpleScsiDvdHandle;
115use serial_16550_resources::ComPort;
116use serial_core::resources::DisconnectedSerialBackendHandle;
117use serial_io::SerialIo;
118use sparse_mmap::alloc_shared_memory;
119use std::cell::RefCell;
120use std::fmt::Write as _;
121use std::future::pending;
122use std::io;
123#[cfg(unix)]
124use std::io::IsTerminal;
125use std::io::Write;
126use std::net::TcpListener;
127use std::path::Path;
128use std::path::PathBuf;
129use std::pin::pin;
130use std::sync::Arc;
131use std::thread;
132use std::time::Duration;
133use std::time::Instant;
134use storvsp_resources::ScsiControllerRequest;
135use storvsp_resources::ScsiDeviceAndPath;
136use storvsp_resources::ScsiPath;
137use tpm_resources::TpmDeviceHandle;
138use tpm_resources::TpmRegisterLayout;
139use tracing_helpers::AnyhowValueExt;
140use ttrpc::TtrpcWorker;
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 hvlite_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 ged_rpc: Option<mesh::Sender<get_resources::ged::GuestEmulationRequest>>,
204 #[cfg(windows)]
205 switch_ports: Vec<vmswitch::kernel::SwitchPort>,
206}
207
208struct ConsoleState<'a> {
209 device: &'a str,
210 input: Box<dyn AsyncWrite + Unpin + Send>,
211}
212
213fn build_switch_list(all_switches: &[cli_args::GenericPcieSwitchCli]) -> Vec<PcieSwitchConfig> {
218 all_switches
219 .iter()
220 .map(|switch_cli| PcieSwitchConfig {
221 name: switch_cli.name.clone(),
222 num_downstream_ports: switch_cli.num_downstream_ports,
223 parent_port: switch_cli.port_name.clone(),
224 })
225 .collect()
226}
227
228fn vm_config_from_command_line(
229 spawner: impl Spawn,
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 + " [OpenVMM]"),
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(hvlite_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 })
724 .collect();
725
726 PcieRootComplexConfig {
727 index: i as u32,
728 name: cli.name.clone(),
729 segment: cli.segment,
730 start_bus: cli.start_bus,
731 end_bus: cli.end_bus,
732 low_mmio_size: cli.low_mmio,
733 high_mmio_size: cli.high_mmio,
734 ports,
735 }
736 })
737 .collect();
738
739 #[cfg(windows)]
740 let vpci_resources: Vec<_> = opt
741 .device
742 .iter()
743 .map(|path| -> anyhow::Result<_> {
744 Ok(virt_whp::device::DeviceHandle(
745 whp::VpciResource::new(
746 None,
747 Default::default(),
748 &whp::VpciResourceDescriptor::Sriov(path, 0, 0),
749 )
750 .with_context(|| format!("opening PCI device {}", path))?,
751 ))
752 })
753 .collect::<Result<_, _>>()?;
754
755 #[cfg(windows)]
757 let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
758 Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
759 } else {
760 None
761 };
762
763 let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc || opt.pcat {
764 let vram = alloc_shared_memory(FRAMEBUFFER_SIZE)?;
765 let (fb, fba) =
766 framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
767 resources.framebuffer_access = Some(fba);
768 Some(fb)
769 } else {
770 None
771 };
772
773 let is_arm = cfg!(guest_arch = "aarch64");
774 let is_x86 = cfg!(guest_arch = "x86_64");
775
776 let load_mode;
777 let with_hv;
778
779 let any_serial_configured = serial0_cfg.is_some()
780 || serial1_cfg.is_some()
781 || serial2_cfg.is_some()
782 || serial3_cfg.is_some();
783
784 let has_com3 = serial2_cfg.is_some();
785
786 let mut chipset = VmManifestBuilder::new(
787 if opt.igvm.is_some() {
788 BaseChipsetType::HclHost
789 } else if opt.pcat {
790 BaseChipsetType::HypervGen1
791 } else if opt.uefi {
792 BaseChipsetType::HypervGen2Uefi
793 } else if opt.hv {
794 BaseChipsetType::HyperVGen2LinuxDirect
795 } else {
796 BaseChipsetType::UnenlightenedLinuxDirect
797 },
798 if is_x86 {
799 MachineArch::X86_64
800 } else {
801 MachineArch::Aarch64
802 },
803 );
804
805 if framebuffer.is_some() {
806 chipset = chipset.with_framebuffer();
807 }
808 if opt.guest_watchdog {
809 chipset = chipset.with_guest_watchdog();
810 }
811 if any_serial_configured {
812 chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
813 }
814 if opt.battery {
815 let (tx, rx) = mesh::channel();
816 tx.send(HostBatteryUpdate::default_present());
817 chipset = chipset.with_battery(rx);
818 }
819 if let Some(cfg) = &opt.debugcon {
820 chipset = chipset.with_debugcon(
821 debugcon_cfg.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
822 cfg.port,
823 );
824 }
825
826 let bios_guid = Guid::new_random();
828
829 let VmChipsetResult {
830 chipset,
831 mut chipset_devices,
832 } = chipset
833 .build()
834 .context("failed to build chipset configuration")?;
835
836 if let Some(path) = &opt.igvm {
837 let file = fs_err::File::open(path)
838 .context("failed to open igvm file")?
839 .into();
840 let cmdline = opt.cmdline.join(" ");
841 with_hv = true;
842
843 load_mode = LoadMode::Igvm {
844 file,
845 cmdline,
846 vtl2_base_address: opt.igvm_vtl2_relocation_type,
847 com_serial: has_com3.then(|| SerialInformation {
848 io_port: ComPort::Com3.io_port(),
849 irq: ComPort::Com3.irq().into(),
850 }),
851 };
852 } else if opt.pcat {
853 if !is_x86 {
855 anyhow::bail!("pcat not supported on this architecture");
856 }
857 with_hv = true;
858
859 let firmware = hvlite_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
860 load_mode = LoadMode::Pcat {
861 firmware,
862 boot_order: opt
863 .pcat_boot_order
864 .map(|x| x.0)
865 .unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
866 };
867 } else if opt.uefi {
868 use hvlite_defs::config::UefiConsoleMode;
869
870 with_hv = true;
871
872 let firmware = fs_err::File::open(
873 (opt.uefi_firmware.0)
874 .as_ref()
875 .context("must provide uefi firmware when booting with uefi")?,
876 )
877 .context("failed to open uefi firmware")?;
878
879 load_mode = LoadMode::Uefi {
882 firmware: firmware.into(),
883 enable_debugging: opt.uefi_debug,
884 enable_memory_protections: opt.uefi_enable_memory_protections,
885 disable_frontpage: opt.disable_frontpage,
886 enable_tpm: opt.tpm,
887 enable_battery: opt.battery,
888 enable_serial: any_serial_configured,
889 enable_vpci_boot: false,
890 uefi_console_mode: opt.uefi_console_mode.map(|m| match m {
891 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
892 UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
893 UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
894 UefiConsoleModeCli::None => UefiConsoleMode::None,
895 }),
896 default_boot_always_attempt: opt.default_boot_always_attempt,
897 bios_guid,
898 };
899 } else {
900 let mut cmdline = "panic=-1 debug".to_string();
902
903 with_hv = opt.hv;
904 if with_hv {
905 cmdline += " pci=off";
906 }
907
908 if !console_str.is_empty() {
909 let _ = write!(&mut cmdline, " console={}", console_str);
910 }
911 if opt.gfx {
912 cmdline += " console=tty";
913 }
914 for extra in &opt.cmdline {
915 let _ = write!(&mut cmdline, " {}", extra);
916 }
917
918 let kernel = fs_err::File::open(
919 (opt.kernel.0)
920 .as_ref()
921 .context("must provide kernel when booting with linux direct")?,
922 )
923 .context("failed to open kernel")?;
924 let initrd = (opt.initrd.0)
925 .as_ref()
926 .map(fs_err::File::open)
927 .transpose()
928 .context("failed to open initrd")?;
929
930 let custom_dsdt = match &opt.custom_dsdt {
931 Some(path) => {
932 let mut v = Vec::new();
933 fs_err::File::open(path)
934 .context("failed to open custom dsdt")?
935 .read_to_end(&mut v)
936 .context("failed to read custom dsdt")?;
937 Some(v)
938 }
939 None => None,
940 };
941
942 load_mode = LoadMode::Linux {
943 kernel: kernel.into(),
944 initrd: initrd.map(Into::into),
945 cmdline,
946 custom_dsdt,
947 enable_serial: any_serial_configured,
948 };
949 }
950
951 let mut vmgs = Some(if let Some(VmgsCli { kind, provision }) = &opt.vmgs {
952 let disk = VmgsDisk {
953 disk: disk_open(kind, false).context("failed to open vmgs disk")?,
954 encryption_policy: if opt.test_gsp_by_id {
955 GuestStateEncryptionPolicy::GspById(true)
956 } else {
957 GuestStateEncryptionPolicy::None(true)
958 },
959 };
960 match provision {
961 ProvisionVmgs::OnEmpty => VmgsResource::Disk(disk),
962 ProvisionVmgs::OnFailure => VmgsResource::ReprovisionOnFailure(disk),
963 ProvisionVmgs::True => VmgsResource::Reprovision(disk),
964 }
965 } else {
966 VmgsResource::Ephemeral
967 });
968
969 if with_get && with_hv {
970 let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
971 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
972 fixed: Some(Default::default()),
973 dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
974 storage_controllers: storage.build_underhill(),
975 nic_devices: underhill_nics,
976 }),
977 namespace_settings: Vec::default(),
978 };
979
980 let (send, guest_request_recv) = mesh::channel();
981 resources.ged_rpc = Some(send);
982
983 let vmgs = vmgs.take().unwrap();
984
985 vmbus_devices.extend([
986 (
987 openhcl_vtl,
988 get_resources::gel::GuestEmulationLogHandle.into_resource(),
989 ),
990 (
991 openhcl_vtl,
992 get_resources::ged::GuestEmulationDeviceHandle {
993 firmware: if opt.pcat {
994 get_resources::ged::GuestFirmwareConfig::Pcat {
995 boot_order: opt
996 .pcat_boot_order
997 .map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
998 .map(|x| match x {
999 hvlite_defs::config::PcatBootDevice::Floppy => {
1000 get_resources::ged::PcatBootDevice::Floppy
1001 }
1002 hvlite_defs::config::PcatBootDevice::HardDrive => {
1003 get_resources::ged::PcatBootDevice::HardDrive
1004 }
1005 hvlite_defs::config::PcatBootDevice::Optical => {
1006 get_resources::ged::PcatBootDevice::Optical
1007 }
1008 hvlite_defs::config::PcatBootDevice::Network => {
1009 get_resources::ged::PcatBootDevice::Network
1010 }
1011 }),
1012 }
1013 } else {
1014 use get_resources::ged::UefiConsoleMode;
1015
1016 get_resources::ged::GuestFirmwareConfig::Uefi {
1017 enable_vpci_boot: storage.has_vtl0_nvme(),
1018 firmware_debug: opt.uefi_debug,
1019 disable_frontpage: opt.disable_frontpage,
1020 console_mode: match opt.uefi_console_mode.unwrap_or(UefiConsoleModeCli::Default) {
1021 UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1022 UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
1023 UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
1024 UefiConsoleModeCli::None => UefiConsoleMode::None,
1025 },
1026 default_boot_always_attempt: opt.default_boot_always_attempt,
1027 }
1028 },
1029 com1: with_vmbus_com1_serial,
1030 com2: with_vmbus_com2_serial,
1031 vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
1032 vmbus_redirection: opt.vmbus_redirect,
1033 vmgs,
1034 framebuffer: opt
1035 .vtl2_gfx
1036 .then(|| SharedFramebufferHandle.into_resource()),
1037 guest_request_recv,
1038 enable_tpm: opt.tpm,
1039 firmware_event_send: None,
1040 secure_boot_enabled: opt.secure_boot,
1041 secure_boot_template: match opt.secure_boot_template {
1042 Some(SecureBootTemplateCli::Windows) => {
1043 get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1044 },
1045 Some(SecureBootTemplateCli::UefiCa) => {
1046 get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1047 }
1048 None => {
1049 get_resources::ged::GuestSecureBootTemplateType::None
1050 },
1051 },
1052 enable_battery: opt.battery,
1053 no_persistent_secrets: true,
1054 igvm_attest_test_config: None,
1055 test_gsp_by_id: opt.test_gsp_by_id,
1056 efi_diagnostics_log_level: {
1057 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1058 EfiDiagnosticsLogLevelCli::Default => get_resources::ged::EfiDiagnosticsLogLevelType::Default,
1059 EfiDiagnosticsLogLevelCli::Info => get_resources::ged::EfiDiagnosticsLogLevelType::Info,
1060 EfiDiagnosticsLogLevelCli::Full => get_resources::ged::EfiDiagnosticsLogLevelType::Full,
1061 }
1062 },
1063 }
1064 .into_resource(),
1065 ),
1066 ]);
1067 }
1068
1069 if opt.tpm && !opt.vtl2 {
1070 let register_layout = if cfg!(guest_arch = "x86_64") {
1071 TpmRegisterLayout::IoPort
1072 } else {
1073 TpmRegisterLayout::Mmio
1074 };
1075
1076 let (ppi_store, nvram_store) = if opt.vmgs.is_some() {
1077 (
1078 VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1079 VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1080 )
1081 } else {
1082 (
1083 EphemeralNonVolatileStoreHandle.into_resource(),
1084 EphemeralNonVolatileStoreHandle.into_resource(),
1085 )
1086 };
1087
1088 chipset_devices.push(ChipsetDeviceHandle {
1089 name: "tpm".to_string(),
1090 resource: TpmDeviceHandle {
1091 ppi_store,
1092 nvram_store,
1093 refresh_tpm_seeds: false,
1094 ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1095 register_layout,
1096 guest_secret_key: None,
1097 logger: None,
1098 is_confidential_vm: false,
1099 bios_guid,
1100 }
1101 .into_resource(),
1102 });
1103 }
1104
1105 let custom_uefi_vars = {
1106 use firmware_uefi_custom_vars::CustomVars;
1107
1108 let base_vars = match opt.secure_boot_template {
1111 Some(template) => {
1112 if is_x86 {
1113 match template {
1114 SecureBootTemplateCli::Windows => {
1115 hyperv_secure_boot_templates::x64::microsoft_windows()
1116 }
1117 SecureBootTemplateCli::UefiCa => {
1118 hyperv_secure_boot_templates::x64::microsoft_uefi_ca()
1119 }
1120 }
1121 } else if is_arm {
1122 match template {
1123 SecureBootTemplateCli::Windows => {
1124 hyperv_secure_boot_templates::aarch64::microsoft_windows()
1125 }
1126 SecureBootTemplateCli::UefiCa => {
1127 hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca()
1128 }
1129 }
1130 } else {
1131 anyhow::bail!("no secure boot template for current guest_arch")
1132 }
1133 }
1134 None => CustomVars::default(),
1135 };
1136
1137 let custom_uefi_json_data = match &opt.custom_uefi_json {
1140 Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?),
1141 None => None,
1142 };
1143
1144 match custom_uefi_json_data {
1146 Some(data) => {
1147 let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?;
1148 base_vars.apply_delta(delta)?
1149 }
1150 None => base_vars,
1151 }
1152 };
1153
1154 let vga_firmware = if opt.pcat {
1155 Some(hvlite_pcat_locator::find_svga_bios(
1156 opt.vga_firmware.as_deref(),
1157 )?)
1158 } else {
1159 None
1160 };
1161
1162 if opt.gfx {
1163 vmbus_devices.extend([
1164 (
1165 DeviceVtl::Vtl0,
1166 SynthVideoHandle {
1167 framebuffer: SharedFramebufferHandle.into_resource(),
1168 }
1169 .into_resource(),
1170 ),
1171 (
1172 DeviceVtl::Vtl0,
1173 SynthKeyboardHandle {
1174 source: MultiplexedInputHandle {
1175 elevation: 1,
1177 }
1178 .into_resource(),
1179 }
1180 .into_resource(),
1181 ),
1182 (
1183 DeviceVtl::Vtl0,
1184 SynthMouseHandle {
1185 source: MultiplexedInputHandle {
1186 elevation: 1,
1188 }
1189 .into_resource(),
1190 }
1191 .into_resource(),
1192 ),
1193 ]);
1194 }
1195
1196 let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
1197 if let Some(path) = path {
1198 cleanup_socket(path.as_ref());
1199 let listener = unix_socket::UnixListener::bind(path)
1200 .with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
1201 Ok(Some(listener))
1202 } else {
1203 Ok(None)
1204 }
1205 };
1206
1207 let vtl0_vsock_listener = vsock_listener(opt.vsock_path.as_deref())?;
1208 let vtl2_vsock_listener = vsock_listener(opt.vtl2_vsock_path.as_deref())?;
1209
1210 let mmio_gaps = if opt.vtl2
1213 && !matches!(
1214 opt.igvm_vtl2_relocation_type,
1215 Vtl2BaseAddressType::Vtl2Allocate { .. },
1216 ) {
1217 if is_x86 {
1218 DEFAULT_MMIO_GAPS_X86_WITH_VTL2.into()
1219 } else {
1220 DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2.into()
1221 }
1222 } else if is_x86 {
1223 DEFAULT_MMIO_GAPS_X86.into()
1224 } else {
1225 DEFAULT_MMIO_GAPS_AARCH64.into()
1226 };
1227
1228 if let Some(path) = &opt.openhcl_dump_path {
1229 let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
1230 task.detach();
1231 vmbus_devices.push((openhcl_vtl, resource));
1232 }
1233
1234 #[cfg(guest_arch = "aarch64")]
1235 let topology_arch = hvlite_defs::config::ArchTopologyConfig::Aarch64(
1236 hvlite_defs::config::Aarch64TopologyConfig {
1237 gic_config: None,
1239 pmu_gsiv: hvlite_defs::config::PmuGsivConfig::Platform,
1240 },
1241 );
1242 #[cfg(guest_arch = "x86_64")]
1243 let topology_arch =
1244 hvlite_defs::config::ArchTopologyConfig::X86(hvlite_defs::config::X86TopologyConfig {
1245 apic_id_offset: opt.apic_id_offset,
1246 x2apic: opt.x2apic,
1247 });
1248
1249 let with_isolation = if let Some(isolation) = &opt.isolation {
1250 if !opt.vtl2 {
1252 anyhow::bail!("isolation is only currently supported with vtl2");
1253 }
1254
1255 if !opt.no_alias_map {
1257 anyhow::bail!("alias map not supported with isolation");
1258 }
1259
1260 match isolation {
1261 cli_args::IsolationCli::Vbs => Some(hvlite_defs::config::IsolationType::Vbs),
1262 }
1263 } else {
1264 None
1265 };
1266
1267 if with_hv {
1268 let (shutdown_send, shutdown_recv) = mesh::channel();
1269 resources.shutdown_ic = Some(shutdown_send);
1270 let (kvp_send, kvp_recv) = mesh::channel();
1271 resources.kvp_ic = Some(kvp_send);
1272 vmbus_devices.extend(
1273 [
1274 hyperv_ic_resources::shutdown::ShutdownIcHandle {
1275 recv: shutdown_recv,
1276 }
1277 .into_resource(),
1278 hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_recv }.into_resource(),
1279 hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
1280 ]
1281 .map(|r| (DeviceVtl::Vtl0, r)),
1282 );
1283 }
1284
1285 if let Some(hive_path) = &opt.imc {
1286 let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
1287 vmbus_devices.push((
1288 DeviceVtl::Vtl0,
1289 vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
1290 ));
1291 }
1292
1293 let mut virtio_devices = Vec::new();
1294 let mut add_virtio_device = |bus, resource: Resource<VirtioDeviceHandle>| {
1295 let bus = match bus {
1296 VirtioBusCli::Auto => {
1297 if with_hv && (cfg!(windows) || cfg!(target_os = "macos")) {
1300 None
1301 } else {
1302 Some(VirtioBus::Pci)
1303 }
1304 }
1305 VirtioBusCli::Mmio => Some(VirtioBus::Mmio),
1306 VirtioBusCli::Pci => Some(VirtioBus::Pci),
1307 VirtioBusCli::Vpci => None,
1308 };
1309 if let Some(bus) = bus {
1310 virtio_devices.push((bus, resource));
1311 } else {
1312 vpci_devices.push(VpciDeviceConfig {
1313 vtl: DeviceVtl::Vtl0,
1314 instance_id: Guid::new_random(),
1315 resource: VirtioPciDeviceHandle(resource).into_resource(),
1316 });
1317 }
1318 };
1319
1320 for cli_cfg in &opt.virtio_net {
1321 if cli_cfg.underhill {
1322 anyhow::bail!("use --net uh:[...] to add underhill NICs")
1323 }
1324 let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
1325 add_virtio_device(
1326 VirtioBusCli::Auto,
1327 virtio_resources::net::VirtioNetHandle {
1328 max_queues: vport.max_queues,
1329 mac_address: vport.mac_address,
1330 endpoint: vport.endpoint,
1331 }
1332 .into_resource(),
1333 );
1334 }
1335
1336 for args in &opt.virtio_fs {
1337 add_virtio_device(
1338 opt.virtio_fs_bus,
1339 virtio_resources::fs::VirtioFsHandle {
1340 tag: args.tag.clone(),
1341 fs: virtio_resources::fs::VirtioFsBackend::HostFs {
1342 root_path: args.path.clone(),
1343 mount_options: args.options.clone(),
1344 },
1345 }
1346 .into_resource(),
1347 );
1348 }
1349
1350 for args in &opt.virtio_fs_shmem {
1351 add_virtio_device(
1352 opt.virtio_fs_bus,
1353 virtio_resources::fs::VirtioFsHandle {
1354 tag: args.tag.clone(),
1355 fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
1356 root_path: args.path.clone(),
1357 },
1358 }
1359 .into_resource(),
1360 );
1361 }
1362
1363 for args in &opt.virtio_9p {
1364 add_virtio_device(
1365 VirtioBusCli::Auto,
1366 virtio_resources::p9::VirtioPlan9Handle {
1367 tag: args.tag.clone(),
1368 root_path: args.path.clone(),
1369 debug: opt.virtio_9p_debug,
1370 }
1371 .into_resource(),
1372 );
1373 }
1374
1375 if let Some(path) = &opt.virtio_pmem {
1376 add_virtio_device(
1377 VirtioBusCli::Auto,
1378 virtio_resources::pmem::VirtioPmemHandle { path: path.clone() }.into_resource(),
1379 );
1380 }
1381
1382 let mut cfg = Config {
1383 chipset,
1384 load_mode,
1385 floppy_disks,
1386 pcie_root_complexes,
1387 pcie_devices: Vec::new(),
1388 pcie_switches,
1389 vpci_devices,
1390 ide_disks: Vec::new(),
1391 memory: MemoryConfig {
1392 mem_size: opt.memory,
1393 mmio_gaps,
1394 prefetch_memory: opt.prefetch,
1395 pcie_ecam_base: DEFAULT_PCIE_ECAM_BASE,
1396 },
1397 processor_topology: ProcessorTopologyConfig {
1398 proc_count: opt.processors,
1399 vps_per_socket: opt.vps_per_socket,
1400 enable_smt: match opt.smt {
1401 cli_args::SmtConfigCli::Auto => None,
1402 cli_args::SmtConfigCli::Force => Some(true),
1403 cli_args::SmtConfigCli::Off => Some(false),
1404 },
1405 arch: Some(topology_arch),
1406 },
1407 hypervisor: HypervisorConfig {
1408 with_hv,
1409 with_vtl2: opt.vtl2.then_some(Vtl2Config {
1410 vtl0_alias_map: !opt.no_alias_map,
1411 late_map_vtl0_memory: match opt.late_map_vtl0_policy {
1412 cli_args::Vtl0LateMapPolicyCli::Off => None,
1413 cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
1414 cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
1415 cli_args::Vtl0LateMapPolicyCli::Exception => {
1416 Some(LateMapVtl0MemoryPolicy::InjectException)
1417 }
1418 },
1419 }),
1420 with_isolation,
1421 user_mode_hv_enlightenments: opt.no_enlightenments,
1422 user_mode_apic: opt.user_mode_apic,
1423 },
1424 #[cfg(windows)]
1425 kernel_vmnics,
1426 input: mesh::Receiver::new(),
1427 framebuffer,
1428 vga_firmware,
1429 vtl2_gfx: opt.vtl2_gfx,
1430 virtio_console_pci: opt.virtio_console_pci,
1431 virtio_serial: virtio_serial_cfg,
1432 virtio_devices,
1433 vmbus: with_hv.then_some(VmbusConfig {
1434 vsock_listener: vtl0_vsock_listener,
1435 vsock_path: opt.vsock_path.clone(),
1436 vtl2_redirect: opt.vmbus_redirect,
1437 vmbus_max_version: opt.vmbus_max_version,
1438 #[cfg(windows)]
1439 vmbusproxy_handle,
1440 }),
1441 vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
1442 vsock_listener: vtl2_vsock_listener,
1443 vsock_path: opt.vtl2_vsock_path.clone(),
1444 ..Default::default()
1445 }),
1446 vmbus_devices,
1447 chipset_devices,
1448 #[cfg(windows)]
1449 vpci_resources,
1450 vmgs,
1451 secure_boot_enabled: opt.secure_boot,
1452 custom_uefi_vars,
1453 firmware_event_send: None,
1454 debugger_rpc: None,
1455 generation_id_recv: None,
1456 rtc_delta_milliseconds: 0,
1457 automatic_guest_reset: !opt.halt_on_reset,
1458 efi_diagnostics_log_level: {
1459 match opt.efi_diagnostics_log_level.unwrap_or_default() {
1460 EfiDiagnosticsLogLevelCli::Default => EfiDiagnosticsLogLevelType::Default,
1461 EfiDiagnosticsLogLevelCli::Info => EfiDiagnosticsLogLevelType::Info,
1462 EfiDiagnosticsLogLevelCli::Full => EfiDiagnosticsLogLevelType::Full,
1463 }
1464 },
1465 };
1466
1467 storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
1468 Ok((cfg, resources))
1469}
1470
1471fn openvmm_terminal_app() -> Option<PathBuf> {
1473 std::env::var_os("OPENVMM_TERM")
1474 .or_else(|| std::env::var_os("HVLITE_TERM"))
1475 .map(Into::into)
1476}
1477
1478fn cleanup_socket(path: &Path) {
1480 #[cfg(windows)]
1481 let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
1482 #[cfg(not(windows))]
1483 let is_socket = path
1484 .metadata()
1485 .is_ok_and(|meta| std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type()));
1486
1487 if is_socket {
1488 let _ = std::fs::remove_file(path);
1489 }
1490}
1491
1492#[cfg(windows)]
1493const DEFAULT_SWITCH: &str = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444";
1494
1495#[cfg(windows)]
1496fn new_switch_port(
1497 switch_id: &str,
1498) -> anyhow::Result<(
1499 hvlite_defs::config::SwitchPortId,
1500 vmswitch::kernel::SwitchPort,
1501)> {
1502 let id = vmswitch::kernel::SwitchPortId {
1503 switch: switch_id.parse().context("invalid switch id")?,
1504 port: Guid::new_random(),
1505 };
1506 let _ = vmswitch::hcn::Network::open(&id.switch)
1507 .with_context(|| format!("could not find switch {}", id.switch))?;
1508
1509 let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
1510
1511 let id = hvlite_defs::config::SwitchPortId {
1512 switch: id.switch,
1513 port: id.port,
1514 };
1515 Ok((id, port))
1516}
1517
1518fn parse_endpoint(
1519 cli_cfg: &NicConfigCli,
1520 index: &mut usize,
1521 resources: &mut VmResources,
1522) -> anyhow::Result<NicConfig> {
1523 let _ = resources;
1524 let endpoint = match &cli_cfg.endpoint {
1525 EndpointConfigCli::Consomme { cidr } => {
1526 net_backend_resources::consomme::ConsommeHandle { cidr: cidr.clone() }.into_resource()
1527 }
1528 EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
1529 EndpointConfigCli::Dio { id } => {
1530 #[cfg(windows)]
1531 {
1532 let (port_id, port) = new_switch_port(id.as_deref().unwrap_or(DEFAULT_SWITCH))?;
1533 resources.switch_ports.push(port);
1534 net_backend_resources::dio::WindowsDirectIoHandle {
1535 switch_port_id: net_backend_resources::dio::SwitchPortId {
1536 switch: port_id.switch,
1537 port: port_id.port,
1538 },
1539 }
1540 .into_resource()
1541 }
1542
1543 #[cfg(not(windows))]
1544 {
1545 let _ = id;
1546 bail!("cannot use dio on non-windows platforms")
1547 }
1548 }
1549 EndpointConfigCli::Tap { name } => {
1550 net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
1551 }
1552 };
1553
1554 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
1556 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
1557
1558 const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-da43-11ed-936a-00155d6db52f");
1560 let instance_id = Guid {
1561 data1: *index as u32,
1562 ..BASE_INSTANCE_ID
1563 };
1564 *index += 1;
1565
1566 Ok(NicConfig {
1567 vtl: cli_cfg.vtl,
1568 instance_id,
1569 endpoint,
1570 mac_address: mac_address.into(),
1571 max_queues: cli_cfg.max_queues,
1572 })
1573}
1574
1575#[derive(Debug)]
1576struct NicConfig {
1577 vtl: DeviceVtl,
1578 instance_id: Guid,
1579 mac_address: MacAddress,
1580 endpoint: Resource<NetEndpointHandleKind>,
1581 max_queues: Option<u16>,
1582}
1583
1584impl NicConfig {
1585 fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
1586 (
1587 self.vtl,
1588 netvsp_resources::NetvspHandle {
1589 instance_id: self.instance_id,
1590 mac_address: self.mac_address,
1591 endpoint: self.endpoint,
1592 max_queues: self.max_queues,
1593 }
1594 .into_resource(),
1595 )
1596 }
1597}
1598
1599enum LayerOrDisk {
1600 Layer(DiskLayerDescription),
1601 Disk(Resource<DiskHandleKind>),
1602}
1603
1604fn disk_open(disk_cli: &DiskCliKind, read_only: bool) -> anyhow::Result<Resource<DiskHandleKind>> {
1605 let mut layers = Vec::new();
1606 disk_open_inner(disk_cli, read_only, &mut layers)?;
1607 if layers.len() == 1 && matches!(layers[0], LayerOrDisk::Disk(_)) {
1608 let LayerOrDisk::Disk(disk) = layers.pop().unwrap() else {
1609 unreachable!()
1610 };
1611 Ok(disk)
1612 } else {
1613 Ok(Resource::new(disk_backend_resources::LayeredDiskHandle {
1614 layers: layers
1615 .into_iter()
1616 .map(|layer| match layer {
1617 LayerOrDisk::Layer(layer) => layer,
1618 LayerOrDisk::Disk(disk) => DiskLayerDescription {
1619 layer: DiskLayerHandle(disk).into_resource(),
1620 read_cache: false,
1621 write_through: false,
1622 },
1623 })
1624 .collect(),
1625 }))
1626 }
1627}
1628
1629fn disk_open_inner(
1630 disk_cli: &DiskCliKind,
1631 read_only: bool,
1632 layers: &mut Vec<LayerOrDisk>,
1633) -> anyhow::Result<()> {
1634 fn layer<T: IntoResource<DiskLayerHandleKind>>(layer: T) -> LayerOrDisk {
1635 LayerOrDisk::Layer(layer.into_resource().into())
1636 }
1637 fn disk<T: IntoResource<DiskHandleKind>>(disk: T) -> LayerOrDisk {
1638 LayerOrDisk::Disk(disk.into_resource())
1639 }
1640 match disk_cli {
1641 &DiskCliKind::Memory(len) => {
1642 layers.push(layer(RamDiskLayerHandle { len: Some(len) }));
1643 }
1644 DiskCliKind::File {
1645 path,
1646 create_with_len,
1647 } => layers.push(LayerOrDisk::Disk(if let Some(size) = create_with_len {
1648 create_disk_type(path, *size)
1649 .with_context(|| format!("failed to create {}", path.display()))?
1650 } else {
1651 open_disk_type(path, read_only)
1652 .with_context(|| format!("failed to open {}", path.display()))?
1653 })),
1654 DiskCliKind::Blob { kind, url } => {
1655 layers.push(disk(disk_backend_resources::BlobDiskHandle {
1656 url: url.to_owned(),
1657 format: match kind {
1658 cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
1659 cli_args::BlobKind::Vhd1 => disk_backend_resources::BlobDiskFormat::FixedVhd1,
1660 },
1661 }))
1662 }
1663 DiskCliKind::MemoryDiff(inner) => {
1664 layers.push(layer(RamDiskLayerHandle { len: None }));
1665 disk_open_inner(inner, true, layers)?;
1666 }
1667 DiskCliKind::PersistentReservationsWrapper(inner) => layers.push(disk(
1668 disk_backend_resources::DiskWithReservationsHandle(disk_open(inner, read_only)?),
1669 )),
1670 DiskCliKind::DelayDiskWrapper {
1671 delay_ms,
1672 disk: inner,
1673 } => layers.push(disk(DelayDiskHandle {
1674 delay: CellUpdater::new(Duration::from_millis(*delay_ms)).cell(),
1675 disk: disk_open(inner, read_only)?,
1676 })),
1677 DiskCliKind::Crypt {
1678 disk: inner,
1679 cipher,
1680 key_file,
1681 } => layers.push(disk(disk_crypt_resources::DiskCryptHandle {
1682 disk: disk_open(inner, read_only)?,
1683 cipher: match cipher {
1684 cli_args::DiskCipher::XtsAes256 => disk_crypt_resources::Cipher::XtsAes256,
1685 },
1686 key: fs_err::read(key_file).context("failed to read key file")?,
1687 })),
1688 DiskCliKind::Sqlite {
1689 path,
1690 create_with_len,
1691 } => {
1692 match (create_with_len.is_some(), path.exists()) {
1697 (true, true) => anyhow::bail!(
1698 "cannot create new sqlite disk at {} - file already exists",
1699 path.display()
1700 ),
1701 (false, false) => anyhow::bail!(
1702 "cannot open sqlite disk at {} - file not found",
1703 path.display()
1704 ),
1705 _ => {}
1706 }
1707
1708 layers.push(layer(SqliteDiskLayerHandle {
1709 dbhd_path: path.display().to_string(),
1710 format_dbhd: create_with_len.map(|len| {
1711 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1712 logically_read_only: false,
1713 len: Some(len),
1714 }
1715 }),
1716 }));
1717 }
1718 DiskCliKind::SqliteDiff { path, create, disk } => {
1719 match (create, path.exists()) {
1724 (true, true) => anyhow::bail!(
1725 "cannot create new sqlite disk at {} - file already exists",
1726 path.display()
1727 ),
1728 (false, false) => anyhow::bail!(
1729 "cannot open sqlite disk at {} - file not found",
1730 path.display()
1731 ),
1732 _ => {}
1733 }
1734
1735 layers.push(layer(SqliteDiskLayerHandle {
1736 dbhd_path: path.display().to_string(),
1737 format_dbhd: create.then_some(
1738 disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1739 logically_read_only: false,
1740 len: None,
1741 },
1742 ),
1743 }));
1744 disk_open_inner(disk, true, layers)?;
1745 }
1746 DiskCliKind::AutoCacheSqlite {
1747 cache_path,
1748 key,
1749 disk,
1750 } => {
1751 layers.push(LayerOrDisk::Layer(DiskLayerDescription {
1752 read_cache: true,
1753 write_through: false,
1754 layer: SqliteAutoCacheDiskLayerHandle {
1755 cache_path: cache_path.clone(),
1756 cache_key: key.clone(),
1757 }
1758 .into_resource(),
1759 }));
1760 disk_open_inner(disk, read_only, layers)?;
1761 }
1762 }
1763 Ok(())
1764}
1765
1766fn do_main() -> anyhow::Result<()> {
1767 #[cfg(windows)]
1768 pal::windows::disable_hard_error_dialog();
1769
1770 tracing_init::enable_tracing()?;
1771
1772 meshworker::run_vmm_mesh_host()?;
1776
1777 let opt = Options::parse();
1778 if let Some(path) = &opt.write_saved_state_proto {
1779 mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
1780 .write_to_path(path)
1781 .context("failed to write protobuf descriptors")?;
1782 return Ok(());
1783 }
1784
1785 if let Some(path) = opt.relay_console_path {
1786 let console_title = opt.relay_console_title.unwrap_or_default();
1787 return console_relay::relay_console(&path, console_title.as_str());
1788 }
1789
1790 if let Some(path) = opt.ttrpc.as_ref().or(opt.grpc.as_ref()) {
1791 block_on(async {
1792 let _ = std::fs::remove_file(path);
1793 let listener =
1794 unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
1795
1796 let transport = if opt.ttrpc.is_some() {
1797 ttrpc::RpcTransport::Ttrpc
1798 } else {
1799 ttrpc::RpcTransport::Grpc
1800 };
1801
1802 let mut handle = launch_local_worker::<TtrpcWorker>(ttrpc::Parameters {
1804 listener,
1805 transport,
1806 })
1807 .await?;
1808
1809 tracing::info!(%transport, path = %path.display(), "listening");
1810
1811 pal::close_stdout().context("failed to close stdout")?;
1813
1814 handle.join().await?;
1815
1816 Ok(())
1817 })
1818 } else {
1819 DefaultPool::run_with(async |driver| {
1820 let mesh = VmmMesh::new(&driver, opt.single_process)?;
1821 let result = run_control(&driver, &mesh, opt).await;
1822 mesh.shutdown().await;
1823 result
1824 })
1825 }
1826}
1827
1828fn maybe_with_radix_u64(s: &str) -> Result<u64, String> {
1829 let (radix, prefix_len) = if s.starts_with("0x") || s.starts_with("0X") {
1830 (16, 2)
1831 } else if s.starts_with("0o") || s.starts_with("0O") {
1832 (8, 2)
1833 } else if s.starts_with("0b") || s.starts_with("0B") {
1834 (2, 2)
1835 } else {
1836 (10, 0)
1837 };
1838
1839 u64::from_str_radix(&s[prefix_len..], radix).map_err(|e| format!("{e}"))
1840}
1841
1842#[derive(Parser)]
1843#[clap(
1844 name = "openvmm",
1845 disable_help_flag = true,
1846 disable_version_flag = true,
1847 no_binary_name = true,
1848 help_template("{subcommands}")
1849)]
1850enum InteractiveCommand {
1851 #[clap(visible_alias = "R")]
1855 Restart,
1856
1857 #[clap(visible_alias = "n")]
1859 Nmi,
1860
1861 #[clap(visible_alias = "p")]
1863 Pause,
1864
1865 #[clap(visible_alias = "r")]
1867 Resume,
1868
1869 #[clap(visible_alias = "psr")]
1871 PulseSaveRestore,
1872
1873 #[clap(visible_alias = "spsr")]
1875 SchedulePulseSaveRestore {
1876 interval: Option<u64>,
1879 },
1880
1881 #[clap(visible_alias = "d")]
1883 AddDisk {
1884 #[clap(long = "ro")]
1885 read_only: bool,
1886 #[clap(long = "dvd")]
1887 is_dvd: bool,
1888 #[clap(long, default_value_t)]
1889 target: u8,
1890 #[clap(long, default_value_t)]
1891 path: u8,
1892 #[clap(long, default_value_t)]
1893 lun: u8,
1894 #[clap(long)]
1895 ram: Option<u64>,
1896 file_path: Option<PathBuf>,
1897 },
1898
1899 #[clap(visible_alias = "D")]
1901 RmDisk {
1902 #[clap(long)]
1903 target: u8,
1904 #[clap(long)]
1905 path: u8,
1906 #[clap(long)]
1907 lun: u8,
1908 },
1909
1910 #[clap(visible_alias = "x")]
1912 Inspect {
1913 #[clap(short, long)]
1915 recursive: bool,
1916 #[clap(short, long, requires("recursive"))]
1918 limit: Option<usize>,
1919 #[clap(short = 'v', long)]
1921 paravisor: bool,
1922 element: Option<String>,
1924 #[clap(short, long, conflicts_with("recursive"))]
1926 update: Option<String>,
1927 },
1928
1929 #[clap(visible_alias = "V")]
1931 RestartVnc,
1932
1933 #[clap(visible_alias = "v")]
1935 Hvsock {
1936 #[clap(short, long)]
1938 term: Option<PathBuf>,
1939 port: u32,
1941 },
1942
1943 #[clap(visible_alias = "q")]
1945 Quit,
1946
1947 #[clap(visible_alias = "i")]
1952 Input { data: Vec<String> },
1953
1954 #[clap(visible_alias = "I")]
1958 InputMode,
1959
1960 Reset,
1962
1963 Shutdown {
1965 #[clap(long, short = 'r')]
1967 reboot: bool,
1968 #[clap(long, short = 'h', conflicts_with = "reboot")]
1970 hibernate: bool,
1971 #[clap(long, short = 'f')]
1973 force: bool,
1974 },
1975
1976 #[clap(visible_alias = "ch")]
1979 ClearHalt,
1980
1981 ServiceVtl2 {
1983 #[clap(long, short = 'u')]
1986 user_mode_only: bool,
1987 #[clap(long, conflicts_with("user_mode_only"))]
1990 igvm: Option<PathBuf>,
1991 },
1992
1993 ReadMemory {
1995 #[clap(value_parser=maybe_with_radix_u64)]
1997 gpa: u64,
1998 #[clap(value_parser=maybe_with_radix_u64)]
2000 size: u64,
2001 #[clap(long, short = 'f')]
2004 file: Option<PathBuf>,
2005 },
2006
2007 WriteMemory {
2009 #[clap(value_parser=maybe_with_radix_u64)]
2011 gpa: u64,
2012 hex: Option<String>,
2015 #[clap(long, short = 'f')]
2017 file: Option<PathBuf>,
2018 },
2019
2020 Panic,
2022
2023 Kvp(kvp::KvpCommand),
2025}
2026
2027struct CommandParser {
2028 app: clap::Command,
2029}
2030
2031impl CommandParser {
2032 fn new() -> Self {
2033 let mut app = InteractiveCommand::command();
2035 for sc in app.get_subcommands_mut() {
2036 *sc = sc
2037 .clone()
2038 .help_template("{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}");
2039 }
2040 Self { app }
2041 }
2042
2043 fn parse(&mut self, line: &str) -> clap::error::Result<InteractiveCommand> {
2044 let args = shell_words::split(line)
2045 .map_err(|err| self.app.error(clap::error::ErrorKind::ValueValidation, err))?;
2046 let matches = self.app.try_get_matches_from_mut(args)?;
2047 InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(&mut self.app))
2048 }
2049}
2050
2051fn new_hvsock_service_id(port: u32) -> Guid {
2052 Guid {
2055 data1: port,
2056 .."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
2057 }
2058}
2059
2060async fn run_control(driver: &DefaultDriver, mesh: &VmmMesh, opt: Options) -> anyhow::Result<()> {
2061 let (mut vm_config, mut resources) = vm_config_from_command_line(driver, &opt)?;
2062
2063 let mut vnc_worker = None;
2064 if opt.gfx || opt.vnc {
2065 let listener = TcpListener::bind(format!("127.0.0.1:{}", opt.vnc_port))
2066 .with_context(|| format!("binding to VNC port {}", opt.vnc_port))?;
2067
2068 let input_send = vm_config.input.sender();
2069 let framebuffer = resources.framebuffer_access.expect("synth video enabled");
2070
2071 let vnc_host = mesh
2072 .make_host("vnc", None)
2073 .await
2074 .context("spawning vnc process failed")?;
2075
2076 vnc_worker = Some(
2077 vnc_host
2078 .launch_worker(
2079 vnc_worker_defs::VNC_WORKER_TCP,
2080 VncParameters {
2081 listener,
2082 framebuffer,
2083 input_send,
2084 },
2085 )
2086 .await?,
2087 )
2088 }
2089
2090 let gdb_worker = if let Some(port) = opt.gdb {
2092 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
2093 .with_context(|| format!("binding to gdb port {}", port))?;
2094
2095 let (req_tx, req_rx) = mesh::channel();
2096 vm_config.debugger_rpc = Some(req_rx);
2097
2098 let gdb_host = mesh
2099 .make_host("gdb", None)
2100 .await
2101 .context("spawning gdbstub process failed")?;
2102
2103 Some(
2104 gdb_host
2105 .launch_worker(
2106 debug_worker_defs::DEBUGGER_WORKER,
2107 debug_worker_defs::DebuggerParameters {
2108 listener,
2109 req_chan: req_tx,
2110 vp_count: vm_config.processor_topology.proc_count,
2111 target_arch: if cfg!(guest_arch = "x86_64") {
2112 debug_worker_defs::TargetArch::X86_64
2113 } else {
2114 debug_worker_defs::TargetArch::Aarch64
2115 },
2116 },
2117 )
2118 .await
2119 .context("failed to launch gdbstub worker")?,
2120 )
2121 } else {
2122 None
2123 };
2124
2125 let (vm_rpc, rpc_recv) = mesh::channel();
2127 let (notify_send, notify_recv) = mesh::channel();
2128 let mut vm_worker = {
2129 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2130
2131 let params = VmWorkerParameters {
2132 hypervisor: opt.hypervisor,
2133 cfg: vm_config,
2134 saved_state: None,
2135 rpc: rpc_recv,
2136 notify: notify_send,
2137 };
2138 vm_host
2139 .launch_worker(VM_WORKER, params)
2140 .await
2141 .context("failed to launch vm worker")?
2142 };
2143
2144 if !opt.paused {
2145 vm_rpc.call(VmRpc::Resume, ()).await?;
2146 }
2147
2148 let paravisor_diag = Arc::new(diag_client::DiagClient::from_dialer(
2149 driver.clone(),
2150 DiagDialer {
2151 driver: driver.clone(),
2152 vm_rpc: vm_rpc.clone(),
2153 openhcl_vtl: if opt.vtl2 {
2154 DeviceVtl::Vtl2
2155 } else {
2156 DeviceVtl::Vtl0
2157 },
2158 },
2159 ));
2160
2161 let mut diag_inspector = DiagInspector::new(driver.clone(), paravisor_diag.clone());
2162
2163 let (console_command_send, console_command_recv) = mesh::channel();
2164 let (inspect_completion_engine_send, inspect_completion_engine_recv) = mesh::channel();
2165
2166 let mut console_in = resources.console_in;
2167 thread::Builder::new()
2168 .name("stdio-thread".to_string())
2169 .spawn(move || {
2170 #[cfg(unix)]
2172 if io::stderr().is_terminal() {
2173 term::revert_terminal_on_panic()
2174 }
2175
2176 let mut rl = rustyline::Editor::<
2177 interactive_console::OpenvmmRustylineEditor,
2178 rustyline::history::FileHistory,
2179 >::with_config(
2180 rustyline::Config::builder()
2181 .completion_type(rustyline::CompletionType::List)
2182 .build(),
2183 )
2184 .unwrap();
2185
2186 rl.set_helper(Some(interactive_console::OpenvmmRustylineEditor {
2187 openvmm_inspect_req: Arc::new(inspect_completion_engine_send),
2188 }));
2189
2190 let history_file = {
2191 const HISTORY_FILE: &str = ".openvmm_history";
2192
2193 let history_folder = None
2196 .or_else(dirs::state_dir)
2197 .or_else(dirs::data_local_dir)
2198 .map(|path| path.join("openvmm"));
2199
2200 if let Some(history_folder) = history_folder {
2201 if let Err(err) = std::fs::create_dir_all(&history_folder) {
2202 tracing::warn!(
2203 error = &err as &dyn std::error::Error,
2204 "could not create directory: {}",
2205 history_folder.display()
2206 )
2207 }
2208
2209 Some(history_folder.join(HISTORY_FILE))
2210 } else {
2211 None
2212 }
2213 };
2214
2215 if let Some(history_file) = &history_file {
2216 tracing::info!("restoring history from {}", history_file.display());
2217 if rl.load_history(history_file).is_err() {
2218 tracing::info!("could not find existing {}", history_file.display());
2219 }
2220 }
2221
2222 rl.bind_sequence(
2224 rustyline::KeyEvent::new('\x08', rustyline::Modifiers::CTRL),
2225 rustyline::Cmd::Kill(rustyline::Movement::BackwardWord(1, rustyline::Word::Emacs)),
2226 );
2227
2228 let mut parser = CommandParser::new();
2229
2230 let mut stdin = io::stdin();
2231 loop {
2232 term::set_raw_console(true).expect("failed to set raw console mode");
2234
2235 if let Some(input) = console_in.as_mut() {
2236 let mut buf = [0; 32];
2237 loop {
2238 let n = stdin.read(&mut buf).unwrap();
2239 let mut b = &buf[..n];
2240 let stop = if let Some(ctrlq) = b.iter().position(|x| *x == 0x11) {
2241 b = &b[..ctrlq];
2242 true
2243 } else {
2244 false
2245 };
2246 block_on(input.as_mut().write_all(b)).expect("BUGBUG");
2247 if stop {
2248 break;
2249 }
2250 }
2251 }
2252
2253 term::set_raw_console(false).expect("failed to set raw console mode");
2254
2255 loop {
2256 let line = rl.readline("openvmm> ");
2257 if line.is_err() {
2258 break;
2259 }
2260 let line = line.unwrap();
2261 let trimmed = line.trim();
2262 if trimmed.is_empty() {
2263 continue;
2264 }
2265 if let Err(err) = rl.add_history_entry(&line) {
2266 tracing::warn!(
2267 err = &err as &dyn std::error::Error,
2268 "error adding to .openvmm_history"
2269 )
2270 }
2271
2272 match parser.parse(trimmed) {
2273 Ok(cmd) => match cmd {
2274 InteractiveCommand::Input { data } => {
2275 let mut data = data.join(" ");
2276 data.push('\n');
2277 if let Some(input) = console_in.as_mut() {
2278 block_on(input.write_all(data.as_bytes())).expect("BUGBUG");
2279 }
2280 }
2281 InteractiveCommand::InputMode => break,
2282 cmd => {
2283 let (processing_done_send, processing_done_recv) =
2285 mesh::oneshot::<()>();
2286 console_command_send.send((cmd, processing_done_send));
2287 let _ = block_on(processing_done_recv);
2288 }
2289 },
2290 Err(err) => {
2291 err.print().unwrap();
2292 }
2293 }
2294
2295 if let Some(history_file) = &history_file {
2296 rl.append_history(history_file).unwrap();
2297 }
2298 }
2299 }
2300 })
2301 .unwrap();
2302
2303 let mut state_change_task = None::<Task<Result<StateChange, RpcError>>>;
2304 let mut pulse_save_restore_interval: Option<Duration> = None;
2305 let mut pending_shutdown = None;
2306
2307 enum StateChange {
2308 Pause(bool),
2309 Resume(bool),
2310 Reset(Result<(), RemoteError>),
2311 PulseSaveRestore(Result<(), PulseSaveRestoreError>),
2312 ServiceVtl2(anyhow::Result<Duration>),
2313 }
2314
2315 enum Event {
2316 Command((InteractiveCommand, mesh::OneshotSender<()>)),
2317 InspectRequestFromCompletionEngine(
2318 (InspectTarget, String, mesh::OneshotSender<inspect::Node>),
2319 ),
2320 Quit,
2321 Halt(vmm_core_defs::HaltReason),
2322 PulseSaveRestore,
2323 Worker(WorkerEvent),
2324 VncWorker(WorkerEvent),
2325 StateChange(Result<StateChange, RpcError>),
2326 ShutdownResult(Result<hyperv_ic_resources::shutdown::ShutdownResult, RpcError>),
2327 }
2328
2329 let mut console_command_recv = console_command_recv
2330 .map(Event::Command)
2331 .chain(futures::stream::repeat_with(|| Event::Quit));
2332
2333 let mut notify_recv = notify_recv.map(Event::Halt);
2334
2335 let mut inspect_completion_engine_recv =
2336 inspect_completion_engine_recv.map(Event::InspectRequestFromCompletionEngine);
2337
2338 let mut quit = false;
2339 loop {
2340 let event = {
2341 let pulse_save_restore = pin!(async {
2342 match pulse_save_restore_interval {
2343 Some(wait) => {
2344 PolledTimer::new(driver).sleep(wait).await;
2345 Event::PulseSaveRestore
2346 }
2347 None => pending().await,
2348 }
2349 });
2350
2351 let vm = (&mut vm_worker).map(Event::Worker);
2352 let vnc = futures::stream::iter(vnc_worker.as_mut())
2353 .flatten()
2354 .map(Event::VncWorker);
2355 let change = futures::stream::iter(state_change_task.as_mut().map(|x| x.into_stream()))
2356 .flatten()
2357 .map(Event::StateChange);
2358 let shutdown = pin!(async {
2359 if let Some(s) = &mut pending_shutdown {
2360 Event::ShutdownResult(s.await)
2361 } else {
2362 pending().await
2363 }
2364 });
2365
2366 (
2367 &mut console_command_recv,
2368 &mut inspect_completion_engine_recv,
2369 &mut notify_recv,
2370 pulse_save_restore.into_stream(),
2371 vm,
2372 vnc,
2373 change,
2374 shutdown.into_stream(),
2375 )
2376 .merge()
2377 .next()
2378 .await
2379 .unwrap()
2380 };
2381
2382 let (cmd, _processing_done_send) = match event {
2383 Event::Command(message) => message,
2384 Event::InspectRequestFromCompletionEngine((vtl, path, res)) => {
2385 let mut inspection =
2386 InspectionBuilder::new(&path)
2387 .depth(Some(1))
2388 .inspect(inspect_obj(
2389 vtl,
2390 mesh,
2391 &vm_worker,
2392 vnc_worker.as_ref(),
2393 gdb_worker.as_ref(),
2394 &mut diag_inspector,
2395 ));
2396 let _ = CancelContext::new()
2397 .with_timeout(Duration::from_secs(1))
2398 .until_cancelled(inspection.resolve())
2399 .await;
2400
2401 let node = inspection.results();
2402 res.send(node);
2403 continue;
2404 }
2405 Event::Quit => break,
2406 Event::Halt(reason) => {
2407 tracing::info!(?reason, "guest halted");
2408 continue;
2409 }
2410 Event::PulseSaveRestore => {
2411 vm_rpc.call(VmRpc::PulseSaveRestore, ()).await??;
2412 continue;
2413 }
2414 Event::Worker(event) => {
2415 match event {
2416 WorkerEvent::Stopped => {
2417 if quit {
2418 tracing::info!("vm stopped");
2419 } else {
2420 tracing::error!("vm worker unexpectedly stopped");
2421 }
2422 break;
2423 }
2424 WorkerEvent::Failed(err) => {
2425 tracing::error!(error = &err as &dyn std::error::Error, "vm worker failed");
2426 break;
2427 }
2428 WorkerEvent::RestartFailed(err) => {
2429 tracing::error!(
2430 error = &err as &dyn std::error::Error,
2431 "vm worker restart failed"
2432 );
2433 }
2434 WorkerEvent::Started => {
2435 tracing::info!("vm worker restarted");
2436 }
2437 }
2438 continue;
2439 }
2440 Event::VncWorker(event) => {
2441 match event {
2442 WorkerEvent::Stopped => tracing::error!("vnc unexpectedly stopped"),
2443 WorkerEvent::Failed(err) => {
2444 tracing::error!(
2445 error = &err as &dyn std::error::Error,
2446 "vnc worker failed"
2447 );
2448 }
2449 WorkerEvent::RestartFailed(err) => {
2450 tracing::error!(
2451 error = &err as &dyn std::error::Error,
2452 "vnc worker restart failed"
2453 );
2454 }
2455 WorkerEvent::Started => {
2456 tracing::info!("vnc worker restarted");
2457 }
2458 }
2459 continue;
2460 }
2461 Event::StateChange(r) => {
2462 match r {
2463 Ok(sc) => match sc {
2464 StateChange::Pause(success) => {
2465 if success {
2466 tracing::info!("pause complete");
2467 } else {
2468 tracing::warn!("already paused");
2469 }
2470 }
2471 StateChange::Resume(success) => {
2472 if success {
2473 tracing::info!("resumed complete");
2474 } else {
2475 tracing::warn!("already running");
2476 }
2477 }
2478 StateChange::Reset(r) => match r {
2479 Ok(()) => tracing::info!("reset complete"),
2480 Err(err) => tracing::error!(
2481 error = &err as &dyn std::error::Error,
2482 "reset failed"
2483 ),
2484 },
2485 StateChange::PulseSaveRestore(r) => match r {
2486 Ok(()) => tracing::info!("pulse save/restore complete"),
2487 Err(err) => tracing::error!(
2488 error = &err as &dyn std::error::Error,
2489 "pulse save/restore failed"
2490 ),
2491 },
2492 StateChange::ServiceVtl2(r) => match r {
2493 Ok(dur) => {
2494 tracing::info!(
2495 duration = dur.as_millis() as i64,
2496 "vtl2 servicing complete"
2497 )
2498 }
2499 Err(err) => tracing::error!(
2500 error = err.as_ref() as &dyn std::error::Error,
2501 "vtl2 servicing failed"
2502 ),
2503 },
2504 },
2505 Err(err) => {
2506 tracing::error!(
2507 error = &err as &dyn std::error::Error,
2508 "communication failure during state change"
2509 );
2510 }
2511 }
2512 state_change_task = None;
2513 continue;
2514 }
2515 Event::ShutdownResult(r) => {
2516 match r {
2517 Ok(r) => match r {
2518 hyperv_ic_resources::shutdown::ShutdownResult::Ok => {
2519 tracing::info!("shutdown initiated");
2520 }
2521 hyperv_ic_resources::shutdown::ShutdownResult::NotReady => {
2522 tracing::error!("shutdown ic not ready");
2523 }
2524 hyperv_ic_resources::shutdown::ShutdownResult::AlreadyInProgress => {
2525 tracing::error!("shutdown already in progress");
2526 }
2527 hyperv_ic_resources::shutdown::ShutdownResult::Failed(hr) => {
2528 tracing::error!("shutdown failed with error code {hr:#x}");
2529 }
2530 },
2531 Err(err) => {
2532 tracing::error!(
2533 error = &err as &dyn std::error::Error,
2534 "communication failure during shutdown"
2535 );
2536 }
2537 }
2538 pending_shutdown = None;
2539 continue;
2540 }
2541 };
2542
2543 fn inspect_obj<'a>(
2544 target: InspectTarget,
2545 mesh: &'a VmmMesh,
2546 vm_worker: &'a WorkerHandle,
2547 vnc_worker: Option<&'a WorkerHandle>,
2548 gdb_worker: Option<&'a WorkerHandle>,
2549 diag_inspector: &'a mut DiagInspector,
2550 ) -> impl 'a + InspectMut {
2551 inspect::adhoc_mut(move |req| match target {
2552 InspectTarget::Host => {
2553 let mut resp = req.respond();
2554 resp.field("mesh", mesh)
2555 .field("vm", vm_worker)
2556 .field("vnc", vnc_worker)
2557 .field("gdb", gdb_worker);
2558 }
2559 InspectTarget::Paravisor => {
2560 diag_inspector.inspect_mut(req);
2561 }
2562 })
2563 }
2564
2565 fn state_change<U: 'static + Send>(
2566 driver: impl Spawn,
2567 vm_rpc: &mesh::Sender<VmRpc>,
2568 state_change_task: &mut Option<Task<Result<StateChange, RpcError>>>,
2569 f: impl FnOnce(Rpc<(), U>) -> VmRpc,
2570 g: impl FnOnce(U) -> StateChange + 'static + Send,
2571 ) {
2572 if state_change_task.is_some() {
2573 tracing::error!("state change already in progress");
2574 } else {
2575 let rpc = vm_rpc.call(f, ());
2576 *state_change_task =
2577 Some(driver.spawn("state-change", async move { Ok(g(rpc.await?)) }));
2578 }
2579 }
2580
2581 match cmd {
2582 InteractiveCommand::Panic => {
2583 panic!("injected panic")
2584 }
2585 InteractiveCommand::Restart => {
2586 let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2588
2589 vm_worker.restart(&vm_host);
2590 }
2591 InteractiveCommand::Pause => {
2592 state_change(
2593 driver,
2594 &vm_rpc,
2595 &mut state_change_task,
2596 VmRpc::Pause,
2597 StateChange::Pause,
2598 );
2599 }
2600 InteractiveCommand::Resume => {
2601 state_change(
2602 driver,
2603 &vm_rpc,
2604 &mut state_change_task,
2605 VmRpc::Resume,
2606 StateChange::Resume,
2607 );
2608 }
2609 InteractiveCommand::Reset => {
2610 state_change(
2611 driver,
2612 &vm_rpc,
2613 &mut state_change_task,
2614 VmRpc::Reset,
2615 StateChange::Reset,
2616 );
2617 }
2618 InteractiveCommand::PulseSaveRestore => {
2619 state_change(
2620 driver,
2621 &vm_rpc,
2622 &mut state_change_task,
2623 VmRpc::PulseSaveRestore,
2624 StateChange::PulseSaveRestore,
2625 );
2626 }
2627 InteractiveCommand::SchedulePulseSaveRestore { interval } => {
2628 pulse_save_restore_interval = match interval {
2629 Some(seconds) if seconds != 0 => Some(Duration::from_secs(seconds)),
2630 _ => {
2631 None
2633 }
2634 }
2635 }
2636 InteractiveCommand::Shutdown {
2637 reboot,
2638 hibernate,
2639 force,
2640 } => {
2641 if pending_shutdown.is_some() {
2642 println!("shutdown already in progress");
2643 } else if let Some(ic) = &resources.shutdown_ic {
2644 let params = hyperv_ic_resources::shutdown::ShutdownParams {
2645 shutdown_type: if hibernate {
2646 hyperv_ic_resources::shutdown::ShutdownType::Hibernate
2647 } else if reboot {
2648 hyperv_ic_resources::shutdown::ShutdownType::Reboot
2649 } else {
2650 hyperv_ic_resources::shutdown::ShutdownType::PowerOff
2651 },
2652 force,
2653 };
2654 pending_shutdown =
2655 Some(ic.call(hyperv_ic_resources::shutdown::ShutdownRpc::Shutdown, params));
2656 } else {
2657 println!("no shutdown ic configured");
2658 }
2659 }
2660 InteractiveCommand::Nmi => {
2661 let _ = vm_rpc.call(VmRpc::Nmi, 0).await;
2662 }
2663 InteractiveCommand::ClearHalt => {
2664 vm_rpc.call(VmRpc::ClearHalt, ()).await.ok();
2665 }
2666 InteractiveCommand::AddDisk {
2667 read_only,
2668 target,
2669 path,
2670 lun,
2671 ram,
2672 file_path,
2673 is_dvd,
2674 } => {
2675 let action = async {
2676 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2677 let disk_type = match ram {
2678 None => {
2679 let path = file_path.context("no filename passed")?;
2680 open_disk_type(path.as_ref(), read_only)
2681 .with_context(|| format!("failed to open {}", path.display()))?
2682 }
2683 Some(size) => {
2684 Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
2685 RamDiskLayerHandle { len: Some(size) },
2686 ))
2687 }
2688 };
2689
2690 let device = if is_dvd {
2691 SimpleScsiDvdHandle {
2692 media: Some(disk_type),
2693 requests: None,
2694 }
2695 .into_resource()
2696 } else {
2697 SimpleScsiDiskHandle {
2698 disk: disk_type,
2699 read_only,
2700 parameters: Default::default(),
2701 }
2702 .into_resource()
2703 };
2704
2705 let cfg = ScsiDeviceAndPath {
2706 path: ScsiPath { path, target, lun },
2707 device,
2708 };
2709
2710 scsi.call_failable(ScsiControllerRequest::AddDevice, cfg)
2711 .await?;
2712
2713 anyhow::Result::<_>::Ok(())
2714 };
2715
2716 if let Err(error) = action.await {
2717 tracing::error!(error = error.as_error(), "error adding disk")
2718 }
2719 }
2720 InteractiveCommand::RmDisk { target, path, lun } => {
2721 let action = async {
2722 let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2723 scsi.call_failable(
2724 ScsiControllerRequest::RemoveDevice,
2725 ScsiPath { target, path, lun },
2726 )
2727 .await?;
2728 anyhow::Ok(())
2729 };
2730
2731 if let Err(error) = action.await {
2732 tracing::error!(error = error.as_error(), "error removing disk")
2733 }
2734 }
2735 InteractiveCommand::Inspect {
2736 recursive,
2737 limit,
2738 paravisor,
2739 element,
2740 update,
2741 } => {
2742 let obj = inspect_obj(
2743 if paravisor {
2744 InspectTarget::Paravisor
2745 } else {
2746 InspectTarget::Host
2747 },
2748 mesh,
2749 &vm_worker,
2750 vnc_worker.as_ref(),
2751 gdb_worker.as_ref(),
2752 &mut diag_inspector,
2753 );
2754
2755 if let Some(value) = update {
2756 let Some(element) = element else {
2757 anyhow::bail!("must provide element for update")
2758 };
2759
2760 let value = async {
2761 let update = inspect::update(&element, &value, obj);
2762 let value = CancelContext::new()
2763 .with_timeout(Duration::from_secs(1))
2764 .until_cancelled(update)
2765 .await??;
2766 anyhow::Ok(value)
2767 }
2768 .await;
2769 match value {
2770 Ok(node) => println!("{:#}", node),
2771 Err(err) => println!("error: {:#}", err),
2772 }
2773 } else {
2774 let element = element.unwrap_or_default();
2775 let depth = if recursive { limit } else { Some(0) };
2776 let node = async {
2777 let mut inspection =
2778 InspectionBuilder::new(&element).depth(depth).inspect(obj);
2779 let _ = CancelContext::new()
2780 .with_timeout(Duration::from_secs(1))
2781 .until_cancelled(inspection.resolve())
2782 .await;
2783 inspection.results()
2784 }
2785 .await;
2786
2787 println!("{:#}", node);
2788 }
2789 }
2790 InteractiveCommand::RestartVnc => {
2791 if let Some(vnc) = &mut vnc_worker {
2792 let action = async {
2793 let vnc_host = mesh
2794 .make_host("vnc", None)
2795 .await
2796 .context("spawning vnc process failed")?;
2797
2798 vnc.restart(&vnc_host);
2799 anyhow::Result::<_>::Ok(())
2800 };
2801
2802 if let Err(error) = action.await {
2803 eprintln!("error: {}", error);
2804 }
2805 } else {
2806 eprintln!("ERROR: no VNC server running");
2807 }
2808 }
2809 InteractiveCommand::Hvsock { term, port } => {
2810 let vm_rpc = &vm_rpc;
2811 let action = async || {
2812 let service_id = new_hvsock_service_id(port);
2813 let socket = vm_rpc
2814 .call_failable(
2815 VmRpc::ConnectHvsock,
2816 (
2817 CancelContext::new().with_timeout(Duration::from_secs(2)),
2818 service_id,
2819 DeviceVtl::Vtl0,
2820 ),
2821 )
2822 .await?;
2823 let socket = PolledSocket::new(driver, socket)?;
2824 let mut console = console_relay::Console::new(
2825 driver.clone(),
2826 term.or_else(openvmm_terminal_app).as_deref(),
2827 Some(ConsoleLaunchOptions {
2828 window_title: Some(format!("HVSock{} [OpenVMM]", port)),
2829 }),
2830 )?;
2831 driver
2832 .spawn("console-relay", async move { console.relay(socket).await })
2833 .detach();
2834 anyhow::Result::<_>::Ok(())
2835 };
2836
2837 if let Err(error) = (action)().await {
2838 eprintln!("error: {}", error);
2839 }
2840 }
2841 InteractiveCommand::ServiceVtl2 {
2842 user_mode_only,
2843 igvm,
2844 } => {
2845 let paravisor_diag = paravisor_diag.clone();
2846 let vm_rpc = vm_rpc.clone();
2847 let igvm = igvm.or_else(|| opt.igvm.clone());
2848 let ged_rpc = resources.ged_rpc.clone();
2849 let r = async move {
2850 let start;
2851 if user_mode_only {
2852 start = Instant::now();
2853 paravisor_diag.restart().await?;
2854 } else {
2855 let path = igvm.context("no igvm file loaded")?;
2856 let file = fs_err::File::open(path)?;
2857 start = Instant::now();
2858 hvlite_helpers::underhill::save_underhill(
2859 &vm_rpc,
2860 ged_rpc.as_ref().context("no GED")?,
2861 GuestServicingFlags::default(),
2862 file.into(),
2863 )
2864 .await?;
2865 hvlite_helpers::underhill::restore_underhill(
2866 &vm_rpc,
2867 ged_rpc.as_ref().context("no GED")?,
2868 )
2869 .await?;
2870 }
2871 let end = Instant::now();
2872 Ok(end - start)
2873 }
2874 .map(|r| Ok(StateChange::ServiceVtl2(r)));
2875 if state_change_task.is_some() {
2876 tracing::error!("state change already in progress");
2877 } else {
2878 state_change_task = Some(driver.spawn("state-change", r));
2879 }
2880 }
2881 InteractiveCommand::Quit => {
2882 tracing::info!("quitting");
2883 resources.scsi_rpc = None;
2886
2887 vm_worker.stop();
2888 quit = true;
2889 }
2890 InteractiveCommand::ReadMemory { gpa, size, file } => {
2891 let size = size as usize;
2892 let data = vm_rpc.call(VmRpc::ReadMemory, (gpa, size)).await?;
2893
2894 match data {
2895 Ok(bytes) => {
2896 if let Some(file) = file {
2897 if let Err(err) = fs_err::write(file, bytes) {
2898 eprintln!("error: {err:?}");
2899 }
2900 } else {
2901 let width = 16;
2902 let show_ascii = true;
2903
2904 let mut dump = String::new();
2905 for (i, chunk) in bytes.chunks(width).enumerate() {
2906 let hex_part: Vec<String> =
2907 chunk.iter().map(|byte| format!("{:02x}", byte)).collect();
2908 let hex_line = hex_part.join(" ");
2909
2910 if show_ascii {
2911 let ascii_part: String = chunk
2912 .iter()
2913 .map(|&byte| {
2914 if byte.is_ascii_graphic() || byte == b' ' {
2915 byte as char
2916 } else {
2917 '.'
2918 }
2919 })
2920 .collect();
2921 dump.push_str(&format!(
2922 "{:04x}: {:<width$} {}\n",
2923 i * width,
2924 hex_line,
2925 ascii_part,
2926 width = width * 3 - 1
2927 ));
2928 } else {
2929 dump.push_str(&format!("{:04x}: {}\n", i * width, hex_line));
2930 }
2931 }
2932
2933 println!("{dump}");
2934 }
2935 }
2936 Err(err) => {
2937 eprintln!("error: {err:?}");
2938 }
2939 }
2940 }
2941 InteractiveCommand::WriteMemory { gpa, hex, file } => {
2942 if hex.is_some() == file.is_some() {
2943 eprintln!("error: either path to the file or the hex string must be specified");
2944 continue;
2945 }
2946
2947 let data = if let Some(file) = file {
2948 let data = fs_err::read(file);
2949 match data {
2950 Ok(data) => data,
2951 Err(err) => {
2952 eprintln!("error: {err:?}");
2953 continue;
2954 }
2955 }
2956 } else if let Some(hex) = hex {
2957 if hex.len() & 1 != 0 {
2958 eprintln!(
2959 "error: expected even number of hex digits (2 hex digits per byte)"
2960 );
2961 continue;
2962 }
2963 let data: Result<Vec<u8>, String> = (0..hex.len())
2964 .step_by(2)
2965 .map(|i| {
2966 u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
2967 format!("invalid hex character at position {}: {}", i, e)
2968 })
2969 })
2970 .collect();
2971
2972 match data {
2973 Ok(data) => data,
2974 Err(err) => {
2975 eprintln!("error: {err}");
2976 continue;
2977 }
2978 }
2979 } else {
2980 unreachable!();
2981 };
2982
2983 if data.is_empty() {
2984 eprintln!("error: no data to write");
2985 continue;
2986 }
2987
2988 if let Err(err) = vm_rpc.call(VmRpc::WriteMemory, (gpa, data)).await? {
2989 eprintln!("error: {err:?}");
2990 }
2991 }
2992 InteractiveCommand::Kvp(command) => {
2993 let Some(kvp) = &resources.kvp_ic else {
2994 eprintln!("error: no kvp ic configured");
2995 continue;
2996 };
2997 if let Err(err) = kvp::handle_kvp(kvp, command).await {
2998 eprintln!("error: {err:#}");
2999 }
3000 }
3001 InteractiveCommand::Input { .. } | InteractiveCommand::InputMode => unreachable!(),
3002 }
3003 }
3004
3005 vm_worker.stop();
3006 vm_worker.join().await?;
3007 Ok(())
3008}
3009
3010struct DiagDialer {
3011 driver: DefaultDriver,
3012 vm_rpc: mesh::Sender<VmRpc>,
3013 openhcl_vtl: DeviceVtl,
3014}
3015
3016impl mesh_rpc::client::Dial for DiagDialer {
3017 type Stream = PolledSocket<unix_socket::UnixStream>;
3018
3019 async fn dial(&mut self) -> io::Result<Self::Stream> {
3020 let service_id = new_hvsock_service_id(1);
3021 let socket = self
3022 .vm_rpc
3023 .call_failable(
3024 VmRpc::ConnectHvsock,
3025 (
3026 CancelContext::new().with_timeout(Duration::from_secs(2)),
3027 service_id,
3028 self.openhcl_vtl,
3029 ),
3030 )
3031 .await
3032 .map_err(io::Error::other)?;
3033
3034 PolledSocket::new(&self.driver, socket)
3035 }
3036}
3037
3038pub struct DiagInspector(DiagInspectorInner);
3045
3046enum DiagInspectorInner {
3047 NotStarted(DefaultDriver, Arc<diag_client::DiagClient>),
3048 Started {
3049 send: mesh::Sender<inspect::Deferred>,
3050 _task: Task<()>,
3051 },
3052 Invalid,
3053}
3054
3055impl DiagInspector {
3056 pub fn new(driver: DefaultDriver, diag_client: Arc<diag_client::DiagClient>) -> Self {
3057 Self(DiagInspectorInner::NotStarted(driver, diag_client))
3058 }
3059
3060 fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
3061 loop {
3062 match self.0 {
3063 DiagInspectorInner::NotStarted { .. } => {
3064 let DiagInspectorInner::NotStarted(driver, client) =
3065 std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
3066 else {
3067 unreachable!()
3068 };
3069 let (send, recv) = mesh::channel();
3070 let task = driver.clone().spawn("diag-inspect", async move {
3071 Self::run(&client, recv).await
3072 });
3073
3074 self.0 = DiagInspectorInner::Started { send, _task: task };
3075 }
3076 DiagInspectorInner::Started { ref send, .. } => break send,
3077 DiagInspectorInner::Invalid => unreachable!(),
3078 }
3079 }
3080 }
3081
3082 async fn run(
3083 diag_client: &diag_client::DiagClient,
3084 mut recv: mesh::Receiver<inspect::Deferred>,
3085 ) {
3086 while let Some(deferred) = recv.next().await {
3087 let info = deferred.external_request();
3088 let result = match info.request_type {
3089 inspect::ExternalRequestType::Inspect { depth } => {
3090 if depth == 0 {
3091 Ok(inspect::Node::Unevaluated)
3092 } else {
3093 diag_client
3095 .inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
3096 .await
3097 }
3098 }
3099 inspect::ExternalRequestType::Update { value } => {
3100 (diag_client.update(info.path, value).await).map(inspect::Node::Value)
3101 }
3102 };
3103 deferred.complete_external(
3104 result.unwrap_or_else(|err| {
3105 inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
3106 }),
3107 inspect::SensitivityLevel::Unspecified,
3108 )
3109 }
3110 }
3111}
3112
3113impl InspectMut for DiagInspector {
3114 fn inspect_mut(&mut self, req: inspect::Request<'_>) {
3115 self.start().send(req.defer());
3116 }
3117}
3118
3119enum InspectTarget {
3120 Host,
3121 Paravisor,
3122}
3123
3124mod interactive_console {
3125 use super::InteractiveCommand;
3126 use rustyline::Helper;
3127 use rustyline::Highlighter;
3128 use rustyline::Hinter;
3129 use rustyline::Validator;
3130
3131 #[derive(Helper, Highlighter, Hinter, Validator)]
3132 pub(crate) struct OpenvmmRustylineEditor {
3133 pub openvmm_inspect_req: std::sync::Arc<
3134 mesh::Sender<(
3135 super::InspectTarget,
3136 String,
3137 mesh::OneshotSender<inspect::Node>,
3138 )>,
3139 >,
3140 }
3141
3142 impl rustyline::completion::Completer for OpenvmmRustylineEditor {
3143 type Candidate = String;
3144
3145 fn complete(
3146 &self,
3147 line: &str,
3148 pos: usize,
3149 _ctx: &rustyline::Context<'_>,
3150 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
3151 let Ok(cmd) = shell_words::split(line) else {
3152 return Ok((0, Vec::with_capacity(0)));
3153 };
3154
3155 let completions = futures::executor::block_on(
3156 clap_dyn_complete::Complete {
3157 cmd,
3158 raw: Some(line.into()),
3159 position: Some(pos),
3160 }
3161 .generate_completions::<InteractiveCommand>(None, self),
3162 );
3163
3164 let pos_from_end = {
3165 let line = line.chars().take(pos).collect::<String>();
3166
3167 let trailing_ws = line.len() - line.trim_end().len();
3168
3169 if trailing_ws > 0 {
3170 line.len() - trailing_ws + 1 } else {
3172 let last_word = shell_words::split(&line)
3173 .unwrap_or_default()
3174 .last()
3175 .cloned()
3176 .unwrap_or_default();
3177
3178 line.len() - last_word.len()
3179 }
3180 };
3181
3182 Ok((pos_from_end, completions))
3183 }
3184 }
3185
3186 impl clap_dyn_complete::CustomCompleterFactory for &OpenvmmRustylineEditor {
3187 type CustomCompleter = OpenvmmComplete;
3188 async fn build(&self, _ctx: &clap_dyn_complete::RootCtx<'_>) -> Self::CustomCompleter {
3189 OpenvmmComplete {
3190 openvmm_inspect_req: self.openvmm_inspect_req.clone(),
3191 }
3192 }
3193 }
3194
3195 pub struct OpenvmmComplete {
3196 openvmm_inspect_req: std::sync::Arc<
3197 mesh::Sender<(
3198 super::InspectTarget,
3199 String,
3200 mesh::OneshotSender<inspect::Node>,
3201 )>,
3202 >,
3203 }
3204
3205 impl clap_dyn_complete::CustomCompleter for OpenvmmComplete {
3206 async fn complete(
3207 &self,
3208 ctx: &clap_dyn_complete::RootCtx<'_>,
3209 subcommand_path: &[&str],
3210 arg_id: &str,
3211 ) -> Vec<String> {
3212 match (subcommand_path, arg_id) {
3213 (["openvmm", "inspect"], "element") => {
3214 let on_error = vec!["failed/to/connect".into()];
3215
3216 let (parent_path, to_complete) = (ctx.to_complete)
3217 .rsplit_once('/')
3218 .unwrap_or(("", ctx.to_complete));
3219
3220 let node = {
3221 let paravisor = {
3222 let raw_arg = ctx
3223 .matches
3224 .subcommand()
3225 .unwrap()
3226 .1
3227 .get_one::<String>("paravisor")
3228 .map(|x| x.as_str())
3229 .unwrap_or_default();
3230 raw_arg == "true"
3231 };
3232
3233 let (tx, rx) = mesh::oneshot();
3234 self.openvmm_inspect_req.send((
3235 if paravisor {
3236 super::InspectTarget::Paravisor
3237 } else {
3238 super::InspectTarget::Host
3239 },
3240 parent_path.to_owned(),
3241 tx,
3242 ));
3243 let Ok(node) = rx.await else {
3244 return on_error;
3245 };
3246
3247 node
3248 };
3249
3250 let mut completions = Vec::new();
3251
3252 if let inspect::Node::Dir(dir) = node {
3253 for entry in dir {
3254 if entry.name.starts_with(to_complete) {
3255 if parent_path.is_empty() {
3256 completions.push(format!("{}/", entry.name))
3257 } else {
3258 completions.push(format!(
3259 "{}/{}{}",
3260 parent_path,
3261 entry.name,
3262 if matches!(entry.node, inspect::Node::Dir(..)) {
3263 "/"
3264 } else {
3265 ""
3266 }
3267 ))
3268 }
3269 }
3270 }
3271 } else {
3272 return on_error;
3273 }
3274
3275 completions
3276 }
3277 _ => Vec::new(),
3278 }
3279 }
3280 }
3281}