@yume-chan/adb 0.0.21 → 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.
Files changed (106) hide show
  1. package/CHANGELOG.json +39 -0
  2. package/CHANGELOG.md +20 -1
  3. package/README.md +46 -366
  4. package/esm/adb.d.ts +9 -5
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +9 -2
  7. package/esm/adb.js.map +1 -1
  8. package/esm/banner.d.ts.map +1 -1
  9. package/esm/banner.js +1 -0
  10. package/esm/banner.js.map +1 -1
  11. package/esm/commands/framebuffer.d.ts +1 -1
  12. package/esm/commands/framebuffer.d.ts.map +1 -1
  13. package/esm/commands/framebuffer.js.map +1 -1
  14. package/esm/commands/reverse.js +1 -1
  15. package/esm/commands/reverse.js.map +1 -1
  16. package/esm/commands/subprocess/command.js.map +1 -1
  17. package/esm/commands/subprocess/protocols/none.d.ts +2 -1
  18. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  19. package/esm/commands/subprocess/protocols/none.js +11 -14
  20. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  21. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  22. package/esm/commands/subprocess/protocols/shell.js +21 -71
  23. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  24. package/esm/commands/subprocess/utils.js +1 -1
  25. package/esm/commands/subprocess/utils.js.map +1 -1
  26. package/esm/commands/sync/list.d.ts +2 -1
  27. package/esm/commands/sync/list.d.ts.map +1 -1
  28. package/esm/commands/sync/list.js +2 -2
  29. package/esm/commands/sync/list.js.map +1 -1
  30. package/esm/commands/sync/pull.d.ts.map +1 -1
  31. package/esm/commands/sync/pull.js +4 -0
  32. package/esm/commands/sync/pull.js.map +1 -1
  33. package/esm/commands/sync/push.js.map +1 -1
  34. package/esm/commands/sync/request.js.map +1 -1
  35. package/esm/commands/sync/response.d.ts +1 -1
  36. package/esm/commands/sync/response.d.ts.map +1 -1
  37. package/esm/commands/sync/response.js +1 -1
  38. package/esm/commands/sync/response.js.map +1 -1
  39. package/esm/commands/sync/socket.d.ts +1 -0
  40. package/esm/commands/sync/socket.d.ts.map +1 -1
  41. package/esm/commands/sync/socket.js +4 -0
  42. package/esm/commands/sync/socket.js.map +1 -1
  43. package/esm/commands/sync/stat.js.map +1 -1
  44. package/esm/commands/sync/sync.d.ts.map +1 -1
  45. package/esm/commands/sync/sync.js +5 -6
  46. package/esm/commands/sync/sync.js.map +1 -1
  47. package/esm/commands/tcpip.d.ts +21 -0
  48. package/esm/commands/tcpip.d.ts.map +1 -1
  49. package/esm/commands/tcpip.js +18 -0
  50. package/esm/commands/tcpip.js.map +1 -1
  51. package/esm/daemon/auth.js.map +1 -1
  52. package/esm/daemon/crypto.js.map +1 -1
  53. package/esm/daemon/dispatcher.d.ts +9 -1
  54. package/esm/daemon/dispatcher.d.ts.map +1 -1
  55. package/esm/daemon/dispatcher.js +107 -51
  56. package/esm/daemon/dispatcher.js.map +1 -1
  57. package/esm/daemon/packet.d.ts +5 -5
  58. package/esm/daemon/packet.d.ts.map +1 -1
  59. package/esm/daemon/packet.js +1 -1
  60. package/esm/daemon/packet.js.map +1 -1
  61. package/esm/daemon/socket.d.ts +8 -13
  62. package/esm/daemon/socket.d.ts.map +1 -1
  63. package/esm/daemon/socket.js +80 -44
  64. package/esm/daemon/socket.js.map +1 -1
  65. package/esm/daemon/transport.d.ts +32 -4
  66. package/esm/daemon/transport.d.ts.map +1 -1
  67. package/esm/daemon/transport.js +61 -31
  68. package/esm/daemon/transport.js.map +1 -1
  69. package/esm/features.d.ts +2 -1
  70. package/esm/features.d.ts.map +1 -1
  71. package/esm/features.js +2 -1
  72. package/esm/features.js.map +1 -1
  73. package/esm/server/client.d.ts +14 -8
  74. package/esm/server/client.d.ts.map +1 -1
  75. package/esm/server/client.js +122 -70
  76. package/esm/server/client.js.map +1 -1
  77. package/esm/server/transport.d.ts +3 -0
  78. package/esm/server/transport.d.ts.map +1 -1
  79. package/esm/server/transport.js +26 -0
  80. package/esm/server/transport.js.map +1 -1
  81. package/esm/utils/auto-reset-event.js.map +1 -1
  82. package/esm/utils/base64.js.map +1 -1
  83. package/esm/utils/conditional-variable.js.map +1 -1
  84. package/esm/utils/hex.js.map +1 -1
  85. package/package.json +14 -14
  86. package/src/adb.ts +22 -7
  87. package/src/banner.ts +1 -0
  88. package/src/commands/framebuffer.ts +1 -1
  89. package/src/commands/reverse.ts +2 -2
  90. package/src/commands/subprocess/protocols/none.ts +15 -19
  91. package/src/commands/subprocess/protocols/shell.ts +28 -98
  92. package/src/commands/subprocess/utils.ts +1 -1
  93. package/src/commands/sync/list.ts +6 -2
  94. package/src/commands/sync/pull.ts +3 -0
  95. package/src/commands/sync/response.ts +2 -2
  96. package/src/commands/sync/socket.ts +5 -0
  97. package/src/commands/sync/sync.ts +5 -6
  98. package/src/commands/tcpip.ts +44 -0
  99. package/src/daemon/dispatcher.ts +156 -65
  100. package/src/daemon/packet.ts +1 -1
  101. package/src/daemon/socket.ts +104 -86
  102. package/src/daemon/transport.ts +104 -38
  103. package/src/features.ts +2 -1
  104. package/src/server/client.ts +141 -91
  105. package/src/server/transport.ts +28 -0
  106. package/tsconfig.build.tsbuildinfo +1 -1
