feat(adb): buffer writes in sync push

This commit is contained in:
Simon Chan 2023-03-05 21:51:59 +08:00
parent 228d2ab7d3
commit 1fb86f5eed
11 changed files with 308 additions and 229 deletions

View file

@ -55,6 +55,7 @@ export class Adb implements Closeable {
): Promise<Adb> { ): Promise<Adb> {
// Initially, set to highest-supported version and payload size. // Initially, set to highest-supported version and payload size.
let version = 0x01000001; let version = 0x01000001;
// Android 4: 4K, Android 7: 256K, Android 9: 1M
let maxPayloadSize = 0x100000; let maxPayloadSize = 0x100000;
const resolver = new PromiseResolver<string>(); const resolver = new PromiseResolver<string>();
@ -167,11 +168,16 @@ export class Adb implements Closeable {
return this.dispatcher.disconnected; return this.dispatcher.disconnected;
} }
private _protocolVersion: number | undefined; private _protocolVersion: number;
public get protocolVersion() { public get protocolVersion() {
return this._protocolVersion; return this._protocolVersion;
} }
private _maxPayloadSize: number;
public get maxPayloadSize() {
return this._maxPayloadSize;
}
private _product: string | undefined; private _product: string | undefined;
public get product() { public get product() {
return this._product; return this._product;
@ -222,6 +228,7 @@ export class Adb implements Closeable {
}); });
this._protocolVersion = version; this._protocolVersion = version;
this._maxPayloadSize = maxPayloadSize;
this.subprocess = new AdbSubprocess(this); this.subprocess = new AdbSubprocess(this);
this.power = new AdbPower(this); this.power = new AdbPower(this);

View file

@ -3,5 +3,6 @@ export * from "./pull.js";
export * from "./push.js"; export * from "./push.js";
export * from "./request.js"; export * from "./request.js";
export * from "./response.js"; export * from "./response.js";
export * from "./socket.js";
export * from "./stat.js"; export * from "./stat.js";
export * from "./sync.js"; export * from "./sync.js";

View file

