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