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.
Files changed (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. 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