openvmm_entry/
lib.rs

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