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/consent.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The one implementation of `GlobalArgs::can_prompt()` — the gate every
|
|
3
|
+
command must pass before it prompts a human or mutates the host.
|
|
4
|
+
|
|
5
|
+
Ported from the Rust oracle's `GlobalArgs::can_prompt()`, whose own
|
|
6
|
+
`--non-interactive` help text states the rule the port must honour verbatim:
|
|
7
|
+
|
|
8
|
+
Never prompt. A command with a documented default takes it (`tan init`
|
|
9
|
+
scaffolds `zephyr-app` into `.`); one without fails instead of asking
|
|
10
|
+
(`tan scaffold` needs `--name`). **The same rule applies unasked when
|
|
11
|
+
stdin or stderr is not a terminal — piped, redirected, or a CI runner.**
|
|
12
|
+
|
|
13
|
+
That last sentence is the half a re-derivation keeps dropping, and dropping it
|
|
14
|
+
is not cosmetic. Before this module existed the check was written out by hand
|
|
15
|
+
in four places and **one of them was wrong**: `doctor --fix` (tan-cli#91)
|
|
16
|
+
tested only `not non_interactive and not ci and not json_mode` and omitted both
|
|
17
|
+
`isatty()` calls, so a CI runner that redirected its output but did not happen
|
|
18
|
+
to pass `--ci` got **unattended host mutation** — demonstrated live with fully
|
|
19
|
+
captured pipes, where `tan doctor --fix` spawned four real `winget install`
|
|
20
|
+
runs (`Git.Git`, `Kitware.CMake`, `Python.Python.3.12`, `Ninja-build.Ninja`)
|
|
21
|
+
with nobody watching. A redirected stdio stream is the single most common shape
|
|
22
|
+
of an automated run, so the omitted condition was the one that mattered most.
|
|
23
|
+
|
|
24
|
+
Hence one function, imported. Duplicating a consent gate means every future
|
|
25
|
+
copy is another chance to drop the clause that makes it a consent gate at all.
|
|
26
|
+
|
|
27
|
+
**Why BOTH `stdin` and `stderr`, not just `stdin`.** A prompt is a
|
|
28
|
+
question-and-answer pair and each half needs its own real terminal: `stdin`
|
|
29
|
+
carries the answer, and `stderr` — never `stdout`, which belongs to the
|
|
30
|
+
envelope — carries the question. `tan doctor --fix < /dev/null` has no way to
|
|
31
|
+
receive consent; `tan doctor --fix 2>log` has no way to ask for it, and would
|
|
32
|
+
block on a question the user never saw. Requiring both is what makes "the user
|
|
33
|
+
actually agreed to this" true rather than merely likely.
|
|
34
|
+
|
|
35
|
+
**Not `stdout`.** Under `--format json` stdout is a single parsed envelope, and
|
|
36
|
+
`| jq` is a normal, fully-interactive way to run tan. Testing `stdout.isatty()`
|
|
37
|
+
would refuse consent in a session where the human is sitting right there. The
|
|
38
|
+
`json_mode` flag already covers the case that actually matters.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from tan.env import stderr_is_tty, stdin_is_tty
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def can_prompt(*, non_interactive: bool, ci: bool, json_mode: bool) -> bool:
|
|
46
|
+
"""Whether this invocation may prompt the user, or take any other action
|
|
47
|
+
that needs a human's live consent (installing a toolchain, overwriting a
|
|
48
|
+
file, relocating a checkout).
|
|
49
|
+
|
|
50
|
+
All five conditions must hold. The three flags are the caller's explicit
|
|
51
|
+
"do not ask me" signals; the two `isatty()` calls are the same rule applied
|
|
52
|
+
**unasked**, for the automated runs that never thought to pass a flag.
|
|
53
|
+
|
|
54
|
+
A command with a documented default takes it when this returns `False`; one
|
|
55
|
+
without a default fails instead of asking.
|
|
56
|
+
|
|
57
|
+
tan-cli#488: `sys.stdin` itself, not just the result of calling `.isatty()`
|
|
58
|
+
on it, can be `None` -- a process launched with its standard handles
|
|
59
|
+
detached (a GUI launcher, `pythonw`-style spawn, or a shell that closed fd
|
|
60
|
+
0 before exec, `0<&-`) leaves `sys.stdin` unbound rather than merely
|
|
61
|
+
non-interactive, and a bare `sys.stdin.isatty()` then raises
|
|
62
|
+
`AttributeError: 'NoneType' object has no attribute 'isatty'`. This is the
|
|
63
|
+
ONE place that guard belongs: every caller of `can_prompt` (`doctor_cmd`,
|
|
64
|
+
`scaffold_cmd`) reaches this line before any caller-side guard could run,
|
|
65
|
+
so duplicating the `is not None` check downstream (as doctor_cmd.py's
|
|
66
|
+
`fix_suppressed_issue` still does, for its own separate per-condition
|
|
67
|
+
explanation) does not help -- this call is the one that crashes first.
|
|
68
|
+
|
|
69
|
+
tan-cli#488 round 5: the SAME detachment guards ONLY `sys.stdin`, not
|
|
70
|
+
`sys.stderr` -- the very next operand of this same `and` chain, and just
|
|
71
|
+
as reachable: the identical detached-process shape can leave `sys.stdin`
|
|
72
|
+
live (redirected to a real, non-tty file) while `sys.stderr` alone is
|
|
73
|
+
unbound, since each handle is detached independently by whatever spawned
|
|
74
|
+
the process. Verified against the real binary, not a mock: a `pty.fork()`
|
|
75
|
+
child with a genuine tty on stdin (`isatty() is True`, so the chain
|
|
76
|
+
reaches this line) and `stderr` closed before `exec` crashed
|
|
77
|
+
`tan doctor --fix` with exactly this `AttributeError` -- `sys.stdin is not
|
|
78
|
+
None` alone does not protect the `sys.stderr.isatty()` call one line
|
|
79
|
+
below it.
|
|
80
|
+
|
|
81
|
+
tan-cli#488 round 6: `is not None` was never the whole guard either --
|
|
82
|
+
it stops a detached (`None`) handle from crashing `.isatty()`, but not a
|
|
83
|
+
handle that EXISTS and simply has no `.isatty()` method, which is
|
|
84
|
+
exactly what `sys.stderr` becomes under `tan --format json`
|
|
85
|
+
(`tan.cli.main` tees it through `_TeeStderr`: `write`/`flush`/
|
|
86
|
+
`getvalue` only). This function's own `not json_mode` operand happens to
|
|
87
|
+
short-circuit ahead of the `sys.stderr` checks before that shape is ever
|
|
88
|
+
reached today, so `can_prompt` itself was never observed to crash on
|
|
89
|
+
it -- but `tan.commands.build_cmd._dispatch` hand-rolled the identical
|
|
90
|
+
`is not None and .isatty()` pair with no `json_mode` operand in front of
|
|
91
|
+
it at all, and DID crash, on a real `tan run --format json`
|
|
92
|
+
(`AttributeError: '_TeeStderr' object has no attribute 'isatty'`,
|
|
93
|
+
measured). Both operands now route through `tan.env.stdin_is_tty`/
|
|
94
|
+
`stderr_is_tty` -- the one shared probe (tan-cli#288) that already
|
|
95
|
+
wraps both exception classes -- rather than leaving this copy's safety
|
|
96
|
+
resting on an operand order a future edit could reshuffle.
|
|
97
|
+
"""
|
|
98
|
+
return (
|
|
99
|
+
not non_interactive
|
|
100
|
+
and not ci
|
|
101
|
+
and not json_mode
|
|
102
|
+
and stdin_is_tty()
|
|
103
|
+
and stderr_is_tty()
|
|
104
|
+
)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`colors`: the default console filter for `tan monitor` (tan-cli#1315).
|
|
3
|
+
|
|
4
|
+
A serial console shows bytes from a device nobody vouches for. Passing them
|
|
5
|
+
straight to the user's terminal lets that device write the clipboard (OSC 52),
|
|
6
|
+
retitle the window, and move or erase the screen. miniterm's own `default`
|
|
7
|
+
filter is safe but prints Zephyr's colours as literal `ESC[1;32m` text. This
|
|
8
|
+
filter keeps colours and nothing else, as an ALLOWLIST (a denylist of known
|
|
9
|
+
bad sequences loses to the next terminal quirk):
|
|
10
|
+
|
|
11
|
+
* a character is output only if it is printable (`unicodedata` category not
|
|
12
|
+
in Cc, Cf, Cs, Co, Cn, Zl, Zp, which also removes bidi/format controls and
|
|
13
|
+
DEL) or one of `\\r \\n \\t \\b`;
|
|
14
|
+
* the ONLY escape ever output is an SGR sequence the filter builds itself,
|
|
15
|
+
`ESC [ <codes> m`, re-emitted canonically from the parsed codes and never
|
|
16
|
+
passed through as received. Allowed codes: 0 1 2 3 4 22 23 24 27 30-37 39
|
|
17
|
+
40-47 49 90-97 100-107, and 38/48 with `;5;n` or `;2;r;g;b` (0-255). Any
|
|
18
|
+
other code (conceal 8, blink 5/6, ...) is dropped and the rest kept; a
|
|
19
|
+
malformed 38/48 drops the remainder. The colon form (`38:2::1:2:3`) is not
|
|
20
|
+
recognised, so that sequence is neutralised whole;
|
|
21
|
+
* every other ESC sequence (non-SGR CSI, OSC, DCS, APC, PM, SOS, charset
|
|
22
|
+
selects, lone ESC), every C1 control (U+0080-U+009F; U+009B/9D/90/98/9E/9F
|
|
23
|
+
start a sequence exactly like their 7-bit forms), CAN, SUB, DEL and every
|
|
24
|
+
other control character becomes one visible placeholder. CAN and SUB abort
|
|
25
|
+
a sequence in progress, as ECMA-48 terminals do;
|
|
26
|
+
* the placeholder is `·`, or `?` when the output stream cannot encode it; a
|
|
27
|
+
`\\r \\n \\t \\b` following a dropped escape is still honoured;
|
|
28
|
+
* it is stream-safe (a sequence split across reads is buffered) and bounded:
|
|
29
|
+
an over-long or unterminated sequence emits a placeholder and resyncs to
|
|
30
|
+
ground state without printing the buffered payload.
|
|
31
|
+
|
|
32
|
+
Accepted residuals (not filterable without breaking colour output): text drawn
|
|
33
|
+
with foreground equal to background is invisible; `\\b` and `\\r` let a device
|
|
34
|
+
overwrite text already on the line; some printable-category characters
|
|
35
|
+
(unassigned-looking Lo/Mn combining marks, zero-width letters) render as
|
|
36
|
+
nothing. A character the output stream's encoding cannot represent (e.g. `世`
|
|
37
|
+
on cp437) is replaced by the placeholder too, so it cannot raise in miniterm's
|
|
38
|
+
reader thread.
|
|
39
|
+
|
|
40
|
+
Because the only escape that can reach the terminal is one the filter
|
|
41
|
+
generated, the terminal is always in ground state when it sees it, which
|
|
42
|
+
removes the filter-vs-terminal parser differential.
|
|
43
|
+
|
|
44
|
+
The class is defined by exec'ing `SOURCE`, so the same text can also be run
|
|
45
|
+
inside a spawned `python -c` (`BOOTSTRAP`) where `tan` is not importable
|
|
46
|
+
(frozen builds), and the in-process and subprocess consoles cannot drift.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
SOURCE = r'''
|
|
52
|
+
import sys as _sys
|
|
53
|
+
import unicodedata as _ud
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _placeholder():
|
|
57
|
+
enc = getattr(_sys.stdout, "encoding", None) or "ascii"
|
|
58
|
+
try:
|
|
59
|
+
"·".encode(enc)
|
|
60
|
+
return "·"
|
|
61
|
+
except (UnicodeError, LookupError):
|
|
62
|
+
return "?"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ColorsFilter:
|
|
66
|
+
PLACEHOLDER = "·" # instances pick `?` when stdout cannot encode it
|
|
67
|
+
_KEEP = "\r\n\t\b"
|
|
68
|
+
_BAD_CATEGORIES = frozenset(["Cc", "Cf", "Cs", "Co", "Cn", "Zl", "Zp"])
|
|
69
|
+
_SGR_SIMPLE = frozenset(
|
|
70
|
+
[0, 1, 2, 3, 4, 22, 23, 24, 27, 39, 49,
|
|
71
|
+
*range(30, 38), *range(40, 48), *range(90, 98), *range(100, 108)]
|
|
72
|
+
)
|
|
73
|
+
_MAX_PARAMS = 32
|
|
74
|
+
_MAX_CSI = 64
|
|
75
|
+
_MAX_STR = 4096
|
|
76
|
+
_MAX_INTER = 16
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.PLACEHOLDER = _placeholder()
|
|
80
|
+
self._enc = getattr(_sys.stdout, "encoding", None) or "ascii"
|
|
81
|
+
self._enc_ok = {}
|
|
82
|
+
self._state = "ground" # ground|esc|inter|csi|str|stresc
|
|
83
|
+
self._buf = ""
|
|
84
|
+
|
|
85
|
+
def tx(self, text):
|
|
86
|
+
return text
|
|
87
|
+
|
|
88
|
+
def echo(self, text):
|
|
89
|
+
return text
|
|
90
|
+
|
|
91
|
+
def rx(self, text):
|
|
92
|
+
out = []
|
|
93
|
+
for ch in text:
|
|
94
|
+
self._step(ch, out)
|
|
95
|
+
return "".join(out)
|
|
96
|
+
|
|
97
|
+
def _reset(self):
|
|
98
|
+
self._state = "ground"
|
|
99
|
+
self._buf = ""
|
|
100
|
+
|
|
101
|
+
def _drop(self, out):
|
|
102
|
+
out.append(self.PLACEHOLDER)
|
|
103
|
+
self._reset()
|
|
104
|
+
|
|
105
|
+
def _encodable(self, ch):
|
|
106
|
+
ok = self._enc_ok.get(ch)
|
|
107
|
+
if ok is None:
|
|
108
|
+
try:
|
|
109
|
+
ch.encode(self._enc)
|
|
110
|
+
ok = True
|
|
111
|
+
except (UnicodeError, LookupError):
|
|
112
|
+
ok = False
|
|
113
|
+
if len(self._enc_ok) < 4096:
|
|
114
|
+
self._enc_ok[ch] = ok
|
|
115
|
+
return ok
|
|
116
|
+
|
|
117
|
+
def _printable(self, ch):
|
|
118
|
+
if ch in self._KEEP:
|
|
119
|
+
return True
|
|
120
|
+
return _ud.category(ch) not in self._BAD_CATEGORIES and self._encodable(ch)
|
|
121
|
+
|
|
122
|
+
def _sgr(self, params):
|
|
123
|
+
# `params` is already known to match ^[0-9;]{0,32}$
|
|
124
|
+
if params == "":
|
|
125
|
+
return "\x1b[0m"
|
|
126
|
+
vals = [int(p) if p else 0 for p in params.split(";")]
|
|
127
|
+
kept, i = [], 0
|
|
128
|
+
while i < len(vals):
|
|
129
|
+
v = vals[i]
|
|
130
|
+
if v in (38, 48):
|
|
131
|
+
mode = vals[i + 1] if i + 1 < len(vals) else None
|
|
132
|
+
if mode == 5 and i + 2 < len(vals) and vals[i + 2] <= 255:
|
|
133
|
+
kept += [v, 5, vals[i + 2]]
|
|
134
|
+
i += 3
|
|
135
|
+
continue
|
|
136
|
+
if mode == 2 and i + 4 < len(vals) and max(vals[i + 2:i + 5]) <= 255:
|
|
137
|
+
kept += [v, 2, *vals[i + 2:i + 5]]
|
|
138
|
+
i += 5
|
|
139
|
+
continue
|
|
140
|
+
break # malformed extended colour: drop the remainder
|
|
141
|
+
if v in self._SGR_SIMPLE:
|
|
142
|
+
kept.append(v)
|
|
143
|
+
i += 1
|
|
144
|
+
if not kept:
|
|
145
|
+
return ""
|
|
146
|
+
return "\x1b[" + ";".join(str(k) for k in kept) + "m"
|
|
147
|
+
|
|
148
|
+
def _step(self, ch, out):
|
|
149
|
+
st = self._state
|
|
150
|
+
o = ord(ch)
|
|
151
|
+
if st == "ground":
|
|
152
|
+
if ch == "\x1b":
|
|
153
|
+
self._state = "esc"
|
|
154
|
+
elif o == 0x9B:
|
|
155
|
+
self._state, self._buf = "csi", ""
|
|
156
|
+
elif o in (0x90, 0x98, 0x9D, 0x9E, 0x9F):
|
|
157
|
+
self._state, self._buf = "str", ""
|
|
158
|
+
elif self._printable(ch):
|
|
159
|
+
out.append(ch)
|
|
160
|
+
else:
|
|
161
|
+
out.append(self.PLACEHOLDER) # C0, C1, DEL, CAN, SUB, format, ...
|
|
162
|
+
return
|
|
163
|
+
if ch in "\x18\x1a": # CAN / SUB abort any sequence in progress
|
|
164
|
+
self._drop(out)
|
|
165
|
+
return
|
|
166
|
+
if st == "esc":
|
|
167
|
+
if ch == "[":
|
|
168
|
+
self._state, self._buf = "csi", ""
|
|
169
|
+
elif ch in "]PX^_":
|
|
170
|
+
self._state, self._buf = "str", ""
|
|
171
|
+
elif 0x20 <= o <= 0x2F:
|
|
172
|
+
self._state, self._buf = "inter", ""
|
|
173
|
+
elif ch == "\x1b":
|
|
174
|
+
out.append(self.PLACEHOLDER) # ESC ESC: still in esc
|
|
175
|
+
elif 0x30 <= o <= 0x7E:
|
|
176
|
+
self._drop(out) # two-byte sequence (ESC c, ESC =, ...)
|
|
177
|
+
else:
|
|
178
|
+
self._drop(out)
|
|
179
|
+
self._step(ch, out) # honour newline etc.; C1 starts its own sequence
|
|
180
|
+
elif st == "inter":
|
|
181
|
+
if 0x20 <= o <= 0x2F:
|
|
182
|
+
self._buf += ch
|
|
183
|
+
if len(self._buf) > self._MAX_INTER:
|
|
184
|
+
self._drop(out)
|
|
185
|
+
elif 0x30 <= o <= 0x7E:
|
|
186
|
+
self._drop(out) # ESC ( B etc.
|
|
187
|
+
elif ch == "\x1b":
|
|
188
|
+
self._drop(out)
|
|
189
|
+
self._state = "esc"
|
|
190
|
+
else:
|
|
191
|
+
self._drop(out)
|
|
192
|
+
self._step(ch, out)
|
|
193
|
+
elif st == "csi":
|
|
194
|
+
if 0x20 <= o <= 0x3F:
|
|
195
|
+
self._buf += ch
|
|
196
|
+
if len(self._buf) > self._MAX_CSI:
|
|
197
|
+
self._drop(out)
|
|
198
|
+
elif 0x40 <= o <= 0x7E:
|
|
199
|
+
params = self._buf
|
|
200
|
+
if (
|
|
201
|
+
ch == "m"
|
|
202
|
+
and len(params) <= self._MAX_PARAMS
|
|
203
|
+
and all(c in "0123456789;" for c in params)
|
|
204
|
+
):
|
|
205
|
+
out.append(self._sgr(params))
|
|
206
|
+
self._reset()
|
|
207
|
+
else:
|
|
208
|
+
self._drop(out)
|
|
209
|
+
elif ch == "\x1b":
|
|
210
|
+
self._drop(out)
|
|
211
|
+
self._state = "esc"
|
|
212
|
+
else:
|
|
213
|
+
self._drop(out) # any other control or non-ASCII aborts it
|
|
214
|
+
self._step(ch, out)
|
|
215
|
+
elif st == "str":
|
|
216
|
+
if ch == "\x07" or o == 0x9C:
|
|
217
|
+
self._drop(out)
|
|
218
|
+
elif ch == "\x1b":
|
|
219
|
+
self._state = "stresc"
|
|
220
|
+
else:
|
|
221
|
+
self._buf += ch
|
|
222
|
+
if len(self._buf) > self._MAX_STR:
|
|
223
|
+
self._drop(out)
|
|
224
|
+
elif st == "stresc":
|
|
225
|
+
if ch == "\\":
|
|
226
|
+
self._drop(out) # ESC \ = ST
|
|
227
|
+
else:
|
|
228
|
+
self._drop(out) # anything else aborts the string...
|
|
229
|
+
self._state = "esc" # ...and the ESC starts a new sequence
|
|
230
|
+
if ch != "\x1b":
|
|
231
|
+
self._step(ch, out)
|
|
232
|
+
'''
|
|
233
|
+
|
|
234
|
+
#: Run by `python -c` for the spawned console: define the filter, register it
|
|
235
|
+
#: in miniterm's own table, hand over to miniterm's `main()` (argv follows).
|
|
236
|
+
BOOTSTRAP = (
|
|
237
|
+
SOURCE
|
|
238
|
+
+ "\nfrom serial.tools import miniterm as _m\n"
|
|
239
|
+
+ "_m.TRANSFORMATIONS['colors'] = ColorsFilter\n"
|
|
240
|
+
+ "_m.main()\n"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
_ns: dict = {}
|
|
244
|
+
exec(compile(SOURCE, "<tan.core.console_filter.SOURCE>", "exec"), _ns) # noqa: S102
|
|
245
|
+
ColorsFilter = _ns["ColorsFilter"]
|