blob: 301e3b644dc4a0b348ac2ba8e70b5df73a2ba91f [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.car.watchdog;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.car.Car;
import android.car.CarManagerBase;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
/**
* Provides APIs and interfaces for client health checking.
*
* @hide
*/
@SystemApi
public final class CarWatchdogManager extends CarManagerBase {
private static final String TAG = CarWatchdogManager.class.getSimpleName();
private static final boolean DEBUG = false; // STOPSHIP if true
private static final int INVALID_SESSION_ID = -1;
private static final int NUMBER_OF_CONDITIONS_TO_BE_MET = 2;
// Message ID representing main thread activeness checking.
private static final int WHAT_CHECK_MAIN_THREAD = 1;
/** Timeout for services which should be responsive. The length is 3,000 milliseconds. */
public static final int TIMEOUT_CRITICAL = 0;
/** Timeout for services which are relatively responsive. The length is 5,000 milliseconds. */
public static final int TIMEOUT_MODERATE = 1;
/** Timeout for all other services. The length is 10,000 milliseconds. */
public static final int TIMEOUT_NORMAL = 2;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "TIMEOUT_", value = {
TIMEOUT_CRITICAL,
TIMEOUT_MODERATE,
TIMEOUT_NORMAL,
})
@Target({ElementType.TYPE_USE})
public @interface TimeoutLengthEnum {}
private final ICarWatchdogService mService;
private final ICarWatchdogClientImpl mClientImpl;
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
private final Object mLock = new Object();
@GuardedBy("mLock")
private CarWatchdogClientCallback mRegisteredClient;
@GuardedBy("mLock")
private Executor mCallbackExecutor;
@GuardedBy("mLock")
private SessionInfo mSession = new SessionInfo(INVALID_SESSION_ID, INVALID_SESSION_ID);
@GuardedBy("mLock")
private int mRemainingConditions;
/**
* CarWatchdogClientCallback is implemented by the clients which want to be health-checked by
* car watchdog server. Every time onCheckHealthStatus is called, they are expected to
* respond by calling {@link CarWatchdogManager.tellClientAlive} within timeout. If they don't
* respond, car watchdog server reports the current state and kills them.
*
* <p>Before car watchdog server kills the client, it calls onPrepareProcessTermination to allow
* them to prepare the termination. They will be killed in 1 second.
*/
public abstract static class CarWatchdogClientCallback {
/**
* Car watchdog server pings the client to check if it is alive.
*
* <p>The callback method is called at the Executor which is specifed in {@link
* #registerClient}.
*
* @param sessionId Unique id to distinguish each health checking.
* @param timeout Time duration within which the client should respond.
*
* @return whether the response is immediately acknowledged. If {@code true}, car watchdog
* server considers that the response is acknowledged already. If {@code false},
* the client should call {@link CarWatchdogManager.tellClientAlive} later to tell
* that it is alive.
*/
public boolean onCheckHealthStatus(int sessionId, @TimeoutLengthEnum int timeout) {
return false;
}
/**
* Car watchdog server notifies the client that it will be terminated in 1 second.
*
* <p>The callback method is called at the Executor which is specifed in {@link
* #registerClient}.
*/
public void onPrepareProcessTermination() {}
}
/** @hide */
public CarWatchdogManager(Car car, IBinder service) {
super(car);
mService = ICarWatchdogService.Stub.asInterface(service);
mClientImpl = new ICarWatchdogClientImpl(this);
}
/**
* Registers the car watchdog clients to {@link CarWatchdogManager}.
*
* <p>It is allowed to register a client from any thread, but only one client can be
* registered. If two or more clients are needed, create a new {@link Car} and register a client
* to it.
*
* @param client Watchdog client implementing {@link CarWatchdogClientCallback} interface.
* @param timeout The time duration within which the client desires to respond. The actual
* timeout is decided by watchdog server.
* @throws IllegalStateException if at least one client is already registered.
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_WATCHDOG)
public void registerClient(@NonNull @CallbackExecutor Executor executor,
@NonNull CarWatchdogClientCallback client, @TimeoutLengthEnum int timeout) {
synchronized (mLock) {
if (mRegisteredClient == client) {
return;
}
if (mRegisteredClient != null) {
throw new IllegalStateException(
"Cannot register the client. Only one client can be registered.");
}
mRegisteredClient = client;
mCallbackExecutor = executor;
}
try {
mService.registerClient(mClientImpl, timeout);
if (DEBUG) {
Log.d(TAG, "Car watchdog client is successfully registered");
}
} catch (RemoteException e) {
synchronized (mLock) {
mRegisteredClient = null;
}
handleRemoteExceptionFromCarService(e);
}
}
/**
* Unregisters the car watchdog client from {@link CarWatchdogManager}.
*
* @param client Watchdog client implementing {@link CarWatchdogClientCallback} interface.
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_WATCHDOG)
public void unregisterClient(@NonNull CarWatchdogClientCallback client) {
synchronized (mLock) {
if (mRegisteredClient != client) {
Log.w(TAG, "Cannot unregister the client. It has not been registered.");
return;
}
mRegisteredClient = null;
mCallbackExecutor = null;
}
try {
mService.unregisterClient(mClientImpl);
if (DEBUG) {
Log.d(TAG, "Car watchdog client is successfully unregistered");
}
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Tells {@link CarWatchdogManager} that the client is alive.
*
* @param client Watchdog client implementing {@link CarWatchdogClientCallback} interface.
* @param sessionId Session id given by {@link CarWatchdogManager}.
* @throws IllegalStateException if {@code client} is not registered.
* @throws IllegalArgumentException if {@code session Id} is not correct.
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_WATCHDOG)
public void tellClientAlive(@NonNull CarWatchdogClientCallback client, int sessionId) {
boolean shouldReport;
synchronized (mLock) {
if (mRegisteredClient != client) {
throw new IllegalStateException(
"Cannot report client status. The client has not been registered.");
}
Preconditions.checkArgument(sessionId != -1 && mSession.currentId == sessionId,
"Cannot report client status. "
+ "The given session id doesn't match the current one.");
if (mSession.lastReportedId == sessionId) {
Log.w(TAG, "The given session id is already reported.");
return;
}
mSession.lastReportedId = sessionId;
mRemainingConditions--;
shouldReport = checkConditionLocked();
}
if (shouldReport) {
reportToService(sessionId);
}
}
/** @hide */
@Override
public void onCarDisconnected() {
// nothing to do
}
private void checkClientStatus(int sessionId, int timeout) {
CarWatchdogClientCallback client;
Executor executor;
mMainHandler.removeMessages(WHAT_CHECK_MAIN_THREAD);
synchronized (mLock) {
if (mRegisteredClient == null) {
Log.w(TAG, "Cannot check client status. The client has not been registered.");
return;
}
mSession.currentId = sessionId;
client = mRegisteredClient;
executor = mCallbackExecutor;
mRemainingConditions = NUMBER_OF_CONDITIONS_TO_BE_MET;
}
// For a car watchdog client to be active, 1) its main thread is active and 2) the client
// responds within timeout. When each condition is met, the remaining task counter is
// decreased. If the remaining task counter is zero, the client is considered active.
mMainHandler.sendMessage(obtainMessage(CarWatchdogManager::checkMainThread, this)
.setWhat(WHAT_CHECK_MAIN_THREAD));
// Call the client callback to check if the client is active.
executor.execute(() -> {
boolean checkDone = client.onCheckHealthStatus(sessionId, timeout);
if (checkDone) {
boolean shouldReport;
synchronized (mLock) {
if (mSession.lastReportedId == sessionId) {
return;
}
mSession.lastReportedId = sessionId;
mRemainingConditions--;
shouldReport = checkConditionLocked();
}
if (shouldReport) {
reportToService(sessionId);
}
}
});
}
private void checkMainThread() {
int sessionId;
boolean shouldReport;
synchronized (mLock) {
mRemainingConditions--;
sessionId = mSession.currentId;
shouldReport = checkConditionLocked();
}
if (shouldReport) {
reportToService(sessionId);
}
}
private boolean checkConditionLocked() {
if (mRemainingConditions < 0) {
Log.wtf(TAG, "Remaining condition is less than zero: should not happen");
}
return mRemainingConditions == 0;
}
private void reportToService(int sessionId) {
try {
mService.tellClientAlive(mClientImpl, sessionId);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
private void notifyProcessTermination() {
CarWatchdogClientCallback client;
Executor executor;
synchronized (mLock) {
if (mRegisteredClient == null) {
Log.w(TAG, "Cannot notify the client. The client has not been registered.");
return;
}
client = mRegisteredClient;
executor = mCallbackExecutor;
}
executor.execute(() -> client.onPrepareProcessTermination());
}
/** @hide */
private static final class ICarWatchdogClientImpl extends ICarWatchdogServiceCallback.Stub {
private final WeakReference<CarWatchdogManager> mManager;
private ICarWatchdogClientImpl(CarWatchdogManager manager) {
mManager = new WeakReference<>(manager);
}
@Override
public void onCheckHealthStatus(int sessionId, int timeout) {
CarWatchdogManager manager = mManager.get();
if (manager != null) {
manager.checkClientStatus(sessionId, timeout);
}
}
@Override
public void onPrepareProcessTermination() {
CarWatchdogManager manager = mManager.get();
if (manager != null) {
manager.notifyProcessTermination();
}
}
}
private final class SessionInfo {
public int currentId;
public int lastReportedId;
SessionInfo(int currentId, int lastReportedId) {
this.currentId = currentId;
this.lastReportedId = lastReportedId;
}
}
}