feat(adb): update feature list (#797)

This commit is contained in:
Simon Chan 2025-09-04 23:20:16 +08:00 committed by GitHub
parent c5f282285a
commit 29de3e4842
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 218 additions and 174 deletions

View file

@ -0,0 +1,5 @@
---
"@yume-chan/adb": major
---
Sync ADB feature list with latest ADB source code. Some features have been renamed to align with ADB source code. These feature flags are considered implementation details and generally not needed for outside consumers, but it's a breaking change anyway.

View file

@ -15,8 +15,14 @@ export class AdbNoneProtocolSubprocessService {
} }
spawn = adbNoneProtocolSpawner(async (command, signal) => { spawn = adbNoneProtocolSpawner(async (command, signal) => {
// `shell,raw:${command}` also triggers raw mode, // Android 7 added `shell,raw:${command}` service which also triggers raw mode,
// But is not supported on Android version <7. // but we want to use the most compatible one.
//
// Similar to SSH, we don't escape the `command`,
// because the command will be invoked by `sh -c`,
// it can contain environment variables (`KEY=value command`),
// and shell expansions (`echo "$KEY"` vs `echo '$KEY'`),
// which we can't know how to properly escape.
const socket = await this.#adb.createSocket( const socket = await this.#adb.createSocket(
`exec:${command.join(" ")}`, `exec:${command.join(" ")}`,
); );
@ -33,8 +39,10 @@ export class AdbNoneProtocolSubprocessService {
command?: string | readonly string[], command?: string | readonly string[],
): Promise<AdbNoneProtocolPtyProcess> { ): Promise<AdbNoneProtocolPtyProcess> {
if (command === undefined) { if (command === undefined) {
// Run the default shell
command = ""; command = "";
} else if (Array.isArray(command)) { } else if (Array.isArray(command)) {
// Don't escape `command`. See `spawn` above for details
command = command.join(" "); command = command.join(" ");
} }

View file

@ -25,7 +25,7 @@ export class AdbSubprocessService {
this.#noneProtocol = new AdbNoneProtocolSubprocessService(adb); this.#noneProtocol = new AdbNoneProtocolSubprocessService(adb);
if (adb.canUseFeature(AdbFeature.ShellV2)) { if (adb.canUseFeature(AdbFeature.Shell2)) {
this.#shellProtocol = new AdbShellProtocolSubprocessService(adb); this.#shellProtocol = new AdbShellProtocolSubprocessService(adb);
} }
} }

View file

@ -12,7 +12,7 @@ export class AdbShellProtocolSubprocessService {
} }
get isSupported() { get isSupported() {
return this.#adb.canUseFeature(AdbFeature.ShellV2); return this.#adb.canUseFeature(AdbFeature.Shell2);
} }
constructor(adb: Adb) { constructor(adb: Adb) {
@ -20,6 +20,7 @@ export class AdbShellProtocolSubprocessService {
} }
spawn = adbShellProtocolSpawner(async (command, signal) => { spawn = adbShellProtocolSpawner(async (command, signal) => {
// Don't escape `command`. See `AdbNoneProtocolSubprocessService.prototype.spawn` for details.
const socket = await this.#adb.createSocket( const socket = await this.#adb.createSocket(
`shell,v2,raw:${command.join(" ")}`, `shell,v2,raw:${command.join(" ")}`,
); );
@ -45,6 +46,7 @@ export class AdbShellProtocolSubprocessService {
service += ":"; service += ":";
if (options) { if (options) {
// Don't escape `command`. See `AdbNoneProtocolSubprocessService.prototype.spawn` for details.
if (typeof options.command === "string") { if (typeof options.command === "string") {
service += options.command; service += options.command;
} else if (Array.isArray(options.command)) { } else if (Array.isArray(options.command)) {

View file

@ -0,0 +1,33 @@
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
function encodeAsciiUnchecked(value: string): Uint8Array<ArrayBuffer> {
const result = new Uint8Array(value.length);
for (let i = 0; i < value.length; i += 1) {
result[i] = value.charCodeAt(i);
}
return result;
}
/**
* Encode ID to numbers for faster comparison.
*
* This function skips all checks. The caller must ensure the input is valid.
*
* @param value A 4 ASCII character string.
* @returns A 32-bit integer by encoding the string as little-endian
*
* #__NO_SIDE_EFFECTS__
*/
export function adbSyncEncodeId(value: string): number {
const buffer = encodeAsciiUnchecked(value);
return getUint32LittleEndian(buffer, 0);
}
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd
export const Lstat = adbSyncEncodeId("STAT");
export const Stat = adbSyncEncodeId("STA2");
export const LstatV2 = adbSyncEncodeId("LST2");
export const Done = adbSyncEncodeId("DONE");
export const Data = adbSyncEncodeId("DATA");

View file

@ -0,0 +1,12 @@
import { adbSyncEncodeId } from "./id-common.js";
export { Data, Done, Lstat, LstatV2, Stat } from "./id-common.js";
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd
export const List = adbSyncEncodeId("LIST");
export const ListV2 = adbSyncEncodeId("LIS2");
export const Send = adbSyncEncodeId("SEND");
export const SendV2 = adbSyncEncodeId("SND2");
export const Receive = adbSyncEncodeId("RECV");

View file

@ -0,0 +1,11 @@
import { adbSyncEncodeId } from "./id-common.js";
export { Data, Done, Lstat, LstatV2, Stat } from "./id-common.js";
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd
export const Entry = adbSyncEncodeId("DENT");
export const EntryV2 = adbSyncEncodeId("DNT2");
export const Ok = adbSyncEncodeId("OKAY");
export const Fail = adbSyncEncodeId("FAIL");

View file

@ -0,0 +1,7 @@
import * as AdbSyncRequestId from "./id-request.js";
import * as AdbSyncResponseId from "./id-response.js";
// Values of `AdbSyncRequestId` and `AdbSyncResponseId` are all generic `number`s
// so there is no point creating types for them
export { AdbSyncRequestId, AdbSyncResponseId };

View file

@ -1,3 +1,4 @@
export * from "./id.js";
export * from "./list.js"; export * from "./list.js";
export * from "./pull.js"; export * from "./pull.js";
export * from "./push.js"; export * from "./push.js";

View file

@ -1,8 +1,9 @@
import type { StructValue } from "@yume-chan/struct"; import type { StructValue } from "@yume-chan/struct";
import { extend, string, u32 } from "@yume-chan/struct"; import { extend, string, u32 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponses } from "./response.js";
import type { AdbSyncSocket } from "./socket.js"; import type { AdbSyncSocket } from "./socket.js";
import type { AdbSyncStat } from "./stat.js"; import type { AdbSyncStat } from "./stat.js";
import { import {
@ -36,7 +37,7 @@ export async function* adbSyncOpenDirV2(
await adbSyncWriteRequest(locked, AdbSyncRequestId.ListV2, path); await adbSyncWriteRequest(locked, AdbSyncRequestId.ListV2, path);
for await (const item of adbSyncReadResponses( for await (const item of adbSyncReadResponses(
locked, locked,
AdbSyncResponseId.Entry2, AdbSyncResponseId.EntryV2,
AdbSyncEntry2Response, AdbSyncEntry2Response,
)) { )) {
// `LST2` can return error codes for failed `lstat` calls. // `LST2` can return error codes for failed `lstat` calls.

View file

@ -2,8 +2,9 @@ import { ReadableStream } from "@yume-chan/stream-extra";
import type { StructValue } from "@yume-chan/struct"; import type { StructValue } from "@yume-chan/struct";
import { buffer, struct, u32 } from "@yume-chan/struct"; import { buffer, struct, u32 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { adbSyncReadResponses, AdbSyncResponseId } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponses } from "./response.js";
import type { AdbSyncSocket } from "./socket.js"; import type { AdbSyncSocket } from "./socket.js";
export const AdbSyncDataResponse = struct( export const AdbSyncDataResponse = struct(

View file

@ -8,8 +8,9 @@ import { struct, u32 } from "@yume-chan/struct";
import { NOOP } from "../../utils/index.js"; import { NOOP } from "../../utils/index.js";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponse } from "./response.js";
import type { AdbSyncSocket, AdbSyncSocketLocked } from "./socket.js"; import type { AdbSyncSocket, AdbSyncSocketLocked } from "./socket.js";
import { LinuxFileType } from "./stat.js"; import { LinuxFileType } from "./stat.js";

View file

@ -1,20 +1,5 @@
import { encodeUtf8, struct, u32 } from "@yume-chan/struct"; import { encodeUtf8, struct, u32 } from "@yume-chan/struct";
import { adbSyncEncodeId } from "./response.js";
export const AdbSyncRequestId = {
List: adbSyncEncodeId("LIST"),
ListV2: adbSyncEncodeId("LIS2"),
Send: adbSyncEncodeId("SEND"),
SendV2: adbSyncEncodeId("SND2"),
Lstat: adbSyncEncodeId("STAT"),
Stat: adbSyncEncodeId("STA2"),
LstatV2: adbSyncEncodeId("LST2"),
Data: adbSyncEncodeId("DATA"),
Done: adbSyncEncodeId("DONE"),
Receive: adbSyncEncodeId("RECV"),
} as const;
export const AdbSyncNumberRequest = struct( export const AdbSyncNumberRequest = struct(
{ id: u32, arg: u32 }, { id: u32, arg: u32 },
{ littleEndian: true }, { littleEndian: true },
@ -26,13 +11,9 @@ export interface AdbSyncWritable {
export async function adbSyncWriteRequest( export async function adbSyncWriteRequest(
writable: AdbSyncWritable, writable: AdbSyncWritable,
id: number | string, id: number,
value: number | string | Uint8Array, value: number | string | Uint8Array,
): Promise<void> { ): Promise<void> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
if (typeof value === "number") { if (typeof value === "number") {
await writable.write( await writable.write(
AdbSyncNumberRequest.serialize({ id, arg: value }), AdbSyncNumberRequest.serialize({ id, arg: value }),

View file

@ -2,39 +2,9 @@ import { getUint32LittleEndian } from "@yume-chan/no-data-view";
import type { AsyncExactReadable, StructDeserializer } from "@yume-chan/struct"; import type { AsyncExactReadable, StructDeserializer } from "@yume-chan/struct";
import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct"; import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct";
import { unreachable } from "../../utils/no-op.js"; import { unreachable } from "../../utils/index.js";
function encodeAsciiUnchecked(value: string): Uint8Array<ArrayBuffer> { import { AdbSyncResponseId } from "./id.js";
const result = new Uint8Array(value.length);
for (let i = 0; i < value.length; i += 1) {
result[i] = value.charCodeAt(i);
}
return result;
}
/**
* Encode ID to numbers for faster comparison
* @param value A 4-character string
* @returns A 32-bit integer by encoding the string as little-endian
*
* #__NO_SIDE_EFFECTS__
*/
export function adbSyncEncodeId(value: string): number {
const buffer = encodeAsciiUnchecked(value);
return getUint32LittleEndian(buffer, 0);
}
export const AdbSyncResponseId = {
Entry: adbSyncEncodeId("DENT"),
Entry2: adbSyncEncodeId("DNT2"),
Lstat: adbSyncEncodeId("STAT"),
Stat: adbSyncEncodeId("STA2"),
Lstat2: adbSyncEncodeId("LST2"),
Done: adbSyncEncodeId("DONE"),
Data: adbSyncEncodeId("DATA"),
Ok: adbSyncEncodeId("OKAY"),
Fail: adbSyncEncodeId("FAIL"),
};
export class AdbSyncError extends Error {} export class AdbSyncError extends Error {}
@ -50,18 +20,14 @@ export const AdbSyncFailResponse = struct(
export async function adbSyncReadResponse<T>( export async function adbSyncReadResponse<T>(
stream: AsyncExactReadable, stream: AsyncExactReadable,
id: number | string, id: number,
type: StructDeserializer<T>, type: StructDeserializer<T>,
): Promise<T> { ): Promise<T> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
const buffer = await stream.readExactly(4); const buffer = await stream.readExactly(4);
switch (getUint32LittleEndian(buffer, 0)) { switch (getUint32LittleEndian(buffer, 0)) {
case AdbSyncResponseId.Fail: case AdbSyncResponseId.Fail:
await AdbSyncFailResponse.deserialize(stream); await AdbSyncFailResponse.deserialize(stream);
throw new Error("Unreachable"); unreachable();
case id: case id:
return await type.deserialize(stream); return await type.deserialize(stream);
default: default:
@ -73,13 +39,9 @@ export async function adbSyncReadResponse<T>(
export async function* adbSyncReadResponses<T>( export async function* adbSyncReadResponses<T>(
stream: AsyncExactReadable, stream: AsyncExactReadable,
id: number | string, id: number,
type: StructDeserializer<T>, type: StructDeserializer<T>,
): AsyncGenerator<T, void, void> { ): AsyncGenerator<T, void, void> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
while (true) { while (true) {
const buffer = await stream.readExactly(4); const buffer = await stream.readExactly(4);
switch (getUint32LittleEndian(buffer, 0)) { switch (getUint32LittleEndian(buffer, 0)) {

View file

@ -1,8 +1,9 @@
import type { StructValue } from "@yume-chan/struct"; import type { StructValue } from "@yume-chan/struct";
import { struct, u32, u64 } from "@yume-chan/struct"; import { struct, u32, u64 } from "@yume-chan/struct";
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js";
import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; import { adbSyncWriteRequest } from "./request.js";
import { adbSyncReadResponse } from "./response.js";
import type { AdbSyncSocket } from "./socket.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
@ -131,7 +132,7 @@ export async function adbSyncLstat(
await adbSyncWriteRequest(locked, AdbSyncRequestId.LstatV2, path); await adbSyncWriteRequest(locked, AdbSyncRequestId.LstatV2, path);
return await adbSyncReadResponse( return await adbSyncReadResponse(
locked, locked,
AdbSyncResponseId.Lstat2, AdbSyncResponseId.LstatV2,
AdbSyncStatResponse, AdbSyncStatResponse,
); );
} else { } else {

View file

@ -21,7 +21,7 @@ import { adbSyncLstat, adbSyncStat } from "./stat.js";
export function dirname(path: string): string { export function dirname(path: string): string {
const end = path.lastIndexOf("/"); const end = path.lastIndexOf("/");
if (end === -1) { if (end === -1) {
throw new Error(`Invalid path`); throw new Error(`Invalid absolute unix path: ${path}`);
} }
if (end === 0) { if (end === 0) {
return "/"; return "/";
@ -43,25 +43,25 @@ export class AdbSync {
protected _socket: AdbSyncSocket; protected _socket: AdbSyncSocket;
readonly #supportsStat: boolean; readonly #supportsStat: boolean;
readonly #supportsListV2: boolean; readonly #supportsLs2: boolean;
readonly #fixedPushMkdir: boolean; readonly #fixedPushMkdir: boolean;
readonly #supportsSendReceiveV2: boolean; readonly #supportsSendReceive2: boolean;
readonly #needPushMkdirWorkaround: boolean; readonly #needPushMkdirWorkaround: boolean;
get supportsStat(): boolean { get supportsStat(): boolean {
return this.#supportsStat; return this.#supportsStat;
} }
get supportsListV2(): boolean { get supportsLs2(): boolean {
return this.#supportsListV2; return this.#supportsLs2;
} }
get fixedPushMkdir(): boolean { get fixedPushMkdir(): boolean {
return this.#fixedPushMkdir; return this.#fixedPushMkdir;
} }
get supportsSendReceiveV2(): boolean { get supportsSendReceive2(): boolean {
return this.#supportsSendReceiveV2; return this.#supportsSendReceive2;
} }
get needPushMkdirWorkaround(): boolean { get needPushMkdirWorkaround(): boolean {
@ -72,15 +72,13 @@ export class AdbSync {
this._adb = adb; this._adb = adb;
this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize); this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize);
this.#supportsStat = adb.canUseFeature(AdbFeature.StatV2); this.#supportsStat = adb.canUseFeature(AdbFeature.Stat2);
this.#supportsListV2 = adb.canUseFeature(AdbFeature.ListV2); this.#supportsLs2 = adb.canUseFeature(AdbFeature.Ls2);
this.#fixedPushMkdir = adb.canUseFeature(AdbFeature.FixedPushMkdir); this.#fixedPushMkdir = adb.canUseFeature(AdbFeature.FixedPushMkdir);
this.#supportsSendReceiveV2 = adb.canUseFeature( this.#supportsSendReceive2 = adb.canUseFeature(AdbFeature.SendReceive2);
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.canUseFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir; this._adb.canUseFeature(AdbFeature.Shell2) && !this.fixedPushMkdir;
} }
/** /**
@ -120,7 +118,7 @@ export class AdbSync {
} }
opendir(path: string): AsyncGenerator<AdbSyncEntry, void, void> { opendir(path: string): AsyncGenerator<AdbSyncEntry, void, void> {
return adbSyncOpenDir(this._socket, path, this.supportsListV2); return adbSyncOpenDir(this._socket, path, this.supportsLs2);
} }
async readdir(path: string) { async readdir(path: string) {
@ -157,7 +155,7 @@ export class AdbSync {
} }
await adbSyncPush({ await adbSyncPush({
v2: this.supportsSendReceiveV2, v2: this.supportsSendReceive2,
socket: this._socket, socket: this._socket,
...options, ...options,
}); });

View file

@ -14,7 +14,7 @@ import type {
AdbTransport, AdbTransport,
} from "../adb.js"; } from "../adb.js";
import { AdbBanner } from "../banner.js"; import { AdbBanner } from "../banner.js";
import { AdbFeature } from "../features.js"; import { AdbDeviceFeatures, AdbFeature } from "../features.js";
import type { AdbAuthenticator, AdbCredentialStore } from "./auth.js"; import type { AdbAuthenticator, AdbCredentialStore } from "./auth.js";
import { AdbDefaultAuthenticator } from "./auth.js"; import { AdbDefaultAuthenticator } from "./auth.js";
@ -23,30 +23,6 @@ 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 = /* #__PURE__ */ (() =>
[
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 readonly AdbFeature[])();
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024; export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
export type AdbDaemonConnection = ReadableWritablePair< export type AdbDaemonConnection = ReadableWritablePair<
@ -154,7 +130,7 @@ export class AdbDaemonTransport implements AdbTransport {
static async authenticate({ static async authenticate({
serial, serial,
connection, connection,
features = ADB_DAEMON_DEFAULT_FEATURES, features = AdbDeviceFeatures,
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
...options ...options
}: AdbDaemonAuthenticationOptions & }: AdbDaemonAuthenticationOptions &
@ -251,11 +227,10 @@ export class AdbDaemonTransport implements AdbTransport {
); );
} }
const actualFeatures = features.slice();
if (initialDelayedAckBytes <= 0) { if (initialDelayedAckBytes <= 0) {
const index = features.indexOf(AdbFeature.DelayedAck); const index = features.indexOf(AdbFeature.DelayedAck);
if (index !== -1) { if (index !== -1) {
actualFeatures.splice(index, 1); features = features.toSpliced(index, 1);
} }
} }
@ -267,9 +242,7 @@ export class AdbDaemonTransport implements AdbTransport {
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( payload: encodeUtf8(`host::features=${features.join(",")}`),
`host::features=${actualFeatures.join(",")}`,
),
}); });
banner = await resolver.promise; banner = await resolver.promise;
@ -289,7 +262,7 @@ export class AdbDaemonTransport implements AdbTransport {
version, version,
maxPayloadSize, maxPayloadSize,
banner, banner,
features: actualFeatures, features,
initialDelayedAckBytes, initialDelayedAckBytes,
preserveConnection: options.preserveConnection, preserveConnection: options.preserveConnection,
readTimeLimit: options.readTimeLimit, readTimeLimit: options.readTimeLimit,
@ -336,7 +309,7 @@ export class AdbDaemonTransport implements AdbTransport {
connection, connection,
version, version,
banner, banner,
features = ADB_DAEMON_DEFAULT_FEATURES, features = AdbDeviceFeatures,
initialDelayedAckBytes, initialDelayedAckBytes,
...options ...options
}: AdbDaemonSocketConnectorConstructionOptions) { }: AdbDaemonSocketConnectorConstructionOptions) {
@ -359,19 +332,19 @@ export class AdbDaemonTransport implements AdbTransport {
initialDelayedAckBytes = 0; initialDelayedAckBytes = 0;
} }
let calculateChecksum: boolean; let shouldCalculateChecksum: boolean;
let appendNullToServiceString: boolean; let shouldAppendNullToServiceString: boolean;
if (version >= ADB_DAEMON_VERSION_OMIT_CHECKSUM) { if (version >= ADB_DAEMON_VERSION_OMIT_CHECKSUM) {
calculateChecksum = false; shouldCalculateChecksum = false;
appendNullToServiceString = false; shouldAppendNullToServiceString = false;
} else { } else {
calculateChecksum = true; shouldCalculateChecksum = true;
appendNullToServiceString = true; shouldAppendNullToServiceString = true;
} }
this.#dispatcher = new AdbPacketDispatcher(connection, { this.#dispatcher = new AdbPacketDispatcher(connection, {
calculateChecksum, calculateChecksum: shouldCalculateChecksum,
appendNullToServiceString, appendNullToServiceString: shouldAppendNullToServiceString,
initialDelayedAckBytes, initialDelayedAckBytes,
...options, ...options,
}); });

View file

@ -0,0 +1,47 @@
// cspell: ignore Libusb
// cspell: ignore devraw
// cspell: ignore Openscreen
// cspell: ignore devicetracker
// The order follows
// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/transport.cpp;l=81;drc=2d3e62c2af54a3e8f8803ea10492e63b8dfe709f
export const Shell2 = "shell_v2";
export const Cmd = "cmd";
export const Stat2 = "stat_v2";
export const Ls2 = "ls_v2";
/**
* server only
*/
export const Libusb = "libusb";
/**
* server only
*/
export const PushSync = "push_sync";
export const Apex = "apex";
export const FixedPushMkdir = "fixed_push_mkdir";
export const Abb = "abb";
export const FixedPushSymlinkTimestamp = "fixed_push_symlink_timestamp";
export const AbbExec = "abb_exec";
export const RemountShell = "remount_shell";
export const TrackApp = "track_app";
export const SendReceive2 = "sendrecv_v2";
export const SendReceive2Brotli = "sendrecv_v2_brotli";
export const SendReceive2Lz4 = "sendrecv_v2_lz4";
export const SendReceive2Zstd = "sendrecv_v2_zstd";
export const SendReceive2DryRunSend = "sendrecv_v2_dry_run_send";
export const DelayedAck = "delayed_ack";
/**
* server only
*/
export const OpenscreenMdns = "openscreen_mdns";
/**
* server only
*/
export const DeviceTrackerProtoFormat = "devicetracker_proto_format";
export const DevRaw = "devraw";
export const AppInfo = "app_info";
/**
* server only
*/
export const ServerStatus = "server_status";

View file

@ -1,15 +1,32 @@
// The order follows import * as AdbFeature from "./features-value.js";
// https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/transport.cpp;l=77;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
export const AdbFeature = {
ShellV2: "shell_v2",
Cmd: "cmd",
StatV2: "stat_v2",
ListV2: "ls_v2",
FixedPushMkdir: "fixed_push_mkdir",
Abb: "abb",
AbbExec: "abb_exec",
SendReceiveV2: "sendrecv_v2",
DelayedAck: "delayed_ack",
} as const;
export type AdbFeature = (typeof AdbFeature)[keyof typeof AdbFeature]; // biome-ignore lint/suspicious/noRedeclare: TypeScript declaration merging for enum-like object
type AdbFeature = (typeof AdbFeature)[keyof typeof AdbFeature];
export { AdbFeature };
// 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 AdbDeviceFeatures = [
AdbFeature.Shell2,
AdbFeature.Cmd,
AdbFeature.Stat2,
AdbFeature.Ls2,
AdbFeature.FixedPushMkdir,
AdbFeature.Apex,
AdbFeature.Abb,
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
// No special handling required.
AdbFeature.FixedPushSymlinkTimestamp,
AdbFeature.AbbExec,
AdbFeature.RemountShell,
AdbFeature.TrackApp,
AdbFeature.SendReceive2,
AdbFeature.SendReceive2Brotli,
AdbFeature.SendReceive2Lz4,
AdbFeature.SendReceive2Zstd,
AdbFeature.SendReceive2DryRunSend,
AdbFeature.DevRaw,
AdbFeature.AppInfo,
AdbFeature.DelayedAck,
] as readonly AdbFeature[];

View file

@ -6,32 +6,10 @@ 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 { AdbDeviceFeatures } from "../features.js";
import type { AdbServerClient } from "./client.js"; import type { AdbServerClient } from "./client.js";
export const ADB_SERVER_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
[
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 readonly AdbFeature[])();
export class AdbServerTransport implements AdbTransport { export class AdbServerTransport implements AdbTransport {
#client: AdbServerClient; #client: AdbServerClient;
@ -52,9 +30,14 @@ export class AdbServerTransport implements AdbTransport {
} }
get clientFeatures() { get clientFeatures() {
// No need to get host features (features supported by ADB server) // This list tells the `Adb` instance how to invoke some commands.
// Because we create all ADB packets ourselves //
return ADB_SERVER_DEFAULT_FEATURES; // Because all device commands are created by the `Adb` instance, not ADB server,
// we don't need to fetch current server's feature list using `host-features` command.
//
// And because all server commands are created by the `AdbServerClient` instance, not `Adb`,
// we don't need to pass server-only features to `Adb` in this list.
return AdbDeviceFeatures;
} }
// eslint-disable-next-line @typescript-eslint/max-params // eslint-disable-next-line @typescript-eslint/max-params

View file

@ -43,7 +43,7 @@ export class ActivityManager extends AdbServiceBase {
options: ActivityManagerStartActivityOptions, options: ActivityManagerStartActivityOptions,
): Promise<void> { ): Promise<void> {
// Android 8 added "start-activity" alias to "start" // Android 8 added "start-activity" alias to "start"
// Use the most compatible command // but we want to use the most compatible one.
const command = buildCommand( const command = buildCommand(
[ActivityManager.ServiceName, "start", "-W"], [ActivityManager.ServiceName, "start", "-W"],
options, options,