feat(bin): support pm list packages

This commit is contained in:
Simon Chan 2023-06-20 14:44:10 +08:00
parent d9685acc6f
commit bf76ce006f
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
3 changed files with 141 additions and 16 deletions

View file

@ -177,6 +177,47 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
};
export interface PackageManagerListPackagesOptions {
listDisabled: boolean;
listEnabled: boolean;
showSourceDir: boolean;
showInstaller: boolean;
listSystem: boolean;
showUid: boolean;
listThirdParty: boolean;
showVersionCode: boolean;
listApexOnly: boolean;
user: "all" | "current" | number;
uid: number;
filter: string;
}
export const PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP: Record<
keyof PackageManagerListPackagesOptions,
string
> = {
listDisabled: "-d",
listEnabled: "-e",
showSourceDir: "-f",
showInstaller: "-i",
listSystem: "-s",
showUid: "-U",
listThirdParty: "-3",
showVersionCode: "--show-versioncode",
listApexOnly: "--apex-only",
user: "--user",
uid: "--uid",
filter: "",
};
export interface PackageManagerListPackagesResult {
packageName: string;
sourceDir?: string | undefined;
versionCode?: number | undefined;
installer?: string | undefined;
uid?: number | undefined;
}
export class PackageManager extends AdbCommandBase {
private _cmd: Cmd;
@ -185,17 +226,16 @@ export class PackageManager extends AdbCommandBase {
this._cmd = new Cmd(adb);
}
private buildInstallArgs(
options?: Partial<PackageManagerInstallOptions>
private buildArguments<T>(
commands: string[],
options: Partial<T> | undefined,
map: Record<keyof T, string>
): string[] {
const args = ["pm", "install"];
const args = ["pm", ...commands];
if (options) {
for (const [key, value] of Object.entries(options)) {
if (value) {
const option =
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
key as keyof PackageManagerInstallOptions
];
const option = map[key as keyof T];
if (option) {
args.push(option);
switch (typeof value) {
@ -213,11 +253,21 @@ export class PackageManager extends AdbCommandBase {
return args;
}
private buildInstallArguments(
options: Partial<PackageManagerInstallOptions> | undefined
): string[] {
return this.buildArguments(
["install"],
options,
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP
);
}
public async install(
apks: string[],
options?: Partial<PackageManagerInstallOptions>
): Promise<string> {
const args = this.buildInstallArgs(options);
const args = this.buildInstallArguments(options);
// WIP: old version of pm doesn't support multiple apks
args.push(...apks);
return await this.adb.subprocess.spawnAndWaitLegacy(args);
@ -241,7 +291,11 @@ export class PackageManager extends AdbCommandBase {
await sync.dispose();
}
const args = this.buildInstallArgs(options);
// Starting from Android 7, `pm` is a only wrapper for `cmd package`,
// and `cmd package` launches faster than `pm`.
// But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy,
// so installing a file must use `pm`.
const args = this.buildInstallArguments(options);
args.push(filePath);
const process = await AdbSubprocessNoneProtocol.raw(
this.adb,
@ -260,16 +314,15 @@ export class PackageManager extends AdbCommandBase {
stream: ReadableStream<Consumable<Uint8Array>>,
options?: Partial<PackageManagerInstallOptions>
): Promise<ReadableStream<string>> {
// Android 7 added both `cmd` command and streaming install support,
// we can't detect whether `pm` supports streaming install,
// so we detect `cmd` command support instead.
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`
const args = this.buildInstallArguments(options);
// Remove `pm` from args, final command will starts with `cmd package install`
args.shift();
args.push("-S", size.toString());
const process = await this._cmd.spawn(false, "package", ...args);
@ -277,5 +330,74 @@ export class PackageManager extends AdbCommandBase {
return process.stdout.pipeThrough(new DecodeUtf8Stream());
}
public static parsePackageListItem(
line: string
): PackageManagerListPackagesResult {
line = line.substring("package:".length);
let packageName: string;
let sourceDir: string | undefined;
let versionCode: number | undefined;
let installer: string | undefined;
let uid: number | undefined;
// Parse backwards
let index = line.indexOf(" uid:");
if (index !== -1) {
uid = Number.parseInt(line.substring(index + " uid:".length), 10);
line = line.substring(0, index);
}
index = line.indexOf(" installer=");
if (index !== -1) {
installer = line.substring(index + " installer=".length);
line = line.substring(0, index);
}
index = line.indexOf(" versionCode:");
if (index !== -1) {
versionCode = Number.parseInt(
line.substring(index + " versionCode:".length),
10
);
line = line.substring(0, index);
}
// `sourceDir` may contain `=` so use `lastIndexOf`
index = line.lastIndexOf("=");
if (index !== -1) {
sourceDir = line.substring(0, index);
packageName = line.substring(index + "=".length);
} else {
packageName = line;
}
return {
packageName,
sourceDir,
versionCode,
installer,
uid,
};
}
public async listPackages(
options?: Partial<PackageManagerListPackagesOptions>
): Promise<PackageManagerListPackagesResult[]> {
const args = this.buildArguments(
["list", "packages"],
options,
PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP
);
if (options?.filter) {
args.push(options.filter);
}
const output = await this.adb.subprocess.spawnAndWaitLegacy(args);
return output
.split("\n")
.filter((line) => !!line)
.map(PackageManager.parsePackageListItem);
}
// TODO: install: support split apk formats (`adb install-multiple`)
}