godot-cli-control 0.2.8__tar.gz → 0.2.10__tar.gz
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.
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/PKG-INFO +4 -1
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/CHANGELOG.md +1 -1
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/README.md +5 -1
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/game_bridge.gd +20 -4
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/input_simulation_api.gd +8 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/low_level_api.gd +12 -11
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/gut/test_game_bridge.gd +50 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/gut/test_input_simulation_api.gd +16 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/gut/test_low_level_api.gd +5 -5
- godot_cli_control-0.2.10/addons/godot_cli_control/tests/run_gut.py +187 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/run_gut.sh +4 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/pyproject.toml +9 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/README.md +3 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/_version.py +2 -2
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/cli.py +22 -3
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/daemon.py +37 -2
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/registry.py +16 -2
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/templates/skill/SKILL.md +7 -3
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_cli.py +41 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_daemon.py +95 -24
- godot_cli_control-0.2.10/python/tests/test_e2e_input.py +175 -0
- godot_cli_control-0.2.10/python/tests/test_e2e_screenshot_gui.py +180 -0
- godot_cli_control-0.2.10/python/tests/test_registry.py +124 -0
- godot_cli_control-0.2.8/python/tests/test_registry.py +0 -74
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/.gitignore +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/LICENSE +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/LICENSE +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bin/run_cli_control.ps1 +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bin/run_cli_control.sh +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/error_codes.gd +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/game_bridge.gd.uid +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/input_simulation_api.gd.uid +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/low_level_api.gd.uid +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/plugin.cfg +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/plugin.gd +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/plugin.gd.uid +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/test_init_walkthrough.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/test_walkthrough.sh +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/__init__.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/__main__.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/_duration.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/bridge.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/client.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/init_cmd.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/pytest_plugin.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/runner.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/skills_install.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/templates/__init__.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/godot_cli_control/templates/skill/__init__.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/__init__.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_bridge.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_cli_helpers.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_client.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_duration.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_init.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_pytest_plugin.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_runner.py +0 -0
- {godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/python/tests/test_skills_install.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: godot-cli-control
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
4
4
|
Summary: WebSocket bridge for headless / scripted control of Godot scenes.
|
|
5
5
|
Author: kesar
|
|
6
6
|
License: MIT
|
|
@@ -176,7 +176,10 @@ coverage run -m pytest python/tests/
|
|
|
176
176
|
coverage report
|
|
177
177
|
|
|
178
178
|
# GUT tests for the Godot plugin (needs GODOT_BIN env var)
|
|
179
|
+
# bash (Linux/macOS):
|
|
179
180
|
GODOT_BIN=/path/to/godot ./addons/godot_cli_control/tests/run_gut.sh
|
|
181
|
+
# cross-platform (Linux/macOS/Windows) — what CI runs:
|
|
182
|
+
GODOT_BIN=/path/to/godot python addons/godot_cli_control/tests/run_gut.py
|
|
180
183
|
```
|
|
181
184
|
|
|
182
185
|
## Documentation
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
- **Error code 1004 collision**: `low_level_api.gd` 的 scene tree 超限改用新业务码 `1005 "scene tree too large"`,与 input_simulation `1004 "combo in progress"` 解耦。新增 `error_codes.gd` 集中常量。
|
|
61
61
|
- **pytest fixture default port**: `--godot-cli-port` 默认从 9877 改为 0(OS-assigned),与 `daemon start` 默认对齐;多项目并行测试不再撞端口。
|
|
62
62
|
- **Addon README error-code table**: 之前只列到 1003,补全 1004 / 1005 / 客户端 -1xxx 段。
|
|
63
|
-
- **Scene tree hard limit bypass (DoS fix)**: `handle_get_scene_tree` 入口现在把 `max_nodes` clamp 到硬墙 `
|
|
63
|
+
- **Scene tree hard limit bypass (DoS fix)**: `handle_get_scene_tree` 入口现在把 `max_nodes` clamp 到硬墙 `_BUILD_TREE_MAX_NODES` (5000)。修复前客户端传 `max_nodes=999999` 会让服务端先把整棵超大树构造成 Dictionary 再被 1005 错误丢弃(内存浪费 / OOM 路径)。
|
|
64
64
|
- **Error code 1003 semantic split**: screenshot 在 viewport texture 为 null 时不再借用 `1003 METHOD_NOT_FOUND`,改用新业务码 `1006 RESOURCE_UNAVAILABLE`。1003 现在是纯 schema 错(不应 retry),1006 是 transient 错(短重试可能成功),agent 据此分别处置。
|
|
65
65
|
|
|
66
66
|
#### Added
|
|
@@ -65,7 +65,11 @@ Compatibility shims are also kept at `addons/godot_cli_control/bin/run_cli_contr
|
|
|
65
65
|
## Running the GUT unit tests
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
+
# Linux / macOS (bash):
|
|
68
69
|
GODOT_BIN=/path/to/godot ./addons/godot_cli_control/tests/run_gut.sh
|
|
70
|
+
|
|
71
|
+
# Cross-platform (Linux / macOS / Windows) — this is what CI runs:
|
|
72
|
+
GODOT_BIN=/path/to/godot python addons/godot_cli_control/tests/run_gut.py
|
|
69
73
|
```
|
|
70
74
|
|
|
71
75
|
The runner builds a throwaway Godot project, `git clone`s a pinned [GUT](https://github.com/bitwes/Gut) release into `addons/gut/`, copies this plugin in, and runs the test files under `addons/godot_cli_control/tests/gut/`. Coverage today is `LowLevelApi` handler boundaries (blacklist, missing-property, node-not-found) and `InputSimulationApi` state machine (combo / press / release / tap / release_all).
|
|
@@ -114,7 +118,7 @@ Three numeric ranges share `error.code`; they never overlap, so a single field i
|
|
|
114
118
|
| `1006` | server | Resource transiently unavailable (e.g. screenshot during scene transition). Rare under normal use — GameBridge waits for viewport first-frame before listening, and `screenshot` retries internally. Safe to retry if you do hit it. |
|
|
115
119
|
| `-32600` | server | Malformed JSON-RPC request |
|
|
116
120
|
| `-32601` | server | Unknown method name |
|
|
117
|
-
| `-32602` | server | Invalid params (incl. blocked methods/properties from the security blacklist,
|
|
121
|
+
| `-32602` | server | Invalid params (incl. blocked methods/properties from the security blacklist, `set` value-type mismatch — e.g. `Vector2` property given an array of wrong length / non-numeric elements, or `hold` given `duration ≤ 0` — use `press` for an indefinite hold) |
|
|
118
122
|
| `-1001` | client | Connection failure (daemon not running, port wrong, proxy hijacking localhost) |
|
|
119
123
|
| `-1002` | client | Timeout waiting for response |
|
|
120
124
|
| `-1003` | client | CLI usage error (combo missing steps, malformed `--steps-json`, …) |
|
{godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/bridge/game_bridge.gd
RENAMED
|
@@ -143,10 +143,7 @@ func _poll_active_peer() -> void:
|
|
|
143
143
|
_active_peer.poll()
|
|
144
144
|
var state: WebSocketPeer.State = _active_peer.get_ready_state()
|
|
145
145
|
if state == WebSocketPeer.STATE_CLOSED:
|
|
146
|
-
|
|
147
|
-
_input_sim_api.release_all()
|
|
148
|
-
_active_peer = null
|
|
149
|
-
_active_stream = null
|
|
146
|
+
_handle_disconnect(_active_peer.get_close_code())
|
|
150
147
|
return
|
|
151
148
|
if state != WebSocketPeer.STATE_OPEN:
|
|
152
149
|
return
|
|
@@ -156,6 +153,25 @@ func _poll_active_peer() -> void:
|
|
|
156
153
|
_handle_message(message)
|
|
157
154
|
|
|
158
155
|
|
|
156
|
+
func _handle_disconnect(close_code: int) -> void:
|
|
157
|
+
print("GameBridge: Client disconnected (close_code=%d)" % close_code)
|
|
158
|
+
# 区分「命令正常结束」与「异常掉线」,决定是否 release_all:
|
|
159
|
+
# - CLI 每条子命令都是独立连接、跑完即干净关闭(WebSocket close frame,
|
|
160
|
+
# code 1000)。此时**不** release_all —— 否则 `hold <dur>` 的定时器还没
|
|
161
|
+
# 倒计时就被清掉(只生效一帧),sticky `press` 也无法跨命令存活。持有的
|
|
162
|
+
# 输入靠各自机制收尾:hold/tap/combo 的 advance_timers 定时器自然结束,
|
|
163
|
+
# sticky press 持续到显式 release / release-all。
|
|
164
|
+
# - 客户端崩溃 / 被 kill / 网络断开时不会发 close frame,get_close_code()
|
|
165
|
+
# 返回 -1(< 0)。此时 release_all 兜底,避免卡死键 + 跨会话脏状态残留。
|
|
166
|
+
# (daemon 启动期的端口探活连接也可能是 -1,但那时没有任何持有输入,
|
|
167
|
+
# release_all 无副作用。)
|
|
168
|
+
# pytest 的 bridge fixture 仍在 teardown 自己调 release_all() 做清理。
|
|
169
|
+
if close_code < 0:
|
|
170
|
+
_input_sim_api.release_all()
|
|
171
|
+
_active_peer = null
|
|
172
|
+
_active_stream = null
|
|
173
|
+
|
|
174
|
+
|
|
159
175
|
func _register_methods() -> void:
|
|
160
176
|
# 低层 API(同步)
|
|
161
177
|
_methods["click"] = {"callable": _low_level_api.handle_click, "kind": "sync"}
|
|
@@ -113,6 +113,14 @@ func handle_hold(params: Dictionary) -> Dictionary:
|
|
|
113
113
|
if not InputMap.has_action(action):
|
|
114
114
|
return _err(CliControlErrorCodes.METHOD_NOT_FOUND, "Unknown action: %s" % action)
|
|
115
115
|
var duration: float = params.get("duration", 0.0) as float
|
|
116
|
+
# duration <= 0 是无意义的「按住 0 秒」:advance_timers 下一帧就释放 → 只生效
|
|
117
|
+
# 一帧。无限按住请用 press(sticky)。CLI preflight 也会拦,这里是防御纵深,
|
|
118
|
+
# 挡住绕过 CLI 的直连 RPC。
|
|
119
|
+
if duration <= 0.0:
|
|
120
|
+
return _err(
|
|
121
|
+
CliControlErrorCodes.INVALID_PARAMS,
|
|
122
|
+
"hold duration must be > 0 (got %s); use press for an indefinite hold" % duration,
|
|
123
|
+
)
|
|
116
124
|
_do_press(action)
|
|
117
125
|
_held_actions[action] = duration
|
|
118
126
|
return {"success": true}
|
|
@@ -7,11 +7,12 @@ extends Node
|
|
|
7
7
|
## 若冷启动或 GUT 跑前遇到 "Class 'CliControlErrorCodes' not found",先跑一次
|
|
8
8
|
## 完整 import(`godot --editor --quit --path .`)让 .godot/global_script_class_cache.cfg 建立。
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# depth=0("无限深度")时的递归深度兜底,防无限递归 / 病态深树。
|
|
11
|
+
const _BUILD_TREE_DEFAULT_MAX_DEPTH: int = 50
|
|
11
12
|
# 总节点数上限:宽场景(1000+ 子项的 Grid/Container)会构造极大 JSON,
|
|
12
13
|
# 超 outbound buffer(默认 10 MB)后客户端拿到截断包 → STATE_CLOSED 断连。
|
|
13
14
|
# 5000 节点对应 ~500 KB JSON,留足余量。
|
|
14
|
-
const
|
|
15
|
+
const _BUILD_TREE_MAX_NODES: int = 5000
|
|
15
16
|
# wait_game_time_async 防呆上限:防止误传 1e9 之类的数值挂死 session
|
|
16
17
|
const _MAX_WAIT_SECONDS: float = 3600.0
|
|
17
18
|
# take_screenshot_async 循环上限:常态下 GameBridge 启动 gate 已保证 viewport
|
|
@@ -370,14 +371,14 @@ func handle_get_children(params: Dictionary) -> Dictionary:
|
|
|
370
371
|
func handle_get_scene_tree(params: Dictionary) -> Dictionary:
|
|
371
372
|
var max_depth: int = params.get("depth", 5) as int
|
|
372
373
|
# max_nodes 是客户端控制的软上限。**入口处 clamp 到硬墙
|
|
373
|
-
#
|
|
374
|
+
# _BUILD_TREE_MAX_NODES (5000)**:防止恶意 / 失误调用传 max_nodes=999999
|
|
374
375
|
# 时让 _build_tree 真把整棵超大树构造成 Dictionary 后才被外层错误返回丢弃
|
|
375
376
|
# (DoS / OOM 路径)。clamp 后 _build_tree 内部的短路单一来源,
|
|
376
377
|
# 同时承担软上限(agent truncated 信号)与硬墙(防爆 outbound buffer)。
|
|
377
378
|
# 不传时也用硬墙做默认,兼容旧客户端。
|
|
378
|
-
var max_nodes: int = params.get("max_nodes",
|
|
379
|
-
if max_nodes <= 0 or max_nodes >
|
|
380
|
-
max_nodes =
|
|
379
|
+
var max_nodes: int = params.get("max_nodes", _BUILD_TREE_MAX_NODES) as int
|
|
380
|
+
if max_nodes <= 0 or max_nodes > _BUILD_TREE_MAX_NODES:
|
|
381
|
+
max_nodes = _BUILD_TREE_MAX_NODES
|
|
381
382
|
var root: Node = get_tree().current_scene
|
|
382
383
|
if root == null:
|
|
383
384
|
root = get_tree().root
|
|
@@ -389,10 +390,10 @@ func handle_get_scene_tree(params: Dictionary) -> Dictionary:
|
|
|
389
390
|
# 因为 max_nodes 已 clamp 到 ≤ LIMIT,counter 最多比 LIMIT 多 ~1,
|
|
390
391
|
# 所以这条分支只在 max_nodes==LIMIT(客户端没传或传了 ≥LIMIT)时被触发。
|
|
391
392
|
# max_nodes < LIMIT 的客户端永远走 truncated 软信号路径,不会撞 1005。
|
|
392
|
-
if counter[0] >
|
|
393
|
+
if counter[0] > _BUILD_TREE_MAX_NODES:
|
|
393
394
|
return _err(
|
|
394
395
|
CliControlErrorCodes.SCENE_TREE_TOO_LARGE,
|
|
395
|
-
"scene tree too large (>%d nodes); lower 'depth' or query a subtree" %
|
|
396
|
+
"scene tree too large (>%d nodes); lower 'depth' or query a subtree" % _BUILD_TREE_MAX_NODES,
|
|
396
397
|
)
|
|
397
398
|
# 软上限:硬墙内但超过 max_nodes 时附加 truncated 信号让 agent 决定分子树。
|
|
398
399
|
var response: Dictionary = {"tree": tree}
|
|
@@ -479,10 +480,10 @@ func _has_property(node: Node, property: String) -> bool:
|
|
|
479
480
|
return false
|
|
480
481
|
|
|
481
482
|
|
|
482
|
-
## depth=0 表示"无限深度",使用硬限制
|
|
483
|
+
## depth=0 表示"无限深度",使用硬限制 _BUILD_TREE_DEFAULT_MAX_DEPTH (50) 防止无限递归。
|
|
483
484
|
## counter 是 by-ref 计数器:超过 max_nodes 时短路(不再递归子节点),
|
|
484
485
|
## 调用方读 counter[0] > max_nodes 决定是否附加 truncated 信号,
|
|
485
|
-
## 读 counter[0] >
|
|
486
|
+
## 读 counter[0] > _BUILD_TREE_MAX_NODES 决定是否走 1005 (SCENE_TREE_TOO_LARGE) 错误路径。
|
|
486
487
|
func _build_tree(node: Node, max_depth: int, current_depth: int, counter: Array[int], max_nodes: int) -> Dictionary:
|
|
487
488
|
counter[0] += 1
|
|
488
489
|
var entry: Dictionary = {
|
|
@@ -497,7 +498,7 @@ func _build_tree(node: Node, max_depth: int, current_depth: int, counter: Array[
|
|
|
497
498
|
if counter[0] > max_nodes:
|
|
498
499
|
# 超软上限:不再下递归,但当前 entry 已计入;调用方附加 truncated 信号
|
|
499
500
|
return entry
|
|
500
|
-
var effective_max: int =
|
|
501
|
+
var effective_max: int = _BUILD_TREE_DEFAULT_MAX_DEPTH if max_depth == 0 else max_depth
|
|
501
502
|
if current_depth < effective_max:
|
|
502
503
|
var children: Array[Dictionary] = []
|
|
503
504
|
for child: Node in node.get_children():
|
|
@@ -70,10 +70,17 @@ class StubInputSimulationApi:
|
|
|
70
70
|
# _register_methods 之前测试会手动调一次。
|
|
71
71
|
combo_callback = send_response
|
|
72
72
|
|
|
73
|
+
# 断连测试用的 release_all 调用计数(不 override 行为,仅记账后走父类)
|
|
74
|
+
var release_all_calls: int = 0
|
|
75
|
+
|
|
73
76
|
func handle_action_press(params: Dictionary) -> Dictionary:
|
|
74
77
|
press_calls.append(params)
|
|
75
78
|
return press_return
|
|
76
79
|
|
|
80
|
+
func release_all() -> void:
|
|
81
|
+
release_all_calls += 1
|
|
82
|
+
super()
|
|
83
|
+
|
|
77
84
|
func handle_combo(params: Dictionary, request_id: String) -> void:
|
|
78
85
|
# 不立刻回响 —— 把 (params, request_id) 记下来;测试通过 finish_combo
|
|
79
86
|
# 显式触发 callback,模拟真实 combo 完成路径。
|
|
@@ -122,6 +129,49 @@ func _last_frame() -> Dictionary:
|
|
|
122
129
|
return _bridge.captured_frames[-1]
|
|
123
130
|
|
|
124
131
|
|
|
132
|
+
# ── 断连按 close code 区分清/不清 ───────────────────────────────────
|
|
133
|
+
# 回归:CLI 每条子命令都是独立连接、跑完即「干净关闭」(close frame,code
|
|
134
|
+
# 1000)。干净关闭不能 release_all,否则 `hold <dur>` 定时器没倒计时就被清掉
|
|
135
|
+
# (只生效一帧),sticky `press` 也无法跨命令存活。只有「异常掉线」(崩溃 /
|
|
136
|
+
# kill / 网络断,get_close_code() == -1)才 release_all 兜底卡死键。
|
|
137
|
+
# handle_hold 是真实逻辑(桩未 override),用它验证 held 状态。
|
|
138
|
+
# 用足够长的 duration,避免测试期间 _process 的 advance_timers 提前释放。
|
|
139
|
+
|
|
140
|
+
func test_clean_disconnect_preserves_inputs() -> void:
|
|
141
|
+
var hold_action := "__test_clean_disc__"
|
|
142
|
+
if not InputMap.has_action(hold_action):
|
|
143
|
+
InputMap.add_action(hold_action)
|
|
144
|
+
_input.handle_hold({"action": hold_action, "duration": 999.0})
|
|
145
|
+
assert_true(hold_action in _input.get_pressed_actions(), "前置:hold 应在持有列表")
|
|
146
|
+
|
|
147
|
+
# 1000 = 正常 WebSocket close frame(CLI 命令跑完)
|
|
148
|
+
_bridge._handle_disconnect(1000)
|
|
149
|
+
|
|
150
|
+
assert_eq(_input.release_all_calls, 0, "干净关闭不应调用 release_all(hold/press 须跨命令存活)")
|
|
151
|
+
assert_true(hold_action in _input.get_pressed_actions(), "干净关闭后 hold 应仍存活")
|
|
152
|
+
assert_eq(_bridge._active_peer, null, "断连应清掉 _active_peer")
|
|
153
|
+
|
|
154
|
+
_input.release_all()
|
|
155
|
+
InputMap.erase_action(hold_action)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
func test_abnormal_disconnect_releases_inputs() -> void:
|
|
159
|
+
var hold_action := "__test_abnormal_disc__"
|
|
160
|
+
if not InputMap.has_action(hold_action):
|
|
161
|
+
InputMap.add_action(hold_action)
|
|
162
|
+
_input.handle_hold({"action": hold_action, "duration": 999.0})
|
|
163
|
+
assert_true(hold_action in _input.get_pressed_actions(), "前置:hold 应在持有列表")
|
|
164
|
+
|
|
165
|
+
# -1 = 异常掉线(无 close frame:崩溃 / kill / 网络断)
|
|
166
|
+
_bridge._handle_disconnect(-1)
|
|
167
|
+
|
|
168
|
+
assert_eq(_input.release_all_calls, 1, "异常掉线应调用 release_all 兜底卡死键")
|
|
169
|
+
assert_false(hold_action in _input.get_pressed_actions(), "异常掉线后 hold 应被释放")
|
|
170
|
+
assert_eq(_bridge._active_peer, null, "断连应清掉 _active_peer")
|
|
171
|
+
|
|
172
|
+
InputMap.erase_action(hold_action)
|
|
173
|
+
|
|
174
|
+
|
|
125
175
|
# ── JSON / 协议层校验:-32600 ──────────────────────────────────────
|
|
126
176
|
|
|
127
177
|
func test_invalid_json_emits_minus_32600_with_empty_id() -> void:
|
|
@@ -83,6 +83,22 @@ func test_hold_unknown_action_returns_1003() -> void:
|
|
|
83
83
|
assert_false(_api.has_active_holds())
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
func test_hold_zero_duration_returns_invalid_params() -> void:
|
|
87
|
+
# duration <= 0 是无意义的「按住 0 秒」;防御纵深拦在服务端(CLI preflight 也拦)。
|
|
88
|
+
var result: Dictionary = _api.handle_hold({"action": "a", "duration": 0.0})
|
|
89
|
+
assert_has(result, "error")
|
|
90
|
+
assert_eq(int(result.error.code), -32602, "duration=0 应回 INVALID_PARAMS(-32602)")
|
|
91
|
+
assert_false(_api.has_active_holds(), "非法 duration 不应进 held 列表")
|
|
92
|
+
assert_false("a" in _api.get_pressed_actions())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
func test_hold_negative_duration_returns_invalid_params() -> void:
|
|
96
|
+
var result: Dictionary = _api.handle_hold({"action": "a", "duration": -1.5})
|
|
97
|
+
assert_has(result, "error")
|
|
98
|
+
assert_eq(int(result.error.code), -32602)
|
|
99
|
+
assert_false(_api.has_active_holds())
|
|
100
|
+
|
|
101
|
+
|
|
86
102
|
func test_combo_unknown_action_aborts_with_1003() -> void:
|
|
87
103
|
# combo step 引用未注册 action:必须 abort 整盘并通过 request_id 回 1003,
|
|
88
104
|
# 否则 _combo_active 卡 true 后续全 1004。
|
|
@@ -631,11 +631,11 @@ func test_build_tree_short_circuits_above_node_limit() -> void:
|
|
|
631
631
|
var leaf: Node = Node.new()
|
|
632
632
|
leaf.name = "Leaf"
|
|
633
633
|
add_child_autofree(leaf)
|
|
634
|
-
var counter: Array[int] = [LowLevelApiScript.
|
|
634
|
+
var counter: Array[int] = [LowLevelApiScript._BUILD_TREE_MAX_NODES]
|
|
635
635
|
# 第 5 参数 max_nodes = LIMIT(5000);leaf 计入后 counter == LIMIT+1 > max_nodes,触发短路
|
|
636
|
-
var entry: Dictionary = _api._build_tree(leaf, 5, 0, counter, LowLevelApiScript.
|
|
636
|
+
var entry: Dictionary = _api._build_tree(leaf, 5, 0, counter, LowLevelApiScript._BUILD_TREE_MAX_NODES)
|
|
637
637
|
# leaf 自身被计入 → counter 变成 LIMIT+1
|
|
638
|
-
assert_eq(int(counter[0]), LowLevelApiScript.
|
|
638
|
+
assert_eq(int(counter[0]), LowLevelApiScript._BUILD_TREE_MAX_NODES + 1)
|
|
639
639
|
# 超 limit 后立刻 return,不下递归 children
|
|
640
640
|
assert_does_not_have(entry, "children")
|
|
641
641
|
|
|
@@ -650,14 +650,14 @@ func test_build_tree_under_limit_includes_children() -> void:
|
|
|
650
650
|
parent.add_child(c1)
|
|
651
651
|
var counter: Array[int] = [0]
|
|
652
652
|
# 第 5 参数 max_nodes 给硬墙 5000,远超 2 个节点,不会触发软截断
|
|
653
|
-
var entry: Dictionary = _api._build_tree(parent, 5, 0, counter, LowLevelApiScript.
|
|
653
|
+
var entry: Dictionary = _api._build_tree(parent, 5, 0, counter, LowLevelApiScript._BUILD_TREE_MAX_NODES)
|
|
654
654
|
assert_has(entry, "children")
|
|
655
655
|
assert_eq((entry.children as Array).size(), 1)
|
|
656
656
|
|
|
657
657
|
|
|
658
658
|
func test_handle_get_scene_tree_clamps_oversized_max_nodes() -> void:
|
|
659
659
|
# P1 回归:恶意/失误客户端传 max_nodes=999999 时,
|
|
660
|
-
# 入口必须 clamp 到
|
|
660
|
+
# 入口必须 clamp 到 _BUILD_TREE_MAX_NODES,
|
|
661
661
|
# 否则 _build_tree 会构造完整大字典再被外层 1005 丢弃(DoS 风险)。
|
|
662
662
|
# 这里 happy path 验证 clamp 不破坏正常调用——简单场景下无 1005 报错、无 truncated 信号。
|
|
663
663
|
var result: Dictionary = _api.handle_get_scene_tree({
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""跨平台 GUT runner —— run_gut.sh 的 Python 等价物(issue #36)。
|
|
3
|
+
|
|
4
|
+
run_gut.sh 是 bash,Windows runner 用不了;macOS 上 GUT cmdln 的 stdout buffering
|
|
5
|
+
历史上偶发丢字。改用 Python stdlib(tempfile / shutil / subprocess)抹平
|
|
6
|
+
mktemp / cp -r / 路径分隔符这些平台差异,让 CI 能在 ubuntu / macOS / windows
|
|
7
|
+
三格统一跑 GDScript 单测。
|
|
8
|
+
|
|
9
|
+
逻辑与 run_gut.sh 一致:
|
|
10
|
+
1. 找 Godot 二进制(GODOT_BIN > macOS 默认 .app > PATH 中的 godot[.exe])。
|
|
11
|
+
2. 临时目录搭最小 Godot 工程 + 复制本 plugin。
|
|
12
|
+
3. clone GUT 固定 tag,搬运 addons/gut。
|
|
13
|
+
4. headless 预热 import(填 .godot/ 缓存)。
|
|
14
|
+
5. headless 跑 gut_cmdln.gd,实时透传输出并缓冲。
|
|
15
|
+
6. 断言 GUT 的 "All tests passed!" marker(cmdln 加载失败时 Godot 仍可能 exit 0,
|
|
16
|
+
所以不能只看 returncode)。
|
|
17
|
+
|
|
18
|
+
用法:
|
|
19
|
+
GODOT_BIN=/path/to/godot python3 run_gut.py
|
|
20
|
+
python3 run_gut.py # 未设 GODOT_BIN 时尝试 macOS 默认 / PATH
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import tempfile
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# Windows 默认 stdout/stderr 编码是 cp1252,编不了中文日志或 Godot 输出里的非 ASCII
|
|
33
|
+
# 字符(issue #36:Windows CI 格曾因 `_log("使用 Godot…")` 在第一行就 UnicodeEncodeError
|
|
34
|
+
# 崩掉,GUT 根本没跑起来)。强制 UTF-8,macOS/Linux 本就默认 UTF-8 不受影响。
|
|
35
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
36
|
+
try:
|
|
37
|
+
_stream.reconfigure(encoding="utf-8") # type: ignore[union-attr]
|
|
38
|
+
except (AttributeError, ValueError): # 非 TextIOWrapper(被重定向)时静默跳过
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
GUT_REF = "v9.4.0" # bumping:检查 https://github.com/bitwes/Gut/releases
|
|
42
|
+
|
|
43
|
+
# 仓库根:本脚本在 addons/godot_cli_control/tests/,往上 3 级。
|
|
44
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
45
|
+
|
|
46
|
+
_PROJECT_GODOT = """\
|
|
47
|
+
config_version=5
|
|
48
|
+
|
|
49
|
+
[application]
|
|
50
|
+
config/name="gut-tests"
|
|
51
|
+
config/features=PackedStringArray("4.4", "GL Compatibility")
|
|
52
|
+
|
|
53
|
+
[rendering]
|
|
54
|
+
renderer/rendering_method="gl_compatibility"
|
|
55
|
+
renderer/rendering_method.mobile="gl_compatibility"
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
_SUCCESS_MARKER = "All tests passed!"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _log(msg: str) -> None:
|
|
62
|
+
print(f"==> {msg}", flush=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fail(msg: str) -> "None":
|
|
66
|
+
print(f"FAIL: {msg}", file=sys.stderr, flush=True)
|
|
67
|
+
raise SystemExit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _find_godot() -> str:
|
|
71
|
+
"""GODOT_BIN > macOS 默认 .app > PATH 中的 godot/godot.exe。"""
|
|
72
|
+
env_bin = os.environ.get("GODOT_BIN")
|
|
73
|
+
if env_bin:
|
|
74
|
+
if not (Path(env_bin).is_file() and os.access(env_bin, os.X_OK)):
|
|
75
|
+
_fail(f"GODOT_BIN 指向的不是可执行文件:{env_bin}")
|
|
76
|
+
return env_bin
|
|
77
|
+
|
|
78
|
+
mac_default = Path("/Applications/Godot.app/Contents/MacOS/Godot")
|
|
79
|
+
if mac_default.is_file() and os.access(mac_default, os.X_OK):
|
|
80
|
+
return str(mac_default)
|
|
81
|
+
|
|
82
|
+
for name in ("godot", "godot.exe", "Godot"):
|
|
83
|
+
found = shutil.which(name)
|
|
84
|
+
if found:
|
|
85
|
+
return found
|
|
86
|
+
|
|
87
|
+
_fail("找不到 Godot 二进制(设置 GODOT_BIN 或加入 PATH)")
|
|
88
|
+
raise AssertionError("unreachable") # 给类型检查器:_fail 永远 raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _run_godot(godot: str, *args: str, capture: bool = False) -> subprocess.CompletedProcess[str]:
|
|
92
|
+
return subprocess.run(
|
|
93
|
+
[godot, *args],
|
|
94
|
+
capture_output=capture,
|
|
95
|
+
text=True,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _clone_gut(dest_parent: Path) -> Path:
|
|
100
|
+
"""clone GUT 到临时目录,返回其 addons/gut 路径。"""
|
|
101
|
+
_log(f"下载 GUT {GUT_REF}")
|
|
102
|
+
gut_src = dest_parent / "gut-src"
|
|
103
|
+
res = subprocess.run(
|
|
104
|
+
[
|
|
105
|
+
"git", "clone", "--depth", "1", "--branch", GUT_REF,
|
|
106
|
+
"https://github.com/bitwes/Gut.git", str(gut_src),
|
|
107
|
+
],
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True,
|
|
110
|
+
)
|
|
111
|
+
if res.returncode != 0:
|
|
112
|
+
_fail(f"git clone GUT 失败:\n{res.stderr}")
|
|
113
|
+
gut_addon = gut_src / "addons" / "gut"
|
|
114
|
+
if not (gut_addon / "gut_cmdln.gd").is_file():
|
|
115
|
+
_fail(f"GUT {GUT_REF} 内未找到 addons/gut/gut_cmdln.gd(GUT 目录结构变了?)")
|
|
116
|
+
return gut_addon
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _run_gut_cmdln(godot: str, proj: Path) -> str:
|
|
120
|
+
"""实时透传 gut_cmdln 输出并缓冲返回(等价 bash 的 `| tee`)。"""
|
|
121
|
+
_log("跑 GUT")
|
|
122
|
+
proc = subprocess.Popen(
|
|
123
|
+
[
|
|
124
|
+
godot, "--headless", "--path", str(proj),
|
|
125
|
+
"-s", "res://addons/gut/gut_cmdln.gd",
|
|
126
|
+
"-gdir=res://addons/godot_cli_control/tests/gut",
|
|
127
|
+
"-gexit",
|
|
128
|
+
],
|
|
129
|
+
stdout=subprocess.PIPE,
|
|
130
|
+
stderr=subprocess.STDOUT,
|
|
131
|
+
text=True,
|
|
132
|
+
bufsize=1,
|
|
133
|
+
)
|
|
134
|
+
buf: list[str] = []
|
|
135
|
+
assert proc.stdout is not None
|
|
136
|
+
for line in proc.stdout:
|
|
137
|
+
sys.stdout.write(line)
|
|
138
|
+
sys.stdout.flush()
|
|
139
|
+
buf.append(line)
|
|
140
|
+
proc.wait()
|
|
141
|
+
return "".join(buf)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main() -> int:
|
|
145
|
+
godot = _find_godot()
|
|
146
|
+
_log(f"使用 Godot: {godot}")
|
|
147
|
+
_run_godot(godot, "--version")
|
|
148
|
+
|
|
149
|
+
# 一个临时父目录装 工程 + GUT clone;with 块退出自动清理(跨平台、无 trap)。
|
|
150
|
+
with tempfile.TemporaryDirectory(prefix="godot-cli-control-gut-") as tmp:
|
|
151
|
+
tmp_path = Path(tmp)
|
|
152
|
+
proj = tmp_path / "proj"
|
|
153
|
+
proj.mkdir()
|
|
154
|
+
_log(f"临时项目: {proj}")
|
|
155
|
+
|
|
156
|
+
# 1) 最小 Godot 工程
|
|
157
|
+
(proj / "project.godot").write_text(_PROJECT_GODOT, encoding="utf-8")
|
|
158
|
+
|
|
159
|
+
# 2) 被测 plugin
|
|
160
|
+
(proj / "addons").mkdir()
|
|
161
|
+
shutil.copytree(
|
|
162
|
+
REPO_ROOT / "addons" / "godot_cli_control",
|
|
163
|
+
proj / "addons" / "godot_cli_control",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# 3) GUT
|
|
167
|
+
gut_addon = _clone_gut(tmp_path)
|
|
168
|
+
shutil.copytree(gut_addon, proj / "addons" / "gut")
|
|
169
|
+
|
|
170
|
+
# 4) headless 预热 import(失败不致命,与 .sh 一致:用 || true 语义)
|
|
171
|
+
_log("import 资源")
|
|
172
|
+
_run_godot(godot, "--headless", "--path", str(proj), "--editor", "--quit",
|
|
173
|
+
capture=True)
|
|
174
|
+
|
|
175
|
+
# 5) 跑 GUT
|
|
176
|
+
output = _run_gut_cmdln(godot, proj)
|
|
177
|
+
|
|
178
|
+
# 6) Godot 在 cmdln 脚本加载失败时仍可能 exit 0 —— 额外断言 GUT 成功 marker。
|
|
179
|
+
if _SUCCESS_MARKER in output:
|
|
180
|
+
_log("GUT PASS")
|
|
181
|
+
return 0
|
|
182
|
+
_fail(f"没看到 GUT 的 '{_SUCCESS_MARKER}' marker —— 看上面输出排查")
|
|
183
|
+
return 1 # unreachable(_fail raise)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
raise SystemExit(main())
|
{godot_cli_control-0.2.8 → godot_cli_control-0.2.10}/addons/godot_cli_control/tests/run_gut.sh
RENAMED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
# 没有把 GUT vendor 进仓库(它是开发依赖,不该跟 plugin 一起发布到 PyPI/AssetLib);
|
|
5
5
|
# 临时项目从头建,避免污染仓库的 .godot/ import 缓存。
|
|
6
6
|
#
|
|
7
|
+
# 跨平台:CI(ubuntu / macOS / windows 三格)跑的是同目录的 run_gut.py
|
|
8
|
+
# (bash 在 Windows 用不了)。本 .sh 保留给 Linux / macOS 本地开发者方便用;
|
|
9
|
+
# 两者逻辑等价,改一个记得对齐另一个。
|
|
10
|
+
#
|
|
7
11
|
# 用法:
|
|
8
12
|
# GODOT_BIN=/path/to/godot ./run_gut.sh
|
|
9
13
|
# (未设置 GODOT_BIN 时尝试 macOS 默认路径或 PATH 中的 godot)
|
|
@@ -22,6 +22,15 @@ godot-cli-control = "godot_cli_control.cli:main"
|
|
|
22
22
|
[project.entry-points.pytest11]
|
|
23
23
|
godot-cli-control = "godot_cli_control.pytest_plugin"
|
|
24
24
|
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
# gui:需要真实显示(开窗)才能跑的端到端测试。普通 unit / headless e2e 不带此
|
|
27
|
+
# marker。本地默认随 GODOT_BIN / GCC_GUI_E2E 未设而 skip;CI 专档(Linux 套
|
|
28
|
+
# xvfb 或 macOS runner)设 GCC_GUI_E2E=1 开启。注册在此避免 unknown-marker 警告,
|
|
29
|
+
# 也让 `pytest -m "not gui"` 能稳定剔除。
|
|
30
|
+
markers = [
|
|
31
|
+
"gui: 需要真实显示的 windowed e2e(screenshot frame_post_draw 路径,issue #64)",
|
|
32
|
+
]
|
|
33
|
+
|
|
25
34
|
[build-system]
|
|
26
35
|
requires = ["hatchling", "hatch-vcs"]
|
|
27
36
|
build-backend = "hatchling.build"
|
|
@@ -159,7 +159,10 @@ coverage run -m pytest python/tests/
|
|
|
159
159
|
coverage report
|
|
160
160
|
|
|
161
161
|
# GUT tests for the Godot plugin (needs GODOT_BIN env var)
|
|
162
|
+
# bash (Linux/macOS):
|
|
162
163
|
GODOT_BIN=/path/to/godot ./addons/godot_cli_control/tests/run_gut.sh
|
|
164
|
+
# cross-platform (Linux/macOS/Windows) — what CI runs:
|
|
165
|
+
GODOT_BIN=/path/to/godot python addons/godot_cli_control/tests/run_gut.py
|
|
163
166
|
```
|
|
164
167
|
|
|
165
168
|
## Documentation
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.10'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 10)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -175,6 +175,22 @@ def _resolve_args_for_call(ns: argparse.Namespace) -> list:
|
|
|
175
175
|
return [_parse_json_arg(a) for a in raw_args]
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def _preflight_hold(ns: argparse.Namespace) -> None:
|
|
179
|
+
"""连 daemon 前校验 hold 的 duration:必须是 > 0 的数字。
|
|
180
|
+
|
|
181
|
+
duration <= 0 会让动作下一帧就释放(只生效一帧),是无意义用法;
|
|
182
|
+
要无限按住应该用 ``press``。preflight 拦住,避免 agent 干等连接重试。
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
duration = float(ns.duration)
|
|
186
|
+
except (TypeError, ValueError):
|
|
187
|
+
raise ValueError(f"hold: duration 必须是数字,收到 {ns.duration!r}")
|
|
188
|
+
if duration <= 0:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"hold: duration 必须 > 0(秒),收到 {duration};要无限按住请用 `press`"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
178
194
|
def _preflight_combo(ns: argparse.Namespace) -> None:
|
|
179
195
|
"""连 daemon 前用同一份解析逻辑校验 combo 输入;抛 ValueError 即用法错。
|
|
180
196
|
|
|
@@ -549,9 +565,10 @@ RPC_SPECS: tuple[RpcSpec, ...] = (
|
|
|
549
565
|
description="按住动作指定时长(秒),到点自动释放。",
|
|
550
566
|
positionals=(
|
|
551
567
|
Positional("action", None, "InputMap 动作名"),
|
|
552
|
-
Positional("duration", None, "
|
|
568
|
+
Positional("duration", None, "按住时长(秒,必须 > 0)"),
|
|
553
569
|
),
|
|
554
570
|
example="hold jump 1.5",
|
|
571
|
+
preflight=_preflight_hold,
|
|
555
572
|
text_formatter=lambda r: f"holding: {r}",
|
|
556
573
|
),
|
|
557
574
|
RpcSpec(
|
|
@@ -893,7 +910,8 @@ def cmd_daemon_status(ns: argparse.Namespace) -> int:
|
|
|
893
910
|
def cmd_daemon_ls(ns: argparse.Namespace) -> int:
|
|
894
911
|
"""跨项目列出运行中的 daemon。
|
|
895
912
|
|
|
896
|
-
|
|
913
|
+
扫全局注册表(POSIX `~/.local/state/godot-cli-control/daemons/`;Windows
|
|
914
|
+
`%LOCALAPPDATA%\\godot-cli-control\\daemons\\`),对每条记录探活。
|
|
897
915
|
死记录会被 list_all 自动清理(连同对应项目的 .cli_control/godot.pid 与 port)。
|
|
898
916
|
JSON 模式:{"ok": true, "result": {"daemons": [...]}}(信封一致)。
|
|
899
917
|
Text 模式:每条一行 `<pid>\t<port>\t<project_root>\t<started_at>`;空时 (no running daemons)。
|
|
@@ -1494,7 +1512,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1494
1512
|
"ls",
|
|
1495
1513
|
help="列出所有正在运行的 daemon(跨项目)",
|
|
1496
1514
|
description=(
|
|
1497
|
-
"
|
|
1515
|
+
"扫描全局注册表(POSIX ~/.local/state/godot-cli-control/daemons/;"
|
|
1516
|
+
"Windows %LOCALAPPDATA%\\godot-cli-control\\daemons\\),"
|
|
1498
1517
|
"列出所有探活通过的 daemon。死记录会被自动清理。"
|
|
1499
1518
|
),
|
|
1500
1519
|
)
|