Skip to main content

underhill_core/
options.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! CLI argument parsing for the underhill core process.
5
6#![warn(missing_docs)]
7
8use anyhow::Context;
9use anyhow::bail;
10use inspect::Inspect;
11use inspect::InspectMut;
12use mesh::MeshPayload;
13use std::collections::BTreeMap;
14use std::ffi::OsStr;
15use std::ffi::OsString;
16use std::path::PathBuf;
17use std::str::FromStr;
18
19#[derive(Clone, Debug, MeshPayload)]
20pub enum TestScenarioConfig {
21    SaveFail,
22    RestoreStuck,
23    SaveStuck,
24
25    /// Exercises a mocked TDISP flow for emulated TDISP devices produced by OpenVMM tests.
26    VpciTdispFlow,
27}
28
29impl FromStr for TestScenarioConfig {
30    type Err = anyhow::Error;
31
32    fn from_str(s: &str) -> Result<TestScenarioConfig, anyhow::Error> {
33        match s {
34            "SERVICING_SAVE_FAIL" => Ok(TestScenarioConfig::SaveFail),
35            "SERVICING_RESTORE_STUCK" => Ok(TestScenarioConfig::RestoreStuck),
36            "SERVICING_SAVE_STUCK" => Ok(TestScenarioConfig::SaveStuck),
37            "TDISP_VPCI_FLOW_TEST" => Ok(TestScenarioConfig::VpciTdispFlow),
38            _ => Err(anyhow::anyhow!("Invalid test config: {}", s)),
39        }
40    }
41}
42
43#[derive(Clone, Debug, MeshPayload)]
44pub enum GuestStateLifetimeCli {
45    Default,
46    ReprovisionOnFailure,
47    Reprovision,
48    Ephemeral,
49}
50
51impl FromStr for GuestStateLifetimeCli {
52    type Err = anyhow::Error;
53
54    fn from_str(s: &str) -> Result<GuestStateLifetimeCli, anyhow::Error> {
55        match s {
56            "DEFAULT" | "0" => Ok(GuestStateLifetimeCli::Default),
57            "REPROVISION_ON_FAILURE" | "1" => Ok(GuestStateLifetimeCli::ReprovisionOnFailure),
58            "REPROVISION" | "2" => Ok(GuestStateLifetimeCli::Reprovision),
59            "EPHEMERAL" | "3" => Ok(GuestStateLifetimeCli::Ephemeral),
60            _ => Err(anyhow::anyhow!("Invalid lifetime: {}", s)),
61        }
62    }
63}
64
65#[derive(Clone, Debug, MeshPayload)]
66pub enum GuestStateEncryptionPolicyCli {
67    Auto,
68    None,
69    GspById,
70    GspKey,
71}
72
73impl FromStr for GuestStateEncryptionPolicyCli {
74    type Err = anyhow::Error;
75
76    fn from_str(s: &str) -> Result<GuestStateEncryptionPolicyCli, anyhow::Error> {
77        match s {
78            "AUTO" | "0" => Ok(GuestStateEncryptionPolicyCli::Auto),
79            "NONE" | "1" => Ok(GuestStateEncryptionPolicyCli::None),
80            "GSP_BY_ID" | "2" => Ok(GuestStateEncryptionPolicyCli::GspById),
81            "GSP_KEY" | "3" => Ok(GuestStateEncryptionPolicyCli::GspKey),
82            _ => Err(anyhow::anyhow!("Invalid encryption policy: {}", s)),
83        }
84    }
85}
86
87#[derive(Clone, Copy, Debug, MeshPayload)]
88pub enum EfiDiagnosticsLogLevelCli {
89    Default,
90    Info,
91    Full,
92}
93
94impl FromStr for EfiDiagnosticsLogLevelCli {
95    type Err = anyhow::Error;
96
97    fn from_str(s: &str) -> Result<EfiDiagnosticsLogLevelCli, anyhow::Error> {
98        match s {
99            "DEFAULT" | "0" => Ok(EfiDiagnosticsLogLevelCli::Default),
100            "INFO" | "1" => Ok(EfiDiagnosticsLogLevelCli::Info),
101            "FULL" | "2" => Ok(EfiDiagnosticsLogLevelCli::Full),
102            _ => Err(anyhow::anyhow!("Invalid EFI diagnostics log level: {}", s)),
103        }
104    }
105}
106
107#[derive(Clone, Debug, MeshPayload, Inspect, InspectMut)]
108pub enum KeepAliveConfig {
109    EnabledHostAndPrivatePoolPresent,
110    DisabledHostAndPrivatePoolPresent,
111    Disabled,
112}
113
114impl FromStr for KeepAliveConfig {
115    type Err = anyhow::Error;
116
117    fn from_str(s: &str) -> Result<KeepAliveConfig, anyhow::Error> {
118        match s.to_lowercase().as_str() {
119            "host,privatepool" | "enabled" => Ok(KeepAliveConfig::EnabledHostAndPrivatePoolPresent),
120            "nohost,privatepool" => Ok(KeepAliveConfig::DisabledHostAndPrivatePoolPresent),
121            "nohost,noprivatepool" => Ok(KeepAliveConfig::Disabled),
122            x if x == "disabled" || x.starts_with("disabled,") => Ok(KeepAliveConfig::Disabled),
123            _ => Err(anyhow::anyhow!("Invalid keepalive config: {}", s)),
124        }
125    }
126}
127
128impl KeepAliveConfig {
129    pub fn is_enabled(&self) -> bool {
130        matches!(self, KeepAliveConfig::EnabledHostAndPrivatePoolPresent)
131    }
132
133    /// Returns the string representation matching the inspect rename attributes.
134    pub fn as_str(&self) -> &'static str {
135        match self {
136            KeepAliveConfig::EnabledHostAndPrivatePoolPresent => "enabled",
137            KeepAliveConfig::DisabledHostAndPrivatePoolPresent => "nohost,privatepool",
138            KeepAliveConfig::Disabled => "disabled",
139        }
140    }
141}
142
143// We've made our own parser here instead of using something like clap in order
144// to save on compiled file size. We don't need all the features a crate can provide.
145/// underhill core command-line and environment variable options.
146pub struct Options {
147    /// (OPENHCL_WAIT_FOR_START=1 | --wait-for-start)
148    ///  wait for a diagnostics start request before initializing and starting the VM
149    pub wait_for_start: bool,
150
151    /// (OPENHCL_SIGNAL_VTL0_STARTED=1)
152    /// immediately signal that VTL0 has started, before doing any
153    /// initialization. This allows VM boot to proceed even if initialization
154    /// may hang (e.g., because you specified OPENHCL_WAIT_FOR_START=1).
155    pub signal_vtl0_started: bool,
156
157    /// (OPENHCL_REFORMAT_VMGS=1 | --reformat-vmgs)
158    /// reformat the VMGS file on boot. useful for running potentially destructive VMGS tests.
159    pub reformat_vmgs: bool,
160
161    /// (OPENHCL_PID_FILE_PATH=/path/to/file | --pid /path/to/file)
162    /// write the PID to the specified path
163    pub pid: Option<PathBuf>,
164
165    /// (OPENHCL_VMBUS_MAX_VERSION=\<number\>)
166    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
167    pub vmbus_max_version: Option<u32>,
168
169    /// (OPENHCL_VMBUS_ENABLE_MNF=1)
170    /// Enable handling of MNF in the Underhill vmbus server, instead of the host.
171    pub vmbus_enable_mnf: Option<bool>,
172
173    /// (OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY=1)
174    /// Force the use of confidential external memory for all non-relay vmbus channels. For testing
175    /// purposes only.
176    ///
177    /// N.B.: Not all vmbus devices support this feature, so enabling it may cause failures.
178    pub vmbus_force_confidential_external_memory: bool,
179
180    /// (OPENHCL_VMBUS_CHANNEL_UNSTICK_DELAY_MS=\<number\>) (default: 100)
181    /// Delay before unsticking a vmbus channel after it has been opened, in milliseconds. Set to
182    /// zero to disable unsticking.
183    pub vmbus_channel_unstick_delay_ms: u64,
184
185    /// (OPENHCL_CMDLINE_APPEND=\<string\>)
186    /// Command line to append to VTL0, only used with direct boot.
187    pub cmdline_append: Option<String>,
188
189    /// (OPENHCL_VNC_PORT=\<number\> | --vnc-port \<number\>) (default: 3)
190    /// VNC (vsock) port number
191    pub vnc_port: u32,
192
193    /// (OPENHCL_GDBSTUB=1)
194    /// Enables the GDB stub for debugging the guest.
195    pub gdbstub: bool,
196
197    /// (OPENHCL_GDBSTUB_PORT=\<number\>) (default: 4)
198    /// GDB stub (vsock) port number.
199    pub gdbstub_port: u32,
200
201    /// (OPENHCL_VTL0_STARTS_PAUSED=1)
202    /// Start with VTL0 paused
203    pub vtl0_starts_paused: bool,
204
205    /// (OPENHCL_FRAMEBUFFER_GPA_BASE=\<number\>)
206    /// Base GPA of the fixed framebuffer mapping for underhill to read.
207    /// If a value is provided, a graphics device is exposed.
208    // TODO: send this value as an IGVM device tree parameter instead
209    pub framebuffer_gpa_base: Option<u64>,
210
211    /// (OPENHCL_SERIAL_WAIT_FOR_RTS=\<bool\>)
212    /// Whether the emulated 16550 waits for guest DTR+RTS before pulling data
213    /// from the host.
214    pub serial_wait_for_rts: bool,
215
216    /// (OPENHCL_FORCE_LOAD_VTL0_IMAGE=\<string\>)
217    /// Force load the specified image in VTL0. The image must support the
218    /// option specified.
219    ///
220    /// Valid options are "pcat, uefi, linux".
221    pub force_load_vtl0_image: Option<String>,
222
223    /// (OPENHCL_NVME_VFIO=1)
224    /// Use the user-mode VFIO NVMe driver instead of the Linux driver.
225    pub nvme_vfio: bool,
226
227    /// (OPENHCL_HIDE_ISOLATION=1)
228    /// Hide the isolation mode from the guest.
229    pub hide_isolation: bool,
230
231    /// (OPENHCL_HALT_ON_GUEST_HALT=1) When receiving a halt request from a
232    /// lower VTL, halt underhill instead of forwarding the halt request to the
233    /// host. This allows for debugging state without the partition state
234    /// changing from the host.
235    pub halt_on_guest_halt: bool,
236
237    /// (OPENHCL_NO_SIDECAR_HOTPLUG=1) Leave sidecar VPs remote even if they
238    /// hit exits.
239    pub no_sidecar_hotplug: bool,
240
241    /// (OPENHCL_NVME_KEEP_ALIVE=\<KeepaliveConfig\>)
242    /// Configure NVMe keep alive behavior when servicing.
243    /// Options are:
244    ///  - "host,privatepool" - Enable keep alive if both host and private pool support it.
245    ///  - "nohost,privatepool" - Used when the host does not support keepalive, but a private pool is present. Keepalive is disabled.
246    ///  - "nohost,noprivatepool" - Keepalive is disabled.
247    ///  - "disabled, X, X" - Keepalive is disabled due to manual
248    ///    override. Host and private pool options are ignored.
249    pub nvme_keep_alive: KeepAliveConfig,
250
251    /// (OPENHCL_MANA_KEEP_ALIVE=\<KeepAliveConfig\>)
252    /// Configure MANA keep alive behavior when servicing.
253    /// Options are:
254    ///  - "host,privatepool" - Enable keep alive if both host and private pool support it.
255    ///  - "nohost,privatepool" - Used when the host does not support keepalive, but a private pool is present. Keepalive is disabled.
256    ///  - "nohost,noprivatepool" - Keepalive is disabled.
257    ///  - "disabled, X, X" - TODO: This needs to be implemented for mana.
258    pub mana_keep_alive: KeepAliveConfig,
259
260    /// (OPENHCL_NVME_ALWAYS_FLR=1)
261    /// Always use the FLR (Function Level Reset) path for NVMe devices,
262    /// even if we would otherwise attempt to use VFIO's NoReset support.
263    pub nvme_always_flr: bool,
264
265    /// (OPENHCL_TEST_CONFIG=\<TestScenarioConfig\>)
266    /// Test configurations are designed to replicate specific behaviors and
267    /// conditions in order to simulate various test scenarios.
268    pub test_configuration: Option<TestScenarioConfig>,
269
270    /// (OPENHCL_DISABLE_UEFI_FRONTPAGE=1) Disable the frontpage in UEFI which
271    /// will result in UEFI terminating, shutting down the guest instead of
272    /// showing the frontpage.
273    pub disable_uefi_frontpage: Option<bool>,
274
275    /// (HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT=1) Instruct UEFI to always attempt a
276    /// default boot, even if existing boot entries fail.
277    pub default_boot_always_attempt: Option<bool>,
278
279    /// (HCL_GUEST_STATE_LIFETIME=\<GuestStateLifetimeCli\>)
280    /// Specify which guest state lifetime to use.
281    pub guest_state_lifetime: Option<GuestStateLifetimeCli>,
282
283    /// (HCL_GUEST_STATE_ENCRYPTION_POLICY=\<GuestStateEncryptionPolicyCli\>)
284    /// Specify which guest state encryption policy to use.
285    pub guest_state_encryption_policy: Option<GuestStateEncryptionPolicyCli>,
286
287    /// (HCL_EFI_DIAGNOSTICS_LOG_LEVEL=\<EfiDiagnosticsLogLevelCli\>)
288    /// Specify the EFI diagnostics log level filter (DEFAULT, INFO, or FULL).
289    /// Overrides the value in DPS when set.
290    pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
291
292    /// (HCL_STRICT_ENCRYPTION_POLICY=1) Strict guest state encryption policy.
293    pub strict_encryption_policy: Option<bool>,
294
295    /// (HCL_ATTEMPT_AK_CERT_CALLBACK=1) Attempt to renew the AK cert.
296    /// If not specified, use the configuration in DPSv2 ManagementVtlFeatures.
297    pub attempt_ak_cert_callback: Option<bool>,
298
299    /// (OPENHCL_ENABLE_VPCI_RELAY=1) Enable the VPCI relay.
300    pub enable_vpci_relay: Option<bool>,
301
302    /// (OPENHCL_DISABLE_PROXY_REDIRECT=1) Disable proxy interrupt redirection.
303    pub disable_proxy_redirect: bool,
304
305    /// (OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT=1) Disable lower VTL timer virtualization.
306    pub disable_lower_vtl_timer_virt: bool,
307
308    /// (OPENHCL_CONFIG_TIMEOUT_IN_SECONDS=\<number\>) (default: 5)
309    /// Timeout in seconds for VM configuration operations, both initial
310    /// configuration and subsequent modifications.
311    pub config_timeout_in_seconds: u64,
312
313    /// (OPENHCL_SERVICING_TIMEOUT_DUMP_COLLECTION_IN_MS=\<number\>) (default: 500)
314    /// The default time to wait in milliseconds for dump collection during a
315    /// panic in servicing.
316    pub servicing_timeout_dump_collection_in_ms: u64,
317}
318
319impl Options {
320    pub(crate) fn parse(
321        extra_args: Vec<String>,
322        extra_env: Vec<(String, Option<String>)>,
323    ) -> anyhow::Result<Self> {
324        // Pull the entire environment into a BTreeMap for manipulation through extra_env.
325        let mut env: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
326        for (key, value) in extra_env {
327            match value {
328                Some(value) => env.insert(key.into(), value.into()),
329                None => env.remove::<OsStr>(key.as_ref()),
330            };
331        }
332
333        // Reads an environment variable, falling back to a legacy variable (replacing
334        // "OPENHCL_" with "UNDERHILL_") if the original is not set.
335        let read_legacy_openhcl_env = |name: &str| -> Option<&OsString> {
336            env.get::<OsStr>(name.as_ref()).or_else(|| {
337                env.get::<OsStr>(
338                    format!(
339                        "UNDERHILL_{}",
340                        name.strip_prefix("OPENHCL_").unwrap_or(name)
341                    )
342                    .as_ref(),
343                )
344            })
345        };
346
347        // Reads an environment variable strings.
348        let read_env = |name: &str| -> Option<&OsString> { env.get::<OsStr>(name.as_ref()) };
349
350        fn parse_bool_opt(value: Option<&OsString>) -> anyhow::Result<Option<bool>> {
351            value
352                .map(|v| {
353                    if v.eq_ignore_ascii_case("true") || v == "1" {
354                        Ok(true)
355                    } else if v.eq_ignore_ascii_case("false") || v == "0" {
356                        Ok(false)
357                    } else {
358                        Err(anyhow::anyhow!(
359                            "invalid boolean environment variable: {}",
360                            v.to_string_lossy()
361                        ))
362                    }
363                })
364                .transpose()
365        }
366
367        fn parse_bool(value: Option<&OsString>) -> bool {
368            parse_bool_opt(value).ok().flatten().unwrap_or_default()
369        }
370
371        let parse_legacy_env_bool = |name| parse_bool(read_legacy_openhcl_env(name));
372        let parse_env_bool = |name: &str| parse_bool(read_env(name));
373        let parse_env_bool_opt = |name: &str| {
374            parse_bool_opt(read_env(name))
375                .map_err(|e| tracing::warn!("failed to parse {name}: {e:#}"))
376                .ok()
377                .flatten()
378        };
379
380        fn parse_number(value: Option<&OsString>) -> anyhow::Result<Option<u64>> {
381            value
382                .map(|v| {
383                    let v = v.to_string_lossy();
384                    v.parse()
385                        .context(format!("invalid numeric environment variable: {v}"))
386                })
387                .transpose()
388        }
389
390        let parse_legacy_env_number = |name| {
391            parse_number(read_legacy_openhcl_env(name))
392                .context(format!("parsing legacy env number: {name}"))
393        };
394        let parse_env_number = |name: &str| {
395            parse_number(read_env(name)).context(format!("parsing env number: {name}"))
396        };
397
398        let mut wait_for_start = parse_legacy_env_bool("OPENHCL_WAIT_FOR_START");
399        let mut reformat_vmgs = parse_legacy_env_bool("OPENHCL_REFORMAT_VMGS");
400        let mut pid = read_legacy_openhcl_env("OPENHCL_PID_FILE_PATH")
401            .map(|x| x.to_string_lossy().into_owned().into());
402        let vmbus_max_version = read_legacy_openhcl_env("OPENHCL_VMBUS_MAX_VERSION")
403            .map(|x| {
404                vmbus_core::parse_vmbus_version(&(x.to_string_lossy()))
405                    .map_err(|x| anyhow::anyhow!("Error parsing vmbus max version: {}", x))
406            })
407            .transpose()?;
408        let vmbus_enable_mnf =
409            read_legacy_openhcl_env("OPENHCL_VMBUS_ENABLE_MNF").map(|v| parse_bool(Some(v)));
410        let vmbus_force_confidential_external_memory =
411            parse_env_bool("OPENHCL_VMBUS_FORCE_CONFIDENTIAL_EXTERNAL_MEMORY");
412        let vmbus_channel_unstick_delay_ms =
413            parse_legacy_env_number("OPENHCL_VMBUS_CHANNEL_UNSTICK_DELAY_MS")?;
414        let cmdline_append = read_legacy_openhcl_env("OPENHCL_CMDLINE_APPEND")
415            .map(|x| x.to_string_lossy().into_owned());
416        let force_load_vtl0_image = read_legacy_openhcl_env("OPENHCL_FORCE_LOAD_VTL0_IMAGE")
417            .map(|x| x.to_string_lossy().into_owned());
418        let mut vnc_port = parse_legacy_env_number("OPENHCL_VNC_PORT")?.map(|x| x as u32);
419        let framebuffer_gpa_base = parse_legacy_env_number("OPENHCL_FRAMEBUFFER_GPA_BASE")?;
420        let vtl0_starts_paused = parse_legacy_env_bool("OPENHCL_VTL0_STARTS_PAUSED");
421        let serial_wait_for_rts = parse_legacy_env_bool("OPENHCL_SERIAL_WAIT_FOR_RTS");
422        let nvme_vfio = parse_legacy_env_bool("OPENHCL_NVME_VFIO");
423        let hide_isolation = parse_env_bool("OPENHCL_HIDE_ISOLATION");
424        let halt_on_guest_halt = parse_legacy_env_bool("OPENHCL_HALT_ON_GUEST_HALT");
425        let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG");
426        let gdbstub = parse_legacy_env_bool("OPENHCL_GDBSTUB");
427        let gdbstub_port = parse_legacy_env_number("OPENHCL_GDBSTUB_PORT")?.map(|x| x as u32);
428        let nvme_keep_alive = read_env("OPENHCL_NVME_KEEP_ALIVE")
429                    .map(|x| {
430                        let s = x.to_string_lossy();
431                        match s.parse::<KeepAliveConfig>() {
432                            Ok(v) => v,
433                            Err(e) => {
434                                tracing::warn!(
435                                    "failed to parse OPENHCL_NVME_KEEP_ALIVE ('{s}'): {e}. Nvme keepalive will be disabled."
436                                );
437                                KeepAliveConfig::Disabled
438                            }
439                        }
440                    })
441                    .unwrap_or(KeepAliveConfig::Disabled);
442        let mana_keep_alive = read_env("OPENHCL_MANA_KEEP_ALIVE")
443                    .map(|x| {
444                        let s = x.to_string_lossy();
445                        match s.parse::<KeepAliveConfig>() {
446                            Ok(v) => v,
447                            Err(e) => {
448                                tracing::warn!(
449                                    "failed to parse OPENHCL_MANA_KEEP_ALIVE ('{s}'): {e}. Mana keepalive will be disabled."
450                                );
451                                KeepAliveConfig::Disabled
452                            }
453                        }
454                    })
455                    .unwrap_or(KeepAliveConfig::Disabled);
456        let nvme_always_flr = parse_env_bool("OPENHCL_NVME_ALWAYS_FLR");
457        let test_configuration = read_env("OPENHCL_TEST_CONFIG").and_then(|x| {
458            x.to_string_lossy()
459                .parse::<TestScenarioConfig>()
460                .map_err(|e| {
461                    tracing::warn!(
462                        "failed to parse OPENHCL_TEST_CONFIG: {}. No test will be simulated.",
463                        e
464                    )
465                })
466                .ok()
467        });
468        let disable_uefi_frontpage = parse_env_bool_opt("OPENHCL_DISABLE_UEFI_FRONTPAGE");
469        let signal_vtl0_started = parse_env_bool("OPENHCL_SIGNAL_VTL0_STARTED");
470        let default_boot_always_attempt = parse_env_bool_opt("HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT");
471        let guest_state_lifetime = read_env("HCL_GUEST_STATE_LIFETIME").and_then(|x| {
472            x.to_string_lossy()
473                .parse::<GuestStateLifetimeCli>()
474                .map_err(|e| tracing::warn!("failed to parse HCL_GUEST_STATE_LIFETIME: {:#}", e))
475                .ok()
476        });
477        let guest_state_encryption_policy =
478            read_env("HCL_GUEST_STATE_ENCRYPTION_POLICY").and_then(|x| {
479                x.to_string_lossy()
480                    .parse::<GuestStateEncryptionPolicyCli>()
481                    .map_err(|e| {
482                        tracing::warn!("failed to parse HCL_GUEST_STATE_ENCRYPTION_POLICY: {:#}", e)
483                    })
484                    .ok()
485            });
486        let efi_diagnostics_log_level = read_env("HCL_EFI_DIAGNOSTICS_LOG_LEVEL").and_then(|x| {
487            x.to_string_lossy()
488                .parse::<EfiDiagnosticsLogLevelCli>()
489                .map_err(|e| {
490                    tracing::warn!("failed to parse HCL_EFI_DIAGNOSTICS_LOG_LEVEL: {:#}", e)
491                })
492                .ok()
493        });
494        let strict_encryption_policy = parse_env_bool_opt("HCL_STRICT_ENCRYPTION_POLICY");
495        let attempt_ak_cert_callback = parse_env_bool_opt("HCL_ATTEMPT_AK_CERT_CALLBACK");
496        let enable_vpci_relay = parse_env_bool_opt("OPENHCL_ENABLE_VPCI_RELAY");
497        let disable_proxy_redirect = parse_env_bool("OPENHCL_DISABLE_PROXY_REDIRECT");
498        let disable_lower_vtl_timer_virt = parse_env_bool("OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT");
499        let config_timeout_in_seconds =
500            parse_legacy_env_number("OPENHCL_CONFIG_TIMEOUT_IN_SECONDS")?.unwrap_or(5);
501        let servicing_timeout_dump_collection_in_ms =
502            parse_env_number("OPENHCL_SERVICING_TIMEOUT_DUMP_COLLECTION_IN_MS")?.unwrap_or(500);
503
504        let mut args = std::env::args().chain(extra_args);
505        // Skip our own filename.
506        args.next();
507
508        while let Some(next) = args.next() {
509            let arg = next;
510
511            match &*arg {
512                "--wait-for-start" => wait_for_start = true,
513                "--reformat-vmgs" => reformat_vmgs = true,
514
515                x if x.starts_with("--") && x.len() > 2 => {
516                    if let Some(eq) = arg.find('=') {
517                        let (name, value) = arg.split_at(eq);
518                        // Don't forget to exclude the '=' itself.
519                        let value = &value[1..];
520                        Self::parse_value_arg(name, value, &mut pid, &mut vnc_port)?;
521                    } else {
522                        if let Some(value) = args.next() {
523                            Self::parse_value_arg(&arg, &value, &mut pid, &mut vnc_port)?;
524                        } else {
525                            bail!("Expected a value after argument {}", arg);
526                        }
527                    }
528                }
529                x => bail!("Unrecognized argument {}", x),
530            }
531        }
532
533        Ok(Self {
534            wait_for_start,
535            signal_vtl0_started,
536            reformat_vmgs,
537            pid,
538            vmbus_max_version,
539            vmbus_enable_mnf,
540            vmbus_force_confidential_external_memory,
541            vmbus_channel_unstick_delay_ms: vmbus_channel_unstick_delay_ms.unwrap_or(100),
542            cmdline_append,
543            vnc_port: vnc_port.unwrap_or(3),
544            framebuffer_gpa_base,
545            gdbstub,
546            gdbstub_port: gdbstub_port.unwrap_or(4),
547            vtl0_starts_paused,
548            serial_wait_for_rts,
549            force_load_vtl0_image,
550            nvme_vfio,
551            hide_isolation,
552            halt_on_guest_halt,
553            no_sidecar_hotplug,
554            nvme_keep_alive,
555            mana_keep_alive,
556            nvme_always_flr,
557            test_configuration,
558            disable_uefi_frontpage,
559            default_boot_always_attempt,
560            guest_state_lifetime,
561            guest_state_encryption_policy,
562            efi_diagnostics_log_level,
563            strict_encryption_policy,
564            attempt_ak_cert_callback,
565            enable_vpci_relay,
566            disable_proxy_redirect,
567            disable_lower_vtl_timer_virt,
568            config_timeout_in_seconds,
569            servicing_timeout_dump_collection_in_ms,
570        })
571    }
572
573    fn parse_value_arg(
574        name: &str,
575        value: &str,
576        pid: &mut Option<PathBuf>,
577        vnc_port: &mut Option<u32>,
578    ) -> anyhow::Result<()> {
579        match name {
580            "--pid" => *pid = Some(value.into()),
581            "--vnc-port" => {
582                *vnc_port = Some(
583                    value
584                        .parse()
585                        .context(format!("Error parsing VNC port {}", value))?,
586                )
587            }
588            x => bail!("Unrecognized argument {}", x),
589        }
590
591        Ok(())
592    }
593}