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
@@ -0,0 +1,14 @@
1
+ import socket
2
+
3
+
4
+ def close_sockets(s1, s2):
5
+ """安全关闭 Socket,避免 Errno 9"""
6
+ for s in (s1, s2):
7
+ try:
8
+ s.shutdown(socket.SHUT_RDWR)
9
+ except Exception:
10
+ pass
11
+ try:
12
+ s.close()
13
+ except Exception:
14
+ pass
@@ -0,0 +1 @@
1
+ """ADB client 到 server 的透明代理。"""
adbproxy/server/app.py ADDED
@@ -0,0 +1,221 @@
1
+ """ADB smart-socket 代理应用生命周期。"""
2
+
3
+ import argparse
4
+ import datetime
5
+ import os
6
+ import signal
7
+ import socket
8
+ import sys
9
+ import threading
10
+
11
+ from adbproxy.config import FILES_DIR, LOG_DIR, PROXY_PORT, REAL_ADB_PORT
12
+ from adbproxy.runtime.adb_process import AdbServerLease
13
+ from adbproxy.runtime.logging import DebugHexLogger, FileLogger
14
+ from adbproxy.server.connection import Connection
15
+
16
+
17
+ class AdbProxy:
18
+ def __init__(self, argv):
19
+ parser = argparse.ArgumentParser(
20
+ description="ADB MITM 代理:转发并解析 adb 协议数据包"
21
+ )
22
+ parser.add_argument(
23
+ "--no-capture",
24
+ action="store_true",
25
+ help="禁用 sync 文件传输落盘到 files/,仅打印摘要",
26
+ )
27
+ parser.add_argument(
28
+ "--debug",
29
+ action="store_true",
30
+ help="每 recv 块原始 hex 落盘到 logs/debug-{date}.log,旁路不改转发",
31
+ )
32
+ args = parser.parse_args(argv)
33
+ self.capture_enabled = not args.no_capture
34
+ try:
35
+ os.makedirs(LOG_DIR, exist_ok=True)
36
+ except OSError:
37
+ pass
38
+ if self.capture_enabled:
39
+ try:
40
+ os.makedirs(FILES_DIR, exist_ok=True)
41
+ except OSError:
42
+ self.capture_enabled = False
43
+ now = datetime.datetime.now()
44
+ fname = (
45
+ now.strftime("%Y-%m-%d-%H:%M:%S-") + f"{now.microsecond // 1000:03d}.log"
46
+ )
47
+ self.log_path = os.path.join(LOG_DIR, fname)
48
+ self.file_logger = FileLogger(self.log_path)
49
+ # P0: --debug 开启时每 recv 一块写一行 hex 到独立 debug 文件,旁路不改转发字节
50
+ self.debug_enabled = args.debug
51
+ if self.debug_enabled:
52
+ debug_fname = (
53
+ now.strftime("%Y-%m-%d-%H:%M:%S-")
54
+ + f"{now.microsecond // 1000:03d}-debug.log"
55
+ )
56
+ self.debug_path = os.path.join(LOG_DIR, debug_fname)
57
+ self.debug_logger = DebugHexLogger(self.debug_path)
58
+ else:
59
+ self.debug_path = None
60
+ self.debug_logger = None
61
+ self._conn_counter = 0
62
+ self._counter_lock = threading.Lock()
63
+ self._connections_lock = threading.Lock()
64
+ self._connections = {}
65
+ self._cleanup_lock = threading.Lock()
66
+ self._cleaned = False
67
+ self.proxy_sock = None
68
+ self._stop = False # 信号处理器置位,serve() 超时轮询据此退出 accept 循环
69
+ self.adb_server_lease = AdbServerLease(
70
+ managed_port=REAL_ADB_PORT,
71
+ restore_port=PROXY_PORT,
72
+ log=self._glog,
73
+ )
74
+
75
+ def _glog(self, msg):
76
+ try:
77
+ print(msg)
78
+ except Exception:
79
+ pass
80
+ self.file_logger.write(msg)
81
+
82
+ def _signal_handler(self, signum, frame):
83
+ # 置标志位 + 日志;close proxy_sock 唤醒 accept 抛 OSError 立即跳出(#5)
84
+ self._stop = True
85
+ self._glog(f"\n[*] 接收到信号 {signum},正在清理...")
86
+ try:
87
+ if self.proxy_sock:
88
+ self.proxy_sock.close()
89
+ except Exception:
90
+ pass
91
+
92
+ def _kill_default_server(self):
93
+ """先检查 5038 未被占用,再按 lease 证据停止需恢复的 5037。"""
94
+ return self.adb_server_lease.prepare_takeover()
95
+
96
+ def init_adb_server(self):
97
+ """启动并验证 5038;只有 start 成功且 post-probe 就绪才获得所有权。"""
98
+ return self.adb_server_lease.start_owned()
99
+
100
+ def serve(self):
101
+ # #4+#7+#顺序修正: kill 默认 5037 → bind 5037 → 起 5038。
102
+ # 必须先 kill:此时 5037 是真 adb server,adb kill-server 连真 server 不会连到 proxy 自身 backlog
103
+ # 被队列吞并转发 host:kill 去杀 5038。bind 前移根治竞态,bind 失败先 cleanup 再退出不留孤儿 5038。
104
+ if not self._kill_default_server():
105
+ self._glog("[!] adb server lease 预检失败,未修改现有实例")
106
+ self.cleanup()
107
+ sys.exit(1)
108
+ self.proxy_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109
+ self.proxy_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
110
+ try:
111
+ self.proxy_sock.bind(("127.0.0.1", PROXY_PORT))
112
+ self.proxy_sock.listen(5)
113
+ except OSError as e:
114
+ self._glog(f"[!] 端口绑定失败: {e}")
115
+ self.cleanup()
116
+ sys.exit(1)
117
+ # bind 成功后起 5038 真实 server
118
+ if not self.init_adb_server():
119
+ self._glog("[!] 5038 未就绪,清理退出")
120
+ self.cleanup()
121
+ sys.exit(1)
122
+ self._glog(
123
+ f"[*] ADB Proxy 运行中。正在监听 {PROXY_PORT},转发至 {REAL_ADB_PORT}..."
124
+ )
125
+ self._glog(f"[*] 日志文件: {self.log_path}")
126
+ if self.debug_logger:
127
+ self._glog(f"[*] debug hex 日志: {self.debug_path}")
128
+ else:
129
+ self._glog("[*] debug hex 日志: <off>(--debug 可开启)")
130
+ # Step 6: 落盘 banner
131
+ if self.capture_enabled:
132
+ self._glog(
133
+ f"[*] 文件传输捕获已开启,落盘目录: {FILES_DIR} (--no-capture 可禁用)"
134
+ )
135
+ else:
136
+ self._glog("[*] 文件传输捕获已禁用(--no-capture),仅打印 sync 摘要不落盘")
137
+ # 装信号处理器 + accept 超时轮询,让 SIGINT/SIGTERM 可靠触发 cleanup。
138
+ # 原因:主线程阻塞在 C 级 accept(),Python SIGINT 只在字节码间检查、不打断阻塞;
139
+ # 超时轮询让标志位 _stop 在每次 accept 超时后被检查,从而可靠退出。
140
+ signal.signal(signal.SIGINT, self._signal_handler)
141
+ signal.signal(signal.SIGTERM, self._signal_handler)
142
+ self.proxy_sock.settimeout(0.5)
143
+ try:
144
+ while not self._stop:
145
+ try:
146
+ client, _ = self.proxy_sock.accept()
147
+ except socket.timeout:
148
+ continue
149
+ except OSError:
150
+ break
151
+ with self._counter_lock:
152
+ self._conn_counter += 1
153
+ conn_id = self._conn_counter
154
+ conn = Connection(
155
+ conn_id,
156
+ client,
157
+ self.file_logger,
158
+ self.capture_enabled,
159
+ self.debug_logger,
160
+ )
161
+ thread = threading.Thread(
162
+ target=self._connection_worker, args=(conn,), daemon=True
163
+ )
164
+ with self._connections_lock:
165
+ self._connections[conn] = thread
166
+ thread.start()
167
+ except KeyboardInterrupt:
168
+ self._glog("\n[*] 接收到中断信号,正在清理环境并退出...")
169
+ finally:
170
+ self.cleanup()
171
+
172
+ def _connection_worker(self, conn):
173
+ try:
174
+ conn.run()
175
+ conn.wait()
176
+ finally:
177
+ with self._connections_lock:
178
+ self._connections.pop(conn, None)
179
+
180
+ def _drain_connections(self):
181
+ with self._connections_lock:
182
+ connections = list(self._connections.items())
183
+ for conn, _thread in connections:
184
+ conn.stop()
185
+ current = threading.current_thread()
186
+ for conn, thread in connections:
187
+ conn.wait(timeout=5)
188
+ if thread is not current:
189
+ thread.join(timeout=5)
190
+
191
+ def cleanup(self):
192
+ cleanup_lock = getattr(self, "_cleanup_lock", None)
193
+ if cleanup_lock is None:
194
+ cleanup_lock = threading.Lock()
195
+ self._cleanup_lock = cleanup_lock
196
+ with cleanup_lock:
197
+ if getattr(self, "_cleaned", False):
198
+ return
199
+ self._cleaned = True
200
+ # 1. 关闭 Proxy Socket,释放 5037,打断 accept
201
+ try:
202
+ if self.proxy_sock:
203
+ self.proxy_sock.close()
204
+ except Exception:
205
+ pass
206
+ # 2. 先停活动连接并等待 relay/observer 收尾,再处理共享进程和日志。
207
+ self._drain_connections()
208
+ # 3. 仅撤销 lease action ledger 中由本进程完成的 stop/start。
209
+ lease = getattr(self, "adb_server_lease", None)
210
+ lease_complete = True
211
+ if lease is not None:
212
+ if lease.cleanup() is False:
213
+ lease_complete = False
214
+ self._glog("[!] adb server 清理未完成;已保留 lease 状态供后续重试")
215
+ self.file_logger.close()
216
+ if self.debug_logger:
217
+ self.debug_logger.close()
218
+ if not lease_complete:
219
+ with cleanup_lock:
220
+ self._cleaned = False
221
+ return lease_complete
@@ -0,0 +1,378 @@
1
+ """ADB 客户端到服务端的代理连接。"""
2
+
3
+ import socket
4
+ import threading
5
+
6
+ from adbproxy.config import REAL_ADB_PORT
7
+ from adbproxy.observers.stream import StreamBuffer
8
+ from adbproxy.protocols.adb import AdbProtocol
9
+ from adbproxy.protocols.smart_socket.conversation import SmartSocketConversation
10
+ from adbproxy.runtime.sockets import close_sockets
11
+
12
+
13
+ class Connection:
14
+ def __init__(
15
+ self, conn_id, client_sock, file_logger, capture_enabled=True, debug_logger=None
16
+ ):
17
+ self.conn_id = conn_id
18
+ self.client = client_sock
19
+ self.file_logger = file_logger
20
+ self.debug_logger = (
21
+ debug_logger # P0: None 或 DebugHexLogger 实例,sendall 后旁路落盘 hex
22
+ )
23
+ self.sb = StreamBuffer(conn_id, file_logger, capture_enabled)
24
+ self.server = None
25
+ self.svc = None # client 首帧 service 串,供 server 侧 host-length-gating 查
26
+ self.shell_okay_consumed = (
27
+ False # shell 服务的 OKAY/FAIL 是否已剥除(仅 server 线程读写)
28
+ )
29
+ self.sync_okay_consumed = (
30
+ False # sync 服务的 OKAY/FAIL 是否已剥除(仅 server 线程读写)
31
+ )
32
+ # pure smart-socket conversation (request/response policy); no I/O
33
+ self.conversation = SmartSocketConversation()
34
+ # 兼容测试与内部状态:暴露 conversation 缓冲/相位字段
35
+ self._service_request_buf = self.conversation._service_request_buf
36
+ self._shell_first_response = self.conversation._shell_first_response
37
+ self._sync_first_response = self.conversation._sync_first_response
38
+ self.host_phase = self.conversation.host_phase
39
+ self.host_status_buf = self.conversation.host_status_buf
40
+ self.host_hex_buf = self.conversation.host_hex_buf
41
+ self.host_payload_remaining = self.conversation.host_payload_remaining
42
+ self.host_fixed_remaining = self.conversation.host_fixed_remaining
43
+ self.host_track_active = self.conversation.host_track_active
44
+ self._last_host_svc = self.conversation._last_host_svc
45
+ self._finished = threading.Event()
46
+ self._finish_lock = threading.Lock()
47
+ self._relay_threads = []
48
+ # 不变量:self.svc 在 client sendall 之前写入,server recv 在收到该 sendall 之后
49
+ # —— TCP 顺序天然 happens-before,无需额外锁
50
+
51
+ def _sync_host_view(self):
52
+ """Mirror conversation host SM fields onto Connection for tests/debug."""
53
+ c = self.conversation
54
+ self.host_phase = c.host_phase
55
+ self.host_status_buf = c.host_status_buf
56
+ self.host_hex_buf = c.host_hex_buf
57
+ self.host_payload_remaining = c.host_payload_remaining
58
+ self.host_fixed_remaining = c.host_fixed_remaining
59
+ self.host_track_active = c.host_track_active
60
+ self._last_host_svc = c._last_host_svc
61
+ self._service_request_buf = c._service_request_buf
62
+ self._shell_first_response = c._shell_first_response
63
+ self._sync_first_response = c._sync_first_response
64
+
65
+ def run(self):
66
+ try:
67
+ self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
68
+ self.server.connect(("127.0.0.1", REAL_ADB_PORT))
69
+ except Exception as e:
70
+ self.sb.logger.event(f"[!] 连接真实 ADB Server 失败: {e}")
71
+ try:
72
+ self.client.close()
73
+ except Exception:
74
+ pass
75
+ self._finish() # #9: 早退路径过收尾,保证 sb.close 幂等、timer 不残留
76
+ return
77
+ t_c = threading.Thread(target=self._client_to_server, daemon=True)
78
+ t_s = threading.Thread(target=self._server_to_client, daemon=True)
79
+ self._relay_threads = [t_c, t_s]
80
+ t_c.start()
81
+ t_s.start()
82
+ # 不 join:任一方 EOF 即在 _finish 里 close_sockets 双向,立即解阻塞对端 recv。
83
+ # 否则 server EOF 后 run 卡在 t_c.join 等 client 线程,而 client 卡在 recv 等
84
+ # 代理关闭发 EOF —— 死锁。
85
+
86
+ def _finish(self):
87
+ """任一方结束时关闭双向 socket 解阻塞对端;once-guard 保证 sb.close 只调一次。"""
88
+ with self._finish_lock:
89
+ if self._finished.is_set():
90
+ return
91
+ close_sockets(self.client, self.server)
92
+ try:
93
+ self.sb.close()
94
+ except Exception:
95
+ pass
96
+ self._finished.set()
97
+
98
+ def stop(self):
99
+ self._finish()
100
+
101
+ def wait(self, timeout=None):
102
+ done = self._finished.wait(timeout)
103
+ if not done:
104
+ return False
105
+ current = threading.current_thread()
106
+ for thread in list(self._relay_threads):
107
+ if thread is not current:
108
+ thread.join(timeout)
109
+ return True
110
+
111
+ def _client_to_server(self):
112
+ # 用"shell/sync/install 是否已激活"驱动解析,而非首帧布尔。
113
+ # adb -s <serial> shell ls /data 在同一 TCP 连接发两帧:
114
+ # 帧1 host:tport:serial:<serial>(host 服务 → shell_mode=None)
115
+ # 帧2 0029shell,v2,...:ls /data\x04(真正的 shell 服务)
116
+ # 故只要 shell/sync/install 未激活,每个 recv 都尝试解析 service 帧。
117
+ # A2: 仅未激活时 feed_request(守卫);解析单源 conversation;activate 副作用留 connection。
118
+ try:
119
+ while True:
120
+ data = self.client.recv(65536) # #11: 64KB recv
121
+ if not data:
122
+ break
123
+ # Phase A — pre-sendall classify / set_mode / activate (I2, S1–S4)
124
+ leftover = b""
125
+ parsed = None
126
+ try:
127
+ if (
128
+ not self.sb.shell.active
129
+ and not self.sb.sync.active
130
+ and not self.sb.install.active
131
+ ):
132
+ for ev in self.conversation.feed_request(data):
133
+ if ev.kind != "service_request" or not ev.svc:
134
+ continue
135
+ svc = ev.svc
136
+ leftover = ev.leftover or b""
137
+ parsed = (svc, leftover)
138
+ self.svc = svc
139
+ self.conversation.svc = svc
140
+ self.sb.logger.event("========")
141
+ self.sb.logger.event(f"📥 [User Command]: {svc}")
142
+ self.sb.shell.set_mode_from_service(svc)
143
+ if AdbProtocol.is_sync_service(svc):
144
+ self.sb.sync.activate()
145
+ if AdbProtocol.is_install_stream_service(svc):
146
+ size = AdbProtocol.parse_install_size(svc)
147
+ if size is not None:
148
+ self.sb.install.activate(size, svc)
149
+ cap = (
150
+ self.sb.install.capture_path
151
+ if (
152
+ self.sb.install.capture_enabled
153
+ and self.sb.install.capture_path
154
+ )
155
+ else "<disabled>"
156
+ )
157
+ self.sb.logger.event(
158
+ f"📤 [install push]: remote={svc} size={size} local_capture={cap}"
159
+ )
160
+ self._sync_host_view()
161
+ except Exception:
162
+ parsed = None
163
+ leftover = b""
164
+ # Phase B — 原样透传 (I1);观察失败不得跳过
165
+ self.server.sendall(data)
166
+ if self.debug_logger is not None:
167
+ try:
168
+ ln = self.debug_logger.write_hex(self.conn_id, "c", data)
169
+ self.sb.logger.event(
170
+ f"📥 [debug]: written to {self.debug_logger.file.name} L{ln} ({len(data)}B, client→server)"
171
+ )
172
+ except Exception:
173
+ pass
174
+ # Phase C — post-sendall payload observe (I3 fail-open)
175
+ try:
176
+ if self.sb.shell.active:
177
+ if parsed is not None:
178
+ if leftover:
179
+ self.sb.add_client(leftover)
180
+ else:
181
+ self.sb.add_client(data)
182
+ elif self.sb.sync.active:
183
+ if parsed is not None:
184
+ if leftover:
185
+ self.sb.add_client(leftover)
186
+ else:
187
+ self.sb.add_client(data)
188
+ else:
189
+ if leftover:
190
+ self.sb.add_client(leftover)
191
+ elif parsed is None:
192
+ self.sb.add_client(data)
193
+ except Exception:
194
+ pass
195
+ except OSError:
196
+ pass
197
+ finally:
198
+ self._finish()
199
+
200
+ def _try_parse_service_frame(self, data):
201
+ """兼容包装:委托 conversation.feed_request / try_parse 单源。"""
202
+ result = self.conversation.try_parse_service_frame(data)
203
+ self._sync_host_view()
204
+ return result
205
+
206
+ def _apply_response_events(self, events, complete):
207
+ """Log status/fail, drain host _status_events, update okay flags, feed payloads."""
208
+ for ev in events:
209
+ if ev.kind == "status" and ev.status is not None:
210
+ self.sb.logger.event(f"📤 [Status]: {ev.status}")
211
+ elif ev.kind == "fail_reason" and ev.reason is not None:
212
+ self.sb.logger.event(
213
+ f"📤 [FAIL]: {ev.reason.decode('utf-8', errors='replace')}"
214
+ )
215
+ # S5: drain side-channel even if already mirrored into events
216
+ for st in self.conversation._status_events:
217
+ # avoid double-log when feed_response already emitted status events
218
+ if not any(
219
+ ev.kind == "status" and ev.status == st for ev in events
220
+ ):
221
+ self.sb.logger.event(f"📤 [Status]: {st}")
222
+ self.conversation._status_events = []
223
+
224
+ if self.sb.shell.active and not self.shell_okay_consumed and complete:
225
+ self.shell_okay_consumed = True
226
+ if self.sb.sync.active and not self.sync_okay_consumed and complete:
227
+ self.sync_okay_consumed = True
228
+
229
+ for ev in events:
230
+ if ev.kind == "payload" and ev.data:
231
+ self.sb.add_server(ev.data)
232
+ self._sync_host_view()
233
+
234
+ def _server_to_client(self):
235
+ # sendall 先于 feed_response(I1/I3);okay_consumed 仅 server 线程写。
236
+ try:
237
+ while True:
238
+ data = self.server.recv(65536)
239
+ if not data:
240
+ break
241
+ self.client.sendall(data)
242
+ if self.debug_logger is not None:
243
+ try:
244
+ ln = self.debug_logger.write_hex(self.conn_id, "s", data)
245
+ self.sb.logger.event(
246
+ f"📤 [debug]: written to {self.debug_logger.file.name} L{ln} ({len(data)}B, server→client)"
247
+ )
248
+ except Exception:
249
+ pass
250
+ try:
251
+ events, complete = self.conversation.feed_response(
252
+ data,
253
+ shell_active=self.sb.shell.active,
254
+ shell_okay_consumed=self.shell_okay_consumed,
255
+ sync_active=self.sb.sync.active,
256
+ sync_okay_consumed=self.sync_okay_consumed,
257
+ svc=self.svc,
258
+ )
259
+ self._apply_response_events(events, complete)
260
+ except Exception:
261
+ pass
262
+ except OSError:
263
+ pass
264
+ finally:
265
+ self._finish()
266
+
267
+ def _parse_shell_status(self, data):
268
+ """增量消费 shell 的首个 OKAY/FAIL,返回 (完成, payload)。"""
269
+ events, complete = self.conversation.feed_response(
270
+ data,
271
+ shell_active=True,
272
+ shell_okay_consumed=False,
273
+ sync_active=False,
274
+ sync_okay_consumed=True,
275
+ svc=self.svc,
276
+ )
277
+ leftover = b""
278
+ for ev in events:
279
+ if ev.kind == "status" and ev.status is not None:
280
+ self.sb.logger.event(f"📤 [Status]: {ev.status}")
281
+ elif ev.kind == "fail_reason" and ev.reason is not None:
282
+ self.sb.logger.event(
283
+ f"📤 [FAIL]: {ev.reason.decode('utf-8', errors='replace')}"
284
+ )
285
+ elif ev.kind == "payload" and ev.data:
286
+ leftover = ev.data
287
+ self._sync_host_view()
288
+ return complete, leftover
289
+
290
+ def _parse_sync_okay(self, data):
291
+ """增量消费 sync 的首个 OKAY/FAIL,返回 (完成, payload)。"""
292
+ events, complete = self.conversation.feed_response(
293
+ data,
294
+ shell_active=False,
295
+ shell_okay_consumed=True,
296
+ sync_active=True,
297
+ sync_okay_consumed=False,
298
+ svc=self.svc,
299
+ )
300
+ leftover = b""
301
+ for ev in events:
302
+ if ev.kind == "status" and ev.status is not None:
303
+ self.sb.logger.event(f"📤 [Status]: {ev.status}")
304
+ elif ev.kind == "fail_reason" and ev.reason is not None:
305
+ self.sb.logger.event(
306
+ f"📤 [FAIL]: {ev.reason.decode('utf-8', errors='replace')}"
307
+ )
308
+ elif ev.kind == "payload" and ev.data:
309
+ leftover = ev.data
310
+ self._sync_host_view()
311
+ return complete, leftover
312
+
313
+ @staticmethod
314
+ def _new_first_response_state():
315
+ """创建仅供 server 线程使用的首响应重组状态。"""
316
+ return SmartSocketConversation._new_first_response_state()
317
+
318
+ def _consume_first_response(self, data, state):
319
+ """兼容包装:委托 conversation.consume_first_response。"""
320
+ events = []
321
+ complete, leftover = self.conversation.consume_first_response(
322
+ data, state, emit=events
323
+ )
324
+ for ev in events:
325
+ if ev.kind == "status" and ev.status is not None:
326
+ self.sb.logger.event(f"📤 [Status]: {ev.status}")
327
+ elif ev.kind == "fail_reason" and ev.reason is not None:
328
+ self.sb.logger.event(
329
+ f"📤 [FAIL]: {ev.reason.decode('utf-8', errors='replace')}"
330
+ )
331
+ self._sync_host_view()
332
+ return complete, leftover
333
+
334
+ def _parse_server_first(self, data):
335
+ """非 host 服务的状态剥除(stateless 兜底);经 feed_response 单源。"""
336
+ events, _complete = self.conversation.feed_response(
337
+ data,
338
+ shell_active=False,
339
+ shell_okay_consumed=True,
340
+ sync_active=False,
341
+ sync_okay_consumed=True,
342
+ svc=self.svc,
343
+ )
344
+ leftover = b""
345
+ for ev in events:
346
+ if ev.kind == "status" and ev.status is not None:
347
+ self.sb.logger.event(f"📤 [Status]: {ev.status}")
348
+ elif ev.kind == "payload" and ev.data:
349
+ leftover = ev.data
350
+ self._sync_host_view()
351
+ return leftover
352
+
353
+ def _parse_host_response(self, data):
354
+ """按 service wire shape 增量消费 host 响应,返回纯 payload bytes。"""
355
+ events, _complete = self.conversation.feed_response(
356
+ data,
357
+ shell_active=False,
358
+ shell_okay_consumed=True,
359
+ sync_active=False,
360
+ sync_okay_consumed=True,
361
+ svc=self.svc,
362
+ )
363
+ leftover = b""
364
+ seen_status = set()
365
+ for ev in events:
366
+ if ev.kind == "status" and ev.status is not None:
367
+ self.sb.logger.event(f"📤 [Status]: {ev.status}")
368
+ seen_status.add(ev.status)
369
+ elif ev.kind == "payload" and ev.data:
370
+ leftover = (
371
+ leftover + ev.data if leftover else ev.data
372
+ )
373
+ for st in self.conversation._status_events:
374
+ if st not in seen_status:
375
+ self.sb.logger.event(f"📤 [Status]: {st}")
376
+ self.conversation._status_events = []
377
+ self._sync_host_view()
378
+ return leftover