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