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