From 8e8f724d90c6835febb8b5865009746aea73a334 Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Tue, 22 Feb 2022 20:23:43 -0600 Subject: Add UnsafeUninstall action. Fixes #6721 --- .../inc/BootstrapperEngine.h | 3 +- .../WixToolset.Mba.Core/IBootstrapperEngine.cs | 5 ++ src/burn/engine/apply.cpp | 13 +++- src/burn/engine/core.cpp | 28 ++++++-- src/burn/engine/elevation.cpp | 17 ++++- src/burn/engine/engine.mc | 21 ++++++ src/burn/engine/logging.cpp | 2 + src/burn/engine/msiengine.cpp | 2 +- src/burn/engine/plan.cpp | 84 ++++++++++++++-------- src/burn/engine/plan.h | 1 - src/burn/engine/userexperience.cpp | 2 +- src/burn/test/BurnUnitTest/PlanTest.cpp | 78 ++++++++++++++++++++ src/test/burn/WixTestTools/BundleInstaller.cs | 30 ++++++++ src/test/burn/WixTestTools/MSIExec.cs | 5 ++ .../ForwardCompatibleBundleTests.cs | 46 ++++++++++-- 15 files changed, 284 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperEngine.h b/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperEngine.h index d45c7b2a..cdb01330 100644 --- a/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperEngine.h +++ b/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperEngine.h @@ -15,13 +15,12 @@ extern "C" { static const HRESULT E_SUSPECTED_AV_INTERFERENCE = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIX, 2000); -// Note that ordering of the enumeration values is important. -// Some code paths use < or > comparisions and simply reording values will break those comparisons. enum BOOTSTRAPPER_ACTION { BOOTSTRAPPER_ACTION_UNKNOWN, BOOTSTRAPPER_ACTION_HELP, BOOTSTRAPPER_ACTION_LAYOUT, + BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL, BOOTSTRAPPER_ACTION_UNINSTALL, BOOTSTRAPPER_ACTION_CACHE, BOOTSTRAPPER_ACTION_INSTALL, diff --git a/src/api/burn/WixToolset.Mba.Core/IBootstrapperEngine.cs b/src/api/burn/WixToolset.Mba.Core/IBootstrapperEngine.cs index 8ead0919..ebea6c4f 100644 --- a/src/api/burn/WixToolset.Mba.Core/IBootstrapperEngine.cs +++ b/src/api/burn/WixToolset.Mba.Core/IBootstrapperEngine.cs @@ -338,6 +338,11 @@ namespace WixToolset.Mba.Core /// Layout, + /// + /// + /// + UnsafeUninstall, + /// /// /// diff --git a/src/burn/engine/apply.cpp b/src/burn/engine/apply.cpp index 73f8fc72..4e652768 100644 --- a/src/burn/engine/apply.cpp +++ b/src/burn/engine/apply.cpp @@ -505,8 +505,7 @@ extern "C" HRESULT ApplyUnregister( registrationType = defaultRegistrationType; - hr = UserExperienceOnUnregisterBegin(&pEngineState->userExperience, ®istrationType); - ExitOnRootFailure(hr, "BA aborted unregister begin."); + UserExperienceOnUnregisterBegin(&pEngineState->userExperience, ®istrationType); // Barring the special cases, if it was determined that we should keep the registration then // do that, otherwise the resume mode is NONE and registration will be removed. @@ -517,7 +516,7 @@ extern "C" HRESULT ApplyUnregister( // If apply failed in any way and we're going to be keeping the bundle registered then // execute any rollback dependency registration actions. - if (fFailed && BURN_RESUME_MODE_NONE < resumeMode) + if (fFailed && BURN_RESUME_MODE_NONE < resumeMode && !pEngineState->plan.fDisableRollback) { // Execute any rollback registration actions. HRESULT hrRegistrationRollback = ExecuteDependentRegistrationActions(pEngineState->companionConnection.hPipe, &pEngineState->registration, pEngineState->plan.rgRollbackRegistrationActions, pEngineState->plan.cRollbackRegistrationActions); @@ -526,6 +525,14 @@ extern "C" HRESULT ApplyUnregister( LogId(REPORT_STANDARD, MSG_SESSION_END, pEngineState->registration.sczRegistrationKey, LoggingResumeModeToString(resumeMode), LoggingRestartToString(restart), LoggingBoolToString(pEngineState->registration.fDisableResume), LoggingRegistrationTypeToString(defaultRegistrationType), LoggingRegistrationTypeToString(registrationType)); + if (BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pEngineState->plan.action) + { + registrationType = BOOTSTRAPPER_REGISTRATION_TYPE_NONE; + resumeMode = BURN_RESUME_MODE_NONE; + + LogId(REPORT_STANDARD, MSG_UNSAFE_SESSION_END); + } + if (pEngineState->registration.fPerMachine) { hr = ElevationSessionEnd(pEngineState->companionConnection.hPipe, resumeMode, restart, pEngineState->registration.fDetectedForeignProviderKeyBundleId, registrationType); diff --git a/src/burn/engine/core.cpp b/src/burn/engine/core.cpp index 8fac7bd0..9d5364a4 100644 --- a/src/burn/engine/core.cpp +++ b/src/burn/engine/core.cpp @@ -488,7 +488,7 @@ extern "C" HRESULT CorePlan( pEngineState->plan.pPayloads = &pEngineState->payloads; pEngineState->plan.wzBundleId = pEngineState->registration.sczId; pEngineState->plan.wzBundleProviderKey = pEngineState->registration.sczId; - pEngineState->plan.fDisableRollback = pEngineState->fDisableRollback; + pEngineState->plan.fDisableRollback = pEngineState->fDisableRollback || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pEngineState->plan.action; pEngineState->plan.fBundleAlreadyRegistered = pEngineState->registration.fInstalled; hr = PlanSetVariables(action, &pEngineState->variables); @@ -756,6 +756,14 @@ extern "C" HRESULT CoreApply( } } + if (BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pEngineState->plan.action) + { + fSuspend = FALSE; + restart = BOOTSTRAPPER_APPLY_RESTART_NONE; + + LogId(REPORT_STANDARD, MSG_UNSAFE_APPLY_COMPLETED); + } + if (fSuspend || BOOTSTRAPPER_APPLY_RESTART_INITIATED == restart) { // Leave cache alone. @@ -773,7 +781,7 @@ extern "C" HRESULT CoreApply( } LExit: - if (fRollbackCache) + if (fRollbackCache && !pEngineState->plan.fDisableRollback) { ApplyCacheRollback(&pEngineState->userExperience, &pEngineState->plan, pEngineState->companionConnection.hCachePipe, &applyContext); } @@ -978,6 +986,9 @@ static HRESULT CoreRecreateCommandLine( case BOOTSTRAPPER_ACTION_UNINSTALL: hr = StrAllocConcat(psczCommandLine, L" /uninstall", 0); break; + case BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL: + hr = StrAllocConcat(psczCommandLine, L" /unsafeuninstall", 0); + break; } ExitOnFailure(hr, "Failed to append action state to command-line"); @@ -1414,6 +1425,13 @@ extern "C" HRESULT CoreParseCommandLine( ExitOnFailure(hr, "Failed to copy path for layout directory."); } } + else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, &argv[i][1], -1, L"unsafeuninstall", -1)) + { + if (BOOTSTRAPPER_ACTION_HELP != pCommand->action) + { + pCommand->action = BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL; + } + } else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, &argv[i][1], -1, L"uninstall", -1)) { if (BOOTSTRAPPER_ACTION_HELP != pCommand->action) @@ -2207,7 +2225,7 @@ static void LogPackages( __in const BOOTSTRAPPER_ACTION action ) { - BOOL fUninstalling = BOOTSTRAPPER_ACTION_UNINSTALL == action; + BOOL fUninstalling = BOOTSTRAPPER_ACTION_UNINSTALL == action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == action; if (pUpgradeBundlePackage) { @@ -2286,8 +2304,8 @@ static void LogPackages( } } - // Display related bundles last if caching, installing, modifying, or repairing. - if (BOOTSTRAPPER_ACTION_UNINSTALL < action) + // Display related bundles last if not uninstalling. + if (!fUninstalling) { LogRelatedBundles(pRelatedBundles, FALSE); } diff --git a/src/burn/engine/elevation.cpp b/src/burn/engine/elevation.cpp index 86b65cf9..d12a151a 100644 --- a/src/burn/engine/elevation.cpp +++ b/src/burn/engine/elevation.cpp @@ -2289,8 +2289,21 @@ static HRESULT OnApplyInitialize( LogId(REPORT_STANDARD, MSG_SYSTEM_RESTORE_POINT_STARTING); - BOOTSTRAPPER_ACTION action = static_cast(dwAction); - SRP_ACTION restoreAction = (BOOTSTRAPPER_ACTION_INSTALL == action) ? SRP_ACTION_INSTALL : (BOOTSTRAPPER_ACTION_UNINSTALL == action) ? SRP_ACTION_UNINSTALL : SRP_ACTION_MODIFY; + SRP_ACTION restoreAction = SRP_ACTION_UNKNOWN; + switch (static_cast(dwAction)) + { + case BOOTSTRAPPER_ACTION_INSTALL: + restoreAction = SRP_ACTION_INSTALL; + break; + case BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL: __fallthrough; + case BOOTSTRAPPER_ACTION_UNINSTALL: + restoreAction = SRP_ACTION_UNINSTALL; + break; + default: + restoreAction = SRP_ACTION_MODIFY; + break; + } + hrStatus = hr = SrpCreateRestorePoint(sczBundleName, restoreAction); if (SUCCEEDED(hr)) { diff --git a/src/burn/engine/engine.mc b/src/burn/engine/engine.mc index 675a5644..4425af12 100644 --- a/src/burn/engine/engine.mc +++ b/src/burn/engine/engine.mc @@ -492,6 +492,13 @@ Language=English Planned rollback boundary: '%1!ls!', vital: %2!hs!, transaction: %3!hs! (default: %4!hs!) . +MessageId=223 +Severity=Success +SymbolicName=MSG_PLAN_NOT_SKIPPED_DUE_TO_DEPENDENTS +Language=English +Ignoring bundle dependents due to action UnsafeUninstall... +. + MessageId=299 Severity=Success SymbolicName=MSG_PLAN_COMPLETE @@ -947,6 +954,20 @@ Language=English package: %1!ls!, install registration state: %2!hs!, cache registration state: %3!hs! . +MessageId=375 +Severity=Success +SymbolicName=MSG_UNSAFE_APPLY_COMPLETED +Language=English +Ignoring suspend and restart values due to action UnsafeUninstall... +. + +MessageId=376 +Severity=Success +SymbolicName=MSG_UNSAFE_SESSION_END +Language=English +Ignoring resume and registration values due to action UnsafeUninstall... +. + MessageId=380 Severity=Warning SymbolicName=MSG_APPLY_SKIPPED diff --git a/src/burn/engine/logging.cpp b/src/burn/engine/logging.cpp index c5dd0ed8..07fc9ef3 100644 --- a/src/burn/engine/logging.cpp +++ b/src/burn/engine/logging.cpp @@ -354,6 +354,8 @@ extern "C" LPCSTR LoggingBurnActionToString( return "Layout"; case BOOTSTRAPPER_ACTION_CACHE: return "Cache"; + case BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL: + return "UnsafeUninstall"; case BOOTSTRAPPER_ACTION_UNINSTALL: return "Uninstall"; case BOOTSTRAPPER_ACTION_INSTALL: diff --git a/src/burn/engine/msiengine.cpp b/src/burn/engine/msiengine.cpp index c27dd8c2..d306f3e0 100644 --- a/src/burn/engine/msiengine.cpp +++ b/src/burn/engine/msiengine.cpp @@ -834,7 +834,7 @@ extern "C" HRESULT MsiEnginePlanInitializePackage( { Assert(BURN_PACKAGE_TYPE_MSI == pPackage->compatiblePackage.type); - pPackage->compatiblePackage.fDefaultRequested = BOOTSTRAPPER_ACTION_UNINSTALL == overallAction; + pPackage->compatiblePackage.fDefaultRequested = BOOTSTRAPPER_ACTION_UNINSTALL == overallAction || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == overallAction; pPackage->compatiblePackage.fRequested = pPackage->compatiblePackage.fDefaultRequested; hr = UserExperienceOnPlanCompatibleMsiPackageBegin(pUserExperience, pPackage->sczId, pPackage->compatiblePackage.compatibleEntry.sczId, pPackage->compatiblePackage.Msi.pVersion, &pPackage->compatiblePackage.fRequested); diff --git a/src/burn/engine/plan.cpp b/src/burn/engine/plan.cpp index f1fb87b8..49dd83f4 100644 --- a/src/burn/engine/plan.cpp +++ b/src/burn/engine/plan.cpp @@ -361,13 +361,15 @@ extern "C" HRESULT PlanDefaultPackageRequestState( else if (BOOTSTRAPPER_RELATION_PATCH == relationType && BURN_PACKAGE_TYPE_MSP == packageType) { // For patch related bundles, only install a patch if currently absent during install, modify, or repair. - if (BOOTSTRAPPER_PACKAGE_STATE_ABSENT == currentState && BOOTSTRAPPER_ACTION_INSTALL <= action) + if (BOOTSTRAPPER_PACKAGE_STATE_ABSENT != currentState) { - *pRequestState = BOOTSTRAPPER_REQUEST_STATE_PRESENT; + *pRequestState = BOOTSTRAPPER_REQUEST_STATE_NONE; } - else + else if (BOOTSTRAPPER_ACTION_INSTALL == action || + BOOTSTRAPPER_ACTION_MODIFY == action || + BOOTSTRAPPER_ACTION_REPAIR == action) { - *pRequestState = BOOTSTRAPPER_REQUEST_STATE_NONE; + *pRequestState = BOOTSTRAPPER_REQUEST_STATE_PRESENT; } } else // pick the best option for the action state and install condition. @@ -375,7 +377,7 @@ extern "C" HRESULT PlanDefaultPackageRequestState( hr = GetActionDefaultRequestState(action, currentState, &defaultRequestState); ExitOnFailure(hr, "Failed to get default request state for action."); - if (BOOTSTRAPPER_ACTION_UNINSTALL != action) + if (BOOTSTRAPPER_ACTION_UNINSTALL != action && BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL != action) { // If we're not doing an uninstall, use the install condition // to determine whether to use the default request state or make the package absent. @@ -485,7 +487,8 @@ extern "C" HRESULT PlanForwardCompatibleBundles( { fRecommendIgnore = FALSE; } - else if (BOOTSTRAPPER_ACTION_UNINSTALL == action || + else if (BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == action || + BOOTSTRAPPER_ACTION_UNINSTALL == action || BOOTSTRAPPER_ACTION_MODIFY == action || BOOTSTRAPPER_ACTION_REPAIR == action) { @@ -552,10 +555,10 @@ extern "C" HRESULT PlanRegistration( HRESULT hr = S_OK; STRINGDICT_HANDLE sdBundleDependents = NULL; STRINGDICT_HANDLE sdIgnoreDependents = NULL; + BOOL fDependentBlocksUninstall = FALSE; pPlan->fCanAffectMachineState = TRUE; // register the bundle since we're modifying machine state. pPlan->fDisallowRemoval = FALSE; // by default the bundle can be planned to be removed - pPlan->fIgnoreAllDependents = pDependencies->fIgnoreAllDependents; // Ensure the bundle is cached if not running from the cache. if (!CacheBundleRunningFromCache(pPlan->pCache)) @@ -563,7 +566,7 @@ extern "C" HRESULT PlanRegistration( pPlan->dwRegistrationOperations |= BURN_REGISTRATION_ACTION_OPERATIONS_CACHE_BUNDLE; } - if (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) + if (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action) { // If our provider key was not owned by a different bundle, // then plan to write our provider key registration to "fix it" if broken @@ -588,7 +591,7 @@ extern "C" HRESULT PlanRegistration( ExitOnFailure(hr, "Failed to add self-dependent to ignore dependents."); } - if (!pPlan->fIgnoreAllDependents) + if (!pDependencies->fIgnoreAllDependents) { // If we are not doing an upgrade, we check to see if there are still dependents on us and if so we skip planning. // However, when being upgraded, we always execute our uninstall because a newer version of us is probably @@ -641,10 +644,9 @@ extern "C" HRESULT PlanRegistration( hr = S_OK; // TODO: callback to the BA and let it have the option to ignore this dependent? - if (!pPlan->fDisallowRemoval) + if (!fDependentBlocksUninstall) { - pPlan->fDisallowRemoval = TRUE; // ensure the registration stays - *pfContinuePlanning = FALSE; // skip the rest of planning. + fDependentBlocksUninstall = TRUE; LogId(REPORT_STANDARD, MSG_PLAN_SKIPPED_DUE_TO_DEPENDENTS); } @@ -653,6 +655,20 @@ extern "C" HRESULT PlanRegistration( } ExitOnFailure(hr, "Failed to check for remaining dependents during planning."); } + + if (fDependentBlocksUninstall) + { + if (BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action) + { + fDependentBlocksUninstall = FALSE; + LogId(REPORT_STANDARD, MSG_PLAN_NOT_SKIPPED_DUE_TO_DEPENDENTS); + } + else + { + pPlan->fDisallowRemoval = TRUE; // ensure the registration stays + *pfContinuePlanning = FALSE; // skip the rest of planning. + } + } } } } @@ -776,11 +792,12 @@ static HRESULT PlanPackagesHelper( HRESULT hr = S_OK; BOOL fBundlePerMachine = pPlan->fPerMachine; // bundle is per-machine if plan starts per-machine. BURN_ROLLBACK_BOUNDARY* pRollbackBoundary = NULL; + BOOL fReverseOrder = BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action; // Initialize the packages. for (DWORD i = 0; i < cPackages; ++i) { - DWORD iPackage = (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) ? cPackages - 1 - i : i; + DWORD iPackage = fReverseOrder ? cPackages - 1 - i : i; BURN_PACKAGE* pPackage = rgPackages + iPackage; hr = InitializePackage(pPlan, pUX, pVariables, pPackage); @@ -790,7 +807,7 @@ static HRESULT PlanPackagesHelper( // Initialize the patch targets after all packages, since they could rely on the requested state of packages that are after the patch's package in the chain. for (DWORD i = 0; i < cPackages; ++i) { - DWORD iPackage = (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) ? cPackages - 1 - i : i; + DWORD iPackage = fReverseOrder ? cPackages - 1 - i : i; BURN_PACKAGE* pPackage = rgPackages + iPackage; if (BURN_PACKAGE_TYPE_MSP == pPackage->type) @@ -803,7 +820,7 @@ static HRESULT PlanPackagesHelper( // Plan the packages. for (DWORD i = 0; i < cPackages; ++i) { - DWORD iPackage = (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) ? cPackages - 1 - i : i; + DWORD iPackage = fReverseOrder ? cPackages - 1 - i : i; BURN_PACKAGE* pPackage = rgPackages + iPackage; hr = ProcessPackage(fBundlePerMachine, pUX, pPlan, pPackage, pLog, pVariables, &pRollbackBoundary); @@ -825,7 +842,7 @@ static HRESULT PlanPackagesHelper( // Plan clean up of packages. for (DWORD i = 0; i < cPackages; ++i) { - DWORD iPackage = (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) ? cPackages - 1 - i : i; + DWORD iPackage = fReverseOrder ? cPackages - 1 - i : i; BURN_PACKAGE* pPackage = rgPackages + iPackage; hr = PlanCleanPackage(pPlan, pPackage); @@ -842,7 +859,7 @@ static HRESULT PlanPackagesHelper( // Let the BA know the actions that were planned. for (DWORD i = 0; i < cPackages; ++i) { - DWORD iPackage = (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) ? cPackages - 1 - i : i; + DWORD iPackage = fReverseOrder ? cPackages - 1 - i : i; BURN_PACKAGE* pPackage = rgPackages + iPackage; UserExperienceOnPlannedPackage(pUX, pPackage->sczId, pPackage->execute, pPackage->rollback, pPackage->fPlannedCache, pPackage->fPlannedUncache); @@ -931,8 +948,9 @@ static HRESULT ProcessPackage( { HRESULT hr = S_OK; BURN_ROLLBACK_BOUNDARY* pEffectiveRollbackBoundary = NULL; + BOOL fBackward = BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action; - pEffectiveRollbackBoundary = (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) ? pPackage->pRollbackBoundaryBackward : pPackage->pRollbackBoundaryForward; + pEffectiveRollbackBoundary = fBackward ? pPackage->pRollbackBoundaryBackward : pPackage->pRollbackBoundaryForward; hr = ProcessPackageRollbackBoundary(pPlan, pUX, pLog, pVariables, pEffectiveRollbackBoundary, ppRollbackBoundary); ExitOnFailure(hr, "Failed to process package rollback boundary."); @@ -1205,6 +1223,7 @@ extern "C" HRESULT PlanDefaultRelatedBundleRequestState( { HRESULT hr = S_OK; int nCompareResult = 0; + BOOL fUninstalling = BOOTSTRAPPER_ACTION_UNINSTALL == action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == action; // Never touch related bundles during Cache. if (BOOTSTRAPPER_ACTION_CACHE == action) @@ -1215,7 +1234,7 @@ extern "C" HRESULT PlanDefaultRelatedBundleRequestState( switch (relatedBundleRelationType) { case BOOTSTRAPPER_RELATION_UPGRADE: - if (BOOTSTRAPPER_RELATION_UPGRADE != commandRelationType && BOOTSTRAPPER_ACTION_UNINSTALL < action) + if (BOOTSTRAPPER_RELATION_UPGRADE != commandRelationType && !fUninstalling) { hr = VerCompareParsedVersions(pRegistrationVersion, pRelatedBundleVersion, &nCompareResult); ExitOnFailure(hr, "Failed to compare bundle version '%ls' to related bundle version '%ls'", pRegistrationVersion ? pRegistrationVersion->sczVersion : NULL, pRelatedBundleVersion ? pRelatedBundleVersion->sczVersion : NULL); @@ -1225,7 +1244,7 @@ extern "C" HRESULT PlanDefaultRelatedBundleRequestState( break; case BOOTSTRAPPER_RELATION_PATCH: __fallthrough; case BOOTSTRAPPER_RELATION_ADDON: - if (BOOTSTRAPPER_ACTION_UNINSTALL == action) + if (fUninstalling) { *pRequestState = BOOTSTRAPPER_REQUEST_STATE_ABSENT; } @@ -1242,7 +1261,7 @@ extern "C" HRESULT PlanDefaultRelatedBundleRequestState( // Automatically repair dependent bundles to restore missing // packages after uninstall unless we're being upgraded with the // assumption that upgrades are cumulative (as intended). - if (BOOTSTRAPPER_RELATION_UPGRADE != commandRelationType && BOOTSTRAPPER_ACTION_UNINSTALL == action) + if (BOOTSTRAPPER_RELATION_UPGRADE != commandRelationType && fUninstalling) { *pRequestState = BOOTSTRAPPER_REQUEST_STATE_REPAIR; } @@ -1270,6 +1289,7 @@ extern "C" HRESULT PlanRelatedBundlesBegin( LPWSTR* rgsczAncestors = NULL; UINT cAncestors = 0; STRINGDICT_HANDLE sdAncestors = NULL; + BOOL fUninstalling = BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action; if (pPlan->pInternalCommand->sczAncestors) { @@ -1329,7 +1349,7 @@ extern "C" HRESULT PlanRelatedBundlesBegin( ExitOnRootFailure(hr, "BA aborted plan related bundle."); // If uninstalling and the dependent related bundle may be executed, ignore its provider key to allow for downgrades with ref-counting. - if (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action && BOOTSTRAPPER_RELATION_DEPENDENT == pRelatedBundle->relationType && BOOTSTRAPPER_REQUEST_STATE_NONE != pRelatedBundle->package.requested) + if (fUninstalling && BOOTSTRAPPER_RELATION_DEPENDENT == pRelatedBundle->relationType && BOOTSTRAPPER_REQUEST_STATE_NONE != pRelatedBundle->package.requested) { if (0 < pRelatedBundle->package.cDependencyProviders) { @@ -1361,6 +1381,9 @@ extern "C" HRESULT PlanRelatedBundlesComplete( HRESULT hr = S_OK; LPWSTR sczIgnoreDependencies = NULL; STRINGDICT_HANDLE sdProviderKeys = NULL; + BOOL fExecutingAnyPackage = FALSE; + BOOL fInstallingAnyPackage = FALSE; + BOOL fUninstalling = BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action; // Get the list of dependencies to ignore to pass to related bundles. hr = DependencyAllocIgnoreDependencies(pPlan, &sczIgnoreDependencies); @@ -1369,9 +1392,6 @@ extern "C" HRESULT PlanRelatedBundlesComplete( hr = DictCreateStringList(&sdProviderKeys, pPlan->cExecuteActions, DICT_FLAG_CASEINSENSITIVE); ExitOnFailure(hr, "Failed to create dictionary for planned packages."); - BOOL fExecutingAnyPackage = FALSE; - BOOL fInstallingAnyPackage = FALSE; - for (DWORD i = 0; i < pPlan->cExecuteActions; ++i) { BOOTSTRAPPER_ACTION_STATE packageAction = BOOTSTRAPPER_ACTION_STATE_NONE; @@ -1444,7 +1464,7 @@ extern "C" HRESULT PlanRelatedBundlesComplete( } // For an uninstall, there is no need to repair dependent bundles if no packages are executing. - if (!fExecutingAnyPackage && BOOTSTRAPPER_RELATION_DEPENDENT == pRelatedBundle->relationType && BOOTSTRAPPER_REQUEST_STATE_REPAIR == pRelatedBundle->package.requested && BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) + if (!fExecutingAnyPackage && BOOTSTRAPPER_RELATION_DEPENDENT == pRelatedBundle->relationType && BOOTSTRAPPER_REQUEST_STATE_REPAIR == pRelatedBundle->package.requested && fUninstalling) { pRelatedBundle->package.requested = BOOTSTRAPPER_REQUEST_STATE_NONE; LogId(REPORT_STANDARD, MSG_PLAN_SKIPPED_DEPENDENT_BUNDLE_REPAIR, pRelatedBundle->package.sczId, LoggingRelationTypeToString(pRelatedBundle->relationType)); @@ -1457,7 +1477,7 @@ extern "C" HRESULT PlanRelatedBundlesComplete( ExitOnFailure(hr, "Failed to copy the list of dependencies to ignore."); // Uninstall addons and patches early in the chain, before other packages are uninstalled. - if (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) + if (fUninstalling) { pdwInsertIndex = &dwExecuteActionEarlyIndex; } @@ -1475,7 +1495,7 @@ extern "C" HRESULT PlanRelatedBundlesComplete( ExitOnFailure(hr, "Failed to begin plan dependency actions to package: %ls", pRelatedBundle->package.sczId); // If uninstalling a related bundle, make sure the bundle is uninstalled after removing registration. - if (pdwInsertIndex && BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action) + if (pdwInsertIndex && fUninstalling) { ++(*pdwInsertIndex); } @@ -1599,9 +1619,10 @@ extern "C" HRESULT PlanCleanPackage( HRESULT hr = S_OK; BOOL fPlanCleanPackage = FALSE; BURN_CLEAN_ACTION* pCleanAction = NULL; + BOOL fUninstalling = BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action || BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL == pPlan->action; // The following is a complex set of logic that determines when a package should be cleaned from the cache. - if (BOOTSTRAPPER_CACHE_TYPE_FORCE > pPackage->cacheType || BOOTSTRAPPER_ACTION_CACHE > pPlan->action) + if (BOOTSTRAPPER_CACHE_TYPE_FORCE > pPackage->cacheType || fUninstalling) { // The following are all different reasons why the package should be cleaned from the cache. // The else-ifs are used to make the conditions easier to see (rather than have them combined @@ -1624,7 +1645,7 @@ extern "C" HRESULT PlanCleanPackage( { fPlanCleanPackage = TRUE; } - else if (BOOTSTRAPPER_ACTION_UNINSTALL == pPlan->action && // uninstalling and + else if (fUninstalling && // uninstalling and BOOTSTRAPPER_REQUEST_STATE_NONE == pPackage->requested && // requested do nothing (aka: default) and BOOTSTRAPPER_ACTION_STATE_NONE == pPackage->execute && // execute is still do nothing and !pPackage->fDependencyManagerWasHere && // dependency manager didn't change execute and @@ -2091,6 +2112,7 @@ static HRESULT GetActionDefaultRequestState( *pRequestState = BOOTSTRAPPER_REQUEST_STATE_REPAIR; break; + case BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL: __fallthrough; case BOOTSTRAPPER_ACTION_UNINSTALL: *pRequestState = BOOTSTRAPPER_REQUEST_STATE_ABSENT; break; @@ -2671,7 +2693,7 @@ static BOOL ForceCache( BOOTSTRAPPER_REQUEST_STATE_CACHE < pPackage->requested; case BOOTSTRAPPER_CACHE_TYPE_FORCE: // All packages that have cacheType set to force should be cached if the bundle is going to be present. - return BOOTSTRAPPER_ACTION_UNINSTALL < pPlan->action; + return BOOTSTRAPPER_ACTION_UNINSTALL != pPlan->action && BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL != pPlan->action; default: return FALSE; } diff --git a/src/burn/engine/plan.h b/src/burn/engine/plan.h index 3dce8e5d..c0936970 100644 --- a/src/burn/engine/plan.h +++ b/src/burn/engine/plan.h @@ -248,7 +248,6 @@ typedef struct _BURN_PLAN BOOL fDisallowRemoval; BOOL fDisableRollback; BOOL fAffectedMachineState; - BOOL fIgnoreAllDependents; LPWSTR sczLayoutDirectory; BOOL fBundleAlreadyRegistered; diff --git a/src/burn/engine/userexperience.cpp b/src/burn/engine/userexperience.cpp index 59988bef..a2f33f80 100644 --- a/src/burn/engine/userexperience.cpp +++ b/src/burn/engine/userexperience.cpp @@ -104,7 +104,7 @@ extern "C" HRESULT UserExperienceLoad( args.pCommand = pCommand; args.pfnBootstrapperEngineProc = EngineForApplicationProc; args.pvBootstrapperEngineProcContext = pEngineContext; - args.qwEngineAPIVersion = MAKEQWORDVERSION(2022, 1, 10, 0); + args.qwEngineAPIVersion = MAKEQWORDVERSION(2022, 2, 22, 0); results.cbSize = sizeof(BOOTSTRAPPER_CREATE_RESULTS); diff --git a/src/burn/test/BurnUnitTest/PlanTest.cpp b/src/burn/test/BurnUnitTest/PlanTest.cpp index 81484234..ba28713f 100644 --- a/src/burn/test/BurnUnitTest/PlanTest.cpp +++ b/src/burn/test/BurnUnitTest/PlanTest.cpp @@ -992,6 +992,84 @@ namespace Bootstrapper ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[0], L"PackageA", BURN_PACKAGE_REGISTRATION_STATE_IGNORED, BURN_PACKAGE_REGISTRATION_STATE_IGNORED); } + [Fact] + void SingleMsiUnsafeUninstallTest() + { + HRESULT hr = S_OK; + BURN_ENGINE_STATE engineState = { }; + BURN_ENGINE_STATE* pEngineState = &engineState; + BURN_PLAN* pPlan = &engineState.plan; + + InitializeEngineStateForCorePlan(wzSingleMsiManifestFileName, pEngineState); + DetectPackagesAsPresentAndCached(pEngineState); + + hr = CorePlan(pEngineState, BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL); + NativeAssert::Succeeded(hr, "CorePlan failed"); + + Assert::Equal(BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL, pPlan->action); + Assert::Equal(TRUE, pPlan->fPerMachine); + Assert::Equal(TRUE, pPlan->fDisableRollback); + + BOOL fRollback = FALSE; + DWORD dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cCacheActions); + + fRollback = TRUE; + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRollbackCacheActions); + + Assert::Equal(0ull, pPlan->qwEstimatedSize); + Assert::Equal(0ull, pPlan->qwCacheSizeTotal); + + fRollback = FALSE; + dwIndex = 0; + DWORD dwExecuteCheckpointId = 1; + ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecutePackageDependency(pPlan, fRollback, dwIndex++, L"PackageA", L"{A6F0CBF7-1578-450C-B9D7-9CF2EEC40002}", unregisterActions1, 1); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecutePackageProvider(pPlan, fRollback, dwIndex++, L"PackageA", unregisterActions1, 1); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteMsiPackage(pPlan, fRollback, dwIndex++, L"PackageA", BOOTSTRAPPER_ACTION_STATE_UNINSTALL, BURN_MSI_PROPERTY_UNINSTALL, INSTALLUILEVEL_NONE, FALSE, BOOTSTRAPPER_MSI_FILE_VERSIONING_MISSING_OR_OLDER, 0); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++); + Assert::Equal(dwIndex, pPlan->cExecuteActions); + + fRollback = TRUE; + dwIndex = 0; + dwExecuteCheckpointId = 1; + ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE); + ValidateExecutePackageDependency(pPlan, fRollback, dwIndex++, L"PackageA", L"{A6F0CBF7-1578-450C-B9D7-9CF2EEC40002}", registerActions1, 1); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecutePackageProvider(pPlan, fRollback, dwIndex++, L"PackageA", registerActions1, 1); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteMsiPackage(pPlan, fRollback, dwIndex++, L"PackageA", BOOTSTRAPPER_ACTION_STATE_INSTALL, BURN_MSI_PROPERTY_INSTALL, INSTALLUILEVEL_NONE, FALSE, BOOTSTRAPPER_MSI_FILE_VERSIONING_MISSING_OR_OLDER, 0); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++); + Assert::Equal(dwIndex, pPlan->cRollbackActions); + + Assert::Equal(1ul, pPlan->cExecutePackagesTotal); + Assert::Equal(1ul, pPlan->cOverallProgressTicksTotal); + + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRestoreRelatedBundleActions); + + dwIndex = 0; + ValidateCleanAction(pPlan, dwIndex++, L"PackageA"); + Assert::Equal(dwIndex, pPlan->cCleanActions); + + UINT uIndex = 0; + ValidatePlannedProvider(pPlan, uIndex++, L"{A6F0CBF7-1578-450C-B9D7-9CF2EEC40002}", NULL); + ValidatePlannedProvider(pPlan, uIndex++, L"{64633047-D172-4BBB-B202-64337D15C952}", NULL); + Assert::Equal(uIndex, pPlan->cPlannedProviders); + + Assert::Equal(1ul, pEngineState->packages.cPackages); + ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[0], L"PackageA", BURN_PACKAGE_REGISTRATION_STATE_ABSENT, BURN_PACKAGE_REGISTRATION_STATE_ABSENT); + } + [Fact] void SlipstreamInstallTest() { diff --git a/src/test/burn/WixTestTools/BundleInstaller.cs b/src/test/burn/WixTestTools/BundleInstaller.cs index a49c4024..2b449ebf 100644 --- a/src/test/burn/WixTestTools/BundleInstaller.cs +++ b/src/test/burn/WixTestTools/BundleInstaller.cs @@ -3,6 +3,7 @@ namespace WixTestTools { using System; + using System.Collections.Generic; using System.IO; using System.Text; @@ -130,6 +131,35 @@ namespace WixTestTools return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments, bundlePath: bundlePath); } + /// + /// Uninstalls the bundle unsafely with optional arguments. + /// + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string UnsafeUninstall(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + var newArgumentList = new List(); + newArgumentList.Add("-unsafeuninstall"); + newArgumentList.AddRange(arguments); + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Custom, newArgumentList.ToArray()); + } + + /// + /// Uninstalls the bundle unsafely at the given path with optional arguments. + /// + /// This should be the bundle in the package cache. + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string UnsafeUninstall(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + var newArgumentList = new List(); + newArgumentList.Add("-unsafeuninstall"); + newArgumentList.AddRange(arguments); + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Custom, newArgumentList.ToArray(), bundlePath: bundlePath); + } + /// /// Executes the bundle with optional arguments. /// diff --git a/src/test/burn/WixTestTools/MSIExec.cs b/src/test/burn/WixTestTools/MSIExec.cs index 8dce96cf..a10a48d6 100644 --- a/src/test/burn/WixTestTools/MSIExec.cs +++ b/src/test/burn/WixTestTools/MSIExec.cs @@ -697,6 +697,11 @@ namespace WixTestTools /// Uninstalls the product as part of cleanup /// Cleanup, + + /// + /// No action automatically added to arguments + /// + Custom, } /// diff --git a/src/test/burn/WixToolsetTest.BurnE2E/ForwardCompatibleBundleTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/ForwardCompatibleBundleTests.cs index eb649c86..357cf515 100644 --- a/src/test/burn/WixToolsetTest.BurnE2E/ForwardCompatibleBundleTests.cs +++ b/src/test/burn/WixToolsetTest.BurnE2E/ForwardCompatibleBundleTests.cs @@ -17,6 +17,38 @@ namespace WixToolsetTest.BurnE2E private const string V100 = "1.0.0.0"; private const string V200 = "2.0.0.0"; + [Fact] + public void CanIgnoreBundleDependentForUnsafeUninstall() + { + string providerId = BundleAProviderId; + string parent = "~BundleAv1"; + string parentSwitch = String.Concat("-parent ", parent); + + var packageAv1 = this.CreatePackageInstaller("PackageAv1"); + var bundleAv1 = this.CreateBundleInstaller("BundleAv1"); + var testBAController = this.CreateTestBAController(); + + packageAv1.VerifyInstalled(false); + + // Install the v1 bundle with a parent. + bundleAv1.Install(arguments: parentSwitch); + bundleAv1.VerifyRegisteredAndInPackageCache(); + + packageAv1.VerifyInstalled(true); + Assert.True(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out var actualProviderVersion)); + Assert.Equal(V100, actualProviderVersion); + Assert.True(BundleRegistration.DependencyDependentExists(providerId, parent)); + + // Cancel package B right away. + testBAController.SetPackageCancelExecuteAtProgress("PackageA", 1); + + bundleAv1.UnsafeUninstall((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_USEREXIT); + bundleAv1.VerifyUnregisteredAndRemovedFromPackageCache(); + + packageAv1.VerifyInstalled(true); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); + } + [Fact] public void CanTrack1ForwardCompatibleDependentThroughMajorUpgrade() { @@ -70,7 +102,7 @@ namespace WixToolsetTest.BurnE2E packageAv1.VerifyInstalled(false); packageAv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } [Fact] @@ -116,7 +148,7 @@ namespace WixToolsetTest.BurnE2E packageAv1.VerifyInstalled(false); packageAv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } [Fact] @@ -198,7 +230,7 @@ namespace WixToolsetTest.BurnE2E packageAv1.VerifyInstalled(false); packageAv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } [Fact] @@ -280,7 +312,7 @@ namespace WixToolsetTest.BurnE2E packageCv1.VerifyInstalled(false); packageCv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } [Fact] @@ -366,7 +398,7 @@ namespace WixToolsetTest.BurnE2E packageAv1.VerifyInstalled(false); packageAv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } [Fact] @@ -414,7 +446,7 @@ namespace WixToolsetTest.BurnE2E packageAv1.VerifyInstalled(false); packageAv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } [Fact] @@ -463,7 +495,7 @@ namespace WixToolsetTest.BurnE2E packageAv1.VerifyInstalled(false); packageAv2.VerifyInstalled(false); - Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out actualProviderVersion)); + Assert.False(BundleRegistration.TryGetDependencyProviderValue(providerId, "Version", out _)); } } } -- cgit v1.2.3-55-g6feb