Merge changes from topic "staged_userdata_restore"

* changes:
  Followup cleanup after refactoring rollback states.
  Use a single list for available and committed rollbacks.
This commit is contained in:
TreeHugger Robot
2019-03-01 18:23:42 +00:00
committed by Android (Google) Code Review
6 changed files with 334 additions and 424 deletions

View File

@@ -187,7 +187,8 @@ public final class RollbackManager {
/**
* Expire the rollback data for a given package.
* This API is meant to facilitate testing of rollback logic for
* expiring rollback data.
* expiring rollback data. Removes rollback data for available and
* recently committed rollbacks that contain the given package.
*
* @param packageName the name of the package to expire data for.
* @throws SecurityException if the caller does not have the

View File

@@ -18,7 +18,6 @@ package com.android.server.rollback;
import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.PackageRollbackInfo.RestoreInfo;
import android.content.rollback.RollbackInfo;
import android.os.storage.StorageManager;
import android.util.IntArray;
import android.util.Log;
@@ -30,9 +29,11 @@ import com.android.server.pm.Installer.InstallerException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Encapsulates the logic for initiating userdata snapshots and rollbacks via installd.
@@ -153,7 +154,7 @@ public class AppDataRollbackHelper {
}
/**
* Computes the list of pending backups for {@code userId} given lists of available rollbacks.
* Computes the list of pending backups for {@code userId} given lists of rollbacks.
* Packages pending backup for the given user are added to {@code pendingBackupPackages} along
* with their corresponding {@code PackageRollbackInfo}.
*
@@ -162,10 +163,10 @@ public class AppDataRollbackHelper {
*/
private static List<RollbackData> computePendingBackups(int userId,
Map<String, PackageRollbackInfo> pendingBackupPackages,
List<RollbackData> availableRollbacks) {
List<RollbackData> rollbacks) {
List<RollbackData> rd = new ArrayList<>();
for (RollbackData data : availableRollbacks) {
for (RollbackData data : rollbacks) {
for (PackageRollbackInfo info : data.info.getPackages()) {
final IntArray pendingBackupUsers = info.getPendingBackups();
if (pendingBackupUsers != null) {
@@ -183,20 +184,20 @@ public class AppDataRollbackHelper {
}
/**
* Computes the list of pending restores for {@code userId} given lists of recent rollbacks.
* Computes the list of pending restores for {@code userId} given lists of rollbacks.
* Packages pending restore are added to {@code pendingRestores} along with their corresponding
* {@code PackageRollbackInfo}.
*
* @return the list of {@code RollbackInfo} that has pending restores. Note that some of the
* @return the list of {@code RollbackData} that has pending restores. Note that some of the
* restores won't be performed, because they might be counteracted by pending backups.
*/
private static List<RollbackInfo> computePendingRestores(int userId,
private static List<RollbackData> computePendingRestores(int userId,
Map<String, PackageRollbackInfo> pendingRestorePackages,
List<RollbackInfo> recentRollbacks) {
List<RollbackInfo> rd = new ArrayList<>();
List<RollbackData> rollbacks) {
List<RollbackData> rd = new ArrayList<>();
for (RollbackInfo data : recentRollbacks) {
for (PackageRollbackInfo info : data.getPackages()) {
for (RollbackData data : rollbacks) {
for (PackageRollbackInfo info : data.info.getPackages()) {
final RestoreInfo ri = info.getRestoreInfo(userId);
if (ri != null) {
pendingRestorePackages.put(info.getPackageName(), info);
@@ -215,18 +216,18 @@ public class AppDataRollbackHelper {
* backups updates corresponding {@code changedRollbackData} with a mapping from {@code userId}
* to a inode of theirs CE user data snapshot.
*
* @return a list {@code RollbackData} that have been changed and should be stored on disk.
* @return the set of {@code RollbackData} that have been changed and should be stored on disk.
*/
public List<RollbackData> commitPendingBackupAndRestoreForUser(int userId,
List<RollbackData> availableRollbacks, List<RollbackInfo> recentlyExecutedRollbacks) {
public Set<RollbackData> commitPendingBackupAndRestoreForUser(int userId,
List<RollbackData> rollbacks) {
final Map<String, PackageRollbackInfo> pendingBackupPackages = new HashMap<>();
final List<RollbackData> pendingBackups = computePendingBackups(userId,
pendingBackupPackages, availableRollbacks);
pendingBackupPackages, rollbacks);
final Map<String, PackageRollbackInfo> pendingRestorePackages = new HashMap<>();
final List<RollbackInfo> pendingRestores = computePendingRestores(userId,
pendingRestorePackages, recentlyExecutedRollbacks);
final List<RollbackData> pendingRestores = computePendingRestores(userId,
pendingRestorePackages, rollbacks);
// First remove unnecessary backups, i.e. when user did not unlock their phone between the
// request to backup data and the request to restore it.
@@ -266,13 +267,13 @@ public class AppDataRollbackHelper {
}
if (!pendingRestorePackages.isEmpty()) {
for (RollbackInfo data : pendingRestores) {
for (PackageRollbackInfo info : data.getPackages()) {
for (RollbackData data : pendingRestores) {
for (PackageRollbackInfo info : data.info.getPackages()) {
final RestoreInfo ri = info.getRestoreInfo(userId);
if (ri != null) {
try {
mInstaller.restoreAppDataSnapshot(info.getPackageName(), ri.appId,
ri.seInfo, userId, data.getRollbackId(),
ri.seInfo, userId, data.info.getRollbackId(),
Installer.FLAG_STORAGE_CE);
info.removeRestoreInfo(ri);
} catch (InstallerException ie) {
@@ -284,7 +285,9 @@ public class AppDataRollbackHelper {
}
}
return pendingBackups;
final Set<RollbackData> changed = new HashSet<>(pendingBackups);
changed.addAll(pendingRestores);
return changed;
}
/**

View File

@@ -16,9 +16,13 @@
package com.android.server.rollback;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.rollback.RollbackInfo;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.util.ArrayList;
@@ -27,6 +31,30 @@ import java.util.ArrayList;
* packages.
*/
class RollbackData {
@IntDef(flag = true, prefix = { "ROLLBACK_STATE_" }, value = {
ROLLBACK_STATE_ENABLING,
ROLLBACK_STATE_AVAILABLE,
ROLLBACK_STATE_COMMITTED,
})
@Retention(RetentionPolicy.SOURCE)
@interface RollbackState {}
/**
* The rollback is in the process of being enabled. It is not yet
* available for use.
*/
static final int ROLLBACK_STATE_ENABLING = 0;
/**
* The rollback is currently available.
*/
static final int ROLLBACK_STATE_AVAILABLE = 1;
/**
* The rollback has been committed.
*/
static final int ROLLBACK_STATE_COMMITTED = 3;
/**
* The rollback info for this rollback.
*/
@@ -40,22 +68,23 @@ class RollbackData {
/**
* The time when the upgrade occurred, for purposes of expiring
* rollback data.
*
* The timestamp is not applicable for all rollback states, but we make
* sure to keep it non-null to avoid potential errors there.
*/
public Instant timestamp;
public @NonNull Instant timestamp;
/**
* The session ID for the staged session if this rollback data represents a staged session,
* {@code -1} otherwise.
*/
public int stagedSessionId;
public final int stagedSessionId;
/**
* A flag to indicate whether the rollback should be considered available
* for use. This will always be true for rollbacks of non-staged sessions.
* For rollbacks of staged sessions, this is not set to true until after
* the staged session has been applied.
* The current state of the rollback.
* ENABLING, AVAILABLE, or COMMITTED.
*/
public boolean isAvailable;
public @RollbackState int state;
/**
* The id of the post-reboot apk session for a staged install, if any.
@@ -85,19 +114,20 @@ class RollbackData {
/* committedSessionId */ -1);
this.backupDir = backupDir;
this.stagedSessionId = stagedSessionId;
this.isAvailable = (stagedSessionId == -1);
this.state = ROLLBACK_STATE_ENABLING;
this.timestamp = Instant.now();
}
/**
* Constructs a RollbackData instance with full rollback data information.
*/
RollbackData(RollbackInfo info, File backupDir, Instant timestamp, int stagedSessionId,
boolean isAvailable, int apkSessionId, boolean restoreUserDataInProgress) {
@RollbackState int state, int apkSessionId, boolean restoreUserDataInProgress) {
this.info = info;
this.backupDir = backupDir;
this.timestamp = timestamp;
this.stagedSessionId = stagedSessionId;
this.isAvailable = isAvailable;
this.state = state;
this.apkSessionId = apkSessionId;
this.restoreUserDataInProgress = restoreUserDataInProgress;
}

View File

@@ -64,6 +64,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -75,7 +76,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
private static final String TAG = "RollbackManager";
// Rollbacks expire after 48 hours.
// TODO: How to test rollback expiration works properly?
private static final long DEFAULT_ROLLBACK_LIFETIME_DURATION_MILLIS =
TimeUnit.HOURS.toMillis(48);
@@ -106,15 +106,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
@GuardedBy("mLock")
private final Map<Integer, Integer> mChildSessions = new HashMap<>();
// Package rollback data available to be used for rolling back a package.
// The list of all rollbacks, including available and committed rollbacks.
// This list is null until the rollback data has been loaded.
@GuardedBy("mLock")
private List<RollbackData> mAvailableRollbacks;
// The list of recently executed rollbacks.
// This list is null until the rollback data has been loaded.
@GuardedBy("mLock")
private List<RollbackInfo> mRecentlyExecutedRollbacks;
private List<RollbackData> mRollbacks;
private final RollbackStore mRollbackStore;
@@ -176,17 +171,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
}, filter, null, getHandler());
// NOTE: A new intent filter is being created here because this broadcast
// doesn't use a data scheme ("package") like above.
IntentFilter sessionUpdatedFilter = new IntentFilter();
sessionUpdatedFilter.addAction(PackageInstaller.ACTION_SESSION_UPDATED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onStagedSessionUpdated(intent);
}
}, sessionUpdatedFilter, null, getHandler());
IntentFilter enableRollbackFilter = new IntentFilter();
enableRollbackFilter.addAction(Intent.ACTION_PACKAGE_ENABLE_ROLLBACK);
try {
@@ -240,9 +224,9 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
List<RollbackInfo> rollbacks = new ArrayList<>();
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
if (data.isAvailable) {
for (int i = 0; i < mRollbacks.size(); ++i) {
RollbackData data = mRollbacks.get(i);
if (data.state == RollbackData.ROLLBACK_STATE_AVAILABLE) {
rollbacks.add(data.info);
}
}
@@ -258,7 +242,13 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
List<RollbackInfo> rollbacks = new ArrayList<>(mRecentlyExecutedRollbacks);
List<RollbackInfo> rollbacks = new ArrayList<>();
for (int i = 0; i < mRollbacks.size(); ++i) {
RollbackData data = mRollbacks.get(i);
if (data.state == RollbackData.ROLLBACK_STATE_COMMITTED) {
rollbacks.add(data.info);
}
}
return new ParceledListSlice<>(rollbacks);
}
}
@@ -290,21 +280,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
Iterator<RollbackData> iter = mAvailableRollbacks.iterator();
Iterator<RollbackData> iter = mRollbacks.iterator();
while (iter.hasNext()) {
RollbackData data = iter.next();
data.timestamp = data.timestamp.plusMillis(timeDifference);
try {
mRollbackStore.saveRollbackData(data);
} catch (IOException ioe) {
// TODO: figure out the right way to deal with this, especially if
// it fails for some data and succeeds for others.
Log.e(TAG, "Unable to save rollback info for : "
+ data.info.getRollbackId(), ioe);
}
saveRollbackData(data);
}
}
}
};
@@ -328,18 +309,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
Log.i(TAG, "Initiating rollback");
RollbackData data = getRollbackForId(rollbackId);
if (data == null) {
if (data == null || data.state != RollbackData.ROLLBACK_STATE_AVAILABLE) {
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE,
"Rollback unavailable");
return;
}
if (data.restoreUserDataInProgress) {
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE,
"Rollback for package is already in progress.");
return;
}
// Verify the RollbackData is up to date with what's installed on
// device.
// TODO: We assume that between now and the time we commit the
@@ -438,13 +413,25 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
final LocalIntentReceiver receiver = new LocalIntentReceiver(
(Intent result) -> {
getHandler().post(() -> {
// We've now completed the rollback, so we mark it as no longer in
// progress.
data.restoreUserDataInProgress = false;
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
if (status != PackageInstaller.STATUS_SUCCESS) {
// Committing the rollback failed, but we
// still have all the info we need to try
// rolling back again, so restore the rollback
// state to how it was before we tried
// committing.
// TODO: Should we just kill this rollback if
// commit failed? Why would we expect commit
// not to fail again?
synchronized (mLock) {
// TODO: Could this cause a rollback to be
// resurrected if it should otherwise have
// expired by now?
data.state = RollbackData.ROLLBACK_STATE_AVAILABLE;
data.restoreUserDataInProgress = false;
}
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_INSTALL,
"Rollback downgrade install failed: "
+ result.getStringExtra(
@@ -452,9 +439,19 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
return;
}
data.info.setCommittedSessionId(parentSessionId);
data.info.getCausePackages().addAll(causePackages);
addRecentlyExecutedRollback(data.info);
synchronized (mLock) {
if (!data.isStaged()) {
// All calls to restoreUserData should have
// completed by now for a non-staged install.
data.restoreUserDataInProgress = false;
}
data.info.setCommittedSessionId(parentSessionId);
data.info.getCausePackages().addAll(causePackages);
}
mRollbackStore.deletePackageCodePaths(data);
saveRollbackData(data);
sendSuccess(statusReceiver);
Intent broadcast = new Intent(Intent.ACTION_ROLLBACK_COMMITTED);
@@ -468,7 +465,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
);
data.restoreUserDataInProgress = true;
synchronized (mLock) {
data.state = RollbackData.ROLLBACK_STATE_COMMITTED;
data.restoreUserDataInProgress = true;
}
parentSession.commit(receiver.getIntentSender());
} catch (IOException e) {
Log.e(TAG, "Rollback failed", e);
@@ -485,8 +485,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
"reloadPersistedData");
synchronized (mLock) {
mAvailableRollbacks = null;
mRecentlyExecutedRollbacks = null;
mRollbacks = null;
}
getHandler().post(() -> {
updateRollbackLifetimeDurationInMillis();
@@ -499,14 +498,9 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.MANAGE_ROLLBACKS,
"expireRollbackForPackage");
// TODO: Should this take a package version number in addition to
// package name? For now, just remove all rollbacks matching the
// package name. This method is only currently used to facilitate
// testing anyway.
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
Iterator<RollbackData> iter = mAvailableRollbacks.iterator();
Iterator<RollbackData> iter = mRollbacks.iterator();
while (iter.hasNext()) {
RollbackData data = iter.next();
for (PackageRollbackInfo info : data.info.getPackages()) {
@@ -522,29 +516,16 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
void onUnlockUser(int userId) {
getHandler().post(() -> {
final List<RollbackData> availableRollbacks;
final List<RollbackInfo> recentlyExecutedRollbacks;
final List<RollbackData> rollbacks;
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
availableRollbacks = new ArrayList<>(mAvailableRollbacks);
recentlyExecutedRollbacks = new ArrayList<>(mRecentlyExecutedRollbacks);
rollbacks = new ArrayList<>(mRollbacks);
}
final List<RollbackData> changed =
mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(userId,
availableRollbacks, recentlyExecutedRollbacks);
final Set<RollbackData> changed =
mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(userId, rollbacks);
for (RollbackData rd : changed) {
try {
mRollbackStore.saveRollbackData(rd);
} catch (IOException ioe) {
Log.e(TAG, "Unable to save rollback info for : "
+ rd.info.getRollbackId(), ioe);
}
}
synchronized (mLock) {
mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
saveRollbackData(rd);
}
});
}
@@ -569,42 +550,55 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
scheduleExpiration(0);
getHandler().post(() -> {
// Check to see if any staged sessions with rollback enabled have
// been applied.
List<RollbackData> staged = new ArrayList<>();
// Check to see if any rollback-enabled staged sessions or staged
// rollback sessions been applied.
List<RollbackData> enabling = new ArrayList<>();
List<RollbackData> restoreInProgress = new ArrayList<>();
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
for (RollbackData data : mAvailableRollbacks) {
if (!data.isAvailable && data.isStaged()) {
staged.add(data);
for (RollbackData data : mRollbacks) {
if (data.isStaged()) {
if (data.state == RollbackData.ROLLBACK_STATE_ENABLING) {
enabling.add(data);
} else if (data.restoreUserDataInProgress) {
restoreInProgress.add(data);
}
}
}
}
for (RollbackData data : staged) {
for (RollbackData data : enabling) {
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
PackageInstaller.SessionInfo session = installer.getSessionInfo(
data.stagedSessionId);
// TODO: What if session is null?
if (session != null) {
if (session.isStagedSessionApplied()) {
synchronized (mLock) {
data.isAvailable = true;
}
try {
mRollbackStore.saveRollbackData(data);
} catch (IOException ioe) {
Log.e(TAG, "Unable to save rollback info for : "
+ data.info.getRollbackId(), ioe);
}
makeRollbackAvailable(data);
} else if (session.isStagedSessionFailed()) {
// TODO: Do we need to remove this from
// mAvailableRollbacks, or is it okay to leave as
// mRollbacks, or is it okay to leave as
// unavailable until the next reboot when it will go
// away on its own?
deleteRollback(data);
}
}
}
for (RollbackData data : restoreInProgress) {
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
PackageInstaller.SessionInfo session = installer.getSessionInfo(
data.stagedSessionId);
// TODO: What if session is null?
if (session != null) {
if (session.isStagedSessionApplied() || session.isStagedSessionFailed()) {
synchronized (mLock) {
data.restoreUserDataInProgress = false;
}
saveRollbackData(data);
}
}
}
});
}
@@ -621,12 +615,11 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
/**
* Load rollback data from storage if it has not already been loaded.
* After calling this function, mAvailableRollbacks and
* mRecentlyExecutedRollbacks will be non-null.
* After calling this function, mRollbacks will be non-null.
*/
@GuardedBy("mLock")
private void ensureRollbackDataLoadedLocked() {
if (mAvailableRollbacks == null) {
if (mRollbacks == null) {
loadAllRollbackDataLocked();
}
}
@@ -639,15 +632,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
*/
@GuardedBy("mLock")
private void loadAllRollbackDataLocked() {
mAvailableRollbacks = mRollbackStore.loadAvailableRollbacks();
for (RollbackData data : mAvailableRollbacks) {
mRollbacks = mRollbackStore.loadAllRollbackData();
for (RollbackData data : mRollbacks) {
mAllocatedRollbackIds.put(data.info.getRollbackId(), true);
}
mRecentlyExecutedRollbacks = mRollbackStore.loadRecentlyExecutedRollbacks();
for (RollbackInfo info : mRecentlyExecutedRollbacks) {
mAllocatedRollbackIds.put(info.getRollbackId(), true);
}
}
/**
@@ -662,17 +650,21 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
Iterator<RollbackData> iter = mAvailableRollbacks.iterator();
Iterator<RollbackData> iter = mRollbacks.iterator();
while (iter.hasNext()) {
RollbackData data = iter.next();
for (PackageRollbackInfo info : data.info.getPackages()) {
if (info.getPackageName().equals(packageName)
&& !packageVersionsEqual(
info.getVersionRolledBackFrom(),
installedVersion)) {
iter.remove();
deleteRollback(data);
break;
// TODO: Should we remove rollbacks in the ENABLING state here?
if (data.state == RollbackData.ROLLBACK_STATE_AVAILABLE
|| data.state == RollbackData.ROLLBACK_STATE_ENABLING) {
for (PackageRollbackInfo info : data.info.getPackages()) {
if (info.getPackageName().equals(packageName)
&& !packageVersionsEqual(
info.getVersionRolledBackFrom(),
installedVersion)) {
iter.remove();
deleteRollback(data);
break;
}
}
}
}
@@ -685,53 +677,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
*/
private void onPackageFullyRemoved(String packageName) {
expireRollbackForPackage(packageName);
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
Iterator<RollbackInfo> iter = mRecentlyExecutedRollbacks.iterator();
boolean changed = false;
while (iter.hasNext()) {
RollbackInfo rollback = iter.next();
for (PackageRollbackInfo info : rollback.getPackages()) {
if (packageName.equals(info.getPackageName())) {
iter.remove();
changed = true;
break;
}
}
}
if (changed) {
mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
}
}
}
/**
* Records that the given package has been recently rolled back.
*/
private void addRecentlyExecutedRollback(RollbackInfo rollback) {
// TODO: if the list of rollbacks gets too big, trim it to only those
// that are necessary to keep track of.
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
// This should never happen because we can't have any pending backups left after
// a rollback has been executed. See AppDataRollbackHelper#restoreAppData where we
// clear all pending backups at the point of restore because they're guaranteed to be
// no-ops.
//
// We may, however, have one or more pending restores left to handle.
for (PackageRollbackInfo target : rollback.getPackages()) {
if (target.getPendingBackups().size() > 0) {
Log.e(TAG, "No backups allowed to be pending for: " + target);
target.getPendingBackups().clear();
}
}
mRecentlyExecutedRollbacks.add(rollback);
mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
}
}
/**
@@ -768,17 +713,16 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
// Check to see if anything needs expiration, and if so, expire it.
// Schedules future expiration as appropriate.
// TODO: Handle cases where the user changes time on the device.
private void runExpiration() {
Instant now = Instant.now();
Instant oldest = null;
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
Iterator<RollbackData> iter = mAvailableRollbacks.iterator();
Iterator<RollbackData> iter = mRollbacks.iterator();
while (iter.hasNext()) {
RollbackData data = iter.next();
if (!data.isAvailable) {
if (data.state != RollbackData.ROLLBACK_STATE_AVAILABLE) {
continue;
}
if (!now.isBefore(data.timestamp.plusMillis(mRollbackLifetimeDurationInMillis))) {
@@ -876,8 +820,8 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
RollbackData rd = null;
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
for (int i = 0; i < mRollbacks.size(); ++i) {
RollbackData data = mRollbacks.get(i);
if (data.apkSessionId == parentSessionId) {
rd = data;
break;
@@ -901,16 +845,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
if (info.getPackageName().equals(packageName)) {
info.getInstalledUsers().addAll(IntArray.wrap(installedUsers));
mAppDataRollbackHelper.snapshotAppData(rd.info.getRollbackId(), info);
try {
mRollbackStore.saveRollbackData(rd);
} catch (IOException ioe) {
// TODO: Hopefully this is okay because we will try
// again to save the rollback when the staged session
// is applied. Just so long as the device doesn't
// reboot before then.
Log.e(TAG, "Unable to save rollback info for : "
+ rd.info.getRollbackId(), ioe);
}
saveRollbackData(rd);
return true;
}
}
@@ -1039,32 +974,33 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
private void restoreUserDataInternal(String packageName, int[] userIds, int appId,
long ceDataInode, String seInfo, int token) {
final RollbackData rollbackData = getRollbackForPackage(packageName);
PackageRollbackInfo info = null;
RollbackData rollbackData = null;
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
for (int i = 0; i < mRollbacks.size(); ++i) {
RollbackData data = mRollbacks.get(i);
if (data.restoreUserDataInProgress) {
info = getPackageRollbackInfo(data, packageName);
if (info != null) {
rollbackData = data;
break;
}
}
}
}
if (rollbackData == null) {
return;
}
if (!rollbackData.restoreUserDataInProgress) {
Log.e(TAG, "Request to restore userData for: " + packageName
+ ", but no rollback in progress.");
return;
}
for (int userId : userIds) {
final PackageRollbackInfo info = getPackageRollbackInfo(rollbackData, packageName);
final boolean changedRollbackData = mAppDataRollbackHelper.restoreAppData(
rollbackData.info.getRollbackId(), info, userId, appId, seInfo);
// We've updated metadata about this rollback, so save it to flash.
if (changedRollbackData) {
try {
mRollbackStore.saveRollbackData(rollbackData);
} catch (IOException ioe) {
// TODO(narayan): What is the right thing to do here ? This isn't a fatal
// error, since it will only result in us trying to restore data again,
// which will be a no-op if there's no data available.
Log.e(TAG, "Unable to save available rollback: " + packageName, ioe);
}
saveRollbackData(rollbackData);
}
}
}
@@ -1108,6 +1044,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
}
completeEnableRollback(sessionId, true);
result.offer(true);
});
@@ -1125,8 +1062,8 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
RollbackData rd = null;
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
for (int i = 0; i < mRollbacks.size(); ++i) {
RollbackData data = mRollbacks.get(i);
if (data.stagedSessionId == originalSessionId) {
data.apkSessionId = apkSessionId;
rd = data;
@@ -1136,12 +1073,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
if (rd != null) {
try {
mRollbackStore.saveRollbackData(rd);
} catch (IOException ioe) {
Log.e(TAG, "Unable to save rollback info for : "
+ rd.info.getRollbackId(), ioe);
}
saveRollbackData(rd);
}
});
}
@@ -1183,21 +1115,21 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
@Override
public void onFinished(int sessionId, boolean success) {
// If sessionId refers to a staged session, we can't deal with it here since the
// session might take an unbounded amount of time to become "ready" after the package
// installer session is committed. In those cases, we respond to it in response to
// a session ready broadcast.
PackageInstaller packageInstaller = mContext.getPackageManager().getPackageInstaller();
PackageInstaller.SessionInfo si = packageInstaller.getSessionInfo(sessionId);
if (si != null && si.isStaged()) {
return;
RollbackData rollback = completeEnableRollback(sessionId, success);
if (rollback != null && !rollback.isStaged()) {
makeRollbackAvailable(rollback);
}
completeEnableRollback(sessionId, success);
}
}
private void completeEnableRollback(int sessionId, boolean success) {
/**
* Add a rollback to the list of rollbacks.
* This should be called after rollback has been enabled for all packages
* in the rollback. It does not make the rollback available yet.
*
* @return the rollback data for a successfully enable-completed rollback.
*/
private RollbackData completeEnableRollback(int sessionId, boolean success) {
RollbackData data = null;
synchronized (mLock) {
Integer parentSessionId = mChildSessions.remove(sessionId);
@@ -1208,107 +1140,71 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
data = mPendingRollbacks.remove(sessionId);
}
if (data != null) {
if (success) {
try {
data.timestamp = Instant.now();
mRollbackStore.saveRollbackData(data);
synchronized (mLock) {
// Note: There is a small window of time between when
// the session has been committed by the package
// manager and when we make the rollback available
// here. Presumably the window is small enough that
// nobody will want to roll back the newly installed
// package before we make the rollback available.
// TODO: We'll lose the rollback data if the
// device reboots between when the session is
// committed and this point. Revisit this after
// adding support for rollback of staged installs.
ensureRollbackDataLoadedLocked();
mAvailableRollbacks.add(data);
}
// TODO(zezeozue): Provide API to explicitly start observing instead
// of doing this for all rollbacks. If we do this for all rollbacks,
// should document in PackageInstaller.SessionParams#setEnableRollback
// After enabling and commiting any rollback, observe packages and
// prepare to rollback if packages crashes too frequently.
List<String> packages = new ArrayList<>();
for (int i = 0; i < data.info.getPackages().size(); i++) {
packages.add(data.info.getPackages().get(i).getPackageName());
}
mPackageHealthObserver.startObservingHealth(packages,
mRollbackLifetimeDurationInMillis);
scheduleExpiration(mRollbackLifetimeDurationInMillis);
} catch (IOException e) {
Log.e(TAG, "Unable to enable rollback", e);
deleteRollback(data);
}
} else {
// The install session was aborted, clean up the pending
// install.
deleteRollback(data);
}
}
}
private void onStagedSessionUpdated(Intent intent) {
PackageInstaller.SessionInfo pi = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
if (pi == null) {
Log.e(TAG, "Missing intent extra: " + PackageInstaller.EXTRA_SESSION);
return;
if (data == null) {
return null;
}
if (pi.isStaged()) {
if (!pi.isStagedSessionFailed()) {
// TODO: The session really isn't "enabled" at this point, since more work might
// be required post reboot.
// TODO: We need to make this case consistent with the call from onFinished.
// Ideally, we'd call completeEnableRollback excatly once per multi-package session
// with the parentSessionId only.
completeEnableRollback(pi.sessionId, pi.isStagedSessionReady());
} else {
// TODO: Clean up the saved rollback when the session fails. This may need to be
// unified with the case where things fail post reboot.
}
} else {
Log.e(TAG, "Received onStagedSessionUpdated for: " + pi.sessionId
+ ", which isn't staged");
if (!success) {
// The install session was aborted, clean up the pending install.
deleteRollback(data);
return null;
}
}
/*
* Returns the RollbackData, if any, for an available rollback that would
* roll back the given package. Note: This assumes we have at most one
* available rollback for a given package at any one time.
*/
private RollbackData getRollbackForPackage(String packageName) {
saveRollbackData(data);
synchronized (mLock) {
// TODO: Have ensureRollbackDataLoadedLocked return the list of
// available rollbacks, to hopefully avoid forgetting to call it?
// Note: There is a small window of time between when
// the session has been committed by the package
// manager and when we make the rollback available
// here. Presumably the window is small enough that
// nobody will want to roll back the newly installed
// package before we make the rollback available.
// TODO: We'll lose the rollback data if the
// device reboots between when the session is
// committed and this point. Revisit this after
// adding support for rollback of staged installs.
ensureRollbackDataLoadedLocked();
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
if (data.isAvailable && getPackageRollbackInfo(data, packageName) != null) {
return data;
}
}
mRollbacks.add(data);
}
return null;
return data;
}
private void makeRollbackAvailable(RollbackData data) {
// TODO: What if the rollback has since been expired, for example due
// to a new package being installed. Won't this revive an expired
// rollback? Consider adding a ROLLBACK_STATE_EXPIRED to address this.
synchronized (mLock) {
data.state = RollbackData.ROLLBACK_STATE_AVAILABLE;
data.timestamp = Instant.now();
}
saveRollbackData(data);
// TODO(zezeozue): Provide API to explicitly start observing instead
// of doing this for all rollbacks. If we do this for all rollbacks,
// should document in PackageInstaller.SessionParams#setEnableRollback
// After enabling and commiting any rollback, observe packages and
// prepare to rollback if packages crashes too frequently.
List<String> packages = new ArrayList<>();
for (int i = 0; i < data.info.getPackages().size(); i++) {
packages.add(data.info.getPackages().get(i).getPackageName());
}
mPackageHealthObserver.startObservingHealth(packages,
mRollbackLifetimeDurationInMillis);
scheduleExpiration(mRollbackLifetimeDurationInMillis);
}
/*
* Returns the RollbackData, if any, for an available rollback with the
* given rollbackId.
* Returns the RollbackData, if any, for a rollback with the given
* rollbackId.
*/
private RollbackData getRollbackForId(int rollbackId) {
synchronized (mLock) {
// TODO: Have ensureRollbackDataLoadedLocked return the list of
// available rollbacks, to hopefully avoid forgetting to call it?
ensureRollbackDataLoadedLocked();
for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
RollbackData data = mAvailableRollbacks.get(i);
if (data.isAvailable && data.info.getRollbackId() == rollbackId) {
for (int i = 0; i < mRollbacks.size(); ++i) {
RollbackData data = mRollbacks.get(i);
if (data.info.getRollbackId() == rollbackId) {
return data;
}
}
@@ -1358,4 +1254,19 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
mRollbackStore.deleteRollbackData(rollbackData);
}
/**
* Saves rollback data, swallowing any IOExceptions.
* For those times when it's not obvious what to do about the IOException.
* TODO: Double check we can't do a better job handling the IOException in
* a cases where this method is called.
*/
private void saveRollbackData(RollbackData rollbackData) {
try {
mRollbackStore.saveRollbackData(rollbackData);
} catch (IOException ioe) {
Log.e(TAG, "Unable to save rollback info for: "
+ rollbackData.info.getRollbackId(), ioe);
}
}
}

View File

@@ -35,6 +35,7 @@ import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.text.ParseException;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
@@ -47,60 +48,44 @@ class RollbackStore {
private static final String TAG = "RollbackManager";
// Assuming the rollback data directory is /data/rollback, we use the
// following directory structure to store persisted data for available and
// recently executed rollbacks:
// following directory structure to store persisted data for rollbacks:
// /data/rollback/
// available/
// XXX/
// rollback.json
// com.package.A/
// base.apk
// com.package.B/
// base.apk
// YYY/
// rollback.json
// com.package.C/
// base.apk
// recently_executed.json
// XXX/
// rollback.json
// com.package.A/
// base.apk
// com.package.B/
// base.apk
// YYY/
// rollback.json
//
// * XXX, YYY are the rollbackIds for the corresponding rollbacks.
// * rollback.json contains all relevant metadata for the rollback. This
// file is not written until the rollback is made available.
// * rollback.json contains all relevant metadata for the rollback.
//
// TODO: Use AtomicFile for all the .json files?
private final File mRollbackDataDir;
private final File mAvailableRollbacksDir;
private final File mRecentlyExecutedRollbacksFile;
RollbackStore(File rollbackDataDir) {
mRollbackDataDir = rollbackDataDir;
mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
}
/**
* Reads the list of available rollbacks from persistent storage.
* Reads the rollback data from persistent storage.
*/
List<RollbackData> loadAvailableRollbacks() {
List<RollbackData> availableRollbacks = new ArrayList<>();
mAvailableRollbacksDir.mkdirs();
for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
List<RollbackData> loadAllRollbackData() {
List<RollbackData> rollbacks = new ArrayList<>();
mRollbackDataDir.mkdirs();
for (File rollbackDir : mRollbackDataDir.listFiles()) {
if (rollbackDir.isDirectory()) {
try {
RollbackData data = loadRollbackData(rollbackDir);
availableRollbacks.add(data);
rollbacks.add(loadRollbackData(rollbackDir));
} catch (IOException e) {
// Note: Deleting the rollbackDir here will cause pending
// rollbacks to be deleted. This should only ever happen
// if reloadPersistedData is called while there are
// pending rollbacks. The reloadPersistedData method is
// currently only for testing, so that should be okay.
Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
removeFile(rollbackDir);
}
}
}
return availableRollbacks;
return rollbacks;
}
/**
@@ -202,38 +187,12 @@ class RollbackStore {
json.getInt("committedSessionId"));
}
/**
* Reads the list of recently executed rollbacks from persistent storage.
*/
List<RollbackInfo> loadRecentlyExecutedRollbacks() {
List<RollbackInfo> recentlyExecutedRollbacks = new ArrayList<>();
if (mRecentlyExecutedRollbacksFile.exists()) {
try {
// TODO: How to cope with changes to the format of this file from
// when RollbackStore is updated in the future?
String jsonString = IoUtils.readFileAsString(
mRecentlyExecutedRollbacksFile.getAbsolutePath());
JSONObject object = new JSONObject(jsonString);
JSONArray array = object.getJSONArray("recentlyExecuted");
for (int i = 0; i < array.length(); ++i) {
recentlyExecutedRollbacks.add(rollbackInfoFromJson(array.getJSONObject(i)));
}
} catch (IOException | JSONException e) {
// TODO: What to do here? Surely we shouldn't just forget about
// everything after the point of exception?
Log.e(TAG, "Failed to read recently executed rollbacks", e);
}
}
return recentlyExecutedRollbacks;
}
/**
* Creates a new RollbackData instance for a non-staged rollback with
* backupDir assigned.
*/
RollbackData createNonStagedRollback(int rollbackId) throws IOException {
File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
return new RollbackData(rollbackId, backupDir, -1);
}
@@ -243,7 +202,7 @@ class RollbackStore {
*/
RollbackData createStagedRollback(int rollbackId, int stagedSessionId)
throws IOException {
File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
return new RollbackData(rollbackId, backupDir, stagedSessionId);
}
@@ -276,6 +235,17 @@ class RollbackStore {
return files;
}
/**
* Deletes all backed up apks and apex files associated with the given
* rollback.
*/
static void deletePackageCodePaths(RollbackData data) {
for (PackageRollbackInfo info : data.info.getPackages()) {
File targetDir = new File(data.backupDir, info.getPackageName());
removeFile(targetDir);
}
}
/**
* Saves the rollback data to persistent storage.
*/
@@ -285,7 +255,7 @@ class RollbackStore {
dataJson.put("info", rollbackInfoToJson(data.info));
dataJson.put("timestamp", data.timestamp.toString());
dataJson.put("stagedSessionId", data.stagedSessionId);
dataJson.put("isAvailable", data.isAvailable);
dataJson.put("state", rollbackStateToString(data.state));
dataJson.put("apkSessionId", data.apkSessionId);
dataJson.put("restoreUserDataInProgress", data.restoreUserDataInProgress);
@@ -304,29 +274,6 @@ class RollbackStore {
removeFile(data.backupDir);
}
/**
* Writes the list of recently executed rollbacks to storage.
*/
void saveRecentlyExecutedRollbacks(List<RollbackInfo> recentlyExecutedRollbacks) {
try {
JSONObject json = new JSONObject();
JSONArray array = new JSONArray();
json.put("recentlyExecuted", array);
for (int i = 0; i < recentlyExecutedRollbacks.size(); ++i) {
RollbackInfo rollback = recentlyExecutedRollbacks.get(i);
array.put(rollbackInfoToJson(rollback));
}
PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile);
pw.println(json.toString());
pw.close();
} catch (IOException | JSONException e) {
// TODO: What to do here?
Log.e(TAG, "Failed to save recently executed rollbacks", e);
}
}
/**
* Reads the metadata for a rollback from the given directory.
* @throws IOException in case of error reading the data.
@@ -342,10 +289,10 @@ class RollbackStore {
backupDir,
Instant.parse(dataJson.getString("timestamp")),
dataJson.getInt("stagedSessionId"),
dataJson.getBoolean("isAvailable"),
rollbackStateFromString(dataJson.getString("state")),
dataJson.getInt("apkSessionId"),
dataJson.getBoolean("restoreUserDataInProgress"));
} catch (JSONException | DateTimeParseException e) {
} catch (JSONException | DateTimeParseException | ParseException e) {
throw new IOException(e);
}
}
@@ -444,7 +391,7 @@ class RollbackStore {
* If the file is a directory, its contents are deleted as well.
* Has no effect if the directory does not exist.
*/
private void removeFile(File file) {
private static void removeFile(File file) {
if (file.isDirectory()) {
for (File child : file.listFiles()) {
removeFile(child);
@@ -454,4 +401,23 @@ class RollbackStore {
file.delete();
}
}
private static String rollbackStateToString(@RollbackData.RollbackState int state) {
switch (state) {
case RollbackData.ROLLBACK_STATE_ENABLING: return "enabling";
case RollbackData.ROLLBACK_STATE_AVAILABLE: return "available";
case RollbackData.ROLLBACK_STATE_COMMITTED: return "committed";
}
throw new AssertionError("Invalid rollback state: " + state);
}
private static @RollbackData.RollbackState int rollbackStateFromString(String state)
throws ParseException {
switch (state) {
case "enabling": return RollbackData.ROLLBACK_STATE_ENABLING;
case "available": return RollbackData.ROLLBACK_STATE_AVAILABLE;
case "committed": return RollbackData.ROLLBACK_STATE_COMMITTED;
}
throw new ParseException("Invalid rollback state: " + state, 0);
}
}

View File

@@ -31,7 +31,6 @@ import static org.mockito.Mockito.when;
import android.content.pm.VersionedPackage;
import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.PackageRollbackInfo.RestoreInfo;
import android.content.rollback.RollbackInfo;
import android.util.IntArray;
import android.util.SparseLongArray;
@@ -46,8 +45,7 @@ import org.mockito.Mockito;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@RunWith(JUnit4.class)
public class AppDataRollbackHelperTest {
@@ -247,13 +245,13 @@ public class AppDataRollbackHelperTest {
-1);
dataForDifferentUser.info.getPackages().add(ignoredInfo);
RollbackInfo rollbackInfo = new RollbackInfo(17239,
Arrays.asList(pendingRestore, wasRecentlyRestored), false,
new ArrayList<>(), -1);
RollbackData dataForRestore = new RollbackData(17239, new File("/does/not/exist"), -1);
dataForRestore.info.getPackages().add(pendingRestore);
dataForRestore.info.getPackages().add(wasRecentlyRestored);
List<RollbackData> changed = helper.commitPendingBackupAndRestoreForUser(37,
Arrays.asList(dataWithPendingBackup, dataWithRecentRestore, dataForDifferentUser),
Collections.singletonList(rollbackInfo));
Set<RollbackData> changed = helper.commitPendingBackupAndRestoreForUser(37,
Arrays.asList(dataWithPendingBackup, dataWithRecentRestore, dataForDifferentUser,
dataForRestore));
InOrder inOrder = Mockito.inOrder(installer);
// Check that pending backup and restore for the same package mutually destroyed each other.
@@ -267,9 +265,10 @@ public class AppDataRollbackHelperTest {
assertEquals(53, pendingBackup.getCeSnapshotInodes().get(37));
// Check that changed returns correct RollbackData.
assertEquals(2, changed.size());
assertEquals(dataWithPendingBackup, changed.get(0));
assertEquals(dataWithRecentRestore, changed.get(1));
assertEquals(3, changed.size());
assertTrue(changed.contains(dataWithPendingBackup));
assertTrue(changed.contains(dataWithRecentRestore));
assertTrue(changed.contains(dataForRestore));
// Check that restore was performed.
inOrder.verify(installer).restoreAppDataSnapshot(