mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-06 03:50:18 +02:00
feat(adb): buffer writes in sync push
This commit is contained in:
parent
228d2ab7d3
commit
1fb86f5eed
11 changed files with 308 additions and 229 deletions
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
95
libraries/adb/src/commands/sync/socket.ts
Normal file
95
libraries/adb/src/commands/sync/socket.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue