blob: b83f4ab25d7c80a3259fe4847cd755b322427391 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.art;
import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
import static com.android.server.art.ArtManagerLocal.SnapshotProfileException;
import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
import static com.android.server.art.model.ArtFlags.DexoptFlags;
import static com.android.server.art.model.ArtFlags.PriorityClassApi;
import static com.android.server.art.model.DexoptResult.DexContainerFileDexoptResult;
import static com.android.server.art.model.DexoptResult.DexoptResultStatus;
import static com.android.server.art.model.DexoptResult.PackageDexoptResult;
import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.GuardedBy;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.DeleteResult;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
import com.android.server.art.model.DexoptStatus;
import com.android.server.art.model.OperationProgress;
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
import libcore.io.Streams;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
/**
* This class handles ART shell commands.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public final class ArtShellCommand extends BasicShellCommandHandler {
private static final String TAG = ArtManagerLocal.TAG;
/** The default location for profile dumps. */
private final static String PROFILE_DEBUG_LOCATION = "/data/misc/profman";
private final ArtManagerLocal mArtManagerLocal;
private final PackageManagerLocal mPackageManagerLocal;
@GuardedBy("sCancellationSignalMap")
@NonNull
private static final Map<String, CancellationSignal> sCancellationSignalMap = new HashMap<>();
public ArtShellCommand(@NonNull ArtManagerLocal artManagerLocal,
@NonNull PackageManagerLocal packageManagerLocal) {
mArtManagerLocal = artManagerLocal;
mPackageManagerLocal = packageManagerLocal;
}
@Override
public int onCommand(String cmd) {
// Apps shouldn't call ART Service shell commands, not even for dexopting themselves.
enforceRootOrShell();
PrintWriter pw = getOutPrintWriter();
try (var snapshot = mPackageManagerLocal.withFilteredSnapshot()) {
switch (cmd) {
case "compile":
return handleCompile(pw, snapshot);
case "reconcile-secondary-dex-files":
pw.println("Warning: 'pm reconcile-secondary-dex-files' is deprecated. It is "
+ "now doing nothing");
return 0;
case "force-dex-opt":
return handleForceDexopt(pw, snapshot);
case "bg-dexopt-job":
return handleBgDexoptJob(pw, snapshot);
case "cancel-bg-dexopt-job":
pw.println("Warning: 'pm cancel-bg-dexopt-job' is deprecated. It is now an "
+ "alias of 'pm bg-dexopt-job --cancel'");
return handleCancelBgDexoptJob(pw);
case "delete-dexopt":
return handleDeleteDexopt(pw, snapshot);
case "dump-profiles":
return handleDumpProfile(pw, snapshot);
case "snapshot-profile":
return handleSnapshotProfile(pw, snapshot);
case "art":
return handleArtCommand(pw, snapshot);
default:
// Can't happen. Only supported commands are forwarded to ART Service.
throw new IllegalArgumentException(
String.format("Unexpected command '%s' forwarded to ART Service", cmd));
}
} catch (IllegalArgumentException | SnapshotProfileException e) {
pw.println("Error: " + e.getMessage());
return 1;
}
}
private int handleArtCommand(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
String subcmd = getNextArgRequired();
switch (subcmd) {
case "dexopt-packages": {
return handleBatchDexopt(pw, snapshot);
}
case "cancel": {
String jobId = getNextArgRequired();
CancellationSignal signal;
synchronized (sCancellationSignalMap) {
signal = sCancellationSignalMap.getOrDefault(jobId, null);
}
if (signal == null) {
pw.println("Job not found");
return 1;
}
signal.cancel();
pw.println("Job cancelled");
return 0;
}
case "dump": {
String packageName = getNextArg();
if (packageName != null) {
mArtManagerLocal.dumpPackage(pw, snapshot, packageName);
} else {
mArtManagerLocal.dump(pw, snapshot);
}
return 0;
}
case "cleanup": {
return handleCleanup(pw, snapshot);
}
case "clear-app-profiles": {
mArtManagerLocal.clearAppProfiles(snapshot, getNextArgRequired());
pw.println("Profiles cleared");
return 0;
}
default:
pw.printf("Error: Unknown 'art' sub-command '%s'\n", subcmd);
pw.println("See 'pm help' for help");
return 1;
}
}
private int handleCompile(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
@DexoptFlags int scopeFlags = 0;
String reason = null;
String compilerFilter = null;
@PriorityClassApi int priorityClass = ArtFlags.PRIORITY_NONE;
String splitArg = null;
boolean force = false;
boolean reset = false;
boolean forAllPackages = false;
boolean legacyClearProfile = false;
boolean verbose = false;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-a":
forAllPackages = true;
break;
case "-r":
reason = getNextArgRequired();
break;
case "-m":
compilerFilter = getNextArgRequired();
break;
case "-p":
priorityClass = parsePriorityClass(getNextArgRequired());
break;
case "-f":
force = true;
break;
case "--primary-dex":
scopeFlags |= ArtFlags.FLAG_FOR_PRIMARY_DEX;
break;
case "--secondary-dex":
scopeFlags |= ArtFlags.FLAG_FOR_SECONDARY_DEX;
break;
case "--include-dependencies":
scopeFlags |= ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
break;
case "--full":
scopeFlags |= ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
break;
case "--split":
splitArg = getNextArgRequired();
break;
case "--reset":
reset = true;
break;
case "-c":
pw.println("Warning: Flag '-c' is deprecated and usually produces undesired "
+ "results. Please use one of the following commands instead.");
pw.println("- To clear the local profiles only, use "
+ "'pm art clear-app-profiles PACKAGE_NAME'. (The existing dexopt "
+ "artifacts will be kept, even if they are derived from the "
+ "profiles.)");
pw.println("- To clear the local profiles and also clear the dexopt artifacts "
+ "that are derived from them, use 'pm compile --reset PACKAGE_NAME'. "
+ "(The package will be reset to the initial state as if it's newly "
+ "installed, which means the package will be re-dexopted if "
+ "necessary, and cloud profiles will be used if exist.)");
pw.println("- To re-dexopt the package with no profile, use "
+ "'pm compile -m verify -f PACKAGE_NAME'. (The local profiles "
+ "will be kept but not used during the dexopt. The dexopt artifacts "
+ "are guaranteed to have no compiled code.)");
legacyClearProfile = true;
break;
case "--check-prof":
getNextArgRequired();
pw.println("Warning: Ignoring obsolete flag '--check-prof'. It is "
+ "unconditionally enabled now");
break;
case "-v":
verbose = true;
break;
default:
pw.println("Error: Unknown option: " + opt);
return 1;
}
}
List<String> packageNames = forAllPackages
? List.copyOf(snapshot.getPackageStates().keySet())
: List.of(getNextArgRequired());
var paramsBuilder = new DexoptParams.Builder(ReasonMapping.REASON_CMDLINE);
if (reason != null) {
if (reason.equals(ReasonMapping.REASON_INACTIVE)) {
pw.println("Warning: '-r inactive' produces undesired results.");
}
if (compilerFilter == null) {
paramsBuilder.setCompilerFilter(ReasonMapping.getCompilerFilterForReason(reason));
}
if (priorityClass == ArtFlags.PRIORITY_NONE) {
paramsBuilder.setPriorityClass(ReasonMapping.getPriorityClassForReason(reason));
}
}
if (compilerFilter != null) {
paramsBuilder.setCompilerFilter(compilerFilter);
}
if (priorityClass != ArtFlags.PRIORITY_NONE) {
paramsBuilder.setPriorityClass(priorityClass);
}
if (force) {
paramsBuilder.setFlags(ArtFlags.FLAG_FORCE, ArtFlags.FLAG_FORCE);
}
if (splitArg != null) {
if (scopeFlags != 0) {
pw.println("Error: '--primary-dex', '--secondary-dex', "
+ "'--include-dependencies', or '--full' must not be set when '--split' "
+ "is set.");
return 1;
}
if (forAllPackages) {
pw.println("Error: '-a' cannot be specified together with '--split'");
return 1;
}
scopeFlags = ArtFlags.FLAG_FOR_PRIMARY_DEX;
paramsBuilder.setFlags(ArtFlags.FLAG_FOR_SINGLE_SPLIT, ArtFlags.FLAG_FOR_SINGLE_SPLIT)
.setSplitName(getSplitName(pw, snapshot, packageNames.get(0), splitArg));
}
if (scopeFlags != 0) {
paramsBuilder.setFlags(scopeFlags,
ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
} else {
paramsBuilder.setFlags(
ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
}
if (forAllPackages) {
// We'll iterate over all packages anyway.
paramsBuilder.setFlags(0, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
}
if (reset) {
return resetPackages(pw, snapshot, packageNames, verbose);
} else {
if (legacyClearProfile) {
// For compat only. Combining this with dexopt usually produces in undesired
// results.
for (String packageName : packageNames) {
mArtManagerLocal.clearAppProfiles(snapshot, packageName);
}
}
return dexoptPackages(pw, snapshot, packageNames, paramsBuilder.build(), verbose);
}
}
private int handleForceDexopt(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
pw.println("Warning: 'pm force-dex-opt' is deprecated. Please use 'pm compile "
+ "-f PACKAGE_NAME' instead");
return dexoptPackages(pw, snapshot, List.of(getNextArgRequired()),
new DexoptParams.Builder(ReasonMapping.REASON_CMDLINE)
.setFlags(ArtFlags.FLAG_FORCE, ArtFlags.FLAG_FORCE)
.setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build(),
false /* verbose */);
}
private int handleBgDexoptJob(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
String opt = getNextOption();
if (opt == null) {
List<String> packageNames = new ArrayList<>();
String arg;
while ((arg = getNextArg()) != null) {
packageNames.add(arg);
}
if (!packageNames.isEmpty()) {
pw.println("Warning: Running 'pm bg-dexopt-job' with package names is deprecated. "
+ "Please use 'pm compile -r bg-dexopt PACKAGE_NAME' instead");
return dexoptPackages(pw, snapshot, packageNames,
new DexoptParams.Builder(ReasonMapping.REASON_BG_DEXOPT).build(),
false /* verbose */);
}
CompletableFuture<BackgroundDexoptJob.Result> runningJob =
mArtManagerLocal.getRunningBackgroundDexoptJob();
if (runningJob != null) {
pw.println("Another job already running. Waiting for it to finish... To cancel it, "
+ "run 'pm bg-dexopt-job --cancel'. in a separate shell.");
pw.flush();
Utils.getFuture(runningJob);
}
CompletableFuture<BackgroundDexoptJob.Result> future =
mArtManagerLocal.startBackgroundDexoptJobAndReturnFuture();
pw.println("Job running... To cancel it, run 'pm bg-dexopt-job --cancel'. in a "
+ "separate shell.");
pw.flush();
BackgroundDexoptJob.Result result = Utils.getFuture(future);
if (result instanceof BackgroundDexoptJob.CompletedResult) {
var completedResult = (BackgroundDexoptJob.CompletedResult) result;
if (completedResult.dexoptResult().getFinalStatus()
== DexoptResult.DEXOPT_CANCELLED) {
pw.println("Job cancelled. See logs for details");
} else {
pw.println("Job finished. See logs for details");
}
} else if (result instanceof BackgroundDexoptJob.FatalErrorResult) {
// Never expected.
pw.println("Job encountered a fatal error");
}
return 0;
}
switch (opt) {
case "--cancel": {
return handleCancelBgDexoptJob(pw);
}
case "--enable": {
// This operation requires the uid to be "system" (1000).
long identityToken = Binder.clearCallingIdentity();
try {
mArtManagerLocal.scheduleBackgroundDexoptJob();
} finally {
Binder.restoreCallingIdentity(identityToken);
}
pw.println("Background dexopt job enabled");
return 0;
}
case "--disable": {
// This operation requires the uid to be "system" (1000).
long identityToken = Binder.clearCallingIdentity();
try {
mArtManagerLocal.unscheduleBackgroundDexoptJob();
} finally {
Binder.restoreCallingIdentity(identityToken);
}
pw.println("Background dexopt job disabled");
return 0;
}
default:
pw.println("Error: Unknown option: " + opt);
return 1;
}
}
private int handleCancelBgDexoptJob(@NonNull PrintWriter pw) {
mArtManagerLocal.cancelBackgroundDexoptJob();
pw.println("Background dexopt job cancelled");
return 0;
}
private int handleCleanup(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
long freedBytes = mArtManagerLocal.cleanup(snapshot);
pw.printf("Freed %d bytes\n", freedBytes);
return 0;
}
private int handleDeleteDexopt(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
DeleteResult result =
mArtManagerLocal.deleteDexoptArtifacts(snapshot, getNextArgRequired());
pw.printf("Freed %d bytes\n", result.getFreedBytes());
return 0;
}
private int handleSnapshotProfile(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
throws SnapshotProfileException {
String splitName = null;
String codePath = null;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "--split":
splitName = getNextArgRequired();
break;
default:
pw.println("Error: Unknown option: " + opt);
return 1;
}
}
String packageName = getNextArgRequired();
if ("--code-path".equals(getNextOption())) {
pw.println("Warning: Specifying a split using '--code-path' is deprecated. Please use "
+ "'--split SPLIT_NAME' instead");
pw.println("Tip: '--split SPLIT_NAME' must be passed before the package name");
codePath = getNextArgRequired();
}
if (splitName != null && codePath != null) {
pw.println("Error: '--split' and '--code-path' cannot be both specified");
return 1;
}
if (packageName.equals(Utils.PLATFORM_PACKAGE_NAME)) {
if (splitName != null) {
pw.println("Error: '--split' must not be specified for boot image profile");
return 1;
}
if (codePath != null) {
pw.println("Error: '--code-path' must not be specified for boot image profile");
return 1;
}
return handleSnapshotBootProfile(pw, snapshot);
}
if (splitName != null && splitName.isEmpty()) {
splitName = null;
}
if (codePath != null) {
splitName = getSplitNameByFullPath(snapshot, packageName, codePath);
}
return handleSnapshotAppProfile(pw, snapshot, packageName, splitName);
}
private int handleSnapshotBootProfile(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
throws SnapshotProfileException {
String outputRelativePath = "android.prof";
ParcelFileDescriptor fd = mArtManagerLocal.snapshotBootImageProfile(snapshot);
writeProfileFdContentsToFile(pw, fd, outputRelativePath);
return 0;
}
private int handleSnapshotAppProfile(@NonNull PrintWriter pw,
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
@Nullable String splitName) throws SnapshotProfileException {
String outputRelativePath = String.format("%s%s.prof", packageName,
splitName != null ? String.format("-split_%s.apk", splitName) : "");
ParcelFileDescriptor fd =
mArtManagerLocal.snapshotAppProfile(snapshot, packageName, splitName);
writeProfileFdContentsToFile(pw, fd, outputRelativePath);
return 0;
}
private int handleDumpProfile(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
throws SnapshotProfileException {
boolean dumpClassesAndMethods = false;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "--dump-classes-and-methods": {
dumpClassesAndMethods = true;
break;
}
default:
pw.println("Error: Unknown option: " + opt);
return 1;
}
}
String packageName = getNextArgRequired();
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
try (var tracing = new Utils.Tracing("dump profiles")) {
for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) {
if (!dexInfo.hasCode()) {
continue;
}
String profileName = PrimaryDexUtils.getProfileName(dexInfo.splitName());
// The path is intentionally inconsistent with the one for "snapshot-profile". This
// is to match the behavior of the legacy PM shell command.
String outputRelativePath =
String.format("%s-%s.prof.txt", packageName, profileName);
ParcelFileDescriptor fd = mArtManagerLocal.dumpAppProfile(
snapshot, packageName, dexInfo.splitName(), dumpClassesAndMethods);
writeProfileFdContentsToFile(pw, fd, outputRelativePath);
}
}
return 0;
}
private int handleBatchDexopt(
@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
String reason = null;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-r":
reason = getNextArgRequired();
break;
default:
pw.println("Error: Unknown option: " + opt);
return 1;
}
}
if (reason == null) {
pw.println("Error: '-r REASON' is required");
return 1;
}
if (!ReasonMapping.BATCH_DEXOPT_REASONS.contains(reason)) {
pw.printf("Error: Invalid batch dexopt reason '%s'. Valid values are: %s\n", reason,
ReasonMapping.BATCH_DEXOPT_REASONS);
return 1;
}
DexoptResult result;
ExecutorService progressCallbackExecutor = Executors.newSingleThreadExecutor();
try (var signal = new WithCancellationSignal(pw, true /* verbose */)) {
result = mArtManagerLocal.dexoptPackages(
snapshot, reason, signal.get(), progressCallbackExecutor, progress -> {
pw.println(String.format("Dexopting apps: %d%%", progress.getPercentage()));
pw.flush();
});
Utils.executeAndWait(progressCallbackExecutor, () -> {
printDexoptResult(pw, result, true /* verbose */, true /* multiPackage */);
});
} finally {
progressCallbackExecutor.shutdown();
}
return 0;
}
@Override
public void onHelp() {
// No one should call this. The help text should be printed by the `onHelp` handler of `cmd
// package`.
throw new UnsupportedOperationException("Unexpected call to 'onHelp'");
}
public static void printHelp(@NonNull PrintWriter pw) {
pw.println("compile [-r COMPILATION_REASON] [-m COMPILER_FILTER] [-p PRIORITY] [-f]");
pw.println(" [--primary-dex] [--secondary-dex] [--include-dependencies] [--full]");
pw.println(" [--split SPLIT_NAME] [--reset] [-a | PACKAGE_NAME]");
pw.println(" Dexopt a package or all packages.");
pw.println(" Options:");
pw.println(" -a Dexopt all packages");
pw.println(" -r Set the compiler filter and the priority based on the given");
pw.println(" compilation reason.");
pw.println(" Available options: 'first-boot', 'boot-after-ota',");
pw.println(" 'boot-after-mainline-update', 'install', 'bg-dexopt', 'cmdline'.");
pw.println(" -m Set the target compiler filter. The filter actually used may be");
pw.println(" different, e.g. 'speed-profile' without profiles present may result in");
pw.println(" 'verify' being used instead. If not specified, this defaults to the");
pw.println(" value given by -r, or the system property 'pm.dexopt.cmdline'.");
pw.println(" Available options (in descending order): 'speed', 'speed-profile',");
pw.println(" 'verify'.");
pw.println(" -p Set the priority of the operation, which determines the resource usage");
pw.println(" and the process priority. If not specified, this defaults to");
pw.println(" the value given by -r, or 'PRIORITY_INTERACTIVE'.");
pw.println(" Available options (in descending order): 'PRIORITY_BOOT',");
pw.println(" 'PRIORITY_INTERACTIVE_FAST', 'PRIORITY_INTERACTIVE',");
pw.println(" 'PRIORITY_BACKGROUND'.");
pw.println(" -f Force dexopt, also when the compiler filter being applied is not");
pw.println(" better than that of the current dexopt artifacts for a package.");
pw.println(" --reset Reset the dexopt state of the package as if the package is newly");
pw.println(" installed.");
pw.println(" More specifically, it clears reference profiles, current profiles, and");
pw.println(" any code compiled from those local profiles. If there is an external");
pw.println(" profile (e.g., a cloud profile), the code compiled from that profile");
pw.println(" will be kept.");
pw.println(" For secondary dex files, it also clears all dexopt artifacts.");
pw.println(" When this flag is set, all the other flags are ignored.");
pw.println(" -v Verbose mode. This mode prints detailed results.");
pw.println(" Scope options:");
pw.println(" --primary-dex Dexopt primary dex files only (all APKs that are installed");
pw.println(" as part of the package, including the base APK and all other split");
pw.println(" APKs).");
pw.println(" --secondary-dex Dexopt secondary dex files only (APKs/JARs that the app");
pw.println(" puts in its own data directory at runtime and loads with custom");
pw.println(" classloaders).");
pw.println(" --include-dependencies Include dependency packages (dependencies that are");
pw.println(" declared by the app with <uses-library> tags and transitive");
pw.println(" dependencies). This option can only be used together with");
pw.println(" '--primary-dex' or '--secondary-dex'.");
pw.println(" --full Dexopt all above. (Recommended)");
pw.println(" --split SPLIT_NAME Only dexopt the given split. If SPLIT_NAME is an empty");
pw.println(" string, only dexopt the base APK.");
pw.println(" Tip: To pass an empty string, use a pair of quotes (\"\").");
pw.println(" When this option is set, '--primary-dex', '--secondary-dex',");
pw.println(" '--include-dependencies', '--full', and '-a' must not be set.");
pw.println(" Note: If none of the scope options above are set, the scope defaults to");
pw.println(" '--primary-dex --include-dependencies'.");
pw.println();
pw.println("delete-dexopt PACKAGE_NAME");
pw.println(" Delete the dexopt artifacts of both primary dex files and secondary dex");
pw.println(" files of a package.");
pw.println();
pw.println("bg-dexopt-job [--cancel | --disable | --enable]");
pw.println(" Control the background dexopt job.");
pw.println(" Without flags, it starts a background dexopt job immediately and waits for");
pw.println(" it to finish. If a job is already started either automatically by the");
pw.println(" system or through this command, it will wait for the running job to");
pw.println(" finish and then start a new one.");
pw.println(" Different from 'pm compile -r bg-dexopt -a', the behavior of this command");
pw.println(" is the same as a real background dexopt job. Specifically,");
pw.println(" - It only dexopts a subset of apps determined by either the system's");
pw.println(" default logic based on app usage data or the custom logic specified by");
pw.println(" the 'ArtManagerLocal.setBatchDexoptStartCallback' Java API.");
pw.println(" - It runs dexopt in parallel, where the concurrency setting is specified");
pw.println(" by the system property 'pm.dexopt.bg-dexopt.concurrency'.");
pw.println(" - If the storage is low, it also downgrades unused apps.");
pw.println(" - It also cleans up obsolete files.");
pw.println(" Options:");
pw.println(" --cancel Cancel any currently running background dexopt job immediately.");
pw.println(" This cancels jobs started either automatically by the system or through");
pw.println(" this command. This command is not blocking.");
pw.println(" --disable: Disable the background dexopt job from being started by the");
pw.println(" job scheduler. If a job is already started by the job scheduler and is");
pw.println(" running, it will be cancelled immediately. Does not affect jobs started");
pw.println(" through this command or by the system in other ways.");
pw.println(" This state will be lost when the system_server process exits.");
pw.println(" --enable: Enable the background dexopt job to be started by the job");
pw.println(" scheduler again, if previously disabled by --disable.");
pw.println(" When a list of package names is passed, this command does NOT start a real");
pw.println(" background dexopt job. Instead, it dexopts the given packages sequentially.");
pw.println(" This usage is deprecated. Please use 'pm compile -r bg-dexopt PACKAGE_NAME'");
pw.println(" instead.");
pw.println();
pw.println("snapshot-profile [android | [--split SPLIT_NAME] PACKAGE_NAME]");
pw.println(" Snapshot the boot image profile or the app profile and save it to");
pw.println(" '" + PROFILE_DEBUG_LOCATION + "'.");
pw.println(" If 'android' is passed, the command snapshots the boot image profile, and");
pw.println(" the output filename is 'android.prof'.");
pw.println(" If a package name is passed, the command snapshots the app profile.");
pw.println(" Options:");
pw.println(" --split SPLIT_NAME If specified, the command snapshots the profile of the");
pw.println(" given split, and the output filename is");
pw.println(" 'PACKAGE_NAME-split_SPLIT_NAME.apk.prof'.");
pw.println(" If not specified, the command snapshots the profile of the base APK,");
pw.println(" and the output filename is 'PACKAGE_NAME.prof'");
pw.println();
pw.println("dump-profiles [--dump-classes-and-methods] PACKAGE_NAME");
pw.println(" Dump the profiles of the given app in text format and save the outputs to");
pw.println(" '" + PROFILE_DEBUG_LOCATION + "'.");
pw.println(" The profile of the base APK is dumped to 'PACKAGE_NAME-primary.prof.txt'");
pw.println(" The profile of a split APK is dumped to");
pw.println(" 'PACKAGE_NAME-SPLIT_NAME.split.prof.txt'");
pw.println();
pw.println("art SUB_COMMAND [ARGS]...");
pw.println(" Run ART Service commands");
pw.println();
pw.println(" Supported sub-commands:");
pw.println();
pw.println(" cancel JOB_ID");
pw.println(" Cancel a job started by a shell command. This doesn't apply to background");
pw.println(" jobs.");
pw.println();
pw.println(" clear-app-profiles PACKAGE_NAME");
pw.println(" Clear the profiles that are collected locally for the given package,");
pw.println(" including the profiles for primary and secondary dex files. More");
pw.println(" specifically, this command clears reference profiles and current");
pw.println(" profiles. External profiles (e.g., cloud profiles) will be kept.");
pw.println();
pw.println(" cleanup");
pw.println(" Cleanup obsolete files, such as dexopt artifacts that are outdated or");
pw.println(" correspond to dex container files that no longer exist.");
pw.println();
pw.println(" dump [PACKAGE_NAME]");
pw.println(" Dumps the dexopt state in text format to stdout.");
pw.println(" If PACKAGE_NAME is empty, the command is for all packages. Otherwise, it");
pw.println(" is for the given package.");
pw.println();
pw.println(" dexopt-packages -r REASON");
pw.println(" Run batch dexopt for the given reason.");
pw.println(" Valid values for REASON: 'first-boot', 'boot-after-ota',");
pw.println(" 'boot-after-mainline-update', 'bg-dexopt'");
pw.println(" This command is different from 'pm compile -r REASON -a'. For example, it");
pw.println(" only dexopts a subset of apps, and it runs dexopt in parallel. See the");
pw.println(" API documentation for 'ArtManagerLocal.dexoptPackages' for details.");
}
private void enforceRootOrShell() {
final int uid = Binder.getCallingUid();
if (uid != Process.ROOT_UID && uid != Process.SHELL_UID) {
throw new SecurityException("ART service shell commands need root or shell access");
}
}
@PriorityClassApi
int parsePriorityClass(@NonNull String priorityClass) {
switch (priorityClass) {
case "PRIORITY_BOOT":
return ArtFlags.PRIORITY_BOOT;
case "PRIORITY_INTERACTIVE_FAST":
return ArtFlags.PRIORITY_INTERACTIVE_FAST;
case "PRIORITY_INTERACTIVE":
return ArtFlags.PRIORITY_INTERACTIVE;
case "PRIORITY_BACKGROUND":
return ArtFlags.PRIORITY_BACKGROUND;
default:
throw new IllegalArgumentException("Unknown priority " + priorityClass);
}
}
@Nullable
private String getSplitName(@NonNull PrintWriter pw,
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
@NonNull String splitArg) {
if (splitArg.isEmpty()) {
return null; // Base APK.
}
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
List<PrimaryDexInfo> dexInfoList = PrimaryDexUtils.getDexInfo(pkg);
for (PrimaryDexInfo dexInfo : dexInfoList) {
if (splitArg.equals(dexInfo.splitName())) {
return splitArg;
}
}
for (PrimaryDexInfo dexInfo : dexInfoList) {
if (splitArg.equals(new File(dexInfo.dexPath()).getName())) {
pw.println("Warning: Specifying a split using a filename is deprecated. Please "
+ "use a split name (or an empty string for the base APK) instead");
return dexInfo.splitName();
}
}
throw new IllegalArgumentException(String.format("Split '%s' not found", splitArg));
}
@Nullable
private String getSplitNameByFullPath(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull String packageName, @NonNull String fullPath) {
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
List<PrimaryDexInfo> dexInfoList = PrimaryDexUtils.getDexInfo(pkg);
for (PrimaryDexInfo dexInfo : dexInfoList) {
if (fullPath.equals(dexInfo.dexPath())) {
return dexInfo.splitName();
}
}
throw new IllegalArgumentException(String.format("Code path '%s' not found", fullPath));
}
private int resetPackages(@NonNull PrintWriter pw,
@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull List<String> packageNames, boolean verbose) {
try (var signal = new WithCancellationSignal(pw, verbose)) {
for (String packageName : packageNames) {
DexoptResult result =
mArtManagerLocal.resetDexoptStatus(snapshot, packageName, signal.get());
printDexoptResult(pw, result, verbose, packageNames.size() > 1);
}
}
return 0;
}
private int dexoptPackages(@NonNull PrintWriter pw,
@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull List<String> packageNames, @NonNull DexoptParams params, boolean verbose) {
try (var signal = new WithCancellationSignal(pw, verbose)) {
for (String packageName : packageNames) {
DexoptResult result =
mArtManagerLocal.dexoptPackage(snapshot, packageName, params, signal.get());
printDexoptResult(pw, result, verbose, packageNames.size() > 1);
}
}
return 0;
}
@NonNull
private String dexoptResultStatusToSimpleString(@DexoptResultStatus int status) {
return (status == DexoptResult.DEXOPT_SKIPPED || status == DexoptResult.DEXOPT_PERFORMED)
? "Success"
: "Failure";
}
private void printDexoptResult(@NonNull PrintWriter pw, @NonNull DexoptResult result,
boolean verbose, boolean multiPackage) {
for (PackageDexoptResult packageResult : result.getPackageDexoptResults()) {
if (verbose) {
pw.printf("[%s]\n", packageResult.getPackageName());
for (DexContainerFileDexoptResult fileResult :
packageResult.getDexContainerFileDexoptResults()) {
pw.println(fileResult);
}
} else if (multiPackage) {
pw.printf("[%s] %s\n", packageResult.getPackageName(),
dexoptResultStatusToSimpleString(packageResult.getStatus()));
}
}
if (verbose) {
pw.println("Final Status: "
+ DexoptResult.dexoptResultStatusToString(result.getFinalStatus()));
} else if (!multiPackage) {
// Multi-package result is printed by the loop above.
pw.println(dexoptResultStatusToSimpleString(result.getFinalStatus()));
}
pw.flush();
}
private void writeProfileFdContentsToFile(@NonNull PrintWriter pw,
@NonNull ParcelFileDescriptor fd, @NonNull String outputRelativePath) {
try {
StructStat st = Os.stat(PROFILE_DEBUG_LOCATION);
if (st.st_uid != Process.SYSTEM_UID || st.st_gid != Process.SHELL_UID
|| (st.st_mode & 0007) != 0) {
throw new RuntimeException(
String.format("%s has wrong permissions: uid=%d, gid=%d, mode=%o",
PROFILE_DEBUG_LOCATION, st.st_uid, st.st_gid, st.st_mode));
}
} catch (ErrnoException e) {
throw new RuntimeException("Unable to stat " + PROFILE_DEBUG_LOCATION, e);
}
Path outputPath = Paths.get(PROFILE_DEBUG_LOCATION, outputRelativePath);
try (InputStream inputStream = new AutoCloseInputStream(fd);
FileOutputStream outputStream = new FileOutputStream(outputPath.toFile())) {
// The system server doesn't have the permission to chown the file to "shell", so we
// make it readable by everyone and put it in a directory that is only accessible by
// "shell", which is created by system/core/rootdir/init.rc. The permissions are
// verified by the code above.
Os.fchmod(outputStream.getFD(), 0644);
Streams.copy(inputStream, outputStream);
pw.printf("Profile saved to '%s'\n", outputPath);
} catch (IOException | ErrnoException e) {
Utils.deleteIfExistsSafe(outputPath);
throw new RuntimeException(e);
}
}
private static class WithCancellationSignal implements AutoCloseable {
@NonNull private final CancellationSignal mSignal = new CancellationSignal();
@NonNull private final String mJobId;
public WithCancellationSignal(@NonNull PrintWriter pw, boolean verbose) {
mJobId = UUID.randomUUID().toString();
if (verbose) {
pw.printf(
"Job running. To cancel it, run 'pm art cancel %s' in a separate shell.\n",
mJobId);
pw.flush();
}
synchronized (sCancellationSignalMap) {
sCancellationSignalMap.put(mJobId, mSignal);
}
}
@NonNull
public CancellationSignal get() {
return mSignal;
}
public void close() {
synchronized (sCancellationSignalMap) {
sCancellationSignalMap.remove(mJobId);
}
}
}
}