From 6f6e4ced9f398ff37a44b91fdba62479cde29d06 Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Thu, 9 Jun 2022 15:30:48 -0500 Subject: Implement ArpEntry flavored ExePackage. 6772 --- src/api/wix/WixToolset.Data/ErrorMessages.cs | 29 +- .../Symbols/WixBundleExePackageSymbol.cs | 51 +++ src/api/wix/WixToolset.Data/WarningMessages.cs | 20 +- src/burn/engine/approvedexe.cpp | 8 +- src/burn/engine/approvedexe.h | 2 +- src/burn/engine/bundlepackageengine.cpp | 6 +- src/burn/engine/elevation.cpp | 2 +- src/burn/engine/engine.mc | 7 + src/burn/engine/exeengine.cpp | 295 ++++++++++++++-- src/burn/engine/package.h | 13 + src/burn/engine/plan.cpp | 3 +- src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj | 1 + src/burn/test/BurnUnitTest/PlanTest.cpp | 261 ++++++++++++++ .../PlanTest/BundlePackage_Multiple_manifest.xml | 2 +- .../ExePackage_PerUserArpEntry_manifest.xml | 1 + .../TestData/PlanTest/Failure_BundleD_manifest.xml | 2 +- .../PlanTest/Slipstream_BundleA_manifest.xml | 2 +- .../Slipstream_BundleA_modified_manifest.xml | 2 +- .../TestData/DependencyTests/BundleD/BundleD.wxs | 2 +- .../BrokenPerUserArpEntryExePackage.wixproj | 17 + .../BrokenPerUserArpEntryExePackage.wxs | 18 + .../PackageFail/PackageFail.wixproj | 12 + .../PerMachineArpEntryExePackage.wixproj | 19 + .../PerMachineArpEntryExePackage.wxs | 18 + .../PerMachineArpEntryExePackageFailure.wixproj | 18 + .../PerMachineArpEntryExePackageFailure.wxs | 19 + .../PerUserArpEntryExePackage.wixproj | 17 + .../PerUserArpEntryExePackage.wxs | 18 + src/test/burn/WixTestTools/ArpEntryInstaller.cs | 43 +++ src/test/burn/WixTestTools/ArpEntryVerifier.cs | 24 ++ src/test/burn/WixTestTools/BundleRegistration.cs | 121 +------ src/test/burn/WixTestTools/BundleVerifier.cs | 23 ++ .../burn/WixTestTools/GenericArpRegistration.cs | 143 ++++++++ .../burn/WixToolsetTest.BurnE2E/BurnE2ETests.cs | 17 + src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs | 4 +- .../burn/WixToolsetTest.BurnE2E/ExePackageTests.cs | 99 ++++++ src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs | 23 -- .../WixToolsetTest.BurnE2E/TestBAController.cs | 217 ------------ .../WixToolsetTest.BurnE2E/Utilities/IWebServer.cs | 23 ++ .../Utilities/TestBAController.cs | 217 ++++++++++++ .../Utilities/TestExeTool.cs | 17 + .../Bundles/CreateBurnManifestCommand.cs | 30 +- .../PerformBundleBackendValidationCommand.cs | 12 +- src/wix/WixToolset.Core/Compiler_Bundle.cs | 308 +++++++++++----- .../BundleManifestFixture.cs | 4 +- .../ExePackageFixture.cs | 389 ++++++++++++++++++++- .../PackagePayloadFixture.cs | 2 +- .../TestData/ExePackage/ArpEntry.wxs | 12 + .../ExePackage/ArpEntryWithDetectCondition.wxs | 12 + .../ExePackage/ArpEntryWithUninstallArguments.wxs | 12 + .../TestData/ExePackage/InvalidArpEntryId.wxs | 11 + .../TestData/ExePackage/InvalidArpEntryVersion.wxs | 11 + .../TestData/ExePackage/MissingDetectCondition.wxs | 9 - .../NonPermanentWithOnlyDetectCondition.wxs | 10 + .../NonPermanentWithOnlyUninstallArguments.wxs | 10 + ...tectConditionOrUninstallArgumentsOrArpEntry.wxs | 9 + .../PermanentWithEmptyDetectCondition.wxs | 11 + ...tectConditionOrUninstallArgumentsOrArpEntry.wxs | 10 + .../RepairablePermanentWithoutDetectCondition.wxs | 12 + ...tectConditionOrUninstallArgumentsOrArpEntry.wxs | 11 + .../TestData/ExePackage/RequireDetectCondition.wxs | 11 - .../UninstallArgumentsWithoutDetectCondition.wxs | 12 + 62 files changed, 2217 insertions(+), 527 deletions(-) create mode 100644 src/burn/test/BurnUnitTest/TestData/PlanTest/ExePackage_PerUserArpEntry_manifest.xml create mode 100644 src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wixproj create mode 100644 src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wxs create mode 100644 src/test/burn/TestData/ExePackageTests/PackageFail/PackageFail.wixproj create mode 100644 src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wixproj create mode 100644 src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wxs create mode 100644 src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wixproj create mode 100644 src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wxs create mode 100644 src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wixproj create mode 100644 src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wxs create mode 100644 src/test/burn/WixTestTools/ArpEntryInstaller.cs create mode 100644 src/test/burn/WixTestTools/ArpEntryVerifier.cs create mode 100644 src/test/burn/WixTestTools/GenericArpRegistration.cs create mode 100644 src/test/burn/WixToolsetTest.BurnE2E/ExePackageTests.cs delete mode 100644 src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs delete mode 100644 src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs create mode 100644 src/test/burn/WixToolsetTest.BurnE2E/Utilities/IWebServer.cs create mode 100644 src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestBAController.cs create mode 100644 src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestExeTool.cs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntry.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithDetectCondition.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithUninstallArguments.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryId.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryVersion.wxs delete mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/MissingDetectCondition.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyDetectCondition.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyUninstallArguments.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithEmptyDetectCondition.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectCondition.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs delete mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RequireDetectCondition.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/UninstallArgumentsWithoutDetectCondition.wxs (limited to 'src') diff --git a/src/api/wix/WixToolset.Data/ErrorMessages.cs b/src/api/wix/WixToolset.Data/ErrorMessages.cs index d3d2932d..31888d1f 100644 --- a/src/api/wix/WixToolset.Data/ErrorMessages.cs +++ b/src/api/wix/WixToolset.Data/ErrorMessages.cs @@ -388,7 +388,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.ExpectedAttribute, "The {0}/@{1} attribute was not found; it is required.", elementName, attributeName); } - public static Message ExpectedAttribute(SourceLineNumber sourceLineNumbers, string elementName, string attribute1Name, string attribute2Name, Boolean eitherOr) + public static Message ExpectedAttribute(SourceLineNumber sourceLineNumbers, string elementName, string attribute1Name, string attribute2Name, bool eitherOr) { return Message(sourceLineNumbers, Ids.ExpectedAttribute, "The {0} element must have a value for exactly one of the {1} or {2} attributes.", elementName, attribute1Name, attribute2Name, eitherOr); } @@ -403,7 +403,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.ExpectedAttribute, "The {0}/@{1} attribute was not found; it is required when attribute {2} has a value of '{3}'.", elementName, attributeName, otherAttributeName, otherAttributeValue); } - public static Message ExpectedAttribute(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string otherAttributeName, string otherAttributeValue, Boolean otherAttributeValueUnless) + public static Message ExpectedAttribute(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string otherAttributeName, string otherAttributeValue, bool otherAttributeValueUnless) { return Message(sourceLineNumbers, Ids.ExpectedAttribute, "The {0}/@{1} attribute was not found; it is required unless the attribute {2} has a value of '{3}'.", elementName, attributeName, otherAttributeName, otherAttributeValue, otherAttributeValueUnless); } @@ -428,6 +428,21 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.ExpectedAttributeOrElement, "Element '{0}' missing attribute '{1}' or child element '{2}'. Exactly one of those is required.", parentElement, attribute, childElement); } + public static Message ExpectedAttributeOrElementWithOtherAttribute(SourceLineNumber sourceLineNumbers, string parentElement, string attribute, string childElement, string otherAttribute) + { + return Message(sourceLineNumbers, Ids.ExpectedAttributeOrElementWithOtherAttribute, "Element '{0}' missing attribute '{1}' or child element '{2}'. Exactly one of those is required when attribute '{3}' is specified.", parentElement, attribute, childElement, otherAttribute); + } + + public static Message ExpectedAttributeOrElementWithOtherAttribute(SourceLineNumber sourceLineNumbers, string parentElement, string attribute, string childElement, string otherAttribute, string otherAttributeValue) + { + return Message(sourceLineNumbers, Ids.ExpectedAttributeOrElementWithOtherAttribute, "Element '{0}' missing attribute '{1}' or child element '{2}'. Exactly one of those is required when attribute '{3}' is specified with value '{4}'.", parentElement, attribute, childElement, otherAttribute, otherAttributeValue); + } + + public static Message ExpectedAttributeOrElementWithoutOtherAttribute(SourceLineNumber sourceLineNumbers, string parentElement, string attribute, string childElement, string otherAttribute) + { + return Message(sourceLineNumbers, Ids.ExpectedAttributeOrElementWithoutOtherAttribute, "Element '{0}' missing attribute '{1}' or child element '{2}'. Exactly one of those is required when attribute '{3}' is not specified.", parentElement, attribute, childElement, otherAttribute); + } + public static Message ExpectedAttributes(SourceLineNumber sourceLineNumbers, string elementName, string attributeName1, string attributeName2) { return Message(sourceLineNumbers, Ids.ExpectedAttributes, "The {0} element's {1} or {2} attribute was not found; one of these is required.", elementName, attributeName1, attributeName2); @@ -839,7 +854,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.IllegalAttributeWithoutOtherAttributes, "The {0}/@{1} attribute can only be specified with one of the following attributes: {2} or {3} present.", elementName, attributeName, otherAttributeName1, otherAttributeName2); } - public static Message IllegalAttributeWithoutOtherAttributes(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string otherAttributeName1, string otherAttributeName2, string otherAttributeValue, Boolean uniquifier) + public static Message IllegalAttributeWithoutOtherAttributes(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string otherAttributeName1, string otherAttributeName2, string otherAttributeValue, bool uniquifier) { return Message(sourceLineNumbers, Ids.IllegalAttributeWithoutOtherAttributes, "The {0}/@{1} attribute can only be specified with one of the following attributes: {2} or {3} present with value '{4}'.", elementName, attributeName, otherAttributeName1, otherAttributeName2, otherAttributeValue, uniquifier); } @@ -899,7 +914,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.IllegalComponentWithAutoGeneratedGuid, "The Component/@Guid attribute's value '*' is not valid for this component because it does not meet the criteria for having an automatically generated guid. Components using a Directory as a KeyPath or containing ODBCDataSource child elements cannot use an automatically generated guid. Make sure your component doesn't have a Directory as the KeyPath and move any ODBCDataSource child elements to components with explicit component guids."); } - public static Message IllegalComponentWithAutoGeneratedGuid(SourceLineNumber sourceLineNumbers, Boolean registryKeyPath) + public static Message IllegalComponentWithAutoGeneratedGuid(SourceLineNumber sourceLineNumbers, bool registryKeyPath) { return Message(sourceLineNumbers, Ids.IllegalComponentWithAutoGeneratedGuid, "The Component/@Guid attribute's value '*' is not valid for this component because it does not meet the criteria for having an automatically generated guid. Components with registry keypaths and files cannot use an automatically generated guid. Create multiple components, each with one file and/or one registry value keypath, to use automatically generated guids.", registryKeyPath); } @@ -1414,12 +1429,12 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.MergeFeatureRequired, "The {0} table contains a row with primary key(s) '{1}' which requires a feature to properly merge from the merge module '{2}'. Nest a MergeRef element with an Id attribute set to the value '{3}' under a Feature element to fix this error.", tableName, primaryKeys, mergeModuleFile, mergeId); } - public static Message MergeLanguageFailed(SourceLineNumber sourceLineNumbers, Int16 language, string mergeModuleFile) + public static Message MergeLanguageFailed(SourceLineNumber sourceLineNumbers, short language, string mergeModuleFile) { return Message(sourceLineNumbers, Ids.MergeLanguageFailed, "The language '{0}' is supported but uses an invalid language transform in the merge module '{1}'.", language, mergeModuleFile); } - public static Message MergeLanguageUnsupported(SourceLineNumber sourceLineNumbers, Int16 language, string mergeModuleFile) + public static Message MergeLanguageUnsupported(SourceLineNumber sourceLineNumbers, short language, string mergeModuleFile) { return Message(sourceLineNumbers, Ids.MergeLanguageUnsupported, "Could not locate language '{0}' (or a transform for this language) in the merge module '{1}'. This is likely due to an incorrectly authored Merge/@Language attribute.", language, mergeModuleFile); } @@ -2710,6 +2725,8 @@ namespace WixToolset.Data MsiTransactionX86BeforeX64Package2 = 410, MsiTransactionInvalidPackage = 411, MsiTransactionInvalidPackage2 = 412, + ExpectedAttributeOrElementWithOtherAttribute = 413, + ExpectedAttributeOrElementWithoutOtherAttribute = 414, } } } diff --git a/src/api/wix/WixToolset.Data/Symbols/WixBundleExePackageSymbol.cs b/src/api/wix/WixToolset.Data/Symbols/WixBundleExePackageSymbol.cs index 6cf200c2..74146716 100644 --- a/src/api/wix/WixToolset.Data/Symbols/WixBundleExePackageSymbol.cs +++ b/src/api/wix/WixToolset.Data/Symbols/WixBundleExePackageSymbol.cs @@ -16,6 +16,9 @@ namespace WixToolset.Data new IntermediateFieldDefinition(nameof(WixBundleExePackageSymbolFields.RepairCommand), IntermediateFieldType.String), new IntermediateFieldDefinition(nameof(WixBundleExePackageSymbolFields.UninstallCommand), IntermediateFieldType.String), new IntermediateFieldDefinition(nameof(WixBundleExePackageSymbolFields.ExeProtocol), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(WixBundleExePackageSymbolFields.DetectionType), IntermediateFieldType.Number), + new IntermediateFieldDefinition(nameof(WixBundleExePackageSymbolFields.ArpId), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(WixBundleExePackageSymbolFields.ArpDisplayVersion), IntermediateFieldType.String), }, typeof(WixBundleExePackageSymbol)); } @@ -33,6 +36,19 @@ namespace WixToolset.Data.Symbols RepairCommand, UninstallCommand, ExeProtocol, + DetectionType, + ArpId, + ArpDisplayVersion, + } + + /// + /// How Burn will detect the ExePackage. + /// + public enum WixBundleExePackageDetectionType + { + None, + Condition, + Arp, } [Flags] @@ -40,6 +56,7 @@ namespace WixToolset.Data.Symbols { None = 0, Bundle = 1, + ArpWin64 = 2, } public class WixBundleExePackageSymbol : IntermediateSymbol @@ -90,6 +107,24 @@ namespace WixToolset.Data.Symbols set => this.Set((int)WixBundleExePackageSymbolFields.ExeProtocol, value); } + public WixBundleExePackageDetectionType DetectionType + { + get => (WixBundleExePackageDetectionType)this.Fields[(int)WixBundleExePackageSymbolFields.DetectionType].AsNumber(); + set => this.Set((int)WixBundleExePackageSymbolFields.DetectionType, (int)value); + } + + public string ArpId + { + get => (string)this.Fields[(int)WixBundleExePackageSymbolFields.ArpId]; + set => this.Set((int)WixBundleExePackageSymbolFields.ArpId, value); + } + + public string ArpDisplayVersion + { + get => (string)this.Fields[(int)WixBundleExePackageSymbolFields.ArpDisplayVersion]; + set => this.Set((int)WixBundleExePackageSymbolFields.ArpDisplayVersion, value); + } + public bool IsBundle { get { return this.Attributes.HasFlag(WixBundleExePackageAttributes.Bundle); } @@ -106,6 +141,22 @@ namespace WixToolset.Data.Symbols } } + public bool ArpWin64 + { + get { return this.Attributes.HasFlag(WixBundleExePackageAttributes.ArpWin64); } + set + { + if (value) + { + this.Attributes |= WixBundleExePackageAttributes.ArpWin64; + } + else + { + this.Attributes &= ~WixBundleExePackageAttributes.ArpWin64; + } + } + } + public bool Repairable => this.RepairCommand != null; public bool Uninstallable => this.UninstallCommand != null; diff --git a/src/api/wix/WixToolset.Data/WarningMessages.cs b/src/api/wix/WixToolset.Data/WarningMessages.cs index 5a5d1e79..5d3f06f8 100644 --- a/src/api/wix/WixToolset.Data/WarningMessages.cs +++ b/src/api/wix/WixToolset.Data/WarningMessages.cs @@ -237,6 +237,11 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.DetectConditionRecommended, "The {0}/@DetectCondition attribute is recommended so the package is only installed when absent.", elementName); } + public static Message ExePackageDetectInformationRecommended(SourceLineNumber sourceLineNumbers) + { + return Message(sourceLineNumbers, Ids.ExePackageDetectInformationRecommended, "The ExePackage/@DetectCondition attribute or child element ArpEntry is recommended so the package is only installed when absent."); + } + public static Message DownloadUrlNotSupportedForAttachedContainers(SourceLineNumber sourceLineNumbers, string containerId) { return Message(sourceLineNumbers, Ids.DownloadUrlNotSupportedForAttachedContainers, "The Container '{0}' is attached but included a @DownloadUrl attribute. Attached Containers cannot be downloaded so the download URL is being ignored.", containerId); @@ -267,7 +272,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.EmptyCabinet, "The cabinet '{0}' does not contain any files. If this installation contains no files, this warning can likely be safely ignored. Otherwise, please add files to the cabinet or remove it.", cabinetName); } - public static Message EmptyCabinet(SourceLineNumber sourceLineNumbers, string cabinetName, Boolean isPatch) + public static Message EmptyCabinet(SourceLineNumber sourceLineNumbers, string cabinetName, bool isPatch) { return Message(sourceLineNumbers, Ids.EmptyCabinet, "The cabinet '{0}' does not contain any files. If this patch contains no files, this warning can likely be safely ignored. Otherwise, try passing -p to torch.exe when first building the transforms, or add a ComponentRef to your PatchFamily authoring to pull changed files into the cabinet.", cabinetName, isPatch); } @@ -322,7 +327,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.IllegalActionInSequence, "The {0} table contains an action '{1}' which is not allowed in this table. If this is a standard action then it is not valid for this table, if it is a custom action or dialog then this table does not accept actions of that type. This action will be left out of the decompiled output.", sequenceTableName, actionName); } - public static Message IllegalColumnValue(SourceLineNumber sourceLineNumbers, string tableName, string columnName, Object value) + public static Message IllegalColumnValue(SourceLineNumber sourceLineNumbers, string tableName, string columnName, object value) { return Message(sourceLineNumbers, Ids.IllegalColumnValue, "The {0}.{1} column's value, '{2}', is not a recognized legal value. This information will be left out of the decompiled output.", tableName, columnName, value); } @@ -407,7 +412,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.MsiTransactionLimitations, "MSI transactions have limitations that make it hard to use them successfully in a bundle. Test the bundle thoroughly, especially in upgrade scenarios and the scenario that required them in the first place."); } - public static Message NestedInstall(SourceLineNumber sourceLineNumbers, string tableName, string columnName, Object value) + public static Message NestedInstall(SourceLineNumber sourceLineNumbers, string tableName, string columnName, object value) { return Message(sourceLineNumbers, Ids.NestedInstall, "The {0}.{1} column's value, '{2}', indicates a nested install. Nested installations are not supported by the WiX team. This action will be left out of the decompiled output.", tableName, columnName, value); } @@ -628,7 +633,7 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.UnknownPermission, "The {0} table contains a row with primary key '{1}' which has an unknown permission at bit {2}.", tableName, primaryKey, bitPosition); } - public static Message UnrepresentableColumnValue(SourceLineNumber sourceLineNumbers, string tableName, string columnName, Object value) + public static Message UnrepresentableColumnValue(SourceLineNumber sourceLineNumbers, string tableName, string columnName, object value) { return Message(sourceLineNumbers, Ids.UnrepresentableColumnValue, "The {0}.{1} column's value, '{2}', cannot currently be represented in the WiX schema.", tableName, columnName, value); } @@ -688,6 +693,11 @@ namespace WixToolset.Data return Message(sourceLineNumbers, Ids.WindowsInstallerFileTooLarge, "The Windows Installer does not support {0} files larger than 2GB in size. Reduce the size or number of files embedded in '{1}' or the installation will likely fail with an unexpected error.", fileDescription, path); } + public static Message InvalidWixVersion(SourceLineNumber sourceLineNumbers, string version, string elementName, string attributeName) + { + return Message(sourceLineNumbers, Ids.InvalidWixVersion, "Invalid WixVersion '{0}' in {1}/@'{2}'. Comparisons may yield unexpected results.", version, elementName, attributeName); + } + private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args) { return new Message(sourceLineNumber, MessageLevel.Warning, (int)id, format, args); @@ -827,6 +837,8 @@ namespace WixToolset.Data WindowsInstallerFileTooLarge = 1158, UnavailableBundleConditionVariable = 1159, DiscardedRollbackBoundary2 = 1160, + ExePackageDetectInformationRecommended = 1161, + InvalidWixVersion = 1162, } } } diff --git a/src/burn/engine/approvedexe.cpp b/src/burn/engine/approvedexe.cpp index b9efd624..2a96868e 100644 --- a/src/burn/engine/approvedexe.cpp +++ b/src/burn/engine/approvedexe.cpp @@ -213,7 +213,7 @@ LExit: extern "C" HRESULT ApprovedExesVerifySecureLocation( __in BURN_CACHE* pCache, __in BURN_VARIABLES* pVariables, - __in BURN_LAUNCH_APPROVED_EXE* pLaunchApprovedExe + __in LPCWSTR wzExecutablePath ) { HRESULT hr = S_OK; @@ -232,7 +232,7 @@ extern "C" HRESULT ApprovedExesVerifySecureLocation( hr = VariableGetString(pVariables, wzSecureFolderVariable, &scz); if (SUCCEEDED(hr)) { - hr = PathDirectoryContainsPath(scz, pLaunchApprovedExe->sczExecutablePath); + hr = PathDirectoryContainsPath(scz, wzExecutablePath); if (S_OK == hr) { ExitFunction(); @@ -252,14 +252,14 @@ extern "C" HRESULT ApprovedExesVerifySecureLocation( // If the package cache is redirected, hr is S_FALSE. if (S_FALSE == hr) { - hr = PathDirectoryContainsPath(sczSecondary, pLaunchApprovedExe->sczExecutablePath); + hr = PathDirectoryContainsPath(sczSecondary, wzExecutablePath); if (S_OK == hr) { ExitFunction(); } } - hr = PathDirectoryContainsPath(scz, pLaunchApprovedExe->sczExecutablePath); + hr = PathDirectoryContainsPath(scz, wzExecutablePath); if (S_OK == hr) { ExitFunction(); diff --git a/src/burn/engine/approvedexe.h b/src/burn/engine/approvedexe.h index 8a3f6779..cbfda601 100644 --- a/src/burn/engine/approvedexe.h +++ b/src/burn/engine/approvedexe.h @@ -67,7 +67,7 @@ HRESULT ApprovedExesLaunch( HRESULT ApprovedExesVerifySecureLocation( __in BURN_CACHE* pCache, __in BURN_VARIABLES* pVariables, - __in BURN_LAUNCH_APPROVED_EXE* pLaunchApprovedExe + __in LPCWSTR wzExecutablePath ); diff --git a/src/burn/engine/bundlepackageengine.cpp b/src/burn/engine/bundlepackageengine.cpp index 6a0343bd..81279cf6 100644 --- a/src/burn/engine/bundlepackageengine.cpp +++ b/src/burn/engine/bundlepackageengine.cpp @@ -365,8 +365,7 @@ extern "C" HRESULT BundlePackageEnginePlanCalculatePackage( break; default: - hr = E_INVALIDARG; - ExitOnRootFailure(hr, "Invalid package current state: %d.", pPackage->currentState); + ExitWithRootFailure(hr, E_INVALIDARG, "Invalid package current state: %d.", pPackage->currentState); } // Calculate the rollback action if there is an execute action. @@ -413,8 +412,7 @@ extern "C" HRESULT BundlePackageEnginePlanCalculatePackage( break; default: - hr = E_INVALIDARG; - ExitOnRootFailure(hr, "Invalid package expected state."); + ExitWithRootFailure(hr, E_INVALIDARG, "Invalid package expected state."); } } diff --git a/src/burn/engine/elevation.cpp b/src/burn/engine/elevation.cpp index 75b24ec3..9c7cf89f 100644 --- a/src/burn/engine/elevation.cpp +++ b/src/burn/engine/elevation.cpp @@ -3858,7 +3858,7 @@ static HRESULT OnLaunchApprovedExe( hr = RegReadString(hKey, pApprovedExe->sczValueName, &pLaunchApprovedExe->sczExecutablePath); ExitOnFailure(hr, "Failed to read the value for the approved exe path."); - hr = ApprovedExesVerifySecureLocation(pCache, pVariables, pLaunchApprovedExe); + hr = ApprovedExesVerifySecureLocation(pCache, pVariables, pLaunchApprovedExe->sczExecutablePath); ExitOnFailure(hr, "Failed to verify the executable path is in a secure location: %ls", pLaunchApprovedExe->sczExecutablePath); if (S_FALSE == hr) { diff --git a/src/burn/engine/engine.mc b/src/burn/engine/engine.mc index 1c03145b..52524edb 100644 --- a/src/burn/engine/engine.mc +++ b/src/burn/engine/engine.mc @@ -331,6 +331,13 @@ Language=English Detected msi package with invalid version, product code: '%1!ls!', version: '%2!ls!' . +MessageId=124 +Severity=Warning +SymbolicName=MSG_DETECTED_EXE_PACKAGE_INVALID_VERSION +Language=English +Detected exe package with invalid version, arp id: '%1!ls!', version: '%2!ls!' +. + MessageId=151 Severity=Error SymbolicName=MSG_FAILED_DETECT_PACKAGE diff --git a/src/burn/engine/exeengine.cpp b/src/burn/engine/exeengine.cpp index 808577bc..3a64ecd8 100644 --- a/src/burn/engine/exeengine.cpp +++ b/src/burn/engine/exeengine.cpp @@ -2,6 +2,16 @@ #include "precomp.h" +static HRESULT DetectArpEntry( + __in const BURN_PACKAGE* pPackage, + __out BOOTSTRAPPER_PACKAGE_STATE* pPackageState, + __out_opt LPWSTR* psczQuietUninstallString + ); +static HRESULT ParseArpUninstallString( + __in_z LPCWSTR wzArpUninstallString, + __inout LPWSTR* psczExecutablePath, + __inout LPWSTR* psczArguments + ); // function definitions @@ -16,18 +26,73 @@ extern "C" HRESULT ExeEngineParsePackageFromXml( IXMLDOMNode* pixnNode = NULL; LPWSTR scz = NULL; - // @DetectCondition - hr = XmlGetAttributeEx(pixnExePackage, L"DetectCondition", &pPackage->Exe.sczDetectCondition); - ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @DetectCondition."); + // @DetectionType + hr = XmlGetAttributeEx(pixnExePackage, L"DetectionType", &scz); + ExitOnRequiredXmlQueryFailure(hr, "Failed to get @DetectionType."); + + if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"condition", -1)) + { + pPackage->Exe.detectionType = BURN_EXE_DETECTION_TYPE_CONDITION; + } + else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"arp", -1)) + { + pPackage->Exe.detectionType = BURN_EXE_DETECTION_TYPE_ARP; + } + else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, scz, -1, L"none", -1)) + { + pPackage->Exe.detectionType = BURN_EXE_DETECTION_TYPE_NONE; + } + else + { + ExitWithRootFailure(hr, E_UNEXPECTED, "Invalid detection type: %ls", scz); + } + + if (BURN_EXE_DETECTION_TYPE_CONDITION == pPackage->Exe.detectionType) + { + // @DetectCondition + hr = XmlGetAttributeEx(pixnExePackage, L"DetectCondition", &pPackage->Exe.sczDetectCondition); + ExitOnRequiredXmlQueryFailure(hr, "Failed to get @DetectCondition."); + + // @UninstallArguments + hr = XmlGetAttributeEx(pixnExePackage, L"UninstallArguments", &pPackage->Exe.sczUninstallArguments); + ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @UninstallArguments."); + + // @Uninstallable + hr = XmlGetYesNoAttribute(pixnExePackage, L"Uninstallable", &pPackage->Exe.fUninstallable); + ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @Uninstallable."); + } + else if (BURN_EXE_DETECTION_TYPE_ARP == pPackage->Exe.detectionType) + { + // @ArpId + hr = XmlGetAttributeEx(pixnExePackage, L"ArpId", &scz); + ExitOnRequiredXmlQueryFailure(hr, "Failed to get @ArpId."); + + hr = PathConcatRelativeToBase(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\", scz, &pPackage->Exe.sczArpKeyPath); + ExitOnFailure(hr, "Failed to build full key path."); + + // @ArpDisplayVersion + hr = XmlGetAttributeEx(pixnExePackage, L"ArpDisplayVersion", &scz); + ExitOnRequiredXmlQueryFailure(hr, "Failed to get @ArpDisplayVersion."); + + hr = VerParseVersion(scz, 0, FALSE, &pPackage->Exe.pArpDisplayVersion); + ExitOnFailure(hr, "Failed to parse @ArpDisplayVersion: %ls", scz); + + if (pPackage->Exe.pArpDisplayVersion->fInvalid) + { + LogId(REPORT_WARNING, MSG_MANIFEST_INVALID_VERSION, scz); + } + + // @ArpWin64 + hr = XmlGetYesNoAttribute(pixnExePackage, L"ArpWin64", &pPackage->Exe.fArpWin64); + ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @ArpWin64."); + + pPackage->Exe.fUninstallable = TRUE; + } // @InstallArguments hr = XmlGetAttributeEx(pixnExePackage, L"InstallArguments", &pPackage->Exe.sczInstallArguments); ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @InstallArguments."); - // @UninstallArguments - hr = XmlGetAttributeEx(pixnExePackage, L"UninstallArguments", &pPackage->Exe.sczUninstallArguments); - ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @UninstallArguments."); - // @RepairArguments hr = XmlGetAttributeEx(pixnExePackage, L"RepairArguments", &pPackage->Exe.sczRepairArguments); ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @RepairArguments."); @@ -36,10 +101,6 @@ extern "C" HRESULT ExeEngineParsePackageFromXml( hr = XmlGetYesNoAttribute(pixnExePackage, L"Repairable", &pPackage->Exe.fRepairable); ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @Repairable."); - // @Uninstallable - hr = XmlGetYesNoAttribute(pixnExePackage, L"Uninstallable", &pPackage->Exe.fUninstallable); - ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @Uninstallable."); - // @Bundle hr = XmlGetYesNoAttribute(pixnExePackage, L"Bundle", &pPackage->Exe.fBundle); ExitOnOptionalXmlQueryFailure(hr, fFoundXml, "Failed to get @Bundle."); @@ -64,8 +125,7 @@ extern "C" HRESULT ExeEngineParsePackageFromXml( } else { - hr = E_UNEXPECTED; - ExitOnFailure(hr, "Invalid protocol type: %ls", scz); + ExitWithRootFailure(hr, E_UNEXPECTED, "Invalid protocol type: %ls", scz); } } @@ -91,6 +151,8 @@ extern "C" void ExeEnginePackageUninitialize( ReleaseStr(pPackage->Exe.sczInstallArguments); ReleaseStr(pPackage->Exe.sczRepairArguments); ReleaseStr(pPackage->Exe.sczUninstallArguments); + ReleaseStr(pPackage->Exe.sczArpKeyPath); + ReleaseVerutilVersion(pPackage->Exe.pArpDisplayVersion); ReleaseMem(pPackage->Exe.rgExitCodes); // free command-line arguments @@ -126,15 +188,31 @@ extern "C" HRESULT ExeEngineDetectPackage( HRESULT hr = S_OK; BOOL fDetected = FALSE; - // evaluate detect condition - if (pPackage->Exe.sczDetectCondition && *pPackage->Exe.sczDetectCondition) + switch (pPackage->Exe.detectionType) { - hr = ConditionEvaluate(pVariables, pPackage->Exe.sczDetectCondition, &fDetected); - ExitOnFailure(hr, "Failed to evaluate executable package detect condition."); - } + case BURN_EXE_DETECTION_TYPE_NONE: + pPackage->currentState = BOOTSTRAPPER_PACKAGE_STATE_ABSENT; + break; + case BURN_EXE_DETECTION_TYPE_CONDITION: + // evaluate detect condition + if (pPackage->Exe.sczDetectCondition && *pPackage->Exe.sczDetectCondition) + { + hr = ConditionEvaluate(pVariables, pPackage->Exe.sczDetectCondition, &fDetected); + ExitOnFailure(hr, "Failed to evaluate EXE package detect condition."); + } + + // update detect state + pPackage->currentState = fDetected ? BOOTSTRAPPER_PACKAGE_STATE_PRESENT : BOOTSTRAPPER_PACKAGE_STATE_ABSENT; - // update detect state - pPackage->currentState = fDetected ? BOOTSTRAPPER_PACKAGE_STATE_PRESENT : BOOTSTRAPPER_PACKAGE_STATE_ABSENT; + break; + case BURN_EXE_DETECTION_TYPE_ARP: + hr = DetectArpEntry(pPackage, &pPackage->currentState, NULL); + ExitOnFailure(hr, "Failed to detect EXE package by ArpEntry."); + + break; + default: + ExitWithRootFailure(hr, E_INVALIDARG, "Unknown EXE package detection type: %d.", pPackage->Exe.detectionType); + } if (pPackage->fCanAffectRegistration) { @@ -187,6 +265,7 @@ extern "C" HRESULT ExeEnginePlanCalculatePackage( } break; + case BOOTSTRAPPER_PACKAGE_STATE_OBSOLETE: __fallthrough; case BOOTSTRAPPER_PACKAGE_STATE_ABSENT: switch (pPackage->requested) { @@ -205,8 +284,7 @@ extern "C" HRESULT ExeEnginePlanCalculatePackage( break; default: - hr = E_INVALIDARG; - ExitOnRootFailure(hr, "Invalid package current state: %d.", pPackage->currentState); + ExitWithRootFailure(hr, E_INVALIDARG, "Invalid package current state: %d.", pPackage->currentState); } // Calculate the rollback action if there is an execute action. @@ -232,6 +310,7 @@ extern "C" HRESULT ExeEnginePlanCalculatePackage( } break; + case BOOTSTRAPPER_PACKAGE_STATE_OBSOLETE: __fallthrough; case BOOTSTRAPPER_PACKAGE_STATE_ABSENT: switch (pPackage->requested) { @@ -251,8 +330,7 @@ extern "C" HRESULT ExeEnginePlanCalculatePackage( break; default: - hr = E_INVALIDARG; - ExitOnRootFailure(hr, "Invalid package expected state."); + ExitWithRootFailure(hr, E_INVALIDARG, "Invalid package expected state."); } } @@ -356,10 +434,41 @@ extern "C" HRESULT ExeEngineExecutePackage( LPWSTR sczUserArgs = NULL; LPWSTR sczUserArgsObfuscated = NULL; LPWSTR sczCommandObfuscated = NULL; + LPWSTR sczArpUninstallString = NULL; + LPWSTR sczArpArguments = NULL; + BOOTSTRAPPER_PACKAGE_STATE applyState = BOOTSTRAPPER_PACKAGE_STATE_UNKNOWN; HANDLE hExecutableFile = INVALID_HANDLE_VALUE; DWORD dwExitCode = 0; BURN_PACKAGE* pPackage = pExecuteAction->exePackage.pPackage; BURN_PAYLOAD* pPackagePayload = pPackage->payloads.rgItems[0].pPayload; + LPCWSTR wzUninstallArguments = pPackage->Exe.sczUninstallArguments; + + if (BURN_EXE_DETECTION_TYPE_ARP == pPackage->Exe.detectionType && + (BOOTSTRAPPER_ACTION_STATE_UNINSTALL == pExecuteAction->exePackage.action || + BOOTSTRAPPER_ACTION_STATE_INSTALL == pExecuteAction->exePackage.action && fRollback)) + { + hr = DetectArpEntry(pPackage, &applyState, &sczArpUninstallString); + ExitOnFailure(hr, "Failed to query ArpEntry for uninstall."); + + if (BOOTSTRAPPER_PACKAGE_STATE_ABSENT == applyState && BOOTSTRAPPER_ACTION_STATE_UNINSTALL == pExecuteAction->exePackage.action) + { + if (fRollback) + { + LogId(REPORT_STANDARD, MSG_ROLLBACK_PACKAGE_SKIPPED, pPackage->sczId, LoggingActionStateToString(pExecuteAction->exePackage.action), LoggingPackageStateToString(applyState)); + } + else + { + LogId(REPORT_STANDARD, MSG_ATTEMPTED_UNINSTALL_ABSENT_PACKAGE, pPackage->sczId); + } + + ExitFunction(); + } + else if (BOOTSTRAPPER_PACKAGE_STATE_ABSENT != applyState && BOOTSTRAPPER_ACTION_STATE_INSTALL == pExecuteAction->exePackage.action) + { + LogId(REPORT_STANDARD, MSG_ROLLBACK_PACKAGE_SKIPPED, pPackage->sczId, LoggingActionStateToString(pExecuteAction->exePackage.action), LoggingPackageStateToString(applyState)); + ExitFunction(); + } + } if (pPackage->Exe.fPseudoPackage && BURN_PAYLOAD_VERIFICATION_UPDATE_BUNDLE != pPackagePayload->verification) { @@ -372,7 +481,30 @@ extern "C" HRESULT ExeEngineExecutePackage( ExitOnFailure(hr, "Failed to build executable path."); hr = PathGetDirectory(sczExecutablePath, &sczCachedDirectory); - ExitOnFailure(hr, "Failed to get cached path for pseudo-package: %ls", pPackage->sczId); + ExitOnFailure(hr, "Failed to get parent directory for pseudo-package: %ls", pPackage->sczId); + } + else if (BURN_EXE_DETECTION_TYPE_ARP == pPackage->Exe.detectionType && BOOTSTRAPPER_ACTION_STATE_UNINSTALL == pExecuteAction->exePackage.action) + { + ExitOnNull(sczArpUninstallString, hr, E_INVALIDARG, "QuietUninstallString is null."); + + hr = ParseArpUninstallString(sczArpUninstallString, &sczExecutablePath, &sczArpArguments); + ExitOnFailure(hr, "Failed to parse QuietUninstallString: %ls.", sczArpUninstallString); + + if (pPackage->fPerMachine) + { + hr = ApprovedExesVerifySecureLocation(pCache, pVariables, sczExecutablePath); + ExitOnFailure(hr, "Failed to verify the QuietUninstallString executable path is in a secure location: %ls", sczExecutablePath); + if (S_FALSE == hr) + { + LogStringLine(REPORT_STANDARD, "The QuietUninstallString executable path is not in a secure location: %ls", sczExecutablePath); + ExitFunction1(hr = HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)); + } + } + + hr = PathGetDirectory(sczExecutablePath, &sczCachedDirectory); + ExitOnFailure(hr, "Failed to get parent directory for QuietUninstallString executable path: %ls", sczExecutablePath); + + wzUninstallArguments = sczArpArguments; } else { @@ -396,7 +528,7 @@ extern "C" HRESULT ExeEngineExecutePackage( break; case BOOTSTRAPPER_ACTION_STATE_UNINSTALL: - wzArguments = pPackage->Exe.sczUninstallArguments; + wzArguments = wzUninstallArguments; break; case BOOTSTRAPPER_ACTION_STATE_REPAIR: @@ -404,8 +536,7 @@ extern "C" HRESULT ExeEngineExecutePackage( break; default: - hr = E_INVALIDARG; - ExitOnFailure(hr, "Invalid Exe package action: %d.", pExecuteAction->exePackage.action); + ExitWithRootFailure(hr, E_INVALIDARG, "Invalid Exe package action: %d.", pExecuteAction->exePackage.action); } // now add optional arguments @@ -443,8 +574,7 @@ extern "C" HRESULT ExeEngineExecutePackage( break; default: - hr = E_INVALIDARG; - ExitOnFailure(hr, "Invalid Exe package action: %d.", pExecuteAction->exePackage.action); + ExitWithRootFailure(hr, E_INVALIDARG, "Invalid Exe package action: %d.", pExecuteAction->exePackage.action); } } } @@ -524,6 +654,8 @@ LExit: StrSecureZeroFreeString(sczUserArgs); ReleaseStr(sczUserArgsObfuscated); ReleaseStr(sczCommandObfuscated); + ReleaseStr(sczArpUninstallString); + ReleaseStr(sczArpArguments); ReleaseFileHandle(hExecutableFile); @@ -894,3 +1026,106 @@ extern "C" HRESULT ExeEngineHandleExitCode( //LExit: return hr; } + +static HRESULT DetectArpEntry( + __in const BURN_PACKAGE* pPackage, + __out BOOTSTRAPPER_PACKAGE_STATE* pPackageState, + __out_opt LPWSTR* psczQuietUninstallString + ) +{ + HRESULT hr = S_OK; + HKEY hKey = NULL; + VERUTIL_VERSION* pVersion = NULL; + int nCompareResult = 0; + HKEY hkRoot = pPackage->fPerMachine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + REG_KEY_BITNESS keyBitness = pPackage->Exe.fArpWin64 ? REG_KEY_64BIT : REG_KEY_32BIT; + + *pPackageState = BOOTSTRAPPER_PACKAGE_STATE_ABSENT; + if (psczQuietUninstallString) + { + ReleaseNullStr(*psczQuietUninstallString); + } + + hr = RegOpenEx(hkRoot, pPackage->Exe.sczArpKeyPath, KEY_READ, keyBitness, &hKey); + if (HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) == hr || HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr) + { + ExitFunction1(hr = S_OK); + } + ExitOnFailure(hr, "Failed to open registry key: %ls.", pPackage->Exe.sczArpKeyPath); + + hr = RegReadWixVersion(hKey, L"DisplayVersion", &pVersion); + if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr) + { + ExitFunction1(hr = S_OK); + } + ExitOnFailure(hr, "Failed to read DisplayVersion."); + + if (pVersion->fInvalid) + { + LogId(REPORT_WARNING, MSG_DETECTED_EXE_PACKAGE_INVALID_VERSION, pPackage->Exe.sczArpKeyPath, pVersion->sczVersion); + } + + hr = VerCompareParsedVersions(pPackage->Exe.pArpDisplayVersion, pVersion, &nCompareResult); + ExitOnFailure(hr, "Failed to compare versions."); + + if (nCompareResult < 0) + { + *pPackageState = BOOTSTRAPPER_PACKAGE_STATE_OBSOLETE; + } + else if (nCompareResult > 0) + { + *pPackageState = BOOTSTRAPPER_PACKAGE_STATE_ABSENT; + } + else + { + *pPackageState = BOOTSTRAPPER_PACKAGE_STATE_PRESENT; + } + + if (psczQuietUninstallString) + { + hr = RegReadString(hKey, L"QuietUninstallString", psczQuietUninstallString); + if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr) + { + hr = S_OK; + } + ExitOnFailure(hr, "Failed to read QuietUninstallString."); + } + +LExit: + ReleaseRegKey(hKey); + ReleaseVerutilVersion(pVersion); + + return hr; +} + +static HRESULT ParseArpUninstallString( + __in_z LPCWSTR wzArpUninstallString, + __inout LPWSTR* psczExecutablePath, + __inout LPWSTR* psczArguments + ) +{ + HRESULT hr = S_OK; + int argc = 0; + LPWSTR* argv = NULL; + + hr = AppParseCommandLine(wzArpUninstallString, &argc, &argv); + ExitOnFailure(hr, "Failed to parse uninstall string as command line: %ls.", wzArpUninstallString); + ExitOnNull(argc, hr, E_INVALIDARG, "Uninstall string must contain an executable path."); + + hr = StrAllocString(psczExecutablePath, argv[0], 0); + ExitOnFailure(hr, "Failed to copy executable path for ArpCommand."); + + for (int i = 1; i < argc; ++i) + { + hr = AppAppendCommandLineArgument(psczArguments, argv[i]); + ExitOnFailure(hr, "Failed to append argument for ArpCommand."); + } + +LExit: + if (argv) + { + AppFreeCommandLineArgs(argv); + } + + return hr; +} diff --git a/src/burn/engine/package.h b/src/burn/engine/package.h index 3ec77baf..85f34de5 100644 --- a/src/burn/engine/package.h +++ b/src/burn/engine/package.h @@ -16,6 +16,13 @@ typedef _BURN_PACKAGE BURN_PACKAGE; const DWORD BURN_PACKAGE_INVALID_PATCH_INDEX = 0x80000000; +enum BURN_EXE_DETECTION_TYPE +{ + BURN_EXE_DETECTION_TYPE_NONE, + BURN_EXE_DETECTION_TYPE_CONDITION, + BURN_EXE_DETECTION_TYPE_ARP, +}; + enum BURN_EXE_EXIT_CODE_TYPE { BURN_EXE_EXIT_CODE_TYPE_NONE, @@ -338,6 +345,12 @@ typedef struct _BURN_PACKAGE } Bundle; struct { + BURN_EXE_DETECTION_TYPE detectionType; + + BOOL fArpWin64; + LPWSTR sczArpKeyPath; + VERUTIL_VERSION* pArpDisplayVersion; + LPWSTR sczDetectCondition; LPWSTR sczInstallArguments; LPWSTR sczRepairArguments; diff --git a/src/burn/engine/plan.cpp b/src/burn/engine/plan.cpp index 52bf6298..419d3272 100644 --- a/src/burn/engine/plan.cpp +++ b/src/burn/engine/plan.cpp @@ -2798,7 +2798,8 @@ static BOOL NeedsCache( { BOOTSTRAPPER_ACTION_STATE action = fExecute ? pPackage->execute : pPackage->rollback; // TODO: bundles could theoretically use package cache - if (BURN_PACKAGE_TYPE_BUNDLE == pPackage->type || BURN_PACKAGE_TYPE_EXE == pPackage->type) // Bundle and Exe packages require the package for all operations (even uninstall). + if (BURN_PACKAGE_TYPE_BUNDLE == pPackage->type || // Bundle and Exe packages require the package for all operations (even uninstall). + BURN_PACKAGE_TYPE_EXE == pPackage->type && BURN_EXE_DETECTION_TYPE_ARP != pPackage->Exe.detectionType) { return BOOTSTRAPPER_ACTION_STATE_NONE != action; } diff --git a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj index e1a28712..f07418a7 100644 --- a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj +++ b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj @@ -81,6 +81,7 @@ + diff --git a/src/burn/test/BurnUnitTest/PlanTest.cpp b/src/burn/test/BurnUnitTest/PlanTest.cpp index be078c5c..2135d9f5 100644 --- a/src/burn/test/BurnUnitTest/PlanTest.cpp +++ b/src/burn/test/BurnUnitTest/PlanTest.cpp @@ -9,6 +9,7 @@ static HRESULT WINAPI PlanTestBAProc( __in_opt LPVOID pvContext ); +static LPCWSTR wzArpEntryExeManifestFileName = L"ExePackage_PerUserArpEntry_manifest.xml"; static LPCWSTR wzMsiTransactionManifestFileName = L"MsiTransaction_BundleAv1_manifest.xml"; static LPCWSTR wzMultipleBundlePackageManifestFileName = L"BundlePackage_Multiple_manifest.xml"; static LPCWSTR wzSingleExeManifestFileName = L"Failure_BundleD_manifest.xml"; @@ -47,6 +48,266 @@ namespace Bootstrapper { } + [Fact] + void ArpEntryExeInstallTest() + { + HRESULT hr = S_OK; + BURN_ENGINE_STATE engineState = { }; + BURN_ENGINE_STATE* pEngineState = &engineState; + BURN_PLAN* pPlan = &engineState.plan; + + InitializeEngineStateForCorePlan(wzArpEntryExeManifestFileName, pEngineState); + DetectAttachedContainerAsAttached(pEngineState); + DetectPermanentPackagesAsPresentAndCached(pEngineState); + + hr = CorePlan(pEngineState, BOOTSTRAPPER_ACTION_INSTALL); + NativeAssert::Succeeded(hr, "CorePlan failed"); + + Assert::Equal(BOOTSTRAPPER_ACTION_INSTALL, pPlan->action); + NativeAssert::StringEqual(L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", pPlan->wzBundleId); + NativeAssert::StringEqual(L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", pPlan->wzBundleProviderKey); + Assert::Equal(FALSE, pPlan->fEnabledForwardCompatibleBundle); + Assert::Equal(FALSE, pPlan->fPerMachine); + Assert::Equal(TRUE, pPlan->fCanAffectMachineState); + Assert::Equal(FALSE, pPlan->fDisableRollback); + Assert::Equal(FALSE, pPlan->fDisallowRemoval); + Assert::Equal(FALSE, pPlan->fDowngrade); + Assert::Equal(BURN_REGISTRATION_ACTION_OPERATIONS_CACHE_BUNDLE | BURN_REGISTRATION_ACTION_OPERATIONS_WRITE_PROVIDER_KEY, pPlan->dwRegistrationOperations); + + BOOL fRollback = FALSE; + DWORD dwIndex = 0; + ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, TRUE, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}"); + Assert::Equal(dwIndex, pPlan->cRegistrationActions); + + fRollback = TRUE; + dwIndex = 0; + ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, FALSE, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}"); + Assert::Equal(dwIndex, pPlan->cRollbackRegistrationActions); + + fRollback = FALSE; + dwIndex = 0; + ValidateCacheCheckpoint(pPlan, fRollback, dwIndex++, 1); + ValidateCachePackage(pPlan, fRollback, dwIndex++, L"TestExe"); + ValidateCacheSignalSyncpoint(pPlan, fRollback, dwIndex++); + Assert::Equal(dwIndex, pPlan->cCacheActions); + + fRollback = TRUE; + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRollbackCacheActions); + + Assert::Equal(119695ull, pPlan->qwCacheSizeTotal); + + fRollback = FALSE; + dwIndex = 0; + DWORD dwExecuteCheckpointId = 2; + ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteWaitCachePackage(pPlan, fRollback, dwIndex++, L"TestExe"); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteExePackage(pPlan, fRollback, dwIndex++, L"TestExe", BOOTSTRAPPER_ACTION_STATE_INSTALL); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++); + Assert::Equal(dwIndex, pPlan->cExecuteActions); + + fRollback = TRUE; + dwIndex = 0; + dwExecuteCheckpointId = 2; + ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteExePackage(pPlan, fRollback, dwIndex++, L"TestExe", BOOTSTRAPPER_ACTION_STATE_UNINSTALL); + 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(2ul, pPlan->cOverallProgressTicksTotal); + + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRestoreRelatedBundleActions); + + dwIndex = 0; + ValidateCleanAction(pPlan, dwIndex++, L"NetFx48Web"); + Assert::Equal(dwIndex, pPlan->cCleanActions); + + UINT uIndex = 0; + ValidatePlannedProvider(pPlan, uIndex++, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", NULL); + Assert::Equal(uIndex, pPlan->cPlannedProviders); + + Assert::Equal(2ul, pEngineState->packages.cPackages); + ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[1], L"TestExe", BURN_PACKAGE_REGISTRATION_STATE_PRESENT, BURN_PACKAGE_REGISTRATION_STATE_PRESENT); + } + + [Fact] + void ArpEntryExeInstallObsoleteTest() + { + HRESULT hr = S_OK; + BURN_ENGINE_STATE engineState = { }; + BURN_ENGINE_STATE* pEngineState = &engineState; + BURN_PLAN* pPlan = &engineState.plan; + + InitializeEngineStateForCorePlan(wzArpEntryExeManifestFileName, pEngineState); + DetectAttachedContainerAsAttached(pEngineState); + DetectPermanentPackagesAsPresentAndCached(pEngineState); + + pEngineState->packages.rgPackages[1].currentState = BOOTSTRAPPER_PACKAGE_STATE_OBSOLETE; + + hr = CorePlan(pEngineState, BOOTSTRAPPER_ACTION_INSTALL); + NativeAssert::Succeeded(hr, "CorePlan failed"); + + Assert::Equal(BOOTSTRAPPER_ACTION_INSTALL, pPlan->action); + NativeAssert::StringEqual(L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", pPlan->wzBundleId); + NativeAssert::StringEqual(L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", pPlan->wzBundleProviderKey); + Assert::Equal(FALSE, pPlan->fEnabledForwardCompatibleBundle); + Assert::Equal(FALSE, pPlan->fPerMachine); + Assert::Equal(TRUE, pPlan->fCanAffectMachineState); + Assert::Equal(FALSE, pPlan->fDisableRollback); + Assert::Equal(FALSE, pPlan->fDisallowRemoval); + Assert::Equal(FALSE, pPlan->fDowngrade); + Assert::Equal(BURN_REGISTRATION_ACTION_OPERATIONS_CACHE_BUNDLE | BURN_REGISTRATION_ACTION_OPERATIONS_WRITE_PROVIDER_KEY, pPlan->dwRegistrationOperations); + + BOOL fRollback = FALSE; + DWORD dwIndex = 0; + ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, TRUE, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}"); + Assert::Equal(dwIndex, pPlan->cRegistrationActions); + + fRollback = TRUE; + dwIndex = 0; + ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, FALSE, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}"); + Assert::Equal(dwIndex, pPlan->cRollbackRegistrationActions); + + fRollback = FALSE; + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cCacheActions); + + fRollback = TRUE; + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRollbackCacheActions); + + 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++); + ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++); + Assert::Equal(dwIndex, pPlan->cExecuteActions); + + fRollback = TRUE; + dwIndex = 0; + dwExecuteCheckpointId = 1; + ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE); + ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++); + ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++); + Assert::Equal(dwIndex, pPlan->cRollbackActions); + + Assert::Equal(0ul, pPlan->cExecutePackagesTotal); + Assert::Equal(0ul, pPlan->cOverallProgressTicksTotal); + + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRestoreRelatedBundleActions); + + dwIndex = 0; + ValidateCleanAction(pPlan, dwIndex++, L"NetFx48Web"); + Assert::Equal(dwIndex, pPlan->cCleanActions); + + UINT uIndex = 0; + ValidatePlannedProvider(pPlan, uIndex++, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", NULL); + Assert::Equal(uIndex, pPlan->cPlannedProviders); + + Assert::Equal(2ul, pEngineState->packages.cPackages); + ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[1], L"TestExe", BURN_PACKAGE_REGISTRATION_STATE_ABSENT, BURN_PACKAGE_REGISTRATION_STATE_ABSENT); + } + + [Fact] + void ArpEntryExeUninstallTest() + { + HRESULT hr = S_OK; + BURN_ENGINE_STATE engineState = { }; + BURN_ENGINE_STATE* pEngineState = &engineState; + BURN_PLAN* pPlan = &engineState.plan; + + InitializeEngineStateForCorePlan(wzArpEntryExeManifestFileName, pEngineState); + DetectPackagesAsPresentAndCached(pEngineState); + + hr = CorePlan(pEngineState, BOOTSTRAPPER_ACTION_UNINSTALL); + NativeAssert::Succeeded(hr, "CorePlan failed"); + + Assert::Equal(BOOTSTRAPPER_ACTION_UNINSTALL, pPlan->action); + NativeAssert::StringEqual(L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", pPlan->wzBundleId); + NativeAssert::StringEqual(L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", pPlan->wzBundleProviderKey); + Assert::Equal(FALSE, pPlan->fEnabledForwardCompatibleBundle); + Assert::Equal(FALSE, pPlan->fPerMachine); + Assert::Equal(TRUE, pPlan->fCanAffectMachineState); + Assert::Equal(FALSE, pPlan->fDisableRollback); + Assert::Equal(FALSE, pPlan->fDisallowRemoval); + Assert::Equal(FALSE, pPlan->fDowngrade); + Assert::Equal(BURN_REGISTRATION_ACTION_OPERATIONS_CACHE_BUNDLE | BURN_REGISTRATION_ACTION_OPERATIONS_WRITE_PROVIDER_KEY, pPlan->dwRegistrationOperations); + + BOOL fRollback = FALSE; + DWORD dwIndex = 0; + ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, FALSE, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}"); + Assert::Equal(dwIndex, pPlan->cRegistrationActions); + + fRollback = TRUE; + dwIndex = 0; + ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, TRUE, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}"); + Assert::Equal(dwIndex, pPlan->cRollbackRegistrationActions); + + fRollback = FALSE; + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cCacheActions); + + fRollback = TRUE; + dwIndex = 0; + Assert::Equal(dwIndex, pPlan->cRollbackCacheActions); + + 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++); + ValidateExecuteExePackage(pPlan, fRollback, dwIndex++, L"TestExe", BOOTSTRAPPER_ACTION_STATE_UNINSTALL); + 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); + ValidateExecuteExePackage(pPlan, fRollback, dwIndex++, L"TestExe", BOOTSTRAPPER_ACTION_STATE_INSTALL); + 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"TestExe"); + ValidateCleanAction(pPlan, dwIndex++, L"NetFx48Web"); + Assert::Equal(dwIndex, pPlan->cCleanActions); + + UINT uIndex = 0; + ValidatePlannedProvider(pPlan, uIndex++, L"{9C459DAD-0E64-40C8-8C9F-4F68E46AB223}", NULL); + Assert::Equal(uIndex, pPlan->cPlannedProviders); + + Assert::Equal(2ul, pEngineState->packages.cPackages); + ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[1], L"TestExe", BURN_PACKAGE_REGISTRATION_STATE_ABSENT, BURN_PACKAGE_REGISTRATION_STATE_ABSENT); + } + [Fact] void MsiTransactionInstallTest() { diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/BundlePackage_Multiple_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/BundlePackage_Multiple_manifest.xml index b02c2056..862156e8 100644 --- a/src/burn/test/BurnUnitTest/TestData/PlanTest/BundlePackage_Multiple_manifest.xml +++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/BundlePackage_Multiple_manifest.xml @@ -1 +1 @@ - + diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/ExePackage_PerUserArpEntry_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/ExePackage_PerUserArpEntry_manifest.xml new file mode 100644 index 00000000..59d6d50f --- /dev/null +++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/ExePackage_PerUserArpEntry_manifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml index 6afb0108..13223db8 100644 --- a/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml +++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_manifest.xml index 184bba38..5cfd8c98 100644 --- a/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_manifest.xml +++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_manifest.xml @@ -1 +1 @@ - + diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_modified_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_modified_manifest.xml index 0b35860b..175bfba1 100644 --- a/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_modified_manifest.xml +++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/Slipstream_BundleA_modified_manifest.xml @@ -1 +1 @@ - + diff --git a/src/test/burn/TestData/DependencyTests/BundleD/BundleD.wxs b/src/test/burn/TestData/DependencyTests/BundleD/BundleD.wxs index f371f674..22152da2 100644 --- a/src/test/burn/TestData/DependencyTests/BundleD/BundleD.wxs +++ b/src/test/burn/TestData/DependencyTests/BundleD/BundleD.wxs @@ -4,7 +4,7 @@ - + diff --git a/src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wixproj b/src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wixproj new file mode 100644 index 00000000..bcb447b0 --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wixproj @@ -0,0 +1,17 @@ + + + + Bundle + {A843CC02-6814-48F5-997D-052073BAE744} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wxs b/src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wxs new file mode 100644 index 00000000..709a7c3c --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/BrokenPerUserArpEntryExePackage/BrokenPerUserArpEntryExePackage.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/burn/TestData/ExePackageTests/PackageFail/PackageFail.wixproj b/src/test/burn/TestData/ExePackageTests/PackageFail/PackageFail.wixproj new file mode 100644 index 00000000..19c5463d --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PackageFail/PackageFail.wixproj @@ -0,0 +1,12 @@ + + + + {6C5A2C71-2009-4B52-BC20-C1C6A5C87EA6} + + + + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wixproj b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wixproj new file mode 100644 index 00000000..ed46e6a5 --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wixproj @@ -0,0 +1,19 @@ + + + + Bundle + x64 + TestBA_x64 + {5923A558-091E-4015-A9CC-BA92E7A2405A} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wxs b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wxs new file mode 100644 index 00000000..a966283f --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackage/PerMachineArpEntryExePackage.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wixproj b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wixproj new file mode 100644 index 00000000..e4219565 --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wixproj @@ -0,0 +1,18 @@ + + + + Bundle + {6436F747-08F7-416F-BE92-DB48F282AFC1} + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wxs b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wxs new file mode 100644 index 00000000..945146f0 --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PerMachineArpEntryExePackageFailure/PerMachineArpEntryExePackageFailure.wxs @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wixproj b/src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wixproj new file mode 100644 index 00000000..06395843 --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wixproj @@ -0,0 +1,17 @@ + + + + Bundle + {E4683E88-AF03-40D0-B308-5C084B0E1FA0} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wxs b/src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wxs new file mode 100644 index 00000000..e2792693 --- /dev/null +++ b/src/test/burn/TestData/ExePackageTests/PerUserArpEntryExePackage/PerUserArpEntryExePackage.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/burn/WixTestTools/ArpEntryInstaller.cs b/src/test/burn/WixTestTools/ArpEntryInstaller.cs new file mode 100644 index 00000000..96d9fab9 --- /dev/null +++ b/src/test/burn/WixTestTools/ArpEntryInstaller.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixTestTools +{ + using System; + using Xunit; + + public partial class ArpEntryInstaller : IDisposable + { + public ArpEntryInstaller(WixTestContext testContext, string id, bool perMachine = true, bool x64 = false) + { + this.ArpId = id; + this.PerMachine = perMachine; + this.X64 = x64; + this.TestContext = testContext; + } + + public string ArpId { get; } + + public bool PerMachine { get; } + + public bool X64 { get; } + + private WixTestContext TestContext { get; } + + public void Unregister(bool assertIfMissing = true) + { + if (this.TryGetRegistration(out var registration)) + { + registration.Delete(); + } + else + { + Assert.True(false, "Tried to unregister when not registered."); + } + } + + public void Dispose() + { + this.Unregister(false); + } + } +} diff --git a/src/test/burn/WixTestTools/ArpEntryVerifier.cs b/src/test/burn/WixTestTools/ArpEntryVerifier.cs new file mode 100644 index 00000000..b3c70337 --- /dev/null +++ b/src/test/burn/WixTestTools/ArpEntryVerifier.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixTestTools +{ + using Xunit; + + public partial class ArpEntryInstaller + { + public bool TryGetRegistration(out GenericArpRegistration registration) + { + bool success = !this.PerMachine ? GenericArpRegistration.TryGetPerUserRegistrationById(this.ArpId, out registration) + : GenericArpRegistration.TryGetPerMachineRegistrationById(this.ArpId, this.X64, out registration); + + return success; + } + + public void VerifyRegistered(bool registered) + { + bool success = this.TryGetRegistration(out _); + + Assert.Equal(registered, success); + } + } +} diff --git a/src/test/burn/WixTestTools/BundleRegistration.cs b/src/test/burn/WixTestTools/BundleRegistration.cs index 3541e7ea..524d4616 100644 --- a/src/test/burn/WixTestTools/BundleRegistration.cs +++ b/src/test/burn/WixTestTools/BundleRegistration.cs @@ -5,108 +5,51 @@ namespace WixTestTools using System; using Microsoft.Win32; - public class BundleRegistration + public class BundleRegistration : GenericArpRegistration { - public const string BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; - public const string BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY_WOW6432NODE = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH = "BundleCachePath"; public const string BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE = "BundleAddonCode"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH = "BundleCachePath"; public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE = "BundleDetectCode"; public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE = "BundlePatchCode"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY = "BundleProviderKey"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_RESUME_COMMAND_LINE = "BundleResumeCommandLine"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_TAG = "BundleTag"; public const string BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE = "BundleUpgradeCode"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME = "DisplayName"; public const string BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION = "BundleVersion"; public const string BURN_REGISTRATION_REGISTRY_ENGINE_VERSION = "EngineVersion"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY = "BundleProviderKey"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_TAG = "BundleTag"; - public const string REGISTRY_REBOOT_PENDING_FORMAT = "{0}.RebootRequired"; - public const string REGISTRY_BUNDLE_INSTALLED = "Installed"; - public const string REGISTRY_BUNDLE_DISPLAY_ICON = "DisplayIcon"; - public const string REGISTRY_BUNDLE_DISPLAY_VERSION = "DisplayVersion"; - public const string REGISTRY_BUNDLE_ESTIMATED_SIZE = "EstimatedSize"; - public const string REGISTRY_BUNDLE_PUBLISHER = "Publisher"; - public const string REGISTRY_BUNDLE_HELP_LINK = "HelpLink"; - public const string REGISTRY_BUNDLE_HELP_TELEPHONE = "HelpTelephone"; - public const string REGISTRY_BUNDLE_URL_INFO_ABOUT = "URLInfoAbout"; - public const string REGISTRY_BUNDLE_URL_UPDATE_INFO = "URLUpdateInfo"; - public const string REGISTRY_BUNDLE_PARENT_DISPLAY_NAME = "ParentDisplayName"; - public const string REGISTRY_BUNDLE_PARENT_KEY_NAME = "ParentKeyName"; - public const string REGISTRY_BUNDLE_COMMENTS = "Comments"; - public const string REGISTRY_BUNDLE_CONTACT = "Contact"; - public const string REGISTRY_BUNDLE_NO_MODIFY = "NoModify"; - public const string REGISTRY_BUNDLE_MODIFY_PATH = "ModifyPath"; - public const string REGISTRY_BUNDLE_NO_ELEVATE_ON_MODIFY = "NoElevateOnModify"; - public const string REGISTRY_BUNDLE_NO_REMOVE = "NoRemove"; - public const string REGISTRY_BUNDLE_SYSTEM_COMPONENT = "SystemComponent"; - public const string REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING = "QuietUninstallString"; - public const string REGISTRY_BUNDLE_UNINSTALL_STRING = "UninstallString"; - public const string REGISTRY_BUNDLE_RESUME_COMMAND_LINE = "BundleResumeCommandLine"; - public const string REGISTRY_BUNDLE_VERSION_MAJOR = "VersionMajor"; - public const string REGISTRY_BUNDLE_VERSION_MINOR = "VersionMinor"; public string[] AddonCodes { get; set; } - public string CachePath { get; set; } + public string BundleVersion { get; set; } - public string DisplayName { get; set; } + public string CachePath { get; set; } public string[] DetectCodes { get; set; } public string EngineVersion { get; set; } - public int? EstimatedSize { get; set; } - - public int? Installed { get; set; } - - public string ModifyPath { get; set; } - public string[] PatchCodes { get; set; } public string ProviderKey { get; set; } - public string Publisher { get; set; } - - public int? SystemComponent { get; set; } - - public string QuietUninstallString { get; set; } - - public string QuietUninstallCommand { get; set; } - - public string QuietUninstallCommandArguments { get; set; } - public string Tag { get; set; } - public string UninstallCommand { get; set; } - - public string UninstallCommandArguments { get; set; } - - public string UninstallString { get; set; } - public string[] UpgradeCodes { get; set; } - public string UrlInfoAbout { get; set; } - - public string UrlUpdateInfo { get; set; } - - public string Version { get; set; } + public static bool TryGetPerMachineBundleRegistrationById(string id, bool x64, out BundleRegistration registration) + { + return TryGetRegistrationById(id, x64, false, out registration); + } - public static bool TryGetPerMachineBundleRegistrationById(string bundleId, bool x64, out BundleRegistration registration) + public static bool TryGetPerUserBundleRegistrationById(string id, out BundleRegistration registration) { - var baseKeyPath = x64 ? BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY : BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY_WOW6432NODE; - var registrationKeyPath = $"{baseKeyPath}\\{bundleId}"; - using var registrationKey = Registry.LocalMachine.OpenSubKey(registrationKeyPath); - var success = registrationKey != null; - registration = success ? GetBundleRegistration(registrationKey) : null; - return success; + return TryGetRegistrationById(id, true, true, out registration); } - public static bool TryGetPerUserBundleRegistrationById(string bundleId, out BundleRegistration registration) + private static bool TryGetRegistrationById(string id, bool x64, bool perUser, out BundleRegistration registration) { - var registrationKeyPath = $"{BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY}\\{bundleId}"; - using var registrationKey = Registry.CurrentUser.OpenSubKey(registrationKeyPath); - var success = registrationKey != null; - registration = success ? GetBundleRegistration(registrationKey) : null; - return success; + registration = GetGenericArpRegistration(id, x64, perUser, key => GetBundleRegistration(key)); + return registration != null; } private static BundleRegistration GetBundleRegistration(RegistryKey idKey) @@ -120,38 +63,8 @@ namespace WixTestTools registration.ProviderKey = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY) as string; registration.Tag = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_TAG) as string; registration.UpgradeCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE) as string[]; - registration.Version = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION) as string; - registration.DisplayName = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME) as string; + registration.BundleVersion = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION) as string; registration.EngineVersion = idKey.GetValue(BURN_REGISTRATION_REGISTRY_ENGINE_VERSION) as string; - registration.EstimatedSize = idKey.GetValue(REGISTRY_BUNDLE_ESTIMATED_SIZE) as int?; - registration.Installed = idKey.GetValue(REGISTRY_BUNDLE_INSTALLED) as int?; - registration.ModifyPath = idKey.GetValue(REGISTRY_BUNDLE_MODIFY_PATH) as string; - registration.Publisher = idKey.GetValue(REGISTRY_BUNDLE_PUBLISHER) as string; - registration.SystemComponent = idKey.GetValue(REGISTRY_BUNDLE_SYSTEM_COMPONENT) as int?; - registration.UrlInfoAbout = idKey.GetValue(REGISTRY_BUNDLE_URL_INFO_ABOUT) as string; - registration.UrlUpdateInfo = idKey.GetValue(REGISTRY_BUNDLE_URL_UPDATE_INFO) as string; - - registration.QuietUninstallString = idKey.GetValue(REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING) as string; - if (!String.IsNullOrEmpty(registration.QuietUninstallString)) - { - var closeQuote = registration.QuietUninstallString.IndexOf("\"", 1); - if (closeQuote > 0) - { - registration.QuietUninstallCommand = registration.QuietUninstallString.Substring(1, closeQuote - 1).Trim(); - registration.QuietUninstallCommandArguments = registration.QuietUninstallString.Substring(closeQuote + 1).Trim(); - } - } - - registration.UninstallString = idKey.GetValue(REGISTRY_BUNDLE_UNINSTALL_STRING) as string; - if (!String.IsNullOrEmpty(registration.UninstallString)) - { - var closeQuote = registration.UninstallString.IndexOf("\"", 1); - if (closeQuote > 0) - { - registration.UninstallCommand = registration.UninstallString.Substring(1, closeQuote - 1).Trim(); - registration.UninstallCommandArguments = registration.UninstallString.Substring(closeQuote + 1).Trim(); - } - } return registration; } diff --git a/src/test/burn/WixTestTools/BundleVerifier.cs b/src/test/burn/WixTestTools/BundleVerifier.cs index 103171cd..ff45a291 100644 --- a/src/test/burn/WixTestTools/BundleVerifier.cs +++ b/src/test/burn/WixTestTools/BundleVerifier.cs @@ -80,6 +80,29 @@ namespace WixTestTools File.Delete(expectedCachePath); } + public bool TryGetArpEntryExePackageConfiguration(string packageId, out string arpId, out string arpVersion, out bool arpWin64, out bool perMachine) + { + using var wixOutput = WixOutput.Read(this.BundlePdb); + var intermediate = Intermediate.Load(wixOutput); + var section = intermediate.Sections.Single(); + var packageSymbol = section.Symbols.OfType().SingleOrDefault(p => p.Id.Id == packageId); + var exePackageSymbol = section.Symbols.OfType().SingleOrDefault(p => p.Id.Id == packageId); + if (packageSymbol == null || exePackageSymbol == null || exePackageSymbol.DetectionType != WixBundleExePackageDetectionType.Arp) + { + arpId = null; + arpVersion = null; + arpWin64 = false; + perMachine = false; + return false; + } + + arpId = exePackageSymbol.ArpId; + arpVersion = exePackageSymbol.ArpDisplayVersion; + arpWin64 = exePackageSymbol.ArpWin64; + perMachine = packageSymbol.PerMachine == true; + return true; + } + public bool TryGetRegistration(out BundleRegistration registration) { var bundleSymbol = this.GetBundleSymbol(); diff --git a/src/test/burn/WixTestTools/GenericArpRegistration.cs b/src/test/burn/WixTestTools/GenericArpRegistration.cs new file mode 100644 index 00000000..d87c4feb --- /dev/null +++ b/src/test/burn/WixTestTools/GenericArpRegistration.cs @@ -0,0 +1,143 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixTestTools +{ + using System; + using Microsoft.Win32; + + public class GenericArpRegistration + { + public const string UNINSTALL_KEY = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + public const string UNINSTALL_KEY_WOW6432NODE = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + + public const string REGISTRY_ARP_INSTALLED = "Installed"; + public const string REGISTRY_ARP_DISPLAY_ICON = "DisplayIcon"; + public const string REGISTRY_ARP_DISPLAY_NAME = "DisplayName"; + public const string REGISTRY_ARP_DISPLAY_VERSION = "DisplayVersion"; + public const string REGISTRY_ARP_ESTIMATED_SIZE = "EstimatedSize"; + public const string REGISTRY_ARP_PUBLISHER = "Publisher"; + public const string REGISTRY_ARP_HELP_LINK = "HelpLink"; + public const string REGISTRY_ARP_HELP_TELEPHONE = "HelpTelephone"; + public const string REGISTRY_ARP_URL_INFO_ABOUT = "URLInfoAbout"; + public const string REGISTRY_ARP_URL_UPDATE_INFO = "URLUpdateInfo"; + public const string REGISTRY_ARP_COMMENTS = "Comments"; + public const string REGISTRY_ARP_CONTACT = "Contact"; + public const string REGISTRY_ARP_NO_MODIFY = "NoModify"; + public const string REGISTRY_ARP_MODIFY_PATH = "ModifyPath"; + public const string REGISTRY_ARP_NO_ELEVATE_ON_MODIFY = "NoElevateOnModify"; + public const string REGISTRY_ARP_NO_REMOVE = "NoRemove"; + public const string REGISTRY_ARP_SYSTEM_COMPONENT = "SystemComponent"; + public const string REGISTRY_ARP_QUIET_UNINSTALL_STRING = "QuietUninstallString"; + public const string REGISTRY_ARP_UNINSTALL_STRING = "UninstallString"; + public const string REGISTRY_ARP_VERSION_MAJOR = "VersionMajor"; + public const string REGISTRY_ARP_VERSION_MINOR = "VersionMinor"; + + public RegistryKey BaseKey { get; set; } + + public string KeyPath { get; set; } + + public string DisplayName { get; set; } + + public string DisplayVersion { get; set; } + + public int? EstimatedSize { get; set; } + + public int? Installed { get; set; } + + public string ModifyPath { get; set; } + + public string Publisher { get; set; } + + public int? SystemComponent { get; set; } + + public string QuietUninstallString { get; set; } + + public string QuietUninstallCommand { get; set; } + + public string QuietUninstallCommandArguments { get; set; } + + public string UninstallCommand { get; set; } + + public string UninstallCommandArguments { get; set; } + + public string UninstallString { get; set; } + + public string UrlInfoAbout { get; set; } + + public string UrlUpdateInfo { get; set; } + + public static bool TryGetPerMachineRegistrationById(string id, bool x64, out GenericArpRegistration registration) + { + return TryGetRegistrationById(id, x64, false, out registration); + } + + public static bool TryGetPerUserRegistrationById(string id, out GenericArpRegistration registration) + { + return TryGetRegistrationById(id, true, true, out registration); + } + + private static bool TryGetRegistrationById(string id, bool x64, bool perUser, out GenericArpRegistration registration) + { + registration = GetGenericArpRegistration(id, x64, perUser, key => new GenericArpRegistration()); + return registration != null; + } + + protected static T GetGenericArpRegistration(string id, bool x64, bool perUser, Func fnCreate) + where T : GenericArpRegistration + { + var baseKey = perUser ? Registry.CurrentUser : Registry.LocalMachine; + var baseKeyPath = x64 ? UNINSTALL_KEY : UNINSTALL_KEY_WOW6432NODE; + var registrationKeyPath = $"{baseKeyPath}\\{id}"; + using var idKey = baseKey.OpenSubKey(registrationKeyPath); + + if (idKey == null) + { + return null; + } + + var registration = fnCreate(idKey); + + registration.BaseKey = baseKey; + registration.KeyPath = registrationKeyPath; + + registration.DisplayName = idKey.GetValue(REGISTRY_ARP_DISPLAY_NAME) as string; + registration.DisplayVersion = idKey.GetValue(REGISTRY_ARP_DISPLAY_VERSION) as string; + registration.EstimatedSize = idKey.GetValue(REGISTRY_ARP_ESTIMATED_SIZE) as int?; + registration.Installed = idKey.GetValue(REGISTRY_ARP_INSTALLED) as int?; + registration.ModifyPath = idKey.GetValue(REGISTRY_ARP_MODIFY_PATH) as string; + registration.Publisher = idKey.GetValue(REGISTRY_ARP_PUBLISHER) as string; + registration.SystemComponent = idKey.GetValue(REGISTRY_ARP_SYSTEM_COMPONENT) as int?; + registration.UrlInfoAbout = idKey.GetValue(REGISTRY_ARP_URL_INFO_ABOUT) as string; + registration.UrlUpdateInfo = idKey.GetValue(REGISTRY_ARP_URL_UPDATE_INFO) as string; + + registration.QuietUninstallString = idKey.GetValue(REGISTRY_ARP_QUIET_UNINSTALL_STRING) as string; + if (!String.IsNullOrEmpty(registration.QuietUninstallString)) + { + var closeQuote = registration.QuietUninstallString.IndexOf("\"", 1); + if (closeQuote > 0) + { + registration.QuietUninstallCommand = registration.QuietUninstallString.Substring(1, closeQuote - 1).Trim(); + registration.QuietUninstallCommandArguments = registration.QuietUninstallString.Substring(closeQuote + 1).Trim(); + } + } + + registration.UninstallString = idKey.GetValue(REGISTRY_ARP_UNINSTALL_STRING) as string; + if (!String.IsNullOrEmpty(registration.UninstallString)) + { + var closeQuote = registration.UninstallString.IndexOf("\"", 1); + if (closeQuote > 0) + { + registration.UninstallCommand = registration.UninstallString.Substring(1, closeQuote - 1).Trim(); + registration.UninstallCommandArguments = registration.UninstallString.Substring(closeQuote + 1).Trim(); + } + } + + return registration; + } + + public void Delete() + { + this.BaseKey.DeleteSubKeyTree(this.KeyPath); + } + } +} diff --git a/src/test/burn/WixToolsetTest.BurnE2E/BurnE2ETests.cs b/src/test/burn/WixToolsetTest.BurnE2E/BurnE2ETests.cs index 6664b849..5caedacb 100644 --- a/src/test/burn/WixToolsetTest.BurnE2E/BurnE2ETests.cs +++ b/src/test/burn/WixToolsetTest.BurnE2E/BurnE2ETests.cs @@ -31,6 +31,23 @@ namespace WixToolsetTest.BurnE2E this.Installers.Push(installer); } + protected ArpEntryInstaller CreateArpEntryInstaller(string id, bool perMachine = true, bool x64 = false) + { + var installer = new ArpEntryInstaller(this.TestContext, id, perMachine, x64); + this.Installers.Push(installer); + return installer; + } + + protected ArpEntryInstaller CreateArpEntryInstaller(BundleInstaller bundleInstaller, string packageId) + { + if (!bundleInstaller.TryGetArpEntryExePackageConfiguration(packageId, out var arpId, out _, out var arpWin64, out var perMachine)) + { + return null; + } + + return this.CreateArpEntryInstaller(arpId, perMachine, arpWin64); + } + protected BundleInstaller CreateBundleInstaller(string name) { var installer = new BundleInstaller(this.TestContext, name); diff --git a/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs index f5a1cda8..e5e3259f 100644 --- a/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs +++ b/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs @@ -33,12 +33,12 @@ namespace WixToolsetTest.BurnE2E if (!File.Exists(targetFilePath)) { - var testTool = new TestTool(Path.Combine(TestData.Get(), "win-x86", "TestExe.exe")) + var testExeTool = new TestExeTool { Arguments = "/lf \"" + targetFilePath + $"|{FiveGB}\"", ExpectedExitCode = 0, }; - testTool.Run(true); + testExeTool.Run(true); } } diff --git a/src/test/burn/WixToolsetTest.BurnE2E/ExePackageTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/ExePackageTests.cs new file mode 100644 index 00000000..dc6515b5 --- /dev/null +++ b/src/test/burn/WixToolsetTest.BurnE2E/ExePackageTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.BurnE2E +{ + using WixTestTools; + using Xunit; + using Xunit.Abstractions; + + public class ExePackageTests : BurnE2ETests + { + public ExePackageTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + [RuntimeFact] + public void CanInstallAndUninstallPerMachineArpEntryExePackage() + { + const string arpId = "{4D9EC36A-1E63-4244-875C-3ECB0A2CAE30}"; + var perMachineArpEntryExePackageBundle = this.CreateBundleInstaller(@"PerMachineArpEntryExePackage"); + var arpEntryExePackage = this.CreateArpEntryInstaller(perMachineArpEntryExePackageBundle, "TestExe"); + + arpEntryExePackage.VerifyRegistered(false); + + var installLogPath = perMachineArpEntryExePackageBundle.Install(); + perMachineArpEntryExePackageBundle.VerifyRegisteredAndInPackageCache(); + arpEntryExePackage.VerifyRegistered(true); + + LogVerifier.MessageInLogFile(installLogPath, $"TestExe.exe\" /regw \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},DisplayVersion,String,1.0.0.0\" /regw \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},QuietUninstallString,String,\\\""); + + var uninstallLogPath = perMachineArpEntryExePackageBundle.Uninstall(); + perMachineArpEntryExePackageBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + arpEntryExePackage.VerifyRegistered(false); + + LogVerifier.MessageInLogFile(uninstallLogPath, $"testexe.exe\" /regd HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId}"); + } + + [RuntimeFact] + public void CanUninstallPerMachineArpEntryExePackageOnRollback() + { + const string arpId = "{80E90929-EEA5-48A7-A680-A0237A1CAD84}"; + var perMachineArpEntryExePackageFailureBundle = this.CreateBundleInstaller(@"PerMachineArpEntryExePackageFailure"); + var arpEntryExePackage = this.CreateArpEntryInstaller(perMachineArpEntryExePackageFailureBundle, "TestExe"); + + arpEntryExePackage.VerifyRegistered(false); + + var installLogPath = perMachineArpEntryExePackageFailureBundle.Install((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE); + perMachineArpEntryExePackageFailureBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + arpEntryExePackage.VerifyRegistered(false); + + LogVerifier.MessageInLogFile(installLogPath, $"TestExe.exe\" /regw \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},DisplayVersion,String,1.0.0.0\" /regw \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},QuietUninstallString,String,\\\""); + LogVerifier.MessageInLogFile(installLogPath, $"testexe.exe\" /regd HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId}"); + } + + [RuntimeFact] + public void CanInstallAndUninstallPerUserArpEntryExePackage() + { + const string arpId = "{9B5300C7-9B34-4670-9614-185B02AB87EF}"; + var perUserArpEntryExePackageBundle = this.CreateBundleInstaller(@"PerUserArpEntryExePackage"); + var arpEntryExePackage = this.CreateArpEntryInstaller(perUserArpEntryExePackageBundle, "TestExe"); + + arpEntryExePackage.VerifyRegistered(false); + + var installLogPath = perUserArpEntryExePackageBundle.Install(); + perUserArpEntryExePackageBundle.VerifyRegisteredAndInPackageCache(); + arpEntryExePackage.VerifyRegistered(true); + + LogVerifier.MessageInLogFile(installLogPath, $"TestExe.exe\" /regw \"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},DisplayVersion,String,1.0.0.0\" /regw \"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},QuietUninstallString,String,\\\""); + + var uninstallLogPath = perUserArpEntryExePackageBundle.Uninstall(); + perUserArpEntryExePackageBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + arpEntryExePackage.VerifyRegistered(false); + + LogVerifier.MessageInLogFile(uninstallLogPath, $"testexe.exe\" /regd HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId}"); + } + + [RuntimeFact] + public void FailsUninstallWhenPerUserArpEntryExePackageMissingQuietUninstallString() + { + const string arpId = "{DE9F8594-5856-4454-AB10-3C01ED246D7D}"; + var brokenPerUserArpEntryExePackageBundle = this.CreateBundleInstaller(@"BrokenPerUserArpEntryExePackage"); + var arpEntryExePackage = this.CreateArpEntryInstaller(brokenPerUserArpEntryExePackageBundle, "TestExe"); + + arpEntryExePackage.VerifyRegistered(false); + brokenPerUserArpEntryExePackageBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + + var installLogPath = brokenPerUserArpEntryExePackageBundle.Install(); + brokenPerUserArpEntryExePackageBundle.VerifyRegisteredAndInPackageCache(); + arpEntryExePackage.VerifyRegistered(true); + + LogVerifier.MessageInLogFile(installLogPath, $"TestExe.exe\" /regw \"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{arpId},DisplayVersion,String,1.0.0.0\""); + + brokenPerUserArpEntryExePackageBundle.Uninstall((int)MSIExec.MSIExecReturnCode.ERROR_INVALID_PARAMETER); + brokenPerUserArpEntryExePackageBundle.VerifyRegisteredAndInPackageCache(); + + arpEntryExePackage.Unregister(); + + brokenPerUserArpEntryExePackageBundle.Uninstall(); + brokenPerUserArpEntryExePackageBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + } + } +} diff --git a/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs b/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs deleted file mode 100644 index a4d46d48..00000000 --- a/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolsetTest.BurnE2E -{ - using System; - using System.Collections.Generic; - - public interface IWebServer : IDisposable - { - bool DisableHeadResponses { get; set; } - bool DisableRangeRequests { get; set; } - - /// - /// Registers a collection of relative URLs (the key) with its absolute path to the file (the value). - /// - void AddFiles(Dictionary physicalPathsByRelativeUrl); - - /// - /// Starts the web server on a new thread. - /// - void Start(); - } -} \ No newline at end of file diff --git a/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs b/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs deleted file mode 100644 index 8e6611a2..00000000 --- a/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolsetTest.BurnE2E -{ - using System; - using Microsoft.Win32; - using WixTestTools; - using WixToolset.Mba.Core; - - public class TestBAController : IDisposable - { - public TestBAController(WixTestContext testContext, bool x64 = false) - { - this.TestGroupName = testContext.TestGroupName; - this.BaseRegKeyPath = x64 ? @"Software\WiX\Tests" : @"Software\WOW6432Node\WiX\Tests"; - this.TestBaseRegKeyPath = String.Format(@"{0}\TestBAControl\{1}", this.BaseRegKeyPath, this.TestGroupName); - } - - private string BaseRegKeyPath { get; } - - private string TestBaseRegKeyPath { get; } - - public string TestGroupName { get; } - - /// - /// Sets a test value in the registry to communicate with the TestBA. - /// - /// Name of the value to set. - /// Value to set. If this is null, the value is removed. - public void SetBurnTestValue(string name, string value) - { - using (var testKey = Registry.LocalMachine.CreateSubKey(this.TestBaseRegKeyPath)) - { - if (String.IsNullOrEmpty(value)) - { - testKey.DeleteValue(name, false); - } - else - { - testKey.SetValue(name, value); - } - } - } - - public void SetExplicitlyElevateAndPlanFromOnElevateBegin(string value = "true") - { - this.SetBurnTestValue("ExplicitlyElevateAndPlanFromOnElevateBegin", value); - } - - public void SetForceKeepRegistration(string value = "true") - { - this.SetBurnTestValue("ForceKeepRegistration", value); - } - - public void SetImmediatelyQuit(string value = "true") - { - this.SetBurnTestValue("ImmediatelyQuit", value); - } - - public void SetQuitAfterDetect(string value = "true") - { - this.SetBurnTestValue("QuitAfterDetect", value); - } - - /// - /// Slows the cache progress of a package. - /// - /// Package identity. - /// Sets or removes the delay on a package being cached. - public void SetPackageSlowCache(string packageId, int? delay) - { - this.SetPackageState(packageId, "SlowCache", delay.HasValue ? delay.ToString() : null); - } - - /// - /// Cancels the cache of a package at a particular progress point. - /// - /// Package identity. - /// Sets or removes the cancel progress on a package being cached. - public void SetPackageCancelCacheAtProgress(string packageId, int? cancelPoint) - { - this.SetPackageState(packageId, "CancelCacheAtProgress", cancelPoint.HasValue ? cancelPoint.ToString() : null); - } - - /// - /// Slows the execute progress of a package. - /// - /// Package identity. - /// Sets or removes the delay on a package being executed. - public void SetPackageSlowExecute(string packageId, int? delay) - { - this.SetPackageState(packageId, "SlowExecute", delay.HasValue ? delay.ToString() : null); - } - - /// - /// Cancels the execute of a package at a particular progress point. - /// - /// Package identity. - /// Sets or removes the cancel progress on a package being executed. - public void SetPackageCancelExecuteAtProgress(string packageId, int? cancelPoint) - { - this.SetPackageState(packageId, "CancelExecuteAtProgress", cancelPoint.HasValue ? cancelPoint.ToString() : null); - } - - /// - /// Cancels the execute of a package at the next progess after the specified MSI action start. - /// - /// Package identity. - /// Sets or removes the cancel progress on a package being executed. - public void SetPackageCancelExecuteAtActionStart(string packageId, string actionName) - { - this.SetPackageState(packageId, "CancelExecuteAtActionStart", actionName); - } - - /// - /// Cancels the execute of a package at a particular OnProgress point. - /// - /// Package identity. - /// Sets or removes the cancel OnProgress point on a package being executed. - public void SetPackageCancelOnProgressAtProgress(string packageId, int? cancelPoint) - { - this.SetPackageState(packageId, "CancelOnProgressAtProgress", cancelPoint.HasValue ? cancelPoint.ToString() : null); - } - - /// - /// Retries the files in use one or more times before canceling. - /// - /// Package identity. - /// Sets or removes the retry count on a package's file in use message. - public void SetPackageRetryExecuteFilesInUse(string packageId, int? retryCount) - { - this.SetPackageState(packageId, "RetryExecuteFilesInUse", retryCount.HasValue ? retryCount.ToString() : null); - } - - /// - /// Sets the requested state for a package that the TestBA will return to the engine during plan. - /// - /// Package identity. - /// State to request. - public void SetPackageRequestedState(string packageId, RequestState state) - { - this.SetPackageState(packageId, "Requested", state.ToString()); - } - - /// - /// Sets the requested state for a package that the TestBA will return to the engine during plan. - /// - /// Package identity. - /// State to request. - public void SetPackageFeatureState(string packageId, string featureId, FeatureState state) - { - this.SetPackageState(packageId, String.Concat(featureId, "Requested"), state.ToString()); - } - - /// - /// Requests the BA to log the test registry value for the specified package. - /// - /// - /// - public void SetPackageRecordTestRegistryValue(string packageId, string value = "true") - { - this.SetPackageState(packageId, "RecordTestRegistryValue", value); - } - - public void SetPackageProcessCancelAction(string packageId, BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action) - { - this.SetPackageState(packageId, "ProcessCancelAction", action.ToString()); - } - - /// - /// Sets the number of times to re-run the Detect phase. - /// - /// Number of times to run Detect (after the first, normal, Detect). - public void SetRedetectCount(int redetectCount) - { - this.SetPackageState(null, "RedetectCount", redetectCount.ToString()); - } - - /// - /// Resets the state for a package that the TestBA will return to the engine during plan. - /// - /// Package identity. - public void ResetPackageStates(string packageId) - { - var key = String.Format(@"{0}\{1}", this.TestBaseRegKeyPath, packageId ?? String.Empty); - Registry.LocalMachine.DeleteSubKey(key); - } - - public void SetVerifyArguments(string verifyArguments) - { - this.SetBurnTestValue("VerifyArguments", verifyArguments); - - } - - private void SetPackageState(string packageId, string name, string value) - { - var key = String.Format(@"{0}\{1}", this.TestBaseRegKeyPath, packageId ?? String.Empty); - using (var packageKey = Registry.LocalMachine.CreateSubKey(key)) - { - if (String.IsNullOrEmpty(value)) - { - packageKey.DeleteValue(name, false); - } - else - { - packageKey.SetValue(name, value); - } - } - } - - public void Dispose() - { - Registry.LocalMachine.DeleteSubKeyTree($@"{this.BaseRegKeyPath}\{this.TestGroupName}", false); - Registry.LocalMachine.DeleteSubKeyTree($@"{this.BaseRegKeyPath}\TestBAControl", false); - } - } -} diff --git a/src/test/burn/WixToolsetTest.BurnE2E/Utilities/IWebServer.cs b/src/test/burn/WixToolsetTest.BurnE2E/Utilities/IWebServer.cs new file mode 100644 index 00000000..a4d46d48 --- /dev/null +++ b/src/test/burn/WixToolsetTest.BurnE2E/Utilities/IWebServer.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.BurnE2E +{ + using System; + using System.Collections.Generic; + + public interface IWebServer : IDisposable + { + bool DisableHeadResponses { get; set; } + bool DisableRangeRequests { get; set; } + + /// + /// Registers a collection of relative URLs (the key) with its absolute path to the file (the value). + /// + void AddFiles(Dictionary physicalPathsByRelativeUrl); + + /// + /// Starts the web server on a new thread. + /// + void Start(); + } +} \ No newline at end of file diff --git a/src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestBAController.cs b/src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestBAController.cs new file mode 100644 index 00000000..8e6611a2 --- /dev/null +++ b/src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestBAController.cs @@ -0,0 +1,217 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.BurnE2E +{ + using System; + using Microsoft.Win32; + using WixTestTools; + using WixToolset.Mba.Core; + + public class TestBAController : IDisposable + { + public TestBAController(WixTestContext testContext, bool x64 = false) + { + this.TestGroupName = testContext.TestGroupName; + this.BaseRegKeyPath = x64 ? @"Software\WiX\Tests" : @"Software\WOW6432Node\WiX\Tests"; + this.TestBaseRegKeyPath = String.Format(@"{0}\TestBAControl\{1}", this.BaseRegKeyPath, this.TestGroupName); + } + + private string BaseRegKeyPath { get; } + + private string TestBaseRegKeyPath { get; } + + public string TestGroupName { get; } + + /// + /// Sets a test value in the registry to communicate with the TestBA. + /// + /// Name of the value to set. + /// Value to set. If this is null, the value is removed. + public void SetBurnTestValue(string name, string value) + { + using (var testKey = Registry.LocalMachine.CreateSubKey(this.TestBaseRegKeyPath)) + { + if (String.IsNullOrEmpty(value)) + { + testKey.DeleteValue(name, false); + } + else + { + testKey.SetValue(name, value); + } + } + } + + public void SetExplicitlyElevateAndPlanFromOnElevateBegin(string value = "true") + { + this.SetBurnTestValue("ExplicitlyElevateAndPlanFromOnElevateBegin", value); + } + + public void SetForceKeepRegistration(string value = "true") + { + this.SetBurnTestValue("ForceKeepRegistration", value); + } + + public void SetImmediatelyQuit(string value = "true") + { + this.SetBurnTestValue("ImmediatelyQuit", value); + } + + public void SetQuitAfterDetect(string value = "true") + { + this.SetBurnTestValue("QuitAfterDetect", value); + } + + /// + /// Slows the cache progress of a package. + /// + /// Package identity. + /// Sets or removes the delay on a package being cached. + public void SetPackageSlowCache(string packageId, int? delay) + { + this.SetPackageState(packageId, "SlowCache", delay.HasValue ? delay.ToString() : null); + } + + /// + /// Cancels the cache of a package at a particular progress point. + /// + /// Package identity. + /// Sets or removes the cancel progress on a package being cached. + public void SetPackageCancelCacheAtProgress(string packageId, int? cancelPoint) + { + this.SetPackageState(packageId, "CancelCacheAtProgress", cancelPoint.HasValue ? cancelPoint.ToString() : null); + } + + /// + /// Slows the execute progress of a package. + /// + /// Package identity. + /// Sets or removes the delay on a package being executed. + public void SetPackageSlowExecute(string packageId, int? delay) + { + this.SetPackageState(packageId, "SlowExecute", delay.HasValue ? delay.ToString() : null); + } + + /// + /// Cancels the execute of a package at a particular progress point. + /// + /// Package identity. + /// Sets or removes the cancel progress on a package being executed. + public void SetPackageCancelExecuteAtProgress(string packageId, int? cancelPoint) + { + this.SetPackageState(packageId, "CancelExecuteAtProgress", cancelPoint.HasValue ? cancelPoint.ToString() : null); + } + + /// + /// Cancels the execute of a package at the next progess after the specified MSI action start. + /// + /// Package identity. + /// Sets or removes the cancel progress on a package being executed. + public void SetPackageCancelExecuteAtActionStart(string packageId, string actionName) + { + this.SetPackageState(packageId, "CancelExecuteAtActionStart", actionName); + } + + /// + /// Cancels the execute of a package at a particular OnProgress point. + /// + /// Package identity. + /// Sets or removes the cancel OnProgress point on a package being executed. + public void SetPackageCancelOnProgressAtProgress(string packageId, int? cancelPoint) + { + this.SetPackageState(packageId, "CancelOnProgressAtProgress", cancelPoint.HasValue ? cancelPoint.ToString() : null); + } + + /// + /// Retries the files in use one or more times before canceling. + /// + /// Package identity. + /// Sets or removes the retry count on a package's file in use message. + public void SetPackageRetryExecuteFilesInUse(string packageId, int? retryCount) + { + this.SetPackageState(packageId, "RetryExecuteFilesInUse", retryCount.HasValue ? retryCount.ToString() : null); + } + + /// + /// Sets the requested state for a package that the TestBA will return to the engine during plan. + /// + /// Package identity. + /// State to request. + public void SetPackageRequestedState(string packageId, RequestState state) + { + this.SetPackageState(packageId, "Requested", state.ToString()); + } + + /// + /// Sets the requested state for a package that the TestBA will return to the engine during plan. + /// + /// Package identity. + /// State to request. + public void SetPackageFeatureState(string packageId, string featureId, FeatureState state) + { + this.SetPackageState(packageId, String.Concat(featureId, "Requested"), state.ToString()); + } + + /// + /// Requests the BA to log the test registry value for the specified package. + /// + /// + /// + public void SetPackageRecordTestRegistryValue(string packageId, string value = "true") + { + this.SetPackageState(packageId, "RecordTestRegistryValue", value); + } + + public void SetPackageProcessCancelAction(string packageId, BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action) + { + this.SetPackageState(packageId, "ProcessCancelAction", action.ToString()); + } + + /// + /// Sets the number of times to re-run the Detect phase. + /// + /// Number of times to run Detect (after the first, normal, Detect). + public void SetRedetectCount(int redetectCount) + { + this.SetPackageState(null, "RedetectCount", redetectCount.ToString()); + } + + /// + /// Resets the state for a package that the TestBA will return to the engine during plan. + /// + /// Package identity. + public void ResetPackageStates(string packageId) + { + var key = String.Format(@"{0}\{1}", this.TestBaseRegKeyPath, packageId ?? String.Empty); + Registry.LocalMachine.DeleteSubKey(key); + } + + public void SetVerifyArguments(string verifyArguments) + { + this.SetBurnTestValue("VerifyArguments", verifyArguments); + + } + + private void SetPackageState(string packageId, string name, string value) + { + var key = String.Format(@"{0}\{1}", this.TestBaseRegKeyPath, packageId ?? String.Empty); + using (var packageKey = Registry.LocalMachine.CreateSubKey(key)) + { + if (String.IsNullOrEmpty(value)) + { + packageKey.DeleteValue(name, false); + } + else + { + packageKey.SetValue(name, value); + } + } + } + + public void Dispose() + { + Registry.LocalMachine.DeleteSubKeyTree($@"{this.BaseRegKeyPath}\{this.TestGroupName}", false); + Registry.LocalMachine.DeleteSubKeyTree($@"{this.BaseRegKeyPath}\TestBAControl", false); + } + } +} diff --git a/src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestExeTool.cs b/src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestExeTool.cs new file mode 100644 index 00000000..a02299d7 --- /dev/null +++ b/src/test/burn/WixToolsetTest.BurnE2E/Utilities/TestExeTool.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixTestTools +{ + using System.IO; + using WixBuildTools.TestSupport; + + public class TestExeTool : TestTool + { + private static readonly string TestExePath32 = Path.Combine(TestData.Get(), "win-x86", "TestExe.exe"); + + public TestExeTool() + : base(TestExePath32) + { + } + } +} diff --git a/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs b/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs index 78e02534..25a155d6 100644 --- a/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs +++ b/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs @@ -411,13 +411,37 @@ namespace WixToolset.Core.Burn.Bundles } else if (package.SpecificPackageSymbol is WixBundleExePackageSymbol exePackage) // EXE { - writer.WriteAttributeString("DetectCondition", exePackage.DetectCondition); writer.WriteAttributeString("InstallArguments", exePackage.InstallCommand); - writer.WriteAttributeString("UninstallArguments", exePackage.UninstallCommand); - writer.WriteAttributeString("Uninstallable", exePackage.Uninstallable ? "yes" : "no"); writer.WriteAttributeString("RepairArguments", exePackage.RepairCommand); writer.WriteAttributeString("Repairable", exePackage.Repairable ? "yes" : "no"); + switch (exePackage.DetectionType) + { + case WixBundleExePackageDetectionType.Condition: + writer.WriteAttributeString("DetectionType", "condition"); + writer.WriteAttributeString("DetectCondition", exePackage.DetectCondition); + + if (exePackage.Uninstallable) + { + writer.WriteAttributeString("UninstallArguments", exePackage.UninstallCommand); + writer.WriteAttributeString("Uninstallable", "yes"); + } + break; + case WixBundleExePackageDetectionType.Arp: + writer.WriteAttributeString("DetectionType", "arp"); + writer.WriteAttributeString("ArpId", exePackage.ArpId); + writer.WriteAttributeString("ArpDisplayVersion", exePackage.ArpDisplayVersion); + + if (exePackage.ArpWin64) + { + writer.WriteAttributeString("ArpWin64", "yes"); + } + break; + case WixBundleExePackageDetectionType.None: + writer.WriteAttributeString("DetectionType", "none"); + break; + } + if (!String.IsNullOrEmpty(exePackage.ExeProtocol)) { writer.WriteAttributeString("Protocol", exePackage.ExeProtocol); diff --git a/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs b/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs index 5d9acb4a..cc3c22db 100644 --- a/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs +++ b/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs @@ -109,7 +109,17 @@ namespace WixToolset.Core.Burn.Bundles if (!packageSymbol.Permanent) { - this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "ExePackage", "DetectCondition", symbol.DetectCondition, BundleConditionPhase.Detect); + if (symbol.DetectionType == WixBundleExePackageDetectionType.Condition) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "ExePackage", "DetectCondition", symbol.DetectCondition, BundleConditionPhase.Detect); + } + else if (symbol.DetectionType == WixBundleExePackageDetectionType.Arp) + { + if (!this.BackendHelper.IsValidWixVersion(symbol.ArpDisplayVersion)) + { + this.Messaging.Write(WarningMessages.InvalidWixVersion(symbol.SourceLineNumbers, symbol.ArpDisplayVersion, "ArpEntry", "Version")); + } + } } } diff --git a/src/wix/WixToolset.Core/Compiler_Bundle.cs b/src/wix/WixToolset.Core/Compiler_Bundle.cs index 36194882..b8f449d6 100644 --- a/src/wix/WixToolset.Core/Compiler_Bundle.cs +++ b/src/wix/WixToolset.Core/Compiler_Bundle.cs @@ -2026,6 +2026,10 @@ namespace WixToolset.Core var bundle = YesNoType.NotSet; var slipstream = YesNoType.NotSet; var hasPayloadInfo = false; + WixBundleExePackageDetectionType? exeDetectionType = WixBundleExePackageDetectionType.None; + string arpId = null; + string arpDisplayVersion = null; + var arpWin64 = YesNoType.NotSet; var expectedNetFx4Args = new string[] { "/q", "/norestart" }; @@ -2204,6 +2208,112 @@ namespace WixToolset.Core compilerPayload.FinishCompilingPackage(); var id = compilerPayload.Id; + // Now that the package ID is known, we can parse the extension attributes... + var contextValues = new Dictionary() { { "PackageId", id.Id } }; + foreach (var attribute in extensionAttributes) + { + this.Core.ParseExtensionAttribute(node, attribute, contextValues); + } + + if (packageType == WixBundlePackageType.Exe && (detectCondition != null || uninstallArguments != null)) + { + exeDetectionType = WixBundleExePackageDetectionType.Condition; + } + + foreach (var child in node.Elements()) + { + if (CompilerCore.WixNamespace == child.Name.Namespace) + { + var allowed = true; + switch (child.Name.LocalName) + { + case "ArpEntry": + allowed = packageType == WixBundlePackageType.Exe; + if (allowed) + { + if (exeDetectionType == WixBundleExePackageDetectionType.Arp) + { + this.Core.Write(ErrorMessages.TooManyChildren(Preprocessor.GetSourceLineNumbers(child), node.Name.LocalName, child.Name.LocalName)); + } + else if (!exeDetectionType.HasValue || exeDetectionType.Value == WixBundleExePackageDetectionType.Condition) + { + exeDetectionType = null; + } + else + { + if (exeDetectionType.Value != WixBundleExePackageDetectionType.None) + { + throw new WixException($"Unexpected WixBundleExePackageDetectionType: {exeDetectionType}"); + } + + exeDetectionType = WixBundleExePackageDetectionType.Arp; + } + + this.ParseExePackageArpEntryElement(child, out arpId, out arpDisplayVersion, out arpWin64); + } + break; + case "SlipstreamMsp": + allowed = (packageType == WixBundlePackageType.Msi); + if (allowed) + { + this.ParseSlipstreamMspElement(child, id.Id); + } + break; + case "MsiProperty": + allowed = (packageType == WixBundlePackageType.Msi || packageType == WixBundlePackageType.Msp); + if (allowed) + { + this.ParseMsiPropertyElement(child, id.Id); + } + break; + case "Payload": + this.ParsePayloadElement(child, ComplexReferenceParentType.Package, id); + break; + case "PayloadGroupRef": + this.ParsePayloadGroupRefElement(child, ComplexReferenceParentType.Package, id); + break; + case "Provides": + this.ParseProvidesElement(child, packageType, id.Id, out _); + break; + case "ExitCode": + allowed = (packageType == WixBundlePackageType.Bundle || packageType == WixBundlePackageType.Exe); + if (allowed) + { + this.ParseExitCodeElement(child, id.Id); + } + break; + case "CommandLine": + allowed = (packageType == WixBundlePackageType.Bundle || packageType == WixBundlePackageType.Exe); + if (allowed) + { + this.ParseCommandLineElement(child, id.Id); + } + break; + case "BundlePackagePayload": + case "ExePackagePayload": + case "MsiPackagePayload": + case "MspPackagePayload": + case "MsuPackagePayload": + allowed = packagePayloadElementName == child.Name.LocalName; + // Handled previously + break; + default: + allowed = false; + break; + } + + if (!allowed) + { + this.Core.UnexpectedElement(node, child); + } + } + else + { + var context = new Dictionary() { { "Id", id.Id } }; + this.Core.ParseExtensionElement(node, child, context); + } + } + if (id.Id == BurnConstants.BundleDefaultBoundaryId) { this.Messaging.Write(CompilerErrors.ReservedValue(sourceLineNumbers, node.Name.LocalName, "Id", id.Id)); @@ -2234,37 +2344,72 @@ namespace WixToolset.Core perMachine = YesNoDefaultType.Default; } - if (permanent == YesNoType.No) + if (exeDetectionType == WixBundleExePackageDetectionType.Arp) { - if (uninstallArguments == null) - { - this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "UninstallArguments", "Permanent", "no")); - } + // Missing attributes are reported when parsing the element. } - else if (permanent == YesNoType.NotSet) + else if (exeDetectionType == WixBundleExePackageDetectionType.Condition) { + if (String.IsNullOrEmpty(detectCondition)) + { + if (permanent == YesNoType.No) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "Permanent", "no")); + } + else if (permanent == YesNoType.NotSet) + { + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "Permanent")); + } + else if (repairArguments != null) + { + this.Core.Write(ErrorMessages.ExpectedAttributeWithValueWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "RepairArguments")); + } + else if (uninstallArguments != null) + { + this.Core.Write(ErrorMessages.ExpectedAttributeWithValueWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "UninstallArguments")); + } + else + { + Debug.Assert(detectCondition == String.Empty); + exeDetectionType = WixBundleExePackageDetectionType.None; + } + } + if (uninstallArguments == null) { - this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(sourceLineNumbers, node.Name.LocalName, "UninstallArguments", "Permanent")); + if (permanent == YesNoType.No) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "UninstallArguments", "Permanent", "no")); + } + else if (permanent == YesNoType.NotSet) + { + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(sourceLineNumbers, node.Name.LocalName, "UninstallArguments", "Permanent")); + } } } - - // Detect condition is recommended or required for Exe packages (depending on whether repair or uninstall arguments were provided). - if (String.IsNullOrEmpty(detectCondition)) + else if (exeDetectionType == WixBundleExePackageDetectionType.None) { - if (repairArguments != null) + if (permanent == YesNoType.No) + { + this.Core.Write(ErrorMessages.ExpectedAttributeOrElementWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "ArpEntry", "Permanent", "no")); + } + else if (permanent == YesNoType.NotSet) { - this.Core.Write(ErrorMessages.ExpectedAttributeWithValueWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "RepairArguments")); + this.Core.Write(ErrorMessages.ExpectedAttributeOrElementWithoutOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "ArpEntry", "Permanent")); } - else if (uninstallArguments != null) + else if (repairArguments != null) { - this.Core.Write(ErrorMessages.ExpectedAttributeWithValueWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "UninstallArguments")); + this.Core.Write(ErrorMessages.ExpectedAttributeOrElementWithOtherAttribute(sourceLineNumbers, node.Name.LocalName, "DetectCondition", "ArpEntry", "RepairArguments")); } else { - this.Core.Write(WarningMessages.DetectConditionRecommended(sourceLineNumbers, node.Name.LocalName)); + this.Core.Write(WarningMessages.ExePackageDetectInformationRecommended(sourceLineNumbers)); } } + else + { + this.Core.Write(ErrorMessages.UnexpectedElementWithAttribute(sourceLineNumbers, node.Name.LocalName, "ArpEntry", String.IsNullOrEmpty(detectCondition) ? "UninstallArguments" : "DetectCondition")); + } if (repairArguments == null && repairCondition != null) { @@ -2341,82 +2486,6 @@ namespace WixToolset.Core } } - // Now that the package ID is known, we can parse the extension attributes... - var contextValues = new Dictionary() { { "PackageId", id.Id } }; - foreach (var attribute in extensionAttributes) - { - this.Core.ParseExtensionAttribute(node, attribute, contextValues); - } - - foreach (var child in node.Elements()) - { - if (CompilerCore.WixNamespace == child.Name.Namespace) - { - var allowed = true; - switch (child.Name.LocalName) - { - case "SlipstreamMsp": - allowed = (packageType == WixBundlePackageType.Msi); - if (allowed) - { - this.ParseSlipstreamMspElement(child, id.Id); - } - break; - case "MsiProperty": - allowed = (packageType == WixBundlePackageType.Msi || packageType == WixBundlePackageType.Msp); - if (allowed) - { - this.ParseMsiPropertyElement(child, id.Id); - } - break; - case "Payload": - this.ParsePayloadElement(child, ComplexReferenceParentType.Package, id); - break; - case "PayloadGroupRef": - this.ParsePayloadGroupRefElement(child, ComplexReferenceParentType.Package, id); - break; - case "Provides": - this.ParseProvidesElement(child, packageType, id.Id, out _); - break; - case "ExitCode": - allowed = (packageType == WixBundlePackageType.Bundle || packageType == WixBundlePackageType.Exe); - if (allowed) - { - this.ParseExitCodeElement(child, id.Id); - } - break; - case "CommandLine": - allowed = (packageType == WixBundlePackageType.Bundle || packageType == WixBundlePackageType.Exe); - if (allowed) - { - this.ParseCommandLineElement(child, id.Id); - } - break; - case "BundlePackagePayload": - case "ExePackagePayload": - case "MsiPackagePayload": - case "MspPackagePayload": - case "MsuPackagePayload": - allowed = packagePayloadElementName == child.Name.LocalName; - // Handled previously - break; - default: - allowed = false; - break; - } - - if (!allowed) - { - this.Core.UnexpectedElement(node, child); - } - } - else - { - var context = new Dictionary() { { "Id", id.Id } }; - this.Core.ParseExtensionElement(node, child, context); - } - } - if (!this.Core.EncounteredError) { var compilerPackagePayload = childCompilerPackagePayload ?? (hasPayloadInfo ? new CompilerPackagePayload(compilerPayload, packageType) : null); @@ -2474,6 +2543,7 @@ namespace WixToolset.Core case WixBundlePackageType.Exe: WixBundleExePackageAttributes exeAttributes = 0; exeAttributes |= (YesNoType.Yes == bundle) ? WixBundleExePackageAttributes.Bundle : 0; + exeAttributes |= (YesNoType.Yes == arpWin64) ? WixBundleExePackageAttributes.ArpWin64 : 0; this.Core.AddSymbol(new WixBundleExePackageSymbol(sourceLineNumbers, id) { @@ -2482,7 +2552,10 @@ namespace WixToolset.Core InstallCommand = installArguments, RepairCommand = repairArguments, UninstallCommand = uninstallArguments, - ExeProtocol = protocol + ExeProtocol = protocol, + DetectionType = exeDetectionType.Value, + ArpId = arpId, + ArpDisplayVersion = arpDisplayVersion, }); break; @@ -2946,6 +3019,57 @@ namespace WixToolset.Core } } + private void ParseExePackageArpEntryElement(XElement node, out string id, out string version, out YesNoType win64) + { + var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node); + id = null; + version = null; + win64 = YesNoType.NotSet; + + foreach (var attrib in node.Attributes()) + { + if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || CompilerCore.WixNamespace == attrib.Name.Namespace) + { + switch (attrib.Name.LocalName) + { + case "Id": + id = this.Core.GetAttributeLongFilename(sourceLineNumbers, attrib); + break; + case "Version": + version = this.Core.GetAttributeValue(sourceLineNumbers, attrib); + break; + case "Win64": + win64 = this.Core.GetAttributeYesNoValue(sourceLineNumbers, attrib); + break; + default: + this.Core.UnexpectedAttribute(node, attrib); + break; + } + } + else + { + this.Core.ParseExtensionAttribute(node, attrib); + } + } + + this.Core.ParseForExtensionElements(node); + + if (String.IsNullOrEmpty(id)) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Id")); + } + + if (String.IsNullOrEmpty(version)) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Version")); + } + + if (win64 == YesNoType.NotSet) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Win64")); + } + } + /// /// Parse CommandLine element. /// diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/BundleManifestFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/BundleManifestFixture.cs index 368cd961..242b9303 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/BundleManifestFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/BundleManifestFixture.cs @@ -354,8 +354,8 @@ namespace WixToolsetTest.CoreIntegration { "ExePackage", new List { "CacheId", "InstallSize", "Size" } }, }; Assert.Equal(2, exePackageElements.Count); - Assert.Equal("", exePackageElements[0].GetTestXml(ignoreAttributesByElementName)); - Assert.Equal("", exePackageElements[1].GetTestXml(ignoreAttributesByElementName)); + Assert.Equal("", exePackageElements[0].GetTestXml(ignoreAttributesByElementName)); + Assert.Equal("", exePackageElements[1].GetTestXml(ignoreAttributesByElementName)); } } diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/ExePackageFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/ExePackageFixture.cs index 67f295e8..32c207f7 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/ExePackageFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/ExePackageFixture.cs @@ -2,8 +2,10 @@ namespace WixToolsetTest.CoreIntegration { + using System.Collections.Generic; using System.IO; using System.Linq; + using System.Xml; using WixBuildTools.TestSupport; using WixToolset.Core.TestPackage; using Xunit; @@ -11,7 +13,259 @@ namespace WixToolsetTest.CoreIntegration public class ExePackageFixture { [Fact] - public void ErrorWhenMissingDetectCondition() + public void CanBuildWithArpEntry() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + var binFolder = Path.Combine(baseFolder, "bin"); + var bundlePath = Path.Combine(binFolder, "test.exe"); + var baFolderPath = Path.Combine(baseFolder, "ba"); + var extractFolderPath = Path.Combine(baseFolder, "extract"); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "ExePackage", "ArpEntry.wxs"), + Path.Combine(folder, "BundleWithPackageGroupRef", "Bundle.wxs"), + "-bindpath", Path.Combine(folder, "SimpleBundle", "data"), + "-bindpath", Path.Combine(folder, ".Data"), + "-intermediateFolder", intermediateFolder, + "-o", bundlePath, + }); + + result.AssertSuccess(); + + Assert.True(File.Exists(bundlePath)); + + var extractResult = BundleExtractor.ExtractBAContainer(null, bundlePath, baFolderPath, extractFolderPath); + extractResult.AssertSuccess(); + + var ignoreAttributes = new Dictionary> + { + { "ExePackage", new List { "CacheId", "Size" } }, + }; + var exePackages = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:Chain/burn:ExePackage") + .Cast() + .Select(e => e.GetTestXml(ignoreAttributes)) + .ToArray(); + WixAssert.CompareLineByLine(new string[] + { + "" + + "" + + "", + }, exePackages); + } + } + + [Fact] + public void WarningWhenInvalidArpEntryVersion() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + var binFolder = Path.Combine(baseFolder, "bin"); + var bundlePath = Path.Combine(binFolder, "test.exe"); + var baFolderPath = Path.Combine(baseFolder, "ba"); + var extractFolderPath = Path.Combine(baseFolder, "extract"); + + var result = WixRunner.Execute(false, new[] + { + "build", + Path.Combine(folder, "ExePackage", "InvalidArpEntryVersion.wxs"), + Path.Combine(folder, "BundleWithPackageGroupRef", "Bundle.wxs"), + "-bindpath", Path.Combine(folder, "SimpleBundle", "data"), + "-bindpath", Path.Combine(folder, ".Data"), + "-intermediateFolder", intermediateFolder, + "-o", bundlePath, + }); + + WixAssert.CompareLineByLine(new[] + { + "Invalid WixVersion '1.0.0.abc' in ArpEntry/@'Version'. Comparisons may yield unexpected results." + }, result.Messages.Select(m => m.ToString()).ToArray()); + result.AssertSuccess(); + + Assert.True(File.Exists(bundlePath)); + + var extractResult = BundleExtractor.ExtractBAContainer(null, bundlePath, baFolderPath, extractFolderPath); + extractResult.AssertSuccess(); + + var ignoreAttributes = new Dictionary> + { + { "ExePackage", new List { "CacheId", "Size" } }, + }; + var exePackages = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:Chain/burn:ExePackage") + .Cast() + .Select(e => e.GetTestXml(ignoreAttributes)) + .ToArray(); + WixAssert.CompareLineByLine(new string[] + { + "" + + "" + + "", + }, exePackages); + } + } + + [Fact] + public void WarningWhenPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + var binFolder = Path.Combine(baseFolder, "bin"); + var bundlePath = Path.Combine(binFolder, "test.exe"); + var baFolderPath = Path.Combine(baseFolder, "ba"); + var extractFolderPath = Path.Combine(baseFolder, "extract"); + + var result = WixRunner.Execute(false, new[] + { + "build", + Path.Combine(folder, "ExePackage", "PermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs"), + Path.Combine(folder, "BundleWithPackageGroupRef", "Bundle.wxs"), + "-bindpath", Path.Combine(folder, "SimpleBundle", "data"), + "-bindpath", Path.Combine(folder, ".Data"), + "-intermediateFolder", intermediateFolder, + "-o", bundlePath, + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackage/@DetectCondition attribute or child element ArpEntry is recommended so the package is only installed when absent." + }, result.Messages.Select(m => m.ToString()).ToArray()); + result.AssertSuccess(); + + Assert.True(File.Exists(bundlePath)); + + var extractResult = BundleExtractor.ExtractBAContainer(null, bundlePath, baFolderPath, extractFolderPath); + extractResult.AssertSuccess(); + + var ignoreAttributes = new Dictionary> + { + { "ExePackage", new List { "CacheId", "Size" } }, + }; + var exePackages = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:Chain/burn:ExePackage") + .Cast() + .Select(e => e.GetTestXml(ignoreAttributes)) + .ToArray(); + WixAssert.CompareLineByLine(new string[] + { + "" + + "" + + "", + }, exePackages); + } + } + + [Fact] + public void NoWarningWhenPermanentWithEmptyDetectCondition() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + var binFolder = Path.Combine(baseFolder, "bin"); + var bundlePath = Path.Combine(binFolder, "test.exe"); + var baFolderPath = Path.Combine(baseFolder, "ba"); + var extractFolderPath = Path.Combine(baseFolder, "extract"); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "ExePackage", "PermanentWithEmptyDetectCondition.wxs"), + Path.Combine(folder, "BundleWithPackageGroupRef", "Bundle.wxs"), + "-bindpath", Path.Combine(folder, "SimpleBundle", "data"), + "-bindpath", Path.Combine(folder, ".Data"), + "-intermediateFolder", intermediateFolder, + "-o", bundlePath, + }); + + result.AssertSuccess(); + + Assert.True(File.Exists(bundlePath)); + + var extractResult = BundleExtractor.ExtractBAContainer(null, bundlePath, baFolderPath, extractFolderPath); + extractResult.AssertSuccess(); + + var ignoreAttributes = new Dictionary> + { + { "ExePackage", new List { "CacheId", "Size" } }, + }; + var exePackages = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:Chain/burn:ExePackage") + .Cast() + .Select(e => e.GetTestXml(ignoreAttributes)) + .ToArray(); + WixAssert.CompareLineByLine(new string[] + { + "" + + "" + + "", + }, exePackages); + } + } + + [Fact] + public void ErrorWhenArpEntryWithDetectCondition() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "ArpEntryWithDetectCondition.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackage element cannot have a child element 'ArpEntry' when attribute 'DetectCondition' is set.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(372, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenArpEntryWithUninstallArguments() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "ArpEntryWithUninstallArguments.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackage element cannot have a child element 'ArpEntry' when attribute 'UninstallArguments' is set.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(372, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenArpEntryWithInvalidId() { var folder = TestData.Get(@"TestData", "ExePackage"); @@ -22,16 +276,135 @@ namespace WixToolsetTest.CoreIntegration var result = WixRunner.Execute(new[] { "build", - Path.Combine(folder, "MissingDetectCondition.wxs"), + Path.Combine(folder, "InvalidArpEntryId.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ArpEntry/@Id attribute's value, '..\\id', is not a valid filename because it contains illegal characters. Legal filenames contain no more than 260 characters and must contain at least one non-period character. Any character except for the follow may be used: \\ ? | > < : / * \".", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(27, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenNonPermanentWithOnlyDetectCondition() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "NonPermanentWithOnlyDetectCondition.wxs"), "-o", Path.Combine(baseFolder, "test.wixlib") }); WixAssert.CompareLineByLine(new[] { "The ExePackage element's UninstallArguments attribute was not found; it is required without attribute Permanent present.", - "The ExePackage/@DetectCondition attribute is recommended so the package is only installed when absent." }, result.Messages.Select(m => m.ToString()).ToArray()); - Assert.Equal(1153, result.ExitCode); + Assert.Equal(408, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenNonPermanentWithOnlyUninstallArguments() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "NonPermanentWithOnlyUninstallArguments.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackage element's DetectCondition attribute was not found; it is required without attribute Permanent present.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(408, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenRepairablePermanentWithoutDetectCondition() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "RepairablePermanentWithoutDetectCondition.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackage/@DetectCondition attribute is required to have a value when attribute RepairArguments is present.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(401, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenRepairablePermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "RepairablePermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "Element 'ExePackage' missing attribute 'DetectCondition' or child element 'ArpEntry'. Exactly one of those is required when attribute 'RepairArguments' is specified.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(413, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenNonPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry() + { + var folder = TestData.Get(@"TestData", "ExePackage"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "NonPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "Element 'ExePackage' missing attribute 'DetectCondition' or child element 'ArpEntry'. Exactly one of those is required when attribute 'Permanent' is not specified.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(414, result.ExitCode); } } @@ -60,7 +433,7 @@ namespace WixToolsetTest.CoreIntegration } [Fact] - public void ErrorWhenRequireDetectCondition() + public void ErrorWhenUninstallArgumentsWithoutDetectCondition() { var folder = TestData.Get(@"TestData", "ExePackage"); @@ -71,10 +444,14 @@ namespace WixToolsetTest.CoreIntegration var result = WixRunner.Execute(new[] { "build", - Path.Combine(folder, "RequireDetectCondition.wxs"), + Path.Combine(folder, "UninstallArgumentsWithoutDetectCondition.wxs"), "-o", Path.Combine(baseFolder, "test.wixlib") }); + WixAssert.CompareLineByLine(new[] + { + "The ExePackage/@DetectCondition attribute is required to have a value when attribute UninstallArguments is present.", + }, result.Messages.Select(m => m.ToString()).ToArray()); Assert.Equal(401, result.ExitCode); } } diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs index 29ea1e7f..da15026a 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs @@ -126,7 +126,7 @@ namespace WixToolsetTest.CoreIntegration { "ExePackage", new List { "CacheId", "InstallSize", "Size" } }, }; Assert.Equal(1, exePackageElements.Count); - Assert.Equal("", exePackageElements[0].GetTestXml(ignoreAttributesByElementName)); + Assert.Equal("", exePackageElements[0].GetTestXml(ignoreAttributesByElementName)); var payloadElements = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:Payload[@Id='burn.exe']"); Assert.Equal(1, payloadElements.Count); diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntry.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntry.wxs new file mode 100644 index 00000000..19b20853 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntry.wxs @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithDetectCondition.wxs new file mode 100644 index 00000000..f9c3414c --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithDetectCondition.wxs @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithUninstallArguments.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithUninstallArguments.wxs new file mode 100644 index 00000000..794c6dd3 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/ArpEntryWithUninstallArguments.wxs @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryId.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryId.wxs new file mode 100644 index 00000000..d81267f2 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryId.wxs @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryVersion.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryVersion.wxs new file mode 100644 index 00000000..3230d4b6 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/InvalidArpEntryVersion.wxs @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/MissingDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/MissingDetectCondition.wxs deleted file mode 100644 index e57180f7..00000000 --- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/MissingDetectCondition.wxs +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyDetectCondition.wxs new file mode 100644 index 00000000..c47af081 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyDetectCondition.wxs @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyUninstallArguments.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyUninstallArguments.wxs new file mode 100644 index 00000000..004fd093 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithOnlyUninstallArguments.wxs @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs new file mode 100644 index 00000000..e57180f7 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/NonPermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithEmptyDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithEmptyDetectCondition.wxs new file mode 100644 index 00000000..f16caabf --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithEmptyDetectCondition.wxs @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs new file mode 100644 index 00000000..53c7b2ef --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/PermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectCondition.wxs new file mode 100644 index 00000000..83adeb29 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectCondition.wxs @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs new file mode 100644 index 00000000..50d01252 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RepairablePermanentWithoutDetectConditionOrUninstallArgumentsOrArpEntry.wxs @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RequireDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RequireDetectCondition.wxs deleted file mode 100644 index 0b094860..00000000 --- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/RequireDetectCondition.wxs +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/UninstallArgumentsWithoutDetectCondition.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/UninstallArgumentsWithoutDetectCondition.wxs new file mode 100644 index 00000000..792b2503 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/ExePackage/UninstallArgumentsWithoutDetectCondition.wxs @@ -0,0 +1,12 @@ + + + + + + + + -- cgit v1.2.3-55-g6feb