@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.
Files changed (57) hide show
  1. package/CHANGELOG.json +18 -0
  2. package/CHANGELOG.md +10 -1
  3. package/esm/adb.d.ts +4 -1
  4. package/esm/adb.d.ts.map +1 -1
  5. package/esm/adb.js +9 -2
  6. package/esm/adb.js.map +1 -1
  7. package/esm/banner.d.ts.map +1 -1
  8. package/esm/banner.js +1 -0
  9. package/esm/banner.js.map +1 -1
  10. package/esm/commands/reverse.js +1 -1
  11. package/esm/commands/reverse.js.map +1 -1
  12. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  13. package/esm/commands/subprocess/protocols/shell.js +15 -69
  14. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  15. package/esm/commands/subprocess/utils.js +1 -1
  16. package/esm/commands/subprocess/utils.js.map +1 -1
  17. package/esm/commands/sync/sync.d.ts.map +1 -1
  18. package/esm/commands/sync/sync.js +5 -6
  19. package/esm/commands/sync/sync.js.map +1 -1
  20. package/esm/daemon/dispatcher.d.ts +7 -0
  21. package/esm/daemon/dispatcher.d.ts.map +1 -1
  22. package/esm/daemon/dispatcher.js +57 -12
  23. package/esm/daemon/dispatcher.js.map +1 -1
  24. package/esm/daemon/socket.d.ts +1 -1
  25. package/esm/daemon/socket.d.ts.map +1 -1
  26. package/esm/daemon/socket.js +36 -8
  27. package/esm/daemon/socket.js.map +1 -1
  28. package/esm/daemon/transport.d.ts +26 -2
  29. package/esm/daemon/transport.d.ts.map +1 -1
  30. package/esm/daemon/transport.js +53 -25
  31. package/esm/daemon/transport.js.map +1 -1
  32. package/esm/features.d.ts +2 -1
  33. package/esm/features.d.ts.map +1 -1
  34. package/esm/features.js +2 -1
  35. package/esm/features.js.map +1 -1
  36. package/esm/server/client.d.ts +2 -0
  37. package/esm/server/client.d.ts.map +1 -1
  38. package/esm/server/client.js +82 -50
  39. package/esm/server/client.js.map +1 -1
  40. package/esm/server/transport.d.ts +3 -0
  41. package/esm/server/transport.d.ts.map +1 -1
  42. package/esm/server/transport.js +26 -0
  43. package/esm/server/transport.js.map +1 -1
  44. package/package.json +13 -13
  45. package/src/adb.ts +15 -2
  46. package/src/banner.ts +1 -0
  47. package/src/commands/reverse.ts +2 -2
  48. package/src/commands/subprocess/protocols/shell.ts +22 -97
  49. package/src/commands/subprocess/utils.ts +1 -1
  50. package/src/commands/sync/sync.ts +5 -6
  51. package/src/daemon/dispatcher.ts +90 -14
  52. package/src/daemon/socket.ts +47 -16
  53. package/src/daemon/transport.ts +82 -24
  54. package/src/features.ts +2 -1
  55. package/src/server/client.ts +87 -51
  56. package/src/server/transport.ts +28 -0
  57. package/tsconfig.build.tsbuildinfo +1 -1
@@ -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
- this.#writePromise = new PromiseResolver();
92
- await this.#dispatcher.sendPacket(
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.#writePromise?.resolve();
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 {
@@ -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(`host::features=${features}`),
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.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
  }
@@ -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
- return stream.readExactly(length);
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
- for (const line of response.split("\n")) {
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
- // `connect` resolves when server writes `OKAY`,
428
- // but for this command the server writes `OKAY` after the condition is met.
429
- 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();
430
466
  }
431
467
 
432
468
  async createTransport(
@@ -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,