项目文件夹

文件
2026-07-13 13:04:25 +08:00

666 行
27 KiB
Python

# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import json
import os
import re
import shlex
import subprocess
import sys
import threading
import time
from threading import Thread
from typing import Any
from esp_coredump import CoreDump
from rich_click import INT
from rich_click import Context
from idf_py_actions.errors import FatalError
from idf_py_actions.serial_ext import BAUD_RATE
from idf_py_actions.serial_ext import PORT
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import ensure_build_directory
from idf_py_actions.tools import generate_hints
from idf_py_actions.tools import get_default_serial_port
from idf_py_actions.tools import get_sdkconfig_value
from idf_py_actions.tools import yellow_print
def chip_rev_to_int(chip_rev: str | None) -> int | None:
# The chip rev will be derived from the elf file if none are returned.
# The chip rev must be supplied for coredump files generated with idf versions less than 5.1 in order to load
# rom elf file.
if not chip_rev or not all(c.isdigit() or c == '.' for c in chip_rev):
return None
if '.' not in chip_rev:
chip_rev += '.0'
major, minor = map(int, chip_rev.split('.'))
return major * 100 + minor
def action_extensions(base_actions: dict, project_path: str) -> dict:
OPENOCD_OUT_FILE = 'openocd_out.txt'
GDBGUI_OUT_FILE = 'gdbgui_out.txt'
# Internal dictionary of currently active processes, threads and their output files
processes: dict = {'threads_to_join': [], 'allow_hints': True}
def _print_hints(file_name: str) -> None:
if not processes['allow_hints']:
return
for hint in generate_hints(file_name):
if sys.stderr.isatty():
yellow_print(hint)
else:
# Hints go to stderr. Flush stdout, so hints are printed last.
sys.stdout.flush()
print(hint, file=sys.stderr)
def _check_openocd_errors(fail_if_openocd_failed: dict, target: str, ctx: Context) -> None:
if fail_if_openocd_failed:
if 'openocd' in processes and processes['openocd'] is not None:
p = processes['openocd']
name = processes['openocd_outfile_name']
# watch OpenOCD (for 5x500ms) to check if it hasn't terminated or outputs an error
for _ in range(5):
if p.poll() is not None:
print(f'OpenOCD exited with {p.poll()}')
break
with open(name, encoding='utf-8') as f:
content = f.read()
if re.search(r'Listening on port \d+ for gdb connections', content):
# expect OpenOCD has started successfully - stop watching
return
time.sleep(0.5)
# OpenOCD exited or is not listening -> print full log and terminate
with open(name, encoding='utf-8') as f:
print(f.read())
raise FatalError(f'Action "{target}" failed due to errors in OpenOCD', ctx)
def _terminate_async_target(target: str) -> None:
if target in processes and processes[target] is not None:
try:
if target + '_outfile' in processes:
processes[target + '_outfile'].close()
p = processes[target]
if p.poll() is None:
p.terminate()
# waiting 10x100ms for the process to terminate gracefully
for _ in range(10):
if p.poll() is not None:
break
time.sleep(0.1)
else:
p.kill()
if target + '_outfile_name' in processes:
_print_hints(processes[target + '_outfile_name'])
except Exception as e:
print(e)
print(f'Failed to close/kill {target}')
processes[target] = None # to indicate this has ended
def _get_espcoredump_instance(
ctx: Context,
args: PropertyDict,
gdb_timeout_sec: int | None = None,
core: str | None = None,
chip_rev: str | None = None,
save_core: str | None = None,
) -> CoreDump:
ensure_build_directory(args, ctx.info_name)
project_desc = get_project_desc(args, ctx)
coredump_to_flash_config = get_sdkconfig_value(
project_desc['config_file'], 'CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH'
)
coredump_to_flash = coredump_to_flash_config.rstrip().endswith('y') if coredump_to_flash_config else False
prog = os.path.join(project_desc['build_dir'], project_desc['app_elf'])
espcoredump_kwargs = dict()
espcoredump_kwargs['baud'] = args.baud
espcoredump_kwargs['gdb_timeout_sec'] = gdb_timeout_sec
espcoredump_kwargs['chip_rev'] = chip_rev_to_int(chip_rev)
# for reproducible builds
extra_gdbinit_file = project_desc['gdbinit_files']['02_prefix_map']
if extra_gdbinit_file:
espcoredump_kwargs['extra_gdbinit_file'] = extra_gdbinit_file
if core:
espcoredump_kwargs['core'] = core
espcoredump_kwargs['core_format'] = 'auto'
espcoredump_kwargs['chip'] = get_sdkconfig_value(project_desc['config_file'], 'CONFIG_IDF_TARGET')
elif coredump_to_flash:
# If the core dump is read from flash, we don't need to specify the --core-format argument at all.
# The format will be determined automatically
args.port = args.port or get_default_serial_port()
else:
print(
'Path to core dump file is not provided. '
"Core dump can't be read from flash since this option is not enabled in menuconfig"
)
sys.exit(1)
espcoredump_kwargs['port'] = args.port
espcoredump_kwargs['parttable_off'] = get_sdkconfig_value(
project_desc['config_file'], 'CONFIG_PARTITION_TABLE_OFFSET'
)
if save_core:
espcoredump_kwargs['save_core'] = save_core
espcoredump_kwargs['prog'] = prog
# compatibility check for esp-coredump < 1.5.2
try:
coredump = CoreDump(**espcoredump_kwargs)
except TypeError as e:
# 'parttable_off' was added in esp-coredump 1.5.2
# remove argument and retry without it
if 'parttable_off' in str(e):
espcoredump_kwargs.pop('parttable_off')
coredump = CoreDump(**espcoredump_kwargs)
else:
raise
return coredump
def is_gdb_with_python(gdb: str) -> bool:
# execute simple python command to check is it supported
return (
subprocess.run([gdb, '--batch-silent', '--ex', 'python import os'], stderr=subprocess.DEVNULL).returncode
== 0
)
def debug_cleanup() -> None:
print('cleaning up debug targets')
for t in processes['threads_to_join']:
if threading.currentThread() != t:
t.join()
_terminate_async_target('openocd')
_terminate_async_target('gdbgui')
_terminate_async_target('gdb')
def post_debug(action: str, ctx: Context, args: PropertyDict, **kwargs: str) -> None:
"""Deal with asynchronous targets, such as openocd running in background"""
if kwargs['block'] == 1:
for target in ['openocd', 'gdbgui']:
if target in processes and processes[target] is not None:
break
else:
return
try:
p = processes[target]
name = processes[target + '_outfile_name']
pos = 0
while True:
with open(name, encoding='utf-8') as f:
f.seek(pos)
for line in f:
print(line.rstrip())
pos = f.tell()
if p.poll() is not None:
print(f'"{target}" exited with {p.poll()}')
break
time.sleep(0.5)
except KeyboardInterrupt:
print('Terminated -> exiting debug utility targets')
if processes['allow_hints']:
ensure_build_directory(args, ctx.info_name) # initialize build context for hints
_terminate_async_target('openocd')
_terminate_async_target('gdbgui')
def get_project_desc(args: PropertyDict, ctx: Context) -> Any:
desc_path = os.path.join(args.build_dir, 'project_description.json')
if not os.path.exists(desc_path):
ensure_build_directory(args, ctx.info_name)
with open(desc_path, encoding='utf-8') as f:
project_desc = json.load(f)
return project_desc
def openocd(
action: str, ctx: Context, args: PropertyDict, openocd_scripts: str | None, openocd_commands: str
) -> None:
"""
Execute openocd as external tool
"""
if os.getenv('OPENOCD_SCRIPTS') is None:
raise FatalError('OPENOCD_SCRIPTS not found in the environment: Please run export.sh/export.bat', ctx)
openocd_arguments = os.getenv('OPENOCD_COMMANDS') if openocd_commands is None else openocd_commands
project_desc = get_project_desc(args, ctx)
if openocd_arguments is None:
# use default value if commands not defined in the environment nor command line
openocd_arguments = project_desc.get('debug_arguments_openocd', '')
print(
'Note: OpenOCD cfg not found (via env variable OPENOCD_COMMANDS nor as a --openocd-commands argument)\n'
f'OpenOCD arguments default to: "{openocd_arguments}"'
)
# script directory is taken from the environment by OpenOCD, update only if command line arguments to override
if openocd_scripts is not None:
openocd_arguments += f' -s {openocd_scripts}'
local_dir = project_desc['build_dir']
args = ['openocd'] + shlex.split(openocd_arguments)
openocd_out_name = os.path.join(local_dir, OPENOCD_OUT_FILE)
openocd_out = open(openocd_out_name, 'w', encoding='utf-8')
try:
process = subprocess.Popen(args, stdout=openocd_out, stderr=subprocess.STDOUT, bufsize=1)
except Exception as e:
print(e)
raise FatalError(
'Error starting openocd. Please make sure it is installed and is present in executable paths', ctx
)
processes['openocd'] = process
processes['openocd_outfile'] = openocd_out
processes['openocd_outfile_name'] = openocd_out_name
print(f'OpenOCD started as a background task {process.pid}')
def get_gdb_args(project_desc: dict[str, Any], gdb_x: tuple, gdb_ex: tuple, gdb_commands: str | None) -> list[str]:
# check if the application was built and ELF file is in place.
app_elf = os.path.join(project_desc.get('build_dir', ''), project_desc.get('app_elf', ''))
if not os.path.exists(app_elf):
raise FatalError('ELF file not found. You need to build & flash the project before running debug targets')
# debugger application name (xtensa-esp32-elf-gdb, riscv32-esp-elf-gdb, ...)
gdb_name = project_desc.get('monitor_toolprefix', '') + 'gdb'
gdb_args = [gdb_name]
gdbinit_files = project_desc.get('gdbinit_files')
if not gdbinit_files:
raise FatalError(
'Please check if the project was configured correctly ("gdbinit_files" not found in '
'"project_description.json").'
)
gdbinit_files = sorted(gdbinit_files.items())
gdb_x_list = list(gdb_x)
gdb_x_names = [os.path.basename(x) for x in gdb_x_list]
# compile predefined gdbinit files options.
for name, path in gdbinit_files:
name = name[len('xx_') :]
if name == 'py_extensions':
if not is_gdb_with_python(gdb_name):
continue
# Replace predefined gdbinit with user's if passed with the same name.
if name in gdb_x_names:
gdb_x_index = gdb_x_names.index(name)
gdb_args.append(f'-x={gdb_x_list[gdb_x_index]}')
gdb_x_list.pop(gdb_x_index)
continue
# If the user provides a gdbinit file with name not in the "gdbinit_files" list,
# we assume the connection logic is defined within it.
# Otherwise, the configuration may be invalid.
if name == 'connect' and gdb_x_list:
continue
gdb_args.append(f'-x={path}')
# append user-defined gdbinit files
for x in gdb_x_list:
gdb_args.append(f'-x={x}')
# add user-defined commands
if gdb_ex:
for ex in gdb_ex:
gdb_args.append('-ex')
gdb_args.append(ex)
# add user defined options
if gdb_commands:
gdb_args += shlex.split(gdb_commands)
return gdb_args
def _get_gdbgui_version(ctx: Context) -> tuple[int, ...]:
subprocess_success = False
try:
completed_process = subprocess.run(
['gdbgui', '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
captured_output = completed_process.stdout.decode('utf-8', 'ignore')
subprocess_success = True
except FileNotFoundError:
# This is happening at least with Python 3.12 when gdbgui is not installed
pass
if not subprocess_success or completed_process.returncode != 0:
raise SystemExit(
'Error occurred while starting gdbgui. Please make sure gdbgui has been installed with '
'pipx based on https://www.gdbgui.com/installation/ and "gdbgui --version" can be run '
'successfully. Gdbgui issues can be reported at https://github.com/cs01/gdbgui/issues.'
)
v = re.search(r'(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?', captured_output)
if not v:
raise SystemExit(f'Error: "gdbgui --version" returned "{captured_output}"')
return tuple(int(i) if i else 0 for i in (v[1], v[2], v[3], v[4]))
def gdbui(
action: str,
ctx: Context,
args: PropertyDict,
gdbgui_port: str | None,
gdbinit: tuple,
ex: tuple,
gdb_commands: str | None,
require_openocd: bool,
) -> None:
"""
Asynchronous GDB-UI target
"""
project_desc = get_project_desc(args, ctx)
local_dir = project_desc['build_dir']
gdb = project_desc['monitor_toolprefix'] + 'gdb'
gdbgui_version = _get_gdbgui_version(ctx)
gdb_args = get_gdb_args(project_desc, gdbinit, ex, gdb_commands)
if gdbgui_version >= (0, 14, 0, 0):
# See breaking changes https://github.com/cs01/gdbgui/blob/master/CHANGELOG.md#01400, especially the
# replacement of command line arguments.
gdbgui_args = ['gdbgui', '-g', ' '.join(gdb_args)]
else:
# this is a workaround for gdbgui
# gdbgui is using shlex.split for the --gdb-args option. When the input is:
# - '"-x=foo -x=bar"', would return ['foo bar']
# - '-x=foo', would return ['-x', 'foo'] and mess up the former option '--gdb-args'
# so for one item, use extra double quotes. for more items, use no extra double quotes.
gdb = gdb_args[0]
gdb_args_list = gdb_args[1:]
gdb_args_str = (
'"{}"'.format(' '.join(gdb_args_list)) if len(gdb_args_list) == 1 else ' '.join(gdb_args_list)
)
gdbgui_args = ['gdbgui', '-g', gdb, '--gdb-args', gdb_args_str]
if gdbgui_port is not None:
gdbgui_args += ['--port', gdbgui_port]
gdbgui_out_name = os.path.join(local_dir, GDBGUI_OUT_FILE)
gdbgui_out = open(gdbgui_out_name, 'w', encoding='utf-8')
env = os.environ.copy()
# The only known solution for https://github.com/cs01/gdbgui/issues/359 is to set the following environment
# variable. The greenlet package cannot be downgraded for compatibility with other requirements (gdbgui,
# pygdbmi).
env['PURE_PYTHON'] = '1'
try:
print('Running: ', gdbgui_args)
process = subprocess.Popen(gdbgui_args, stdout=gdbgui_out, stderr=subprocess.STDOUT, bufsize=1, env=env)
except (OSError, subprocess.CalledProcessError) as e:
print(e)
raise FatalError('Error starting gdbgui', ctx)
processes['gdbgui'] = process
processes['gdbgui_outfile'] = gdbgui_out
processes['gdbgui_outfile_name'] = gdbgui_out_name
print(f'gdbgui started as a background task {process.pid}')
_check_openocd_errors(fail_if_openocd_failed, action, ctx)
def global_callback(ctx: Context, global_args: PropertyDict, tasks: list) -> None:
def move_to_front(task_name: str) -> None:
for index, task in enumerate(tasks):
if task.name == task_name:
tasks.insert(0, tasks.pop(index))
break
processes['allow_hints'] = not ctx.params['no_hints']
debug_targets = any([task.name in ('openocd', 'gdbgui') for task in tasks])
if debug_targets:
# Register the meta cleanup callback -> called on FatalError
ctx.meta['cleanup'] = debug_cleanup
move_to_front('gdbgui') # possibly 2nd
move_to_front('openocd') # always 1st
# followed by "monitor", "gdb" or "gdbtui" in any order
post_action = ctx.invoke(ctx.command.get_command(ctx, 'post_debug'))
if any([task.name in ('monitor', 'gdb', 'gdbtui') for task in tasks]):
post_action.action_args['block'] = 0
else:
post_action.action_args['block'] = 1
tasks.append(post_action) # always last
if any([task.name == 'openocd' for task in tasks]):
for task in tasks:
if task.name in ('gdb', 'gdbgui', 'gdbtui'):
task.action_args['require_openocd'] = True
def gdbtui(
action: str,
ctx: Context,
args: PropertyDict,
gdbinit: tuple,
ex: tuple,
gdb_commands: str,
require_openocd: bool,
) -> None:
"""
Synchronous GDB target with text ui mode
"""
gdb(action, ctx, args, False, 1, gdbinit, ex, gdb_commands, require_openocd)
def gdb(
action: str,
ctx: Context,
args: PropertyDict,
batch: bool,
gdb_tui: int | None,
gdbinit: tuple,
ex: tuple,
gdb_commands: str | None,
require_openocd: bool,
) -> None:
"""
Synchronous GDB target
"""
watch_openocd = Thread(
target=_check_openocd_errors,
args=(
fail_if_openocd_failed,
action,
ctx,
),
)
watch_openocd.start()
processes['threads_to_join'].append(watch_openocd)
project_desc = get_project_desc(args, ctx)
gdb_args = get_gdb_args(project_desc, gdbinit, ex, gdb_commands)
if gdb_tui is not None:
gdb_args += ['-tui']
if batch:
gdb_args += ['--batch']
p = subprocess.Popen(gdb_args)
processes['gdb'] = p
while True:
try:
p.wait()
break
except KeyboardInterrupt:
# Catching Keyboard interrupt, as this is used for breaking running program in gdb
continue
finally:
watch_openocd.join()
try:
processes['threads_to_join'].remove(watch_openocd)
except ValueError:
# Valid scenario: watch_openocd task won't be in the list if openocd not started from idf.py
pass
def coredump_info(
action: str,
ctx: Context,
args: PropertyDict,
gdb_timeout_sec: int,
core: str | None = None,
chip_rev: str | None = None,
save_core: str | None = None,
) -> None:
espcoredump = _get_espcoredump_instance(
ctx=ctx, args=args, gdb_timeout_sec=gdb_timeout_sec, core=core, chip_rev=chip_rev, save_core=save_core
)
espcoredump.info_corefile()
def coredump_debug(
action: str,
ctx: Context,
args: PropertyDict,
core: str | None = None,
chip_rev: str | None = None,
save_core: str | None = None,
) -> None:
espcoredump = _get_espcoredump_instance(ctx=ctx, args=args, core=core, chip_rev=chip_rev, save_core=save_core)
espcoredump.dbg_corefile()
coredump_base = [
{
'names': ['--core', '-c'],
'help': 'Path to core dump file (if skipped core dump will be read from flash)',
},
{
'names': ['--chip-rev'],
'help': 'Specify the chip revision (e.g., 0.1). If provided, the corresponding ROM ELF file will be used '
'for decoding the core dump, improving stack traces. This is only needed for core dumps from '
'ESP-IDF older than v5.2. Newer versions already contain chip revision information.',
'hidden': True,
},
{
'names': ['--save-core', '-s'],
'help': 'Save core to file. Otherwise temporary core file will be deleted.',
},
]
gdb_timeout_sec_opt = {
'names': ['--gdb-timeout-sec'],
'type': INT,
'default': 3,
'help': 'Overwrite the default internal delay for gdb responses',
}
fail_if_openocd_failed = {
'names': ['--require-openocd', '--require_openocd'],
'help': 'Fail this target if openocd (this targets dependency) failed.\n',
'is_flag': True,
'default': False,
}
gdbinit = {
'names': ['--gdbinit', '--x', '-x'],
'help': 'Specify the name of gdbinit file to use\n',
'default': None,
'multiple': True,
}
ex = {
'names': ['--ex', '-ex'],
'help': ('Execute given GDB command.\n'),
'default': None,
'multiple': True,
}
gdb_commands = {
'names': ['--gdb-commands', '--gdb_commands'],
'help': ('Command line arguments for gdb.\n'),
'default': None,
}
debug_actions = {
'global_action_callbacks': [global_callback],
'actions': {
'openocd': {
'callback': openocd,
'help': 'Run openocd from current path',
'options': [
{
'names': ['--openocd-scripts', '--openocd_scripts'],
'help': ('Script directory for openocd cfg files.\n'),
'default': None,
},
{
'names': ['--openocd-commands', '--openocd_commands'],
'help': ('Command line arguments for openocd.\n'),
'default': None,
},
],
'order_dependencies': ['all', 'flash'],
},
'gdb': {
'callback': gdb,
'help': 'Run the GDB.',
'options': [
{
'names': ['--batch'],
'help': ('exit after processing gdbinit.\n'),
'hidden': True,
'is_flag': True,
'default': False,
},
{
'names': ['--gdb-tui', '--gdb_tui'],
'help': ('run gdb in TUI mode\n'),
'default': None,
},
gdbinit,
ex,
gdb_commands,
fail_if_openocd_failed,
],
'order_dependencies': ['all', 'flash'],
},
'gdbgui': {
'callback': gdbui,
'help': 'GDB UI in default browser.',
'options': [
{
'names': ['--gdbgui-port', '--gdbgui_port'],
'help': ('The port on which gdbgui will be hosted. Default: 5000\n'),
'default': None,
},
gdbinit,
ex,
gdb_commands,
fail_if_openocd_failed,
],
'order_dependencies': ['all', 'flash'],
},
'gdbtui': {
'callback': gdbtui,
'help': 'GDB TUI mode.',
'options': [gdbinit, ex, gdb_commands, fail_if_openocd_failed],
'order_dependencies': ['all', 'flash'],
},
'coredump-info': {
'callback': coredump_info,
'help': 'Print crashed task’s registers, callstack, list of available tasks in the system, '
'memory regions and contents of memory stored in core dump (TCBs and stacks)',
'options': coredump_base + [PORT, BAUD_RATE, gdb_timeout_sec_opt], # type: ignore
'order_dependencies': ['all', 'flash'],
},
'coredump-debug': {
'callback': coredump_debug,
'help': 'Create core dump ELF file and run GDB debug session with this file.',
'options': coredump_base + [PORT, BAUD_RATE], # type: ignore
'order_dependencies': ['all', 'flash'],
},
'post-debug': {
'callback': post_debug,
'help': 'Utility target to read the output of async debug action and stop them.',
'options': [
{
'names': ['--block', '--block'],
'help': ('Set to 1 for blocking the console on the outputs of async debug actions\n'),
'default': 0,
},
],
'order_dependencies': [],
},
'post_debug': {
'callback': post_debug,
'deprecated': {
'since': 'v4.4',
'removed': 'v5.0',
'exit_with_error': True,
'message': 'Have you wanted to run "post-debug" instead?',
},
'hidden': True,
'help': 'Utility target to read the output of async debug action and stop them.',
'options': [
{
'names': ['--block', '--block'],
'help': ('Set to 1 for blocking the console on the outputs of async debug actions\n'),
'default': 0,
},
],
'order_dependencies': [],
},
},
}
return debug_actions