• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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;
18 
19 import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE;
20 import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES;
21 import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES;
22 import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
23 
24 import android.Manifest;
25 import android.annotation.MainThread;
26 import android.annotation.Nullable;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.ServiceConnection;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ResolveInfo;
33 import android.content.pm.ServiceInfo;
34 import android.os.IBinder;
35 import android.os.RemoteCallback;
36 import android.os.RemoteException;
37 import android.os.UserHandle;
38 import android.service.watchdog.ExplicitHealthCheckService;
39 import android.service.watchdog.IExplicitHealthCheckService;
40 import android.text.TextUtils;
41 import android.util.ArraySet;
42 import android.util.Slog;
43 
44 import com.android.internal.annotations.GuardedBy;
45 
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.Iterator;
49 import java.util.List;
50 import java.util.Objects;
51 import java.util.Set;
52 import java.util.function.Consumer;
53 
54 // TODO(b/120598832): Add tests
55 /**
56  * Controls the connections with {@link ExplicitHealthCheckService}.
57  */
58 class ExplicitHealthCheckController {
59     private static final String TAG = "ExplicitHealthCheckController";
60     private final Object mLock = new Object();
61     private final Context mContext;
62 
63     // Called everytime a package passes the health check, so the watchdog is notified of the
64     // passing check. In practice, should never be null after it has been #setEnabled.
65     // To prevent deadlocks between the controller and watchdog threads, we have
66     // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
67     // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer.
68     @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer;
69     // Called everytime after a successful #syncRequest call, so the watchdog can receive packages
70     // supporting health checks and update its internal state. In practice, should never be null
71     // after it has been #setEnabled.
72     // To prevent deadlocks between the controller and watchdog threads, we have
73     // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
74     // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer.
75     @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer;
76     // Called everytime we need to notify the watchdog to sync requests between itself and the
77     // health check service. In practice, should never be null after it has been #setEnabled.
78     // To prevent deadlocks between the controller and watchdog threads, we have
79     // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
80     // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable.
81     @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable;
82     // Actual binder object to the explicit health check service.
83     @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService;
84     // Connection to the explicit health check service, necessary to unbind.
85     // We should only try to bind if mConnection is null, non-null indicates we
86     // are connected or at least connecting.
87     @GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
88     // Bind state of the explicit health check service.
89     @GuardedBy("mLock") private boolean mEnabled;
90 
ExplicitHealthCheckController(Context context)91     ExplicitHealthCheckController(Context context) {
92         mContext = context;
93     }
94 
95     /** Enables or disables explicit health checks. */
setEnabled(boolean enabled)96     public void setEnabled(boolean enabled) {
97         synchronized (mLock) {
98             Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled."));
99             mEnabled = enabled;
100         }
101     }
102 
103     /**
104      * Sets callbacks to listen to important events from the controller.
105      *
106      * <p> Should be called once at initialization before any other calls to the controller to
107      * ensure a happens-before relationship of the set parameters and visibility on other threads.
108      */
setCallbacks(Consumer<String> passedConsumer, Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable)109     public void setCallbacks(Consumer<String> passedConsumer,
110             Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) {
111         synchronized (mLock) {
112             if (mPassedConsumer != null || mSupportedConsumer != null
113                     || mNotifySyncRunnable != null) {
114                 Slog.wtf(TAG, "Resetting health check controller callbacks");
115             }
116 
117             mPassedConsumer = Objects.requireNonNull(passedConsumer);
118             mSupportedConsumer = Objects.requireNonNull(supportedConsumer);
119             mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable);
120         }
121     }
122 
123     /**
124      * Calls the health check service to request or cancel packages based on
125      * {@code newRequestedPackages}.
126      *
127      * <p> Supported packages in {@code newRequestedPackages} that have not been previously
128      * requested will be requested while supported packages not in {@code newRequestedPackages}
129      * but were previously requested will be cancelled.
130      *
131      * <p> This handles binding and unbinding to the health check service as required.
132      *
133      * <p> Note, calling this may modify {@code newRequestedPackages}.
134      *
135      * <p> Note, this method is not thread safe, all calls should be serialized.
136      */
syncRequests(Set<String> newRequestedPackages)137     public void syncRequests(Set<String> newRequestedPackages) {
138         boolean enabled;
139         synchronized (mLock) {
140             enabled = mEnabled;
141         }
142 
143         if (!enabled) {
144             Slog.i(TAG, "Health checks disabled, no supported packages");
145             // Call outside lock
146             mSupportedConsumer.accept(Collections.emptyList());
147             return;
148         }
149 
150         getSupportedPackages(supportedPackageConfigs -> {
151             // Notify the watchdog without lock held
152             mSupportedConsumer.accept(supportedPackageConfigs);
153             getRequestedPackages(previousRequestedPackages -> {
154                 synchronized (mLock) {
155                     // Hold lock so requests and cancellations are sent atomically.
156                     // It is important we don't mix requests from multiple threads.
157 
158                     Set<String> supportedPackages = new ArraySet<>();
159                     for (PackageConfig config : supportedPackageConfigs) {
160                         supportedPackages.add(config.getPackageName());
161                     }
162                     // Note, this may modify newRequestedPackages
163                     newRequestedPackages.retainAll(supportedPackages);
164 
165                     // Cancel packages no longer requested
166                     actOnDifference(previousRequestedPackages,
167                             newRequestedPackages, p -> cancel(p));
168                     // Request packages not yet requested
169                     actOnDifference(newRequestedPackages,
170                             previousRequestedPackages, p -> request(p));
171 
172                     if (newRequestedPackages.isEmpty()) {
173                         Slog.i(TAG, "No more health check requests, unbinding...");
174                         unbindService();
175                         return;
176                     }
177                 }
178             });
179         });
180     }
181 
actOnDifference(Collection<String> collection1, Collection<String> collection2, Consumer<String> action)182     private void actOnDifference(Collection<String> collection1, Collection<String> collection2,
183             Consumer<String> action) {
184         Iterator<String> iterator = collection1.iterator();
185         while (iterator.hasNext()) {
186             String packageName = iterator.next();
187             if (!collection2.contains(packageName)) {
188                 action.accept(packageName);
189             }
190         }
191     }
192 
193     /**
194      * Requests an explicit health check for {@code packageName}.
195      * After this request, the callback registered on {@link #setCallbacks} can receive explicit
196      * health check passed results.
197      */
request(String packageName)198     private void request(String packageName) {
199         synchronized (mLock) {
200             if (!prepareServiceLocked("request health check for " + packageName)) {
201                 return;
202             }
203 
204             Slog.i(TAG, "Requesting health check for package " + packageName);
205             try {
206                 mRemoteService.request(packageName);
207             } catch (RemoteException e) {
208                 Slog.w(TAG, "Failed to request health check for package " + packageName, e);
209             }
210         }
211     }
212 
213     /**
214      * Cancels all explicit health checks for {@code packageName}.
215      * After this request, the callback registered on {@link #setCallbacks} can no longer receive
216      * explicit health check passed results.
217      */
cancel(String packageName)218     private void cancel(String packageName) {
219         synchronized (mLock) {
220             if (!prepareServiceLocked("cancel health check for " + packageName)) {
221                 return;
222             }
223 
224             Slog.i(TAG, "Cancelling health check for package " + packageName);
225             try {
226                 mRemoteService.cancel(packageName);
227             } catch (RemoteException e) {
228                 // Do nothing, if the service is down, when it comes up, we will sync requests,
229                 // if there's some other error, retrying wouldn't fix anyways.
230                 Slog.w(TAG, "Failed to cancel health check for package " + packageName, e);
231             }
232         }
233     }
234 
235     /**
236      * Returns the packages that we can request explicit health checks for.
237      * The packages will be returned to the {@code consumer}.
238      */
getSupportedPackages(Consumer<List<PackageConfig>> consumer)239     private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) {
240         synchronized (mLock) {
241             if (!prepareServiceLocked("get health check supported packages")) {
242                 return;
243             }
244 
245             Slog.d(TAG, "Getting health check supported packages");
246             try {
247                 mRemoteService.getSupportedPackages(new RemoteCallback(result -> {
248                     List<PackageConfig> packages =
249                             result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class);
250                     Slog.i(TAG, "Explicit health check supported packages " + packages);
251                     consumer.accept(packages);
252                 }));
253             } catch (RemoteException e) {
254                 // Request failed, treat as if all observed packages are supported, if any packages
255                 // expire during this period, we may incorrectly treat it as failing health checks
256                 // even if we don't support health checks for the package.
257                 Slog.w(TAG, "Failed to get health check supported packages", e);
258             }
259         }
260     }
261 
262     /**
263      * Returns the packages for which health checks are currently in progress.
264      * The packages will be returned to the {@code consumer}.
265      */
getRequestedPackages(Consumer<List<String>> consumer)266     private void getRequestedPackages(Consumer<List<String>> consumer) {
267         synchronized (mLock) {
268             if (!prepareServiceLocked("get health check requested packages")) {
269                 return;
270             }
271 
272             Slog.d(TAG, "Getting health check requested packages");
273             try {
274                 mRemoteService.getRequestedPackages(new RemoteCallback(result -> {
275                     List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES);
276                     Slog.i(TAG, "Explicit health check requested packages " + packages);
277                     consumer.accept(packages);
278                 }));
279             } catch (RemoteException e) {
280                 // Request failed, treat as if we haven't requested any packages, if any packages
281                 // were actually requested, they will not be cancelled now. May be cancelled later
282                 Slog.w(TAG, "Failed to get health check requested packages", e);
283             }
284         }
285     }
286 
287     /**
288      * Binds to the explicit health check service if the controller is enabled and
289      * not already bound.
290      */
bindService()291     private void bindService() {
292         synchronized (mLock) {
293             if (!mEnabled || mConnection != null || mRemoteService != null) {
294                 if (!mEnabled) {
295                     Slog.i(TAG, "Not binding to service, service disabled");
296                 } else if (mRemoteService != null) {
297                     Slog.i(TAG, "Not binding to service, service already connected");
298                 } else {
299                     Slog.i(TAG, "Not binding to service, service already connecting");
300                 }
301                 return;
302             }
303             ComponentName component = getServiceComponentNameLocked();
304             if (component == null) {
305                 Slog.wtf(TAG, "Explicit health check service not found");
306                 return;
307             }
308 
309             Intent intent = new Intent();
310             intent.setComponent(component);
311             mConnection = new ServiceConnection() {
312                 @Override
313                 public void onServiceConnected(ComponentName name, IBinder service) {
314                     Slog.i(TAG, "Explicit health check service is connected " + name);
315                     initState(service);
316                 }
317 
318                 @Override
319                 @MainThread
320                 public void onServiceDisconnected(ComponentName name) {
321                     // Service crashed or process was killed, #onServiceConnected will be called.
322                     // Don't need to re-bind.
323                     Slog.i(TAG, "Explicit health check service is disconnected " + name);
324                     synchronized (mLock) {
325                         mRemoteService = null;
326                     }
327                 }
328 
329                 @Override
330                 public void onBindingDied(ComponentName name) {
331                     // Application hosting service probably got updated
332                     // Need to re-bind.
333                     Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name);
334                     unbindService();
335                     bindService();
336                 }
337 
338                 @Override
339                 public void onNullBinding(ComponentName name) {
340                     // Should never happen. Service returned null from #onBind.
341                     Slog.wtf(TAG, "Explicit health check service binding is null?? " + name);
342                 }
343             };
344 
345             mContext.bindServiceAsUser(intent, mConnection,
346                     Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
347             Slog.i(TAG, "Explicit health check service is bound");
348         }
349     }
350 
351     /** Unbinds the explicit health check service. */
unbindService()352     private void unbindService() {
353         synchronized (mLock) {
354             if (mRemoteService != null) {
355                 mContext.unbindService(mConnection);
356                 mRemoteService = null;
357                 mConnection = null;
358             }
359             Slog.i(TAG, "Explicit health check service is unbound");
360         }
361     }
362 
363     @GuardedBy("mLock")
364     @Nullable
getServiceInfoLocked()365     private ServiceInfo getServiceInfoLocked() {
366         final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE);
367         final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
368                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA
369                         |  PackageManager.MATCH_SYSTEM_ONLY);
370         if (resolveInfo == null || resolveInfo.serviceInfo == null) {
371             Slog.w(TAG, "No valid components found.");
372             return null;
373         }
374         return resolveInfo.serviceInfo;
375     }
376 
377     @GuardedBy("mLock")
378     @Nullable
getServiceComponentNameLocked()379     private ComponentName getServiceComponentNameLocked() {
380         final ServiceInfo serviceInfo = getServiceInfoLocked();
381         if (serviceInfo == null) {
382             return null;
383         }
384 
385         final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
386         if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE
387                 .equals(serviceInfo.permission)) {
388             Slog.w(TAG, name.flattenToShortString() + " does not require permission "
389                     + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE);
390             return null;
391         }
392         return name;
393     }
394 
initState(IBinder service)395     private void initState(IBinder service) {
396         synchronized (mLock) {
397             if (!mEnabled) {
398                 Slog.w(TAG, "Attempting to connect disabled service?? Unbinding...");
399                 // Very unlikely, but we disabled the service after binding but before we connected
400                 unbindService();
401                 return;
402             }
403             mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
404             try {
405                 mRemoteService.setCallback(new RemoteCallback(result -> {
406                     String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
407                     if (!TextUtils.isEmpty(packageName)) {
408                         if (mPassedConsumer == null) {
409                             Slog.wtf(TAG, "Health check passed for package " + packageName
410                                     + "but no consumer registered.");
411                         } else {
412                             // Call without lock held
413                             mPassedConsumer.accept(packageName);
414                         }
415                     } else {
416                         Slog.wtf(TAG, "Empty package passed explicit health check?");
417                     }
418                 }));
419                 Slog.i(TAG, "Service initialized, syncing requests");
420             } catch (RemoteException e) {
421                 Slog.wtf(TAG, "Could not setCallback on explicit health check service");
422             }
423         }
424         // Calling outside lock
425         mNotifySyncRunnable.run();
426     }
427 
428     /**
429      * Prepares the health check service to receive requests.
430      *
431      * @return {@code true} if it is ready and we can proceed with a request,
432      * {@code false} otherwise. If it is not ready, and the service is enabled,
433      * we will bind and the request should be automatically attempted later.
434      */
435     @GuardedBy("mLock")
prepareServiceLocked(String action)436     private boolean prepareServiceLocked(String action) {
437         if (mRemoteService != null && mEnabled) {
438             return true;
439         }
440         Slog.i(TAG, "Service not ready to " + action
441                 + (mEnabled ? ". Binding..." : ". Disabled"));
442         if (mEnabled) {
443             bindService();
444         }
445         return false;
446     }
447 }
448