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

Commit fcd4a04c authored by Narayan Kamath's avatar Narayan Kamath
Browse files

Staged Rollbacks Part 1: Pre reboot data copy.

For staged sessions, the staging manager calls through to the
Rollback manager during its pre-reboot flow if package rollback
is enabled for the session. It passes through a list of sessionIds
representing a single package session or siblings in a multi package
session that rollback must be enabled for. The rollback manager then
performs the following actions :

- Makes a copy of APKs and APEXes corresponding to the sessionIds
  provided by the staging manager.

- Creates and persists RollbackData objects for the staged session.
  and marks that they're part of a staged install and not a regular
  install. This change augments RollbackData with sessionIds of
  the staged session and PackageRollbackInfo with a flag indicating
  whether or not it represents an APEX.

In a follow up change, the StagingManager will call through to the
RollbackManager post reboot and provide a mapping between the staged
session ID for the APK section of the staged install. This will allow
the RollbackManager to make userdata backups and finally mark the Rollback
as available.

Test: atest RollbackTest AppDataRollbackHelperTest
Test: adb install-multi-package --staged --enable-rollback \
        ./apex.test.apex ./RollbackTestAppAv2.apk

Change-Id: I4a7b154844684ddb26b7c95d39be36f542fb5d4b
parent e51c8d32
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -40,4 +40,11 @@ interface IRollbackManager {

    // Exposed for test purposes only.
    void expireRollbackForPackage(String packageName);

    // Used by the staging manager to notify the RollbackManager that a session is
    // being staged. In the case of multi-package sessions, the specified sessionId
    // is that of the parent session.
    //
    // NOTE: This call is synchronous.
    boolean notifyStagedSession(int sessionId);
}
+15 −1
Original line number Diff line number Diff line
@@ -68,6 +68,11 @@ public final class PackageRollbackInfo implements Parcelable {
    // NOTE: Not a part of the Parcelable representation of this object.
    private final ArrayList<RestoreInfo> mPendingRestores;

    /**
     * Whether this instance represents the PackageRollbackInfo for an APEX module.
     */
    private final boolean mIsApex;

    /**
     * Returns the name of the package to roll back from.
     */
@@ -115,19 +120,27 @@ public final class PackageRollbackInfo implements Parcelable {
        mPendingRestores.remove(ri);
    }

    /** @hide */
    public boolean isApex() {
        return mIsApex;
    }

    /** @hide */
    public PackageRollbackInfo(VersionedPackage packageRolledBackFrom,
            VersionedPackage packageRolledBackTo,
            @NonNull IntArray pendingBackups, @NonNull ArrayList<RestoreInfo> pendingRestores) {
            @NonNull IntArray pendingBackups, @NonNull ArrayList<RestoreInfo> pendingRestores,
            boolean isApex) {
        this.mVersionRolledBackFrom = packageRolledBackFrom;
        this.mVersionRolledBackTo = packageRolledBackTo;
        this.mPendingBackups = pendingBackups;
        this.mPendingRestores = pendingRestores;
        this.mIsApex = isApex;
    }

    private PackageRollbackInfo(Parcel in) {
        this.mVersionRolledBackFrom = VersionedPackage.CREATOR.createFromParcel(in);
        this.mVersionRolledBackTo = VersionedPackage.CREATOR.createFromParcel(in);
        this.mIsApex = in.readBoolean();
        this.mPendingRestores = null;
        this.mPendingBackups = null;
    }
@@ -141,6 +154,7 @@ public final class PackageRollbackInfo implements Parcelable {
    public void writeToParcel(Parcel out, int flags) {
        mVersionRolledBackFrom.writeToParcel(out, flags);
        mVersionRolledBackTo.writeToParcel(out, flags);
        out.writeBoolean(mIsApex);
    }

    public static final Parcelable.Creator<PackageRollbackInfo> CREATOR =
+19 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.apex.ApexInfo;
import android.apex.ApexInfoList;
import android.apex.ApexSessionInfo;
import android.apex.IApexService;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
@@ -33,6 +34,7 @@ import android.content.pm.PackageParser.SigningDetails;
import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
import android.content.pm.ParceledListSlice;
import android.content.pm.Signature;
import android.content.rollback.IRollbackManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -238,6 +240,23 @@ public class StagingManager {
            }
        }

        if ((session.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
            // If rollback is enabled for this session, we call through to the RollbackManager
            // with the list of sessions it must enable rollback for. Note that notifyStagedSession
            // is a synchronous operation.
            final IRollbackManager rm = IRollbackManager.Stub.asInterface(
                    ServiceManager.getService(Context.ROLLBACK_SERVICE));
            try {
                // NOTE: To stay consistent with the non-staged install flow, we don't fail the
                // entire install if rollbacks can't be enabled.
                if (!rm.notifyStagedSession(session.sessionId)) {
                    Slog.e(TAG, "Unable to enable rollback for session: " + session.sessionId);
                }
            } catch (RemoteException re) {
                // Cannot happen, the rollback manager is in the same process.
            }
        }

        session.setStagedSessionReady();
        if (!sendMarkStagedSessionReadyRequest(session.sessionId)) {
            session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
+8 −1
Original line number Diff line number Diff line
@@ -49,6 +49,12 @@ class RollbackData {
     */
    public Instant timestamp;

    /**
     * The session ID for the staged session if this rollback data represents a staged session,
     * {@code -1} otherwise.
     */
    public int stagedSessionId;

    /**
     * Whether this Rollback is currently in progress. This field is true from the point
     * we commit a {@code PackageInstaller} session containing these packages to the point the
@@ -57,8 +63,9 @@ class RollbackData {
    // NOTE: All accesses to this field are from the RollbackManager handler thread.
    public boolean inProgress = false;

    RollbackData(int rollbackId, File backupDir) {
    RollbackData(int rollbackId, File backupDir, int stagedSessionId) {
        this.rollbackId = rollbackId;
        this.backupDir = backupDir;
        this.stagedSessionId = stagedSessionId;
    }
}
+163 −57
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Implementation of service that manages APK level rollbacks.
@@ -156,6 +157,17 @@ 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 {
@@ -718,7 +730,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
            return false;
        }

        return enableRollbackForSession(session, installedUsers);
        return enableRollbackForSession(session, installedUsers, true);
    }

    /**
@@ -727,7 +739,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
     * the child sessions, not the parent session.
     */
    private boolean enableRollbackForSession(PackageInstaller.SessionInfo session,
            int[] installedUsers) {
            int[] installedUsers, boolean snapshotUserData) {
        // TODO: Don't attempt to enable rollback for split installs.
        final int installFlags = session.installFlags;
        if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
@@ -749,15 +761,17 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        }

        String packageName = newPackage.packageName;
        Log.i(TAG, "Enabling rollback for install of " + packageName);
        Log.i(TAG, "Enabling rollback for install of " + packageName
                + ", session:" + session.sessionId);

        VersionedPackage newVersion = new VersionedPackage(packageName, newPackage.versionCode);
        final boolean isApex = ((installFlags & PackageManager.INSTALL_APEX) != 0);

        // Get information about the currently installed package.
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pkgInfo = null;
        try {
            pkgInfo = pm.getPackageInfo(packageName, 0);
            pkgInfo = pm.getPackageInfo(packageName, isApex ? PackageManager.MATCH_APEX : 0);
        } catch (PackageManager.NameNotFoundException e) {
            // TODO: Support rolling back fresh package installs rather than
            // fail here. Test this case.
@@ -768,17 +782,13 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        VersionedPackage installedVersion = new VersionedPackage(packageName,
                pkgInfo.getLongVersionCode());

        final boolean isApex = ((installFlags & PackageManager.INSTALL_APEX) != 0);
        final IntArray pendingBackups;
        if (isApex) {
            pendingBackups = IntArray.wrap(new int[0]);
        } else {
        IntArray pendingBackups = IntArray.wrap(new int[0]);
        if (snapshotUserData && !isApex) {
            pendingBackups = mUserdataHelper.snapshotAppData(packageName, installedUsers);
        }

        // TODO: Record if this is an apex or not.
        PackageRollbackInfo info = new PackageRollbackInfo(newVersion, installedVersion,
                pendingBackups, new ArrayList<>());
                pendingBackups, new ArrayList<>(), isApex);
        RollbackData data;
        try {
            int childSessionId = session.getSessionId();
@@ -786,6 +796,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
            if (parentSessionId == PackageInstaller.SessionInfo.INVALID_ID) {
                parentSessionId = childSessionId;
            }

            synchronized (mLock) {
                // TODO: no need to add to mChildSessions if childSessionId is
                // the same as parentSessionId.
@@ -793,7 +804,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
                data = mPendingRollbacks.get(parentSessionId);
                if (data == null) {
                    int rollbackId = allocateRollbackIdLocked();
                    if (session.isStaged()) {
                        data = mRollbackStore.createPendingStagedRollback(rollbackId,
                                parentSessionId);
                    } else {
                        data = mRollbackStore.createAvailableRollback(rollbackId);
                    }
                    mPendingRollbacks.put(parentSessionId, data);
                }
                data.packages.add(info);
@@ -844,6 +860,56 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        });
    }

    @Override
    public boolean notifyStagedSession(int sessionId) {
        final LinkedBlockingQueue<Boolean> result = new LinkedBlockingQueue<>();

        // NOTE: We post this runnable on the RollbackManager's binder thread because we'd prefer
        // to preserve the invariant that all operations that modify state happen there.
        getHandler().post(() -> {
            PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();

            final PackageInstaller.SessionInfo session = installer.getSessionInfo(sessionId);
            if (session == null) {
                Log.e(TAG, "No matching install session for: " + sessionId);
                result.offer(false);
                return;
            }

            if (!session.isMultiPackage()) {
                if (!enableRollbackForSession(session, null, false)) {
                    Log.e(TAG, "Unable to enable rollback for session: " + sessionId);
                    result.offer(false);
                    return;
                }
            } else {
                for (int childSessionId : session.getChildSessionIds()) {
                    final PackageInstaller.SessionInfo childSession =
                            installer.getSessionInfo(childSessionId);
                    if (childSession == null) {
                        Log.e(TAG, "No matching child install session for: " + childSessionId);
                        result.offer(false);
                        return;
                    }
                    if (!enableRollbackForSession(childSession, null, false)) {
                        Log.e(TAG, "Unable to enable rollback for session: " + sessionId);
                        result.offer(false);
                        return;
                    }
                }
            }

            result.offer(true);
        });

        try {
            return result.take();
        } catch (InterruptedException ie) {
            Log.e(TAG, "Interrupted while waiting for notifyStagedSession response");
            return false;
        }
    }

    /**
     * Gets the version of the package currently installed.
     * Returns null if the package is not currently installed.
@@ -881,12 +947,28 @@ 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;
            }

            completeEnableRollback(sessionId, success);
        }
    }

    private void completeEnableRollback(int sessionId, boolean success) {
        RollbackData data = null;
        synchronized (mLock) {
            Integer parentSessionId = mChildSessions.remove(sessionId);
            if (parentSessionId != null) {
                sessionId = parentSessionId;
            }

            data = mPendingRollbacks.remove(sessionId);
        }

@@ -933,6 +1015,30 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
            }
        }
    }

    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 (pi.isStaged()) {
            if (!pi.isSessionFailed()) {
                // 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.isSessionReady());
            } 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");
        }
    }

    /*
Loading