Skip to content

nvx-rs: run the guest programs' main through one shared function - #679

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
nvx-rs-quality/guest-run-main
Oct 21, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
nvx-rs-quality/guest-run-main

Conversation

@ppenna

Copy link
Copy Markdown
Contributor

Summary

Eight of the nine guest programs repeated the same main: restore the SIGPIPE disposition that the process inherited, collect the process's arguments, run the program, and turn its Result<(), CommandError> into the exit code that ExitStatus selects. Four copies were identical nine-line blocks, and the other four differed only in the devices and output streams that they pass on.

This adds status::run_main, which holds that sequence once, next to the ExitStatus and CommandError that it maps. Each program's main now hands it a closure that calls the program's run with what is the program's own, so main states only that, and no program can skip a step or map its outcome another way.

Changes

  • nvx-rs/src/guest/status.rs: adds run_main(run: impl FnOnce(&[CString]) -> Result<(), CommandError>) -> ExitCode, which calls sys::restore_sigpipe(), passes sys::args() to run, and returns ExitStatus::from(outcome).into(). The module documentation names it.
  • Every site that now shares it:
    • console_pending.rs, ingress/mod.rs, time/mod.rs, and time_probe/mod.rs: their identical main bodies become run_main(|args| run(args, &mut sys::Stdout::new(), &mut io::stderr())).
    • reseed.rs: the same, with c"/dev/random" first.
    • exit.rs (PORT_DEVICE), device_io.rs (&mut sys::DirectStdout), and mmio_write.rs (c"/dev/mem" and &mut io::stderr()): their one-line bodies call run_main the same way.
  • The test modules of console_pending, device_io, exit, mmio_write, reseed, and time_probe convert outcomes with ExitStatus themselves, so they import it now that their parent modules no longer use it.

git diff --shortstat: 9 files changed, 41 insertions(+), 51 deletions(-).

Behavior

Unchanged:

  • run_main makes the same calls in the same order as each main did: sys::restore_sigpipe(), then sys::args(), then the program's run with the same arguments and streams, then the same From<Result<(), CommandError>> for ExitStatus and From<ExitStatus> for ExitCode conversions. Command lines, exit statuses, and output are those of BASE.
  • The only difference is when the output adapters drop: sys::Stdout, sys::DirectStdout, and io::Stderr now drop when the closure returns, before the outcome converts, rather than at the end of main's statement. None of them buffers anything or acts when dropped (sys::Stdout only records whether standard output is a terminal, and standard error is unbuffered), so nothing observable moves.
  • The nvx-rs CLI is built with --features cli, which does not compile the guest module, so it is unaffected.

tests/guest_tools.rs runs each built program and covers the entry point: usage errors exit with 2 and failures with 1, and nvx-console-pending and nvx-device-io end on SIGPIPE at a closed pipe unless the disposition was ignored across exec. A unit test of run_main would reset the test process's own SIGPIPE disposition, so it has none; those integration tests cover it.

