blob: 9c19aeccd59a3d80312379f5462b1cf2289973b5 [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.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("ro.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());
}
}