1111import shutil
1212import subprocess as sp
1313import 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+
226253def 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+
395423def main ():
396424 'Runs the user-requested actions.'
397425
0 commit comments