@@ -26,28 +26,79 @@ 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;
53
+
54
+ export type AdbDaemonConnection = ReadableWritablePair<
55
+ AdbPacketData,
56
+ Consumable<AdbPacketInit>
57
+ >;
29
58
 
30
59
  interface AdbDaemonAuthenticationOptions {
31
60
  serial: string;
32
- connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>;
61
+ connection: AdbDaemonConnection;
33
62
  credentialStore: AdbCredentialStore;
34
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;
35
74
  /**
36
75
  * Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
37
76
  */
38
77
  preserveConnection?: boolean | undefined;
78
+ debugSlowRead?: boolean | undefined;
39
79
  }
40
80
 
41
81
  interface AdbDaemonSocketConnectorConstructionOptions {
42
82
  serial: string;
43
- connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>;
83
+ connection: AdbDaemonConnection;
44
84
  version: number;
45
85
  maxPayloadSize: number;
46
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;
47
97
  /**
48
98
  * Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
49
99
  */
50
100
  preserveConnection?: boolean | undefined;
101
+ debugSlowRead?: boolean | undefined;
51
102
  }
52
103
 
53
104
  export class AdbDaemonTransport implements AdbTransport {
@@ -64,7 +115,9 @@ export class AdbDaemonTransport implements AdbTransport {
64
115
  connection,
65
116
  credentialStore,
66
117
  authenticators = ADB_DEFAULT_AUTHENTICATORS,
67
- preserveConnection,
118
+ features = ADB_DAEMON_DEFAULT_FEATURES,
119
+ initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
120
+ ...options
68
121
  }: AdbDaemonAuthenticationOptions): Promise<AdbDaemonTransport> {
69
122
  // Initially, set to highest-supported version and payload size.
70
123
  let version = 0x01000001;
@@ -94,9 +147,8 @@ export class AdbDaemonTransport implements AdbTransport {
94
147
  resolver.resolve(decodeUtf8(packet.payload));
95
148
  break;
96
149
  case AdbCommand.Auth: {
97
- const response = await authProcessor.process(
98
- packet,
99
- );
150
+ const response =
151
+ await authProcessor.process(packet);
100
152
  await sendPacket(response);
101
153
  break;
102
154
  }
@@ -138,38 +190,25 @@ export class AdbDaemonTransport implements AdbTransport {
138
190
  await ConsumableWritableStream.write(writer, init as AdbPacketInit);
139
191
  }
140
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
+
141
201
  let banner: string;
142
202
  try {
143
- // https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
144
- // There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
145
- const features = [
146
- AdbFeature.ShellV2,
147
- AdbFeature.Cmd,
148
- AdbFeature.StatV2,
149
- AdbFeature.ListV2,
150
- AdbFeature.FixedPushMkdir,
151
- "apex",
152
- AdbFeature.Abb,
153
- // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
154
- // No special handling required.
155
- "fixed_push_symlink_timestamp",
156
- AdbFeature.AbbExec,
157
- "remount_shell",
158
- "track_app",
159
- AdbFeature.SendReceiveV2,
160
- "sendrecv_v2_brotli",
161
- "sendrecv_v2_lz4",
162
- "sendrecv_v2_zstd",
163
- "sendrecv_v2_dry_run_send",
164
- ].join(",");
165
-
166
203
  await sendPacket({
167
204
  command: AdbCommand.Connect,
168
205
  arg0: version,
169
206
  arg1: maxPayloadSize,
170
207
  // The terminating `;` is required in formal definition
171
208
  // But ADB daemon (all versions) can still work without it
172
- payload: encodeUtf8(`host::features=${features}`),
209
+ payload: encodeUtf8(
210
+ `host::features=${actualFeatures.join(",")}`,
211
+ ),
173
212
  });
174
213
 
175
214
  banner = await resolver.promise;
@@ -189,10 +228,17 @@ export class AdbDaemonTransport implements AdbTransport {
189
228
  version,
190
229
  maxPayloadSize,
191
230
  banner,
192
- preserveConnection,
231
+ features: actualFeatures,
232
+ initialDelayedAckBytes,
233
+ ...options,
193
234
  });
194
235
  }
195
236
 
237
+ #connection: AdbDaemonConnection;
238
+ get connection() {
239
+ return this.#connection;
240
+ }
241
+
196
242
  readonly #dispatcher: AdbPacketDispatcher;
197
243
 
198
244
  #serial: string;
@@ -205,9 +251,8 @@ export class AdbDaemonTransport implements AdbTransport {
205
251
  return this.#protocolVersion;
206
252
  }
207
253
 
208
- #maxPayloadSize: number;
209
254
  get maxPayloadSize() {
210
- return this.#maxPayloadSize;
255
+ return this.#dispatcher.options.maxPayloadSize;
211
256
  }
212
257
 
213
258
  #banner: AdbBanner;
@@ -219,16 +264,38 @@ export class AdbDaemonTransport implements AdbTransport {
219
264
  return this.#dispatcher.disconnected;
220
265
  }
221
266
 
267
+ #clientFeatures: readonly AdbFeature[];
268
+ get clientFeatures() {
269
+ return this.#clientFeatures;
270
+ }
271
+
222
272
  constructor({
223
273
  serial,
224
274
  connection,
225
275
  version,
226
- maxPayloadSize,
227
276
  banner,
228
- preserveConnection,
277
+ features = ADB_DAEMON_DEFAULT_FEATURES,
278
+ initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
279
+ ...options
229
280
  }: AdbDaemonSocketConnectorConstructionOptions) {
230
281
  this.#serial = serial;
282
+ this.#connection = connection;
231
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
+ }
232
299
 
233
300
  let calculateChecksum: boolean;
234
301
  let appendNullToServiceString: boolean;
@@ -243,12 +310,11 @@ export class AdbDaemonTransport implements AdbTransport {
243
310
  this.#dispatcher = new AdbPacketDispatcher(connection, {
244
311
  calculateChecksum,
245
312
  appendNullToServiceString,
246
- maxPayloadSize,
247
- preserveConnection,
313
+ initialDelayedAckBytes,
314
+ ...options,
248
315
  });
