| /* |
| * Copyright (C) 2018 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.pm; |
| |
| import android.annotation.NonNull; |
| import android.apex.ApexInfo; |
| import android.apex.ApexInfoList; |
| import android.apex.ApexSessionInfo; |
| import android.apex.ApexSessionParams; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.IIntentReceiver; |
| import android.content.IIntentSender; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.IntentSender; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageInstaller; |
| import android.content.pm.PackageInstaller.SessionInfo; |
| import android.content.pm.PackageInstaller.SessionInfo.StagedSessionErrorCode; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.PackageParser.PackageParserException; |
| import android.content.pm.PackageParser.SigningDetails; |
| import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion; |
| import android.content.pm.parsing.PackageInfoWithoutStateUtils; |
| import android.content.rollback.RollbackInfo; |
| import android.content.rollback.RollbackManager; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.os.Trace; |
| import android.os.UserHandle; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.IntArray; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.TimingsTraceLog; |
| import android.util.apk.ApkSignatureVerifier; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.content.PackageHelper; |
| import com.android.internal.os.BackgroundThread; |
| import com.android.internal.util.Preconditions; |
| import com.android.server.LocalServices; |
| import com.android.server.SystemService; |
| import com.android.server.SystemServiceManager; |
| import com.android.server.pm.parsing.PackageParser2; |
| import com.android.server.pm.parsing.pkg.AndroidPackage; |
| import com.android.server.pm.parsing.pkg.AndroidPackageUtils; |
| import com.android.server.pm.parsing.pkg.ParsedPackage; |
| import com.android.server.rollback.RollbackManagerInternal; |
| import com.android.server.rollback.WatchdogRollbackLogger; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Predicate; |
| import java.util.function.Supplier; |
| |
| /** |
| * This class handles staged install sessions, i.e. install sessions that require packages to |
| * be installed only after a reboot. |
| */ |
| public class StagingManager { |
| |
| private static final String TAG = "StagingManager"; |
| |
| private final ApexManager mApexManager; |
| private final PowerManager mPowerManager; |
| private final Context mContext; |
| private final PreRebootVerificationHandler mPreRebootVerificationHandler; |
| private final Supplier<PackageParser2> mPackageParserSupplier; |
| |
| private final File mFailureReasonFile = new File("/metadata/staged-install/failure_reason.txt"); |
| private String mFailureReason; |
| |
| @GuardedBy("mStagedSessions") |
| private final SparseArray<StagedSession> mStagedSessions = new SparseArray<>(); |
| |
| @GuardedBy("mFailedPackageNames") |
| private final List<String> mFailedPackageNames = new ArrayList<>(); |
| private String mNativeFailureReason; |
| |
| @GuardedBy("mSuccessfulStagedSessionIds") |
| private final List<Integer> mSuccessfulStagedSessionIds = new ArrayList<>(); |
| |
| interface StagedSession { |
| boolean isMultiPackage(); |
| boolean isApexSession(); |
| boolean isCommitted(); |
| boolean isInTerminalState(); |
| boolean isDestroyed(); |
| boolean isSessionReady(); |
| boolean isSessionApplied(); |
| boolean isSessionFailed(); |
| List<StagedSession> getChildSessions(); |
| String getPackageName(); |
| int getParentSessionId(); |
| int sessionId(); |
| PackageInstaller.SessionParams sessionParams(); |
| boolean sessionContains(Predicate<StagedSession> filter); |
| boolean containsApkSession(); |
| boolean containsApexSession(); |
| void setSessionReady(); |
| void setSessionFailed(@StagedSessionErrorCode int errorCode, String errorMessage); |
| void setSessionApplied(); |
| void installSession(IntentSender statusReceiver); |
| boolean hasParentSessionId(); |
| long getCommittedMillis(); |
| void abandon(); |
| boolean notifyStartPreRebootVerification(); |
| void notifyEndPreRebootVerification(); |
| void verifySession(); |
| } |
| |
| StagingManager(Context context, Supplier<PackageParser2> packageParserSupplier) { |
| this(context, packageParserSupplier, ApexManager.getInstance()); |
| } |
| |
| @VisibleForTesting |
| StagingManager(Context context, Supplier<PackageParser2> packageParserSupplier, |
| ApexManager apexManager) { |
| mContext = context; |
| mPackageParserSupplier = packageParserSupplier; |
| |
| mApexManager = apexManager; |
| mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); |
| mPreRebootVerificationHandler = new PreRebootVerificationHandler( |
| BackgroundThread.get().getLooper()); |
| |
| if (mFailureReasonFile.exists()) { |
| try (BufferedReader reader = new BufferedReader(new FileReader(mFailureReasonFile))) { |
| mFailureReason = reader.readLine(); |
| } catch (Exception ignore) { } |
| } |
| } |
| |
| /** |
| This class manages lifecycle events for StagingManager. |
| */ |
| public static final class Lifecycle extends SystemService { |
| private static StagingManager sStagingManager; |
| |
| public Lifecycle(Context context) { |
| super(context); |
| } |
| |
| void startService(StagingManager stagingManager) { |
| sStagingManager = stagingManager; |
| LocalServices.getService(SystemServiceManager.class).startService(this); |
| } |
| |
| @Override |
| public void onStart() { |
| // no-op |
| } |
| |
| @Override |
| public void onBootPhase(int phase) { |
| if (phase == SystemService.PHASE_BOOT_COMPLETED && sStagingManager != null) { |
| sStagingManager.markStagedSessionsAsSuccessful(); |
| sStagingManager.markBootCompleted(); |
| } |
| } |
| } |
| |
| private void markBootCompleted() { |
| mApexManager.markBootCompleted(); |
| } |
| |
| /** |
| * Validates the signature used to sign the container of the new apex package |
| * |
| * @param newApexPkg The new apex package that is being installed |
| */ |
| private void validateApexSignature(PackageInfo newApexPkg) |
| throws PackageManagerException { |
| // Get signing details of the new package |
| final String apexPath = newApexPkg.applicationInfo.sourceDir; |
| final String packageName = newApexPkg.packageName; |
| int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( |
| newApexPkg.applicationInfo.targetSdkVersion); |
| |
| final SigningDetails newSigningDetails; |
| try { |
| newSigningDetails = ApkSignatureVerifier.verify(apexPath, minSignatureScheme); |
| } catch (PackageParserException e) { |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Failed to parse APEX package " + apexPath + " : " + e, e); |
| } |
| |
| // Get signing details of the existing package |
| final PackageInfo existingApexPkg = mApexManager.getPackageInfo(packageName, |
| ApexManager.MATCH_ACTIVE_PACKAGE); |
| if (existingApexPkg == null) { |
| // This should never happen, because submitSessionToApexService ensures that no new |
| // apexes were installed. |
| throw new IllegalStateException("Unknown apex package " + packageName); |
| } |
| |
| final SigningDetails existingSigningDetails; |
| try { |
| existingSigningDetails = ApkSignatureVerifier.verify( |
| existingApexPkg.applicationInfo.sourceDir, SignatureSchemeVersion.JAR); |
| } catch (PackageParserException e) { |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Failed to parse APEX package " + existingApexPkg.applicationInfo.sourceDir |
| + " : " + e, e); |
| } |
| |
| // Verify signing details for upgrade |
| if (newSigningDetails.checkCapability(existingSigningDetails, |
| SigningDetails.CertCapabilities.INSTALLED_DATA) |
| || existingSigningDetails.checkCapability(newSigningDetails, |
| SigningDetails.CertCapabilities.ROLLBACK)) { |
| return; |
| } |
| |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "APK-container signature of APEX package " + packageName + " with version " |
| + newApexPkg.versionCodeMajor + " and path " + apexPath + " is not" |
| + " compatible with the one currently installed on device"); |
| } |
| |
| private List<PackageInfo> submitSessionToApexService(@NonNull StagedSession session, |
| int rollbackId) throws PackageManagerException { |
| final IntArray childSessionIds = new IntArray(); |
| if (session.isMultiPackage()) { |
| for (StagedSession s : session.getChildSessions()) { |
| if (s.isApexSession()) { |
| childSessionIds.add(s.sessionId()); |
| } |
| } |
| } |
| ApexSessionParams apexSessionParams = new ApexSessionParams(); |
| apexSessionParams.sessionId = session.sessionId(); |
| apexSessionParams.childSessionIds = childSessionIds.toArray(); |
| if (session.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK) { |
| apexSessionParams.isRollback = true; |
| apexSessionParams.rollbackId = rollbackId; |
| } else { |
| if (rollbackId != -1) { |
| apexSessionParams.hasRollbackEnabled = true; |
| apexSessionParams.rollbackId = rollbackId; |
| } |
| } |
| // submitStagedSession will throw a PackageManagerException if apexd verification fails, |
| // which will be propagated to populate stagedSessionErrorMessage of this session. |
| final ApexInfoList apexInfoList = mApexManager.submitStagedSession(apexSessionParams); |
| final List<PackageInfo> result = new ArrayList<>(); |
| final List<String> apexPackageNames = new ArrayList<>(); |
| for (ApexInfo apexInfo : apexInfoList.apexInfos) { |
| final PackageInfo packageInfo; |
| final int flags = PackageManager.GET_META_DATA; |
| try (PackageParser2 packageParser = mPackageParserSupplier.get()) { |
| File apexFile = new File(apexInfo.modulePath); |
| final ParsedPackage parsedPackage = packageParser.parsePackage( |
| apexFile, flags, false); |
| packageInfo = PackageInfoWithoutStateUtils.generate(parsedPackage, apexInfo, flags); |
| if (packageInfo == null) { |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Unable to generate package info: " + apexInfo.modulePath); |
| } |
| } catch (PackageParserException e) { |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Failed to parse APEX package " + apexInfo.modulePath + " : " + e, e); |
| } |
| final PackageInfo activePackage = mApexManager.getPackageInfo(packageInfo.packageName, |
| ApexManager.MATCH_ACTIVE_PACKAGE); |
| if (activePackage == null) { |
| Slog.w(TAG, "Attempting to install new APEX package " + packageInfo.packageName); |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "It is forbidden to install new APEX packages."); |
| } |
| checkRequiredVersionCode(session, activePackage); |
| checkDowngrade(session, activePackage, packageInfo); |
| result.add(packageInfo); |
| apexPackageNames.add(packageInfo.packageName); |
| } |
| Slog.d(TAG, "Session " + session.sessionId() + " has following APEX packages: " |
| + apexPackageNames); |
| return result; |
| } |
| |
| private int retrieveRollbackIdForCommitSession(int sessionId) throws PackageManagerException { |
| RollbackManager rm = mContext.getSystemService(RollbackManager.class); |
| |
| final List<RollbackInfo> rollbacks = rm.getRecentlyCommittedRollbacks(); |
| for (int i = 0, size = rollbacks.size(); i < size; i++) { |
| final RollbackInfo rollback = rollbacks.get(i); |
| if (rollback.getCommittedSessionId() == sessionId) { |
| return rollback.getRollbackId(); |
| } |
| } |
| throw new PackageManagerException( |
| "Could not find rollback id for commit session: " + sessionId); |
| } |
| |
| private void checkRequiredVersionCode(final StagedSession session, |
| final PackageInfo activePackage) throws PackageManagerException { |
| if (session.sessionParams().requiredInstalledVersionCode |
| == PackageManager.VERSION_CODE_HIGHEST) { |
| return; |
| } |
| final long activeVersion = activePackage.applicationInfo.longVersionCode; |
| if (activeVersion != session.sessionParams().requiredInstalledVersionCode) { |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Installed version of APEX package " + activePackage.packageName |
| + " does not match required. Active version: " + activeVersion |
| + " required: " + session.sessionParams().requiredInstalledVersionCode); |
| } |
| } |
| |
| private void checkDowngrade(final StagedSession session, |
| final PackageInfo activePackage, final PackageInfo newPackage) |
| throws PackageManagerException { |
| final long activeVersion = activePackage.applicationInfo.longVersionCode; |
| final long newVersionCode = newPackage.applicationInfo.longVersionCode; |
| final boolean isAppDebuggable = (activePackage.applicationInfo.flags |
| & ApplicationInfo.FLAG_DEBUGGABLE) != 0; |
| final boolean allowsDowngrade = PackageManagerServiceUtils.isDowngradePermitted( |
| session.sessionParams().installFlags, isAppDebuggable); |
| if (activeVersion > newVersionCode && !allowsDowngrade) { |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Downgrade of APEX package " + newPackage.packageName |
| + " is not allowed. Active version: " + activeVersion |
| + " attempted: " + newVersionCode); |
| } |
| } |
| |
| // Reverts apex sessions and user data (if checkpoint is supported). Also reboots the device. |
| private void abortCheckpoint(String failureReason, boolean supportsCheckpoint, |
| boolean needsCheckpoint) { |
| Slog.e(TAG, failureReason); |
| try { |
| if (supportsCheckpoint && needsCheckpoint) { |
| // Store failure reason for next reboot |
| try (BufferedWriter writer = |
| new BufferedWriter(new FileWriter(mFailureReasonFile))) { |
| writer.write(failureReason); |
| } catch (Exception e) { |
| Slog.w(TAG, "Failed to save failure reason: ", e); |
| } |
| |
| // Only revert apex sessions if device supports updating apex |
| if (mApexManager.isApexSupported()) { |
| mApexManager.revertActiveSessions(); |
| } |
| |
| PackageHelper.getStorageManager().abortChanges( |
| "abort-staged-install", false /*retry*/); |
| } |
| } catch (Exception e) { |
| Slog.wtf(TAG, "Failed to abort checkpoint", e); |
| // Only revert apex sessions if device supports updating apex |
| if (mApexManager.isApexSupported()) { |
| mApexManager.revertActiveSessions(); |
| } |
| mPowerManager.reboot(null); |
| } |
| } |
| |
| /** |
| * Utility function for extracting apex sessions out of multi-package/single session. |
| */ |
| private List<StagedSession> extractApexSessions(StagedSession session) { |
| List<StagedSession> apexSessions = new ArrayList<>(); |
| if (session.isMultiPackage()) { |
| for (StagedSession s : session.getChildSessions()) { |
| if (s.containsApexSession()) { |
| apexSessions.add(s); |
| } |
| } |
| } else { |
| apexSessions.add(session); |
| } |
| return apexSessions; |
| } |
| |
| /** |
| * Checks if all apk-in-apex were installed without errors for all of the apex sessions. Throws |
| * error for any apk-in-apex failed to install. |
| * |
| * @throws PackageManagerException if any apk-in-apex failed to install |
| */ |
| private void checkInstallationOfApkInApexSuccessful(StagedSession session) |
| throws PackageManagerException { |
| final List<StagedSession> apexSessions = extractApexSessions(session); |
| if (apexSessions.isEmpty()) { |
| return; |
| } |
| |
| for (StagedSession apexSession : apexSessions) { |
| String packageName = apexSession.getPackageName(); |
| String errorMsg = mApexManager.getApkInApexInstallError(packageName); |
| if (errorMsg != null) { |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Failed to install apk-in-apex of " + packageName + " : " + errorMsg); |
| } |
| } |
| } |
| |
| /** |
| * Perform snapshot and restore as required both for APEXes themselves and for apks in APEX. |
| * Apks inside apex are not installed using apk-install flow. They are scanned from the system |
| * directory directly by PackageManager, as such, RollbackManager need to handle their data |
| * separately here. |
| */ |
| private void snapshotAndRestoreForApexSession(StagedSession session) { |
| boolean doSnapshotOrRestore = |
| (session.sessionParams().installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0 |
| || session.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK; |
| if (!doSnapshotOrRestore) { |
| return; |
| } |
| |
| // Find all the apex sessions that needs processing |
| final List<StagedSession> apexSessions = extractApexSessions(session); |
| if (apexSessions.isEmpty()) { |
| return; |
| } |
| |
| final UserManagerInternal um = LocalServices.getService(UserManagerInternal.class); |
| final int[] allUsers = um.getUserIds(); |
| RollbackManagerInternal rm = LocalServices.getService(RollbackManagerInternal.class); |
| |
| for (int i = 0, sessionsSize = apexSessions.size(); i < sessionsSize; i++) { |
| final String packageName = apexSessions.get(i).getPackageName(); |
| // Perform any snapshots or restores for the APEX itself |
| snapshotAndRestoreApexUserData(packageName, allUsers, rm); |
| |
| // Process the apks inside the APEX |
| final List<String> apksInApex = mApexManager.getApksInApex(packageName); |
| for (int j = 0, apksSize = apksInApex.size(); j < apksSize; j++) { |
| snapshotAndRestoreApkInApexUserData(apksInApex.get(j), allUsers, rm); |
| } |
| } |
| } |
| |
| private void snapshotAndRestoreApexUserData( |
| String packageName, int[] allUsers, RollbackManagerInternal rm) { |
| // appId, ceDataInode, and seInfo are not needed for APEXes |
| rm.snapshotAndRestoreUserData(packageName, UserHandle.toUserHandles(allUsers), 0, 0, |
| null, 0 /*token*/); |
| } |
| |
| private void snapshotAndRestoreApkInApexUserData( |
| String packageName, int[] allUsers, RollbackManagerInternal rm) { |
| PackageManagerInternal mPmi = LocalServices.getService(PackageManagerInternal.class); |
| AndroidPackage pkg = mPmi.getPackage(packageName); |
| if (pkg == null) { |
| Slog.e(TAG, "Could not find package: " + packageName |
| + "for snapshotting/restoring user data."); |
| return; |
| } |
| |
| int appId = -1; |
| long ceDataInode = -1; |
| final PackageSetting ps = mPmi.getPackageSetting(packageName); |
| if (ps != null) { |
| appId = ps.appId; |
| ceDataInode = ps.getCeDataInode(UserHandle.USER_SYSTEM); |
| // NOTE: We ignore the user specified in the InstallParam because we know this is |
| // an update, and hence need to restore data for all installed users. |
| final int[] installedUsers = ps.queryInstalledUsers(allUsers, true); |
| |
| final String seInfo = AndroidPackageUtils.getSeInfo(pkg, ps); |
| rm.snapshotAndRestoreUserData(packageName, UserHandle.toUserHandles(installedUsers), |
| appId, ceDataInode, seInfo, 0 /*token*/); |
| } |
| } |
| |
| /** |
| * Prepares for the logging of apexd reverts by storing the native failure reason if necessary, |
| * and adding the package name of the session which apexd reverted to the list of reverted |
| * session package names. |
| * Logging needs to wait until the ACTION_BOOT_COMPLETED broadcast is sent. |
| */ |
| private void prepareForLoggingApexdRevert(@NonNull StagedSession session, |
| @NonNull String nativeFailureReason) { |
| synchronized (mFailedPackageNames) { |
| mNativeFailureReason = nativeFailureReason; |
| if (session.getPackageName() != null) { |
| mFailedPackageNames.add(session.getPackageName()); |
| } |
| } |
| } |
| |
| private void resumeSession(@NonNull StagedSession session, boolean supportsCheckpoint, |
| boolean needsCheckpoint) throws PackageManagerException { |
| Slog.d(TAG, "Resuming session " + session.sessionId()); |
| |
| final boolean hasApex = session.containsApexSession(); |
| |
| // Before we resume session, we check if revert is needed or not. Typically, we enter file- |
| // system checkpoint mode when we reboot first time in order to install staged sessions. We |
| // want to install staged sessions in this mode as rebooting now will revert user data. If |
| // something goes wrong, then we reboot again to enter fs-rollback mode. Rebooting now will |
| // have no effect on user data, so mark the sessions as failed instead. |
| // If checkpoint is supported, then we only resume sessions if we are in checkpointing mode. |
| // If not, we fail all sessions. |
| if (supportsCheckpoint && !needsCheckpoint) { |
| String revertMsg = "Reverting back to safe state. Marking " + session.sessionId() |
| + " as failed."; |
| final String reasonForRevert = getReasonForRevert(); |
| if (!TextUtils.isEmpty(reasonForRevert)) { |
| revertMsg += " Reason for revert: " + reasonForRevert; |
| } |
| Slog.d(TAG, revertMsg); |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_UNKNOWN, revertMsg); |
| return; |
| } |
| |
| // Handle apk and apk-in-apex installation |
| if (hasApex) { |
| checkInstallationOfApkInApexSuccessful(session); |
| checkDuplicateApkInApex(session); |
| snapshotAndRestoreForApexSession(session); |
| Slog.i(TAG, "APEX packages in session " + session.sessionId() |
| + " were successfully activated. Proceeding with APK packages, if any"); |
| } |
| // The APEX part of the session is activated, proceed with the installation of APKs. |
| Slog.d(TAG, "Installing APK packages in session " + session.sessionId()); |
| TimingsTraceLog t = new TimingsTraceLog( |
| "StagingManagerTiming", Trace.TRACE_TAG_PACKAGE_MANAGER); |
| t.traceBegin("installApksInSession"); |
| installApksInSession(session); |
| t.traceEnd(); |
| |
| Slog.d(TAG, "Marking session " + session.sessionId() + " as applied"); |
| session.setSessionApplied(); |
| if (hasApex) { |
| if (supportsCheckpoint) { |
| // Store the session ID, which will be marked as successful by ApexManager upon |
| // boot completion. |
| synchronized (mSuccessfulStagedSessionIds) { |
| mSuccessfulStagedSessionIds.add(session.sessionId()); |
| } |
| } else { |
| // Mark sessions as successful immediately on non-checkpointing devices. |
| mApexManager.markStagedSessionSuccessful(session.sessionId()); |
| } |
| } |
| } |
| |
| void onInstallationFailure(StagedSession session, PackageManagerException e, |
| boolean supportsCheckpoint, boolean needsCheckpoint) { |
| session.setSessionFailed(e.error, e.getMessage()); |
| abortCheckpoint("Failed to install sessionId: " + session.sessionId() |
| + " Error: " + e.getMessage(), supportsCheckpoint, needsCheckpoint); |
| |
| // If checkpoint is not supported, we have to handle failure for one staged session. |
| if (!session.containsApexSession()) { |
| return; |
| } |
| |
| if (!mApexManager.revertActiveSessions()) { |
| Slog.e(TAG, "Failed to abort APEXd session"); |
| } else { |
| Slog.e(TAG, |
| "Successfully aborted apexd session. Rebooting device in order to revert " |
| + "to the previous state of APEXd."); |
| mPowerManager.reboot(null); |
| } |
| } |
| |
| private String getReasonForRevert() { |
| if (!TextUtils.isEmpty(mFailureReason)) { |
| return mFailureReason; |
| } |
| if (!TextUtils.isEmpty(mNativeFailureReason)) { |
| return "Session reverted due to crashing native process: " + mNativeFailureReason; |
| } |
| return ""; |
| } |
| |
| /** |
| * Throws a PackageManagerException if there are duplicate packages in apk and apk-in-apex. |
| */ |
| private void checkDuplicateApkInApex(@NonNull StagedSession session) |
| throws PackageManagerException { |
| if (!session.isMultiPackage()) { |
| return; |
| } |
| final Set<String> apkNames = new ArraySet<>(); |
| for (StagedSession s : session.getChildSessions()) { |
| if (!s.isApexSession()) { |
| apkNames.add(s.getPackageName()); |
| } |
| } |
| final List<StagedSession> apexSessions = extractApexSessions(session); |
| for (StagedSession apexSession : apexSessions) { |
| String packageName = apexSession.getPackageName(); |
| for (String apkInApex : mApexManager.getApksInApex(packageName)) { |
| if (!apkNames.add(apkInApex)) { |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Package: " + packageName + " in session: " |
| + apexSession.sessionId() + " has duplicate apk-in-apex: " |
| + apkInApex, null); |
| |
| } |
| } |
| } |
| } |
| |
| private void installApksInSession(StagedSession session) |
| throws PackageManagerException { |
| if (!session.containsApkSession()) { |
| return; |
| } |
| |
| final LocalIntentReceiverSync receiver = new LocalIntentReceiverSync(); |
| session.installSession(receiver.getIntentSender()); |
| 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 " |
| + session.sessionId() + " [" + errorMessage + "]"); |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, errorMessage); |
| } |
| } |
| |
| void commitSession(@NonNull StagedSession session) { |
| // Store this parent session which will be used to check overlapping later |
| createSession(session); |
| mPreRebootVerificationHandler.startPreRebootVerification(session); |
| } |
| |
| private int getSessionIdForParentOrSelf(StagedSession session) { |
| return session.hasParentSessionId() ? session.getParentSessionId() : session.sessionId(); |
| } |
| |
| private StagedSession getParentSessionOrSelf(StagedSession session) { |
| return session.hasParentSessionId() |
| ? getStagedSession(session.getParentSessionId()) |
| : session; |
| } |
| |
| private boolean isRollback(StagedSession session) { |
| final StagedSession root = getParentSessionOrSelf(session); |
| return root.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK; |
| } |
| |
| /** |
| * <p> Check if the session provided is non-overlapping with the active staged sessions. |
| * |
| * <p> A session is non-overlapping if it meets one of the following conditions: </p> |
| * <ul> |
| * <li>It is a parent session</li> |
| * <li>It is already one of the active sessions</li> |
| * <li>Its package name is not same as any of the active sessions</li> |
| * </ul> |
| * @throws PackageManagerException if session fails the check |
| */ |
| @VisibleForTesting |
| void checkNonOverlappingWithStagedSessions(@NonNull StagedSession session) |
| throws PackageManagerException { |
| if (session.isMultiPackage()) { |
| // We cannot say a parent session overlaps until we process its children |
| return; |
| } |
| |
| String packageName = session.getPackageName(); |
| if (packageName == null) { |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Cannot stage session " + session.sessionId() + " with package name null"); |
| } |
| |
| boolean supportsCheckpoint; |
| try { |
| supportsCheckpoint = PackageHelper.getStorageManager().supportsCheckpoint(); |
| } catch (RemoteException e) { |
| throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Can't query fs-checkpoint status : " + e); |
| } |
| |
| final boolean isRollback = isRollback(session); |
| |
| synchronized (mStagedSessions) { |
| for (int i = 0; i < mStagedSessions.size(); i++) { |
| final StagedSession stagedSession = mStagedSessions.valueAt(i); |
| if (stagedSession.hasParentSessionId() || !stagedSession.isCommitted() |
| || stagedSession.isInTerminalState() |
| || stagedSession.isDestroyed()) { |
| continue; |
| } |
| |
| if (stagedSession.getCommittedMillis() > session.getCommittedMillis()) { |
| // Ignore sessions that are committed after the provided session. When there are |
| // overlaps between sessions, we will fail the one committed later instead of |
| // the earlier one. |
| continue; |
| } |
| |
| // From here on, stagedSession is a parent active staged session |
| |
| // Check if session is one of the active sessions |
| if (getSessionIdForParentOrSelf(session) == stagedSession.sessionId()) { |
| Slog.w(TAG, "Session " + session.sessionId() + " is already staged"); |
| continue; |
| } |
| |
| if (isRollback && !isRollback(stagedSession)) { |
| // If the new session is a rollback, then it gets priority. The existing |
| // session is failed to reduce risk and avoid an SDK extension dependency |
| // violation. |
| final StagedSession root = stagedSession; |
| if (!ensureActiveApexSessionIsAborted(root)) { |
| Slog.e(TAG, "Failed to abort apex session " + root.sessionId()); |
| // Safe to ignore active apex session abort failure since session |
| // will be marked failed on next step and staging directory for session |
| // will be deleted. |
| } |
| root.setSessionFailed( |
| SessionInfo.STAGED_SESSION_CONFLICT, |
| "Session was failed by rollback session: " + session.sessionId()); |
| Slog.i(TAG, "Session " + root.sessionId() + " is marked failed due to " |
| + "rollback session: " + session.sessionId()); |
| } else if (stagedSession.sessionContains( |
| s -> s.getPackageName().equals(packageName))) { |
| // New session cannot have same package name as one of the active sessions |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Package: " + session.getPackageName() + " in session: " |
| + session.sessionId() |
| + " has been staged already by session: " |
| + stagedSession.sessionId(), null); |
| } |
| |
| // Staging multiple root sessions is not allowed if device doesn't support |
| // checkpoint. If session and stagedSession do not have common ancestor, they are |
| // from two different root sessions. |
| if (!supportsCheckpoint) { |
| throw new PackageManagerException( |
| SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, |
| "Cannot stage multiple sessions without checkpoint support", null); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns id of a committed and non-finalized stated session that contains same |
| * {@code packageName}, or {@code -1} if no sessions have this {@code packageName} staged. |
| */ |
| int getSessionIdByPackageName(@NonNull String packageName) { |
| synchronized (mStagedSessions) { |
| for (int i = 0; i < mStagedSessions.size(); i++) { |
| StagedSession stagedSession = mStagedSessions.valueAt(i); |
| if (!stagedSession.isCommitted() || stagedSession.isDestroyed() |
| || stagedSession.isInTerminalState()) { |
| continue; |
| } |
| if (stagedSession.getPackageName().equals(packageName)) { |
| return stagedSession.sessionId(); |
| } |
| } |
| } |
| return -1; |
| } |
| |
| @VisibleForTesting |
| void createSession(@NonNull StagedSession sessionInfo) { |
| synchronized (mStagedSessions) { |
| mStagedSessions.append(sessionInfo.sessionId(), sessionInfo); |
| } |
| } |
| |
| void abortSession(@NonNull StagedSession session) { |
| synchronized (mStagedSessions) { |
| mStagedSessions.remove(session.sessionId()); |
| } |
| } |
| |
| /** |
| * <p>Abort committed staged session |
| */ |
| void abortCommittedSession(@NonNull StagedSession session) { |
| int sessionId = session.sessionId(); |
| if (session.isInTerminalState()) { |
| Slog.w(TAG, "Cannot abort session in final state: " + sessionId); |
| return; |
| } |
| if (!session.isDestroyed()) { |
| throw new IllegalStateException("Committed session must be destroyed before aborting it" |
| + " from StagingManager"); |
| } |
| if (getStagedSession(sessionId) == null) { |
| Slog.w(TAG, "Session " + sessionId + " has been abandoned already"); |
| return; |
| } |
| |
| // A session could be marked ready once its pre-reboot verification ends |
| if (session.isSessionReady()) { |
| if (!ensureActiveApexSessionIsAborted(session)) { |
| // Failed to ensure apex session is aborted, so it can still be staged. We can still |
| // safely cleanup the staged session since pre-reboot verification is complete. |
| // Also, cleaning up the stageDir prevents the apex from being activated. |
| Slog.e(TAG, "Failed to abort apex session " + session.sessionId()); |
| } |
| } |
| |
| // Session was successfully aborted from apexd (if required) and pre-reboot verification |
| // is also complete. It is now safe to clean up the session from system. |
| abortSession(session); |
| } |
| |
| /** |
| * Ensure that there is no active apex session staged in apexd for the given session. |
| * |
| * @return returns true if it is ensured that there is no active apex session, otherwise false |
| */ |
| private boolean ensureActiveApexSessionIsAborted(StagedSession session) { |
| if (!session.containsApexSession()) { |
| return true; |
| } |
| final ApexSessionInfo apexSession = mApexManager.getStagedSessionInfo(session.sessionId()); |
| if (apexSession == null || isApexSessionFinalized(apexSession)) { |
| return true; |
| } |
| return mApexManager.abortStagedSession(session.sessionId()); |
| } |
| |
| private boolean isApexSessionFinalized(ApexSessionInfo session) { |
| /* checking if the session is in a final state, i.e., not active anymore */ |
| return session.isUnknown || session.isActivationFailed || session.isSuccess |
| || session.isReverted; |
| } |
| |
| private static boolean isApexSessionFailed(ApexSessionInfo apexSessionInfo) { |
| // isRevertInProgress is included to cover the scenario, when a device is rebooted |
| // during the revert, and apexd fails to resume the revert after reboot. |
| return apexSessionInfo.isActivationFailed || apexSessionInfo.isUnknown |
| || apexSessionInfo.isReverted || apexSessionInfo.isRevertInProgress |
| || apexSessionInfo.isRevertFailed; |
| } |
| |
| private void handleNonReadyAndDestroyedSessions(List<StagedSession> sessions) { |
| int j = sessions.size(); |
| for (int i = 0; i < j; ) { |
| // Maintain following invariant: |
| // * elements at positions [0, i) should be kept |
| // * elements at positions [j, n) should be remove. |
| // * n = sessions.size() |
| StagedSession session = sessions.get(i); |
| if (session.isDestroyed()) { |
| // Device rebooted before abandoned session was cleaned up. |
| session.abandon(); |
| StagedSession session2 = sessions.set(j - 1, session); |
| sessions.set(i, session2); |
| j--; |
| } else if (!session.isSessionReady()) { |
| // The framework got restarted before the pre-reboot verification could complete, |
| // restart the verification. |
| mPreRebootVerificationHandler.startPreRebootVerification(session); |
| StagedSession session2 = sessions.set(j - 1, session); |
| sessions.set(i, session2); |
| j--; |
| } else { |
| i++; |
| } |
| } |
| // Delete last j elements. |
| sessions.subList(j, sessions.size()).clear(); |
| } |
| |
| void restoreSessions(@NonNull List<StagedSession> sessions, boolean isDeviceUpgrading) { |
| TimingsTraceLog t = new TimingsTraceLog( |
| "StagingManagerTiming", Trace.TRACE_TAG_PACKAGE_MANAGER); |
| t.traceBegin("restoreSessions"); |
| |
| // Do not resume sessions if boot completed already |
| if (SystemProperties.getBoolean("sys.boot_completed", false)) { |
| return; |
| } |
| |
| for (int i = 0; i < sessions.size(); i++) { |
| StagedSession session = sessions.get(i); |
| // Quick check that PackageInstallerService gave us sessions we expected. |
| Preconditions.checkArgument(!session.hasParentSessionId(), |
| session.sessionId() + " is a child session"); |
| Preconditions.checkArgument(session.isCommitted(), |
| session.sessionId() + " is not committed"); |
| Preconditions.checkArgument(!session.isInTerminalState(), |
| session.sessionId() + " is in terminal state"); |
| // Store this parent session which will be used to check overlapping later |
| createSession(session); |
| } |
| |
| if (isDeviceUpgrading) { |
| // TODO(ioffe): check that corresponding apex sessions are failed. |
| // The preconditions used during pre-reboot verification might have changed when device |
| // is upgrading. Fail all the sessions and exit early. |
| for (int i = 0; i < sessions.size(); i++) { |
| StagedSession session = sessions.get(i); |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Build fingerprint has changed"); |
| } |
| return; |
| } |
| |
| boolean needsCheckpoint = false; |
| boolean supportsCheckpoint = false; |
| try { |
| supportsCheckpoint = PackageHelper.getStorageManager().supportsCheckpoint(); |
| needsCheckpoint = PackageHelper.getStorageManager().needsCheckpoint(); |
| } catch (RemoteException e) { |
| // This means that vold has crashed, and device is in a bad state. |
| throw new IllegalStateException("Failed to get checkpoint status", e); |
| } |
| |
| if (sessions.size() > 1 && !supportsCheckpoint) { |
| throw new IllegalStateException("Detected multiple staged sessions on a device without " |
| + "fs-checkpoint support"); |
| } |
| |
| // Do a set of quick checks before resuming individual sessions: |
| // 1. Schedule a pre-reboot verification for non-ready sessions. |
| // 2. Abandon destroyed sessions. |
| handleNonReadyAndDestroyedSessions(sessions); // mutates |sessions| |
| |
| // 3. Check state of apex sessions is consistent. All non-applied sessions will be marked |
| // as failed. |
| final SparseArray<ApexSessionInfo> apexSessions = mApexManager.getSessions(); |
| boolean hasFailedApexSession = false; |
| boolean hasAppliedApexSession = false; |
| for (int i = 0; i < sessions.size(); i++) { |
| StagedSession session = sessions.get(i); |
| if (!session.containsApexSession()) { |
| // At this point we are only interested in apex sessions. |
| continue; |
| } |
| final ApexSessionInfo apexSession = apexSessions.get(session.sessionId()); |
| if (apexSession == null || apexSession.isUnknown) { |
| hasFailedApexSession = true; |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, "apexd did " |
| + "not know anything about a staged session supposed to be activated"); |
| continue; |
| } else if (isApexSessionFailed(apexSession)) { |
| hasFailedApexSession = true; |
| if (!TextUtils.isEmpty(apexSession.crashingNativeProcess)) { |
| prepareForLoggingApexdRevert(session, apexSession.crashingNativeProcess); |
| } |
| String errorMsg = "APEX activation failed."; |
| final String reasonForRevert = getReasonForRevert(); |
| if (!TextUtils.isEmpty(reasonForRevert)) { |
| errorMsg += " Reason: " + reasonForRevert; |
| } else if (!TextUtils.isEmpty(apexSession.errorMessage)) { |
| errorMsg += " Error: " + apexSession.errorMessage; |
| } |
| Slog.d(TAG, errorMsg); |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, errorMsg); |
| continue; |
| } else if (apexSession.isActivated || apexSession.isSuccess) { |
| hasAppliedApexSession = true; |
| continue; |
| } else if (apexSession.isStaged) { |
| // Apexd did not apply the session for some unknown reason. There is no guarantee |
| // that apexd will install it next time. Safer to proactively mark it as failed. |
| hasFailedApexSession = true; |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Staged session " + session.sessionId() + " at boot didn't activate nor " |
| + "fail. Marking it as failed anyway."); |
| } else { |
| Slog.w(TAG, "Apex session " + session.sessionId() + " is in impossible state"); |
| hasFailedApexSession = true; |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Impossible state"); |
| } |
| } |
| |
| if (hasAppliedApexSession && hasFailedApexSession) { |
| abortCheckpoint("Found both applied and failed apex sessions", supportsCheckpoint, |
| needsCheckpoint); |
| return; |
| } |
| |
| if (hasFailedApexSession) { |
| // Either of those means that we failed at least one apex session, hence we should fail |
| // all other sessions. |
| for (int i = 0; i < sessions.size(); i++) { |
| StagedSession session = sessions.get(i); |
| if (session.isSessionFailed()) { |
| // Session has been already failed in the loop above. |
| continue; |
| } |
| session.setSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Another apex session failed"); |
| } |
| return; |
| } |
| |
| // Time to resume sessions. |
| for (int i = 0; i < sessions.size(); i++) { |
| StagedSession session = sessions.get(i); |
| try { |
| resumeSession(session, supportsCheckpoint, needsCheckpoint); |
| } catch (PackageManagerException e) { |
| onInstallationFailure(session, e, supportsCheckpoint, needsCheckpoint); |
| } catch (Exception e) { |
| Slog.e(TAG, "Staged install failed due to unhandled exception", e); |
| onInstallationFailure(session, new PackageManagerException( |
| SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Staged install failed due to unhandled exception: " + e), |
| supportsCheckpoint, needsCheckpoint); |
| } |
| } |
| t.traceEnd(); |
| } |
| |
| private void logFailedApexSessionsIfNecessary() { |
| synchronized (mFailedPackageNames) { |
| if (!mFailedPackageNames.isEmpty()) { |
| WatchdogRollbackLogger.logApexdRevert(mContext, |
| mFailedPackageNames, mNativeFailureReason); |
| } |
| } |
| } |
| |
| private void markStagedSessionsAsSuccessful() { |
| synchronized (mSuccessfulStagedSessionIds) { |
| for (int i = 0; i < mSuccessfulStagedSessionIds.size(); i++) { |
| mApexManager.markStagedSessionSuccessful(mSuccessfulStagedSessionIds.get(i)); |
| } |
| } |
| } |
| |
| void systemReady() { |
| new Lifecycle(mContext).startService(this); |
| // Register the receiver of boot completed intent for staging manager. |
| mContext.registerReceiver(new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context ctx, Intent intent) { |
| onBootCompletedBroadcastReceived(); |
| ctx.unregisterReceiver(this); |
| } |
| }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); |
| |
| mFailureReasonFile.delete(); |
| } |
| |
| @VisibleForTesting |
| void onBootCompletedBroadcastReceived() { |
| mPreRebootVerificationHandler.readyToStart(); |
| BackgroundThread.getExecutor().execute(() -> logFailedApexSessionsIfNecessary()); |
| } |
| |
| private static class LocalIntentReceiverSync { |
| private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>(); |
| |
| private final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { |
| @Override |
| public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, |
| IIntentReceiver finishedReceiver, String requiredPermission, |
| Bundle options) { |
| try { |
| mResult.offer(intent, 5, TimeUnit.SECONDS); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| }; |
| |
| public IntentSender getIntentSender() { |
| return new IntentSender((IIntentSender) mLocalSender); |
| } |
| |
| public Intent getResult() { |
| try { |
| return mResult.take(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| private StagedSession getStagedSession(int sessionId) { |
| StagedSession session; |
| synchronized (mStagedSessions) { |
| session = mStagedSessions.get(sessionId); |
| } |
| return session; |
| } |
| |
| // TODO(b/136257624): Temporary API to let PMS communicate with StagingManager. When all |
| // verification logic is extracted out of StagingManager into PMS, we can remove |
| // this. |
| void notifyVerificationComplete(StagedSession session) { |
| mPreRebootVerificationHandler.onPreRebootVerificationComplete(session); |
| } |
| |
| // TODO(b/136257624): Temporary API to let PMS communicate with StagingManager. When all |
| // verification logic is extracted out of StagingManager into PMS, we can remove |
| // this. |
| void notifyPreRebootVerification_Apk_Complete(@NonNull StagedSession session) { |
| mPreRebootVerificationHandler.notifyPreRebootVerification_Apk_Complete(session); |
| } |
| |
| private final class PreRebootVerificationHandler extends Handler { |
| // Hold sessions before handler gets ready to do the verification. |
| private List<StagedSession> mPendingSessions; |
| private boolean mIsReady; |
| |
| 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) { |
| final int sessionId = msg.arg1; |
| final int rollbackId = msg.arg2; |
| final StagedSession session = (StagedSession) msg.obj; |
| if (session.isDestroyed() || session.isSessionFailed()) { |
| // No point in running verification on a destroyed/failed session |
| onPreRebootVerificationComplete(session); |
| return; |
| } |
| try { |
| switch (msg.what) { |
| case MSG_PRE_REBOOT_VERIFICATION_START: |
| handlePreRebootVerification_Start(session); |
| break; |
| case MSG_PRE_REBOOT_VERIFICATION_APEX: |
| handlePreRebootVerification_Apex(session, rollbackId); |
| break; |
| case MSG_PRE_REBOOT_VERIFICATION_APK: |
| handlePreRebootVerification_Apk(session); |
| break; |
| case MSG_PRE_REBOOT_VERIFICATION_END: |
| handlePreRebootVerification_End(session); |
| break; |
| } |
| } catch (Exception e) { |
| Slog.e(TAG, "Pre-reboot verification failed due to unhandled exception", e); |
| onPreRebootVerificationFailure(session, |
| SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, |
| "Pre-reboot verification failed due to unhandled exception: " + e); |
| } |
| } |
| |
| // Notify the handler that system is ready, and reschedule the pre-reboot verifications. |
| private synchronized void readyToStart() { |
| mIsReady = true; |
| if (mPendingSessions != null) { |
| for (int i = 0; i < mPendingSessions.size(); i++) { |
| StagedSession session = mPendingSessions.get(i); |
| startPreRebootVerification(session); |
| } |
| mPendingSessions = null; |
| } |
| } |
| |
| // Method for starting the pre-reboot verification |
| private synchronized void startPreRebootVerification( |
| @NonNull StagedSession session) { |
| if (!mIsReady) { |
| if (mPendingSessions == null) { |
| mPendingSessions = new ArrayList<>(); |
| } |
| mPendingSessions.add(session); |
| return; |
| } |
| |
| if (session.notifyStartPreRebootVerification()) { |
| int sessionId = session.sessionId(); |
| Slog.d(TAG, "Starting preRebootVerification for session " + sessionId); |
| obtainMessage(MSG_PRE_REBOOT_VERIFICATION_START, sessionId, -1, session) |
| .sendToTarget(); |
| } |
| } |
| |
| private void onPreRebootVerificationFailure(StagedSession session, |
| @SessionInfo.StagedSessionErrorCode int errorCode, String errorMessage) { |
| if (!ensureActiveApexSessionIsAborted(session)) { |
| Slog.e(TAG, "Failed to abort apex session " + session.sessionId()); |
| // Safe to ignore active apex session abortion failure since session will be marked |
| // failed on next step and staging directory for session will be deleted. |
| } |
| session.setSessionFailed(errorCode, errorMessage); |
| onPreRebootVerificationComplete(session); |
| } |
| |
| // Things to do when pre-reboot verification completes for a particular sessionId |
| private void onPreRebootVerificationComplete(StagedSession session) { |
| int sessionId = session.sessionId(); |
| Slog.d(TAG, "Stopping preRebootVerification for session " + sessionId); |
| session.notifyEndPreRebootVerification(); |
| } |
| |
| private void notifyPreRebootVerification_Start_Complete( |
| @NonNull StagedSession session, int rollbackId) { |
| obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APEX, session.sessionId(), rollbackId, |
| session).sendToTarget(); |
| } |
| |
| private void notifyPreRebootVerification_Apex_Complete( |
| @NonNull StagedSession session) { |
| obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APK, session.sessionId(), -1, session) |
| .sendToTarget(); |
| } |
| |
| private void notifyPreRebootVerification_Apk_Complete( |
| @NonNull StagedSession session) { |
| obtainMessage(MSG_PRE_REBOOT_VERIFICATION_END, session.sessionId(), -1, session) |
| .sendToTarget(); |
| } |
| |
| /** |
| * A placeholder state for starting the pre reboot verification. |
| * |
| * See {@link PreRebootVerificationHandler} to see all nodes of pre reboot verification |
| */ |
| private void handlePreRebootVerification_Start(@NonNull StagedSession session) { |
| try { |
| if (session.isMultiPackage()) { |
| for (StagedSession s : session.getChildSessions()) { |
| checkNonOverlappingWithStagedSessions(s); |
| } |
| } else { |
| checkNonOverlappingWithStagedSessions(session); |
| } |
| } catch (PackageManagerException e) { |
| onPreRebootVerificationFailure(session, e.error, e.getMessage()); |
| return; |
| } |
| |
| int rollbackId = -1; |
| if ((session.sessionParams().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 RollbackManagerInternal rm = |
| LocalServices.getService(RollbackManagerInternal.class); |
| try { |
| // NOTE: To stay consistent with the non-staged install flow, we don't fail the |
| // entire install if rollbacks can't be enabled. |
| rollbackId = rm.notifyStagedSession(session.sessionId()); |
| } catch (RuntimeException re) { |
| Slog.e(TAG, "Failed to notifyStagedSession for session: " |
| + session.sessionId(), re); |
| } |
| } else if (session.sessionParams().installReason |
| == PackageManager.INSTALL_REASON_ROLLBACK) { |
| try { |
| rollbackId = retrieveRollbackIdForCommitSession(session.sessionId()); |
| } catch (PackageManagerException e) { |
| onPreRebootVerificationFailure(session, e.error, e.getMessage()); |
| return; |
| } |
| } |
| |
| notifyPreRebootVerification_Start_Complete(session, rollbackId); |
| } |
| |
| /** |
| * 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 StagedSession session, int rollbackId) { |
| final boolean hasApex = session.containsApexSession(); |
| |
| // 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) { |
| final List<PackageInfo> apexPackages; |
| try { |
| apexPackages = submitSessionToApexService(session, rollbackId); |
| for (int i = 0, size = apexPackages.size(); i < size; i++) { |
| validateApexSignature(apexPackages.get(i)); |
| } |
| } catch (PackageManagerException e) { |
| onPreRebootVerificationFailure(session, e.error, e.getMessage()); |
| return; |
| } |
| |
| final PackageManagerInternal packageManagerInternal = |
| LocalServices.getService(PackageManagerInternal.class); |
| packageManagerInternal.pruneCachedApksInApex(apexPackages); |
| } |
| |
| notifyPreRebootVerification_Apex_Complete(session); |
| } |
| |
| /** |
| * Pre-reboot verification state for apk files. Session is sent to |
| * {@link PackageManagerService} for verification and it notifies back the result via |
| * {@link #notifyPreRebootVerification_Apk_Complete} |
| */ |
| private void handlePreRebootVerification_Apk(@NonNull StagedSession session) { |
| if (!session.containsApkSession()) { |
| notifyPreRebootVerification_Apk_Complete(session); |
| return; |
| } |
| session.verifySession(); |
| } |
| |
| /** |
| * 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 StagedSession session) { |
| // Before marking the session as ready, start checkpoint service if available |
| try { |
| if (PackageHelper.getStorageManager().supportsCheckpoint()) { |
| PackageHelper.getStorageManager().startCheckpoint(2); |
| } |
| } catch (Exception e) { |
| // Failed to get hold of StorageManager |
| Slog.e(TAG, "Failed to get hold of StorageManager", e); |
| onPreRebootVerificationFailure(session, SessionInfo.STAGED_SESSION_UNKNOWN, |
| "Failed to get hold of StorageManager"); |
| return; |
| } |
| |
| // Stop pre-reboot verification before marking session ready. From this point on, if we |
| // abandon the session then it will be cleaned up immediately. If session is abandoned |
| // after this point, then even if for some reason system tries to install the session |
| // or activate its apex, there won't be any files to work with as they will be cleaned |
| // up by the system as part of abandonment. If session is abandoned before this point, |
| // then the session is already destroyed and cannot be marked ready anymore. |
| onPreRebootVerificationComplete(session); |
| |
| // 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.setSessionReady(); |
| if (session.isSessionReady()) { |
| final boolean hasApex = session.containsApexSession(); |
| if (hasApex) { |
| try { |
| mApexManager.markStagedSessionReady(session.sessionId()); |
| } catch (PackageManagerException e) { |
| session.setSessionFailed(e.error, e.getMessage()); |
| return; |
| } |
| } |
| } |
| } |
| } |
| } |