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
tan/core/trusted_path.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Is this path trustworthy as an interlock binary? (tan-cli#1452)
|
|
3
|
+
|
|
4
|
+
The same rule PR #1454 (`flash_raw.py`, tan-cli#1457) applies to the
|
|
5
|
+
reservation-enforcing J-Link wrapper: an ABSOLUTE path that resolves (symlinks
|
|
6
|
+
followed) to an executable regular file outside the cwd, where BOTH chains --
|
|
7
|
+
the path as written and the resolved target, each with its parents -- are owned
|
|
8
|
+
by root or the current user and are not writable by others or by a group anyone
|
|
9
|
+
else belongs to (a sticky directory is allowed).
|
|
10
|
+
|
|
11
|
+
`tan reset` additionally requires the per-session lease nonce under
|
|
12
|
+
`JLINK_RUN_PLACE` (tan-cli#1457): `reset_cmd` calls
|
|
13
|
+
`flash_raw._reservation_refusal`, the same gate `tan flash --raw` uses. This module
|
|
14
|
+
only judges the wrapper path; the two path checks are still separate copies.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import stat
|
|
20
|
+
|
|
21
|
+
WRAPPER_ENV = "TAN_JLINK_WRAPPER"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _current_user() -> str:
|
|
25
|
+
"""The account name of the real uid (never `USER`, which the caller controls)."""
|
|
26
|
+
import pwd # noqa: PLC0415 (POSIX only: this module must import on Windows)
|
|
27
|
+
|
|
28
|
+
return pwd.getpwuid(os.getuid()).pw_name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _private_group(gid: int) -> bool:
|
|
32
|
+
"""Whether `gid` is a group only the current user belongs to."""
|
|
33
|
+
import grp # noqa: PLC0415 (POSIX only)
|
|
34
|
+
import pwd # noqa: PLC0415
|
|
35
|
+
|
|
36
|
+
me = _current_user()
|
|
37
|
+
try:
|
|
38
|
+
members = set(grp.getgrgid(gid).gr_mem)
|
|
39
|
+
except KeyError:
|
|
40
|
+
return False
|
|
41
|
+
return members <= {me} and all(u.pw_name == me for u in pwd.getpwall() if u.pw_gid == gid)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _node_problem(node: str, st: os.stat_result) -> str | None:
|
|
45
|
+
mode = st.st_mode
|
|
46
|
+
sticky_dir = stat.S_ISDIR(mode) and bool(mode & stat.S_ISVTX)
|
|
47
|
+
if st.st_uid not in (0, os.getuid()):
|
|
48
|
+
return f"{node} is owned by uid {st.st_uid}, not root or you"
|
|
49
|
+
if stat.S_ISLNK(mode):
|
|
50
|
+
return None # a symlink's own mode bits are always 0777 and mean nothing
|
|
51
|
+
if mode & stat.S_IWOTH and not sticky_dir:
|
|
52
|
+
return f"{node} is world-writable"
|
|
53
|
+
if mode & stat.S_IWGRP and not sticky_dir and not (
|
|
54
|
+
st.st_gid == os.getgid() and _private_group(st.st_gid)
|
|
55
|
+
):
|
|
56
|
+
return f"{node} is group-writable by a group others belong to"
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _chain_problem(start: str) -> str | None:
|
|
61
|
+
node = start
|
|
62
|
+
while True:
|
|
63
|
+
try:
|
|
64
|
+
st = os.lstat(node)
|
|
65
|
+
except OSError:
|
|
66
|
+
return f"{node} cannot be inspected"
|
|
67
|
+
problem = _node_problem(node, st)
|
|
68
|
+
if problem:
|
|
69
|
+
return problem
|
|
70
|
+
parent = os.path.dirname(node)
|
|
71
|
+
if parent == node:
|
|
72
|
+
return None
|
|
73
|
+
node = parent
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def unsafe(path: str) -> str | None:
|
|
77
|
+
"""Why `path` cannot be trusted as an interlock binary, or `None`."""
|
|
78
|
+
if not os.path.isabs(path):
|
|
79
|
+
return "is not an absolute path"
|
|
80
|
+
real = os.path.realpath(path)
|
|
81
|
+
cwd = os.path.realpath(os.getcwd())
|
|
82
|
+
if real == cwd or real.startswith(cwd + os.sep):
|
|
83
|
+
return "lives under the current directory"
|
|
84
|
+
if not (os.path.isfile(real) and os.access(real, os.X_OK)):
|
|
85
|
+
return "is not an executable file"
|
|
86
|
+
return _chain_problem(os.path.abspath(path)) or _chain_problem(real)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
#: Short reason codes (no paths): the operator's own `TAN_JLINK_WRAPPER` is already known.
|
|
90
|
+
REASON_ENV_UNSET = "wrapper-env-unset"
|
|
91
|
+
REASON_PATH_UNSAFE = "wrapper-path-unsafe"
|
|
92
|
+
REASON_NOT_RESOLVED = "wrapper-not-resolved-binary"
|
|
93
|
+
REASON_UNSUPPORTED = "unsupported-platform"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _supported() -> bool:
|
|
97
|
+
if not hasattr(os, "getuid"):
|
|
98
|
+
return False
|
|
99
|
+
try:
|
|
100
|
+
import grp # noqa: F401, PLC0415
|
|
101
|
+
import pwd # noqa: F401, PLC0415
|
|
102
|
+
except ImportError:
|
|
103
|
+
return False
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def configured_wrapper() -> tuple[str | None, str | None]:
|
|
108
|
+
"""`(path, None)` when `TAN_JLINK_WRAPPER` names a safe wrapper, else `(None, reason code)`."""
|
|
109
|
+
if not _supported():
|
|
110
|
+
return None, REASON_UNSUPPORTED
|
|
111
|
+
configured = os.environ.get(WRAPPER_ENV, "").strip()
|
|
112
|
+
if not configured:
|
|
113
|
+
return None, REASON_ENV_UNSET
|
|
114
|
+
if unsafe(configured) is not None:
|
|
115
|
+
return None, REASON_PATH_UNSAFE
|
|
116
|
+
return configured, None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_configured_wrapper(exe: str | None) -> tuple[bool, str | None]:
|
|
120
|
+
"""`(True, None)` only when `exe` IS the wrapper named by `TAN_JLINK_WRAPPER`
|
|
121
|
+
(absolute, trustworthy, same realpath). Otherwise `(False, reason code)`: a
|
|
122
|
+
marker string, a PATH name or a look-alike proves nothing. The trust is
|
|
123
|
+
operator-asserted: the variable attests a PATH, not that the file really masks
|
|
124
|
+
the other probes."""
|
|
125
|
+
if not _supported():
|
|
126
|
+
return False, REASON_UNSUPPORTED
|
|
127
|
+
configured = os.environ.get(WRAPPER_ENV, "").strip()
|
|
128
|
+
if not configured:
|
|
129
|
+
return False, REASON_ENV_UNSET
|
|
130
|
+
if unsafe(configured) is not None:
|
|
131
|
+
return False, REASON_PATH_UNSAFE
|
|
132
|
+
if not exe or not os.path.isabs(exe) or os.path.realpath(exe) != os.path.realpath(configured):
|
|
133
|
+
return False, REASON_NOT_RESOLVED
|
|
134
|
+
return True, None
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""U-Boot autoboot break-in: pure matching/timing logic for `tan monitor
|
|
3
|
+
--break-uboot` (tan-cli#1315).
|
|
4
|
+
|
|
5
|
+
The bench recipe is "power-cycle the board, then spam a key on the console
|
|
6
|
+
until the `=> ` prompt appears". This module is that loop and nothing else: it
|
|
7
|
+
takes an already-open port-like object (anything with `write(bytes)` and
|
|
8
|
+
`read(size) -> bytes`; a pyserial `Serial`, a `loop://` port, an `rfc2217://`
|
|
9
|
+
or `socket://` URL port, or a test fake), keeps sending the interrupt key,
|
|
10
|
+
watches the received stream for the prompt, and stops the moment it is seen
|
|
11
|
+
or the deadline passes. It never opens a port and never controls power --
|
|
12
|
+
the operator or labgrid does the power-cycle.
|
|
13
|
+
|
|
14
|
+
The clock is injected so the timing logic is testable without sleeping.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Protocol
|
|
23
|
+
|
|
24
|
+
#: pyserial `Serial` reads below this many seconds keep the loop responsive
|
|
25
|
+
#: and also pace the key presses (one write per read window).
|
|
26
|
+
DEFAULT_INTERVAL_S = 0.1
|
|
27
|
+
DEFAULT_KEY = b" "
|
|
28
|
+
DEFAULT_PROMPT = b"=> "
|
|
29
|
+
DEFAULT_TIMEOUT_S = 30.0
|
|
30
|
+
|
|
31
|
+
#: How much received text the envelope carries (`bytesSeenTail`), and how
|
|
32
|
+
#: much is kept for prompt matching (a prompt cannot be longer than this).
|
|
33
|
+
TAIL_BYTES = 256
|
|
34
|
+
_MATCH_WINDOW = 4096
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BytePort(Protocol):
|
|
38
|
+
def write(self, data: bytes, /) -> object: ...
|
|
39
|
+
def read(self, size: int = 1, /) -> bytes: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class BreakInResult:
|
|
44
|
+
caught: bool
|
|
45
|
+
elapsed_s: float
|
|
46
|
+
bytes_seen: int
|
|
47
|
+
tail: str
|
|
48
|
+
keys_sent: int = 0
|
|
49
|
+
#: Seconds from the first key until the first byte came back (None if none did).
|
|
50
|
+
first_byte_s: float | None = None
|
|
51
|
+
#: The last (up to 4 KiB of) raw bytes received, so a caller can carry them
|
|
52
|
+
#: into the next phase (`--capture` after `--break-uboot`).
|
|
53
|
+
window: bytes = b""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_SIMPLE_ESCAPES = {"r": b"\r", "n": b"\n", "t": b"\t", "\\": b"\\"}
|
|
57
|
+
_HEX = "0123456789abcdefABCDEF"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_escaped(text: str, what: str) -> bytes:
|
|
61
|
+
"""Turn a CLI string into bytes. Supported escapes: `\\xNN` (exactly two
|
|
62
|
+
hex digits), `\\r`, `\\n`, `\\t`, `\\\\`. Any other backslash sequence is
|
|
63
|
+
rejected rather than guessed at; other characters are taken literally
|
|
64
|
+
(UTF-8). Raises `ValueError` naming `what` on a bad escape or empty result.
|
|
65
|
+
"""
|
|
66
|
+
out = bytearray()
|
|
67
|
+
i = 0
|
|
68
|
+
while i < len(text):
|
|
69
|
+
ch = text[i]
|
|
70
|
+
if ch != "\\":
|
|
71
|
+
out += ch.encode("utf-8")
|
|
72
|
+
i += 1
|
|
73
|
+
continue
|
|
74
|
+
nxt = text[i + 1 : i + 2]
|
|
75
|
+
if nxt in _SIMPLE_ESCAPES:
|
|
76
|
+
out += _SIMPLE_ESCAPES[nxt]
|
|
77
|
+
i += 2
|
|
78
|
+
elif nxt == "x" and len(text[i + 2 : i + 4]) == 2 and all(c in _HEX for c in text[i + 2 : i + 4]):
|
|
79
|
+
out.append(int(text[i + 2 : i + 4], 16))
|
|
80
|
+
i += 4
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"{what} {text!r}: unsupported escape at position {i} "
|
|
84
|
+
"(allowed: \\xNN, \\r, \\n, \\t, \\\\)"
|
|
85
|
+
)
|
|
86
|
+
if not out:
|
|
87
|
+
raise ValueError(f"{what} must not be empty")
|
|
88
|
+
return bytes(out)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _tail_text(buf: bytes) -> str:
|
|
92
|
+
if len(buf) > TAIL_BYTES:
|
|
93
|
+
buf = buf[-TAIL_BYTES:]
|
|
94
|
+
# Do not start mid-way through a multibyte UTF-8 character.
|
|
95
|
+
while buf and 0x80 <= buf[0] < 0xC0:
|
|
96
|
+
buf = buf[1:]
|
|
97
|
+
return buf.decode("utf-8", errors="replace")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def break_into_uboot(
|
|
101
|
+
port: BytePort,
|
|
102
|
+
*,
|
|
103
|
+
key: bytes = DEFAULT_KEY,
|
|
104
|
+
prompt: bytes = DEFAULT_PROMPT,
|
|
105
|
+
timeout_s: float = DEFAULT_TIMEOUT_S,
|
|
106
|
+
interval_s: float = DEFAULT_INTERVAL_S,
|
|
107
|
+
clock: Callable[[], float] = time.monotonic,
|
|
108
|
+
) -> BreakInResult:
|
|
109
|
+
"""Send `key` repeatedly until `prompt` is received or `timeout_s` passes.
|
|
110
|
+
|
|
111
|
+
One iteration = drain what is already waiting (return if the prompt is in
|
|
112
|
+
it), send a key if `interval_s` has passed since the last one, then one
|
|
113
|
+
`read` (the port's own read timeout, set by the caller to about
|
|
114
|
+
`interval_s`, paces the loop). The deadline is checked after the read, so
|
|
115
|
+
at least one key is always sent. At most the one key already in flight
|
|
116
|
+
when the prompt arrives is sent; none after.
|
|
117
|
+
"""
|
|
118
|
+
start = clock()
|
|
119
|
+
deadline = start + timeout_s
|
|
120
|
+
window = b""
|
|
121
|
+
seen = 0
|
|
122
|
+
keys = 0
|
|
123
|
+
first_byte = None
|
|
124
|
+
last_write = None
|
|
125
|
+
|
|
126
|
+
def absorb(chunk: bytes) -> bool:
|
|
127
|
+
nonlocal window, seen, first_byte
|
|
128
|
+
if first_byte is None:
|
|
129
|
+
first_byte = clock() - start
|
|
130
|
+
seen += len(chunk)
|
|
131
|
+
window = (window + chunk)[-_MATCH_WINDOW:]
|
|
132
|
+
return prompt in window
|
|
133
|
+
|
|
134
|
+
def result(caught: bool) -> BreakInResult:
|
|
135
|
+
return BreakInResult(
|
|
136
|
+
caught, clock() - start, seen, _tail_text(window), keys, first_byte, window
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
# Drain everything already received BEFORE deciding to send another
|
|
141
|
+
# key, so no key goes out once the prompt is already in hand.
|
|
142
|
+
waiting = int(getattr(port, "in_waiting", 0) or 0)
|
|
143
|
+
if waiting and absorb(port.read(waiting) or b""):
|
|
144
|
+
return result(True)
|
|
145
|
+
now = clock()
|
|
146
|
+
if last_write is None or now - last_write >= interval_s:
|
|
147
|
+
port.write(key)
|
|
148
|
+
keys += 1
|
|
149
|
+
last_write = now
|
|
150
|
+
chunk = port.read(max(1, int(getattr(port, "in_waiting", 0) or 0))) or b""
|
|
151
|
+
if chunk and absorb(chunk):
|
|
152
|
+
return result(True)
|
|
153
|
+
if clock() >= deadline:
|
|
154
|
+
return result(False)
|