Make apk session commit during pre-reboot verification asynchronous
This breaks down existing pre-reboot verification logic into multiple states and arrange them in a linear order. Each state is triggerred by a message. Bug: 137282250 Test: atest CtsStagedInstallHostTestCases Test: atest RollbackTest Change-Id: I17c1c5e43a631d7c061413556f419244ffc276db Merged-In: I17c1c5e43a631d7c061413556f419244ffc276db
This commit is contained in:
@@ -39,6 +39,8 @@ import android.content.rollback.IRollbackManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.PowerManager;
|
||||
import android.os.RemoteException;
|
||||
@@ -58,6 +60,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -72,7 +75,7 @@ public class StagingManager {
|
||||
private final PackageInstallerService mPi;
|
||||
private final ApexManager mApexManager;
|
||||
private final PowerManager mPowerManager;
|
||||
private final Handler mBgHandler;
|
||||
private final PreRebootVerificationHandler mPreRebootVerificationHandler;
|
||||
|
||||
@GuardedBy("mStagedSessions")
|
||||
private final SparseArray<PackageInstallerSession> mStagedSessions = new SparseArray<>();
|
||||
@@ -81,7 +84,8 @@ public class StagingManager {
|
||||
mPi = pi;
|
||||
mApexManager = am;
|
||||
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
mBgHandler = BackgroundThread.getHandler();
|
||||
mPreRebootVerificationHandler = new PreRebootVerificationHandler(
|
||||
BackgroundThread.get().getLooper());
|
||||
}
|
||||
|
||||
private void updateStoredSession(@NonNull PackageInstallerSession sessionInfo) {
|
||||
@@ -249,75 +253,6 @@ public class StagingManager {
|
||||
return (session.params.installFlags & PackageManager.INSTALL_APEX) != 0;
|
||||
}
|
||||
|
||||
private void preRebootVerification(@NonNull PackageInstallerSession session) {
|
||||
Slog.d(TAG, "Starting preRebootVerification for session " + session.sessionId);
|
||||
final boolean hasApex = sessionContainsApex(session);
|
||||
// APEX checks. For single-package sessions, check if they contain an APEX. For
|
||||
// multi-package sessions, find all the child sessions that contain an APEX.
|
||||
if (hasApex) {
|
||||
try {
|
||||
final List<PackageInfo> apexPackages = submitSessionToApexService(session);
|
||||
for (PackageInfo apexPackage : apexPackages) {
|
||||
validateApexSignature(apexPackage, session.params.installFlags);
|
||||
}
|
||||
} catch (PackageManagerException e) {
|
||||
session.setStagedSessionFailed(e.error, e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionContainsApk(session)) {
|
||||
try {
|
||||
Slog.d(TAG, "Running a pre-reboot verification for APKs in session "
|
||||
+ session.sessionId + " by performing a dry-run install");
|
||||
installApksInSession(session, /* preReboot */ true);
|
||||
// TODO(b/118865310): abort the session on apexd.
|
||||
} catch (PackageManagerException e) {
|
||||
session.setStagedSessionFailed(e.error, e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively mark session as ready before calling apexd. Although this call order looks
|
||||
// counter-intuitive, this is the easiest way to ensure that session won't end up in the
|
||||
// inconsistent state:
|
||||
// - If device gets rebooted right before call to apexd, then apexd will never activate
|
||||
// apex files of this staged session. This will result in StagingManager failing the
|
||||
// session.
|
||||
// On the other hand, if the order of the calls was inverted (first call apexd, then mark
|
||||
// session as ready), then if a device gets rebooted right after the call to apexd, only
|
||||
// apex part of the train will be applied, leaving device in an inconsistent state.
|
||||
Slog.d(TAG, "Marking session " + session.sessionId + " as ready");
|
||||
session.setStagedSessionReady();
|
||||
if (!hasApex) {
|
||||
// Session doesn't contain apex, nothing to do.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mApexManager.markStagedSessionReady(session.sessionId);
|
||||
} catch (PackageManagerException e) {
|
||||
session.setStagedSessionFailed(e.error, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean sessionContains(@NonNull PackageInstallerSession session,
|
||||
Predicate<PackageInstallerSession> filter) {
|
||||
if (!session.isMultiPackage()) {
|
||||
@@ -366,7 +301,7 @@ public class StagingManager {
|
||||
// Greedily re-trigger the pre-reboot verification.
|
||||
Slog.d(TAG, "Found pending staged session " + session.sessionId + " still to be "
|
||||
+ "verified, resuming pre-reboot verification");
|
||||
mBgHandler.post(() -> preRebootVerification(session));
|
||||
mPreRebootVerificationHandler.startPreRebootVerification(session);
|
||||
return;
|
||||
}
|
||||
if (!apexSessionInfo.isActivated && !apexSessionInfo.isSuccess) {
|
||||
@@ -476,34 +411,52 @@ public class StagingManager {
|
||||
}
|
||||
|
||||
private void commitApkSession(@NonNull PackageInstallerSession apkSession,
|
||||
int originalSessionId, boolean preReboot) throws PackageManagerException {
|
||||
PackageInstallerSession originalSession, boolean preReboot)
|
||||
throws PackageManagerException {
|
||||
final int errorCode = preReboot ? SessionInfo.STAGED_SESSION_VERIFICATION_FAILED
|
||||
: SessionInfo.STAGED_SESSION_ACTIVATION_FAILED;
|
||||
if (!preReboot) {
|
||||
if ((apkSession.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
|
||||
// If rollback is available for this session, notify the rollback
|
||||
// manager of the apk session so it can properly enable rollback.
|
||||
final IRollbackManager rm = IRollbackManager.Stub.asInterface(
|
||||
ServiceManager.getService(Context.ROLLBACK_SERVICE));
|
||||
try {
|
||||
rm.notifyStagedApkSession(originalSessionId, apkSession.sessionId);
|
||||
} catch (RemoteException re) {
|
||||
// Cannot happen, the rollback manager is in the same process.
|
||||
}
|
||||
if (preReboot) {
|
||||
final LocalIntentReceiverAsync receiver = new LocalIntentReceiverAsync(
|
||||
(Intent result) -> {
|
||||
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE);
|
||||
if (status != PackageInstaller.STATUS_SUCCESS) {
|
||||
final String errorMessage = result.getStringExtra(
|
||||
PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Slog.e(TAG, "Failure to install APK staged session "
|
||||
+ originalSession.sessionId + " [" + errorMessage + "]");
|
||||
originalSession.setStagedSessionFailed(errorCode, errorMessage);
|
||||
return;
|
||||
}
|
||||
mPreRebootVerificationHandler.notifyPreRebootVerification_Apk_Complete(
|
||||
originalSession);
|
||||
});
|
||||
apkSession.commit(receiver.getIntentSender(), false);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((apkSession.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
|
||||
// If rollback is available for this session, notify the rollback
|
||||
// manager of the apk session so it can properly enable rollback.
|
||||
final IRollbackManager rm = IRollbackManager.Stub.asInterface(
|
||||
ServiceManager.getService(Context.ROLLBACK_SERVICE));
|
||||
try {
|
||||
rm.notifyStagedApkSession(originalSession.sessionId, apkSession.sessionId);
|
||||
} catch (RemoteException re) {
|
||||
// Cannot happen, the rollback manager is in the same process.
|
||||
}
|
||||
}
|
||||
|
||||
final LocalIntentReceiver receiver = new LocalIntentReceiver();
|
||||
final LocalIntentReceiverSync receiver = new LocalIntentReceiverSync();
|
||||
apkSession.commit(receiver.getIntentSender(), false);
|
||||
final Intent result = receiver.getResult();
|
||||
final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE);
|
||||
if (status != PackageInstaller.STATUS_SUCCESS) {
|
||||
|
||||
final String errorMessage = result.getStringExtra(
|
||||
PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Slog.e(TAG, "Failure to install APK staged session " + originalSessionId + " ["
|
||||
+ errorMessage + "]");
|
||||
Slog.e(TAG, "Failure to install APK staged session "
|
||||
+ originalSession.sessionId + " [" + errorMessage + "]");
|
||||
throw new PackageManagerException(errorCode, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -515,7 +468,7 @@ public class StagingManager {
|
||||
if (!session.isMultiPackage() && !isApexSession(session)) {
|
||||
// APK single-packaged staged session. Do a regular install.
|
||||
PackageInstallerSession apkSession = createAndWriteApkSession(session, preReboot);
|
||||
commitApkSession(apkSession, session.sessionId, preReboot);
|
||||
commitApkSession(apkSession, session, preReboot);
|
||||
} else if (session.isMultiPackage()) {
|
||||
// For multi-package staged sessions containing APKs, we identify which child sessions
|
||||
// contain an APK, and with those then create a new multi-package group of sessions,
|
||||
@@ -565,14 +518,14 @@ public class StagingManager {
|
||||
"Failed to add a child session " + apkChildSession.sessionId);
|
||||
}
|
||||
}
|
||||
commitApkSession(apkParentSession, session.sessionId, preReboot);
|
||||
commitApkSession(apkParentSession, session, preReboot);
|
||||
}
|
||||
// APEX single-package staged session, nothing to do.
|
||||
}
|
||||
|
||||
void commitSession(@NonNull PackageInstallerSession session) {
|
||||
updateStoredSession(session);
|
||||
mBgHandler.post(() -> preRebootVerification(session));
|
||||
mPreRebootVerificationHandler.startPreRebootVerification(session);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -699,7 +652,7 @@ public class StagingManager {
|
||||
if (!session.isStagedSessionReady()) {
|
||||
// The framework got restarted before the pre-reboot verification could complete,
|
||||
// restart the verification.
|
||||
mBgHandler.post(() -> preRebootVerification(session));
|
||||
mPreRebootVerificationHandler.startPreRebootVerification(session);
|
||||
} else {
|
||||
// Session had already being marked ready. Start the checks to verify if there is any
|
||||
// follow-up work.
|
||||
@@ -707,14 +660,34 @@ public class StagingManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static class LocalIntentReceiver {
|
||||
private static class LocalIntentReceiverAsync {
|
||||
final Consumer<Intent> mConsumer;
|
||||
|
||||
LocalIntentReceiverAsync(Consumer<Intent> consumer) {
|
||||
mConsumer = consumer;
|
||||
}
|
||||
|
||||
private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
|
||||
@Override
|
||||
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
|
||||
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
|
||||
mConsumer.accept(intent);
|
||||
}
|
||||
};
|
||||
|
||||
public IntentSender getIntentSender() {
|
||||
return new IntentSender((IIntentSender) mLocalSender);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LocalIntentReceiverSync {
|
||||
private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
|
||||
|
||||
private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
|
||||
@Override
|
||||
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
|
||||
IIntentReceiver finishedReceiver, String requiredPermission,
|
||||
Bundle options) {
|
||||
IIntentReceiver finishedReceiver, String requiredPermission,
|
||||
Bundle options) {
|
||||
try {
|
||||
mResult.offer(intent, 5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -735,4 +708,179 @@ public class StagingManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class PreRebootVerificationHandler extends Handler {
|
||||
|
||||
PreRebootVerificationHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for states of pre reboot verification. The states are arranged linearly (shown
|
||||
* below) with each state either calling the next state, or calling some other method that
|
||||
* eventually calls the next state.
|
||||
*
|
||||
* <p><ul>
|
||||
* <li>MSG_PRE_REBOOT_VERIFICATION_START</li>
|
||||
* <li>MSG_PRE_REBOOT_VERIFICATION_APEX</li>
|
||||
* <li>MSG_PRE_REBOOT_VERIFICATION_APK</li>
|
||||
* <li>MSG_PRE_REBOOT_VERIFICATION_END</li>
|
||||
* </ul></p>
|
||||
*
|
||||
* Details about each of state can be found in corresponding handler of node.
|
||||
*/
|
||||
private static final int MSG_PRE_REBOOT_VERIFICATION_START = 1;
|
||||
private static final int MSG_PRE_REBOOT_VERIFICATION_APEX = 2;
|
||||
private static final int MSG_PRE_REBOOT_VERIFICATION_APK = 3;
|
||||
private static final int MSG_PRE_REBOOT_VERIFICATION_END = 4;
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
PackageInstallerSession session = (PackageInstallerSession) msg.obj;
|
||||
switch (msg.what) {
|
||||
case MSG_PRE_REBOOT_VERIFICATION_START:
|
||||
handlePreRebootVerification_Start(session);
|
||||
break;
|
||||
case MSG_PRE_REBOOT_VERIFICATION_APEX:
|
||||
handlePreRebootVerification_Apex(session);
|
||||
break;
|
||||
case MSG_PRE_REBOOT_VERIFICATION_APK:
|
||||
handlePreRebootVerification_Apk(session);
|
||||
break;
|
||||
case MSG_PRE_REBOOT_VERIFICATION_END:
|
||||
handlePreRebootVerification_End(session);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Method for starting the pre-reboot verification
|
||||
private void startPreRebootVerification(PackageInstallerSession session) {
|
||||
obtainMessage(MSG_PRE_REBOOT_VERIFICATION_START, session).sendToTarget();
|
||||
}
|
||||
|
||||
private void notifyPreRebootVerification_Start_Complete(PackageInstallerSession session) {
|
||||
obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APEX, session).sendToTarget();
|
||||
}
|
||||
|
||||
private void notifyPreRebootVerification_Apex_Complete(PackageInstallerSession session) {
|
||||
obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APK, session).sendToTarget();
|
||||
}
|
||||
|
||||
private void notifyPreRebootVerification_Apk_Complete(PackageInstallerSession session) {
|
||||
obtainMessage(MSG_PRE_REBOOT_VERIFICATION_END, session).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* A dummy state for starting the pre reboot verification.
|
||||
*
|
||||
* See {@link PreRebootVerificationHandler} to see all nodes of pre reboot verification
|
||||
*/
|
||||
private void handlePreRebootVerification_Start(@NonNull PackageInstallerSession session) {
|
||||
Slog.d(TAG, "Starting preRebootVerification for session " + session.sessionId);
|
||||
notifyPreRebootVerification_Start_Complete(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-reboot verification state for apex files:
|
||||
*
|
||||
* <p><ul>
|
||||
* <li>submits session to apex service</li>
|
||||
* <li>validates signatures of apex files</li>
|
||||
* </ul></p>
|
||||
*/
|
||||
private void handlePreRebootVerification_Apex(@NonNull PackageInstallerSession session) {
|
||||
final boolean hasApex = sessionContainsApex(session);
|
||||
|
||||
// APEX checks. For single-package sessions, check if they contain an APEX. For
|
||||
// multi-package sessions, find all the child sessions that contain an APEX.
|
||||
if (hasApex) {
|
||||
try {
|
||||
final List<PackageInfo> apexPackages =
|
||||
submitSessionToApexService(session);
|
||||
for (PackageInfo apexPackage : apexPackages) {
|
||||
validateApexSignature(
|
||||
apexPackage, session.params.installFlags);
|
||||
}
|
||||
} catch (PackageManagerException e) {
|
||||
session.setStagedSessionFailed(e.error, e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
notifyPreRebootVerification_Apex_Complete(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-reboot verification state for apk files:
|
||||
* <p><ul>
|
||||
* <li>performs a dry-run install of apk</li>
|
||||
* </ul></p>
|
||||
*/
|
||||
private void handlePreRebootVerification_Apk(@NonNull PackageInstallerSession session) {
|
||||
if (!sessionContainsApk(session)) {
|
||||
notifyPreRebootVerification_Apk_Complete(session);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Slog.d(TAG, "Running a pre-reboot verification for APKs in session "
|
||||
+ session.sessionId + " by performing a dry-run install");
|
||||
|
||||
// installApksInSession will notify the handler when APK verification is complete
|
||||
installApksInSession(session, /* preReboot */ true);
|
||||
// TODO(b/118865310): abort the session on apexd.
|
||||
} catch (PackageManagerException e) {
|
||||
session.setStagedSessionFailed(e.error, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-reboot verification state for wrapping up:
|
||||
* <p><ul>
|
||||
* <li>enables rollback if required</li>
|
||||
* <li>marks session as ready</li>
|
||||
* </ul></p>
|
||||
*/
|
||||
private void handlePreRebootVerification_End(@NonNull PackageInstallerSession session) {
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively mark session as ready before calling apexd. Although this call order
|
||||
// looks counter-intuitive, this is the easiest way to ensure that session won't end up
|
||||
// in the inconsistent state:
|
||||
// - If device gets rebooted right before call to apexd, then apexd will never activate
|
||||
// apex files of this staged session. This will result in StagingManager failing
|
||||
// the session.
|
||||
// On the other hand, if the order of the calls was inverted (first call apexd, then
|
||||
// mark session as ready), then if a device gets rebooted right after the call to apexd,
|
||||
// only apex part of the train will be applied, leaving device in an inconsistent state.
|
||||
Slog.d(TAG, "Marking session " + session.sessionId + " as ready");
|
||||
session.setStagedSessionReady();
|
||||
final boolean hasApex = sessionContainsApex(session);
|
||||
if (!hasApex) {
|
||||
// Session doesn't contain apex, nothing to do.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mApexManager.markStagedSessionReady(session.sessionId);
|
||||
} catch (PackageManagerException e) {
|
||||
session.setStagedSessionFailed(e.error, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user