feat(bin): add cmd wrapper

This commit is contained in:
Simon Chan 2023-03-02 16:01:01 +08:00
parent 9c5d2d8a5c
commit 225e369f53
19 changed files with 511 additions and 332 deletions

View file

@ -2,8 +2,30 @@
// cspell:ignore instantapp
// cspell:ignore apks
import { AdbCommandBase } from "@yume-chan/adb";
import type { WritableStream } from "@yume-chan/stream-extra";
import type { Adb } from "@yume-chan/adb";
import {
AdbCommandBase,
AdbSubprocessNoneProtocol,
escapeArg,
} from "@yume-chan/adb";
import type { ReadableStream } from "@yume-chan/stream-extra";
import { DecodeUtf8Stream, WrapReadableStream } from "@yume-chan/stream-extra";
import { Cmd } from "./cmd.js";
export enum PackageManagerInstallLocation {
Auto,
InternalOnly,
PreferExternal,
}
export enum PackageManagerInstallReason {
Unknown,
AdminPolicy,
DeviceRestore,
DeviceSetup,
UserRequest,
}
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
export interface PackageManagerInstallOptions {
@ -22,7 +44,7 @@ export interface PackageManagerInstallOptions {
/**
* `-f`
*/
internal: boolean;
internalStorage: boolean;
/**
* `-d`
*/
@ -78,11 +100,11 @@ export interface PackageManagerInstallOptions {
/**
* `--install-location`
*/
installLocation: number;
installLocation: PackageManagerInstallLocation;
/**
* `--install-reason`
*/
installReason: number;
installReason: PackageManagerInstallReason;
/**
* `--force-uuid`
*/
@ -121,116 +143,135 @@ export interface PackageManagerInstallOptions {
bypassLowTargetSdkBlock: boolean;
}
export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
keyof PackageManagerInstallOptions,
string
> = {
skipExisting: "-R",
installerPackageName: "-i",
allowTest: "-t",
internalStorage: "-f",
requestDowngrade: "-d",
grantRuntimePermissions: "-g",
restrictPermissions: "--restrict-permissions",
doNotKill: "--dont-kill",
originatingUri: "--originating-uri",
refererUri: "--referrer",
inheritFrom: "-p",
packageName: "--pkg",
abi: "--abi",
instantApp: "--instant",
full: "--full",
preload: "--preload",
userId: "--user",
installLocation: "--install-location",
installReason: "--install-reason",
forceUuid: "--force-uuid",
apex: "--apex",
forceNonStaged: "--force-non-staged",
staged: "--staged",
forceQueryable: "--force-queryable",
enableRollback: "--enable-rollback",
stagedReadyTimeout: "--staged-ready-timeout",
skipVerification: "--skip-verification",
bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
};
export class PackageManager extends AdbCommandBase {
private _cmd: Cmd;
public constructor(adb: Adb) {
super(adb);
this._cmd = new Cmd(adb);
}
private buildInstallArgs(
options: Partial<PackageManagerInstallOptions>
options?: Partial<PackageManagerInstallOptions>
): string[] {
const args = ["pm", "install"];
if (options.skipExisting) {
args.push("-R");
}
if (options.installerPackageName) {
args.push("-i", options.installerPackageName);
}
if (options.allowTest) {
args.push("-t");
}
if (options.internal) {
args.push("-f");
}
if (options.requestDowngrade) {
args.push("-d");
}
if (options.grantRuntimePermissions) {
args.push("-g");
}
if (options.restrictPermissions) {
args.push("--restrict-permissions");
}
if (options.doNotKill) {
args.push("--dont-kill");
}
if (options.originatingUri) {
args.push("--originating-uri", options.originatingUri);
}
if (options.refererUri) {
args.push("--referrer", options.refererUri);
}
if (options.inheritFrom) {
args.push("-p", options.inheritFrom);
}
if (options.packageName) {
args.push("--pkg", options.packageName);
}
if (options.abi) {
args.push("--abi", options.abi);
}
if (options.instantApp) {
args.push("--instant");
}
if (options.full) {
args.push("--full");
}
if (options.preload) {
args.push("--preload");
}
if (options.userId) {
args.push("--user", options.userId.toString());
}
if (options.installLocation) {
args.push("--install-location", options.installLocation.toString());
}
if (options.installReason) {
args.push("--install-reason", options.installReason.toString());
}
if (options.forceUuid) {
args.push("--force-uuid", options.forceUuid);
}
if (options.apex) {
args.push("--apex");
}
if (options.forceNonStaged) {
args.push("--force-non-staged");
}
if (options.staged) {
args.push("--staged");
}
if (options.forceQueryable) {
args.push("--force-queryable");
}
if (options.enableRollback) {
args.push("--enable-rollback");
}
if (options.stagedReadyTimeout) {
args.push(
"--staged-ready-timeout",
options.stagedReadyTimeout.toString()
);
}
if (options.skipVerification) {
args.push("--skip-verification");
}
if (options.bypassLowTargetSdkBlock) {
args.push("--bypass-low-target-sdk-block");
if (options) {
for (const [key, value] of Object.entries(options)) {
if (value) {
const option =
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
key as keyof PackageManagerInstallOptions
];
if (option) {
args.push(option);
switch (typeof value) {
case "number":
args.push(value.toString());
break;
case "string":
args.push(value);
break;
}
}
}
}
}
return args;
}
public async install(
options: Partial<PackageManagerInstallOptions>,
...apks: string[]
): Promise<void> {
apks: string[],
options?: Partial<PackageManagerInstallOptions>
): Promise<string> {
const args = this.buildInstallArgs(options);
// WIP: old version of pm doesn't support multiple apks
args.push(...apks);
await this.adb.subprocess.spawnAndWaitLegacy(args);
return await this.adb.subprocess.spawnAndWaitLegacy(args);
}
public async pushAndInstallStream(
stream: ReadableStream<Uint8Array>,
options?: Partial<PackageManagerInstallOptions>
): Promise<ReadableStream<string>> {
const sync = await this.adb.sync();
const fileName = Math.random().toString().substring(2);
const filePath = `/data/local/tmp/${fileName}.apk`;
try {
await sync.write(filePath, stream);
} finally {
await sync.dispose();
}
const args = this.buildInstallArgs(options);
const process = await AdbSubprocessNoneProtocol.raw(
this.adb,
args.map(escapeArg).join(" ")
);
return new WrapReadableStream({
start: () => process.stdout.pipeThrough(new DecodeUtf8Stream()),
close: async () => {
await this.adb.rm(filePath);
},
});
}
public async installStream(
options: Partial<PackageManagerInstallOptions>,
size: number
): Promise<WritableStream<Uint8Array>> {
size: number,
stream: ReadableStream<Uint8Array>,
options?: Partial<PackageManagerInstallOptions>
): Promise<ReadableStream<string>> {
if (!this._cmd.supportsCmd) {
return this.pushAndInstallStream(stream, options);
}
// Android 7 added `cmd package` and piping apk to stdin,
// the source code suggests using `cmd package` over `pm`, but didn't say why.
// However, `cmd package install` can't read `/data/local/tmp` folder due to SELinux policy,
// so even ADB today is still using `pm install` for non-streaming installs.
const args = this.buildInstallArgs(options);
// Remove `pm` from args, final command will be `cmd package install`
args.shift();
args.push("-S", size.toString());
return (await this.adb.subprocess.spawn(args)).stdin;
const process = await this._cmd.spawn(false, "package", ...args);
await stream.pipeTo(process.stdin);
return process.stdout.pipeThrough(new DecodeUtf8Stream());
}
// TODO: install: support split apk formats (`adb install-multiple`)
}