mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
feat(adb): update feature list (#797)
This commit is contained in:
parent
c5f282285a
commit
29de3e4842
21 changed files with 218 additions and 174 deletions
5
.changeset/four-ants-post.md
Normal file
5
.changeset/four-ants-post.md
Normal 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.
|
|
@ -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(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
33
libraries/adb/src/commands/sync/id-common.ts
Normal file
33
libraries/adb/src/commands/sync/id-common.ts
Normal 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");
|
12
libraries/adb/src/commands/sync/id-request.ts
Normal file
12
libraries/adb/src/commands/sync/id-request.ts
Normal 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");
|
11
libraries/adb/src/commands/sync/id-response.ts
Normal file
11
libraries/adb/src/commands/sync/id-response.ts
Normal 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");
|
7
libraries/adb/src/commands/sync/id.ts
Normal file
7
libraries/adb/src/commands/sync/id.ts
Normal 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 };
|
|
@ -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";
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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 }),
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
47
libraries/adb/src/features-value.ts
Normal file
47
libraries/adb/src/features-value.ts
Normal 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";
|
|
@ -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[];
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue