tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan faultdecode` -- decode an ARM Cortex-M (ARMv8-M) fault dump.
|
|
3
|
+
|
|
4
|
+
Port of `alp_cli/faultdecode.py` (alp-sdk `scripts/alp_cli/faultdecode.py`).
|
|
5
|
+
Until now `tan faultdecode` was a thin forwarder to `python -m alp_cli
|
|
6
|
+
faultdecode` (`crates/tan-cli/src/commands/sdk_cli.rs`); this is the native
|
|
7
|
+
replacement, so `--json`'s OUTPUT CONTRACT is that forward's own: the SDK's
|
|
8
|
+
`--json` report shape (`fault_detected`/`inputs`/`flags`/`addresses`/
|
|
9
|
+
`root_cause`/`symbols`), unwrapped, on stdout. `sdk_cli.rs`'s success path
|
|
10
|
+
streamed the child's stdio through untouched; `--json` reproduces exactly what
|
|
11
|
+
a caller already received through that pipe, so nothing downstream (a saved
|
|
12
|
+
script, the forwarder itself) observes a change.
|
|
13
|
+
|
|
14
|
+
`--format json` is a DIFFERENT surface, and tan-cli#399 is the decision to
|
|
15
|
+
make it one: it emits tan's `{command,ok,exitCode,project,data,issues}`
|
|
16
|
+
envelope with the report as `data`. `--format json` is what the vscode
|
|
17
|
+
extension drives (`contract/README.md:4-8`), and its `isEnvelope` guard
|
|
18
|
+
(`alp-sdk-vscode src/alpCli/service.ts:705-716`) requires `command`, `ok`,
|
|
19
|
+
`exitCode` and `issues[]` -- the unwrapped report has none of them, so
|
|
20
|
+
`parseEnvelope` returned `null`, `classifyOutcome` saw rc 0, and a successful
|
|
21
|
+
decode rendered as the literal string `Command completed.` with `root_cause`
|
|
22
|
+
on the wire and unreachable. The v0.4.1 oracle maps the global `--format json`
|
|
23
|
+
onto the child's `--json` and therefore prints the raw report for both
|
|
24
|
+
spellings; that divergence is deliberate and is the whole content of the fix.
|
|
25
|
+
`--json` wins when both are given, so an existing caller cannot have its shape
|
|
26
|
+
changed by a `--format json` something else in the chain added.
|
|
27
|
+
|
|
28
|
+
A firmware engineer pastes the fault registers a HardFault prints (CFSR, HFSR,
|
|
29
|
+
optionally DFSR, BFAR, MMFAR) and gets back the human-readable cause plus, when
|
|
30
|
+
an ELF is supplied, the faulting symbol and `file:line` -- instead of staring at
|
|
31
|
+
CFSR hex.
|
|
32
|
+
|
|
33
|
+
The decode/parse/render core lives in `tan.core.faultdecode` (pure, no I/O);
|
|
34
|
+
this module owns the CLI surface plus the two things that touch the outside
|
|
35
|
+
world: reading a pasted dump (`--file`/stdin) and best-effort `addr2line`
|
|
36
|
+
symbolication.
|
|
37
|
+
|
|
38
|
+
It is strictly HW-free: pure register arithmetic. Symbolication is best-effort
|
|
39
|
+
and optional -- if no ELF or no `addr2line`-class tool is found it is skipped,
|
|
40
|
+
never fatal. Exit code is 0 for any successful analysis (even "no fault flags
|
|
41
|
+
set"); only genuinely bad input (no registers, an unparseable value, or a bad
|
|
42
|
+
`--elf`/`--file` path) is nonzero (`ExitCode.VALIDATION_FAILURE`, matching the
|
|
43
|
+
original's `click.BadParameter`/`click.UsageError`, both exit 2).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import json as _json
|
|
49
|
+
import os
|
|
50
|
+
import queue
|
|
51
|
+
import subprocess
|
|
52
|
+
import sys
|
|
53
|
+
import threading
|
|
54
|
+
import time
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
|
|
58
|
+
import typer
|
|
59
|
+
|
|
60
|
+
from tan.commands.doctor_cmd import on_path
|
|
61
|
+
from tan.core.faultdecode import (
|
|
62
|
+
Symbol,
|
|
63
|
+
decode,
|
|
64
|
+
parse_dump,
|
|
65
|
+
report_to_json,
|
|
66
|
+
render_human,
|
|
67
|
+
)
|
|
68
|
+
from tan.core.inert import NOT_APPLICABLE, inert_help
|
|
69
|
+
from tan.core.subprocess_env import spawn_env
|
|
70
|
+
from tan.env import no_color_requested, stdin_is_tty
|
|
71
|
+
from tan.envelope import Envelope, Issue, Project, emit
|
|
72
|
+
from tan.exit_codes import ExitCode
|
|
73
|
+
from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
|
|
74
|
+
|
|
75
|
+
_ADDR2LINE_TOOLS = ("arm-zephyr-eabi-addr2line", "llvm-addr2line", "addr2line")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolve_symbol(addr: int, elf: Path) -> Symbol | None:
|
|
79
|
+
"""Resolve ``addr`` to ``func`` + ``file:line`` via an addr2line-class tool.
|
|
80
|
+
|
|
81
|
+
Tries ``arm-zephyr-eabi-addr2line`` then ``llvm-addr2line`` then plain
|
|
82
|
+
``addr2line``. Returns ``None`` (caller skips gracefully) if no tool is on
|
|
83
|
+
PATH or the lookup fails -- symbolication is a convenience, never required.
|
|
84
|
+
|
|
85
|
+
Uses `doctor_cmd.on_path`, NOT `shutil.which` (tan-cli#503): on Windows,
|
|
86
|
+
`shutil.which` inserts `os.curdir` ahead of `$PATH`, so an
|
|
87
|
+
`addr2line.exe`/`llvm-addr2line.exe`/`arm-zephyr-eabi-addr2line.exe`
|
|
88
|
+
sitting at the root of a checked-out project is reported as "available".
|
|
89
|
+
Spawning it by bare NAME afterwards (rather than the resolved absolute
|
|
90
|
+
path) would reopen the same hole a second way -- `CreateProcess`'s own
|
|
91
|
+
current-directory-first search -- even past a hardened probe, so the
|
|
92
|
+
resolved absolute path from `on_path` is what gets spawned below.
|
|
93
|
+
|
|
94
|
+
Since tan-cli#532 that is no longer a second pattern: `on_path` delegates
|
|
95
|
+
to `tan.core.tool_lookup.resolve_tool`, the same lookup `flash_cmd`,
|
|
96
|
+
`size_cmd` and `build/execute.py` call, so every tool probe in tan now
|
|
97
|
+
resolves identically. This docstring previously said those three
|
|
98
|
+
hand-rolled their own walks returning a bare `bool` -- true when it was
|
|
99
|
+
written, false since #567 consolidated them, and stated in the past tense
|
|
100
|
+
here so the next reader does not act on the older shape.
|
|
101
|
+
"""
|
|
102
|
+
tool = next((p for t in _ADDR2LINE_TOOLS if (p := on_path(t))), None)
|
|
103
|
+
if tool is None or not elf.is_file():
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
proc = subprocess.run(
|
|
107
|
+
[tool, "-f", "-C", "-e", str(elf), f"0x{addr:x}"],
|
|
108
|
+
capture_output=True, text=True, timeout=15, env=spawn_env(), check=False,
|
|
109
|
+
)
|
|
110
|
+
except (OSError, subprocess.SubprocessError): # pragma: no cover - env-dependent
|
|
111
|
+
return None
|
|
112
|
+
lines = [ln.strip() for ln in (proc.stdout or "").splitlines() if ln.strip()]
|
|
113
|
+
if not lines:
|
|
114
|
+
return None
|
|
115
|
+
func = lines[0]
|
|
116
|
+
location = lines[1] if len(lines) > 1 else "??:?"
|
|
117
|
+
if func in ("??", "") and location in ("??:?", "??:0", ":?"):
|
|
118
|
+
return None # tool ran but knows nothing about this address
|
|
119
|
+
return Symbol(addr=addr, func=func or "??", location=location or "??:?")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _use_color(no_color: bool) -> bool:
|
|
123
|
+
"""`alp_cli.diagnostic._use_color(False if no_color else None)`: `--no-color`
|
|
124
|
+
forces off, else `NO_COLOR` forces off, else follow stdout's own tty-ness --
|
|
125
|
+
stdout, not stderr, because this command's human text goes to stdout (the
|
|
126
|
+
original's `click.echo` default), matching the forwarded SDK behaviour this
|
|
127
|
+
replaces."""
|
|
128
|
+
if no_color:
|
|
129
|
+
return False
|
|
130
|
+
if no_color_requested():
|
|
131
|
+
return False
|
|
132
|
+
try:
|
|
133
|
+
return sys.stdout.isatty()
|
|
134
|
+
except (AttributeError, ValueError):
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class NegativeRegisterValue(Exception):
|
|
139
|
+
"""A fault register was given a negative value (tan-cli#616).
|
|
140
|
+
|
|
141
|
+
Not a `typer.BadParameter`: this refusal carries a REGISTERED issue code
|
|
142
|
+
(`faultdecode.invalid-register-value`) so a `--format json` consumer can
|
|
143
|
+
branch on it, the same way `faultdecode.no-registers` already lets it tell
|
|
144
|
+
"nothing to analyse" from a tan crash. `BadParameter` would land on
|
|
145
|
+
`cli.main`'s generic `command: "cli"` / `cli.parse-error` fallback instead
|
|
146
|
+
-- the two-paths-of-one-verb disagreement tan-cli#399 closed everywhere
|
|
147
|
+
else in this command.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, option: str, value: str, *, too_wide: bool = False) -> None:
|
|
151
|
+
self.option = option
|
|
152
|
+
self.value = value
|
|
153
|
+
self.message = (
|
|
154
|
+
f"--{option}: {value!r} does not fit in 32 bits -- a fault register is "
|
|
155
|
+
"a 32-bit word, so this is likely a mistyped extra digit."
|
|
156
|
+
if too_wide
|
|
157
|
+
else f"--{option}: {value!r} is negative -- a fault register value is "
|
|
158
|
+
"unsigned, so there is nothing to decode. Pass the value exactly as "
|
|
159
|
+
"the dump printed it (hex, with or without a 0x prefix)."
|
|
160
|
+
)
|
|
161
|
+
super().__init__(self.message)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _parse_hexint(option: str, value: str | None) -> int | None:
|
|
165
|
+
"""A fault-register value, hex by default: these are CPU status registers,
|
|
166
|
+
always read in hex, so a bare ``8200`` means ``0x8200`` (not decimal 8200)
|
|
167
|
+
and ``0x8200`` works too. Mirrors the original's `_HexInt.convert`.
|
|
168
|
+
|
|
169
|
+
A NEGATIVE value is refused (tan-cli#616). `int(text, 16)` happily accepts
|
|
170
|
+
a leading `-`, and everything downstream then treats the result as a
|
|
171
|
+
register word: `--cfsr=-8200` rendered `"0x-0008200"` (not a hex integer in
|
|
172
|
+
any sense), decoded twelve flags that are not set out of Python's infinite
|
|
173
|
+
two's-complement sign extension, concluded "Stack overflow", and exited 0.
|
|
174
|
+
A register is unsigned by construction, so a sign is a user error and the
|
|
175
|
+
only correct answer is a refusal -- confident nonsense at exit 0 is the
|
|
176
|
+
worst possible one for a tool whose entire output is a diagnosis.
|
|
177
|
+
"""
|
|
178
|
+
if value is None:
|
|
179
|
+
return None
|
|
180
|
+
text = value.strip()
|
|
181
|
+
try:
|
|
182
|
+
# base 16 accepts an optional 0x prefix and a bare hex run alike.
|
|
183
|
+
parsed = int(text, 16)
|
|
184
|
+
except ValueError as err:
|
|
185
|
+
raise typer.BadParameter(
|
|
186
|
+
f"{value!r} is not a valid integer (try 0x...)", param_hint=f"--{option}"
|
|
187
|
+
) from err
|
|
188
|
+
if parsed < 0:
|
|
189
|
+
raise NegativeRegisterValue(option, value)
|
|
190
|
+
if parsed > 0xFFFFFFFF:
|
|
191
|
+
raise NegativeRegisterValue(option, value, too_wide=True)
|
|
192
|
+
return parsed
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _refuse(
|
|
196
|
+
code: str,
|
|
197
|
+
message: str,
|
|
198
|
+
*,
|
|
199
|
+
envelope_mode: bool,
|
|
200
|
+
extra_issues: list[Issue] | None = None,
|
|
201
|
+
) -> typer.Exit:
|
|
202
|
+
"""Build `faultdecode`'s refusal on whichever surface the caller asked for,
|
|
203
|
+
and return the `typer.Exit` for the caller to `raise`.
|
|
204
|
+
|
|
205
|
+
One place, because the refusals have to agree with the success path AND
|
|
206
|
+
with each other about whether stdout is an envelope (tan-cli#399): a second
|
|
207
|
+
refusal that wrote plain text under `--format json` would hand the consumer
|
|
208
|
+
`command: "cli"` here and `command: "faultdecode"` one invocation later,
|
|
209
|
+
which is the exact defect that issue closed for the first one.
|
|
210
|
+
|
|
211
|
+
`extra_issues` (tan-cli#537 constraint 4): a bound firing on the implicit
|
|
212
|
+
stdin read is announced here TOO, not only on the success path -- a bound
|
|
213
|
+
can fire with bytes read but no complete register set, and that refusal
|
|
214
|
+
used to say nothing about the truncation at all. Included in the envelope
|
|
215
|
+
issues[] AND echoed to stderr in every mode (not just `--format json`),
|
|
216
|
+
same as the success path's own announcement.
|
|
217
|
+
"""
|
|
218
|
+
for issue in extra_issues or ():
|
|
219
|
+
typer.echo(f"Warning: {issue.message}", err=True)
|
|
220
|
+
if envelope_mode:
|
|
221
|
+
emit(
|
|
222
|
+
Envelope(
|
|
223
|
+
"faultdecode",
|
|
224
|
+
Project(root=None, board_yaml=None),
|
|
225
|
+
None,
|
|
226
|
+
[*(extra_issues or ()), Issue(code, "error", message)],
|
|
227
|
+
ExitCode.VALIDATION_FAILURE,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
typer.echo(f"Error: {message}", err=True)
|
|
232
|
+
return typer.Exit(int(ExitCode.VALIDATION_FAILURE))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
#: Chunk size for each background stdin read. `.read1(n)` (see
|
|
236
|
+
#: `_read_stdin_bounded` for why not `.read(n)`) never blocks trying to fill
|
|
237
|
+
#: this, so its exact value only bounds worst-case per-chunk latency, not
|
|
238
|
+
#: correctness -- 64 KiB is the same figure `shutil.copyfileobj` defaults to.
|
|
239
|
+
_STDIN_CHUNK_BYTES = 65536
|
|
240
|
+
|
|
241
|
+
#: Idle bound (tan-cli#537): how long the background reader waits, after
|
|
242
|
+
#: either the LAST chunk that arrived or start (if none yet), before
|
|
243
|
+
#: deciding no more is coming. Reset on every chunk -- this is what lets a
|
|
244
|
+
#: slow-but-steady producer (a serial capture, a script draining a device)
|
|
245
|
+
#: read in full, which a TOTAL budget (attempts 2 and 6/7) cannot do.
|
|
246
|
+
#:
|
|
247
|
+
#: Measured, not argued: an ad-hoc bench script (tan-cli#537, not checked in) fed a
|
|
248
|
+
#: real OS pipe 26 lines with a 0.24s `time.sleep` between each and recorded
|
|
249
|
+
#: the gaps the reader thread actually observed -- max 0.250s under this
|
|
250
|
+
#: environment's own scheduling jitter, essentially the nominal 0.24s with no
|
|
251
|
+
#: slack to spare. 2.0s is that measured worst case with roughly 8x headroom,
|
|
252
|
+
#: not a number chosen by reasoning about the idle window in the abstract --
|
|
253
|
+
#: that reasoning is exactly what produced the reverted "20x the idle window"
|
|
254
|
+
#: comment in attempt 6/7, which was arithmetically false for this same
|
|
255
|
+
#: shape.
|
|
256
|
+
_STDIN_IDLE_TIMEOUT_S = 2.0
|
|
257
|
+
|
|
258
|
+
#: Byte cap (tan-cli#537): a hard ceiling on accumulated stdin bytes. No
|
|
259
|
+
#: earlier attempt had one -- unguarded, `yes "CFSR: 0x00008200" | tan
|
|
260
|
+
#: faultdecode` reached 409.7 MB RSS over 9.59s (issue #537, measured). A
|
|
261
|
+
#: real pasted fault dump is at most a few KB; 1 MiB is roughly three orders
|
|
262
|
+
#: of magnitude below that measured unguarded growth while staying two-plus
|
|
263
|
+
#: orders of magnitude above any legitimate paste. Bounds the ACCUMULATED
|
|
264
|
+
#: BUFFER itself (checked after every chunk), not the read loop -- a large
|
|
265
|
+
#: total-cap alone would still let the buffer grow unbounded until it fired.
|
|
266
|
+
_STDIN_BYTE_CAP = 1_048_576
|
|
267
|
+
|
|
268
|
+
#: Total bound (tan-cli#537): a backstop so a producer that never goes idle
|
|
269
|
+
#: for `_STDIN_IDLE_TIMEOUT_S` and never reaches `_STDIN_BYTE_CAP` (a
|
|
270
|
+
#: continuous writer trickling below both) still terminates. NOT the primary
|
|
271
|
+
#: defence -- the idle bound is, for exactly the reason attempts 6/7 got a
|
|
272
|
+
#: fixed total wrong: any fixed total remains capable of truncating a
|
|
273
|
+
#: sufficiently slow legitimate producer. 30s is ~5x the 6.2s measured
|
|
274
|
+
#: slow-but-steady shape (the same ad-hoc bench), which is comfortably
|
|
275
|
+
#: inside it because the IDLE bound is what actually governs that case (each
|
|
276
|
+
#: gap is ~0.24s, far under `_STDIN_IDLE_TIMEOUT_S`); this total exists only
|
|
277
|
+
#: to catch the case the idle bound cannot. Whichever bound fires, the
|
|
278
|
+
#: read still ANNOUNCES it (`_stdin_bound_message`) rather than silently
|
|
279
|
+
#: discarding -- that announcement, not the value chosen here, is what makes
|
|
280
|
+
#: a truncation recoverable instead of a confident wrong root cause.
|
|
281
|
+
_STDIN_TOTAL_TIMEOUT_S = 30.0
|
|
282
|
+
|
|
283
|
+
#: Bound on `_read_stdin_bounded`'s internal `chunk_queue` (tan-cli#537
|
|
284
|
+
#: follow-up): the byte cap above bounds the RETURNED buffer, but the
|
|
285
|
+
#: background reader thread is ABANDONED, not joined, once any bound fires
|
|
286
|
+
#: (see `_read_stdin_bounded`'s own docstring on why) -- an unbounded
|
|
287
|
+
#: `queue.Queue()` let that abandoned thread keep calling `os.read` + `put`
|
|
288
|
+
#: against a producer that never stops (the `yes` shape) with nobody ever
|
|
289
|
+
#: calling `.get()` again, growing without limit. Measured from the moment
|
|
290
|
+
#: the byte cap fired against a real `yes` pipe: 640.1 MB at +0.5s, 1059.6 MB
|
|
291
|
+
#: at +1.0s, 2001.9 MB at +2.0s, 3845.8 MB at +4.0s -- bounded today only by
|
|
292
|
+
#: how fast the process happens to exit, which is not a bound. 16 chunks is
|
|
293
|
+
#: `_STDIN_BYTE_CAP // _STDIN_CHUNK_BYTES` -- exactly enough that the queue
|
|
294
|
+
#: never has to reject a chunk before the byte cap itself would have ended
|
|
295
|
+
#: the main loop anyway -- plus headroom for ordinary scheduling jitter
|
|
296
|
+
#: between the reader thread producing and the main loop's `.get()`
|
|
297
|
+
#: consuming, so this never discards a chunk the main loop was still going
|
|
298
|
+
#: to want.
|
|
299
|
+
_STDIN_DRAIN_QUEUE_MAXSIZE = (_STDIN_BYTE_CAP // _STDIN_CHUNK_BYTES) + 16
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass(frozen=True)
|
|
303
|
+
class _StdinReadOutcome:
|
|
304
|
+
"""Result of one bounded background stdin BYTE read (tan-cli#537)."""
|
|
305
|
+
|
|
306
|
+
#: Raw bytes accumulated before whichever bound fired (or clean EOF).
|
|
307
|
+
data: bytes
|
|
308
|
+
#: Which bound fired, or `None` on a clean EOF (the writer closed the
|
|
309
|
+
#: pipe on its own, no bound needed to intervene at all).
|
|
310
|
+
bound: str | None # None | "idle" | "byte-cap" | "total-cap"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _read_stdin_bounded(
|
|
314
|
+
buffer: object,
|
|
315
|
+
*,
|
|
316
|
+
idle_s: float = _STDIN_IDLE_TIMEOUT_S,
|
|
317
|
+
byte_cap: int = _STDIN_BYTE_CAP,
|
|
318
|
+
total_s: float = _STDIN_TOTAL_TIMEOUT_S,
|
|
319
|
+
chunk_bytes: int = _STDIN_CHUNK_BYTES,
|
|
320
|
+
) -> _StdinReadOutcome:
|
|
321
|
+
"""Read `buffer` (a binary stream, e.g. `sys.stdin.buffer`) to bytes,
|
|
322
|
+
bounded three ways at once (tan-cli#537): an IDLE timeout that resets on
|
|
323
|
+
every chunk, a hard BYTE CAP on the accumulated buffer, and a TOTAL
|
|
324
|
+
timeout as a backstop. All state (the queue, the buffer, the deadlines)
|
|
325
|
+
is local to this call -- tan-cli#537's own module-global `_PREREAD_STDIN`
|
|
326
|
+
stash is what let one invocation's empty read leak into the next
|
|
327
|
+
in-process call; nothing here survives past the `return`.
|
|
328
|
+
|
|
329
|
+
ONE mechanism on every platform: a single daemon thread looping a
|
|
330
|
+
single-syscall chunk read and pushing each chunk to a `queue.Queue`,
|
|
331
|
+
with EOF pushed in-band as `b""` -- never inferred from a clock. This
|
|
332
|
+
deletes the select()-on-POSIX/thread-on-Windows split the previous
|
|
333
|
+
reader carried: `select()` is WinSock-only on Windows (anonymous pipes
|
|
334
|
+
are not selectable there at all), and neither `PeekNamedPipe` nor a
|
|
335
|
+
console wait handle answers "will this read terminate?" -- only "is a
|
|
336
|
+
byte available?", a different question. A chunked thread reader needs
|
|
337
|
+
none of that and behaves identically everywhere.
|
|
338
|
+
|
|
339
|
+
Never `buffer.read(n)`: `io.BufferedReader.read(n)` blocks issuing
|
|
340
|
+
repeated raw reads until it collects `n` bytes OR hits EOF -- it does NOT
|
|
341
|
+
return early just because some bytes are already available. Measured
|
|
342
|
+
(the same ad-hoc bench): with `.read(65536)` a producer
|
|
343
|
+
writing 26 lines with 0.24s gaps between them delivered exactly ONE
|
|
344
|
+
chunk containing the whole dump, at EOF, 6.29s after the first byte --
|
|
345
|
+
the per-chunk idle-reset this function exists to provide never had
|
|
346
|
+
anything to reset against, silently degrading to the "wait for the
|
|
347
|
+
whole thing" case attempt 6/7 already got a total budget wrong for.
|
|
348
|
+
|
|
349
|
+
The chunk primitive is `os.read(fd, chunk_bytes)` on `buffer`'s real file
|
|
350
|
+
descriptor when it has one (a genuine `sys.stdin.buffer` over a pipe,
|
|
351
|
+
file, or console), falling back to `buffer.read1(chunk_bytes)` only when
|
|
352
|
+
it does not (`typer.testing.CliRunner`'s in-memory `BytesIO`-backed
|
|
353
|
+
stdin double, whose `.fileno()` raises `io.UnsupportedOperation` --
|
|
354
|
+
`CliRunner` hands the command an already-complete buffer, so `.read1()`
|
|
355
|
+
there returns immediately and reaches its own in-band EOF quickly; it
|
|
356
|
+
never blocks on a real held-open pipe, so it carries none of the risk
|
|
357
|
+
the next paragraph describes). Both return AT MOST ONE underlying
|
|
358
|
+
system read's worth of data rather than trying to fill `chunk_bytes` --
|
|
359
|
+
the same bench run measured 27 `.read1()` chunks, one per line plus the
|
|
360
|
+
terminating empty read, with gaps at 0.239-0.250s, matching the
|
|
361
|
+
writer's own 0.24s sleep, and `os.read` shares that same one-syscall
|
|
362
|
+
contract by construction (it does not go through any buffering layer
|
|
363
|
+
at all).
|
|
364
|
+
|
|
365
|
+
`os.read(fd, ...)` over `buffer.read1(...)` on a REAL stdin is not a
|
|
366
|
+
style choice, it is the fix for a second, more severe defect this
|
|
367
|
+
function's own first cut hit: with the reader parked in
|
|
368
|
+
`buffer.read1()` (a `BufferedReader` method) when the idle bound fires
|
|
369
|
+
and the caller proceeds without joining the thread, a normal process
|
|
370
|
+
exit then aborts -- measured, reproducibly, via
|
|
371
|
+
`test_registers_on_the_command_line_never_wait_for_an_open_stdin_pipe`
|
|
372
|
+
(a REAL subprocess, not `CliRunner`) -- with `Fatal Python error:
|
|
373
|
+
_enter_buffered_busy: could not acquire lock for <_io.BufferedReader
|
|
374
|
+
name='<stdin>'> at interpreter shutdown, possibly due to daemon
|
|
375
|
+
threads` and exit code -6 (SIGABRT): CPython's interpreter-shutdown
|
|
376
|
+
finalizer tries to flush/close `sys.stdin`, which needs the
|
|
377
|
+
`BufferedReader`'s internal per-object lock, and the abandoned thread
|
|
378
|
+
is holding that same lock parked in a real blocking read syscall that
|
|
379
|
+
will never return (the parent still owns the open write end) --
|
|
380
|
+
exactly the "daemon thread parked in a read at interpreter exit" risk
|
|
381
|
+
the design flagged for Windows specifically, reproduced here on Linux
|
|
382
|
+
too. `os.read(fd, n)` is a bare syscall on the raw descriptor with no
|
|
383
|
+
Python-level stream OBJECT and therefore no such lock for the
|
|
384
|
+
finalizer to contend on; the same reproduction, switched to it, exits
|
|
385
|
+
0 cleanly (verified with a second ad-hoc reproduction script,
|
|
386
|
+
tan-cli#537, not checked in).
|
|
387
|
+
|
|
388
|
+
Termination is never inferred: EOF is the in-band `b""` chunk, the idle
|
|
389
|
+
bound fires only via `queue.Queue.get(timeout=...)` actually elapsing
|
|
390
|
+
with nothing delivered, the byte cap fires only once the accumulated
|
|
391
|
+
total has actually reached it, and the total bound fires only once the
|
|
392
|
+
wall clock has actually run out. Whichever one fires, what already
|
|
393
|
+
arrived is kept (never the discard-on-timeout shape of attempt 1, where
|
|
394
|
+
`got.append(sys.stdin.read())` only ran on `read()` RETURNING, so a join
|
|
395
|
+
timeout threw away every buffered byte).
|
|
396
|
+
|
|
397
|
+
The reader thread is abandoned, not joined, on every exit from the loop
|
|
398
|
+
below except a clean EOF: it may still be parked in a read when this
|
|
399
|
+
function returns (a producer that never closes its end), and it holds
|
|
400
|
+
nothing but stdin, so leaving it for the daemon-thread interpreter-exit
|
|
401
|
+
path to reap is the correct outcome -- the same trade the previous
|
|
402
|
+
reader already made for exactly the same reason, and now safe to make
|
|
403
|
+
(see the `os.read` paragraph above) because nothing it holds blocks
|
|
404
|
+
that exit any more.
|
|
405
|
+
"""
|
|
406
|
+
try:
|
|
407
|
+
fd: int | None = buffer.fileno() # type: ignore[attr-defined]
|
|
408
|
+
except (AttributeError, OSError, ValueError):
|
|
409
|
+
fd = None
|
|
410
|
+
|
|
411
|
+
chunk_queue: queue.Queue[bytes] = queue.Queue(maxsize=_STDIN_DRAIN_QUEUE_MAXSIZE)
|
|
412
|
+
|
|
413
|
+
def _read_one_chunk() -> bytes:
|
|
414
|
+
if fd is not None:
|
|
415
|
+
return os.read(fd, chunk_bytes)
|
|
416
|
+
return buffer.read1(chunk_bytes) # type: ignore[attr-defined]
|
|
417
|
+
|
|
418
|
+
def _drain() -> None:
|
|
419
|
+
# `put_nowait` + discard-on-`Full`, not a blocking `put` against a
|
|
420
|
+
# bounded queue: once the main loop below has broken out of its own
|
|
421
|
+
# `while True` (a bound fired) it never calls `.get()` again, so a
|
|
422
|
+
# blocking `put()` here would just move the "abandoned daemon thread
|
|
423
|
+
# parked in a blocking call" risk from the read syscall to the
|
|
424
|
+
# queue's own lock instead of removing it -- the exact shape this
|
|
425
|
+
# function's `os.read` fix (see its docstring) exists to avoid, only
|
|
426
|
+
# one layer up. Discarding keeps this thread doing exactly what it
|
|
427
|
+
# was already doing (repeatedly calling `_read_one_chunk`) with
|
|
428
|
+
# nothing new to block on, so it drains the pipe (the fix a real OS
|
|
429
|
+
# pipe's own backpressure gives a writer) without retaining what it
|
|
430
|
+
# reads once nobody is left to consume it.
|
|
431
|
+
try:
|
|
432
|
+
while True:
|
|
433
|
+
chunk = _read_one_chunk()
|
|
434
|
+
try:
|
|
435
|
+
chunk_queue.put_nowait(chunk)
|
|
436
|
+
except queue.Full:
|
|
437
|
+
pass
|
|
438
|
+
if not chunk:
|
|
439
|
+
return
|
|
440
|
+
except (OSError, ValueError): # pragma: no cover - env-dependent
|
|
441
|
+
try:
|
|
442
|
+
chunk_queue.put_nowait(b"")
|
|
443
|
+
except queue.Full:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
reader = threading.Thread(target=_drain, daemon=True)
|
|
447
|
+
reader.start()
|
|
448
|
+
|
|
449
|
+
chunks: list[bytes] = []
|
|
450
|
+
total = 0
|
|
451
|
+
bound: str | None = None
|
|
452
|
+
start = time.monotonic()
|
|
453
|
+
while True:
|
|
454
|
+
remaining_total = total_s - (time.monotonic() - start)
|
|
455
|
+
if remaining_total <= 0:
|
|
456
|
+
bound = "total-cap"
|
|
457
|
+
break
|
|
458
|
+
wait = min(idle_s, remaining_total)
|
|
459
|
+
try:
|
|
460
|
+
chunk = chunk_queue.get(timeout=wait)
|
|
461
|
+
except queue.Empty:
|
|
462
|
+
# Which bound actually expired depends on which of the two was
|
|
463
|
+
# the shorter (and therefore limiting) wait, NOT on which one is
|
|
464
|
+
# checked first above: if `remaining_total` was the smaller of
|
|
465
|
+
# the two, the wait timed out because the TOTAL deadline was
|
|
466
|
+
# imminent, not because the reader went idle for a full
|
|
467
|
+
# `idle_s` -- mislabelling that as "idle" would misreport a
|
|
468
|
+
# total-cap termination on a producer whose chunks are only
|
|
469
|
+
# slightly slower than `idle_s - remaining_total`.
|
|
470
|
+
bound = "total-cap" if wait < idle_s else "idle"
|
|
471
|
+
break
|
|
472
|
+
if not chunk:
|
|
473
|
+
break # clean EOF -- no bound fired, whatever arrived is complete
|
|
474
|
+
chunks.append(chunk)
|
|
475
|
+
total += len(chunk)
|
|
476
|
+
if total >= byte_cap:
|
|
477
|
+
bound = "byte-cap"
|
|
478
|
+
break
|
|
479
|
+
|
|
480
|
+
data = b"".join(chunks)
|
|
481
|
+
if bound == "byte-cap":
|
|
482
|
+
data = data[:byte_cap]
|
|
483
|
+
return _StdinReadOutcome(data=data, bound=bound)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _stdin_bound_message(bound: str, bytes_read: int) -> str:
|
|
487
|
+
"""The stderr/`issues[]` announcement for whichever bound
|
|
488
|
+
`_read_stdin_bounded` fired (tan-cli#537 constraint 4): names the bound
|
|
489
|
+
BY NAME and the remedy -- `--file -` is already the explicit
|
|
490
|
+
read-to-EOF path, the unbounded opt-in a truncated caller should be told
|
|
491
|
+
to reach for. A truncated decode the engineer is told about is a
|
|
492
|
+
recoverable inconvenience; a silent one is a confident wrong root
|
|
493
|
+
cause, which is the whole cost this design refuses to pay silently."""
|
|
494
|
+
remedy = "use `--file -` to read the complete dump instead (reads to EOF, no bound)"
|
|
495
|
+
if bound == "idle":
|
|
496
|
+
return (
|
|
497
|
+
f"stdin idle for {_STDIN_IDLE_TIMEOUT_S:g}s after {bytes_read} byte(s) "
|
|
498
|
+
f"without reaching EOF -- decoded what arrived; {remedy}."
|
|
499
|
+
)
|
|
500
|
+
if bound == "byte-cap":
|
|
501
|
+
return (
|
|
502
|
+
f"stdin exceeded the {_STDIN_BYTE_CAP}-byte cap without reaching EOF -- "
|
|
503
|
+
f"decoded the first {_STDIN_BYTE_CAP} bytes only; {remedy}."
|
|
504
|
+
)
|
|
505
|
+
if bound == "total-cap":
|
|
506
|
+
return (
|
|
507
|
+
f"stdin did not reach EOF within {_STDIN_TOTAL_TIMEOUT_S:g}s -- decoded "
|
|
508
|
+
f"the {bytes_read} byte(s) read so far; {remedy}."
|
|
509
|
+
)
|
|
510
|
+
raise AssertionError(f"unknown stdin bound: {bound!r}") # pragma: no cover
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@dataclass(frozen=True)
|
|
514
|
+
class ImplicitStdinResult:
|
|
515
|
+
"""Result of the IMPLICIT (unnamed, auto-consume) stdin read (tan-cli#537)
|
|
516
|
+
-- `... | tan faultdecode`, no `--file` on the command line at all.
|
|
517
|
+
|
|
518
|
+
Never raises: a detached/replaced/absent stdin degrades to
|
|
519
|
+
`attempted=False, text=""`, the same "nothing to offer" answer a real
|
|
520
|
+
closed pipe gives, because the implicit path's whole job is "just work
|
|
521
|
+
when there is something to read, fall through cleanly when there is
|
|
522
|
+
not" -- `--file -` is the surface that raises on a stdin it cannot
|
|
523
|
+
serve, because THAT read was named explicitly.
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
#: The decoded dump text -- "" when nothing was read at all.
|
|
527
|
+
text: str
|
|
528
|
+
#: Which bound fired during the read, or `None` when either no read was
|
|
529
|
+
#: attempted or the read reached a clean EOF with no bound needed.
|
|
530
|
+
bound: str | None
|
|
531
|
+
#: Whether a background read was actually started (False for a TTY, a
|
|
532
|
+
#: detached/`None` stdin, or a text-only replacement with no `.buffer`).
|
|
533
|
+
attempted: bool
|
|
534
|
+
#: How many bytes were actually read (0 when not attempted, or when an
|
|
535
|
+
#: attempted read got nothing before its idle bound fired).
|
|
536
|
+
bytes_read: int
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _read_implicit_stdin() -> ImplicitStdinResult:
|
|
540
|
+
"""The IMPLICIT stdin reader tan-cli#537 exists to redesign.
|
|
541
|
+
|
|
542
|
+
Reads `sys.stdin.buffer` as BYTES via `_read_stdin_bounded`, accumulates,
|
|
543
|
+
and decodes EXACTLY ONCE at the end with `bytes.decode("utf-8",
|
|
544
|
+
errors="ignore")` -- byte-identical to what `--file <path>` already does
|
|
545
|
+
via `Path.read_text(encoding="utf-8", errors="ignore")` (same codec, same
|
|
546
|
+
error handler), so decode parity with `--file` stops being a property
|
|
547
|
+
tested for and becomes one that cannot be violated: there is one decode
|
|
548
|
+
call, on the complete buffer, with the same two operands on both paths.
|
|
549
|
+
This is also what deletes the entire class that produced attempts 3 and
|
|
550
|
+
5: no chunk boundary exists for a multi-byte sequence to split across (no
|
|
551
|
+
decode happens per chunk at all), so no `UnicodeDecodeError` can be
|
|
552
|
+
raised mid-read for an `except` to swallow, and nothing here ever calls
|
|
553
|
+
`.reconfigure()`, so its `io.UnsupportedOperation` on an already-read
|
|
554
|
+
stream never arises either.
|
|
555
|
+
|
|
556
|
+
Routes the TTY/detached/replaced-stream checks through this repo's own
|
|
557
|
+
guarded probes rather than hand-rolling a second copy of either
|
|
558
|
+
(tan-cli#488's `_TeeStderr` class of bug): `stdin_is_tty()` -- not a bare
|
|
559
|
+
`sys.stdin.isatty()` -- absorbs both `sys.stdin is None` and a replaced
|
|
560
|
+
stream with no `.isatty()` at all. Reading `.buffer` adds ONE further
|
|
561
|
+
shape neither existing probe covers: a text-only replacement stream
|
|
562
|
+
(e.g. a test harness's `io.StringIO`) has no `.buffer` attribute at all
|
|
563
|
+
-- `getattr(..., "buffer", None)` degrades that to `attempted=False`
|
|
564
|
+
cleanly, matching every other "nothing to offer" case, rather than
|
|
565
|
+
raising `AttributeError` from inside a background thread.
|
|
566
|
+
"""
|
|
567
|
+
if sys.stdin is None or stdin_is_tty():
|
|
568
|
+
return ImplicitStdinResult(text="", bound=None, attempted=False, bytes_read=0)
|
|
569
|
+
buf = getattr(sys.stdin, "buffer", None)
|
|
570
|
+
if buf is None:
|
|
571
|
+
return ImplicitStdinResult(text="", bound=None, attempted=False, bytes_read=0)
|
|
572
|
+
outcome = _read_stdin_bounded(buf)
|
|
573
|
+
text = outcome.data.decode("utf-8", errors="ignore")
|
|
574
|
+
return ImplicitStdinResult(
|
|
575
|
+
text=text, bound=outcome.bound, attempted=True, bytes_read=len(outcome.data)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _read_dump(file_: str | None) -> str:
|
|
580
|
+
"""Read a pasted dump from `--file <path>` or `--file -` (stdin to EOF).
|
|
581
|
+
|
|
582
|
+
Returns `""` when `file_` is `None`: the IMPLICIT auto-consume path
|
|
583
|
+
(no `--file` on the command line at all) is `_read_implicit_stdin`
|
|
584
|
+
above, a SEPARATE function since tan-cli#537 -- folding it into this one
|
|
585
|
+
(the pre-#537 shape) is what let a module-global stash of pre-read bytes
|
|
586
|
+
(`_PREREAD_STDIN`) survive between invocations; splitting it out means
|
|
587
|
+
this function needs no such stash at all.
|
|
588
|
+
|
|
589
|
+
`--file -` is the EXPLICIT unbounded opt-in `_stdin_bound_message` tells
|
|
590
|
+
a truncated implicit-read caller to reach for: it reads to EOF with NO
|
|
591
|
+
idle/byte/total bound, whatever else was on the command line, because
|
|
592
|
+
the caller named this read specifically and a dump has no terminator
|
|
593
|
+
other than EOF. Reads `.buffer` and decodes once with the same
|
|
594
|
+
`errors="ignore"` UTF-8 decode `--file <path>` and the implicit path
|
|
595
|
+
both use, rather than the previous text-layer `sys.stdin.read()`, which
|
|
596
|
+
could itself raise `UnicodeDecodeError` on a non-UTF-8 host and broke
|
|
597
|
+
decode parity a second way.
|
|
598
|
+
|
|
599
|
+
tan-cli#488 round 5 class sweep: `sys.stdin` itself, not just the result
|
|
600
|
+
of calling `.isatty()` on it, can be `None` -- a process launched with
|
|
601
|
+
its standard handles detached (a GUI launcher, a `pythonw`-style spawn,
|
|
602
|
+
or a shell that closed fd 0 before exec). `--file -` names stdin
|
|
603
|
+
explicitly, so a detached stdin (or one replaced by a text-only stream
|
|
604
|
+
with no `.buffer`) is refused with a clear message rather than a raw
|
|
605
|
+
`AttributeError`.
|
|
606
|
+
"""
|
|
607
|
+
if file_ is None:
|
|
608
|
+
return ""
|
|
609
|
+
if file_ == "-":
|
|
610
|
+
if sys.stdin is None:
|
|
611
|
+
raise typer.BadParameter(
|
|
612
|
+
"stdin is detached (no standard input handle for this process), "
|
|
613
|
+
"so `--file -` cannot read a dump from it",
|
|
614
|
+
param_hint="--file",
|
|
615
|
+
)
|
|
616
|
+
buf = getattr(sys.stdin, "buffer", None)
|
|
617
|
+
if buf is None:
|
|
618
|
+
raise typer.BadParameter(
|
|
619
|
+
"stdin has no binary buffer to read from (it has been replaced "
|
|
620
|
+
"by a text-only stream), so `--file -` cannot read a dump from it",
|
|
621
|
+
param_hint="--file",
|
|
622
|
+
)
|
|
623
|
+
return buf.read().decode("utf-8", errors="ignore")
|
|
624
|
+
return Path(file_).read_text(encoding="utf-8", errors="ignore")
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _check_elf_path(value: str | None) -> Path | None:
|
|
629
|
+
"""`click.Path(exists=True, dir_okay=False)` equivalent for `--elf`."""
|
|
630
|
+
if value is None:
|
|
631
|
+
return None
|
|
632
|
+
path = Path(value)
|
|
633
|
+
if not path.exists():
|
|
634
|
+
raise typer.BadParameter(f"'{value}' does not exist.", param_hint="--elf")
|
|
635
|
+
if path.is_dir():
|
|
636
|
+
raise typer.BadParameter(f"'{value}' is a directory.", param_hint="--elf")
|
|
637
|
+
return path
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _check_file_path(value: str | None) -> str | None:
|
|
641
|
+
"""`click.Path(exists=True, dir_okay=False, allow_dash=True)` equivalent for
|
|
642
|
+
`--file`: `-` is passed through untouched (means stdin, see `_read_dump`)."""
|
|
643
|
+
if value is None or value == "-":
|
|
644
|
+
return value
|
|
645
|
+
path = Path(value)
|
|
646
|
+
if not path.exists():
|
|
647
|
+
raise typer.BadParameter(f"'{value}' does not exist.", param_hint="--file")
|
|
648
|
+
if path.is_dir():
|
|
649
|
+
raise typer.BadParameter(f"'{value}' is a directory.", param_hint="--file")
|
|
650
|
+
return value
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def faultdecode(
|
|
654
|
+
ctx: typer.Context,
|
|
655
|
+
cfsr: str = typer.Option(None, "--cfsr", help="Configurable Fault Status Register."),
|
|
656
|
+
hfsr: str = typer.Option(None, "--hfsr", help="HardFault Status Register."),
|
|
657
|
+
dfsr: str = typer.Option(None, "--dfsr", help="Debug Fault Status Register (optional)."),
|
|
658
|
+
bfar: str = typer.Option(None, "--bfar", help="BusFault Address Register."),
|
|
659
|
+
mmfar: str = typer.Option(None, "--mmfar", help="MemManage Fault Address Register."),
|
|
660
|
+
mmfsr: str = typer.Option(
|
|
661
|
+
None, "--mmfsr", help="MemManage sub-register (composed into CFSR)."
|
|
662
|
+
),
|
|
663
|
+
bfsr: str = typer.Option(None, "--bfsr", help="BusFault sub-register (composed into CFSR)."),
|
|
664
|
+
ufsr: str = typer.Option(
|
|
665
|
+
None, "--ufsr", help="UsageFault sub-register (composed into CFSR)."
|
|
666
|
+
),
|
|
667
|
+
pc: str = typer.Option(None, "--pc", help="Program Counter (symbolicated with --elf)."),
|
|
668
|
+
lr: str = typer.Option(None, "--lr", help="Link Register (symbolicated with --elf)."),
|
|
669
|
+
elf: str = typer.Option(
|
|
670
|
+
None,
|
|
671
|
+
"--elf",
|
|
672
|
+
help="ELF for --pc/--lr symbolication (best-effort; skipped if no tool/elf).",
|
|
673
|
+
),
|
|
674
|
+
file_: str = typer.Option(
|
|
675
|
+
None,
|
|
676
|
+
"--file",
|
|
677
|
+
help="Read a pasted dump from this file ('-' for stdin) and grep registers.",
|
|
678
|
+
),
|
|
679
|
+
as_json: bool = typer.Option(False, "--json", help="Emit a machine-readable JSON report."),
|
|
680
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable ANSI colours."),
|
|
681
|
+
project: str = typer.Option(
|
|
682
|
+
None,
|
|
683
|
+
"--project",
|
|
684
|
+
metavar="PATH",
|
|
685
|
+
help=inert_help(
|
|
686
|
+
"Project root. Not read: faultdecode is pure ARMv8-M register "
|
|
687
|
+
"arithmetic and opens no project.",
|
|
688
|
+
NOT_APPLICABLE,
|
|
689
|
+
),
|
|
690
|
+
),
|
|
691
|
+
sdk_root: str = typer.Option(
|
|
692
|
+
None,
|
|
693
|
+
"--sdk-root",
|
|
694
|
+
metavar="PATH",
|
|
695
|
+
help=inert_help(
|
|
696
|
+
"alp-sdk checkout root. Not read: faultdecode is pure ARMv8-M "
|
|
697
|
+
"register arithmetic and drives no alp-sdk checkout.",
|
|
698
|
+
NOT_APPLICABLE,
|
|
699
|
+
),
|
|
700
|
+
),
|
|
701
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
702
|
+
board_yaml: str = typer.Option(None, "--board-yaml", hidden=True),
|
|
703
|
+
target: str = typer.Option(None, "--target", hidden=True),
|
|
704
|
+
all_targets: bool = typer.Option(False, "--all", hidden=True),
|
|
705
|
+
verbose: bool = typer.Option(False, "--verbose", hidden=True),
|
|
706
|
+
quiet: bool = typer.Option(False, "--quiet", hidden=True),
|
|
707
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", hidden=True),
|
|
708
|
+
ci: bool = typer.Option(False, "--ci", hidden=True),
|
|
709
|
+
) -> None:
|
|
710
|
+
"""Decode an ARM Cortex-M (ARMv8-M) fault dump.
|
|
711
|
+
|
|
712
|
+
Supply registers as flags, and/or paste a dump via ``--file``/stdin and it
|
|
713
|
+
greps the register names out. Explicit flags win over a parsed dump.
|
|
714
|
+
|
|
715
|
+
`--project`/`--sdk-root`/`--board-yaml`/`--target`/`--all`/`--verbose`/
|
|
716
|
+
`--quiet`/`--non-interactive`/`--ci` are declared, not consumed: this
|
|
717
|
+
command reads no board.yaml and drives no alp-sdk checkout -- it is pure
|
|
718
|
+
ARMv8-M register arithmetic, same as the SDK original it replaces -- but
|
|
719
|
+
the oracle's clap `GlobalArgs` are `global = true`, so every verb
|
|
720
|
+
(`faultdecode` included) accepts all of them; a caller (or a saved
|
|
721
|
+
script) that passes any through unconditionally must not get a parse
|
|
722
|
+
error -- `tan faultdecode --ci ...` exits the same with or without `--ci`
|
|
723
|
+
on the oracle. `--no-color` is the one exception in this group with real
|
|
724
|
+
meaning (see `_use_color`), and `--format` is documented separately
|
|
725
|
+
below.
|
|
726
|
+
|
|
727
|
+
`--format` is accepted BEFORE the subcommand too (`tan --format json
|
|
728
|
+
faultdecode ...`), same as `debug-config`; the root callback records it on
|
|
729
|
+
`ctx.obj` and this option overrides it when repeated after the command
|
|
730
|
+
name. Both positions land on the same shape: tan's own envelope, with the
|
|
731
|
+
decode report as `data`, exactly like `debug-config`'s (tan-cli#399). The
|
|
732
|
+
oracle instead maps the global `--format json` onto the forwarded child's
|
|
733
|
+
`--json` (`crates/tan-cli/src/commands/sdk_cli.rs`) and prints the
|
|
734
|
+
unwrapped report for both spellings -- a deliberate divergence, taken
|
|
735
|
+
because `--format json` is the surface the vscode extension parses and it
|
|
736
|
+
rejects any document without `command`/`ok`/`exitCode`/`issues[]`. The
|
|
737
|
+
unwrapped report is unchanged and still one flag away: `--json`, which
|
|
738
|
+
also wins when both are given.
|
|
739
|
+
"""
|
|
740
|
+
del board_yaml, target, all_targets, verbose, quiet, non_interactive, ci
|
|
741
|
+
resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
|
|
742
|
+
# `--json` wins over `--format json`: it is the older, unwrapped surface
|
|
743
|
+
# every saved script and the Rust forwarder use (tan-cli#399).
|
|
744
|
+
envelope_mode = resolved_format == "json" and not as_json
|
|
745
|
+
|
|
746
|
+
elf_path = _check_elf_path(elf)
|
|
747
|
+
file_value = _check_file_path(file_)
|
|
748
|
+
|
|
749
|
+
# 1. Gather registers. The FLAGS are parsed first, before any dump is read
|
|
750
|
+
# (tan-cli#388): reading stdin first meant `--cfsr 0x8200` -- the
|
|
751
|
+
# primary documented invocation, where no dump is needed or wanted --
|
|
752
|
+
# still blocked on a stdin nobody was ever going to write to.
|
|
753
|
+
try:
|
|
754
|
+
cfsr_i = _parse_hexint("cfsr", cfsr)
|
|
755
|
+
hfsr_i = _parse_hexint("hfsr", hfsr)
|
|
756
|
+
dfsr_i = _parse_hexint("dfsr", dfsr)
|
|
757
|
+
bfar_i = _parse_hexint("bfar", bfar)
|
|
758
|
+
mmfar_i = _parse_hexint("mmfar", mmfar)
|
|
759
|
+
mmfsr_i = _parse_hexint("mmfsr", mmfsr)
|
|
760
|
+
bfsr_i = _parse_hexint("bfsr", bfsr)
|
|
761
|
+
ufsr_i = _parse_hexint("ufsr", ufsr)
|
|
762
|
+
pc_i = _parse_hexint("pc", pc)
|
|
763
|
+
lr_i = _parse_hexint("lr", lr)
|
|
764
|
+
except NegativeRegisterValue as err:
|
|
765
|
+
raise _refuse(
|
|
766
|
+
"faultdecode.invalid-register-value", err.message, envelope_mode=envelope_mode
|
|
767
|
+
) from None
|
|
768
|
+
|
|
769
|
+
# Read the dump UNCONDITIONALLY (tan-cli#503, defect 1) -- not gated on
|
|
770
|
+
# whether any register flag was also given. A prior version skipped this
|
|
771
|
+
# read whenever ANY of cfsr/hfsr/dfsr/bfar/mmfar/mmfsr/bfsr/ufsr was
|
|
772
|
+
# supplied, which broke the command's own documented contract ("Explicit
|
|
773
|
+
# flags win over a parsed dump" only holds if the dump is still read when
|
|
774
|
+
# flags are present) two ways: a piped CFSR/BFAR was silently dropped in
|
|
775
|
+
# favour of the flag's registers alone (measured: `... | tan faultdecode
|
|
776
|
+
# --hfsr 0x40000000` reported a self-contradictory "Forced HardFault ...
|
|
777
|
+
# its own status bits are clear" while the piped CFSR=0x00008200/
|
|
778
|
+
# BFAR=0xdeadbeef it never read said otherwise), and --bfar/--mmfar
|
|
779
|
+
# counted as "a register was given" without satisfying the cfsr/hfsr/dfsr
|
|
780
|
+
# gate below, so the dump was skipped AND the command still refused with
|
|
781
|
+
# `faultdecode.no-registers`.
|
|
782
|
+
#
|
|
783
|
+
# `--file` (path or `-`) is explicit and unbounded; with no `--file` at
|
|
784
|
+
# all the IMPLICIT path (tan-cli#537) reads `sys.stdin.buffer` on a
|
|
785
|
+
# background thread bounded three ways (idle/byte-cap/total, see
|
|
786
|
+
# `_read_stdin_bounded`), so making this unconditional does not reopen
|
|
787
|
+
# tan-cli#388's unbounded hang -- measured,
|
|
788
|
+
# `test_registers_on_the_command_line_never_wait_for_an_open_stdin_pipe`
|
|
789
|
+
# (a real held-open OS pipe, not `CliRunner`) still returns in well under
|
|
790
|
+
# a second, not the 20 s bound it fails at.
|
|
791
|
+
implicit_bound: str | None = None
|
|
792
|
+
implicit_bytes = 0
|
|
793
|
+
implicit_silent = False
|
|
794
|
+
if file_value is not None:
|
|
795
|
+
dump_text = _read_dump(file_value)
|
|
796
|
+
else:
|
|
797
|
+
implicit = _read_implicit_stdin()
|
|
798
|
+
dump_text = implicit.text
|
|
799
|
+
implicit_bound = implicit.bound
|
|
800
|
+
implicit_bytes = implicit.bytes_read
|
|
801
|
+
implicit_silent = implicit.attempted and implicit.bound == "idle" and implicit.bytes_read == 0
|
|
802
|
+
# tan-cli#537 constraint 4: whichever bound fired on the IMPLICIT stdin
|
|
803
|
+
# read, announce it -- on stderr in EVERY mode (not only `--format json`'s
|
|
804
|
+
# `issues[]`), naming the bound and the `--file -` remedy. Only when
|
|
805
|
+
# something was actually read: a bound firing on zero bytes is the
|
|
806
|
+
# separate "silent holder" refusal (`implicit_silent` above), not a
|
|
807
|
+
# truncation. Built HERE, before the no-registers check below, so a bound
|
|
808
|
+
# that fires with bytes read but no complete register set still gets
|
|
809
|
+
# this announcement on the refusal path -- not only on the success path
|
|
810
|
+
# that used to be the only place this list was built.
|
|
811
|
+
stdin_issues: list[Issue] = []
|
|
812
|
+
if implicit_bound is not None and implicit_bytes > 0:
|
|
813
|
+
stdin_bound_message = _stdin_bound_message(implicit_bound, implicit_bytes)
|
|
814
|
+
stdin_issues.append(Issue("faultdecode.stdin-truncated", "warning", stdin_bound_message))
|
|
815
|
+
parsed: dict[str, int] = parse_dump(dump_text) if dump_text else {}
|
|
816
|
+
|
|
817
|
+
def pick(name: str, flag_val: int | None) -> int | None:
|
|
818
|
+
return flag_val if flag_val is not None else parsed.get(name)
|
|
819
|
+
|
|
820
|
+
cfsr_v = pick("cfsr", cfsr_i)
|
|
821
|
+
# Compose CFSR from explicit sub-registers when no combined CFSR was given.
|
|
822
|
+
if cfsr_i is None and (mmfsr_i is not None or bfsr_i is not None or ufsr_i is not None):
|
|
823
|
+
cfsr_v = (
|
|
824
|
+
((mmfsr_i or 0) & 0xFF)
|
|
825
|
+
| (((bfsr_i or 0) & 0xFF) << 8)
|
|
826
|
+
| (((ufsr_i or 0) & 0xFFFF) << 16)
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
hfsr_v = pick("hfsr", hfsr_i)
|
|
830
|
+
dfsr_v = pick("dfsr", dfsr_i)
|
|
831
|
+
bfar_v = pick("bfar", bfar_i)
|
|
832
|
+
mmfar_v = pick("mmfar", mmfar_i)
|
|
833
|
+
pc_v = pick("pc", pc_i)
|
|
834
|
+
lr_v = pick("lr", lr_i)
|
|
835
|
+
|
|
836
|
+
# No status registers at all => bad input (this is an analysis tool, but it
|
|
837
|
+
# needs *something* to analyse). Exit nonzero with a usage hint.
|
|
838
|
+
if cfsr_v is None and hfsr_v is None and dfsr_v is None:
|
|
839
|
+
if implicit_silent:
|
|
840
|
+
# tan-cli#537: distinguishes "no stdin at all" (TTY, detached, a
|
|
841
|
+
# closed pipe already at EOF) from "a pipe WAS open and offered
|
|
842
|
+
# nothing" -- today's refusal could not tell the two apart, and
|
|
843
|
+
# the second one is the shape a caller can actually fix by
|
|
844
|
+
# waiting longer (`--file -`) rather than by piping something in
|
|
845
|
+
# the first place.
|
|
846
|
+
no_registers = (
|
|
847
|
+
"no fault registers supplied, and stdin was open but silent for "
|
|
848
|
+
f"{_STDIN_IDLE_TIMEOUT_S:g}s -- pass --cfsr/--hfsr/--dfsr, or use "
|
|
849
|
+
"`--file -` to wait for a dump indefinitely."
|
|
850
|
+
)
|
|
851
|
+
elif stdin_issues:
|
|
852
|
+
# tan-cli#537 constraint 4's hole: a bound fired WITH bytes read
|
|
853
|
+
# (unlike `implicit_silent` above, which is zero bytes), but
|
|
854
|
+
# nothing in what arrived parsed into a complete register set.
|
|
855
|
+
# The plain "no fault registers supplied" wording below would
|
|
856
|
+
# tell this caller they supplied nothing at all, when tan in
|
|
857
|
+
# fact stopped reading part-way through a real dump -- distinct
|
|
858
|
+
# from "no dump arrived" and the exact misdirection the issue
|
|
859
|
+
# calls out as worse than the truncation itself.
|
|
860
|
+
no_registers = (
|
|
861
|
+
"no fault registers supplied -- stdin delivered data, but tan "
|
|
862
|
+
f"stopped reading part-way through it: {stdin_issues[0].message}"
|
|
863
|
+
)
|
|
864
|
+
else:
|
|
865
|
+
no_registers = (
|
|
866
|
+
"no fault registers supplied -- pass --cfsr/--hfsr/--dfsr "
|
|
867
|
+
"or pipe a dump via --file/-/stdin."
|
|
868
|
+
)
|
|
869
|
+
# The refusal has to agree with the success path about whether stdout is
|
|
870
|
+
# an envelope (tan-cli#399); leaving it to `cli.main`'s generic fallback
|
|
871
|
+
# gave the consumer `command: "cli"` here and `command: "faultdecode"`
|
|
872
|
+
# one invocation later. `_refuse` is that agreement, shared with the
|
|
873
|
+
# negative-register refusal above. `extra_issues=stdin_issues` is this
|
|
874
|
+
# branch's own fix for constraint 4's hole: without it, a bound firing
|
|
875
|
+
# on a would-be refusal announced nowhere at all.
|
|
876
|
+
raise _refuse(
|
|
877
|
+
"faultdecode.no-registers",
|
|
878
|
+
no_registers,
|
|
879
|
+
envelope_mode=envelope_mode,
|
|
880
|
+
extra_issues=stdin_issues or None,
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
report = decode(
|
|
884
|
+
cfsr=cfsr_v or 0,
|
|
885
|
+
hfsr=hfsr_v or 0,
|
|
886
|
+
dfsr=dfsr_v or 0,
|
|
887
|
+
bfar=bfar_v,
|
|
888
|
+
mmfar=mmfar_v,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# 2. Best-effort symbolication (optional, never fatal).
|
|
892
|
+
symbols: dict[str, Symbol] = {}
|
|
893
|
+
if elf_path is not None:
|
|
894
|
+
for which, addr in (("pc", pc_v), ("lr", lr_v)):
|
|
895
|
+
if addr is not None:
|
|
896
|
+
sym = resolve_symbol(addr, elf_path)
|
|
897
|
+
if sym is not None:
|
|
898
|
+
symbols[which] = sym
|
|
899
|
+
|
|
900
|
+
# `stdin_issues` was already built above (before the no-registers check),
|
|
901
|
+
# so the same list -- and the same announcement -- covers both the
|
|
902
|
+
# refusal path (via `_refuse`'s `extra_issues`) and this success path.
|
|
903
|
+
for stdin_issue in stdin_issues:
|
|
904
|
+
typer.echo(f"Warning: {stdin_issue.message}", err=True)
|
|
905
|
+
|
|
906
|
+
if as_json:
|
|
907
|
+
typer.echo(_json.dumps(report_to_json(report, symbols or None), indent=2))
|
|
908
|
+
elif envelope_mode:
|
|
909
|
+
# `project` is hardcoded null/null, not resolved: this command reads no
|
|
910
|
+
# board.yaml and drives no checkout (see the docstring's
|
|
911
|
+
# declared-not-consumed list), so reporting a root here would claim a
|
|
912
|
+
# resolution that never happened.
|
|
913
|
+
emit(
|
|
914
|
+
Envelope(
|
|
915
|
+
"faultdecode",
|
|
916
|
+
Project(root=None, board_yaml=None),
|
|
917
|
+
report_to_json(report, symbols or None),
|
|
918
|
+
stdin_issues,
|
|
919
|
+
ExitCode.SUCCESS,
|
|
920
|
+
)
|
|
921
|
+
)
|
|
922
|
+
else:
|
|
923
|
+
color = _use_color(no_color)
|
|
924
|
+
typer.echo(render_human(report, symbols or None, color))
|
|
925
|
+
if pc_v is not None and elf_path is None:
|
|
926
|
+
typer.echo(
|
|
927
|
+
" (note: --pc given without --elf -- pass --elf <app.elf> to resolve the symbol)",
|
|
928
|
+
err=True,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
# Analysis tool: a successful decode is always exit 0 (even "no flags set").
|