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 = scheduler.schedule(new JobInfo.Builder(STAGED_APEX_JOB_ID, serviceName) 56 // Wait in case more APEXes are staged 57 .setMinimumLatency(TimeUnit.MINUTES.toMillis(60)) 58 // We consume CPU, power, and storage 59 .setRequiresDeviceIdle(true) 60 .setRequiresCharging(true) 61 .setRequiresStorageNotLow(true) 62 .build()); 63 if (result == JobScheduler.RESULT_SUCCESS) { 64 IsolatedCompilationMetrics.onCompilationScheduled( 65 IsolatedCompilationMetrics.SCHEDULING_SUCCESS); 66 } else { 67 IsolatedCompilationMetrics.onCompilationScheduled( 68 IsolatedCompilationMetrics.SCHEDULING_FAILURE); 69 Log.e(TAG, "Failed to schedule staged APEX job"); 70 } 71 } 72 isStagedApexJobScheduled(JobScheduler scheduler)73 static boolean isStagedApexJobScheduled(JobScheduler scheduler) { 74 return scheduler.getPendingJob(STAGED_APEX_JOB_ID) != null; 75 } 76 77 @Override onStartJob(JobParameters params)78 public boolean onStartJob(JobParameters params) { 79 Log.i(TAG, "Starting job"); 80 81 // This function (and onStopJob) are only ever called on the main thread, so we don't have 82 // to worry about two starts at once, or start and stop happening at once. But onCompletion 83 // can be called on any thread, so we need to be careful with that. 84 85 CompilationJob oldJob = mCurrentJob.get(); 86 if (oldJob != null) { 87 // We're already running a job, give up on this one 88 Log.w(TAG, "Another job is in progress, skipping"); 89 return false; // Already finished 90 } 91 92 IsolatedCompilationMetrics metrics = new IsolatedCompilationMetrics(); 93 94 CompilationJob newJob = new CompilationJob(IsolatedCompilationJobService.this::onCompletion, 95 params, metrics); 96 mCurrentJob.set(newJob); 97 98 // This can take some time - we need to start up a VM - so we do it on a separate 99 // thread. This thread exits as soon as the compilation Task has been started (or 100 // there's a failure), and then compilation continues in composd and the VM. 101 new Thread("IsolatedCompilationJob_starter") { 102 @Override 103 public void run() { 104 try { 105 newJob.start(); 106 } catch (RuntimeException e) { 107 Log.e(TAG, "Starting CompilationJob failed", e); 108 metrics.onCompilationEnded(IsolatedCompilationMetrics.RESULT_FAILED_TO_START); 109 mCurrentJob.set(null); 110 newJob.stop(); // Just in case it managed to start before failure 111 jobFinished(params, /*wantReschedule=*/ false); 112 } 113 } 114 }.start(); 115 return true; // Job is running in the background 116 } 117 118 @Override onStopJob(JobParameters params)119 public boolean onStopJob(JobParameters params) { 120 CompilationJob job = mCurrentJob.getAndSet(null); 121 if (job == null) { 122 return false; // No need to reschedule, we'd finished 123 } else { 124 job.stop(); 125 return true; // We didn't get to finish, please re-schedule 126 } 127 } 128 onCompletion(JobParameters params, boolean succeeded)129 void onCompletion(JobParameters params, boolean succeeded) { 130 Log.i(TAG, "onCompletion, succeeded=" + succeeded); 131 132 CompilationJob job = mCurrentJob.getAndSet(null); 133 if (job == null) { 134 // No need to call jobFinished if we've been told to stop. 135 return; 136 } 137 // On success we don't need to reschedule. 138 // On failure we could reschedule, but that could just use a lot of resources and still 139 // fail; instead we just let odsign do compilation on reboot if necessary. 140 jobFinished(params, /*wantReschedule=*/ false); 141 } 142 143 interface CompilationCallback { onCompletion(JobParameters params, boolean succeeded)144 void onCompletion(JobParameters params, boolean succeeded); 145 } 146 147 static class CompilationJob extends ICompilationTaskCallback.Stub 148 implements IBinder.DeathRecipient { 149 private final IsolatedCompilationMetrics mMetrics; 150 private final AtomicReference<ICompilationTask> mTask = new AtomicReference<>(); 151 private final CompilationCallback mCallback; 152 private final JobParameters mParams; 153 private volatile boolean mStopRequested = false; 154 CompilationJob(CompilationCallback callback, JobParameters params, IsolatedCompilationMetrics metrics)155 CompilationJob(CompilationCallback callback, JobParameters params, 156 IsolatedCompilationMetrics metrics) { 157 mCallback = requireNonNull(callback); 158 mParams = params; 159 mMetrics = requireNonNull(metrics); 160 } 161 start()162 void start() { 163 IBinder binder = ServiceManager.waitForService("android.system.composd"); 164 IIsolatedCompilationService composd = 165 IIsolatedCompilationService.Stub.asInterface(binder); 166 167 if (composd == null) { 168 throw new IllegalStateException("Unable to find composd service"); 169 } 170 171 try { 172 ICompilationTask composTask = composd.startStagedApexCompile(this); 173 mMetrics.onCompilationStarted(); 174 mTask.set(composTask); 175 composTask.asBinder().linkToDeath(this, 0); 176 } catch (RemoteException e) { 177 throw e.rethrowAsRuntimeException(); 178 } 179 180 if (mStopRequested) { 181 // We were asked to stop while we were starting the task. We need to 182 // cancel it now, since we couldn't before. 183 cancelTask(); 184 } 185 } 186 stop()187 void stop() { 188 mStopRequested = true; 189 cancelTask(); 190 } 191 cancelTask()192 private void cancelTask() { 193 ICompilationTask task = mTask.getAndSet(null); 194 if (task == null) { 195 return; 196 } 197 198 Log.i(TAG, "Cancelling task"); 199 try { 200 task.cancel(); 201 } catch (RuntimeException | RemoteException e) { 202 // If canceling failed we'll assume it means that the task has already failed; 203 // there's nothing else we can do anyway. 204 Log.w(TAG, "Failed to cancel CompilationTask", e); 205 } 206 207 mMetrics.onCompilationJobCanceled(mParams.getStopReason()); 208 try { 209 task.asBinder().unlinkToDeath(this, 0); 210 } catch (NoSuchElementException e) { 211 // Harmless 212 } 213 } 214 215 @Override binderDied()216 public void binderDied() { 217 onCompletion(false, IsolatedCompilationMetrics.RESULT_COMPOSD_DIED); 218 } 219 220 @Override onSuccess()221 public void onSuccess() { 222 onCompletion(true, IsolatedCompilationMetrics.RESULT_SUCCESS); 223 } 224 225 @Override onFailure(byte reason, String message)226 public void onFailure(byte reason, String message) { 227 int result; 228 switch (reason) { 229 case ICompilationTaskCallback.FailureReason.CompilationFailed: 230 result = IsolatedCompilationMetrics.RESULT_COMPILATION_FAILED; 231 break; 232 233 case ICompilationTaskCallback.FailureReason.UnexpectedCompilationResult: 234 result = IsolatedCompilationMetrics.RESULT_UNEXPECTED_COMPILATION_RESULT; 235 break; 236 237 case ICompilationTaskCallback.FailureReason.FailedToEnableFsverity: 238 result = IsolatedCompilationMetrics.RESULT_FAILED_TO_ENABLE_FSVERITY; 239 break; 240 241 default: 242 result = IsolatedCompilationMetrics.RESULT_UNKNOWN_FAILURE; 243 break; 244 } 245 Log.w(TAG, "Compilation failed: " + message); 246 onCompletion(false, result); 247 } 248 onCompletion(boolean succeeded, @CompilationResult int result)249 private void onCompletion(boolean succeeded, @CompilationResult int result) { 250 ICompilationTask task = mTask.getAndSet(null); 251 if (task != null) { 252 mMetrics.onCompilationEnded(result); 253 mCallback.onCompletion(mParams, succeeded); 254 try { 255 task.asBinder().unlinkToDeath(this, 0); 256 } catch (NoSuchElementException e) { 257 // Harmless 258 } 259 } 260 } 261 } 262 } 263