Follow-ups

  • nvx-managed-agent's main in guest/managed_agent/mod.rs can call run_main too once nvx-rs: add caller-owned guest cgroup and child-status APIs #655, which changes that file, has merged. Its main creates RealSystem before it reads the arguments, so that change should confirm that creating it inside the closure is harmless.
  • The guest programs' test modules each build their [program, arguments...] vector of CStrings with a helper of their own (args in device_io, exit, time, and time_probe, and inline in console_pending, mmio_write, and reseed's fixture). One test helper beside sys::args could replace them; guest/sys/mod.rs is changed by open pull requests (nvx-rs: add caller-owned guest cgroup and child-status APIs #655 and nvx-rs: hold the guest port permission for one transaction at a time #662), so this one leaves them.
  • nvx-time-probe's latency, warp, and clock name themselves in 11 calls of bad_option(b"COMMAND", ..., err); Arguments could carry its command name and report an invalid option or value itself.

Validation

  • BASE: 916a61f0297900db13d7d9d04d170f01ef9ed71f, origin/dev when the run started and still when this pull request was opened: a performance-baseline update after the merge of nvx-rs: run the control session on the shared NVXS session's host role #641. No open pull request to dev changes the files of this one.

  • TREE: f9932126516d0fa6cf0cac0ebc940cee07a18a9e, the tree of this commit. Every host applied the patch to BASE and checked that git write-tree equals TREE.

  • Release on every host: v0.1.0-dev.dad3e41d7f05, from the merge of nvx-rs: run the control session on the shared NVXS session's host role #641, the nearest first-parent ancestor of BASE that had packages, two commits earlier. Its OpenVMM, d0bdbed8f6e3b86271277dc22658109e2e633b2d, is the pinned revision.

  • download installed the release's nvx-rs, whose provenance names BASE's nvx-rs sources. TREE changes only the guest module, which the CLI's cli feature does not compile. Each host built the openvmm_e2e and managed_lifecycle test binaries after its other Cargo work, and its hypervisor test logs show no compilation.

  • Guest artifacts were rebuilt from TREE on prometheus30, whose user namespaces let Alpine's apk run its scripts:

    • build-guest-binaries --output;
    • test-guest-binaries --binaries, where 34 tests passed;
    • the Alpine build-initramfs with NVX_GUEST_BINARIES.

    initramfs.cpio.gz (SHA-256 e4fb1eb5ccff8de2584849992bbcaca7d18ea08db6b27a19bfbce588443fa749), its packages manifest, and its provenance went through this machine to the other two hosts, where the SHA-256 digests matched.

  • Local checks on Windows with Rust 1.95.0:

    • rustfmt;
    • Clippy with -D warnings, with all targets and features, for Windows, x86_64-unknown-linux-gnu, and x86_64-unknown-linux-musl, and on the library with only guest for musl;
    • the steps that no host could run, because the hosts lack the toolchain or target: the Windows MSRV (1.89) checks of nvx, aci_edge_sandboxes, and the runtime adapter's protocol-only and host-only configurations; the macOS (aarch64-apple-darwin) checks of nvx and aci_edge_sandboxes; and the MSRV check of the runtime adapter's guest-only configuration, which this machine ran as the action defines it, starting with its rustup target add, which gave the local 1.89 the musl target; I removed the target again afterwards.

    The change adds, moves, and deletes no file and changes no Markdown, so the CLI tests of validate-nvx and the link check had nothing to cover.

  • One deviation, now undone: my first probe of prometheus28 listed the targets of 1.89, which that host did not have, and rustup installed it to answer. I uninstalled it at once, before any gate ran, so the host keeps only its own toolchains, and its gates skipped the MSRV steps, which ran on this machine.

Host profile Backend Host type check-nvx-rs and check-aci-edge-sandboxes without a hypervisor Guest artifacts test-aci-edge-sandboxes test-microvm, default suite
prometheus32 kvm baremetal Pass: every Linux step, including MSRV 1.89, check-guest-binaries (398 tests), 754 nvx tests with all features, guest_tools among them, and 362 with the default ones, artifact bundling (15), the runtime adapter's protocol-only, host-only, and musl guest-only configurations (13, 14, and 21 tests), 158 and 85 aci_edge_sandboxes tests, and agent bundling (4). The macOS checks and the guest-only MSRV check ran locally Copied in, digests match Pass (18/18) Pass (41 scenario runs)
prometheus30 mshv baremetal None; prometheus32 and prometheus28 ran the gates Built and tested here (34 tests) Pass (18/18) Pass (41 scenario runs)
prometheus28 whp baremetal Pass: every Windows step, including 429 nvx tests with all features and 358 with the default ones, artifact bundling (15), the runtime adapter's protocol-only and host-only configurations (13 and 14 tests), 158 and 85 aci_edge_sandboxes tests, and agent bundling (4). The MSRV checks ran locally Copied in, digests match Pass (18/18) Pass (41 scenario runs)

Every gate and test passed at its first attempt. The Linux gates ran in the foreground of their SSH sessions with no signal ignored (SigIgn: 0), so the CLI's Ctrl-C tests ran as intended. Each suite's managed-lifecycle scenario ran the managed_lifecycle test with Cargo, which passed. OpenVMM warned of a host CPU-profile fallback only in the cpu-profile-fallback scenario, which forces one.

The time-ABI evidence lines counted no capture or restore check over its CPU-time budget on any host, and no boot check over budget on prometheus32 or prometheus28. On prometheus30, one of 58 boot checks, in the capture of the 4-vCPU smp-snapshot scenario, used 6,114 µs of CPU against its 6,000 µs budget; CI reports these counts and gates on none of them. Rerunning that case on the same host measured 4,089 µs, and the change adds no work to nvx-time's checks: its main makes the same calls as before.

Left to CI:

  • the OpenVMM unit and VMM suites, test-openvmm-unit and test-openvmm;
  • the microVM scenarios that run only when named: smp-lapic, managed-exec-config, and sandbox-lifecycle;
  • the debug-kernel scenarios, the Ubuntu and Azure Linux guests, and guest determinism;
  • the benchmarks;
  • the virtual-machine runners;
  • check-quality and validate-nvx, whose inputs this change does not touch.

Eight of the nine guest programs repeated the same main: restore the
SIGPIPE disposition that the process inherited, collect the process's
arguments, run the program, and turn its outcome into the exit code
that ExitStatus selects. Four copies were identical, and the others
differed only in the devices and output streams that they pass on.

status::run_main now holds that sequence, next to the ExitStatus and
CommandError that it maps. Each program's main hands it a closure that
calls the program's run function with what is the program's own, such
as /dev/port, /dev/mem, or /dev/random and its output streams, so that
main states only that, and no program can skip a step or map its
outcome another way. The test modules that convert outcomes themselves
now import ExitStatus, which their parents no longer use.

Behavior is unchanged: run_main makes the same calls in the same
order, restore_sigpipe, then args, then the program's run function
with the same arguments, and converts the outcome with the same From
implementations. The output adapters that the closures create now drop
before the outcome converts, but none of them buffers anything or acts
when dropped. nvx-managed-agent keeps its main, because an open pull
request changes its module.

The guest_tools integration tests run each built program and cover
the entry point: usage errors exit with 2, failures with 1, and
nvx-console-pending and nvx-device-io end on SIGPIPE at a closed pipe
unless the disposition was ignored across exec.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused extraction preserves call order, arguments, and exit-code mapping, with no unresolved findings.

No open issues 🎉

What changed in this PR

Centralizes startup and exit-code handling for eight NVX guest programs without changing their behavior.

Changes:

  • Adds status::run_main to restore SIGPIPE, collect arguments, and map outcomes to exit codes.
  • Replaces duplicated entry-point logic while preserving each program’s devices and streams.
  • Moves ExitStatus imports into tests where needed.
File Description
nvx-rs/​src/​guest/​time/​mod.rs Delegates startup to run_main.
nvx-rs/​src/​guest/​time_probe/​mod.rs Uses shared startup and updates test imports.
nvx-rs/​src/​guest/​status.rs Defines and documents run_main.
nvx-rs/​src/​guest/​reseed.rs Uses shared startup, retaining /dev/random.
nvx-rs/​src/​guest/​mmio_write.rs Uses shared startup, retaining /dev/mem.
nvx-rs/​src/​guest/​ingress/​mod.rs Delegates startup to run_main.
nvx-rs/​src/​guest/​exit.rs Uses shared startup, retaining PORT_DEVICE.
nvx-rs/​src/​guest/​device_io.rs Uses shared startup, retaining direct output.
nvx-rs/​src/​guest/​console_pending.rs Uses shared startup and updates test imports.

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants