openvmm_entry/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This module implements the interactive control process and the entry point
5//! for the worker process.
6
7#![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
19// `pub` so that the missing_docs warning fires for options without
20// documentation.
21pub use cli_args::Options;
22use console_relay::ConsoleLaunchOptions;
23
24use crate::cli_args::SecureBootTemplateCli;
25use anyhow::Context;
26use anyhow::bail;
27use chipset_resources::battery::HostBatteryUpdate;
28use clap::CommandFactory;
29use clap::FromArgMatches;
30use clap::Parser;
31use cli_args::DiskCliKind;
32use cli_args::EfiDiagnosticsLogLevelCli;
33use cli_args::EndpointConfigCli;
34use cli_args::NicConfigCli;
35use cli_args::ProvisionVmgs;
36use cli_args::SerialConfigCli;
37use cli_args::UefiConsoleModeCli;
38use cli_args::VirtioBusCli;
39use cli_args::VmgsCli;
40use crash_dump::spawn_dump_handler;
41use disk_backend_resources::DelayDiskHandle;
42use disk_backend_resources::DiskLayerDescription;
43use disk_backend_resources::layer::DiskLayerHandle;
44use disk_backend_resources::layer::RamDiskLayerHandle;
45use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
46use disk_backend_resources::layer::SqliteDiskLayerHandle;
47use floppy_resources::FloppyDiskConfig;
48use framebuffer::FRAMEBUFFER_SIZE;
49use framebuffer::FramebufferAccess;
50use futures::AsyncReadExt;
51use futures::AsyncWrite;
52use futures::AsyncWriteExt;
53use futures::FutureExt;
54use futures::StreamExt;
55use futures::executor::block_on;
56use futures::io::AllowStdIo;
57use futures_concurrency::stream::Merge;
58use gdma_resources::GdmaDeviceHandle;
59use gdma_resources::VportDefinition;
60use get_resources::ged::GuestServicingFlags;
61use guid::Guid;
62use input_core::MultiplexedInputHandle;
63use inspect::InspectMut;
64use inspect::InspectionBuilder;
65use io::Read;
66use memory_range::MemoryRange;
67use mesh::CancelContext;
68use mesh::CellUpdater;
69use mesh::error::RemoteError;
70use mesh::rpc::Rpc;
71use mesh::rpc::RpcError;
72use mesh::rpc::RpcSend;
73use mesh_worker::WorkerEvent;
74use mesh_worker::WorkerHandle;
75use meshworker::VmmMesh;
76use net_backend_resources::mac_address::MacAddress;
77use nvme_resources::NamespaceDefinition;
78use nvme_resources::NvmeControllerRequest;
79use openvmm_defs::config::Config;
80use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64;
81use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2;
82use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86;
83use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86_WITH_VTL2;
84use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
85use openvmm_defs::config::DeviceVtl;
86use openvmm_defs::config::EfiDiagnosticsLogLevelType;
87use openvmm_defs::config::HypervisorConfig;
88use openvmm_defs::config::LateMapVtl0MemoryPolicy;
89use openvmm_defs::config::LoadMode;
90use openvmm_defs::config::MemoryConfig;
91use openvmm_defs::config::PcieDeviceConfig;
92use openvmm_defs::config::PcieRootComplexConfig;
93use openvmm_defs::config::PcieRootPortConfig;
94use openvmm_defs::config::PcieSwitchConfig;
95use openvmm_defs::config::ProcessorTopologyConfig;
96use openvmm_defs::config::SerialInformation;
97use openvmm_defs::config::VirtioBus;
98use openvmm_defs::config::VmbusConfig;
99use openvmm_defs::config::VpciDeviceConfig;
100use openvmm_defs::config::Vtl2BaseAddressType;
101use openvmm_defs::config::Vtl2Config;
102use openvmm_defs::rpc::PulseSaveRestoreError;
103use openvmm_defs::rpc::VmRpc;
104use openvmm_defs::worker::VM_WORKER;
105use openvmm_defs::worker::VmWorkerParameters;
106use openvmm_helpers::disk::create_disk_type;
107use openvmm_helpers::disk::open_disk_type;
108use pal_async::DefaultDriver;
109use pal_async::DefaultPool;
110use pal_async::socket::PolledSocket;
111use pal_async::task::Spawn;
112use pal_async::task::Task;
113use pal_async::timer::PolledTimer;
114use scsidisk_resources::SimpleScsiDiskHandle;
115use scsidisk_resources::SimpleScsiDvdHandle;
116use serial_16550_resources::ComPort;
117use serial_core::resources::DisconnectedSerialBackendHandle;
118use sparse_mmap::alloc_shared_memory;
119use std::cell::RefCell;
120use std::fmt::Write as _;
121use std::future::pending;
122use std::io;
123#[cfg(unix)]
124use std::io::IsTerminal;
125use std::io::Write;
126use std::net::TcpListener;
127use std::path::Path;
128use std::path::PathBuf;
129use std::pin::pin;
130use std::sync::Arc;
131use std::thread;
132use std::time::Duration;
133use std::time::Instant;
134use storvsp_resources::ScsiControllerRequest;
135use storvsp_resources::ScsiDeviceAndPath;
136use storvsp_resources::ScsiPath;
137use tpm_resources::TpmDeviceHandle;
138use tpm_resources::TpmRegisterLayout;
139use tracing_helpers::AnyhowValueExt;
140use uidevices_resources::SynthKeyboardHandle;
141use uidevices_resources::SynthMouseHandle;
142use uidevices_resources::SynthVideoHandle;
143use video_core::SharedFramebufferHandle;
144use virtio_resources::VirtioPciDeviceHandle;
145use vm_manifest_builder::BaseChipsetType;
146use vm_manifest_builder::MachineArch;
147use vm_manifest_builder::VmChipsetResult;
148use vm_manifest_builder::VmManifestBuilder;
149use vm_resource::IntoResource;
150use vm_resource::Resource;
151use vm_resource::kind::DiskHandleKind;
152use vm_resource::kind::DiskLayerHandleKind;
153use vm_resource::kind::NetEndpointHandleKind;
154use vm_resource::kind::VirtioDeviceHandle;
155use vm_resource::kind::VmbusDeviceHandleKind;
156use vmbus_serial_resources::VmbusSerialDeviceHandle;
157use vmbus_serial_resources::VmbusSerialPort;
158use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
159use vmgs_resources::GuestStateEncryptionPolicy;
160use vmgs_resources::VmgsDisk;
161use vmgs_resources::VmgsFileHandle;
162use vmgs_resources::VmgsResource;
163use vmotherboard::ChipsetDeviceHandle;
164use vnc_worker_defs::VncParameters;
165
166pub fn openvmm_main() {
167    // Save the current state of the terminal so we can restore it back to
168    // normal before exiting.
169    #[cfg(unix)]
170    let orig_termios = io::stderr().is_terminal().then(term::get_termios);
171
172    let exit_code = match do_main() {
173        Ok(_) => 0,
174        Err(err) => {
175            eprintln!("fatal error: {:?}", err);
176            1
177        }
178    };
179
180    // Restore the terminal to its initial state.
181    #[cfg(unix)]
182    if let Some(orig_termios) = orig_termios {
183        term::set_termios(orig_termios);
184    }
185
186    // Terminate the process immediately without graceful shutdown of DLLs or
187    // C++ destructors or anything like that. This is all unnecessary and saves
188    // time on Windows.
189    //
190    // Do flush stdout, though, since there may be buffered data.
191    let _ = io::stdout().flush();
192    pal::process::terminate(exit_code);
193}
194
195#[derive(Default)]
196struct VmResources {
197    console_in: Option<Box<dyn AsyncWrite + Send + Unpin>>,
198    framebuffer_access: Option<FramebufferAccess>,
199    shutdown_ic: Option<mesh::Sender<hyperv_ic_resources::shutdown::ShutdownRpc>>,
200    kvp_ic: Option<mesh::Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
201    scsi_rpc: Option<mesh::Sender<ScsiControllerRequest>>,
202    nvme_vtl2_rpc: Option<mesh::Sender<NvmeControllerRequest>>,
203    ged_rpc: Option<mesh::Sender<get_resources::ged::GuestEmulationRequest>>,
204    vtl2_settings: Option<vtl2_settings_proto::Vtl2Settings>,
205    #[cfg(windows)]
206    switch_ports: Vec<vmswitch::kernel::SwitchPort>,
207}
208
209impl VmResources {
210    /// Modify the cached VTL2 settings and send them to OpenHCL via the GED.
211    ///
212    /// This follows the same pattern as petri's `modify_vtl2_settings`: the cache
213    /// is modified locally, then the entire settings are sent to OpenHCL.
214    async fn modify_vtl2_settings(
215        &mut self,
216        f: impl FnOnce(&mut vtl2_settings_proto::Vtl2Settings),
217    ) -> anyhow::Result<()> {
218        let mut settings_copy = self
219            .vtl2_settings
220            .clone()
221            .context("vtl2 settings not configured")?;
222
223        f(&mut settings_copy);
224
225        let ged_rpc = self.ged_rpc.as_ref().context("no GED configured")?;
226
227        ged_rpc
228            .call_failable(
229                get_resources::ged::GuestEmulationRequest::ModifyVtl2Settings,
230                prost::Message::encode_to_vec(&settings_copy),
231            )
232            .await?;
233
234        // Settings successfully applied, update our cache
235        self.vtl2_settings = Some(settings_copy);
236        Ok(())
237    }
238
239    /// Add a VTL0 SCSI LUN backed by a VTL2 storage device.
240    ///
241    /// This modifies the VTL2 settings to add a new LUN to the specified SCSI controller,
242    /// backed by the given VTL2 device (NVMe namespace or SCSI disk).
243    async fn add_vtl0_scsi_disk(
244        &mut self,
245        controller_guid: Guid,
246        lun: u32,
247        device_type: vtl2_settings_proto::physical_device::DeviceType,
248        device_path: Guid,
249        sub_device_path: u32,
250    ) -> anyhow::Result<()> {
251        let mut not_found = false;
252        self.modify_vtl2_settings(|settings| {
253            let dynamic = settings.dynamic.get_or_insert_with(Default::default);
254
255            // Find the SCSI controller, bail out if not found (we can't create new controllers at runtime)
256            let scsi_controller = dynamic.storage_controllers.iter_mut().find(|c| {
257                c.instance_id == controller_guid.to_string()
258                    && c.protocol
259                        == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
260            });
261
262            let Some(scsi_controller) = scsi_controller else {
263                not_found = true;
264                return;
265            };
266
267            // Add the LUN backed by the VTL2 storage device. If the LUN exists already, UH will reject the settings
268            scsi_controller.luns.push(vtl2_settings_proto::Lun {
269                location: lun,
270                device_id: Guid::new_random().to_string(),
271                vendor_id: "OpenVMM".to_string(),
272                product_id: "Disk".to_string(),
273                product_revision_level: "1.0".to_string(),
274                serial_number: "0".to_string(),
275                model_number: "1".to_string(),
276                physical_devices: Some(vtl2_settings_proto::PhysicalDevices {
277                    r#type: vtl2_settings_proto::physical_devices::BackingType::Single.into(),
278                    device: Some(vtl2_settings_proto::PhysicalDevice {
279                        device_type: device_type.into(),
280                        device_path: device_path.to_string(),
281                        sub_device_path,
282                    }),
283                    devices: Vec::new(),
284                }),
285                is_dvd: false,
286                ..Default::default()
287            });
288        })
289        .await?;
290
291        if not_found {
292            anyhow::bail!("SCSI controller {} not found", controller_guid);
293        }
294        Ok(())
295    }
296
297    /// Remove a VTL0 SCSI LUN.
298    ///
299    /// This modifies the VTL2 settings to remove a LUN from the specified SCSI controller.
300    async fn remove_vtl0_scsi_disk(
301        &mut self,
302        controller_guid: Guid,
303        lun: u32,
304    ) -> anyhow::Result<()> {
305        self.modify_vtl2_settings(|settings| {
306            let dynamic = settings.dynamic.as_mut();
307            if let Some(dynamic) = dynamic {
308                // Find the SCSI controller
309                if let Some(scsi_controller) = dynamic.storage_controllers.iter_mut().find(|c| {
310                    c.instance_id == controller_guid.to_string()
311                        && c.protocol
312                            == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
313                }) {
314                    // Remove the LUN
315                    scsi_controller.luns.retain(|l| l.location != lun);
316                }
317            }
318        })
319        .await
320    }
321
322    /// Find and remove a VTL0 SCSI LUN backed by a specific NVMe namespace.
323    ///
324    /// Returns the LUN number that was removed, or None if no matching LUN was found.
325    async fn remove_vtl0_scsi_disk_by_nvme_nsid(
326        &mut self,
327        controller_guid: Guid,
328        nvme_controller_guid: Guid,
329        nsid: u32,
330    ) -> anyhow::Result<Option<u32>> {
331        let mut removed_lun = None;
332        self.modify_vtl2_settings(|settings| {
333            let dynamic = settings.dynamic.as_mut();
334            if let Some(dynamic) = dynamic {
335                // Find the SCSI controller
336                if let Some(scsi_controller) = dynamic.storage_controllers.iter_mut().find(|c| {
337                    c.instance_id == controller_guid.to_string()
338                        && c.protocol
339                            == vtl2_settings_proto::storage_controller::StorageProtocol::Scsi as i32
340                }) {
341                    // Find and remove the LUN backed by this NVMe namespace
342                    let nvme_controller_str = nvme_controller_guid.to_string();
343                    scsi_controller.luns.retain(|l| {
344                        let dominated_by_nsid = l.physical_devices.as_ref().is_some_and(|pd| {
345                            pd.device.as_ref().is_some_and(|d| {
346                                d.device_type
347                                    == vtl2_settings_proto::physical_device::DeviceType::Nvme as i32
348                                    && d.device_path == nvme_controller_str
349                                    && d.sub_device_path == nsid
350                            })
351                        });
352                        if dominated_by_nsid {
353                            removed_lun = Some(l.location);
354                            false // Remove this LUN
355                        } else {
356                            true // Keep this LUN
357                        }
358                    });
359                }
360            }
361        })
362        .await?;
363        Ok(removed_lun)
364    }
365}
366
367struct ConsoleState<'a> {
368    device: &'a str,
369    input: Box<dyn AsyncWrite + Unpin + Send>,
370}
371
372/// Build a flat list of switches with their parent port assignments.
373///
374/// This function converts hierarchical CLI switch definitions into a flat list
375/// where each switch specifies its parent port directly.
376fn build_switch_list(all_switches: &[cli_args::GenericPcieSwitchCli]) -> Vec<PcieSwitchConfig> {
377    all_switches
378        .iter()
379        .map(|switch_cli| PcieSwitchConfig {
380            name: switch_cli.name.clone(),
381            num_downstream_ports: switch_cli.num_downstream_ports,
382            parent_port: switch_cli.port_name.clone(),
383            hotplug: switch_cli.hotplug,
384        })
385        .collect()
386}
387
388async fn vm_config_from_command_line(
389    spawner: impl Spawn,
390    mesh: &VmmMesh,
391    opt: &Options,
392) -> anyhow::Result<(Config, VmResources)> {
393    let (_, serial_driver) = DefaultPool::spawn_on_thread("serial");
394    // Ensure the serial driver stays alive with no tasks.
395    serial_driver.spawn("leak", pending::<()>()).detach();
396
397    let openhcl_vtl = if opt.vtl2 {
398        DeviceVtl::Vtl2
399    } else {
400        DeviceVtl::Vtl0
401    };
402
403    let console_state: RefCell<Option<ConsoleState<'_>>> = RefCell::new(None);
404    let setup_serial = |name: &str, cli_cfg, device| -> anyhow::Result<_> {
405        Ok(match cli_cfg {
406            SerialConfigCli::Console => {
407                if let Some(console_state) = console_state.borrow().as_ref() {
408                    bail!("console already set by {}", console_state.device);
409                }
410                let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
411                let (serial_read, serial_write) = AsyncReadExt::split(serial);
412                *console_state.borrow_mut() = Some(ConsoleState {
413                    device,
414                    input: Box::new(serial_write),
415                });
416                thread::Builder::new()
417                    .name(name.to_owned())
418                    .spawn(move || {
419                        let _ = block_on(futures::io::copy(
420                            serial_read,
421                            &mut AllowStdIo::new(term::raw_stdout()),
422                        ));
423                    })
424                    .unwrap();
425                Some(config)
426            }
427            SerialConfigCli::Stderr => {
428                let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
429                thread::Builder::new()
430                    .name(name.to_owned())
431                    .spawn(move || {
432                        let _ = block_on(futures::io::copy(
433                            serial,
434                            &mut AllowStdIo::new(term::raw_stderr()),
435                        ));
436                    })
437                    .unwrap();
438                Some(config)
439            }
440            SerialConfigCli::File(path) => {
441                let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
442                let file = fs_err::File::create(path).context("failed to create file")?;
443
444                thread::Builder::new()
445                    .name(name.to_owned())
446                    .spawn(move || {
447                        let _ = block_on(futures::io::copy(serial, &mut AllowStdIo::new(file)));
448                    })
449                    .unwrap();
450                Some(config)
451            }
452            SerialConfigCli::None => None,
453            SerialConfigCli::Pipe(path) => {
454                Some(serial_io::bind_serial(&path).context("failed to bind serial")?)
455            }
456            SerialConfigCli::Tcp(addr) => {
457                Some(serial_io::bind_tcp_serial(&addr).context("failed to bind serial")?)
458            }
459            SerialConfigCli::NewConsole(app, window_title) => {
460                let path = console_relay::random_console_path();
461                let config =
462                    serial_io::bind_serial(&path).context("failed to bind console serial")?;
463                let window_title =
464                    window_title.unwrap_or_else(|| name.to_uppercase() + " [OpenVMM]");
465
466                console_relay::launch_console(
467                    app.or_else(openvmm_terminal_app).as_deref(),
468                    &path,
469                    ConsoleLaunchOptions {
470                        window_title: Some(window_title),
471                    },
472                )
473                .context("failed to launch console")?;
474
475                Some(config)
476            }
477        })
478    };
479
480    let mut vmbus_devices = Vec::new();
481
482    let serial0_cfg = setup_serial(
483        "com1",
484        opt.com1.clone().unwrap_or(SerialConfigCli::Console),
485        if cfg!(guest_arch = "x86_64") {
486            "ttyS0"
487        } else {
488            "ttyAMA0"
489        },
490    )?;
491    let serial1_cfg = setup_serial(
492        "com2",
493        opt.com2.clone().unwrap_or(SerialConfigCli::None),
494        if cfg!(guest_arch = "x86_64") {
495            "ttyS1"
496        } else {
497            "ttyAMA1"
498        },
499    )?;
500    let serial2_cfg = setup_serial(
501        "com3",
502        opt.com3.clone().unwrap_or(SerialConfigCli::None),
503        if cfg!(guest_arch = "x86_64") {
504            "ttyS2"
505        } else {
506            "ttyAMA2"
507        },
508    )?;
509    let serial3_cfg = setup_serial(
510        "com4",
511        opt.com4.clone().unwrap_or(SerialConfigCli::None),
512        if cfg!(guest_arch = "x86_64") {
513            "ttyS3"
514        } else {
515            "ttyAMA3"
516        },
517    )?;
518    let with_vmbus_com1_serial = if let Some(vmbus_com1_cfg) = setup_serial(
519        "vmbus_com1",
520        opt.vmbus_com1_serial
521            .clone()
522            .unwrap_or(SerialConfigCli::None),
523        "vmbus_com1",
524    )? {
525        vmbus_devices.push((
526            openhcl_vtl,
527            VmbusSerialDeviceHandle {
528                port: VmbusSerialPort::Com1,
529                backend: vmbus_com1_cfg,
530            }
531            .into_resource(),
532        ));
533        true
534    } else {
535        false
536    };
537    let with_vmbus_com2_serial = if let Some(vmbus_com2_cfg) = setup_serial(
538        "vmbus_com2",
539        opt.vmbus_com2_serial
540            .clone()
541            .unwrap_or(SerialConfigCli::None),
542        "vmbus_com2",
543    )? {
544        vmbus_devices.push((
545            openhcl_vtl,
546            VmbusSerialDeviceHandle {
547                port: VmbusSerialPort::Com2,
548                backend: vmbus_com2_cfg,
549            }
550            .into_resource(),
551        ));
552        true
553    } else {
554        false
555    };
556    let debugcon_cfg = setup_serial(
557        "debugcon",
558        opt.debugcon
559            .clone()
560            .map(|cfg| cfg.serial)
561            .unwrap_or(SerialConfigCli::None),
562        "debugcon",
563    )?;
564
565    let mut resources = VmResources::default();
566    let mut console_str = "";
567    if let Some(ConsoleState { device, input }) = console_state.into_inner() {
568        resources.console_in = Some(input);
569        console_str = device;
570    }
571
572    if opt.shared_memory {
573        tracing::warn!("--shared-memory/-M flag has no effect and will be removed");
574    }
575
576    const MAX_PROCESSOR_COUNT: u32 = 1024;
577
578    if opt.processors == 0 || opt.processors > MAX_PROCESSOR_COUNT {
579        bail!("invalid proc count: {}", opt.processors);
580    }
581
582    // Total SCSI channel count should not exceed the processor count
583    // (at most, one channel per VP).
584    if opt.scsi_sub_channels > (MAX_PROCESSOR_COUNT - 1) as u16 {
585        bail!(
586            "invalid SCSI sub-channel count: requested {}, max {}",
587            opt.scsi_sub_channels,
588            MAX_PROCESSOR_COUNT - 1
589        );
590    }
591
592    let with_get = opt.get || (opt.vtl2 && !opt.no_get);
593
594    let mut storage = storage_builder::StorageBuilder::new(with_get.then_some(openhcl_vtl));
595    for &cli_args::DiskCli {
596        vtl,
597        ref kind,
598        read_only,
599        is_dvd,
600        underhill,
601        ref pcie_port,
602    } in &opt.disk
603    {
604        if pcie_port.is_some() {
605            anyhow::bail!("`--disk` is incompatible with PCIe");
606        }
607
608        storage.add(
609            vtl,
610            underhill,
611            storage_builder::DiskLocation::Scsi(None),
612            kind,
613            is_dvd,
614            read_only,
615        )?;
616    }
617
618    for &cli_args::IdeDiskCli {
619        ref kind,
620        read_only,
621        channel,
622        device,
623        is_dvd,
624    } in &opt.ide
625    {
626        storage.add(
627            DeviceVtl::Vtl0,
628            None,
629            storage_builder::DiskLocation::Ide(channel, device),
630            kind,
631            is_dvd,
632            read_only,
633        )?;
634    }
635
636    for &cli_args::DiskCli {
637        vtl,
638        ref kind,
639        read_only,
640        is_dvd,
641        underhill,
642        ref pcie_port,
643    } in &opt.nvme
644    {
645        storage.add(
646            vtl,
647            underhill,
648            storage_builder::DiskLocation::Nvme(None, pcie_port.clone()),
649            kind,
650            is_dvd,
651            read_only,
652        )?;
653    }
654
655    let floppy_disks: Vec<_> = opt
656        .floppy
657        .iter()
658        .map(|disk| -> anyhow::Result<_> {
659            let &cli_args::FloppyDiskCli {
660                ref kind,
661                read_only,
662            } = disk;
663            Ok(FloppyDiskConfig {
664                disk_type: disk_open(kind, read_only)?,
665                read_only,
666            })
667        })
668        .collect::<Result<Vec<_>, _>>()?;
669
670    let mut mana_nics = [(); 3].map(|()| None);
671    let mut underhill_nics = Vec::new();
672    let mut vpci_devices = Vec::new();
673
674    let mut nic_index = 0;
675    for cli_cfg in &opt.net {
676        let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
677        if cli_cfg.underhill {
678            if !opt.no_alias_map {
679                anyhow::bail!("must specify --no-alias-map to offer NICs to VTL2");
680            }
681            let mana = mana_nics[openhcl_vtl as usize].get_or_insert_with(|| {
682                let vpci_instance_id = Guid::new_random();
683                underhill_nics.push(vtl2_settings_proto::NicDeviceLegacy {
684                    instance_id: vpci_instance_id.to_string(),
685                    subordinate_instance_id: None,
686                    max_sub_channels: None,
687                });
688                (vpci_instance_id, GdmaDeviceHandle { vports: Vec::new() })
689            });
690            mana.1.vports.push(VportDefinition {
691                mac_address: vport.mac_address,
692                endpoint: vport.endpoint,
693            });
694        } else {
695            vmbus_devices.push(vport.into_netvsp_handle());
696        }
697    }
698
699    if opt.nic {
700        let nic_config = parse_endpoint(
701            &NicConfigCli {
702                vtl: DeviceVtl::Vtl0,
703                endpoint: EndpointConfigCli::Consomme { cidr: None },
704                max_queues: None,
705                underhill: false,
706            },
707            &mut nic_index,
708            &mut resources,
709        )?;
710        vmbus_devices.push(nic_config.into_netvsp_handle());
711    }
712
713    if opt.mcr {
714        tracing::info!("Instantiating MCR controller");
715
716        // Arbitrary but constant instance ID to be consistent across boots.
717        const MCR_INSTANCE_ID: Guid = guid::guid!("07effd8f-7501-426c-a947-d8345f39113d");
718
719        vpci_devices.push(VpciDeviceConfig {
720            vtl: DeviceVtl::Vtl0,
721            instance_id: MCR_INSTANCE_ID,
722            resource: mcr_resources::McrControllerHandle {
723                instance_id: MCR_INSTANCE_ID,
724            }
725            .into_resource(),
726        });
727    }
728
729    // Build initial PCIe devices list from CLI options. Storage devices
730    // (e.g., NVMe controllers on PCIe ports) are added later by storage_builder.
731    let mut pcie_devices = Vec::new();
732    for (index, cli_cfg) in opt.pcie_remote.iter().enumerate() {
733        tracing::info!(
734            port_name = %cli_cfg.port_name,
735            socket_addr = ?cli_cfg.socket_addr,
736            "instantiating PCIe remote device"
737        );
738
739        // Generate a deterministic instance ID based on index
740        const PCIE_REMOTE_BASE_INSTANCE_ID: Guid =
741            guid::guid!("28ed784d-c059-429f-9d9a-46bea02562c0");
742        let instance_id = Guid {
743            data1: index as u32,
744            ..PCIE_REMOTE_BASE_INSTANCE_ID
745        };
746
747        pcie_devices.push(PcieDeviceConfig {
748            port_name: cli_cfg.port_name.clone(),
749            resource: pcie_remote_resources::PcieRemoteHandle {
750                instance_id,
751                socket_addr: cli_cfg.socket_addr.clone(),
752                hu: cli_cfg.hu,
753                controller: cli_cfg.controller,
754            }
755            .into_resource(),
756        });
757    }
758
759    #[cfg(windows)]
760    let mut kernel_vmnics = Vec::new();
761    #[cfg(windows)]
762    for (index, switch_id) in opt.kernel_vmnic.iter().enumerate() {
763        // Pick a random MAC address.
764        let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
765        getrandom::fill(&mut mac_address[3..]).expect("rng failure");
766
767        // Pick a fixed instance ID based on the index.
768        const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-435d-11ee-9f59-00155d5016fc");
769        let instance_id = Guid {
770            data1: index as u32,
771            ..BASE_INSTANCE_ID
772        };
773
774        let switch_id = if switch_id == "default" {
775            DEFAULT_SWITCH
776        } else {
777            switch_id
778        };
779        let (port_id, port) = new_switch_port(switch_id)?;
780        resources.switch_ports.push(port);
781
782        kernel_vmnics.push(openvmm_defs::config::KernelVmNicConfig {
783            instance_id,
784            mac_address: mac_address.into(),
785            switch_port_id: port_id,
786        });
787    }
788
789    for vport in &opt.mana {
790        let vport = parse_endpoint(vport, &mut nic_index, &mut resources)?;
791        mana_nics[vport.vtl as usize]
792            .get_or_insert_with(|| (Guid::new_random(), GdmaDeviceHandle { vports: Vec::new() }))
793            .1
794            .vports
795            .push(VportDefinition {
796                mac_address: vport.mac_address,
797                endpoint: vport.endpoint,
798            });
799    }
800
801    vpci_devices.extend(mana_nics.into_iter().enumerate().filter_map(|(vtl, nic)| {
802        nic.map(|(instance_id, handle)| VpciDeviceConfig {
803            vtl: match vtl {
804                0 => DeviceVtl::Vtl0,
805                1 => DeviceVtl::Vtl1,
806                2 => DeviceVtl::Vtl2,
807                _ => unreachable!(),
808            },
809            instance_id,
810            resource: handle.into_resource(),
811        })
812    }));
813
814    // If VTL2 is enabled, and we are not in VTL2 self allocate mode, provide an
815    // mmio gap for VTL2.
816    let use_vtl2_gap = opt.vtl2
817        && !matches!(
818            opt.igvm_vtl2_relocation_type,
819            Vtl2BaseAddressType::Vtl2Allocate { .. },
820        );
821
822    #[cfg(guest_arch = "aarch64")]
823    let arch = MachineArch::Aarch64;
824    #[cfg(guest_arch = "x86_64")]
825    let arch = MachineArch::X86_64;
826
827    let mmio_gaps: Vec<MemoryRange> = match (use_vtl2_gap, arch) {
828        (true, MachineArch::X86_64) => DEFAULT_MMIO_GAPS_X86_WITH_VTL2.into(),
829        (true, MachineArch::Aarch64) => DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2.into(),
830        (false, MachineArch::X86_64) => DEFAULT_MMIO_GAPS_X86.into(),
831        (false, MachineArch::Aarch64) => DEFAULT_MMIO_GAPS_AARCH64.into(),
832    };
833
834    let mut pci_ecam_gaps = Vec::new();
835    let mut pci_mmio_gaps = Vec::new();
836
837    let mut low_mmio_start = mmio_gaps.first().context("expected mmio gap")?.start();
838    let mut high_mmio_end = mmio_gaps.last().context("expected second mmio gap")?.end();
839
840    let mut pcie_root_complexes = Vec::new();
841    for (i, rc_cli) in opt.pcie_root_complex.iter().enumerate() {
842        let ports = opt
843            .pcie_root_port
844            .iter()
845            .filter(|port_cli| port_cli.root_complex_name == rc_cli.name)
846            .map(|port_cli| PcieRootPortConfig {
847                name: port_cli.name.clone(),
848                hotplug: port_cli.hotplug,
849            })
850            .collect();
851
852        const ONE_MB: u64 = 1024 * 1024;
853        let low_mmio_size = (rc_cli.low_mmio as u64).next_multiple_of(ONE_MB);
854        let high_mmio_size = rc_cli
855            .high_mmio
856            .checked_next_multiple_of(ONE_MB)
857            .context("high mmio rounding error")?;
858        let ecam_size = (((rc_cli.end_bus - rc_cli.start_bus) as u64) + 1) * 256 * 4096;
859
860        let low_pci_mmio_start = low_mmio_start
861            .checked_sub(low_mmio_size)
862            .context("pci low mmio underflow")?;
863        let ecam_start = low_pci_mmio_start
864            .checked_sub(ecam_size)
865            .context("pci ecam underflow")?;
866        low_mmio_start = ecam_start;
867        high_mmio_end = high_mmio_end
868            .checked_add(high_mmio_size)
869            .context("pci high mmio overflow")?;
870
871        let ecam_range = MemoryRange::new(ecam_start..ecam_start + ecam_size);
872        let low_mmio = MemoryRange::new(low_pci_mmio_start..low_pci_mmio_start + low_mmio_size);
873        let high_mmio = MemoryRange::new(high_mmio_end - high_mmio_size..high_mmio_end);
874
875        pci_ecam_gaps.push(ecam_range);
876        pci_mmio_gaps.push(low_mmio);
877        pci_mmio_gaps.push(high_mmio);
878
879        pcie_root_complexes.push(PcieRootComplexConfig {
880            index: i as u32,
881            name: rc_cli.name.clone(),
882            segment: rc_cli.segment,
883            start_bus: rc_cli.start_bus,
884            end_bus: rc_cli.end_bus,
885            ecam_range,
886            low_mmio,
887            high_mmio,
888            ports,
889        });
890    }
891
892    pci_ecam_gaps.sort();
893    pci_mmio_gaps.sort();
894
895    let pcie_switches = build_switch_list(&opt.pcie_switch);
896
897    #[cfg(windows)]
898    let vpci_resources: Vec<_> = opt
899        .device
900        .iter()
901        .map(|path| -> anyhow::Result<_> {
902            Ok(virt_whp::device::DeviceHandle(
903                whp::VpciResource::new(
904                    None,
905                    Default::default(),
906                    &whp::VpciResourceDescriptor::Sriov(path, 0, 0),
907                )
908                .with_context(|| format!("opening PCI device {}", path))?,
909            ))
910        })
911        .collect::<Result<_, _>>()?;
912
913    // Create a vmbusproxy handle if needed by any devices.
914    #[cfg(windows)]
915    let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
916        Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
917    } else {
918        None
919    };
920
921    let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc || opt.pcat {
922        let vram = alloc_shared_memory(FRAMEBUFFER_SIZE)?;
923        let (fb, fba) =
924            framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
925        resources.framebuffer_access = Some(fba);
926        Some(fb)
927    } else {
928        None
929    };
930
931    let load_mode;
932    let with_hv;
933
934    let any_serial_configured = serial0_cfg.is_some()
935        || serial1_cfg.is_some()
936        || serial2_cfg.is_some()
937        || serial3_cfg.is_some();
938
939    let has_com3 = serial2_cfg.is_some();
940
941    let mut chipset = VmManifestBuilder::new(
942        if opt.igvm.is_some() {
943            BaseChipsetType::HclHost
944        } else if opt.pcat {
945            BaseChipsetType::HypervGen1
946        } else if opt.uefi {
947            BaseChipsetType::HypervGen2Uefi
948        } else if opt.hv {
949            BaseChipsetType::HyperVGen2LinuxDirect
950        } else {
951            BaseChipsetType::UnenlightenedLinuxDirect
952        },
953        arch,
954    );
955
956    if framebuffer.is_some() {
957        chipset = chipset.with_framebuffer();
958    }
959    if opt.guest_watchdog {
960        chipset = chipset.with_guest_watchdog();
961    }
962    if any_serial_configured {
963        chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
964    }
965    if opt.battery {
966        let (tx, rx) = mesh::channel();
967        tx.send(HostBatteryUpdate::default_present());
968        chipset = chipset.with_battery(rx);
969    }
970    if let Some(cfg) = &opt.debugcon {
971        chipset = chipset.with_debugcon(
972            debugcon_cfg.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
973            cfg.port,
974        );
975    }
976
977    // TODO: load from VMGS file if it exists
978    let bios_guid = Guid::new_random();
979
980    let VmChipsetResult {
981        chipset,
982        mut chipset_devices,
983    } = chipset
984        .build()
985        .context("failed to build chipset configuration")?;
986
987    if let Some(path) = &opt.igvm {
988        let file = fs_err::File::open(path)
989            .context("failed to open igvm file")?
990            .into();
991        let cmdline = opt.cmdline.join(" ");
992        with_hv = true;
993
994        load_mode = LoadMode::Igvm {
995            file,
996            cmdline,
997            vtl2_base_address: opt.igvm_vtl2_relocation_type,
998            com_serial: has_com3.then(|| SerialInformation {
999                io_port: ComPort::Com3.io_port(),
1000                irq: ComPort::Com3.irq().into(),
1001            }),
1002        };
1003    } else if opt.pcat {
1004        // Emit a nice error early instead of complaining about missing firmware.
1005        if arch != MachineArch::X86_64 {
1006            anyhow::bail!("pcat not supported on this architecture");
1007        }
1008        with_hv = true;
1009
1010        let firmware = openvmm_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
1011        load_mode = LoadMode::Pcat {
1012            firmware,
1013            boot_order: opt
1014                .pcat_boot_order
1015                .map(|x| x.0)
1016                .unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
1017        };
1018    } else if opt.uefi {
1019        use openvmm_defs::config::UefiConsoleMode;
1020
1021        with_hv = true;
1022
1023        let firmware = fs_err::File::open(
1024            (opt.uefi_firmware.0)
1025                .as_ref()
1026                .context("must provide uefi firmware when booting with uefi")?,
1027        )
1028        .context("failed to open uefi firmware")?;
1029
1030        // TODO: It would be better to default memory protections to on, but currently Linux does not boot via UEFI due to what
1031        //       appears to be a GRUB memory protection fault. Memory protections are therefore only enabled if configured.
1032        load_mode = LoadMode::Uefi {
1033            firmware: firmware.into(),
1034            enable_debugging: opt.uefi_debug,
1035            enable_memory_protections: opt.uefi_enable_memory_protections,
1036            disable_frontpage: opt.disable_frontpage,
1037            enable_tpm: opt.tpm,
1038            enable_battery: opt.battery,
1039            enable_serial: any_serial_configured,
1040            enable_vpci_boot: false,
1041            uefi_console_mode: opt.uefi_console_mode.map(|m| match m {
1042                UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1043                UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
1044                UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
1045                UefiConsoleModeCli::None => UefiConsoleMode::None,
1046            }),
1047            default_boot_always_attempt: opt.default_boot_always_attempt,
1048            bios_guid,
1049        };
1050    } else {
1051        // Linux Direct
1052        let mut cmdline = "panic=-1 debug".to_string();
1053
1054        with_hv = opt.hv;
1055        if with_hv {
1056            cmdline += " pci=off";
1057        }
1058
1059        if !console_str.is_empty() {
1060            let _ = write!(&mut cmdline, " console={}", console_str);
1061        }
1062        if opt.gfx {
1063            cmdline += " console=tty";
1064        }
1065        for extra in &opt.cmdline {
1066            let _ = write!(&mut cmdline, " {}", extra);
1067        }
1068
1069        let kernel = fs_err::File::open(
1070            (opt.kernel.0)
1071                .as_ref()
1072                .context("must provide kernel when booting with linux direct")?,
1073        )
1074        .context("failed to open kernel")?;
1075        let initrd = (opt.initrd.0)
1076            .as_ref()
1077            .map(fs_err::File::open)
1078            .transpose()
1079            .context("failed to open initrd")?;
1080
1081        let custom_dsdt = match &opt.custom_dsdt {
1082            Some(path) => {
1083                let mut v = Vec::new();
1084                fs_err::File::open(path)
1085                    .context("failed to open custom dsdt")?
1086                    .read_to_end(&mut v)
1087                    .context("failed to read custom dsdt")?;
1088                Some(v)
1089            }
1090            None => None,
1091        };
1092
1093        load_mode = LoadMode::Linux {
1094            kernel: kernel.into(),
1095            initrd: initrd.map(Into::into),
1096            cmdline,
1097            custom_dsdt,
1098            enable_serial: any_serial_configured,
1099        };
1100    }
1101
1102    let mut vmgs = Some(if let Some(VmgsCli { kind, provision }) = &opt.vmgs {
1103        let disk = VmgsDisk {
1104            disk: disk_open(kind, false).context("failed to open vmgs disk")?,
1105            encryption_policy: if opt.test_gsp_by_id {
1106                GuestStateEncryptionPolicy::GspById(true)
1107            } else {
1108                GuestStateEncryptionPolicy::None(true)
1109            },
1110        };
1111        match provision {
1112            ProvisionVmgs::OnEmpty => VmgsResource::Disk(disk),
1113            ProvisionVmgs::OnFailure => VmgsResource::ReprovisionOnFailure(disk),
1114            ProvisionVmgs::True => VmgsResource::Reprovision(disk),
1115        }
1116    } else {
1117        VmgsResource::Ephemeral
1118    });
1119
1120    if with_get && with_hv {
1121        let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
1122            version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
1123            fixed: Some(Default::default()),
1124            dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
1125                storage_controllers: storage.build_underhill(opt.vmbus_redirect),
1126                nic_devices: underhill_nics,
1127            }),
1128            namespace_settings: Vec::default(),
1129        };
1130
1131        // Cache the VTL2 settings for later modification via the interactive console.
1132        resources.vtl2_settings = Some(vtl2_settings.clone());
1133
1134        let (send, guest_request_recv) = mesh::channel();
1135        resources.ged_rpc = Some(send);
1136
1137        let vmgs = vmgs.take().unwrap();
1138
1139        vmbus_devices.extend([
1140            (
1141                openhcl_vtl,
1142                get_resources::gel::GuestEmulationLogHandle.into_resource(),
1143            ),
1144            (
1145                openhcl_vtl,
1146                get_resources::ged::GuestEmulationDeviceHandle {
1147                    firmware: if opt.pcat {
1148                        get_resources::ged::GuestFirmwareConfig::Pcat {
1149                            boot_order: opt
1150                                .pcat_boot_order
1151                                .map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
1152                                .map(|x| match x {
1153                                    openvmm_defs::config::PcatBootDevice::Floppy => {
1154                                        get_resources::ged::PcatBootDevice::Floppy
1155                                    }
1156                                    openvmm_defs::config::PcatBootDevice::HardDrive => {
1157                                        get_resources::ged::PcatBootDevice::HardDrive
1158                                    }
1159                                    openvmm_defs::config::PcatBootDevice::Optical => {
1160                                        get_resources::ged::PcatBootDevice::Optical
1161                                    }
1162                                    openvmm_defs::config::PcatBootDevice::Network => {
1163                                        get_resources::ged::PcatBootDevice::Network
1164                                    }
1165                                }),
1166                        }
1167                    } else {
1168                        use get_resources::ged::UefiConsoleMode;
1169
1170                        get_resources::ged::GuestFirmwareConfig::Uefi {
1171                            enable_vpci_boot: storage.has_vtl0_nvme(),
1172                            firmware_debug: opt.uefi_debug,
1173                            disable_frontpage: opt.disable_frontpage,
1174                            console_mode: match opt.uefi_console_mode.unwrap_or(UefiConsoleModeCli::Default) {
1175                                UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1176                                UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
1177                                UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
1178                                UefiConsoleModeCli::None => UefiConsoleMode::None,
1179                            },
1180                            default_boot_always_attempt: opt.default_boot_always_attempt,
1181                        }
1182                    },
1183                    com1: with_vmbus_com1_serial,
1184                    com2: with_vmbus_com2_serial,
1185                    serial_tx_only: opt.serial_tx_only,
1186                    vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
1187                    vmbus_redirection: opt.vmbus_redirect,
1188                    vmgs,
1189                    framebuffer: opt
1190                        .vtl2_gfx
1191                        .then(|| SharedFramebufferHandle.into_resource()),
1192                    guest_request_recv,
1193                    enable_tpm: opt.tpm,
1194                    firmware_event_send: None,
1195                    secure_boot_enabled: opt.secure_boot,
1196                    secure_boot_template: match opt.secure_boot_template {
1197                        Some(SecureBootTemplateCli::Windows) => {
1198                            get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1199                        },
1200                        Some(SecureBootTemplateCli::UefiCa) => {
1201                            get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1202                        }
1203                        None => {
1204                            get_resources::ged::GuestSecureBootTemplateType::None
1205                        },
1206                    },
1207                    enable_battery: opt.battery,
1208                    no_persistent_secrets: true,
1209                    igvm_attest_test_config: None,
1210                    test_gsp_by_id: opt.test_gsp_by_id,
1211                    efi_diagnostics_log_level: {
1212                        match opt.efi_diagnostics_log_level.unwrap_or_default() {
1213                            EfiDiagnosticsLogLevelCli::Default => get_resources::ged::EfiDiagnosticsLogLevelType::Default,
1214                            EfiDiagnosticsLogLevelCli::Info => get_resources::ged::EfiDiagnosticsLogLevelType::Info,
1215                            EfiDiagnosticsLogLevelCli::Full => get_resources::ged::EfiDiagnosticsLogLevelType::Full,
1216                        }
1217                    },
1218                    hv_sint_enabled: false,
1219                }
1220                .into_resource(),
1221            ),
1222        ]);
1223    }
1224
1225    if opt.tpm && !opt.vtl2 {
1226        let register_layout = if cfg!(guest_arch = "x86_64") {
1227            TpmRegisterLayout::IoPort
1228        } else {
1229            TpmRegisterLayout::Mmio
1230        };
1231
1232        let (ppi_store, nvram_store) = if opt.vmgs.is_some() {
1233            (
1234                VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1235                VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1236            )
1237        } else {
1238            (
1239                EphemeralNonVolatileStoreHandle.into_resource(),
1240                EphemeralNonVolatileStoreHandle.into_resource(),
1241            )
1242        };
1243
1244        chipset_devices.push(ChipsetDeviceHandle {
1245            name: "tpm".to_string(),
1246            resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1247                device: TpmDeviceHandle {
1248                    ppi_store,
1249                    nvram_store,
1250                    nvram_size: None,
1251                    refresh_tpm_seeds: false,
1252                    ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1253                    register_layout,
1254                    guest_secret_key: None,
1255                    logger: None,
1256                    is_confidential_vm: false,
1257                    bios_guid,
1258                }
1259                .into_resource(),
1260                worker_host: mesh.make_host("tpm", None).await?,
1261            }
1262            .into_resource(),
1263        });
1264    }
1265
1266    let custom_uefi_vars = {
1267        use firmware_uefi_custom_vars::CustomVars;
1268
1269        // load base vars from specified template, or use an empty set of base
1270        // vars if none was specified.
1271        let base_vars = match opt.secure_boot_template {
1272            Some(template) => match (arch, template) {
1273                (MachineArch::X86_64, SecureBootTemplateCli::Windows) => {
1274                    hyperv_secure_boot_templates::x64::microsoft_windows()
1275                }
1276                (MachineArch::X86_64, SecureBootTemplateCli::UefiCa) => {
1277                    hyperv_secure_boot_templates::x64::microsoft_uefi_ca()
1278                }
1279                (MachineArch::Aarch64, SecureBootTemplateCli::Windows) => {
1280                    hyperv_secure_boot_templates::aarch64::microsoft_windows()
1281                }
1282                (MachineArch::Aarch64, SecureBootTemplateCli::UefiCa) => {
1283                    hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca()
1284                }
1285            },
1286            None => CustomVars::default(),
1287        };
1288
1289        // TODO: fallback to VMGS read if no command line flag was given
1290
1291        let custom_uefi_json_data = match &opt.custom_uefi_json {
1292            Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?),
1293            None => None,
1294        };
1295
1296        // obtain the final custom uefi vars by applying the delta onto the base vars
1297        match custom_uefi_json_data {
1298            Some(data) => {
1299                let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?;
1300                base_vars.apply_delta(delta)?
1301            }
1302            None => base_vars,
1303        }
1304    };
1305
1306    let vga_firmware = if opt.pcat {
1307        Some(openvmm_pcat_locator::find_svga_bios(
1308            opt.vga_firmware.as_deref(),
1309        )?)
1310    } else {
1311        None
1312    };
1313
1314    if opt.gfx {
1315        vmbus_devices.extend([
1316            (
1317                DeviceVtl::Vtl0,
1318                SynthVideoHandle {
1319                    framebuffer: SharedFramebufferHandle.into_resource(),
1320                }
1321                .into_resource(),
1322            ),
1323            (
1324                DeviceVtl::Vtl0,
1325                SynthKeyboardHandle {
1326                    source: MultiplexedInputHandle {
1327                        // Save 0 for PS/2
1328                        elevation: 1,
1329                    }
1330                    .into_resource(),
1331                }
1332                .into_resource(),
1333            ),
1334            (
1335                DeviceVtl::Vtl0,
1336                SynthMouseHandle {
1337                    source: MultiplexedInputHandle {
1338                        // Save 0 for PS/2
1339                        elevation: 1,
1340                    }
1341                    .into_resource(),
1342                }
1343                .into_resource(),
1344            ),
1345        ]);
1346    }
1347
1348    let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
1349        if let Some(path) = path {
1350            cleanup_socket(path.as_ref());
1351            let listener = unix_socket::UnixListener::bind(path)
1352                .with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
1353            Ok(Some(listener))
1354        } else {
1355            Ok(None)
1356        }
1357    };
1358
1359    let vtl0_vsock_listener = vsock_listener(opt.vsock_path.as_deref())?;
1360    let vtl2_vsock_listener = vsock_listener(opt.vtl2_vsock_path.as_deref())?;
1361
1362    if let Some(path) = &opt.openhcl_dump_path {
1363        let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
1364        task.detach();
1365        vmbus_devices.push((openhcl_vtl, resource));
1366    }
1367
1368    #[cfg(guest_arch = "aarch64")]
1369    let topology_arch = openvmm_defs::config::ArchTopologyConfig::Aarch64(
1370        openvmm_defs::config::Aarch64TopologyConfig {
1371            // TODO: allow this to be configured from the command line
1372            gic_config: None,
1373            pmu_gsiv: openvmm_defs::config::PmuGsivConfig::Platform,
1374        },
1375    );
1376    #[cfg(guest_arch = "x86_64")]
1377    let topology_arch =
1378        openvmm_defs::config::ArchTopologyConfig::X86(openvmm_defs::config::X86TopologyConfig {
1379            apic_id_offset: opt.apic_id_offset,
1380            x2apic: opt.x2apic,
1381        });
1382
1383    let with_isolation = if let Some(isolation) = &opt.isolation {
1384        // TODO: For now, isolation is only supported with VTL2.
1385        if !opt.vtl2 {
1386            anyhow::bail!("isolation is only currently supported with vtl2");
1387        }
1388
1389        // TODO: Alias map support is not yet implement with isolation.
1390        if !opt.no_alias_map {
1391            anyhow::bail!("alias map not supported with isolation");
1392        }
1393
1394        match isolation {
1395            cli_args::IsolationCli::Vbs => Some(openvmm_defs::config::IsolationType::Vbs),
1396        }
1397    } else {
1398        None
1399    };
1400
1401    if with_hv {
1402        let (shutdown_send, shutdown_recv) = mesh::channel();
1403        resources.shutdown_ic = Some(shutdown_send);
1404        let (kvp_send, kvp_recv) = mesh::channel();
1405        resources.kvp_ic = Some(kvp_send);
1406        vmbus_devices.extend(
1407            [
1408                hyperv_ic_resources::shutdown::ShutdownIcHandle {
1409                    recv: shutdown_recv,
1410                }
1411                .into_resource(),
1412                hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_recv }.into_resource(),
1413                hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
1414            ]
1415            .map(|r| (DeviceVtl::Vtl0, r)),
1416        );
1417    }
1418
1419    if let Some(hive_path) = &opt.imc {
1420        let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
1421        vmbus_devices.push((
1422            DeviceVtl::Vtl0,
1423            vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
1424        ));
1425    }
1426
1427    let mut virtio_devices = Vec::new();
1428    let mut add_virtio_device = |bus, resource: Resource<VirtioDeviceHandle>| {
1429        let bus = match bus {
1430            VirtioBusCli::Auto => {
1431                // Use VPCI when possible (currently only on Windows and macOS due
1432                // to KVM backend limitations).
1433                if with_hv && (cfg!(windows) || cfg!(target_os = "macos")) {
1434                    None
1435                } else {
1436                    Some(VirtioBus::Pci)
1437                }
1438            }
1439            VirtioBusCli::Mmio => Some(VirtioBus::Mmio),
1440            VirtioBusCli::Pci => Some(VirtioBus::Pci),
1441            VirtioBusCli::Vpci => None,
1442        };
1443        if let Some(bus) = bus {
1444            virtio_devices.push((bus, resource));
1445        } else {
1446            vpci_devices.push(VpciDeviceConfig {
1447                vtl: DeviceVtl::Vtl0,
1448                instance_id: Guid::new_random(),
1449                resource: VirtioPciDeviceHandle(resource).into_resource(),
1450            });
1451        }
1452    };
1453
1454    for cli_cfg in &opt.virtio_net {
1455        if cli_cfg.underhill {
1456            anyhow::bail!("use --net uh:[...] to add underhill NICs")
1457        }
1458        let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
1459        add_virtio_device(
1460            VirtioBusCli::Auto,
1461            virtio_resources::net::VirtioNetHandle {
1462                max_queues: vport.max_queues,
1463                mac_address: vport.mac_address,
1464                endpoint: vport.endpoint,
1465            }
1466            .into_resource(),
1467        );
1468    }
1469
1470    for args in &opt.virtio_fs {
1471        add_virtio_device(
1472            opt.virtio_fs_bus,
1473            virtio_resources::fs::VirtioFsHandle {
1474                tag: args.tag.clone(),
1475                fs: virtio_resources::fs::VirtioFsBackend::HostFs {
1476                    root_path: args.path.clone(),
1477                    mount_options: args.options.clone(),
1478                },
1479            }
1480            .into_resource(),
1481        );
1482    }
1483
1484    for args in &opt.virtio_fs_shmem {
1485        add_virtio_device(
1486            opt.virtio_fs_bus,
1487            virtio_resources::fs::VirtioFsHandle {
1488                tag: args.tag.clone(),
1489                fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
1490                    root_path: args.path.clone(),
1491                },
1492            }
1493            .into_resource(),
1494        );
1495    }
1496
1497    for args in &opt.virtio_9p {
1498        add_virtio_device(
1499            VirtioBusCli::Auto,
1500            virtio_resources::p9::VirtioPlan9Handle {
1501                tag: args.tag.clone(),
1502                root_path: args.path.clone(),
1503                debug: opt.virtio_9p_debug,
1504            }
1505            .into_resource(),
1506        );
1507    }
1508
1509    if let Some(path) = &opt.virtio_pmem {
1510        add_virtio_device(
1511            VirtioBusCli::Auto,
1512            virtio_resources::pmem::VirtioPmemHandle { path: path.clone() }.into_resource(),
1513        );
1514    }
1515
1516    let mut cfg = Config {
1517        chipset,
1518        load_mode,
1519        floppy_disks,
1520        pcie_root_complexes,
1521        pcie_devices,
1522        pcie_switches,
1523        vpci_devices,
1524        ide_disks: Vec::new(),
1525        memory: MemoryConfig {
1526            mem_size: opt.memory,
1527            mmio_gaps,
1528            pci_ecam_gaps,
1529            pci_mmio_gaps,
1530            prefetch_memory: opt.prefetch,
1531        },
1532        processor_topology: ProcessorTopologyConfig {
1533            proc_count: opt.processors,
1534            vps_per_socket: opt.vps_per_socket,
1535            enable_smt: match opt.smt {
1536                cli_args::SmtConfigCli::Auto => None,
1537                cli_args::SmtConfigCli::Force => Some(true),
1538                cli_args::SmtConfigCli::Off => Some(false),
1539            },
1540            arch: Some(topology_arch),
1541        },
1542        hypervisor: HypervisorConfig {
1543            with_hv,
1544            with_vtl2: opt.vtl2.then_some(Vtl2Config {
1545                vtl0_alias_map: !opt.no_alias_map,
1546                late_map_vtl0_memory: match opt.late_map_vtl0_policy {
1547                    cli_args::Vtl0LateMapPolicyCli::Off => None,
1548                    cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
1549                    cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
1550                    cli_args::Vtl0LateMapPolicyCli::Exception => {
1551                        Some(LateMapVtl0MemoryPolicy::InjectException)
1552                    }
1553                },
1554            }),
1555            with_isolation,
1556            user_mode_hv_enlightenments: opt.no_enlightenments,
1557            user_mode_apic: opt.user_mode_apic,
1558        },
1559        #[cfg(windows)]
1560        kernel_vmnics,
1561        input: mesh::Receiver::new(),
1562        framebuffer,
1563        vga_firmware,
1564        vtl2_gfx: opt.vtl2_gfx,
1565        virtio_devices,
1566        vmbus: with_hv.then_some(VmbusConfig {
1567            vsock_listener: vtl0_vsock_listener,
1568            vsock_path: opt.vsock_path.clone(),
1569            vtl2_redirect: opt.vmbus_redirect,
1570            vmbus_max_version: opt.vmbus_max_version,
1571            #[cfg(windows)]
1572            vmbusproxy_handle,
1573        }),
1574        vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
1575            vsock_listener: vtl2_vsock_listener,
1576            vsock_path: opt.vtl2_vsock_path.clone(),
1577            ..Default::default()
1578        }),
1579        vmbus_devices,
1580        chipset_devices,
1581        #[cfg(windows)]
1582        vpci_resources,
1583        vmgs,
1584        secure_boot_enabled: opt.secure_boot,
1585        custom_uefi_vars,
1586        firmware_event_send: None,
1587        debugger_rpc: None,
1588        generation_id_recv: None,
1589        rtc_delta_milliseconds: 0,
1590        automatic_guest_reset: !opt.halt_on_reset,
1591        efi_diagnostics_log_level: {
1592            match opt.efi_diagnostics_log_level.unwrap_or_default() {
1593                EfiDiagnosticsLogLevelCli::Default => EfiDiagnosticsLogLevelType::Default,
1594                EfiDiagnosticsLogLevelCli::Info => EfiDiagnosticsLogLevelType::Info,
1595                EfiDiagnosticsLogLevelCli::Full => EfiDiagnosticsLogLevelType::Full,
1596            }
1597        },
1598    };
1599
1600    storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
1601    Ok((cfg, resources))
1602}
1603
1604/// Gets the terminal to use for externally launched console windows.
1605fn openvmm_terminal_app() -> Option<PathBuf> {
1606    std::env::var_os("OPENVMM_TERM")
1607        .or_else(|| std::env::var_os("HVLITE_TERM"))
1608        .map(Into::into)
1609}
1610
1611// Tries to remove `path` if it is confirmed to be a Unix socket.
1612fn cleanup_socket(path: &Path) {
1613    #[cfg(windows)]
1614    let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
1615    #[cfg(not(windows))]
1616    let is_socket = path
1617        .metadata()
1618        .is_ok_and(|meta| std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type()));
1619
1620    if is_socket {
1621        let _ = std::fs::remove_file(path);
1622    }
1623}
1624
1625#[cfg(windows)]
1626const DEFAULT_SWITCH: &str = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444";
1627
1628#[cfg(windows)]
1629fn new_switch_port(
1630    switch_id: &str,
1631) -> anyhow::Result<(
1632    openvmm_defs::config::SwitchPortId,
1633    vmswitch::kernel::SwitchPort,
1634)> {
1635    let id = vmswitch::kernel::SwitchPortId {
1636        switch: switch_id.parse().context("invalid switch id")?,
1637        port: Guid::new_random(),
1638    };
1639    let _ = vmswitch::hcn::Network::open(&id.switch)
1640        .with_context(|| format!("could not find switch {}", id.switch))?;
1641
1642    let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
1643
1644    let id = openvmm_defs::config::SwitchPortId {
1645        switch: id.switch,
1646        port: id.port,
1647    };
1648    Ok((id, port))
1649}
1650
1651fn parse_endpoint(
1652    cli_cfg: &NicConfigCli,
1653    index: &mut usize,
1654    resources: &mut VmResources,
1655) -> anyhow::Result<NicConfig> {
1656    let _ = resources;
1657    let endpoint = match &cli_cfg.endpoint {
1658        EndpointConfigCli::Consomme { cidr } => {
1659            net_backend_resources::consomme::ConsommeHandle { cidr: cidr.clone() }.into_resource()
1660        }
1661        EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
1662        EndpointConfigCli::Dio { id } => {
1663            #[cfg(windows)]
1664            {
1665                let (port_id, port) = new_switch_port(id.as_deref().unwrap_or(DEFAULT_SWITCH))?;
1666                resources.switch_ports.push(port);
1667                net_backend_resources::dio::WindowsDirectIoHandle {
1668                    switch_port_id: net_backend_resources::dio::SwitchPortId {
1669                        switch: port_id.switch,
1670                        port: port_id.port,
1671                    },
1672                }
1673                .into_resource()
1674            }
1675
1676            #[cfg(not(windows))]
1677            {
1678                let _ = id;
1679                bail!("cannot use dio on non-windows platforms")
1680            }
1681        }
1682        EndpointConfigCli::Tap { name } => {
1683            net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
1684        }
1685    };
1686
1687    // Pick a random MAC address.
1688    let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
1689    getrandom::fill(&mut mac_address[3..]).expect("rng failure");
1690
1691    // Pick a fixed instance ID based on the index.
1692    const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-da43-11ed-936a-00155d6db52f");
1693    let instance_id = Guid {
1694        data1: *index as u32,
1695        ..BASE_INSTANCE_ID
1696    };
1697    *index += 1;
1698
1699    Ok(NicConfig {
1700        vtl: cli_cfg.vtl,
1701        instance_id,
1702        endpoint,
1703        mac_address: mac_address.into(),
1704        max_queues: cli_cfg.max_queues,
1705    })
1706}
1707
1708#[derive(Debug)]
1709struct NicConfig {
1710    vtl: DeviceVtl,
1711    instance_id: Guid,
1712    mac_address: MacAddress,
1713    endpoint: Resource<NetEndpointHandleKind>,
1714    max_queues: Option<u16>,
1715}
1716
1717impl NicConfig {
1718    fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
1719        (
1720            self.vtl,
1721            netvsp_resources::NetvspHandle {
1722                instance_id: self.instance_id,
1723                mac_address: self.mac_address,
1724                endpoint: self.endpoint,
1725                max_queues: self.max_queues,
1726            }
1727            .into_resource(),
1728        )
1729    }
1730}
1731
1732enum LayerOrDisk {
1733    Layer(DiskLayerDescription),
1734    Disk(Resource<DiskHandleKind>),
1735}
1736
1737fn disk_open(disk_cli: &DiskCliKind, read_only: bool) -> anyhow::Result<Resource<DiskHandleKind>> {
1738    let mut layers = Vec::new();
1739    disk_open_inner(disk_cli, read_only, &mut layers)?;
1740    if layers.len() == 1 && matches!(layers[0], LayerOrDisk::Disk(_)) {
1741        let LayerOrDisk::Disk(disk) = layers.pop().unwrap() else {
1742            unreachable!()
1743        };
1744        Ok(disk)
1745    } else {
1746        Ok(Resource::new(disk_backend_resources::LayeredDiskHandle {
1747            layers: layers
1748                .into_iter()
1749                .map(|layer| match layer {
1750                    LayerOrDisk::Layer(layer) => layer,
1751                    LayerOrDisk::Disk(disk) => DiskLayerDescription {
1752                        layer: DiskLayerHandle(disk).into_resource(),
1753                        read_cache: false,
1754                        write_through: false,
1755                    },
1756                })
1757                .collect(),
1758        }))
1759    }
1760}
1761
1762fn disk_open_inner(
1763    disk_cli: &DiskCliKind,
1764    read_only: bool,
1765    layers: &mut Vec<LayerOrDisk>,
1766) -> anyhow::Result<()> {
1767    fn layer<T: IntoResource<DiskLayerHandleKind>>(layer: T) -> LayerOrDisk {
1768        LayerOrDisk::Layer(layer.into_resource().into())
1769    }
1770    fn disk<T: IntoResource<DiskHandleKind>>(disk: T) -> LayerOrDisk {
1771        LayerOrDisk::Disk(disk.into_resource())
1772    }
1773    match disk_cli {
1774        &DiskCliKind::Memory(len) => {
1775            layers.push(layer(RamDiskLayerHandle { len: Some(len) }));
1776        }
1777        DiskCliKind::File {
1778            path,
1779            create_with_len,
1780        } => layers.push(LayerOrDisk::Disk(if let Some(size) = create_with_len {
1781            create_disk_type(path, *size)
1782                .with_context(|| format!("failed to create {}", path.display()))?
1783        } else {
1784            open_disk_type(path, read_only)
1785                .with_context(|| format!("failed to open {}", path.display()))?
1786        })),
1787        DiskCliKind::Blob { kind, url } => {
1788            layers.push(disk(disk_backend_resources::BlobDiskHandle {
1789                url: url.to_owned(),
1790                format: match kind {
1791                    cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
1792                    cli_args::BlobKind::Vhd1 => disk_backend_resources::BlobDiskFormat::FixedVhd1,
1793                },
1794            }))
1795        }
1796        DiskCliKind::MemoryDiff(inner) => {
1797            layers.push(layer(RamDiskLayerHandle { len: None }));
1798            disk_open_inner(inner, true, layers)?;
1799        }
1800        DiskCliKind::PersistentReservationsWrapper(inner) => layers.push(disk(
1801            disk_backend_resources::DiskWithReservationsHandle(disk_open(inner, read_only)?),
1802        )),
1803        DiskCliKind::DelayDiskWrapper {
1804            delay_ms,
1805            disk: inner,
1806        } => layers.push(disk(DelayDiskHandle {
1807            delay: CellUpdater::new(Duration::from_millis(*delay_ms)).cell(),
1808            disk: disk_open(inner, read_only)?,
1809        })),
1810        DiskCliKind::Crypt {
1811            disk: inner,
1812            cipher,
1813            key_file,
1814        } => layers.push(disk(disk_crypt_resources::DiskCryptHandle {
1815            disk: disk_open(inner, read_only)?,
1816            cipher: match cipher {
1817                cli_args::DiskCipher::XtsAes256 => disk_crypt_resources::Cipher::XtsAes256,
1818            },
1819            key: fs_err::read(key_file).context("failed to read key file")?,
1820        })),
1821        DiskCliKind::Sqlite {
1822            path,
1823            create_with_len,
1824        } => {
1825            // FUTURE: this code should be responsible for opening
1826            // file-handle(s) itself, and passing them into sqlite via a custom
1827            // vfs. For now though - simply check if the file exists or not, and
1828            // perform early validation of filesystem-level create options.
1829            match (create_with_len.is_some(), path.exists()) {
1830                (true, true) => anyhow::bail!(
1831                    "cannot create new sqlite disk at {} - file already exists",
1832                    path.display()
1833                ),
1834                (false, false) => anyhow::bail!(
1835                    "cannot open sqlite disk at {} - file not found",
1836                    path.display()
1837                ),
1838                _ => {}
1839            }
1840
1841            layers.push(layer(SqliteDiskLayerHandle {
1842                dbhd_path: path.display().to_string(),
1843                format_dbhd: create_with_len.map(|len| {
1844                    disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1845                        logically_read_only: false,
1846                        len: Some(len),
1847                    }
1848                }),
1849            }));
1850        }
1851        DiskCliKind::SqliteDiff { path, create, disk } => {
1852            // FUTURE: this code should be responsible for opening
1853            // file-handle(s) itself, and passing them into sqlite via a custom
1854            // vfs. For now though - simply check if the file exists or not, and
1855            // perform early validation of filesystem-level create options.
1856            match (create, path.exists()) {
1857                (true, true) => anyhow::bail!(
1858                    "cannot create new sqlite disk at {} - file already exists",
1859                    path.display()
1860                ),
1861                (false, false) => anyhow::bail!(
1862                    "cannot open sqlite disk at {} - file not found",
1863                    path.display()
1864                ),
1865                _ => {}
1866            }
1867
1868            layers.push(layer(SqliteDiskLayerHandle {
1869                dbhd_path: path.display().to_string(),
1870                format_dbhd: create.then_some(
1871                    disk_backend_resources::layer::SqliteDiskLayerFormatParams {
1872                        logically_read_only: false,
1873                        len: None,
1874                    },
1875                ),
1876            }));
1877            disk_open_inner(disk, true, layers)?;
1878        }
1879        DiskCliKind::AutoCacheSqlite {
1880            cache_path,
1881            key,
1882            disk,
1883        } => {
1884            layers.push(LayerOrDisk::Layer(DiskLayerDescription {
1885                read_cache: true,
1886                write_through: false,
1887                layer: SqliteAutoCacheDiskLayerHandle {
1888                    cache_path: cache_path.clone(),
1889                    cache_key: key.clone(),
1890                }
1891                .into_resource(),
1892            }));
1893            disk_open_inner(disk, read_only, layers)?;
1894        }
1895    }
1896    Ok(())
1897}
1898
1899fn do_main() -> anyhow::Result<()> {
1900    #[cfg(windows)]
1901    pal::windows::disable_hard_error_dialog();
1902
1903    tracing_init::enable_tracing()?;
1904
1905    // Try to run as a worker host.
1906    // On success the worker runs to completion and then exits the process (does
1907    // not return). Any worker host setup errors are return and bubbled up.
1908    meshworker::run_vmm_mesh_host()?;
1909
1910    let opt = Options::parse();
1911    if let Some(path) = &opt.write_saved_state_proto {
1912        mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
1913            .write_to_path(path)
1914            .context("failed to write protobuf descriptors")?;
1915        return Ok(());
1916    }
1917
1918    if let Some(path) = opt.relay_console_path {
1919        let console_title = opt.relay_console_title.unwrap_or_default();
1920        return console_relay::relay_console(&path, console_title.as_str());
1921    }
1922
1923    #[cfg(any(feature = "grpc", feature = "ttrpc"))]
1924    if let Some(path) = opt.ttrpc.as_ref().or(opt.grpc.as_ref()) {
1925        return block_on(async {
1926            let _ = std::fs::remove_file(path);
1927            let listener =
1928                unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
1929
1930            let transport = if opt.ttrpc.is_some() {
1931                ttrpc::RpcTransport::Ttrpc
1932            } else {
1933                ttrpc::RpcTransport::Grpc
1934            };
1935
1936            // This is a local launch
1937            let mut handle =
1938                mesh_worker::launch_local_worker::<ttrpc::TtrpcWorker>(ttrpc::Parameters {
1939                    listener,
1940                    transport,
1941                })
1942                .await?;
1943
1944            tracing::info!(%transport, path = %path.display(), "listening");
1945
1946            // Signal the the parent process that the server is ready.
1947            pal::close_stdout().context("failed to close stdout")?;
1948
1949            handle.join().await?;
1950
1951            Ok(())
1952        });
1953    }
1954
1955    DefaultPool::run_with(async |driver| {
1956        let mesh = VmmMesh::new(&driver, opt.single_process)?;
1957        let result = run_control(&driver, &mesh, opt).await;
1958        mesh.shutdown().await;
1959        result
1960    })
1961}
1962
1963fn maybe_with_radix_u64(s: &str) -> Result<u64, String> {
1964    let (radix, prefix_len) = if s.starts_with("0x") || s.starts_with("0X") {
1965        (16, 2)
1966    } else if s.starts_with("0o") || s.starts_with("0O") {
1967        (8, 2)
1968    } else if s.starts_with("0b") || s.starts_with("0B") {
1969        (2, 2)
1970    } else {
1971        (10, 0)
1972    };
1973
1974    u64::from_str_radix(&s[prefix_len..], radix).map_err(|e| format!("{e}"))
1975}
1976
1977#[derive(Parser)]
1978#[clap(
1979    name = "openvmm",
1980    disable_help_flag = true,
1981    disable_version_flag = true,
1982    no_binary_name = true,
1983    help_template("{subcommands}")
1984)]
1985enum InteractiveCommand {
1986    /// Restart the VM worker (experimental).
1987    ///
1988    /// This restarts the VM worker while preserving state.
1989    #[clap(visible_alias = "R")]
1990    Restart,
1991
1992    /// Inject an NMI.
1993    #[clap(visible_alias = "n")]
1994    Nmi,
1995
1996    /// Pause the VM.
1997    #[clap(visible_alias = "p")]
1998    Pause,
1999
2000    /// Resume the VM.
2001    #[clap(visible_alias = "r")]
2002    Resume,
2003
2004    /// Do a pulsed save restore (pause, save, reset, restore, resume) to the VM.
2005    #[clap(visible_alias = "psr")]
2006    PulseSaveRestore,
2007
2008    /// Schedule a pulsed save restore (pause, save, reset, restore, resume) to the VM.
2009    #[clap(visible_alias = "spsr")]
2010    SchedulePulseSaveRestore {
2011        /// The interval between pulse save restore operations in seconds.
2012        /// None or 0 means any previous scheduled pulse save restores will be cleared.
2013        interval: Option<u64>,
2014    },
2015
2016    /// Hot add a disk to the VTL0 guest.
2017    #[clap(visible_alias = "d")]
2018    AddDisk {
2019        #[clap(long = "ro")]
2020        read_only: bool,
2021        #[clap(long = "dvd")]
2022        is_dvd: bool,
2023        #[clap(long, default_value_t)]
2024        target: u8,
2025        #[clap(long, default_value_t)]
2026        path: u8,
2027        #[clap(long, default_value_t)]
2028        lun: u8,
2029        #[clap(long)]
2030        ram: Option<u64>,
2031        file_path: Option<PathBuf>,
2032    },
2033
2034    /// Hot remove a disk from the VTL0 guest.
2035    #[clap(visible_alias = "D")]
2036    RmDisk {
2037        #[clap(long)]
2038        target: u8,
2039        #[clap(long)]
2040        path: u8,
2041        #[clap(long)]
2042        lun: u8,
2043    },
2044
2045    /// Manage VTL2 settings (storage controllers, NICs exposed to VTL0).
2046    #[clap(subcommand)]
2047    Vtl2Settings(Vtl2SettingsCommand),
2048
2049    /// Hot add an NVMe namespace to VTL2, and optionally to VTL0.
2050    AddNvmeNs {
2051        #[clap(long = "ro")]
2052        read_only: bool,
2053        /// The namespace ID.
2054        #[clap(long)]
2055        nsid: u32,
2056        /// Create a RAM-backed namespace of the specified size in bytes.
2057        #[clap(long)]
2058        ram: Option<u64>,
2059        /// Path to a file to use as the backing store.
2060        file_path: Option<PathBuf>,
2061        /// Also expose this namespace to VTL0 via VTL2 settings as a SCSI disk
2062        /// with the specified LUN number.
2063        #[clap(long)]
2064        vtl0_lun: Option<u32>,
2065    },
2066
2067    /// Hot remove an NVMe namespace from VTL2.
2068    RmNvmeNs {
2069        /// The namespace ID to remove.
2070        #[clap(long)]
2071        nsid: u32,
2072        /// Also remove the VTL0 SCSI disk backed by this namespace.
2073        #[clap(long)]
2074        vtl0: bool,
2075    },
2076
2077    /// Inspect program state.
2078    #[clap(visible_alias = "x")]
2079    Inspect {
2080        /// Enumerate state recursively.
2081        #[clap(short, long)]
2082        recursive: bool,
2083        /// The recursive depth limit.
2084        #[clap(short, long, requires("recursive"))]
2085        limit: Option<usize>,
2086        /// Target the paravisor.
2087        #[clap(short = 'v', long)]
2088        paravisor: bool,
2089        /// The element path to inspect.
2090        element: Option<String>,
2091        /// Update the path with a new value.
2092        #[clap(short, long, conflicts_with("recursive"))]
2093        update: Option<String>,
2094    },
2095
2096    /// Restart the VNC worker.
2097    #[clap(visible_alias = "V")]
2098    RestartVnc,
2099
2100    /// Start an hvsocket terminal window.
2101    #[clap(visible_alias = "v")]
2102    Hvsock {
2103        /// the terminal emulator to run (defaults to conhost.exe or xterm)
2104        #[clap(short, long)]
2105        term: Option<PathBuf>,
2106        /// the vsock port to connect to
2107        port: u32,
2108    },
2109
2110    /// Quit the program.
2111    #[clap(visible_alias = "q")]
2112    Quit,
2113
2114    /// Write input to the VM console.
2115    ///
2116    /// This will write each input parameter to the console's associated serial
2117    /// port, separated by spaces.
2118    #[clap(visible_alias = "i")]
2119    Input { data: Vec<String> },
2120
2121    /// Switch to input mode.
2122    ///
2123    /// Once in input mode, Ctrl-Q returns to command mode.
2124    #[clap(visible_alias = "I")]
2125    InputMode,
2126
2127    /// Reset the VM.
2128    Reset,
2129
2130    /// Send a request to the VM to shut it down.
2131    Shutdown {
2132        /// Reboot the VM instead of powering it off.
2133        #[clap(long, short = 'r')]
2134        reboot: bool,
2135        /// Hibernate the VM instead of powering it off.
2136        #[clap(long, short = 'h', conflicts_with = "reboot")]
2137        hibernate: bool,
2138        /// Tell the guest to force the power state transition.
2139        #[clap(long, short = 'f')]
2140        force: bool,
2141    },
2142
2143    /// Clears the current halt condition, resuming the VPs if the VM is
2144    /// running.
2145    #[clap(visible_alias = "ch")]
2146    ClearHalt,
2147
2148    /// Update the image in VTL2.
2149    ServiceVtl2 {
2150        /// Just restart the user-mode paravisor process, not the full
2151        /// firmware.
2152        #[clap(long, short = 'u')]
2153        user_mode_only: bool,
2154        /// The path to the new IGVM file. If missing, use the originally
2155        /// configured path.
2156        #[clap(long, conflicts_with("user_mode_only"))]
2157        igvm: Option<PathBuf>,
2158        /// Enable keepalive when servicing VTL2 devices.
2159        /// Default is `true`.
2160        #[clap(long, short = 'n', default_missing_value = "true")]
2161        nvme_keepalive: bool,
2162        /// Enable keepalive when servicing VTL2 devices.
2163        /// Default is `false`.
2164        #[clap(long)]
2165        mana_keepalive: bool,
2166    },
2167
2168    /// Read guest memory
2169    ReadMemory {
2170        /// Guest physical address to start at.
2171        #[clap(value_parser=maybe_with_radix_u64)]
2172        gpa: u64,
2173        /// How many bytes to dump.
2174        #[clap(value_parser=maybe_with_radix_u64)]
2175        size: u64,
2176        /// File to save the data to. If omitted,
2177        /// the data will be presented as a hex dump.
2178        #[clap(long, short = 'f')]
2179        file: Option<PathBuf>,
2180    },
2181
2182    /// Write guest memory
2183    WriteMemory {
2184        /// Guest physical address to start at
2185        #[clap(value_parser=maybe_with_radix_u64)]
2186        gpa: u64,
2187        /// Hex string encoding data, with no `0x` radix.
2188        /// If omitted, the source file must be specified.
2189        hex: Option<String>,
2190        /// File to write the data from.
2191        #[clap(long, short = 'f')]
2192        file: Option<PathBuf>,
2193    },
2194
2195    /// Inject an artificial panic into OpenVMM
2196    Panic,
2197
2198    /// Use KVP to interact with the guest.
2199    Kvp(kvp::KvpCommand),
2200}
2201
2202/// Subcommands for managing VTL2 settings.
2203#[derive(clap::Subcommand)]
2204enum Vtl2SettingsCommand {
2205    /// Show the current VTL2 settings.
2206    Show,
2207
2208    /// Add a SCSI disk to VTL0 backed by a VTL2 storage device.
2209    ///
2210    /// The backing device can be either a VTL2 NVMe namespace or a VTL2 SCSI disk.
2211    AddScsiDisk {
2212        /// The VTL0 SCSI controller instance ID (GUID). Defaults to the standard
2213        /// OpenVMM VTL0 SCSI instance.
2214        #[clap(long)]
2215        controller: Option<String>,
2216        /// The SCSI LUN to expose to VTL0.
2217        #[clap(long)]
2218        lun: u32,
2219        /// The backing VTL2 NVMe namespace ID.
2220        #[clap(
2221            long,
2222            conflicts_with = "backing_scsi_lun",
2223            required_unless_present = "backing_scsi_lun"
2224        )]
2225        backing_nvme_nsid: Option<u32>,
2226        /// The backing VTL2 SCSI LUN.
2227        #[clap(
2228            long,
2229            conflicts_with = "backing_nvme_nsid",
2230            required_unless_present = "backing_nvme_nsid"
2231        )]
2232        backing_scsi_lun: Option<u32>,
2233    },
2234
2235    /// Remove a SCSI disk from VTL0.
2236    RmScsiDisk {
2237        /// The SCSI controller instance ID (GUID). Defaults to the standard
2238        /// OpenVMM VTL0 SCSI instance.
2239        #[clap(long)]
2240        controller: Option<String>,
2241        /// The SCSI LUN to remove.
2242        #[clap(long)]
2243        lun: u32,
2244    },
2245}
2246
2247struct CommandParser {
2248    app: clap::Command,
2249}
2250
2251impl CommandParser {
2252    fn new() -> Self {
2253        // Update the help template for each subcommand.
2254        let mut app = InteractiveCommand::command();
2255        for sc in app.get_subcommands_mut() {
2256            *sc = sc
2257                .clone()
2258                .help_template("{about-with-newline}\n{usage-heading}\n    {usage}\n\n{all-args}");
2259        }
2260        Self { app }
2261    }
2262
2263    fn parse(&mut self, line: &str) -> clap::error::Result<InteractiveCommand> {
2264        let args = shell_words::split(line)
2265            .map_err(|err| self.app.error(clap::error::ErrorKind::ValueValidation, err))?;
2266        let matches = self.app.try_get_matches_from_mut(args)?;
2267        InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(&mut self.app))
2268    }
2269}
2270
2271fn new_hvsock_service_id(port: u32) -> Guid {
2272    // This GUID is an embedding of the AF_VSOCK port into an
2273    // AF_HYPERV service ID.
2274    Guid {
2275        data1: port,
2276        .."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
2277    }
2278}
2279
2280async fn run_control(driver: &DefaultDriver, mesh: &VmmMesh, opt: Options) -> anyhow::Result<()> {
2281    let (mut vm_config, mut resources) = vm_config_from_command_line(driver, mesh, &opt).await?;
2282
2283    let mut vnc_worker = None;
2284    if opt.gfx || opt.vnc {
2285        let listener = TcpListener::bind(format!("127.0.0.1:{}", opt.vnc_port))
2286            .with_context(|| format!("binding to VNC port {}", opt.vnc_port))?;
2287
2288        let input_send = vm_config.input.sender();
2289        let framebuffer = resources
2290            .framebuffer_access
2291            .take()
2292            .expect("synth video enabled");
2293
2294        let vnc_host = mesh
2295            .make_host("vnc", None)
2296            .await
2297            .context("spawning vnc process failed")?;
2298
2299        vnc_worker = Some(
2300            vnc_host
2301                .launch_worker(
2302                    vnc_worker_defs::VNC_WORKER_TCP,
2303                    VncParameters {
2304                        listener,
2305                        framebuffer,
2306                        input_send,
2307                    },
2308                )
2309                .await?,
2310        )
2311    }
2312
2313    // spin up the debug worker
2314    let gdb_worker = if let Some(port) = opt.gdb {
2315        let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
2316            .with_context(|| format!("binding to gdb port {}", port))?;
2317
2318        let (req_tx, req_rx) = mesh::channel();
2319        vm_config.debugger_rpc = Some(req_rx);
2320
2321        let gdb_host = mesh
2322            .make_host("gdb", None)
2323            .await
2324            .context("spawning gdbstub process failed")?;
2325
2326        Some(
2327            gdb_host
2328                .launch_worker(
2329                    debug_worker_defs::DEBUGGER_WORKER,
2330                    debug_worker_defs::DebuggerParameters {
2331                        listener,
2332                        req_chan: req_tx,
2333                        vp_count: vm_config.processor_topology.proc_count,
2334                        target_arch: if cfg!(guest_arch = "x86_64") {
2335                            debug_worker_defs::TargetArch::X86_64
2336                        } else {
2337                            debug_worker_defs::TargetArch::Aarch64
2338                        },
2339                    },
2340                )
2341                .await
2342                .context("failed to launch gdbstub worker")?,
2343        )
2344    } else {
2345        None
2346    };
2347
2348    // spin up the VM
2349    let (vm_rpc, rpc_recv) = mesh::channel();
2350    let (notify_send, notify_recv) = mesh::channel();
2351    let mut vm_worker = {
2352        let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2353
2354        let params = VmWorkerParameters {
2355            hypervisor: opt.hypervisor,
2356            cfg: vm_config,
2357            saved_state: None,
2358            rpc: rpc_recv,
2359            notify: notify_send,
2360        };
2361        vm_host
2362            .launch_worker(VM_WORKER, params)
2363            .await
2364            .context("failed to launch vm worker")?
2365    };
2366
2367    if !opt.paused {
2368        vm_rpc.call(VmRpc::Resume, ()).await?;
2369    }
2370
2371    let paravisor_diag = Arc::new(diag_client::DiagClient::from_dialer(
2372        driver.clone(),
2373        DiagDialer {
2374            driver: driver.clone(),
2375            vm_rpc: vm_rpc.clone(),
2376            openhcl_vtl: if opt.vtl2 {
2377                DeviceVtl::Vtl2
2378            } else {
2379                DeviceVtl::Vtl0
2380            },
2381        },
2382    ));
2383
2384    let mut diag_inspector = DiagInspector::new(driver.clone(), paravisor_diag.clone());
2385
2386    let (console_command_send, console_command_recv) = mesh::channel();
2387    let (inspect_completion_engine_send, inspect_completion_engine_recv) = mesh::channel();
2388
2389    let mut console_in = resources.console_in.take();
2390    thread::Builder::new()
2391        .name("stdio-thread".to_string())
2392        .spawn(move || {
2393            // install panic hook to restore cooked terminal (linux)
2394            #[cfg(unix)]
2395            if io::stderr().is_terminal() {
2396                term::revert_terminal_on_panic()
2397            }
2398
2399            let mut rl = rustyline::Editor::<
2400                interactive_console::OpenvmmRustylineEditor,
2401                rustyline::history::FileHistory,
2402            >::with_config(
2403                rustyline::Config::builder()
2404                    .completion_type(rustyline::CompletionType::List)
2405                    .build(),
2406            )
2407            .unwrap();
2408
2409            rl.set_helper(Some(interactive_console::OpenvmmRustylineEditor {
2410                openvmm_inspect_req: Arc::new(inspect_completion_engine_send),
2411            }));
2412
2413            let history_file = {
2414                const HISTORY_FILE: &str = ".openvmm_history";
2415
2416                // using a `None` to kick off the `.or()` chain in order to make
2417                // it a bit easier to visually inspect the fallback chain.
2418                let history_folder = None
2419                    .or_else(dirs::state_dir)
2420                    .or_else(dirs::data_local_dir)
2421                    .map(|path| path.join("openvmm"));
2422
2423                if let Some(history_folder) = history_folder {
2424                    if let Err(err) = std::fs::create_dir_all(&history_folder) {
2425                        tracing::warn!(
2426                            error = &err as &dyn std::error::Error,
2427                            "could not create directory: {}",
2428                            history_folder.display()
2429                        )
2430                    }
2431
2432                    Some(history_folder.join(HISTORY_FILE))
2433                } else {
2434                    None
2435                }
2436            };
2437
2438            if let Some(history_file) = &history_file {
2439                tracing::info!("restoring history from {}", history_file.display());
2440                if rl.load_history(history_file).is_err() {
2441                    tracing::info!("could not find existing {}", history_file.display());
2442                }
2443            }
2444
2445            // Enable Ctrl-Backspace to delete the current word.
2446            rl.bind_sequence(
2447                rustyline::KeyEvent::new('\x08', rustyline::Modifiers::CTRL),
2448                rustyline::Cmd::Kill(rustyline::Movement::BackwardWord(1, rustyline::Word::Emacs)),
2449            );
2450
2451            let mut parser = CommandParser::new();
2452
2453            let mut stdin = io::stdin();
2454            loop {
2455                // Raw console text until Ctrl-Q.
2456                term::set_raw_console(true).expect("failed to set raw console mode");
2457
2458                if let Some(input) = console_in.as_mut() {
2459                    let mut buf = [0; 32];
2460                    loop {
2461                        let n = stdin.read(&mut buf).unwrap();
2462                        let mut b = &buf[..n];
2463                        let stop = if let Some(ctrlq) = b.iter().position(|x| *x == 0x11) {
2464                            b = &b[..ctrlq];
2465                            true
2466                        } else {
2467                            false
2468                        };
2469                        block_on(input.as_mut().write_all(b)).expect("BUGBUG");
2470                        if stop {
2471                            break;
2472                        }
2473                    }
2474                }
2475
2476                term::set_raw_console(false).expect("failed to set raw console mode");
2477
2478                loop {
2479                    let line = rl.readline("openvmm> ");
2480                    if line.is_err() {
2481                        break;
2482                    }
2483                    let line = line.unwrap();
2484                    let trimmed = line.trim();
2485                    if trimmed.is_empty() {
2486                        continue;
2487                    }
2488                    if let Err(err) = rl.add_history_entry(&line) {
2489                        tracing::warn!(
2490                            err = &err as &dyn std::error::Error,
2491                            "error adding to .openvmm_history"
2492                        )
2493                    }
2494
2495                    match parser.parse(trimmed) {
2496                        Ok(cmd) => match cmd {
2497                            InteractiveCommand::Input { data } => {
2498                                let mut data = data.join(" ");
2499                                data.push('\n');
2500                                if let Some(input) = console_in.as_mut() {
2501                                    block_on(input.write_all(data.as_bytes())).expect("BUGBUG");
2502                                }
2503                            }
2504                            InteractiveCommand::InputMode => break,
2505                            cmd => {
2506                                // Send the command to the main thread for processing.
2507                                let (processing_done_send, processing_done_recv) =
2508                                    mesh::oneshot::<()>();
2509                                console_command_send.send((cmd, processing_done_send));
2510                                let _ = block_on(processing_done_recv);
2511                            }
2512                        },
2513                        Err(err) => {
2514                            err.print().unwrap();
2515                        }
2516                    }
2517
2518                    if let Some(history_file) = &history_file {
2519                        rl.append_history(history_file).unwrap();
2520                    }
2521                }
2522            }
2523        })
2524        .unwrap();
2525
2526    let mut state_change_task = None::<Task<Result<StateChange, RpcError>>>;
2527    let mut pulse_save_restore_interval: Option<Duration> = None;
2528    let mut pending_shutdown = None;
2529
2530    enum StateChange {
2531        Pause(bool),
2532        Resume(bool),
2533        Reset(Result<(), RemoteError>),
2534        PulseSaveRestore(Result<(), PulseSaveRestoreError>),
2535        ServiceVtl2(anyhow::Result<Duration>),
2536    }
2537
2538    enum Event {
2539        Command((InteractiveCommand, mesh::OneshotSender<()>)),
2540        InspectRequestFromCompletionEngine(
2541            (InspectTarget, String, mesh::OneshotSender<inspect::Node>),
2542        ),
2543        Quit,
2544        Halt(vmm_core_defs::HaltReason),
2545        PulseSaveRestore,
2546        Worker(WorkerEvent),
2547        VncWorker(WorkerEvent),
2548        StateChange(Result<StateChange, RpcError>),
2549        ShutdownResult(Result<hyperv_ic_resources::shutdown::ShutdownResult, RpcError>),
2550    }
2551
2552    let mut console_command_recv = console_command_recv
2553        .map(Event::Command)
2554        .chain(futures::stream::repeat_with(|| Event::Quit));
2555
2556    let mut notify_recv = notify_recv.map(Event::Halt);
2557
2558    let mut inspect_completion_engine_recv =
2559        inspect_completion_engine_recv.map(Event::InspectRequestFromCompletionEngine);
2560
2561    let mut quit = false;
2562    loop {
2563        let event = {
2564            let pulse_save_restore = pin!(async {
2565                match pulse_save_restore_interval {
2566                    Some(wait) => {
2567                        PolledTimer::new(driver).sleep(wait).await;
2568                        Event::PulseSaveRestore
2569                    }
2570                    None => pending().await,
2571                }
2572            });
2573
2574            let vm = (&mut vm_worker).map(Event::Worker);
2575            let vnc = futures::stream::iter(vnc_worker.as_mut())
2576                .flatten()
2577                .map(Event::VncWorker);
2578            let change = futures::stream::iter(state_change_task.as_mut().map(|x| x.into_stream()))
2579                .flatten()
2580                .map(Event::StateChange);
2581            let shutdown = pin!(async {
2582                if let Some(s) = &mut pending_shutdown {
2583                    Event::ShutdownResult(s.await)
2584                } else {
2585                    pending().await
2586                }
2587            });
2588
2589            (
2590                &mut console_command_recv,
2591                &mut inspect_completion_engine_recv,
2592                &mut notify_recv,
2593                pulse_save_restore.into_stream(),
2594                vm,
2595                vnc,
2596                change,
2597                shutdown.into_stream(),
2598            )
2599                .merge()
2600                .next()
2601                .await
2602                .unwrap()
2603        };
2604
2605        let (cmd, _processing_done_send) = match event {
2606            Event::Command(message) => message,
2607            Event::InspectRequestFromCompletionEngine((vtl, path, res)) => {
2608                let mut inspection =
2609                    InspectionBuilder::new(&path)
2610                        .depth(Some(1))
2611                        .inspect(inspect_obj(
2612                            vtl,
2613                            mesh,
2614                            &vm_worker,
2615                            vnc_worker.as_ref(),
2616                            gdb_worker.as_ref(),
2617                            &mut diag_inspector,
2618                        ));
2619                let _ = CancelContext::new()
2620                    .with_timeout(Duration::from_secs(1))
2621                    .until_cancelled(inspection.resolve())
2622                    .await;
2623
2624                let node = inspection.results();
2625                res.send(node);
2626                continue;
2627            }
2628            Event::Quit => break,
2629            Event::Halt(reason) => {
2630                tracing::info!(?reason, "guest halted");
2631                continue;
2632            }
2633            Event::PulseSaveRestore => {
2634                vm_rpc.call(VmRpc::PulseSaveRestore, ()).await??;
2635                continue;
2636            }
2637            Event::Worker(event) => {
2638                match event {
2639                    WorkerEvent::Stopped => {
2640                        if quit {
2641                            tracing::info!("vm stopped");
2642                        } else {
2643                            tracing::error!("vm worker unexpectedly stopped");
2644                        }
2645                        break;
2646                    }
2647                    WorkerEvent::Failed(err) => {
2648                        tracing::error!(error = &err as &dyn std::error::Error, "vm worker failed");
2649                        break;
2650                    }
2651                    WorkerEvent::RestartFailed(err) => {
2652                        tracing::error!(
2653                            error = &err as &dyn std::error::Error,
2654                            "vm worker restart failed"
2655                        );
2656                    }
2657                    WorkerEvent::Started => {
2658                        tracing::info!("vm worker restarted");
2659                    }
2660                }
2661                continue;
2662            }
2663            Event::VncWorker(event) => {
2664                match event {
2665                    WorkerEvent::Stopped => tracing::error!("vnc unexpectedly stopped"),
2666                    WorkerEvent::Failed(err) => {
2667                        tracing::error!(
2668                            error = &err as &dyn std::error::Error,
2669                            "vnc worker failed"
2670                        );
2671                    }
2672                    WorkerEvent::RestartFailed(err) => {
2673                        tracing::error!(
2674                            error = &err as &dyn std::error::Error,
2675                            "vnc worker restart failed"
2676                        );
2677                    }
2678                    WorkerEvent::Started => {
2679                        tracing::info!("vnc worker restarted");
2680                    }
2681                }
2682                continue;
2683            }
2684            Event::StateChange(r) => {
2685                match r {
2686                    Ok(sc) => match sc {
2687                        StateChange::Pause(success) => {
2688                            if success {
2689                                tracing::info!("pause complete");
2690                            } else {
2691                                tracing::warn!("already paused");
2692                            }
2693                        }
2694                        StateChange::Resume(success) => {
2695                            if success {
2696                                tracing::info!("resumed complete");
2697                            } else {
2698                                tracing::warn!("already running");
2699                            }
2700                        }
2701                        StateChange::Reset(r) => match r {
2702                            Ok(()) => tracing::info!("reset complete"),
2703                            Err(err) => tracing::error!(
2704                                error = &err as &dyn std::error::Error,
2705                                "reset failed"
2706                            ),
2707                        },
2708                        StateChange::PulseSaveRestore(r) => match r {
2709                            Ok(()) => tracing::info!("pulse save/restore complete"),
2710                            Err(err) => tracing::error!(
2711                                error = &err as &dyn std::error::Error,
2712                                "pulse save/restore failed"
2713                            ),
2714                        },
2715                        StateChange::ServiceVtl2(r) => match r {
2716                            Ok(dur) => {
2717                                tracing::info!(
2718                                    duration = dur.as_millis() as i64,
2719                                    "vtl2 servicing complete"
2720                                )
2721                            }
2722                            Err(err) => tracing::error!(
2723                                error = err.as_ref() as &dyn std::error::Error,
2724                                "vtl2 servicing failed"
2725                            ),
2726                        },
2727                    },
2728                    Err(err) => {
2729                        tracing::error!(
2730                            error = &err as &dyn std::error::Error,
2731                            "communication failure during state change"
2732                        );
2733                    }
2734                }
2735                state_change_task = None;
2736                continue;
2737            }
2738            Event::ShutdownResult(r) => {
2739                match r {
2740                    Ok(r) => match r {
2741                        hyperv_ic_resources::shutdown::ShutdownResult::Ok => {
2742                            tracing::info!("shutdown initiated");
2743                        }
2744                        hyperv_ic_resources::shutdown::ShutdownResult::NotReady => {
2745                            tracing::error!("shutdown ic not ready");
2746                        }
2747                        hyperv_ic_resources::shutdown::ShutdownResult::AlreadyInProgress => {
2748                            tracing::error!("shutdown already in progress");
2749                        }
2750                        hyperv_ic_resources::shutdown::ShutdownResult::Failed(hr) => {
2751                            tracing::error!("shutdown failed with error code {hr:#x}");
2752                        }
2753                    },
2754                    Err(err) => {
2755                        tracing::error!(
2756                            error = &err as &dyn std::error::Error,
2757                            "communication failure during shutdown"
2758                        );
2759                    }
2760                }
2761                pending_shutdown = None;
2762                continue;
2763            }
2764        };
2765
2766        fn inspect_obj<'a>(
2767            target: InspectTarget,
2768            mesh: &'a VmmMesh,
2769            vm_worker: &'a WorkerHandle,
2770            vnc_worker: Option<&'a WorkerHandle>,
2771            gdb_worker: Option<&'a WorkerHandle>,
2772            diag_inspector: &'a mut DiagInspector,
2773        ) -> impl 'a + InspectMut {
2774            inspect::adhoc_mut(move |req| match target {
2775                InspectTarget::Host => {
2776                    let mut resp = req.respond();
2777                    resp.field("mesh", mesh)
2778                        .field("vm", vm_worker)
2779                        .field("vnc", vnc_worker)
2780                        .field("gdb", gdb_worker);
2781                }
2782                InspectTarget::Paravisor => {
2783                    diag_inspector.inspect_mut(req);
2784                }
2785            })
2786        }
2787
2788        fn state_change<U: 'static + Send>(
2789            driver: impl Spawn,
2790            vm_rpc: &mesh::Sender<VmRpc>,
2791            state_change_task: &mut Option<Task<Result<StateChange, RpcError>>>,
2792            f: impl FnOnce(Rpc<(), U>) -> VmRpc,
2793            g: impl FnOnce(U) -> StateChange + 'static + Send,
2794        ) {
2795            if state_change_task.is_some() {
2796                tracing::error!("state change already in progress");
2797            } else {
2798                let rpc = vm_rpc.call(f, ());
2799                *state_change_task =
2800                    Some(driver.spawn("state-change", async move { Ok(g(rpc.await?)) }));
2801            }
2802        }
2803
2804        match cmd {
2805            InteractiveCommand::Panic => {
2806                panic!("injected panic")
2807            }
2808            InteractiveCommand::Restart => {
2809                // create a new host process
2810                let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
2811
2812                vm_worker.restart(&vm_host);
2813            }
2814            InteractiveCommand::Pause => {
2815                state_change(
2816                    driver,
2817                    &vm_rpc,
2818                    &mut state_change_task,
2819                    VmRpc::Pause,
2820                    StateChange::Pause,
2821                );
2822            }
2823            InteractiveCommand::Resume => {
2824                state_change(
2825                    driver,
2826                    &vm_rpc,
2827                    &mut state_change_task,
2828                    VmRpc::Resume,
2829                    StateChange::Resume,
2830                );
2831            }
2832            InteractiveCommand::Reset => {
2833                state_change(
2834                    driver,
2835                    &vm_rpc,
2836                    &mut state_change_task,
2837                    VmRpc::Reset,
2838                    StateChange::Reset,
2839                );
2840            }
2841            InteractiveCommand::PulseSaveRestore => {
2842                state_change(
2843                    driver,
2844                    &vm_rpc,
2845                    &mut state_change_task,
2846                    VmRpc::PulseSaveRestore,
2847                    StateChange::PulseSaveRestore,
2848                );
2849            }
2850            InteractiveCommand::SchedulePulseSaveRestore { interval } => {
2851                pulse_save_restore_interval = match interval {
2852                    Some(seconds) if seconds != 0 => Some(Duration::from_secs(seconds)),
2853                    _ => {
2854                        // Treat None and 0 seconds as do not perform scheduled pulse save restores anymore.
2855                        None
2856                    }
2857                }
2858            }
2859            InteractiveCommand::Shutdown {
2860                reboot,
2861                hibernate,
2862                force,
2863            } => {
2864                if pending_shutdown.is_some() {
2865                    println!("shutdown already in progress");
2866                } else if let Some(ic) = &resources.shutdown_ic {
2867                    let params = hyperv_ic_resources::shutdown::ShutdownParams {
2868                        shutdown_type: if hibernate {
2869                            hyperv_ic_resources::shutdown::ShutdownType::Hibernate
2870                        } else if reboot {
2871                            hyperv_ic_resources::shutdown::ShutdownType::Reboot
2872                        } else {
2873                            hyperv_ic_resources::shutdown::ShutdownType::PowerOff
2874                        },
2875                        force,
2876                    };
2877                    pending_shutdown =
2878                        Some(ic.call(hyperv_ic_resources::shutdown::ShutdownRpc::Shutdown, params));
2879                } else {
2880                    println!("no shutdown ic configured");
2881                }
2882            }
2883            InteractiveCommand::Nmi => {
2884                let _ = vm_rpc.call(VmRpc::Nmi, 0).await;
2885            }
2886            InteractiveCommand::ClearHalt => {
2887                vm_rpc.call(VmRpc::ClearHalt, ()).await.ok();
2888            }
2889            InteractiveCommand::AddDisk {
2890                read_only,
2891                target,
2892                path,
2893                lun,
2894                ram,
2895                file_path,
2896                is_dvd,
2897            } => {
2898                let action = async {
2899                    let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2900                    let disk_type = match ram {
2901                        None => {
2902                            let path = file_path.context("no filename passed")?;
2903                            open_disk_type(path.as_ref(), read_only)
2904                                .with_context(|| format!("failed to open {}", path.display()))?
2905                        }
2906                        Some(size) => {
2907                            Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
2908                                RamDiskLayerHandle { len: Some(size) },
2909                            ))
2910                        }
2911                    };
2912
2913                    let device = if is_dvd {
2914                        SimpleScsiDvdHandle {
2915                            media: Some(disk_type),
2916                            requests: None,
2917                        }
2918                        .into_resource()
2919                    } else {
2920                        SimpleScsiDiskHandle {
2921                            disk: disk_type,
2922                            read_only,
2923                            parameters: Default::default(),
2924                        }
2925                        .into_resource()
2926                    };
2927
2928                    let cfg = ScsiDeviceAndPath {
2929                        path: ScsiPath { path, target, lun },
2930                        device,
2931                    };
2932
2933                    scsi.call_failable(ScsiControllerRequest::AddDevice, cfg)
2934                        .await?;
2935
2936                    anyhow::Result::<_>::Ok(())
2937                };
2938
2939                if let Err(error) = action.await {
2940                    tracing::error!(error = error.as_error(), "error adding disk")
2941                }
2942            }
2943            InteractiveCommand::RmDisk { target, path, lun } => {
2944                let action = async {
2945                    let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
2946                    scsi.call_failable(
2947                        ScsiControllerRequest::RemoveDevice,
2948                        ScsiPath { target, path, lun },
2949                    )
2950                    .await?;
2951                    anyhow::Ok(())
2952                };
2953
2954                if let Err(error) = action.await {
2955                    tracing::error!(error = error.as_error(), "error removing disk")
2956                }
2957            }
2958            InteractiveCommand::Vtl2Settings(cmd) => {
2959                if resources.vtl2_settings.is_none() {
2960                    eprintln!("error: no VTL2 settings (not running with VTL2?)");
2961                    continue;
2962                }
2963                let action = async {
2964                    match cmd {
2965                        Vtl2SettingsCommand::Show => {
2966                            let settings = resources.vtl2_settings.as_ref().unwrap();
2967                            println!("{:#?}", settings);
2968                        }
2969                        Vtl2SettingsCommand::AddScsiDisk {
2970                            controller,
2971                            lun,
2972                            backing_nvme_nsid,
2973                            backing_scsi_lun,
2974                        } => {
2975                            // Determine the backing device type and path
2976                            let (device_type, device_path, sub_device_path) = match (
2977                                backing_nvme_nsid,
2978                                backing_scsi_lun,
2979                            ) {
2980                                (Some(nsid), None) => (
2981                                    vtl2_settings_proto::physical_device::DeviceType::Nvme,
2982                                    storage_builder::NVME_VTL2_INSTANCE_ID,
2983                                    nsid,
2984                                ),
2985                                (None, Some(scsi_lun)) => (
2986                                    vtl2_settings_proto::physical_device::DeviceType::Vscsi,
2987                                    storage_builder::SCSI_VTL2_INSTANCE_ID,
2988                                    scsi_lun,
2989                                ),
2990                                (Some(_), Some(_)) => {
2991                                    anyhow::bail!(
2992                                        "can't specify both --backing-nvme-nsid and --backing-scsi-lun"
2993                                    );
2994                                }
2995                                (None, None) => {
2996                                    anyhow::bail!(
2997                                        "must specify either --backing-nvme-nsid or --backing-scsi-lun"
2998                                    );
2999                                }
3000                            };
3001
3002                            // Default to the standard OpenVMM VTL0 SCSI instance
3003                            let controller_guid = controller
3004                                .map(|s| s.parse())
3005                                .transpose()
3006                                .context("invalid controller GUID")?
3007                                .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3008
3009                            resources
3010                                .add_vtl0_scsi_disk(
3011                                    controller_guid,
3012                                    lun,
3013                                    device_type,
3014                                    device_path,
3015                                    sub_device_path,
3016                                )
3017                                .await?;
3018
3019                            let backing_desc = if backing_nvme_nsid.is_some() {
3020                                format!("nvme_nsid={}", sub_device_path)
3021                            } else {
3022                                format!("scsi_lun={}", sub_device_path)
3023                            };
3024                            println!(
3025                                "Added VTL0 SCSI disk: controller={}, lun={}, backing={}",
3026                                controller_guid, lun, backing_desc
3027                            );
3028                        }
3029                        Vtl2SettingsCommand::RmScsiDisk { controller, lun } => {
3030                            // Default to the standard OpenVMM VTL0 SCSI instance
3031                            let controller_guid = controller
3032                                .map(|s| s.parse())
3033                                .transpose()
3034                                .context("invalid controller GUID")?
3035                                .unwrap_or(storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE);
3036
3037                            resources
3038                                .remove_vtl0_scsi_disk(controller_guid, lun)
3039                                .await?;
3040
3041                            println!(
3042                                "Removed VTL0 SCSI disk: controller={}, lun={}",
3043                                controller_guid, lun
3044                            );
3045                        }
3046                    }
3047                    anyhow::Ok(())
3048                };
3049
3050                if let Err(error) = action.await {
3051                    eprintln!("error: {}", error);
3052                }
3053            }
3054            InteractiveCommand::AddNvmeNs {
3055                read_only,
3056                nsid,
3057                ram,
3058                file_path,
3059                vtl0_lun,
3060            } => {
3061                if resources.vtl2_settings.is_none() {
3062                    eprintln!("error: add-nvme-ns requires --vtl2 mode");
3063                    continue;
3064                }
3065                let action = async {
3066                    let nvme = resources
3067                        .nvme_vtl2_rpc
3068                        .as_ref()
3069                        .context("no vtl2 nvme controller")?;
3070                    let disk_type = match (ram, file_path) {
3071                        (None, Some(path)) => open_disk_type(path.as_ref(), read_only)
3072                            .with_context(|| format!("failed to open {}", path.display()))?,
3073                        (Some(size), None) => {
3074                            Resource::new(disk_backend_resources::LayeredDiskHandle::single_layer(
3075                                RamDiskLayerHandle { len: Some(size) },
3076                            ))
3077                        }
3078                        (None, None) => {
3079                            anyhow::bail!("must specify either file path or --ram");
3080                        }
3081                        (Some(_), Some(_)) => {
3082                            anyhow::bail!("cannot specify both file path and --ram");
3083                        }
3084                    };
3085
3086                    let ns = NamespaceDefinition {
3087                        nsid,
3088                        read_only,
3089                        disk: disk_type,
3090                    };
3091
3092                    nvme.call_failable(NvmeControllerRequest::AddNamespace, ns)
3093                        .await?;
3094                    println!("Added namespace {}", nsid);
3095
3096                    // If --vtl0-lun was specified, add a SCSI disk to VTL0 backed by the NVMe namespace
3097                    if let Some(lun) = vtl0_lun {
3098                        resources
3099                            .add_vtl0_scsi_disk(
3100                                storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3101                                lun,
3102                                vtl2_settings_proto::physical_device::DeviceType::Nvme,
3103                                storage_builder::NVME_VTL2_INSTANCE_ID,
3104                                nsid,
3105                            )
3106                            .await?;
3107                        println!("Exposed namespace {} to VTL0 as SCSI lun={}", nsid, lun);
3108                    }
3109
3110                    Ok(())
3111                };
3112
3113                if let Err(error) = action.await {
3114                    eprintln!("error adding nvme namespace: {}", error);
3115                }
3116            }
3117            InteractiveCommand::RmNvmeNs { nsid, vtl0 } => {
3118                if resources.vtl2_settings.is_none() {
3119                    eprintln!("error: rm-nvme-ns requires --vtl2 mode");
3120                    continue;
3121                }
3122                let action = async {
3123                    // If --vtl0 was specified, find and remove the SCSI disk backed by this namespace
3124                    if vtl0 {
3125                        let removed_lun = resources
3126                            .remove_vtl0_scsi_disk_by_nvme_nsid(
3127                                storage_builder::UNDERHILL_VTL0_SCSI_INSTANCE,
3128                                storage_builder::NVME_VTL2_INSTANCE_ID,
3129                                nsid,
3130                            )
3131                            .await?;
3132                        if let Some(lun) = removed_lun {
3133                            println!("Removed VTL0 SCSI lun={}", lun);
3134                        } else {
3135                            println!("No VTL0 SCSI disk found backed by NVMe nsid={}", nsid);
3136                        }
3137                    }
3138
3139                    let nvme = resources
3140                        .nvme_vtl2_rpc
3141                        .as_ref()
3142                        .context("no vtl2 nvme controller")?;
3143                    nvme.call_failable(NvmeControllerRequest::RemoveNamespace, nsid)
3144                        .await?;
3145                    println!("Removed NVMe namespace {}", nsid);
3146                    anyhow::Ok(())
3147                };
3148
3149                if let Err(error) = action.await {
3150                    eprintln!("error removing nvme namespace: {}", error);
3151                }
3152            }
3153            InteractiveCommand::Inspect {
3154                recursive,
3155                limit,
3156                paravisor,
3157                element,
3158                update,
3159            } => {
3160                let obj = inspect_obj(
3161                    if paravisor {
3162                        InspectTarget::Paravisor
3163                    } else {
3164                        InspectTarget::Host
3165                    },
3166                    mesh,
3167                    &vm_worker,
3168                    vnc_worker.as_ref(),
3169                    gdb_worker.as_ref(),
3170                    &mut diag_inspector,
3171                );
3172
3173                if let Some(value) = update {
3174                    let Some(element) = element else {
3175                        anyhow::bail!("must provide element for update")
3176                    };
3177
3178                    let value = async {
3179                        let update = inspect::update(&element, &value, obj);
3180                        let value = CancelContext::new()
3181                            .with_timeout(Duration::from_secs(1))
3182                            .until_cancelled(update)
3183                            .await??;
3184                        anyhow::Ok(value)
3185                    }
3186                    .await;
3187                    match value {
3188                        Ok(node) => match &node.kind {
3189                            inspect::ValueKind::String(s) => println!("{s}"),
3190                            _ => println!("{:#}", node),
3191                        },
3192                        Err(err) => println!("error: {:#}", err),
3193                    }
3194                } else {
3195                    let element = element.unwrap_or_default();
3196                    let depth = if recursive { limit } else { Some(0) };
3197                    let node = async {
3198                        let mut inspection =
3199                            InspectionBuilder::new(&element).depth(depth).inspect(obj);
3200                        let _ = CancelContext::new()
3201                            .with_timeout(Duration::from_secs(1))
3202                            .until_cancelled(inspection.resolve())
3203                            .await;
3204                        inspection.results()
3205                    }
3206                    .await;
3207
3208                    println!("{:#}", node);
3209                }
3210            }
3211            InteractiveCommand::RestartVnc => {
3212                if let Some(vnc) = &mut vnc_worker {
3213                    let action = async {
3214                        let vnc_host = mesh
3215                            .make_host("vnc", None)
3216                            .await
3217                            .context("spawning vnc process failed")?;
3218
3219                        vnc.restart(&vnc_host);
3220                        anyhow::Result::<_>::Ok(())
3221                    };
3222
3223                    if let Err(error) = action.await {
3224                        eprintln!("error: {}", error);
3225                    }
3226                } else {
3227                    eprintln!("ERROR: no VNC server running");
3228                }
3229            }
3230            InteractiveCommand::Hvsock { term, port } => {
3231                let vm_rpc = &vm_rpc;
3232                let action = async || {
3233                    let service_id = new_hvsock_service_id(port);
3234                    let socket = vm_rpc
3235                        .call_failable(
3236                            VmRpc::ConnectHvsock,
3237                            (
3238                                CancelContext::new().with_timeout(Duration::from_secs(2)),
3239                                service_id,
3240                                DeviceVtl::Vtl0,
3241                            ),
3242                        )
3243                        .await?;
3244                    let socket = PolledSocket::new(driver, socket)?;
3245                    let mut console = console_relay::Console::new(
3246                        driver.clone(),
3247                        term.or_else(openvmm_terminal_app).as_deref(),
3248                        Some(ConsoleLaunchOptions {
3249                            window_title: Some(format!("HVSock{} [OpenVMM]", port)),
3250                        }),
3251                    )?;
3252                    driver
3253                        .spawn("console-relay", async move { console.relay(socket).await })
3254                        .detach();
3255                    anyhow::Result::<_>::Ok(())
3256                };
3257
3258                if let Err(error) = (action)().await {
3259                    eprintln!("error: {}", error);
3260                }
3261            }
3262            InteractiveCommand::ServiceVtl2 {
3263                user_mode_only,
3264                igvm,
3265                mana_keepalive,
3266                nvme_keepalive,
3267            } => {
3268                let paravisor_diag = paravisor_diag.clone();
3269                let vm_rpc = vm_rpc.clone();
3270                let igvm = igvm.or_else(|| opt.igvm.clone());
3271                let ged_rpc = resources.ged_rpc.clone();
3272                let r = async move {
3273                    let start;
3274                    if user_mode_only {
3275                        start = Instant::now();
3276                        paravisor_diag.restart().await?;
3277                    } else {
3278                        let path = igvm.context("no igvm file loaded")?;
3279                        let file = fs_err::File::open(path)?;
3280                        start = Instant::now();
3281                        openvmm_helpers::underhill::save_underhill(
3282                            &vm_rpc,
3283                            ged_rpc.as_ref().context("no GED")?,
3284                            GuestServicingFlags {
3285                                nvme_keepalive,
3286                                mana_keepalive,
3287                            },
3288                            file.into(),
3289                        )
3290                        .await?;
3291                        openvmm_helpers::underhill::restore_underhill(
3292                            &vm_rpc,
3293                            ged_rpc.as_ref().context("no GED")?,
3294                        )
3295                        .await?;
3296                    }
3297                    let end = Instant::now();
3298                    Ok(end - start)
3299                }
3300                .map(|r| Ok(StateChange::ServiceVtl2(r)));
3301                if state_change_task.is_some() {
3302                    tracing::error!("state change already in progress");
3303                } else {
3304                    state_change_task = Some(driver.spawn("state-change", r));
3305                }
3306            }
3307            InteractiveCommand::Quit => {
3308                tracing::info!("quitting");
3309                // Work around the detached SCSI task holding up worker stop.
3310                // TODO: Fix the underlying bug
3311                resources.scsi_rpc = None;
3312                resources.nvme_vtl2_rpc = None;
3313
3314                vm_worker.stop();
3315                quit = true;
3316            }
3317            InteractiveCommand::ReadMemory { gpa, size, file } => {
3318                let size = size as usize;
3319                let data = vm_rpc.call(VmRpc::ReadMemory, (gpa, size)).await?;
3320
3321                match data {
3322                    Ok(bytes) => {
3323                        if let Some(file) = file {
3324                            if let Err(err) = fs_err::write(file, bytes) {
3325                                eprintln!("error: {err:?}");
3326                            }
3327                        } else {
3328                            let width = 16;
3329                            let show_ascii = true;
3330
3331                            let mut dump = String::new();
3332                            for (i, chunk) in bytes.chunks(width).enumerate() {
3333                                let hex_part: Vec<String> =
3334                                    chunk.iter().map(|byte| format!("{:02x}", byte)).collect();
3335                                let hex_line = hex_part.join(" ");
3336
3337                                if show_ascii {
3338                                    let ascii_part: String = chunk
3339                                        .iter()
3340                                        .map(|&byte| {
3341                                            if byte.is_ascii_graphic() || byte == b' ' {
3342                                                byte as char
3343                                            } else {
3344                                                '.'
3345                                            }
3346                                        })
3347                                        .collect();
3348                                    dump.push_str(&format!(
3349                                        "{:04x}: {:<width$}  {}\n",
3350                                        i * width,
3351                                        hex_line,
3352                                        ascii_part,
3353                                        width = width * 3 - 1
3354                                    ));
3355                                } else {
3356                                    dump.push_str(&format!("{:04x}: {}\n", i * width, hex_line));
3357                                }
3358                            }
3359
3360                            println!("{dump}");
3361                        }
3362                    }
3363                    Err(err) => {
3364                        eprintln!("error: {err:?}");
3365                    }
3366                }
3367            }
3368            InteractiveCommand::WriteMemory { gpa, hex, file } => {
3369                if hex.is_some() == file.is_some() {
3370                    eprintln!("error: either path to the file or the hex string must be specified");
3371                    continue;
3372                }
3373
3374                let data = if let Some(file) = file {
3375                    let data = fs_err::read(file);
3376                    match data {
3377                        Ok(data) => data,
3378                        Err(err) => {
3379                            eprintln!("error: {err:?}");
3380                            continue;
3381                        }
3382                    }
3383                } else if let Some(hex) = hex {
3384                    if hex.len() & 1 != 0 {
3385                        eprintln!(
3386                            "error: expected even number of hex digits (2 hex digits per byte)"
3387                        );
3388                        continue;
3389                    }
3390                    let data: Result<Vec<u8>, String> = (0..hex.len())
3391                        .step_by(2)
3392                        .map(|i| {
3393                            u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
3394                                format!("invalid hex character at position {}: {}", i, e)
3395                            })
3396                        })
3397                        .collect();
3398
3399                    match data {
3400                        Ok(data) => data,
3401                        Err(err) => {
3402                            eprintln!("error: {err}");
3403                            continue;
3404                        }
3405                    }
3406                } else {
3407                    unreachable!();
3408                };
3409
3410                if data.is_empty() {
3411                    eprintln!("error: no data to write");
3412                    continue;
3413                }
3414
3415                if let Err(err) = vm_rpc.call(VmRpc::WriteMemory, (gpa, data)).await? {
3416                    eprintln!("error: {err:?}");
3417                }
3418            }
3419            InteractiveCommand::Kvp(command) => {
3420                let Some(kvp) = &resources.kvp_ic else {
3421                    eprintln!("error: no kvp ic configured");
3422                    continue;
3423                };
3424                if let Err(err) = kvp::handle_kvp(kvp, command).await {
3425                    eprintln!("error: {err:#}");
3426                }
3427            }
3428            InteractiveCommand::Input { .. } | InteractiveCommand::InputMode => unreachable!(),
3429        }
3430    }
3431
3432    vm_worker.stop();
3433    vm_worker.join().await?;
3434    Ok(())
3435}
3436
3437struct DiagDialer {
3438    driver: DefaultDriver,
3439    vm_rpc: mesh::Sender<VmRpc>,
3440    openhcl_vtl: DeviceVtl,
3441}
3442
3443impl mesh_rpc::client::Dial for DiagDialer {
3444    type Stream = PolledSocket<unix_socket::UnixStream>;
3445
3446    async fn dial(&mut self) -> io::Result<Self::Stream> {
3447        let service_id = new_hvsock_service_id(1);
3448        let socket = self
3449            .vm_rpc
3450            .call_failable(
3451                VmRpc::ConnectHvsock,
3452                (
3453                    CancelContext::new().with_timeout(Duration::from_secs(2)),
3454                    service_id,
3455                    self.openhcl_vtl,
3456                ),
3457            )
3458            .await
3459            .map_err(io::Error::other)?;
3460
3461        PolledSocket::new(&self.driver, socket)
3462    }
3463}
3464
3465/// An object that implements [`InspectMut`] by sending an inspect request over
3466/// TTRPC to the guest (typically the paravisor running in VTL2), then stitching
3467/// the response back into the inspect tree.
3468///
3469/// This also caches the TTRPC connection to the guest so that only the first
3470/// inspect request has to wait for the connection to be established.
3471pub struct DiagInspector(DiagInspectorInner);
3472
3473enum DiagInspectorInner {
3474    NotStarted(DefaultDriver, Arc<diag_client::DiagClient>),
3475    Started {
3476        send: mesh::Sender<inspect::Deferred>,
3477        _task: Task<()>,
3478    },
3479    Invalid,
3480}
3481
3482impl DiagInspector {
3483    pub fn new(driver: DefaultDriver, diag_client: Arc<diag_client::DiagClient>) -> Self {
3484        Self(DiagInspectorInner::NotStarted(driver, diag_client))
3485    }
3486
3487    fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
3488        loop {
3489            match self.0 {
3490                DiagInspectorInner::NotStarted { .. } => {
3491                    let DiagInspectorInner::NotStarted(driver, client) =
3492                        std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
3493                    else {
3494                        unreachable!()
3495                    };
3496                    let (send, recv) = mesh::channel();
3497                    let task = driver.clone().spawn("diag-inspect", async move {
3498                        Self::run(&client, recv).await
3499                    });
3500
3501                    self.0 = DiagInspectorInner::Started { send, _task: task };
3502                }
3503                DiagInspectorInner::Started { ref send, .. } => break send,
3504                DiagInspectorInner::Invalid => unreachable!(),
3505            }
3506        }
3507    }
3508
3509    async fn run(
3510        diag_client: &diag_client::DiagClient,
3511        mut recv: mesh::Receiver<inspect::Deferred>,
3512    ) {
3513        while let Some(deferred) = recv.next().await {
3514            let info = deferred.external_request();
3515            let result = match info.request_type {
3516                inspect::ExternalRequestType::Inspect { depth } => {
3517                    if depth == 0 {
3518                        Ok(inspect::Node::Unevaluated)
3519                    } else {
3520                        // TODO: Support taking timeouts from the command line
3521                        diag_client
3522                            .inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
3523                            .await
3524                    }
3525                }
3526                inspect::ExternalRequestType::Update { value } => {
3527                    (diag_client.update(info.path, value).await).map(inspect::Node::Value)
3528                }
3529            };
3530            deferred.complete_external(
3531                result.unwrap_or_else(|err| {
3532                    inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
3533                }),
3534                inspect::SensitivityLevel::Unspecified,
3535            )
3536        }
3537    }
3538}
3539
3540impl InspectMut for DiagInspector {
3541    fn inspect_mut(&mut self, req: inspect::Request<'_>) {
3542        self.start().send(req.defer());
3543    }
3544}
3545
3546enum InspectTarget {
3547    Host,
3548    Paravisor,
3549}
3550
3551mod interactive_console {
3552    use super::InteractiveCommand;
3553    use rustyline::Helper;
3554    use rustyline::Highlighter;
3555    use rustyline::Hinter;
3556    use rustyline::Validator;
3557
3558    #[derive(Helper, Highlighter, Hinter, Validator)]
3559    pub(crate) struct OpenvmmRustylineEditor {
3560        pub openvmm_inspect_req: std::sync::Arc<
3561            mesh::Sender<(
3562                super::InspectTarget,
3563                String,
3564                mesh::OneshotSender<inspect::Node>,
3565            )>,
3566        >,
3567    }
3568
3569    impl rustyline::completion::Completer for OpenvmmRustylineEditor {
3570        type Candidate = String;
3571
3572        fn complete(
3573            &self,
3574            line: &str,
3575            pos: usize,
3576            _ctx: &rustyline::Context<'_>,
3577        ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
3578            let Ok(cmd) = shell_words::split(line) else {
3579                return Ok((0, Vec::with_capacity(0)));
3580            };
3581
3582            let completions = futures::executor::block_on(
3583                clap_dyn_complete::Complete {
3584                    cmd,
3585                    raw: Some(line.into()),
3586                    position: Some(pos),
3587                }
3588                .generate_completions::<InteractiveCommand>(None, self),
3589            );
3590
3591            let pos_from_end = {
3592                let line = line.chars().take(pos).collect::<String>();
3593
3594                let trailing_ws = line.len() - line.trim_end().len();
3595
3596                if trailing_ws > 0 {
3597                    line.len() - trailing_ws + 1 // +1 for the space
3598                } else {
3599                    let last_word = shell_words::split(&line)
3600                        .unwrap_or_default()
3601                        .last()
3602                        .cloned()
3603                        .unwrap_or_default();
3604
3605                    line.len() - last_word.len()
3606                }
3607            };
3608
3609            Ok((pos_from_end, completions))
3610        }
3611    }
3612
3613    impl clap_dyn_complete::CustomCompleterFactory for &OpenvmmRustylineEditor {
3614        type CustomCompleter = OpenvmmComplete;
3615        async fn build(&self, _ctx: &clap_dyn_complete::RootCtx<'_>) -> Self::CustomCompleter {
3616            OpenvmmComplete {
3617                openvmm_inspect_req: self.openvmm_inspect_req.clone(),
3618            }
3619        }
3620    }
3621
3622    pub struct OpenvmmComplete {
3623        openvmm_inspect_req: std::sync::Arc<
3624            mesh::Sender<(
3625                super::InspectTarget,
3626                String,
3627                mesh::OneshotSender<inspect::Node>,
3628            )>,
3629        >,
3630    }
3631
3632    impl clap_dyn_complete::CustomCompleter for OpenvmmComplete {
3633        async fn complete(
3634            &self,
3635            ctx: &clap_dyn_complete::RootCtx<'_>,
3636            subcommand_path: &[&str],
3637            arg_id: &str,
3638        ) -> Vec<String> {
3639            match (subcommand_path, arg_id) {
3640                (["openvmm", "inspect"], "element") => {
3641                    let on_error = vec!["failed/to/connect".into()];
3642
3643                    let (parent_path, to_complete) = (ctx.to_complete)
3644                        .rsplit_once('/')
3645                        .unwrap_or(("", ctx.to_complete));
3646
3647                    let node = {
3648                        let paravisor = {
3649                            let raw_arg = ctx
3650                                .matches
3651                                .subcommand()
3652                                .unwrap()
3653                                .1
3654                                .get_one::<String>("paravisor")
3655                                .map(|x| x.as_str())
3656                                .unwrap_or_default();
3657                            raw_arg == "true"
3658                        };
3659
3660                        let (tx, rx) = mesh::oneshot();
3661                        self.openvmm_inspect_req.send((
3662                            if paravisor {
3663                                super::InspectTarget::Paravisor
3664                            } else {
3665                                super::InspectTarget::Host
3666                            },
3667                            parent_path.to_owned(),
3668                            tx,
3669                        ));
3670                        let Ok(node) = rx.await else {
3671                            return on_error;
3672                        };
3673
3674                        node
3675                    };
3676
3677                    let mut completions = Vec::new();
3678
3679                    if let inspect::Node::Dir(dir) = node {
3680                        for entry in dir {
3681                            if entry.name.starts_with(to_complete) {
3682                                if parent_path.is_empty() {
3683                                    completions.push(format!("{}/", entry.name))
3684                                } else {
3685                                    completions.push(format!(
3686                                        "{}/{}{}",
3687                                        parent_path,
3688                                        entry.name,
3689                                        if matches!(entry.node, inspect::Node::Dir(..)) {
3690                                            "/"
3691                                        } else {
3692                                            ""
3693                                        }
3694                                    ))
3695                                }
3696                            }
3697                        }
3698                    } else {
3699                        return on_error;
3700                    }
3701
3702                    completions
3703                }
3704                _ => Vec::new(),
3705            }
3706        }
3707    }
3708}