Skip to content

Commit 7682eba

Browse files
nicholasbishopGabrielMajeri
authored andcommitted
Fix logs cut off after screenshot test
OVMF sets up the stdout handle such that writes go to all serial devices (as well as the visual console when not running headless). This is convenient since the logger writes to stdout, so connecting a serial device to QEMU's stdout writes logs from the test runner to the host. However, once a serial device is opened in exclusive mode it is disconnected from stdout. Since the test runner needs to open a serial device in exclusive mode, a single serial device isn't sufficient -- once the device is opened in exclusive mode it will stop receiving logs. So, open two serial devices for the QEMU test. The first one is connected to the host's stdout, and serves just to transport logs. The second one is connected to a pipe, and used to receive the SCREENSHOT command and send the response. That second will also receive logs up until the test runner opens the handle in exclusive mode, but we can just read and ignore those lines. Fixes rust-osdev#327
1 parent 5b92cb3 commit 7682eba

File tree

2 files changed

+73
-39
lines changed

2 files changed

+73
-39
lines changed

build.py

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import shutil
1212
import subprocess as sp
1313
import sys
14+
import threading
1415

1516
## Configurable settings
1617
# Path to workspace directory (which contains the top-level `Cargo.toml`)
@@ -223,6 +224,32 @@ def find_ovmf():
223224

224225
raise FileNotFoundError(f'OVMF files not found anywhere')
225226

227+
def echo_filtered_stdout(stdout):
228+
"""Print lines read from the QEMU process's stdout."""
229+
# This regex is used to detect and strip ANSI escape codes. These
230+
# escapes are added by the console output protocol when writing to
231+
# the serial device.
232+
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
233+
234+
for line in stdout:
235+
# Print out the processed QEMU output for logging & inspection.
236+
# Strip ending and trailing whitespace + ANSI escape codes
237+
# (This simplifies log analysis and keeps the terminal clean)
238+
print(ansi_escape.sub('', line.strip()))
239+
240+
class Pipe:
241+
def __init__(self, base_name):
242+
self.qemu_arg = f'pipe:{base_name}'
243+
self.input_path = f'{base_name}.in'
244+
self.output_path = f'{base_name}.out'
245+
246+
os.mkfifo(self.input_path)
247+
os.mkfifo(self.output_path)
248+
249+
def remove_files(self):
250+
os.remove(self.input_path)
251+
os.remove(self.output_path)
252+
226253
def run_qemu():
227254
'Runs the code in QEMU.'
228255

@@ -231,7 +258,9 @@ def run_qemu():
231258

232259
ovmf_code, ovmf_vars = ovmf_files(find_ovmf())
233260

234-
qemu_monitor_pipe = 'qemu-monitor'
261+
# Set up named pipes as communication channels with QEMU.
262+
qemu_monitor_pipe = Pipe('qemu-monitor')
263+
serial_pipe = Pipe('serial-pipe')
235264

236265
arch = SETTINGS['arch']
237266

@@ -284,12 +313,17 @@ def run_qemu():
284313
# Mount a local directory as a FAT partition.
285314
'-drive', f'format=raw,file=fat:rw:{esp_dir()}',
286315

287-
# Connect the serial port to the host. OVMF is kind enough to connect
288-
# the UEFI stdout and stdin to that port too.
316+
# Open two serial devices. The first one is connected to the
317+
# host's stdout, and serves to just transport logs. The second
318+
# one is connected to a pipe, and used to receive the SCREENSHOT
319+
# command and send the response. That second will also receive
320+
# logs up until the test runner opens the handle in exclusive
321+
# mode, but we can just read and ignore those lines.
289322
'-serial', 'stdio',
323+
'-serial', serial_pipe.qemu_arg,
290324

291325
# Map the QEMU monitor to a pair of named pipes
292-
'-qmp', f'pipe:{qemu_monitor_pipe}',
326+
'-qmp', qemu_monitor_pipe.qemu_arg,
293327
])
294328

295329
# For now these only work on x86_64
@@ -317,43 +351,35 @@ def run_qemu():
317351
if SETTINGS['verbose']:
318352
print(' '.join(cmd))
319353

