Skip to content

Commit 12a7b44

Browse files
test-runner: Drop the second serial device
The screenshot test sends a command to the host via the serial device, then waits for a reply before continuing. We used a second serial device just for this request and reply so that we could open the protocol in exclusive mode without breaking logging for tests after that. It turns out there's a simpler way to do this: we can use the same serial device as the one used for logs, and after we're finished use `connect_controller` to restore the connection between the serial device and the console output.
1 parent 733634b commit 12a7b44

File tree

2 files changed

+71
-67
lines changed

2 files changed

+71
-67
lines changed

uefi-test-runner/src/main.rs

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extern crate alloc;
1111
use alloc::string::ToString;
1212
use uefi::prelude::*;
1313
use uefi::proto::console::serial::Serial;
14+
use uefi::Result;
1415
use uefi_services::{print, println};
1516

1617
mod boot;
@@ -75,46 +76,71 @@ fn check_revision(rev: uefi::table::Revision) {
7576
);
7677
}
7778

79+
/// Send the `request` string to the host via the `serial` device, then
80+
/// wait up to 10 seconds to receive a reply. Returns an error if the
81+
/// reply is not `"OK\n"`.
82+
fn send_request_helper(serial: &mut Serial, request: &str) -> Result {
83+
// Set a 10 second timeout for the read and write operations.
84+
let mut io_mode = *serial.io_mode();
85+
io_mode.timeout = 10_000_000;
86+
serial.set_attributes(&io_mode)?;
87+
88+
// Send a screenshot request to the host.
89+
serial.write(request.as_bytes()).discard_errdata()?;
90+
91+
// Wait for the host's acknowledgement before moving forward.
92+
let mut reply = [0; 3];
93+
serial.read(&mut reply[..]).discard_errdata()?;
94+
95+
if reply == *b"OK\n" {
96+
Ok(())
97+
} else {
98+
Err(Status::ABORTED.into())
99+
}
100+
}
101+
78102
/// Ask the test runner to check the current screen output against a reference.
79103
fn check_screenshot(bt: &BootServices, name: &str) {
80-
let serial_handles = bt
81-
.find_handles::<Serial>()
82-
.expect("Failed to get serial handles");
83-
84-
// Use the second serial device handle. Opening a serial device
85-
// in exclusive mode breaks the connection between stdout and
86-
// the serial device, and we don't want that to happen to the
87-
// first serial device since it's used for log transport.
88-
let serial_handle = *serial_handles
89-
.get(1)
90-
.expect("Second serial device is missing");
91-
104+
let request = format!("SCREENSHOT: {name}\n");
105+
106+
let serial_handle = bt
107+
.get_handle_for_protocol::<Serial>()
108+
.expect("Failed to get serial handle");
109+
110+
// Open the serial protocol in exclusive mode.
111+
//
112+
// EDK2's [console splitter driver] periodically tries to sample
113+
// from console devices to see if keys are being pressed, which will
114+
// overwrite the timeout set below and potentially swallow the reply
115+
// from the host. Opening in exclusive mode stops the driver from
116+
// using this device. However, it also prevents logs from from going
117+
// to the serial device, so we have to restore the connection at the
118+
// end with `connect_controller`.
119+
//
120+
// [console splitter driver]: https://github.com/tianocore/edk2/blob/HEAD/MdeModulePkg/Universal/Console/ConSplitterDxe/ConSplitter.c
92121
let mut serial = bt
93122
.open_protocol_exclusive::<Serial>(serial_handle)
94123
.expect("Could not open serial protocol");
95124

96-
// Set a large timeout to avoid problems with CI
97-
let mut io_mode = *serial.io_mode();
98-
io_mode.timeout = 10_000_000;
99-
serial
100-
.set_attributes(&io_mode)
101-
.expect("Failed to configure serial port timeout");
102-
103-
// Send a screenshot request to the host
104-
serial
105-
.write(b"SCREENSHOT: ")
106-
.expect("Failed to send request");
107-
let name_bytes = name.as_bytes();
108-
serial.write(name_bytes).expect("Failed to send request");
109-
serial.write(b"\n").expect("Failed to send request");
110-
111-
// Wait for the host's acknowledgement before moving forward
112-
let mut reply = [0; 3];
113-
serial
114-
.read(&mut reply[..])
115-
.expect("Failed to read host reply");
116-
117-
assert_eq!(&reply[..], b"OK\n", "Unexpected screenshot request reply");
125+
// Send the request, but don't check the result yet so that first
126+
// we can reconnect the console output for the logger.
127+
let res = send_request_helper(&mut serial, &request);
128+
129+
// Release the serial device and reconnect all controllers to the
130+
// serial handle. This is necessary to restore the connection
131+
// between the console output device used for logging and the serial
132+
// device, which was broken when we opened the protocol in exclusive
133+
// mode above.
134+
drop(serial);
135+
let _ = bt.connect_controller(serial_handle, None, None, true);
136+
137+
if let Err(err) = res {
138+
panic!(
139+
"request failed: \"{}\": {:?}",
140+
request.trim_end(),
141+
err.status()
142+
);
143+
}
118144
}
119145

