diff options
5 files changed, 486 insertions, 4 deletions
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp index fe0c3af5e0..a9a0a1a206 100644 --- a/libartservice/service/Android.bp +++ b/libartservice/service/Android.bp @@ -82,6 +82,7 @@ java_defaults { libs: [ "androidx.annotation_annotation", "auto_value_annotations", + "sdk_module-lib_current_framework-configinfrastructure", "sdk_module-lib_current_framework-permission-s", // TODO(b/256866172): Transitive dependency, for r8 only. "framework-statsd.stubs.module_lib", @@ -235,6 +236,10 @@ android_test { "art_module_source_build_java_defaults", ], + libs: [ + "sdk_module-lib_current_framework-configinfrastructure", + ], + static_libs: [ "androidx.test.ext.junit", "androidx.test.ext.truth", diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java index afe2ab0538..727290f995 100644 --- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java +++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java @@ -930,6 +930,11 @@ public final class ArtManagerLocal { @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) public void onApexStaged(@NonNull String[] stagedApexModuleNames) { // TODO(b/311377497): Check system requirements. + mInjector.getPreRebootDexoptJob().unschedule(); + // Although `unschedule` implies `cancel`, we explicitly call `cancel` here to wait for + // the job to exit, if it's running. + mInjector.getPreRebootDexoptJob().cancel(true /* blocking */); + mInjector.getPreRebootDexoptJob().updateOtaSlot(null); mInjector.getPreRebootDexoptJob().schedule(); } diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java index 9203143abe..ff714ee1b8 100644 --- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java +++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java @@ -190,6 +190,9 @@ public final class ArtShellCommand extends BasicShellCommandHandler { // TODO(b/311377497): Remove this command once the development is done. return handlePreRebootDexopt(pw); } + case "on-ota-staged": { + return handleOnOtaStaged(pw); + } default: pw.printf("Error: Unknown 'art' sub-command '%s'\n", subcmd); pw.println("See 'pm help' for help"); @@ -667,6 +670,67 @@ public final class ArtShellCommand extends BasicShellCommandHandler { } } + private int handleOnOtaStaged(@NonNull PrintWriter pw) { + if (!SdkLevel.isAtLeastV()) { + pw.println("Error: Unsupported command 'on-ota-staged'"); + return 1; + } + + int uid = Binder.getCallingUid(); + if (uid != Process.ROOT_UID) { + throw new SecurityException("Only root can call 'on-ota-staged'"); + } + + String otaSlot = null; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--slot": + otaSlot = getNextArgRequired(); + break; + default: + pw.println("Error: Unknown option: " + opt); + return 1; + } + } + + if (otaSlot == null) { + pw.println("Error: '--slot' must be specified"); + return 1; + } + + int code; + + // This operation requires the uid to be "system" (1000). + long identityToken = Binder.clearCallingIdentity(); + try { + mArtManagerLocal.getPreRebootDexoptJob().unschedule(); + // Although `unschedule` implies `cancel`, we explicitly call `cancel` here to wait for + // the job to exit, if it's running. + mArtManagerLocal.getPreRebootDexoptJob().cancel(true /* blocking */); + mArtManagerLocal.getPreRebootDexoptJob().updateOtaSlot(otaSlot); + code = mArtManagerLocal.getPreRebootDexoptJob().schedule(); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + switch (code) { + case ArtFlags.SCHEDULE_SUCCESS: + pw.println("Job scheduled"); + return 0; + case ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP: + pw.println("Job disabled by system property"); + return 1; + case ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE: + pw.println("Failed to schedule job"); + return 1; + default: + // Can't happen. + throw new IllegalStateException("Unknown result code: " + code); + } + } + @Override public void onHelp() { // No one should call this. The help text should be printed by the `onHelp` handler of `cmd @@ -816,6 +880,12 @@ public final class ArtShellCommand extends BasicShellCommandHandler { 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."); + pw.println(); + pw.println(" on-ota-staged --slot SLOT"); + pw.println(" Notifies ART Service that an OTA update is staged. A Pre-reboot Dexopt"); + pw.println(" job is scheduled for it."); + pw.println(" Options:"); + pw.println(" --slot SLOT The slot that contains the OTA update, '_a' or '_b'."); } private void enforceRootOrShell() { diff --git a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java index b8a55af7e0..08ff12c0b2 100644 --- a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java +++ b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java @@ -19,15 +19,29 @@ package com.android.server.art; import static com.android.server.art.model.ArtFlags.ScheduleStatus; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.job.JobInfo; import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.ComponentName; import android.content.Context; import android.os.Build; +import android.os.CancellationSignal; +import android.os.SystemProperties; +import android.provider.DeviceConfig; +import android.util.Log; +import android.util.Slog; import androidx.annotation.RequiresApi; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.ArtServiceJobInterface; +import com.android.server.art.prereboot.PreRebootDriver; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; /** * The Pre-reboot Dexopt job. @@ -50,6 +64,13 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { @NonNull private final Injector mInjector; + // Job state variables. + @GuardedBy("this") @Nullable private CompletableFuture<Void> mRunningJob = null; + @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null; + + /** The slot that contains the OTA update, "_a" or "_b", or null for a Mainline update. */ + @GuardedBy("this") @Nullable private String mOtaSlot = null; + public PreRebootDexoptJob(@NonNull Context context) { this(new Injector(context)); } @@ -62,19 +83,134 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { @Override public boolean onStartJob( @NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) { + // No need to handle exceptions thrown by the future because exceptions are handled inside + // the future itself. + var unused = start().thenRunAsync(() -> { + try { + // If it failed, it means something went wrong, so we don't reschedule the job + // because it will likely fail again. If it's cancelled, the job will be rescheduled + // because the return value of `onStopJob` will be respected, and this call will be + // ignored. + jobService.jobFinished(params, false /* wantsReschedule */); + } catch (RuntimeException e) { + Slog.wtf(TAG, "Unexpected exception", e); + } + }); // "true" means the job will continue running until `jobFinished` is called. - return false; + return true; } @Override public boolean onStopJob(@NonNull JobParameters params) { - // "true" means to execute again in the same interval with the default retry policy. + cancel(false /* blocking */); + // "true" means to execute again with the default retry policy. return true; } public @ScheduleStatus int schedule() { - // TODO(b/311377497): Schedule the job. - return ArtFlags.SCHEDULE_SUCCESS; + if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { + throw new IllegalStateException("This job cannot be scheduled"); + } + + if (!SystemProperties.getBoolean("dalvik.vm.enable_pr_dexopt", false /* def */) + && !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_RUNTIME, "enable_pr_dexopt", + false /* defaultValue */)) { + Log.i(TAG, "Pre-reboot Dexopt Job is not enabled by system property"); + return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; + } + + // If `pm.dexopt.disable_bg_dexopt` is set, the user probably means to disable any dexopt + // jobs in the background. + if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) { + Log.i(TAG, + "Pre-reboot Dexopt Job is disabled by system property " + + "'pm.dexopt.disable_bg_dexopt'"); + return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; + } + + JobInfo info = new JobInfo + .Builder(JOB_ID, + new ComponentName(JOB_PKG_NAME, + BackgroundDexoptJobService.class.getName())) + .setRequiresDeviceIdle(true) + .setRequiresCharging(true) + .setRequiresBatteryNotLow(true) + .build(); + + /* @JobScheduler.Result */ int result = mInjector.getJobScheduler().schedule(info); + if (result == JobScheduler.RESULT_SUCCESS) { + Log.i(TAG, "Pre-reboot Dexopt Job scheduled"); + return ArtFlags.SCHEDULE_SUCCESS; + } else { + Log.i(TAG, "Failed to schedule Pre-reboot Dexopt Job"); + return ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE; + } + } + + public void unschedule() { + if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { + throw new IllegalStateException("This job cannot be unscheduled"); + } + + mInjector.getJobScheduler().cancel(JOB_ID); + } + + @NonNull + public synchronized CompletableFuture<Void> start() { + if (mRunningJob != null) { + Log.i(TAG, "Job is already running"); + return mRunningJob; + } + + String otaSlot = mOtaSlot; + var cancellationSignal = mCancellationSignal = new CancellationSignal(); + mRunningJob = new CompletableFuture().runAsync(() -> { + try { + // TODO(b/336239721): Consume the result and report metrics. + mInjector.getPreRebootDriver().run(otaSlot, cancellationSignal); + } catch (RuntimeException e) { + Log.e(TAG, "Fatal error", e); + } finally { + synchronized (this) { + mRunningJob = null; + mCancellationSignal = null; + } + } + }); + return mRunningJob; + } + + /** + * Cancels the job. + * + * @param blocking whether to wait for the job to exit. + */ + public void cancel(boolean blocking) { + CompletableFuture<Void> runningJob = null; + synchronized (this) { + if (mRunningJob == null) { + return; + } + + mCancellationSignal.cancel(); + Log.i(TAG, "Job cancelled"); + runningJob = mRunningJob; + } + if (blocking) { + Utils.getFuture(runningJob); + } + } + + public synchronized void updateOtaSlot(@NonNull String value) { + Utils.check(value == null || value.equals("_a") || value.equals("_b")); + // It's not possible that this method is called with two different slots. + Utils.check(mOtaSlot == null || value == null || Objects.equals(mOtaSlot, value)); + // An OTA update has a higher priority than a Mainline update. When there are both a pending + // OTA update and a pending Mainline update, the system discards the Mainline update on the + // reboot. + if (mOtaSlot == null && value != null) { + mOtaSlot = value; + } } /** @@ -89,5 +225,15 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface { Injector(@NonNull Context context) { mContext = context; } + + @NonNull + public JobScheduler getJobScheduler() { + return Objects.requireNonNull(mContext.getSystemService(JobScheduler.class)); + } + + @NonNull + public PreRebootDriver getPreRebootDriver() { + return new PreRebootDriver(mContext); + } } } diff --git a/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java b/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java new file mode 100644 index 0000000000..bc30d2c058 --- /dev/null +++ b/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java @@ -0,0 +1,256 @@ +/* + * 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; + +import static com.android.server.art.PreRebootDexoptJob.JOB_ID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.os.CancellationSignal; +import android.os.SystemProperties; +import android.provider.DeviceConfig; + +import androidx.test.filters.SmallTest; + +import com.android.server.art.model.ArtFlags; +import com.android.server.art.prereboot.PreRebootDriver; +import com.android.server.art.testing.StaticMockitoRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +@SmallTest +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class PreRebootDexoptJobTest { + private static final long TIMEOUT_SEC = 10; + + @Rule + public StaticMockitoRule mockitoRule = new StaticMockitoRule( + SystemProperties.class, BackgroundDexoptJobService.class, DeviceConfig.class); + + @Mock private PreRebootDexoptJob.Injector mInjector; + @Mock private JobScheduler mJobScheduler; + @Mock private PreRebootDriver mPreRebootDriver; + private PreRebootDexoptJob mPreRebootDexoptJob; + + @Before + public void setUp() throws Exception { + // By default, the job is enabled by a build-time flag. + lenient() + .when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean())) + .thenReturn(false); + lenient() + .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) + .thenReturn(true); + lenient() + .when(DeviceConfig.getBoolean( + eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean())) + .thenReturn(false); + + lenient().when(mInjector.getJobScheduler()).thenReturn(mJobScheduler); + lenient().when(mInjector.getPreRebootDriver()).thenReturn(mPreRebootDriver); + + mPreRebootDexoptJob = new PreRebootDexoptJob(mInjector); + lenient().when(BackgroundDexoptJobService.getJob(JOB_ID)).thenReturn(mPreRebootDexoptJob); + } + + @Test + public void testSchedule() throws Exception { + var captor = ArgumentCaptor.forClass(JobInfo.class); + when(mJobScheduler.schedule(captor.capture())).thenReturn(JobScheduler.RESULT_SUCCESS); + + assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS); + + JobInfo jobInfo = captor.getValue(); + assertThat(jobInfo.isPeriodic()).isFalse(); + assertThat(jobInfo.isRequireDeviceIdle()).isTrue(); + assertThat(jobInfo.isRequireCharging()).isTrue(); + assertThat(jobInfo.isRequireBatteryNotLow()).isTrue(); + } + + @Test + public void testScheduleDisabled() { + when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean())) + .thenReturn(true); + + assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); + + verify(mJobScheduler, never()).schedule(any()); + } + + @Test + public void testScheduleNotEnabled() { + when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) + .thenReturn(false); + + assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP); + + verify(mJobScheduler, never()).schedule(any()); + } + + @Test + public void testScheduleEnabledByPhenotypeFlag() { + lenient() + .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean())) + .thenReturn(false); + lenient() + .when(DeviceConfig.getBoolean( + eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean())) + .thenReturn(true); + when(mJobScheduler.schedule(any())).thenReturn(JobScheduler.RESULT_SUCCESS); + + assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS); + + verify(mJobScheduler).schedule(any()); + } + + @Test + public void testUnschedule() { + mPreRebootDexoptJob.unschedule(); + verify(mJobScheduler).cancel(JOB_ID); + } + + @Test + public void testStart() { + when(mPreRebootDriver.run(any(), any())).thenReturn(true); + + Utils.getFuture(mPreRebootDexoptJob.start()); + } + + @Test + public void testStartAlreadyRunning() { + Semaphore dexoptDone = new Semaphore(0); + when(mPreRebootDriver.run(any(), any())).thenAnswer(invocation -> { + assertThat(dexoptDone.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); + return true; + }); + + Future<Void> future1 = mPreRebootDexoptJob.start(); + Future<Void> future2 = mPreRebootDexoptJob.start(); + assertThat(future1).isSameInstanceAs(future2); + + dexoptDone.release(); + Utils.getFuture(future1); + + verify(mPreRebootDriver, times(1)).run(any(), any()); + } + + @Test + public void testStartAnother() { + when(mPreRebootDriver.run(any(), any())).thenReturn(true); + + Future<Void> future1 = mPreRebootDexoptJob.start(); + Utils.getFuture(future1); + Future<Void> future2 = mPreRebootDexoptJob.start(); + Utils.getFuture(future2); + assertThat(future1).isNotSameInstanceAs(future2); + } + + @Test + public void testCancel() { + Semaphore dexoptCancelled = new Semaphore(0); + Semaphore jobExited = new Semaphore(0); + when(mPreRebootDriver.run(any(), any())).thenAnswer(invocation -> { + assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue(); + var cancellationSignal = invocation.<CancellationSignal>getArgument(1); + assertThat(cancellationSignal.isCanceled()).isTrue(); + jobExited.release(); + return true; + }); + + var unused = mPreRebootDexoptJob.start(); + Future<Void> future = new CompletableFuture().runAsync( + () -> { mPreRebootDexoptJob.cancel(true /* blocking */); }); + dexoptCancelled.release(); + Utils.getFuture(future); + // Check that `cancel` is really blocking. + assertThat(jobExited.tryAcquire()).isTrue(); + } + + @Test + public void testUpdateOtaSlotOtaThenMainline() { + mPreRebootDexoptJob.updateOtaSlot("_b"); + mPreRebootDexoptJob.updateOtaSlot(null); + + when(mPreRebootDriver.run(eq("_b"), any())).thenReturn(true); + + Utils.getFuture(mPreRebootDexoptJob.start()); + } + + @Test + public void testUpdateOtaSlotMainlineThenOta() { + mPreRebootDexoptJob.updateOtaSlot(null); + mPreRebootDexoptJob.updateOtaSlot("_a"); + + when(mPreRebootDriver.run(eq("_a"), any())).thenReturn(true); + + Utils.getFuture(mPreRebootDexoptJob.start()); + } + + @Test + public void testUpdateOtaSlotMainlineThenMainline() { + mPreRebootDexoptJob.updateOtaSlot(null); + mPreRebootDexoptJob.updateOtaSlot(null); + + when(mPreRebootDriver.run(isNull(), any())).thenReturn(true); + + Utils.getFuture(mPreRebootDexoptJob.start()); + } + + @Test + public void testUpdateOtaSlotOtaThenOta() { + mPreRebootDexoptJob.updateOtaSlot("_b"); + mPreRebootDexoptJob.updateOtaSlot("_b"); + + when(mPreRebootDriver.run(eq("_b"), any())).thenReturn(true); + + Utils.getFuture(mPreRebootDexoptJob.start()); + } + + @Test(expected = IllegalStateException.class) + public void testUpdateOtaSlotOtaThenOtaDifferentSlots() { + mPreRebootDexoptJob.updateOtaSlot("_b"); + mPreRebootDexoptJob.updateOtaSlot("_a"); + } + + @Test(expected = IllegalStateException.class) + public void testUpdateOtaSlotOtaBogusSlot() { + mPreRebootDexoptJob.updateOtaSlot("_bogus"); + } +} |