320-
# This regex can be used to detect and strip ANSI escape codes when
321-
# analyzing the output of the test runner.
322-
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
323-
324-
# Setup named pipes as a communication channel with QEMU's monitor
325-
monitor_input_path = f'{qemu_monitor_pipe}.in'
326-
os.mkfifo(monitor_input_path)
327-
monitor_output_path = f'{qemu_monitor_pipe}.out'
328-
os.mkfifo(monitor_output_path)
329-
330354
# Start QEMU
331-
qemu = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, universal_newlines=True)
355+
qemu = sp.Popen(cmd, stdout=sp.PIPE, universal_newlines=True)
356+
thread = threading.Thread(target=echo_filtered_stdout, args=(qemu.stdout,))
357+
thread.start()
332358
try:
333359
# Connect to the QEMU monitor
334-
with open(monitor_input_path, mode='w') as monitor_input, \
335-
open(monitor_output_path, mode='r') as monitor_output:
360+
with open(qemu_monitor_pipe.input_path, mode='w') as monitor_input, \
361+
open(qemu_monitor_pipe.output_path, mode='r') as monitor_output, \
362+
open(serial_pipe.input_path, mode='w') as serial_input, \
363+
open(serial_pipe.output_path, mode='r') as serial_output:
336364
# Execute the QEMU monitor handshake, doing basic sanity checks
337365
assert monitor_output.readline().startswith('{"QMP":')
338366
print('{"execute": "qmp_capabilities"}', file=monitor_input, flush=True)
339367
assert monitor_output.readline() == '{"return": {}}\n'
340368

341-
# Iterate over stdout...
342-
for line in qemu.stdout:
343-
# Strip ending and trailing whitespace + ANSI escape codes
344-
# (This simplifies log analysis and keeps the terminal clean)
345-
stripped = ansi_escape.sub('', line.strip())
346-
347-
# Skip lines which contain nothing else
348-
if not stripped:
349-
continue
350-
351-
# Print out the processed QEMU output for logging & inspection
352-
print(stripped)
369+
# Iterate over the second serial device's output...
370+
for line in serial_output:
371+
# Strip whitespace from the end. No need to strip ANSI
372+
# escape codes like in the stdout, because those escape
373+
# codes are inserted by the console output protocol,
374+
# whereas the "SCREENSHOT" line we are interested in is
375+
# written via the serial protocol.
376+
line = line.rstrip()
353377

354378
# If the app requests a screenshot, take it
355-
if stripped.startswith("SCREENSHOT: "):
356-
reference_name = stripped[12:]
379+
if line.startswith("SCREENSHOT: "):
380+
print(line)
381+
382+
reference_name = line[12:]
357383

358384
# Ask QEMU to take a screenshot
359385
monitor_command = '{"execute": "screendump", "arguments": {"filename": "screenshot.ppm"}}'
@@ -366,7 +392,7 @@ def run_qemu():
366392
assert reply == {"return": {}}
367393

368394
# Tell the VM that the screenshot was taken
369-
print('OK', file=qemu.stdin, flush=True)
395+
print('OK', file=serial_input, flush=True)
370396

371397
# Compare screenshot to the reference file specified by the user
372398
# TODO: Add an operating mode where the reference is created if it doesn't exist
@@ -384,14 +410,16 @@ def run_qemu():
384410
qemu.kill()
385411
status = -1
386412

387-
# Delete the monitor pipes
388-
os.remove(monitor_input_path)
389-
os.remove(monitor_output_path)
413+
# Delete the pipes
414+
qemu_monitor_pipe.remove_files()
415+
serial_pipe.remove_files()
390416

391417
# Throw an exception if QEMU failed
392418
if status != 0 and status != 3:
393419
raise sp.CalledProcessError(cmd=cmd, returncode=status)
394420

421+
thread.join()
422+
395423
def main():
396424
'Runs the user-requested actions.'
397425

uefi-test-runner/src/main.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,17 @@ fn check_revision(rev: uefi::table::Revision) {
7777
/// inspection of the output.
7878
fn check_screenshot(image: Handle, bt: &BootServices, name: &str) {
7979
if cfg!(feature = "qemu") {
80-
let serial_handle = *bt
80+
let serial_handles = bt
8181
.find_handles::<Serial>()
82-
.expect_success("Failed to get serial handles")
83-
.first()
84-
.expect("Could not find serial port");
82+
.expect_success("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");
8591

8692
let serial = bt
8793
.open_protocol::<Serial>(

0 commit comments

Comments
 (0)