@yume-chan/adb 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +51 -0
- package/CHANGELOG.md +24 -1
- package/LICENSE +1 -1
- package/esm/adb.d.ts +6 -3
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +11 -4
- package/esm/adb.js.map +1 -1
- package/esm/banner.d.ts.map +1 -1
- package/esm/banner.js +1 -0
- package/esm/banner.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 +23 -11
- 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 +14 -69
- 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/subprocess/utils.js +1 -1
- package/esm/commands/subprocess/utils.js.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 +9 -7
- 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 +38 -6
- package/esm/daemon/dispatcher.d.ts.map +1 -1
- package/esm/daemon/dispatcher.js +86 -28
- 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 +11 -6
- package/esm/daemon/socket.d.ts.map +1 -1
- package/esm/daemon/socket.js +42 -12
- package/esm/daemon/socket.js.map +1 -1
- package/esm/daemon/transport.d.ts +75 -11
- package/esm/daemon/transport.d.ts.map +1 -1
- package/esm/daemon/transport.js +60 -33
- package/esm/daemon/transport.js.map +1 -1
- package/esm/features.d.ts +2 -1
- package/esm/features.d.ts.map +1 -1
- package/esm/features.js +2 -1
- package/esm/features.js.map +1 -1
- package/esm/server/client.d.ts +128 -48
- package/esm/server/client.d.ts.map +1 -1
- package/esm/server/client.js +378 -145
- package/esm/server/client.js.map +1 -1
- package/esm/server/transport.d.ts +3 -0
- package/esm/server/transport.d.ts.map +1 -1
- package/esm/server/transport.js +30 -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 +13 -15
- package/src/adb.ts +22 -6
- package/src/banner.ts +1 -0
- package/src/commands/power.ts +7 -5
- package/src/commands/reverse.ts +31 -18
- package/src/commands/subprocess/command.ts +3 -3
- package/src/commands/subprocess/protocols/none.ts +2 -2
- package/src/commands/subprocess/protocols/shell.ts +21 -100
- package/src/commands/subprocess/protocols/types.ts +2 -2
- package/src/commands/subprocess/utils.ts +1 -1
- 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 +13 -9
- 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 +175 -37
- package/src/daemon/packet.ts +21 -14
- package/src/daemon/socket.ts +62 -28
- package/src/daemon/transport.ts +145 -40
- package/src/features.ts +2 -1
- package/src/server/client.ts +472 -224
- package/src/server/transport.ts +32 -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,12 @@ 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
|
-
|
|
17
|
+
EMPTY_UINT8_ARRAY,
|
|
21
18
|
SyncPromise,
|
|
22
19
|
decodeUtf8,
|
|
23
20
|
encodeUtf8,
|
|
@@ -26,159 +23,164 @@ import {
|
|
|
26
23
|
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
|
27
24
|
import { AdbBanner } from "../banner.js";
|
|
28
25
|
import type { AdbFeature } from "../features.js";
|
|
29
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
NOOP,
|
|
28
|
+
hexToNumber,
|
|
29
|
+
sequenceEqual,
|
|
30
|
+
write4HexDigits,
|
|
31
|
+
} from "../utils/index.js";
|
|
30
32
|
|
|
31
33
|
import { AdbServerTransport } from "./transport.js";
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
signal?: AbortSignal | undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface AdbServerConnection
|
|
39
|
-
extends ReadableWritablePair<Uint8Array, Uint8Array>,
|
|
40
|
-
Closeable {
|
|
41
|
-
get closed(): Promise<void>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface AdbServerConnector {
|
|
45
|
-
connect(
|
|
46
|
-
options?: AdbServerConnectionOptions,
|
|
47
|
-
): ValueOrPromise<AdbServerConnection>;
|
|
48
|
-
|
|
49
|
-
addReverseTunnel(
|
|
50
|
-
handler: AdbIncomingSocketHandler,
|
|
51
|
-
address?: string,
|
|
52
|
-
): ValueOrPromise<string>;
|
|
53
|
-
|
|
54
|
-
removeReverseTunnel(address: string): ValueOrPromise<void>;
|
|
35
|
+
const OKAY = encodeUtf8("OKAY");
|
|
36
|
+
const FAIL = encodeUtf8("FAIL");
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
transportId: bigint;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type AdbServerDeviceSelector =
|
|
64
|
-
| { transportId: bigint }
|
|
65
|
-
| { serial: string }
|
|
66
|
-
| { usb: true }
|
|
67
|
-
| { tcp: true }
|
|
68
|
-
| undefined;
|
|
69
|
-
|
|
70
|
-
export interface AdbServerDevice {
|
|
71
|
-
serial: string;
|
|
72
|
-
product?: string | undefined;
|
|
73
|
-
model?: string | undefined;
|
|
74
|
-
device?: string | undefined;
|
|
75
|
-
transportId: bigint;
|
|
76
|
-
}
|
|
38
|
+
class AdbServerStream {
|
|
39
|
+
#connection: AdbServerClient.ServerConnection;
|
|
40
|
+
#buffered: BufferedReadableStream;
|
|
41
|
+
#writer: WritableStreamDefaultWriter<Uint8Array>;
|
|
77
42
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
constructor(connection: AdbServerClient.ServerConnection) {
|
|
44
|
+
this.#connection = connection;
|
|
45
|
+
this.#buffered = new BufferedReadableStream(connection.readable);
|
|
46
|
+
this.#writer = connection.writable.getWriter();
|
|
47
|
+
}
|
|
82
48
|
|
|
83
|
-
|
|
84
|
-
this.
|
|
49
|
+
readExactly(length: number): ValueOrPromise<Uint8Array> {
|
|
50
|
+
return this.#buffered.readExactly(length);
|
|
85
51
|
}
|
|
86
52
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
static readString(
|
|
90
|
-
stream: ExactReadable | AsyncExactReadable,
|
|
91
|
-
): string | PromiseLike<string> {
|
|
92
|
-
return SyncPromise.try(() => stream.readExactly(4))
|
|
53
|
+
readString() {
|
|
54
|
+
return SyncPromise.try(() => this.readExactly(4))
|
|
93
55
|
.then((buffer) => {
|
|
94
56
|
const length = hexToNumber(buffer);
|
|
95
|
-
|
|
57
|
+
if (length === 0) {
|
|
58
|
+
return EMPTY_UINT8_ARRAY;
|
|
59
|
+
} else {
|
|
60
|
+
return this.readExactly(length);
|
|
61
|
+
}
|
|
96
62
|
})
|
|
97
|
-
.then((
|
|
98
|
-
|
|
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);
|
|
99
84
|
})
|
|
100
85
|
.valueOrPromise();
|
|
101
86
|
}
|
|
102
87
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
buffer.set(
|
|
110
|
-
|
|
111
|
-
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);
|
|
112
96
|
}
|
|
113
97
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
119
104
|
return;
|
|
120
105
|
}
|
|
121
106
|
|
|
122
|
-
if (response
|
|
123
|
-
const reason = await
|
|
107
|
+
if (sequenceEqual(response, FAIL)) {
|
|
108
|
+
const reason = await this.readString();
|
|
124
109
|
throw new Error(reason);
|
|
125
110
|
}
|
|
126
111
|
|
|
127
|
-
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;
|
|
128
146
|
}
|
|
129
147
|
|
|
130
|
-
async
|
|
148
|
+
async createConnection(
|
|
131
149
|
request: string,
|
|
132
|
-
options?:
|
|
133
|
-
): Promise<
|
|
134
|
-
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);
|
|
135
154
|
|
|
136
155
|
try {
|
|
137
|
-
|
|
138
|
-
await AdbServerClient.writeString(writer, request);
|
|
139
|
-
writer.releaseLock();
|
|
156
|
+
await stream.writeString(request);
|
|
140
157
|
} catch (e) {
|
|
141
|
-
await
|
|
142
|
-
await connection.close();
|
|
158
|
+
await stream.dispose();
|
|
143
159
|
throw e;
|
|
144
160
|
}
|
|
145
161
|
|
|
146
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
147
162
|
try {
|
|
148
163
|
// `raceSignal` throws when the signal is aborted,
|
|
149
164
|
// so the `catch` block can close the connection.
|
|
150
|
-
await raceSignal(
|
|
151
|
-
|
|
152
|
-
options?.signal,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
readable: readable.release(),
|
|
157
|
-
writable: connection.writable,
|
|
158
|
-
get closed() {
|
|
159
|
-
return connection.closed;
|
|
160
|
-
},
|
|
161
|
-
async close() {
|
|
162
|
-
await connection.close();
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
+
await raceSignal(() => stream.readOkay(), options?.signal);
|
|
166
|
+
return stream;
|
|
165
167
|
} catch (e) {
|
|
166
|
-
await
|
|
167
|
-
await connection.close();
|
|
168
|
+
await stream.dispose();
|
|
168
169
|
throw e;
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
/**
|
|
174
|
+
* `adb version`
|
|
175
|
+
*/
|
|
172
176
|
async getVersion(): Promise<number> {
|
|
173
|
-
const connection = await this.
|
|
174
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
177
|
+
const connection = await this.createConnection("host:version");
|
|
175
178
|
try {
|
|
176
|
-
const length = hexToNumber(await
|
|
177
|
-
const version = hexToNumber(await
|
|
179
|
+
const length = hexToNumber(await connection.readExactly(4));
|
|
180
|
+
const version = hexToNumber(await connection.readExactly(length));
|
|
178
181
|
return version;
|
|
179
182
|
} finally {
|
|
180
|
-
connection.
|
|
181
|
-
readable.cancel().catch(NOOP);
|
|
183
|
+
await connection.dispose();
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -191,82 +193,123 @@ export class AdbServerClient {
|
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
|
|
196
|
+
/**
|
|
197
|
+
* `adb kill-server`
|
|
198
|
+
*/
|
|
194
199
|
async killServer(): Promise<void> {
|
|
195
|
-
const connection = await this.
|
|
196
|
-
connection.
|
|
197
|
-
connection.readable.cancel().catch(NOOP);
|
|
200
|
+
const connection = await this.createConnection("host:kill");
|
|
201
|
+
await connection.dispose();
|
|
198
202
|
}
|
|
199
203
|
|
|
204
|
+
/**
|
|
205
|
+
* `adb host-features`
|
|
206
|
+
*/
|
|
200
207
|
async getServerFeatures(): Promise<AdbFeature[]> {
|
|
201
|
-
const connection = await this.
|
|
202
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
208
|
+
const connection = await this.createConnection("host:host-features");
|
|
203
209
|
try {
|
|
204
|
-
const response = await
|
|
210
|
+
const response = await connection.readString();
|
|
205
211
|
return response.split(",") as AdbFeature[];
|
|
206
212
|
} finally {
|
|
207
|
-
connection.
|
|
208
|
-
readable.cancel().catch(NOOP);
|
|
213
|
+
await connection.dispose();
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const line of response.split("\n")) {
|
|
219
|
-
if (!line) {
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
217
|
+
parseDeviceList(value: string): AdbServerClient.Device[] {
|
|
218
|
+
const devices: AdbServerClient.Device[] = [];
|
|
219
|
+
for (const line of value.split("\n")) {
|
|
220
|
+
if (!line) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
const parts = line.split(" ").filter(Boolean);
|
|
225
|
+
const serial = parts[0]!;
|
|
226
|
+
const status = parts[1]!;
|
|
227
|
+
if (status !== "device" && status !== "unauthorized") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
231
|
+
let product: string | undefined;
|
|
232
|
+
let model: string | undefined;
|
|
233
|
+
let device: string | undefined;
|
|
234
|
+
let transportId: bigint | undefined;
|
|
235
|
+
for (let i = 2; i < parts.length; i += 1) {
|
|
236
|
+
const [key, value] = parts[i]!.split(":");
|
|
237
|
+
switch (key) {
|
|
238
|
+
case "product":
|
|
239
|
+
product = value;
|
|
240
|
+
break;
|
|
241
|
+
case "model":
|
|
242
|
+
model = value;
|
|
243
|
+
break;
|
|
244
|
+
case "device":
|
|
245
|
+
device = value;
|
|
246
|
+
break;
|
|
247
|
+
case "transport_id":
|
|
248
|
+
transportId = BigInt(value!);
|
|
249
|
+
break;
|
|
250
250
|
}
|
|
251
|
-
if (!transportId) {
|
|
252
|
-
throw new Error(`No transport id for device ${serial}`);
|
|
253
|
-
}
|
|
254
|
-
devices.push({
|
|
255
|
-
serial,
|
|
256
|
-
product,
|
|
257
|
-
model,
|
|
258
|
-
device,
|
|
259
|
-
transportId,
|
|
260
|
-
});
|
|
261
251
|
}
|
|
262
|
-
|
|
252
|
+
if (!transportId) {
|
|
253
|
+
throw new Error(`No transport id for device ${serial}`);
|
|
254
|
+
}
|
|
255
|
+
devices.push({
|
|
256
|
+
serial,
|
|
257
|
+
authenticating: status === "unauthorized",
|
|
258
|
+
product,
|
|
259
|
+
model,
|
|
260
|
+
device,
|
|
261
|
+
transportId,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return devices;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* `adb devices -l`
|
|
269
|
+
*/
|
|
270
|
+
async getDevices(): Promise<AdbServerClient.Device[]> {
|
|
271
|
+
const connection = await this.createConnection("host:devices-l");
|
|
272
|
+
try {
|
|
273
|
+
const response = await connection.readString();
|
|
274
|
+
return this.parseDeviceList(response);
|
|
263
275
|
} finally {
|
|
264
|
-
connection.
|
|
265
|
-
readable.cancel().catch(NOOP);
|
|
276
|
+
await connection.dispose();
|
|
266
277
|
}
|
|
267
278
|
}
|
|
268
279
|
|
|
269
|
-
|
|
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;
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
if (e === signal?.reason) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
} finally {
|
|
305
|
+
await connection.dispose();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
formatDeviceService(
|
|
310
|
+
device: AdbServerClient.DeviceSelector,
|
|
311
|
+
command: string,
|
|
312
|
+
) {
|
|
270
313
|
if (!device) {
|
|
271
314
|
return `host:${command}`;
|
|
272
315
|
}
|
|
@@ -282,7 +325,23 @@ export class AdbServerClient {
|
|
|
282
325
|
if ("tcp" in device) {
|
|
283
326
|
return `host-local:${command}`;
|
|
284
327
|
}
|
|
285
|
-
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
|
+
}
|
|
286
345
|
}
|
|
287
346
|
|
|
288
347
|
/**
|
|
@@ -293,23 +352,36 @@ export class AdbServerClient {
|
|
|
293
352
|
* @returns The transport ID of the selected device, and the features supported by the device.
|
|
294
353
|
*/
|
|
295
354
|
async getDeviceFeatures(
|
|
296
|
-
device:
|
|
355
|
+
device: AdbServerClient.DeviceSelector,
|
|
297
356
|
): Promise<{ transportId: bigint; features: AdbFeature[] }> {
|
|
298
|
-
//
|
|
299
|
-
// so
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
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);
|
|
306
379
|
try {
|
|
307
|
-
const
|
|
308
|
-
const featuresString = await AdbServerClient.readString(readable);
|
|
380
|
+
const featuresString = await stream.readString();
|
|
309
381
|
const features = featuresString.split(",") as AdbFeature[];
|
|
310
|
-
return { transportId:
|
|
382
|
+
return { transportId: connection.transportId, features };
|
|
311
383
|
} finally {
|
|
312
|
-
await
|
|
384
|
+
await stream.dispose();
|
|
313
385
|
}
|
|
314
386
|
}
|
|
315
387
|
|
|
@@ -317,12 +389,12 @@ export class AdbServerClient {
|
|
|
317
389
|
* Creates a connection that will forward the service to device.
|
|
318
390
|
* @param device The device selector
|
|
319
391
|
* @param service The service to forward
|
|
320
|
-
* @returns An `
|
|
392
|
+
* @returns An `AdbServerClient.Socket` that can be used to communicate with the service
|
|
321
393
|
*/
|
|
322
|
-
async
|
|
323
|
-
device:
|
|
394
|
+
async createDeviceConnection(
|
|
395
|
+
device: AdbServerClient.DeviceSelector,
|
|
324
396
|
service: string,
|
|
325
|
-
): Promise<
|
|
397
|
+
): Promise<AdbServerClient.Socket> {
|
|
326
398
|
await this.validateVersion();
|
|
327
399
|
|
|
328
400
|
let switchService: string;
|
|
@@ -339,68 +411,62 @@ export class AdbServerClient {
|
|
|
339
411
|
} else if ("tcp" in device) {
|
|
340
412
|
switchService = `host:tport:local`;
|
|
341
413
|
} else {
|
|
342
|
-
throw new
|
|
414
|
+
throw new TypeError("Invalid device selector");
|
|
343
415
|
}
|
|
344
416
|
|
|
345
|
-
const connection = await this.
|
|
417
|
+
const connection = await this.createConnection(switchService);
|
|
346
418
|
|
|
347
419
|
try {
|
|
348
|
-
|
|
349
|
-
await AdbServerClient.writeString(writer, service);
|
|
350
|
-
writer.releaseLock();
|
|
420
|
+
await connection.writeString(service);
|
|
351
421
|
} catch (e) {
|
|
352
|
-
await connection.
|
|
353
|
-
await connection.close();
|
|
422
|
+
await connection.dispose();
|
|
354
423
|
throw e;
|
|
355
424
|
}
|
|
356
425
|
|
|
357
|
-
const readable = new BufferedReadableStream(connection.readable);
|
|
358
426
|
try {
|
|
359
427
|
if (transportId === undefined) {
|
|
360
|
-
const array = await
|
|
361
|
-
|
|
362
|
-
const dataView = new DataView(
|
|
363
|
-
array.buffer,
|
|
364
|
-
array.byteOffset,
|
|
365
|
-
array.byteLength,
|
|
366
|
-
);
|
|
367
|
-
transportId = BigIntFieldType.Uint64.getter(dataView, 0, true);
|
|
428
|
+
const array = await connection.readExactly(8);
|
|
429
|
+
transportId = getUint64LittleEndian(array, 0);
|
|
368
430
|
}
|
|
369
431
|
|
|
370
|
-
await
|
|
432
|
+
await connection.readOkay();
|
|
433
|
+
|
|
434
|
+
const socket = connection.release();
|
|
371
435
|
|
|
372
436
|
return {
|
|
373
437
|
transportId,
|
|
374
438
|
service,
|
|
375
|
-
readable: readable
|
|
439
|
+
readable: socket.readable,
|
|
376
440
|
writable: new WrapWritableStream(
|
|
377
|
-
|
|
378
|
-
).bePipedThroughFrom(new
|
|
441
|
+
socket.writable,
|
|
442
|
+
).bePipedThroughFrom(new MaybeConsumable.UnwrapStream()),
|
|
379
443
|
get closed() {
|
|
380
|
-
return
|
|
444
|
+
return socket.closed;
|
|
381
445
|
},
|
|
382
446
|
async close() {
|
|
383
|
-
await
|
|
447
|
+
await socket.close();
|
|
384
448
|
},
|
|
385
449
|
};
|
|
386
450
|
} catch (e) {
|
|
387
|
-
await
|
|
388
|
-
await connection.close();
|
|
451
|
+
await connection.dispose();
|
|
389
452
|
throw e;
|
|
390
453
|
}
|
|
391
454
|
}
|
|
392
455
|
|
|
393
456
|
/**
|
|
394
457
|
* Wait for a device to be connected or disconnected.
|
|
458
|
+
*
|
|
459
|
+
* `adb wait-for-<state>`
|
|
460
|
+
*
|
|
395
461
|
* @param device The device selector
|
|
396
462
|
* @param state The state to wait for
|
|
397
463
|
* @param options The options
|
|
398
464
|
* @returns A promise that resolves when the condition is met.
|
|
399
465
|
*/
|
|
400
466
|
async waitFor(
|
|
401
|
-
device:
|
|
467
|
+
device: AdbServerClient.DeviceSelector,
|
|
402
468
|
state: "device" | "disconnect",
|
|
403
|
-
options?:
|
|
469
|
+
options?: AdbServerClient.ServerConnectionOptions,
|
|
404
470
|
): Promise<void> {
|
|
405
471
|
let type: string;
|
|
406
472
|
if (!device) {
|
|
@@ -414,7 +480,7 @@ export class AdbServerClient {
|
|
|
414
480
|
} else if ("tcp" in device) {
|
|
415
481
|
type = "local";
|
|
416
482
|
} else {
|
|
417
|
-
throw new
|
|
483
|
+
throw new TypeError("Invalid device selector");
|
|
418
484
|
}
|
|
419
485
|
|
|
420
486
|
// `waitFor` can't use `connectDevice`, because the device
|
|
@@ -424,13 +490,16 @@ export class AdbServerClient {
|
|
|
424
490
|
`wait-for-${type}-${state}`,
|
|
425
491
|
);
|
|
426
492
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
493
|
+
const connection = await this.createConnection(service, options);
|
|
494
|
+
try {
|
|
495
|
+
await connection.readOkay();
|
|
496
|
+
} finally {
|
|
497
|
+
await connection.dispose();
|
|
498
|
+
}
|
|
430
499
|
}
|
|
431
500
|
|
|
432
501
|
async createTransport(
|
|
433
|
-
device:
|
|
502
|
+
device: AdbServerClient.DeviceSelector,
|
|
434
503
|
): Promise<AdbServerTransport> {
|
|
435
504
|
const { transportId, features } = await this.getDeviceFeatures(device);
|
|
436
505
|
|
|
@@ -456,7 +525,7 @@ export class AdbServerClient {
|
|
|
456
525
|
}
|
|
457
526
|
|
|
458
527
|
export async function raceSignal<T>(
|
|
459
|
-
callback: () =>
|
|
528
|
+
callback: () => PromiseLike<T>,
|
|
460
529
|
...signals: (AbortSignal | undefined)[]
|
|
461
530
|
): Promise<T> {
|
|
462
531
|
const abortPromise = new PromiseResolver<never>();
|
|
@@ -485,3 +554,182 @@ export async function raceSignal<T>(
|
|
|
485
554
|
}
|
|
486
555
|
}
|
|
487
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
|
+
}
|