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