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.
- adbproxy/__init__.py +2 -0
- adbproxy/__main__.py +5 -0
- adbproxy/cli/__init__.py +1 -0
- adbproxy/cli/device.py +13 -0
- adbproxy/cli/main.py +60 -0
- adbproxy/cli/server.py +13 -0
- adbproxy/config.py +39 -0
- adbproxy/device/__init__.py +1 -0
- adbproxy/device/app.py +532 -0
- adbproxy/device/backend.py +242 -0
- adbproxy/device/banner.py +33 -0
- adbproxy/device/connection.py +505 -0
- adbproxy/device/session.py +304 -0
- adbproxy/device/stream_table.py +125 -0
- adbproxy/observers/__init__.py +1 -0
- adbproxy/observers/events.py +19 -0
- adbproxy/observers/install.py +126 -0
- adbproxy/observers/shell.py +371 -0
- adbproxy/observers/stream.py +94 -0
- adbproxy/observers/sync.py +550 -0
- adbproxy/protocols/__init__.py +1 -0
- adbproxy/protocols/adb.py +66 -0
- adbproxy/protocols/payload/__init__.py +17 -0
- adbproxy/protocols/payload/install.py +44 -0
- adbproxy/protocols/payload/shell_v2.py +28 -0
- adbproxy/protocols/payload/sync.py +180 -0
- adbproxy/protocols/smart_socket/__init__.py +33 -0
- adbproxy/protocols/smart_socket/classifier.py +78 -0
- adbproxy/protocols/smart_socket/conversation.py +335 -0
- adbproxy/protocols/smart_socket/handshake.py +75 -0
- adbproxy/protocols/smart_socket/wire.py +82 -0
- adbproxy/protocols/transport/__init__.py +52 -0
- adbproxy/protocols/transport/codec.py +90 -0
- adbproxy/protocols/transport/constants.py +21 -0
- adbproxy/protocols/transport/errors.py +5 -0
- adbproxy/runtime/__init__.py +1 -0
- adbproxy/runtime/adb_process.py +195 -0
- adbproxy/runtime/capture.py +41 -0
- adbproxy/runtime/logging.py +113 -0
- adbproxy/runtime/sockets.py +14 -0
- adbproxy/server/__init__.py +1 -0
- adbproxy/server/app.py +221 -0
- adbproxy/server/connection.py +378 -0
- adbproxy-0.1.0.dist-info/METADATA +254 -0
- adbproxy-0.1.0.dist-info/RECORD +49 -0
- adbproxy-0.1.0.dist-info/WHEEL +5 -0
- adbproxy-0.1.0.dist-info/entry_points.txt +4 -0
- adbproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
- adbproxy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from adbproxy.config import IDLE_TIMEOUT, MAX_CMD, MAX_V2_BUF
|
|
5
|
+
from adbproxy.protocols.payload.shell_v2 import split_v2_frames
|
|
6
|
+
from adbproxy.protocols.smart_socket.classifier import ServiceClassifier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ShellSession:
|
|
10
|
+
"""处理 shell 服务的命令/输出配对与 PTY 回显/提示符剥离。
|
|
11
|
+
非 shell 流(host: / sync: 等)由 StreamBuffer 走原碎片缓冲路径或委托 SyncSession。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# shell_mode 取值:None / oneshot_raw / oneshot_v2 / interactive_pty / interactive_v2
|
|
15
|
+
# AOSP shell_service 默认 PS1:user@host:cwd# / ...:/ $,覆盖 root@device:/ # 等
|
|
16
|
+
prompt_re = re.compile(rb"\r?\n?[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:[^\r\n]*[#$] ")
|
|
17
|
+
|
|
18
|
+
def __init__(self, logger):
|
|
19
|
+
self.logger = logger
|
|
20
|
+
self.lock = threading.Lock()
|
|
21
|
+
self.shell_mode = None
|
|
22
|
+
self.is_v2 = False
|
|
23
|
+
self.has_echo = False
|
|
24
|
+
# 拆为 client/server 独立缓冲,避免单缓冲被双方共用导致串扰触发 v2_corrupt_s 粘性降级
|
|
25
|
+
self.c_v2_buf = bytearray() # client 侧 v2 半包重组缓冲
|
|
26
|
+
self.s_v2_buf = bytearray() # server 侧 v2 半包重组缓冲
|
|
27
|
+
self.v2_corrupt_c = False # client 侧 v2 解析失败标志
|
|
28
|
+
self.v2_corrupt_s = False # server 侧 v2 解析失败标志
|
|
29
|
+
self.pending_cmd = None
|
|
30
|
+
self.cmd_buf = bytearray()
|
|
31
|
+
self.out_buf = bytearray()
|
|
32
|
+
# P2: client stdin 展示缓冲(只读旁路,与 cmd_buf 解耦,不影响 _maybe_commit_cmd 配对)
|
|
33
|
+
self.in_disp_buf = bytearray()
|
|
34
|
+
self.idle_timer = None
|
|
35
|
+
self._idle_gen = (
|
|
36
|
+
0 # idle Timer 世代号:_reset_idle 自增,回调校验防竿态多跑一次 flush
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def active(self):
|
|
41
|
+
return self.shell_mode is not None
|
|
42
|
+
|
|
43
|
+
def set_mode_from_service(self, svc):
|
|
44
|
+
"""首帧调用:按 service 串定 shell 模式与 has_echo。回显判据 = type==kPty。"""
|
|
45
|
+
kind, args, cmd = ServiceClassifier.parse_service(svc)
|
|
46
|
+
if kind != "shell":
|
|
47
|
+
self.shell_mode = None
|
|
48
|
+
return
|
|
49
|
+
self.is_v2 = "v2" in args
|
|
50
|
+
has_raw = "raw" in args
|
|
51
|
+
has_pty = "pty" in args
|
|
52
|
+
# 对应 daemon/services.cpp:103-113:type = cmd.empty()?kPty:kRaw;若 pty arg 则覆盖 kPty
|
|
53
|
+
self.has_echo = has_pty or (cmd == "" and not has_raw)
|
|
54
|
+
if cmd:
|
|
55
|
+
self.shell_mode = "oneshot_v2" if self.is_v2 else "oneshot_raw"
|
|
56
|
+
self.pending_cmd = self._normalize_cmd(cmd.encode())
|
|
57
|
+
else:
|
|
58
|
+
self.shell_mode = "interactive_v2" if self.is_v2 else "interactive_pty"
|
|
59
|
+
|
|
60
|
+
def on_client(self, data):
|
|
61
|
+
with self.lock:
|
|
62
|
+
self._reset_idle()
|
|
63
|
+
if self.shell_mode == "interactive_v2":
|
|
64
|
+
self._on_client_v2(data)
|
|
65
|
+
elif self.shell_mode == "interactive_pty":
|
|
66
|
+
self._on_client_nonv2(data)
|
|
67
|
+
# oneshot:命令已在 service 串,此处通常无 stdin
|
|
68
|
+
|
|
69
|
+
def _on_client_v2(self, data):
|
|
70
|
+
self.c_v2_buf.extend(data)
|
|
71
|
+
if self.v2_corrupt_c:
|
|
72
|
+
return
|
|
73
|
+
frames, leftover, ok = split_v2_frames(self.c_v2_buf)
|
|
74
|
+
if not ok:
|
|
75
|
+
self.v2_corrupt_c = True
|
|
76
|
+
return
|
|
77
|
+
self.c_v2_buf = leftover
|
|
78
|
+
for id_, payload in frames:
|
|
79
|
+
if id_ == 0: # stdin
|
|
80
|
+
self.cmd_buf.extend(payload)
|
|
81
|
+
# P2: client stdin 展示缓冲累积(只读旁路,与 cmd_buf 配对解耦,idle flush 打 [pty-input])
|
|
82
|
+
self.in_disp_buf.extend(payload)
|
|
83
|
+
self._maybe_commit_cmd()
|
|
84
|
+
# id 4(window change)/5(close) 忽略
|
|
85
|
+
if len(self.cmd_buf) > MAX_CMD:
|
|
86
|
+
self.cmd_buf = self.cmd_buf[:MAX_CMD] # 截断而非静默清空
|
|
87
|
+
self.logger.event("[!] shell cmd over MAX_CMD, truncated")
|
|
88
|
+
|
|
89
|
+
def _on_client_nonv2(self, data):
|
|
90
|
+
self.cmd_buf.extend(data)
|
|
91
|
+
# P2: interactive_pty stdin 展示缓冲累积(只读旁路,与 cmd_buf 配对解耦)
|
|
92
|
+
self.in_disp_buf.extend(data)
|
|
93
|
+
self._maybe_commit_cmd()
|
|
94
|
+
if len(self.cmd_buf) > MAX_CMD:
|
|
95
|
+
self.cmd_buf = self.cmd_buf[:MAX_CMD] # 截断而非静默清空
|
|
96
|
+
self.logger.event("[!] shell cmd over MAX_CMD, truncated")
|
|
97
|
+
|
|
98
|
+
def _maybe_commit_cmd(self):
|
|
99
|
+
"""client 流累积到 \\r/\\n = 一条命令完成 -> pending_cmd。"""
|
|
100
|
+
idx = -1
|
|
101
|
+
for i, b in enumerate(self.cmd_buf):
|
|
102
|
+
if b in (0x0A, 0x0D):
|
|
103
|
+
idx = i
|
|
104
|
+
break
|
|
105
|
+
if idx < 0:
|
|
106
|
+
return
|
|
107
|
+
cmd_bytes = bytes(self.cmd_buf[:idx])
|
|
108
|
+
self.cmd_buf = self.cmd_buf[idx + 1 :]
|
|
109
|
+
cmd = self._normalize_cmd(cmd_bytes)
|
|
110
|
+
if cmd:
|
|
111
|
+
self.pending_cmd = cmd
|
|
112
|
+
|
|
113
|
+
def on_server(self, data):
|
|
114
|
+
with self.lock:
|
|
115
|
+
self._reset_idle()
|
|
116
|
+
if self.shell_mode in ("oneshot_v2", "interactive_v2"):
|
|
117
|
+
self._on_server_v2(data)
|
|
118
|
+
else:
|
|
119
|
+
self._on_server_nonv2(data)
|
|
120
|
+
|
|
121
|
+
def _on_server_v2(self, data):
|
|
122
|
+
self.s_v2_buf.extend(data)
|
|
123
|
+
if self.v2_corrupt_s:
|
|
124
|
+
self.logger.debug(f"📤 [Server Stream]: <v2 corrupt, raw {len(data)}B>")
|
|
125
|
+
return
|
|
126
|
+
frames, leftover, ok = split_v2_frames(self.s_v2_buf)
|
|
127
|
+
if not ok:
|
|
128
|
+
self.v2_corrupt_s = True
|
|
129
|
+
self.logger.debug("📤 [Server Stream]: <v2 frame invalid, degrading>")
|
|
130
|
+
return
|
|
131
|
+
self.s_v2_buf = leftover
|
|
132
|
+
for id_, payload in frames:
|
|
133
|
+
if id_ in (1, 2): # stdout / stderr
|
|
134
|
+
self.out_buf.extend(payload)
|
|
135
|
+
if len(self.out_buf) > MAX_V2_BUF:
|
|
136
|
+
self._flush_output()
|
|
137
|
+
elif id_ == 3: # exit
|
|
138
|
+
self._flush_output()
|
|
139
|
+
self._reset_session()
|
|
140
|
+
# id 4/5 忽略
|
|
141
|
+
|
|
142
|
+
def _on_server_nonv2(self, data):
|
|
143
|
+
if self.pending_cmd is not None:
|
|
144
|
+
self.out_buf.extend(data)
|
|
145
|
+
if len(self.out_buf) > MAX_V2_BUF:
|
|
146
|
+
self._flush_output()
|
|
147
|
+
else:
|
|
148
|
+
# 交互 PTY 首屏 banner/提示符(命令尚未提交):分离提示符行单独标注
|
|
149
|
+
raw = bytes(data)
|
|
150
|
+
pos = 0
|
|
151
|
+
for m in self.prompt_re.finditer(raw):
|
|
152
|
+
if m.start() > pos:
|
|
153
|
+
t = raw[pos : m.start()].decode("utf-8", errors="replace")
|
|
154
|
+
if t.strip():
|
|
155
|
+
self.logger.debug(f"📤 [Server Stream]: {t!r}")
|
|
156
|
+
self.logger.debug(
|
|
157
|
+
f"📤 [prompt]: {m.group().decode('utf-8', errors='replace')!r}"
|
|
158
|
+
)
|
|
159
|
+
pos = m.end()
|
|
160
|
+
if pos < len(raw):
|
|
161
|
+
t = raw[pos:].decode("utf-8", errors="replace")
|
|
162
|
+
if t.strip():
|
|
163
|
+
self.logger.debug(f"📤 [Server Stream]: {t!r}")
|
|
164
|
+
|
|
165
|
+
def _flush_output(self):
|
|
166
|
+
"""输出当前 out_buf(可重复调用,分块带 $ cmd 标签)。不清 pending_cmd/cmd_buf,
|
|
167
|
+
不调 _cancel_idle。锁契约:调用方须已持 self.lock(本方法不自取,防 Lock 重入死锁)。
|
|
168
|
+
|
|
169
|
+
按 shell_mode 分支(3B 综合方案):
|
|
170
|
+
- interactive_v2/interactive_pty:不剥 echo、不查 pending_cmd;out_buf 按行切分打
|
|
171
|
+
[pty-output]/[prompt]/[pty-partial],末尾半行保留续接(消除逐字符碎块与 kept raw 噪声)。
|
|
172
|
+
同时 flush client stdin 展示缓冲(in_disp_buf)打 [pty-input]/[pty-input-partial]。
|
|
173
|
+
- oneshot_v2/oneshot_raw/None:保留原逻辑(strip_echo + _strip_prompts + $ cmd 标签)。
|
|
174
|
+
"""
|
|
175
|
+
mode = self.shell_mode
|
|
176
|
+
if mode in ("interactive_v2", "interactive_pty"):
|
|
177
|
+
self._flush_interactive()
|
|
178
|
+
self._flush_interactive_input()
|
|
179
|
+
return
|
|
180
|
+
# oneshot 分支(含 shell_mode=None 兜底:首屏未定模式按 oneshot 走,不误入 interactive)
|
|
181
|
+
if self.pending_cmd is not None and self.out_buf:
|
|
182
|
+
out = bytes(self.out_buf)
|
|
183
|
+
if self.has_echo:
|
|
184
|
+
stripped = self._strip_echo(out, self.pending_cmd)
|
|
185
|
+
if stripped is not None:
|
|
186
|
+
out = stripped
|
|
187
|
+
else:
|
|
188
|
+
self.logger.debug("📥 [echo-strip]: no prefix match, kept raw")
|
|
189
|
+
# 剥 PTY 提示符行,不混入配对块(#3)
|
|
190
|
+
out = self._strip_prompts(out)
|
|
191
|
+
text = out.decode("utf-8", errors="replace").replace("\x00", "")
|
|
192
|
+
if text.strip():
|
|
193
|
+
self.logger.event(f"========\n$ {self.pending_cmd}\n{text.strip()}")
|
|
194
|
+
self.out_buf = bytearray()
|
|
195
|
+
|
|
196
|
+
def _flush_interactive(self):
|
|
197
|
+
"""interactive 分支(server 输出):不剥 echo、不查 pending_cmd;out_buf 按行切分打
|
|
198
|
+
[pty-output]/[prompt]/[pty-partial]。整行输出后清出已打印部分;末尾无换行的半行打
|
|
199
|
+
[pty-partial] 并保留在 out_buf 续接(不丢字节)。退格还原+末尾 \\r 剥除由 _emit_pty_line 统一处理。
|
|
200
|
+
锁契约:调用方须已持 self.lock。"""
|
|
201
|
+
if not self.out_buf:
|
|
202
|
+
return
|
|
203
|
+
raw = bytes(self.out_buf)
|
|
204
|
+
# 找最后一个行结束符(\r 或 \n)
|
|
205
|
+
last_nl = raw.rfind(b"\n")
|
|
206
|
+
last_cr = raw.rfind(b"\r")
|
|
207
|
+
last = max(last_nl, last_cr)
|
|
208
|
+
if last < 0:
|
|
209
|
+
# 整块无换行 → 半行,打 [pty-partial] 不清出(保留续接)
|
|
210
|
+
self._emit_pty_line(raw, direction="output", partial=True)
|
|
211
|
+
return # out_buf 保持原样,等下次续接
|
|
212
|
+
consumed = raw[: last + 1] # 到最后换行符为止(含),整行区
|
|
213
|
+
partial = raw[last + 1 :] # 末尾半行(无换行),保留续接
|
|
214
|
+
# 按行切分:每行含其结束符(\r\n / \r / \n)
|
|
215
|
+
pos = 0
|
|
216
|
+
for m in re.finditer(rb"\r\n|\r|\n", consumed):
|
|
217
|
+
line = consumed[pos : m.end()]
|
|
218
|
+
pos = m.end()
|
|
219
|
+
self._emit_pty_line(line, direction="output")
|
|
220
|
+
# 末尾半行
|
|
221
|
+
if partial:
|
|
222
|
+
self._emit_pty_line(partial, direction="output", partial=True)
|
|
223
|
+
# out_buf 仅保留半行(无半行则清空)
|
|
224
|
+
self.out_buf = bytearray(partial)
|
|
225
|
+
|
|
226
|
+
def _flush_interactive_input(self):
|
|
227
|
+
"""interactive 分支(client 输入):in_disp_buf 按行切分打 [pty-input]/[pty-input-partial]。
|
|
228
|
+
只读旁路,与 cmd_buf/_maybe_commit_cmd 命令配对逻辑完全解耦(不清 cmd_buf)。
|
|
229
|
+
退格还原+末尾 \\r 剥除由 _emit_pty_line 统一处理。锁契约:调用方须已持 self.lock。"""
|
|
230
|
+
if not self.in_disp_buf:
|
|
231
|
+
return
|
|
232
|
+
raw = bytes(self.in_disp_buf)
|
|
233
|
+
last_nl = raw.rfind(b"\n")
|
|
234
|
+
last_cr = raw.rfind(b"\r")
|
|
235
|
+
last = max(last_nl, last_cr)
|
|
236
|
+
if last < 0:
|
|
237
|
+
# 整块无换行 → 半行,打 [pty-input-partial] 不清出(保留续接)
|
|
238
|
+
self._emit_pty_line(raw, direction="input", partial=True)
|
|
239
|
+
return # in_disp_buf 保持原样,等下次续接
|
|
240
|
+
consumed = raw[: last + 1]
|
|
241
|
+
partial = raw[last + 1 :]
|
|
242
|
+
pos = 0
|
|
243
|
+
for m in re.finditer(rb"\r\n|\r|\n", consumed):
|
|
244
|
+
line = consumed[pos : m.end()]
|
|
245
|
+
pos = m.end()
|
|
246
|
+
self._emit_pty_line(line, direction="input")
|
|
247
|
+
# in_disp_buf 仅保留半行(无半行则清空)
|
|
248
|
+
self.in_disp_buf = bytearray(partial)
|
|
249
|
+
|
|
250
|
+
def _emit_pty_line(self, line, direction="output", partial=False):
|
|
251
|
+
"""单行 PTY 输入/输出展示:先退格还原,再剥末尾 \\r/\\r\\n,按 direction 标签。
|
|
252
|
+
prompt 行(行首匹配 prompt_re)仍标 [prompt]。空行跳过。统一行 emit 入口。
|
|
253
|
+
direction: "input"(client stdin)/"output"(server stdout);partial=True 标 [pty-*-partial]。
|
|
254
|
+
锁契约:调用方须已持 self.lock。"""
|
|
255
|
+
text = line.decode("utf-8", errors="replace").replace("\x00", "")
|
|
256
|
+
if not text.strip():
|
|
257
|
+
return
|
|
258
|
+
text = self._apply_backspace(text)
|
|
259
|
+
text = text.rstrip("\r\n")
|
|
260
|
+
if not text.strip():
|
|
261
|
+
return
|
|
262
|
+
if partial:
|
|
263
|
+
label = "[pty-input-partial]" if direction == "input" else "[pty-partial]"
|
|
264
|
+
elif self.prompt_re.match(line):
|
|
265
|
+
label = "[prompt]"
|
|
266
|
+
else:
|
|
267
|
+
label = "[pty-input]" if direction == "input" else "[pty-output]"
|
|
268
|
+
self.logger.debug(f"📥 {label}: {text!r}")
|
|
269
|
+
|
|
270
|
+
def _reset_session(self):
|
|
271
|
+
"""彻底清状态:仅在 exit(id==3)/close 调用。锁契约:调用方须已持 self.lock。"""
|
|
272
|
+
self.pending_cmd = None
|
|
273
|
+
self.out_buf = bytearray()
|
|
274
|
+
self.cmd_buf = bytearray()
|
|
275
|
+
self.in_disp_buf = bytearray()
|
|
276
|
+
# 复位粘性降级标志与 v2 半包缓冲,避免新连接或后续命令复用旧状态
|
|
277
|
+
self.v2_corrupt_c = False
|
|
278
|
+
self.v2_corrupt_s = False
|
|
279
|
+
self.c_v2_buf = bytearray()
|
|
280
|
+
self.s_v2_buf = bytearray()
|
|
281
|
+
self._cancel_idle()
|
|
282
|
+
|
|
283
|
+
def _strip_prompts(self, chunk):
|
|
284
|
+
"""从输出块剥离 PTY 提示符行并单独标 [prompt]。剥不净保留原文兜底。"""
|
|
285
|
+
kept = bytearray()
|
|
286
|
+
last = 0
|
|
287
|
+
for m in self.prompt_re.finditer(chunk):
|
|
288
|
+
if m.start() > last:
|
|
289
|
+
kept.extend(chunk[last : m.start()])
|
|
290
|
+
self.logger.debug(
|
|
291
|
+
f"📤 [prompt]: {m.group().decode('utf-8', errors='replace')!r}"
|
|
292
|
+
)
|
|
293
|
+
last = m.end()
|
|
294
|
+
kept.extend(chunk[last:])
|
|
295
|
+
return bytes(kept)
|
|
296
|
+
|
|
297
|
+
def _reset_idle(self):
|
|
298
|
+
"""每次 on_client/on_server 入口重置 0.15s 兜底计时。调用方须持 self.lock。"""
|
|
299
|
+
self._cancel_idle()
|
|
300
|
+
self._idle_gen += 1
|
|
301
|
+
gen = self._idle_gen
|
|
302
|
+
|
|
303
|
+
# 世代号闭包捕获:回调入口校验 gen,杜绝 Timer 已进持锁等待时 cancel no-op 多跑一次 flush
|
|
304
|
+
def fire():
|
|
305
|
+
self._on_idle(gen)
|
|
306
|
+
|
|
307
|
+
self.idle_timer = threading.Timer(IDLE_TIMEOUT, fire)
|
|
308
|
+
self.idle_timer.daemon = True
|
|
309
|
+
self.idle_timer.start()
|
|
310
|
+
|
|
311
|
+
def _cancel_idle(self):
|
|
312
|
+
if self.idle_timer:
|
|
313
|
+
self.idle_timer.cancel()
|
|
314
|
+
self.idle_timer = None
|
|
315
|
+
|
|
316
|
+
def _on_idle(self, gen):
|
|
317
|
+
"""Timer 回调 wrapper:世代号校验后持锁调 _flush_output(不 reset,保留 pending_cmd)。"""
|
|
318
|
+
with self.lock:
|
|
319
|
+
if gen != self._idle_gen:
|
|
320
|
+
return # 旧世代 Timer,已被 _reset_idle 作废,丢弃本次 flush
|
|
321
|
+
self._flush_output()
|
|
322
|
+
|
|
323
|
+
def _normalize_cmd(self, buf):
|
|
324
|
+
"""归一化命令文本:退格(\\x7f/\\x08)删前字符、剥控制符/ANSI、strip。"""
|
|
325
|
+
out = bytearray()
|
|
326
|
+
skip_ansi = False
|
|
327
|
+
for b in buf:
|
|
328
|
+
if skip_ansi:
|
|
329
|
+
if 0x40 <= b <= 0x7E: # CSI 终止符
|
|
330
|
+
skip_ansi = False
|
|
331
|
+
continue
|
|
332
|
+
if b == 0x1B: # ESC -> 跳过 ANSI 序列
|
|
333
|
+
skip_ansi = True
|
|
334
|
+
continue
|
|
335
|
+
if b in (0x7F, 0x08): # DEL / BS
|
|
336
|
+
if out:
|
|
337
|
+
out.pop()
|
|
338
|
+
continue
|
|
339
|
+
if b < 0x20 and b not in (0x0A, 0x0D): # 其它控制符
|
|
340
|
+
continue
|
|
341
|
+
out.append(b)
|
|
342
|
+
return out.decode("utf-8", errors="replace").strip("\r\n ").strip()
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _apply_backspace(text):
|
|
346
|
+
"""退格还原:\\x08(BS)/\\x7f(DEL) 删前字符,纯列表扫描一次(不用正则)。
|
|
347
|
+
镜像 _normalize_cmd 的 0x7f/0x08 pop 逻辑,但只做退格还原、不剥 ANSI/控制符(PTY 行展示用)。
|
|
348
|
+
对 BS-SP-BS 与裸 BS 都正确:普通字符入栈,BS/DEL 弹栈顶。"""
|
|
349
|
+
out = []
|
|
350
|
+
for ch in text:
|
|
351
|
+
if ch in ("\x08", "\x7f"):
|
|
352
|
+
if out:
|
|
353
|
+
out.pop()
|
|
354
|
+
continue
|
|
355
|
+
out.append(ch)
|
|
356
|
+
return "".join(out)
|
|
357
|
+
|
|
358
|
+
def _strip_echo(self, chunk, cmd):
|
|
359
|
+
"""剥离开头等于命令回显的前缀。返回剥除后的 bytes,或 None(未匹配,调用方保留原文)。"""
|
|
360
|
+
for prefix in (cmd + "\r\n", cmd + "\r", cmd + "\n", cmd):
|
|
361
|
+
pb = prefix.encode()
|
|
362
|
+
if chunk.startswith(pb):
|
|
363
|
+
return chunk[len(pb) :]
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
def close(self):
|
|
367
|
+
"""连接关闭时收尾未决配对块并彻底 reset。"""
|
|
368
|
+
with self.lock:
|
|
369
|
+
self._cancel_idle()
|
|
370
|
+
self._flush_output()
|
|
371
|
+
self._reset_session()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
from adbproxy.config import FILES_DIR, IDLE_TIMEOUT
|
|
4
|
+
from adbproxy.observers.events import ClientBytes, ServerBytes
|
|
5
|
+
from adbproxy.observers.install import InstallStreamCapture
|
|
6
|
+
from adbproxy.observers.shell import ShellSession
|
|
7
|
+
from adbproxy.observers.sync import SyncSession
|
|
8
|
+
from adbproxy.runtime.logging import Logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StreamBuffer:
|
|
12
|
+
def __init__(self, conn_id, file_logger, capture_enabled=True):
|
|
13
|
+
self.logger = Logger(conn_id, file_logger)
|
|
14
|
+
self.shell = ShellSession(self.logger)
|
|
15
|
+
self.sync = SyncSession(self.logger, FILES_DIR, capture_enabled)
|
|
16
|
+
self.install = InstallStreamCapture(self.logger, FILES_DIR, capture_enabled)
|
|
17
|
+
self.c_buf = bytearray()
|
|
18
|
+
self.s_buf = bytearray()
|
|
19
|
+
self.lock = threading.Lock()
|
|
20
|
+
self.timer = None
|
|
21
|
+
|
|
22
|
+
def handle(self, event):
|
|
23
|
+
"""Route a shallow observer event (ClientBytes / ServerBytes)."""
|
|
24
|
+
if isinstance(event, ClientBytes):
|
|
25
|
+
self._on_client_bytes(event.data)
|
|
26
|
+
elif isinstance(event, ServerBytes):
|
|
27
|
+
self._on_server_bytes(event.data)
|
|
28
|
+
|
|
29
|
+
def add_client(self, data):
|
|
30
|
+
self.handle(ClientBytes(data))
|
|
31
|
+
|
|
32
|
+
def add_server(self, data):
|
|
33
|
+
self.handle(ServerBytes(data))
|
|
34
|
+
|
|
35
|
+
def _on_client_bytes(self, data):
|
|
36
|
+
# 优先级:shell > sync > install > 碎片缓冲
|
|
37
|
+
if self.shell.active:
|
|
38
|
+
self.shell.on_client(data)
|
|
39
|
+
elif self.sync.active:
|
|
40
|
+
self.sync.on_client(data)
|
|
41
|
+
elif self.install.active:
|
|
42
|
+
take = self.install.on_client(data)
|
|
43
|
+
leftover = data[take:]
|
|
44
|
+
if leftover:
|
|
45
|
+
# server 提前回 Success 等残余字节交回可读路径(通常为空)
|
|
46
|
+
with self.lock:
|
|
47
|
+
self.c_buf.extend(leftover)
|
|
48
|
+
self._reset_timer()
|
|
49
|
+
else:
|
|
50
|
+
with self.lock:
|
|
51
|
+
self.c_buf.extend(data)
|
|
52
|
+
self._reset_timer()
|
|
53
|
+
|
|
54
|
+
def _on_server_bytes(self, data):
|
|
55
|
+
if self.shell.active:
|
|
56
|
+
self.shell.on_server(data)
|
|
57
|
+
elif self.sync.active:
|
|
58
|
+
self.sync.on_server(data)
|
|
59
|
+
else:
|
|
60
|
+
with self.lock:
|
|
61
|
+
self.s_buf.extend(data)
|
|
62
|
+
self._reset_timer()
|
|
63
|
+
|
|
64
|
+
def _reset_timer(self):
|
|
65
|
+
if self.timer:
|
|
66
|
+
self.timer.cancel()
|
|
67
|
+
self.timer = threading.Timer(IDLE_TIMEOUT, self._flush)
|
|
68
|
+
self.timer.daemon = True
|
|
69
|
+
self.timer.start()
|
|
70
|
+
|
|
71
|
+
def _flush(self):
|
|
72
|
+
with self.lock:
|
|
73
|
+
c_data = self.c_buf
|
|
74
|
+
s_data = self.s_buf
|
|
75
|
+
self.c_buf = bytearray()
|
|
76
|
+
self.s_buf = bytearray()
|
|
77
|
+
if c_data:
|
|
78
|
+
text = bytes(c_data).decode("utf-8", errors="replace").replace("\x00", "")
|
|
79
|
+
if text.strip():
|
|
80
|
+
self.logger.debug(f"📥 [Client Stream]: {text!r}")
|
|
81
|
+
if s_data:
|
|
82
|
+
text = bytes(s_data).decode("utf-8", errors="replace").replace("\x00", "")
|
|
83
|
+
if text.strip():
|
|
84
|
+
if len(text) > 800:
|
|
85
|
+
text = text[:800] + "\n... [Data Truncated]"
|
|
86
|
+
self.logger.debug(f"📤 [Server Stream]:\n{text.strip()}")
|
|
87
|
+
|
|
88
|
+
def close(self):
|
|
89
|
+
self.shell.close()
|
|
90
|
+
self.sync.close()
|
|
91
|
+
self.install.close()
|
|
92
|
+
if self.timer:
|
|
93
|
+
self.timer.cancel()
|
|
94
|
+
self._flush()
|