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

Commit e5009cd8 authored by Zimuzo's avatar Zimuzo
Browse files

Add PackageHealthObserverImpact

When a package fails health check, observers will report the impact of their
action on the user. Only the observer with the least user impact will be
allowed to take action.

Bug: 120598832
Test: atest PackageWatchdogTest
Change-Id: I15f358cd599431e1d7ea211aea5b1391f4aa33ab
parent 6ab2e4a9
Loading
Loading
Loading
Loading
+77 −45
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package com.android.server;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Environment;
@@ -46,6 +49,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
@@ -55,7 +59,8 @@ import java.util.Set;

/**
 * Monitors the health of packages on the system and notifies interested observers when packages
 * fail. All registered observers will be notified until an observer takes a mitigation action.
 * fail. On failure, the registered observer with the least user impacting mitigation will
 * be notified.
 */
public class PackageWatchdog {
    private static final String TAG = "PackageWatchdog";
@@ -78,7 +83,8 @@ public class PackageWatchdog {
    private final Context mContext;
    // Handler to run package cleanup runnables
    private final Handler mTimerHandler;
    private final Handler mIoHandler;
    // Handler for processing IO and observer actions
    private final Handler mWorkerHandler;
    // Contains (observer-name -> observer-handle) that have ever been registered from
    // previous boots. Observers with all packages expired are periodically pruned.
    // It is saved to disk on system shutdown and repouplated on startup so it survives reboots.
@@ -101,7 +107,7 @@ public class PackageWatchdog {
        mPolicyFile = new AtomicFile(new File(new File(Environment.getDataDirectory(), "system"),
                        "package-watchdog.xml"));
        mTimerHandler = new Handler(Looper.myLooper());
        mIoHandler = BackgroundThread.getHandler();
        mWorkerHandler = BackgroundThread.getHandler();
        mPackageCleanup = this::rescheduleCleanup;
        loadFromFile();
    }
@@ -115,7 +121,7 @@ public class PackageWatchdog {
        mContext = context;
        mPolicyFile = new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml"));
        mTimerHandler = new Handler(looper);
        mIoHandler = mTimerHandler;
        mWorkerHandler = mTimerHandler;
        mPackageCleanup = this::rescheduleCleanup;
        loadFromFile();
    }
@@ -228,49 +234,46 @@ public class PackageWatchdog {
    /**
     * Called when a process fails either due to a crash or ANR.
     *
     * <p>All registered observers for the packages contained in the process will be notified in
     * order of priority until an observer signifies that it has taken action and other observers
     * should not notified.
     * <p>For each package contained in the process, one registered observer with the least user
     * impact will be notified for mitigation.
     *
     * <p>This method could be called frequently if there is a severe problem on the device.
     */
    public void onPackageFailure(String[] packages) {
        ArrayMap<String, List<PackageHealthObserver>> packagesToReport = new ArrayMap<>();
        mWorkerHandler.post(() -> {
            synchronized (mLock) {
                if (mAllObservers.isEmpty()) {
                    return;
                }

                for (int pIndex = 0; pIndex < packages.length; pIndex++) {
                // Observers interested in receiving packageName failures
                List<PackageHealthObserver> observersToNotify = new ArrayList<>();
                    String packageToReport = packages[pIndex];
                    // Observer that will receive failure for packageToReport
                    PackageHealthObserver currentObserverToNotify = null;
                    int currentObserverImpact = Integer.MAX_VALUE;

                    // Find observer with least user impact
                    for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
                    PackageHealthObserver registeredObserver =
                            mAllObservers.valueAt(oIndex).mRegisteredObserver;
                    if (registeredObserver != null) {
                        observersToNotify.add(registeredObserver);
                    }
                }
                // Save interested observers and notify them outside the lock
                if (!observersToNotify.isEmpty()) {
                    packagesToReport.put(packages[pIndex], observersToNotify);
                        ObserverInternal observer = mAllObservers.valueAt(oIndex);
                        PackageHealthObserver registeredObserver = observer.mRegisteredObserver;
                        if (registeredObserver != null
                                && observer.onPackageFailure(packageToReport)) {
                            int impact = registeredObserver.onHealthCheckFailed(packageToReport);
                            if (impact != PackageHealthObserverImpact.USER_IMPACT_NONE
                                    && impact < currentObserverImpact) {
                                currentObserverToNotify = registeredObserver;
                                currentObserverImpact = impact;
                            }
                        }
                    }

        // Notify observers
        for (int pIndex = 0; pIndex < packagesToReport.size(); pIndex++) {
            List<PackageHealthObserver> observers = packagesToReport.valueAt(pIndex);
            String packageName = packages[pIndex];
            for (int oIndex = 0; oIndex < observers.size(); oIndex++) {
                PackageHealthObserver observer = observers.get(oIndex);
                if (mAllObservers.get(observer.getName()).onPackageFailure(packageName)
                        && observer.onHealthCheckFailed(packageName)) {
                    // Observer has handled, do not notify others
                    break;
                    // Execute action with least user impact
                    if (currentObserverToNotify != null) {
                        currentObserverToNotify.execute(packageToReport);
                    }
                }
            }
        });
    }

    // TODO(zezeozue): Optimize write? Maybe only write a separate smaller file?
@@ -278,21 +281,46 @@ public class PackageWatchdog {
    /** Writes the package information to file during shutdown. */
    public void writeNow() {
        if (!mAllObservers.isEmpty()) {
            mIoHandler.removeCallbacks(this::saveToFile);
            mWorkerHandler.removeCallbacks(this::saveToFile);
            pruneObservers(SystemClock.uptimeMillis() - mUptimeAtLastRescheduleMs);
            saveToFile();
            Slog.i(TAG, "Last write to update package durations");
        }
    }

    /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
    @Retention(SOURCE)
    @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_NONE,
                     PackageHealthObserverImpact.USER_IMPACT_LOW,
                     PackageHealthObserverImpact.USER_IMPACT_MEDIUM,
                     PackageHealthObserverImpact.USER_IMPACT_HIGH})
    public @interface PackageHealthObserverImpact {
        /** No action to take. */
        int USER_IMPACT_NONE = 0;
        /* Action has low user impact, user of a device will barely notice. */
        int USER_IMPACT_LOW = 1;
        /* Action has medium user impact, user of a device will likely notice. */
        int USER_IMPACT_MEDIUM = 3;
        /* Action has high user impact, a last resort, user of a device will be very frustrated. */
        int USER_IMPACT_HIGH = 5;
    }

    /** Register instances of this interface to receive notifications on package failure. */
    public interface PackageHealthObserver {
        /**
         * Called when health check fails for the {@code packageName}.
         * @return {@code true} if action was taken and other observers should not be notified of
         * this failure, {@code false} otherwise.
         *
         * @return any one of {@link PackageHealthObserverImpact} to express the impact
         * to the user on {@link #execute}
         */
        @PackageHealthObserverImpact int onHealthCheckFailed(String packageName);

        /**
         * Executes mitigation for {@link #onHealthCheckFailed}.
         *
         * @return {@code true} if action was executed successfully, {@code false} otherwise
         */
        boolean onHealthCheckFailed(String packageName);
        boolean execute(String packageName);

        // TODO(zezeozue): Ensure uniqueness?
        /**
@@ -442,8 +470,8 @@ public class PackageWatchdog {
    }

    private void saveToFileAsync() {
        mIoHandler.removeCallbacks(this::saveToFile);
        mIoHandler.post(this::saveToFile);
        mWorkerHandler.removeCallbacks(this::saveToFile);
        mWorkerHandler.post(this::saveToFile);
    }

    /**
@@ -606,7 +634,11 @@ public class PackageWatchdog {
            } else {
                mFailures++;
            }
            return mFailures >= TRIGGER_FAILURE_COUNT;
            boolean failed = mFailures >= TRIGGER_FAILURE_COUNT;
            if (failed) {
                mFailures = 0;
            }
            return failed;
        }
    }
}
+52 −32
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.os.HandlerThread;

import com.android.server.PackageWatchdog;
import com.android.server.PackageWatchdog.PackageHealthObserver;
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;

import java.util.List;

@@ -39,10 +40,12 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve
    private static final String TAG = "RollbackPackageHealthObserver";
    private static final String NAME = "rollback-observer";
    private Context mContext;
    private RollbackManager mRollbackManager;
    private Handler mHandler;

    RollbackPackageHealthObserver(Context context) {
        mContext = context;
        mRollbackManager = mContext.getSystemService(RollbackManager.class);
        HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver");
        handlerThread.start();
        mHandler = handlerThread.getThreadHandler();
@@ -50,33 +53,26 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve
    }

    @Override
    public boolean onHealthCheckFailed(String packageName) {
        RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
        for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) {
            for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
                if (packageName.equals(packageRollback.getPackageName())) {
                    // TODO(zezeozue): Only rollback if rollback version == failed package version
                    mHandler.post(() -> executeRollback(rollbackManager, rollback));
                    return true;
                }
    public int onHealthCheckFailed(String packageName) {
        RollbackInfo rollback = getAvailableRollback(packageName);
        if (rollback == null) {
            // Don't handle the notification, no rollbacks available for the package
            return PackageHealthObserverImpact.USER_IMPACT_NONE;
        }
        }
        // Don't handle the notification, no rollbacks available
        return false;
        // Rollback is available, we may get a callback into #execute
        return PackageHealthObserverImpact.USER_IMPACT_MEDIUM;
    }

    /**
     * Start observing health of {@code packages} for {@code durationMs}.
     * This may cause {@code packages} to be rolled back if they crash too freqeuntly.
     */
    public void startObservingHealth(List<String> packages, long durationMs) {
        PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
    @Override
    public boolean execute(String packageName) {
        RollbackInfo rollback = getAvailableRollback(packageName);
        if (rollback == null) {
            // Expected a rollback to be available, what happened?
            return false;
        }

    private void executeRollback(RollbackManager manager, RollbackInfo rollback) {
        // TODO(zezeozue): Log initiated metrics
        // TODO(zezeozue): Only rollback if rollback version == failed package version
        LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver((Intent result) -> {
            mHandler.post(() -> {
            int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
                    PackageInstaller.STATUS_FAILURE);
            if (status == PackageInstaller.STATUS_SUCCESS) {
@@ -87,12 +83,36 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve
                // Rollback failed other observers should have a shot
            }
        });
        });
        manager.commitRollback(rollback, rollbackReceiver.getIntentSender());

        // TODO(zezeozue): Log initiated metrics
        mHandler.post(() ->
                mRollbackManager.commitRollback(rollback, rollbackReceiver.getIntentSender()));
        // Assume rollback executed successfully
        return true;
    }

    @Override
    public String getName() {
        return NAME;
    }

    /**
     * Start observing health of {@code packages} for {@code durationMs}.
     * This may cause {@code packages} to be rolled back if they crash too freqeuntly.
     */
    public void startObservingHealth(List<String> packages, long durationMs) {
        PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
    }

    private RollbackInfo getAvailableRollback(String packageName) {
        for (RollbackInfo rollback : mRollbackManager.getAvailableRollbacks()) {
            for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
                if (packageName.equals(packageRollback.getPackageName())) {
                    // TODO(zezeozue): Only rollback if rollback version == failed package version
                    return rollback;
                }
            }
        }
        return null;
    }
}
+183 −33

File changed.

Preview size limit exceeded, changes collapsed.