adbproxy 0.1.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 (49) hide show
  1. adbproxy/__init__.py +2 -0
  2. adbproxy/__main__.py +5 -0
  3. adbproxy/cli/__init__.py +1 -0
  4. adbproxy/cli/device.py +13 -0
  5. adbproxy/cli/main.py +60 -0
  6. adbproxy/cli/server.py +13 -0
  7. adbproxy/config.py +39 -0
  8. adbproxy/device/__init__.py +1 -0
  9. adbproxy/device/app.py +532 -0
  10. adbproxy/device/backend.py +242 -0
  11. adbproxy/device/banner.py +33 -0
  12. adbproxy/device/connection.py +505 -0
  13. adbproxy/device/session.py +304 -0
  14. adbproxy/device/stream_table.py +125 -0
  15. adbproxy/observers/__init__.py +1 -0
  16. adbproxy/observers/events.py +19 -0
  17. adbproxy/observers/install.py +126 -0
  18. adbproxy/observers/shell.py +371 -0
  19. adbproxy/observers/stream.py +94 -0
  20. adbproxy/observers/sync.py +550 -0
  21. adbproxy/protocols/__init__.py +1 -0
  22. adbproxy/protocols/adb.py +66 -0
  23. adbproxy/protocols/payload/__init__.py +17 -0
  24. adbproxy/protocols/payload/install.py +44 -0
  25. adbproxy/protocols/payload/shell_v2.py +28 -0
  26. adbproxy/protocols/payload/sync.py +180 -0
  27. adbproxy/protocols/smart_socket/__init__.py +33 -0
  28. adbproxy/protocols/smart_socket/classifier.py +78 -0
  29. adbproxy/protocols/smart_socket/conversation.py +335 -0
  30. adbproxy/protocols/smart_socket/handshake.py +75 -0
  31. adbproxy/protocols/smart_socket/wire.py +82 -0
  32. adbproxy/protocols/transport/__init__.py +52 -0
  33. adbproxy/protocols/transport/codec.py +90 -0
  34. adbproxy/protocols/transport/constants.py +21 -0
  35. adbproxy/protocols/transport/errors.py +5 -0
  36. adbproxy/runtime/__init__.py +1 -0
  37. adbproxy/runtime/adb_process.py +195 -0
  38. adbproxy/runtime/capture.py +41 -0
  39. adbproxy/runtime/logging.py +113 -0
  40. adbproxy/runtime/sockets.py +14 -0
  41. adbproxy/server/__init__.py +1 -0
  42. adbproxy/server/app.py +221 -0
  43. adbproxy/server/connection.py +378 -0
  44. adbproxy-0.1.0.dist-info/METADATA +254 -0
  45. adbproxy-0.1.0.dist-info/RECORD +49 -0
  46. adbproxy-0.1.0.dist-info/WHEEL +5 -0
  47. adbproxy-0.1.0.dist-info/entry_points.txt +4 -0
  48. adbproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
  49. adbproxy-0.1.0.dist-info/top_level.txt +1 -0
