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,304 @@
|
|
|
1
|
+
"""Device transport session decision layer (Track 2b).
|
|
2
|
+
|
|
3
|
+
``AdbdSession.receive`` / ``on_session_event`` produce TransportAction lists.
|
|
4
|
+
No socket I/O, no thread join, no ``_try_flush`` algorithm.
|
|
5
|
+
Connection executes actions and owns flush/backpressure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from adbproxy.device.banner import build_cnxn_banner
|
|
14
|
+
from adbproxy.protocols.transport import (
|
|
15
|
+
A_AUTH,
|
|
16
|
+
A_CLSE,
|
|
17
|
+
A_CNXN,
|
|
18
|
+
A_OKAY,
|
|
19
|
+
A_OPEN,
|
|
20
|
+
A_STLS,
|
|
21
|
+
A_VERSION,
|
|
22
|
+
A_WRTE,
|
|
23
|
+
LOCAL_MAXDATA,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---- TransportAction (plan §4.2.3 A) ----
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Ignore:
|
|
32
|
+
reason: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class RejectOpen:
|
|
37
|
+
peer_local: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class AcceptOpen:
|
|
42
|
+
peer_local: int
|
|
43
|
+
service: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class AttachObserver:
|
|
48
|
+
entry: Any # StreamEntry
|
|
49
|
+
service: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class OpenBackend:
|
|
54
|
+
entry: Any
|
|
55
|
+
generation: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class SendPacket:
|
|
60
|
+
cmd: int
|
|
61
|
+
arg0: int
|
|
62
|
+
arg1: int
|
|
63
|
+
payload: bytes = b""
|
|
64
|
+
log_status: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ForwardToBackend:
|
|
69
|
+
entry: Any
|
|
70
|
+
payload: bytes
|
|
71
|
+
also_okay: bool = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class QueueFromBackend:
|
|
76
|
+
entry: Any
|
|
77
|
+
data: bytes
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class SetCanSend:
|
|
82
|
+
entry: Any
|
|
83
|
+
value: bool = True
|
|
84
|
+
flush: bool = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class CloseStream:
|
|
89
|
+
entry: Any
|
|
90
|
+
send_clse: bool = True
|
|
91
|
+
close_arg0: int | None = None
|
|
92
|
+
close_arg1: int | None = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class MarkTransportTerminal:
|
|
97
|
+
reason: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class ResetStreamsForCnxn:
|
|
102
|
+
"""Close all existing streams without sending CLSE (CNXN reset)."""
|
|
103
|
+
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class NegotiateCodec:
|
|
109
|
+
version: int
|
|
110
|
+
peer_maxdata: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class LogEvent:
|
|
115
|
+
message: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Session events from workers (plan §4.2.3 B)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class BackendReady:
|
|
123
|
+
entry: Any
|
|
124
|
+
generation: int
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class BackendFailed:
|
|
129
|
+
entry: Any
|
|
130
|
+
generation: int
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AdbdSession:
|
|
134
|
+
"""Pure-ish decision engine for one front transport connection.
|
|
135
|
+
|
|
136
|
+
Holds CNXN flag, banner config, and stream_table reference. Mutates stream
|
|
137
|
+
table state under caller-held conventions; never performs socket I/O.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
stream_table,
|
|
143
|
+
*,
|
|
144
|
+
banner_features: str,
|
|
145
|
+
banner_type: str,
|
|
146
|
+
banner_product,
|
|
147
|
+
banner_model,
|
|
148
|
+
banner_device,
|
|
149
|
+
peer_maxdata: int,
|
|
150
|
+
):
|
|
151
|
+
self.stream_table = stream_table
|
|
152
|
+
self.banner_features = banner_features
|
|
153
|
+
self.banner_type = banner_type
|
|
154
|
+
self.banner_product = banner_product
|
|
155
|
+
self.banner_model = banner_model
|
|
156
|
+
self.banner_device = banner_device
|
|
157
|
+
self.peer_maxdata = peer_maxdata
|
|
158
|
+
self._cnxn_done = False
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def cnxn_done(self) -> bool:
|
|
162
|
+
return self._cnxn_done
|
|
163
|
+
|
|
164
|
+
def receive(self, packet) -> list:
|
|
165
|
+
"""Map one transport packet to a list of TransportAction."""
|
|
166
|
+
cmd, arg0, arg1, payload = packet
|
|
167
|
+
if cmd == A_CNXN:
|
|
168
|
+
return self._on_cnxn(arg0, arg1, payload)
|
|
169
|
+
if cmd == A_OPEN:
|
|
170
|
+
return self._on_open(arg0, arg1, payload)
|
|
171
|
+
if cmd == A_OKAY:
|
|
172
|
+
return self._on_okay(arg0, arg1, payload)
|
|
173
|
+
if cmd == A_WRTE:
|
|
174
|
+
return self._on_wrte(arg0, arg1, payload)
|
|
175
|
+
if cmd == A_CLSE:
|
|
176
|
+
return self._on_clse(arg0, arg1, payload)
|
|
177
|
+
if cmd == A_STLS:
|
|
178
|
+
return [MarkTransportTerminal("STLS/TLS 首版不支持")]
|
|
179
|
+
if cmd == A_AUTH:
|
|
180
|
+
return [MarkTransportTerminal("AUTH 配对模拟首版不支持")]
|
|
181
|
+
return [LogEvent(f"[!] 未知传输命令 0x{cmd:08x}")]
|
|
182
|
+
|
|
183
|
+
def on_session_event(self, event) -> list:
|
|
184
|
+
"""Handle BackendReady / BackendFailed from worker threads."""
|
|
185
|
+
if isinstance(event, BackendReady):
|
|
186
|
+
return self._on_backend_ready(event.entry, event.generation)
|
|
187
|
+
if isinstance(event, BackendFailed):
|
|
188
|
+
return self._on_backend_failed(event.entry, event.generation)
|
|
189
|
+
return [Ignore(reason=f"unknown event {type(event)!r}")]
|
|
190
|
+
|
|
191
|
+
def _on_cnxn(self, arg0, arg1, payload) -> list:
|
|
192
|
+
actions: list = [ResetStreamsForCnxn()]
|
|
193
|
+
self._cnxn_done = False
|
|
194
|
+
self.peer_maxdata = arg1 or self.peer_maxdata
|
|
195
|
+
parts = payload.split(b"::", 1)
|
|
196
|
+
banner = parts[1].decode("utf-8", errors="replace") if len(parts) > 1 else ""
|
|
197
|
+
actions.append(LogEvent("========"))
|
|
198
|
+
actions.append(
|
|
199
|
+
LogEvent(f"📥 [CNXN]: version=0x{arg0:08x} maxdata={self.peer_maxdata}")
|
|
200
|
+
)
|
|
201
|
+
if banner:
|
|
202
|
+
actions.append(LogEvent(f" banner: {banner}"))
|
|
203
|
+
actions.append(NegotiateCodec(arg0, self.peer_maxdata))
|
|
204
|
+
our_banner = build_cnxn_banner(
|
|
205
|
+
banner_type=self.banner_type,
|
|
206
|
+
features=self.banner_features,
|
|
207
|
+
product=self.banner_product,
|
|
208
|
+
model=self.banner_model,
|
|
209
|
+
device=self.banner_device,
|
|
210
|
+
)
|
|
211
|
+
actions.append(
|
|
212
|
+
SendPacket(
|
|
213
|
+
A_CNXN,
|
|
214
|
+
A_VERSION,
|
|
215
|
+
LOCAL_MAXDATA,
|
|
216
|
+
our_banner.encode("utf-8"),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
self._cnxn_done = True
|
|
220
|
+
return actions
|
|
221
|
+
|
|
222
|
+
def _on_open(self, arg0, arg1, payload) -> list:
|
|
223
|
+
if not self._cnxn_done:
|
|
224
|
+
return [
|
|
225
|
+
LogEvent("[!] OPEN 在 CNXN 握手前到达,忽略该包"),
|
|
226
|
+
Ignore(reason="open_before_cnxn"),
|
|
227
|
+
]
|
|
228
|
+
if arg0 == 0:
|
|
229
|
+
return [
|
|
230
|
+
LogEvent("[!] OPEN local ID 为 0,忽略该包"),
|
|
231
|
+
Ignore(reason="open_zero_local"),
|
|
232
|
+
]
|
|
233
|
+
advertised_features = set((self.banner_features or "").split(","))
|
|
234
|
+
if arg1 != 0 and "delayed_ack" not in advertised_features:
|
|
235
|
+
return [
|
|
236
|
+
LogEvent(
|
|
237
|
+
f"[!] OPEN send-buffer={arg1} 但未广告 delayed_ack,关闭该流"
|
|
238
|
+
),
|
|
239
|
+
RejectOpen(peer_local=arg0),
|
|
240
|
+
]
|
|
241
|
+
peer_local = arg0
|
|
242
|
+
service = payload.rstrip(b"\x00").decode("utf-8", errors="ignore")
|
|
243
|
+
entry = self.stream_table.allocate(peer_local, service)
|
|
244
|
+
entry.state = "opening"
|
|
245
|
+
entry.generation = entry.local_id
|
|
246
|
+
return [
|
|
247
|
+
AcceptOpen(peer_local=peer_local, service=service),
|
|
248
|
+
AttachObserver(entry=entry, service=service),
|
|
249
|
+
OpenBackend(entry=entry, generation=entry.generation),
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
def _on_okay(self, arg0, arg1, payload) -> list:
|
|
253
|
+
entry = self.stream_table.find_by_pair(arg1, arg0)
|
|
254
|
+
if entry is None:
|
|
255
|
+
return [Ignore(reason="okay_unknown_pair")]
|
|
256
|
+
return [SetCanSend(entry=entry, value=True, flush=True)]
|
|
257
|
+
|
|
258
|
+
def _on_wrte(self, arg0, arg1, payload) -> list:
|
|
259
|
+
entry = self.stream_table.find_by_pair(arg1, arg0)
|
|
260
|
+
if entry is None:
|
|
261
|
+
return [Ignore(reason="wrte_unknown_pair")]
|
|
262
|
+
return [ForwardToBackend(entry=entry, payload=payload, also_okay=True)]
|
|
263
|
+
|
|
264
|
+
def _on_clse(self, arg0, arg1, payload) -> list:
|
|
265
|
+
entry = self.stream_table.find_by_pair(arg1, arg0)
|
|
266
|
+
if entry is None and arg0 == 0 and arg1:
|
|
267
|
+
candidate = self.stream_table.find_by_remote(arg1)
|
|
268
|
+
if candidate is not None and candidate.state == "opening":
|
|
269
|
+
entry = candidate
|
|
270
|
+
if entry is None:
|
|
271
|
+
return [Ignore(reason="clse_unknown")]
|
|
272
|
+
return [CloseStream(entry=entry)]
|
|
273
|
+
|
|
274
|
+
def _on_backend_ready(self, entry, generation) -> list:
|
|
275
|
+
current = self.stream_table.get(entry.local_id)
|
|
276
|
+
with entry.lock:
|
|
277
|
+
if (
|
|
278
|
+
current is not entry
|
|
279
|
+
or entry.generation != generation
|
|
280
|
+
or entry.closed
|
|
281
|
+
or entry.state != "opening"
|
|
282
|
+
):
|
|
283
|
+
return [Ignore(reason="backend_ready_stale")]
|
|
284
|
+
entry.state = "ready"
|
|
285
|
+
entry.can_send = True
|
|
286
|
+
# OKAY first; connection flushes after send (SetCanSend flush=True)
|
|
287
|
+
return [
|
|
288
|
+
SendPacket(A_OKAY, entry.local_id, entry.remote_id),
|
|
289
|
+
SetCanSend(entry=entry, value=True, flush=True),
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
def _on_backend_failed(self, entry, generation) -> list:
|
|
293
|
+
current = self.stream_table.get(entry.local_id)
|
|
294
|
+
with entry.lock:
|
|
295
|
+
if current is not entry or entry.generation != generation:
|
|
296
|
+
return [Ignore(reason="backend_failed_stale")]
|
|
297
|
+
return [
|
|
298
|
+
CloseStream(
|
|
299
|
+
entry=entry,
|
|
300
|
+
send_clse=True,
|
|
301
|
+
close_arg0=0,
|
|
302
|
+
close_arg1=entry.remote_id,
|
|
303
|
+
)
|
|
304
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Device transport stream registry (Track 2a).
|
|
2
|
+
|
|
3
|
+
Dual-lock semantics (must preserve):
|
|
4
|
+
- ``lock`` (registry): streams dict, next local id allocation, find/pop
|
|
5
|
+
- ``entry.lock`` / ``entry.flow_cv``: per-stream queue, can_send, closed, flush wait
|
|
6
|
+
|
|
7
|
+
No TransportAction; no flush algorithm.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from collections import deque
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StreamEntry:
|
|
17
|
+
"""per-stream 状态:本端/对端 id、service、后端、v1 停等流控标志、幂等 closed、
|
|
18
|
+
per-stream StreamBuffer(隔离 shell/sync/install 解析,防多 stream 串流)。"""
|
|
19
|
+
|
|
20
|
+
__slots__ = (
|
|
21
|
+
"local_id",
|
|
22
|
+
"remote_id",
|
|
23
|
+
"service",
|
|
24
|
+
"back",
|
|
25
|
+
"can_send",
|
|
26
|
+
"back_eof",
|
|
27
|
+
"closed",
|
|
28
|
+
"send_queue",
|
|
29
|
+
"queued_bytes",
|
|
30
|
+
"lock",
|
|
31
|
+
"flow_cv",
|
|
32
|
+
"sb",
|
|
33
|
+
"state",
|
|
34
|
+
"generation",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def __init__(self, local_id, remote_id, service):
|
|
38
|
+
self.local_id = local_id
|
|
39
|
+
self.remote_id = remote_id
|
|
40
|
+
self.service = service
|
|
41
|
+
self.back = None
|
|
42
|
+
self.can_send = False
|
|
43
|
+
self.back_eof = False
|
|
44
|
+
self.closed = False
|
|
45
|
+
self.send_queue = deque()
|
|
46
|
+
self.queued_bytes = 0
|
|
47
|
+
self.lock = threading.Lock()
|
|
48
|
+
self.flow_cv = threading.Condition(self.lock)
|
|
49
|
+
self.sb = None
|
|
50
|
+
self.state = "ready"
|
|
51
|
+
self.generation = 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Backward-compatible alias for tests that import ``_StreamEntry``.
|
|
55
|
+
_StreamEntry = StreamEntry
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StreamTable:
|
|
59
|
+
"""Registry of open streams for one TransportConnection.
|
|
60
|
+
|
|
61
|
+
Owns the registry lock and stream map. Entry-level locks remain on StreamEntry.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self._lock = threading.Lock()
|
|
66
|
+
self._streams: dict = {}
|
|
67
|
+
self._next_local_id = 1
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def lock(self):
|
|
71
|
+
"""Registry lock (same role as TransportConnection._streams_lock)."""
|
|
72
|
+
return self._lock
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def streams(self) -> dict:
|
|
76
|
+
return self._streams
|
|
77
|
+
|
|
78
|
+
def allocate(self, remote_id, service) -> StreamEntry:
|
|
79
|
+
"""Allocate local id, create StreamEntry, insert into map. Holds registry lock."""
|
|
80
|
+
with self._lock:
|
|
81
|
+
local_id = self._next_local_id
|
|
82
|
+
self._next_local_id += 1
|
|
83
|
+
entry = StreamEntry(local_id, remote_id, service)
|
|
84
|
+
self._streams[local_id] = entry
|
|
85
|
+
return entry
|
|
86
|
+
|
|
87
|
+
def get(self, local_id):
|
|
88
|
+
with self._lock:
|
|
89
|
+
return self._streams.get(local_id)
|
|
90
|
+
|
|
91
|
+
def find_by_local(self, local_id):
|
|
92
|
+
return self.get(local_id)
|
|
93
|
+
|
|
94
|
+
def find_by_remote(self, remote_id):
|
|
95
|
+
with self._lock:
|
|
96
|
+
for e in self._streams.values():
|
|
97
|
+
if e.remote_id == remote_id:
|
|
98
|
+
return e
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def find_by_pair(self, local_id, remote_id):
|
|
102
|
+
if not local_id or not remote_id:
|
|
103
|
+
return None
|
|
104
|
+
with self._lock:
|
|
105
|
+
entry = self._streams.get(local_id)
|
|
106
|
+
if entry is not None and entry.remote_id == remote_id:
|
|
107
|
+
return entry
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def remove_if_same(self, entry: StreamEntry) -> bool:
|
|
111
|
+
"""Pop entry from registry if map still points at this object."""
|
|
112
|
+
with self._lock:
|
|
113
|
+
if self._streams.get(entry.local_id) is entry:
|
|
114
|
+
self._streams.pop(entry.local_id, None)
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def snapshot(self) -> list:
|
|
119
|
+
"""List current entries (for CNXN reset / finish)."""
|
|
120
|
+
with self._lock:
|
|
121
|
+
return list(self._streams.values())
|
|
122
|
+
|
|
123
|
+
def clear_map_locked(self):
|
|
124
|
+
"""Caller already holds registry lock; clear map without releasing."""
|
|
125
|
+
self._streams.clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""只读的 shell、sync 和 install 流观察器。"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shallow observer events for StreamBuffer routing.
|
|
2
|
+
|
|
3
|
+
O-track (phase2): only ClientBytes/ServerBytes. Activate* and deep
|
|
4
|
+
session-internal event SMs are follow-ups — do not expand here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ClientBytes:
|
|
14
|
+
data: bytes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ServerBytes:
|
|
19
|
+
data: bytes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from adbproxy.config import MAX_SYNC_FILE
|
|
5
|
+
from adbproxy.runtime.capture import open_unique_capture
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InstallStreamCapture:
|
|
9
|
+
"""adb install 流式 raw 字节捕获:apk 内容只落盘,绝不进控制台/日志。
|
|
10
|
+
|
|
11
|
+
命中 abb_exec:package install(-write)? -S <size> 后激活,
|
|
12
|
+
按 -S size 计数捕获 client→server 的 raw apk 流;server→client 只回 Success/Failure
|
|
13
|
+
短文本,不在此落盘(交碎片流走可读路径)。单文件上限复用 MAX_SYNC_FILE。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, logger, files_dir, capture_enabled=True):
|
|
17
|
+
self.logger = logger
|
|
18
|
+
self.files_dir = files_dir
|
|
19
|
+
self.capture_enabled = capture_enabled
|
|
20
|
+
self.lock = threading.Lock()
|
|
21
|
+
self.remaining = 0 # 待捕获字节数(按 -S size 递减)
|
|
22
|
+
self.total_size = 0
|
|
23
|
+
self.remote_label = None # service 串或其中的 name,用于事件与文件命名
|
|
24
|
+
self.accumulated = 0 # 已落盘字节数
|
|
25
|
+
self.file_fp = None # 落盘文件句柄("wb" 覆写,时间戳防重名)
|
|
26
|
+
self.capture_path = None
|
|
27
|
+
self.capture_overflow = False # 超 MAX_SYNC_FILE 后停止写盘
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def active(self):
|
|
31
|
+
return self.remaining > 0
|
|
32
|
+
|
|
33
|
+
def activate(self, total_size, remote_label):
|
|
34
|
+
"""由 Connection 在 client 首帧命中 install 流式服务时调用。开 capture 文件并置计数。"""
|
|
35
|
+
with self.lock:
|
|
36
|
+
self.total_size = total_size
|
|
37
|
+
self.remaining = total_size
|
|
38
|
+
self.accumulated = 0
|
|
39
|
+
self.capture_overflow = False
|
|
40
|
+
self.remote_label = remote_label
|
|
41
|
+
self._open_capture()
|
|
42
|
+
|
|
43
|
+
def on_client(self, data):
|
|
44
|
+
"""捕获 client→server raw apk 字节。返回 take(本次消耗字节数),
|
|
45
|
+
data[take:] 由调用方交回可读路径。apk 字节绝不进 StreamBuffer.c_buf。"""
|
|
46
|
+
if not data or self.remaining <= 0:
|
|
47
|
+
return 0
|
|
48
|
+
with self.lock:
|
|
49
|
+
if self.remaining <= 0:
|
|
50
|
+
return 0
|
|
51
|
+
take = min(self.remaining, len(data))
|
|
52
|
+
chunk = bytes(data[:take])
|
|
53
|
+
self._write_capture(chunk)
|
|
54
|
+
self.accumulated += len(chunk)
|
|
55
|
+
self.remaining -= take
|
|
56
|
+
if self.remaining <= 0:
|
|
57
|
+
cap = (
|
|
58
|
+
self.capture_path
|
|
59
|
+
if (self.capture_enabled and self.capture_path)
|
|
60
|
+
else None
|
|
61
|
+
)
|
|
62
|
+
if cap:
|
|
63
|
+
# 路径=创建路径(无 rename),唯一 captured
|
|
64
|
+
self.logger.event(
|
|
65
|
+
f"[install captured]: {cap} size={self.total_size}"
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
# --no-capture:无 captured,但仍打 size 摘要不崩
|
|
69
|
+
self.logger.event(f"[install done]: size={self.total_size}")
|
|
70
|
+
self._close_capture()
|
|
71
|
+
return take
|
|
72
|
+
|
|
73
|
+
def on_server(self, data):
|
|
74
|
+
"""install 上下文 server 只回 Success/Failure 短文本,不在此落盘。
|
|
75
|
+
返回 False 让 StreamBuffer 走可读路径(不拦 server)。"""
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _open_capture(self):
|
|
79
|
+
# 统一命名:install-apk-conn{N}-{size}B-{time_ns}-{seq}.apk
|
|
80
|
+
# activate 时 -S size 已知,一次命名无 rename;唯一性由 capture token 保证
|
|
81
|
+
if not self.capture_enabled:
|
|
82
|
+
self.capture_path = None
|
|
83
|
+
self.file_fp = None
|
|
84
|
+
return
|
|
85
|
+
try:
|
|
86
|
+
prefix = f"install-apk-conn{self.logger.conn_id}-{self.total_size}B"
|
|
87
|
+
self.capture_path, self.file_fp = open_unique_capture(
|
|
88
|
+
self.files_dir, prefix, ".apk"
|
|
89
|
+
)
|
|
90
|
+
except IOError as e:
|
|
91
|
+
self.logger.event(f"[!] install capture open failed: {e}")
|
|
92
|
+
self.capture_path = None
|
|
93
|
+
self.file_fp = None
|
|
94
|
+
self.capture_overflow = True
|
|
95
|
+
|
|
96
|
+
def _write_capture(self, payload):
|
|
97
|
+
"""流式写盘:每段 payload 直接 write 到已 open file_fp。IOError 内化不冒泡。"""
|
|
98
|
+
if not self.file_fp or self.capture_overflow:
|
|
99
|
+
return
|
|
100
|
+
if self.accumulated + len(payload) > MAX_SYNC_FILE:
|
|
101
|
+
self.logger.event(
|
|
102
|
+
f"[!] install file > {MAX_SYNC_FILE} bytes, stop capturing"
|
|
103
|
+
)
|
|
104
|
+
self._close_capture()
|
|
105
|
+
self.capture_overflow = True
|
|
106
|
+
return
|
|
107
|
+
try:
|
|
108
|
+
self.file_fp.write(payload)
|
|
109
|
+
except IOError as e:
|
|
110
|
+
self.logger.event(f"[!] install capture write failed: {e}")
|
|
111
|
+
self._close_capture()
|
|
112
|
+
self.capture_overflow = True
|
|
113
|
+
|
|
114
|
+
def _close_capture(self):
|
|
115
|
+
if self.file_fp:
|
|
116
|
+
try:
|
|
117
|
+
self.file_fp.close()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
self.file_fp = None
|
|
121
|
+
|
|
122
|
+
def close(self):
|
|
123
|
+
"""连接关闭时兜底关闭未收完的文件(保留部分落盘便于排查)。"""
|
|
124
|
+
with self.lock:
|
|
125
|
+
self._close_capture()
|
|
126
|
+
self.remaining = 0
|