Skip to content

Commit 4ce2ed0

Browse files
committed
qemu: add a helper script to assist with qemu debug
Utility to help with developer, CI, AI usage. Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
1 parent 82e5e63 commit 4ce2ed0

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

scripts/sof-qemu-run.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
# Copyright(c) 2024 Intel Corporation. All rights reserved.
4+
"""
5+
sof-qemu-run.py - Automated QEMU test runner and crash analyzer
6+
7+
This script runs `west -v build -t run` for Zephyr, monitors the output, and
8+
waits for 2 seconds after the last log event. If a Zephyr/QEMU crash occurs,
9+
it decodes it using `sof-crash-decode.py`. If no crash occurred, it enters
10+
the QEMU monitor (Ctrl-A c), issues `info registers`, and passes the output
11+
to the crash decoder.
12+
"""
13+
14+
import sys
15+
import pexpect
16+
import subprocess
17+
import argparse
18+
import os
19+
import sys
20+
import re
21+
22+
# ANSI Color Codes
23+
COLOR_RED = "\x1b[31;1m"
24+
COLOR_YELLOW = "\x1b[33;1m"
25+
COLOR_RESET = "\x1b[0m"
26+
27+
def colorize_line(line):
28+
"""Colorize significant Zephyr log items."""
29+
if "<err>" in line or "[ERR]" in line or "Fatal fault" in line or "Oops" in line:
30+
return COLOR_RED + line + COLOR_RESET
31+
elif "<wrn>" in line or "[WRN]" in line:
32+
return COLOR_YELLOW + line + COLOR_RESET
33+
return line
34+
35+
def check_for_crash(output):
36+
"""
37+
Check if the collected output indicates a crash.
38+
Look for known Zephyr/Xtensa oops or exceptions, or QEMU crash register dumps.
39+
"""
40+
crash_keywords = [
41+
"Fatal fault",
42+
"Oops",
43+
"ZEPHYR FATAL ERROR",
44+
"Exception",
45+
"PC=", # QEMU PC output format
46+
"EXCCAUSE=",
47+
"Backtrace:"
48+
]
49+
for keyword in crash_keywords:
50+
if keyword in output:
51+
return True
52+
return False
53+
54+
def run_sof_crash_decode(build_dir, dump_content):
55+
"""
56+
Invokes sof-crash-decode.py and feeds it the crash dump via stdin.
57+
"""
58+
# Find sof-crash-decode.py in the same directory as this script, or fallback to relative path
59+
decoder_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sof-crash-decode.py")
60+
if not os.path.isfile(decoder_script):
61+
decoder_script = "sof-crash-decode.py"
62+
63+
print("\n====================================")
64+
print("Running sof-crash-decode.py Analysis")
65+
print("====================================\n")
66+
67+
cmd = [sys.executable, decoder_script, "--build-dir", build_dir]
68+
69+
try:
70+
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
71+
proc.communicate(input=dump_content.encode('utf-8'))
72+
except Exception as e:
73+
print(f"Failed to run sof-crash-decode.py: {e}")
74+
75+
def main():
76+
parser = argparse.ArgumentParser(description="Run QEMU via west and automatically decode crashes.")
77+
parser.add_argument("--build-dir", default="build", help="Path to the build directory containing zephyr.elf, linker.cmd, etc. Defaults to 'build'.")
78+
parser.add_argument("--log-file", default="qemu-run.log", help="Path to save the QEMU output log. Defaults to 'qemu-run.log'.")
79+
args = parser.parse_args()
80+
81+
# Make absolute path just in case
82+
build_dir = os.path.abspath(args.build_dir)
83+
84+
print(f"Starting QEMU test runner. Monitoring for crashes (Build Dir: {args.build_dir})...")
85+
86+
# We will use pexpect to spawn the west command to get PTY features
87+
import shutil
88+
west_path = shutil.which("west")
89+
if not west_path:
90+
print("[sof-qemu-run] Error: 'west' command not found in PATH.")
91+
print("Please ensure you have sourced the Zephyr environment (e.g., source zephyr-env.sh).")
92+
sys.exit(1)
93+
94+
child = pexpect.spawn(west_path, ["-v", "build", "-t", "run"], encoding='utf-8')
95+
96+
# We will accumulate output to check for crashes
97+
full_output = ""
98+
last_output = ""
99+
100+
with open(args.log_file, "w") as log_file:
101+
try:
102+
# Loop reading output until EOF or a timeout occurs
103+
qemu_started = False
104+
while True:
105+
try:
106+
# Read character by character or line by line
107+
# Pexpect's readline() doesn't consistently trigger timeout on idle
108+
# We can use read_nonblocking and an explicit exceptTIMEOUT
109+
index = child.expect([r'\r\n', pexpect.TIMEOUT, pexpect.EOF], timeout=2)
110+
if index == 0:
111+
line = child.before + '\n'
112+
# Strip ANSI escape codes from output to write raw text to log file
113+
clean_line = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', line)
114+
log_file.write(clean_line)
115+
log_file.flush()
116+
117+
colored_line = colorize_line(line)
118+
sys.stdout.write(colored_line)
119+
sys.stdout.flush()
120+
121+
full_output += line
122+
if not qemu_started and ("Booting Zephyr OS" in line or "To exit from QEMU" in line or "qemu-system-" in line):
123+
qemu_started = True
124+
elif index == 1: # TIMEOUT
125+
if qemu_started or check_for_crash(full_output):
126+
print("\n\n[sof-qemu-run] 2 seconds passed since last log event. Checking status...")
127+
break
128+
else:
129+
# Still building or loading, continue waiting
130+
pass
131+
elif index == 2: # EOF
132+
print("\n\n[sof-qemu-run] QEMU process terminated.")
133+
break
134+
135+
except pexpect.TIMEOUT:
136+
if qemu_started or check_for_crash(full_output):
137+
print("\n\n[sof-qemu-run] 2 seconds passed since last log event. Checking status...")
138+
break
139+
else:
140+
# Still building or loading, continue waiting
141+
pass
142+
except pexpect.EOF:
143+
print("\n\n[sof-qemu-run] QEMU process terminated.")
144+
break
145+
146+
except KeyboardInterrupt:
147+
print("\n[sof-qemu-run] Interrupted by user.")
148+
# Proceed with what we have
149+
150+
crashed = check_for_crash(full_output)
151+
152+
if crashed:
153+
print("\n[sof-qemu-run] Detected crash signature in standard output!")
154+
# Stop QEMU if it's still running
155+
if child.isalive():
156+
child.sendline("\x01x") # Ctrl-A x to quit qemu
157+
child.close(force=True)
158+
159+
run_sof_crash_decode(build_dir, full_output)
160+
else:
161+
print("\n[sof-qemu-run] No crash detected. Interacting with QEMU Monitor to grab registers...")
162+
163+
# We need to send Ctrl-A c to enter the monitor
164+
if child.isalive():
165+
child.send("\x01c") # Ctrl-A c
166+
try:
167+
# Wait for (qemu) prompt
168+
child.expect(r"\(qemu\)", timeout=5)
169+
# Send "info registers"
170+
child.sendline("info registers")
171+
# Wait for the next prompt
172+
child.expect(r"\(qemu\)", timeout=5)
173+
174+
info_regs_output = child.before
175+
print("\n[sof-qemu-run] Successfully extracted registers from QEMU monitor.\n")
176+
177+
# Quit qemu safely
178+
child.sendline("quit")
179+
child.expect(pexpect.EOF, timeout=2)
180+
child.close()
181+
182+
# Run the decoder on the intercepted register output
183+
run_sof_crash_decode(build_dir, info_regs_output)
184+
except pexpect.TIMEOUT:
185+
print("\n[sof-qemu-run] Timed out waiting for QEMU monitor. Is it running?")
186+
child.close(force=True)
187+
except pexpect.EOF:
188+
print("\n[sof-qemu-run] QEMU terminated before we could run monitor commands.")
189+
else:
190+
print("\n[sof-qemu-run] Process is no longer alive, cannot extract registers.")
191+
192+
if __name__ == "__main__":
193+
main()

