@yume-chan/adb 0.0.19 → 0.0.21

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 (197) hide show
  1. package/CHANGELOG.json +57 -0
  2. package/CHANGELOG.md +26 -1
  3. package/README.md +222 -103
  4. package/esm/adb.d.ts +28 -45
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +34 -213
  7. package/esm/adb.js.map +1 -1
  8. package/esm/banner.d.ts +17 -0
  9. package/esm/banner.d.ts.map +1 -0
  10. package/esm/banner.js +67 -0
  11. package/esm/banner.js.map +1 -0
  12. package/esm/commands/base.d.ts.map +1 -1
  13. package/esm/commands/base.js.map +1 -1
  14. package/esm/commands/framebuffer.d.ts +9 -0
  15. package/esm/commands/framebuffer.d.ts.map +1 -1
  16. package/esm/commands/framebuffer.js +27 -3
  17. package/esm/commands/framebuffer.js.map +1 -1
  18. package/esm/commands/power.d.ts +1 -1
  19. package/esm/commands/power.d.ts.map +1 -1
  20. package/esm/commands/power.js +2 -2
  21. package/esm/commands/power.js.map +1 -1
  22. package/esm/commands/reverse.d.ts +17 -18
  23. package/esm/commands/reverse.d.ts.map +1 -1
  24. package/esm/commands/reverse.js +53 -48
  25. package/esm/commands/reverse.js.map +1 -1
  26. package/esm/commands/subprocess/command.d.ts +1 -1
  27. package/esm/commands/subprocess/command.d.ts.map +1 -1
  28. package/esm/commands/subprocess/command.js +15 -13
  29. package/esm/commands/subprocess/command.js.map +1 -1
  30. package/esm/commands/subprocess/protocols/none.d.ts +3 -8
  31. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  32. package/esm/commands/subprocess/protocols/none.js +16 -16
  33. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  34. package/esm/commands/subprocess/protocols/shell.d.ts +3 -9
  35. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  36. package/esm/commands/subprocess/protocols/shell.js +63 -57
  37. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  38. package/esm/commands/subprocess/protocols/types.d.ts +1 -2
  39. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  40. package/esm/commands/sync/list.js +2 -2
  41. package/esm/commands/sync/push.d.ts +14 -3
  42. package/esm/commands/sync/push.d.ts.map +1 -1
  43. package/esm/commands/sync/push.js +9 -9
  44. package/esm/commands/sync/push.js.map +1 -1
  45. package/esm/commands/sync/request.js +2 -2
  46. package/esm/commands/sync/request.js.map +1 -1
  47. package/esm/commands/sync/response.d.ts +5 -3
  48. package/esm/commands/sync/response.d.ts.map +1 -1
  49. package/esm/commands/sync/response.js +7 -5
  50. package/esm/commands/sync/response.js.map +1 -1
  51. package/esm/commands/sync/socket.d.ts +8 -14
  52. package/esm/commands/sync/socket.d.ts.map +1 -1
  53. package/esm/commands/sync/socket.js +35 -32
  54. package/esm/commands/sync/socket.js.map +1 -1
  55. package/esm/commands/sync/stat.js +4 -4
  56. package/esm/commands/sync/stat.js.map +1 -1
  57. package/esm/commands/sync/sync.d.ts +26 -18
  58. package/esm/commands/sync/sync.d.ts.map +1 -1
  59. package/esm/commands/sync/sync.js +36 -25
  60. package/esm/commands/sync/sync.js.map +1 -1
  61. package/esm/commands/tcpip.d.ts +2 -2
  62. package/esm/commands/tcpip.d.ts.map +1 -1
  63. package/esm/commands/tcpip.js +4 -2
  64. package/esm/commands/tcpip.js.map +1 -1
  65. package/esm/{auth.d.ts → daemon/auth.d.ts} +12 -11
  66. package/esm/daemon/auth.d.ts.map +1 -0
  67. package/esm/{auth.js → daemon/auth.js} +31 -22
  68. package/esm/daemon/auth.js.map +1 -0
  69. package/esm/{crypto.d.ts → daemon/crypto.d.ts} +5 -5
  70. package/esm/daemon/crypto.d.ts.map +1 -0
  71. package/esm/{crypto.js → daemon/crypto.js} +24 -28
  72. package/esm/daemon/crypto.js.map +1 -0
  73. package/esm/{backend.d.ts → daemon/device.d.ts} +2 -2
  74. package/esm/daemon/device.d.ts.map +1 -0
  75. package/esm/daemon/device.js +2 -0
  76. package/esm/daemon/device.js.map +1 -0
  77. package/esm/{socket → daemon}/dispatcher.d.ts +12 -30
  78. package/esm/daemon/dispatcher.d.ts.map +1 -0
  79. package/esm/{socket → daemon}/dispatcher.js +80 -78
  80. package/esm/daemon/dispatcher.js.map +1 -0
  81. package/esm/daemon/index.d.ts +8 -0
  82. package/esm/daemon/index.d.ts.map +1 -0
  83. package/esm/daemon/index.js +8 -0
  84. package/esm/daemon/index.js.map +1 -0
  85. package/esm/{packet.d.ts → daemon/packet.d.ts} +4 -4
  86. package/esm/daemon/packet.d.ts.map +1 -0
  87. package/esm/{packet.js → daemon/packet.js} +2 -2
  88. package/esm/{packet.js.map → daemon/packet.js.map} +1 -1
  89. package/esm/{socket → daemon}/socket.d.ts +15 -19
  90. package/esm/daemon/socket.d.ts.map +1 -0
  91. package/esm/{socket → daemon}/socket.js +49 -42
  92. package/esm/daemon/socket.js.map +1 -0
  93. package/esm/daemon/transport.d.ts +53 -0
  94. package/esm/daemon/transport.d.ts.map +1 -0
  95. package/esm/daemon/transport.js +186 -0
  96. package/esm/daemon/transport.js.map +1 -0
  97. package/esm/features.js +1 -1
  98. package/esm/features.js.map +1 -1
  99. package/esm/index.d.ts +3 -5
  100. package/esm/index.d.ts.map +1 -1
  101. package/esm/index.js +3 -5
  102. package/esm/index.js.map +1 -1
  103. package/esm/server/client.d.ts +78 -0
  104. package/esm/server/client.d.ts.map +1 -0
  105. package/esm/server/client.js +328 -0
  106. package/esm/server/client.js.map +1 -0
  107. package/esm/server/index.d.ts +3 -0
  108. package/esm/server/index.d.ts.map +1 -0
  109. package/esm/server/index.js +3 -0
  110. package/esm/server/index.js.map +1 -0
  111. package/esm/server/transport.d.ts +19 -0
  112. package/esm/server/transport.d.ts.map +1 -0
  113. package/esm/server/transport.js +44 -0
  114. package/esm/server/transport.js.map +1 -0
  115. package/esm/utils/auto-reset-event.d.ts +1 -2
  116. package/esm/utils/auto-reset-event.d.ts.map +1 -1
  117. package/esm/utils/auto-reset-event.js +12 -12
  118. package/esm/utils/auto-reset-event.js.map +1 -1
  119. package/esm/utils/base64.js +5 -5
  120. package/esm/utils/base64.js.map +1 -1
  121. package/esm/utils/conditional-variable.d.ts +1 -2
  122. package/esm/utils/conditional-variable.d.ts.map +1 -1
  123. package/esm/utils/conditional-variable.js +10 -10
  124. package/esm/utils/conditional-variable.js.map +1 -1
  125. package/esm/utils/hex.d.ts +3 -0
  126. package/esm/utils/hex.d.ts.map +1 -0
  127. package/esm/utils/hex.js +55 -0
  128. package/esm/utils/hex.js.map +1 -0
  129. package/esm/utils/index.d.ts +2 -0
  130. package/esm/utils/index.d.ts.map +1 -1
  131. package/esm/utils/index.js +2 -0
  132. package/esm/utils/index.js.map +1 -1
  133. package/esm/utils/no-op.d.ts +7 -0
  134. package/esm/utils/no-op.d.ts.map +1 -0
  135. package/esm/utils/no-op.js +12 -0
  136. package/esm/utils/no-op.js.map +1 -0
  137. package/package.json +12 -12
  138. package/src/adb.ts +75 -274
  139. package/src/banner.ts +82 -0
  140. package/src/commands/base.ts +1 -1
  141. package/src/commands/framebuffer.ts +31 -3
  142. package/src/commands/power.ts +10 -10
  143. package/src/commands/reverse.ts +77 -82
  144. package/src/commands/subprocess/command.ts +24 -25
  145. package/src/commands/subprocess/protocols/none.ts +29 -30
  146. package/src/commands/subprocess/protocols/shell.ts +87 -89
  147. package/src/commands/subprocess/protocols/types.ts +1 -2
  148. package/src/commands/sync/list.ts +7 -7
  149. package/src/commands/sync/pull.ts +4 -4
  150. package/src/commands/sync/push.ts +28 -16
  151. package/src/commands/sync/request.ts +4 -4
  152. package/src/commands/sync/response.ts +13 -11
  153. package/src/commands/sync/socket.ts +48 -46
  154. package/src/commands/sync/stat.ts +5 -5
  155. package/src/commands/sync/sync.ts +55 -43
  156. package/src/commands/tcpip.ts +6 -4
  157. package/src/{auth.ts → daemon/auth.ts} +62 -43
  158. package/src/{crypto.ts → daemon/crypto.ts} +33 -41
  159. package/src/{backend.ts → daemon/device.ts} +1 -1
  160. package/src/{socket → daemon}/dispatcher.ts +103 -109
  161. package/src/daemon/index.ts +7 -0
  162. package/src/{packet.ts → daemon/packet.ts} +6 -6
  163. package/src/{socket → daemon}/socket.ts +77 -67
  164. package/src/daemon/transport.ts +281 -0
  165. package/src/index.ts +3 -5
  166. package/src/server/client.ts +473 -0
  167. package/src/server/index.ts +2 -0
  168. package/src/server/transport.ts +77 -0
  169. package/src/utils/auto-reset-event.ts +16 -16
  170. package/src/utils/base64.ts +5 -5
  171. package/src/utils/conditional-variable.ts +14 -14
  172. package/src/utils/hex.ts +58 -0
  173. package/src/utils/index.ts +2 -0
  174. package/src/utils/no-op.ts +12 -0
  175. package/tsconfig.build.json +12 -1
  176. package/tsconfig.build.tsbuildinfo +1 -1
  177. package/esm/auth.d.ts.map +0 -1
  178. package/esm/auth.js.map +0 -1
  179. package/esm/backend.d.ts.map +0 -1
  180. package/esm/backend.js +0 -2
  181. package/esm/backend.js.map +0 -1
  182. package/esm/commands/install.d.ts +0 -10
  183. package/esm/commands/install.d.ts.map +0 -1
  184. package/esm/commands/install.js +0 -29
  185. package/esm/commands/install.js.map +0 -1
  186. package/esm/crypto.d.ts.map +0 -1
  187. package/esm/crypto.js.map +0 -1
  188. package/esm/packet.d.ts.map +0 -1
  189. package/esm/socket/dispatcher.d.ts.map +0 -1
  190. package/esm/socket/dispatcher.js.map +0 -1
  191. package/esm/socket/index.d.ts +0 -3
  192. package/esm/socket/index.d.ts.map +0 -1
  193. package/esm/socket/index.js +0 -3
  194. package/esm/socket/index.js.map +0 -1
  195. package/esm/socket/socket.d.ts.map +0 -1
  196. package/esm/socket/socket.js.map +0 -1
  197. package/src/socket/index.ts +0 -2
