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,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()