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,75 @@
1
+ """Backend smart-socket handshake sequencer helpers (pure framing, no I/O).
2
+
3
+ Blocking handshake stays in AdbClientBackend.start(): send frame → recv exact
4
+ status → optional FAIL body. This module only builds frames and interprets
5
+ status/hex4 words — never a multi-shape incremental conversation loop.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from adbproxy.protocols.smart_socket.wire import (
13
+ FAIL,
14
+ OKAY,
15
+ decode_hex4,
16
+ format_service_frame,
17
+ parse_status,
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class HandshakeStatus:
23
+ """Result of interpreting a 4-byte smart-socket status word."""
24
+
25
+ word: str # "OKAY" | "FAIL" | other decoded
26
+ ok: bool
27
+ fail: bool
28
+ valid: bool # True when word is exactly OKAY or FAIL
29
+
30
+
31
+ class SmartSocketHandshake:
32
+ """Client-side handshake helpers for adb server smart-socket.
33
+
34
+ One logical use per stream: tport frame → status → service frame → status.
35
+ """
36
+
37
+ @staticmethod
38
+ def build_transport_frame(serial: str) -> bytes:
39
+ """Encode ``host:transport:<serial>`` smart-socket frame."""
40
+ return format_service_frame(f"host:transport:{serial}")
41
+
42
+ @staticmethod
43
+ def build_service_frame(service: bytes | str) -> bytes:
44
+ """Encode service smart-socket frame; raises ValueError if body > 0xffff."""
45
+ return format_service_frame(service)
46
+
47
+ @staticmethod
48
+ def interpret_status(data: bytes | bytearray | memoryview) -> HandshakeStatus:
49
+ """Interpret exactly 4 bytes as OKAY/FAIL (or invalid)."""
50
+ raw = bytes(data)
51
+ if len(raw) != 4:
52
+ return HandshakeStatus(word="", ok=False, fail=False, valid=False)
53
+ try:
54
+ word = parse_status(raw)
55
+ except ValueError:
56
+ word = raw.decode("ascii", errors="ignore")
57
+ return HandshakeStatus(word=word, ok=False, fail=False, valid=False)
58
+ return HandshakeStatus(
59
+ word=word,
60
+ ok=word == "OKAY",
61
+ fail=word == "FAIL",
62
+ valid=True,
63
+ )
64
+
65
+ @staticmethod
66
+ def decode_fail_length(hex4: bytes | bytearray | memoryview) -> int | None:
67
+ """Decode FAIL protocol-string length prefix; None if invalid hex4."""
68
+ try:
69
+ return decode_hex4(hex4)
70
+ except ValueError:
71
+ return None
72
+
73
+ # re-export status constants for backend clarity
74
+ OKAY = OKAY
75
+ FAIL = FAIL
@@ -0,0 +1,82 @@
1
+ """Pure smart-socket wire primitives: OKAY/FAIL, hex4, protocol-string frames.
2
+
3
+ No I/O, no policy, no connection state. Shared by server conversation and
4
+ backend handshake (encode/decode only).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ OKAY = b"OKAY"
10
+ FAIL = b"FAIL"
11
+ _STATUS_VALUES = {OKAY, FAIL}
12
+
13
+
14
+ def encode_hex4(length: int) -> bytes:
15
+ """Encode a non-negative integer as 4 ASCII hex digits (big-endian nibble text)."""
16
+ if length < 0 or length > 0xFFFF:
17
+ raise ValueError(f"hex4 length out of range: {length}")
18
+ return f"{length:04x}".encode("ascii")
19
+
20
+
21
+ def decode_hex4(data: bytes | bytearray | memoryview) -> int:
22
+ """Decode exactly 4 ASCII hex digits to int. Raises ValueError on bad input."""
23
+ raw = bytes(data)
24
+ if len(raw) != 4:
25
+ raise ValueError(f"hex4 requires exactly 4 bytes, got {len(raw)}")
26
+ try:
27
+ return int(raw.decode("ascii"), 16)
28
+ except (UnicodeDecodeError, ValueError) as exc:
29
+ raise ValueError(f"invalid hex4: {raw!r}") from exc
30
+
31
+
32
+ def try_parse_hex4_length_prefix(
33
+ buf: bytes | bytearray | memoryview,
34
+ ) -> tuple[int, bytes] | None:
35
+ """If buf starts with a complete hex4 length prefix, return (length, rest).
36
+
37
+ Returns None when fewer than 4 bytes are available. Raises ValueError when
38
+ the 4-byte prefix is not valid ASCII hex.
39
+ """
40
+ raw = bytes(buf)
41
+ if len(raw) < 4:
42
+ return None
43
+ length = decode_hex4(raw[:4])
44
+ return length, raw[4:]
45
+
46
+
47
+ def encode_protocol_string(body: bytes | str) -> bytes:
48
+ """Encode a length-prefixed protocol string: <hex4><body>."""
49
+ raw = body.encode("utf-8") if isinstance(body, str) else bytes(body)
50
+ if len(raw) > 0xFFFF:
51
+ raise ValueError(f"protocol-string body too large: {len(raw)}")
52
+ return encode_hex4(len(raw)) + raw
53
+
54
+
55
+ def format_service_frame(service: bytes | str) -> bytes:
56
+ """Encode smart-socket ``<hex4><body>``; body must be ≤ 0xffff bytes."""
57
+ body = service.encode("utf-8") if isinstance(service, str) else bytes(service)
58
+ if len(body) > 0xFFFF:
59
+ raise ValueError(f"smart-socket service body too large: {len(body)}")
60
+ return encode_hex4(len(body)) + body
61
+
62
+
63
+ def parse_status(data: bytes | bytearray | memoryview) -> str:
64
+ """Parse a 4-byte status word into 'OKAY' or 'FAIL'. Raises ValueError otherwise."""
65
+ raw = bytes(data)
66
+ if len(raw) != 4:
67
+ raise ValueError(f"status requires exactly 4 bytes, got {len(raw)}")
68
+ if raw not in _STATUS_VALUES:
69
+ raise ValueError(f"invalid status word: {raw!r}")
70
+ return raw.decode("ascii")
71
+
72
+
73
+ def try_parse_status(buf: bytes | bytearray | memoryview) -> tuple[str, bytes] | None:
74
+ """If buf has ≥4 bytes, parse the leading status word and return (status, rest).
75
+
76
+ Returns None when fewer than 4 bytes are available. Raises ValueError on an
77
+ invalid 4-byte status word.
78
+ """
79
+ raw = bytes(buf)
80
+ if len(raw) < 4:
81
+ return None
82
+ return parse_status(raw[:4]), raw[4:]
@@ -0,0 +1,52 @@
1
+ """Transport protocol package (constants / errors / codec).
2
+
3
+ Public surface remains ``from adbproxy.protocols.transport import ...``.
4
+ """
5
+
6
+ from adbproxy.protocols.transport.codec import TransportCodec
7
+ from adbproxy.protocols.transport.constants import (
8
+ A_AUTH,
9
+ A_CLSE,
10
+ A_CNXN,
11
+ A_OKAY,
12
+ A_OPEN,
13
+ A_STLS,
14
+ A_VERSION,
15
+ A_VERSION_MIN,
16
+ A_VERSION_SKIP_CHECKSUM,
17
+ A_WRTE,
18
+ DEFAULT_ADB_SERVER_PORT,
19
+ DEFAULT_BANNER_FEATURES,
20
+ DEFAULT_LISTEN_PORT,
21
+ HEADER_FMT,
22
+ HEADER_SIZE,
23
+ LOCAL_MAXDATA,
24
+ MAX_PAYLOAD,
25
+ MAX_PAYLOAD_V1,
26
+ MAX_SEND_QUEUE,
27
+ )
28
+ from adbproxy.protocols.transport.errors import TransportProtocolError
29
+
30
+ __all__ = [
31
+ "A_AUTH",
32
+ "A_CLSE",
33
+ "A_CNXN",
34
+ "A_OKAY",
35
+ "A_OPEN",
36
+ "A_STLS",
37
+ "A_VERSION",
38
+ "A_VERSION_MIN",
39
+ "A_VERSION_SKIP_CHECKSUM",
40
+ "A_WRTE",
41
+ "DEFAULT_ADB_SERVER_PORT",
42
+ "DEFAULT_BANNER_FEATURES",
43
+ "DEFAULT_LISTEN_PORT",
44
+ "HEADER_FMT",
45
+ "HEADER_SIZE",
46
+ "LOCAL_MAXDATA",
47
+ "MAX_PAYLOAD",
48
+ "MAX_PAYLOAD_V1",
49
+ "MAX_SEND_QUEUE",
50
+ "TransportCodec",
51
+ "TransportProtocolError",
52
+ ]
@@ -0,0 +1,90 @@
1
+ """Transport 24B header encode/decode + streaming framing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+
7
+ from adbproxy.protocols.transport.constants import (
8
+ A_VERSION,
9
+ A_VERSION_MIN,
10
+ A_VERSION_SKIP_CHECKSUM,
11
+ HEADER_FMT,
12
+ HEADER_SIZE,
13
+ LOCAL_MAXDATA,
14
+ MAX_PAYLOAD,
15
+ MAX_PAYLOAD_V1,
16
+ )
17
+ from adbproxy.protocols.transport.errors import TransportProtocolError
18
+
19
+
20
+ class TransportCodec:
21
+ """传输协议 24B 头编解码 + 流式分帧。自包含,可单独搬迁。
22
+ 头格式(little-endian 6×uint32):command, arg0, arg1, data_length, data_check, magic。
23
+ magic = command ^ 0xFFFFFFFF;data_check = sum(payload) & 0xFFFFFFFF(adb.cpp:113-119)。"""
24
+
25
+ def __init__(self):
26
+ self.buf = bytearray()
27
+ self.max_payload = MAX_PAYLOAD_V1 # 握手前 4KB,CNXN 后按双端 maxdata 协商裁剪
28
+ self.protocol_version = A_VERSION_MIN
29
+
30
+ def negotiate(self, peer_version, peer_maxdata):
31
+ """按 AOSP update_version 协商协议版本和 payload 上限。"""
32
+ self.protocol_version = min(peer_version, A_VERSION)
33
+ self.set_max_payload(peer_maxdata)
34
+
35
+ def encode(self, command, arg0, arg1, payload=b""):
36
+ """按已协商版本决定是否省略 outgoing checksum。"""
37
+ checksum_enabled = self.protocol_version < A_VERSION_SKIP_CHECKSUM
38
+ return self.pack(
39
+ command,
40
+ arg0,
41
+ arg1,
42
+ payload,
43
+ checksum_enabled=checksum_enabled,
44
+ )
45
+
46
+ def set_max_payload(self, peer_maxdata):
47
+ """CNXN 握手后协商 max_payload = min(双端 maxdata, 1MB)。"""
48
+ self.max_payload = min(peer_maxdata, LOCAL_MAXDATA, MAX_PAYLOAD)
49
+
50
+ @staticmethod
51
+ def pack(command, arg0, arg1, payload=b"", *, checksum_enabled=True):
52
+ """组 24B 头 + payload。checksum = sum(payload)&0xFFFFFFFF(无 payload 为 0)。"""
53
+ data_len = len(payload)
54
+ checksum = (
55
+ (sum(payload) & 0xFFFFFFFF) if payload and checksum_enabled else 0
56
+ )
57
+ magic = command ^ 0xFFFFFFFF
58
+ header = struct.pack(HEADER_FMT, command, arg0, arg1, data_len, checksum, magic)
59
+ return header + payload
60
+
61
+ @staticmethod
62
+ def unpack_header(buf):
63
+ """解 24B 头 -> (cmd,arg0,arg1,dlen,chk,magic)。"""
64
+ return struct.unpack(HEADER_FMT, bytes(buf[:HEADER_SIZE]))
65
+
66
+ def feed(self, chunk):
67
+ """流式累积字节 -> 产出完整 apacket 列表。apacket = (cmd,arg0,arg1,payload:bytes)。
68
+ 半包头/半 payload 留 buf 续接。magic 不匹配/长度越界:抛出终止性协议错误。"""
69
+ packets = []
70
+ self.buf.extend(chunk)
71
+ while True:
72
+ if len(self.buf) < HEADER_SIZE:
73
+ break
74
+ cmd, arg0, arg1, dlen, chk, magic = struct.unpack(
75
+ HEADER_FMT, bytes(self.buf[:HEADER_SIZE])
76
+ )
77
+ if magic != (cmd ^ 0xFFFFFFFF):
78
+ raise TransportProtocolError(
79
+ f"invalid transport magic: command=0x{cmd:08x} magic=0x{magic:08x}"
80
+ )
81
+ if dlen > self.max_payload:
82
+ raise TransportProtocolError(
83
+ f"transport payload {dlen} exceeds max {self.max_payload}"
84
+ )
85
+ if len(self.buf) < HEADER_SIZE + dlen:
86
+ break # 半包,等 payload 收齐
87
+ payload = bytes(self.buf[HEADER_SIZE : HEADER_SIZE + dlen])
88
+ del self.buf[: HEADER_SIZE + dlen]
89
+ packets.append((cmd, arg0, arg1, payload))
90
+ return packets
@@ -0,0 +1,21 @@
1
+ """Transport protocol constants (AOSP-aligned command words and limits)."""
2
+
3
+ A_CNXN = 0x4E584E43
4
+ A_AUTH = 0x48545541
5
+ A_OPEN = 0x4E45504F
6
+ A_OKAY = 0x59414B4F
7
+ A_CLSE = 0x45534C43
8
+ A_WRTE = 0x45545257
9
+ A_STLS = 0x534C5453
10
+ A_VERSION_MIN = 0x01000000
11
+ A_VERSION_SKIP_CHECKSUM = 0x01000001
12
+ A_VERSION = 0x01000001
13
+ MAX_PAYLOAD_V1 = 4 * 1024 # adb.h:33,握手前未知对端 maxdata
14
+ MAX_PAYLOAD = 1024 * 1024 # adb.h:34
15
+ LOCAL_MAXDATA = 0x40000 # 256KB,本端 maxdata(adb.cpp send_connect arg1)
16
+ HEADER_FMT = "<6I" # 24B 头 little-endian 6×uint32
17
+ HEADER_SIZE = 24 # struct.calcsize(HEADER_FMT)
18
+ MAX_SEND_QUEUE = 4 * LOCAL_MAXDATA # 发送队列背压上限,超限关流 CLSE
19
+ DEFAULT_LISTEN_PORT = 5566
20
+ DEFAULT_ADB_SERVER_PORT = 5037
21
+ DEFAULT_BANNER_FEATURES = "shell_v2,sendrecv_v2" # 显式不含 delayed_ack
@@ -0,0 +1,5 @@
1
+ """Transport protocol errors."""
2
+
3
+
4
+ class TransportProtocolError(ValueError):
5
+ """传输头不可恢复时终止当前 transport。"""
@@ -0,0 +1 @@
1
+ """socket、日志、抓取和进程运行时工具。"""
@@ -0,0 +1,195 @@
1
+ import socket
2
+ import subprocess
3
+ import time
4
+
5
+
6
+ def probe_adb_server(port):
7
+ """仅探测端口是否可连接;探活结果本身不代表进程所有权。"""
8
+ sock = None
9
+ try:
10
+ sock = socket.create_connection(("127.0.0.1", port), timeout=0.5)
11
+ return True
12
+ except OSError:
13
+ return False
14
+ finally:
15
+ if sock is not None:
16
+ try:
17
+ sock.close()
18
+ except Exception:
19
+ pass
20
+
21
+
22
+ class AdbServerLease:
23
+ """记录本进程对 adb server 执行过的动作,并只撤销有证据的所有权。"""
24
+
25
+ def __init__(
26
+ self,
27
+ managed_port,
28
+ restore_port=None,
29
+ *,
30
+ runner=None,
31
+ probe=None,
32
+ sleeper=None,
33
+ probe_attempts=50,
34
+ probe_interval=0.1,
35
+ cleanup_attempts=3,
36
+ log=None,
37
+ ):
38
+ self.managed_port = managed_port
39
+ self.restore_port = restore_port
40
+ self.runner = runner or subprocess.run
41
+ self.probe = probe or probe_adb_server
42
+ self.sleeper = sleeper or time.sleep
43
+ self.probe_attempts = probe_attempts
44
+ self.probe_interval = probe_interval
45
+ self.cleanup_attempts = cleanup_attempts
46
+ self.log = log or (lambda _message: None)
47
+ self.initial_state = None
48
+ self.stopped_by_us = False
49
+ self.started_by_us = False
50
+ self.restore_required = False
51
+ self.preflight_absent = False
52
+ self.start_command_succeeded = False
53
+ self.post_start_ready = False
54
+ self._cleaned = False
55
+
56
+ def _command(self, action, port):
57
+ if port == 5037:
58
+ return ["adb", f"{action}-server"]
59
+ return ["adb", "-P", str(port), f"{action}-server"]
60
+
61
+ def _run(self, command, timeout):
62
+ try:
63
+ result = self.runner(
64
+ command,
65
+ stdout=subprocess.DEVNULL,
66
+ stderr=subprocess.DEVNULL,
67
+ timeout=timeout,
68
+ )
69
+ except subprocess.TimeoutExpired:
70
+ self.log(f"[!] adb 调用超时: {' '.join(command)}")
71
+ return False
72
+ except FileNotFoundError:
73
+ self.log(f"[!] adb 未找到: {' '.join(command)}")
74
+ return False
75
+ if result.returncode != 0:
76
+ self.log(
77
+ f"[!] adb 命令失败 returncode={result.returncode}: {' '.join(command)}"
78
+ )
79
+ return False
80
+ return True
81
+
82
+ def _wait_for(self, port, expected):
83
+ for attempt in range(self.probe_attempts):
84
+ if bool(self.probe(port)) is expected:
85
+ return True
86
+ if attempt + 1 < self.probe_attempts:
87
+ self.sleeper(self.probe_interval)
88
+ return False
89
+
90
+ def acquire_owned(self):
91
+ """为单端口启动场景获取 lease;预先存在的实例绝不接管。"""
92
+ initially_running = bool(self.probe(self.managed_port))
93
+ self.initial_state = "running" if initially_running else "absent"
94
+ if initially_running:
95
+ self.log(
96
+ f"[!] adb server :{self.managed_port} 已存在,拒绝推断或接管所有权"
97
+ )
98
+ return False
99
+ self.preflight_absent = True
100
+ return self.start_owned()
101
+
102
+ def prepare_takeover(self):
103
+ """代理接管前先确认后端端口为空,再按证据停止需恢复的默认实例。"""
104
+ if self.restore_port is None:
105
+ raise ValueError("restore_port is required for takeover")
106
+ managed_running = bool(self.probe(self.managed_port))
107
+ restore_running = bool(self.probe(self.restore_port)) if not managed_running else None
108
+ self.initial_state = {
109
+ "managed": "running" if managed_running else "absent",
110
+ "restore": (
111
+ "unknown"
112
+ if restore_running is None
113
+ else ("running" if restore_running else "absent")
114
+ ),
115
+ }
116
+ if managed_running:
117
+ self.log(
118
+ f"[!] adb server :{self.managed_port} 已存在,停止默认实例前终止接管"
119
+ )
120
+ return False
121
+ self.preflight_absent = True
122
+ if not restore_running:
123
+ return True
124
+ if not self._run(self._command("kill", self.restore_port), timeout=5):
125
+ return False
126
+ if not self._wait_for(self.restore_port, False):
127
+ self.log(f"[!] adb server :{self.restore_port} 停止后仍可连接")
128
+ return False
129
+ self.stopped_by_us = True
130
+ self.restore_required = True
131
+ return True
132
+
133
+ def start_owned(self):
134
+ """仅在 preflight 已证明端口为空时启动并提交所有权。"""
135
+ if not self.preflight_absent:
136
+ self.log("[!] 缺少 preflight absent 证据,拒绝启动 adb server")
137
+ return False
138
+ if not self._run(self._command("start", self.managed_port), timeout=10):
139
+ return False
140
+ self.start_command_succeeded = True
141
+ if not self._wait_for(self.managed_port, True):
142
+ self.log(f"[!] adb server :{self.managed_port} 启动后探活失败,回滚")
143
+ self._rollback_started_command()
144
+ return False
145
+ self.post_start_ready = True
146
+ self.started_by_us = True
147
+ return True
148
+
149
+ def _rollback_started_command(self):
150
+ if not (self.preflight_absent and self.start_command_succeeded):
151
+ return True
152
+ if not self._cleanup_action("kill", self.managed_port, 5, False):
153
+ self.log(
154
+ f"[!] adb server :{self.managed_port} 回滚未完成;保留启动证据供 cleanup 重试"
155
+ )
156
+ return False
157
+ self.start_command_succeeded = False
158
+ self.started_by_us = False
159
+ self.post_start_ready = False
160
+ return True
161
+
162
+ def _cleanup_action(self, action, port, timeout, expected_running):
163
+ """有界重试清理动作;命令和端口状态都成功后才提交 ledger。"""
164
+ command = self._command(action, port)
165
+ for attempt in range(self.cleanup_attempts):
166
+ if self._run(command, timeout=timeout):
167
+ if self._wait_for(port, expected_running):
168
+ return True
169
+ self.log(
170
+ f"[!] adb server :{port} 执行 {action} 后状态未达到预期"
171
+ )
172
+ if attempt + 1 < self.cleanup_attempts:
173
+ self.sleeper(self.probe_interval)
174
+ return False
175
+
176
+ def cleanup(self):
177
+ """幂等撤销 action ledger;失败时保留证据,允许后续再次清理。"""
178
+ if self._cleaned:
179
+ return True
180
+ complete = True
181
+ if self.started_by_us or self.start_command_succeeded:
182
+ if self._cleanup_action("kill", self.managed_port, 5, False):
183
+ self.started_by_us = False
184
+ self.start_command_succeeded = False
185
+ self.post_start_ready = False
186
+ else:
187
+ complete = False
188
+ if self.restore_required and self.restore_port is not None:
189
+ if self._cleanup_action("start", self.restore_port, 10, True):
190
+ self.restore_required = False
191
+ self.stopped_by_us = False
192
+ else:
193
+ complete = False
194
+ self._cleaned = complete
195
+ return complete
@@ -0,0 +1,41 @@
1
+ import os
2
+ import threading
3
+ import time
4
+
5
+
6
+ _CAPTURE_NAME_LOCK = threading.Lock()
7
+ _CAPTURE_NAME_COUNTER = 0
8
+
9
+
10
+ def _next_capture_token():
11
+ """生成进程内单调且含纳秒时间的抓包名 token(无 pid)。
12
+
13
+ 形态:``{time_ns}-{seq}``。seq 为进程内成功打开 capture 的单调序号。
14
+ """
15
+ global _CAPTURE_NAME_COUNTER
16
+ with _CAPTURE_NAME_LOCK:
17
+ _CAPTURE_NAME_COUNTER += 1
18
+ counter = _CAPTURE_NAME_COUNTER
19
+ return f"{time.time_ns()}-{counter}"
20
+
21
+
22
+ def open_unique_capture(directory, prefix, suffix):
23
+ """以排他创建方式打开抓包文件,避免并发同名覆盖。
24
+
25
+ 最终名:``{prefix}-{time_ns}-{seq}{suffix}``。
26
+ """
27
+ os.makedirs(directory, exist_ok=True)
28
+ while True:
29
+ name = f"{prefix}-{_next_capture_token()}{suffix}"
30
+ path = os.path.join(directory, name)
31
+ try:
32
+ return path, open(path, "xb")
33
+ except FileExistsError:
34
+ continue
35
+
36
+
37
+ def reserve_unique_capture_path(directory, prefix, suffix):
38
+ """为后续原子 rename 排他预留唯一目标路径。"""
39
+ path, fp = open_unique_capture(directory, prefix, suffix)
40
+ fp.close()
41
+ return path
@@ -0,0 +1,113 @@
1
+ import threading
2
+
3
+
4
+ class FileLogger:
5
+ """进程级共享文件写入器,持锁串行;日志失败时自动降级为静默。"""
6
+
7
+ def __init__(self, path):
8
+ self.lock = threading.Lock()
9
+ try:
10
+ self.file = open(path, "a", encoding="utf-8")
11
+ except Exception:
12
+ self.file = None
13
+
14
+ def write(self, line):
15
+ if self.file is None:
16
+ return
17
+ try:
18
+ with self.lock:
19
+ self.file.write(line + "\n")
20
+ self.file.flush()
21
+ except Exception:
22
+ pass
23
+
24
+ def close(self):
25
+ try:
26
+ if self.file is not None:
27
+ self.file.close()
28
+ except Exception:
29
+ pass
30
+
31
+
32
+ class DebugHexLogger:
33
+ """P0: 原始 hex 落盘器(per-proxy 单文件)。每 recv 一块写一行 hex + 一个递增 line_no。
34
+ sendall 原样转发在前,本类仅旁路落盘,写盘异常内化不阻断转发。自包含,可单独搬迁。
35
+
36
+ 行格式(一个 line_no = 一块 recv):
37
+ # L{line_no} Conn#{conn_id} dir={c|s} len={n}\n{hexdump}\n
38
+ hexdump 内部按 16 字节折行可读(带 ASCII 侧栏),共用块首 line_no。"""
39
+
40
+ def __init__(self, path):
41
+ self.lock = threading.Lock()
42
+ self.line_no = 0
43
+ try:
44
+ self.file = open(path, "a", encoding="utf-8")
45
+ except Exception:
46
+ self.file = None
47
+
48
+ def write_hex(self, conn_id, direction, data):
49
+ """落盘一块 hex,返回块首 line_no。direction: "c"(client→server)/"s"(server→client)。
50
+ 异常降级:只返回当前 line_no 不抛(调用方仅用于打提示,失败已在内化)。"""
51
+ try:
52
+ if self.file is None:
53
+ return self.line_no
54
+ with self.lock:
55
+ self.line_no += 1
56
+ ln = self.line_no
57
+ d = direction if direction in ("c", "s") else "?"
58
+ self.file.write(f"# L{ln} Conn#{conn_id} dir={d} len={len(data)}\n")
59
+ self.file.write(self._hexdump(data))
60
+ self.file.write("\n")
61
+ self.file.flush()
62
+ return ln
63
+ except Exception:
64
+ return self.line_no # 写盘失败降级,不阻断转发
65
+
66
+ @staticmethod
67
+ def _hexdump(data):
68
+ """按 16 字节折行 hex + ASCII 侧栏(可读)。空数据返回空串。"""
69
+ if not data:
70
+ return ""
71
+ lines = []
72
+ n = len(data)
73
+ for off in range(0, n, 16):
74
+ chunk = data[off : off + 16]
75
+ hexs = " ".join(f"{b:02x}" for b in chunk)
76
+ ascii_ = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in chunk)
77
+ lines.append(f"{off:04x} {hexs:<47} |{ascii_}|")
78
+ return "\n".join(lines) + "\n"
79
+
80
+ def close(self):
81
+ try:
82
+ if self.file is not None:
83
+ self.file.close()
84
+ except Exception:
85
+ pass
86
+
87
+
88
+ class Logger:
89
+ """per-conn 日志门面:正常时 tee 到控制台和文件,任一输出失败均静默降级。"""
90
+
91
+ def __init__(self, conn_id, file_logger):
92
+ self.conn_id = conn_id
93
+ self.fl = file_logger
94
+
95
+ def _fmt(self, msg):
96
+ return f"[Conn #{self.conn_id}] {msg}"
97
+
98
+ def _write(self, msg):
99
+ line = self._fmt(msg)
100
+ try:
101
+ print(line)
102
+ except Exception:
103
+ pass
104
+ try:
105
+ self.fl.write(line)
106
+ except Exception:
107
+ pass
108
+
109
+ def event(self, msg):
110
+ self._write(msg)
111
+
112
+ def debug(self, msg):
113
+ self._write(msg)