120146
fn shutdown(image: uefi::Handle, mut st: SystemTable<Boot>) -> ! {

xtask/src/qemu.rs

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use crate::{net, platform};
77
use anyhow::{bail, Context, Result};
88
use regex::bytes::Regex;
99
use serde_json::{json, Value};
10+
use std::env;
1011
use std::ffi::OsString;
1112
use std::io::{BufRead, BufReader, Read, Write};
1213
use std::path::{Path, PathBuf};
1314
use std::process::{Child, Command, Stdio};
14-
use std::{env, thread};
1515
use tempfile::TempDir;
1616

1717
#[derive(Clone, Copy, Debug)]
@@ -281,35 +281,22 @@ impl Io {
281281
}
282282
}
283283

284-
fn echo_filtered_stdout(mut child_io: Io) {
284+
fn process_qemu_io(mut monitor_io: Io, mut serial_io: Io, tmp_dir: &Path) -> Result<()> {
285285
// This regex is used to detect and strip ANSI escape codes. These
286286
// escapes are added by the console output protocol when writing to
287287
// the serial device.
288288
let ansi_escape = Regex::new(r"(\x9b|\x1b\[)[0-?]*[ -/]*[@-~]").expect("invalid regex");
289289

290-
while let Ok(line) = child_io.read_line() {
291-
let line = line.trim();
292-
let stripped = ansi_escape.replace_all(line.as_bytes(), &b""[..]);
293-
let stripped = String::from_utf8(stripped.into()).expect("line is not utf8");
294-
295-
// Print out the processed QEMU output for logging & inspection.
296-
println!("{stripped}");
297-
}
298-
}
299-
300-
fn process_qemu_io(mut monitor_io: Io, mut serial_io: Io, tmp_dir: &Path) -> Result<()> {
301290
// Execute the QEMU monitor handshake, doing basic sanity checks.
302291
assert!(monitor_io.read_line()?.starts_with(r#"{"QMP":"#));
303292
monitor_io.write_json(json!({"execute": "qmp_capabilities"}))?;
304293
assert_eq!(monitor_io.read_json()?, json!({"return": {}}));
305294

306295
while let Ok(line) = serial_io.read_line() {
307-
// Strip whitespace from the end. No need to strip ANSI escape
308-
// codes like in the stdout, because those escape codes are
309-
// inserted by the console output protocol, whereas the
310-
// "SCREENSHOT" line we are interested in is written via the
311-
// serial protocol.
296+
// Strip whitespace and ANSI escape codes.
312297
let line = line.trim_end();
298+
let line = ansi_escape.replace_all(line.as_bytes(), &b""[..]);
299+
let line = String::from_utf8(line.into()).expect("line is not utf8");
313300

314301
// If the app requests a screenshot, take it.
315302
if let Some(reference_name) = line.strip_prefix("SCREENSHOT: ") {
@@ -343,6 +330,8 @@ fn process_qemu_io(mut monitor_io: Io, mut serial_io: Io, tmp_dir: &Path) -> Res
343330
expected == actual,
344331
"screenshot does not match reference image"
345332
)
333+
} else {
334+
println!("{line}");
346335
}
347336
}
348337

@@ -510,16 +499,10 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
510499
cmd.arg(drive_arg);
511500

512501
let qemu_monitor_pipe = Pipe::new(tmp_dir, "qemu-monitor")?;
513-
let serial_pipe = Pipe::new(tmp_dir, "serial")?;
514-
515-
// Open two serial devices. The first one is connected to the host's
516-
// stdout, and serves to just transport logs. The second one is
517-
// connected to a pipe, and used to receive the SCREENSHOT command
518-
// and send the response. That second will also receive logs up
519-
// until the test runner opens the handle in exclusive mode, but we
520-
// can just read and ignore those lines.
502+
503+
// Open a serial device connected to stdio. This is used for
504+
// printing logs and to receive and reply to commands.
521505
cmd.args(["-serial", "stdio"]);
522-
cmd.args(["-serial", serial_pipe.qemu_arg()]);
523506

524507
// Map the QEMU monitor to a pair of named pipes
525508
cmd.args(["-qmp", qemu_monitor_pipe.qemu_arg()]);
@@ -543,21 +526,16 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
543526
let mut child = ChildWrapper(cmd.spawn().context("failed to launch qemu")?);
544527

545528
let monitor_io = qemu_monitor_pipe.open_io()?;
546-
let serial_io = serial_pipe.open_io()?;
547529
let child_io = Io::new(
548530
child.0.stdout.take().unwrap(),
549531
child.0.stdin.take().unwrap(),
550532
);
551533

552-
// Start a thread to process stdout from the child.
553-
let stdout_thread = thread::spawn(|| echo_filtered_stdout(child_io));
554-
555534
// Capture the result to check it, but first wait for the child to
556535
// exit.
557-
let res = process_qemu_io(monitor_io, serial_io, tmp_dir);
536+
let res = process_qemu_io(monitor_io, child_io, tmp_dir);
558537
let status = child.0.wait()?;
559538

560-
stdout_thread.join().expect("stdout thread panicked");
561539
if let Some(echo_service) = echo_service {
562540
echo_service.stop();
563541
}

0 commit comments

Comments
 (0)