debug-control-plane 0.1.0__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.
- debug_control_plane-0.1.0/LICENSE +21 -0
- debug_control_plane-0.1.0/PKG-INFO +98 -0
- debug_control_plane-0.1.0/README.md +81 -0
- debug_control_plane-0.1.0/debug_control_plane/__init__.py +7 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/__init__.py +82 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/device_candidates.py +326 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/device_pool.py +369 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/discovery/__init__.py +14 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/discovery/cross_identify.py +396 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/discovery/lan_scan.py +183 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/discovery/manual_registry.py +245 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/discovery/usb_identity.py +228 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/discovery/vpn_immune.py +103 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/endpoint.py +317 -0
- debug_control_plane-0.1.0/debug_control_plane/device_discovery/protocol.py +230 -0
- debug_control_plane-0.1.0/debug_control_plane/mcp_plane/__init__.py +47 -0
- debug_control_plane-0.1.0/debug_control_plane/mcp_plane/bridge_client.py +525 -0
- debug_control_plane-0.1.0/debug_control_plane/mcp_plane/capability_mirror.py +642 -0
- debug_control_plane-0.1.0/debug_control_plane/mcp_plane/semantic_provider.py +57 -0
- debug_control_plane-0.1.0/debug_control_plane/mcp_plane/server.py +874 -0
- debug_control_plane-0.1.0/debug_control_plane.egg-info/PKG-INFO +98 -0
- debug_control_plane-0.1.0/debug_control_plane.egg-info/SOURCES.txt +39 -0
- debug_control_plane-0.1.0/debug_control_plane.egg-info/dependency_links.txt +1 -0
- debug_control_plane-0.1.0/debug_control_plane.egg-info/entry_points.txt +2 -0
- debug_control_plane-0.1.0/debug_control_plane.egg-info/requires.txt +7 -0
- debug_control_plane-0.1.0/debug_control_plane.egg-info/top_level.txt +1 -0
- debug_control_plane-0.1.0/pyproject.toml +43 -0
- debug_control_plane-0.1.0/setup.cfg +4 -0
- debug_control_plane-0.1.0/tests/test_bridge_client.py +811 -0
- debug_control_plane-0.1.0/tests/test_capability_mirror.py +641 -0
- debug_control_plane-0.1.0/tests/test_device_candidates.py +304 -0
- debug_control_plane-0.1.0/tests/test_device_pool.py +389 -0
- debug_control_plane-0.1.0/tests/test_discovery_cross_identify.py +576 -0
- debug_control_plane-0.1.0/tests/test_discovery_lan_scan.py +527 -0
- debug_control_plane-0.1.0/tests/test_discovery_manual_registry.py +513 -0
- debug_control_plane-0.1.0/tests/test_discovery_usb_identity.py +510 -0
- debug_control_plane-0.1.0/tests/test_discovery_vpn_immune.py +330 -0
- debug_control_plane-0.1.0/tests/test_e2e_mock.py +1118 -0
- debug_control_plane-0.1.0/tests/test_endpoint.py +209 -0
- debug_control_plane-0.1.0/tests/test_import_sanity.py +100 -0
- debug_control_plane-0.1.0/tests/test_server.py +1134 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present tangxiaolu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debug-control-plane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-product reusable debug control plane (device discovery + MCP adapter).
|
|
5
|
+
Author: tangxiaolu
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: mcp<2,>=1.28
|
|
11
|
+
Requires-Dist: httpx>=0.28
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
14
|
+
Requires-Dist: pytest-cov>=4; extra == "test"
|
|
15
|
+
Requires-Dist: ruff>=0.4; extra == "test"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# debug-control-plane (Python)
|
|
19
|
+
|
|
20
|
+
Multi-product reusable debug control plane — Python slice.
|
|
21
|
+
多产品复用的调试控制平面——Python 切片。
|
|
22
|
+
|
|
23
|
+
Provides / 提供:
|
|
24
|
+
- **device_discovery**: USB/WiFi device discovery + cross-identity pairing (USB identity source / LAN scan / VPN-immune subnet / device pool with TTL).
|
|
25
|
+
USB/WiFi 设备发现 + 跨身份配对(USB 身份源 / LAN 扫描 / VPN 免疫网段 / 带 TTL 的设备池)。
|
|
26
|
+
- **mcp_plane**: MCP adapter over the R019 8-endpoint debug HTTP protocol — capability mirror (dynamic tools), bridge client, bare server, and a `SemanticProvider` protocol hook for product semantic sugar.
|
|
27
|
+
建立在 R019 8 端点调试 HTTP 协议之上的 MCP 适配器——能力镜像(动态 tools)、桥客户端、裸服务端,以及供产品语义糖接入的 `SemanticProvider` 协议钩子。
|
|
28
|
+
|
|
29
|
+
## Dependency direction / 依赖方向
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
business app (pantas_launcher / future products) 业务应用(pantas_launcher / 未来产品)
|
|
33
|
+
│ depends on (downward) 依赖(向下)
|
|
34
|
+
▼
|
|
35
|
+
debug_control_plane (this package) 本包
|
|
36
|
+
│ depends on (downward) 依赖(向下)
|
|
37
|
+
▼
|
|
38
|
+
mcp (SDK) · httpx (HTTP client)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This package is **pure infrastructure**: it MUST NOT import any business package (`pantas_launcher`, `host4_flutter_gmacro`, gamepad/feature code). Business apps depend on this package; never the reverse.
|
|
42
|
+
本包是**纯基础设施**:绝不 import 任何业务包(`pantas_launcher`、`host4_flutter_gmacro`、手柄/特性代码)。业务应用依赖本包,反之禁止。
|
|
43
|
+
|
|
44
|
+
## Install / 安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# editable install — picks up source changes without reinstall
|
|
48
|
+
# 可编辑安装——源码改动即时生效,无需重装
|
|
49
|
+
python -m pip install -e .
|
|
50
|
+
|
|
51
|
+
# import sanity check / 导入自检
|
|
52
|
+
python -c "import debug_control_plane; print(debug_control_plane.__version__)"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Layout / 结构
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
python/
|
|
59
|
+
├── pyproject.toml # single-package, two sub-packages / 单包双子包
|
|
60
|
+
├── README.md # this file
|
|
61
|
+
├── LICENSE # MIT
|
|
62
|
+
└── debug_control_plane/
|
|
63
|
+
├── __init__.py # __version__ = "0.1.0"
|
|
64
|
+
├── device_discovery/ # USB/LAN device discovery + device pool
|
|
65
|
+
│ ├── device_candidates.py
|
|
66
|
+
│ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
|
|
67
|
+
│ ├── endpoint.py
|
|
68
|
+
│ ├── protocol.py
|
|
69
|
+
│ └── discovery/
|
|
70
|
+
│ ├── usb_identity.py # USB identity source (adb / flutter devices) / USB 身份源
|
|
71
|
+
│ ├── lan_scan.py # LAN /hello probe / LAN /hello 探测
|
|
72
|
+
│ ├── vpn_immune.py # VPN-immune subnet calc / VPN 免疫网段计算
|
|
73
|
+
│ ├── cross_identify.py # USB ⊕ LAN cross pairing / USB ⊕ LAN 交叉配对
|
|
74
|
+
│ └── manual_registry.py # manually-told devices / 人工告知设备
|
|
75
|
+
└── mcp_plane/ # MCP adapter
|
|
76
|
+
├── capability_mirror.py # dynamic tools from /hello / 由 /hello 动态镜像 tools
|
|
77
|
+
├── bridge_client.py # HTTP bridge client / HTTP 桥客户端
|
|
78
|
+
├── server.py # bare stdio MCP server / 裸 stdio MCP 服务端
|
|
79
|
+
└── semantic_provider.py # SemanticProvider Protocol (product hook) / 语义糖协议钩子
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Console script / 控制台脚本
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
debug-control-plane-server
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no business capability assembly) for debugging / plane self-testing.
|
|
89
|
+
指向 `debug_control_plane.mcp_plane.server:main`——裸服务端(无业务能力装配),用于调试 / 平面自测。
|
|
90
|
+
|
|
91
|
+
## Version / 版本
|
|
92
|
+
|
|
93
|
+
`0.1.0` — initial release, API unstable. 首发,API 不稳定。
|
|
94
|
+
|
|
95
|
+
## License / 许可证
|
|
96
|
+
|
|
97
|
+
Licensed under the MIT License — see [LICENSE](LICENSE).
|
|
98
|
+
采用 MIT 许可证授权——见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# debug-control-plane (Python)
|
|
2
|
+
|
|
3
|
+
Multi-product reusable debug control plane — Python slice.
|
|
4
|
+
多产品复用的调试控制平面——Python 切片。
|
|
5
|
+
|
|
6
|
+
Provides / 提供:
|
|
7
|
+
- **device_discovery**: USB/WiFi device discovery + cross-identity pairing (USB identity source / LAN scan / VPN-immune subnet / device pool with TTL).
|
|
8
|
+
USB/WiFi 设备发现 + 跨身份配对(USB 身份源 / LAN 扫描 / VPN 免疫网段 / 带 TTL 的设备池)。
|
|
9
|
+
- **mcp_plane**: MCP adapter over the R019 8-endpoint debug HTTP protocol — capability mirror (dynamic tools), bridge client, bare server, and a `SemanticProvider` protocol hook for product semantic sugar.
|
|
10
|
+
建立在 R019 8 端点调试 HTTP 协议之上的 MCP 适配器——能力镜像(动态 tools)、桥客户端、裸服务端,以及供产品语义糖接入的 `SemanticProvider` 协议钩子。
|
|
11
|
+
|
|
12
|
+
## Dependency direction / 依赖方向
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
business app (pantas_launcher / future products) 业务应用(pantas_launcher / 未来产品)
|
|
16
|
+
│ depends on (downward) 依赖(向下)
|
|
17
|
+
▼
|
|
18
|
+
debug_control_plane (this package) 本包
|
|
19
|
+
│ depends on (downward) 依赖(向下)
|
|
20
|
+
▼
|
|
21
|
+
mcp (SDK) · httpx (HTTP client)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This package is **pure infrastructure**: it MUST NOT import any business package (`pantas_launcher`, `host4_flutter_gmacro`, gamepad/feature code). Business apps depend on this package; never the reverse.
|
|
25
|
+
本包是**纯基础设施**:绝不 import 任何业务包(`pantas_launcher`、`host4_flutter_gmacro`、手柄/特性代码)。业务应用依赖本包,反之禁止。
|
|
26
|
+
|
|
27
|
+
## Install / 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# editable install — picks up source changes without reinstall
|
|
31
|
+
# 可编辑安装——源码改动即时生效,无需重装
|
|
32
|
+
python -m pip install -e .
|
|
33
|
+
|
|
34
|
+
# import sanity check / 导入自检
|
|
35
|
+
python -c "import debug_control_plane; print(debug_control_plane.__version__)"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Layout / 结构
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
python/
|
|
42
|
+
├── pyproject.toml # single-package, two sub-packages / 单包双子包
|
|
43
|
+
├── README.md # this file
|
|
44
|
+
├── LICENSE # MIT
|
|
45
|
+
└── debug_control_plane/
|
|
46
|
+
├── __init__.py # __version__ = "0.1.0"
|
|
47
|
+
├── device_discovery/ # USB/LAN device discovery + device pool
|
|
48
|
+
│ ├── device_candidates.py
|
|
49
|
+
│ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
|
|
50
|
+
│ ├── endpoint.py
|
|
51
|
+
│ ├── protocol.py
|
|
52
|
+
│ └── discovery/
|
|
53
|
+
│ ├── usb_identity.py # USB identity source (adb / flutter devices) / USB 身份源
|
|
54
|
+
│ ├── lan_scan.py # LAN /hello probe / LAN /hello 探测
|
|
55
|
+
│ ├── vpn_immune.py # VPN-immune subnet calc / VPN 免疫网段计算
|
|
56
|
+
│ ├── cross_identify.py # USB ⊕ LAN cross pairing / USB ⊕ LAN 交叉配对
|
|
57
|
+
│ └── manual_registry.py # manually-told devices / 人工告知设备
|
|
58
|
+
└── mcp_plane/ # MCP adapter
|
|
59
|
+
├── capability_mirror.py # dynamic tools from /hello / 由 /hello 动态镜像 tools
|
|
60
|
+
├── bridge_client.py # HTTP bridge client / HTTP 桥客户端
|
|
61
|
+
├── server.py # bare stdio MCP server / 裸 stdio MCP 服务端
|
|
62
|
+
└── semantic_provider.py # SemanticProvider Protocol (product hook) / 语义糖协议钩子
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Console script / 控制台脚本
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
debug-control-plane-server
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no business capability assembly) for debugging / plane self-testing.
|
|
72
|
+
指向 `debug_control_plane.mcp_plane.server:main`——裸服务端(无业务能力装配),用于调试 / 平面自测。
|
|
73
|
+
|
|
74
|
+
## Version / 版本
|
|
75
|
+
|
|
76
|
+
`0.1.0` — initial release, API unstable. 首发,API 不稳定。
|
|
77
|
+
|
|
78
|
+
## License / 许可证
|
|
79
|
+
|
|
80
|
+
Licensed under the MIT License — see [LICENSE](LICENSE).
|
|
81
|
+
采用 MIT 许可证授权——见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""debug_control_plane — multi-product debug control plane.
|
|
2
|
+
|
|
3
|
+
Reusable across products: device discovery (USB/WiFi/identity) + MCP adapter
|
|
4
|
+
(debug HTTP protocol → MCP tool surface). Extracted from pantas_launcher R019/
|
|
5
|
+
R020 (S2 Python slice, R021-BF004).
|
|
6
|
+
"""
|
|
7
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""device_discovery — 设备发现平面 (网络 DTO + 设备池 + 发现逻辑).
|
|
2
|
+
|
|
3
|
+
BF006 迁入 (R021):
|
|
4
|
+
- network 3 模块 (protocol / endpoint / device_candidates, 字节级零修改,
|
|
5
|
+
自旧 network 子包)
|
|
6
|
+
- device_pool (认身份不认地址)
|
|
7
|
+
- discovery/ 5 模块 (USB / LAN / 手动 / 交叉识别)
|
|
8
|
+
- AD-B9: DeviceUnreachable 下沉 (直继承 Exception 脱离协议层基类反向依赖;
|
|
9
|
+
BF007 删旧定义 + 改正向 import)
|
|
10
|
+
|
|
11
|
+
零业务依赖: 不 import 旧 network 子包 / host4 gmacro / launcher /
|
|
12
|
+
协议层 client / mcp plane. 纯 stdlib + 同包相对 import, 可独立 pip install.
|
|
13
|
+
"""
|
|
14
|
+
from .device_candidates import (
|
|
15
|
+
CommandRunner,
|
|
16
|
+
ConnectedDeviceEndpoint,
|
|
17
|
+
ConnectedDeviceInventory,
|
|
18
|
+
IosDeviceCandidate,
|
|
19
|
+
discover_connected_device_endpoints,
|
|
20
|
+
discover_connected_device_inventory,
|
|
21
|
+
discover_connected_devices,
|
|
22
|
+
discover_ios_flutter_candidates,
|
|
23
|
+
)
|
|
24
|
+
from .device_pool import DevicePool, DeviceRecord
|
|
25
|
+
from .discovery.cross_identify import CrossIdentify
|
|
26
|
+
from .discovery.lan_scan import LanCandidate, LanScan
|
|
27
|
+
from .discovery.manual_registry import ManualRegistry
|
|
28
|
+
from .discovery.usb_identity import UsbCandidate, UsbIdentity
|
|
29
|
+
from .discovery.vpn_immune import VpnImmune
|
|
30
|
+
from .endpoint import (
|
|
31
|
+
Endpoint,
|
|
32
|
+
UrlOpen,
|
|
33
|
+
default_urlopen,
|
|
34
|
+
discover_default_endpoints,
|
|
35
|
+
discover_targets,
|
|
36
|
+
local_ipv4_addresses,
|
|
37
|
+
probe_hello,
|
|
38
|
+
)
|
|
39
|
+
from .protocol import (
|
|
40
|
+
ControllerProfile,
|
|
41
|
+
DebugEvent,
|
|
42
|
+
DeviceUnreachable,
|
|
43
|
+
NetworkState,
|
|
44
|
+
NetworkTarget,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# protocol (BF006 network DTO + AD-B9 DeviceUnreachable)
|
|
49
|
+
"NetworkTarget",
|
|
50
|
+
"NetworkState",
|
|
51
|
+
"DebugEvent",
|
|
52
|
+
"ControllerProfile",
|
|
53
|
+
"DeviceUnreachable",
|
|
54
|
+
# endpoint (BF006 network probe)
|
|
55
|
+
"Endpoint",
|
|
56
|
+
"UrlOpen",
|
|
57
|
+
"default_urlopen",
|
|
58
|
+
"probe_hello",
|
|
59
|
+
"discover_targets",
|
|
60
|
+
"discover_default_endpoints",
|
|
61
|
+
"local_ipv4_addresses",
|
|
62
|
+
# device_candidates (BF006 connected-device inventory)
|
|
63
|
+
"CommandRunner",
|
|
64
|
+
"ConnectedDeviceEndpoint",
|
|
65
|
+
"ConnectedDeviceInventory",
|
|
66
|
+
"IosDeviceCandidate",
|
|
67
|
+
"discover_connected_device_endpoints",
|
|
68
|
+
"discover_connected_device_inventory",
|
|
69
|
+
"discover_connected_devices",
|
|
70
|
+
"discover_ios_flutter_candidates",
|
|
71
|
+
# device_pool (BF006 认身份不认地址)
|
|
72
|
+
"DevicePool",
|
|
73
|
+
"DeviceRecord",
|
|
74
|
+
# discovery (BF006 USB/LAN/手动/交叉)
|
|
75
|
+
"LanScan",
|
|
76
|
+
"LanCandidate",
|
|
77
|
+
"UsbIdentity",
|
|
78
|
+
"UsbCandidate",
|
|
79
|
+
"ManualRegistry",
|
|
80
|
+
"VpnImmune",
|
|
81
|
+
"CrossIdentify",
|
|
82
|
+
]
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Connected device endpoint discovery for Network mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .endpoint import Endpoint
|
|
13
|
+
|
|
14
|
+
CommandRunner = Callable[[list[str], float], str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ConnectedDeviceEndpoint:
|
|
19
|
+
endpoint: Endpoint
|
|
20
|
+
label: str
|
|
21
|
+
platform: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ConnectedDeviceInventory:
|
|
26
|
+
device_id: str
|
|
27
|
+
label: str
|
|
28
|
+
platform: str
|
|
29
|
+
state: str
|
|
30
|
+
endpoint: Endpoint | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def discover_connected_device_endpoints(
|
|
34
|
+
*,
|
|
35
|
+
default_port: int = 18080,
|
|
36
|
+
run_command: CommandRunner | None = None,
|
|
37
|
+
) -> list[Endpoint]:
|
|
38
|
+
devices = discover_connected_devices(
|
|
39
|
+
default_port=default_port,
|
|
40
|
+
run_command=run_command,
|
|
41
|
+
)
|
|
42
|
+
return [device.endpoint for device in devices]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def discover_connected_device_inventory(
|
|
46
|
+
*,
|
|
47
|
+
default_port: int = 18080,
|
|
48
|
+
run_command: CommandRunner | None = None,
|
|
49
|
+
) -> list[ConnectedDeviceInventory]:
|
|
50
|
+
runner = run_command or _run_command
|
|
51
|
+
devices: list[ConnectedDeviceInventory] = []
|
|
52
|
+
|
|
53
|
+
for device in _android_connected_devices(runner, default_port):
|
|
54
|
+
devices.append(
|
|
55
|
+
ConnectedDeviceInventory(
|
|
56
|
+
device_id=f"{device.endpoint.host}:{device.endpoint.port}",
|
|
57
|
+
label=device.label,
|
|
58
|
+
platform="android",
|
|
59
|
+
state="connected",
|
|
60
|
+
endpoint=device.endpoint,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
for device in _ios_inventory_devices(runner):
|
|
65
|
+
if not any(
|
|
66
|
+
existing.platform == "ios" and existing.label == device.label
|
|
67
|
+
for existing in devices
|
|
68
|
+
):
|
|
69
|
+
devices.append(device)
|
|
70
|
+
|
|
71
|
+
return devices
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def discover_connected_devices(
|
|
75
|
+
*,
|
|
76
|
+
default_port: int = 18080,
|
|
77
|
+
run_command: CommandRunner | None = None,
|
|
78
|
+
) -> list[ConnectedDeviceEndpoint]:
|
|
79
|
+
runner = run_command or _run_command
|
|
80
|
+
devices: list[ConnectedDeviceEndpoint] = []
|
|
81
|
+
seen: set[tuple[str, int]] = set()
|
|
82
|
+
|
|
83
|
+
for device in _android_connected_devices(runner, default_port):
|
|
84
|
+
key = (device.endpoint.host, device.endpoint.port)
|
|
85
|
+
if key not in seen:
|
|
86
|
+
seen.add(key)
|
|
87
|
+
devices.append(device)
|
|
88
|
+
|
|
89
|
+
return devices
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _android_connected_devices(
|
|
93
|
+
run_command: CommandRunner,
|
|
94
|
+
default_port: int,
|
|
95
|
+
) -> list[ConnectedDeviceEndpoint]:
|
|
96
|
+
try:
|
|
97
|
+
output = run_command(["adb", "devices", "-l"], 5.0)
|
|
98
|
+
except OSError:
|
|
99
|
+
return []
|
|
100
|
+
devices: list[ConnectedDeviceEndpoint] = []
|
|
101
|
+
for line in output.splitlines()[1:]:
|
|
102
|
+
parts = line.split()
|
|
103
|
+
if len(parts) < 2 or parts[1] != "device":
|
|
104
|
+
continue
|
|
105
|
+
serial = parts[0]
|
|
106
|
+
ip = _android_device_ip(serial, run_command)
|
|
107
|
+
if ip is None:
|
|
108
|
+
continue
|
|
109
|
+
model = _metadata_value(parts, "model") or serial
|
|
110
|
+
devices.append(
|
|
111
|
+
ConnectedDeviceEndpoint(
|
|
112
|
+
endpoint=Endpoint(ip, default_port),
|
|
113
|
+
label=model,
|
|
114
|
+
platform="android",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
return devices
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _android_device_ip(serial: str, run_command: CommandRunner) -> str | None:
|
|
121
|
+
try:
|
|
122
|
+
output = run_command(["adb", "-s", serial, "shell", "ip", "route", "get", "1.1.1.1"], 5.0)
|
|
123
|
+
except OSError:
|
|
124
|
+
return None
|
|
125
|
+
match = re.search(r"\bsrc\s+(\d+\.\d+\.\d+\.\d+)\b", output)
|
|
126
|
+
return match.group(1) if match else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _ios_inventory_devices(run_command: CommandRunner) -> list[ConnectedDeviceInventory]:
|
|
130
|
+
try:
|
|
131
|
+
output = run_command(["xcrun", "devicectl", "list", "devices"], 8.0)
|
|
132
|
+
except OSError:
|
|
133
|
+
return []
|
|
134
|
+
devices: list[ConnectedDeviceInventory] = []
|
|
135
|
+
for line in output.splitlines():
|
|
136
|
+
if ".coredevice.local" not in line:
|
|
137
|
+
continue
|
|
138
|
+
parsed = _parse_ios_device_line(line)
|
|
139
|
+
if parsed is None:
|
|
140
|
+
continue
|
|
141
|
+
label, identifier, state = parsed
|
|
142
|
+
if state != "connected":
|
|
143
|
+
continue
|
|
144
|
+
devices.append(
|
|
145
|
+
ConnectedDeviceInventory(
|
|
146
|
+
device_id=identifier,
|
|
147
|
+
label=label,
|
|
148
|
+
platform="ios",
|
|
149
|
+
state=state,
|
|
150
|
+
endpoint=None,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return devices
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _parse_ios_device_line(line: str) -> tuple[str, str, str] | None:
|
|
157
|
+
hostname = _extract_coredevice_hostname(line)
|
|
158
|
+
if hostname is None:
|
|
159
|
+
return None
|
|
160
|
+
prefix, suffix = line.split(hostname, 1)
|
|
161
|
+
parts = suffix.split()
|
|
162
|
+
if len(parts) < 2:
|
|
163
|
+
return None
|
|
164
|
+
label = prefix.strip() or hostname
|
|
165
|
+
identifier = parts[0]
|
|
166
|
+
state = parts[1]
|
|
167
|
+
return label, identifier, state
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _is_available_ios_device(line: str) -> bool:
|
|
171
|
+
return " connected " in f" {line} "
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _extract_coredevice_hostname(line: str) -> str | None:
|
|
175
|
+
match = re.search(r"([\w.-]+\.coredevice\.local)", line)
|
|
176
|
+
return match.group(1) if match else None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _metadata_value(parts: list[str], key: str) -> str | None:
|
|
180
|
+
prefix = f"{key}:"
|
|
181
|
+
for part in parts:
|
|
182
|
+
if part.startswith(prefix):
|
|
183
|
+
return part[len(prefix) :]
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _run_command(command: list[str], timeout: float) -> str:
|
|
188
|
+
try:
|
|
189
|
+
result = subprocess.run(
|
|
190
|
+
command,
|
|
191
|
+
check=False,
|
|
192
|
+
capture_output=True,
|
|
193
|
+
text=True,
|
|
194
|
+
timeout=timeout,
|
|
195
|
+
)
|
|
196
|
+
except subprocess.TimeoutExpired as error:
|
|
197
|
+
output = error.stdout or ""
|
|
198
|
+
if isinstance(output, bytes):
|
|
199
|
+
return output.decode("utf-8", errors="replace")
|
|
200
|
+
return output
|
|
201
|
+
if result.returncode != 0:
|
|
202
|
+
raise OSError(result.stderr.strip() or f"command failed: {' '.join(command)}")
|
|
203
|
+
return result.stdout
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# R020-BF002: iOS device discovery via `flutter devices` (USB identity source).
|
|
208
|
+
#
|
|
209
|
+
# 新增独立函数,不改旧 `_ios_inventory_devices`(xcrun devicectl 路径,保留供 GUI
|
|
210
|
+
# 兼容)。原因:iPhone X 真机 iOS 16,`xcrun devicectl` 永远 unavailable
|
|
211
|
+
# (memory ios16-device-devicectl-pitfall),`flutter devices` 是可用路径。
|
|
212
|
+
#
|
|
213
|
+
# 设计见 .dev-flow/R020/analysis/2026-08-08--mcp-bridge-device-discovery-backend.md
|
|
214
|
+
# §2「iOS 发现不可用」+ analysis iOS/Android §USB 通道
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# macOS 上 flutter 不在默认 PATH,显式回退路径(防止 mcp_debug_bridge 进程环境没装 fvm)
|
|
219
|
+
_FLUTTER_CANDIDATES: tuple[str, ...] = (
|
|
220
|
+
"flutter",
|
|
221
|
+
"/usr/local/bin/flutter",
|
|
222
|
+
"/opt/homebrew/bin/flutter",
|
|
223
|
+
"fvm",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True)
|
|
228
|
+
class IosDeviceCandidate:
|
|
229
|
+
"""iOS 设备身份候选(来自 `flutter devices` 解析)。
|
|
230
|
+
|
|
231
|
+
供 R020 mcp_debug_bridge/discovery USB 身份源消费(BF004 UsbCandidate 用此)。
|
|
232
|
+
`device_id` = usbmuxd id(如 3992f440...,稳定身份源);
|
|
233
|
+
`model` = 机型显示名(iPhone X / iPhone 14 Pro 等,弱唯一供交叉识别用)。
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
device_id: str # usbmuxd id (`flutter devices --machine` id 字段)
|
|
237
|
+
model: str # 机型 (`flutter devices --machine` name 字段)
|
|
238
|
+
platform: str = "ios"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def discover_ios_flutter_candidates(
|
|
242
|
+
*,
|
|
243
|
+
run_command: CommandRunner | None = None,
|
|
244
|
+
) -> list[IosDeviceCandidate]:
|
|
245
|
+
"""iOS 设备身份候选(经 `flutter devices`,不调 devicectl)。
|
|
246
|
+
|
|
247
|
+
R020-BF002 落地:替代 iOS 16 不可用的 `_ios_inventory_devices`(devicectl 路径)。
|
|
248
|
+
解析 `flutter devices --machine` 的 JSON 输出,只取真机(emulator/simulator 排除)。
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
run_command: 可注入命令执行器(默认 _run_command),便于 mock 测试
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
IosDeviceCandidate 列表(空列表表示无真机或 flutter 不可用)
|
|
255
|
+
"""
|
|
256
|
+
runner = run_command or _run_command
|
|
257
|
+
output = _try_flutter_devices_machine(runner)
|
|
258
|
+
if output is None:
|
|
259
|
+
return []
|
|
260
|
+
return _parse_flutter_devices_machine(output)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _try_flutter_devices_machine(run_command: CommandRunner) -> str | None:
|
|
264
|
+
"""尝试多个 flutter 候选命令,返回首个成功的 `flutter devices --machine` 输出。
|
|
265
|
+
|
|
266
|
+
所有候选都失败(或 _FLUTTER_CANDIDATES 为空)→ None。
|
|
267
|
+
"""
|
|
268
|
+
for flutter in _FLUTTER_CANDIDATES:
|
|
269
|
+
try:
|
|
270
|
+
return run_command([flutter, "devices", "--machine"], 8.0)
|
|
271
|
+
except OSError:
|
|
272
|
+
continue
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _parse_flutter_devices_machine(output: str) -> list[IosDeviceCandidate]:
|
|
277
|
+
"""解析 `flutter devices --machine` JSON。
|
|
278
|
+
|
|
279
|
+
`flutter devices --machine` 输出 JSON 数组,元素结构(节选):
|
|
280
|
+
{
|
|
281
|
+
"id": "<usbmuxd id 或 emulator-id>",
|
|
282
|
+
"name": "iPhone X",
|
|
283
|
+
"targetPlatform": "ios",
|
|
284
|
+
"emulator": false,
|
|
285
|
+
"category": "mobile",
|
|
286
|
+
"platform": "ios" / "android-ios",
|
|
287
|
+
...
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
真机筛选:`emulator == false` 且 `targetPlatform`/`platform` 含 "ios"。
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
data = json.loads(output)
|
|
294
|
+
except (json.JSONDecodeError, ValueError):
|
|
295
|
+
return []
|
|
296
|
+
if not isinstance(data, list):
|
|
297
|
+
return []
|
|
298
|
+
candidates: list[IosDeviceCandidate] = []
|
|
299
|
+
for entry in data:
|
|
300
|
+
if not isinstance(entry, dict):
|
|
301
|
+
continue
|
|
302
|
+
if not _is_ios_physical_device(entry):
|
|
303
|
+
continue
|
|
304
|
+
device_id = entry.get("id")
|
|
305
|
+
model = entry.get("name") or entry.get("model")
|
|
306
|
+
if not isinstance(device_id, str) or not device_id:
|
|
307
|
+
continue
|
|
308
|
+
if not isinstance(model, str):
|
|
309
|
+
model = device_id
|
|
310
|
+
candidates.append(IosDeviceCandidate(device_id=device_id, model=model))
|
|
311
|
+
return candidates
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _is_ios_physical_device(entry: dict[str, Any]) -> bool:
|
|
315
|
+
"""判断 flutter devices 条目是否 iOS 物理真机(排除 simulator/emulator)。"""
|
|
316
|
+
is_emulator = entry.get("emulator")
|
|
317
|
+
if is_emulator is True:
|
|
318
|
+
return False
|
|
319
|
+
platform = str(entry.get("targetPlatform") or entry.get("platform") or "")
|
|
320
|
+
if "ios" not in platform:
|
|
321
|
+
return False
|
|
322
|
+
# simulator 设备类别/id 形如 "iOS Simulator" / "ios-simulator"
|
|
323
|
+
category = str(entry.get("category") or "")
|
|
324
|
+
if "simulator" in category.lower():
|
|
325
|
+
return False
|
|
326
|
+
return True
|