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