@yume-chan/adb 3.0.0-beta.1 → 3.0.0-beta.2
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/esm/adb.d.ts +7 -2
- package/esm/adb.d.ts.map +1 -1
- package/esm/adb.js +2 -2
- package/esm/adb.js.map +1 -1
- package/esm/banner.spec.d.ts +2 -0
- package/esm/banner.spec.d.ts.map +1 -0
- package/esm/daemon/auth/packet-processor.spec.d.ts +2 -0
- package/esm/daemon/auth/packet-processor.spec.d.ts.map +1 -0
- package/esm/daemon/auth/public-key.spec.d.ts +2 -0
- package/esm/daemon/auth/public-key.spec.d.ts.map +1 -0
- package/esm/daemon/crypto.spec.d.ts +2 -0
- package/esm/daemon/crypto.spec.d.ts.map +1 -0
- package/esm/server/client.spec.d.ts +2 -0
- package/esm/server/client.spec.d.ts.map +1 -0
- package/esm/server/transport.d.ts +1 -1
- package/esm/server/transport.d.ts.map +1 -1
- package/esm/server/transport.js +2 -2
- package/esm/server/transport.js.map +1 -1
- package/esm/service/reverse/response.spec.d.ts +2 -0
- package/esm/service/reverse/response.spec.d.ts.map +1 -0
- package/esm/service/subprocess/none/process.spec.d.ts +2 -0
- package/esm/service/subprocess/none/process.spec.d.ts.map +1 -0
- package/esm/service/subprocess/none/spawner.spec.d.ts +2 -0
- package/esm/service/subprocess/none/spawner.spec.d.ts.map +1 -0
- package/esm/service/subprocess/shell/process.spec.d.ts +2 -0
- package/esm/service/subprocess/shell/process.spec.d.ts.map +1 -0
- package/esm/service/subprocess/shell/shared.spec.d.ts +2 -0
- package/esm/service/subprocess/shell/shared.spec.d.ts.map +1 -0
- package/esm/service/subprocess/shell/spawner.spec.d.ts +2 -0
- package/esm/service/subprocess/shell/spawner.spec.d.ts.map +1 -0
- package/esm/service/subprocess/utils.spec.d.ts +2 -0
- package/esm/service/subprocess/utils.spec.d.ts.map +1 -0
- package/esm/service/sync/compression/adapters.d.ts +16 -0
- package/esm/service/sync/compression/adapters.d.ts.map +1 -0
- package/esm/service/sync/compression/adapters.js +84 -0
- package/esm/service/sync/compression/adapters.js.map +1 -0
- package/esm/service/sync/compression/format-ns.d.ts.map +1 -1
- package/esm/service/sync/compression/format-ns.js +1 -0
- package/esm/service/sync/compression/format-ns.js.map +1 -1
- package/esm/service/sync/compression/format.d.ts +1 -0
- package/esm/service/sync/compression/format.d.ts.map +1 -1
- package/esm/service/sync/compression/format.js +1 -0
- package/esm/service/sync/compression/format.js.map +1 -1
- package/esm/service/sync/compression/index.d.ts +1 -1
- package/esm/service/sync/compression/index.d.ts.map +1 -1
- package/esm/service/sync/compression/index.js +1 -1
- package/esm/service/sync/compression/index.js.map +1 -1
- package/esm/service/sync/compression/mode.d.ts +6 -0
- package/esm/service/sync/compression/mode.d.ts.map +1 -0
- package/esm/service/sync/compression/mode.js +5 -0
- package/esm/service/sync/compression/mode.js.map +1 -0
- package/esm/service/sync/compression/native.d.ts +8 -0
- package/esm/service/sync/compression/native.d.ts.map +1 -0
- package/esm/service/sync/compression/native.js +53 -0
- package/esm/service/sync/compression/native.js.map +1 -0
- package/esm/service/sync/id/request.d.ts +1 -0
- package/esm/service/sync/id/request.d.ts.map +1 -1
- package/esm/service/sync/id/request.js +1 -0
- package/esm/service/sync/id/request.js.map +1 -1
- package/esm/service/sync/request/flag.d.ts +9 -0
- package/esm/service/sync/request/flag.d.ts.map +1 -0
- package/esm/service/sync/request/flag.js +8 -0
- package/esm/service/sync/request/flag.js.map +1 -0
- package/esm/service/sync/request/pull.d.ts +25 -2
- package/esm/service/sync/request/pull.d.ts.map +1 -1
- package/esm/service/sync/request/pull.js +76 -16
- package/esm/service/sync/request/pull.js.map +1 -1
- package/esm/service/sync/request/push.d.ts +36 -14
- package/esm/service/sync/request/push.d.ts.map +1 -1
- package/esm/service/sync/request/push.js +176 -60
- package/esm/service/sync/request/push.js.map +1 -1
- package/esm/service/sync/service.d.ts +32 -16
- package/esm/service/sync/service.d.ts.map +1 -1
- package/esm/service/sync/service.js +58 -13
- package/esm/service/sync/service.js.map +1 -1
- package/esm/service/sync/socket-pool.d.ts +1 -0
- package/esm/service/sync/socket-pool.d.ts.map +1 -1
- package/esm/service/sync/socket-pool.js +45 -9
- package/esm/service/sync/socket-pool.js.map +1 -1
- package/esm/service/sync/socket-pool.spec.d.ts +2 -0
- package/esm/service/sync/socket-pool.spec.d.ts.map +1 -0
- package/esm/service/sync/socket.d.ts +4 -0
- package/esm/service/sync/socket.d.ts.map +1 -1
- package/esm/service/sync/socket.js +16 -0
- package/esm/service/sync/socket.js.map +1 -1
- package/esm/service/sync/socket.spec.d.ts +2 -0
- package/esm/service/sync/socket.spec.d.ts.map +1 -0
- package/esm/utils/base64.spec.d.ts +2 -0
- package/esm/utils/base64.spec.d.ts.map +1 -0
- package/esm/utils/md5.spec.d.ts +2 -0
- package/esm/utils/md5.spec.d.ts.map +1 -0
- package/esm/utils/ref.d.ts +3 -5
- package/esm/utils/ref.d.ts.map +1 -1
- package/esm/utils/ref.js +12 -6
- package/esm/utils/ref.js.map +1 -1
- package/package.json +5 -5
- package/src/adb.ts +15 -3
- package/src/server/transport.ts +5 -1
- package/src/service/sync/compression/adapters.ts +137 -0
- package/src/service/sync/compression/format-ns.ts +2 -0
- package/src/service/sync/compression/format.ts +5 -0
- package/src/service/sync/compression/index.ts +1 -1
- package/src/service/sync/compression/mode.ts +6 -0
- package/src/service/sync/compression/native.ts +73 -0
- package/src/service/sync/id/request.ts +1 -0
- package/src/service/sync/request/flag.ts +9 -0
- package/src/service/sync/request/pull.ts +135 -27
- package/src/service/sync/request/push.ts +241 -91
- package/src/service/sync/service.ts +87 -27
- package/src/service/sync/socket-pool.ts +50 -9
- package/src/service/sync/socket.ts +23 -1
- package/src/utils/ref.ts +17 -7
- package/yume-chan-adb-3.0.0-beta.0.tgz +0 -0
- package/esm/service/sync/compression/compression.d.ts +0 -17
- package/esm/service/sync/compression/compression.d.ts.map +0 -1
- package/esm/service/sync/compression/compression.js +0 -92
- package/esm/service/sync/compression/compression.js.map +0 -1
- package/src/service/sync/compression/compression.ts +0 -126
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { PromiseResolver } from "@yume-chan/async";
|
|
2
|
+
import type {
|
|
3
|
+
TransformStream,
|
|
4
|
+
WritableStream,
|
|
5
|
+
WritableStreamDefaultController,
|
|
6
6
|
} from "@yume-chan/stream-extra";
|
|
7
|
+
import { DistributionStream, MaybeConsumable } from "@yume-chan/stream-extra";
|
|
7
8
|
import { struct, u32 } from "@yume-chan/struct";
|
|
8
9
|
|
|
9
|
-
import { NOOP } from "../../../utils/
|
|
10
|
+
import { NOOP } from "../../../utils/no-op.js";
|
|
10
11
|
import { LinuxFileType } from "../android.js";
|
|
11
12
|
import { Compression } from "../compression/index.js";
|
|
12
13
|
import { RequestId, ResponseId } from "../id/index.js";
|
|
@@ -14,93 +15,166 @@ import type { SocketPool } from "../socket-pool.js";
|
|
|
14
15
|
import type { Socket } from "../socket.js";
|
|
15
16
|
import { Error as AdbSyncError } from "../socket.js";
|
|
16
17
|
|
|
18
|
+
import { SyncFlag } from "./flag.js";
|
|
19
|
+
|
|
17
20
|
export const MaxPacketSize = 64 * 1024;
|
|
18
21
|
|
|
19
22
|
export const OkResponse = struct({ unused: u32 }, { littleEndian: true });
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
export interface SendSession {
|
|
25
|
+
/**
|
|
26
|
+
* The writable stream to write the file content into.
|
|
27
|
+
*/
|
|
28
|
+
writable: WritableStream<MaybeConsumable<Uint8Array>>;
|
|
29
|
+
/**
|
|
30
|
+
* Gets the number of bytes written into {@link writable}.
|
|
31
|
+
*/
|
|
32
|
+
bytesWritten: number;
|
|
33
|
+
/**
|
|
34
|
+
* When using Send v2, gets the compression format used (might be `None`).
|
|
35
|
+
*
|
|
36
|
+
* When using Send v1, this will always be `undefined`.
|
|
37
|
+
*/
|
|
38
|
+
compression?: Compression.Format | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* When using Send v2, gets the size of the compressed data sent to the device.
|
|
41
|
+
* Might be same as {@link bytesWritten} if no compression was applied.
|
|
42
|
+
*/
|
|
43
|
+
bytesCompressed: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class SendWritableStream extends MaybeConsumable.WritableStream<Uint8Array> {
|
|
47
|
+
#pool: SocketPool;
|
|
48
|
+
#socket: Socket;
|
|
49
|
+
|
|
50
|
+
#resolver = new PromiseResolver<void>();
|
|
51
|
+
#controller: WritableStreamDefaultController;
|
|
52
|
+
|
|
53
|
+
#bytesWritten = 0;
|
|
54
|
+
get bytesWritten() {
|
|
55
|
+
return this.#bytesWritten;
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const abortController = new AbortController();
|
|
40
|
-
file.pipeThrough(new DistributionStream(packetSize, true))
|
|
41
|
-
.pipeTo(
|
|
42
|
-
new MaybeConsumable.WritableStream({
|
|
43
|
-
write(chunk) {
|
|
44
|
-
return socket.writeRequest(RequestId.Data, chunk);
|
|
45
|
-
},
|
|
46
|
-
}),
|
|
47
|
-
{ signal: abortController.signal },
|
|
48
|
-
)
|
|
49
|
-
.then(async () => {
|
|
50
|
-
await socket.writeRequest(RequestId.Done, mtime);
|
|
51
|
-
await socket.flush();
|
|
52
|
-
}, NOOP);
|
|
58
|
+
constructor(pool: SocketPool, socket: Socket, mtime: number) {
|
|
59
|
+
let controller!: WritableStreamDefaultController;
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
super({
|
|
62
|
+
start: (controller_) => {
|
|
63
|
+
controller = controller_;
|
|
64
|
+
|
|
65
|
+
// Start reading response immediately,
|
|
66
|
+
// the server can send error response before the whole file is sent.
|
|
67
|
+
socket.readResponse(ResponseId.Ok, OkResponse).then(
|
|
68
|
+
() => this.#finish(),
|
|
69
|
+
(e) => this.#finish(e),
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
write: async (chunk) => {
|
|
73
|
+
try {
|
|
74
|
+
this.#bytesWritten += chunk.length;
|
|
75
|
+
await socket.writeRequest(RequestId.Data, chunk);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
await this.#finish(e);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
close: async () => {
|
|
81
|
+
try {
|
|
82
|
+
await socket.writeRequest(RequestId.Done, mtime);
|
|
83
|
+
await socket.flush();
|
|
84
|
+
} catch (e) {
|
|
85
|
+
await this.#finish(e);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await this.#resolver.promise;
|
|
89
|
+
},
|
|
90
|
+
abort: async (reason) => {
|
|
91
|
+
// Write anything other than `Data` or `Done`
|
|
92
|
+
// will cause the server to delete the file.
|
|
93
|
+
await socket.writeRequest(ResponseId.Fail, 0);
|
|
94
|
+
|
|
95
|
+
// Socket not reusable after aborting, so discard it.
|
|
96
|
+
await this.#finish(reason);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.#pool = pool;
|
|
101
|
+
this.#socket = socket;
|
|
102
|
+
|
|
103
|
+
this.#controller = controller;
|
|
104
|
+
// Suppress unhandled rejection warning when the promise is not awaited.
|
|
105
|
+
this.#resolver.promise.catch(NOOP);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#trySetError(reason: unknown) {
|
|
109
|
+
try {
|
|
110
|
+
this.#controller.error(reason);
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore if controller is already closed or errored
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async #finish(error?: unknown) {
|
|
117
|
+
try {
|
|
118
|
+
await this.#pool.release(
|
|
119
|
+
this.#socket,
|
|
120
|
+
!(error instanceof AdbSyncError),
|
|
121
|
+
);
|
|
122
|
+
if (error) {
|
|
123
|
+
this.#trySetError(error);
|
|
124
|
+
this.#resolver.reject(error);
|
|
125
|
+
} else {
|
|
126
|
+
this.#resolver.resolve();
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// TOOD: use `SuppressedError` when universally supported
|
|
130
|
+
this.#trySetError(e);
|
|
131
|
+
this.#resolver.reject(e);
|
|
132
|
+
}
|
|
59
133
|
}
|
|
60
134
|
}
|
|
61
135
|
|
|
62
136
|
export interface SendV1Options {
|
|
63
137
|
pool: SocketPool;
|
|
64
|
-
|
|
65
|
-
file: ReadableStream<MaybeConsumable<Uint8Array>>;
|
|
138
|
+
path: string;
|
|
66
139
|
type?: LinuxFileType | undefined;
|
|
67
140
|
permission?: number | undefined;
|
|
68
141
|
mtime?: number | undefined;
|
|
69
142
|
packetSize?: number | undefined;
|
|
70
143
|
}
|
|
71
144
|
|
|
72
|
-
export function sendV1({
|
|
145
|
+
export async function sendV1({
|
|
73
146
|
pool,
|
|
74
|
-
|
|
75
|
-
file,
|
|
147
|
+
path,
|
|
76
148
|
type = LinuxFileType.File,
|
|
77
149
|
permission = 0o666,
|
|
78
150
|
mtime = (Date.now() / 1000) | 0,
|
|
79
151
|
packetSize = MaxPacketSize,
|
|
80
|
-
}: SendV1Options) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
152
|
+
}: SendV1Options): Promise<SendSession> {
|
|
153
|
+
const mode = (type << 12) | permission;
|
|
154
|
+
const request = path + "," + mode;
|
|
155
|
+
|
|
156
|
+
const socket = await pool.acquire();
|
|
157
|
+
try {
|
|
158
|
+
await socket.writeRequest(RequestId.Send, request);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
await pool.release(socket, !(e instanceof AdbSyncError));
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
94
163
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Lz4: 2,
|
|
99
|
-
Zstd: 4,
|
|
100
|
-
DryRun: 0x80000000,
|
|
101
|
-
} as const;
|
|
164
|
+
const distributeStream = new DistributionStream(packetSize, true);
|
|
165
|
+
const sendStream = new SendWritableStream(pool, socket, mtime);
|
|
166
|
+
void distributeStream.readable.pipeTo(sendStream).catch(NOOP);
|
|
102
167
|
|
|
103
|
-
|
|
168
|
+
return {
|
|
169
|
+
writable: distributeStream.writable,
|
|
170
|
+
get bytesWritten() {
|
|
171
|
+
return sendStream.bytesWritten;
|
|
172
|
+
},
|
|
173
|
+
get bytesCompressed() {
|
|
174
|
+
return sendStream.bytesWritten;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
104
178
|
|
|
105
179
|
export const SendV2Request = struct(
|
|
106
180
|
{ id: u32, mode: u32, flags: u32 },
|
|
@@ -108,6 +182,16 @@ export const SendV2Request = struct(
|
|
|
108
182
|
);
|
|
109
183
|
|
|
110
184
|
export interface SendV2Options extends SendV1Options {
|
|
185
|
+
/**
|
|
186
|
+
* The format to compress the file stream for sending.
|
|
187
|
+
*
|
|
188
|
+
* If the device or current runtime doesn't support the specified format,
|
|
189
|
+
* an Error will be thrown.
|
|
190
|
+
*
|
|
191
|
+
* If `undefined` is specified, no compression will be used.
|
|
192
|
+
* (this behavior is different from `AdbSync.Service.prototype.write`,
|
|
193
|
+
* which will automatically choose the best format)
|
|
194
|
+
*/
|
|
111
195
|
compression?: Compression.Format | undefined;
|
|
112
196
|
|
|
113
197
|
/**
|
|
@@ -119,35 +203,47 @@ export interface SendV2Options extends SendV1Options {
|
|
|
119
203
|
dryRun?: boolean | undefined;
|
|
120
204
|
}
|
|
121
205
|
|
|
122
|
-
export function sendV2({
|
|
206
|
+
export async function sendV2({
|
|
123
207
|
pool,
|
|
124
|
-
|
|
125
|
-
file,
|
|
208
|
+
path,
|
|
126
209
|
type = LinuxFileType.File,
|
|
127
210
|
permission = 0o666,
|
|
128
211
|
mtime = (Date.now() / 1000) | 0,
|
|
129
212
|
packetSize = MaxPacketSize,
|
|
130
213
|
compression = Compression.Format.None,
|
|
131
214
|
dryRun = false,
|
|
132
|
-
}: SendV2Options) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
215
|
+
}: SendV2Options): Promise<SendSession> {
|
|
216
|
+
let flags: SyncFlag = SyncFlag.None;
|
|
217
|
+
let compressStream: TransformStream<Uint8Array, Uint8Array> | undefined;
|
|
218
|
+
|
|
219
|
+
// Validate `compression` before acquiring the socket
|
|
220
|
+
switch (compression) {
|
|
221
|
+
case Compression.Format.Brotli:
|
|
222
|
+
flags |= SyncFlag.Brotli;
|
|
223
|
+
compressStream = Compression.createCompressionStream(
|
|
224
|
+
Compression.Format.Brotli,
|
|
225
|
+
);
|
|
226
|
+
break;
|
|
227
|
+
case Compression.Format.Lz4:
|
|
228
|
+
flags |= SyncFlag.Lz4;
|
|
229
|
+
compressStream = Compression.createCompressionStream(
|
|
230
|
+
Compression.Format.Lz4,
|
|
231
|
+
);
|
|
232
|
+
break;
|
|
233
|
+
case Compression.Format.Zstd:
|
|
234
|
+
flags |= SyncFlag.Zstd;
|
|
235
|
+
compressStream = Compression.createCompressionStream(
|
|
236
|
+
Compression.Format.Zstd,
|
|
237
|
+
);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (dryRun) {
|
|
241
|
+
flags |= SyncFlag.DryRun;
|
|
242
|
+
}
|
|
149
243
|
|
|
150
|
-
|
|
244
|
+
const socket = await pool.acquire();
|
|
245
|
+
try {
|
|
246
|
+
await socket.writeRequest(RequestId.SendV2, path);
|
|
151
247
|
await socket.write(
|
|
152
248
|
SendV2Request.serialize({
|
|
153
249
|
id: RequestId.SendV2,
|
|
@@ -155,9 +251,63 @@ export function sendV2({
|
|
|
155
251
|
flags,
|
|
156
252
|
}),
|
|
157
253
|
);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
await pool.release(socket, !(e instanceof AdbSyncError));
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const distributeStream = new DistributionStream(packetSize, true);
|
|
260
|
+
const sendStream = new SendWritableStream(pool, socket, mtime);
|
|
261
|
+
|
|
262
|
+
if (!compressStream) {
|
|
263
|
+
const pipe = distributeStream.readable.pipeTo(sendStream);
|
|
264
|
+
|
|
265
|
+
const writer = distributeStream.writable.getWriter();
|
|
266
|
+
return {
|
|
267
|
+
writable: new MaybeConsumable.WritableStream({
|
|
268
|
+
write(chunk) {
|
|
269
|
+
return writer.write(chunk);
|
|
270
|
+
},
|
|
271
|
+
async close() {
|
|
272
|
+
await writer.close();
|
|
273
|
+
await pipe;
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
get bytesWritten() {
|
|
277
|
+
return sendStream.bytesWritten;
|
|
278
|
+
},
|
|
279
|
+
compression: Compression.Format.None,
|
|
280
|
+
get bytesCompressed() {
|
|
281
|
+
return sendStream.bytesWritten;
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pipe = compressStream.readable
|
|
287
|
+
.pipeThrough(distributeStream)
|
|
288
|
+
.pipeTo(sendStream);
|
|
158
289
|
|
|
159
|
-
|
|
160
|
-
|
|
290
|
+
const writer = compressStream.writable.getWriter();
|
|
291
|
+
let bytesWritten = 0;
|
|
292
|
+
return {
|
|
293
|
+
writable: new MaybeConsumable.WritableStream({
|
|
294
|
+
write(chunk) {
|
|
295
|
+
bytesWritten += chunk.length;
|
|
296
|
+
return writer.write(chunk);
|
|
297
|
+
},
|
|
298
|
+
async close() {
|
|
299
|
+
await writer.close();
|
|
300
|
+
await pipe;
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
get bytesWritten() {
|
|
304
|
+
return bytesWritten;
|
|
305
|
+
},
|
|
306
|
+
compression,
|
|
307
|
+
get bytesCompressed() {
|
|
308
|
+
return sendStream.bytesWritten;
|
|
309
|
+
},
|
|
310
|
+
};
|
|
161
311
|
}
|
|
162
312
|
|
|
163
313
|
export interface SendOptions extends SendV2Options {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
MaybeConsumable,
|
|
4
|
-
ReadableStream,
|
|
5
|
-
} from "@yume-chan/stream-extra";
|
|
1
|
+
import type { AbortSignal, ReadableStream } from "@yume-chan/stream-extra";
|
|
2
|
+
import { MaybeConsumable } from "@yume-chan/stream-extra";
|
|
6
3
|
|
|
7
4
|
import type { Adb } from "../../adb.js";
|
|
8
5
|
import { AdbFeature } from "../../features.js";
|
|
@@ -53,20 +50,20 @@ export function dirname(path: string): string {
|
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
export interface WriteOptions {
|
|
56
|
-
|
|
57
|
-
file: ReadableStream<MaybeConsumable<Uint8Array>>;
|
|
53
|
+
path: string;
|
|
58
54
|
type?: LinuxFileType | undefined;
|
|
59
55
|
permission?: number | undefined;
|
|
60
56
|
mtime?: number | undefined;
|
|
61
57
|
/**
|
|
62
58
|
* The format to compress the file stream for sending.
|
|
63
59
|
*
|
|
64
|
-
* If the device doesn't support compressed sending,
|
|
60
|
+
* If the device doesn't support compressed sending, the value will be ignored.
|
|
65
61
|
*
|
|
66
|
-
*
|
|
62
|
+
* Otherwise, if the device or current runtime doesn't support the specified format,
|
|
67
63
|
* an Error will be thrown.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
64
|
+
*
|
|
65
|
+
* If `undefined` is specified, automatically choose the best format
|
|
66
|
+
* supported by both device and current runtime.
|
|
70
67
|
*/
|
|
71
68
|
compression?: Compression.Format | undefined;
|
|
72
69
|
dryRun?: boolean | undefined;
|
|
@@ -203,36 +200,73 @@ export class Service {
|
|
|
203
200
|
}
|
|
204
201
|
|
|
205
202
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* @param
|
|
209
|
-
* @returns A
|
|
203
|
+
* Creates a readable stream to read the content of a file on device.
|
|
204
|
+
* @param path The full path of the file on device to read.
|
|
205
|
+
* @param compression The compression format to use for reading the file. If the device doesn't support compressed reading, the value will be ignored.
|
|
206
|
+
* @returns A {@link Receive.ReceiveSession} object that contains a readable stream to read the file content.
|
|
210
207
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
createReadable(
|
|
209
|
+
path: string,
|
|
210
|
+
compression?: Compression.Format,
|
|
211
|
+
): Receive.ReceiveSession {
|
|
212
|
+
if (this.#supportsSendReceive2) {
|
|
213
|
+
if (compression === undefined) {
|
|
214
|
+
compression = Compression.chooseFormat(
|
|
215
|
+
this._adb,
|
|
216
|
+
Compression.Mode.Decompress,
|
|
217
|
+
);
|
|
218
|
+
} else if (
|
|
219
|
+
!Compression.canUseFormat(
|
|
220
|
+
this._adb,
|
|
221
|
+
compression,
|
|
222
|
+
Compression.Mode.Decompress,
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Compression type ${Compression.FormatNameMap[compression]} is not supported`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return Receive.receiveV2(this.#socketPool, path, compression);
|
|
230
|
+
} else {
|
|
231
|
+
return Receive.receiveV1(this.#socketPool, path);
|
|
232
|
+
}
|
|
213
233
|
}
|
|
214
234
|
|
|
215
235
|
/**
|
|
216
|
-
*
|
|
236
|
+
* Creates a readable stream to read the content of a file on device.
|
|
217
237
|
*
|
|
218
|
-
* @param
|
|
238
|
+
* @param path The full path of the file on device to read.
|
|
239
|
+
* @returns A readable stream to read the file content.
|
|
219
240
|
*/
|
|
220
|
-
|
|
241
|
+
read(path: string): ReadableStream<Uint8Array> {
|
|
242
|
+
const session = this.createReadable(path);
|
|
243
|
+
return session.readable;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a writable stream to write a file
|
|
248
|
+
* @param options The options for writing the file, including the path, type, permission, mtime, and compression format.
|
|
249
|
+
* @returns A {@link SendSession} object that contains a writable stream to write the file content.
|
|
250
|
+
*/
|
|
251
|
+
async createWritable(options: WriteOptions): Promise<Send.SendSession> {
|
|
221
252
|
if (this.needPushMkdirWorkaround) {
|
|
222
253
|
// It may fail if `filename` already exists.
|
|
223
254
|
// Ignore the result.
|
|
224
255
|
// TODO: sync: test push mkdir workaround (need an Android 8 device)
|
|
225
256
|
await this._adb.subprocess.noneProtocol
|
|
226
|
-
.spawn(["mkdir", "-p", escapeArg(dirname(options.
|
|
257
|
+
.spawn(["mkdir", "-p", escapeArg(dirname(options.path))])
|
|
227
258
|
.wait();
|
|
228
259
|
}
|
|
229
260
|
|
|
230
261
|
if (this.supportsSendReceive2) {
|
|
231
262
|
if (options.compression === undefined) {
|
|
232
|
-
options
|
|
233
|
-
|
|
234
|
-
Compression.
|
|
235
|
-
|
|
263
|
+
options = {
|
|
264
|
+
...options,
|
|
265
|
+
compression: Compression.chooseFormat(
|
|
266
|
+
this._adb,
|
|
267
|
+
Compression.Mode.Compress,
|
|
268
|
+
),
|
|
269
|
+
};
|
|
236
270
|
} else if (
|
|
237
271
|
!Compression.canUseFormat(
|
|
238
272
|
this._adb,
|
|
@@ -241,15 +275,41 @@ export class Service {
|
|
|
241
275
|
)
|
|
242
276
|
) {
|
|
243
277
|
throw new Error(
|
|
244
|
-
`Compression type ${options.compression} is not supported`,
|
|
278
|
+
`Compression type ${Compression.FormatNameMap[options.compression]} is not supported`,
|
|
245
279
|
);
|
|
246
280
|
}
|
|
281
|
+
} else {
|
|
282
|
+
options = {
|
|
283
|
+
...options,
|
|
284
|
+
compression: undefined,
|
|
285
|
+
};
|
|
247
286
|
}
|
|
248
287
|
|
|
249
|
-
await Send.send({
|
|
288
|
+
return await Send.send({
|
|
250
289
|
version: this.supportsSendReceive2 ? 2 : 1,
|
|
251
290
|
pool: this.#socketPool,
|
|
252
291
|
...options,
|
|
253
292
|
});
|
|
254
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Writes a file to the device using a readable stream.
|
|
297
|
+
* @param options The options for writing the file, including the path, type, permission, mtime, compression format, and a readable stream to read the file content.
|
|
298
|
+
* @returns An object that contains the number of bytes written, the compression format used (if any), and the size of the compressed data written (if any).
|
|
299
|
+
*/
|
|
300
|
+
async write(
|
|
301
|
+
options: WriteOptions & {
|
|
302
|
+
readable: ReadableStream<MaybeConsumable<Uint8Array>>;
|
|
303
|
+
},
|
|
304
|
+
): Promise<Omit<Send.SendSession, "writable">> {
|
|
305
|
+
const session = await this.createWritable(options);
|
|
306
|
+
await options.readable.pipeTo(
|
|
307
|
+
new MaybeConsumable.WrapWritableStream(session.writable),
|
|
308
|
+
);
|
|
309
|
+
return {
|
|
310
|
+
bytesWritten: session.bytesWritten,
|
|
311
|
+
compression: session.compression,
|
|
312
|
+
bytesCompressed: session.bytesCompressed,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
255
315
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Adb } from "../../adb.js";
|
|
2
|
+
import { Ref } from "../../utils/ref.js";
|
|
2
3
|
|
|
3
4
|
import { Error as AdbSyncError, Socket } from "./socket.js";
|
|
4
5
|
|
|
@@ -8,6 +9,7 @@ export class SocketPool {
|
|
|
8
9
|
readonly #idleTimeout: number;
|
|
9
10
|
readonly #availableSockets: Socket[] = [];
|
|
10
11
|
readonly #inUseSockets = new Set<Socket>();
|
|
12
|
+
#ref = new Ref({ unref: true });
|
|
11
13
|
#closed = false;
|
|
12
14
|
|
|
13
15
|
constructor(adb: Adb, maxSize = 4, idleTimeout = 60000) {
|
|
@@ -26,18 +28,32 @@ export class SocketPool {
|
|
|
26
28
|
// Clear the idle timeout
|
|
27
29
|
socket.clearIdleTimer();
|
|
28
30
|
this.#inUseSockets.add(socket);
|
|
31
|
+
this.#ref.ref();
|
|
29
32
|
return socket;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
// Add ref early to keep the process alive while initializing the new socket
|
|
37
|
+
this.#ref.ref();
|
|
38
|
+
|
|
39
|
+
// Create a new socket
|
|
40
|
+
const adbSocket = await this.#adb.createSocket("sync:", {
|
|
41
|
+
unref: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (this.#closed) {
|
|
45
|
+
await adbSocket.close();
|
|
46
|
+
throw new Error("SocketPool is closed");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const socket = new Socket(adbSocket, this.#adb.maxPayloadSize);
|
|
50
|
+
this.#inUseSockets.add(socket);
|
|
51
|
+
|
|
52
|
+
return socket;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
this.#ref.unref();
|
|
55
|
+
throw e;
|
|
37
56
|
}
|
|
38
|
-
const socket = new Socket(adbSocket, this.#adb.maxPayloadSize);
|
|
39
|
-
this.#inUseSockets.add(socket);
|
|
40
|
-
return socket;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
async withSocket<T>(fn: (socket: Socket) => Promise<T>): Promise<T> {
|
|
@@ -52,8 +68,32 @@ export class SocketPool {
|
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
async *withSocketGenerator<T>(
|
|
72
|
+
fn: (socket: Socket) => AsyncGenerator<T, void, void>,
|
|
73
|
+
): AsyncGenerator<T, void, void> {
|
|
74
|
+
const socket = await this.acquire();
|
|
75
|
+
let discard = true;
|
|
76
|
+
try {
|
|
77
|
+
for await (const value of fn(socket)) {
|
|
78
|
+
yield value;
|
|
79
|
+
}
|
|
80
|
+
discard = false;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
discard = !(e instanceof AdbSyncError);
|
|
83
|
+
throw e;
|
|
84
|
+
} finally {
|
|
85
|
+
// When the generator is retuned early, `discard` will have the default value of `true`,
|
|
86
|
+
// the socket needs to be closed, so the server will stop sending data.
|
|
87
|
+
await this.release(socket, discard);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
55
91
|
async release(socket: Socket, discard = false): Promise<void> {
|
|
56
|
-
this.#inUseSockets.delete(socket)
|
|
92
|
+
if (!this.#inUseSockets.delete(socket)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.#ref.unref();
|
|
57
97
|
|
|
58
98
|
// If discarding or we already have enough sockets in the pool, close this socket
|
|
59
99
|
if (discard || this.#availableSockets.length >= this.#maxSize) {
|
|
@@ -91,6 +131,7 @@ export class SocketPool {
|
|
|
91
131
|
(socket) => socket.close(),
|
|
92
132
|
);
|
|
93
133
|
this.#inUseSockets.clear();
|
|
134
|
+
this.#ref.dispose();
|
|
94
135
|
|
|
95
136
|
await Promise.all([...closePromises, ...inUseClosePromises]);
|
|
96
137
|
}
|