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