249
316
 
250
317
  this.#protocolVersion = version;
251
- this.#maxPayloadSize = maxPayloadSize;
252
318
  }
253
319
 
254
320
  connect(service: string): ValueOrPromise<AdbSocket> {
package/src/features.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // The order follows
2
- // https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
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
  }
@@ -3,13 +3,11 @@
3
3
  import { PromiseResolver } from "@yume-chan/async";
4
4
  import type {
5
5
  AbortSignal,
6
- Consumable,
7
6
  ReadableWritablePair,
8
7
  WritableStreamDefaultWriter,
9
8
  } from "@yume-chan/stream-extra";
10
9
  import {
11
10
  BufferedReadableStream,
12
- DuplexStreamFactory,
13
11
  UnwrapConsumableStream,
14
12
  WrapWritableStream,
15
13
  } from "@yume-chan/stream-extra";
@@ -20,15 +18,16 @@ import type {
20
18
  } from "@yume-chan/struct";
21
19
  import {
22
20
  BigIntFieldType,
21
+ EMPTY_UINT8_ARRAY,
23
22
  SyncPromise,
24
23
  decodeUtf8,
25
24
  encodeUtf8,
26
25
  } from "@yume-chan/struct";
27
26
 
28
- import type { AdbIncomingSocketHandler, AdbSocket } from "../adb.js";
27
+ import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
29
28
  import { AdbBanner } from "../banner.js";
30
29
  import type { AdbFeature } from "../features.js";