@@ -0,0 +1,473 @@
1
+ // cspell:ignore tport
2
+
3
+ import { PromiseResolver } from "@yume-chan/async";
4
+ import type {
5
+ AbortSignal,
6
+ Consumable,
7
+ ReadableWritablePair,
8
+ WritableStreamDefaultWriter,
9
+ } from "@yume-chan/stream-extra";
10
+ import {
11
+ BufferedReadableStream,
12
+ DuplexStreamFactory,
13
+ UnwrapConsumableStream,
14
+ WrapWritableStream,
15
+ } from "@yume-chan/stream-extra";
16
+ import type {
17
+ AsyncExactReadable,
18
+ ExactReadable,
19
+ ValueOrPromise,
20
+ } from "@yume-chan/struct";
21
+ import {
22
+ BigIntFieldType,
23
+ SyncPromise,
24
+ decodeUtf8,
25
+ encodeUtf8,
26
+ } from "@yume-chan/struct";
27
+
28
+ import type { AdbIncomingSocketHandler, AdbSocket } from "../adb.js";
29
+ import { AdbBanner } from "../banner.js";
30
+ import type { AdbFeature } from "../features.js";
31
+ import { NOOP, hexToNumber, numberToHex } from "../utils/index.js";
32
+
33
+ import { AdbServerTransport } from "./transport.js";
34
+
35
+ export interface AdbServerConnectionOptions {
36
+ unref?: boolean | undefined;
37
+ signal?: AbortSignal | undefined;
38
+ }
39
+
40
+ export interface AdbServerConnection {
41
+ connect(
42
+ options?: AdbServerConnectionOptions,
43
+ ): ValueOrPromise<ReadableWritablePair<Uint8Array, Uint8Array>>;
44
+
45
+ addReverseTunnel(
46
+ handler: AdbIncomingSocketHandler,
47
+ address?: string,
48
+ ): ValueOrPromise<string>;
49
+
50
+ removeReverseTunnel(address: string): ValueOrPromise<void>;
51
+
52
+ clearReverseTunnels(): ValueOrPromise<void>;
53
+ }
54
+
55
+ export interface AdbServerSocket extends AdbSocket {
56
+ transportId: bigint;
57
+ }
58
+
59
+ export type AdbServerDeviceSelector =
60
+ | {
61
+ serial: string;
62
+ }
63
+ | { transportId: bigint }
64
+ | { usb: true }
65
+ | { tcp: true }
66
+ | undefined;
67
+
68
+ export interface AdbServerDevice {
69
+ serial: string;
70
+ product?: string | undefined;
71
+ model?: string | undefined;
72
+ device?: string | undefined;
73
+ transportId: bigint;
74
+ }
75
+
76
+ export class AdbServerClient {
77
+ static readonly VERSION = 41;
78
+
79
+ readonly connection: AdbServerConnection;
80
+
81
+ constructor(connection: AdbServerConnection) {
82
+ this.connection = connection;
83
+ }
84
+
85
+ static readString(stream: ExactReadable): string;
86
+ static readString(stream: AsyncExactReadable): PromiseLike<string>;
87
+ static readString(
88
+ stream: ExactReadable | AsyncExactReadable,
89
+ ): string | PromiseLike<string> {
90
+ return SyncPromise.try(() => stream.readExactly(4))
91
+ .then((buffer) => {
92
+ const length = hexToNumber(buffer);
93
+ return stream.readExactly(length);
94
+ })
95
+ .then((valueBuffer) => {
96
+ return decodeUtf8(valueBuffer);
97
+ })
98
+ .valueOrPromise();
99
+ }
100
+
101
+ static async writeString(
102
+ writer: WritableStreamDefaultWriter<Uint8Array>,
103
+ value: string,
104
+ ): Promise<void> {
105
+ const valueBuffer = encodeUtf8(value);
106
+ const buffer = new Uint8Array(4 + valueBuffer.length);
107
+ buffer.set(numberToHex(valueBuffer.length));
108
+ buffer.set(valueBuffer, 4);
109
+ await writer.write(buffer);
110
+ }
111
+
112
+ static async readOkay(
113
+ stream: ExactReadable | AsyncExactReadable,
114
+ ): Promise<void> {
115
+ const response = decodeUtf8(await stream.readExactly(4));
116
+ if (response === "OKAY") {
117
+ return;
118
+ }
119
+
120
+ if (response === "FAIL") {
121
+ const reason = await AdbServerClient.readString(stream);
122
+ throw new Error(reason);
123
+ }
124
+
125
+ throw new Error(`Unexpected response: ${response}`);
126
+ }
127
+
128
+ async connect(
129
+ request: string,
130
+ options?: AdbServerConnectionOptions,
131
+ ): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
132
+ const connection = await this.connection.connect(options);
133
+
134
+ const writer = connection.writable.getWriter();
135
+ await AdbServerClient.writeString(writer, request);
136
+
137
+ const readable = new BufferedReadableStream(connection.readable);
138
+
139
+ try {
140
+ // `raceSignal` throws if the signal is aborted,
141
+ // so the `catch` block can close the connection.
142
+ await raceSignal(
143
+ () => AdbServerClient.readOkay(readable),
144
+ options?.signal,
145
+ );
146
+
147
+ writer.releaseLock();
148
+ return {
149
+ readable: readable.release(),
150
+ writable: connection.writable,
151
+ };
152
+ } catch (e) {
153
+ writer.close().catch(NOOP);
154
+ readable.cancel().catch(NOOP);
155
+ throw e;
156
+ }
157
+ }
158
+
159
+ async getVersion(): Promise<number> {
160
+ const connection = await this.connect("host:version");
161
+ const readable = new BufferedReadableStream(connection.readable);
162
+ try {
163
+ const length = hexToNumber(await readable.readExactly(4));
164
+ const version = hexToNumber(await readable.readExactly(length));
165
+ return version;
166
+ } finally {
167
+ connection.writable.close().catch(NOOP);
168
+ readable.cancel().catch(NOOP);
169
+ }
170
+ }
171
+
172
+ async validateVersion() {
173
+ const version = await this.getVersion();
174
+ if (version !== AdbServerClient.VERSION) {
175
+ throw new Error(
176
+ `adb server version (${version}) doesn't match this client (${AdbServerClient.VERSION})`,
177
+ );
178
+ }
179
+ }
180
+
181
+ async killServer(): Promise<void> {
182
+ const connection = await this.connect("host:kill");
183
+ connection.writable.close().catch(NOOP);
184
+ connection.readable.cancel().catch(NOOP);
185
+ }
186
+
187
+ async getServerFeatures(): Promise<AdbFeature[]> {
188
+ const connection = await this.connect("host:host-features");
189
+ const readable = new BufferedReadableStream(connection.readable);
190
+ try {
191
+ const response = await AdbServerClient.readString(readable);
192
+ return response.split(",") as AdbFeature[];
193
+ } finally {
194
+ connection.writable.close().catch(NOOP);
195
+ readable.cancel().catch(NOOP);
196
+ }
197
+ }
198
+
199
+ async getDevices(): Promise<AdbServerDevice[]> {
200
+ const connection = await this.connect("host:devices-l");
201
+ const readable = new BufferedReadableStream(connection.readable);
202
+ try {
203
+ const devices: AdbServerDevice[] = [];
204
+ 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;
250
+ } finally {
251
+ connection.writable.close().catch(NOOP);
252
+ readable.cancel().catch(NOOP);
253
+ }
254
+ }
255
+
256
+ formatDeviceService(device: AdbServerDeviceSelector, command: string) {
257
+ if (!device) {
258
+ return `host:${command}`;
259
+ }
260
+ if ("transportId" in device) {
261
+ return `host-transport-id:${device.transportId}:${command}`;
262
+ }
263
+ if ("serial" in device) {
264
+ return `host-serial:${device.serial}:${command}`;
265
+ }
266
+ if ("usb" in device) {
267
+ return `host-usb:${command}`;
268
+ }
269
+ if ("tcp" in device) {
270
+ return `host-local:${command}`;
271
+ }
272
+ throw new Error("Invalid device selector");
273
+ }
274
+
275
+ /**
276
+ * Gets the features supported by the device.
277
+ * The transport ID of the selected device is also returned,
278
+ * so the caller can execute other commands against the same device.
279
+ * @param device The device selector
280
+ * @returns The transport ID of the selected device, and the features supported by the device.
281
+ */
282
+ async getDeviceFeatures(
283
+ device: AdbServerDeviceSelector,
284
+ ): Promise<{ transportId: bigint; features: AdbFeature[] }> {
285
+ // Usually the client sends a device command using `connectDevice`,
286
+ // so the command got forwarded and handled by ADB daemon.
287
+ // However, in fact, `connectDevice` only forwards unknown services to device,
288
+ // if the service is a host command, it will still be handled by ADB server.
289
+ // Also, if the command is about a device, but didn't specify a selector,
290
+ // it will be executed against the device selected previously by `connectDevice`.
291
+ // Using this method, we can get the transport ID and device features in one connection.
292
+ const socket = await this.connectDevice(device, "host:features");
293
+ try {
294
+ const readable = new BufferedReadableStream(socket.readable);
295
+ const featuresString = await AdbServerClient.readString(readable);
296
+ const features = featuresString.split(",") as AdbFeature[];
297
+ return { transportId: socket.transportId, features };
298
+ } finally {
299
+ await socket.close();
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Creates a connection that will forward the service to device.
305
+ * @param device The device selector
306
+ * @param service The service to forward
307
+ * @returns An `AdbServerSocket` that can be used to communicate with the service
308
+ */
309
+ async connectDevice(
310
+ device: AdbServerDeviceSelector,
311
+ service: string,
312
+ ): Promise<AdbServerSocket> {
313
+ await this.validateVersion();
314
+
315
+ let switchService: string;
316
+ let transportId: bigint | undefined;
317
+ if (!device) {
318
+ switchService = `host:tport:any`;
319
+ } else if ("transportId" in device) {
320
+ switchService = `host:transport-id:${device.transportId}`;
321
+ transportId = device.transportId;
322
+ } else if ("serial" in device) {
323
+ switchService = `host:tport:serial:${device.serial}`;
324
+ } else if ("usb" in device) {
325
+ switchService = `host:tport:usb`;
326
+ } else if ("tcp" in device) {
327
+ switchService = `host:tport:local`;
328
+ } else {
329
+ throw new Error("Invalid device selector");
330
+ }
331
+
332
+ const connection = await this.connect(switchService);
333
+ const readable = new BufferedReadableStream(connection.readable);
334
+ const writer = connection.writable.getWriter();
335
+ try {
336
+ if (transportId === undefined) {
337
+ const array = await readable.readExactly(8);
338
+ // TODO: switch to a more performant algorithm.
339
+ const dataView = new DataView(
340
+ array.buffer,
341
+ array.byteOffset,
342
+ array.byteLength,
343
+ );
344
+ transportId = BigIntFieldType.Uint64.getter(dataView, 0, true);
345
+ }
346
+
347
+ await AdbServerClient.writeString(writer, service);
348
+ await AdbServerClient.readOkay(readable);
349
+
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
+ return {
364
+ transportId,
365
+ service,
366
+ readable: wrapReadable,
367
+ writable: wrapWritable,
368
+ close() {
369
+ return duplex.close();
370
+ },
371
+ };
372
+ } catch (e) {
373
+ writer.close().catch(NOOP);
374
+ readable.cancel().catch(NOOP);
375
+ throw e;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Wait for a device to be connected or disconnected.
381
+ * @param device The device selector
382
+ * @param state The state to wait for
383
+ * @param options The options
384
+ * @returns A promise that resolves when the condition is met.
385
+ */
386
+ async waitFor(
387
+ device: AdbServerDeviceSelector,
388
+ state: "device" | "disconnect",
389
+ options?: AdbServerConnectionOptions,
390
+ ): Promise<void> {
391
+ let type: string;
392
+ if (!device) {
393
+ type = "any";
394
+ } else if ("transportId" in device) {
395
+ type = "any";
396
+ } else if ("serial" in device) {
397
+ type = "any";
398
+ } else if ("usb" in device) {
399
+ type = "usb";
400
+ } else if ("tcp" in device) {
401
+ type = "local";
402
+ } else {
403
+ throw new Error("Invalid device selector");
404
+ }
405
+
406
+ // `waitFor` can't use `connectDevice`, because the device
407
+ // might not be available yet.
408
+ const service = this.formatDeviceService(
409
+ device,
410
+ `wait-for-${type}-${state}`,
411
+ );
412
+
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);
416
+ }
417
+
418
+ async createTransport(
419
+ device: AdbServerDeviceSelector,
420
+ ): Promise<AdbServerTransport> {
421
+ const { transportId, features } = await this.getDeviceFeatures(device);
422
+
423
+ const devices = await this.getDevices();
424
+ const info = devices.find(
425
+ (device) => device.transportId === transportId,
426
+ );
427
+
428
+ const banner = new AdbBanner(
429
+ info?.product,
430
+ info?.model,
431
+ info?.device,
432
+ features,
433
+ );
434
+
435
+ return new AdbServerTransport(
436
+ this,
437
+ info?.serial ?? "",
438
+ banner,
439
+ transportId,
440
+ );
441
+ }
442
+ }
443
+
444
+ async function raceSignal<T>(
445
+ callback: () => Promise<T>,
446
+ ...signals: (AbortSignal | undefined)[]
447
+ ): Promise<T> {
448
+ const abortPromise = new PromiseResolver<never>();
449
+ function abort(this: AbortSignal) {
450
+ abortPromise.reject(this.reason);
451
+ }
452
+
453
+ try {
454
+ for (const signal of signals) {
455
+ if (!signal) {
456
+ continue;
457
+ }
458
+ if (signal.aborted) {
459
+ throw signal.reason;
460
+ }
461
+ signal.addEventListener("abort", abort);
462
+ }
463
+
464
+ return await Promise.race([callback(), abortPromise.promise]);
465
+ } finally {
466
+ for (const signal of signals) {
467
+ if (!signal) {
468
+ continue;
469
+ }
470
+ signal.removeEventListener("abort", abort);
471
+ }
472
+ }
473
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./client.js";
2
+ export * from "./transport.js";
@@ -0,0 +1,77 @@
1
+ import { PromiseResolver } from "@yume-chan/async";
2
+ import { AbortController } from "@yume-chan/stream-extra";
3
+ import type { ValueOrPromise } from "@yume-chan/struct";
4
+
5
+ import type {
6
+ AdbIncomingSocketHandler,
7
+ AdbSocket,
8
+ AdbTransport,
9
+ } from "../adb.js";
10
+ import type { AdbBanner } from "../banner.js";
11
+
12
+ import type { AdbServerClient } from "./client.js";
13
+
14
+ export class AdbServerTransport implements AdbTransport {
15
+ #client: AdbServerClient;
16
+
17
+ readonly serial: string;
18
+
19
+ readonly transportId: bigint;
20
+
21
+ readonly maxPayloadSize: number = 1 * 1024 * 1024;
22
+
23
+ readonly banner: AdbBanner;
24
+
25
+ #closed = new PromiseResolver<void>();
26
+ #waitAbortController = new AbortController();
27
+ readonly disconnected: Promise<void>;
28
+
29
+ constructor(
30
+ client: AdbServerClient,
31
+ serial: string,
32
+ banner: AdbBanner,
33
+ transportId: bigint,
34
+ ) {
35
+ this.#client = client;
36
+ this.serial = serial;
37
+ this.banner = banner;
38
+ this.transportId = transportId;
39
+
40
+ this.disconnected = Promise.race([
41
+ this.#closed.promise,
42
+ client.waitFor({ transportId }, "disconnect", {
43
+ signal: this.#waitAbortController.signal,
44
+ unref: true,
45
+ }),
46
+ ]);
47
+ }
48
+
49
+ async connect(service: string): Promise<AdbSocket> {
50
+ return await this.#client.connectDevice(
51
+ {
52
+ transportId: this.transportId,
53
+ },
54
+ service,
55
+ );
56
+ }
57
+
58
+ async addReverseTunnel(
59
+ handler: AdbIncomingSocketHandler,
60
+ address?: string,
61
+ ): Promise<string> {
62
+ return await this.#client.connection.addReverseTunnel(handler, address);
63
+ }
64
+
65
+ async removeReverseTunnel(address: string): Promise<void> {
66
+ await this.#client.connection.removeReverseTunnel(address);
67
+ }
68
+
69
+ async clearReverseTunnels(): Promise<void> {
70
+ await this.#client.connection.clearReverseTunnels();
71
+ }
72
+
73
+ close(): ValueOrPromise<void> {
74
+ this.#closed.resolve();
75
+ this.#waitAbortController.abort();
76
+ }
77
+ }
@@ -2,39 +2,39 @@ import { PromiseResolver } from "@yume-chan/async";
2
2
  import type { Disposable } from "@yume-chan/event";
3
3
 
4
4
  export class AutoResetEvent implements Disposable {
5
- private _set: boolean;
6
- private readonly _queue: PromiseResolver<void>[] = [];
5
+ #set: boolean;
6
+ readonly #queue: PromiseResolver<void>[] = [];
7
7
 
8
- public constructor(initialSet = false) {
9
- this._set = initialSet;
8
+ constructor(initialSet = false) {
9
+ this.#set = initialSet;
10
10
  }
11
11
 
12
- public wait(): Promise<void> {
13
- if (!this._set) {
14
- this._set = true;
12
+ wait(): Promise<void> {
13
+ if (!this.#set) {
14
+ this.#set = true;
15
15
 
16
- if (this._queue.length === 0) {
16
+ if (this.#queue.length === 0) {
17
17
  return Promise.resolve();
18
18
  }
19
19
  }
20
20
 
21
21
  const resolver = new PromiseResolver<void>();
22
- this._queue.push(resolver);
22
+ this.#queue.push(resolver);
23
23
  return resolver.promise;
24
24
  }
25
25
 
26
- public notifyOne() {
27
- if (this._queue.length !== 0) {
28
- this._queue.pop()!.resolve();
26
+ notifyOne() {
27
+ if (this.#queue.length !== 0) {
28
+ this.#queue.pop()!.resolve();
29
29
  } else {
30
- this._set = false;
30
+ this.#set = false;
31
31
  }
32
32
  }
33
33
 
34
- public dispose() {
35
- for (const item of this._queue) {
34
+ dispose() {
35
+ for (const item of this.#queue) {
36
36
  item.reject(new Error("The AutoResetEvent has been disposed"));
37
37
  }
38
- this._queue.length = 0;
38
+ this.#queue.length = 0;
39
39
  }
40
40
  }
@@ -26,7 +26,7 @@ addRange("/", "/");
26
26
  * @returns Length of the output in bytes
27
27
  */
28
28
  export function calculateBase64EncodedLength(
29
- inputLength: number
29
+ inputLength: number,
30
30
  ): [outputLength: number, paddingLength: number] {
31
31
  const remainder = inputLength % 3;
32
32
  const paddingLength = remainder !== 0 ? 3 - remainder : 0;
@@ -54,10 +54,10 @@ export function encodeBase64(input: Uint8Array): Uint8Array;
54
54
  export function encodeBase64(input: Uint8Array, output: Uint8Array): number;
55
55
  export function encodeBase64(
56
56
  input: Uint8Array,
57
- output?: Uint8Array
57
+ output?: Uint8Array,
58
58
  ): Uint8Array | number {
59
59
  const [outputLength, paddingLength] = calculateBase64EncodedLength(
60
- input.length
60
+ input.length,
61
61
  );
62
62
 
63
63
  if (!output) {
@@ -123,7 +123,7 @@ export function encodeBase64(
123
123
  function encodeForward(
124
124
  input: Uint8Array,
125
125
  output: Uint8Array,
126
- paddingLength: number
126
+ paddingLength: number,
127
127
  ) {
128
128
  let inputIndex = 0;
129
129
  let outputIndex = 0;
@@ -200,7 +200,7 @@ function encodeForward(
200
200
  function encodeBackward(
201
201
  input: Uint8Array,
202
202
  output: Uint8Array,
203
- paddingLength: number
203
+ paddingLength: number,
204
204
  ) {
205
205
  let inputIndex = input.length - 1;
206
206
  let outputIndex = output.length - 1;
@@ -7,39 +7,39 @@ interface WaitEntry {
7
7
  }
8
8
 
9
9
  export class ConditionalVariable implements Disposable {
10
- private _locked = false;
11
- private readonly _queue: WaitEntry[] = [];
10
+ #locked = false;
11
+ readonly #queue: WaitEntry[] = [];
12
12
 
13
- public wait(condition: () => boolean): Promise<void> {
14
- if (!this._locked) {
15
- this._locked = true;
16
- if (this._queue.length === 0 && condition()) {
13
+ wait(condition: () => boolean): Promise<void> {
14
+ if (!this.#locked) {
15
+ this.#locked = true;
16
+ if (this.#queue.length === 0 && condition()) {
17
17
  return Promise.resolve();
18
18
  }
19
19
  }
20
20
 
21
21
  const resolver = new PromiseResolver<void>();
22
- this._queue.push({ condition, resolver });
22
+ this.#queue.push({ condition, resolver });
23
23
  return resolver.promise;
24
24
  }
25
25
 
26
- public notifyOne() {
27
- const entry = this._queue.shift();
26
+ notifyOne() {
27
+ const entry = this.#queue.shift();
28
28
  if (entry) {
29
29
  if (entry.condition()) {
30
30
  entry.resolver.resolve();
31
31
  }
32
32
  } else {
33
- this._locked = false;
33
+ this.#locked = false;
34
34
  }
35
35
  }
36
36
 
37
- public dispose(): void {
38
- for (const item of this._queue) {
37
+ dispose(): void {
38
+ for (const item of this.#queue) {
39
39
  item.resolver.reject(
40
- new Error("The ConditionalVariable has been disposed")
40
+ new Error("The ConditionalVariable has been disposed"),
41
41
  );
42
42
  }
43
- this._queue.length = 0;
43
+ this.#queue.length = 0;
44
44
  }
45
45
  }