Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit caa435ef authored by Zimuzo's avatar Zimuzo Committed by Zimuzo Ezeozue
Browse files

Add explicit health check to PackageWatchdog

PackageWatchdog now uses the ExplicitHealthCheckController introduced
in Ia030671c99699bd8d8273f32a97a1d3b7b015d3b when observing packages.

Bug: 120598832
Test: Manually tested that after an APEX update, the network stack
does not pass the explicit health check until WiFi is connected
successfully. If Wi-Fi is never connected and the network stack
monitoring duration is exceeded, the update is rolled back.

Change-Id: I75d3cc909cabb4a4eb34df1d5022d1afc629dac3
parent d9246ef1
Loading
Loading
Loading
Loading
+133 −59
Original line number Original line Diff line number Diff line
@@ -39,7 +39,9 @@ import android.text.TextUtils;
import android.util.Slog;
import android.util.Slog;


import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;


import java.util.Collections;
import java.util.List;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Consumer;


@@ -50,10 +52,22 @@ class ExplicitHealthCheckController {
    private static final String TAG = "ExplicitHealthCheckController";
    private static final String TAG = "ExplicitHealthCheckController";
    private final Object mLock = new Object();
    private final Object mLock = new Object();
    private final Context mContext;
    private final Context mContext;
    @GuardedBy("mLock") @Nullable private StateCallback mStateCallback;

    // Called everytime the service is connected, so the watchdog can sync it's state with
    // the health check service. In practice, should never be null after it has been #setEnabled.
    @GuardedBy("mLock") @Nullable private Runnable mOnConnected;
    // 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.
    @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer;
    // Actual binder object to the explicit health check service.
    @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService;
    @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService;
    @GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
    // Cache for packages supporting explicit health checks. This cache should not change while
    // the health check service is running.
    @GuardedBy("mLock") @Nullable private List<String> mSupportedPackages;
    @GuardedBy("mLock") @Nullable private List<String> mSupportedPackages;
    // Connection to the explicit health check service, necessary to unbind
    @GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
    // Bind state of the explicit health check service.
    @GuardedBy("mLock") private boolean mEnabled;


    ExplicitHealthCheckController(Context context) {
    ExplicitHealthCheckController(Context context) {
        mContext = context;
        mContext = context;
@@ -61,28 +75,40 @@ class ExplicitHealthCheckController {


    /**
    /**
     * Requests an explicit health check for {@code packageName}.
     * Requests an explicit health check for {@code packageName}.
     * After this request, the callback registered on {@link startService} can receive explicit
     * After this request, the callback registered on {@link #setCallbacks} can receive explicit
     * health check passed results.
     * health check passed results.
     *
     *
     * @throws IllegalStateException if the service is not started
     * @throws IllegalStateException if the service is not started
     */
     */
    public void request(String packageName) throws RemoteException {
    public void request(String packageName) throws RemoteException {
        synchronized (mLock) {
        synchronized (mLock) {
            if (!mEnabled) {
                return;
            }

            enforceServiceReadyLocked();
            enforceServiceReadyLocked();

            Slog.i(TAG, "Requesting health check for package " + packageName);
            mRemoteService.request(packageName);
            mRemoteService.request(packageName);
        }
        }
    }
    }


    /**
    /**
     * Cancels all explicit health checks for {@code packageName}.
     * Cancels all explicit health checks for {@code packageName}.
     * After this request, the callback registered on {@link startService} can no longer receive
     * After this request, the callback registered on {@link #setCallbacks} can no longer receive
     * explicit health check passed results.
     * explicit health check passed results.
     *
     *
     * @throws IllegalStateException if the service is not started
     * @throws IllegalStateException if the service is not started
     */
     */
    public void cancel(String packageName) throws RemoteException {
    public void cancel(String packageName) throws RemoteException {
        synchronized (mLock) {
        synchronized (mLock) {
            if (!mEnabled) {
                return;
            }

            enforceServiceReadyLocked();
            enforceServiceReadyLocked();

            Slog.i(TAG, "Cancelling health check for package " + packageName);
            mRemoteService.cancel(packageName);
            mRemoteService.cancel(packageName);
        }
        }
    }
    }
@@ -95,13 +121,21 @@ class ExplicitHealthCheckController {
     */
     */
    public void getSupportedPackages(Consumer<List<String>> consumer) throws RemoteException {
    public void getSupportedPackages(Consumer<List<String>> consumer) throws RemoteException {
        synchronized (mLock) {
        synchronized (mLock) {
            if (!mEnabled) {
                consumer.accept(Collections.emptyList());
                return;
            }

            enforceServiceReadyLocked();
            enforceServiceReadyLocked();

            if (mSupportedPackages == null) {
            if (mSupportedPackages == null) {
                Slog.d(TAG, "Getting health check supported packages");
                mRemoteService.getSupportedPackages(new RemoteCallback(result -> {
                mRemoteService.getSupportedPackages(new RemoteCallback(result -> {
                    mSupportedPackages = result.getStringArrayList(EXTRA_SUPPORTED_PACKAGES);
                    mSupportedPackages = result.getStringArrayList(EXTRA_SUPPORTED_PACKAGES);
                    consumer.accept(mSupportedPackages);
                    consumer.accept(mSupportedPackages);
                }));
                }));
            } else {
            } else {
                Slog.d(TAG, "Getting cached health check supported packages");
                consumer.accept(mSupportedPackages);
                consumer.accept(mSupportedPackages);
            }
            }
        }
        }
@@ -115,95 +149,113 @@ class ExplicitHealthCheckController {
     */
     */
    public void getRequestedPackages(Consumer<List<String>> consumer) throws RemoteException {
    public void getRequestedPackages(Consumer<List<String>> consumer) throws RemoteException {
        synchronized (mLock) {
        synchronized (mLock) {
            if (!mEnabled) {
                consumer.accept(Collections.emptyList());
                return;
            }

            enforceServiceReadyLocked();
            enforceServiceReadyLocked();

            Slog.d(TAG, "Getting health check requested packages");
            mRemoteService.getRequestedPackages(new RemoteCallback(
            mRemoteService.getRequestedPackages(new RemoteCallback(
                    result -> consumer.accept(
                    result -> consumer.accept(
                            result.getStringArrayList(EXTRA_REQUESTED_PACKAGES))));
                            result.getStringArrayList(EXTRA_REQUESTED_PACKAGES))));
        }
        }
    }
    }


    /** Enables or disables explicit health checks. */
    public void setEnabled(boolean enabled) {
        synchronized (mLock) {
            if (enabled == mEnabled) {
                return;
            }

            Slog.i(TAG, "Setting explicit health checks enabled " + enabled);
            mEnabled = enabled;
            if (enabled) {
                bindService();
            } else {
                unbindService();
            }
        }
    }

    /**
    /**
     * Starts the explicit health check service.
     * Sets callbacks to listen to important events from the controller.
     *
     * Should be called at initialization.
     * @param stateCallback will receive important state changes changes
     * @param passedConsumer will accept packages that pass explicit health checks
     *
     * @throws IllegalStateException if the service is already started
     */
     */
    public void startService(StateCallback stateCallback, Consumer<String> passedConsumer) {
    public void setCallbacks(Runnable onConnected, Consumer<String> passedConsumer) {
        Preconditions.checkNotNull(onConnected);
        Preconditions.checkNotNull(passedConsumer);
        mOnConnected = onConnected;
        mPassedConsumer = passedConsumer;
    }

    /** Binds to the explicit health check service. */
    private void bindService() {
        synchronized (mLock) {
        synchronized (mLock) {
            if (mRemoteService != null) {
            if (mRemoteService != null) {
                throw new IllegalStateException("Explicit health check service already started.");
                return;
            }
            }
            mStateCallback = stateCallback;
            ComponentName component = getServiceComponentNameLocked();
            if (component == null) {
                Slog.wtf(TAG, "Explicit health check service not found");
                return;
            }

            Intent intent = new Intent();
            intent.setComponent(component);
            // TODO: Fix potential race conditions during mConnection state transitions.
            // E.g after #onServiceDisconected, the mRemoteService object is invalid until
            // we get an #onServiceConnected.
            mConnection = new ServiceConnection() {
            mConnection = new ServiceConnection() {
                @Override
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                public void onServiceConnected(ComponentName name, IBinder service) {
                    synchronized (mLock) {
                    initState(service);
                        mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
                        try {
                            mRemoteService.setCallback(new RemoteCallback(result -> {
                                String packageName =
                                        result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
                                if (!TextUtils.isEmpty(packageName)) {
                                    passedConsumer.accept(packageName);
                                } else {
                                    Slog.w(TAG, "Empty package passed explicit health check?");
                                }
                            }));
                            mStateCallback.onStart();
                    Slog.i(TAG, "Explicit health check service is connected " + name);
                    Slog.i(TAG, "Explicit health check service is connected " + name);
                        } catch (RemoteException e) {
                            Slog.wtf(TAG, "Coud not setCallback on explicit health check service");
                        }
                    }
                }
                }


                @Override
                @Override
                @MainThread
                @MainThread
                public void onServiceDisconnected(ComponentName name) {
                public void onServiceDisconnected(ComponentName name) {
                    resetState();
                    // 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);
                    Slog.i(TAG, "Explicit health check service is disconnected " + name);
                }
                }


                @Override
                @Override
                public void onBindingDied(ComponentName name) {
                public void onBindingDied(ComponentName name) {
                    resetState();
                    // Application hosting service probably got updated
                    // Need to re-bind.
                    synchronized (mLock) {
                        if (mEnabled) {
                            unbindService();
                            bindService();
                        }
                    }
                    Slog.i(TAG, "Explicit health check service binding is dead " + name);
                    Slog.i(TAG, "Explicit health check service binding is dead " + name);
                }
                }


                @Override
                @Override
                public void onNullBinding(ComponentName name) {
                public void onNullBinding(ComponentName name) {
                    resetState();
                    // Should never happen. Service returned null from #onBind.
                    Slog.i(TAG, "Explicit health check service binding is null " + name);
                    Slog.wtf(TAG, "Explicit health check service binding is null?? " + name);
                }
                }
            };
            };


            ComponentName component = getServiceComponentNameLocked();
            Slog.i(TAG, "Binding to explicit health service");
            if (component != null) {
                Intent intent = new Intent();
                intent.setComponent(component);
            mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE,
            mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE,
                    UserHandle.of(UserHandle.USER_SYSTEM));
                    UserHandle.of(UserHandle.USER_SYSTEM));
        }
        }
    }
    }
    }

    // TODO: Differentiate between expected vs unexpected stop?
    /** Callback to receive important {@link ExplicitHealthCheckController} state changes. */
    abstract static class StateCallback {
        /** The controller is ready and we can request explicit health checks for packages */
        public void onStart() {}

        /** The controller is not ready and we cannot request explicit health checks for packages */
        public void onStop() {}
    }


    /** Stops the explicit health check service. */
    /** Unbinds the explicit health check service. */
    public void stopService() {
    private void unbindService() {
        synchronized (mLock) {
        synchronized (mLock) {
            if (mRemoteService != null) {
            if (mRemoteService != null) {
                Slog.i(TAG, "Unbinding from explicit health service");
                mContext.unbindService(mConnection);
                mContext.unbindService(mConnection);
                mRemoteService = null;
            }
            }
        }
        }
    }
    }
@@ -247,19 +299,41 @@ class ExplicitHealthCheckController {
        return name;
        return name;
    }
    }


    private void resetState() {
    private void initState(IBinder service) {
        synchronized (mLock) {
        synchronized (mLock) {
            mStateCallback.onStop();
            mStateCallback = null;
            mSupportedPackages = null;
            mSupportedPackages = null;
            mRemoteService = null;
            mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
            mConnection = null;
            try {
                mRemoteService.setCallback(new RemoteCallback(result -> {
                    String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
                    if (!TextUtils.isEmpty(packageName)) {
                        synchronized (mLock) {
                            if (mPassedConsumer == null) {
                                Slog.w(TAG, "Health check passed for package " + packageName
                                        + "but no consumer registered.");
                            } else {
                                mPassedConsumer.accept(packageName);
                            }
                        }
                    } else {
                        Slog.w(TAG, "Empty package passed explicit health check?");
                    }
                }));
                if (mOnConnected == null) {
                    Slog.w(TAG, "Health check service connected but no runnable registered.");
                } else {
                    mOnConnected.run();
                }
            } catch (RemoteException e) {
                Slog.wtf(TAG, "Could not setCallback on explicit health check service");
            }
        }
        }
    }
    }


    @GuardedBy("mLock")
    @GuardedBy("mLock")
    private void enforceServiceReadyLocked() {
    private void enforceServiceReadyLocked() {
        if (mRemoteService == null) {
        if (mRemoteService == null) {
            // TODO: Try to bind to service
            throw new IllegalStateException("Explicit health check service not ready");
            throw new IllegalStateException("Explicit health check service not ready");
        }
        }
    }
    }
+223 −75

File changed.

Preview size limit exceeded, changes collapsed.

+1 −2
Original line number Original line Diff line number Diff line
@@ -166,8 +166,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve
     * This may cause {@code packages} to be rolled back if they crash too freqeuntly.
     * This may cause {@code packages} to be rolled back if they crash too freqeuntly.
     */
     */
    public void startObservingHealth(List<String> packages, long durationMs) {
    public void startObservingHealth(List<String> packages, long durationMs) {
        PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs,
        PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
                false /* withExplicitHealthCheck */);
    }
    }


    /** Verifies the rollback state after a reboot. */
    /** Verifies the rollback state after a reboot. */
+206 −32

File changed.

Preview size limit exceeded, changes collapsed.