Skip to content

Commit 5aa8b02

Browse files
ppennaCopilot
andcommitted
microVM: drain the boot console's terminal output at guest exit
A guest-requested exit drained portb's output and its host relay, and then OpenVMM terminated the process. When --virtio-console console attaches the boot console to the terminal, its relay copies the anonymous pair to stdout on a thread of its own, which nothing awaited, so the process could end before the relay wrote the output that the guest had printed last. The guest's virtio-console driver waits until the device completes each of its writes, and the device writes each descriptor to the pair before completing it, so the output was already in the pair; only the relay, waiting for the reactor to report it, was behind. microsoft/nvx#549 moves NVX's interactive and test consoles to the boot console, where a benchmark lost its completion marker at nvx-exit. The boot console's relay now takes drain requests. It holds a non-blocking duplicate of its end of the pair, from which it reads what the pair holds without waiting for the reactor, and writes and flushes it before it answers. MicrovmOutputDrain asks it at a guest-requested exit, after portb, within the same five-second deadline, and before a guest-requested capture, whose source process ends once the snapshot commits. Without a boot console on the terminal, nothing changes. Tests: the exit drain writes the output that the reactor has yet to report, which it lost before this change; the capture flush writes the pending output and the relay keeps copying; a failed write fails the drain; and on both Linux sockets and Windows pipes, the duplicate reads a pair until it is empty without waiting for more. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent eda13d8 commit 5aa8b02

7 files changed

Lines changed: 408 additions & 37 deletions

File tree

‎Guide/src/reference/openvmm/management/cli.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,8 @@ Serial devices can be configured to appear as different devices inside the guest
10231023
restore command must explicitly resupply the matching client attachment.
10241024
* `console`: require the restore caller to supply the same inherited terminal
10251025
attachment. Portb recovery output moves to stderr while the terminal is
1026-
attached to `hvc1`.
1026+
attached to `hvc1`. A guest-requested exit writes the boot console output
1027+
that the device accepted before OpenVMM exits.
10271028
* `none`: keep the device present and explicitly discard guest TX while
10281029
disconnected.
10291030

@@ -1144,13 +1145,17 @@ a supervisor can tell the exit reasons apart.
11441145
For microVMs, a process-status shutdown through port `0x604`, or a power event
11451146
configured to exit, drains previously accepted portb console output before the
11461147
process exits. This includes the host stdout/stderr relay, not just the device's
1147-
transmit buffer. Draining is bounded to five seconds; an output error or timeout
1148+
transmit buffer. When `--virtio-console console` attaches the boot console to
1149+
the terminal, the drain also writes the boot console output that the device
1150+
accepted before the shutdown, which the host relay has yet to write to stdout.
1151+
Draining is bounded to five seconds; an output error or timeout
11481152
is reported as a failure rather than a successful exit with truncated output.
11491153
In RPC-server mode, a drain failure also fails pending `WaitVm` requests and
11501154
terminates the server with an error instead of leaving it uninitialized.
11511155
A guest-requested snapshot capture instead flushes accepted portb output to its
1152-
endpoint before quiescing, without closing it because the capture can still roll
1153-
back; see `--snapshot-destination`.
1156+
endpoint, and the boot console's terminal output, before quiescing, without
1157+
closing them because the capture can still roll back; see
1158+
`--snapshot-destination`.
11541159

11551160
* `--crash-dump-path <PATH>`: when the guest triple-faults, write a
11561161
WinDbg-compatible `.vmrs` dump of the VM's processor state and guest memory to

‎openvmm/openvmm_entry/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ workspace = true
126126
features = [
127127
"Win32_Foundation",
128128
"Win32_Storage_FileSystem",
129+
"Win32_System_Pipes",
129130
]
130131

131132
[build-dependencies]

‎openvmm/openvmm_entry/src/microvm/config.rs‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use super::network::microvm_network_endpoint;
2222
use super::network::microvm_restore_network;
2323
use super::output::MicrovmOutputDrain;
2424
use super::output::OutputCompletion;
25+
use super::output::RelayDrain;
26+
use super::output::spawn_drainable_output;
2527
use super::output::spawn_output;
2628
use super::restore::fresh_microvm_generation_id;
2729
use crate::ConsoleState;
@@ -78,6 +80,8 @@ pub(crate) struct MicrovmConfigBuilder<'a> {
7880
control_broker_config: Option<VirtioControlConsoleBrokerConfig>,
7981
control_console_backend: Option<Resource<SerialBackendHandle>>,
8082
portb: Option<(Resource<SerialBackendHandle>, OutputCompletion)>,
83+
/// The drain of the boot console's relay to the terminal.
84+
console_relay: Option<RelayDrain>,
8185
resources: MicrovmResources,
8286
}
8387

