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