@yume-chan/adb 0.0.18 → 0.0.20

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