@yume-chan/adb 0.0.23 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +33 -0
- package/CHANGELOG.md +15 -1
- package/LICENSE +1 -1
- package/esm/adb.d.ts +2 -2
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +2 -2
- package/esm/adb.js.map +1 -1
- package/esm/commands/power.d.ts.map +1 -1
- package/esm/commands/power.js +6 -5
- package/esm/commands/power.js.map +1 -1
- package/esm/commands/reverse.d.ts.map +1 -1
- package/esm/commands/reverse.js +22 -10
- package/esm/commands/reverse.js.map +1 -1
- package/esm/commands/subprocess/command.js +3 -3
- package/esm/commands/subprocess/command.js.map +1 -1
- package/esm/commands/subprocess/protocols/none.d.ts +2 -2
- package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/shell.d.ts +3 -3
- package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
- package/esm/commands/subprocess/protocols/shell.js +5 -6
- package/esm/commands/subprocess/protocols/shell.js.map +1 -1
- package/esm/commands/subprocess/protocols/types.d.ts +2 -2
- package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
- package/esm/commands/sync/list.d.ts +2 -5
- package/esm/commands/sync/list.d.ts.map +1 -1
- package/esm/commands/sync/list.js +2 -4
- package/esm/commands/sync/list.js.map +1 -1
- package/esm/commands/sync/pull.d.ts +1 -4
- package/esm/commands/sync/pull.d.ts.map +1 -1
- package/esm/commands/sync/pull.js +2 -2
- package/esm/commands/sync/pull.js.map +1 -1
- package/esm/commands/sync/push.d.ts +4 -7
- package/esm/commands/sync/push.d.ts.map +1 -1
- package/esm/commands/sync/push.js +3 -6
- package/esm/commands/sync/push.js.map +1 -1
- package/esm/commands/sync/request.d.ts +13 -18
- package/esm/commands/sync/request.d.ts.map +1 -1
- package/esm/commands/sync/request.js +23 -28
- package/esm/commands/sync/request.js.map +1 -1
- package/esm/commands/sync/response.d.ts +18 -12
- package/esm/commands/sync/response.d.ts.map +1 -1
- package/esm/commands/sync/response.js +39 -17
- package/esm/commands/sync/response.js.map +1 -1
- package/esm/commands/sync/socket.d.ts +2 -2
- package/esm/commands/sync/socket.d.ts.map +1 -1
- package/esm/commands/sync/socket.js +10 -5
- package/esm/commands/sync/socket.js.map +1 -1
- package/esm/commands/sync/stat.d.ts +0 -3
- package/esm/commands/sync/stat.d.ts.map +1 -1
- package/esm/commands/sync/stat.js +0 -2
- package/esm/commands/sync/stat.js.map +1 -1
- package/esm/commands/sync/sync.d.ts +4 -3
- package/esm/commands/sync/sync.d.ts.map +1 -1
- package/esm/commands/sync/sync.js +4 -1
- package/esm/commands/sync/sync.js.map +1 -1
- package/esm/commands/tcpip.js +1 -1
- package/esm/commands/tcpip.js.map +1 -1
- package/esm/daemon/auth.js +1 -1
- package/esm/daemon/auth.js.map +1 -1
- package/esm/daemon/crypto.d.ts +3 -2
- package/esm/daemon/crypto.d.ts.map +1 -1
- package/esm/daemon/crypto.js +56 -49
- package/esm/daemon/crypto.js.map +1 -1
- package/esm/daemon/dispatcher.d.ts +35 -10
- package/esm/daemon/dispatcher.d.ts.map +1 -1
- package/esm/daemon/dispatcher.js +44 -31
- package/esm/daemon/dispatcher.js.map +1 -1
- package/esm/daemon/packet.d.ts +2 -2
- package/esm/daemon/packet.d.ts.map +1 -1
- package/esm/daemon/packet.js +13 -12
- package/esm/daemon/packet.js.map +1 -1
- package/esm/daemon/socket.d.ts +10 -5
- package/esm/daemon/socket.d.ts.map +1 -1
- package/esm/daemon/socket.js +17 -15
- package/esm/daemon/socket.js.map +1 -1
- package/esm/daemon/transport.d.ts +55 -15
- package/esm/daemon/transport.d.ts.map +1 -1
- package/esm/daemon/transport.js +9 -10
- package/esm/daemon/transport.js.map +1 -1
- package/esm/server/client.d.ts +128 -50
- package/esm/server/client.d.ts.map +1 -1
- package/esm/server/client.js +325 -124
- package/esm/server/client.js.map +1 -1
- package/esm/server/transport.js +4 -4
- package/esm/server/transport.js.map +1 -1
- package/esm/utils/base64.d.ts.map +1 -1
- package/esm/utils/base64.js +33 -22
- package/esm/utils/base64.js.map +1 -1
- package/esm/utils/hex.d.ts +1 -1
- package/esm/utils/hex.d.ts.map +1 -1
- package/esm/utils/hex.js +12 -14
- package/esm/utils/hex.js.map +1 -1
- package/esm/utils/index.d.ts +1 -1
- package/esm/utils/index.d.ts.map +1 -1
- package/esm/utils/index.js +1 -1
- package/esm/utils/index.js.map +1 -1
- package/esm/utils/sequence-equal.d.ts +2 -0
- package/esm/utils/sequence-equal.d.ts.map +1 -0
- package/esm/utils/sequence-equal.js +12 -0
- package/esm/utils/sequence-equal.js.map +1 -0
- package/package.json +12 -14
- package/src/adb.ts +7 -4
- package/src/commands/power.ts +7 -5
- package/src/commands/reverse.ts +30 -17
- package/src/commands/subprocess/command.ts +3 -3
- package/src/commands/subprocess/protocols/none.ts +2 -2
- package/src/commands/subprocess/protocols/shell.ts +7 -11
- package/src/commands/subprocess/protocols/types.ts +2 -2
- package/src/commands/sync/list.ts +2 -4
- package/src/commands/sync/pull.ts +2 -2
- package/src/commands/sync/push.ts +7 -10
- package/src/commands/sync/request.ts +32 -32
- package/src/commands/sync/response.ts +47 -20
- package/src/commands/sync/socket.ts +13 -8
- package/src/commands/sync/stat.ts +0 -2
- package/src/commands/sync/sync.ts +8 -3
- package/src/commands/tcpip.ts +1 -1
- package/src/daemon/auth.ts +1 -1
- package/src/daemon/crypto.ts +64 -55
- package/src/daemon/dispatcher.ts +104 -42
- package/src/daemon/packet.ts +21 -14
- package/src/daemon/socket.ts +28 -25
- package/src/daemon/transport.ts +71 -24
- package/src/server/client.ts +420 -208
- package/src/server/transport.ts +4 -4
- package/src/utils/base64.ts +33 -22
- package/src/utils/hex.ts +16 -14
- package/src/utils/index.ts +1 -1
- package/src/utils/sequence-equal.ts +13 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/esm/utils/conditional-variable.d.ts +0 -8
- package/esm/utils/conditional-variable.d.ts.map +0 -1
- package/esm/utils/conditional-variable.js +0 -34
- package/esm/utils/conditional-variable.js.map +0 -1
- package/src/utils/conditional-variable.ts +0 -45
package/src/server/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// cspell:ignore tport
|
|
2
2
|
|
|
3
3
|
import { PromiseResolver } from "@yume-chan/async";
|
|
4
|
+
import { getUint64LittleEndian } from "@yume-chan/no-data-view";
|
|
4
5
|
import type {
|
|
5
6
|
AbortSignal,
|
|
6
7
|
ReadableWritablePair,
|
|
@@ -8,16 +9,11 @@ import type {
|
|
|
8
9
|
} from "@yume-chan/stream-extra";
|
|
9
10
|
import {
|
|
10
11
|
BufferedReadableStream,
|
|
11
|
-
|
|
12
|
+
MaybeConsumable,
|
|
12
13
|
WrapWritableStream,
|
|
13
14
|
} from "@yume-chan/stream-extra";
|
|
14
|
-
import type {
|
|
15
|
-
AsyncExactReadable,
|
|
16
|
-
ExactReadable,
|
|
17
|
-
ValueOrPromise,
|
|
18
|
-
} from "@yume-chan/struct";
|
|
15
|
+
import type { ValueOrPromise } from "@yume-chan/struct";
|
|
19
16
|
import {
|
|
20
|
-
BigIntFieldType,
|
|
21
17
|
EMPTY_UINT8_ARRAY,
|
|
22
18
|
SyncPromise,
|
|
23
19
|
decodeUtf8,
|
|
@@ -27,163 +23,164 @@ import {
|
|
|
27
23
|
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
|
28
24
|
import { AdbBanner } from "../banner.js";
|
|
29
25
|
import type { AdbFeature } from "../features.js";
|
|
30
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
NOOP,
|
|
28
|
+
hexToNumber,
|
|
29
|
+
sequenceEqual,
|
|
30
|
+
write4HexDigits,
|
|
31
|
+
} from "../utils/index.js";
|
|
31
32
|
|
|
32
33
|
import { AdbServerTransport } from "./transport.js";
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
signal?: AbortSignal | undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface AdbServerConnection
|
|
40
|
-
extends ReadableWritablePair<Uint8Array, Uint8Array>,
|
|
41
|
-
Closeable {
|
|
42
|
-
get closed(): Promise<void>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface AdbServerConnector {
|
|
46
|
-
connect(
|
|
47
|
-
options?: AdbServerConnectionOptions,
|
|
48
|
-
): ValueOrPromise<AdbServerConnection>;
|
|
49
|
-
|
|
50
|
-
addReverseTunnel(
|
|
51
|
-
handler: AdbIncomingSocketHandler,
|
|
52
|
-
address?: string,
|
|
53
|
-
): ValueOrPromise<string>;
|
|
54
|
-
|
|
55
|
-
removeReverseTunnel(address: string): ValueOrPromise<void>;
|
|
35
|
+
const OKAY = encodeUtf8("OKAY");
|
|
36
|
+
const FAIL = encodeUtf8("FAIL");
|
|
56
37
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
transportId: bigint;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type AdbServerDeviceSelector =
|
|
65
|
-
| { transportId: bigint }
|
|
66
|
-
| { serial: string }
|
|
67
|
-
| { usb: true }
|
|
68
|
-
| { tcp: true }
|
|
69
|
-
| undefined;
|
|
70
|
-
|
|
71
|
-
export interface AdbServerDevice {
|
|
72
|
-
serial: string;
|
|
73
|
-
product?: string | undefined;
|
|
74
|
-
model?: string | undefined;
|
|
75
|
-
device?: string | undefined;
|
|
76
|
-
transportId: bigint;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export class AdbServerClient {
|
|
80
|
-
static readonly VERSION = 41;
|
|
38
|
+
class AdbServerStream {
|
|
39
|
+
#connection: AdbServerClient.ServerConnection;
|
|
40
|
+
#buffered: BufferedReadableStream;
|
|
41
|
+
#writer: WritableStreamDefaultWriter<Uint8Array>;
|
|
81
42
|
|
|
82
|
-
|
|
43
|
+
constructor(connection: AdbServerClient.ServerConnection) {
|
|
44
|
+
this.#connection = connection;
|
|
45
|
+
this.#buffered = new BufferedReadableStream(connection.readable);
|
|
46
|
+
this.#writer = connection.writable.getWriter();
|
|
47
|
+
}
|
|
83
48
|
|
|
84
|
-
|
|
85
|
-
this.
|
|
49
|
+
readExactly(length: number): ValueOrPromise<Uint8Array> {
|
|
50
|
+
return this.#buffered.readExactly(length);
|
|
86
51
|
}
|
|
87
52
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
static readString(
|
|
91
|
-
stream: ExactReadable | AsyncExactReadable,
|
|
92
|
-
): string | PromiseLike<string> {
|
|
93
|
-
return SyncPromise.try(() => stream.readExactly(4))
|
|
53
|
+
readString() {
|
|
54
|
+
return SyncPromise.try(() => this.readExactly(4))
|
|
94
55
|
.then((buffer) => {
|
|
95
56
|
const length = hexToNumber(buffer);
|
|
96
57
|
if (length === 0) {
|
|
97
58
|
return EMPTY_UINT8_ARRAY;
|
|
98
59
|
} else {
|
|
99
|
-
return
|
|
60
|
+
return this.readExactly(length);
|
|
100
61
|
}
|
|
101
62
|
})
|
|
102
|
-
.then((
|
|
103
|
-
|
|
63
|
+
.then((buffer) => {
|
|
64
|
+
// TODO: Investigate using stream mode `TextDecoder` for long strings.
|
|
65
|
+
// Because concatenating strings uses rope data structure,
|
|
66
|
+
// which only points to the original strings and doesn't copy the data,
|
|
67
|
+
// it's more efficient than concatenating `Uint8Array`s.
|
|
68
|
+
//
|
|
69
|
+
// ```
|
|
70
|
+
// const decoder = new TextDecoder();
|
|
71
|
+
// let result = '';
|
|
72
|
+
// for await (const chunk of stream.iterateExactly(length)) {
|
|
73
|
+
// result += decoder.decode(chunk, { stream: true });
|
|
74
|
+
// }
|
|
75
|
+
// result += decoder.decode();
|
|
76
|
+
// return result;
|
|
77
|
+
// ```
|
|
78
|
+
//
|
|
79
|
+
// Although, it will be super complex to use `SyncPromise` with async iterator,
|
|
80
|
+
// `stream.iterateExactly` need to return an
|
|
81
|
+
// `Iterator<Uint8Array | Promise<Uint8Array>>` instead of a true async iterator.
|
|
82
|
+
// Maybe `SyncPromise` should support async iterators directly.
|
|
83
|
+
return decodeUtf8(buffer);
|
|
104
84
|
})
|
|
105
85
|
.valueOrPromise();
|
|
106
86
|
}
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
buffer.set(
|
|
115
|
-
|
|
116
|
-
await writer.write(buffer);
|
|
88
|
+
async writeString(value: string): Promise<void> {
|
|
89
|
+
// TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
|
|
90
|
+
// That way allocates a new string (hopefully only a rope) instead of a new buffer
|
|
91
|
+
const encoded = encodeUtf8(value);
|
|
92
|
+
const buffer = new Uint8Array(4 + encoded.length);
|
|
93
|
+
write4HexDigits(buffer, 0, encoded.length);
|
|
94
|
+
buffer.set(encoded, 4);
|
|
95
|
+
await this.#writer.write(buffer);
|
|
117
96
|
}
|
|
118
97
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
98
|
+
async readOkay(): Promise<void> {
|
|
99
|
+
const response = await this.readExactly(4);
|
|
100
|
+
if (sequenceEqual(response, OKAY)) {
|
|
101
|
+
// `OKAY` is followed by data length and data
|
|
102
|
+
// But different services want to parse the data differently
|
|
103
|
+
// So don't read the data here
|
|
124
104
|
return;
|
|
125
105
|
}
|
|
126
106
|
|
|
127
|
-
if (response
|
|
128
|
-
const reason = await
|
|
107
|
+
if (sequenceEqual(response, FAIL)) {
|
|
108
|
+
const reason = await this.readString();
|
|
129
109
|
throw new Error(reason);
|
|
130
110
|
}
|
|
131
111
|
|
|
132
|
-
throw new Error(`Unexpected response: ${response}`);
|
|
112
|
+
throw new Error(`Unexpected response: ${decodeUtf8(response)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
release() {
|
|
116
|
+
this.#writer.releaseLock();
|
|
117
|
+
return {
|
|
118
|
+
readable: this.#buffered.release(),
|
|
119
|
+
writable: this.#connection.writable,
|
|
120
|
+
closed: this.#connection.closed,
|
|
121
|
+
close: () => this.#connection.close(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async dispose() {
|
|
126
|
+
await this.#buffered.cancel().catch(NOOP);
|
|
127
|
+
await this.#writer.close().catch(NOOP);
|
|
128
|
+
try {
|
|
129
|
+
await this.#connection.close();
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class AdbServerClient {
|
|
137
|
+
static readonly VERSION = 41;
|
|
138
|
+
|
|
139
|
+
readonly connector: AdbServerClient.ServerConnector;
|
|
140
|
+
|
|
141
|
+
readonly wireless = new AdbServerClient.WirelessCommands(this);
|
|
142
|
+
readonly mDns = new AdbServerClient.MDnsCommands(this);
|
|
143
|
+
|
|
144
|
+
constructor(connection: AdbServerClient.ServerConnector) {
|
|
145
|
+
this.connector = connection;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
|
-
async
|
|
148
|
+
async createConnection(
|
|
136
149
|
request: string,
|
|
137
|
-
options?:
|
|
138
|
-
): Promise<
|
|
139
|
-
const connection = await this.
|
|
150
|
+
options?: AdbServerClient.ServerConnectionOptions,
|
|
151
|
+
): Promise<AdbServerStream> {
|
|
152
|
+
const connection = await this.connector.connect(options);
|
|
153
|
+
const stream = new AdbServerStream(connection);
|
|
140
154
|
|
|
141
155
|
try {
|
|
142
|
-
|
|
143
|
-
await AdbServerClient.writeString(writer, request);
|
|
144
|
-
writer.releaseLock();
|
|
156
|
+
await stream.writeString(request);
|
|
145
157
|
} catch (e) {
|
|
146
|
-
await
|
|
147
|
-
await connection.close();
|
|
158
|
+
await stream.dispose();
|
|
148
159
|
throw e;
|
|
149
160
|
}
|
|
150
161
|
|
|
151
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
152
162
|
try {
|
|
153
163
|
// `raceSignal` throws when the signal is aborted,
|
|
154
164
|
// so the `catch` block can close the connection.
|
|
155
|
-
await raceSignal(
|
|
156
|
-
|
|
157
|
-
options?.signal,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
readable: readable.release(),
|
|
162
|
-
writable: connection.writable,
|
|
163
|
-
get closed() {
|
|
164
|
-
return connection.closed;
|
|
165
|
-
},
|
|
166
|
-
async close() {
|
|
167
|
-
await connection.close();
|
|
168
|
-
},
|
|
169
|
-
};
|
|
165
|
+
await raceSignal(() => stream.readOkay(), options?.signal);
|
|
166
|
+
return stream;
|
|
170
167
|
} catch (e) {
|
|
171
|
-
await
|
|
172
|
-
await connection.close();
|
|
168
|
+
await stream.dispose();
|
|
173
169
|
throw e;
|
|
174
170
|
}
|
|
175
171
|
}
|
|
176
172
|
|
|
173
|
+
/**
|
|
174
|
+
* `adb version`
|
|
175
|
+
*/
|
|
177
176
|
async getVersion(): Promise<number> {
|
|
178
|
-
const connection = await this.
|
|
179
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
177
|
+
const connection = await this.createConnection("host:version");
|
|
180
178
|
try {
|
|
181
|
-
const length = hexToNumber(await
|
|
182
|
-
const version = hexToNumber(await
|
|
179
|
+
const length = hexToNumber(await connection.readExactly(4));
|
|
180
|
+
const version = hexToNumber(await connection.readExactly(length));
|
|
183
181
|
return version;
|
|
184
182
|
} finally {
|
|
185
|
-
connection.
|
|
186
|
-
readable.cancel().catch(NOOP);
|
|
183
|
+
await connection.dispose();
|
|
187
184
|
}
|
|
188
185
|
}
|
|
189
186
|
|
|
@@ -196,26 +193,29 @@ export class AdbServerClient {
|
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
|
|
196
|
+
/**
|
|
197
|
+
* `adb kill-server`
|
|
198
|
+
*/
|
|
199
199
|
async killServer(): Promise<void> {
|
|
200
|
-
const connection = await this.
|
|
201
|
-
connection.
|
|
202
|
-
connection.readable.cancel().catch(NOOP);
|
|
200
|
+
const connection = await this.createConnection("host:kill");
|
|
201
|
+
await connection.dispose();
|
|
203
202
|
}
|
|
204
203
|
|
|
204
|
+
/**
|
|
205
|
+
* `adb host-features`
|
|
206
|
+
*/
|
|
205
207
|
async getServerFeatures(): Promise<AdbFeature[]> {
|
|
206
|
-
const connection = await this.
|
|
207
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
208
|
+
const connection = await this.createConnection("host:host-features");
|
|
208
209
|
try {
|
|
209
|
-
const response = await
|
|
210
|
+
const response = await connection.readString();
|
|
210
211
|
return response.split(",") as AdbFeature[];
|
|
211
212
|
} finally {
|
|
212
|
-
connection.
|
|
213
|
-
readable.cancel().catch(NOOP);
|
|
213
|
+
await connection.dispose();
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
parseDeviceList(value: string):
|
|
218
|
-
const devices:
|
|
217
|
+
parseDeviceList(value: string): AdbServerClient.Device[] {
|
|
218
|
+
const devices: AdbServerClient.Device[] = [];
|
|
219
219
|
for (const line of value.split("\n")) {
|
|
220
220
|
if (!line) {
|
|
221
221
|
continue;
|
|
@@ -224,7 +224,7 @@ export class AdbServerClient {
|
|
|
224
224
|
const parts = line.split(" ").filter(Boolean);
|
|
225
225
|
const serial = parts[0]!;
|
|
226
226
|
const status = parts[1]!;
|
|
227
|
-
if (status !== "device") {
|
|
227
|
+
if (status !== "device" && status !== "unauthorized") {
|
|
228
228
|
continue;
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -254,6 +254,7 @@ export class AdbServerClient {
|
|
|
254
254
|
}
|
|
255
255
|
devices.push({
|
|
256
256
|
serial,
|
|
257
|
+
authenticating: status === "unauthorized",
|
|
257
258
|
product,
|
|
258
259
|
model,
|
|
259
260
|
device,
|
|
@@ -263,43 +264,52 @@ export class AdbServerClient {
|
|
|
263
264
|
return devices;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
/**
|
|
268
|
+
* `adb devices -l`
|
|
269
|
+
*/
|
|
270
|
+
async getDevices(): Promise<AdbServerClient.Device[]> {
|
|
271
|
+
const connection = await this.createConnection("host:devices-l");
|
|
269
272
|
try {
|
|
270
|
-
const response = await
|
|
273
|
+
const response = await connection.readString();
|
|
271
274
|
return this.parseDeviceList(response);
|
|
272
275
|
} finally {
|
|
273
|
-
connection.
|
|
274
|
-
readable.cancel().catch(NOOP);
|
|
276
|
+
await connection.dispose();
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Track the device list.
|
|
282
|
+
*
|
|
283
|
+
* @param signal An optional `AbortSignal` to stop tracking
|
|
284
|
+
*
|
|
285
|
+
* When `signal` is aborted, `trackDevices` will return normally, instead of throwing `signal.reason`.
|
|
286
|
+
*/
|
|
287
|
+
async *trackDevices(
|
|
288
|
+
signal?: AbortSignal,
|
|
289
|
+
): AsyncGenerator<AdbServerClient.Device[], void, void> {
|
|
290
|
+
const connection = await this.createConnection("host:track-devices-l");
|
|
291
|
+
try {
|
|
292
|
+
while (true) {
|
|
293
|
+
const response = await raceSignal(
|
|
294
|
+
async () => await connection.readString(),
|
|
295
|
+
signal,
|
|
296
|
+
);
|
|
297
|
+
const devices = this.parseDeviceList(response);
|
|
298
|
+
yield devices;
|
|
293
299
|
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
} catch (e) {
|
|
301
|
+
if (e === signal?.reason) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
} finally {
|
|
305
|
+
await connection.dispose();
|
|
306
|
+
}
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
formatDeviceService(
|
|
309
|
+
formatDeviceService(
|
|
310
|
+
device: AdbServerClient.DeviceSelector,
|
|
311
|
+
command: string,
|
|
312
|
+
) {
|
|
303
313
|
if (!device) {
|
|
304
314
|
return `host:${command}`;
|
|
305
315
|
}
|
|
@@ -315,7 +325,23 @@ export class AdbServerClient {
|
|
|
315
325
|
if ("tcp" in device) {
|
|
316
326
|
return `host-local:${command}`;
|
|
317
327
|
}
|
|
318
|
-
throw new
|
|
328
|
+
throw new TypeError("Invalid device selector");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* `adb -s <device> reconnect` or `adb reconnect offline`
|
|
333
|
+
*/
|
|
334
|
+
async reconnectDevice(device: AdbServerClient.DeviceSelector | "offline") {
|
|
335
|
+
const connection = await this.createConnection(
|
|
336
|
+
device === "offline"
|
|
337
|
+
? "host:reconnect-offline"
|
|
338
|
+
: this.formatDeviceService(device, "reconnect"),
|
|
339
|
+
);
|
|
340
|
+
try {
|
|
341
|
+
await connection.readString();
|
|
342
|
+
} finally {
|
|
343
|
+
await connection.dispose();
|
|
344
|
+
}
|
|
319
345
|
}
|
|
320
346
|
|
|
321
347
|
/**
|
|
@@ -326,23 +352,36 @@ export class AdbServerClient {
|
|
|
326
352
|
* @returns The transport ID of the selected device, and the features supported by the device.
|
|
327
353
|
*/
|
|
328
354
|
async getDeviceFeatures(
|
|
329
|
-
device:
|
|
355
|
+
device: AdbServerClient.DeviceSelector,
|
|
330
356
|
): Promise<{ transportId: bigint; features: AdbFeature[] }> {
|
|
331
|
-
//
|
|
332
|
-
// so
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
357
|
+
// On paper, `host:features` is a host service (device features are cached in host),
|
|
358
|
+
// so it shouldn't use `createDeviceConnection`,
|
|
359
|
+
// which is used to forward the service to the device.
|
|
360
|
+
//
|
|
361
|
+
// However, `createDeviceConnection` is a two step process:
|
|
362
|
+
//
|
|
363
|
+
// 1. Send a switch device service to host, to switch the connection to the device.
|
|
364
|
+
// 2. Send the actual service to host, let it forward the service to the device.
|
|
365
|
+
//
|
|
366
|
+
// In step 2, the host only forward the service to device if the service is unknown to host.
|
|
367
|
+
// If the service is a host service, it's still handled by host.
|
|
368
|
+
//
|
|
369
|
+
// Even better, if the service needs a device selector, but the selector is not provided,
|
|
370
|
+
// the service will be executed against the device selected by the switch device service.
|
|
371
|
+
// So we can use all device selector formats for the host service,
|
|
372
|
+
// and get the transport ID in the same time.
|
|
373
|
+
const connection = await this.createDeviceConnection(
|
|
374
|
+
device,
|
|
375
|
+
"host:features",
|
|
376
|
+
);
|
|
377
|
+
// Luckily `AdbServerClient.Socket` is compatible with `AdbServerClient.ServerConnection`
|
|
378
|
+
const stream = new AdbServerStream(connection);
|
|
339
379
|
try {
|
|
340
|
-
const
|
|
341
|
-
const featuresString = await AdbServerClient.readString(readable);
|
|
380
|
+
const featuresString = await stream.readString();
|
|
342
381
|
const features = featuresString.split(",") as AdbFeature[];
|
|
343
|
-
return { transportId:
|
|
382
|
+
return { transportId: connection.transportId, features };
|
|
344
383
|
} finally {
|
|
345
|
-
await
|
|
384
|
+
await stream.dispose();
|
|
346
385
|
}
|
|
347
386
|
}
|
|
348
387
|
|
|
@@ -350,12 +389,12 @@ export class AdbServerClient {
|
|
|
350
389
|
* Creates a connection that will forward the service to device.
|
|
351
390
|
* @param device The device selector
|
|
352
391
|
* @param service The service to forward
|
|
353
|
-
* @returns An `
|
|
392
|
+
* @returns An `AdbServerClient.Socket` that can be used to communicate with the service
|
|
354
393
|
*/
|
|
355
|
-
async
|
|
356
|
-
device:
|
|
394
|
+
async createDeviceConnection(
|
|
395
|
+
device: AdbServerClient.DeviceSelector,
|
|
357
396
|
service: string,
|
|
358
|
-
): Promise<
|
|
397
|
+
): Promise<AdbServerClient.Socket> {
|
|
359
398
|
await this.validateVersion();
|
|
360
399
|
|
|
361
400
|
let switchService: string;
|
|
@@ -372,68 +411,62 @@ export class AdbServerClient {
|
|
|
372
411
|
} else if ("tcp" in device) {
|
|
373
412
|
switchService = `host:tport:local`;
|
|
374
413
|
} else {
|
|
375
|
-
throw new
|
|
414
|
+
throw new TypeError("Invalid device selector");
|
|
376
415
|
}
|
|
377
416
|
|
|
378
|
-
const connection = await this.
|
|
417
|
+
const connection = await this.createConnection(switchService);
|
|
379
418
|
|
|
380
419
|
try {
|
|
381
|
-
|
|
382
|
-
await AdbServerClient.writeString(writer, service);
|
|
383
|
-
writer.releaseLock();
|
|
420
|
+
await connection.writeString(service);
|
|
384
421
|
} catch (e) {
|
|
385
|
-
await connection.
|
|
386
|
-
await connection.close();
|
|
422
|
+
await connection.dispose();
|
|
387
423
|
throw e;
|
|
388
424
|
}
|
|
389
425
|
|
|
390
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
391
426
|
try {
|
|
392
427
|
if (transportId === undefined) {
|
|
393
|
-
const array = await
|
|
394
|
-
|
|
395
|
-
const dataView = new DataView(
|
|
396
|
-
array.buffer,
|
|
397
|
-
array.byteOffset,
|
|
398
|
-
array.byteLength,
|
|
399
|
-
);
|
|
400
|
-
transportId = BigIntFieldType.Uint64.getter(dataView, 0, true);
|
|
428
|
+
const array = await connection.readExactly(8);
|
|
429
|
+
transportId = getUint64LittleEndian(array, 0);
|
|
401
430
|
}
|
|
402
431
|
|
|
403
|
-
await
|
|
432
|
+
await connection.readOkay();
|
|
433
|
+
|
|
434
|
+
const socket = connection.release();
|
|
404
435
|
|
|
405
436
|
return {
|
|
406
437
|
transportId,
|
|
407
438
|
service,
|
|
408
|
-
readable: readable
|
|
439
|
+
readable: socket.readable,
|
|
409
440
|
writable: new WrapWritableStream(
|
|
410
|
-
|
|
411
|
-
).bePipedThroughFrom(new
|
|
441
|
+
socket.writable,
|
|
442
|
+
).bePipedThroughFrom(new MaybeConsumable.UnwrapStream()),
|
|
412
443
|
get closed() {
|
|
413
|
-
return
|
|
444
|
+
return socket.closed;
|
|
414
445
|
},
|
|
415
446
|
async close() {
|
|
416
|
-
await
|
|
447
|
+
await socket.close();
|
|
417
448
|
},
|
|
418
449
|
};
|
|
419
450
|
} catch (e) {
|
|
420
|
-
await
|
|
421
|
-
await connection.close();
|
|
451
|
+
await connection.dispose();
|
|
422
452
|
throw e;
|
|
423
453
|
}
|
|
424
454
|
}
|
|
425
455
|
|
|
426
456
|
/**
|
|
427
457
|
* Wait for a device to be connected or disconnected.
|
|
458
|
+
*
|
|
459
|
+
* `adb wait-for-<state>`
|
|
460
|
+
*
|
|
428
461
|
* @param device The device selector
|
|
429
462
|
* @param state The state to wait for
|
|
430
463
|
* @param options The options
|
|
431
464
|
* @returns A promise that resolves when the condition is met.
|
|
432
465
|
*/
|
|
433
466
|
async waitFor(
|
|
434
|
-
device:
|
|
467
|
+
device: AdbServerClient.DeviceSelector,
|
|
435
468
|
state: "device" | "disconnect",
|
|
436
|
-
options?:
|
|
469
|
+
options?: AdbServerClient.ServerConnectionOptions,
|
|
437
470
|
): Promise<void> {
|
|
438
471
|
let type: string;
|
|
439
472
|
if (!device) {
|
|
@@ -447,7 +480,7 @@ export class AdbServerClient {
|
|
|
447
480
|
} else if ("tcp" in device) {
|
|
448
481
|
type = "local";
|
|
449
482
|
} else {
|
|
450
|
-
throw new
|
|
483
|
+
throw new TypeError("Invalid device selector");
|
|
451
484
|
}
|
|
452
485
|
|
|
453
486
|
// `waitFor` can't use `connectDevice`, because the device
|
|
@@ -457,16 +490,16 @@ export class AdbServerClient {
|
|
|
457
490
|
`wait-for-${type}-${state}`,
|
|
458
491
|
);
|
|
459
492
|
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
493
|
+
const connection = await this.createConnection(service, options);
|
|
494
|
+
try {
|
|
495
|
+
await connection.readOkay();
|
|
496
|
+
} finally {
|
|
497
|
+
await connection.dispose();
|
|
498
|
+
}
|
|
466
499
|
}
|
|
467
500
|
|
|
468
501
|
async createTransport(
|
|
469
|
-
device:
|
|
502
|
+
device: AdbServerClient.DeviceSelector,
|
|
470
503
|
): Promise<AdbServerTransport> {
|
|
471
504
|
const { transportId, features } = await this.getDeviceFeatures(device);
|
|
472
505
|
|
|
@@ -492,7 +525,7 @@ export class AdbServerClient {
|
|
|
492
525
|
}
|
|
493
526
|
|
|
494
527
|
export async function raceSignal<T>(
|
|
495
|
-
callback: () =>
|
|
528
|
+
callback: () => PromiseLike<T>,
|
|
496
529
|
...signals: (AbortSignal | undefined)[]
|
|
497
530
|
): Promise<T> {
|
|
498
531
|
const abortPromise = new PromiseResolver<never>();
|
|
@@ -521,3 +554,182 @@ export async function raceSignal<T>(
|
|
|
521
554
|
}
|
|
522
555
|
}
|
|
523
556
|
}
|
|
557
|
+
|
|
558
|
+
export namespace AdbServerClient {
|
|
559
|
+
export interface ServerConnectionOptions {
|
|
560
|
+
unref?: boolean | undefined;
|
|
561
|
+
signal?: AbortSignal | undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export interface ServerConnection
|
|
565
|
+
extends ReadableWritablePair<Uint8Array, Uint8Array>,
|
|
566
|
+
Closeable {
|
|
567
|
+
get closed(): Promise<void>;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export interface ServerConnector {
|
|
571
|
+
connect(
|
|
572
|
+
options?: ServerConnectionOptions,
|
|
573
|
+
): ValueOrPromise<ServerConnection>;
|
|
574
|
+
|
|
575
|
+
addReverseTunnel(
|
|
576
|
+
handler: AdbIncomingSocketHandler,
|
|
577
|
+
address?: string,
|
|
578
|
+
): ValueOrPromise<string>;
|
|
579
|
+
|
|
580
|
+
removeReverseTunnel(address: string): ValueOrPromise<void>;
|
|
581
|
+
|
|
582
|
+
clearReverseTunnels(): ValueOrPromise<void>;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface Socket extends AdbSocket {
|
|
586
|
+
transportId: bigint;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export type DeviceSelector =
|
|
590
|
+
| { transportId: bigint }
|
|
591
|
+
| { serial: string }
|
|
592
|
+
| { usb: true }
|
|
593
|
+
| { tcp: true }
|
|
594
|
+
| undefined;
|
|
595
|
+
|
|
596
|
+
export interface Device {
|
|
597
|
+
serial: string;
|
|
598
|
+
authenticating: boolean;
|
|
599
|
+
product?: string | undefined;
|
|
600
|
+
model?: string | undefined;
|
|
601
|
+
device?: string | undefined;
|
|
602
|
+
transportId: bigint;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export class NetworkError extends Error {
|
|
606
|
+
constructor(message: string) {
|
|
607
|
+
super(message);
|
|
608
|
+
this.name = "NetworkError";
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export class UnauthorizedError extends Error {
|
|
613
|
+
constructor(message: string) {
|
|
614
|
+
super(message);
|
|
615
|
+
this.name = "UnauthorizedError";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export class AlreadyConnectedError extends Error {
|
|
620
|
+
constructor(message: string) {
|
|
621
|
+
super(message);
|
|
622
|
+
this.name = "AlreadyConnectedError";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export class WirelessCommands {
|
|
627
|
+
#client: AdbServerClient;
|
|
628
|
+
|
|
629
|
+
constructor(client: AdbServerClient) {
|
|
630
|
+
this.#client = client;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* `adb pair <password> <address>`
|
|
635
|
+
*/
|
|
636
|
+
async pair(address: string, password: string): Promise<void> {
|
|
637
|
+
const connection = await this.#client.createConnection(
|
|
638
|
+
`host:pair:${password}:${address}`,
|
|
639
|
+
);
|
|
640
|
+
try {
|
|
641
|
+
const response = await connection.readExactly(4);
|
|
642
|
+
// `response` is either `FAIL`, or 4 hex digits for length of the string
|
|
643
|
+
if (sequenceEqual(response, FAIL)) {
|
|
644
|
+
throw new Error(await connection.readString());
|
|
645
|
+
}
|
|
646
|
+
const length = hexToNumber(response);
|
|
647
|
+
// Ignore the string as it's always `Successful ...`
|
|
648
|
+
await connection.readExactly(length);
|
|
649
|
+
} finally {
|
|
650
|
+
await connection.dispose();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* `adb connect <address>`
|
|
656
|
+
*/
|
|
657
|
+
async connect(address: string): Promise<void> {
|
|
658
|
+
const connection = await this.#client.createConnection(
|
|
659
|
+
`host:connect:${address}`,
|
|
660
|
+
);
|
|
661
|
+
try {
|
|
662
|
+
const response = await connection.readString();
|
|
663
|
+
switch (response) {
|
|
664
|
+
case `already connected to ${address}`:
|
|
665
|
+
throw new AdbServerClient.AlreadyConnectedError(
|
|
666
|
+
response,
|
|
667
|
+
);
|
|
668
|
+
case `failed to connect to ${address}`: // `adb pair` mode not authorized
|
|
669
|
+
case `failed to authenticate to ${address}`: // `adb tcpip` mode not authorized
|
|
670
|
+
throw new AdbServerClient.UnauthorizedError(response);
|
|
671
|
+
case `connected to ${address}`:
|
|
672
|
+
return;
|
|
673
|
+
default:
|
|
674
|
+
throw new AdbServerClient.NetworkError(response);
|
|
675
|
+
}
|
|
676
|
+
} finally {
|
|
677
|
+
await connection.dispose();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* `adb disconnect <address>`
|
|
683
|
+
*/
|
|
684
|
+
async disconnect(address: string): Promise<void> {
|
|
685
|
+
const connection = await this.#client.createConnection(
|
|
686
|
+
`host:disconnect:${address}`,
|
|
687
|
+
);
|
|
688
|
+
try {
|
|
689
|
+
await connection.readString();
|
|
690
|
+
} finally {
|
|
691
|
+
await connection.dispose();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export class MDnsCommands {
|
|
697
|
+
#client: AdbServerClient;
|
|
698
|
+
|
|
699
|
+
constructor(client: AdbServerClient) {
|
|
700
|
+
this.#client = client;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async check() {
|
|
704
|
+
const connection =
|
|
705
|
+
await this.#client.createConnection("host:mdns:check");
|
|
706
|
+
try {
|
|
707
|
+
const response = await connection.readString();
|
|
708
|
+
return !response.startsWith("ERROR:");
|
|
709
|
+
} finally {
|
|
710
|
+
await connection.dispose();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async getServices() {
|
|
715
|
+
const connection =
|
|
716
|
+
await this.#client.createConnection("host:mdns:services");
|
|
717
|
+
try {
|
|
718
|
+
const response = await connection.readString();
|
|
719
|
+
return response
|
|
720
|
+
.split("\n")
|
|
721
|
+
.filter(Boolean)
|
|
722
|
+
.map((line) => {
|
|
723
|
+
const parts = line.split("\t");
|
|
724
|
+
return {
|
|
725
|
+
name: parts[0]!,
|
|
726
|
+
service: parts[1]!,
|
|
727
|
+
address: parts[2]!,
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
} finally {
|
|
731
|
+
await connection.dispose();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|