31
- import { NOOP, hexToNumber, numberToHex } from "../utils/index.js";
30
+ import { NOOP, hexToNumber, numberToHex, unreachable } from "../utils/index.js";
32
31
 
33
32
  import { AdbServerTransport } from "./transport.js";
34
33
 
@@ -37,10 +36,16 @@ export interface AdbServerConnectionOptions {
37
36
  signal?: AbortSignal | undefined;
38
37
  }
39
38
 
40
- export interface AdbServerConnection {
39
+ export interface AdbServerConnection
40
+ extends ReadableWritablePair<Uint8Array, Uint8Array>,
41
+ Closeable {
42
+ get closed(): Promise<void>;
43
+ }
44
+
45
+ export interface AdbServerConnector {
41
46
  connect(
42
47
  options?: AdbServerConnectionOptions,
43
- ): ValueOrPromise<ReadableWritablePair<Uint8Array, Uint8Array>>;
48
+ ): ValueOrPromise<AdbServerConnection>;
44
49
 
45
50
  addReverseTunnel(
46
51
  handler: AdbIncomingSocketHandler,
@@ -57,10 +62,8 @@ export interface AdbServerSocket extends AdbSocket {
57
62
  }
58
63
 
59
64
  export type AdbServerDeviceSelector =
60
- | {
61
- serial: string;
62
- }
63
65
  | { transportId: bigint }
66
+ | { serial: string }
64
67
  | { usb: true }
65
68
  | { tcp: true }
66
69
  | undefined;
@@ -76,9 +79,9 @@ export interface AdbServerDevice {
76
79
  export class AdbServerClient {
77
80
  static readonly VERSION = 41;
78
81
 
79
- readonly connection: AdbServerConnection;
82
+ readonly connection: AdbServerConnector;
80
83
 
81
- constructor(connection: AdbServerConnection) {
84
+ constructor(connection: AdbServerConnector) {
82
85
  this.connection = connection;
83
86
  }
84
87
 
@@ -90,7 +93,11 @@ export class AdbServerClient {
90
93
  return SyncPromise.try(() => stream.readExactly(4))
91
94
  .then((buffer) => {
92
95
  const length = hexToNumber(buffer);
93
- return stream.readExactly(length);
96
+ if (length === 0) {
97
+ return EMPTY_UINT8_ARRAY;
98
+ } else {
99
+ return stream.readExactly(length);
100
+ }
94
101
  })
95
102
  .then((valueBuffer) => {
96
103
  return decodeUtf8(valueBuffer);
@@ -128,30 +135,41 @@ export class AdbServerClient {
128
135
  async connect(
129
136
  request: string,
130
137
  options?: AdbServerConnectionOptions,
131
- ): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
138
+ ): Promise<AdbServerConnection> {
132
139
  const connection = await this.connection.connect(options);
133
140
 
134
- const writer = connection.writable.getWriter();
135
- await AdbServerClient.writeString(writer, request);
141
+ try {
142
+ const writer = connection.writable.getWriter();
143
+ await AdbServerClient.writeString(writer, request);
144
+ writer.releaseLock();
145
+ } catch (e) {
146
+ await connection.readable.cancel();
147
+ await connection.close();
148
+ throw e;
149
+ }
136
150
 
137
151
  const readable = new BufferedReadableStream(connection.readable);
138
-
139
152
  try {
140
- // `raceSignal` throws if the signal is aborted,
153
+ // `raceSignal` throws when the signal is aborted,
141
154
  // so the `catch` block can close the connection.
142
155
  await raceSignal(
143
156
  () => AdbServerClient.readOkay(readable),
144
157
  options?.signal,
145
158
  );
146
159
 
147
- writer.releaseLock();
148
160
  return {
149
161
  readable: readable.release(),
150
162
  writable: connection.writable,
163
+ get closed() {
164
+ return connection.closed;
165
+ },
166
+ async close() {
167
+ await connection.close();
168
+ },
151
169
  };
152
170
  } catch (e) {
153
- writer.close().catch(NOOP);
154
- readable.cancel().catch(NOOP);
171
+ await readable.cancel().catch(NOOP);
172
+ await connection.close();
155
173
  throw e;
156
174
  }
157
175
  }
@@ -196,63 +214,91 @@ export class AdbServerClient {
196
214
  }
197
215
  }
198
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
+
199
266
  async getDevices(): Promise<AdbServerDevice[]> {
200
267
  const connection = await this.connect("host:devices-l");
201
268
  const readable = new BufferedReadableStream(connection.readable);
202
269
  try {
203
- const devices: AdbServerDevice[] = [];
204
270
  const response = await AdbServerClient.readString(readable);
205
- for (const line of response.split("\n")) {
206
- if (!line) {
207
- continue;
208
- }
209
-
210
- const parts = line.split(" ").filter(Boolean);
211
- const serial = parts[0]!;
212
- const status = parts[1]!;
213
- if (status !== "device") {
214
- continue;
215
- }
216
-
217
- let product: string | undefined;
218
- let model: string | undefined;
219
- let device: string | undefined;
220
- let transportId: bigint | undefined;
221
- for (let i = 2; i < parts.length; i += 1) {
222
- const [key, value] = parts[i]!.split(":");
223
- switch (key) {
224
- case "product":
225
- product = value;
226
- break;
227
- case "model":
228
- model = value;
229
- break;
230
- case "device":
231
- device = value;
232
- break;
233
- case "transport_id":
234
- transportId = BigInt(value!);
235
- break;
236
- }
237
- }
238
- if (!transportId) {
239
- throw new Error(`No transport id for device ${serial}`);
240
- }
241
- devices.push({
242
- serial,
243
- product,
244
- model,
245
- device,
246
- transportId,
247
- });
248
- }
249
- return devices;
271
+ return this.parseDeviceList(response);
250
272
  } finally {
251
273
  connection.writable.close().catch(NOOP);
252
274
  readable.cancel().catch(NOOP);
253
275
  }
254
276
  }
255
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
+
256
302
  formatDeviceService(device: AdbServerDeviceSelector, command: string) {
257
303
  if (!device) {
258
304
  return `host:${command}`;
@@ -330,8 +376,18 @@ export class AdbServerClient {
330
376
  }
331
377
 
332
378
  const connection = await this.connect(switchService);
379
+
380
+ try {
381
+ const writer = connection.writable.getWriter();
382
+ await AdbServerClient.writeString(writer, service);
383
+ writer.releaseLock();
384
+ } catch (e) {
385
+ await connection.readable.cancel();
386
+ await connection.close();
387
+ throw e;
388
+ }
389
+
333
390
  const readable = new BufferedReadableStream(connection.readable);
334
- const writer = connection.writable.getWriter();
335
391
  try {
336
392
  if (transportId === undefined) {
337
393
  const array = await readable.readExactly(8);
@@ -344,34 +400,25 @@ export class AdbServerClient {
344
400
  transportId = BigIntFieldType.Uint64.getter(dataView, 0, true);
345
401
  }
346
402
 
347
- await AdbServerClient.writeString(writer, service);
348
403
  await AdbServerClient.readOkay(readable);
349
404
 
350
- writer.releaseLock();
351
-
352
- const duplex = new DuplexStreamFactory<
353
- Uint8Array,
354
- Consumable<Uint8Array>
355
- >();
356
- const wrapReadable = duplex.wrapReadable(readable.release());
357
- const wrapWritable = duplex.createWritable(
358
- new WrapWritableStream(connection.writable).bePipedThroughFrom(
359
- new UnwrapConsumableStream(),
360
- ),
361
- );
362
-
363
405
  return {
364
406
  transportId,
365
407
  service,
366
- readable: wrapReadable,
367
- writable: wrapWritable,
368
- close() {
369
- return duplex.close();
408
+ readable: readable.release(),
409
+ writable: new WrapWritableStream(
410
+ connection.writable,
411
+ ).bePipedThroughFrom(new UnwrapConsumableStream()),
412
+ get closed() {
413
+ return connection.closed;
414
+ },
415
+ async close() {
416
+ await connection.close();
370
417
  },
371
418
  };
372
419
  } catch (e) {
373
- writer.close().catch(NOOP);
374
- readable.cancel().catch(NOOP);
420
+ await readable.cancel().catch(NOOP);
421
+ await connection.close();
375
422
  throw e;
376
423
  }
377
424
  }
@@ -410,9 +457,12 @@ export class AdbServerClient {
410
457
  `wait-for-${type}-${state}`,
411
458
  );
412
459
 
413
- // `connect` resolves when server writes `OKAY`,
414
- // but for this command the server writes `OKAY` after the condition is met.
415
- await this.connect(service, options);
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();
416
466
  }
417
467
 
418
468
  async createTransport(
@@ -441,7 +491,7 @@ export class AdbServerClient {
441
491
  }
442
492
  }
443
493
 
444
- async function raceSignal<T>(
494
+ export async function raceSignal<T>(
445
495
  callback: () => Promise<T>,
446
496
  ...signals: (AbortSignal | undefined)[]
447
497
  ): Promise<T> {
@@ -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,