@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.
- package/CHANGELOG.json +57 -0
- package/CHANGELOG.md +26 -1
- package/README.md +222 -103
- package/esm/adb.d.ts +28 -45
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +34 -213
- package/esm/adb.js.map +1 -1
- package/esm/banner.d.ts +17 -0
- package/esm/banner.d.ts.map +1 -0
- package/esm/banner.js +67 -0
- package/esm/banner.js.map +1 -0
- package/esm/commands/base.d.ts.map +1 -1
- package/esm/commands/base.js.map +1 -1
- package/esm/commands/framebuffer.d.ts +9 -0
- package/esm/commands/framebuffer.d.ts.map +1 -1
- package/esm/commands/framebuffer.js +27 -3
- package/esm/commands/framebuffer.js.map +1 -1
- package/esm/commands/power.d.ts +1 -1
- package/esm/commands/power.d.ts.map +1 -1
- package/esm/commands/power.js +2 -2
- package/esm/commands/power.js.map +1 -1
- package/esm/commands/reverse.d.ts +17 -18
- package/esm/commands/reverse.d.ts.map +1 -1
- package/esm/commands/reverse.js +53 -48
- package/esm/commands/reverse.js.map +1 -1
- package/esm/commands/subprocess/command.d.ts +1 -1
- package/esm/commands/subprocess/command.d.ts.map +1 -1
- package/esm/commands/subprocess/command.js +15 -13
- package/esm/commands/subprocess/command.js.map +1 -1
- package/esm/commands/subprocess/protocols/none.d.ts +3 -8
- package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/none.js +16 -16
- package/esm/commands/subprocess/protocols/none.js.map +1 -1
- package/esm/commands/subprocess/protocols/shell.d.ts +3 -9
- package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/shell.js +63 -57
- package/esm/commands/subprocess/protocols/shell.js.map +1 -1
- package/esm/commands/subprocess/protocols/types.d.ts +1 -2
- package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
- package/esm/commands/sync/list.js +2 -2
- package/esm/commands/sync/push.d.ts +14 -3
- package/esm/commands/sync/push.d.ts.map +1 -1
- package/esm/commands/sync/push.js +9 -9
- package/esm/commands/sync/push.js.map +1 -1
- package/esm/commands/sync/request.js +2 -2
- package/esm/commands/sync/request.js.map +1 -1
- package/esm/commands/sync/response.d.ts +5 -3
- package/esm/commands/sync/response.d.ts.map +1 -1
- package/esm/commands/sync/response.js +7 -5
- package/esm/commands/sync/response.js.map +1 -1
- package/esm/commands/sync/socket.d.ts +8 -14
- package/esm/commands/sync/socket.d.ts.map +1 -1
- package/esm/commands/sync/socket.js +35 -32
- package/esm/commands/sync/socket.js.map +1 -1
- package/esm/commands/sync/stat.js +4 -4
- package/esm/commands/sync/stat.js.map +1 -1
- package/esm/commands/sync/sync.d.ts +26 -18
- package/esm/commands/sync/sync.d.ts.map +1 -1
- package/esm/commands/sync/sync.js +36 -25
- package/esm/commands/sync/sync.js.map +1 -1
- package/esm/commands/tcpip.d.ts +2 -2
- package/esm/commands/tcpip.d.ts.map +1 -1
- package/esm/commands/tcpip.js +4 -2
- package/esm/commands/tcpip.js.map +1 -1
- package/esm/{auth.d.ts → daemon/auth.d.ts} +12 -11
- package/esm/daemon/auth.d.ts.map +1 -0
- package/esm/{auth.js → daemon/auth.js} +31 -22
- package/esm/daemon/auth.js.map +1 -0
- package/esm/{crypto.d.ts → daemon/crypto.d.ts} +5 -5
- package/esm/daemon/crypto.d.ts.map +1 -0
- package/esm/{crypto.js → daemon/crypto.js} +24 -28
- package/esm/daemon/crypto.js.map +1 -0
- package/esm/{backend.d.ts → daemon/device.d.ts} +2 -2
- package/esm/daemon/device.d.ts.map +1 -0
- package/esm/daemon/device.js +2 -0
- package/esm/daemon/device.js.map +1 -0
- package/esm/{socket → daemon}/dispatcher.d.ts +12 -30
- package/esm/daemon/dispatcher.d.ts.map +1 -0
- package/esm/{socket → daemon}/dispatcher.js +80 -78
- package/esm/daemon/dispatcher.js.map +1 -0
- package/esm/daemon/index.d.ts +8 -0
- package/esm/daemon/index.d.ts.map +1 -0
- package/esm/daemon/index.js +8 -0
- package/esm/daemon/index.js.map +1 -0
- package/esm/{packet.d.ts → daemon/packet.d.ts} +4 -4
- package/esm/daemon/packet.d.ts.map +1 -0
- package/esm/{packet.js → daemon/packet.js} +2 -2
- package/esm/{packet.js.map → daemon/packet.js.map} +1 -1
- package/esm/{socket → daemon}/socket.d.ts +15 -19
- package/esm/daemon/socket.d.ts.map +1 -0
- package/esm/{socket → daemon}/socket.js +49 -42
- package/esm/daemon/socket.js.map +1 -0
- package/esm/daemon/transport.d.ts +53 -0
- package/esm/daemon/transport.d.ts.map +1 -0
- package/esm/daemon/transport.js +186 -0
- package/esm/daemon/transport.js.map +1 -0
- package/esm/features.js +1 -1
- package/esm/features.js.map +1 -1
- package/esm/index.d.ts +3 -5
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -5
- package/esm/index.js.map +1 -1
- package/esm/server/client.d.ts +78 -0
- package/esm/server/client.d.ts.map +1 -0
- package/esm/server/client.js +328 -0
- package/esm/server/client.js.map +1 -0
- package/esm/server/index.d.ts +3 -0
- package/esm/server/index.d.ts.map +1 -0
- package/esm/server/index.js +3 -0
- package/esm/server/index.js.map +1 -0
- package/esm/server/transport.d.ts +19 -0
- package/esm/server/transport.d.ts.map +1 -0
- package/esm/server/transport.js +44 -0
- package/esm/server/transport.js.map +1 -0
- package/esm/utils/auto-reset-event.d.ts +1 -2
- package/esm/utils/auto-reset-event.d.ts.map +1 -1
- package/esm/utils/auto-reset-event.js +12 -12
- package/esm/utils/auto-reset-event.js.map +1 -1
- package/esm/utils/base64.js +5 -5
- package/esm/utils/base64.js.map +1 -1
- package/esm/utils/conditional-variable.d.ts +1 -2
- package/esm/utils/conditional-variable.d.ts.map +1 -1
- package/esm/utils/conditional-variable.js +10 -10
- package/esm/utils/conditional-variable.js.map +1 -1
- package/esm/utils/hex.d.ts +3 -0
- package/esm/utils/hex.d.ts.map +1 -0
- package/esm/utils/hex.js +55 -0
- package/esm/utils/hex.js.map +1 -0
- package/esm/utils/index.d.ts +2 -0
- package/esm/utils/index.d.ts.map +1 -1
- package/esm/utils/index.js +2 -0
- package/esm/utils/index.js.map +1 -1
- package/esm/utils/no-op.d.ts +7 -0
- package/esm/utils/no-op.d.ts.map +1 -0
- package/esm/utils/no-op.js +12 -0
- package/esm/utils/no-op.js.map +1 -0
- package/package.json +12 -12
- package/src/adb.ts +75 -274
- package/src/banner.ts +82 -0
- package/src/commands/base.ts +1 -1
- package/src/commands/framebuffer.ts +31 -3
- package/src/commands/power.ts +10 -10
- package/src/commands/reverse.ts +77 -82
- package/src/commands/subprocess/command.ts +24 -25
- package/src/commands/subprocess/protocols/none.ts +29 -30
- package/src/commands/subprocess/protocols/shell.ts +87 -89
- package/src/commands/subprocess/protocols/types.ts +1 -2
- package/src/commands/sync/list.ts +7 -7
- package/src/commands/sync/pull.ts +4 -4
- package/src/commands/sync/push.ts +28 -16
- package/src/commands/sync/request.ts +4 -4
- package/src/commands/sync/response.ts +13 -11
- package/src/commands/sync/socket.ts +48 -46
- package/src/commands/sync/stat.ts +5 -5
- package/src/commands/sync/sync.ts +55 -43
- package/src/commands/tcpip.ts +6 -4
- package/src/{auth.ts → daemon/auth.ts} +62 -43
- package/src/{crypto.ts → daemon/crypto.ts} +33 -41
- package/src/{backend.ts → daemon/device.ts} +1 -1
- package/src/{socket → daemon}/dispatcher.ts +103 -109
- package/src/daemon/index.ts +7 -0
- package/src/{packet.ts → daemon/packet.ts} +6 -6
- package/src/{socket → daemon}/socket.ts +77 -67
- package/src/daemon/transport.ts +281 -0
- package/src/index.ts +3 -5
- package/src/server/client.ts +473 -0
- package/src/server/index.ts +2 -0
- package/src/server/transport.ts +77 -0
- package/src/utils/auto-reset-event.ts +16 -16
- package/src/utils/base64.ts +5 -5
- package/src/utils/conditional-variable.ts +14 -14
- package/src/utils/hex.ts +58 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/no-op.ts +12 -0
- package/tsconfig.build.json +12 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/esm/auth.d.ts.map +0 -1
- package/esm/auth.js.map +0 -1
- package/esm/backend.d.ts.map +0 -1
- package/esm/backend.js +0 -2
- package/esm/backend.js.map +0 -1
- package/esm/commands/install.d.ts +0 -10
- package/esm/commands/install.d.ts.map +0 -1
- package/esm/commands/install.js +0 -29
- package/esm/commands/install.js.map +0 -1
- package/esm/crypto.d.ts.map +0 -1
- package/esm/crypto.js.map +0 -1
- package/esm/packet.d.ts.map +0 -1
- package/esm/socket/dispatcher.d.ts.map +0 -1
- package/esm/socket/dispatcher.js.map +0 -1
- package/esm/socket/index.d.ts +0 -3
- package/esm/socket/index.d.ts.map +0 -1
- package/esm/socket/index.js +0 -3
- package/esm/socket/index.js.map +0 -1
- package/esm/socket/socket.d.ts.map +0 -1
- package/esm/socket/socket.js.map +0 -1
- 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,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
|
-
|
|
6
|
-
|
|
5
|
+
#set: boolean;
|
|
6
|
+
readonly #queue: PromiseResolver<void>[] = [];
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
this
|
|
8
|
+
constructor(initialSet = false) {
|
|
9
|
+
this.#set = initialSet;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
if (!this
|
|
14
|
-
this
|
|
12
|
+
wait(): Promise<void> {
|
|
13
|
+
if (!this.#set) {
|
|
14
|
+
this.#set = true;
|
|
15
15
|
|
|
16
|
-
if (this.
|
|
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.
|
|
22
|
+
this.#queue.push(resolver);
|
|
23
23
|
return resolver.promise;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
if (this.
|
|
28
|
-
this.
|
|
26
|
+
notifyOne() {
|
|
27
|
+
if (this.#queue.length !== 0) {
|
|
28
|
+
this.#queue.pop()!.resolve();
|
|
29
29
|
} else {
|
|
30
|
-
this
|
|
30
|
+
this.#set = false;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
for (const item of this
|
|
34
|
+
dispose() {
|
|
35
|
+
for (const item of this.#queue) {
|
|
36
36
|
item.reject(new Error("The AutoResetEvent has been disposed"));
|
|
37
37
|
}
|
|
38
|
-
this.
|
|
38
|
+
this.#queue.length = 0;
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/utils/base64.ts
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
10
|
+
#locked = false;
|
|
11
|
+
readonly #queue: WaitEntry[] = [];
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
if (!this
|
|
15
|
-
this
|
|
16
|
-
if (this.
|
|
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.
|
|
22
|
+
this.#queue.push({ condition, resolver });
|
|
23
23
|
return resolver.promise;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
const entry = this.
|
|
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
|
|
33
|
+
this.#locked = false;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
for (const item of this
|
|
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.
|
|
43
|
+
this.#queue.length = 0;
|
|
44
44
|
}
|
|
45
45
|
}
|