1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.compos; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.app.job.JobInfo; 22 import android.app.job.JobParameters; 23 import android.app.job.JobScheduler; 24 import android.app.job.JobService; 25 import android.content.ComponentName; 26 import android.os.IBinder; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.system.composd.ICompilationTask; 30 import android.system.composd.ICompilationTaskCallback; 31 import android.system.composd.IIsolatedCompilationService; 32 import android.util.Log; 33 34 import com.android.server.compos.IsolatedCompilationMetrics.CompilationResult; 35 36 import java.util.NoSuchElementException; 37 import java.util.concurrent.TimeUnit; 38 import java.util.concurrent.atomic.AtomicReference; 39 40 /** 41 * A job scheduler service responsible for performing Isolated Compilation when scheduled. 42 * 43 * @hide 44 */ 45 public class IsolatedCompilationJobService extends JobService { 46 private static final String TAG = IsolatedCompilationJobService.class.getName(); 47 private static final int STAGED_APEX_JOB_ID = 5132251; 48 49 private final AtomicReference<CompilationJob> mCurrentJob = new AtomicReference<>(); 50 scheduleStagedApexJob(JobScheduler scheduler)51 static void scheduleStagedApexJob(JobScheduler scheduler) { 52 ComponentName serviceName = 53 new ComponentName("android", IsolatedCompilationJobService.class.getName()); 54 55 int result = 56 scheduler.schedule( 57 new JobInfo.Builder(STAGED_APEX_JOB_ID, serviceName) 58 // Wait in case more APEXes are staged 59 .setMinimumLatency(TimeUnit.MINUTES.toMillis(60)) 60 // We consume CPU, power, and storage 61 .setRequiresDeviceIdle(true) 62 .setRequiresCharging(true) 63 .setRequiresStorageNotLow(true) 64 .build()); 65 if (result == JobScheduler.RESULT_SUCCESS) { 66 IsolatedCompilationMetrics.onCompilationScheduled( 67 IsolatedCompilationMetrics.SCHEDULING_SUCCESS); 68 } else { 69 IsolatedCompilationMetrics.onCompilationScheduled( 70 IsolatedCompilationMetrics.SCHEDULING_FAILURE); 71 Log.e(TAG, "Failed to schedule staged APEX job"); 72 } 73 } 74 isStagedApexJobScheduled(JobScheduler scheduler)75 static boolean isStagedApexJobScheduled(JobScheduler scheduler) { 76 return scheduler.getPendingJob(STAGED_APEX_JOB_ID) != null; 77 } 78 79 @Override onStartJob(JobParameters params)80 public boolean onStartJob(JobParameters params) { 81 Log.i(TAG, "Starting job"); 82 83 // This function (and onStopJob) are only ever called on the main thread, so we don't have 84 // to worry about two starts at once, or start and stop happening at once. But onCompletion 85 // can be called on any thread, so we need to be careful with that. 86 87 CompilationJob oldJob = mCurrentJob.get(); 88 if (oldJob != null) { 89 // We're already running a job, give up on this one 90 Log.w(TAG, "Another job is in progress, skipping"); 91 return false; // Already finished 92 } 93 94 IsolatedCompilationMetrics metrics = new IsolatedCompilationMetrics(); 95 96 CompilationJob newJob = 97 new CompilationJob( 98 IsolatedCompilationJobService.this::onCompletion, params, metrics); 99 mCurrentJob.set(newJob); 100 101 // This can take some time - we need to start up a VM - so we do it on a separate 102 // thread. This thread exits as soon as the compilation Task has been started (or 103 // there's a failure), and then compilation continues in composd and the VM. 104 new Thread("IsolatedCompilationJob_starter") { 105 @Override 106 public void run() { 107 try { 108 newJob.start(); 109 } catch (RuntimeException e) { 110 Log.e(TAG, "Starting CompilationJob failed", e); 111 metrics.onCompilationEnded(IsolatedCompilationMetrics.RESULT_FAILED_TO_START); 112 mCurrentJob.set(null); 113 newJob.stop(); // Just in case it managed to start before failure 114 jobFinished(params, /* wantReschedule= */ false); 115 } 116 } 117 }.start(); 118 return true; // Job is running in the background 119 } 120 121 @Override onStopJob(JobParameters params)122 public boolean onStopJob(JobParameters params) { 123 CompilationJob job = mCurrentJob.getAndSet(null); 124 if (job == null) { 125 return false; // No need to reschedule, we'd finished 126 } else { 127 job.stop(); 128 return true; // We didn't get to finish, please re-schedule 129 } 130 } 131 onCompletion(JobParameters params, boolean succeeded)132 void onCompletion(JobParameters params, boolean succeeded) { 133 Log.i(TAG, "onCompletion, succeeded=" + succeeded); 134 135 CompilationJob job = mCurrentJob.getAndSet(null); 136 if (job == null) { 137 // No need to call jobFinished if we've been told to stop. 138 return; 139 } 140 // On success we don't need to reschedule. 141 // On failure we could reschedule, but that could just use a lot of resources and still 142 // fail; instead we just let odsign do compilation on reboot if necessary. 143 jobFinished(params, /* wantReschedule= */ false); 144 } 145 146 interface CompilationCallback { onCompletion(JobParameters params, boolean succeeded)147 void onCompletion(JobParameters params, boolean succeeded); 148 } 149 150 static class CompilationJob extends ICompilationTaskCallback.Stub 151 implements IBinder.DeathRecipient { 152 private final IsolatedCompilationMetrics mMetrics; 153 private final AtomicReference<ICompilationTask> mTask = new AtomicReference<>(); 154 private final CompilationCallback mCallback; 155 private final JobParameters mParams; 156 private volatile boolean mStopRequested = false; 157 CompilationJob( CompilationCallback callback, JobParameters params, IsolatedCompilationMetrics metrics)158 CompilationJob( 159 CompilationCallback callback, 160 JobParameters params, 161 IsolatedCompilationMetrics metrics) { 162 mCallback = requireNonNull(callback); 163 mParams = params; 164 mMetrics = requireNonNull(metrics); 165 } 166 start()167 void start() { 168 IBinder binder = ServiceManager.waitForService("android.system.composd"); 169 IIsolatedCompilationService composd = 170 IIsolatedCompilationService.Stub.asInterface(binder); 171 172 if (composd == null) { 173 throw new IllegalStateException("Unable to find composd service"); 174 } 175 176 try { 177 ICompilationTask composTask = composd.startStagedApexCompile(this, "microdroid"); 178 mMetrics.onCompilationStarted(); 179 mTask.set(composTask); 180 composTask.asBinder().linkToDeath(this, 0); 181 } catch (RemoteException e) { 182 throw e.rethrowAsRuntimeException(); 183 } 184 185 if (mStopRequested) { 186 // We were asked to stop while we were starting the task. We need to 187 // cancel it now, since we couldn't before. 188 cancelTask(); 189 } 190 } 191 stop()192 void stop() { 193 mStopRequested = true; 194 cancelTask(); 195 } 196 cancelTask()197 private void cancelTask() { 198 ICompilationTask task = mTask.getAndSet(null); 199 if (task == null) { 200 return; 201 } 202 203 Log.i(TAG, "Cancelling task"); 204 try { 205 task.cancel(); 206 } catch (RuntimeException | RemoteException e) { 207 // If canceling failed we'll assume it means that the task has already failed; 208 // there's nothing else we can do anyway. 209 Log.w(TAG, "Failed to cancel CompilationTask", e); 210 } 211 212 mMetrics.onCompilationJobCanceled(mParams.getStopReason()); 213 try { 214 task.asBinder().unlinkToDeath(this, 0); 215 } catch (NoSuchElementException e) { 216 // Harmless 217 } 218 } 219 220 @Override binderDied()221 public void binderDied() { 222 onCompletion(false, IsolatedCompilationMetrics.RESULT_COMPOSD_DIED); 223 } 224 225 @Override onSuccess()226 public void onSuccess() { 227 onCompletion(true, IsolatedCompilationMetrics.RESULT_SUCCESS); 228 } 229 230 @Override onFailure(byte reason, String message)231 public void onFailure(byte reason, String message) { 232 int result; 233 switch (reason) { 234 case ICompilationTaskCallback.FailureReason.CompilationFailed: 235 result = IsolatedCompilationMetrics.RESULT_COMPILATION_FAILED; 236 break; 237 238 case ICompilationTaskCallback.FailureReason.UnexpectedCompilationResult: 239 result = IsolatedCompilationMetrics.RESULT_UNEXPECTED_COMPILATION_RESULT; 240 break; 241 242 case ICompilationTaskCallback.FailureReason.FailedToEnableFsverity: 243 result = IsolatedCompilationMetrics.RESULT_FAILED_TO_ENABLE_FSVERITY; 244 break; 245 246 default: 247 result = IsolatedCompilationMetrics.RESULT_UNKNOWN_FAILURE; 248 break; 249 } 250 Log.w(TAG, "Compilation failed: " + message); 251 onCompletion(false, result); 252 } 253 onCompletion(boolean succeeded, @CompilationResult int result)254 private void onCompletion(boolean succeeded, @CompilationResult int result) { 255 ICompilationTask task = mTask.getAndSet(null); 256 if (task != null) { 257 mMetrics.onCompilationEnded(result); 258 mCallback.onCompletion(mParams, succeeded); 259 try { 260 task.asBinder().unlinkToDeath(this, 0); 261 } catch (NoSuchElementException e) { 262 // Harmless 263 } 264 } 265 } 266 } 267 } 268