refactor(adb): pre-encode packet ID as number in sync command

This commit is contained in:
Simon Chan 2024-05-24 16:55:26 +08:00
parent 25efbe6402
commit 2db3e8f8b9
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
8 changed files with 90 additions and 48 deletions

View file

@ -17,8 +17,7 @@ export interface AdbSyncEntry extends AdbSyncStat {
export const AdbSyncEntryResponse = new Struct({ littleEndian: true }) export const AdbSyncEntryResponse = new Struct({ littleEndian: true })
.concat(AdbSyncLstatResponse) .concat(AdbSyncLstatResponse)
.uint32("nameLength") .uint32("nameLength")
.string("name", { lengthField: "nameLength" }) .string("name", { lengthField: "nameLength" });
.extra({ id: AdbSyncResponseId.Entry as const });
export type AdbSyncEntryResponse = export type AdbSyncEntryResponse =
(typeof AdbSyncEntryResponse)["TDeserializeResult"]; (typeof AdbSyncEntryResponse)["TDeserializeResult"];
@ -26,8 +25,7 @@ export type AdbSyncEntryResponse =
export const AdbSyncEntry2Response = new Struct({ littleEndian: true }) export const AdbSyncEntry2Response = new Struct({ littleEndian: true })
.concat(AdbSyncStatResponse) .concat(AdbSyncStatResponse)
.uint32("nameLength") .uint32("nameLength")
.string("name", { lengthField: "nameLength" }) .string("name", { lengthField: "nameLength" });
.extra({ id: AdbSyncResponseId.Entry2 as const });
export type AdbSyncEntry2Response = export type AdbSyncEntry2Response =
(typeof AdbSyncEntry2Response)["TDeserializeResult"]; (typeof AdbSyncEntry2Response)["TDeserializeResult"];

View file

@ -8,8 +8,7 @@ import type { AdbSyncSocket } from "./socket.js";
export const AdbSyncDataResponse = new Struct({ littleEndian: true }) export const AdbSyncDataResponse = new Struct({ littleEndian: true })
.uint32("dataLength") .uint32("dataLength")
.uint8Array("data", { lengthField: "dataLength" }) .uint8Array("data", { lengthField: "dataLength" });
.extra({ id: AdbSyncResponseId.Data as const });
export type AdbSyncDataResponse = export type AdbSyncDataResponse =
(typeof AdbSyncDataResponse)["TDeserializeResult"]; (typeof AdbSyncDataResponse)["TDeserializeResult"];
@ -52,6 +51,7 @@ export function adbSyncPull(
socket: AdbSyncSocket, socket: AdbSyncSocket,
path: string, path: string,
): ReadableStream<Uint8Array> { ): ReadableStream<Uint8Array> {
// TODO: use `ReadableStream.from` when it's supported
return new PushReadableStream(async (controller) => { return new PushReadableStream(async (controller) => {
for await (const data of adbSyncPullGenerator(socket, path)) { for await (const data of adbSyncPullGenerator(socket, path)) {
await controller.enqueue(data); await controller.enqueue(data);

View file

@ -111,7 +111,7 @@ export interface AdbSyncPushV2Options extends AdbSyncPushV1Options {
} }
export const AdbSyncSendV2Request = new Struct({ littleEndian: true }) export const AdbSyncSendV2Request = new Struct({ littleEndian: true })
.uint32("id", placeholder<AdbSyncRequestId>()) .uint32("id")
.uint32("mode") .uint32("mode")
.uint32("flags", placeholder<AdbSyncSendV2Flags>()); .uint32("flags", placeholder<AdbSyncSendV2Flags>());

View file

@ -2,21 +2,23 @@ import Struct from "@yume-chan/struct";
import { encodeUtf8 } from "../../utils/index.js"; import { encodeUtf8 } from "../../utils/index.js";
export enum AdbSyncRequestId { import { adbSyncEncodeId } from "./response.js";
List = "LIST",
ListV2 = "LIS2", export namespace AdbSyncRequestId {
Send = "SEND", export const List = adbSyncEncodeId("LIST");
SendV2 = "SND2", export const ListV2 = adbSyncEncodeId("LIS2");
Lstat = "STAT", export const Send = adbSyncEncodeId("SEND");
Stat = "STA2", export const SendV2 = adbSyncEncodeId("SND2");
LstatV2 = "LST2", export const Lstat = adbSyncEncodeId("STAT");
Data = "DATA", export const Stat = adbSyncEncodeId("STA2");
Done = "DONE", export const LstatV2 = adbSyncEncodeId("LST2");
Receive = "RECV", export const Data = adbSyncEncodeId("DATA");
export const Done = adbSyncEncodeId("DONE");
export const Receive = adbSyncEncodeId("RECV");
} }
export const AdbSyncNumberRequest = new Struct({ littleEndian: true }) export const AdbSyncNumberRequest = new Struct({ littleEndian: true })
.string("id", { length: 4 }) .uint32("id")
.uint32("arg"); .uint32("arg");
export interface AdbSyncWritable { export interface AdbSyncWritable {
@ -25,9 +27,13 @@ export interface AdbSyncWritable {
export async function adbSyncWriteRequest( export async function adbSyncWriteRequest(
writable: AdbSyncWritable, writable: AdbSyncWritable,
id: AdbSyncRequestId | string, id: number | string,
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 }),
@ -39,9 +45,8 @@ export async function adbSyncWriteRequest(
value = encodeUtf8(value); value = encodeUtf8(value);
} }
// `writable` will copy inputs to an internal buffer, // `writable` is buffered, it copies inputs to an internal buffer,
// so we write header and `buffer` separately, // so don't concatenate headers and data here, that will be an unnecessary copy.
// to avoid an extra copy.
await writable.write( await writable.write(
AdbSyncNumberRequest.serialize({ id, arg: value.byteLength }), AdbSyncNumberRequest.serialize({ id, arg: value.byteLength }),
); );

View file

@ -1,3 +1,4 @@
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
import type { import type {
AsyncExactReadable, AsyncExactReadable,
StructLike, StructLike,
@ -7,16 +8,34 @@ import Struct from "@yume-chan/struct";
import { decodeUtf8 } from "../../utils/index.js"; import { decodeUtf8 } from "../../utils/index.js";
export enum AdbSyncResponseId { function encodeAsciiUnchecked(value: string): Uint8Array {
Entry = "DENT", const result = new Uint8Array(value.length);
Entry2 = "DNT2", for (let i = 0; i < value.length; i += 1) {
Lstat = "STAT", result[i] = value.charCodeAt(i);
Stat = "STA2", }
Lstat2 = "LST2", return result;
Done = "DONE", }
Data = "DATA",
Ok = "OKAY", /**
Fail = "FAIL", * 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
*/
export function adbSyncEncodeId(value: string): number {
const buffer = encodeAsciiUnchecked(value);
return getUint32LittleEndian(buffer, 0);
}
export namespace AdbSyncResponseId {
export const Entry = adbSyncEncodeId("DENT");
export const Entry2 = adbSyncEncodeId("DNT2");
export const Lstat = adbSyncEncodeId("STAT");
export const Stat = adbSyncEncodeId("STA2");
export const Lstat2 = adbSyncEncodeId("LST2");
export const Done = adbSyncEncodeId("DONE");
export const Data = adbSyncEncodeId("DATA");
export const Ok = adbSyncEncodeId("OKAY");
export const Fail = adbSyncEncodeId("FAIL");
} }
export class AdbSyncError extends Error {} export class AdbSyncError extends Error {}
@ -30,18 +49,24 @@ export const AdbSyncFailResponse = new Struct({ littleEndian: true })
export async function adbSyncReadResponse<T>( export async function adbSyncReadResponse<T>(
stream: AsyncExactReadable, stream: AsyncExactReadable,
id: AdbSyncResponseId, id: number | string,
type: StructLike<T>, type: StructLike<T>,
): Promise<T> { ): Promise<T> {
const actualId = decodeUtf8(await stream.readExactly(4)); if (typeof id === "string") {
switch (actualId) { id = adbSyncEncodeId(id);
}
const buffer = await stream.readExactly(4);
switch (getUint32LittleEndian(buffer, 0)) {
case AdbSyncResponseId.Fail: case AdbSyncResponseId.Fail:
await AdbSyncFailResponse.deserialize(stream); await AdbSyncFailResponse.deserialize(stream);
throw new Error("Unreachable"); throw new Error("Unreachable");
case id: case id:
return await type.deserialize(stream); return await type.deserialize(stream);
default: default:
throw new Error(`Expected '${id}', but got '${actualId}'`); throw new Error(
`Expected '${id}', but got '${decodeUtf8(buffer)}'`,
);
} }
} }
@ -49,12 +74,16 @@ export async function* adbSyncReadResponses<
T extends Struct<object, PropertyKey, object, unknown>, T extends Struct<object, PropertyKey, object, unknown>,
>( >(
stream: AsyncExactReadable, stream: AsyncExactReadable,
id: AdbSyncResponseId, id: number | string,
type: T, type: T,
): AsyncGenerator<StructValueType<T>, void, void> { ): AsyncGenerator<StructValueType<T>, void, void> {
if (typeof id === "string") {
id = adbSyncEncodeId(id);
}
while (true) { while (true) {
const actualId = decodeUtf8(await stream.readExactly(4)); const buffer = await stream.readExactly(4);
switch (actualId) { switch (getUint32LittleEndian(buffer, 0)) {
case AdbSyncResponseId.Fail: case AdbSyncResponseId.Fail:
await AdbSyncFailResponse.deserialize(stream); await AdbSyncFailResponse.deserialize(stream);
throw new Error("Unreachable"); throw new Error("Unreachable");
@ -70,7 +99,7 @@ export async function* adbSyncReadResponses<
break; break;
default: default:
throw new Error( throw new Error(
`Expected '${id}' or '${AdbSyncResponseId.Done}', but got '${actualId}'`, `Expected '${id}' or '${AdbSyncResponseId.Done}', but got '${decodeUtf8(buffer)}'`,
); );
} }
} }

View file

@ -29,7 +29,6 @@ export const AdbSyncLstatResponse = new Struct({ littleEndian: true })
.int32("size") .int32("size")
.int32("mtime") .int32("mtime")
.extra({ .extra({
id: AdbSyncResponseId.Lstat as const,
get type() { get type() {
return (this.mode >> 12) as LinuxFileType; return (this.mode >> 12) as LinuxFileType;
}, },
@ -83,7 +82,6 @@ export const AdbSyncStatResponse = new Struct({ littleEndian: true })
.uint64("mtime") .uint64("mtime")
.uint64("ctime") .uint64("ctime")
.extra({ .extra({
id: AdbSyncResponseId.Stat as const,
get type() { get type() {
return (this.mode >> 12) as LinuxFileType; return (this.mode >> 12) as LinuxFileType;
}, },

View file

@ -9,6 +9,7 @@ import type { AdbSyncEntry } from "./list.js";
import { adbSyncOpenDir } from "./list.js"; import { adbSyncOpenDir } from "./list.js";
import { adbSyncPull } from "./pull.js"; import { adbSyncPull } from "./pull.js";
import { adbSyncPush } from "./push.js"; import { adbSyncPush } from "./push.js";
import type { AdbSyncSocketLocked } from "./socket.js";
import { AdbSyncSocket } from "./socket.js"; import { AdbSyncSocket } from "./socket.js";
import type { AdbSyncStat, LinuxFileType } from "./stat.js"; import type { AdbSyncStat, LinuxFileType } from "./stat.js";
import { adbSyncLstat, adbSyncStat } from "./stat.js"; import { adbSyncLstat, adbSyncStat } from "./stat.js";
@ -150,7 +151,7 @@ export class AdbSync extends AutoDisposable {
*/ */
async write(options: AdbSyncWriteOptions): Promise<void> { async write(options: AdbSyncWriteOptions): Promise<void> {
if (this.needPushMkdirWorkaround) { if (this.needPushMkdirWorkaround) {
// It may fail if the path is already existed. // It may fail if `filename` already exists.
// Ignore the result. // Ignore the result.
// TODO: sync: test push mkdir workaround (need an Android 8 device) // TODO: sync: test push mkdir workaround (need an Android 8 device)
await this._adb.subprocess.spawnAndWait([ await this._adb.subprocess.spawnAndWait([
@ -167,6 +168,10 @@ export class AdbSync extends AutoDisposable {
}); });
} }
lockSocket(): Promise<AdbSyncSocketLocked> {
return this._socket.lock();
}
override async dispose() { override async dispose() {
super.dispose(); super.dispose();
await this._socket.close(); await this._socket.close();

View file

@ -273,10 +273,17 @@ export class PackageManager extends AdbCommandBase {
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP, PACKAGE_MANAGER_INSTALL_OPTIONS_MAP,
); );
if (!options?.skipExisting) { if (!options?.skipExisting) {
// Today `pm` defaults to replace existing application (`-r` will be ignored), /*
// but old versions defaults to skip existing application (like `-R`, but obviously * | behavior | previous version | modern version |
// they didn't have this switch and ignores it if present). * | -------------------- | -------------------- | -------------------- |
// If `skipExisting` is not set, add `-r` to ensure compatibility with old versions. * | replace existing app | requires `-r` | default behavior [1] |
* | skip existing app | default behavior [2] | requires `-R` |
*
* [1]: `-r` recognized but ignored)
* [2]: `-R` not recognized but ignored
*
* So add `-r` when `skipExisting` is `false` for compatibility.
*/
args.push("-r"); args.push("-r");
} }
return args; return args;