adbproxy/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """ADB proxy implementations and protocol observers."""
2
+
adbproxy/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """python -m adbproxy → 统一 CLI。"""
2
+
3
+ from adbproxy.cli.main import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """代理程序命令行入口(统一 adbproxy 与兼容旧脚本)。"""
adbproxy/cli/device.py ADDED
@@ -0,0 +1,13 @@
1
+ """ADB 设备端代理命令行入口。"""
2
+
3
+ import sys
4
+
5
+ from adbproxy.device.app import ClientMock
6
+
7
+
8
+ def main(argv=None):
9
+ ClientMock(sys.argv[1:] if argv is None else argv).serve()
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
adbproxy/cli/main.py ADDED
@@ -0,0 +1,60 @@
1
+ """统一 CLI:adbproxy server | device。
2
+
3
+ 不修改现有 cli/server.py、cli/device.py;通过临时改写 sys.argv
4
+ 再调用旧入口,保证子命令参数与直接运行旧脚本一致。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+
12
+ _TOP_HELP = """\
13
+ usage: adbproxy {server,device} ...
14
+
15
+ 子命令:
16
+ server client↔adb server smart-socket MITM(原 adb-server-proxy)
17
+ device 模拟网络 adbd / transport 转发(原 adb-device-proxy)
18
+
19
+ 示例:
20
+ adbproxy server --help
21
+ adbproxy device --listen 5566 --target usb
22
+ python -m adbproxy server --debug
23
+ """
24
+
25
+
26
+ def main(argv=None):
27
+ argv = list(sys.argv[1:] if argv is None else argv)
28
+
29
+ if not argv:
30
+ print(_TOP_HELP, file=sys.stderr)
31
+ return 2
32
+
33
+ if argv[0] in ("-h", "--help"):
34
+ print(_TOP_HELP)
35
+ return 0
36
+
37
+ cmd, rest = argv[0], argv[1:]
38
+ if cmd == "server":
39
+ from adbproxy.cli import server as sub
40
+ elif cmd == "device":
41
+ from adbproxy.cli import device as sub
42
+ else:
43
+ print(f"unknown command: {cmd}", file=sys.stderr)
44
+ print(_TOP_HELP, file=sys.stderr)
45
+ return 2
46
+
47
+ old_argv = sys.argv
48
+ try:
49
+ # 旧入口固定读 sys.argv[1:];剥掉子命令名后再调用,避免把
50
+ # "server"/"device" 传给 AdbProxy / ClientMock 的 argparse。
51
+ sys.argv = [old_argv[0], *rest]
52
+ result = sub.main()
53
+ finally:
54
+ sys.argv = old_argv
55
+
56
+ return 0 if result is None else result
57
+
58
+
59
+ if __name__ == "__main__":
60
+ raise SystemExit(main())
adbproxy/cli/server.py ADDED
@@ -0,0 +1,13 @@
1
+ """ADB 服务端代理命令行入口。"""
2
+
3
+ import sys
4
+
5
+ from adbproxy.server.app import AdbProxy
6
+
7
+
8
+ def main():
9
+ AdbProxy(sys.argv[1:]).serve()
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
adbproxy/config.py ADDED
@@ -0,0 +1,39 @@
1
+ import os
2
+ import struct
3
+
4
+
5
+ PROXY_PORT = 5037
6
+ REAL_ADB_PORT = 5038
7
+ # 源码布局:本文件在 src/adbproxy/config.py,项目根为 src 的上两级。
8
+ # 运行产物统一落到 <project>/out/{logs,files},可用 ADB_PROXY_OUT_DIR 覆盖 out 根目录。
9
+ _SRC_DIR = os.path.dirname(os.path.abspath(__file__))
10
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(_SRC_DIR))
11
+ _OUT_ROOT = os.environ.get(
12
+ "ADB_PROXY_OUT_DIR", os.path.join(_PROJECT_ROOT, "out")
13
+ )
14
+ LOG_DIR = os.path.join(_OUT_ROOT, "logs")
15
+ FILES_DIR = os.path.join(_OUT_ROOT, "files")
16
+ IDLE_TIMEOUT = 0.15
17
+ MAX_V2_BUF = 1 << 20 # 1 MiB,v2 半包重组缓冲上限
18
+ MAX_CMD = 4096 # 单条命令累积上限
19
+ SYNC_DATA_MAX = (
20
+ 64 * 1024
21
+ ) # 与 AOSP file_sync_protocol.h:146 一致,DATA 单块上限(校验用)
22
+ MAX_SYNC_FILE = 256 * 1024 * 1024 # 单文件落盘上限 256 MiB,防磁盘失控
23
+ # sync v2 定长 struct 大小(核对 file_sync_protocol.h,packed little-endian)
24
+ # sync_stat_v2: id,error,dev,ino,mode,nlink,uid,gid,size,atime,mtime,ctime
25
+ _STAT_V2_FMT = "<IIQQIIIIQqqq"
26
+ _STAT_V2_SIZE = struct.calcsize(_STAT_V2_FMT) # 72
27
+ # sync_dent_v2: stat_v2 + namelen
28
+ _DENT_V2_FMT = _STAT_V2_FMT + "I"
29
+ _DENT_V2_SIZE = struct.calcsize(_DENT_V2_FMT) # 76,后随 namelen 字节 name
30
+ # sync_stat_v1: id,mode,size,mtime
31
+ _STAT_V1_SIZE = struct.calcsize("<IIII") # 16
32
+ # sync_dent_v1: id,mode,size,mtime,namelen
33
+ _DENT_V1_SIZE = struct.calcsize("<IIIII") # 20,后随 namelen 字节 name
34
+ _SEND_V2_SIZE = struct.calcsize("<III") # sync_send_v2: id,mode,flags = 12
35
+ _RECV_V2_SIZE = struct.calcsize("<II") # sync_recv_v2: id,flags = 8
36
+
37
+ DEFAULT_LISTEN_PORT = 5566
38
+ DEFAULT_ADB_SERVER_PORT = 5037
39
+ DEFAULT_BANNER_FEATURES = "shell_v2,sendrecv_v2"
@@ -0,0 +1 @@
1
+ """ADB server 到 daemon 的传输代理。"""
adbproxy/device/app.py ADDED
@@ -0,0 +1,532 @@
1
+ import argparse
2
+ import datetime
3
+ import os
4
+ import signal
5
+ import socket
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+
10
+ from adbproxy.config import (
11
+ DEFAULT_ADB_SERVER_PORT,
12
+ DEFAULT_BANNER_FEATURES,
13
+ DEFAULT_LISTEN_PORT,
14
+ FILES_DIR,
15
+ LOG_DIR,
16
+ )
17
+ from adbproxy.protocols.adb import AdbProtocol
18
+ from adbproxy.runtime.adb_process import AdbServerLease
19
+ from adbproxy.runtime.logging import DebugHexLogger, FileLogger
20
+
21
+ from .connection import TransportConnection, _next_conn_id
22
+
23
+
24
+ class ClientMock:
25
+ """主流程:监听 --listen、accept 循环、可选自起 adb server / net 目标 connect、信号清理。
26
+
27
+ CLI 参数解析、运行生命周期与参考脚本保持一致。
28
+ """
29
+
30
+ def __init__(self, argv):
31
+ parser = argparse.ArgumentParser(
32
+ description="client-mock:模拟网络 adbd,MITM 转发到真实设备(传输协议↔5037 文本协议)"
33
+ )
34
+ parser.add_argument(
35
+ "--listen",
36
+ type=int,
37
+ default=DEFAULT_LISTEN_PORT,
38
+ help="监听端口(默认 5566)",
39
+ )
40
+ parser.add_argument(
41
+ "--target",
42
+ default="usb",
43
+ help="目标设备:serial:<s> | net:<ip>:<port> | usb(默认 usb 自动取首个 USB 设备)",
44
+ )
45
+ parser.add_argument(
46
+ "--adb-server-port",
47
+ type=int,
48
+ default=DEFAULT_ADB_SERVER_PORT,
49
+ help="后端 adb server 端口(默认 5037 复用系统;非 5037 自起并退出清理)",
50
+ )
51
+ parser.add_argument(
52
+ "--debug",
53
+ action="store_true",
54
+ help="每 recv 原始字节 hex 落盘 logs/debug-*.log",
55
+ )
56
+ parser.add_argument(
57
+ "--no-capture", action="store_true", help="禁用 sync/install 文件落盘"
58
+ )
59
+ parser.add_argument(
60
+ "--banner-type",
61
+ default="device",
62
+ help="CNXN banner type 前缀(效仿 adbd -b,默认 device)",
63
+ )
64
+ args = parser.parse_args(argv)
65
+ self.listen_port = args.listen
66
+ self.target = args.target
67
+ self.adb_server_port = args.adb_server_port
68
+ self.banner_type = (
69
+ args.banner_type
70
+ ) # 效仿 adbd adb_device_banner (adb_trace.cpp:34 + main.cpp:350)
71
+ self.banner_product = None # ro.product.name,None=回退
72
+ self.banner_model = None # ro.product.model
73
+ self.banner_device = None # ro.product.device
74
+ self.banner_features = DEFAULT_BANNER_FEATURES # 回退初值,serve() 中 _resolve_banner_features 覆盖
75
+ self.capture_enabled = not args.no_capture
76
+ self.debug_enabled = args.debug
77
+ # 日志目录/文件
78
+ try:
79
+ os.makedirs(LOG_DIR, exist_ok=True)
80
+ except OSError:
81
+ pass
82
+ if self.capture_enabled:
83
+ try:
84
+ os.makedirs(FILES_DIR, exist_ok=True)
85
+ except OSError:
86
+ self.capture_enabled = False
87
+ now = datetime.datetime.now()
88
+ stamp = now.strftime("%Y-%m-%d-%H:%M:%S-") + f"{now.microsecond // 1000:03d}"
89
+ self.log_path = os.path.join(LOG_DIR, stamp + ".log")
90
+ self.file_logger = FileLogger(self.log_path)
91
+ if self.debug_enabled:
92
+ self.debug_path = os.path.join(LOG_DIR, stamp + "-debug.log")
93
+ self.debug_logger = DebugHexLogger(self.debug_path)
94
+ else:
95
+ self.debug_path = None
96
+ self.debug_logger = None
97
+ self._conn_counter = 0
98
+ self._counter_lock = threading.Lock()
99
+ self._connections_lock = threading.Lock()
100
+ self._connections = {}
101
+ self._cleanup_lock = threading.Lock()
102
+ self._cleaned = False
103
+ self.listen_sock = None
104
+ self._stop = False
105
+ self.adb_server_lease = AdbServerLease(
106
+ managed_port=self.adb_server_port,
107
+ log=self._glog,
108
+ )
109
+ self.serial = None # serve() 解析 --target 后填
110
+
111
+ def _glog(self, msg):
112
+ try:
113
+ print(msg)
114
+ except Exception:
115
+ pass
116
+ self.file_logger.write(msg)
117
+
118
+ # ---------- 目标解析 ----------
119
+ def _resolve_target(self):
120
+ t = self.target
121
+ if t.startswith("serial:"):
122
+ return t[len("serial:") :]
123
+ if t.startswith("net:"):
124
+ addr = t[len("net:") :]
125
+ self._glog(f"[*] adb connect {addr} ...")
126
+ try:
127
+ result = subprocess.run(
128
+ ["adb", "-P", str(self.adb_server_port), "connect", addr],
129
+ stdout=subprocess.DEVNULL,
130
+ stderr=subprocess.DEVNULL,
131
+ timeout=10,
132
+ )
133
+ except subprocess.TimeoutExpired:
134
+ self._glog("[!] adb 调用超时: connect")
135
+ return None
136
+ except FileNotFoundError:
137
+ self._glog("[!] adb 未找到: connect")
138
+ return None
139
+ if result.returncode != 0:
140
+ self._glog(
141
+ f"[!] adb connect {addr} 失败,returncode={result.returncode}"
142
+ )
143
+ return None
144
+ return addr
145
+ if t == "usb" or not t:
146
+ return self._first_usb_serial()
147
+ return t # 当作裸 serial
148
+
149
+ def _first_usb_serial(self):
150
+ try:
151
+ r = subprocess.run(
152
+ ["adb", "-P", str(self.adb_server_port), "devices", "-l"],
153
+ capture_output=True,
154
+ text=True,
155
+ timeout=5,
156
+ )
157
+ except subprocess.TimeoutExpired:
158
+ self._glog("[!] adb 调用超时: devices")
159
+ return None
160
+ except FileNotFoundError:
161
+ self._glog("[!] adb 未找到: devices")
162
+ return None
163
+ if r.returncode != 0:
164
+ self._glog(f"[!] adb devices -l 失败,returncode={r.returncode}")
165
+ return None
166
+ for line in r.stdout.splitlines()[1:]:
167
+ line = line.strip()
168
+ if not line:
169
+ continue
170
+ parts = line.split()
171
+ if len(parts) < 3:
172
+ continue
173
+ ser, st, *metadata = parts
174
+ if st != "device":
175
+ continue
176
+ if not any(field.startswith("usb:") for field in metadata):
177
+ continue
178
+ return ser
179
+ return None
180
+
181
+ # ---------- 查询后端真机 banner features ----------
182
+ def _resolve_banner_features(self):
183
+ """启动时经 5037 单帧 host-serial:<serial>:features 查后端真机 CNXN banner
184
+ features,原样 advertised 给前端,仅硬剥 delayed_ack(front v1 停等流控不支持,
185
+ banner 含 delayed_ack 触发 adb.cpp:507 mismatch → send_close)。压缩子 feature
186
+ (sendrecv_v2_brotli/lz4/zstd/dry_run_send)一律保留,靠 raw 透传。失败返回 None,
187
+ 调用方用 DEFAULT_BANNER_FEATURES 回退不崩。
188
+
189
+ 帧序:单帧 host-serial:<serial>:features(对齐真 adb client
190
+ format_host_command("features"),adb_client.cpp:415-422)。非 pin-transport 双握手:
191
+ host:transport 返回 SwitchedTransport 后 smart_socket_data.clear()(sockets.cpp:865),
192
+ 下一帧裸 features 无 host-* 前缀会被误路由到传输流路径向设备发 A_OPEN service=features
193
+ → 设备无此流服务 → CLSE → 静默失败。"""
194
+ try:
195
+ sock = socket.create_connection(
196
+ ("127.0.0.1", self.adb_server_port), timeout=5
197
+ )
198
+ except Exception as e:
199
+ self._glog(f"[!] 查询 banner features 连接 5037 失败: {e}")
200
+ return None
201
+ try:
202
+ sock.settimeout(5)
203
+ body = f"host-serial:{self.serial}:features"
204
+ sock.sendall(AdbProtocol.format_service_frame(body))
205
+ # 4B status(recv 凑齐半包)
206
+ st = self._recv_exact_sock(sock, 4)
207
+ if st is None:
208
+ self._glog("[!] 查询 banner features 读 status EOF")
209
+ return None
210
+ s = st.decode("ascii", errors="ignore")
211
+ if s == "OKAY":
212
+ hb = self._recv_exact_sock(sock, 4)
213
+ if hb is None:
214
+ self._glog("[!] 查询 banner features 读 hex4 EOF")
215
+ return None
216
+ try:
217
+ plen = int(hb.decode("ascii", errors="ignore"), 16)
218
+ except ValueError:
219
+ self._glog(f"[!] 查询 banner features hex4 非法: {hb!r}")
220
+ return None
221
+ payload = self._recv_exact_sock(sock, plen) if plen else b""
222
+ if payload is None:
223
+ self._glog("[!] 查询 banner features 读 payload EOF")
224
+ return None
225
+ feats = payload.decode("utf-8", errors="replace")
226
+ elif s == "FAIL":
227
+ hb = self._recv_exact_sock(sock, 4)
228
+ plen = 0
229
+ if hb:
230
+ try:
231
+ plen = int(hb.decode("ascii", errors="ignore"), 16)
232
+ except ValueError:
233
+ plen = 0
234
+ fbody = self._recv_exact_sock(sock, plen) if plen else b""
235
+ self._glog(
236
+ f"[!] 查询 banner features FAIL: {fbody.decode('utf-8', errors='replace')!r}"
237
+ )
238
+ return None
239
+ else:
240
+ self._glog(f"[!] 查询 banner features status 异常: {s!r}")
241
+ return None
242
+ except Exception as e:
243
+ self._glog(f"[!] 查询 banner features 异常: {e}")
244
+ return None
245
+ finally:
246
+ try:
247
+ sock.close()
248
+ except Exception:
249
+ pass
250
+ # 硬剥 delayed_ack(front v1 流控硬约束);压缩子 feature 一律保留
251
+ feats_list = [f for f in feats.split(",") if f and f != "delayed_ack"]
252
+ return ",".join(feats_list)
253
+
254
+ # ---------- 查询后端真机 banner ro.product.*(效仿 adbd GetProperty 循环)----------
255
+ def _resolve_banner_fields(self):
256
+ """效仿 adbd get_connection_string (adb.cpp:294-316) 的 GetProperty(prop) 循环:
257
+ 逐属性经 5037 pin+shell 两步读后端 raw 系统属性(= __system_property_get 远端等价)。
258
+ shell 须先 host:transport:<serial> pin(adb.cpp:1339-1347 SwitchedTransport)再同 socket
259
+ 发 shell:,adb server 经 connect_to_remote(sockets.cpp:925-934)A_OPEN 到设备
260
+ ShellService(daemon/services.cpp:354 ConsumePrefix("shell")),输出 raw 流至 EOF
261
+ (ReadOrderlyShutdown,adb_client.cpp:234)。成功返回 (product,model,device),
262
+ 失败 None。解析只读旁路,启动期查询 socket 与 per-conn 转发隔离,不影响 sendall 透传。"""
263
+ _BANNER_PROPS = ("ro.product.name", "ro.product.model", "ro.product.device")
264
+ values = []
265
+ for prop in _BANNER_PROPS:
266
+ val = self._query_getprop(prop)
267
+ if val is None:
268
+ return None # 任一属性失败→整体回退(效仿 adbd 不容部分缺失)
269
+ values.append(val)
270
+ return tuple(values)
271
+
272
+ def _query_getprop(self, prop):
273
+ """单次 shell:getprop 查询(pin+shell 两步同 socket,architect round2 修订)。
274
+ 返回 strip 后属性值,失败 None。逐属性独立 socket:shell EOF 后 adb server 关
275
+ socket 不可复用,故 3 socket/3 RTT。"""
276
+ try:
277
+ sock = socket.create_connection(
278
+ ("127.0.0.1", self.adb_server_port), timeout=5
279
+ )
280
+ except Exception as e:
281
+ self._glog(f"[!] 查询 {prop} 连接 5037 失败: {e}")
282
+ return None
283
+ try:
284
+ sock.settimeout(5)
285
+ # ① pin transport: host:transport:<serial>(legacy,无 transport_id 回写)
286
+ # 对齐 problem3 AdbClientBackend.start() 既有 pin 模式(adb.cpp:1339-1347)
287
+ tport = f"host:transport:{self.serial}"
288
+ sock.sendall(AdbProtocol.format_service_frame(tport))
289
+ ts = self._recv_exact_sock(sock, 4)
290
+ if ts is None:
291
+ self._glog(f"[!] 查询 {prop} 读 tport status EOF")
292
+ return None
293
+ ts = ts.decode("ascii", errors="ignore")
294
+ if ts == "FAIL": # tport FAIL:读 hex4+error(device 未找到等)
295
+ self._read_fail_body(sock, f"tport {prop}")
296
+ return None
297
+ if ts != "OKAY":
298
+ self._glog(f"[!] 查询 {prop} tport status 异常: {ts!r}")
299
+ return None
300
+ # ② 同 socket 发 shell:getprop <prop>(无 host 前缀,走 connect_to_remote)
301
+ svc = f"shell:getprop {prop}"
302
+ sock.sendall(AdbProtocol.format_service_frame(svc))
303
+ st = self._recv_exact_sock(sock, 4)
304
+ if st is None:
305
+ self._glog(f"[!] 查询 {prop} 读 svc status EOF")
306
+ return None
307
+ s = st.decode("ascii", errors="ignore")
308
+ if s == "OKAY":
309
+ # shell 流式:recv 循环至 EOF(ReadOrderlyShutdown 语义)
310
+ stream = bytearray()
311
+ while True:
312
+ try:
313
+ chunk = sock.recv(4096)
314
+ except OSError:
315
+ break # 超时/异常→EOF
316
+ if not chunk:
317
+ break # adb server 命令结束关 socket
318
+ stream.extend(chunk)
319
+ return stream.decode("utf-8", errors="replace").strip()
320
+ elif s == "FAIL":
321
+ self._read_fail_body(sock, f"shell {prop}")
322
+ return None
323
+ else:
324
+ self._glog(f"[!] 查询 {prop} svc status 异常: {s!r}")
325
+ return None
326
+ except Exception as e:
327
+ self._glog(f"[!] 查询 {prop} 异常: {e}")
328
+ return None
329
+ finally:
330
+ try:
331
+ sock.close()
332
+ except Exception:
333
+ pass
334
+
335
+ def _read_fail_body(self, sock, label):
336
+ """读 FAIL 后的 hex4+error body 并打日志(复用 features 分支模式,tport/shell 共用)。"""
337
+ hb = self._recv_exact_sock(sock, 4)
338
+ plen = 0
339
+ if hb:
340
+ try:
341
+ plen = int(hb.decode("ascii", errors="ignore"), 16)
342
+ except ValueError:
343
+ plen = 0
344
+ fbody = self._recv_exact_sock(sock, plen) if plen else b""
345
+ self._glog(
346
+ f"[!] 查询 {label} FAIL: {fbody.decode('utf-8', errors='replace')!r}"
347
+ if fbody
348
+ else f"[!] 查询 {label} FAIL"
349
+ )
350
+
351
+ @staticmethod
352
+ def _recv_exact_sock(sock, n):
353
+ """精确读 n 字节(循环拼半包)。EOF/异常返回 None。"""
354
+ buf = bytearray()
355
+ while len(buf) < n:
356
+ try:
357
+ chunk = sock.recv(n - len(buf))
358
+ except OSError:
359
+ return None
360
+ if not chunk:
361
+ return None
362
+ buf.extend(chunk)
363
+ return bytes(buf)
364
+
365
+ # ---------- 自起后端 adb server ----------
366
+ def _ensure_adb_server(self):
367
+ if self.adb_server_port == DEFAULT_ADB_SERVER_PORT:
368
+ self.adb_server_lease.initial_state = "reused"
369
+ return True # 复用系统 5037
370
+ self._glog(f"[*] 自起 adb server @ 端口 {self.adb_server_port} ...")
371
+ return self.adb_server_lease.acquire_owned()
372
+
373
+ # ---------- 信号 ----------
374
+ def _signal_handler(self, signum, frame):
375
+ self._stop = True
376
+ self._glog(f"\n[*] 接收到信号 {signum},正在清理...")
377
+ try:
378
+ if self.listen_sock:
379
+ self.listen_sock.close()
380
+ except Exception:
381
+ pass
382
+
383
+ # ---------- serve ----------
384
+ def serve(self):
385
+ if not self._ensure_adb_server():
386
+ self.cleanup()
387
+ sys.exit(1)
388
+ self.serial = self._resolve_target()
389
+ if not self.serial:
390
+ self._glog(f"[!] 无法解析 --target {self.target!r}:未取到可用设备 serial")
391
+ self.cleanup()
392
+ sys.exit(1)
393
+ self._glog(f"[*] target serial={self.serial}")
394
+ resolved = self._resolve_banner_features()
395
+ if resolved is not None:
396
+ self.banner_features = resolved
397
+ self._glog(
398
+ f"[*] resolved banner features (from back :{self.adb_server_port}): {resolved}"
399
+ )
400
+ else:
401
+ self._glog(
402
+ f"[!] banner features 查询失败,回退默认: {DEFAULT_BANNER_FEATURES}"
403
+ )
404
+ fields = self._resolve_banner_fields()
405
+ if fields is not None:
406
+ self.banner_product, self.banner_model, self.banner_device = fields
407
+ self._glog(
408
+ f"[*] resolved banner fields (效仿 adbd GetProperty, from back :{self.adb_server_port}): "
409
+ f"name={self.banner_product} model={self.banner_model} device={self.banner_device}"
410
+ )
411
+ else:
412
+ self._glog(
413
+ "[!] banner fields 查询失败,banner 回退修复前形态(type=device, ro.product.name=client-mock)"
414
+ )
415
+ # bind listen
416
+ self.listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
417
+ self.listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
418
+ try:
419
+ self.listen_sock.bind(("127.0.0.1", self.listen_port))
420
+ self.listen_sock.listen(5)
421
+ except OSError as e:
422
+ self._glog(f"[!] 监听端口 {self.listen_port} 绑定失败: {e}")
423
+ self.cleanup()
424
+ sys.exit(1)
425
+ self._glog(
426
+ f"[*] client-mock 运行中,监听 {self.listen_port},后端 adb server :{self.adb_server_port},target={self.serial}"
427
+ )
428
+ self._glog(f"[*] 日志文件: {self.log_path}")
429
+ if self.debug_logger:
430
+ self._glog(f"[*] debug hex 日志: {self.debug_path}")
431
+ else:
432
+ self._glog("[*] debug hex 日志: <off>(--debug 可开启)")
433
+ if self.capture_enabled:
434
+ self._glog(
435
+ f"[*] 文件传输捕获已开启,落盘目录: {FILES_DIR}(--no-capture 可禁用)"
436
+ )
437
+ else:
438
+ self._glog(
439
+ "[*] 文件传输捕获已禁用(--no-capture),仅打印 sync/install 摘要"
440
+ )
441
+ self._glog(
442
+ "[*] 用法:adb connect 127.0.0.1:%d,然后 adb -s 127.0.0.1:%d <cmd>"
443
+ % (self.listen_port, self.listen_port)
444
+ )
445
+ signal.signal(signal.SIGINT, self._signal_handler)
446
+ signal.signal(signal.SIGTERM, self._signal_handler)
447
+ self.listen_sock.settimeout(0.5)
448
+ try:
449
+ while not self._stop:
450
+ try:
451
+ client, _ = self.listen_sock.accept()
452
+ except socket.timeout:
453
+ continue
454
+ except OSError:
455
+ break
456
+ conn_id = _next_conn_id() # 与 per-stream sb 共用计数器,保证全局唯一
457
+ conn = TransportConnection(
458
+ conn_id,
459
+ client,
460
+ self.file_logger,
461
+ self.capture_enabled,
462
+ self.debug_logger,
463
+ self.serial,
464
+ self.adb_server_port,
465
+ self.banner_features,
466
+ self.banner_type,
467
+ self.banner_product,
468
+ self.banner_model,
469
+ self.banner_device,
470
+ )
471
+ thread = threading.Thread(
472
+ target=self._connection_worker, args=(conn,), daemon=True
473
+ )
474
+ with self._connections_lock:
475
+ self._connections[conn] = thread
476
+ thread.start()
477
+ except KeyboardInterrupt:
478
+ self._glog("\n[*] 接收到中断信号,正在清理环境并退出...")
479
+ finally:
480
+ self.cleanup()
481
+
482
+ def _connection_worker(self, conn):
483
+ try:
484
+ conn.run()
485
+ conn.wait()
486
+ finally:
487
+ with self._connections_lock:
488
+ self._connections.pop(conn, None)
489
+
490
+ def _drain_connections(self):
491
+ with self._connections_lock:
492
+ connections = list(self._connections.items())
493
+ for conn, _thread in connections:
494
+ conn.stop()
495
+ current = threading.current_thread()
496
+ for conn, thread in connections:
497
+ conn.wait(timeout=5)
498
+ if thread is not current:
499
+ thread.join(timeout=5)
500
+
501
+ def cleanup(self):
502
+ cleanup_lock = getattr(self, "_cleanup_lock", None)
503
+ if cleanup_lock is None:
504
+ cleanup_lock = threading.Lock()
505
+ self._cleanup_lock = cleanup_lock
506
+ with cleanup_lock:
507
+ if getattr(self, "_cleaned", False):
508
+ return
509
+ self._cleaned = True
510
+ # 1. 关 listen socket 释放端口,打断 accept
511
+ try:
512
+ if self.listen_sock:
513
+ self.listen_sock.close()
514
+ except Exception:
515
+ pass
516
+ # 2. 先停活动连接并等待 transport/backend/observer 收尾。
517
+ self._drain_connections()
518
+ # 3. 只清理 lease 明确拥有的 adb server;端口存在不构成 ownership。
519
+ lease = getattr(self, "adb_server_lease", None)
520
+ lease_complete = True
521
+ if lease is not None:
522
+ if lease.cleanup() is False:
523
+ lease_complete = False
524
+ self._glog("[!] adb server 清理未完成;已保留 lease 状态供后续重试")
525
+ # net 目标不主动 disconnect(让 adb server 自然回收)
526
+ self.file_logger.close()
527
+ if self.debug_logger:
528
+ self.debug_logger.close()
529
+ if not lease_complete:
530
+ with cleanup_lock:
531
+ self._cleaned = False
532
+ return lease_complete