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,505 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan monitor --break-uboot`: the in-process session (tan-cli#1315).
|
|
3
|
+
|
|
4
|
+
Split out of `monitor_cmd.py` to keep that file small: option validation, the
|
|
5
|
+
port open, the break-in call, and the hand-over to an in-process
|
|
6
|
+
`serial.tools.miniterm.Miniterm` on the SAME open port. Reopening the port
|
|
7
|
+
after the break-in could toggle DTR/RTS on a USB-UART adapter and reset the
|
|
8
|
+
board, losing the prompt just caught, so the held port is never closed
|
|
9
|
+
between the loop and the console. Every path out of this module closes it.
|
|
10
|
+
|
|
11
|
+
`monitor_cmd` imports this module lazily (it needs `MonitorError`, defined
|
|
12
|
+
there), so the plain `tan monitor` path never loads it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import errno
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import stat
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
23
|
+
|
|
24
|
+
from tan.commands.monitor_cmd import DATA_SCHEMA_VERSION, MonitorError, _pyserial_missing
|
|
25
|
+
from tan.core import console_filter as console_filter_mod
|
|
26
|
+
from tan.core import serial_actions, serial_capture, uboot_breakin
|
|
27
|
+
from tan.envelope import Issue
|
|
28
|
+
from tan.exit_codes import ExitCode
|
|
29
|
+
|
|
30
|
+
#: A stalled rfc2217:// or socket:// endpoint must not block a key write past
|
|
31
|
+
#: the break-in deadline; one write waits at most this long.
|
|
32
|
+
WRITE_TIMEOUT_S = 1.0
|
|
33
|
+
|
|
34
|
+
BreakOpts = tuple[bytes, bytes, float]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _bad_option(message: str) -> MonitorError:
|
|
38
|
+
return MonitorError(
|
|
39
|
+
"monitor.break-bad-option",
|
|
40
|
+
message,
|
|
41
|
+
ExitCode.VALIDATION_FAILURE,
|
|
42
|
+
{"schemaVersion": DATA_SCHEMA_VERSION},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def break_opts(
|
|
47
|
+
break_uboot: bool, key: str | None, prompt: str | None, timeout_s: float | None
|
|
48
|
+
) -> BreakOpts | None:
|
|
49
|
+
"""Validate `--break-uboot` and its companions.
|
|
50
|
+
|
|
51
|
+
`--break-key`/`--prompt`/`--break-timeout` mean nothing without
|
|
52
|
+
`--break-uboot`, so giving one alone is refused rather than ignored.
|
|
53
|
+
"""
|
|
54
|
+
if not break_uboot:
|
|
55
|
+
if key is not None or prompt is not None or timeout_s is not None:
|
|
56
|
+
raise _bad_option("--break-key/--prompt/--break-timeout require --break-uboot")
|
|
57
|
+
return None
|
|
58
|
+
timeout_s = uboot_breakin.DEFAULT_TIMEOUT_S if timeout_s is None else timeout_s
|
|
59
|
+
try:
|
|
60
|
+
if not timeout_s > 0:
|
|
61
|
+
raise ValueError("--break-timeout must be greater than 0")
|
|
62
|
+
return (
|
|
63
|
+
uboot_breakin.parse_escaped(
|
|
64
|
+
uboot_breakin.DEFAULT_KEY.decode() if key is None else key, "--break-key"
|
|
65
|
+
),
|
|
66
|
+
uboot_breakin.parse_escaped(
|
|
67
|
+
uboot_breakin.DEFAULT_PROMPT.decode() if prompt is None else prompt, "--prompt"
|
|
68
|
+
),
|
|
69
|
+
timeout_s,
|
|
70
|
+
)
|
|
71
|
+
except ValueError as err:
|
|
72
|
+
raise _bad_option(str(err)) from err
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _stdin_is_tty() -> bool:
|
|
76
|
+
try:
|
|
77
|
+
return sys.stdin.isatty()
|
|
78
|
+
except (AttributeError, ValueError):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _GuardedWrites:
|
|
83
|
+
"""A view of `ser` whose `write` gives up after `WRITE_TIMEOUT_S`, for
|
|
84
|
+
ports whose pyserial backend rejects `write_timeout` (rfc2217://). The
|
|
85
|
+
stuck write is abandoned on a daemon thread and unblocks when the caller
|
|
86
|
+
closes the port."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, ser) -> None:
|
|
89
|
+
self._ser = ser
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def in_waiting(self) -> int:
|
|
93
|
+
return self._ser.in_waiting
|
|
94
|
+
|
|
95
|
+
def read(self, size: int = 1) -> bytes:
|
|
96
|
+
return self._ser.read(size)
|
|
97
|
+
|
|
98
|
+
def write(self, data: bytes) -> None:
|
|
99
|
+
failure: list[BaseException] = []
|
|
100
|
+
|
|
101
|
+
def go() -> None:
|
|
102
|
+
try:
|
|
103
|
+
self._ser.write(data)
|
|
104
|
+
except BaseException as err: # noqa: BLE001 (re-raised in the caller)
|
|
105
|
+
failure.append(err)
|
|
106
|
+
|
|
107
|
+
worker = threading.Thread(target=go, daemon=True)
|
|
108
|
+
worker.start()
|
|
109
|
+
worker.join(WRITE_TIMEOUT_S)
|
|
110
|
+
if worker.is_alive():
|
|
111
|
+
raise OSError(f"write stalled for more than {WRITE_TIMEOUT_S}s")
|
|
112
|
+
if failure:
|
|
113
|
+
raise failure[0]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def open_port(port: str, baud: int, capture: bool = False):
|
|
117
|
+
"""Open `port` in-process (local tty, by-id path or any pyserial URL) the
|
|
118
|
+
way miniterm does: no explicit DTR/RTS, pyserial's open-time defaults.
|
|
119
|
+
|
|
120
|
+
pyserial's `rfc2217://` rejects `write_timeout` (NotImplementedError), so
|
|
121
|
+
that case reopens without it and marks the port so the break-in loop
|
|
122
|
+
guards its writes itself (`_GuardedWrites`). Any other open failure is
|
|
123
|
+
reported as `monitor.break-open-failed`, never as an internal failure."""
|
|
124
|
+
try:
|
|
125
|
+
import serial # noqa: PLC0415 (optional at runtime)
|
|
126
|
+
except ImportError as err:
|
|
127
|
+
raise _pyserial_missing() from err
|
|
128
|
+
interval = uboot_breakin.DEFAULT_INTERVAL_S
|
|
129
|
+
try:
|
|
130
|
+
try:
|
|
131
|
+
return serial.serial_for_url(port, baud, timeout=interval, write_timeout=WRITE_TIMEOUT_S)
|
|
132
|
+
except NotImplementedError:
|
|
133
|
+
ser = serial.serial_for_url(port, baud, timeout=interval)
|
|
134
|
+
ser._tan_guard_writes = True
|
|
135
|
+
return ser
|
|
136
|
+
except Exception as err: # noqa: BLE001 -- anything an open can raise maps to one issue
|
|
137
|
+
data = {"schemaVersion": DATA_SCHEMA_VERSION, "port": port, "baud": baud}
|
|
138
|
+
if capture:
|
|
139
|
+
raise MonitorError(
|
|
140
|
+
"monitor.capture-open-failed",
|
|
141
|
+
f"could not open '{port}' for --capture: {err}",
|
|
142
|
+
ExitCode.RUNTIME_FAILURE,
|
|
143
|
+
data,
|
|
144
|
+
) from err
|
|
145
|
+
raise MonitorError(
|
|
146
|
+
"monitor.break-open-failed",
|
|
147
|
+
f"could not open '{port}' for --break-uboot: {err}",
|
|
148
|
+
ExitCode.RUNTIME_FAILURE,
|
|
149
|
+
data,
|
|
150
|
+
) from err
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def break_in(ser, port: str, baud: int, opts: BreakOpts) -> tuple[dict, bytes]:
|
|
154
|
+
"""Run the loop on the open `ser`; the port stays OPEN either way (the
|
|
155
|
+
caller owns closing it). Returns the envelope's `breakIn` block and the
|
|
156
|
+
raw tail of what was received (4 KiB at most)."""
|
|
157
|
+
key, prompt, timeout_s = opts
|
|
158
|
+
try:
|
|
159
|
+
target = _GuardedWrites(ser) if getattr(ser, "_tan_guard_writes", False) else ser
|
|
160
|
+
result = uboot_breakin.break_into_uboot(target, key=key, prompt=prompt, timeout_s=timeout_s)
|
|
161
|
+
except OSError as err: # includes pyserial's write/read timeouts
|
|
162
|
+
raise MonitorError(
|
|
163
|
+
"monitor.break-io-failed",
|
|
164
|
+
f"serial I/O failed on '{port}' during --break-uboot: {err}",
|
|
165
|
+
ExitCode.RUNTIME_FAILURE,
|
|
166
|
+
{"schemaVersion": DATA_SCHEMA_VERSION, "port": port, "baud": baud},
|
|
167
|
+
) from err
|
|
168
|
+
block = {
|
|
169
|
+
"caught": result.caught,
|
|
170
|
+
"elapsedSeconds": round(result.elapsed_s, 3),
|
|
171
|
+
"timeoutSeconds": timeout_s,
|
|
172
|
+
"bytesSeen": result.bytes_seen,
|
|
173
|
+
"bytesSeenTail": result.tail,
|
|
174
|
+
"keysSent": result.keys_sent,
|
|
175
|
+
"firstByteSeconds": None if result.first_byte_s is None else round(result.first_byte_s, 3),
|
|
176
|
+
}
|
|
177
|
+
return block, result.window
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def attach_miniterm(ser, json_mode: bool, console_filter: str = "colors") -> int:
|
|
181
|
+
"""Run the interactive console on the ALREADY-OPEN `ser` (no reopen).
|
|
182
|
+
|
|
183
|
+
Same session settings as `serial.tools.miniterm.main` except for the
|
|
184
|
+
options tan does not expose: exit Ctrl+], menu Ctrl+T, UTF-8, CRLF. The
|
|
185
|
+
output filter is `console_filter` (`--filter`, default tan's `colors`:
|
|
186
|
+
SGR colours render, every other escape is neutralised; see
|
|
187
|
+
`tan.core.console_filter`). Assumes tan is not running as `python -m tan`
|
|
188
|
+
from an untrusted project directory (that puts the cwd on `sys.path`
|
|
189
|
+
ahead of pyserial); the released `tan` is frozen, and the spawned plain
|
|
190
|
+
console runs from an empty cwd. Under `--format json` the
|
|
191
|
+
`Console` is built with `sys.stdout` pointed at stderr, the same rule
|
|
192
|
+
`monitor_cmd._child_stdout` states for the spawned path.
|
|
193
|
+
|
|
194
|
+
Closes `ser` on every path, including a `Console` that cannot be built
|
|
195
|
+
(it reads termios attributes off stdin).
|
|
196
|
+
"""
|
|
197
|
+
term = None
|
|
198
|
+
started = False
|
|
199
|
+
real_stdout = sys.stdout
|
|
200
|
+
try:
|
|
201
|
+
try:
|
|
202
|
+
from serial.tools import miniterm # noqa: PLC0415 (optional at runtime)
|
|
203
|
+
except ImportError as err:
|
|
204
|
+
raise _pyserial_missing() from err
|
|
205
|
+
try:
|
|
206
|
+
import termios # noqa: PLC0415 (POSIX only)
|
|
207
|
+
|
|
208
|
+
console_errors: tuple[type[BaseException], ...] = (OSError, ValueError, termios.error)
|
|
209
|
+
except ImportError:
|
|
210
|
+
console_errors = (OSError, ValueError)
|
|
211
|
+
try:
|
|
212
|
+
miniterm.TRANSFORMATIONS["colors"] = console_filter_mod.ColorsFilter
|
|
213
|
+
if json_mode:
|
|
214
|
+
sys.stdout = sys.__stderr__ if sys.__stderr__ is not None else sys.stderr
|
|
215
|
+
try:
|
|
216
|
+
term = miniterm.Miniterm(ser, echo=False, eol="crlf", filters=[console_filter])
|
|
217
|
+
finally:
|
|
218
|
+
sys.stdout = real_stdout
|
|
219
|
+
term.exit_character = chr(0x1D)
|
|
220
|
+
term.menu_character = chr(0x14)
|
|
221
|
+
term.raw = False
|
|
222
|
+
term.set_rx_encoding("UTF-8")
|
|
223
|
+
term.set_tx_encoding("UTF-8")
|
|
224
|
+
term.start()
|
|
225
|
+
started = True
|
|
226
|
+
try:
|
|
227
|
+
term.join(True)
|
|
228
|
+
except KeyboardInterrupt:
|
|
229
|
+
pass
|
|
230
|
+
term.join()
|
|
231
|
+
except console_errors as err:
|
|
232
|
+
raise MonitorError(
|
|
233
|
+
"monitor.launch-failed",
|
|
234
|
+
f"failed to run the console on the open port: {err}",
|
|
235
|
+
ExitCode.RUNTIME_FAILURE,
|
|
236
|
+
{"schemaVersion": DATA_SCHEMA_VERSION},
|
|
237
|
+
) from err
|
|
238
|
+
finally:
|
|
239
|
+
if term is not None:
|
|
240
|
+
if started:
|
|
241
|
+
term.stop() # reader/writer threads must not outlive a failed run
|
|
242
|
+
term.close()
|
|
243
|
+
ser.close()
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def run(
|
|
248
|
+
port: str,
|
|
249
|
+
baud: int,
|
|
250
|
+
json_mode: bool,
|
|
251
|
+
opts: BreakOpts,
|
|
252
|
+
non_interactive: bool,
|
|
253
|
+
console_filter: str = "colors",
|
|
254
|
+
) -> tuple[dict, list[Issue], ExitCode]:
|
|
255
|
+
"""`--break-uboot` end to end. `--non-interactive` stops after the
|
|
256
|
+
break-in (exit 0 if caught); otherwise the console takes over the
|
|
257
|
+
still-open port."""
|
|
258
|
+
base = {"schemaVersion": DATA_SCHEMA_VERSION, "port": port, "baud": baud}
|
|
259
|
+
if not non_interactive and not _stdin_is_tty():
|
|
260
|
+
raise MonitorError(
|
|
261
|
+
"monitor.no-tty",
|
|
262
|
+
"the interactive console needs a terminal on stdin; pass --non-interactive "
|
|
263
|
+
"to stop after the break-in.",
|
|
264
|
+
ExitCode.RUNTIME_FAILURE,
|
|
265
|
+
base,
|
|
266
|
+
)
|
|
267
|
+
ser = open_port(port, baud)
|
|
268
|
+
handed_over = False
|
|
269
|
+
data = base
|
|
270
|
+
try:
|
|
271
|
+
block, _raw = break_in(ser, port, baud, opts)
|
|
272
|
+
data = {**base, "breakIn": block}
|
|
273
|
+
if not block["caught"]:
|
|
274
|
+
return (
|
|
275
|
+
data,
|
|
276
|
+
[Issue(
|
|
277
|
+
"monitor.break-timeout",
|
|
278
|
+
"error",
|
|
279
|
+
f"no prompt `{opts[1].decode('utf-8', 'replace')}` within {opts[2]}s on {port}; "
|
|
280
|
+
"power-cycle the board and retry, or raise --break-timeout.",
|
|
281
|
+
)],
|
|
282
|
+
ExitCode.RUNTIME_FAILURE,
|
|
283
|
+
)
|
|
284
|
+
shown = opts[1].decode("utf-8", errors="replace")
|
|
285
|
+
print(f"monitor: caught prompt `{shown}` after {block['elapsedSeconds']}s", file=sys.stderr)
|
|
286
|
+
if non_interactive:
|
|
287
|
+
return data, [], ExitCode.SUCCESS
|
|
288
|
+
handed_over = True
|
|
289
|
+
except MonitorError as err:
|
|
290
|
+
err.data = {**err.data, **data}
|
|
291
|
+
raise
|
|
292
|
+
finally:
|
|
293
|
+
if not handed_over:
|
|
294
|
+
ser.close()
|
|
295
|
+
# attach_miniterm owns (and always closes) `ser` from here.
|
|
296
|
+
print(f"monitor: {port} @ {baud} (Ctrl+] to quit)", file=sys.stderr)
|
|
297
|
+
try:
|
|
298
|
+
attach_miniterm(ser, json_mode, console_filter)
|
|
299
|
+
except MonitorError as err:
|
|
300
|
+
err.data = {**err.data, **data}
|
|
301
|
+
raise
|
|
302
|
+
return data, [], ExitCode.SUCCESS
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
#: (compiled --until or None, duration seconds, --log path or None)
|
|
306
|
+
CaptureOpts = tuple["re.Pattern[str] | None", float, "str | None"]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def capture_opts(
|
|
310
|
+
capture: bool,
|
|
311
|
+
duration: float | None,
|
|
312
|
+
until: str | None,
|
|
313
|
+
log: str | None,
|
|
314
|
+
filter_given: bool = False,
|
|
315
|
+
) -> CaptureOpts | None:
|
|
316
|
+
"""Validate `--capture`/`--duration`/`--until`/`--log` (tan-cli#1324).
|
|
317
|
+
The companions are refused without `--capture` rather than ignored, and
|
|
318
|
+
`--filter` is refused WITH it (it shapes the interactive console; the log
|
|
319
|
+
always holds raw bytes). `--non-interactive` is accepted: capture never
|
|
320
|
+
prompts."""
|
|
321
|
+
try:
|
|
322
|
+
if not capture:
|
|
323
|
+
if duration is not None or until is not None or log is not None:
|
|
324
|
+
raise ValueError("--duration/--until/--log require --capture")
|
|
325
|
+
return None
|
|
326
|
+
if filter_given:
|
|
327
|
+
raise ValueError("--filter applies to the interactive console, not --capture")
|
|
328
|
+
pattern = serial_capture.compile_until(until) if until is not None else None
|
|
329
|
+
if duration is None:
|
|
330
|
+
if pattern is None:
|
|
331
|
+
raise ValueError("--capture needs --duration and/or --until")
|
|
332
|
+
duration = serial_capture.DEFAULT_UNTIL_DURATION_S
|
|
333
|
+
if not duration > 0:
|
|
334
|
+
raise ValueError("--duration must be greater than 0")
|
|
335
|
+
except ValueError as err:
|
|
336
|
+
raise MonitorError(
|
|
337
|
+
"monitor.capture-bad-option",
|
|
338
|
+
str(err),
|
|
339
|
+
ExitCode.VALIDATION_FAILURE,
|
|
340
|
+
{"schemaVersion": DATA_SCHEMA_VERSION},
|
|
341
|
+
) from err
|
|
342
|
+
return pattern, duration, log
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _open_log(log: str, data: dict):
|
|
346
|
+
"""Create the `--log` file: 0600, never through a symlink, truncating.
|
|
347
|
+
(The mode only applies when the file is created; an existing file keeps
|
|
348
|
+
its own.)"""
|
|
349
|
+
# O_NOFOLLOW only guards the LAST path component (and is a no-op on
|
|
350
|
+
# Windows). O_TRUNC is deferred until the type check so a FIFO/device is
|
|
351
|
+
# never truncated or blocked on.
|
|
352
|
+
flags = os.O_WRONLY | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
353
|
+
fd = None
|
|
354
|
+
try:
|
|
355
|
+
# O_NOFOLLOW is a no-op on Windows, so check the last component
|
|
356
|
+
# explicitly everywhere (and again after the open, to catch a swap).
|
|
357
|
+
try:
|
|
358
|
+
if os.path.islink(log):
|
|
359
|
+
raise OSError("refusing to write through a symlink")
|
|
360
|
+
except ValueError as err: # embedded NUL
|
|
361
|
+
raise OSError(str(err)) from err
|
|
362
|
+
try:
|
|
363
|
+
fd = os.open(log, flags, 0o600)
|
|
364
|
+
except OSError as err:
|
|
365
|
+
if err.errno == errno.ENXIO: # FIFO with no reader, or a device node
|
|
366
|
+
raise OSError("not a regular file") from err
|
|
367
|
+
raise
|
|
368
|
+
opened = os.fstat(fd)
|
|
369
|
+
if not stat.S_ISREG(opened.st_mode):
|
|
370
|
+
raise OSError("not a regular file")
|
|
371
|
+
try:
|
|
372
|
+
now = os.lstat(log)
|
|
373
|
+
except OSError:
|
|
374
|
+
now = None
|
|
375
|
+
if now is None or stat.S_ISLNK(now.st_mode) or not os.path.samestat(opened, now):
|
|
376
|
+
raise OSError("the log path changed while it was being opened")
|
|
377
|
+
os.ftruncate(fd, 0)
|
|
378
|
+
f = os.fdopen(fd, "wb")
|
|
379
|
+
fd = None
|
|
380
|
+
return f
|
|
381
|
+
except OSError as err:
|
|
382
|
+
if fd is not None:
|
|
383
|
+
os.close(fd)
|
|
384
|
+
raise _log_failed(log, err, data) from err
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def check_log_path(log: str, data: dict) -> str:
|
|
388
|
+
"""Validate `--log` WITHOUT opening or creating it, so a bad path is
|
|
389
|
+
refused before the (possibly slow) serial connect. Returns the absolute
|
|
390
|
+
path. The open itself re-checks (`_open_log`), closing the race."""
|
|
391
|
+
path = os.path.abspath(log)
|
|
392
|
+
try:
|
|
393
|
+
if os.path.lexists(path):
|
|
394
|
+
if os.path.islink(path):
|
|
395
|
+
raise OSError("refusing to write through a symlink")
|
|
396
|
+
if not stat.S_ISREG(os.stat(path).st_mode):
|
|
397
|
+
raise OSError("not a regular file")
|
|
398
|
+
if not os.access(path, os.W_OK):
|
|
399
|
+
raise OSError("permission denied")
|
|
400
|
+
else:
|
|
401
|
+
parent = os.path.dirname(path) or "."
|
|
402
|
+
if not os.path.isdir(parent):
|
|
403
|
+
raise OSError("parent directory does not exist")
|
|
404
|
+
if not os.access(parent, os.W_OK | os.X_OK):
|
|
405
|
+
raise OSError("parent directory is not writable")
|
|
406
|
+
except (OSError, ValueError) as err:
|
|
407
|
+
raise _log_failed(path, err, data) from err
|
|
408
|
+
return path
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _log_failed(log: str, err: BaseException, data: dict) -> MonitorError:
|
|
412
|
+
return MonitorError(
|
|
413
|
+
"monitor.capture-log-failed",
|
|
414
|
+
f"cannot write --log '{log}': {err}",
|
|
415
|
+
ExitCode.WRITE_FAILURE,
|
|
416
|
+
data,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def run_capture(
|
|
421
|
+
port: str,
|
|
422
|
+
baud: int,
|
|
423
|
+
cap: CaptureOpts,
|
|
424
|
+
opts: BreakOpts | None,
|
|
425
|
+
act: "serial_actions.ActionSpec | None" = None,
|
|
426
|
+
) -> tuple[dict, list[Issue], ExitCode]:
|
|
427
|
+
"""Headless capture on one in-process port: optional break-in first, then
|
|
428
|
+
read for the duration / until the regex matches. No TTY is needed. The
|
|
429
|
+
log (`--log`) is opened only after the port is open and any break-in has
|
|
430
|
+
succeeded, and always receives RAW bytes (no console filter), starting
|
|
431
|
+
with the tail of the break-in output when there was one. `act`
|
|
432
|
+
(tan-cli#1451) adds `--send` input and the `--reopen-at` baud switch; a
|
|
433
|
+
reopen replaces the held port, so the port closed at the end is the
|
|
434
|
+
actions' current one."""
|
|
435
|
+
pattern, duration, log = cap
|
|
436
|
+
data: dict = {"schemaVersion": DATA_SCHEMA_VERSION, "port": port, "baud": baud}
|
|
437
|
+
log_path = check_log_path(log, data) if log is not None else None
|
|
438
|
+
ser = open_port(port, baud, capture=True)
|
|
439
|
+
sink = None
|
|
440
|
+
actions = None
|
|
441
|
+
try:
|
|
442
|
+
initial = b""
|
|
443
|
+
if opts is not None:
|
|
444
|
+
data["breakIn"], initial = break_in(ser, port, baud, opts)
|
|
445
|
+
if not data["breakIn"]["caught"]:
|
|
446
|
+
return (
|
|
447
|
+
data,
|
|
448
|
+
[Issue("monitor.break-timeout", "error",
|
|
449
|
+
f"no prompt `{opts[1].decode('utf-8', 'replace')}` within {opts[2]}s on {port}.")],
|
|
450
|
+
ExitCode.RUNTIME_FAILURE,
|
|
451
|
+
)
|
|
452
|
+
if log_path is not None:
|
|
453
|
+
sink = _open_log(log_path, data)
|
|
454
|
+
if act is not None:
|
|
455
|
+
from tan.commands import monitor_actions # noqa: PLC0415 (only with actions)
|
|
456
|
+
|
|
457
|
+
actions = monitor_actions.build(act, ser, port)
|
|
458
|
+
try:
|
|
459
|
+
res = serial_capture.capture(
|
|
460
|
+
ser, duration_s=duration, until=pattern, sink=sink, initial=initial,
|
|
461
|
+
actions=actions,
|
|
462
|
+
)
|
|
463
|
+
except serial_actions.ActionError as err:
|
|
464
|
+
data["capture"] = {"actions": monitor_actions.report(actions)}
|
|
465
|
+
raise monitor_actions.action_error(err, port, data) from err
|
|
466
|
+
except serial_capture.SinkError as err:
|
|
467
|
+
raise _log_failed(log_path, err, data) from err
|
|
468
|
+
except OSError as err:
|
|
469
|
+
raise MonitorError(
|
|
470
|
+
"monitor.capture-io-failed",
|
|
471
|
+
f"serial I/O failed on '{port}' during --capture: {err}",
|
|
472
|
+
ExitCode.RUNTIME_FAILURE,
|
|
473
|
+
data,
|
|
474
|
+
) from err
|
|
475
|
+
finally:
|
|
476
|
+
(actions.port if actions is not None else ser).close()
|
|
477
|
+
if sink is not None:
|
|
478
|
+
try:
|
|
479
|
+
sink.close()
|
|
480
|
+
except OSError as err:
|
|
481
|
+
# Raising from `finally` would mask an in-flight error; the data
|
|
482
|
+
# was flushed per chunk, so a close failure is reported only
|
|
483
|
+
# when nothing else is.
|
|
484
|
+
if sys.exc_info()[0] is None:
|
|
485
|
+
raise _log_failed(log_path, err, data) from err
|
|
486
|
+
data["capture"] = {
|
|
487
|
+
"untilGiven": pattern is not None,
|
|
488
|
+
"matched": res.matched,
|
|
489
|
+
"matchedLine": res.matched_line,
|
|
490
|
+
"elapsedSeconds": round(res.elapsed_s, 3),
|
|
491
|
+
"durationSeconds": duration,
|
|
492
|
+
"bytesSeen": res.bytes_seen,
|
|
493
|
+
"bytesSeenTail": res.tail,
|
|
494
|
+
"logFile": log_path,
|
|
495
|
+
}
|
|
496
|
+
if actions is not None:
|
|
497
|
+
data["capture"]["actions"] = monitor_actions.report(actions)
|
|
498
|
+
if pattern is not None and not res.matched:
|
|
499
|
+
return (
|
|
500
|
+
data,
|
|
501
|
+
[Issue("monitor.capture-timeout", "error",
|
|
502
|
+
f"no line matched --until within {duration}s on {port}.")],
|
|
503
|
+
ExitCode.RUNTIME_FAILURE,
|
|
504
|
+
)
|
|
505
|
+
return data, [], ExitCode.SUCCESS
|