feat(adb): support delayed ack

This commit is contained in:
Simon Chan 2024-02-01 23:04:21 +08:00
parent 59d78dae20
commit ac6dc1e57c
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
9 changed files with 267 additions and 61 deletions

View file

@ -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> {

View file

@ -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) {

View file

@ -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;
} }
/** /**

View file

@ -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;

View file

@ -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 {

View file

@ -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,
}); });

View file

@ -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",
} }

View file

@ -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,

View file

@ -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(