@yume-chan/adb 0.0.22 → 0.0.24

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 (149) hide show
  1. package/CHANGELOG.json +51 -0
  2. package/CHANGELOG.md +24 -1
  3. package/LICENSE +1 -1
  4. package/esm/adb.d.ts +6 -3
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +11 -4
  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/power.d.ts.map +1 -1
  12. package/esm/commands/power.js +6 -5
  13. package/esm/commands/power.js.map +1 -1
  14. package/esm/commands/reverse.d.ts.map +1 -1
  15. package/esm/commands/reverse.js +23 -11
  16. package/esm/commands/reverse.js.map +1 -1
  17. package/esm/commands/subprocess/command.js +3 -3
  18. package/esm/commands/subprocess/command.js.map +1 -1
  19. package/esm/commands/subprocess/protocols/none.d.ts +2 -2
  20. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  21. package/esm/commands/subprocess/protocols/shell.d.ts +3 -3
  22. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  23. package/esm/commands/subprocess/protocols/shell.js +14 -69
  24. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  25. package/esm/commands/subprocess/protocols/types.d.ts +2 -2
  26. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  27. package/esm/commands/subprocess/utils.js +1 -1
  28. package/esm/commands/subprocess/utils.js.map +1 -1
  29. package/esm/commands/sync/list.d.ts +2 -5
  30. package/esm/commands/sync/list.d.ts.map +1 -1
  31. package/esm/commands/sync/list.js +2 -4
  32. package/esm/commands/sync/list.js.map +1 -1
  33. package/esm/commands/sync/pull.d.ts +1 -4
  34. package/esm/commands/sync/pull.d.ts.map +1 -1
  35. package/esm/commands/sync/pull.js +2 -2
  36. package/esm/commands/sync/pull.js.map +1 -1
  37. package/esm/commands/sync/push.d.ts +4 -7
  38. package/esm/commands/sync/push.d.ts.map +1 -1
  39. package/esm/commands/sync/push.js +3 -6
  40. package/esm/commands/sync/push.js.map +1 -1
  41. package/esm/commands/sync/request.d.ts +13 -18
  42. package/esm/commands/sync/request.d.ts.map +1 -1
  43. package/esm/commands/sync/request.js +23 -28
  44. package/esm/commands/sync/request.js.map +1 -1
  45. package/esm/commands/sync/response.d.ts +18 -12
  46. package/esm/commands/sync/response.d.ts.map +1 -1
  47. package/esm/commands/sync/response.js +39 -17
  48. package/esm/commands/sync/response.js.map +1 -1
  49. package/esm/commands/sync/socket.d.ts +2 -2
  50. package/esm/commands/sync/socket.d.ts.map +1 -1
  51. package/esm/commands/sync/socket.js +10 -5
  52. package/esm/commands/sync/socket.js.map +1 -1
  53. package/esm/commands/sync/stat.d.ts +0 -3
  54. package/esm/commands/sync/stat.d.ts.map +1 -1
  55. package/esm/commands/sync/stat.js +0 -2
  56. package/esm/commands/sync/stat.js.map +1 -1
  57. package/esm/commands/sync/sync.d.ts +4 -3
  58. package/esm/commands/sync/sync.d.ts.map +1 -1
  59. package/esm/commands/sync/sync.js +9 -7
  60. package/esm/commands/sync/sync.js.map +1 -1
  61. package/esm/commands/tcpip.js +1 -1
  62. package/esm/commands/tcpip.js.map +1 -1
  63. package/esm/daemon/auth.js +1 -1
  64. package/esm/daemon/auth.js.map +1 -1
  65. package/esm/daemon/crypto.d.ts +3 -2
  66. package/esm/daemon/crypto.d.ts.map +1 -1
  67. package/esm/daemon/crypto.js +56 -49
  68. package/esm/daemon/crypto.js.map +1 -1
  69. package/esm/daemon/dispatcher.d.ts +38 -6
  70. package/esm/daemon/dispatcher.d.ts.map +1 -1
  71. package/esm/daemon/dispatcher.js +86 -28
  72. package/esm/daemon/dispatcher.js.map +1 -1
  73. package/esm/daemon/packet.d.ts +2 -2
  74. package/esm/daemon/packet.d.ts.map +1 -1
  75. package/esm/daemon/packet.js +13 -12
  76. package/esm/daemon/packet.js.map +1 -1
  77. package/esm/daemon/socket.d.ts +11 -6
  78. package/esm/daemon/socket.d.ts.map +1 -1
  79. package/esm/daemon/socket.js +42 -12
  80. package/esm/daemon/socket.js.map +1 -1
  81. package/esm/daemon/transport.d.ts +75 -11
  82. package/esm/daemon/transport.d.ts.map +1 -1
  83. package/esm/daemon/transport.js +60 -33
  84. package/esm/daemon/transport.js.map +1 -1
  85. package/esm/features.d.ts +2 -1
  86. package/esm/features.d.ts.map +1 -1
  87. package/esm/features.js +2 -1
  88. package/esm/features.js.map +1 -1
  89. package/esm/server/client.d.ts +128 -48
  90. package/esm/server/client.d.ts.map +1 -1
  91. package/esm/server/client.js +378 -145
  92. package/esm/server/client.js.map +1 -1
  93. package/esm/server/transport.d.ts +3 -0
  94. package/esm/server/transport.d.ts.map +1 -1
  95. package/esm/server/transport.js +30 -4
  96. package/esm/server/transport.js.map +1 -1
  97. package/esm/utils/base64.d.ts.map +1 -1
  98. package/esm/utils/base64.js +33 -22
  99. package/esm/utils/base64.js.map +1 -1
  100. package/esm/utils/hex.d.ts +1 -1
  101. package/esm/utils/hex.d.ts.map +1 -1
  102. package/esm/utils/hex.js +12 -14
  103. package/esm/utils/hex.js.map +1 -1
  104. package/esm/utils/index.d.ts +1 -1
  105. package/esm/utils/index.d.ts.map +1 -1
  106. package/esm/utils/index.js +1 -1
  107. package/esm/utils/index.js.map +1 -1
  108. package/esm/utils/sequence-equal.d.ts +2 -0
  109. package/esm/utils/sequence-equal.d.ts.map +1 -0
  110. package/esm/utils/sequence-equal.js +12 -0
  111. package/esm/utils/sequence-equal.js.map +1 -0
  112. package/package.json +13 -15
  113. package/src/adb.ts +22 -6
  114. package/src/banner.ts +1 -0
  115. package/src/commands/power.ts +7 -5
  116. package/src/commands/reverse.ts +31 -18
  117. package/src/commands/subprocess/command.ts +3 -3
  118. package/src/commands/subprocess/protocols/none.ts +2 -2
  119. package/src/commands/subprocess/protocols/shell.ts +21 -100
  120. package/src/commands/subprocess/protocols/types.ts +2 -2
  121. package/src/commands/subprocess/utils.ts +1 -1
  122. package/src/commands/sync/list.ts +2 -4
  123. package/src/commands/sync/pull.ts +2 -2
  124. package/src/commands/sync/push.ts +7 -10
  125. package/src/commands/sync/request.ts +32 -32
  126. package/src/commands/sync/response.ts +47 -20
  127. package/src/commands/sync/socket.ts +13 -8
  128. package/src/commands/sync/stat.ts +0 -2
  129. package/src/commands/sync/sync.ts +13 -9
  130. package/src/commands/tcpip.ts +1 -1
  131. package/src/daemon/auth.ts +1 -1
  132. package/src/daemon/crypto.ts +64 -55
  133. package/src/daemon/dispatcher.ts +175 -37
  134. package/src/daemon/packet.ts +21 -14
  135. package/src/daemon/socket.ts +62 -28
  136. package/src/daemon/transport.ts +145 -40
  137. package/src/features.ts +2 -1
  138. package/src/server/client.ts +472 -224
  139. package/src/server/transport.ts +32 -4
  140. package/src/utils/base64.ts +33 -22
  141. package/src/utils/hex.ts +16 -14
  142. package/src/utils/index.ts +1 -1
  143. package/src/utils/sequence-equal.ts +13 -0
  144. package/tsconfig.build.tsbuildinfo +1 -1
  145. package/esm/utils/conditional-variable.d.ts +0 -8
  146. package/esm/utils/conditional-variable.d.ts.map +0 -1
  147. package/esm/utils/conditional-variable.js +0 -34
  148. package/esm/utils/conditional-variable.js.map +0 -1
  149. package/src/utils/conditional-variable.ts +0 -45