scripts/sof-qemu-run.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
# Copyright(c) 2024 Intel Corporation. All rights reserved.
4+
5+
# Define the build directory from the first argument (or default)
6+
BUILD_DIR="${1:-build}"
7+
8+
# Find and source the zephyr environment script, typically via the sof-venv wrapper
9+
# or directly if running in a known zephyrproject layout.
10+
# We will use the existing helper sof-venv.sh to get the right environment.
11+
12+
# Get the directory of this script
13+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14+
SOF_WORKSPACE="$(dirname "$(dirname "$SCRIPT_DIR")")"
15+
16+
# check if Zephyr environment is set up
17+
if [ ! -z "$SOF_WORKSPACE" ]; then
18+
VENV_DIR="$SOF_WORKSPACE/.venv"
19+
echo "Using SOF environment at $SOF_WORKSPACE"
20+
else
21+
# default to the local workspace
22+
VENV_DIR="${SOF_WORKSPACE}/.venv"
23+
echo "Using default SOF environment at $VENV_DIR"
24+
fi
25+
26+
# start the virtual environment
27+
source ${VENV_DIR}/bin/activate
28+
29+
# Execute the QEMU runner from within the correct build directory
30+
cd "${BUILD_DIR}" || exit 1
31+
32+
# Finally run the python script which will now correctly inherit 'west' from the sourced environment.
33+
python3 "${SCRIPT_DIR}/sof-qemu-run.py" --build-dir "${BUILD_DIR}"
34+

0 commit comments

Comments
 (0)