@@ -224,6 +228,7 @@ impl<'a> MicrovmConfigBuilder<'a> {
224228
control_broker_config,
225229
control_console_backend: None,
226230
portb: None,
231+
console_relay: None,
227232
resources,
228233
})
229234
}
@@ -264,13 +269,14 @@ impl<'a> MicrovmConfigBuilder<'a> {
264269
} else {
265270
SerialConfigCli::Console
266271
};
267-
self.portb = Some(setup_host_console(
272+
let (backend, output, _) = setup_host_console(
268273
"microvm-portb",
269274
backend,
270275
"hvc0",
271276
console_state,
272277
serial_driver,
273-
)?);
278+
)?;
279+
self.portb = Some((backend, output));
274280
Ok(())
275281
}
276282

@@ -320,13 +326,14 @@ impl<'a> MicrovmConfigBuilder<'a> {
320326
)?)
321327
}
322328
SerialConfigCli::Console => {
323-
let (backend, _) = setup_host_console(
329+
let (backend, _, relay) = setup_host_console(
324330
"virtio-console",
325331
SerialConfigCli::Console,
326332
"hvc1",
327333
console_state,
328334
serial_driver,
329335
)?;
336+
self.console_relay = relay;
330337
Some(backend)
331338
}
332339
SerialConfigCli::None => Some(DisconnectedSerialBackendHandle.into_resource()),
@@ -407,7 +414,8 @@ impl<'a> MicrovmConfigBuilder<'a> {
407414
return Ok(());
408415
};
409416
let opt = self.opt;
410-
let (drain, output_drain) = MicrovmOutputDrain::new(Some(output_completion));
417+
let (drain, output_drain) =
418+
MicrovmOutputDrain::new(Some(output_completion), self.console_relay.take());
411419
self.resources.output_drain = Some(drain);
412420
let (generation_id, time_abi_restore) = if self.restore.machine_contract.is_some() {
413421
let network = if self.restore.network_settings {
@@ -856,32 +864,39 @@ fn aggregate_child_kind(
856864
}
857865

858866
/// Connects a portb or boot virtio-console endpoint to the host console or
859-
/// stderr, returning its backend and output-relay completion.
867+
/// stderr, returning its backend, its output-relay completion, and, for the
868+
/// console, the drain of its relay.
860869
fn setup_host_console(
861870
name: &str,
862871
backend: SerialConfigCli,
863872
device: &'static str,
864873
console_state: &RefCell<Option<ConsoleState<'static>>>,
865874
serial_driver: &DefaultDriver,
866-
) -> anyhow::Result<(Resource<SerialBackendHandle>, OutputCompletion)> {
875+
) -> anyhow::Result<(
876+
Resource<SerialBackendHandle>,
877+
OutputCompletion,
878+
Option<RelayDrain>,
879+
)> {
867880
Ok(match backend {
868881
SerialConfigCli::Console => {
869882
if let Some(console_state) = console_state.borrow().as_ref() {
870883
bail!("console already set by {}", console_state.device);
871884
}
872-
let (config, serial) = serial_io::anonymous_serial_pair(serial_driver)?;
885+
let (config, serial, pending) =
886+
serial_io::anonymous_serial_pair_with_pending(serial_driver)?;
873887
let (serial_read, serial_write) = AsyncReadExt::split(serial);
874888
*console_state.borrow_mut() = Some(ConsoleState {
875889
device,
876890
input: Box::new(serial_write),
877891
});
878-
let completed = spawn_output(name, serial_read, term::raw_stdout())?;
879-
(config, completed)
892+
let (completed, relay) =
893+
spawn_drainable_output(name, serial_read, pending, term::raw_stdout())?;
894+
(config, completed, Some(relay))
880895
}
881896
SerialConfigCli::Stderr => {
882897
let (config, serial) = serial_io::anonymous_serial_pair(serial_driver)?;
883898
let completed = spawn_output(name, serial, term::raw_stderr())?;
884-
(config, completed)
899+
(config, completed, None)
885900
}
886901
_ => unreachable!("microVM host consoles use the console or stderr"),
887902
})

0 commit comments

Comments
 (0)