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