diff options
author | Jiakai Zhang <jiakaiz@google.com> | 2024-04-10 16:18:37 +0100 |
---|---|---|
committer | Jiakai Zhang <jiakaiz@google.com> | 2024-04-30 10:43:18 +0000 |
commit | bd45d4629ef5a62baab677ef4e7708a89ff6ec12 (patch) | |
tree | f5712fa780491d5d1f913629cc408b02d1b65789 | |
parent | bb55c03f8899339755b4b5fcdbe434e58b8909ce (diff) | |
download | art-bd45d4629ef5a62baab677ef4e7708a89ff6ec12.tar.gz |
Implement Pre-reboot Dexopt (part 2).
MUST_SLEEP=can't use Sleeper, which is in Guava.
Bug: 311377497
Test: adb shell pm art pre-reboot-dexopt
Test: -
1. system/update_engine/scripts/update_device.py out/dist/aosp_cf_x86_64_phone-ota-*.zip
2. adb shell pm art pre-reboot-dexopt --slot _b
Change-Id: I718de09f8a10d476e1919f3ef3e1d0bb2b4073e3
10 files changed, 438 insertions, 5 deletions
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java index a9a357776e..276e6eb7f5 100644 --- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java +++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java @@ -169,7 +169,7 @@ public final class ArtManagerLocal { public int handleShellCommand(@NonNull Binder target, @NonNull ParcelFileDescriptor in, @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, @NonNull String[] args) { - return new ArtShellCommand(this, mInjector.getPackageManagerLocal()) + return new ArtShellCommand(this, mInjector.getPackageManagerLocal(), mInjector.getContext()) .exec(target, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java index 592f9cd36f..9203143abe 100644 --- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java +++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java @@ -31,6 +31,7 @@ import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptSt import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.os.Binder; import android.os.Build; import android.os.CancellationSignal; @@ -45,12 +46,14 @@ import androidx.annotation.RequiresApi; import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.BasicShellCommandHandler; +import com.android.modules.utils.build.SdkLevel; 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.art.prereboot.PreRebootDriver; import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageState; @@ -92,15 +95,17 @@ public final class ArtShellCommand extends BasicShellCommandHandler { private final ArtManagerLocal mArtManagerLocal; private final PackageManagerLocal mPackageManagerLocal; + private final Context mContext; @GuardedBy("sCancellationSignalMap") @NonNull private static final Map<String, CancellationSignal> sCancellationSignalMap = new HashMap<>(); public ArtShellCommand(@NonNull ArtManagerLocal artManagerLocal, - @NonNull PackageManagerLocal packageManagerLocal) { + @NonNull PackageManagerLocal packageManagerLocal, @NonNull Context context) { mArtManagerLocal = artManagerLocal; mPackageManagerLocal = packageManagerLocal; + mContext = context; } @Override @@ -181,6 +186,10 @@ public final class ArtShellCommand extends BasicShellCommandHandler { pw.println("Profiles cleared"); return 0; } + case "pre-reboot-dexopt": { + // TODO(b/311377497): Remove this command once the development is done. + return handlePreRebootDexopt(pw); + } default: pw.printf("Error: Unknown 'art' sub-command '%s'\n", subcmd); pw.println("See 'pm help' for help"); @@ -631,6 +640,33 @@ public final class ArtShellCommand extends BasicShellCommandHandler { return 0; } + private int handlePreRebootDexopt(@NonNull PrintWriter pw) { + if (!SdkLevel.isAtLeastV()) { + pw.println("Error: Unsupported command 'pre-reboot-dexopt'"); + return 1; + } + + String otaSlot = null; + + String opt = getNextOption(); + if ("--slot".equals(opt)) { + otaSlot = getNextArgRequired(); + } else if (opt != null) { + pw.println("Error: Unknown option: " + opt); + return 1; + } + + try (var signal = new WithCancellationSignal(pw, true /* verbose */)) { + if (new PreRebootDriver(mContext).run(otaSlot, signal.get())) { + pw.println("Job finished. See logs for details"); + return 0; + } else { + pw.println("Job failed. See logs for details"); + return 1; + } + } + } + @Override public void onHelp() { // No one should call this. The help text should be printed by the `onHelp` handler of `cmd diff --git a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java index 1510bb4e84..b8a55af7e0 100644 --- a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java +++ b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java @@ -29,7 +29,13 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.ArtServiceJobInterface; -/** @hide */ +/** + * The Pre-reboot Dexopt job. + * + * During Pre-reboot Dexopt, the old version of this code is run. + * + * @hide + */ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) public class PreRebootDexoptJob implements ArtServiceJobInterface { private static final String TAG = ArtManagerLocal.TAG; diff --git a/libartservice/service/java/com/android/server/art/ReasonMapping.java b/libartservice/service/java/com/android/server/art/ReasonMapping.java index 0048ebcddf..90ea0d4b62 100644 --- a/libartservice/service/java/com/android/server/art/ReasonMapping.java +++ b/libartservice/service/java/com/android/server/art/ReasonMapping.java @@ -62,6 +62,8 @@ public class ReasonMapping { public static final String REASON_CMDLINE = "cmdline"; /** Downgrading the compiler filter when an app is not used for a long time. */ public static final String REASON_INACTIVE = "inactive"; + /** @hide */ + public static final String REASON_PRE_REBOOT_DEXOPT = "ab-ota"; // Reasons for Play Install Hints (go/install-hints). public static final String REASON_INSTALL_FAST = "install-fast"; @@ -78,8 +80,9 @@ public class ReasonMapping { // Keep this in sync with `ArtShellCommand.printHelp`. /** @hide */ - public static final Set<String> BATCH_DEXOPT_REASONS = Set.of(REASON_FIRST_BOOT, - REASON_BOOT_AFTER_OTA, REASON_BOOT_AFTER_MAINLINE_UPDATE, REASON_BG_DEXOPT); + public static final Set<String> BATCH_DEXOPT_REASONS = + Set.of(REASON_FIRST_BOOT, REASON_BOOT_AFTER_OTA, REASON_BOOT_AFTER_MAINLINE_UPDATE, + REASON_BG_DEXOPT, REASON_PRE_REBOOT_DEXOPT); /** * Reasons for {@link ArtManagerLocal#dexoptPackages}. @@ -92,6 +95,7 @@ public class ReasonMapping { REASON_BOOT_AFTER_OTA, REASON_BOOT_AFTER_MAINLINE_UPDATE, REASON_BG_DEXOPT, + REASON_PRE_REBOOT_DEXOPT, }) // clang-format on @Retention(RetentionPolicy.SOURCE) @@ -175,6 +179,7 @@ public class ReasonMapping { case REASON_CMDLINE: return ArtFlags.PRIORITY_INTERACTIVE; case REASON_BG_DEXOPT: + case REASON_PRE_REBOOT_DEXOPT: case REASON_INACTIVE: case REASON_INSTALL_BULK: case REASON_INSTALL_BULK_SECONDARY: diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java index 9358373eef..b05a597c7d 100644 --- a/libartservice/service/java/com/android/server/art/Utils.java +++ b/libartservice/service/java/com/android/server/art/Utils.java @@ -469,6 +469,14 @@ public final class Utils { return uid == Process.SYSTEM_UID || uid == Process.ROOT_UID || uid == Process.SHELL_UID; } + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Slog.wtf(TAG, "Sleep interrupted", e); + } + } + @AutoValue public abstract static class Abi { static @NonNull Abi create( diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java new file mode 100644 index 0000000000..2e43383161 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootDriver.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 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.prereboot; + +import static com.android.server.art.IDexoptChrootSetup.CHROOT_DIR; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.ArtModuleServiceManager; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; +import android.util.Slog; + +import androidx.annotation.RequiresApi; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.art.ArtManagerLocal; +import com.android.server.art.ArtModuleServiceInitializer; +import com.android.server.art.GlobalInjector; +import com.android.server.art.IDexoptChrootSetup; +import com.android.server.art.Utils; + +import dalvik.system.DelegateLastClassLoader; + +/** + * Drives Pre-reboot Dexopt, through reflection. + * + * During Pre-reboot Dexopt, the old version of this code is run. + * + * @hide + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +public class PreRebootDriver { + private static final String TAG = ArtManagerLocal.TAG; + + @NonNull private final Injector mInjector; + + public PreRebootDriver(@NonNull Context context) { + this(new Injector(context)); + } + + @VisibleForTesting + public PreRebootDriver(@NonNull Injector injector) { + mInjector = injector; + } + + /** + * Runs Pre-reboot Dexopt and returns whether it is successful. + * + * @param otaSlot The slot that contains the OTA update, "_a" or "_b", or null for a Mainline + * update. + */ + public boolean run(@Nullable String otaSlot, @NonNull CancellationSignal cancellationSignal) { + try { + setUp(otaSlot); + runFromChroot(cancellationSignal); + return true; + } catch (RemoteException e) { + Utils.logArtdException(e); + } catch (ServiceSpecificException e) { + Log.e(TAG, "Failed to set up chroot", e); + } catch (ReflectiveOperationException e) { + Log.e(TAG, "Failed to run pre-reboot dexopt", e); + } finally { + tearDown(); + } + return false; + } + + private void setUp(@Nullable String otaSlot) throws RemoteException { + mInjector.getDexoptChrootSetup().setUp(otaSlot); + } + + private void tearDown() { + // In general, the teardown unmounts apexes and partitions, and open files can keep the + // mounts busy so that they cannot be unmounted. Therefore, two things can prevent the + // teardown from succeeding: a running Pre-reboot artd process and the new `service-art.jar` + // opened and mapped by system server. They are managed by the service manager and the + // runtime respectively. There aren't reliable APIs to kill the former or close the latter, + // so we have to do them by triggering GC and finalization, with sleep and retry mechanism. + for (int numRetries = 3; numRetries > 0;) { + try { + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + // Wait for the service manager to shut down artd. The shutdown is asynchronous. + Utils.sleep(5000); + mInjector.getDexoptChrootSetup().tearDown(); + return; + } catch (RemoteException e) { + Utils.logArtdException(e); + } catch (ServiceSpecificException e) { + Log.e(TAG, "Failed to tear down chroot", e); + } catch (IllegalStateException e) { + // Not expected, but we still want retries in such an extreme case. + Slog.wtf(TAG, "Unexpected exception", e); + } + + if (--numRetries > 0) { + Log.i(TAG, "Retrying...."); + Utils.sleep(30000); + } + } + } + + private void runFromChroot(@NonNull CancellationSignal cancellationSignal) + throws ReflectiveOperationException { + String chrootArtDir = CHROOT_DIR + "/apex/com.android.art"; + String dexPath = chrootArtDir + "/javalib/service-art.jar"; + var classLoader = + new DelegateLastClassLoader(dexPath, this.getClass().getClassLoader() /* parent */); + Class<?> preRebootManagerClass = + classLoader.loadClass("com.android.server.art.prereboot.PreRebootManager"); + // Check if the dex file is loaded successfully. Note that the constructor of + // `DelegateLastClassLoader` does not throw when the load fails. + if (preRebootManagerClass == PreRebootManager.class) { + throw new IllegalStateException(String.format("Failed to load %s", dexPath)); + } + Object preRebootManager = preRebootManagerClass.getConstructor().newInstance(); + preRebootManagerClass + .getMethod("run", ArtModuleServiceManager.class, Context.class, + CancellationSignal.class) + .invoke(preRebootManager, ArtModuleServiceInitializer.getArtModuleServiceManager(), + mInjector.getContext(), cancellationSignal); + } + + /** + * Injector pattern for testing purpose. + * + * @hide + */ + @VisibleForTesting + public static class Injector { + @NonNull private final Context mContext; + + Injector(@NonNull Context context) { + mContext = context; + } + + @NonNull + public Context getContext() { + return mContext; + } + + @NonNull + public IDexoptChrootSetup getDexoptChrootSetup() { + return GlobalInjector.getInstance().getDexoptChrootSetup(); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java new file mode 100644 index 0000000000..6d808ae12c --- /dev/null +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootGlobalInjector.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2024 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.prereboot; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.ArtModuleServiceManager; +import android.os.Build; +import android.os.RemoteException; + +import androidx.annotation.RequiresApi; + +import com.android.server.art.ArtdRefCache; +import com.android.server.art.DexUseManagerLocal; +import com.android.server.art.GlobalInjector; +import com.android.server.art.IArtd; +import com.android.server.art.IDexoptChrootSetup; + +import java.util.Objects; + +/** + * The implementation of the Global injector that is used when the code is running Pre-reboot + * Dexopt. + * + * During Pre-reboot Dexopt, the new version of this code is run. + * + * @hide + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +public class PreRebootGlobalInjector extends GlobalInjector { + @NonNull private ArtModuleServiceManager mArtModuleServiceManager; + @Nullable private DexUseManagerLocal mDexUseManager; + + private PreRebootGlobalInjector(@NonNull ArtModuleServiceManager artModuleServiceManager) { + mArtModuleServiceManager = artModuleServiceManager; + } + + public static void init( + @NonNull ArtModuleServiceManager artModuleServiceManager, @NonNull Context context) { + var instance = new PreRebootGlobalInjector(artModuleServiceManager); + GlobalInjector.setInstance(instance); + try (var pin = ArtdRefCache.getInstance().new Pin()) { + // Fail early if artd cannot be initialized. + ArtdRefCache.getInstance().getArtd(); + instance.mDexUseManager = DexUseManagerLocal.createInstance(context); + } + } + + @Override + public boolean isPreReboot() { + return true; + } + + @Override + public void checkArtModuleServiceManager() {} + + @Override + @NonNull + public IArtd getArtd() { + IArtd artd = IArtd.Stub.asInterface( + mArtModuleServiceManager.getArtdPreRebootServiceRegisterer().waitForService()); + if (artd == null) { + throw new IllegalStateException("Unable to connect to artd for pre-reboot dexopt"); + } + try { + artd.preRebootInit(); + } catch (RemoteException e) { + throw new IllegalStateException("Unable to initialize artd for pre-reboot dexopt", e); + } + return artd; + } + + @Override + @NonNull + public IDexoptChrootSetup getDexoptChrootSetup() { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull + public DexUseManagerLocal getDexUseManager() { + return Objects.requireNonNull(mDexUseManager); + } +} diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java new file mode 100644 index 0000000000..d6947ba156 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManager.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 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.prereboot; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.ArtModuleServiceManager; +import android.os.Build; +import android.os.CancellationSignal; + +import androidx.annotation.RequiresApi; + +import com.android.server.LocalManagerRegistry; +import com.android.server.art.ArtManagerLocal; +import com.android.server.art.ArtdRefCache; +import com.android.server.art.ReasonMapping; +import com.android.server.pm.PackageManagerLocal; + +import java.util.Objects; + +/** + * Implementation of {@link PreRebootManagerInterface}. + * + * DO NOT add a constructor with parameters! There can't be stability guarantees on constructors as + * they can't be checked against the interface. + * + * During Pre-reboot Dexopt, the new version of this code is run. + * + * @hide + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +public class PreRebootManager implements PreRebootManagerInterface { + public void run(@NonNull ArtModuleServiceManager artModuleServiceManager, + @NonNull Context context, @NonNull CancellationSignal cancellationSignal) { + try { + PreRebootGlobalInjector.init(artModuleServiceManager, context); + ArtManagerLocal artManagerLocal = new ArtManagerLocal(context); + PackageManagerLocal packageManagerLocal = Objects.requireNonNull( + LocalManagerRegistry.getManager(PackageManagerLocal.class)); + try (var snapshot = packageManagerLocal.withFilteredSnapshot()) { + artManagerLocal.dexoptPackages(snapshot, ReasonMapping.REASON_PRE_REBOOT_DEXOPT, + cancellationSignal, null /* processCallbackExecutor */, + null /* processCallback */); + } + } finally { + ArtdRefCache.getInstance().reset(); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java new file mode 100644 index 0000000000..8422debdbc --- /dev/null +++ b/libartservice/service/java/com/android/server/art/prereboot/PreRebootManagerInterface.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 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.prereboot; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.ArtModuleServiceManager; +import android.os.Build; +import android.os.CancellationSignal; + +import androidx.annotation.RequiresApi; + +/** + * The interface for the entry point of Pre-reboot Dexopt, called through reflection from an old + * version of the ART module. This interface must be kept stable from one version of the ART module + * to another. In principle, a method here should be kept as long as devices that receive Mainline + * updates call it from their old factory installed modules, unless there is a good reason to drop + * the Pre-reboot Dexopt support earlier for certain versions of the ART module. + * + * Dropping the support for certain versions will only make devices lose the opportunity to optimize + * apps before the reboot, but it won't cause severe results such as crashes because even the oldest + * version that uses this interface can elegantly handle reflection exceptions. + * + * During Pre-reboot Dexopt, the new version of this code is run. + * + * @hide + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +public interface PreRebootManagerInterface { + void run(@NonNull ArtModuleServiceManager artModuleServiceManager, @NonNull Context context, + @NonNull CancellationSignal cancellationSignal); +} diff --git a/libartservice/service/proguard.flags b/libartservice/service/proguard.flags index 8ef413f3a3..3203cb6b56 100644 --- a/libartservice/service/proguard.flags +++ b/libartservice/service/proguard.flags @@ -8,3 +8,6 @@ # A job service is referenced by the framework through reflection. -keep class * extends android.app.job.JobService { *; } + +# PreRebootManager is called through reflection. +-keep class com.android.server.art.prereboot.PreRebootManager { *; } |