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