mod cli_args;
mod console;
mod meshworker;
mod serial_io;
mod storage_builder;
mod tracing_init;
mod ttrpc;
pub use cli_args::Options;
use crate::cli_args::SecureBootTemplateCli;
use anyhow::bail;
use anyhow::Context;
use chipset_resources::battery::HostBatteryUpdate;
use clap::CommandFactory;
use clap::FromArgMatches;
use clap::Parser;
use cli_args::DiskCliKind;
use cli_args::EndpointConfigCli;
use cli_args::NicConfigCli;
use cli_args::SerialConfigCli;
use cli_args::UefiConsoleModeCli;
use floppy_resources::FloppyDiskConfig;
use framebuffer::FramebufferAccess;
use framebuffer::FRAMEBUFFER_SIZE;
use futures::executor::block_on;
use futures::io::AllowStdIo;
use futures::AsyncReadExt;
use futures::AsyncWrite;
use futures::AsyncWriteExt;
use futures::FutureExt;
use futures::StreamExt;
use futures_concurrency::stream::Merge;
use gdma_resources::GdmaDeviceHandle;
use gdma_resources::VportDefinition;
use guid::Guid;
use hvlite_defs::config::Config;
use hvlite_defs::config::DeviceVtl;
use hvlite_defs::config::HypervisorConfig;
use hvlite_defs::config::LateMapVtl0MemoryPolicy;
use hvlite_defs::config::LoadMode;
use hvlite_defs::config::MemoryConfig;
use hvlite_defs::config::ProcessorTopologyConfig;
use hvlite_defs::config::SerialInformation;
use hvlite_defs::config::VirtioBus;
use hvlite_defs::config::VmbusConfig;
use hvlite_defs::config::VpciDeviceConfig;
use hvlite_defs::config::Vtl2BaseAddressType;
use hvlite_defs::config::Vtl2Config;
use hvlite_defs::config::DEFAULT_MMIO_GAPS;
use hvlite_defs::config::DEFAULT_MMIO_GAPS_WITH_VTL2;
use hvlite_defs::config::DEFAULT_PCAT_BOOT_ORDER;
use hvlite_defs::rpc::PulseSaveRestoreError;
use hvlite_defs::rpc::VmRpc;
use hvlite_defs::worker::VmWorkerParameters;
use hvlite_defs::worker::VM_WORKER;
use hvlite_helpers::crash_dump::spawn_dump_handler;
use hvlite_helpers::disk::open_disk_type;
use input_core::MultiplexedInputHandle;
use inspect::InspectMut;
use inspect::InspectionBuilder;
use io::Read;
use mesh::error::RemoteError;
use mesh::rpc::Rpc;
use mesh::rpc::RpcSend;
use mesh::CancelContext;
use mesh::RecvError;
use mesh_worker::launch_local_worker;
use mesh_worker::WorkerEvent;
use mesh_worker::WorkerHandle;
use meshworker::VmmMesh;
use net_backend_resources::mac_address::MacAddress;
use pal_async::driver::Driver;
use pal_async::pipe::PolledPipe;
use pal_async::task::Spawn;
use pal_async::task::Task;
use pal_async::timer::PolledTimer;
use pal_async::DefaultDriver;
use pal_async::DefaultPool;
use scsidisk_resources::SimpleScsiDiskHandle;
use scsidisk_resources::SimpleScsiDvdHandle;
use serial_16550_resources::ComPort;
use serial_io::SerialIo;
use sparse_mmap::alloc_shared_memory;
use std::cell::RefCell;
use std::fmt::Write as _;
use std::future::pending;
use std::io;
#[cfg(unix)]
use std::io::IsTerminal;
use std::io::Write;
use std::net::TcpListener;
use std::path::Path;
use std::path::PathBuf;
use std::pin::pin;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use storvsp_resources::ScsiControllerRequest;
use storvsp_resources::ScsiDeviceAndPath;
use storvsp_resources::ScsiPath;
use tpm_resources::TpmDeviceHandle;
use tpm_resources::TpmRegisterLayout;
use tracing_helpers::AnyhowValueExt;
use ttrpc::TtrpcWorker;
use uidevices_resources::SynthKeyboardHandle;
use uidevices_resources::SynthMouseHandle;
use uidevices_resources::SynthVideoHandle;
use video_core::SharedFramebufferHandle;
use vm_manifest_builder::BaseChipsetType;
use vm_manifest_builder::MachineArch;
use vm_manifest_builder::VmChipsetResult;
use vm_manifest_builder::VmManifestBuilder;
use vm_resource::kind::DiskHandleKind;
use vm_resource::kind::NetEndpointHandleKind;
use vm_resource::kind::VmbusDeviceHandleKind;
use vm_resource::IntoResource;
use vm_resource::Resource;
use vmbus_serial_resources::VmbusSerialDeviceHandle;
use vmbus_serial_resources::VmbusSerialPort;
use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
use vmgs_resources::VmgsFileHandle;
use vmotherboard::ChipsetDeviceHandle;
use vnc_worker_defs::VncParameters;
pub fn hvlite_main() {
#[cfg(unix)]
let orig_termios = io::stderr().is_terminal().then(term::get_termios);
let exit_code = match do_main() {
Ok(_) => 0,
Err(err) => {
eprintln!("fatal error: {:?}", err);
1
}
};
#[cfg(unix)]
if let Some(orig_termios) = orig_termios {
term::set_termios(orig_termios);
}
let _ = io::stdout().flush();
pal::process::terminate(exit_code);
}
#[derive(Default)]
struct VmResources {
console_in: Option<Box<dyn AsyncWrite + Send + Unpin>>,
framebuffer_access: Option<FramebufferAccess>,
shutdown_ic: Option<mesh::Sender<hyperv_ic_resources::shutdown::ShutdownRpc>>,
scsi_rpc: Option<mesh::Sender<ScsiControllerRequest>>,
ged_rpc: Option<mesh::Sender<get_resources::ged::GuestEmulationRequest>>,
#[cfg(windows)]
switch_ports: Vec<vmswitch::kernel::SwitchPort>,
}
struct ConsoleState<'a> {
device: &'a str,
input: Box<dyn AsyncWrite + Unpin + Send>,
}
fn vm_config_from_command_line(
spawner: impl Spawn,
opt: &Options,
) -> anyhow::Result<(Config, VmResources)> {
let serial_pool = DefaultPool::new();
let serial_driver = serial_pool.driver();
serial_driver.spawn("leak", pending::<()>()).detach();
thread::Builder::new()
.name("serial".to_string())
.spawn(|| serial_pool.run())
.unwrap();
let console_state: RefCell<Option<ConsoleState<'_>>> = RefCell::new(None);
let setup_serial = |name: &str, cli_cfg, device| -> anyhow::Result<_> {
Ok(match cli_cfg {
SerialConfigCli::Console => {
if let Some(console_state) = console_state.borrow().as_ref() {
bail!("console already set by {}", console_state.device);
}
let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
let (serial_read, serial_write) = AsyncReadExt::split(serial);
*console_state.borrow_mut() = Some(ConsoleState {
device,
input: Box::new(serial_write),
});
thread::Builder::new()
.name(name.to_owned())
.spawn(move || {
let _ = block_on(futures::io::copy(
serial_read,
&mut AllowStdIo::new(term::raw_stdout()),
));
})
.unwrap();
Some(config)
}
SerialConfigCli::Stderr => {
let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
thread::Builder::new()
.name(name.to_owned())
.spawn(move || {
let _ = block_on(futures::io::copy(
serial,
&mut AllowStdIo::new(term::raw_stderr()),
));
})
.unwrap();
Some(config)
}
SerialConfigCli::None => None,
SerialConfigCli::Pipe(path) => {
Some(serial_io::bind_serial(&path).context("failed to bind serial")?)
}
SerialConfigCli::NewConsole(app) => {
let path = console::random_console_path();
let config =
serial_io::bind_serial(&path).context("failed to bind console serial")?;
console::launch_console(app.as_deref(), &path)
.context("failed to launch console")?;
Some(config)
}
})
};
let setup_serial_virtio = |name, cli_cfg, device| -> anyhow::Result<_> {
Ok(match cli_cfg {
SerialConfigCli::Console => {
if console_state.borrow().is_some() {
bail!("console already set");
}
let mut io = SerialIo::new().context("creating serial IO")?;
io.spawn_copy_out(name, term::raw_stdout());
*console_state.borrow_mut() = Some(ConsoleState {
device,
input: Box::new(PolledPipe::new(&serial_driver, io.input.unwrap())?),
});
Some(io.config)
}
SerialConfigCli::Stderr => {
let mut io = SerialIo::new().context("creating serial IO")?;
io.spawn_copy_out(name, term::raw_stderr());
io.config.input = None;
Some(io.config)
}
SerialConfigCli::None => None,
SerialConfigCli::Pipe(path) => {
let mut io = SerialIo::new().context("creating serial IO")?;
io.spawn_copy_listener(serial_driver.clone(), name, &path)
.with_context(|| format!("listening on pipe {}", path.display()))?
.detach();
Some(io.config)
}
SerialConfigCli::NewConsole(app) => {
let path = console::random_console_path();
let mut io = SerialIo::new().context("creating serial IO")?;
io.spawn_copy_listener(serial_driver.clone(), name, &path)
.with_context(|| format!("listening on pipe {}", path.display()))?
.detach();
console::launch_console(app.as_deref(), &path)
.context("failed to launch console")?;
Some(io.config)
}
})
};
let virtio_console = opt.virtio_console || opt.virtio_console_pci;
let mut vmbus_devices = Vec::new();
let serial0_cfg = setup_serial(
"com1",
opt.com1.clone().unwrap_or({
if !virtio_console {
SerialConfigCli::Console
} else {
SerialConfigCli::None
}
}),
if cfg!(guest_arch = "x86_64") {
"ttyS0"
} else {
"ttyAMA0"
},
)?;
let serial1_cfg = setup_serial(
"com2",
opt.com2.clone().unwrap_or(SerialConfigCli::None),
if cfg!(guest_arch = "x86_64") {
"ttyS1"
} else {
"ttyAMA1"
},
)?;
let serial2_cfg = setup_serial(
"com3",
opt.com3.clone().unwrap_or(SerialConfigCli::None),
if cfg!(guest_arch = "x86_64") {
"ttyS2"
} else {
"ttyAMA2"
},
)?;
let serial3_cfg = setup_serial(
"com4",
opt.com4.clone().unwrap_or(SerialConfigCli::None),
if cfg!(guest_arch = "x86_64") {
"ttyS3"
} else {
"ttyAMA3"
},
)?;
let virtio_serial_cfg = setup_serial_virtio(
"virtio_serial",
opt.virtio_serial.clone().unwrap_or({
if virtio_console {
SerialConfigCli::Console
} else {
SerialConfigCli::None
}
}),
if opt.virtio_console_pci {
"hvc1"
} else {
"hvc0"
},
)?;
let with_vmbus_com1_serial = if let Some(vmbus_com1_cfg) = setup_serial(
"vmbus_com1",
opt.vmbus_com1_serial
.clone()
.unwrap_or(SerialConfigCli::None),
"vmbus_com1",
)? {
vmbus_devices.push((
DeviceVtl::Vtl2,
VmbusSerialDeviceHandle {
port: VmbusSerialPort::Com1,
backend: vmbus_com1_cfg,
}
.into_resource(),
));
true
} else {
false
};
let with_vmbus_com2_serial = if let Some(vmbus_com2_cfg) = setup_serial(
"vmbus_com2",
opt.vmbus_com2_serial
.clone()
.unwrap_or(SerialConfigCli::None),
"vmbus_com2",
)? {
vmbus_devices.push((
DeviceVtl::Vtl2,
VmbusSerialDeviceHandle {
port: VmbusSerialPort::Com2,
backend: vmbus_com2_cfg,
}
.into_resource(),
));
true
} else {
false
};
let mut resources = VmResources::default();
let mut console_str = "";
if let Some(ConsoleState { device, input }) = console_state.into_inner() {
resources.console_in = Some(input);
console_str = device;
}
if opt.shared_memory {
tracing::warn!("--shared-memory/-M flag has no effect and will be removed");
}
const MAX_PROCESSOR_COUNT: u32 = 1024;
if opt.processors == 0 || opt.processors > MAX_PROCESSOR_COUNT {
bail!("invalid proc count: {}", opt.processors);
}
if opt.scsi_sub_channels > (MAX_PROCESSOR_COUNT - 1) as u16 {
bail!(
"invalid SCSI sub-channel count: requested {}, max {}",
opt.scsi_sub_channels,
MAX_PROCESSOR_COUNT - 1
);
}
let mut storage = storage_builder::StorageBuilder::new();
for &cli_args::DiskCli {
vtl,
ref kind,
read_only,
is_dvd,
underhill,
} in &opt.disk
{
storage.add(
vtl,
underhill,
storage_builder::DiskLocation::Scsi(None),
kind,
is_dvd,
read_only,
)?;
}
for &cli_args::IdeDiskCli {
ref kind,
read_only,
channel,
device,
is_dvd,
} in &opt.ide
{
storage.add(
DeviceVtl::Vtl0,
None,
storage_builder::DiskLocation::Ide(channel, device),
kind,
is_dvd,
read_only,
)?;
}
for &cli_args::DiskCli {
vtl,
ref kind,
read_only,
is_dvd,
underhill,
} in &opt.nvme
{
storage.add(
vtl,
underhill,
storage_builder::DiskLocation::Nvme(None),
kind,
is_dvd,
read_only,
)?;
}
let floppy_disks: Vec<_> = opt
.floppy
.iter()
.map(|disk| -> anyhow::Result<_> {
let &cli_args::FloppyDiskCli {
ref kind,
read_only,
} = disk;
Ok(FloppyDiskConfig {
disk_type: disk_open(kind, read_only)?,
read_only,
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut mana_nics = [(); 3].map(|()| None);
let mut underhill_nics = Vec::new();
let mut vpci_devices = Vec::new();
let mut nic_index = 0;
for cli_cfg in &opt.net {
let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
if cli_cfg.underhill {
if !opt.no_alias_map {
anyhow::bail!("must specify --no-alias-map to offer NICs to VTL2");
}
let mana = mana_nics[2].get_or_insert_with(|| {
let vpci_instance_id = Guid::new_random();
underhill_nics.push(vtl2_settings_proto::NicDeviceLegacy {
instance_id: vpci_instance_id.to_string(),
subordinate_instance_id: None,
max_sub_channels: None,
});
(vpci_instance_id, GdmaDeviceHandle { vports: Vec::new() })
});
mana.1.vports.push(VportDefinition {
mac_address: vport.mac_address,
endpoint: vport.endpoint,
});
} else {
vmbus_devices.push(vport.into_netvsp_handle());
}
}
if opt.nic {
let nic_config = parse_endpoint(
&NicConfigCli {
vtl: DeviceVtl::Vtl0,
endpoint: EndpointConfigCli::Consomme { cidr: None },
max_queues: None,
underhill: false,
},
&mut nic_index,
&mut resources,
)?;
vmbus_devices.push(nic_config.into_netvsp_handle());
}
if opt.mcr {
tracing::info!("Instantiating MCR controller");
const MCR_INSTANCE_ID: Guid = Guid::from_static_str("07effd8f-7501-426c-a947-d8345f39113d");
vpci_devices.push(VpciDeviceConfig {
vtl: DeviceVtl::Vtl0,
instance_id: MCR_INSTANCE_ID,
resource: mcr_resources::McrControllerHandle {
instance_id: MCR_INSTANCE_ID,
}
.into_resource(),
});
}
#[cfg(windows)]
let mut kernel_vmnics = Vec::new();
#[cfg(windows)]
for (index, switch_id) in opt.kernel_vmnic.iter().enumerate() {
let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
getrandom::getrandom(&mut mac_address[3..]).expect("rng failure");
const BASE_INSTANCE_ID: Guid =
Guid::from_static_str("00000000-435d-11ee-9f59-00155d5016fc");
let instance_id = Guid {
data1: index as u32,
..BASE_INSTANCE_ID
};
let switch_id = if switch_id == "default" {
DEFAULT_SWITCH
} else {
switch_id
};
let (port_id, port) = new_switch_port(switch_id)?;
resources.switch_ports.push(port);
kernel_vmnics.push(hvlite_defs::config::KernelVmNicConfig {
instance_id,
mac_address: mac_address.into(),
switch_port_id: port_id,
});
}
for vport in &opt.mana {
let vport = parse_endpoint(vport, &mut nic_index, &mut resources)?;
mana_nics[vport.vtl as usize]
.get_or_insert_with(|| (Guid::new_random(), GdmaDeviceHandle { vports: Vec::new() }))
.1
.vports
.push(VportDefinition {
mac_address: vport.mac_address,
endpoint: vport.endpoint,
});
}
vpci_devices.extend(mana_nics.into_iter().enumerate().filter_map(|(vtl, nic)| {
nic.map(|(instance_id, handle)| VpciDeviceConfig {
vtl: match vtl {
0 => DeviceVtl::Vtl0,
1 => DeviceVtl::Vtl1,
2 => DeviceVtl::Vtl2,
_ => unreachable!(),
},
instance_id,
resource: handle.into_resource(),
})
}));
#[cfg(windows)]
let vpci_resources: Vec<_> = opt
.device
.iter()
.map(|path| -> anyhow::Result<_> {
Ok(virt_whp::device::DeviceHandle(
whp::VpciResource::new(
None,
Default::default(),
&whp::VpciResourceDescriptor::Sriov(path, 0, 0),
)
.with_context(|| format!("opening PCI device {}", path))?,
))
})
.collect::<Result<_, _>>()?;
#[cfg(windows)]
let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
} else {
None
};
let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc || opt.pcat {
let vram = alloc_shared_memory(FRAMEBUFFER_SIZE)?;
let (fb, fba) =
framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
resources.framebuffer_access = Some(fba);
Some(fb)
} else {
None
};
let is_arm = cfg!(guest_arch = "aarch64");
let is_x86 = cfg!(guest_arch = "x86_64");
let load_mode;
let with_hv;
let any_serial_configured = serial0_cfg.is_some()
|| serial1_cfg.is_some()
|| serial2_cfg.is_some()
|| serial3_cfg.is_some();
let has_com3 = serial2_cfg.is_some();
let mut chipset = VmManifestBuilder::new(
if opt.igvm.is_some() {
BaseChipsetType::HclHost
} else if opt.pcat {
BaseChipsetType::HypervGen1
} else if opt.uefi {
BaseChipsetType::HypervGen2Uefi
} else if opt.hv {
BaseChipsetType::HyperVGen2LinuxDirect
} else {
BaseChipsetType::UnenlightenedLinuxDirect
},
if is_x86 {
MachineArch::X86_64
} else {
MachineArch::Aarch64
},
);
if framebuffer.is_some() {
chipset = chipset.with_framebuffer();
}
if opt.guest_watchdog {
chipset = chipset.with_guest_watchdog();
}
if any_serial_configured {
chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
}
if opt.battery {
let (tx, rx) = mesh::channel();
tx.send(HostBatteryUpdate::default_present());
chipset = chipset.with_battery(rx);
}
let VmChipsetResult {
chipset,
mut chipset_devices,
} = chipset
.build()
.context("failed to build chipset configuration")?;
if let Some(path) = &opt.igvm {
let file = fs_err::File::open(path)
.context("failed to open igvm file")?
.into();
let cmdline = opt.cmdline.join(" ");
with_hv = true;
load_mode = LoadMode::Igvm {
file,
cmdline,
vtl2_base_address: opt.igvm_vtl2_relocation_type,
com_serial: has_com3.then(|| SerialInformation {
io_port: ComPort::Com3.io_port(),
irq: ComPort::Com3.irq().into(),
}),
};
} else if opt.pcat {
if !is_x86 {
anyhow::bail!("pcat not supported on this architecture");
}
with_hv = true;
let firmware = hvlite_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
load_mode = LoadMode::Pcat {
firmware,
boot_order: opt
.pcat_boot_order
.map(|x| x.0)
.unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
};
} else if opt.uefi {
use hvlite_defs::config::UefiConsoleMode;
with_hv = true;
let firmware = fs_err::File::open(
(opt.uefi_firmware.0)
.as_ref()
.context("must provide uefi firmware when booting with uefi")?,
)
.context("failed to open uefi firmware")?;
load_mode = LoadMode::Uefi {
firmware: firmware.into(),
enable_debugging: opt.uefi_debug,
enable_memory_protections: opt.uefi_enable_memory_protections,
disable_frontpage: opt.disable_frontpage,
enable_tpm: opt.tpm,
enable_battery: opt.battery,
enable_serial: any_serial_configured,
enable_vpci_boot: false,
uefi_console_mode: opt.uefi_console_mode.map(|m| match m {
UefiConsoleModeCli::Default => UefiConsoleMode::Default,
UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
UefiConsoleModeCli::None => UefiConsoleMode::None,
}),
};
} else {
let mut cmdline = "panic=-1 debug".to_string();
with_hv = opt.hv;
if with_hv {
cmdline += " pci=off";
}
if !console_str.is_empty() {
let _ = write!(&mut cmdline, " console={}", console_str);
}
if opt.gfx {
cmdline += " console=tty";
}
for extra in &opt.cmdline {
let _ = write!(&mut cmdline, " {}", extra);
}
let kernel = fs_err::File::open(
(opt.kernel.0)
.as_ref()
.context("must provide kernel when booting with linux direct")?,
)
.context("failed to open kernel")?;
let initrd = (opt.initrd.0)
.as_ref()
.map(fs_err::File::open)
.transpose()
.context("failed to open initrd")?;
let custom_dsdt = match &opt.custom_dsdt {
Some(path) => {
let mut v = Vec::new();
fs_err::File::open(path)
.context("failed to open custom dsdt")?
.read_to_end(&mut v)
.context("failed to read custom dsdt")?;
Some(v)
}
None => None,
};
load_mode = LoadMode::Linux {
kernel: kernel.into(),
initrd: initrd.map(Into::into),
cmdline,
custom_dsdt,
enable_serial: any_serial_configured,
};
}
let openhcl_vtl = if opt.vtl2 {
DeviceVtl::Vtl2
} else {
DeviceVtl::Vtl0
};
if (opt.vtl2 || opt.get) && with_hv {
let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
fixed: Some(Default::default()),
dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
storage_controllers: storage.build_underhill(),
nic_devices: underhill_nics,
}),
namespace_settings: Vec::default(),
};
let (send, guest_request_recv) = mesh::channel();
resources.ged_rpc = Some(send);
vmbus_devices.extend([
(
openhcl_vtl,
get_resources::gel::GuestEmulationLogHandle.into_resource(),
),
(
openhcl_vtl,
get_resources::ged::GuestEmulationDeviceHandle {
firmware: if opt.pcat {
get_resources::ged::GuestFirmwareConfig::Pcat {
boot_order: opt
.pcat_boot_order
.map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
.map(|x| match x {
hvlite_defs::config::PcatBootDevice::Floppy => {
get_resources::ged::PcatBootDevice::Floppy
}
hvlite_defs::config::PcatBootDevice::HardDrive => {
get_resources::ged::PcatBootDevice::HardDrive
}
hvlite_defs::config::PcatBootDevice::Optical => {
get_resources::ged::PcatBootDevice::Optical
}
hvlite_defs::config::PcatBootDevice::Network => {
get_resources::ged::PcatBootDevice::Network
}
}),
}
} else {
use get_resources::ged::UefiConsoleMode;
get_resources::ged::GuestFirmwareConfig::Uefi {
enable_vpci_boot: storage.has_vtl0_nvme(),
firmware_debug: opt.uefi_debug,
disable_frontpage: opt.disable_frontpage,
console_mode: match opt.uefi_console_mode.unwrap_or(UefiConsoleModeCli::Default) {
UefiConsoleModeCli::Default => UefiConsoleMode::Default,
UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
UefiConsoleModeCli::None => UefiConsoleMode::None,
},
}
},
com1: with_vmbus_com1_serial,
com2: with_vmbus_com2_serial,
vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
vmbus_redirection: opt.vmbus_redirect,
framebuffer: opt
.vtl2_gfx
.then(|| SharedFramebufferHandle.into_resource()),
guest_request_recv,
enable_tpm: opt.tpm,
firmware_event_send: None,
secure_boot_enabled: opt.secure_boot,
secure_boot_template: match opt.secure_boot_template {
Some(SecureBootTemplateCli::Windows) => {
get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
},
Some(SecureBootTemplateCli::UefiCa) => {
get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthoritiy
}
None => {
get_resources::ged::GuestSecureBootTemplateType::None
},
},
enable_battery: opt.battery,
}
.into_resource(),
),
]);
}
if opt.tpm && !opt.vtl2 {
let register_layout = if cfg!(guest_arch = "x86_64") {
TpmRegisterLayout::IoPort
} else {
TpmRegisterLayout::Mmio
};
let (ppi_store, nvram_store) = if opt.vmgs_file.is_some() {
(
VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
)
} else {
(
EphemeralNonVolatileStoreHandle.into_resource(),
EphemeralNonVolatileStoreHandle.into_resource(),
)
};
chipset_devices.push(ChipsetDeviceHandle {
name: "tpm".to_string(),
resource: TpmDeviceHandle {
ppi_store,
nvram_store,
refresh_tpm_seeds: false,
get_attestation_report: None,
request_ak_cert: None,
register_layout,
guest_secret_key: None,
}
.into_resource(),
});
}
let custom_uefi_vars = {
use firmware_uefi_custom_vars::CustomVars;
let base_vars = match opt.secure_boot_template {
Some(template) => {
if is_x86 {
match template {
SecureBootTemplateCli::Windows => {
hyperv_secure_boot_templates::x64::microsoft_windows()
}
SecureBootTemplateCli::UefiCa => {
hyperv_secure_boot_templates::x64::microsoft_uefi_ca()
}
}
} else if is_arm {
match template {
SecureBootTemplateCli::Windows => {
hyperv_secure_boot_templates::aarch64::microsoft_windows()
}
SecureBootTemplateCli::UefiCa => {
hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca()
}
}
} else {
anyhow::bail!("no secure boot template for current guest_arch")
}
}
None => CustomVars::default(),
};
let custom_uefi_json_data = match &opt.custom_uefi_json {
Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?),
None => None,
};
match custom_uefi_json_data {
Some(data) => {
let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?;
base_vars.apply_delta(delta)?
}
None => base_vars,
}
};
let vga_firmware = if opt.pcat {
Some(hvlite_pcat_locator::find_svga_bios(
opt.vga_firmware.as_deref(),
)?)
} else {
None
};
if opt.gfx {
vmbus_devices.extend([
(
DeviceVtl::Vtl0,
SynthVideoHandle {
framebuffer: SharedFramebufferHandle.into_resource(),
}
.into_resource(),
),
(
DeviceVtl::Vtl0,
SynthKeyboardHandle {
source: MultiplexedInputHandle {
elevation: 1,
}
.into_resource(),
}
.into_resource(),
),
(
DeviceVtl::Vtl0,
SynthMouseHandle {
source: MultiplexedInputHandle {
elevation: 1,
}
.into_resource(),
}
.into_resource(),
),
]);
}
let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
if let Some(path) = path {
cleanup_socket(path.as_ref());
let listener = unix_socket::UnixListener::bind(path)
.with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
Ok(Some(listener))
} else {
Ok(None)
}
};
let vtl0_vsock_listener = vsock_listener(opt.vsock_path.as_deref())?;
let vtl2_vsock_listener = vsock_listener(opt.vtl2_vsock_path.as_deref())?;
let mmio_gaps = if opt.vtl2
&& !matches!(
opt.igvm_vtl2_relocation_type,
Vtl2BaseAddressType::Vtl2Allocate { .. },
) {
DEFAULT_MMIO_GAPS_WITH_VTL2.into()
} else {
DEFAULT_MMIO_GAPS.into()
};
if let Some(path) = &opt.underhill_dump_path {
let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
task.detach();
vmbus_devices.push((openhcl_vtl, resource));
}
#[cfg(guest_arch = "aarch64")]
let topology_arch = hvlite_defs::config::Aarch64TopologyConfig {
gic_config: None,
};
#[cfg(guest_arch = "x86_64")]
let topology_arch = hvlite_defs::config::X86TopologyConfig {
apic_id_offset: opt.apic_id_offset,
x2apic: opt.x2apic,
};
let with_isolation = if let Some(isolation) = &opt.isolation {
if !opt.vtl2 {
anyhow::bail!("isolation is only currently supported with vtl2");
}
if !opt.no_alias_map {
anyhow::bail!("alias map not supported with isolation");
}
match isolation {
cli_args::IsolationCli::Vbs => Some(hvlite_defs::config::IsolationType::Vbs),
}
} else {
None
};
if with_hv {
let (send, recv) = mesh::channel();
resources.shutdown_ic = Some(send);
vmbus_devices.push((
DeviceVtl::Vtl0,
hyperv_ic_resources::shutdown::ShutdownIcHandle { recv }.into_resource(),
));
}
if let Some(hive_path) = &opt.imc {
let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
vmbus_devices.push((
DeviceVtl::Vtl0,
vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
));
}
let mut virtio_devices = Vec::new();
for cli_cfg in &opt.virtio_net {
if cli_cfg.underhill {
anyhow::bail!("use --net uh:[...] to add underhill NICs")
}
let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
virtio_devices.push((
VirtioBus::Auto,
virtio_resources::net::VirtioNetHandle {
max_queues: vport.max_queues,
mac_address: vport.mac_address,
endpoint: vport.endpoint,
}
.into_resource(),
));
}
for (tag, root_path) in &opt.virtio_fs {
virtio_devices.push((
opt.virtio_fs_bus,
virtio_resources::fs::VirtioFsHandle {
tag: tag.clone(),
fs: virtio_resources::fs::VirtioFsBackend::HostFs {
root_path: root_path.clone(),
},
}
.into_resource(),
));
}
for (tag, root_path) in &opt.virtio_fs_shmem {
virtio_devices.push((
opt.virtio_fs_bus,
virtio_resources::fs::VirtioFsHandle {
tag: tag.clone(),
fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
root_path: root_path.clone(),
},
}
.into_resource(),
));
}
for (tag, root_path) in &opt.virtio_9p {
virtio_devices.push((
VirtioBus::Auto,
virtio_resources::p9::VirtioPlan9Handle {
tag: tag.clone(),
root_path: root_path.clone(),
debug: opt.virtio_9p_debug,
}
.into_resource(),
));
}
if let Some(path) = &opt.virtio_pmem {
virtio_devices.push((
VirtioBus::Auto,
virtio_resources::pmem::VirtioPmemHandle { path: path.clone() }.into_resource(),
));
}
let mut cfg = Config {
chipset,
load_mode,
floppy_disks,
vpci_devices,
ide_disks: Vec::new(),
memory: MemoryConfig {
mem_size: opt.memory,
mmio_gaps,
prefetch_memory: opt.prefetch,
},
processor_topology: ProcessorTopologyConfig {
proc_count: opt.processors,
vps_per_socket: opt.vps_per_socket,
enable_smt: match opt.smt {
cli_args::SmtConfigCli::Auto => None,
cli_args::SmtConfigCli::Force => Some(true),
cli_args::SmtConfigCli::Off => Some(false),
},
arch: topology_arch,
},
hypervisor: HypervisorConfig {
with_hv,
with_vtl2: opt.vtl2.then_some(Vtl2Config {
vtl0_alias_map: !opt.no_alias_map,
late_map_vtl0_memory: match opt.late_map_vtl0_policy {
cli_args::Vtl0LateMapPolicyCli::Off => None,
cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
cli_args::Vtl0LateMapPolicyCli::Exception => {
Some(LateMapVtl0MemoryPolicy::InjectException)
}
},
vtl2_emulates_apic: opt.vtl2_emulates_apic,
}),
with_isolation,
user_mode_hv_enlightenments: opt.no_enlightenments,
user_mode_apic: opt.user_mode_apic,
},
#[cfg(windows)]
kernel_vmnics,
input: mesh::MpscReceiver::new(),
framebuffer,
vga_firmware,
vtl2_gfx: opt.vtl2_gfx,
virtio_console_pci: opt.virtio_console_pci,
virtio_serial: virtio_serial_cfg,
virtio_devices,
vmbus: with_hv.then_some(VmbusConfig {
vsock_listener: vtl0_vsock_listener,
vsock_path: opt.vsock_path.clone(),
vtl2_redirect: opt.vmbus_redirect,
vmbus_max_version: opt.vmbus_max_version,
#[cfg(windows)]
vmbusproxy_handle,
}),
vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
vsock_listener: vtl2_vsock_listener,
vsock_path: opt.vtl2_vsock_path.clone(),
..Default::default()
}),
vmbus_devices,
chipset_devices,
#[cfg(windows)]
vpci_resources,
vmgs_file: opt
.vmgs_file
.as_ref()
.map(|p| p.to_string_lossy().into_owned()),
secure_boot_enabled: opt.secure_boot,
custom_uefi_vars,
firmware_event_send: None,
debugger_rpc: None,
generation_id_recv: None,
};
storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
Ok((cfg, resources))
}
fn cleanup_socket(path: &Path) {
#[cfg(windows)]
let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
#[cfg(not(windows))]
let is_socket = path.metadata().map_or(false, |meta| {
std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type())
});
if is_socket {
let _ = std::fs::remove_file(path);
}
}
#[cfg(windows)]
const DEFAULT_SWITCH: &str = "C08CB7B8-9B3C-408E-8E30-5E16A3AEB444";
#[cfg(windows)]
fn new_switch_port(
switch_id: &str,
) -> anyhow::Result<(
hvlite_defs::config::SwitchPortId,
vmswitch::kernel::SwitchPort,
)> {
let id = vmswitch::kernel::SwitchPortId {
switch: switch_id.parse().context("invalid switch id")?,
port: Guid::new_random(),
};
let _ = vmswitch::hcn::Network::open(&id.switch)
.with_context(|| format!("could not find switch {}", id.switch))?;
let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
let id = hvlite_defs::config::SwitchPortId {
switch: id.switch,
port: id.port,
};
Ok((id, port))
}
fn parse_endpoint(
cli_cfg: &NicConfigCli,
index: &mut usize,
resources: &mut VmResources,
) -> anyhow::Result<NicConfig> {
let _ = resources;
let endpoint = match &cli_cfg.endpoint {
EndpointConfigCli::Consomme { cidr } => {
net_backend_resources::consomme::ConsommeHandle { cidr: cidr.clone() }.into_resource()
}
EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
EndpointConfigCli::Dio { id } => {
#[cfg(windows)]
{
let (port_id, port) = new_switch_port(id.as_deref().unwrap_or(DEFAULT_SWITCH))?;
resources.switch_ports.push(port);
net_backend_resources::dio::WindowsDirectIoHandle {
switch_port_id: net_backend_resources::dio::SwitchPortId {
switch: port_id.switch,
port: port_id.port,
},
}
.into_resource()
}
#[cfg(not(windows))]
{
let _ = id;
bail!("cannot use dio on non-windows platforms")
}
}
EndpointConfigCli::Tap { name } => {
net_backend_resources::tap::TapHandle { name: name.clone() }.into_resource()
}
};
let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
getrandom::getrandom(&mut mac_address[3..]).expect("rng failure");
const BASE_INSTANCE_ID: Guid = Guid::from_static_str("00000000-da43-11ed-936a-00155d6db52f");
let instance_id = Guid {
data1: *index as u32,
..BASE_INSTANCE_ID
};
*index += 1;
Ok(NicConfig {
vtl: cli_cfg.vtl,
instance_id,
endpoint,
mac_address: mac_address.into(),
max_queues: cli_cfg.max_queues,
})
}
#[derive(Debug)]
struct NicConfig {
vtl: DeviceVtl,
instance_id: Guid,
mac_address: MacAddress,
endpoint: Resource<NetEndpointHandleKind>,
max_queues: Option<u16>,
}
impl NicConfig {
fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
(
self.vtl,
netvsp_resources::NetvspHandle {
instance_id: self.instance_id,
mac_address: self.mac_address,
endpoint: self.endpoint,
max_queues: self.max_queues,
}
.into_resource(),
)
}
}
fn disk_open(disk_cli: &DiskCliKind, read_only: bool) -> anyhow::Result<Resource<DiskHandleKind>> {
let disk_type = match disk_cli {
&DiskCliKind::Memory(len) => Resource::new(disk_backend_resources::RamDiskHandle { len }),
DiskCliKind::File(path) => open_disk_type(path, read_only)
.with_context(|| format!("failed to open {}", path.display()))?,
DiskCliKind::Blob { kind, url } => Resource::new(disk_backend_resources::BlobDiskHandle {
url: url.to_owned(),
format: match kind {
cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
cli_args::BlobKind::Vhd1 => disk_backend_resources::BlobDiskFormat::FixedVhd1,
},
}),
DiskCliKind::MemoryDiff(inner) => {
Resource::new(disk_backend_resources::RamDiffDiskHandle {
lower: disk_open(inner, true)?,
})
}
DiskCliKind::PersistentReservationsWrapper(inner) => Resource::new(
disk_backend_resources::DiskWithReservationsHandle(disk_open(inner, read_only)?),
),
};
Ok(disk_type)
}
fn do_main() -> anyhow::Result<()> {
#[cfg(windows)]
pal::windows::disable_hard_error_dialog();
tracing_init::enable_tracing()?;
meshworker::run_vmm_mesh_host()?;
let opt = Options::parse();
if let Some(path) = &opt.write_saved_state_proto {
mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
.write_to_path(path)
.context("failed to write protobuf descriptors")?;
return Ok(());
}
if let Some(path) = opt.relay_console_path {
return console::relay_console(&path);
}
if let Some(path) = opt.ttrpc.as_ref().or(opt.grpc.as_ref()) {
block_on(async {
let _ = std::fs::remove_file(path);
let listener =
unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
let transport = if opt.ttrpc.is_some() {
ttrpc::RpcTransport::Ttrpc
} else {
ttrpc::RpcTransport::Grpc
};
let mut handle = launch_local_worker::<TtrpcWorker>(ttrpc::Parameters {
listener,
transport,
})
.await?;
tracing::info!(%transport, path = %path.display(), "listening");
pal::close_stdout().context("failed to close stdout")?;
handle.join().await?;
Ok(())
})
} else {
DefaultPool::run_with(|driver| async move {
let mesh = VmmMesh::new(&driver, opt.single_process)?;
let result = run_control(&driver, &mesh, opt).await;
mesh.shutdown().await;
result
})
}
}
fn maybe_with_radix_u64(s: &str) -> Result<u64, String> {
let (radix, prefix_len) = if s.starts_with("0x") || s.starts_with("0X") {
(16, 2)
} else if s.starts_with("0o") || s.starts_with("0O") {
(8, 2)
} else if s.starts_with("0b") || s.starts_with("0B") {
(2, 2)
} else {
(10, 0)
};
u64::from_str_radix(&s[prefix_len..], radix).map_err(|e| format!("{e}"))
}
#[derive(Parser)]
#[clap(
disable_help_flag = true,
disable_version_flag = true,
no_binary_name = true,
help_template("{subcommands}")
)]
enum InteractiveCommand {
#[clap(visible_alias = "R")]
Restart,
#[clap(visible_alias = "n")]
Nmi,
#[clap(visible_alias = "p")]
Pause,
#[clap(visible_alias = "r")]
Resume,
#[clap(visible_alias = "psr")]
PulseSaveRestore,
#[clap(visible_alias = "spsr")]
SchedulePulseSaveRestore {
interval: Option<u64>,
},
#[clap(visible_alias = "d")]
AddDisk {
#[clap(long = "ro")]
read_only: bool,
#[clap(long = "dvd")]
is_dvd: bool,
#[clap(long, default_value_t)]
target: u8,
#[clap(long, default_value_t)]
path: u8,
#[clap(long, default_value_t)]
lun: u8,
#[clap(long)]
ram: Option<u64>,
file_path: Option<PathBuf>,
},
#[clap(visible_alias = "D")]
RmDisk {
#[clap(long)]
target: u8,
#[clap(long)]
path: u8,
#[clap(long)]
lun: u8,
},
#[clap(visible_alias = "x")]
Inspect {
#[clap(short, long)]
recursive: bool,
#[clap(short, long, requires("recursive"))]
limit: Option<usize>,
#[clap(short = 'v', long)]
paravisor: bool,
element: Option<String>,
#[clap(short, long, conflicts_with("recursive"))]
update: Option<String>,
},
#[clap(visible_alias = "V")]
RestartVnc,
#[clap(visible_alias = "v")]
Hvsock {
#[clap(short, long)]
term: Option<String>,
port: u32,
},
#[clap(visible_alias = "q")]
Quit,
#[clap(visible_alias = "i")]
Input { data: Vec<String> },
#[clap(visible_alias = "I")]
InputMode,
Reset,
Shutdown {
#[clap(long, short = 'r')]
reboot: bool,
#[clap(long, short = 'h', conflicts_with = "reboot")]
hibernate: bool,
#[clap(long, short = 'f')]
force: bool,
},
#[clap(visible_alias = "ch")]
ClearHalt,
ServiceVtl2 {
#[clap(long, short = 'u')]
user_mode_only: bool,
#[clap(long, conflicts_with("user_mode_only"))]
igvm: Option<PathBuf>,
},
ReadMemory {
#[clap(value_parser=maybe_with_radix_u64)]
gpa: u64,
#[clap(value_parser=maybe_with_radix_u64)]
size: u64,
#[clap(long, short = 'f')]
file: Option<PathBuf>,
},
WriteMemory {
#[clap(value_parser=maybe_with_radix_u64)]
gpa: u64,
hex: Option<String>,
#[clap(long, short = 'f')]
file: Option<PathBuf>,
},
Panic,
}
struct CommandParser {
app: clap::Command,
}
impl CommandParser {
fn new() -> Self {
let mut app = InteractiveCommand::command();
for sc in app.get_subcommands_mut() {
*sc = sc
.clone()
.help_template("{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}");
}
Self { app }
}
fn parse(&mut self, line: &str) -> clap::error::Result<InteractiveCommand> {
let args = shell_words::split(line)
.map_err(|err| self.app.error(clap::error::ErrorKind::ValueValidation, err))?;
let matches = self.app.try_get_matches_from_mut(args)?;
InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(&mut self.app))
}
}
fn new_hvsock_service_id(port: u32) -> Guid {
Guid {
data1: port,
.."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
}
}
async fn run_control(driver: &DefaultDriver, mesh: &VmmMesh, opt: Options) -> anyhow::Result<()> {
let (mut vm_config, mut resources) = vm_config_from_command_line(driver, &opt)?;
let mut vnc_worker = None;
if opt.gfx || opt.vnc {
let listener = TcpListener::bind(format!("127.0.0.1:{}", opt.vnc_port))
.with_context(|| format!("binding to VNC port {}", opt.vnc_port))?;
let input_send = vm_config.input.sender();
let framebuffer = resources.framebuffer_access.expect("synth video enabled");
let vnc_host = mesh
.make_host("vnc", None)
.await
.context("spawning vnc process failed")?;
vnc_worker = Some(
vnc_host
.launch_worker(
vnc_worker_defs::VNC_WORKER_TCP,
VncParameters {
listener,
framebuffer,
input_send,
},
)
.await?,
)
}
let gdb_worker = if let Some(port) = opt.gdb {
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
.with_context(|| format!("binding to gdb port {}", port))?;
let (req_tx, req_rx) = mesh::channel();
vm_config.debugger_rpc = Some(req_rx);
let gdb_host = mesh
.make_host("gdb", None)
.await
.context("spawning gdbstub process failed")?;
Some(
gdb_host
.launch_worker(
debug_worker_defs::DEBUGGER_WORKER,
debug_worker_defs::DebuggerParameters {
listener,
req_chan: req_tx,
vp_count: vm_config.processor_topology.proc_count,
},
)
.await
.context("failed to launch gdbstub worker")?,
)
} else {
None
};
let (vm_rpc, rpc_recv) = mesh::channel();
let vm_rpc = Arc::new(vm_rpc);
let (notify_send, notify_recv) = mesh::channel();
let mut vm_worker = {
let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
let params = VmWorkerParameters {
hypervisor: opt.hypervisor,
cfg: vm_config,
saved_state: None,
rpc: rpc_recv,
notify: notify_send,
};
vm_host
.launch_worker(VM_WORKER, params)
.await
.context("failed to launch vm worker")?
};
if !opt.paused {
vm_rpc.call(VmRpc::Resume, ()).await?;
}
let mut diag_inspector = DiagInspector::new(
driver.clone(),
vm_rpc.clone(),
if opt.vtl2 {
DeviceVtl::Vtl2
} else {
DeviceVtl::Vtl0
},
);
let (console_command_send, console_command_recv) = mesh::channel();
let (inspect_completion_engine_send, inspect_completion_engine_recv) = mesh::channel();
let mut console_in = resources.console_in;
thread::Builder::new()
.name("stdio-thread".to_string())
.spawn(move || {
#[cfg(unix)]
if io::stderr().is_terminal() {
term::revert_terminal_on_panic()
}
let mut rl = rustyline::Editor::<
interactive_console::HvLiteRustylineEditor,
rustyline::history::FileHistory,
>::with_config(
rustyline::Config::builder()
.completion_type(rustyline::CompletionType::List)
.build(),
)
.unwrap();
rl.set_helper(Some(interactive_console::HvLiteRustylineEditor {
hvlite_inspect_req: Arc::new(inspect_completion_engine_send),
}));
let history_file = {
const HISTORY_FILE: &str = ".hvlite_history";
let history_folder = None
.or_else(dirs::state_dir)
.or_else(dirs::data_local_dir)
.map(|path| path.join("hvlite"));
if let Some(history_folder) = history_folder {
if let Err(err) = std::fs::create_dir_all(&history_folder) {
tracing::warn!(
error = &err as &dyn std::error::Error,
"could not create directory: {}",
history_folder.display()
)
}
Some(history_folder.join(HISTORY_FILE))
} else {
None
}
};
if let Some(history_file) = &history_file {
tracing::info!("restoring history from {}", history_file.display());
if rl.load_history(history_file).is_err() {
tracing::info!("could not find existing {}", history_file.display());
}
}
rl.bind_sequence(
rustyline::KeyEvent::new('\x08', rustyline::Modifiers::CTRL),
rustyline::Cmd::Kill(rustyline::Movement::BackwardWord(1, rustyline::Word::Emacs)),
);
let mut parser = CommandParser::new();
let mut stdin = io::stdin();
loop {
term::set_raw_console(true);
if let Some(input) = console_in.as_mut() {
let mut buf = [0; 32];
loop {
let n = stdin.read(&mut buf).unwrap();
let mut b = &buf[..n];
let stop = if let Some(ctrlq) = b.iter().position(|x| *x == 0x11) {
b = &b[..ctrlq];
true
} else {
false
};
block_on(input.as_mut().write_all(b)).expect("BUGBUG");
if stop {
break;
}
}
}
term::set_raw_console(false);
loop {
let line = rl.readline("openvmm> ");
if line.is_err() {
break;
}
let line = line.unwrap();
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Err(err) = rl.add_history_entry(&line) {
tracing::warn!(
err = &err as &dyn std::error::Error,
"error adding to .hvlite_history"
)
}
match parser.parse(trimmed) {
Ok(cmd) => match cmd {
InteractiveCommand::Input { data } => {
let mut data = data.join(" ");
data.push('\n');
if let Some(input) = console_in.as_mut() {
block_on(input.write_all(data.as_bytes())).expect("BUGBUG");
}
}
InteractiveCommand::InputMode => break,
cmd => {
let (processing_done_send, processing_done_recv) =
mesh::oneshot::<()>();
console_command_send.send((cmd, processing_done_send));
let _ = block_on(processing_done_recv);
}
},
Err(err) => {
err.print().unwrap();
}
}
if let Some(history_file) = &history_file {
rl.append_history(history_file).unwrap();
}
}
}
})
.unwrap();
let mut state_change_task = None::<Task<Result<StateChange, RecvError>>>;
let mut pulse_save_restore_interval: Option<Duration> = None;
let mut pending_shutdown = None;
enum StateChange {
Pause(bool),
Resume(bool),
Reset(Result<(), RemoteError>),
PulseSaveRestore(Result<(), PulseSaveRestoreError>),
}
enum Event {
Command((InteractiveCommand, mesh::OneshotSender<()>)),
InspectRequestFromCompletionEngine(
(InspectTarget, String, mesh::OneshotSender<inspect::Node>),
),
Quit,
Halt(vmm_core_defs::HaltReason),
PulseSaveRestore,
Worker(WorkerEvent),
VncWorker(WorkerEvent),
StateChange(Result<StateChange, RecvError>),
ShutdownResult(Result<hyperv_ic_resources::shutdown::ShutdownResult, RecvError>),
}
let mut console_command_recv = console_command_recv
.map(Event::Command)
.chain(futures::stream::repeat_with(|| Event::Quit));
let mut notify_recv = notify_recv.map(Event::Halt);
let mut inspect_completion_engine_recv =
inspect_completion_engine_recv.map(Event::InspectRequestFromCompletionEngine);
let mut quit = false;
loop {
let event = {
let pulse_save_restore = pin!(async {
match pulse_save_restore_interval {
Some(wait) => {
PolledTimer::new(driver).sleep(wait).await;
Event::PulseSaveRestore
}
None => pending().await,
}
});
let vm = (&mut vm_worker).map(Event::Worker);
let vnc = futures::stream::iter(vnc_worker.as_mut())
.flatten()
.map(Event::VncWorker);
let change = futures::stream::iter(state_change_task.as_mut().map(|x| x.into_stream()))
.flatten()
.map(Event::StateChange);
let shutdown = pin!(async {
if let Some(s) = &mut pending_shutdown {
Event::ShutdownResult(s.await)
} else {
pending().await
}
});
(
&mut console_command_recv,
&mut inspect_completion_engine_recv,
&mut notify_recv,
pulse_save_restore.into_stream(),
vm,
vnc,
change,
shutdown.into_stream(),
)
.merge()
.next()
.await
.unwrap()
};
let (cmd, _processing_done_send) = match event {
Event::Command(message) => message,
Event::InspectRequestFromCompletionEngine((vtl, path, res)) => {
let mut inspection =
InspectionBuilder::new(&path)
.depth(Some(1))
.inspect(inspect_obj(
vtl,
mesh,
&vm_worker,
vnc_worker.as_ref(),
gdb_worker.as_ref(),
&mut diag_inspector,
));
let _ = CancelContext::new()
.with_timeout(Duration::from_secs(1))
.until_cancelled(inspection.resolve())
.await;
let node = inspection.results();
res.send(node);
continue;
}
Event::Quit => break,
Event::Halt(reason) => {
match reason {
vmm_core_defs::HaltReason::Reset
if !opt.halt_on_reset && state_change_task.is_none() =>
{
tracing::info!("guest-initiated reset");
state_change(
driver,
&vm_rpc,
&mut state_change_task,
VmRpc::Reset,
StateChange::Reset,
);
}
_ => {
tracing::info!(?reason, "guest halted");
}
}
continue;
}
Event::PulseSaveRestore => {
vm_rpc.call(VmRpc::PulseSaveRestore, ()).await??;
continue;
}
Event::Worker(event) => {
match event {
WorkerEvent::Stopped => {
if quit {
tracing::info!("vm stopped");
} else {
tracing::error!("vm worker unexpectedly stopped");
}
break;
}
WorkerEvent::Failed(err) => {
tracing::error!(error = &err as &dyn std::error::Error, "vm worker failed");
break;
}
WorkerEvent::RestartFailed(err) => {
tracing::error!(
error = &err as &dyn std::error::Error,
"vm worker restart failed"
);
}
WorkerEvent::Started => {
tracing::info!("vm worker restarted");
}
}
continue;
}
Event::VncWorker(event) => {
match event {
WorkerEvent::Stopped => tracing::error!("vnc unexpectedly stopped"),
WorkerEvent::Failed(err) => {
tracing::error!(
error = &err as &dyn std::error::Error,
"vnc worker failed"
);
}
WorkerEvent::RestartFailed(err) => {
tracing::error!(
error = &err as &dyn std::error::Error,
"vnc worker restart failed"
);
}
WorkerEvent::Started => {
tracing::info!("vnc worker restarted");
}
}
continue;
}
Event::StateChange(r) => {
match r {
Ok(sc) => match sc {
StateChange::Pause(success) => {
if success {
tracing::info!("pause complete");
} else {
tracing::warn!("already paused");
}
}
StateChange::Resume(success) => {
if success {
tracing::info!("resumed complete");
} else {
tracing::warn!("already running");
}
}
StateChange::Reset(r) => match r {
Ok(()) => tracing::info!("reset complete"),
Err(err) => tracing::error!(
error = &err as &dyn std::error::Error,
"reset failed"
),
},
StateChange::PulseSaveRestore(r) => match r {
Ok(()) => tracing::info!("pulse save/restore complete"),
Err(err) => tracing::error!(
error = &err as &dyn std::error::Error,
"pulse save/restore failed"
),
},
},
Err(err) => {
tracing::error!(
error = &err as &dyn std::error::Error,
"communication failure during state change"
);
}
}
state_change_task = None;
continue;
}
Event::ShutdownResult(r) => {
match r {
Ok(r) => match r {
hyperv_ic_resources::shutdown::ShutdownResult::Ok => {
tracing::info!("shutdown initiated");
}
hyperv_ic_resources::shutdown::ShutdownResult::NotReady => {
tracing::error!("shutdown ic not ready");
}
hyperv_ic_resources::shutdown::ShutdownResult::AlreadyInProgress => {
tracing::error!("shutdown already in progress");
}
hyperv_ic_resources::shutdown::ShutdownResult::Failed(hr) => {
tracing::error!("shutdown failed with error code {hr:#x}");
}
},
Err(err) => {
tracing::error!(
error = &err as &dyn std::error::Error,
"communication failure during shutdown"
);
}
}
pending_shutdown = None;
continue;
}
};
fn inspect_obj<'a>(
target: InspectTarget,
mesh: &'a VmmMesh,
vm_worker: &'a WorkerHandle,
vnc_worker: Option<&'a WorkerHandle>,
gdb_worker: Option<&'a WorkerHandle>,
diag_inspector: &'a mut DiagInspector,
) -> impl 'a + InspectMut {
inspect::adhoc_mut(move |req| match target {
InspectTarget::Host => {
let mut resp = req.respond();
resp.field("mesh", mesh)
.field("vm", vm_worker)
.field("vnc", vnc_worker)
.field("gdb", gdb_worker);
}
InspectTarget::Paravisor => {
diag_inspector.inspect_mut(req);
}
})
}
fn state_change<U: 'static + Send>(
driver: impl Spawn,
vm_rpc: &mesh::Sender<VmRpc>,
state_change_task: &mut Option<Task<Result<StateChange, RecvError>>>,
f: impl FnOnce(Rpc<(), U>) -> VmRpc,
g: impl FnOnce(U) -> StateChange + 'static + Send,
) {
if state_change_task.is_some() {
tracing::error!("state change already in progress");
} else {
let rpc = vm_rpc.call(f, ());
*state_change_task =
Some(driver.spawn("state-change", async move { Ok(g(rpc.await?)) }));
}
}
match cmd {
InteractiveCommand::Panic => {
panic!("injected panic")
}
InteractiveCommand::Restart => {
let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
vm_worker.restart(&vm_host);
}
InteractiveCommand::Pause => {
state_change(
driver,
&vm_rpc,
&mut state_change_task,
VmRpc::Pause,
StateChange::Pause,
);
}
InteractiveCommand::Resume => {
state_change(
driver,
&vm_rpc,
&mut state_change_task,
VmRpc::Resume,
StateChange::Resume,
);
}
InteractiveCommand::Reset => {
state_change(
driver,
&vm_rpc,
&mut state_change_task,
VmRpc::Reset,
StateChange::Reset,
);
}
InteractiveCommand::PulseSaveRestore => {
state_change(
driver,
&vm_rpc,
&mut state_change_task,
VmRpc::PulseSaveRestore,
StateChange::PulseSaveRestore,
);
}
InteractiveCommand::SchedulePulseSaveRestore { interval } => {
pulse_save_restore_interval = match interval {
Some(seconds) if seconds != 0 => Some(Duration::from_secs(seconds)),
_ => {
None
}
}
}
InteractiveCommand::Shutdown {
reboot,
hibernate,
force,
} => {
if pending_shutdown.is_some() {
println!("shutdown already in progress");
} else if let Some(ic) = &resources.shutdown_ic {
let params = hyperv_ic_resources::shutdown::ShutdownParams {
shutdown_type: if hibernate {
hyperv_ic_resources::shutdown::ShutdownType::Hibernate
} else if reboot {
hyperv_ic_resources::shutdown::ShutdownType::Reboot
} else {
hyperv_ic_resources::shutdown::ShutdownType::PowerOff
},
force,
};
pending_shutdown =
Some(ic.call(hyperv_ic_resources::shutdown::ShutdownRpc::Shutdown, params));
} else {
println!("no shutdown ic configured");
}
}
InteractiveCommand::Nmi => {
let _ = vm_rpc.call(VmRpc::Nmi, 0).await;
}
InteractiveCommand::ClearHalt => {
vm_rpc.call(VmRpc::ClearHalt, ()).await.ok();
}
InteractiveCommand::AddDisk {
read_only,
target,
path,
lun,
ram,
file_path,
is_dvd,
} => {
let action = async {
let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
let disk_type = match ram {
None => {
let path = file_path.context("no filename passed")?;
open_disk_type(path.as_ref(), read_only)
.with_context(|| format!("failed to open {}", path.display()))?
}
Some(size) => {
Resource::new(disk_backend_resources::RamDiskHandle { len: size })
}
};
let device = if is_dvd {
SimpleScsiDvdHandle {
media: Some(disk_type),
requests: None,
}
.into_resource()
} else {
SimpleScsiDiskHandle {
disk: disk_type,
read_only,
parameters: Default::default(),
}
.into_resource()
};
let cfg = ScsiDeviceAndPath {
path: ScsiPath { path, target, lun },
device,
};
scsi.call_failable(ScsiControllerRequest::AddDevice, cfg)
.await?;
anyhow::Result::<_>::Ok(())
};
if let Err(error) = action.await {
tracing::error!(error = error.as_error(), "error adding disk")
}
}
InteractiveCommand::RmDisk { target, path, lun } => {
let action = async {
let scsi = resources.scsi_rpc.as_ref().context("no scsi controller")?;
scsi.call_failable(
ScsiControllerRequest::RemoveDevice,
ScsiPath { target, path, lun },
)
.await?;
anyhow::Ok(())
};
if let Err(error) = action.await {
tracing::error!(error = error.as_error(), "error removing disk")
}
}
InteractiveCommand::Inspect {
recursive,
limit,
paravisor,
element,
update,
} => {
let obj = inspect_obj(
if paravisor {
InspectTarget::Paravisor
} else {
InspectTarget::Host
},
mesh,
&vm_worker,
vnc_worker.as_ref(),
gdb_worker.as_ref(),
&mut diag_inspector,
);
if let Some(value) = update {
let Some(element) = element else {
anyhow::bail!("must provide element for update")
};
let value = async {
let update = inspect::update(&element, &value, obj);
let value = CancelContext::new()
.with_timeout(Duration::from_secs(1))
.until_cancelled(update)
.await??;
anyhow::Ok(value)
}
.await;
match value {
Ok(node) => println!("{:#}", node),
Err(err) => println!("error: {:#}", err),
}
} else {
let element = element.unwrap_or_default();
let depth = if recursive { limit } else { Some(0) };
let node = async {
let mut inspection =
InspectionBuilder::new(&element).depth(depth).inspect(obj);
let _ = CancelContext::new()
.with_timeout(Duration::from_secs(1))
.until_cancelled(inspection.resolve())
.await;
inspection.results()
}
.await;
println!("{:#}", node);
}
}
InteractiveCommand::RestartVnc => {
if let Some(vnc) = &mut vnc_worker {
let action = || async move {
let vnc_host = mesh
.make_host("vnc", None)
.await
.context("spawning vnc process failed")?;
vnc.restart(&vnc_host);
anyhow::Result::<_>::Ok(())
};
if let Err(error) = (action)().await {
eprintln!("error: {}", error);
}
} else {
eprintln!("ERROR: no VNC server running");
}
}
InteractiveCommand::Hvsock { term, port } => {
let vm_rpc = &vm_rpc;
let action = || async move {
let service_id = new_hvsock_service_id(port);
let pool = DefaultPool::new();
let socket = vm_rpc
.call_failable(
VmRpc::ConnectHvsock,
(
CancelContext::new().with_timeout(Duration::from_secs(2)),
service_id,
DeviceVtl::Vtl0,
),
)
.await?;
let path = console::relay_console_server(&pool.driver(), socket)?;
console::launch_console(term.as_ref().map(|x| x.as_ref()), &path)?;
thread::spawn(move || pool.run());
anyhow::Result::<_>::Ok(())
};
if let Err(error) = (action)().await {
eprintln!("error: {}", error);
}
}
InteractiveCommand::ServiceVtl2 {
user_mode_only,
igvm,
} => {
let r = async {
let start;
if user_mode_only {
let diag = connect_diag(driver.clone(), &vm_rpc, DeviceVtl::Vtl2).await?;
start = Instant::now();
diag.restart().await?;
} else {
let path = igvm
.as_ref()
.or(opt.igvm.as_ref())
.context("no igvm file loaded")?;
let file = fs_err::File::open(path)?;
start = Instant::now();
hvlite_helpers::underhill::service_underhill(
&vm_rpc,
resources.ged_rpc.as_ref().context("no GED")?,
file.into(),
)
.await?;
}
anyhow::Ok(start)
}
.await;
let end = Instant::now();
match r {
Ok(start) => {
println!("servicing time: {}ms", (end - start).as_millis());
}
Err(err) => eprintln!("error: {:#}", err),
}
}
InteractiveCommand::Quit => {
tracing::info!("quitting");
resources.scsi_rpc = None;
vm_worker.stop();
quit = true;
}
InteractiveCommand::ReadMemory { gpa, size, file } => {
let size = size as usize;
let data = vm_rpc.call(VmRpc::ReadMemory, (gpa, size)).await?;
match data {
Ok(bytes) => {
if let Some(file) = file {
if let Err(err) = fs_err::write(file, bytes) {
eprintln!("error: {err:?}");
}
} else {
let width = 16;
let show_ascii = true;
let mut dump = String::new();
for (i, chunk) in bytes.chunks(width).enumerate() {
let hex_part: Vec<String> =
chunk.iter().map(|byte| format!("{:02x}", byte)).collect();
let hex_line = hex_part.join(" ");
if show_ascii {
let ascii_part: String = chunk
.iter()
.map(|&byte| {
if byte.is_ascii_graphic() || byte == b' ' {
byte as char
} else {
'.'
}
})
.collect();
dump.push_str(&format!(
"{:04x}: {:<width$} {}\n",
i * width,
hex_line,
ascii_part,
width = width * 3 - 1
));
} else {
dump.push_str(&format!("{:04x}: {}\n", i * width, hex_line));
}
}
println!("{dump}");
}
}
Err(err) => {
eprintln!("error: {err:?}");
}
}
}
InteractiveCommand::WriteMemory { gpa, hex, file } => {
if hex.is_some() == file.is_some() {
eprintln!("error: either path to the file or the hex string must be specified");
continue;
}
let data = if let Some(file) = file {
let data = fs_err::read(file);
match data {
Ok(data) => data,
Err(err) => {
eprintln!("error: {err:?}");
continue;
}
}
} else if let Some(hex) = hex {
if hex.len() & 1 != 0 {
eprintln!(
"error: expected even number of hex digits (2 hex digits per byte)"
);
continue;
}
let data: Result<Vec<u8>, String> = (0..hex.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
format!("invalid hex character at position {}: {}", i, e)
})
})
.collect();
match data {
Ok(data) => data,
Err(err) => {
eprintln!("error: {err}");
continue;
}
}
} else {
unreachable!();
};
if data.is_empty() {
eprintln!("error: no data to write");
continue;
}
if let Err(err) = vm_rpc.call(VmRpc::WriteMemory, (gpa, data)).await? {
eprintln!("error: {err:?}");
}
}
InteractiveCommand::Input { .. } | InteractiveCommand::InputMode => unreachable!(),
}
}
vm_worker.stop();
vm_worker.join().await?;
Ok(())
}
async fn connect_diag(
driver: impl Driver + Spawn,
vm_rpc: &mesh::Sender<VmRpc>,
openhcl_vtl: DeviceVtl,
) -> anyhow::Result<diag_client::DiagClient> {
let service_id = new_hvsock_service_id(1);
let socket = vm_rpc
.call_failable(
VmRpc::ConnectHvsock,
(
CancelContext::new().with_timeout(Duration::from_secs(2)),
service_id,
openhcl_vtl,
),
)
.await?;
let diag_client = diag_client::DiagClient::from_conn(driver, socket);
Ok(diag_client)
}
pub struct DiagInspector(DiagInspectorInner);
enum DiagInspectorInner {
NotStarted {
driver: DefaultDriver,
vm_rpc: Arc<mesh::Sender<VmRpc>>,
openhcl_vtl: DeviceVtl,
},
Started {
send: mesh::Sender<inspect::Deferred>,
_task: Task<()>,
},
Invalid,
}
impl DiagInspector {
pub fn new(
driver: DefaultDriver,
vm_rpc: Arc<mesh::Sender<VmRpc>>,
openhcl_vtl: DeviceVtl,
) -> Self {
Self(DiagInspectorInner::NotStarted {
driver,
vm_rpc,
openhcl_vtl,
})
}
fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
loop {
match self.0 {
DiagInspectorInner::NotStarted { .. } => {
let DiagInspectorInner::NotStarted {
driver,
vm_rpc,
openhcl_vtl,
} = std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
else {
unreachable!()
};
let (send, recv) = mesh::channel();
let task = driver
.clone()
.spawn("diag-inspect", Self::run(driver, vm_rpc, recv, openhcl_vtl));
self.0 = DiagInspectorInner::Started { send, _task: task };
}
DiagInspectorInner::Started { ref send, .. } => break send,
DiagInspectorInner::Invalid => unreachable!(),
}
}
}
async fn run(
driver: DefaultDriver,
vm_rpc: Arc<mesh::Sender<VmRpc>>,
mut recv: mesh::Receiver<inspect::Deferred>,
openhcl_vtl: DeviceVtl,
) {
let mut last_client = None;
while let Some(deferred) = recv.next().await {
let client = if let Some(client) = &mut last_client {
client
} else {
match connect_diag(driver.clone(), &vm_rpc, openhcl_vtl).await {
Ok(client) => last_client.insert(client),
Err(err) => {
deferred.complete_external(
inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}"))),
inspect::SensitivityLevel::Unspecified,
);
continue;
}
}
};
let info = deferred.external_request();
let result = match info.request_type {
inspect::ExternalRequestType::Inspect { depth } => {
if depth == 0 {
Ok(inspect::Node::Unevaluated)
} else {
client
.inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
.await
}
}
inspect::ExternalRequestType::Update { value } => {
(client.update(info.path, value).await).map(inspect::Node::Value)
}
};
deferred.complete_external(
result.unwrap_or_else(|err| {
last_client = None;
inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
}),
inspect::SensitivityLevel::Unspecified,
)
}
}
}
impl InspectMut for DiagInspector {
fn inspect_mut(&mut self, req: inspect::Request<'_>) {
self.start().send(req.defer());
}
}
enum InspectTarget {
Host,
Paravisor,
}
mod interactive_console {
use super::InteractiveCommand;
use rustyline::Helper;
use rustyline::Highlighter;
use rustyline::Hinter;
use rustyline::Validator;
#[derive(Helper, Highlighter, Hinter, Validator)]
pub(crate) struct HvLiteRustylineEditor {
pub hvlite_inspect_req: std::sync::Arc<
mesh::Sender<(
super::InspectTarget,
String,
mesh::OneshotSender<inspect::Node>,
)>,
>,
}
impl rustyline::completion::Completer for HvLiteRustylineEditor {
type Candidate = String;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
let Ok(cmd) = shell_words::split(line) else {
return Ok((0, Vec::with_capacity(0)));
};
let completions = futures::executor::block_on(
clap_dyn_complete::Complete {
cmd,
raw: Some(line.into()),
position: Some(pos),
}
.generate_completions::<InteractiveCommand>(None, self),
);
let pos_from_end = {
let line = line.chars().take(pos).collect::<String>();
let trailing_ws = line.len() - line.trim_end().len();
if trailing_ws > 0 {
line.len() - trailing_ws + 1 } else {
let last_word = shell_words::split(&line)
.unwrap_or_default()
.last()
.cloned()
.unwrap_or_default();
line.len() - last_word.len()
}
};
Ok((pos_from_end, completions))
}
}
impl clap_dyn_complete::CustomCompleterFactory for &HvLiteRustylineEditor {
type CustomCompleter = HvLiteComplete;
async fn build(&self, _ctx: &clap_dyn_complete::RootCtx<'_>) -> Self::CustomCompleter {
HvLiteComplete {
hvlite_inspect_req: self.hvlite_inspect_req.clone(),
}
}
}
pub struct HvLiteComplete {
hvlite_inspect_req: std::sync::Arc<
mesh::Sender<(
super::InspectTarget,
String,
mesh::OneshotSender<inspect::Node>,
)>,
>,
}
impl clap_dyn_complete::CustomCompleter for HvLiteComplete {
async fn complete(
&self,
ctx: &clap_dyn_complete::RootCtx<'_>,
subcommand_path: &[&str],
arg_id: &str,
) -> Vec<String> {
match (subcommand_path, arg_id) {
(["hvlite_entry", "inspect"], "element") => {
let on_error = vec!["failed/to/connect".into()];
let (parent_path, to_complete) = (ctx.to_complete)
.rsplit_once('/')
.unwrap_or(("", ctx.to_complete));
let node = {
let paravisor = {
let raw_arg = ctx
.matches
.subcommand()
.unwrap()
.1
.get_one::<String>("paravisor")
.map(|x| x.as_str())
.unwrap_or_default();
raw_arg == "true"
};
let (tx, rx) = mesh::oneshot();
self.hvlite_inspect_req.send((
if paravisor {
super::InspectTarget::Paravisor
} else {
super::InspectTarget::Host
},
parent_path.to_owned(),
tx,
));
let Ok(node) = rx.await else {
return on_error;
};
node
};
let mut completions = Vec::new();
if let inspect::Node::Dir(dir) = node {
for entry in dir {
if entry.name.starts_with(to_complete) {
if parent_path.is_empty() {
completions.push(format!("{}/", entry.name))
} else {
completions.push(format!(
"{}/{}{}",
parent_path,
entry.name,
if matches!(entry.node, inspect::Node::Dir(..)) {
"/"
} else {
""
}
))
}
}
}
} else {
return on_error;
}
completions
}
_ => Vec::new(),
}
}
}
}