@yume-chan/adb 0.0.22 → 0.0.23
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.
- package/CHANGELOG.json +18 -0
- package/CHANGELOG.md +10 -1
- package/esm/adb.d.ts +4 -1
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +9 -2
- package/esm/adb.js.map +1 -1
- package/esm/banner.d.ts.map +1 -1
- package/esm/banner.js +1 -0
- package/esm/banner.js.map +1 -1
- package/esm/commands/reverse.js +1 -1
- package/esm/commands/reverse.js.map +1 -1
- package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/shell.js +15 -69
- package/esm/commands/subprocess/protocols/shell.js.map +1 -1
- package/esm/commands/subprocess/utils.js +1 -1
- package/esm/commands/subprocess/utils.js.map +1 -1
- package/esm/commands/sync/sync.d.ts.map +1 -1
- package/esm/commands/sync/sync.js +5 -6
- package/esm/commands/sync/sync.js.map +1 -1
- package/esm/daemon/dispatcher.d.ts +7 -0
- package/esm/daemon/dispatcher.d.ts.map +1 -1
- package/esm/daemon/dispatcher.js +57 -12
- package/esm/daemon/dispatcher.js.map +1 -1
- package/esm/daemon/socket.d.ts +1 -1
- package/esm/daemon/socket.d.ts.map +1 -1
- package/esm/daemon/socket.js +36 -8
- package/esm/daemon/socket.js.map +1 -1
- package/esm/daemon/transport.d.ts +26 -2
- package/esm/daemon/transport.d.ts.map +1 -1
- package/esm/daemon/transport.js +53 -25
- package/esm/daemon/transport.js.map +1 -1
- package/esm/features.d.ts +2 -1
- package/esm/features.d.ts.map +1 -1
- package/esm/features.js +2 -1
- package/esm/features.js.map +1 -1
- package/esm/server/client.d.ts +2 -0
- package/esm/server/client.d.ts.map +1 -1
- package/esm/server/client.js +82 -50
- package/esm/server/client.js.map +1 -1
- package/esm/server/transport.d.ts +3 -0
- package/esm/server/transport.d.ts.map +1 -1
- package/esm/server/transport.js +26 -0
- package/esm/server/transport.js.map +1 -1
- package/package.json +13 -13
- package/src/adb.ts +15 -2
- package/src/banner.ts +1 -0
- package/src/commands/reverse.ts +2 -2
- package/src/commands/subprocess/protocols/shell.ts +22 -97
- package/src/commands/subprocess/utils.ts +1 -1
- package/src/commands/sync/sync.ts +5 -6
- package/src/daemon/dispatcher.ts +90 -14
- package/src/daemon/socket.ts +47 -16
- package/src/daemon/transport.ts +82 -24
- package/src/features.ts +2 -1
- package/src/server/client.ts +87 -51
- package/src/server/transport.ts +28 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/daemon/socket.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PromiseResolver } from "@yume-chan/async";
|
|
2
2
|
import type { Disposable } from "@yume-chan/event";
|
|
3
3
|
import type {
|
|
4
|
+
AbortSignal,
|
|
4
5
|
Consumable,
|
|
5
6
|
PushReadableStreamController,
|
|
6
7
|
ReadableStream,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
} from "@yume-chan/stream-extra";
|
|
14
15
|
|
|
15
16
|
import type { AdbSocket } from "../adb.js";
|
|
16
|
-
import { raceSignal } from "../server/index.js";
|
|
17
17
|
|
|
18
18
|
import type { AdbPacketDispatcher } from "./dispatcher.js";
|
|
19
19
|
import { AdbCommand } from "./packet.js";
|
|
@@ -49,7 +49,6 @@ export class AdbDaemonSocketController
|
|
|
49
49
|
return this.#readable;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
#writePromise: PromiseResolver<void> | undefined;
|
|
53
52
|
#writableController!: WritableStreamDefaultController;
|
|
54
53
|
readonly writable: WritableStream<Consumable<Uint8Array>>;
|
|
55
54
|
|
|
@@ -65,6 +64,17 @@ export class AdbDaemonSocketController
|
|
|
65
64
|
return this.#socket;
|
|
66
65
|
}
|
|
67
66
|
|
|
67
|
+
#availableWriteBytesChanged: PromiseResolver<void> | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* When delayed ack is disabled, can be `Infinity` if the socket is ready to write.
|
|
70
|
+
* Exactly one packet can be written no matter how large it is. Or `-1` if the socket
|
|
71
|
+
* is waiting for ack.
|
|
72
|
+
*
|
|
73
|
+
* When delayed ack is enabled, a non-negative finite number indicates the number of
|
|
74
|
+
* bytes that can be written to the socket before receiving an ack.
|
|
75
|
+
*/
|
|
76
|
+
#availableWriteBytes = 0;
|
|
77
|
+
|
|
68
78
|
constructor(options: AdbDaemonSocketConstructionOptions) {
|
|
69
79
|
this.#dispatcher = options.dispatcher;
|
|
70
80
|
this.localId = options.localId;
|
|
@@ -88,18 +98,8 @@ export class AdbDaemonSocketController
|
|
|
88
98
|
start < size;
|
|
89
99
|
start = end, end += chunkSize
|
|
90
100
|
) {
|
|
91
|
-
|
|
92
|
-
await this.#
|
|
93
|
-
AdbCommand.Write,
|
|
94
|
-
this.localId,
|
|
95
|
-
this.remoteId,
|
|
96
|
-
data.subarray(start, end),
|
|
97
|
-
);
|
|
98
|
-
// Wait for ack packet
|
|
99
|
-
await raceSignal(
|
|
100
|
-
() => this.#writePromise!.promise,
|
|
101
|
-
controller.signal,
|
|
102
|
-
);
|
|
101
|
+
const chunk = data.subarray(start, end);
|
|
102
|
+
await this.#writeChunk(chunk, controller.signal);
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
105
|
});
|
|
@@ -107,6 +107,34 @@ export class AdbDaemonSocketController
|
|
|
107
107
|
this.#socket = new AdbDaemonSocket(this);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
async #writeChunk(data: Uint8Array, signal: AbortSignal) {
|
|
111
|
+
const length = data.byteLength;
|
|
112
|
+
while (this.#availableWriteBytes < length) {
|
|
113
|
+
// Only one lock is required because Web Streams API guarantees
|
|
114
|
+
// that `write` is not reentrant.
|
|
115
|
+
const resolver = new PromiseResolver<void>();
|
|
116
|
+
signal.addEventListener("abort", () => {
|
|
117
|
+
resolver.reject(signal.reason);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.#availableWriteBytesChanged = resolver;
|
|
121
|
+
await resolver.promise;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.#availableWriteBytes === Infinity) {
|
|
125
|
+
this.#availableWriteBytes = -1;
|
|
126
|
+
} else {
|
|
127
|
+
this.#availableWriteBytes -= length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await this.#dispatcher.sendPacket(
|
|
131
|
+
AdbCommand.Write,
|
|
132
|
+
this.localId,
|
|
133
|
+
this.remoteId,
|
|
134
|
+
data,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
async enqueue(data: Uint8Array) {
|
|
111
139
|
// Consumers can `cancel` the `readable` if they are not interested in future data.
|
|
112
140
|
// Throw away the data if that happens.
|
|
@@ -124,8 +152,9 @@ export class AdbDaemonSocketController
|
|
|
124
152
|
}
|
|
125
153
|
}
|
|
126
154
|
|
|
127
|
-
ack() {
|
|
128
|
-
this.#
|
|
155
|
+
public ack(bytes: number) {
|
|
156
|
+
this.#availableWriteBytes += bytes;
|
|
157
|
+
this.#availableWriteBytesChanged?.resolve();
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
async close(): Promise<void> {
|
|
@@ -134,6 +163,8 @@ export class AdbDaemonSocketController
|
|
|
134
163
|
}
|
|
135
164
|
this.#closed = true;
|
|
136
165
|
|
|
166
|
+
this.#availableWriteBytesChanged?.reject(new Error("Socket closed"));
|
|
167
|
+
|
|
137
168
|
try {
|
|
138
169
|
this.#writableController.error(new Error("Socket closed"));
|
|
139
170
|
} catch {
|
package/src/daemon/transport.ts
CHANGED
|
@@ -26,6 +26,30 @@ import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
|
|
26
26
|
import { AdbCommand, calculateChecksum } from "./packet.js";
|
|
27
27
|
|
|
28
28
|
export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001;
|
|
29
|
+
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
|
30
|
+
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
|
|
31
|
+
export const ADB_DAEMON_DEFAULT_FEATURES = [
|
|
32
|
+
AdbFeature.ShellV2,
|
|
33
|
+
AdbFeature.Cmd,
|
|
34
|
+
AdbFeature.StatV2,
|
|
35
|
+
AdbFeature.ListV2,
|
|
36
|
+
AdbFeature.FixedPushMkdir,
|
|
37
|
+
"apex",
|
|
38
|
+
AdbFeature.Abb,
|
|
39
|
+
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
|
40
|
+
// No special handling required.
|
|
41
|
+
"fixed_push_symlink_timestamp",
|
|
42
|
+
AdbFeature.AbbExec,
|
|
43
|
+
"remount_shell",
|
|
44
|
+
"track_app",
|
|
45
|
+
AdbFeature.SendReceiveV2,
|
|
46
|
+
"sendrecv_v2_brotli",
|
|
47
|
+
"sendrecv_v2_lz4",
|
|
48
|
+
"sendrecv_v2_zstd",
|
|
49
|
+
"sendrecv_v2_dry_run_send",
|
|
50
|
+
AdbFeature.DelayedAck,
|
|
51
|
+
] as AdbFeature[];
|
|
52
|
+
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
|
|
29
53
|
|
|
30
54
|
export type AdbDaemonConnection = ReadableWritablePair<
|
|
31
55
|
AdbPacketData,
|
|
@@ -37,6 +61,16 @@ interface AdbDaemonAuthenticationOptions {
|
|
|
37
61
|
connection: AdbDaemonConnection;
|
|
38
62
|
credentialStore: AdbCredentialStore;
|
|
39
63
|
authenticators?: AdbAuthenticator[];
|
|
64
|
+
features?: readonly AdbFeature[];
|
|
65
|
+
/**
|
|
66
|
+
* The number of bytes the device can send before receiving an ack packet.
|
|
67
|
+
*
|
|
68
|
+
* Set to 0 or any negative value to disable delayed ack in handshake.
|
|
69
|
+
* Otherwise the value must be in the range of unsigned 32-bit integer.
|
|
70
|
+
*
|
|
71
|
+
* Delayed ack requires Android 14, this option is ignored on older versions.
|
|
72
|
+
*/
|
|
73
|
+
initialDelayedAckBytes?: number;
|
|
40
74
|
/**
|
|
41
75
|
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
|
|
42
76
|
*/
|
|
@@ -50,6 +84,16 @@ interface AdbDaemonSocketConnectorConstructionOptions {
|
|
|
50
84
|
version: number;
|
|
51
85
|
maxPayloadSize: number;
|
|
52
86
|
banner: string;
|
|
87
|
+
features?: readonly AdbFeature[];
|
|
88
|
+
/**
|
|
89
|
+
* The number of bytes the device can send before receiving an ack packet.
|
|
90
|
+
*
|
|
91
|
+
* Set to 0 or any negative value to disable delayed ack in handshake.
|
|
92
|
+
* Otherwise the value must be in the range of unsigned 32-bit integer.
|
|
93
|
+
*
|
|
94
|
+
* Delayed ack requires Android 14, this option is ignored on older versions.
|
|
95
|
+
*/
|
|
96
|
+
initialDelayedAckBytes?: number;
|
|
53
97
|
/**
|
|
54
98
|
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
|
|
55
99
|
*/
|
|
@@ -71,6 +115,8 @@ export class AdbDaemonTransport implements AdbTransport {
|
|
|
71
115
|
connection,
|
|
72
116
|
credentialStore,
|
|
73
117
|
authenticators = ADB_DEFAULT_AUTHENTICATORS,
|
|
118
|
+
features = ADB_DAEMON_DEFAULT_FEATURES,
|
|
119
|
+
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
|
74
120
|
...options
|
|
75
121
|
}: AdbDaemonAuthenticationOptions): Promise<AdbDaemonTransport> {
|
|
76
122
|
// Initially, set to highest-supported version and payload size.
|
|
@@ -144,38 +190,25 @@ export class AdbDaemonTransport implements AdbTransport {
|
|
|
144
190
|
await ConsumableWritableStream.write(writer, init as AdbPacketInit);
|
|
145
191
|
}
|
|
146
192
|
|
|
193
|
+
const actualFeatures = features.slice();
|
|
194
|
+
if (initialDelayedAckBytes <= 0) {
|
|
195
|
+
const index = features.indexOf(AdbFeature.DelayedAck);
|
|
196
|
+
if (index !== -1) {
|
|
197
|
+
actualFeatures.splice(index, 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
147
201
|
let banner: string;
|
|
148
202
|
try {
|
|
149
|
-
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
|
150
|
-
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
|
|
151
|
-
const features = [
|
|
152
|
-
AdbFeature.ShellV2,
|
|
153
|
-
AdbFeature.Cmd,
|
|
154
|
-
AdbFeature.StatV2,
|
|
155
|
-
AdbFeature.ListV2,
|
|
156
|
-
AdbFeature.FixedPushMkdir,
|
|
157
|
-
"apex",
|
|
158
|
-
AdbFeature.Abb,
|
|
159
|
-
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
|
160
|
-
// No special handling required.
|
|
161
|
-
"fixed_push_symlink_timestamp",
|
|
162
|
-
AdbFeature.AbbExec,
|
|
163
|
-
"remount_shell",
|
|
164
|
-
"track_app",
|
|
165
|
-
AdbFeature.SendReceiveV2,
|
|
166
|
-
"sendrecv_v2_brotli",
|
|
167
|
-
"sendrecv_v2_lz4",
|
|
168
|
-
"sendrecv_v2_zstd",
|
|
169
|
-
"sendrecv_v2_dry_run_send",
|
|
170
|
-
].join(",");
|
|
171
|
-
|
|
172
203
|
await sendPacket({
|
|
173
204
|
command: AdbCommand.Connect,
|
|
174
205
|
arg0: version,
|
|
175
206
|
arg1: maxPayloadSize,
|
|
176
207
|
// The terminating `;` is required in formal definition
|
|
177
208
|
// But ADB daemon (all versions) can still work without it
|
|
178
|
-
payload: encodeUtf8(
|
|
209
|
+
payload: encodeUtf8(
|
|
210
|
+
`host::features=${actualFeatures.join(",")}`,
|
|
211
|
+
),
|
|
179
212
|
});
|
|
180
213
|
|
|
181
214
|
banner = await resolver.promise;
|
|
@@ -195,6 +228,8 @@ export class AdbDaemonTransport implements AdbTransport {
|
|
|
195
228
|
version,
|
|
196
229
|
maxPayloadSize,
|
|
197
230
|
banner,
|
|
231
|
+
features: actualFeatures,
|
|
232
|
+
initialDelayedAckBytes,
|
|
198
233
|
...options,
|
|
199
234
|
});
|
|
200
235
|
}
|
|
@@ -229,16 +264,38 @@ export class AdbDaemonTransport implements AdbTransport {
|
|
|
229
264
|
return this.#dispatcher.disconnected;
|
|
230
265
|
}
|
|
231
266
|
|
|
267
|
+
#clientFeatures: readonly AdbFeature[];
|
|
268
|
+
get clientFeatures() {
|
|
269
|
+
return this.#clientFeatures;
|
|
270
|
+
}
|
|
271
|
+
|
|
232
272
|
constructor({
|
|
233
273
|
serial,
|
|
234
274
|
connection,
|
|
235
275
|
version,
|
|
236
276
|
banner,
|
|
277
|
+
features = ADB_DAEMON_DEFAULT_FEATURES,
|
|
278
|
+
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
|
237
279
|
...options
|
|
238
280
|
}: AdbDaemonSocketConnectorConstructionOptions) {
|
|
239
281
|
this.#serial = serial;
|
|
240
282
|
this.#connection = connection;
|
|
241
283
|
this.#banner = AdbBanner.parse(banner);
|
|
284
|
+
this.#clientFeatures = features;
|
|
285
|
+
|
|
286
|
+
if (features.includes(AdbFeature.DelayedAck)) {
|
|
287
|
+
if (initialDelayedAckBytes <= 0) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
"`initialDelayedAckBytes` must be greater than 0 when DelayedAck feature is enabled.",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!this.#banner.features.includes(AdbFeature.DelayedAck)) {
|
|
294
|
+
initialDelayedAckBytes = 0;
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
initialDelayedAckBytes = 0;
|
|
298
|
+
}
|
|
242
299
|
|
|
243
300
|
let calculateChecksum: boolean;
|
|
244
301
|
let appendNullToServiceString: boolean;
|
|
@@ -253,6 +310,7 @@ export class AdbDaemonTransport implements AdbTransport {
|
|
|
253
310
|
this.#dispatcher = new AdbPacketDispatcher(connection, {
|
|
254
311
|
calculateChecksum,
|
|
255
312
|
appendNullToServiceString,
|
|
313
|
+
initialDelayedAckBytes,
|
|
256
314
|
...options,
|
|
257
315
|
});
|
|
258
316
|
|
package/src/features.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The order follows
|
|
2
|
-
// https://android.
|
|
2
|
+
// https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/transport.cpp;l=77;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
|
|
3
3
|
export enum AdbFeature {
|
|
4
4
|
ShellV2 = "shell_v2",
|
|
5
5
|
Cmd = "cmd",
|
|
@@ -9,4 +9,5 @@ export enum AdbFeature {
|
|
|
9
9
|
Abb = "abb",
|
|
10
10
|
AbbExec = "abb_exec",
|
|
11
11
|
SendReceiveV2 = "sendrecv_v2",
|
|
12
|
+
DelayedAck = "delayed_ack",
|
|
12
13
|
}
|
package/src/server/client.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
} from "@yume-chan/struct";
|
|
19
19
|
import {
|
|
20
20
|
BigIntFieldType,
|
|
21
|
+
EMPTY_UINT8_ARRAY,
|
|
21
22
|
SyncPromise,
|
|
22
23
|
decodeUtf8,
|
|
23
24
|
encodeUtf8,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
|
27
28
|
import { AdbBanner } from "../banner.js";
|
|
28
29
|
import type { AdbFeature } from "../features.js";
|
|
29
|
-
import { NOOP, hexToNumber, numberToHex } from "../utils/index.js";
|
|
30
|
+
import { NOOP, hexToNumber, numberToHex, unreachable } from "../utils/index.js";
|
|
30
31
|
|
|
31
32
|
import { AdbServerTransport } from "./transport.js";
|
|
32
33
|
|
|
@@ -92,7 +93,11 @@ export class AdbServerClient {
|
|
|
92
93
|
return SyncPromise.try(() => stream.readExactly(4))
|
|
93
94
|
.then((buffer) => {
|
|
94
95
|
const length = hexToNumber(buffer);
|
|
95
|
-
|
|
96
|
+
if (length === 0) {
|
|
97
|
+
return EMPTY_UINT8_ARRAY;
|
|
98
|
+
} else {
|
|
99
|
+
return stream.readExactly(length);
|
|
100
|
+
}
|
|
96
101
|
})
|
|
97
102
|
.then((valueBuffer) => {
|
|
98
103
|
return decodeUtf8(valueBuffer);
|
|
@@ -209,63 +214,91 @@ export class AdbServerClient {
|
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
parseDeviceList(value: string): AdbServerDevice[] {
|
|
218
|
+
const devices: AdbServerDevice[] = [];
|
|
219
|
+
for (const line of value.split("\n")) {
|
|
220
|
+
if (!line) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const parts = line.split(" ").filter(Boolean);
|
|
225
|
+
const serial = parts[0]!;
|
|
226
|
+
const status = parts[1]!;
|
|
227
|
+
if (status !== "device") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let product: string | undefined;
|
|
232
|
+
let model: string | undefined;
|
|
233
|
+
let device: string | undefined;
|
|
234
|
+
let transportId: bigint | undefined;
|
|
235
|
+
for (let i = 2; i < parts.length; i += 1) {
|
|
236
|
+
const [key, value] = parts[i]!.split(":");
|
|
237
|
+
switch (key) {
|
|
238
|
+
case "product":
|
|
239
|
+
product = value;
|
|
240
|
+
break;
|
|
241
|
+
case "model":
|
|
242
|
+
model = value;
|
|
243
|
+
break;
|
|
244
|
+
case "device":
|
|
245
|
+
device = value;
|
|
246
|
+
break;
|
|
247
|
+
case "transport_id":
|
|
248
|
+
transportId = BigInt(value!);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!transportId) {
|
|
253
|
+
throw new Error(`No transport id for device ${serial}`);
|
|
254
|
+
}
|
|
255
|
+
devices.push({
|
|
256
|
+
serial,
|
|
257
|
+
product,
|
|
258
|
+
model,
|
|
259
|
+
device,
|
|
260
|
+
transportId,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return devices;
|
|
264
|
+
}
|
|
265
|
+
|
|
212
266
|
async getDevices(): Promise<AdbServerDevice[]> {
|
|
213
267
|
const connection = await this.connect("host:devices-l");
|
|
214
268
|
const readable = new BufferedReadableStream(connection.readable);
|
|
215
269
|
try {
|
|
216
|
-
const devices: AdbServerDevice[] = [];
|
|
217
270
|
const response = await AdbServerClient.readString(readable);
|
|
218
|
-
|
|
219
|
-
if (!line) {
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const parts = line.split(" ").filter(Boolean);
|
|
224
|
-
const serial = parts[0]!;
|
|
225
|
-
const status = parts[1]!;
|
|
226
|
-
if (status !== "device") {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let product: string | undefined;
|
|
231
|
-
let model: string | undefined;
|
|
232
|
-
let device: string | undefined;
|
|
233
|
-
let transportId: bigint | undefined;
|
|
234
|
-
for (let i = 2; i < parts.length; i += 1) {
|
|
235
|
-
const [key, value] = parts[i]!.split(":");
|
|
236
|
-
switch (key) {
|
|
237
|
-
case "product":
|
|
238
|
-
product = value;
|
|
239
|
-
break;
|
|
240
|
-
case "model":
|
|
241
|
-
model = value;
|
|
242
|
-
break;
|
|
243
|
-
case "device":
|
|
244
|
-
device = value;
|
|
245
|
-
break;
|
|
246
|
-
case "transport_id":
|
|
247
|
-
transportId = BigInt(value!);
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (!transportId) {
|
|
252
|
-
throw new Error(`No transport id for device ${serial}`);
|
|
253
|
-
}
|
|
254
|
-
devices.push({
|
|
255
|
-
serial,
|
|
256
|
-
product,
|
|
257
|
-
model,
|
|
258
|
-
device,
|
|
259
|
-
transportId,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
return devices;
|
|
271
|
+
return this.parseDeviceList(response);
|
|
263
272
|
} finally {
|
|
264
273
|
connection.writable.close().catch(NOOP);
|
|
265
274
|
readable.cancel().catch(NOOP);
|
|
266
275
|
}
|
|
267
276
|
}
|
|
268
277
|
|
|
278
|
+
async trackDevices(
|
|
279
|
+
callback: (devices: AdbServerDevice[]) => void,
|
|
280
|
+
): Promise<() => void> {
|
|
281
|
+
const connection = await this.connect("host:track-devices-l");
|
|
282
|
+
const readable = new BufferedReadableStream(connection.readable);
|
|
283
|
+
let running = true;
|
|
284
|
+
(async () => {
|
|
285
|
+
try {
|
|
286
|
+
while (running) {
|
|
287
|
+
const response = await AdbServerClient.readString(readable);
|
|
288
|
+
const devices = this.parseDeviceList(response);
|
|
289
|
+
callback(devices);
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// ignore
|
|
293
|
+
}
|
|
294
|
+
})().catch(unreachable);
|
|
295
|
+
return () => {
|
|
296
|
+
running = false;
|
|
297
|
+
readable.cancel().catch(NOOP);
|
|
298
|
+
Promise.resolve(connection.close()).catch(NOOP);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
269
302
|
formatDeviceService(device: AdbServerDeviceSelector, command: string) {
|
|
270
303
|
if (!device) {
|
|
271
304
|
return `host:${command}`;
|
|
@@ -424,9 +457,12 @@ export class AdbServerClient {
|
|
|
424
457
|
`wait-for-${type}-${state}`,
|
|
425
458
|
);
|
|
426
459
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
await
|
|
460
|
+
const socket = await this.connect(service, options);
|
|
461
|
+
const readable = new BufferedReadableStream(socket.readable);
|
|
462
|
+
await AdbServerClient.readOkay(readable);
|
|
463
|
+
|
|
464
|
+
await readable.cancel();
|
|
465
|
+
await socket.close();
|
|
430
466
|
}
|
|
431
467
|
|
|
432
468
|
async createTransport(
|
package/src/server/transport.ts
CHANGED
|
@@ -8,9 +8,31 @@ import type {
|
|
|
8
8
|
AdbTransport,
|
|
9
9
|
} from "../adb.js";
|
|
10
10
|
import type { AdbBanner } from "../banner.js";
|
|
11
|
+
import { AdbFeature } from "../features.js";
|
|
11
12
|
|
|
12
13
|
import type { AdbServerClient } from "./client.js";
|
|
13
14
|
|
|
15
|
+
export const ADB_SERVER_DEFAULT_FEATURES = [
|
|
16
|
+
AdbFeature.ShellV2,
|
|
17
|
+
AdbFeature.Cmd,
|
|
18
|
+
AdbFeature.StatV2,
|
|
19
|
+
AdbFeature.ListV2,
|
|
20
|
+
AdbFeature.FixedPushMkdir,
|
|
21
|
+
"apex",
|
|
22
|
+
AdbFeature.Abb,
|
|
23
|
+
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
|
24
|
+
// No special handling required.
|
|
25
|
+
"fixed_push_symlink_timestamp",
|
|
26
|
+
AdbFeature.AbbExec,
|
|
27
|
+
"remount_shell",
|
|
28
|
+
"track_app",
|
|
29
|
+
AdbFeature.SendReceiveV2,
|
|
30
|
+
"sendrecv_v2_brotli",
|
|
31
|
+
"sendrecv_v2_lz4",
|
|
32
|
+
"sendrecv_v2_zstd",
|
|
33
|
+
"sendrecv_v2_dry_run_send",
|
|
34
|
+
] as AdbFeature[];
|
|
35
|
+
|
|
14
36
|
export class AdbServerTransport implements AdbTransport {
|
|
15
37
|
#client: AdbServerClient;
|
|
16
38
|
|
|
@@ -26,6 +48,12 @@ export class AdbServerTransport implements AdbTransport {
|
|
|
26
48
|
#waitAbortController = new AbortController();
|
|
27
49
|
readonly disconnected: Promise<void>;
|
|
28
50
|
|
|
51
|
+
get clientFeatures() {
|
|
52
|
+
// No need to get host features (features supported by ADB server)
|
|
53
|
+
// Because we create all ADB packets ourselves
|
|
54
|
+
return ADB_SERVER_DEFAULT_FEATURES;
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
constructor(
|
|
30
58
|
client: AdbServerClient,
|
|
31
59
|
serial: string,
|