blob: f7c4aac2e04fbb5bf9b983320c140f4c7dbd6da6 [file] [log] [blame]
/*
* 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.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 = Preconditions.checkNotNull(passedConsumer);
mSupportedConsumer = Preconditions.checkNotNull(supportedConsumer);
mNotifySyncRunnable = Preconditions.checkNotNull(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);
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.of(UserHandle.USER_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;
}
}