diff options
Diffstat (limited to 'services/core/java/com/android/server/ExplicitHealthCheckController.java')
-rw-r--r-- | services/core/java/com/android/server/ExplicitHealthCheckController.java | 454 |
1 files changed, 454 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/ExplicitHealthCheckController.java b/services/core/java/com/android/server/ExplicitHealthCheckController.java new file mode 100644 index 000000000000..3d610d3747c9 --- /dev/null +++ b/services/core/java/com/android/server/ExplicitHealthCheckController.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2019 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; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES; +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; + +import android.Manifest; +import android.annotation.MainThread; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.watchdog.ExplicitHealthCheckService; +import android.service.watchdog.IExplicitHealthCheckService; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +// TODO(b/120598832): Add tests +/** + * Controls the connections with {@link ExplicitHealthCheckService}. + */ +class ExplicitHealthCheckController { + private static final String TAG = "ExplicitHealthCheckController"; + private final Object mLock = new Object(); + private final Context mContext; + + // Called everytime a package passes the health check, so the watchdog is notified of the + // passing check. In practice, should never be null after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. + @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer; + // Called everytime after a successful #syncRequest call, so the watchdog can receive packages + // supporting health checks and update its internal state. In practice, should never be null + // after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. + @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer; + // Called everytime we need to notify the watchdog to sync requests between itself and the + // health check service. In practice, should never be null after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable. + @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable; + // Actual binder object to the explicit health check service. + @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService; + // Connection to the explicit health check service, necessary to unbind. + // We should only try to bind if mConnection is null, non-null indicates we + // are connected or at least connecting. + @GuardedBy("mLock") @Nullable private ServiceConnection mConnection; + // Bind state of the explicit health check service. + @GuardedBy("mLock") private boolean mEnabled; + + ExplicitHealthCheckController(Context context) { + mContext = context; + } + + /** Enables or disables explicit health checks. */ + public void setEnabled(boolean enabled) { + synchronized (mLock) { + Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled.")); + mEnabled = enabled; + } + } + + /** + * Sets callbacks to listen to important events from the controller. + * + * <p> Should be called once at initialization before any other calls to the controller to + * ensure a happens-before relationship of the set parameters and visibility on other threads. + */ + public void setCallbacks(Consumer<String> passedConsumer, + Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { + synchronized (mLock) { + if (mPassedConsumer != null || mSupportedConsumer != null + || mNotifySyncRunnable != null) { + Slog.wtf(TAG, "Resetting health check controller callbacks"); + } + + mPassedConsumer = Objects.requireNonNull(passedConsumer); + mSupportedConsumer = Objects.requireNonNull(supportedConsumer); + mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable); + } + } + + /** + * Calls the health check service to request or cancel packages based on + * {@code newRequestedPackages}. + * + * <p> Supported packages in {@code newRequestedPackages} that have not been previously + * requested will be requested while supported packages not in {@code newRequestedPackages} + * but were previously requested will be cancelled. + * + * <p> This handles binding and unbinding to the health check service as required. + * + * <p> Note, calling this may modify {@code newRequestedPackages}. + * + * <p> Note, this method is not thread safe, all calls should be serialized. + */ + public void syncRequests(Set<String> newRequestedPackages) { + boolean enabled; + synchronized (mLock) { + enabled = mEnabled; + } + + if (!enabled) { + Slog.i(TAG, "Health checks disabled, no supported packages"); + // Call outside lock + mSupportedConsumer.accept(Collections.emptyList()); + return; + } + + getSupportedPackages(supportedPackageConfigs -> { + // Notify the watchdog without lock held + mSupportedConsumer.accept(supportedPackageConfigs); + getRequestedPackages(previousRequestedPackages -> { + synchronized (mLock) { + // Hold lock so requests and cancellations are sent atomically. + // It is important we don't mix requests from multiple threads. + + Set<String> supportedPackages = new ArraySet<>(); + for (PackageConfig config : supportedPackageConfigs) { + supportedPackages.add(config.getPackageName()); + } + // Note, this may modify newRequestedPackages + newRequestedPackages.retainAll(supportedPackages); + + // Cancel packages no longer requested + actOnDifference(previousRequestedPackages, + newRequestedPackages, p -> cancel(p)); + // Request packages not yet requested + actOnDifference(newRequestedPackages, + previousRequestedPackages, p -> request(p)); + + if (newRequestedPackages.isEmpty()) { + Slog.i(TAG, "No more health check requests, unbinding..."); + unbindService(); + return; + } + } + }); + }); + } + + private void actOnDifference(Collection<String> collection1, Collection<String> collection2, + Consumer<String> action) { + Iterator<String> iterator = collection1.iterator(); + while (iterator.hasNext()) { + String packageName = iterator.next(); + if (!collection2.contains(packageName)) { + action.accept(packageName); + } + } + } + + /** + * Requests an explicit health check for {@code packageName}. + * After this request, the callback registered on {@link #setCallbacks} can receive explicit + * health check passed results. + */ + private void request(String packageName) { + synchronized (mLock) { + if (!prepareServiceLocked("request health check for " + packageName)) { + return; + } + + Slog.i(TAG, "Requesting health check for package " + packageName); + try { + mRemoteService.request(packageName); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to request health check for package " + packageName, e); + } + } + } + + /** + * Cancels all explicit health checks for {@code packageName}. + * After this request, the callback registered on {@link #setCallbacks} can no longer receive + * explicit health check passed results. + */ + private void cancel(String packageName) { + synchronized (mLock) { + if (!prepareServiceLocked("cancel health check for " + packageName)) { + return; + } + + Slog.i(TAG, "Cancelling health check for package " + packageName); + try { + mRemoteService.cancel(packageName); + } catch (RemoteException e) { + // Do nothing, if the service is down, when it comes up, we will sync requests, + // if there's some other error, retrying wouldn't fix anyways. + Slog.w(TAG, "Failed to cancel health check for package " + packageName, e); + } + } + } + + /** + * Returns the packages that we can request explicit health checks for. + * The packages will be returned to the {@code consumer}. + */ + private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) { + synchronized (mLock) { + if (!prepareServiceLocked("get health check supported packages")) { + return; + } + + Slog.d(TAG, "Getting health check supported packages"); + try { + mRemoteService.getSupportedPackages(new RemoteCallback(result -> { + List<PackageConfig> packages = + result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class); + Slog.i(TAG, "Explicit health check supported packages " + packages); + consumer.accept(packages); + })); + } catch (RemoteException e) { + // Request failed, treat as if all observed packages are supported, if any packages + // expire during this period, we may incorrectly treat it as failing health checks + // even if we don't support health checks for the package. + Slog.w(TAG, "Failed to get health check supported packages", e); + } + } + } + + /** + * Returns the packages for which health checks are currently in progress. + * The packages will be returned to the {@code consumer}. + */ + private void getRequestedPackages(Consumer<List<String>> consumer) { + synchronized (mLock) { + if (!prepareServiceLocked("get health check requested packages")) { + return; + } + + Slog.d(TAG, "Getting health check requested packages"); + try { + mRemoteService.getRequestedPackages(new RemoteCallback(result -> { + List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES); + Slog.i(TAG, "Explicit health check requested packages " + packages); + consumer.accept(packages); + })); + } catch (RemoteException e) { + // Request failed, treat as if we haven't requested any packages, if any packages + // were actually requested, they will not be cancelled now. May be cancelled later + Slog.w(TAG, "Failed to get health check requested packages", e); + } + } + } + + /** + * Binds to the explicit health check service if the controller is enabled and + * not already bound. + */ + private void bindService() { + synchronized (mLock) { + if (!mEnabled || mConnection != null || mRemoteService != null) { + if (!mEnabled) { + Slog.i(TAG, "Not binding to service, service disabled"); + } else if (mRemoteService != null) { + Slog.i(TAG, "Not binding to service, service already connected"); + } else { + Slog.i(TAG, "Not binding to service, service already connecting"); + } + return; + } + ComponentName component = getServiceComponentNameLocked(); + if (component == null) { + Slog.wtf(TAG, "Explicit health check service not found"); + return; + } + + Intent intent = new Intent(); + intent.setComponent(component); + mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Slog.i(TAG, "Explicit health check service is connected " + name); + initState(service); + } + + @Override + @MainThread + public void onServiceDisconnected(ComponentName name) { + // Service crashed or process was killed, #onServiceConnected will be called. + // Don't need to re-bind. + Slog.i(TAG, "Explicit health check service is disconnected " + name); + synchronized (mLock) { + mRemoteService = null; + } + } + + @Override + public void onBindingDied(ComponentName name) { + // Application hosting service probably got updated + // Need to re-bind. + Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name); + unbindService(); + bindService(); + } + + @Override + public void onNullBinding(ComponentName name) { + // Should never happen. Service returned null from #onBind. + Slog.wtf(TAG, "Explicit health check service binding is null?? " + name); + } + }; + + mContext.bindServiceAsUser(intent, mConnection, + Context.BIND_AUTO_CREATE, UserHandle.SYSTEM); + Slog.i(TAG, "Explicit health check service is bound"); + } + } + + /** Unbinds the explicit health check service. */ + private void unbindService() { + synchronized (mLock) { + if (mRemoteService != null) { + mContext.unbindService(mConnection); + mRemoteService = null; + mConnection = null; + } + Slog.i(TAG, "Explicit health check service is unbound"); + } + } + + @GuardedBy("mLock") + @Nullable + private ServiceInfo getServiceInfoLocked() { + final String packageName = + mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); + if (packageName == null) { + Slog.w(TAG, "no external services package!"); + return null; + } + + final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); + intent.setPackage(packageName); + final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + Slog.w(TAG, "No valid components found."); + return null; + } + return resolveInfo.serviceInfo; + } + + @GuardedBy("mLock") + @Nullable + private ComponentName getServiceComponentNameLocked() { + final ServiceInfo serviceInfo = getServiceInfoLocked(); + if (serviceInfo == null) { + return null; + } + + final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); + if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE + .equals(serviceInfo.permission)) { + Slog.w(TAG, name.flattenToShortString() + " does not require permission " + + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE); + return null; + } + return name; + } + + private void initState(IBinder service) { + synchronized (mLock) { + if (!mEnabled) { + Slog.w(TAG, "Attempting to connect disabled service?? Unbinding..."); + // Very unlikely, but we disabled the service after binding but before we connected + unbindService(); + return; + } + mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service); + try { + mRemoteService.setCallback(new RemoteCallback(result -> { + String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE); + if (!TextUtils.isEmpty(packageName)) { + if (mPassedConsumer == null) { + Slog.wtf(TAG, "Health check passed for package " + packageName + + "but no consumer registered."); + } else { + // Call without lock held + mPassedConsumer.accept(packageName); + } + } else { + Slog.wtf(TAG, "Empty package passed explicit health check?"); + } + })); + Slog.i(TAG, "Service initialized, syncing requests"); + } catch (RemoteException e) { + Slog.wtf(TAG, "Could not setCallback on explicit health check service"); + } + } + // Calling outside lock + mNotifySyncRunnable.run(); + } + + /** + * Prepares the health check service to receive requests. + * + * @return {@code true} if it is ready and we can proceed with a request, + * {@code false} otherwise. If it is not ready, and the service is enabled, + * we will bind and the request should be automatically attempted later. + */ + @GuardedBy("mLock") + private boolean prepareServiceLocked(String action) { + if (mRemoteService != null && mEnabled) { + return true; + } + Slog.i(TAG, "Service not ready to " + action + + (mEnabled ? ". Binding..." : ". Disabled")); + if (mEnabled) { + bindService(); + } + return false; + } +} |