@ -1,11 +1,8 @@
import type {
BufferedReadableStream,
WritableStreamDefaultWriter,
} from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct"; import Struct from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js"; import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
import type { AdbSyncSocket } from "./socket.js";
import type { AdbSyncStat } from "./stat.js"; import type { AdbSyncStat } from "./stat.js";
import { AdbSyncLstatResponse, AdbSyncStatResponse } from "./stat.js"; import { AdbSyncLstatResponse, AdbSyncStatResponse } from "./stat.js";
@ -31,16 +28,15 @@ export const AdbSyncEntry2Response = new Struct({ littleEndian: true })
export type AdbSyncEntry2Response = export type AdbSyncEntry2Response =
(typeof AdbSyncEntry2Response)["TDeserializeResult"]; (typeof AdbSyncEntry2Response)["TDeserializeResult"];
export async function* adbSyncOpenDir( export async function* adbSyncOpenDirV2(
stream: BufferedReadableStream, socket: AdbSyncSocket,
writer: WritableStreamDefaultWriter<Uint8Array>, path: string
path: string, ): AsyncGenerator<AdbSyncEntry2Response, void, void> {
v2: boolean const locked = await socket.lock();
): AsyncGenerator<AdbSyncEntry, void, void> { try {
if (v2) { await adbSyncWriteRequest(locked, AdbSyncRequestId.List2, path);
await adbSyncWriteRequest(writer, AdbSyncRequestId.List2, path);
for await (const item of adbSyncReadResponses( for await (const item of adbSyncReadResponses(
stream, locked,
AdbSyncResponseId.Entry2, AdbSyncResponseId.Entry2,
AdbSyncEntry2Response AdbSyncEntry2Response
)) { )) {
@ -52,13 +48,39 @@ export async function* adbSyncOpenDir(
} }
yield item; yield item;
} }
} else { } finally {
await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path); locked.release();
}
}
export async function* adbSyncOpenDirV1(
socket: AdbSyncSocket,
path: string
): AsyncGenerator<AdbSyncEntryResponse, void, void> {
const locked = await socket.lock();
try {
await adbSyncWriteRequest(locked, AdbSyncRequestId.List, path);
for await (const item of adbSyncReadResponses( for await (const item of adbSyncReadResponses(
stream, locked,
AdbSyncResponseId.Entry, AdbSyncResponseId.Entry,
AdbSyncEntryResponse AdbSyncEntryResponse
)) { )) {
yield item;
}
} finally {
locked.release();
}
}
export async function* adbSyncOpenDir(
socket: AdbSyncSocket,
path: string,
v2: boolean
): AsyncGenerator<AdbSyncEntry, void, void> {
if (v2) {
yield* adbSyncOpenDirV2(socket, path);
} else {
for await (const item of adbSyncOpenDirV1(socket, path)) {
// Convert to same format as `AdbSyncEntry2Response` for easier consumption. // Convert to same format as `AdbSyncEntry2Response` for easier consumption.
// However it will add some overhead. // However it will add some overhead.
yield { yield {

View file

@ -1,12 +1,10 @@
import type { import type { ReadableStream } from "@yume-chan/stream-extra";
BufferedReadableStream, import { PushReadableStream } from "@yume-chan/stream-extra";
WritableStreamDefaultWriter,
} from "@yume-chan/stream-extra";
import { ReadableStream } from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct"; import Struct from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js"; import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js";
import type { AdbSyncSocket } from "./socket.js";
export const AdbSyncDataResponse = new Struct({ littleEndian: true }) export const AdbSyncDataResponse = new Struct({ littleEndian: true })
.uint32("dataLength") .uint32("dataLength")
@ -16,47 +14,44 @@ export const AdbSyncDataResponse = new Struct({ littleEndian: true })
export type AdbSyncDataResponse = export type AdbSyncDataResponse =
(typeof AdbSyncDataResponse)["TDeserializeResult"]; (typeof AdbSyncDataResponse)["TDeserializeResult"];
export async function* adbSyncPullGenerator(
socket: AdbSyncSocket,
path: string
): AsyncGenerator<Uint8Array, void, void> {
const locked = await socket.lock();
let done = false;
try {
await adbSyncWriteRequest(locked, AdbSyncRequestId.Receive, path);
for await (const packet of adbSyncReadResponses(
locked,
AdbSyncResponseId.Data,
AdbSyncDataResponse
)) {
yield packet.data;
}
done = true;
} finally {
if (!done) {
// sync pull can't be cancelled, so we have to read all data
for await (const packet of adbSyncReadResponses(
locked,
AdbSyncResponseId.Data,
AdbSyncDataResponse
)) {
void packet;
}
}
locked.release();
}
}
export function adbSyncPull( export function adbSyncPull(
stream: BufferedReadableStream, socket: AdbSyncSocket,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string path: string
): ReadableStream<Uint8Array> { ): ReadableStream<Uint8Array> {
let generator!: AsyncGenerator<AdbSyncDataResponse, void, void>; return new PushReadableStream(async (controller) => {
return new ReadableStream<Uint8Array>( for await (const data of adbSyncPullGenerator(socket, path)) {
{ await controller.enqueue(data);
async start() {
// TODO: If `ReadableStream.from(AsyncGenerator)` is added to spec, use it instead.
await adbSyncWriteRequest(
writer,
AdbSyncRequestId.Receive,
path
);
generator = adbSyncReadResponses(
stream,
AdbSyncResponseId.Data,
AdbSyncDataResponse
);
},
async pull(controller) {
const { done, value } = await generator.next();
if (done) {
controller.close();
return;
}
controller.enqueue(value.data);
},
cancel() {
generator.return().catch((e) => {
void e;
});
throw new Error(`Sync commands can't be canceled.`);
},
},
{
highWaterMark: 16 * 1024,
size(chunk) {
return chunk.byteLength;
},
} }
); });
} }

View file

@ -1,13 +1,10 @@
import type { import type { ReadableStream } from "@yume-chan/stream-extra";
BufferedReadableStream,
ReadableStream,
WritableStreamDefaultWriter,
} from "@yume-chan/stream-extra";
import { ChunkStream, WritableStream } from "@yume-chan/stream-extra"; import { ChunkStream, WritableStream } from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct"; import Struct from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js";
import type { AdbSyncSocket } from "./socket.js";
import { LinuxFileType } from "./stat.js"; import { LinuxFileType } from "./stat.js";
export const AdbSyncOkResponse = new Struct({ littleEndian: true }).uint32( export const AdbSyncOkResponse = new Struct({ littleEndian: true }).uint32(
@ -17,25 +14,37 @@ export const AdbSyncOkResponse = new Struct({ littleEndian: true }).uint32(
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024; export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
export async function adbSyncPush( export async function adbSyncPush(
stream: BufferedReadableStream, socket: AdbSyncSocket,
writer: WritableStreamDefaultWriter<Uint8Array>,
filename: string, filename: string,
file: ReadableStream<Uint8Array>, file: ReadableStream<Uint8Array>,
mode: number = (LinuxFileType.File << 12) | 0o666, mode: number = (LinuxFileType.File << 12) | 0o666,
mtime: number = (Date.now() / 1000) | 0, mtime: number = (Date.now() / 1000) | 0,
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE packetSize: number = ADB_SYNC_MAX_PACKET_SIZE
) { ) {
const pathAndMode = `${filename},${mode.toString()}`; const locked = await socket.lock();
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode); try {
const pathAndMode = `${filename},${mode.toString()}`;
await adbSyncWriteRequest(locked, AdbSyncRequestId.Send, pathAndMode);
await file.pipeThrough(new ChunkStream(packetSize)).pipeTo( await file.pipeThrough(new ChunkStream(packetSize)).pipeTo(
new WritableStream({ new WritableStream({
write: async (chunk) => { write: async (chunk) => {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk); await adbSyncWriteRequest(
}, locked,
}) AdbSyncRequestId.Data,
); chunk
);
},
})
);
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime); await adbSyncWriteRequest(locked, AdbSyncRequestId.Done, mtime);
await adbSyncReadResponse(stream, AdbSyncResponseId.Ok, AdbSyncOkResponse); await adbSyncReadResponse(
locked,
AdbSyncResponseId.Ok,
AdbSyncOkResponse
);
} finally {
locked.release();
}
} }

View file

@ -1,4 +1,3 @@
import type { WritableStreamDefaultWriter } from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct"; import Struct from "@yume-chan/struct";
import { encodeUtf8 } from "../../utils/index.js"; import { encodeUtf8 } from "../../utils/index.js";
@ -23,27 +22,32 @@ export const AdbSyncDataRequest = new Struct({ littleEndian: true })
.fields(AdbSyncNumberRequest) .fields(AdbSyncNumberRequest)
.uint8Array("data", { lengthField: "arg" }); .uint8Array("data", { lengthField: "arg" });
export interface AdbSyncWritable {
write(buffer: Uint8Array): Promise<void>;
}
export async function adbSyncWriteRequest( export async function adbSyncWriteRequest(
writer: WritableStreamDefaultWriter<Uint8Array>, writable: AdbSyncWritable,
id: AdbSyncRequestId | string, id: AdbSyncRequestId | string,
value: number | string | Uint8Array value: number | string | Uint8Array
): Promise<void> { ): Promise<void> {
let buffer: Uint8Array;
if (typeof value === "number") { if (typeof value === "number") {
buffer = AdbSyncNumberRequest.serialize({ const buffer = AdbSyncNumberRequest.serialize({
id, id,
arg: value, arg: value,
}); });
await writable.write(buffer);
} else if (typeof value === "string") { } else if (typeof value === "string") {
buffer = AdbSyncDataRequest.serialize({ // Let `writable` buffer writes
id, const buffer = encodeUtf8(value);
data: encodeUtf8(value), await writable.write(
}); AdbSyncNumberRequest.serialize({ id, arg: buffer.byteLength })
);
await writable.write(buffer);
} else { } else {
buffer = AdbSyncDataRequest.serialize({ await writable.write(
id, AdbSyncNumberRequest.serialize({ id, arg: value.byteLength })
data: value, );
}); await writable.write(value);
} }
await writer.write(buffer);
} }

View file

@ -1,5 +1,8 @@
import type { BufferedReadableStream } from "@yume-chan/stream-extra"; import type {
import type { StructLike, StructValueType } from "@yume-chan/struct"; StructAsyncDeserializeStream,
StructLike,
StructValueType,
} from "@yume-chan/struct";
import Struct from "@yume-chan/struct"; import Struct from "@yume-chan/struct";
import { decodeUtf8 } from "../../utils/index.js"; import { decodeUtf8 } from "../../utils/index.js";
@ -24,7 +27,7 @@ export const AdbSyncFailResponse = new Struct({ littleEndian: true })
}); });
export async function adbSyncReadResponse<T>( export async function adbSyncReadResponse<T>(
stream: BufferedReadableStream, stream: StructAsyncDeserializeStream,
id: AdbSyncResponseId, id: AdbSyncResponseId,
type: StructLike<T> type: StructLike<T>
): Promise<T> { ): Promise<T> {
@ -43,7 +46,7 @@ export async function adbSyncReadResponse<T>(
export async function* adbSyncReadResponses< export async function* adbSyncReadResponses<
T extends Struct<object, PropertyKey, object, any> T extends Struct<object, PropertyKey, object, any>
>( >(
stream: BufferedReadableStream, stream: StructAsyncDeserializeStream,
id: AdbSyncResponseId, id: AdbSyncResponseId,
type: T type: T
): AsyncGenerator<StructValueType<T>, void, void> { ): AsyncGenerator<StructValueType<T>, void, void> {

View file

@ -0,0 +1,95 @@
import type { WritableStreamDefaultWriter } from "@yume-chan/stream-extra";
import { BufferedReadableStream } from "@yume-chan/stream-extra";
import type { StructAsyncDeserializeStream } from "@yume-chan/struct";
import type { AdbSocket } from "../../index.js";
import { AutoResetEvent } from "../../index.js";
export class AdbSyncSocketLocked implements StructAsyncDeserializeStream {
private _writer: WritableStreamDefaultWriter<Uint8Array>;
private _readable: BufferedReadableStream;
private _bufferSize: number;
private _buffered: Uint8Array[] = [];
private _bufferedLength = 0;
private _lock: AutoResetEvent;
public constructor(
writer: WritableStreamDefaultWriter<Uint8Array>,
readable: BufferedReadableStream,
bufferSize: number,
lock: AutoResetEvent
) {
this._writer = writer;
this._readable = readable;
this._bufferSize = bufferSize;
this._lock = lock;
}
public async flush() {
if (this._bufferedLength === 0) {
return;
}
if (this._buffered.length === 1) {
await this._writer.write(this._buffered[0]!);
this._buffered.length = 0;
this._bufferedLength = 0;
return;
}
const data = new Uint8Array(this._bufferedLength);
let offset = 0;
for (const chunk of this._buffered) {
data.set(chunk, offset);
offset += chunk.byteLength;
}
this._buffered.length = 0;
this._bufferedLength = 0;
// Let AdbSocket chunk the data for us
await this._writer.write(data);
}
public async write(data: Uint8Array) {
this._buffered.push(data);
this._bufferedLength += data.byteLength;
if (this._bufferedLength >= this._bufferSize) {
await this.flush();
}
}
public async read(length: number) {
await this.flush();
return await this._readable.read(length);
}
public release(): void {
this._buffered.length = 0;
this._bufferedLength = 0;
this._lock.notifyOne();
}
}
export class AdbSyncSocket {
private _lock = new AutoResetEvent();
private _socket: AdbSocket;
private _locked: AdbSyncSocketLocked;
public constructor(socket: AdbSocket, bufferSize: number) {
this._socket = socket;
this._locked = new AdbSyncSocketLocked(
socket.writable.getWriter(),
new BufferedReadableStream(socket.readable),
bufferSize,
this._lock
);
}
public async lock() {
await this._lock.wait();
return this._locked;
}
public async close() {
await this._socket.close();
}
}

View file

@ -1,11 +1,8 @@
import type {
BufferedReadableStream,
WritableStreamDefaultWriter,
} from "@yume-chan/stream-extra";
import Struct, { placeholder } from "@yume-chan/struct"; import Struct, { placeholder } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js";
import type { AdbSyncSocket } from "./socket.js";
// https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36 // https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36
export enum LinuxFileType { export enum LinuxFileType {
@ -104,49 +101,57 @@ export type AdbSyncStatResponse =
(typeof AdbSyncStatResponse)["TDeserializeResult"]; (typeof AdbSyncStatResponse)["TDeserializeResult"];
export async function adbSyncLstat( export async function adbSyncLstat(
stream: BufferedReadableStream, socket: AdbSyncSocket,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string, path: string,
v2: boolean v2: boolean
): Promise<AdbSyncStat> { ): Promise<AdbSyncStat> {
if (v2) { const locked = await socket.lock();
await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat2, path); try {
return await adbSyncReadResponse( if (v2) {
stream, await adbSyncWriteRequest(locked, AdbSyncRequestId.Lstat2, path);
AdbSyncResponseId.Lstat2, return await adbSyncReadResponse(
AdbSyncStatResponse locked,
); AdbSyncResponseId.Lstat2,
} else { AdbSyncStatResponse
await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat, path); );
const response = await adbSyncReadResponse( } else {
stream, await adbSyncWriteRequest(locked, AdbSyncRequestId.Lstat, path);
AdbSyncResponseId.Lstat, const response = await adbSyncReadResponse(
AdbSyncLstatResponse locked,
); AdbSyncResponseId.Lstat,
return { AdbSyncLstatResponse
mode: response.mode, );
// Convert to `BigInt` to make it compatible with `AdbSyncStatResponse` return {
size: BigInt(response.size), mode: response.mode,
mtime: BigInt(response.mtime), // Convert to `BigInt` to make it compatible with `AdbSyncStatResponse`
get type() { size: BigInt(response.size),
return response.type; mtime: BigInt(response.mtime),
}, get type() {
get permission() { return response.type;
return response.permission; },
}, get permission() {
}; return response.permission;
},
};
}
} finally {
locked.release();
} }
} }
export async function adbSyncStat( export async function adbSyncStat(
stream: BufferedReadableStream, socket: AdbSyncSocket,
writer: WritableStreamDefaultWriter<Uint8Array>,
path: string path: string
): Promise<AdbSyncStatResponse> { ): Promise<AdbSyncStatResponse> {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path); const locked = await socket.lock();
return await adbSyncReadResponse( try {
stream, await adbSyncWriteRequest(locked, AdbSyncRequestId.Stat, path);
AdbSyncResponseId.Stat, return await adbSyncReadResponse(
AdbSyncStatResponse locked,
); AdbSyncResponseId.Stat,
AdbSyncStatResponse
);
} finally {
locked.release();
}
} }

View file

@ -1,23 +1,16 @@
import { AutoDisposable } from "@yume-chan/event"; import { AutoDisposable } from "@yume-chan/event";
import type { import type { ReadableStream } from "@yume-chan/stream-extra";
ReadableStream,
WritableStreamDefaultWriter,
} from "@yume-chan/stream-extra";
import {
BufferedReadableStream,
WrapReadableStream,
} from "@yume-chan/stream-extra";
import type { Adb } from "../../adb.js"; import type { Adb } from "../../adb.js";
import { AdbFeature } from "../../features.js"; import { AdbFeature } from "../../features.js";
import type { AdbSocket } from "../../socket/index.js"; import type { AdbSocket } from "../../socket/index.js";
import { AutoResetEvent } from "../../utils/index.js";
import { escapeArg } from "../subprocess/index.js"; import { escapeArg } from "../subprocess/index.js";
import type { AdbSyncEntry } from "./list.js"; import type { AdbSyncEntry } from "./list.js";
import { adbSyncOpenDir } from "./list.js"; import { adbSyncOpenDir } from "./list.js";
import { adbSyncPull } from "./pull.js"; import { adbSyncPull } from "./pull.js";
import { adbSyncPush } from "./push.js"; import { adbSyncPush } from "./push.js";
import { AdbSyncSocket } from "./socket.js";
import { adbSyncLstat, adbSyncStat } from "./stat.js"; import { adbSyncLstat, adbSyncStat } from "./stat.js";
/** /**
@ -37,55 +30,38 @@ export function dirname(path: string): string {
} }
export class AdbSync extends AutoDisposable { export class AdbSync extends AutoDisposable {
protected adb: Adb; protected _adb: Adb;
protected _socket: AdbSyncSocket;
protected stream: BufferedReadableStream;
// Getting another writer on a locked WritableStream will throw.
// We don't want this behavior on higher-level APIs.
// So we acquire the writer early and use a blocking lock to guard it.
protected writer: WritableStreamDefaultWriter<Uint8Array>;
protected sendLock = this.addDisposable(new AutoResetEvent());
public get supportsStat(): boolean { public get supportsStat(): boolean {
return this.adb.supportsFeature(AdbFeature.StatV2); return this._adb.supportsFeature(AdbFeature.StatV2);
} }
public get supportsList2(): boolean { public get supportsList2(): boolean {
return this.adb.supportsFeature(AdbFeature.ListV2); return this._adb.supportsFeature(AdbFeature.ListV2);
} }
public get fixedPushMkdir(): boolean { public get fixedPushMkdir(): boolean {
return this.adb.supportsFeature(AdbFeature.FixedPushMkdir); return this._adb.supportsFeature(AdbFeature.FixedPushMkdir);
} }
public get needPushMkdirWorkaround(): boolean { public get needPushMkdirWorkaround(): boolean {
// https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361 // https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
return ( return (
this.adb.supportsFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir this._adb.supportsFeature(AdbFeature.ShellV2) &&
!this.fixedPushMkdir
); );
} }
public constructor(adb: Adb, socket: AdbSocket) { public constructor(adb: Adb, socket: AdbSocket) {
super(); super();
this.adb = adb; this._adb = adb;
this.stream = new BufferedReadableStream(socket.readable); this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize);
this.writer = socket.writable.getWriter();
} }
public async lstat(path: string) { public async lstat(path: string) {
await this.sendLock.wait(); return await adbSyncLstat(this._socket, path, this.supportsStat);
try {
return adbSyncLstat(
this.stream,
this.writer,
path,
this.supportsStat
);
} finally {
this.sendLock.notifyOne();
}
} }
public async stat(path: string) { public async stat(path: string) {
@ -93,13 +69,7 @@ export class AdbSync extends AutoDisposable {
throw new Error("Not supported"); throw new Error("Not supported");
} }
await this.sendLock.wait(); return await adbSyncStat(this._socket, path);
try {
return adbSyncStat(this.stream, this.writer, path);
} finally {
this.sendLock.notifyOne();
}
} }
public async isDirectory(path: string): Promise<boolean> { public async isDirectory(path: string): Promise<boolean> {
@ -111,21 +81,8 @@ export class AdbSync extends AutoDisposable {
} }
} }
public async *opendir( public opendir(path: string): AsyncGenerator<AdbSyncEntry, void, void> {
path: string return adbSyncOpenDir(this._socket, path, this.supportsList2);
): AsyncGenerator<AdbSyncEntry, void, void> {
await this.sendLock.wait();
try {
yield* adbSyncOpenDir(
this.stream,
this.writer,
path,
this.supportsList2
);
} finally {
this.sendLock.notifyOne();
}
} }
public async readdir(path: string) { public async readdir(path: string) {
@ -143,15 +100,7 @@ export class AdbSync extends AutoDisposable {
* @returns A `ReadableStream` that reads from the file. * @returns A `ReadableStream` that reads from the file.
*/ */
public read(filename: string): ReadableStream<Uint8Array> { public read(filename: string): ReadableStream<Uint8Array> {
return new WrapReadableStream({ return adbSyncPull(this._socket, filename);
start: async () => {
await this.sendLock.wait();
return adbSyncPull(this.stream, this.writer, filename);
},
close: () => {
this.sendLock.notifyOne();
},
});
} }
/** /**
@ -169,35 +118,22 @@ export class AdbSync extends AutoDisposable {
mode?: number, mode?: number,
mtime?: number mtime?: number
) { ) {
await this.sendLock.wait(); if (this.needPushMkdirWorkaround) {
// It may fail if the path is already existed.
try { // Ignore the result.
if (this.needPushMkdirWorkaround) { // TODO: sync: test push mkdir workaround (need an Android 8 device)
// It may fail if the path is already existed. await this._adb.subprocess.spawnAndWait([
// Ignore the result. "mkdir",
// TODO: sync: test push mkdir workaround (need an Android 8 device) "-p",
await this.adb.subprocess.spawnAndWait([ escapeArg(dirname(filename)),
"mkdir", ]);
"-p",
escapeArg(dirname(filename)),
]);
}
await adbSyncPush(
this.stream,
this.writer,
filename,
file,
mode,
mtime
);
} finally {
this.sendLock.notifyOne();
} }
await adbSyncPush(this._socket, filename, file, mode, mtime);
} }
public override async dispose() { public override async dispose() {
super.dispose(); super.dispose();
await this.writer.close(); await this._socket.close();
} }
} }

View file

@ -81,7 +81,9 @@ export class AdbPacketSerializeStream extends TransformStream<
); );
if (init.payload.byteLength) { if (init.payload.byteLength) {
// Enqueue payload separately to avoid copying // USB protocol preserves packet boundaries,
// so we must write payload separately as native ADB does,
// otherwise the read operation on device will fail.
controller.enqueue(init.payload); controller.enqueue(init.payload);
} }
}, },