mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 17:59:50 +02:00
feat(adb): support delayed ack
This commit is contained in:
parent
59d78dae20
commit
ac6dc1e57c
9 changed files with 267 additions and 61 deletions
|
@ -40,6 +40,8 @@ export interface AdbTransport extends Closeable {
|
||||||
|
|
||||||
readonly disconnected: Promise<void>;
|
readonly disconnected: Promise<void>;
|
||||||
|
|
||||||
|
readonly clientFeatures: readonly AdbFeature[];
|
||||||
|
|
||||||
connect(service: string): ValueOrPromise<AdbSocket>;
|
connect(service: string): ValueOrPromise<AdbSocket>;
|
||||||
|
|
||||||
addReverseTunnel(
|
addReverseTunnel(
|
||||||
|
@ -71,6 +73,14 @@ export class Adb implements Closeable {
|
||||||
return this.transport.disconnected;
|
return this.transport.disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get clientFeatures() {
|
||||||
|
return this.transport.clientFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get deviceFeatures() {
|
||||||
|
return this.banner.features;
|
||||||
|
}
|
||||||
|
|
||||||
readonly subprocess: AdbSubprocess;
|
readonly subprocess: AdbSubprocess;
|
||||||
readonly power: AdbPower;
|
readonly power: AdbPower;
|
||||||
readonly reverse: AdbReverseCommand;
|
readonly reverse: AdbReverseCommand;
|
||||||
|
@ -85,8 +95,11 @@ export class Adb implements Closeable {
|
||||||
this.tcpip = new AdbTcpIpCommand(this);
|
this.tcpip = new AdbTcpIpCommand(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsFeature(feature: AdbFeature): boolean {
|
canUseFeature(feature: AdbFeature): boolean {
|
||||||
return this.banner.features.includes(feature);
|
return (
|
||||||
|
this.clientFeatures.includes(feature) &&
|
||||||
|
this.deviceFeatures.includes(feature)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSocket(service: string): Promise<AdbSocket> {
|
async createSocket(service: string): Promise<AdbSocket> {
|
||||||
|
|
|
@ -47,7 +47,7 @@ type AdbShellProtocolPacket = StructValueType<typeof AdbShellProtocolPacket>;
|
||||||
*/
|
*/
|
||||||
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||||
static isSupported(adb: Adb) {
|
static isSupported(adb: Adb) {
|
||||||
return adb.supportsFeature(AdbFeature.ShellV2);
|
return adb.canUseFeature(AdbFeature.ShellV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pty(adb: Adb, command: string) {
|
static async pty(adb: Adb, command: string) {
|
||||||
|
|
|
@ -74,16 +74,15 @@ export class AdbSync extends AutoDisposable {
|
||||||
this._adb = adb;
|
this._adb = adb;
|
||||||
this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize);
|
this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize);
|
||||||
|
|
||||||
this.#supportsStat = adb.supportsFeature(AdbFeature.StatV2);
|
this.#supportsStat = adb.canUseFeature(AdbFeature.StatV2);
|
||||||
this.#supportsListV2 = adb.supportsFeature(AdbFeature.ListV2);
|
this.#supportsListV2 = adb.canUseFeature(AdbFeature.ListV2);
|
||||||
this.#fixedPushMkdir = adb.supportsFeature(AdbFeature.FixedPushMkdir);
|
this.#fixedPushMkdir = adb.canUseFeature(AdbFeature.FixedPushMkdir);
|
||||||
this.#supportsSendReceiveV2 = adb.supportsFeature(
|
this.#supportsSendReceiveV2 = adb.canUseFeature(
|
||||||
AdbFeature.SendReceiveV2,
|
AdbFeature.SendReceiveV2,
|
||||||
);
|
);
|
||||||
// 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
|
||||||
this.#needPushMkdirWorkaround =
|
this.#needPushMkdirWorkaround =
|
||||||
this._adb.supportsFeature(AdbFeature.ShellV2) &&
|
this._adb.canUseFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir;
|
||||||
!this.fixedPushMkdir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
ConsumableWritableStream,
|
ConsumableWritableStream,
|
||||||
WritableStream,
|
WritableStream,
|
||||||
} from "@yume-chan/stream-extra";
|
} from "@yume-chan/stream-extra";
|
||||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
import { EMPTY_UINT8_ARRAY, NumberFieldType } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
||||||
import { decodeUtf8, encodeUtf8 } from "../utils/index.js";
|
import { decodeUtf8, encodeUtf8 } from "../utils/index.js";
|
||||||
|
@ -32,6 +32,13 @@ export interface AdbPacketDispatcherOptions {
|
||||||
*/
|
*/
|
||||||
appendNullToServiceString: boolean;
|
appendNullToServiceString: boolean;
|
||||||
maxPayloadSize: number;
|
maxPayloadSize: number;
|
||||||
|
/**
|
||||||
|
* The number of bytes the device can send before receiving an ack packet.
|
||||||
|
|
||||||
|
* Set to 0 or any negative value to disable delayed ack.
|
||||||
|
* Otherwise the value must be in the range of unsigned 32-bit integer.
|
||||||
|
*/
|
||||||
|
initialDelayedAckBytes: number;
|
||||||
/**
|
/**
|
||||||
* Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
|
* Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
|
||||||
*/
|
*/
|
||||||
|
@ -39,6 +46,11 @@ export interface AdbPacketDispatcherOptions {
|
||||||
debugSlowRead?: boolean | undefined;
|
debugSlowRead?: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SocketOpenResult {
|
||||||
|
remoteId: number;
|
||||||
|
availableWriteBytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dispatcher is the "dumb" part of the connection handling logic.
|
* The dispatcher is the "dumb" part of the connection handling logic.
|
||||||
*
|
*
|
||||||
|
@ -79,6 +91,10 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
options: AdbPacketDispatcherOptions,
|
options: AdbPacketDispatcherOptions,
|
||||||
) {
|
) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
// Don't allow negative values in dispatcher
|
||||||
|
if (this.options.initialDelayedAckBytes < 0) {
|
||||||
|
this.options.initialDelayedAckBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
connection.readable
|
connection.readable
|
||||||
.pipeTo(
|
.pipeTo(
|
||||||
|
@ -169,15 +185,39 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleOkay(packet: AdbPacketData) {
|
#handleOkay(packet: AdbPacketData) {
|
||||||
if (this.#initializers.resolve(packet.arg1, packet.arg0)) {
|
let ackBytes: number;
|
||||||
|
if (this.options.initialDelayedAckBytes !== 0) {
|
||||||
|
if (packet.payload.byteLength !== 4) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid OKAY packet. Payload size should be 4",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ackBytes = NumberFieldType.Uint32.deserialize(packet.payload, true);
|
||||||
|
} else {
|
||||||
|
if (packet.payload.byteLength !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid OKAY packet. Payload size should be 0",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ackBytes = Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.#initializers.resolve(packet.arg1, {
|
||||||
|
remoteId: packet.arg0,
|
||||||
|
availableWriteBytes: ackBytes,
|
||||||
|
} satisfies SocketOpenResult)
|
||||||
|
) {
|
||||||
// Device successfully created the socket
|
// Device successfully created the socket
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = this.#sockets.get(packet.arg1);
|
const socket = this.#sockets.get(packet.arg1);
|
||||||
if (socket) {
|
if (socket) {
|
||||||
// Device has received last `WRTE` to the socket
|
// When delayed ack is enabled, device has received `ackBytes` from the socket.
|
||||||
socket.ack();
|
// When delayed ack is disabled, device has received last `WRTE` packet from the socket,
|
||||||
|
// `ackBytes` is `Infinity` in this case.
|
||||||
|
socket.ack(ackBytes);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +226,18 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
|
void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sendOkay(localId: number, remoteId: number, ackBytes: number) {
|
||||||
|
let payload: Uint8Array;
|
||||||
|
if (this.options.initialDelayedAckBytes !== 0) {
|
||||||
|
payload = new Uint8Array(4);
|
||||||
|
new DataView(payload.buffer).setUint32(0, ackBytes, true);
|
||||||
|
} else {
|
||||||
|
payload = EMPTY_UINT8_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
|
||||||
|
}
|
||||||
|
|
||||||
async #handleOpen(packet: AdbPacketData) {
|
async #handleOpen(packet: AdbPacketData) {
|
||||||
// `AsyncOperationManager` doesn't support skipping IDs
|
// `AsyncOperationManager` doesn't support skipping IDs
|
||||||
// Use `add` + `resolve` to simulate this behavior
|
// Use `add` + `resolve` to simulate this behavior
|
||||||
|
@ -193,9 +245,20 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
this.#initializers.resolve(localId, undefined);
|
this.#initializers.resolve(localId, undefined);
|
||||||
|
|
||||||
const remoteId = packet.arg0;
|
const remoteId = packet.arg0;
|
||||||
let service = decodeUtf8(packet.payload);
|
let initialDelayedAckBytes = packet.arg1;
|
||||||
if (service.endsWith("\0")) {
|
const service = decodeUtf8(packet.payload);
|
||||||
service = service.substring(0, service.length - 1);
|
|
||||||
|
if (this.options.initialDelayedAckBytes === 0) {
|
||||||
|
if (initialDelayedAckBytes !== 0) {
|
||||||
|
throw new Error("Invalid OPEN packet. arg1 should be 0");
|
||||||
|
}
|
||||||
|
initialDelayedAckBytes = Infinity;
|
||||||
|
} else {
|
||||||
|
if (initialDelayedAckBytes === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid OPEN packet. arg1 should be greater than 0",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = this.#incomingSocketHandlers.get(service);
|
const handler = this.#incomingSocketHandlers.get(service);
|
||||||
|
@ -211,11 +274,16 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
localCreated: false,
|
localCreated: false,
|
||||||
service,
|
service,
|
||||||
});
|
});
|
||||||
|
controller.ack(initialDelayedAckBytes);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handler(controller.socket);
|
await handler(controller.socket);
|
||||||
this.#sockets.set(localId, controller);
|
this.#sockets.set(localId, controller);
|
||||||
await this.sendPacket(AdbCommand.Okay, localId, remoteId);
|
await this.#sendOkay(
|
||||||
|
localId,
|
||||||
|
remoteId,
|
||||||
|
this.options.initialDelayedAckBytes,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.sendPacket(AdbCommand.Close, 0, remoteId);
|
await this.sendPacket(AdbCommand.Close, 0, remoteId);
|
||||||
}
|
}
|
||||||
|
@ -238,10 +306,10 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
}),
|
}),
|
||||||
(async () => {
|
(async () => {
|
||||||
await socket.enqueue(packet.payload);
|
await socket.enqueue(packet.payload);
|
||||||
await this.sendPacket(
|
await this.#sendOkay(
|
||||||
AdbCommand.Okay,
|
|
||||||
packet.arg1,
|
packet.arg1,
|
||||||
packet.arg0,
|
packet.arg0,
|
||||||
|
packet.payload.length,
|
||||||
);
|
);
|
||||||
handled = true;
|
handled = true;
|
||||||
})(),
|
})(),
|
||||||
|
@ -255,11 +323,17 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
service += "\0";
|
service += "\0";
|
||||||
}
|
}
|
||||||
|
|
||||||
const [localId, initializer] = this.#initializers.add<number>();
|
const [localId, initializer] =
|
||||||
await this.sendPacket(AdbCommand.Open, localId, 0, service);
|
this.#initializers.add<SocketOpenResult>();
|
||||||
|
await this.sendPacket(
|
||||||
|
AdbCommand.Open,
|
||||||
|
localId,
|
||||||
|
this.options.initialDelayedAckBytes,
|
||||||
|
service,
|
||||||
|
);
|
||||||
|
|
||||||
// Fulfilled by `handleOk`
|
// Fulfilled by `handleOk`
|
||||||
const remoteId = await initializer;
|
const { remoteId, availableWriteBytes } = await initializer;
|
||||||
const controller = new AdbDaemonSocketController({
|
const controller = new AdbDaemonSocketController({
|
||||||
dispatcher: this,
|
dispatcher: this,
|
||||||
localId,
|
localId,
|
||||||
|
@ -267,6 +341,7 @@ export class AdbPacketDispatcher implements Closeable {
|
||||||
localCreated: true,
|
localCreated: true,
|
||||||
service,
|
service,
|
||||||
});
|
});
|
||||||
|
controller.ack(availableWriteBytes);
|
||||||
this.#sockets.set(localId, controller);
|
this.#sockets.set(localId, controller);
|
||||||
|
|
||||||
return controller.socket;
|
return controller.socket;
|
||||||
|
|
|
@ -49,7 +49,6 @@ export class AdbDaemonSocketController
|
||||||
return this.#readable;
|
return this.#readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
#writePromise: PromiseResolver<void> | undefined;
|
|
||||||
#writableController!: WritableStreamDefaultController;
|
#writableController!: WritableStreamDefaultController;
|
||||||
readonly writable: WritableStream<Consumable<Uint8Array>>;
|
readonly writable: WritableStream<Consumable<Uint8Array>>;
|
||||||
|
|
||||||
|
@ -65,6 +64,23 @@ export class AdbDaemonSocketController
|
||||||
return this.#socket;
|
return this.#socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#availableWriteBytesChanged: PromiseResolver<void> | undefined;
|
||||||
|
/**
|
||||||
|
* When delayed ack is disabled, can be `Infinity` if the socket is ready to write.
|
||||||
|
* Exactly one packet can be written no matter how large it is. Or `-1` if the socket
|
||||||
|
* is waiting for ack.
|
||||||
|
*
|
||||||
|
* When delayed ack is enabled, a non-negative finite number indicates the number of
|
||||||
|
* bytes that can be written to the socket before receiving an ack.
|
||||||
|
*/
|
||||||
|
#availableWriteBytes = 0;
|
||||||
|
/**
|
||||||
|
* Gets the number of bytes that can be written to the socket without blocking.
|
||||||
|
*/
|
||||||
|
public get availableWriteBytes() {
|
||||||
|
return this.#availableWriteBytes;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(options: AdbDaemonSocketConstructionOptions) {
|
constructor(options: AdbDaemonSocketConstructionOptions) {
|
||||||
this.#dispatcher = options.dispatcher;
|
this.#dispatcher = options.dispatcher;
|
||||||
this.localId = options.localId;
|
this.localId = options.localId;
|
||||||
|
@ -88,17 +104,30 @@ export class AdbDaemonSocketController
|
||||||
start < size;
|
start < size;
|
||||||
start = end, end += chunkSize
|
start = end, end += chunkSize
|
||||||
) {
|
) {
|
||||||
this.#writePromise = new PromiseResolver();
|
const chunk = data.subarray(start, end);
|
||||||
|
const length = chunk.byteLength;
|
||||||
|
while (this.#availableWriteBytes < length) {
|
||||||
|
// Only one lock is required because Web Streams API guarantees
|
||||||
|
// that `write` is not reentrant.
|
||||||
|
this.#availableWriteBytesChanged =
|
||||||
|
new PromiseResolver();
|
||||||
|
await raceSignal(
|
||||||
|
() => this.#availableWriteBytesChanged!.promise,
|
||||||
|
controller.signal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#availableWriteBytes === Infinity) {
|
||||||
|
this.#availableWriteBytes = -1;
|
||||||
|
} else {
|
||||||
|
this.#availableWriteBytes -= length;
|
||||||
|
}
|
||||||
|
|
||||||
await this.#dispatcher.sendPacket(
|
await this.#dispatcher.sendPacket(
|
||||||
AdbCommand.Write,
|
AdbCommand.Write,
|
||||||
this.localId,
|
this.localId,
|
||||||
this.remoteId,
|
this.remoteId,
|
||||||
data.subarray(start, end),
|
chunk,
|
||||||
);
|
|
||||||
// Wait for ack packet
|
|
||||||
await raceSignal(
|
|
||||||
() => this.#writePromise!.promise,
|
|
||||||
controller.signal,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -124,8 +153,9 @@ export class AdbDaemonSocketController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ack() {
|
public ack(bytes: number) {
|
||||||
this.#writePromise?.resolve();
|
this.#availableWriteBytes += bytes;
|
||||||
|
this.#availableWriteBytesChanged?.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
@ -134,6 +164,8 @@ export class AdbDaemonSocketController
|
||||||
}
|
}
|
||||||
this.#closed = true;
|
this.#closed = true;
|
||||||
|
|
||||||
|
this.#availableWriteBytesChanged?.reject(new Error("Socket closed"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.#writableController.error(new Error("Socket closed"));
|
this.#writableController.error(new Error("Socket closed"));
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -26,6 +26,30 @@ import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||||
import { AdbCommand, calculateChecksum } from "./packet.js";
|
import { AdbCommand, calculateChecksum } from "./packet.js";
|
||||||
|
|
||||||
export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001;
|
export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001;
|
||||||
|
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
||||||
|
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
|
||||||
|
export const ADB_DAEMON_DEFAULT_FEATURES = [
|
||||||
|
AdbFeature.ShellV2,
|
||||||
|
AdbFeature.Cmd,
|
||||||
|
AdbFeature.StatV2,
|
||||||
|
AdbFeature.ListV2,
|
||||||
|
AdbFeature.FixedPushMkdir,
|
||||||
|
"apex",
|
||||||
|
AdbFeature.Abb,
|
||||||
|
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
||||||
|
// No special handling required.
|
||||||
|
"fixed_push_symlink_timestamp",
|
||||||
|
AdbFeature.AbbExec,
|
||||||
|
"remount_shell",
|
||||||
|
"track_app",
|
||||||
|
AdbFeature.SendReceiveV2,
|
||||||
|
"sendrecv_v2_brotli",
|
||||||
|
"sendrecv_v2_lz4",
|
||||||
|
"sendrecv_v2_zstd",
|
||||||
|
"sendrecv_v2_dry_run_send",
|
||||||
|
AdbFeature.DelayedAck,
|
||||||
|
] as AdbFeature[];
|
||||||
|
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
|
||||||
|
|
||||||
export type AdbDaemonConnection = ReadableWritablePair<
|
export type AdbDaemonConnection = ReadableWritablePair<
|
||||||
AdbPacketData,
|
AdbPacketData,
|
||||||
|
@ -37,6 +61,16 @@ interface AdbDaemonAuthenticationOptions {
|
||||||
connection: AdbDaemonConnection;
|
connection: AdbDaemonConnection;
|
||||||
credentialStore: AdbCredentialStore;
|
credentialStore: AdbCredentialStore;
|
||||||
authenticators?: AdbAuthenticator[];
|
authenticators?: AdbAuthenticator[];
|
||||||
|
features?: readonly AdbFeature[];
|
||||||
|
/**
|
||||||
|
* The number of bytes the device can send before receiving an ack packet.
|
||||||
|
*
|
||||||
|
* Set to 0 or any negative value to disable delayed ack in handshake.
|
||||||
|
* Otherwise the value must be in the range of unsigned 32-bit integer.
|
||||||
|
*
|
||||||
|
* Delayed ack requires Android 14, this option is ignored on older versions.
|
||||||
|
*/
|
||||||
|
initialDelayedAckBytes?: number;
|
||||||
/**
|
/**
|
||||||
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
|
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
|
||||||
*/
|
*/
|
||||||
|
@ -50,6 +84,16 @@ interface AdbDaemonSocketConnectorConstructionOptions {
|
||||||
version: number;
|
version: number;
|
||||||
maxPayloadSize: number;
|
maxPayloadSize: number;
|
||||||
banner: string;
|
banner: string;
|
||||||
|
features?: readonly AdbFeature[];
|
||||||
|
/**
|
||||||
|
* The number of bytes the device can send before receiving an ack packet.
|
||||||
|
*
|
||||||
|
* Set to 0 or any negative value to disable delayed ack in handshake.
|
||||||
|
* Otherwise the value must be in the range of unsigned 32-bit integer.
|
||||||
|
*
|
||||||
|
* Delayed ack requires Android 14, this option is ignored on older versions.
|
||||||
|
*/
|
||||||
|
initialDelayedAckBytes?: number;
|
||||||
/**
|
/**
|
||||||
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
|
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
|
||||||
*/
|
*/
|
||||||
|
@ -71,6 +115,8 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
connection,
|
connection,
|
||||||
credentialStore,
|
credentialStore,
|
||||||
authenticators = ADB_DEFAULT_AUTHENTICATORS,
|
authenticators = ADB_DEFAULT_AUTHENTICATORS,
|
||||||
|
features = ADB_DAEMON_DEFAULT_FEATURES,
|
||||||
|
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
||||||
...options
|
...options
|
||||||
}: AdbDaemonAuthenticationOptions): Promise<AdbDaemonTransport> {
|
}: AdbDaemonAuthenticationOptions): Promise<AdbDaemonTransport> {
|
||||||
// Initially, set to highest-supported version and payload size.
|
// Initially, set to highest-supported version and payload size.
|
||||||
|
@ -144,38 +190,25 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
await ConsumableWritableStream.write(writer, init as AdbPacketInit);
|
await ConsumableWritableStream.write(writer, init as AdbPacketInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actualFeatures = features.slice();
|
||||||
|
if (initialDelayedAckBytes <= 0) {
|
||||||
|
const index = features.indexOf(AdbFeature.DelayedAck);
|
||||||
|
if (index !== -1) {
|
||||||
|
actualFeatures.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let banner: string;
|
let banner: string;
|
||||||
try {
|
try {
|
||||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
|
||||||
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
|
|
||||||
const features = [
|
|
||||||
AdbFeature.ShellV2,
|
|
||||||
AdbFeature.Cmd,
|
|
||||||
AdbFeature.StatV2,
|
|
||||||
AdbFeature.ListV2,
|
|
||||||
AdbFeature.FixedPushMkdir,
|
|
||||||
"apex",
|
|
||||||
AdbFeature.Abb,
|
|
||||||
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
|
||||||
// No special handling required.
|
|
||||||
"fixed_push_symlink_timestamp",
|
|
||||||
AdbFeature.AbbExec,
|
|
||||||
"remount_shell",
|
|
||||||
"track_app",
|
|
||||||
AdbFeature.SendReceiveV2,
|
|
||||||
"sendrecv_v2_brotli",
|
|
||||||
"sendrecv_v2_lz4",
|
|
||||||
"sendrecv_v2_zstd",
|
|
||||||
"sendrecv_v2_dry_run_send",
|
|
||||||
].join(",");
|
|
||||||
|
|
||||||
await sendPacket({
|
await sendPacket({
|
||||||
command: AdbCommand.Connect,
|
command: AdbCommand.Connect,
|
||||||
arg0: version,
|
arg0: version,
|
||||||
arg1: maxPayloadSize,
|
arg1: maxPayloadSize,
|
||||||
// The terminating `;` is required in formal definition
|
// The terminating `;` is required in formal definition
|
||||||
// But ADB daemon (all versions) can still work without it
|
// But ADB daemon (all versions) can still work without it
|
||||||
payload: encodeUtf8(`host::features=${features}`),
|
payload: encodeUtf8(
|
||||||
|
`host::features=${actualFeatures.join(",")}`,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
banner = await resolver.promise;
|
banner = await resolver.promise;
|
||||||
|
@ -195,6 +228,8 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
version,
|
version,
|
||||||
maxPayloadSize,
|
maxPayloadSize,
|
||||||
banner,
|
banner,
|
||||||
|
features: actualFeatures,
|
||||||
|
initialDelayedAckBytes,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -229,16 +264,38 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
return this.#dispatcher.disconnected;
|
return this.#dispatcher.disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#clientFeatures: readonly AdbFeature[];
|
||||||
|
get clientFeatures() {
|
||||||
|
return this.#clientFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
serial,
|
serial,
|
||||||
connection,
|
connection,
|
||||||
version,
|
version,
|
||||||
banner,
|
banner,
|
||||||
|
features = ADB_DAEMON_DEFAULT_FEATURES,
|
||||||
|
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
||||||
...options
|
...options
|
||||||
}: AdbDaemonSocketConnectorConstructionOptions) {
|
}: AdbDaemonSocketConnectorConstructionOptions) {
|
||||||
this.#serial = serial;
|
this.#serial = serial;
|
||||||
this.#connection = connection;
|
this.#connection = connection;
|
||||||
this.#banner = AdbBanner.parse(banner);
|
this.#banner = AdbBanner.parse(banner);
|
||||||
|
this.#clientFeatures = features;
|
||||||
|
|
||||||
|
if (features.includes(AdbFeature.DelayedAck)) {
|
||||||
|
if (initialDelayedAckBytes <= 0) {
|
||||||
|
throw new Error(
|
||||||
|
"`initialDelayedAckBytes` must be greater than 0 when DelayedAck feature is enabled.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#banner.features.includes(AdbFeature.DelayedAck)) {
|
||||||
|
initialDelayedAckBytes = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initialDelayedAckBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let calculateChecksum: boolean;
|
let calculateChecksum: boolean;
|
||||||
let appendNullToServiceString: boolean;
|
let appendNullToServiceString: boolean;
|
||||||
|
@ -253,6 +310,7 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
this.#dispatcher = new AdbPacketDispatcher(connection, {
|
this.#dispatcher = new AdbPacketDispatcher(connection, {
|
||||||
calculateChecksum,
|
calculateChecksum,
|
||||||
appendNullToServiceString,
|
appendNullToServiceString,
|
||||||
|
initialDelayedAckBytes,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// The order follows
|
// The order follows
|
||||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
// https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/transport.cpp;l=77;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
|
||||||
export enum AdbFeature {
|
export enum AdbFeature {
|
||||||
ShellV2 = "shell_v2",
|
ShellV2 = "shell_v2",
|
||||||
Cmd = "cmd",
|
Cmd = "cmd",
|
||||||
|
@ -9,4 +9,5 @@ export enum AdbFeature {
|
||||||
Abb = "abb",
|
Abb = "abb",
|
||||||
AbbExec = "abb_exec",
|
AbbExec = "abb_exec",
|
||||||
SendReceiveV2 = "sendrecv_v2",
|
SendReceiveV2 = "sendrecv_v2",
|
||||||
|
DelayedAck = "delayed_ack",
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,31 @@ import type {
|
||||||
AdbTransport,
|
AdbTransport,
|
||||||
} from "../adb.js";
|
} from "../adb.js";
|
||||||
import type { AdbBanner } from "../banner.js";
|
import type { AdbBanner } from "../banner.js";
|
||||||
|
import { AdbFeature } from "../features.js";
|
||||||
|
|
||||||
import type { AdbServerClient } from "./client.js";
|
import type { AdbServerClient } from "./client.js";
|
||||||
|
|
||||||
|
export const ADB_SERVER_DEFAULT_FEATURES = [
|
||||||
|
AdbFeature.ShellV2,
|
||||||
|
AdbFeature.Cmd,
|
||||||
|
AdbFeature.StatV2,
|
||||||
|
AdbFeature.ListV2,
|
||||||
|
AdbFeature.FixedPushMkdir,
|
||||||
|
"apex",
|
||||||
|
AdbFeature.Abb,
|
||||||
|
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
||||||
|
// No special handling required.
|
||||||
|
"fixed_push_symlink_timestamp",
|
||||||
|
AdbFeature.AbbExec,
|
||||||
|
"remount_shell",
|
||||||
|
"track_app",
|
||||||
|
AdbFeature.SendReceiveV2,
|
||||||
|
"sendrecv_v2_brotli",
|
||||||
|
"sendrecv_v2_lz4",
|
||||||
|
"sendrecv_v2_zstd",
|
||||||
|
"sendrecv_v2_dry_run_send",
|
||||||
|
] as AdbFeature[];
|
||||||
|
|
||||||
export class AdbServerTransport implements AdbTransport {
|
export class AdbServerTransport implements AdbTransport {
|
||||||
#client: AdbServerClient;
|
#client: AdbServerClient;
|
||||||
|
|
||||||
|
@ -26,6 +48,12 @@ export class AdbServerTransport implements AdbTransport {
|
||||||
#waitAbortController = new AbortController();
|
#waitAbortController = new AbortController();
|
||||||
readonly disconnected: Promise<void>;
|
readonly disconnected: Promise<void>;
|
||||||
|
|
||||||
|
get clientFeatures() {
|
||||||
|
// No need to get host features (features supported by ADB server)
|
||||||
|
// Because we create all ADB packets ourselves
|
||||||
|
return ADB_SERVER_DEFAULT_FEATURES;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
client: AdbServerClient,
|
client: AdbServerClient,
|
||||||
serial: string,
|
serial: string,
|
||||||
|
|
|
@ -35,10 +35,10 @@ export class Cmd extends AdbCommandBase {
|
||||||
|
|
||||||
constructor(adb: Adb) {
|
constructor(adb: Adb) {
|
||||||
super(adb);
|
super(adb);
|
||||||
this.#supportsShellV2 = adb.supportsFeature(AdbFeature.ShellV2);
|
this.#supportsShellV2 = adb.canUseFeature(AdbFeature.ShellV2);
|
||||||
this.#supportsCmd = adb.supportsFeature(AdbFeature.Cmd);
|
this.#supportsCmd = adb.canUseFeature(AdbFeature.Cmd);
|
||||||
this.#supportsAbb = adb.supportsFeature(AdbFeature.Abb);
|
this.#supportsAbb = adb.canUseFeature(AdbFeature.Abb);
|
||||||
this.#supportsAbbExec = adb.supportsFeature(AdbFeature.AbbExec);
|
this.#supportsAbbExec = adb.canUseFeature(AdbFeature.AbbExec);
|
||||||
}
|
}
|
||||||
|
|
||||||
async spawn(
|
async spawn(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue