mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-14 12:17:11 +01:00
debug(scripts): decoder typing & refactoring
- typing info to clean-up path <-> string and arguments mixups - argparse.FileType deprecated, use another built-in 'fileinput' that allows similar behaviour - tool selection always uses shutil.which result for a full cmd path - tool exec errors treated as script errors
This commit is contained in:
@@ -5,12 +5,16 @@
|
||||
# Stack line detection from https://github.com/platformio/platform-espressif8266/ monitor exception filter by Vojtěch Boček (@Tasssadar)
|
||||
# - https://github.com/platformio/platform-espressif8266/commits?author=Tasssadar
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import shutil
|
||||
import pathlib
|
||||
import fileinput
|
||||
|
||||
import typing
|
||||
from typing import Callable, Iterator, TypedDict, Literal
|
||||
|
||||
# https://github.com/me-no-dev/EspExceptionDecoder/blob/349d17e4c9896306e2c00b4932be3ba510cad208/src/EspExceptionDecoder.java#L59-L90
|
||||
EXCEPTION_CODES = (
|
||||
@@ -54,35 +58,52 @@ EXCEPTION_CODES = (
|
||||
)
|
||||
|
||||
|
||||
def run_command(cmd: list[str]) -> list[str]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, check=True, universal_newlines=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.stdout:
|
||||
print(e.stdout, file=sys.stdout)
|
||||
if e.stderr:
|
||||
print(e.stderr, file=sys.stderr)
|
||||
e.cmd = e.cmd[0]
|
||||
sys.exit(e.returncode)
|
||||
|
||||
return result.stdout.splitlines()
|
||||
|
||||
|
||||
# similar to java version, which used `list` and re-formatted it
|
||||
# instead, simply use an already short-format `info line`
|
||||
# TODO `info symbol`? revert to `list`?
|
||||
def addresses_gdb(gdb, elf, addresses):
|
||||
cmd = [gdb, "--batch"]
|
||||
def addresses_gdb(gdb: pathlib.Path, elf: pathlib.Path, addresses: list[str]):
|
||||
cmd = [str(gdb), "--batch"]
|
||||
for address in addresses:
|
||||
if not address.startswith("0x"):
|
||||
address = f"0x{address}"
|
||||
cmd.extend(["--ex", f"info line *{address}"])
|
||||
cmd.append(elf)
|
||||
cmd.append(str(elf))
|
||||
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
|
||||
for line in proc.stdout.readlines():
|
||||
if "No line number" in line:
|
||||
continue
|
||||
yield line.strip()
|
||||
for line in run_command(cmd):
|
||||
if "No line number" in line:
|
||||
continue
|
||||
yield line.strip()
|
||||
|
||||
|
||||
# original approach using addr2line, which is pretty enough already
|
||||
def addresses_addr2line(addr2line, elf, addresses):
|
||||
def addresses_addr2line(
|
||||
addr2line: pathlib.Path, elf: pathlib.Path, addresses: list[str]
|
||||
):
|
||||
cmd = [
|
||||
addr2line,
|
||||
str(addr2line),
|
||||
"--addresses",
|
||||
"--inlines",
|
||||
"--functions",
|
||||
"--pretty-print",
|
||||
"--demangle",
|
||||
"--exe",
|
||||
elf,
|
||||
str(elf),
|
||||
]
|
||||
|
||||
for address in addresses:
|
||||
@@ -90,19 +111,23 @@ def addresses_addr2line(addr2line, elf, addresses):
|
||||
address = f"0x{address}"
|
||||
cmd.append(address)
|
||||
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
|
||||
for line in proc.stdout.readlines():
|
||||
if "??:0" in line:
|
||||
continue
|
||||
elif "inlined by" in line:
|
||||
yield f" {line.strip()}"
|
||||
else:
|
||||
yield line.strip()
|
||||
for line in run_command(cmd):
|
||||
if "??:0" in line:
|
||||
continue
|
||||
elif "inlined by" in line:
|
||||
yield f" {line.strip()}"
|
||||
|
||||
yield line.strip()
|
||||
|
||||
|
||||
def decode_lines(format_addresses, elf, lines):
|
||||
type Tool = Literal["gdb", "addr2line"]
|
||||
type Formatter = Callable[[pathlib.Path, list[str]], None]
|
||||
|
||||
|
||||
def decode_lines(
|
||||
format_addresses: Formatter, elf: pathlib.Path, lines: fileinput.FileInput
|
||||
):
|
||||
ANY_ADDR_RE = re.compile(r"0x[0-9a-fA-F]{8}|[0-9a-fA-F]{8}")
|
||||
HEX_ADDR_RE = re.compile(r"0x[0-9a-f]{8}")
|
||||
|
||||
MEM_ERR_LINE_RE = re.compile(r"^(Stack|last failed alloc call)")
|
||||
|
||||
@@ -117,7 +142,7 @@ def decode_lines(format_addresses, elf, lines):
|
||||
|
||||
# either print everything as-is, or cache current string and dump after stack contents end
|
||||
last_stack = None
|
||||
stack_addresses = {}
|
||||
stack_addresses: dict[str, list[str]] = {}
|
||||
|
||||
in_stack = False
|
||||
|
||||
@@ -141,9 +166,9 @@ def decode_lines(format_addresses, elf, lines):
|
||||
# 3fffffb0: feefeffe feefeffe 3ffe85d8 401004ed
|
||||
elif IGNORE_FIRMWARE_RE.match(line):
|
||||
continue
|
||||
elif in_stack and STACK_LINE_RE.match(line):
|
||||
_, addrs = line.split(":")
|
||||
addrs = ANY_ADDR_RE.findall(addrs)
|
||||
elif in_stack and STACK_LINE_RE.match(line) and last_stack:
|
||||
_, _, raw_addrs = line.partition(":")
|
||||
addrs = ANY_ADDR_RE.findall(raw_addrs)
|
||||
stack_addresses.setdefault(last_stack, [])
|
||||
stack_addresses[last_stack].extend(addrs)
|
||||
# epc1=0xfffefefe epc2=0xfefefefe epc3=0xefefefef excvaddr=0xfefefefe depc=0xfefefefe
|
||||
@@ -185,41 +210,59 @@ def decode_lines(format_addresses, elf, lines):
|
||||
print_all_addresses(stack_addresses)
|
||||
|
||||
|
||||
TOOLS = {"gdb": addresses_gdb, "addr2line": addresses_addr2line}
|
||||
type Output = Callable[[pathlib.Path, pathlib.Path, list[str]], Iterator[str]]
|
||||
|
||||
|
||||
def select_tool(toolchain_path, tool, func):
|
||||
path = f"xtensa-lx106-elf-{tool}"
|
||||
class ToolsDict(TypedDict):
|
||||
gdb: Output
|
||||
addr2line: Output
|
||||
|
||||
|
||||
TOOLS: ToolsDict = {
|
||||
"gdb": addresses_gdb,
|
||||
"addr2line": addresses_addr2line,
|
||||
}
|
||||
|
||||
|
||||
def select_tool(toolchain_path: pathlib.Path | None, tool: Tool) -> Formatter:
|
||||
cmd = f"xtensa-lx106-elf-{tool}"
|
||||
if toolchain_path:
|
||||
path = os.path.join(toolchain_path, path)
|
||||
cmd = str(toolchain_path / cmd)
|
||||
|
||||
if not shutil.which(path):
|
||||
raise FileNotFoundError(path)
|
||||
path = shutil.which(cmd)
|
||||
if not path:
|
||||
raise FileNotFoundError(cmd)
|
||||
|
||||
def formatter(func, path):
|
||||
def formatter(output: Output):
|
||||
def wrapper(elf, addresses):
|
||||
return func(path, elf, addresses)
|
||||
return output(path, elf, addresses)
|
||||
|
||||
return wrapper
|
||||
|
||||
return formatter(func, path)
|
||||
return formatter(TOOLS[tool])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--tool", choices=TOOLS, default="addr2line")
|
||||
parser.add_argument(
|
||||
"--toolchain-path", help="Sets path to Xtensa tools, when they are not in PATH"
|
||||
"--toolchain-path",
|
||||
type=pathlib.Path,
|
||||
help="Sets path to Xtensa tools, when they are not in PATH",
|
||||
)
|
||||
# XXX typing.get_args(Tool) is an empty tuple, see https://github.com/python/cpython/issues/112472
|
||||
parser.add_argument(
|
||||
"--tool", choices=typing.get_args(Tool.__value__), default="addr2line"
|
||||
)
|
||||
|
||||
parser.add_argument("firmware_elf")
|
||||
parser.add_argument(
|
||||
"postmortem", nargs="?", type=argparse.FileType("r"), default=sys.stdin
|
||||
)
|
||||
parser.add_argument("firmware_elf", type=pathlib.Path)
|
||||
parser.add_argument("postmortem", nargs="?", type=str, default="-")
|
||||
|
||||
args = parser.parse_args()
|
||||
decode_lines(
|
||||
select_tool(args.toolchain_path, args.tool, TOOLS[args.tool]),
|
||||
args.firmware_elf,
|
||||
args.postmortem,
|
||||
)
|
||||
|
||||
tool: Tool = args.tool
|
||||
toolchain_path: pathlib.Path | None = args.toolchain_path
|
||||
|
||||
firmware_elf: pathlib.Path = args.firmware_elf
|
||||
|
||||
with fileinput.input(files=args.postmortem, encoding="utf-8") as postmortem:
|
||||
decode_lines(select_tool(toolchain_path, tool), firmware_elf, postmortem)
|
||||
|
||||
Reference in New Issue
Block a user