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