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