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