@@ -1,90 +1,144 @@
1
1
  // cspell:ignore tport
2
2
  import { PromiseResolver } from "@yume-chan/async";
3
- import { BufferedReadableStream, UnwrapConsumableStream, WrapWritableStream, } from "@yume-chan/stream-extra";
4
- import { BigIntFieldType, SyncPromise, decodeUtf8, encodeUtf8, } from "@yume-chan/struct";
3
+ import { getUint64LittleEndian } from "@yume-chan/no-data-view";
4
+ import { BufferedReadableStream, MaybeConsumable, WrapWritableStream, } from "@yume-chan/stream-extra";
5
+ import { EMPTY_UINT8_ARRAY, SyncPromise, decodeUtf8, encodeUtf8, } from "@yume-chan/struct";
5
6
  import { AdbBanner } from "../banner.js";
6
- import { NOOP, hexToNumber, numberToHex } from "../utils/index.js";
7
+ import { NOOP, hexToNumber, sequenceEqual, write4HexDigits, } from "../utils/index.js";
7
8
  import { AdbServerTransport } from "./transport.js";
8
- export class AdbServerClient {
9
- static VERSION = 41;
10
- connection;
9
+ const OKAY = encodeUtf8("OKAY");
10
+ const FAIL = encodeUtf8("FAIL");
11
+ class AdbServerStream {
12
+ #connection;
13
+ #buffered;
14
+ #writer;
11
15
  constructor(connection) {
12
- this.connection = connection;
16
+ this.#connection = connection;
17
+ this.#buffered = new BufferedReadableStream(connection.readable);
18
+ this.#writer = connection.writable.getWriter();
19
+ }
20
+ readExactly(length) {
21
+ return this.#buffered.readExactly(length);
13
22
  }
14
- static readString(stream) {
15
- return SyncPromise.try(() => stream.readExactly(4))
23
+ readString() {
24
+ return SyncPromise.try(() => this.readExactly(4))
16
25
  .then((buffer) => {
17
26
  const length = hexToNumber(buffer);
18
- return stream.readExactly(length);
27
+ if (length === 0) {
28
+ return EMPTY_UINT8_ARRAY;
29
+ }
30
+ else {
31
+ return this.readExactly(length);
32
+ }
19
33
  })
20
- .then((valueBuffer) => {
21
- return decodeUtf8(valueBuffer);
34
+ .then((buffer) => {
35
+ // TODO: Investigate using stream mode `TextDecoder` for long strings.
36
+ // Because concatenating strings uses rope data structure,
37
+ // which only points to the original strings and doesn't copy the data,
38
+ // it's more efficient than concatenating `Uint8Array`s.
39
+ //
40
+ // ```
41
+ // const decoder = new TextDecoder();
42
+ // let result = '';
43
+ // for await (const chunk of stream.iterateExactly(length)) {
44
+ // result += decoder.decode(chunk, { stream: true });
45
+ // }
46
+ // result += decoder.decode();
47
+ // return result;
48
+ // ```
49
+ //
50
+ // Although, it will be super complex to use `SyncPromise` with async iterator,
51
+ // `stream.iterateExactly` need to return an
52
+ // `Iterator<Uint8Array | Promise<Uint8Array>>` instead of a true async iterator.
53
+ // Maybe `SyncPromise` should support async iterators directly.
54
+ return decodeUtf8(buffer);
22
55
  })
23
56
  .valueOrPromise();
24
57
  }
25
- static async writeString(writer, value) {
26
- const valueBuffer = encodeUtf8(value);
27
- const buffer = new Uint8Array(4 + valueBuffer.length);
28
- buffer.set(numberToHex(valueBuffer.length));
29
- buffer.set(valueBuffer, 4);
30
- await writer.write(buffer);
58
+ async writeString(value) {
59
+ // TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
60
+ // That way allocates a new string (hopefully only a rope) instead of a new buffer
61
+ const encoded = encodeUtf8(value);
62
+ const buffer = new Uint8Array(4 + encoded.length);
63
+ write4HexDigits(buffer, 0, encoded.length);
64
+ buffer.set(encoded, 4);
65
+ await this.#writer.write(buffer);
31
66
  }
32
- static async readOkay(stream) {
33
- const response = decodeUtf8(await stream.readExactly(4));
34
- if (response === "OKAY") {
67
+ async readOkay() {
68
+ const response = await this.readExactly(4);
69
+ if (sequenceEqual(response, OKAY)) {
70
+ // `OKAY` is followed by data length and data
71
+ // But different services want to parse the data differently
72
+ // So don't read the data here
35
73
  return;
36
74
  }
37
- if (response === "FAIL") {
38
- const reason = await AdbServerClient.readString(stream);
75
+ if (sequenceEqual(response, FAIL)) {
76
+ const reason = await this.readString();
39
77
  throw new Error(reason);
40
78
  }
41
- throw new Error(`Unexpected response: ${response}`);
79
+ throw new Error(`Unexpected response: ${decodeUtf8(response)}`);
42
80
  }
43
- async connect(request, options) {
44
- const connection = await this.connection.connect(options);
81
+ release() {
82
+ this.#writer.releaseLock();
83
+ return {
84
+ readable: this.#buffered.release(),
85
+ writable: this.#connection.writable,
86
+ closed: this.#connection.closed,
87
+ close: () => this.#connection.close(),
88
+ };
89
+ }
90
+ async dispose() {
91
+ await this.#buffered.cancel().catch(NOOP);
92
+ await this.#writer.close().catch(NOOP);
45
93
  try {
46
- const writer = connection.writable.getWriter();
47
- await AdbServerClient.writeString(writer, request);
48
- writer.releaseLock();
94
+ await this.#connection.close();
95
+ }
96
+ catch {
97
+ // ignore
98
+ }
99
+ }
100
+ }
101
+ export class AdbServerClient {
102
+ static VERSION = 41;
103
+ connector;
104
+ wireless = new AdbServerClient.WirelessCommands(this);
105
+ mDns = new AdbServerClient.MDnsCommands(this);
106
+ constructor(connection) {
107
+ this.connector = connection;
108
+ }
109
+ async createConnection(request, options) {
110
+ const connection = await this.connector.connect(options);
111
+ const stream = new AdbServerStream(connection);
112
+ try {
113
+ await stream.writeString(request);
49
114
  }
50
115
  catch (e) {
51
- await connection.readable.cancel();
52
- await connection.close();
116
+ await stream.dispose();
53
117
  throw e;
54
118
  }
55
- const readable = new BufferedReadableStream(connection.readable);
56
119
  try {
57
120
  // `raceSignal` throws when the signal is aborted,
58
121
  // so the `catch` block can close the connection.
59
- await raceSignal(() => AdbServerClient.readOkay(readable), options?.signal);
60
- return {
61
- readable: readable.release(),
62
- writable: connection.writable,
63
- get closed() {
64
- return connection.closed;
65
- },
66
- async close() {
67
- await connection.close();
68
- },
69
- };
122
+ await raceSignal(() => stream.readOkay(), options?.signal);
123
+ return stream;
70
124
  }
71
125
  catch (e) {
72
- await readable.cancel().catch(NOOP);
73
- await connection.close();
126
+ await stream.dispose();
74
127
  throw e;
75
128
  }
76
129
  }
130
+ /**
131
+ * `adb version`
132
+ */
77
133
  async getVersion() {
78
- const connection = await this.connect("host:version");
79
- const readable = new BufferedReadableStream(connection.readable);
134
+ const connection = await this.createConnection("host:version");
80
135
  try {
81
- const length = hexToNumber(await readable.readExactly(4));
82
- const version = hexToNumber(await readable.readExactly(length));
136
+ const length = hexToNumber(await connection.readExactly(4));
137
+ const version = hexToNumber(await connection.readExactly(length));
83
138
  return version;
84
139
  }
85
140
  finally {
86
- connection.writable.close().catch(NOOP);
87
- readable.cancel().catch(NOOP);
141
+ await connection.dispose();
88
142
  }
89
143
  }
90
144
  async validateVersion() {
@@ -93,76 +147,109 @@ export class AdbServerClient {
93
147
  throw new Error(`adb server version (${version}) doesn't match this client (${AdbServerClient.VERSION})`);
94
148
  }
95
149
  }
150
+ /**
151
+ * `adb kill-server`
152
+ */
96
153
  async killServer() {
97
- const connection = await this.connect("host:kill");
98
- connection.writable.close().catch(NOOP);
99
- connection.readable.cancel().catch(NOOP);
154
+ const connection = await this.createConnection("host:kill");
155
+ await connection.dispose();
100
156
  }
157
+ /**
158
+ * `adb host-features`
159
+ */
101
160
  async getServerFeatures() {
102
- const connection = await this.connect("host:host-features");
103
- const readable = new BufferedReadableStream(connection.readable);
161
+ const connection = await this.createConnection("host:host-features");
104
162
  try {
105
- const response = await AdbServerClient.readString(readable);
163
+ const response = await connection.readString();
106
164
  return response.split(",");
107
165
  }
108
166
  finally {
109
- connection.writable.close().catch(NOOP);
110
- readable.cancel().catch(NOOP);
167
+ await connection.dispose();
111
168
  }
112
169
  }
170
+ parseDeviceList(value) {
171
+ const devices = [];
172
+ for (const line of value.split("\n")) {
173
+ if (!line) {
174
+ continue;
175
+ }
176
+ const parts = line.split(" ").filter(Boolean);
177
+ const serial = parts[0];
178
+ const status = parts[1];
179
+ if (status !== "device" && status !== "unauthorized") {
180
+ continue;
181
+ }
182
+ let product;
183
+ let model;
184
+ let device;
185
+ let transportId;
186
+ for (let i = 2; i < parts.length; i += 1) {
187
+ const [key, value] = parts[i].split(":");
188
+ switch (key) {
189
+ case "product":
190
+ product = value;
191
+ break;
192
+ case "model":
193
+ model = value;
194
+ break;
195
+ case "device":
196
+ device = value;
197
+ break;
198
+ case "transport_id":
199
+ transportId = BigInt(value);
200
+ break;
201
+ }
202
+ }
203
+ if (!transportId) {
204
+ throw new Error(`No transport id for device ${serial}`);
205
+ }
206
+ devices.push({
207
+ serial,
208
+ authenticating: status === "unauthorized",
209
+ product,
210
+ model,
211
+ device,
212
+ transportId,
213
+ });
214
+ }
215
+ return devices;
216
+ }
217
+ /**
218
+ * `adb devices -l`
219
+ */
113
220
  async getDevices() {
114
- const connection = await this.connect("host:devices-l");
115
- const readable = new BufferedReadableStream(connection.readable);
221
+ const connection = await this.createConnection("host:devices-l");
116
222
  try {
117
- const devices = [];
118
- const response = await AdbServerClient.readString(readable);
119
- for (const line of response.split("\n")) {
120
- if (!line) {
121
- continue;
122
- }
123
- const parts = line.split(" ").filter(Boolean);
124
- const serial = parts[0];
125
- const status = parts[1];
126
- if (status !== "device") {
127
- continue;
128
- }
129
- let product;
130
- let model;
131
- let device;
132
- let transportId;
133
- for (let i = 2; i < parts.length; i += 1) {
134
- const [key, value] = parts[i].split(":");
135
- switch (key) {
136
- case "product":
137
- product = value;
138
- break;
139
- case "model":
140
- model = value;
141
- break;
142
- case "device":
143
- device = value;
144
- break;
145
- case "transport_id":
146
- transportId = BigInt(value);
147
- break;
148
- }
149
- }
150
- if (!transportId) {
151
- throw new Error(`No transport id for device ${serial}`);
152
- }
153
- devices.push({
154
- serial,
155
- product,
156
- model,
157
- device,
158
- transportId,
159
- });
223
+ const response = await connection.readString();
224
+ return this.parseDeviceList(response);
225
+ }
226
+ finally {
227
+ await connection.dispose();
228
+ }
229
+ }
230
+ /**
231
+ * Track the device list.
232
+ *
233
+ * @param signal An optional `AbortSignal` to stop tracking
234
+ *
235
+ * When `signal` is aborted, `trackDevices` will return normally, instead of throwing `signal.reason`.
236
+ */
237
+ async *trackDevices(signal) {
238
+ const connection = await this.createConnection("host:track-devices-l");
239
+ try {
240
+ while (true) {
241
+ const response = await raceSignal(async () => await connection.readString(), signal);
242
+ const devices = this.parseDeviceList(response);
243
+ yield devices;
244
+ }
245
+ }
246
+ catch (e) {
247
+ if (e === signal?.reason) {
248
+ return;
160
249
  }
161
- return devices;
162
250
  }
163
251
  finally {
164
- connection.writable.close().catch(NOOP);
165
- readable.cancel().catch(NOOP);
252
+ await connection.dispose();
166
253
  }
167
254
  }
168
255
  formatDeviceService(device, command) {
@@ -181,7 +268,21 @@ export class AdbServerClient {
181
268
  if ("tcp" in device) {
182
269
  return `host-local:${command}`;
183
270
  }
184
- throw new Error("Invalid device selector");
271
+ throw new TypeError("Invalid device selector");
272
+ }
273
+ /**
274
+ * `adb -s <device> reconnect` or `adb reconnect offline`
275
+ */
276
+ async reconnectDevice(device) {
277
+ const connection = await this.createConnection(device === "offline"
278
+ ? "host:reconnect-offline"
279
+ : this.formatDeviceService(device, "reconnect"));
280
+ try {
281
+ await connection.readString();
282
+ }
283
+ finally {
284
+ await connection.dispose();
285
+ }
185
286
  }
186
287
  /**
187
288
  * Gets the features supported by the device.
@@ -191,31 +292,41 @@ export class AdbServerClient {
191
292
  * @returns The transport ID of the selected device, and the features supported by the device.
192
293
  */
193
294
  async getDeviceFeatures(device) {
194
- // Usually the client sends a device command using `connectDevice`,
195
- // so the command got forwarded and handled by ADB daemon.
196
- // However, in fact, `connectDevice` only forwards unknown services to device,
197
- // if the service is a host command, it will still be handled by ADB server.
198
- // Also, if the command is about a device, but didn't specify a selector,
199
- // it will be executed against the device selected previously by `connectDevice`.
200
- // Using this method, we can get the transport ID and device features in one connection.
201
- const socket = await this.connectDevice(device, "host:features");
295
+ // On paper, `host:features` is a host service (device features are cached in host),
296
+ // so it shouldn't use `createDeviceConnection`,
297
+ // which is used to forward the service to the device.
298
+ //
299
+ // However, `createDeviceConnection` is a two step process:
300
+ //
301
+ // 1. Send a switch device service to host, to switch the connection to the device.
302
+ // 2. Send the actual service to host, let it forward the service to the device.
303
+ //
304
+ // In step 2, the host only forward the service to device if the service is unknown to host.
305
+ // If the service is a host service, it's still handled by host.
306
+ //
307
+ // Even better, if the service needs a device selector, but the selector is not provided,
308
+ // the service will be executed against the device selected by the switch device service.
309
+ // So we can use all device selector formats for the host service,
310
+ // and get the transport ID in the same time.
311
+ const connection = await this.createDeviceConnection(device, "host:features");
312
+ // Luckily `AdbServerClient.Socket` is compatible with `AdbServerClient.ServerConnection`
313
+ const stream = new AdbServerStream(connection);
202
314
  try {
203
- const readable = new BufferedReadableStream(socket.readable);
204
- const featuresString = await AdbServerClient.readString(readable);
315
+ const featuresString = await stream.readString();
205
316
  const features = featuresString.split(",");
206
- return { transportId: socket.transportId, features };
317
+ return { transportId: connection.transportId, features };
207
318
  }
208
319
  finally {
209
- await socket.close();
320
+ await stream.dispose();
210
321
  }
211
322
  }
212
323
  /**
213
324
  * Creates a connection that will forward the service to device.
214
325
  * @param device The device selector
215
326
  * @param service The service to forward
216
- * @returns An `AdbServerSocket` that can be used to communicate with the service
327
+ * @returns An `AdbServerClient.Socket` that can be used to communicate with the service
217
328
  */
218
- async connectDevice(device, service) {
329
+ async createDeviceConnection(device, service) {
219
330
  await this.validateVersion();
220
331
  let switchService;
221
332
  let transportId;
@@ -236,49 +347,46 @@ export class AdbServerClient {
236
347
  switchService = `host:tport:local`;
237
348
  }
238
349
  else {
239
- throw new Error("Invalid device selector");
350
+ throw new TypeError("Invalid device selector");
240
351
  }
241
- const connection = await this.connect(switchService);
352
+ const connection = await this.createConnection(switchService);
242
353
  try {
243
- const writer = connection.writable.getWriter();
244
- await AdbServerClient.writeString(writer, service);
245
- writer.releaseLock();
354
+ await connection.writeString(service);
246
355
  }
247
356
  catch (e) {
248
- await connection.readable.cancel();
249
- await connection.close();
357
+ await connection.dispose();
250
358
  throw e;
251
359
  }
252
- const readable = new BufferedReadableStream(connection.readable);
253
360
  try {
254
361
  if (transportId === undefined) {
255
- const array = await readable.readExactly(8);
256
- // TODO: switch to a more performant algorithm.
257
- const dataView = new DataView(array.buffer, array.byteOffset, array.byteLength);
258
- transportId = BigIntFieldType.Uint64.getter(dataView, 0, true);
362
+ const array = await connection.readExactly(8);
363
+ transportId = getUint64LittleEndian(array, 0);
259
364
  }
260
- await AdbServerClient.readOkay(readable);
365
+ await connection.readOkay();
366
+ const socket = connection.release();
261
367
  return {
262
368
  transportId,
263
369
  service,
264
- readable: readable.release(),
265
- writable: new WrapWritableStream(connection.writable).bePipedThroughFrom(new UnwrapConsumableStream()),
370
+ readable: socket.readable,
371
+ writable: new WrapWritableStream(socket.writable).bePipedThroughFrom(new MaybeConsumable.UnwrapStream()),
266
372
  get closed() {
267
- return connection.closed;
373
+ return socket.closed;
268
374
  },
269
375
  async close() {
270
- await connection.close();
376
+ await socket.close();
271
377
  },
272
378
  };
273
379
  }
274
380
  catch (e) {
275
- await readable.cancel().catch(NOOP);
276
- await connection.close();
381
+ await connection.dispose();
277
382
  throw e;
278
383
  }
279
384
  }
280
385
  /**
281
386
  * Wait for a device to be connected or disconnected.
387
+ *
388
+ * `adb wait-for-<state>`
389
+ *
282
390
  * @param device The device selector
283
391
  * @param state The state to wait for
284
392
  * @param options The options
@@ -302,14 +410,18 @@ export class AdbServerClient {
302
410
  type = "local";
303
411
  }
304
412
  else {
305
- throw new Error("Invalid device selector");
413
+ throw new TypeError("Invalid device selector");
306
414
  }
307
415
  // `waitFor` can't use `connectDevice`, because the device
308
416
  // might not be available yet.
309
417
  const service = this.formatDeviceService(device, `wait-for-${type}-${state}`);
310
- // `connect` resolves when server writes `OKAY`,
311
- // but for this command the server writes `OKAY` after the condition is met.
312
- await this.connect(service, options);
418
+ const connection = await this.createConnection(service, options);
419
+ try {
420
+ await connection.readOkay();
421
+ }
422
+ finally {
423
+ await connection.dispose();
424
+ }
313
425
  }
314
426
  async createTransport(device) {
315
427
  const { transportId, features } = await this.getDeviceFeatures(device);
@@ -345,4 +457,125 @@ export async function raceSignal(callback, ...signals) {
345
457
  }
346
458
  }
347
459
  }
460
+ (function (AdbServerClient) {
461
+ class NetworkError extends Error {
462
+ constructor(message) {
463
+ super(message);
464
+ this.name = "NetworkError";
465
+ }
466
+ }
467
+ AdbServerClient.NetworkError = NetworkError;
468
+ class UnauthorizedError extends Error {
469
+ constructor(message) {
470
+ super(message);
471
+ this.name = "UnauthorizedError";
472
+ }
473
+ }
474
+ AdbServerClient.UnauthorizedError = UnauthorizedError;
475
+ class AlreadyConnectedError extends Error {
476
+ constructor(message) {
477
+ super(message);
478
+ this.name = "AlreadyConnectedError";
479
+ }
480
+ }
481
+ AdbServerClient.AlreadyConnectedError = AlreadyConnectedError;
482
+ class WirelessCommands {
483
+ #client;
484
+ constructor(client) {
485
+ this.#client = client;
486
+ }
487
+ /**
488
+ * `adb pair <password> <address>`
489
+ */
490
+ async pair(address, password) {
491
+ const connection = await this.#client.createConnection(`host:pair:${password}:${address}`);
492
+ try {
493
+ const response = await connection.readExactly(4);
494
+ // `response` is either `FAIL`, or 4 hex digits for length of the string
495
+ if (sequenceEqual(response, FAIL)) {
496
+ throw new Error(await connection.readString());
497
+ }
498
+ const length = hexToNumber(response);
499
+ // Ignore the string as it's always `Successful ...`
500
+ await connection.readExactly(length);
501
+ }
502
+ finally {
503
+ await connection.dispose();
504
+ }
505
+ }
506
+ /**
507
+ * `adb connect <address>`
508
+ */
509
+ async connect(address) {
510
+ const connection = await this.#client.createConnection(`host:connect:${address}`);
511
+ try {
512
+ const response = await connection.readString();
513
+ switch (response) {
514
+ case `already connected to ${address}`:
515
+ throw new AdbServerClient.AlreadyConnectedError(response);
516
+ case `failed to connect to ${address}`: // `adb pair` mode not authorized
517
+ case `failed to authenticate to ${address}`: // `adb tcpip` mode not authorized
518
+ throw new AdbServerClient.UnauthorizedError(response);
519
+ case `connected to ${address}`:
520
+ return;
521
+ default:
522
+ throw new AdbServerClient.NetworkError(response);
523
+ }
524
+ }
525
+ finally {
526
+ await connection.dispose();
527
+ }
528
+ }
529
+ /**
530
+ * `adb disconnect <address>`
531
+ */
532
+ async disconnect(address) {
533
+ const connection = await this.#client.createConnection(`host:disconnect:${address}`);
534
+ try {
535
+ await connection.readString();
536
+ }
537
+ finally {
538
+ await connection.dispose();
539
+ }
540
+ }
541
+ }
542
+ AdbServerClient.WirelessCommands = WirelessCommands;
543
+ class MDnsCommands {
544
+ #client;
545
+ constructor(client) {
546
+ this.#client = client;
547
+ }
548
+ async check() {
549
+ const connection = await this.#client.createConnection("host:mdns:check");
550
+ try {
551
+ const response = await connection.readString();
552
+ return !response.startsWith("ERROR:");
553
+ }
554
+ finally {
555
+ await connection.dispose();
556
+ }
557
+ }
558
+ async getServices() {
559
+ const connection = await this.#client.createConnection("host:mdns:services");
560
+ try {
561
+ const response = await connection.readString();
562
+ return response
563
+ .split("\n")
564
+ .filter(Boolean)
565
+ .map((line) => {
566
+ const parts = line.split("\t");
567
+ return {
568
+ name: parts[0],
569
+ service: parts[1],
570
+ address: parts[2],
571
+ };
572
+ });
573
+ }
574
+ finally {
575
+ await connection.dispose();
576
+ }
577
+ }
578
+ }
579
+ AdbServerClient.MDnsCommands = MDnsCommands;
580
+ })(AdbServerClient || (AdbServerClient = {}));
348
581
  //# sourceMappingURL=client.js.map