| /* |
| * 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.rollback; |
| |
| import android.annotation.Nullable; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageInstaller; |
| import android.content.pm.PackageManager; |
| import android.content.pm.VersionedPackage; |
| import android.content.rollback.PackageRollbackInfo; |
| import android.content.rollback.RollbackInfo; |
| import android.content.rollback.RollbackManager; |
| import android.os.Environment; |
| import android.os.FileUtils; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.PowerManager; |
| import android.os.SystemProperties; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| import android.util.StatsLog; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.server.PackageWatchdog; |
| import com.android.server.PackageWatchdog.PackageHealthObserver; |
| import com.android.server.PackageWatchdog.PackageHealthObserverImpact; |
| |
| import libcore.io.IoUtils; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * {@link PackageHealthObserver} for {@link RollbackManagerService}. |
| * This class monitors crashes and triggers RollbackManager rollback accordingly. |
| * It also monitors native crashes for some short while after boot. |
| * |
| * @hide |
| */ |
| public final class RollbackPackageHealthObserver implements PackageHealthObserver { |
| private static final String TAG = "RollbackPackageHealthObserver"; |
| private static final String NAME = "rollback-observer"; |
| private static final int INVALID_ROLLBACK_ID = -1; |
| // TODO: make the following values configurable via DeviceConfig |
| private static final long NATIVE_CRASH_POLLING_INTERVAL_MILLIS = |
| TimeUnit.SECONDS.toMillis(30); |
| private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10; |
| |
| private final Context mContext; |
| private final Handler mHandler; |
| private final File mLastStagedRollbackIdFile; |
| // Staged rollback ids that have been committed but their session is not yet ready |
| @GuardedBy("mPendingStagedRollbackIds") |
| private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>(); |
| // this field is initialized in the c'tor and then only accessed from mHandler thread, so |
| // no need to guard with a lock |
| private long mNumberOfNativeCrashPollsRemaining; |
| |
| RollbackPackageHealthObserver(Context context) { |
| mContext = context; |
| mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; |
| HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); |
| handlerThread.start(); |
| mHandler = handlerThread.getThreadHandler(); |
| File dataDir = new File(Environment.getDataDirectory(), "rollback-observer"); |
| dataDir.mkdirs(); |
| mLastStagedRollbackIdFile = new File(dataDir, "last-staged-rollback-id"); |
| PackageWatchdog.getInstance(mContext).registerHealthObserver(this); |
| } |
| |
| @Override |
| public int onHealthCheckFailed(VersionedPackage failedPackage) { |
| if (getAvailableRollback(mContext.getSystemService(RollbackManager.class), failedPackage) |
| == null) { |
| // Don't handle the notification, no rollbacks available for the package |
| return PackageHealthObserverImpact.USER_IMPACT_NONE; |
| } else { |
| // Rollback is available, we may get a callback into #execute |
| return PackageHealthObserverImpact.USER_IMPACT_MEDIUM; |
| } |
| } |
| |
| @Override |
| public boolean execute(VersionedPackage failedPackage) { |
| RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); |
| VersionedPackage moduleMetadataPackage = getModuleMetadataPackage(); |
| RollbackInfo rollback = getAvailableRollback(rollbackManager, failedPackage); |
| |
| if (rollback == null) { |
| Slog.w(TAG, "Expected rollback but no valid rollback found for package: [ " |
| + failedPackage.getPackageName() + "] with versionCode: [" |
| + failedPackage.getVersionCode() + "]"); |
| return false; |
| } |
| |
| logEvent(moduleMetadataPackage, |
| StatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE); |
| LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver((Intent result) -> { |
| int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, |
| RollbackManager.STATUS_FAILURE); |
| if (status == RollbackManager.STATUS_SUCCESS) { |
| if (rollback.isStaged()) { |
| int rollbackId = rollback.getRollbackId(); |
| synchronized (mPendingStagedRollbackIds) { |
| mPendingStagedRollbackIds.add(rollbackId); |
| } |
| BroadcastReceiver listener = |
| listenForStagedSessionReady(rollbackManager, rollbackId, |
| moduleMetadataPackage); |
| handleStagedSessionChange(rollbackManager, rollbackId, listener, |
| moduleMetadataPackage); |
| } else { |
| logEvent(moduleMetadataPackage, |
| StatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS); |
| } |
| } else { |
| logEvent(moduleMetadataPackage, |
| StatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE); |
| } |
| }); |
| |
| mHandler.post(() -> |
| rollbackManager.commitRollback(rollback.getRollbackId(), |
| Collections.singletonList(failedPackage), |
| 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); |
| } |
| |
| /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot |
| * to check for native crashes and mitigate them if needed. |
| */ |
| public void onBootCompletedAsync() { |
| mHandler.post(()->onBootCompleted()); |
| } |
| |
| private void onBootCompleted() { |
| RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); |
| PackageInstaller packageInstaller = mContext.getPackageManager().getPackageInstaller(); |
| String moduleMetadataPackageName = getModuleMetadataPackageName(); |
| VersionedPackage newModuleMetadataPackage = getModuleMetadataPackage(); |
| |
| if (getAvailableRollback(rollbackManager, newModuleMetadataPackage) != null) { |
| scheduleCheckAndMitigateNativeCrashes(); |
| } |
| |
| int rollbackId = popLastStagedRollbackId(); |
| if (rollbackId == INVALID_ROLLBACK_ID) { |
| // No staged rollback before reboot |
| return; |
| } |
| |
| RollbackInfo rollback = null; |
| for (RollbackInfo info : rollbackManager.getRecentlyCommittedRollbacks()) { |
| if (rollbackId == info.getRollbackId()) { |
| rollback = info; |
| break; |
| } |
| } |
| |
| if (rollback == null) { |
| Slog.e(TAG, "rollback info not found for last staged rollback: " + rollbackId); |
| return; |
| } |
| |
| // Use the version of the metadata package that was installed before |
| // we rolled back for logging purposes. |
| VersionedPackage oldModuleMetadataPackage = null; |
| for (PackageRollbackInfo packageRollback : rollback.getPackages()) { |
| if (packageRollback.getPackageName().equals(moduleMetadataPackageName)) { |
| oldModuleMetadataPackage = packageRollback.getVersionRolledBackFrom(); |
| break; |
| } |
| } |
| |
| int sessionId = rollback.getCommittedSessionId(); |
| PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); |
| if (sessionInfo == null) { |
| Slog.e(TAG, "On boot completed, could not load session id " + sessionId); |
| return; |
| } |
| if (sessionInfo.isStagedSessionApplied()) { |
| logEvent(oldModuleMetadataPackage, |
| StatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS); |
| } else if (sessionInfo.isStagedSessionReady()) { |
| // TODO: What do for staged session ready but not applied |
| } else { |
| logEvent(oldModuleMetadataPackage, |
| StatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE); |
| } |
| } |
| |
| private RollbackInfo getAvailableRollback(RollbackManager rollbackManager, |
| VersionedPackage failedPackage) { |
| for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) { |
| for (PackageRollbackInfo packageRollback : rollback.getPackages()) { |
| boolean hasFailedPackage = packageRollback.getPackageName().equals( |
| failedPackage.getPackageName()) |
| && packageRollback.getVersionRolledBackFrom().getVersionCode() |
| == failedPackage.getVersionCode(); |
| if (hasFailedPackage) { |
| return rollback; |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private String getModuleMetadataPackageName() { |
| String packageName = mContext.getResources().getString( |
| R.string.config_defaultModuleMetadataProvider); |
| if (TextUtils.isEmpty(packageName)) { |
| return null; |
| } |
| return packageName; |
| } |
| |
| @Nullable |
| private VersionedPackage getModuleMetadataPackage() { |
| String packageName = getModuleMetadataPackageName(); |
| if (packageName == null) { |
| return null; |
| } |
| |
| try { |
| return new VersionedPackage(packageName, mContext.getPackageManager().getPackageInfo( |
| packageName, 0 /* flags */).getLongVersionCode()); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.w(TAG, "Module metadata provider not found"); |
| return null; |
| } |
| } |
| |
| private BroadcastReceiver listenForStagedSessionReady(RollbackManager rollbackManager, |
| int rollbackId, VersionedPackage moduleMetadataPackage) { |
| BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| handleStagedSessionChange(rollbackManager, |
| rollbackId, this /* BroadcastReceiver */, moduleMetadataPackage); |
| } |
| }; |
| IntentFilter sessionUpdatedFilter = |
| new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED); |
| mContext.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter); |
| return sessionUpdatedReceiver; |
| } |
| |
| private void handleStagedSessionChange(RollbackManager rollbackManager, int rollbackId, |
| BroadcastReceiver listener, VersionedPackage moduleMetadataPackage) { |
| PackageInstaller packageInstaller = |
| mContext.getPackageManager().getPackageInstaller(); |
| List<RollbackInfo> recentRollbacks = |
| rollbackManager.getRecentlyCommittedRollbacks(); |
| for (int i = 0; i < recentRollbacks.size(); i++) { |
| RollbackInfo recentRollback = recentRollbacks.get(i); |
| int sessionId = recentRollback.getCommittedSessionId(); |
| if ((rollbackId == recentRollback.getRollbackId()) |
| && (sessionId != PackageInstaller.SessionInfo.INVALID_ID)) { |
| PackageInstaller.SessionInfo sessionInfo = |
| packageInstaller.getSessionInfo(sessionId); |
| if (sessionInfo.isStagedSessionReady() && markStagedSessionHandled(rollbackId)) { |
| mContext.unregisterReceiver(listener); |
| saveLastStagedRollbackId(rollbackId); |
| logEvent(moduleMetadataPackage, |
| StatsLog |
| .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED); |
| mContext.getSystemService(PowerManager.class).reboot("Rollback staged install"); |
| } else if (sessionInfo.isStagedSessionFailed() |
| && markStagedSessionHandled(rollbackId)) { |
| logEvent(moduleMetadataPackage, |
| StatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE); |
| mContext.unregisterReceiver(listener); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns {@code true} if staged session associated with {@code rollbackId} was marked |
| * as handled, {@code false} if already handled. |
| */ |
| private boolean markStagedSessionHandled(int rollbackId) { |
| synchronized (mPendingStagedRollbackIds) { |
| return mPendingStagedRollbackIds.remove(rollbackId); |
| } |
| } |
| |
| private void saveLastStagedRollbackId(int stagedRollbackId) { |
| try { |
| FileOutputStream fos = new FileOutputStream(mLastStagedRollbackIdFile); |
| PrintWriter pw = new PrintWriter(fos); |
| pw.println(stagedRollbackId); |
| pw.flush(); |
| FileUtils.sync(fos); |
| pw.close(); |
| } catch (IOException e) { |
| Slog.e(TAG, "Failed to save last staged rollback id", e); |
| mLastStagedRollbackIdFile.delete(); |
| } |
| } |
| |
| private int popLastStagedRollbackId() { |
| int rollbackId = INVALID_ROLLBACK_ID; |
| if (!mLastStagedRollbackIdFile.exists()) { |
| return rollbackId; |
| } |
| |
| try { |
| rollbackId = Integer.parseInt( |
| IoUtils.readFileAsString(mLastStagedRollbackIdFile.getAbsolutePath()).trim()); |
| } catch (IOException | NumberFormatException e) { |
| Slog.e(TAG, "Failed to retrieve last staged rollback id", e); |
| } |
| mLastStagedRollbackIdFile.delete(); |
| return rollbackId; |
| } |
| |
| private static void logEvent(@Nullable VersionedPackage moduleMetadataPackage, int type) { |
| Slog.i(TAG, "Watchdog event occurred of type: " + type); |
| if (moduleMetadataPackage != null) { |
| StatsLog.logWatchdogRollbackOccurred(type, moduleMetadataPackage.getPackageName(), |
| moduleMetadataPackage.getVersionCode()); |
| } |
| } |
| |
| /** |
| * This method should be only called on mHandler thread, since it modifies |
| * {@link #mNumberOfNativeCrashPollsRemaining} and we want to keep this class lock free. |
| */ |
| private void checkAndMitigateNativeCrashes() { |
| mNumberOfNativeCrashPollsRemaining--; |
| // Check if native watchdog reported a crash |
| if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) { |
| execute(getModuleMetadataPackage()); |
| // we stop polling after an attempt to execute rollback, regardless of whether the |
| // attempt succeeds or not |
| } else { |
| if (mNumberOfNativeCrashPollsRemaining > 0) { |
| mHandler.postDelayed(() -> checkAndMitigateNativeCrashes(), |
| NATIVE_CRASH_POLLING_INTERVAL_MILLIS); |
| } |
| } |
| } |
| |
| /** |
| * Since this method can eventually trigger a RollbackManager rollback, it should be called |
| * only once boot has completed {@code onBootCompleted} and not earlier, because the install |
| * session must be entirely completed before we try to rollback. |
| */ |
| private void scheduleCheckAndMitigateNativeCrashes() { |
| Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check " |
| + "and mitigate native crashes"); |
| mHandler.post(()->checkAndMitigateNativeCrashes()); |
| } |
| } |