openvmm_entry/
lib.rs

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