mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-04 10:19:17 +02:00
refactor(adb): simplify reading sync responses
This commit is contained in:
parent
c9000c5beb
commit
1486eed3cf
18 changed files with 164 additions and 183 deletions
|
@ -63,6 +63,7 @@ export class Adb implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), {
|
}), {
|
||||||
|
// Don't cancel the source ReadableStream on AbortSignal abort.
|
||||||
preventCancel: true,
|
preventCancel: true,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
})
|
})
|
||||||
|
@ -146,7 +147,7 @@ export class Adb implements Closeable {
|
||||||
private _device: string | undefined;
|
private _device: string | undefined;
|
||||||
public get device() { return this._device; }
|
public get device() { return this._device; }
|
||||||
|
|
||||||
private _features: AdbFeatures[] | undefined;
|
private _features: AdbFeatures[] = [];
|
||||||
public get features() { return this._features; }
|
public get features() { return this._features; }
|
||||||
|
|
||||||
public readonly subprocess: AdbSubprocess;
|
public readonly subprocess: AdbSubprocess;
|
||||||
|
@ -190,8 +191,6 @@ export class Adb implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseBanner(banner: string): void {
|
private parseBanner(banner: string): void {
|
||||||
this._features = [];
|
|
||||||
|
|
||||||
const pieces = banner.split('::');
|
const pieces = banner.split('::');
|
||||||
if (pieces.length > 1) {
|
if (pieces.length > 1) {
|
||||||
const props = pieces[1]!;
|
const props = pieces[1]!;
|
||||||
|
@ -224,8 +223,13 @@ export class Adb implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addIncomingSocketHandler(handler: AdbIncomingSocketHandler) {
|
/**
|
||||||
return this.dispatcher.addIncomingSocketHandler(handler);
|
* Add a handler for incoming socket.
|
||||||
|
* @param handler A function to call with new incoming sockets. It must return `true` if it accepts the socket.
|
||||||
|
* @returns A function to remove the handler.
|
||||||
|
*/
|
||||||
|
public onIncomingSocket(handler: AdbIncomingSocketHandler) {
|
||||||
|
return this.dispatcher.onIncomingSocket(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createSocket(service: string): Promise<AdbSocket> {
|
public async createSocket(service: string): Promise<AdbSocket> {
|
||||||
|
|
|
@ -7,19 +7,6 @@ const Version =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
.uint32('version');
|
.uint32('version');
|
||||||
|
|
||||||
/*
|
|
||||||
* ADB uses 8 int32 fields to describe bit depths
|
|
||||||
* The only combination I have seen is RGBA8888, which is
|
|
||||||
* red_offset: 0
|
|
||||||
* red_length: 8
|
|
||||||
* blue_offset: 16
|
|
||||||
* blue_length: 8
|
|
||||||
* green_offset: 8
|
|
||||||
* green_length: 8
|
|
||||||
* alpha_offset: 24
|
|
||||||
* alpha_length: 8
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const AdbFrameBufferV1 =
|
export const AdbFrameBufferV1 =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
.uint32('bpp')
|
.uint32('bpp')
|
||||||
|
@ -57,6 +44,22 @@ export const AdbFrameBufferV2 =
|
||||||
|
|
||||||
export type AdbFrameBufferV2 = typeof AdbFrameBufferV2['TDeserializeResult'];
|
export type AdbFrameBufferV2 = typeof AdbFrameBufferV2['TDeserializeResult'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADB uses 8 int32 fields to describe bit depths
|
||||||
|
*
|
||||||
|
* The only combination I have seen is RGBA8888, which is
|
||||||
|
*
|
||||||
|
* red_offset: 0
|
||||||
|
* red_length: 8
|
||||||
|
* blue_offset: 16
|
||||||
|
* blue_length: 8
|
||||||
|
* green_offset: 8
|
||||||
|
* green_length: 8
|
||||||
|
* alpha_offset: 24
|
||||||
|
* alpha_length: 8
|
||||||
|
*
|
||||||
|
* But it doesn't mean that other combinations are not possible.
|
||||||
|
*/
|
||||||
export type AdbFrameBuffer = AdbFrameBufferV1 | AdbFrameBufferV2;
|
export type AdbFrameBuffer = AdbFrameBufferV1 | AdbFrameBufferV2;
|
||||||
|
|
||||||
export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
|
export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
|
||||||
|
@ -65,6 +68,7 @@ export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
|
||||||
const { version } = await Version.deserialize(stream);
|
const { version } = await Version.deserialize(stream);
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case 1:
|
case 1:
|
||||||
|
// TODO: AdbFrameBuffer: does all v1 responses uses the same color space? Add it so the command returns same format for all versions.
|
||||||
return AdbFrameBufferV1.deserialize(stream);
|
return AdbFrameBufferV1.deserialize(stream);
|
||||||
case 2:
|
case 2:
|
||||||
return AdbFrameBufferV2.deserialize(stream);
|
return AdbFrameBufferV2.deserialize(stream);
|
||||||
|
|
|
@ -13,6 +13,7 @@ export function install(
|
||||||
return new WrapWritableStream<Uint8Array>({
|
return new WrapWritableStream<Uint8Array>({
|
||||||
async start() {
|
async start() {
|
||||||
// TODO: install: support other install apk methods (streaming, etc.)
|
// TODO: install: support other install apk methods (streaming, etc.)
|
||||||
|
// TODO: install: support split apk formats (`adb install-multiple`)
|
||||||
|
|
||||||
// Upload apk file to tmp folder
|
// Upload apk file to tmp folder
|
||||||
sync = await adb.sync();
|
sync = await adb.sync();
|
||||||
|
|
|
@ -41,7 +41,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.adb = adb;
|
this.adb = adb;
|
||||||
this.addDisposable(this.adb.addIncomingSocketHandler(this.handleIncomingSocket));
|
this.addDisposable(this.adb.onIncomingSocket(this.handleIncomingSocket));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleIncomingSocket = async (socket: AdbSocket) => {
|
protected handleIncomingSocket = async (socket: AdbSocket) => {
|
||||||
|
@ -80,8 +80,8 @@ export class AdbReverseCommand extends AutoDisposable {
|
||||||
/**
|
/**
|
||||||
* @param deviceAddress The address adbd on device is listening on. Can be `tcp:0` to let adbd choose an available TCP port by itself.
|
* @param deviceAddress The address adbd on device is listening on. Can be `tcp:0` to let adbd choose an available TCP port by itself.
|
||||||
* @param localAddress Native ADB client will open a connection to this address when reverse connection received. In WebADB, it's only used to uniquely identify a reverse tunnel registry, `handler` will be called to handle the connection.
|
* @param localAddress Native ADB client will open a connection to this address when reverse connection received. In WebADB, it's only used to uniquely identify a reverse tunnel registry, `handler` will be called to handle the connection.
|
||||||
* @param handler A callback to handle incoming connections
|
* @param handler A callback to handle incoming connections. It must return `true` if it accepts the connection.
|
||||||
* @returns If `deviceAddress` is `tcp:0`, return `tcp:{ACTUAL_LISTENING_PORT}`; otherwise, return `deviceAddress`.
|
* @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
|
||||||
*/
|
*/
|
||||||
public async add(
|
public async add(
|
||||||
deviceAddress: string,
|
deviceAddress: string,
|
||||||
|
@ -91,7 +91,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
||||||
const stream = await this.sendRequest(`reverse:forward:${deviceAddress};${localAddress}`);
|
const stream = await this.sendRequest(`reverse:forward:${deviceAddress};${localAddress}`);
|
||||||
|
|
||||||
// `tcp:0` tells the device to pick an available port.
|
// `tcp:0` tells the device to pick an available port.
|
||||||
// Begin with Android 8, device will respond with the selected port for all `tcp:` requests.
|
// On Android >=8, device will respond with the selected port for all `tcp:` requests.
|
||||||
if (deviceAddress.startsWith('tcp:')) {
|
if (deviceAddress.startsWith('tcp:')) {
|
||||||
let length: number | undefined;
|
let length: number | undefined;
|
||||||
try {
|
try {
|
||||||
|
@ -101,7 +101,7 @@ export class AdbReverseCommand extends AutoDisposable {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device before Android 8 doesn't have this response.
|
// Android <8 doesn't have this response.
|
||||||
// (the stream is closed now)
|
// (the stream is closed now)
|
||||||
// Can be safely ignored.
|
// Can be safely ignored.
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
|
||||||
|
|
||||||
public static async raw(adb: Adb, command: string) {
|
public static async raw(adb: Adb, command: string) {
|
||||||
// `shell,raw:${command}` also triggers raw mode,
|
// `shell,raw:${command}` also triggers raw mode,
|
||||||
// But is not supported before Android 7.
|
// But is not supported on Android version <7.
|
||||||
return new AdbSubprocessNoneProtocol(await adb.createSocket(`exec:${command}`));
|
return new AdbSubprocessNoneProtocol(await adb.createSocket(`exec:${command}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,8 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
|
||||||
public constructor(socket: AdbSocket) {
|
public constructor(socket: AdbSocket) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
|
|
||||||
|
// Link `stdout`, `stderr` and `stdin` together,
|
||||||
|
// so closing any of them will close the others.
|
||||||
this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
|
this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
|
||||||
close: async () => {
|
close: async () => {
|
||||||
await this.socket.close();
|
await this.socket.close();
|
||||||
|
@ -62,7 +64,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize() {
|
public resize() {
|
||||||
// Not supported
|
// Not supported, but don't throw.
|
||||||
}
|
}
|
||||||
|
|
||||||
public kill() {
|
public kill() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PromiseResolver } from '@yume-chan/async';
|
import { PromiseResolver } from '@yume-chan/async';
|
||||||
import { pipeFrom, PushReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, type WritableStreamDefaultWriter, type PushReadableStreamController, type ReadableStream } from '@yume-chan/stream-extra';
|
import { pipeFrom, PushReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, type PushReadableStreamController, type ReadableStream, type WritableStreamDefaultWriter } from '@yume-chan/stream-extra';
|
||||||
import Struct, { placeholder, type StructValueType } from '@yume-chan/struct';
|
import Struct, { placeholder, type StructValueType } from '@yume-chan/struct';
|
||||||
|
|
||||||
import type { Adb } from '../../../adb.js';
|
import type { Adb } from '../../../adb.js';
|
||||||
|
@ -103,7 +103,7 @@ class MultiplexStream<T>{
|
||||||
*/
|
*/
|
||||||
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||||
public static isSupported(adb: Adb) {
|
public static isSupported(adb: Adb) {
|
||||||
return adb.features!.includes(AdbFeatures.ShellV2);
|
return adb.features.includes(AdbFeatures.ShellV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async pty(adb: Adb, command: string) {
|
public static async pty(adb: Adb, command: string) {
|
||||||
|
@ -179,7 +179,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||||
data: encodeUtf8(
|
data: encodeUtf8(
|
||||||
// The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}`
|
// The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}`
|
||||||
// However, according to https://linux.die.net/man/4/tty_ioctl
|
// However, according to https://linux.die.net/man/4/tty_ioctl
|
||||||
// `x_pixels` and `y_pixels` are not used, so always passing `0` is fine.
|
// `x_pixels` and `y_pixels` are unused, so always sending `0` should be fine.
|
||||||
`${rows}x${cols},0x0\0`
|
`${rows}x${cols},0x0\0`
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,17 +6,17 @@ import type { AdbSocket } from '../../../socket/index.js';
|
||||||
|
|
||||||
export interface AdbSubprocessProtocol {
|
export interface AdbSubprocessProtocol {
|
||||||
/**
|
/**
|
||||||
* A WritableStream that writes to the `stdin` pipe.
|
* A WritableStream that writes to the `stdin` stream.
|
||||||
*/
|
*/
|
||||||
readonly stdin: WritableStream<Uint8Array>;
|
readonly stdin: WritableStream<Uint8Array>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `stdout` pipe of the process.
|
* The `stdout` stream of the process.
|
||||||
*/
|
*/
|
||||||
readonly stdout: ReadableStream<Uint8Array>;
|
readonly stdout: ReadableStream<Uint8Array>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `stderr` pipe of the process.
|
* The `stderr` stream of the process.
|
||||||
*
|
*
|
||||||
* Note: Some `AdbSubprocessProtocol` doesn't separate `stdout` and `stderr`,
|
* Note: Some `AdbSubprocessProtocol` doesn't separate `stdout` and `stderr`,
|
||||||
* All output will be sent to `stdout`.
|
* All output will be sent to `stdout`.
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { BufferedReadableStream, WritableStreamDefaultWriter } from '@yume-
|
||||||
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 { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js';
|
import { adbSyncReadResponses, AdbSyncResponseId } from './response.js';
|
||||||
import { AdbSyncLstatResponse, AdbSyncStatResponse, type AdbSyncStat } from './stat.js';
|
import { AdbSyncLstatResponse, AdbSyncStatResponse, type AdbSyncStat } from './stat.js';
|
||||||
|
|
||||||
export interface AdbSyncEntry extends AdbSyncStat {
|
export interface AdbSyncEntry extends AdbSyncStat {
|
||||||
|
@ -27,61 +27,28 @@ export const AdbSyncEntry2Response =
|
||||||
|
|
||||||
export type AdbSyncEntry2Response = typeof AdbSyncEntry2Response['TDeserializeResult'];
|
export type AdbSyncEntry2Response = typeof AdbSyncEntry2Response['TDeserializeResult'];
|
||||||
|
|
||||||
const LIST_V1_RESPONSE_TYPES = {
|
|
||||||
[AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
|
|
||||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntryResponse.size),
|
|
||||||
};
|
|
||||||
|
|
||||||
const LIST_V2_RESPONSE_TYPES = {
|
|
||||||
[AdbSyncResponseId.Entry2]: AdbSyncEntry2Response,
|
|
||||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntry2Response.size),
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function* adbSyncOpenDir(
|
export async function* adbSyncOpenDir(
|
||||||
stream: BufferedReadableStream,
|
stream: BufferedReadableStream,
|
||||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||||
path: string,
|
path: string,
|
||||||
v2: boolean,
|
v2: boolean,
|
||||||
): AsyncGenerator<AdbSyncEntry, void, void> {
|
): AsyncGenerator<AdbSyncEntry, void, void> {
|
||||||
let requestId: AdbSyncRequestId.List | AdbSyncRequestId.List2;
|
|
||||||
let responseTypes: typeof LIST_V1_RESPONSE_TYPES | typeof LIST_V2_RESPONSE_TYPES;
|
|
||||||
|
|
||||||
if (v2) {
|
if (v2) {
|
||||||
requestId = AdbSyncRequestId.List2;
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.List2, path);
|
||||||
responseTypes = LIST_V2_RESPONSE_TYPES;
|
yield* adbSyncReadResponses(stream, AdbSyncResponseId.Entry2, AdbSyncEntry2Response);
|
||||||
} else {
|
} else {
|
||||||
requestId = AdbSyncRequestId.List;
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path);
|
||||||
responseTypes = LIST_V1_RESPONSE_TYPES;
|
for await (const item of adbSyncReadResponses(stream, AdbSyncResponseId.Entry, AdbSyncEntryResponse)) {
|
||||||
}
|
// Convert to same format as `AdbSyncEntry2Response` for easier consumption.
|
||||||
|
// However it will add some overhead.
|
||||||
await adbSyncWriteRequest(writer, requestId, path);
|
yield {
|
||||||
|
mode: item.mode,
|
||||||
while (true) {
|
size: BigInt(item.size),
|
||||||
const response = await adbSyncReadResponse(stream, responseTypes);
|
mtime: BigInt(item.mtime),
|
||||||
switch (response.id) {
|
get type() { return item.type; },
|
||||||
case AdbSyncResponseId.Entry:
|
get permission() { return item.permission; },
|
||||||
yield {
|
name: item.name,
|
||||||
mode: response.mode,
|
};
|
||||||
size: BigInt(response.size),
|
|
||||||
mtime: BigInt(response.mtime),
|
|
||||||
get type() { return response.type; },
|
|
||||||
get permission() { return response.permission; },
|
|
||||||
name: response.name,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case AdbSyncResponseId.Entry2:
|
|
||||||
// `LST2` can return error codes for failed `lstat` calls.
|
|
||||||
// `LIST` just ignores them.
|
|
||||||
// But they only contain `name` so still pretty useless.
|
|
||||||
if (response.error !== 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
yield response;
|
|
||||||
break;
|
|
||||||
case AdbSyncResponseId.Done:
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected response id');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { BufferedReadableStream, ReadableStream, WritableStreamDefaultWriter } f
|
||||||
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 { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js';
|
import { adbSyncReadResponses, AdbSyncResponseId } from './response.js';
|
||||||
|
|
||||||
export const AdbSyncDataResponse =
|
export const AdbSyncDataResponse =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
|
@ -10,35 +10,33 @@ export const AdbSyncDataResponse =
|
||||||
.uint8Array('data', { lengthField: 'dataLength' })
|
.uint8Array('data', { lengthField: 'dataLength' })
|
||||||
.extra({ id: AdbSyncResponseId.Data as const });
|
.extra({ id: AdbSyncResponseId.Data as const });
|
||||||
|
|
||||||
const RESPONSE_TYPES = {
|
export type AdbSyncDataResponse = typeof AdbSyncDataResponse['TDeserializeResult'];
|
||||||
[AdbSyncResponseId.Data]: AdbSyncDataResponse,
|
|
||||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncDataResponse.size),
|
|
||||||
};
|
|
||||||
|
|
||||||
export function adbSyncPull(
|
export function adbSyncPull(
|
||||||
stream: BufferedReadableStream,
|
stream: BufferedReadableStream,
|
||||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||||
path: string,
|
path: string,
|
||||||
): ReadableStream<Uint8Array> {
|
): ReadableStream<Uint8Array> {
|
||||||
|
let generator!: AsyncGenerator<AdbSyncDataResponse, void, void>;
|
||||||
return new ReadableStream<Uint8Array>({
|
return new ReadableStream<Uint8Array>({
|
||||||
async start() {
|
async start() {
|
||||||
|
// TODO: If `ReadableStream.from(AsyncGenerator)` is added to spec, use it instead.
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path);
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Receive, path);
|
||||||
|
generator = adbSyncReadResponses(stream, AdbSyncResponseId.Data, AdbSyncDataResponse);
|
||||||
},
|
},
|
||||||
async pull(controller) {
|
async pull(controller) {
|
||||||
const response = await adbSyncReadResponse(stream, RESPONSE_TYPES);
|
const { done, value } = await generator.next();
|
||||||
switch (response.id) {
|
if (done) {
|
||||||
case AdbSyncResponseId.Data:
|
controller.close();
|
||||||
controller.enqueue(response.data!);
|
return;
|
||||||
break;
|
|
||||||
case AdbSyncResponseId.Done:
|
|
||||||
controller.close();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected response id');
|
|
||||||
}
|
}
|
||||||
|
controller.enqueue(value.data);
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
throw new Error(`Sync commands don't support cancel.`);
|
try {
|
||||||
|
generator.return();
|
||||||
|
} catch { }
|
||||||
|
throw new Error(`Sync commands can't be canceled.`);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
highWaterMark: 16 * 1024,
|
highWaterMark: 16 * 1024,
|
||||||
|
|
|
@ -9,10 +9,6 @@ export const AdbSyncOkResponse =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
.uint32('unused');
|
.uint32('unused');
|
||||||
|
|
||||||
const ResponseTypes = {
|
|
||||||
[AdbSyncResponseId.Ok]: AdbSyncOkResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
|
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
|
||||||
|
|
||||||
export function adbSyncPush(
|
export function adbSyncPush(
|
||||||
|
@ -34,7 +30,7 @@ export function adbSyncPush(
|
||||||
},
|
},
|
||||||
async close() {
|
async close() {
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
|
||||||
await adbSyncReadResponse(stream, ResponseTypes);
|
await adbSyncReadResponse(stream, AdbSyncResponseId.Ok, AdbSyncOkResponse);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new ChunkStream(packetSize)
|
new ChunkStream(packetSize)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { BufferedReadableStream } from '@yume-chan/stream-extra';
|
import type { BufferedReadableStream } from '@yume-chan/stream-extra';
|
||||||
import Struct, { type StructAsyncDeserializeStream, type StructLike, type StructValueType } from '@yume-chan/struct';
|
import Struct, { StructValueType, type StructLike } from '@yume-chan/struct';
|
||||||
|
|
||||||
import { decodeUtf8 } from '../../utils/index.js';
|
import { decodeUtf8 } from '../../utils/index.js';
|
||||||
|
|
||||||
|
@ -15,25 +15,6 @@ export enum AdbSyncResponseId {
|
||||||
Fail = 'FAIL',
|
Fail = 'FAIL',
|
||||||
}
|
}
|
||||||
|
|
||||||
// DONE responses' size are always same as the request's normal response.
|
|
||||||
// For example DONE responses for LIST requests are 16 bytes (same as DENT responses),
|
|
||||||
// but DONE responses for STAT requests are 12 bytes (same as STAT responses)
|
|
||||||
// So we need to know responses' size in advance.
|
|
||||||
export class AdbSyncDoneResponse implements StructLike<AdbSyncDoneResponse> {
|
|
||||||
private length: number;
|
|
||||||
|
|
||||||
public readonly id = AdbSyncResponseId.Done;
|
|
||||||
|
|
||||||
public constructor(length: number) {
|
|
||||||
this.length = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deserialize(stream: StructAsyncDeserializeStream): Promise<this> {
|
|
||||||
await stream.read(this.length);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AdbSyncFailResponse =
|
export const AdbSyncFailResponse =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
.uint32('messageLength')
|
.uint32('messageLength')
|
||||||
|
@ -42,24 +23,46 @@ export const AdbSyncFailResponse =
|
||||||
throw new Error(object.message);
|
throw new Error(object.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function adbSyncReadResponse<T extends Record<string, StructLike<any>>>(
|
export async function adbSyncReadResponse<T>(
|
||||||
stream: BufferedReadableStream,
|
stream: BufferedReadableStream,
|
||||||
types: T,
|
id: AdbSyncResponseId,
|
||||||
// When `T` is a union type, `T[keyof T]` only includes their common keys.
|
type: StructLike<T>,
|
||||||
// For example, let `type T = { a: string, b: string } | { a: string, c: string}`,
|
): Promise<T> {
|
||||||
// `keyof T` is `'a'`, not `'a' | 'b' | 'c'`.
|
const actualId = decodeUtf8(await stream.read(4));
|
||||||
// However, `T extends unknown ? keyof T : never` will distribute `T`,
|
switch (actualId) {
|
||||||
// so returns all keys.
|
case AdbSyncResponseId.Fail:
|
||||||
): Promise<StructValueType<T extends unknown ? T[keyof T] : never>> {
|
await AdbSyncFailResponse.deserialize(stream);
|
||||||
const id = decodeUtf8(await stream.read(4));
|
throw new Error('Unreachable');
|
||||||
|
case id:
|
||||||
if (id === AdbSyncResponseId.Fail) {
|
return await type.deserialize(stream);
|
||||||
await AdbSyncFailResponse.deserialize(stream);
|
default:
|
||||||
|
throw new Error(`Expected '${id}', but got '${actualId}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* adbSyncReadResponses<T extends Struct<any, any, any, any>>(
|
||||||
|
stream: BufferedReadableStream,
|
||||||
|
id: AdbSyncResponseId,
|
||||||
|
type: T,
|
||||||
|
): AsyncGenerator<StructValueType<T>, void, void> {
|
||||||
|
while (true) {
|
||||||
|
const actualId = decodeUtf8(await stream.read(4));
|
||||||
|
switch (actualId) {
|
||||||
|
case AdbSyncResponseId.Fail:
|
||||||
|
await AdbSyncFailResponse.deserialize(stream);
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
case AdbSyncResponseId.Done:
|
||||||
|
// `DONE` responses' size are always same as the request's normal response.
|
||||||
|
//
|
||||||
|
// For example, `DONE` responses for `LIST` requests are 16 bytes (same as `DENT` responses),
|
||||||
|
// but `DONE` responses for `STAT` requests are 12 bytes (same as `STAT` responses).
|
||||||
|
await stream.read(type.size);
|
||||||
|
return;
|
||||||
|
case id:
|
||||||
|
yield await type.deserialize(stream);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Expected '${id}' or '${AdbSyncResponseId.Done}', but got '${actualId}'`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (types[id]) {
|
|
||||||
return types[id]!.deserialize(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Expected '${Object.keys(types).join(', ')}', but got '${id}'`);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,50 +96,26 @@ export const AdbSyncStatResponse =
|
||||||
|
|
||||||
export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult'];
|
export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult'];
|
||||||
|
|
||||||
const STAT_RESPONSE_TYPES = {
|
|
||||||
[AdbSyncResponseId.Stat]: AdbSyncStatResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LSTAT_RESPONSE_TYPES = {
|
|
||||||
[AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LSTAT_V2_RESPONSE_TYPES = {
|
|
||||||
[AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function adbSyncLstat(
|
export async function adbSyncLstat(
|
||||||
stream: BufferedReadableStream,
|
stream: BufferedReadableStream,
|
||||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||||
path: string,
|
path: string,
|
||||||
v2: boolean,
|
v2: boolean,
|
||||||
): Promise<AdbSyncStat> {
|
): Promise<AdbSyncStat> {
|
||||||
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
|
|
||||||
let responseTypes: typeof LSTAT_RESPONSE_TYPES | typeof LSTAT_V2_RESPONSE_TYPES;
|
|
||||||
|
|
||||||
if (v2) {
|
if (v2) {
|
||||||
requestId = AdbSyncRequestId.Lstat2;
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat2, path);
|
||||||
responseTypes = LSTAT_V2_RESPONSE_TYPES;
|
return await adbSyncReadResponse(stream, AdbSyncResponseId.Lstat2, AdbSyncStatResponse);
|
||||||
} else {
|
} else {
|
||||||
requestId = AdbSyncRequestId.Lstat;
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Lstat, path);
|
||||||
responseTypes = LSTAT_RESPONSE_TYPES;
|
const response = await adbSyncReadResponse(stream, AdbSyncResponseId.Lstat, AdbSyncLstatResponse);
|
||||||
}
|
return {
|
||||||
|
mode: response.mode,
|
||||||
await adbSyncWriteRequest(writer, requestId, path);
|
// Convert to `BigInt` to make it compatible with `AdbSyncStatResponse`
|
||||||
const response = await adbSyncReadResponse(stream, responseTypes);
|
size: BigInt(response.size),
|
||||||
|
mtime: BigInt(response.mtime),
|
||||||
switch (response.id) {
|
get type() { return response.type; },
|
||||||
case AdbSyncResponseId.Lstat:
|
get permission() { return response.permission; },
|
||||||
return {
|
};
|
||||||
mode: response.mode,
|
|
||||||
// Convert to `BigInt` to make it compatible with `AdbSyncStatResponse`
|
|
||||||
size: BigInt(response.size),
|
|
||||||
mtime: BigInt(response.mtime),
|
|
||||||
get type() { return response.type; },
|
|
||||||
get permission() { return response.permission; },
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,5 +125,5 @@ export async function adbSyncStat(
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<AdbSyncStatResponse> {
|
): Promise<AdbSyncStatResponse> {
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
|
||||||
return await adbSyncReadResponse(stream, STAT_RESPONSE_TYPES);
|
return await adbSyncReadResponse(stream, AdbSyncResponseId.Stat, AdbSyncStatResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,20 +38,20 @@ export class AdbSync extends AutoDisposable {
|
||||||
protected sendLock = this.addDisposable(new AutoResetEvent());
|
protected sendLock = this.addDisposable(new AutoResetEvent());
|
||||||
|
|
||||||
public get supportsStat(): boolean {
|
public get supportsStat(): boolean {
|
||||||
return this.adb.features!.includes(AdbFeatures.StatV2);
|
return this.adb.features.includes(AdbFeatures.StatV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get supportsList2(): boolean {
|
public get supportsList2(): boolean {
|
||||||
return this.adb.features!.includes(AdbFeatures.ListV2);
|
return this.adb.features.includes(AdbFeatures.ListV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get fixedPushMkdir(): boolean {
|
public get fixedPushMkdir(): boolean {
|
||||||
return this.adb.features!.includes(AdbFeatures.FixedPushMkdir);
|
return this.adb.features.includes(AdbFeatures.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 this.adb.features!.includes(AdbFeatures.ShellV2) && !this.fixedPushMkdir;
|
return this.adb.features.includes(AdbFeatures.ShellV2) && !this.fixedPushMkdir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(adb: Adb, socket: AdbSocket) {
|
public constructor(adb: Adb, socket: AdbSocket) {
|
||||||
|
|
|
@ -96,7 +96,7 @@ export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
|
||||||
|
|
||||||
// Taken from https://stackoverflow.com/a/51562038
|
// Taken from https://stackoverflow.com/a/51562038
|
||||||
// I can't understand, but it does work
|
// I can't understand, but it does work
|
||||||
// Only used with numbers less than 2^32 so doesn't need BigInt
|
// Only used with numbers smaller than 2^32 so doesn't need BigInt
|
||||||
export function modInverse(a: number, m: number) {
|
export function modInverse(a: number, m: number) {
|
||||||
a = (a % m + m) % m;
|
a = (a % m + m) % m;
|
||||||
if (!a || m < 2) {
|
if (!a || m < 2) {
|
||||||
|
|
|
@ -168,7 +168,12 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
// the device may also respond with two `CLSE` packets.
|
// the device may also respond with two `CLSE` packets.
|
||||||
}
|
}
|
||||||
|
|
||||||
public addIncomingSocketHandler(handler: AdbIncomingSocketHandler): RemoveEventListener {
|
/**
|
||||||
|
* Add a handler for incoming socket.
|
||||||
|
* @param handler A function to call with new incoming sockets. It must return `true` if it accepts the socket.
|
||||||
|
* @returns A function to remove the handler.
|
||||||
|
*/
|
||||||
|
public onIncomingSocket(handler: AdbIncomingSocketHandler): RemoveEventListener {
|
||||||
this._incomingSocketHandlers.add(handler);
|
this._incomingSocketHandlers.add(handler);
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
this._incomingSocketHandlers.delete(handler);
|
this._incomingSocketHandlers.delete(handler);
|
||||||
|
@ -178,7 +183,7 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleOpen(packet: AdbPacketData) {
|
private async handleOpen(packet: AdbPacketData) {
|
||||||
// AsyncOperationManager doesn't support get and skip an ID
|
// `AsyncOperationManager` doesn't support skipping IDs
|
||||||
// Use `add` + `resolve` to simulate this behavior
|
// Use `add` + `resolve` to simulate this behavior
|
||||||
const [localId] = this.initializers.add<number>();
|
const [localId] = this.initializers.add<number>();
|
||||||
this.initializers.resolve(localId, undefined);
|
this.initializers.resolve(localId, undefined);
|
||||||
|
|
|
@ -128,7 +128,7 @@ export class AdbSocketController implements AdbSocketInfo, ReadableWritablePair<
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AdbSocket is a duplex stream.
|
* A duplex stream representing a socket to ADB daemon.
|
||||||
*
|
*
|
||||||
* To close it, call either `socket.close()`,
|
* To close it, call either `socket.close()`,
|
||||||
* `socket.readable.cancel()`, `socket.readable.getReader().cancel()`,
|
* `socket.readable.cancel()`, `socket.readable.getReader().cancel()`,
|
||||||
|
|
|
@ -19,15 +19,38 @@ addRange('0', '9');
|
||||||
addRange('+', '+');
|
addRange('+', '+');
|
||||||
addRange('/', '/');
|
addRange('/', '/');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the required length of the output buffer for the given input length.
|
||||||
|
*
|
||||||
|
* @param inputLength Length of the input in bytes
|
||||||
|
* @returns Length of the output in bytes
|
||||||
|
*/
|
||||||
export function calculateBase64EncodedLength(inputLength: number): [outputLength: number, paddingLength: number] {
|
export function calculateBase64EncodedLength(inputLength: number): [outputLength: number, paddingLength: number] {
|
||||||
const remainder = inputLength % 3;
|
const remainder = inputLength % 3;
|
||||||
const paddingLength = remainder !== 0 ? 3 - remainder : 0;
|
const paddingLength = remainder !== 0 ? 3 - remainder : 0;
|
||||||
return [(inputLength + paddingLength) / 3 * 4, paddingLength];
|
return [(inputLength + paddingLength) / 3 * 4, paddingLength];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the given input buffer into base64.
|
||||||
|
*
|
||||||
|
* @param input The input buffer
|
||||||
|
* @returns The encoded output buffer
|
||||||
|
*/
|
||||||
export function encodeBase64(
|
export function encodeBase64(
|
||||||
input: Uint8Array,
|
input: Uint8Array,
|
||||||
): Uint8Array;
|
): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Encode the given input into base64 and write it to the output buffer.
|
||||||
|
*
|
||||||
|
* The output buffer must be at least as long as the value returned by `calculateBase64EncodedLength`.
|
||||||
|
* It can points to the same buffer as the input, as long as `output.offset <= input.offset - input.length / 3`,
|
||||||
|
* or `output.offset >= input.offset - 1`
|
||||||
|
*
|
||||||
|
* @param input The input buffer
|
||||||
|
* @param output The output buffer
|
||||||
|
* @returns The number of bytes written to the output buffer
|
||||||
|
*/
|
||||||
export function encodeBase64(
|
export function encodeBase64(
|
||||||
input: Uint8Array,
|
input: Uint8Array,
|
||||||
output: Uint8Array,
|
output: Uint8Array,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { ValueOrPromise } from "../utils.js";
|
import type { ValueOrPromise } from "../utils.js";
|
||||||
|
|
||||||
|
// TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying
|
||||||
|
|
||||||
export interface StructDeserializeStream {
|
export interface StructDeserializeStream {
|
||||||
/**
|
/**
|
||||||
* Read data from the underlying data source.
|
* Read data from the underlying data source.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue