diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index fbabf579ad..4bda655f7d 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -91,5 +91,6 @@ # Because it doesn't handle argument -Words well ^src/PowerShell/tests/ ^tools/CorrelationTestbed/.*\.ps1$ +^tools/DevInSandbox/.*\.ps1$ ^tools/COMTrace/ComTrace.wprp$ ignore$ diff --git a/doc/Settings.md b/doc/Settings.md index dda04d2a95..04810d5c20 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -280,17 +280,6 @@ You can enable the feature as shown below. }, ``` -### configuration - -This feature enables the configuration commands. These commands allow configuring the system into a desired state. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "configuration": true - }, -``` - ### resume This feature enables support for some commands to resume. @@ -323,3 +312,14 @@ You can enable the feature as shown below. "proxy": true }, ``` + +### sideBySide + +This feature enables experimental improvements for supporting multiple instances of a package being installed on a system. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "sideBySide": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 80e224487b..ce8b5d47e1 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -275,6 +275,11 @@ "description": "Enable support for proxies", "type": "boolean", "default": false + }, + "sideBySide": { + "description": "Enable support for improved side-by-side handling", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index ffc41ed56e..732c81cbca 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -30,7 +30,7 @@ namespace AppInstaller::CLI case Execution::Args::Type::Query: return { type, "query"_liv, 'q', ArgTypeCategory::PackageQuery | ArgTypeCategory::SinglePackageQuery }; case Execution::Args::Type::MultiQuery: - return { type, "query"_liv, 'q', ArgTypeCategory::PackageQuery }; + return { type, "query"_liv, 'q', ArgTypeCategory::PackageQuery | ArgTypeCategory::MultiplePackages }; case Execution::Args::Type::Manifest: return { type, "manifest"_liv, 'm', ArgTypeCategory::Manifest }; @@ -101,6 +101,10 @@ namespace AppInstaller::CLI return { type, "preserve"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::PurgePreserve }; case Execution::Args::Type::ProductCode: return { type, "product-code"_liv, ArgTypeCategory::SinglePackageQuery }; + case Execution::Args::Type::AllVersions: + return { type, "all-versions"_liv, "all"_liv, ArgTypeCategory::CopyFlagToSubContext, ArgTypeExclusiveSet::AllAndTargetVersion }; + case Execution::Args::Type::TargetVersion: + return { type, "version"_liv, 'v', ArgTypeCategory::SinglePackageQuery, ArgTypeExclusiveSet::AllAndTargetVersion }; //Source Command case Execution::Args::Type::SourceName: @@ -388,6 +392,10 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::AllowRebootArgumentDescription, ArgumentType::Flag }; case Args::Type::IgnoreResumeLimit: return Argument{ type, Resource::String::IgnoreResumeLimitArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::Resume }; + case Args::Type::AllVersions: + return Argument{ type, Resource::String::UninstallAllVersionsArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help, ExperimentalFeature::Feature::SideBySide }; + case Args::Type::TargetVersion: + return Argument{ type, Resource::String::TargetVersionArgumentDescription, ArgumentType::Standard }; case Args::Type::Proxy: return Argument{ type, Resource::String::ProxyArgumentDescription, ArgumentType::Standard, ExperimentalFeature::Feature::Proxy, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions }; case Args::Type::NoProxy: diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h index 741c2de068..c203e4276f 100644 --- a/src/AppInstallerCLICore/Argument.h +++ b/src/AppInstallerCLICore/Argument.h @@ -86,6 +86,7 @@ namespace AppInstaller::CLI PinType = 0x8, StubType = 0x10, Proxy = 0x20, + AllAndTargetVersion = 0x40, // This must always be at the end Max diff --git a/src/AppInstallerCLICore/Commands/RepairCommand.cpp b/src/AppInstallerCLICore/Commands/RepairCommand.cpp index 58668a783b..210dabb584 100644 --- a/src/AppInstallerCLICore/Commands/RepairCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RepairCommand.cpp @@ -20,7 +20,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Name), // -n Argument::ForType(Args::Type::Channel), Argument::ForType(Args::Type::Moniker), // -mn - Argument::ForType(Args::Type::Version), // -v + Argument::ForType(Args::Type::TargetVersion), // -v Argument::ForType(Args::Type::ProductCode), Argument::ForType(Args::Type::InstallArchitecture), // -arch Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, @@ -60,12 +60,16 @@ namespace AppInstaller::CLI return; } + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + switch (valueType) { case Execution::Args::Type::Id: case Execution::Args::Type::Name: case Execution::Args::Type::Moniker: - case Execution::Args::Type::Version: + case Execution::Args::Type::TargetVersion: case Execution::Args::Type::Channel: case Execution::Args::Type::Source: context << diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp index a0b0ae91a8..bf531daa61 100644 --- a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -23,11 +23,12 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Name), Argument::ForType(Args::Type::Moniker), Argument::ForType(Args::Type::ProductCode), - Argument::ForType(Args::Type::Version), + Argument::ForType(Args::Type::TargetVersion), + Argument::ForType(Args::Type::AllVersions), Argument::ForType(Args::Type::Channel), Argument::ForType(Args::Type::Source), Argument::ForType(Args::Type::Exact), - Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, + Argument{ Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Args::Type::Interactive), Argument::ForType(Args::Type::Silent), Argument::ForType(Args::Type::Force), @@ -75,7 +76,7 @@ namespace AppInstaller::CLI case Execution::Args::Type::Id: case Execution::Args::Type::Name: case Execution::Args::Type::Moniker: - case Execution::Args::Type::Version: + case Execution::Args::Type::TargetVersion: case Execution::Args::Type::Channel: case Execution::Args::Type::Source: case Execution::Args::Type::ProductCode: @@ -111,7 +112,6 @@ namespace AppInstaller::CLI // --manifest case where new manifest is provided context << Workflow::GetManifestFromArg << - Workflow::ReportManifestIdentity << Workflow::SearchSourceUsingManifest << Workflow::EnsureOneMatchFromSearchResult(OperationType::Uninstall) << Workflow::UninstallSinglePackage; @@ -125,7 +125,6 @@ namespace AppInstaller::CLI Workflow::SearchSourceForSingle << Workflow::HandleSearchResultFailures << Workflow::EnsureOneMatchFromSearchResult(OperationType::Uninstall) << - Workflow::ReportPackageIdentity << Workflow::UninstallSinglePackage; } else diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 8ea1e0754d..58034b5932 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -54,6 +54,8 @@ namespace AppInstaller::CLI::Execution Purge, // Removes all files and directories related to a package during an uninstall. Only applies to the portable installerType. Preserve, // Retains any files and directories created by the portable exe. ProductCode, // Uninstalls using the product code as the identifier. + AllVersions, // Uninstall all versions of the package + TargetVersion, // The specific version to target //Source Command SourceName, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index a05c3c691a..30ae374ebe 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -574,6 +574,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(StateHeader); WINGET_DEFINE_RESOURCE_STRINGID(SystemArchitecture); WINGET_DEFINE_RESOURCE_STRINGID(TagArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(TargetVersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ThankYou); WINGET_DEFINE_RESOURCE_STRINGID(ThirdPartSoftwareNotices); WINGET_DEFINE_RESOURCE_STRINGID(ToolDescription); @@ -585,9 +586,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Unavailable); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallAllVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandReportDependencies); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedDueToMultipleVersions); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedWithCode); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowStartingPackageUninstall); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowUninstallSuccess); diff --git a/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp b/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp index 57789b8b1d..2090335e46 100644 --- a/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/CompletionFlow.cpp @@ -82,6 +82,25 @@ namespace AppInstaller::CLI::Workflow } } + void CompleteWithSearchResultInstalledVersions(Execution::Context& context) + { + const std::string& word = context.Get().Word(); + auto stream = context.Reporter.Completion(); + + auto installedPackage = context.Get()->GetInstalled(); + + if (installedPackage) + { + for (const auto& vc : installedPackage->GetVersionKeys()) + { + if (word.empty() || Utility::ICUCaseInsensitiveStartsWith(vc.Version, word)) + { + OutputCompletionString(stream, vc.Version); + } + } + } + } + void CompleteWithSearchResultChannels(Execution::Context& context) { const std::string& word = context.Get().Word(); @@ -174,6 +193,13 @@ namespace AppInstaller::CLI::Workflow Workflow::EnsureOneMatchFromSearchResult(OperationType::Completion) << Workflow::CompleteWithSearchResultVersions; break; + case Execution::Args::Type::TargetVersion: + // Here we require that the standard search finds a single entry, and we list the installed versions. + context << + Workflow::SearchSourceForSingle << + Workflow::EnsureOneMatchFromSearchResult(OperationType::Completion) << + Workflow::CompleteWithSearchResultInstalledVersions; + break; case Execution::Args::Type::Channel: // Here we require that the standard search finds a single entry, and we list those channels. context << diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp index f74bece11b..ace205fc44 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -31,6 +31,26 @@ namespace AppInstaller::CLI::Workflow } } + void GetPinKeysForInstalled(const std::shared_ptr& installedVersion, std::set& pinKeys) + { + auto installedType = Manifest::ConvertToInstallerTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); + std::vector propertyStrings; + + if (Manifest::DoesInstallerTypeUsePackageFamilyName(installedType)) + { + propertyStrings = installedVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); + } + else if (Manifest::DoesInstallerTypeUseProductCode(installedType)) + { + propertyStrings = installedVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); + } + + for (const auto& value : propertyStrings) + { + pinKeys.emplace(Pinning::PinKey::GetPinKeyForInstalled(value)); + } + } + std::set GetPinKeysForPackage(Execution::Context& context) { auto package = context.Get(); @@ -42,7 +62,7 @@ namespace AppInstaller::CLI::Workflow auto installedVersion = GetInstalledVersion(package); if (installedVersion) { - pinKeys.emplace(Pinning::PinKey::GetPinKeyForInstalled(installedVersion->GetProperty(PackageVersionProperty::Id))); + GetPinKeysForInstalled(installedVersion, pinKeys); } } else diff --git a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp index f4158c4622..44e05419d5 100644 --- a/src/AppInstallerCLICore/Workflows/RepairFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/RepairFlow.cpp @@ -441,7 +441,7 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); } - std::string_view requestedVersion = context.Args.Contains(Execution::Args::Type::Version) ? context.Args.GetArg(Execution::Args::Type::Version) : installedVersion.ToString(); + std::string_view requestedVersion = context.Args.Contains(Execution::Args::Type::TargetVersion) ? context.Args.GetArg(Execution::Args::Type::TargetVersion) : installedVersion.ToString(); // If it's Store source with only one version unknown, use the unknown version for available version mapping. const auto& package = context.Get(); auto packageVersions = GetAvailableVersionsForInstalledVersion(package, installedPackage); diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index 225e58cd64..169b3b77be 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -69,9 +69,82 @@ namespace AppInstaller::CLI::Workflow } void UninstallSinglePackage(Execution::Context& context) + { + std::shared_ptr package = context.Get(); + std::shared_ptr installed = package->GetInstalled(); + std::vector installedVersionKeys; + if (installed) + { + installedVersionKeys = installed->GetVersionKeys(); + } + + // Handle multiple installed versions when we have been told to uninstall all of them. + if (installedVersionKeys.size() > 1 && context.Args.Contains(Execution::Args::Type::AllVersions)) + { + bool allSucceeded = true; + size_t versionsCount = installedVersionKeys.size(); + size_t versionsProgress = 0; + + for (const auto& key : installedVersionKeys) + { + context.Reporter.Info() << '(' << ++versionsProgress << '/' << versionsCount << ") "_liv; + + // We want to do best effort to uninstall all versions regardless of previous failures + auto subContextPtr = context.CreateSubContext(); + Execution::Context& uninstallContext = *subContextPtr; + auto previousThreadGlobals = uninstallContext.SetForCurrentThread(); + + uninstallContext.Add(package); + uninstallContext.Add(installed->GetVersion(key)); + + // Prevent individual exceptions from breaking out of the loop + try + { + uninstallContext << + Workflow::UninstallSinglePackageVersion; + } + catch (...) + { + uninstallContext.SetTerminationHR(Workflow::HandleException(uninstallContext, std::current_exception())); + } + + uninstallContext.Reporter.Info() << std::endl; + + if (uninstallContext.IsTerminated()) + { + if (context.IsTerminated() && context.GetTerminationHR() == E_ABORT) + { + // This means that the subcontext being terminated is due to an overall abort + context.Reporter.Info() << Resource::String::Cancelled << std::endl; + return; + } + + allSucceeded = false; + } + } + + if (!allSucceeded) + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_UNINSTALL_FAILED); + } + } + else if (installedVersionKeys.size() > 1 && !context.Args.Contains(Execution::Args::Type::TargetVersion)) + { + context.Reporter.Error() << Resource::String::UninstallFailedDueToMultipleVersions << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); + } + else + { + context << + Workflow::GetInstalledPackageVersion << + Workflow::UninstallSinglePackageVersion; + } + } + + void UninstallSinglePackageVersion(Execution::Context& context) { context << - Workflow::GetInstalledPackageVersion << + Workflow::ReportInstalledPackageVersionIdentity << Workflow::EnsureSupportForUninstall << Workflow::GetUninstallInfo << Workflow::GetDependenciesInfoForUninstall << diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.h b/src/AppInstallerCLICore/Workflows/UninstallFlow.h index 5beb75fcde..4952bf0062 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.h @@ -12,6 +12,13 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void UninstallSinglePackage(Execution::Context& context); + // Uninstalls a single package version. This also does the reporting, user interaction, and recording + // for single-package uninstallation. + // RequiredArgs: None + // Inputs: InstalledPackageVersion + // Outputs: None + void UninstallSinglePackageVersion(Execution::Context& context); + // Uninstalls multiple packages. // RequiredArgs: None // Inputs: PackageSubContexts diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp index 4c72c468d5..99d0a82e04 100644 --- a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -87,6 +87,7 @@ namespace AppInstaller::CLI::Workflow // The only way to enter this portion of the statement with isUpgrade is if the version is available if (isUpgrade) { + AICLI_LOG(CLI, Verbose, << "Updating from [" << installedVersion.ToString() << "] to [" << key.Version << "]"); upgradeVersionAvailable = true; } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index b0045126af..752455b7d8 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -800,15 +800,27 @@ namespace AppInstaller::CLI::Workflow for (const auto& match : searchResult.Matches) { - auto installedVersion = GetInstalledVersion(match.Package); + auto installedPackage = match.Package->GetInstalled(); + if (!installedPackage) + { + continue; + } - if (installedVersion) + // We only want to evaluate update availability for the latest version. + bool isFirstInstalledVersion = true; + + for (const auto& installedVersionKey : installedPackage->GetVersionKeys()) { + bool isFirstInstalledVersionLocal = isFirstInstalledVersion; + isFirstInstalledVersion = false; + + auto installedVersion = installedPackage->GetVersion(installedVersionKey); + auto evaluator = pinningData.CreatePinStateEvaluator(pinBehavior, installedVersion); auto availableVersions = GetAvailableVersionsForInstalledVersion(match.Package, installedVersion); auto latestVersion = evaluator.GetLatestAvailableVersionForPins(availableVersions); - bool updateAvailable = evaluator.IsUpdate(latestVersion); + bool updateAvailable = isFirstInstalledVersionLocal && evaluator.IsUpdate(latestVersion); bool updateIsPinned = false; if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::IncludeUnknown) && Utility::Version(installedVersion->GetProperty(PackageVersionProperty::Version)).IsUnknown() && updateAvailable) @@ -818,7 +830,7 @@ namespace AppInstaller::CLI::Workflow continue; } - if (m_onlyShowUpgrades && !updateAvailable) + if (m_onlyShowUpgrades && !updateAvailable && isFirstInstalledVersionLocal) { // Reuse the evaluator to check if there is an update outside of the pinning auto unpinnedLatestVersion = availableVersions->GetLatestVersion(); @@ -1214,6 +1226,13 @@ namespace AppInstaller::CLI::Workflow ReportIdentity(context, {}, Resource::String::ReportIdentityFound, package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); } + void ReportInstalledPackageVersionIdentity(Execution::Context& context) + { + auto package = context.Get(); + auto version = context.Get(); + ReportIdentity(context, {}, Resource::String::ReportIdentityFound, version->GetProperty(PackageVersionProperty::Name), package ? package->GetProperty(PackageProperty::Id) : version->GetProperty(PackageVersionProperty::Id)); + } + void ReportManifestIdentity(Execution::Context& context) { const auto& manifest = context.Get(); @@ -1349,7 +1368,38 @@ namespace AppInstaller::CLI::Workflow void GetInstalledPackageVersion(Execution::Context& context) { - context.Add(GetInstalledVersion(context.Get())); + std::shared_ptr installed = context.Get()->GetInstalled(); + + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) + { + if (installed) + { + // TODO: This may need to be expanded dramatically to enable targeting across a variety of dimensions (architecture, etc.) + // Alternatively, if we make it easier to see the fully unique package identifiers, we may avoid that need. + if (context.Args.Contains(Execution::Args::Type::TargetVersion)) + { + Repository::PackageVersionKey versionKey{ "", context.Args.GetArg(Execution::Args::Type::TargetVersion) , "" }; + std::shared_ptr installedVersion = installed->GetVersion(versionKey); + + if (!installedVersion) + { + context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound(Utility::LocIndView{ versionKey.Version }) << std::endl; + // This error maintains consistency with passing an available version to commands + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); + } + + context.Add(std::move(installedVersion)); + } + else + { + context.Add(installed->GetLatestVersion()); + } + } + } + else + { + context.Add(GetInstalledVersion(context.Get())); + } } void ReportExecutionStage::operator()(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index 6595256426..c8cdce1f2c 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -353,6 +353,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportPackageIdentity(Execution::Context& context); + // Reports the installed package version identity. + // Required Args: None + // Inputs: InstalledPackageVersion + // Outputs: None + void ReportInstalledPackageVersionIdentity(Execution::Context& context); + // Reports the manifest's identity. // Required Args: None // Inputs: Manifest diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 4c068b9579..1071986a90 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs index 52bc0c528e..498a8c45de 100644 --- a/src/AppInstallerCLIE2ETests/ListCommand.cs +++ b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -105,20 +105,21 @@ public void ListWithUpgradeCode() public void ListWithScopeExeInstalledAsMachine() { System.Guid guid = System.Guid.NewGuid(); + string identifier = "AppInstallerTest.TestExeInstaller"; string productCode = guid.ToString(); var installDir = TestCommon.GetRandomTestDir(); - var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --override \"/InstallDir {installDir} /ProductID {productCode} /UseHKLM\""); + var result = TestCommon.RunAICLICommand("install", $"{identifier} --override \"/InstallDir {installDir} /ProductID {productCode} /UseHKLM\""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); // List with user scope will not find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope user"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.False(result.StdOut.Contains(productCode)); + Assert.False(result.StdOut.Contains(identifier)); // List with machine scope will find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope machine"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains(productCode)); + Assert.True(result.StdOut.Contains(identifier)); TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); } @@ -130,20 +131,21 @@ public void ListWithScopeExeInstalledAsMachine() public void ListWithScopeExeInstalledAsUser() { System.Guid guid = System.Guid.NewGuid(); + string identifier = "AppInstallerTest.TestExeInstaller"; string productCode = guid.ToString(); var installDir = TestCommon.GetRandomTestDir(); - var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --override \"/InstallDir {installDir} /ProductID {productCode}\""); + var result = TestCommon.RunAICLICommand("install", $"{identifier} --override \"/InstallDir {installDir} /ProductID {productCode}\""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); // List with user scope will find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope user"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains(productCode)); + Assert.True(result.StdOut.Contains(identifier)); // List with machine scope will not find the package result = TestCommon.RunAICLICommand("list", $"{productCode} --scope machine"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.False(result.StdOut.Contains(productCode)); + Assert.False(result.StdOut.Contains(identifier)); TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 3f4c5df8e9..3db1f51ecf 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2817,6 +2817,16 @@ Please specify one of them using the --source option to proceed. The SQLite connection was terminated to prevent corruption. + + Uninstall all versions + + + The version to act upon + + + Multiple versions of this package are installed. Either refine the search, pass the `--version` argument to select one, or pass the `--all-versions` flag to uninstall all of them. + {Locked="--version,--all-versions"} + Enable Windows Package Manager proxy command line options Describes a Group Policy that can enable the use of the --proxy option to set a proxy diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp index dbbddb7cff..d1c464a8fc 100644 --- a/src/AppInstallerCLITests/CompositeSource.cpp +++ b/src/AppInstallerCLITests/CompositeSource.cpp @@ -32,9 +32,10 @@ struct ComponentTestSource : public TestSource { ComponentTestSource() = default; - ComponentTestSource(std::string_view identifier) + ComponentTestSource(std::string_view identifier, SourceOrigin origin = SourceOrigin::Default) { Details.Identifier = identifier; + Details.Origin = origin; } SearchResult Search(const SearchRequest& request) const override @@ -52,47 +53,6 @@ struct ComponentTestSource : public TestSource SearchResult Everything; }; -// A helper to create the sources used by the majority of tests in this file. -struct CompositeTestSetup -{ - CompositeTestSetup(CompositeSearchBehavior behavior = CompositeSearchBehavior::Installed) : Composite("*Tests") - { - Installed = std::make_shared("InstalledTestSource1"); - Available = std::make_shared("AvailableTestSource1"); - Composite.SetInstalledSource(Source{ Installed }, behavior); - Composite.AddAvailableSource(Source{ Available }); - } - - SearchResult Search() - { - SearchRequest request; - request.Query = RequestMatch(MatchType::Exact, s_Everything_Query); - return Composite.Search(request); - } - - std::shared_ptr Installed; - std::shared_ptr Available; - CompositeSource Composite; -}; - -// A helper to create the sources used by the majority of tests in this file. -struct CompositeWithTrackingTestSetup : public CompositeTestSetup -{ - CompositeWithTrackingTestSetup() : TrackingFactory([&](const SourceDetails&) { return Tracking; }) - { - Tracking = std::make_shared(SourceDetails{}, SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET)); - TestHook_SetSourceFactoryOverride(std::string{ PackageTrackingCatalogSourceFactory::Type() }, TrackingFactory); - } - - ~CompositeWithTrackingTestSetup() - { - TestHook_ClearSourceFactoryOverrides(); - } - - TestSourceFactory TrackingFactory; - std::shared_ptr Tracking; -}; - // A helper to make matches. struct Criteria : public PackageMatchFilter { @@ -160,7 +120,7 @@ struct TestPackageHelper return *this; } - operator std::shared_ptr() + std::shared_ptr ToPackage() { if (!m_package) { @@ -177,6 +137,11 @@ struct TestPackageHelper return m_package; } + operator std::shared_ptr() + { + return ToPackage(); + } + operator const Manifest::Manifest& () const { return m_manifest; @@ -190,15 +155,66 @@ struct TestPackageHelper bool m_hideSystemReferenceStrings = false; }; -TestPackageHelper MakeInstalled() +// A helper to create the sources used by the majority of tests in this file. +struct CompositeTestSetup { - return { /* isInstalled */ true}; -} + CompositeTestSetup(CompositeSearchBehavior behavior = CompositeSearchBehavior::Installed) : Composite("*Tests") + { + Installed = std::make_shared("InstalledTestSource1", SourceOrigin::Predefined); + Available = std::make_shared("AvailableTestSource1"); + Composite.SetInstalledSource(Source{ Installed }, behavior); + Composite.AddAvailableSource(Source{ Available }); + } -TestPackageHelper MakeAvailable(std::shared_ptr source) + SearchResult Search() + { + SearchRequest request; + request.Query = RequestMatch(MatchType::Exact, s_Everything_Query); + return Composite.Search(request); + } + + TestPackageHelper MakeInstalled(std::shared_ptr source) + { + return { /* isInstalled */ true, std::move(source)}; + } + + TestPackageHelper MakeInstalled() + { + return MakeInstalled(Installed); + } + + TestPackageHelper MakeAvailable(std::shared_ptr source) + { + return { /* isInstalled */ false, std::move(source) }; + } + + TestPackageHelper MakeAvailable() + { + return MakeAvailable(Available); + } + + std::shared_ptr Installed; + std::shared_ptr Available; + CompositeSource Composite; +}; + +// A helper to create the sources used by the majority of tests in this file. +struct CompositeWithTrackingTestSetup : public CompositeTestSetup { - return { /* isInstalled */ false, source}; -} + CompositeWithTrackingTestSetup() : TrackingFactory([&](const SourceDetails&) { return Tracking; }) + { + Tracking = std::make_shared(SourceDetails{}, SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET)); + TestHook_SetSourceFactoryOverride(std::string{ PackageTrackingCatalogSourceFactory::Type() }, TrackingFactory); + } + + ~CompositeWithTrackingTestSetup() + { + TestHook_ClearSourceFactoryOverrides(); + } + + TestSourceFactory TrackingFactory; + std::shared_ptr Tracking; +}; bool SearchRequestIncludes(const std::vector& filters, PackageMatchField field, MatchType type, std::optional value = {}) { @@ -227,7 +243,7 @@ TEST_CASE("CompositeSource_PackageFamilyName_NotAvailable", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); SearchResult result = setup.Search(); @@ -241,13 +257,13 @@ TEST_CASE("CompositeSource_PackageFamilyName_Available", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -264,7 +280,7 @@ TEST_CASE("CompositeSource_ProductCode_NotAvailable", "[CompositeSource]") std::string pc = "thiscouldbeapc"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPC(pc), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPC(pc), Criteria()); SearchResult result = setup.Search(); @@ -278,13 +294,13 @@ TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") std::string pc = "thiscouldbeapc"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPC(pc), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPC(pc), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::ProductCode, MatchType::Exact, pc); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPC(pc), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPC(pc), Criteria()); return result; }; @@ -299,13 +315,13 @@ TEST_CASE("CompositeSource_ProductCode_Available", "[CompositeSource]") TEST_CASE("CompositeSource_NameAndPublisher_Match", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled(), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled(), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable(), Criteria()); return result; }; @@ -322,12 +338,12 @@ TEST_CASE("CompositeSource_MultiMatch_FindsStrongMatch", "[CompositeSource]") std::string name = "MatchingName"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN("sortof_apfn"), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); - result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(name), Criteria(PackageMatchField::PackageFamilyName)); + result.Matches.emplace_back(setup.MakeAvailable().WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(setup.MakeAvailable().WithDefaultName(name), Criteria(PackageMatchField::PackageFamilyName)); return result; }; @@ -345,12 +361,12 @@ TEST_CASE("CompositeSource_MultiMatch_FindsStrongMatch", "[CompositeSource]") TEST_CASE("CompositeSource_MultiMatch_DoesNotFindStrongMatch", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN("sortof_apfn"), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId("Another diff ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(setup.MakeAvailable().WithId("A different ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); + result.Matches.emplace_back(setup.MakeAvailable().WithId("Another diff ID"), Criteria(PackageMatchField::NormalizedNameAndPublisher)); return result; }; @@ -366,8 +382,8 @@ TEST_CASE("CompositeSource_FoundByBothRootSearches", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -407,17 +423,17 @@ TEST_CASE("CompositeSource_OnlyAvailableFoundByRootSearch", "[CompositeSource]") RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); return result; }; - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -434,13 +450,13 @@ TEST_CASE("CompositeSource_FoundByAvailableRootSearch_NotInstalled", "[Composite std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -456,8 +472,8 @@ TEST_CASE("CompositeSource_UpdateWithBetterMatchCriteria", "[CompositeSource]") MatchType type = MatchType::Exact; CompositeTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); @@ -504,7 +520,7 @@ TEST_CASE("CompositePackage_PropertyFromInstalled", "[CompositeSource]") std::string id = "Special test ID"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithId(id), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithId(id), Criteria()); SearchResult result = setup.Search(); @@ -518,11 +534,11 @@ TEST_CASE("CompositePackage_PropertyFromAvailable", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(id), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithId(id), Criteria()); return result; }; @@ -538,7 +554,7 @@ TEST_CASE("CompositePackage_AvailableVersions_ChannelFilteredOut", "[CompositeSo std::string channel = "Channel"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { Manifest::Manifest noChannel = MakeDefaultManifest(); @@ -580,7 +596,7 @@ TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[Composite std::string channel = "Channel"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn).WithChannel(channel), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn).WithChannel(channel), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) { Manifest::Manifest noChannel = MakeDefaultManifest(); @@ -628,14 +644,14 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource std::shared_ptr secondAvailable = std::make_shared(); setup.Composite.AddAvailableSource(Source{ secondAvailable }); - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); setup.Available->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(firstName), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithDefaultName(firstName), Criteria()); return result; }; @@ -644,7 +660,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(secondAvailable).WithDefaultName(secondName), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable(secondAvailable).WithDefaultName(secondName), Criteria()); return result; }; @@ -667,14 +683,14 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou std::shared_ptr secondAvailable = std::make_shared(); setup.Composite.AddAvailableSource(Source{ secondAvailable }); - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); secondAvailable->SearchFunction = [&](const SearchRequest& request) { RequireSearchRequestIncludes(request.Inclusions, PackageMatchField::PackageFamilyName, MatchType::Exact, pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithDefaultName(secondName), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithDefaultName(secondName), Criteria()); return result; }; @@ -690,9 +706,9 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi { std::string pfn = "sortof_apfn"; - auto installedPackage = MakeInstalled().WithPFN(pfn); - CompositeTestSetup setup; + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + std::shared_ptr secondAvailable = std::make_shared(); setup.Composite.AddAvailableSource(Source{ secondAvailable }); @@ -705,8 +721,8 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi return result; }; - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); - secondAvailable->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); + secondAvailable->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); SearchResult result = setup.Search(); @@ -719,7 +735,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi TEST_CASE("CompositeSource_IsSame", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN("sortof_apfn"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN("sortof_apfn"), Criteria()); SearchResult result1 = setup.Search(); REQUIRE(result1.Matches.size() == 1); @@ -740,7 +756,7 @@ TEST_CASE("CompositeSource_AvailableSearchFailure", "[CompositeSource]") AvailableSucceeds->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable({}).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(TestPackageHelper{ /* isInstalled */ false }.WithPFN(pfn), Criteria()); return result; }; @@ -784,8 +800,8 @@ TEST_CASE("CompositeSource_InstalledToAvailableCorrelationSearchFailure", "[Comp std::string pfn = "sortof_apfn"; CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeInstalled().WithPFN(pfn), Criteria()); - setup.Available->Everything.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithPFN(pfn), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); std::shared_ptr AvailableFails = std::make_shared(); AvailableFails->SearchFunction = [&](const SearchRequest&) -> SearchResult { THROW_HR(expectedHR); }; @@ -823,7 +839,7 @@ TEST_CASE("CompositeSource_InstalledAvailableSearchFailure", "[CompositeSource]" setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPFN(pfn), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPFN(pfn), Criteria()); return result; }; @@ -864,8 +880,8 @@ TEST_CASE("CompositeSource_TrackingPackageFound", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -914,8 +930,8 @@ TEST_CASE("CompositeSource_TrackingPackageFound_MetadataPopulatedFromTracking", std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -975,8 +991,8 @@ TEST_CASE("CompositeSource_TrackingFound_AvailableNot", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Installed->SearchFunction = [&](const SearchRequest& request) @@ -1007,8 +1023,8 @@ TEST_CASE("CompositeSource_TrackingFound_AvailablePath", "[CompositeSource]") std::string availableID = "Available.ID"; std::string pfn = "sortof_apfn"; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Installed->SearchFunction = [&](const SearchRequest& request) { @@ -1049,8 +1065,8 @@ TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") std::string pfn = "sortof_apfn"; CompositeWithTrackingTestSetup setup; - auto installedPackage = MakeInstalled().WithPFN(pfn); - auto availablePackage = MakeAvailable(setup.Available).WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); + auto installedPackage = setup.MakeInstalled().WithPFN(pfn); + auto availablePackage = setup.MakeAvailable().WithPFN(pfn).WithId(availableID).WithDefaultName(s_Everything_Query); setup.Available->Everything.Matches.emplace_back(availablePackage, Criteria()); @@ -1064,7 +1080,7 @@ TEST_CASE("CompositeSource_TrackingFound_NotInstalled", "[CompositeSource]") TEST_CASE("CompositeSource_NullInstalledVersion", "[CompositeSource]") { CompositeTestSetup setup; - setup.Installed->Everything.Matches.emplace_back(MakeAvailable(setup.Available), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeAvailable(), Criteria()); // We are mostly testing to see if a null installed version causes an AV or not SearchResult result = setup.Search(); @@ -1074,7 +1090,7 @@ TEST_CASE("CompositeSource_NullInstalledVersion", "[CompositeSource]") TEST_CASE("CompositeSource_NullAvailableVersion", "[CompositeSource]") { CompositeTestSetup setup{ CompositeSearchBehavior::AvailablePackages }; - setup.Available->Everything.Matches.emplace_back(MakeInstalled(), Criteria()); + setup.Available->Everything.Matches.emplace_back(setup.MakeInstalled(), Criteria()); // We are mostly testing to see if a null available version causes an AV or not REQUIRE_THROWS_HR(setup.Search(), E_UNEXPECTED); @@ -1156,7 +1172,7 @@ TEST_CASE("CompositeSource_Pinning_AvailableVersionPinned", "[CompositeSource][P TestUserSettings userSettings; CompositeTestSetup setup; - auto installedPackage = TestCompositePackage::Make(MakeDefaultManifest("1.0.1"sv), TestCompositePackage::MetadataMap{}); + auto installedPackage = setup.MakeInstalled().WithVersion("1.0.1"sv); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) @@ -1267,7 +1283,7 @@ TEST_CASE("CompositeSource_Pinning_OneSourcePinned", "[CompositeSource][PinFlow] TestUserSettings userSettings; CompositeTestSetup setup; - auto installedPackage = TestCompositePackage::Make(MakeDefaultManifest("1.0"sv), TestCompositePackage::MetadataMap{}); + auto installedPackage = setup.MakeInstalled().WithVersion("1.0"sv); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) @@ -1323,7 +1339,7 @@ TEST_CASE("CompositeSource_Pinning_OneSourceGated", "[CompositeSource][PinFlow]" TestUserSettings userSettings; CompositeTestSetup setup; - auto installedPackage = TestCompositePackage::Make(MakeDefaultManifest("1.0"sv), TestCompositePackage::MetadataMap{}); + auto installedPackage = setup.MakeInstalled().WithVersion("1.0.1"sv); setup.Installed->Everything.Matches.emplace_back(installedPackage, Criteria()); setup.Available->SearchFunction = [&](const SearchRequest&) @@ -1389,11 +1405,12 @@ TEST_CASE("CompositeSource_Pinning_MultipleInstalled", "[CompositeSource][PinFlo std::string productCode1 = "product-code1"; std::string productCode2 = "product-code2"; + CompositeTestSetup setup; + // Installed packages differ in product code and version - auto installedPackage1 = MakeInstalled().WithId(productCode1).WithPC(productCode1).WithVersion("1.1"sv); - auto installedPackage2 = MakeInstalled().WithId(productCode2).WithPC(productCode2).WithVersion("1.2"sv); + auto installedPackage1 = setup.MakeInstalled().WithId(productCode1).WithPC(productCode1).WithVersion("1.1"sv); + auto installedPackage2 = setup.MakeInstalled().WithId(productCode2).WithPC(productCode2).WithVersion("1.2"sv); - CompositeTestSetup setup; setup.Installed->SearchFunction = [&](const SearchRequest& request) { bool isSearchById = SearchRequestIncludes(request.Inclusions, PackageMatchField::Id, MatchType::Exact, packageId); @@ -1416,7 +1433,7 @@ TEST_CASE("CompositeSource_Pinning_MultipleInstalled", "[CompositeSource][PinFlo setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(packageId).WithVersion("2.0"sv), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithId(packageId).WithVersion("2.0"sv), Criteria()); return result; }; @@ -1520,7 +1537,7 @@ TEST_CASE("CompositeSource_CorrelateToInstalledContainsManifestData", "[Composit setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithPC("hello"), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithPC("hello"), Criteria()); return result; }; @@ -1551,7 +1568,7 @@ TEST_CASE("CompositeSource_Respects_FeatureFlag_ManifestMayContainAdditionalSyst setup.Available->SearchFunction = [&](const SearchRequest&) { SearchResult result; - result.Matches.emplace_back(MakeAvailable(setup.Available).WithId(id).WithPC(productCode1).HideSRS(), Criteria()); + result.Matches.emplace_back(setup.MakeAvailable().WithId(id).WithPC(productCode1).HideSRS(), Criteria()); return result; }; @@ -1577,3 +1594,224 @@ TEST_CASE("CompositeSource_Respects_FeatureFlag_ManifestMayContainAdditionalSyst REQUIRE(productCodeSearched); } } + +TEST_CASE("CompositeSource_SxS_TwoVersions_NoAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("1.0").WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("2.0").WithPC(productCode2), Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 2); +} + +TEST_CASE("CompositeSource_SxS_TwoVersions_DifferentAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage1 = setup.MakeAvailable().ToPackage(); + auto availablePackage2 = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("1.0").WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("2.0").WithPC(productCode2), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest& request) + { + SearchResult result; + + std::string productCode; + for (const auto& item : request.Inclusions) + { + if (item.Field == PackageMatchField::ProductCode) + { + productCode = item.Value; + break; + } + } + + if (productCode == productCode1) + { + result.Matches.emplace_back(availablePackage1, Criteria()); + } + else if (productCode == productCode1) + { + result.Matches.emplace_back(availablePackage2, Criteria()); + } + + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 2); +} + +TEST_CASE("CompositeSource_SxS_TwoVersions_SameAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version1).WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(availablePackage, Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 2); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} + +TEST_CASE("CompositeSource_SxS_ThreeVersions_SameAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string version3 = "3.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + std::string productCode3 = "PC3"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version1).WithPC(productCode1), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version3).WithPC(productCode3), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(availablePackage, Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 3); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version3; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} + +TEST_CASE("CompositeSource_SxS_TwoVersions_SameAvailable_Tracking", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeWithTrackingTestSetup setup; + auto installedPackage1 = setup.MakeInstalled().WithVersion(version1).WithPC(productCode1); + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->Everything.Matches.emplace_back(installedPackage1, Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + setup.Tracking->GetIndex().AddManifest(installedPackage1); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(availablePackage, Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 2); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} + +TEST_CASE("CompositeSource_SxS_Available_TwoVersions_SameAvailable", "[CompositeSource][SideBySide]") +{ + auto enableFeature = TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature::SideBySide); + + std::string version1 = "1.0"; + std::string version2 = "2.0"; + std::string productCode1 = "PC1"; + std::string productCode2 = "PC2"; + + CompositeTestSetup setup; + auto availablePackage = setup.MakeAvailable().ToPackage(); + + setup.Installed->SearchFunction = [&](const SearchRequest&) + { + SearchResult result; + result.Matches.emplace_back(setup.MakeInstalled().WithVersion(version1).WithPC(productCode1), Criteria()); + result.Matches.emplace_back(setup.MakeInstalled().WithVersion(version2).WithPC(productCode2), Criteria()); + return result; + }; + + setup.Available->Everything.Matches.emplace_back(availablePackage, Criteria()); + + SearchResult result = setup.Search(); + + REQUIRE(result.Matches.size() == 1); + auto package = result.Matches[0].Package; + REQUIRE(package); + auto installedPackage = package->GetInstalled(); + REQUIRE(installedPackage); + auto installedVersions = installedPackage->GetVersionKeys(); + REQUIRE(installedVersions.size() == 2); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version1; })); + REQUIRE(std::any_of(installedVersions.begin(), installedVersions.end(), [&](const PackageVersionKey& key) { return key.Version == version2; })); + auto availablePackages = package->GetAvailable(); + REQUIRE(availablePackages.size() == 1); + REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); +} diff --git a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp index 23efc240bf..1a5c1ec942 100644 --- a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp +++ b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp @@ -147,6 +147,11 @@ std::shared_ptr CreatePredefinedInstalledSource(Factory::Filter filter return factory->Create(details)->Open(progress); } +SQLiteIndex CreateMemoryIndex() +{ + return SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, SQLite::Version::Latest(), SQLiteIndex::CreateOptions::SupportPathless); +} + TEST_CASE("ARPHelper_GetARPForArchitecture", "[arphelper][list]") { auto systemArch = GetSystemArchitecture(); @@ -296,7 +301,7 @@ TEST_CASE("ARPHelper_PopulateIndexFromKey_Single", "[arphelper][list]") AddARPEntryToKey(root.get(), helper, entry); - auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + auto index = CreateMemoryIndex(); helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); auto result = index.Search({}); @@ -332,7 +337,7 @@ TEST_CASE("ARPHelper_PopulateIndexFromKey_SingleValid", "[arphelper][list]") { "Nothing" }, }); - auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + auto index = CreateMemoryIndex(); helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); auto result = index.Search({}); @@ -368,7 +373,7 @@ TEST_CASE("ARPHelper_PopulateIndexFromKey_Two", "[arphelper][list]") AddARPEntryToKey(root.get(), helper, entry1); AddARPEntryToKey(root.get(), helper, entry2); - auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + auto index = CreateMemoryIndex(); helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); REQUIRE(index.Search({}).Matches.size() == 2); diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp index 1364f49e4b..64bf580cc5 100644 --- a/src/AppInstallerCLITests/TestCommon.cpp +++ b/src/AppInstallerCLITests/TestCommon.cpp @@ -249,6 +249,23 @@ namespace TestCommon AppInstaller::Settings::SetUserSettingsOverride(nullptr); } + std::unique_ptr TestUserSettings::EnableExperimentalFeature(Settings::ExperimentalFeature::Feature feature, bool keepFileSettings) + { + std::unique_ptr result = std::make_unique(keepFileSettings); + + // Due to the template usage, this needs to be updated for any features that want to use it. + switch (feature) + { + case Settings::ExperimentalFeature::Feature::SideBySide: + result->Set(true); + break; + default: + THROW_HR(E_NOTIMPL); + } + + return result; + } + bool InstallCertFromSignedPackage(const std::filesystem::path& package) { auto [certContext, certStore] = AppInstaller::Msix::GetCertContextFromMsix(package); diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h index 0d3e699767..a6f844cf10 100644 --- a/src/AppInstallerCLITests/TestCommon.h +++ b/src/AppInstallerCLITests/TestCommon.h @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include +#include #include #define REQUIRE_THROWS_HR(_expr_, _hr_) REQUIRE_THROWS_MATCHES(_expr_, wil::ResultException, ::TestCommon::ResultExceptionHRMatcher(_hr_)) @@ -141,6 +143,8 @@ namespace TestCommon { m_settings[S].emplace(std::move(value)); } + + static std::unique_ptr EnableExperimentalFeature(AppInstaller::Settings::ExperimentalFeature::Feature feature, bool keepFileSettings = false); }; // Below cert installation/uninstallation methods require admin privilege, diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index 06050b6402..8999f1a24f 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -9,6 +9,15 @@ using namespace AppInstaller::Repository; namespace TestCommon { + namespace + { + size_t GetNextTestPackageId() + { + static std::atomic_size_t packageId(0); + return ++packageId; + } + } + TestPackageVersion::TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata, std::weak_ptr source) : VersionManifest(manifest), Metadata(std::move(installationMetadata)), Source(source) {} @@ -120,6 +129,7 @@ namespace TestCommon TestPackage::TestPackage(const std::vector& available, std::weak_ptr source, bool hideSystemReferenceStringsOnVersion) : Source(source) { + DefaultIsSameIdentity = GetNextTestPackageId(); for (const auto& manifest : available) { Versions.emplace_back(TestPackageVersion::Make(manifest, source, hideSystemReferenceStringsOnVersion)); @@ -129,6 +139,7 @@ namespace TestCommon TestPackage::TestPackage(const Manifest& installed, MetadataMap installationMetadata, std::weak_ptr source) : Source(source) { + DefaultIsSameIdentity = GetNextTestPackageId(); Versions.emplace_back(TestPackageVersion::Make(installed, std::move(installationMetadata), source)); } @@ -203,23 +214,14 @@ namespace TestCommon return IsSameOverride(this, other); } - const TestPackage* otherAvailable = PackageCast(other); + const TestPackage* otherPackage = PackageCast(other); - if (!otherAvailable || - Versions.size() != otherAvailable->Versions.size()) + if (otherPackage && DefaultIsSameIdentity == otherPackage->DefaultIsSameIdentity) { - return false; + return true; } - for (size_t i = 0; i < Versions.size(); ++i) - { - if (Versions[i].get() != otherAvailable->Versions[i].get()) - { - return false; - } - } - - return true; + return false; } const void* TestPackage::CastTo(IPackageType type) const diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h index f86789f0ab..27033c45bf 100644 --- a/src/AppInstallerCLITests/TestSource.h +++ b/src/AppInstallerCLITests/TestSource.h @@ -74,6 +74,7 @@ namespace TestCommon std::vector> Versions; std::weak_ptr Source; + size_t DefaultIsSameIdentity = 0; std::function IsSameOverride; }; diff --git a/src/AppInstallerCommonCore/Architecture.cpp b/src/AppInstallerCommonCore/Architecture.cpp index ffab223d45..6f8ec557f3 100644 --- a/src/AppInstallerCommonCore/Architecture.cpp +++ b/src/AppInstallerCommonCore/Architecture.cpp @@ -187,6 +187,25 @@ namespace AppInstaller::Utility return Architecture::Unknown; } + std::optional<::AppInstaller::Utility::Architecture> ConvertToArchitectureEnum(winrt::Windows::System::ProcessorArchitecture architecture) + { + switch (architecture) + { + case winrt::Windows::System::ProcessorArchitecture::X86: + return ::AppInstaller::Utility::Architecture::X86; + case winrt::Windows::System::ProcessorArchitecture::Arm: + return ::AppInstaller::Utility::Architecture::Arm; + case winrt::Windows::System::ProcessorArchitecture::X64: + return ::AppInstaller::Utility::Architecture::X64; + case winrt::Windows::System::ProcessorArchitecture::Neutral: + return ::AppInstaller::Utility::Architecture::Neutral; + case winrt::Windows::System::ProcessorArchitecture::Arm64: + return ::AppInstaller::Utility::Architecture::Arm64; + } + + return {}; + } + LocIndView ToString(Architecture architecture) { switch (architecture) diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index d531a36fe8..5b7845a042 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,6 +44,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Configuration03: return userSettings.Get(); + case ExperimentalFeature::Feature::SideBySide: + return userSettings.Get(); case ExperimentalFeature::Feature::Proxy: return userSettings.Get(); default: @@ -79,6 +81,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Configuration03: return ExperimentalFeature{ "Configuration Schema 0.3", "configuration03", "https://aka.ms/winget-settings", Feature::Configuration03 }; + case Feature::SideBySide: + return ExperimentalFeature{ "Side-by-side improvements", "sideBySide", "https://aka.ms/winget-settings", Feature::SideBySide }; case Feature::Proxy: return ExperimentalFeature{ "Proxy", "proxy", "https://aka.ms/winget-settings", Feature::Proxy }; default: diff --git a/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h b/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h index e75e0c7918..c2d9127035 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once - +#include #include #include +#include namespace AppInstaller::Utility { @@ -22,6 +23,9 @@ namespace AppInstaller::Utility // Converts a string to corresponding enum Architecture ConvertToArchitectureEnum(std::string_view archStr); + // Converts an ProcessorArchitecture to an Architecture + std::optional ConvertToArchitectureEnum(winrt::Windows::System::ProcessorArchitecture architecture); + // Converts an Architecture to a string_view LocIndView ToString(Architecture architecture); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index e2847cd0a4..00e47f419c 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -26,6 +26,7 @@ namespace AppInstaller::Settings Resume = 0x2, Configuration03 = 0x4, Proxy = 0x8, + SideBySide = 0x10, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h index f69be6d682..2039b81a5e 100644 --- a/src/AppInstallerCommonCore/Public/winget/Pin.h +++ b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -43,7 +43,7 @@ namespace AppInstaller::Pinning // but they break when the package is updated outside of winget. struct PinKey { - PinKey() {} + PinKey() = default; PinKey(const Manifest::Manifest::string_t& packageId, std::string_view sourceId) : PackageId(packageId), SourceId(sourceId) {} @@ -80,6 +80,12 @@ namespace AppInstaller::Pinning struct Pin { + Pin(const Pin&) = default; + Pin& operator=(const Pin& other) = default; + + Pin(Pin&&) = default; + Pin& operator=(Pin&&) = default; + static Pin CreateBlockingPin(PinKey&& pinKey); static Pin CreatePinningPin(PinKey&& pinKey); static Pin CreateGatingPin(PinKey&& pinKey, Utility::GatedVersion&& gatedVersion); diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index ba156f8cf8..fa8f1b412d 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -72,6 +72,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFResume, EFConfiguration03, + EFSideBySide, EFProxy, // Telemetry TelemetryDisable, @@ -152,6 +153,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration03, bool, bool, false, ".experimentalFeatures.configuration03"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFSideBySide, bool, bool, false, ".experimentalFeatures.sideBySide"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFProxy, bool, bool, false, ".experimentalFeatures.proxy"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 12d4d378aa..091ba83423 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -261,6 +261,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(EFConfiguration03) + WINGET_VALIDATE_PASS_THROUGH(EFSideBySide) WINGET_VALIDATE_PASS_THROUGH(EFProxy) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index 8ce367e9a9..11ae60ecff 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -100,14 +100,12 @@ namespace AppInstaller::Repository } } - // For a given package from a tracking catalog, get the latest write time and its corresponding package version. + // For a given package from a tracking catalog, get the latest write time. // Look at all versions rather than just the latest to account for the potential of downgrading. - std::pair> GetLatestTrackingWriteTimeAndPackageVersion( - const std::shared_ptr& trackingCompositePackage) + std::chrono::system_clock::time_point GetLatestTrackingWriteTime( + const std::shared_ptr& trackingPackage) { - std::shared_ptr trackingPackage = OnlyAvailable(trackingCompositePackage); - std::chrono::system_clock::time_point resultTime{}; - std::shared_ptr resultVersion; + std::chrono::system_clock::time_point result{}; for (const auto& key : trackingPackage->GetVersionKeys()) { @@ -127,16 +125,15 @@ namespace AppInstaller::Repository std::chrono::system_clock::time_point versionTime = Utility::ConvertUnixEpochToSystemClock(unixEpoch); - if (versionTime > resultTime) + if (versionTime > result) { - resultTime = versionTime; - resultVersion = version; + result = versionTime; } } } } - return { resultTime, std::move(resultVersion) }; + return result; } // An installed package's version reported in ARP does not necessarily match the versions used for the manifest. @@ -341,50 +338,78 @@ namespace AppInstaller::Repository }; // An IPackage for the installed package of a CompositePackage. - // Supports only a single version of a single package at this time. struct CompositeInstalledPackage : public IPackage { static constexpr IPackageType PackageType = IPackageType::CompositeInstalledPackage; - CompositeInstalledPackage(std::shared_ptr package, Source trackingSource, std::shared_ptr trackingPackageVersion, std::string overrideVersion = {}) : - m_package(std::move(package)), m_trackingSource(std::move(trackingSource)), m_trackingPackageVersion(std::move(trackingPackageVersion)), m_overrideVersion(std::move(overrideVersion)) - {} + CompositeInstalledPackage(std::shared_ptr package) + { + AddPackageAndVersionKeyData(std::move(package)); + } Utility::LocIndString GetProperty(PackageProperty property) const override { - return m_package->GetProperty(property); + THROW_HR_IF(E_UNEXPECTED, m_packages.empty() || m_versionKeyData.empty()); + + // Use the highest version for package properties + return m_packages[m_versionKeyData[0].PackageIndex]->GetProperty(property); } std::vector GetVersionKeys() const override { - auto result = m_package->GetVersionKeys(); - THROW_HR_IF(E_UNEXPECTED, result.size() != 1); - if (!m_overrideVersion.empty()) - { - result.front().Version = m_overrideVersion; - } - return result; + return { m_versionKeyData.begin(), m_versionKeyData.end() }; } - std::shared_ptr GetVersion(const PackageVersionKey& versionKey) const + std::shared_ptr GetVersion(const PackageVersionKey& versionKey) const override { - return (GetVersionKeys().front().IsMatch(versionKey)) ? GetLatestVersion() : std::shared_ptr{}; + std::shared_ptr installedVersion; + std::string overrideVersion; + + for (const VersionKeyData& key : m_versionKeyData) + { + if (key.IsMatch(versionKey)) + { + installedVersion = key.InstalledVersion; + overrideVersion = key.Version; + break; + } + } + + if (installedVersion) + { + // Get the appropriate tracking version or latest if it is not found. + // The tracking package uses the mapped version. + std::shared_ptr trackingPackageVersion; + if (m_trackingPackage) + { + // Remove our use of the package id as source + PackageVersionKey versionKey_NoSource = versionKey; + versionKey_NoSource.SourceId.clear(); + + trackingPackageVersion = m_trackingPackage->GetVersion(versionKey_NoSource); + + if (!trackingPackageVersion) + { + trackingPackageVersion = m_trackingPackage->GetLatestVersion(); + } + } + + return std::make_shared(std::move(installedVersion), m_trackingSource, std::move(trackingPackageVersion), std::move(overrideVersion)); + } + + return nullptr; } std::shared_ptr GetLatestVersion() const override { - return std::make_shared(m_package->GetLatestVersion(), m_trackingSource, m_trackingPackageVersion, m_overrideVersion); + return GetVersion({}); } Source GetSource() const override { // If there is a tracking source, use it instead to indicate that it came from there. - if (m_trackingSource) - { - return m_trackingSource; - } - - return m_package->GetSource(); + // Otherwise, all of the installed packages should be from the same source. + return m_trackingSource ? m_trackingSource : m_packages[0]->GetSource(); } bool IsSame(const IPackage* other) const override @@ -393,7 +418,31 @@ namespace AppInstaller::Repository if (otherPackage) { - return m_package->IsSame(otherPackage->m_package.get()); + if (m_packages.size() != otherPackage->m_packages.size()) + { + return false; + } + + for (const auto& subPackage : m_packages) + { + bool foundSame = false; + + for (const auto& otherSubPackage : otherPackage->m_packages) + { + if (subPackage->IsSame(otherSubPackage.get())) + { + foundSame = true; + break; + } + } + + if (!foundSame) + { + return false; + } + } + + return true; } return false; @@ -409,54 +458,194 @@ namespace AppInstaller::Repository return nullptr; } + void SetTracking( + Source trackingSource, + std::shared_ptr trackingPackage, + std::chrono::system_clock::time_point trackingWriteTime) + { + m_trackingSource = std::move(trackingSource); + m_trackingPackage = std::move(trackingPackage); + m_trackingWriteTime = trackingWriteTime; + } + + Source GetTrackingSource() const + { + return m_trackingSource; + } + + const std::shared_ptr& GetTrackingPackage() const + { + return m_trackingPackage; + } + + std::chrono::system_clock::time_point GetTrackingPackageWriteTime() const + { + return m_trackingWriteTime; + } + + bool ContainsInstalledPackage(const IPackage* installedPackage) const + { + for (const auto& package : m_packages) + { + if (package->IsSame(installedPackage)) + { + return true; + } + } + + return false; + } + + void FoldInstalledIn(const std::shared_ptr& other) + { + for (const auto& package : other->m_packages) + { + AddPackageAndVersionKeyData(package); + } + } + + // Set a version that will override the version string from the installed package + void SetOverrideInstalledVersion(const std::shared_ptr& availablePackage) + { + if (availablePackage) + { + m_availablePackageVersionOverride = availablePackage; + + for (auto& key : m_versionKeyData) + { + if (Manifest::DoesInstallerTypeSupportArpVersionRange(key.InstalledType)) + { + key.Version = GetMappedInstalledVersion(key.InstalledVersion->GetProperty(PackageVersionProperty::Version), availablePackage); + } + } + } + } + + bool IsEmpty() const + { + return m_versionKeyData.empty(); + } + private: - std::shared_ptr m_package; + // Contains information about all of the version keys. + // We use the `SourceId` field to store the installed package identifier so that we can disambiguate keys is they have the same version. + struct VersionKeyData : public PackageVersionKey + { + size_t PackageIndex; + std::shared_ptr InstalledVersion; + Manifest::InstallerTypeEnum InstalledType; + Utility::VersionAndChannel VersionAndChannel; + + bool operator<(const VersionKeyData& other) const + { + return VersionAndChannel < other.VersionAndChannel; + } + }; + + // Adds the package and version key data to the composite. + // The version keys are then sorted so that the first (index 0) in the vector has the highest version. + // Note that it may tied for highest version if, for instance, the same version is installed for different architectures. + void AddPackageAndVersionKeyData(std::shared_ptr package) + { + // We don't want this to happen, but it could. Rather than a crash, we will log it and move on. + if (!package) + { + AICLI_LOG(Repo, Verbose, << "AddPackageAndVersionKeyData called with an empty package"); + return; + } + + size_t packageIndex = m_packages.size(); + std::string packageIdentifier = package->GetProperty(PackageProperty::Id); + bool versionAdded = false; + + for (const auto& versionKey : package->GetVersionKeys()) + { + VersionKeyData keyData{ versionKey }; + + keyData.PackageIndex = packageIndex; + keyData.InstalledVersion = package->GetVersion(versionKey); + + if (!keyData.InstalledVersion) + { + AICLI_LOG(Repo, Verbose, << "AddPackageAndVersionKeyData: Package [" << packageIdentifier << "] did not return a version for [" << versionKey.Version << "]"); + continue; + } + + // We use the `SourceId` field to store the installed package identifier so that we can disambiguate keys if they have the same version. + keyData.SourceId = packageIdentifier; + + keyData.InstalledType = Manifest::ConvertToInstallerTypeEnum(keyData.InstalledVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); + if (m_availablePackageVersionOverride && Manifest::DoesInstallerTypeSupportArpVersionRange(keyData.InstalledType)) + { + keyData.Version = GetMappedInstalledVersion(keyData.InstalledVersion->GetProperty(PackageVersionProperty::Version), m_availablePackageVersionOverride); + } + + keyData.VersionAndChannel = Utility::VersionAndChannel{ keyData.Version, keyData.Channel }; + + m_versionKeyData.emplace_back(std::move(keyData)); + versionAdded = true; + } + + if (versionAdded) + { + m_packages.emplace_back(std::move(package)); + + std::sort(m_versionKeyData.begin(), m_versionKeyData.end()); + } + } + + std::vector> m_packages; + std::vector m_versionKeyData; Source m_trackingSource; - std::shared_ptr m_trackingPackageVersion; - std::string m_overrideVersion; + std::shared_ptr m_trackingPackage; + std::chrono::system_clock::time_point m_trackingWriteTime = std::chrono::system_clock::time_point::min(); + std::shared_ptr m_availablePackageVersionOverride; }; // An ICompositePackage for the CompositeSource. struct CompositePackage : public ICompositePackage { // The availablePackage may only contain one available package within it, as it is expected to be the output of a search on a single source. - CompositePackage(const std::shared_ptr& installedPackage, const std::shared_ptr& availablePackage = {}) + CompositePackage(const std::shared_ptr& installedPackage, const std::shared_ptr& availablePackage = {}, bool setPrimary = false) { if (installedPackage) { - m_installedPackage = installedPackage->GetInstalled(); + m_installedPackage = std::make_shared(installedPackage->GetInstalled()); + + // If the installed package result existed, but didn't actually create any installed versions, drop it. + if (m_installedPackage->IsEmpty()) + { + m_installedPackage.reset(); + } } - AddAvailablePackage(availablePackage); + AddAvailablePackage(availablePackage, setPrimary); } Utility::LocIndString GetProperty(PackageProperty property) const override { - std::shared_ptr truth; - if (m_defaultAvailablePackage) + IPackage* truth = nullptr; + if (m_primaryAvailablePackage) { - truth = m_defaultAvailablePackage; + truth = m_primaryAvailablePackage.get(); } - if (!truth) + if (!truth && !m_availablePackages.empty()) { - truth = m_trackingPackage; + truth = m_availablePackages[0].get(); } if (!truth) { - truth = m_installedPackage; + truth = m_installedPackage.get(); } + THROW_HR_IF(E_UNEXPECTED, !truth); + return truth->GetProperty(property); } std::shared_ptr GetInstalled() override { - if (m_installedPackage) - { - return std::make_shared(m_installedPackage, m_trackingSource, m_trackingPackageVersion, m_overrideInstalledVersion); - } - - return {}; + return m_installedPackage; } std::vector> GetAvailable() override @@ -464,6 +653,11 @@ namespace AppInstaller::Repository return m_availablePackages; } + const std::vector>& GetAvailablePackages() + { + return m_availablePackages; + } + bool IsSameAsAnyAvailable(const IPackage* other) const { if (other) @@ -480,69 +674,96 @@ namespace AppInstaller::Repository return false; } - std::shared_ptr GetInstalledPackage() const + const std::shared_ptr& GetInstalledPackage() const { return m_installedPackage; } - const std::shared_ptr& GetTrackingPackage() const + bool ContainsInstalledPackage(const IPackage* installedPackage) const { - return m_trackingPackage; + return m_installedPackage ? m_installedPackage->ContainsInstalledPackage(installedPackage) : false; } - void AddAvailablePackage(const std::shared_ptr& availablePackage) + void AddAvailablePackage(const std::shared_ptr& availablePackage, bool setPrimary = false) { if (availablePackage) { - std::shared_ptr singlePackage = OnlyAvailable(availablePackage); + // Disable primary if feature not enabled + setPrimary = setPrimary && ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide); + + m_availablePackages.emplace_back(OnlyAvailable(availablePackage)); - if (!m_defaultAvailablePackage) + if (setPrimary) { - // Set override only with the first available version found - m_defaultAvailablePackage = singlePackage; - TrySetOverrideInstalledVersion(m_defaultAvailablePackage); + m_primaryAvailablePackage = m_availablePackages.back(); } - m_availablePackages.emplace_back(std::move(singlePackage)); + // Set override for primary or with the first available version found + if (setPrimary || m_availablePackages.size() == 1) + { + TrySetOverrideInstalledVersion(m_availablePackages.back()); + } } } - void SetTracking(Source trackingSource, std::shared_ptr trackingPackage, std::shared_ptr trackingPackageVersion) + std::shared_ptr& GetPrimaryAvailablePackage() { - m_trackingSource = std::move(trackingSource); - m_trackingPackage = std::move(trackingPackage); - m_trackingPackageVersion = std::move(trackingPackageVersion); + return m_primaryAvailablePackage; } - const Source& GetTrackingSource() const + Source GetTrackingSource() const { - return m_trackingSource; + return m_installedPackage ? m_installedPackage->GetTrackingSource() : Source{}; + } + + std::shared_ptr GetTrackingPackage() const + { + return m_installedPackage ? m_installedPackage->GetTrackingPackage() : std::shared_ptr{}; + } + + std::chrono::system_clock::time_point GetTrackingPackageWriteTime() const + { + return m_installedPackage ? m_installedPackage->GetTrackingPackageWriteTime() : std::chrono::system_clock::time_point::min(); + } + + void SetTracking( + Source trackingSource, + std::shared_ptr trackingPackage, + std::chrono::system_clock::time_point trackingWriteTime) + { + if (m_installedPackage) + { + m_installedPackage->SetTracking(std::move(trackingSource), std::move(trackingPackage), trackingWriteTime); + } + } + + void FoldInstalledIn(const std::shared_ptr& other) + { + if (other->m_installedPackage) + { + if (m_installedPackage) + { + m_installedPackage->FoldInstalledIn(other->m_installedPackage); + } + else + { + m_installedPackage = other->m_installedPackage; + } + } } private: // Try to set a version that will override the version string from the installed package - void TrySetOverrideInstalledVersion(std::shared_ptr availablePackage) + void TrySetOverrideInstalledVersion(const std::shared_ptr& availablePackage) { if (m_installedPackage && availablePackage) { - auto installedVersion = m_installedPackage->GetLatestVersion(); - if (installedVersion) - { - auto installedType = Manifest::ConvertToInstallerTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::InstalledType]); - if (Manifest::DoesInstallerTypeSupportArpVersionRange(installedType)) - { - m_overrideInstalledVersion = GetMappedInstalledVersion(installedVersion->GetProperty(PackageVersionProperty::Version), availablePackage); - } - } + m_installedPackage->SetOverrideInstalledVersion(availablePackage); } } - std::shared_ptr m_installedPackage; - Source m_trackingSource; - std::shared_ptr m_trackingPackage; - std::shared_ptr m_trackingPackageVersion; - std::string m_overrideInstalledVersion; - std::shared_ptr m_defaultAvailablePackage; + std::shared_ptr m_installedPackage; + std::shared_ptr m_primaryAvailablePackage; std::vector> m_availablePackages; }; @@ -658,13 +879,122 @@ namespace AppInstaller::Repository result.Purpose = searchPurpose; return result; } + + std::shared_ptr AddSystemReferenceStringsFromTrackingPackage(const PackageTrackingCatalog& trackingCatalog, const Utility::LocIndString& identifier, std::string_view sourceIdentifier) + { + SearchRequest trackingRequest; + trackingRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, identifier.get()); + + SearchResult trackingResult = trackingCatalog.Search(trackingRequest); + + if (trackingResult.Matches.size() == 1) + { + std::shared_ptr result = OnlyAvailable(trackingResult.Matches[0].Package); + AddSystemReferenceStrings(result.get()); + return result; + } + else + { + AICLI_LOG(Repo, Warning, << "Found multiple results for Id [" << identifier << "] in tracking catalog for: " << sourceIdentifier); + return {}; + } + } + + void AddSystemReferenceStrings(IPackage* package) + { + for (auto const& versionKey : package->GetVersionKeys()) + { + auto version = package->GetVersion(versionKey); + AddSystemReferenceStrings(version.get()); + } + } + + void AddSystemReferenceStrings(IPackageVersion* version) + { + GetSystemReferenceStrings( + version, + PackageVersionMultiProperty::PackageFamilyName, + PackageMatchField::PackageFamilyName); + + GetSystemReferenceStrings( + version, + PackageVersionMultiProperty::ProductCode, + PackageMatchField::ProductCode); + + GetSystemReferenceStrings( + version, + PackageVersionMultiProperty::UpgradeCode, + PackageMatchField::UpgradeCode); + + GetNameAndPublisher( + version); + } + + void AddSystemReferenceStringsFromManifest(const Manifest::Manifest& manifest) + { + for (const auto& pfn : manifest.GetPackageFamilyNames()) + { + AddIfNotPresent(SystemReferenceString{ PackageMatchField::PackageFamilyName, Utility::LocIndString{ pfn } }); + } + for (const auto& productCode : manifest.GetProductCodes()) + { + AddIfNotPresent(SystemReferenceString{ PackageMatchField::ProductCode, Utility::LocIndString{ productCode } }); + } + for (const auto& upgradeCode : manifest.GetUpgradeCodes()) + { + AddIfNotPresent(SystemReferenceString{ PackageMatchField::UpgradeCode, Utility::LocIndString{ upgradeCode } }); + } + for (const auto& name : manifest.GetPackageNames()) + { + for (const auto& publisher : manifest.GetPublishers()) + { + AddIfNotPresent(SystemReferenceString{ + PackageMatchField::NormalizedNameAndPublisher, + Utility::LocIndString{ name }, + Utility::LocIndString{ publisher } }); + } + } + } + + private: + void GetSystemReferenceStrings( + IPackageVersion* installedVersion, + PackageVersionMultiProperty prop, + PackageMatchField field) + { + for (auto&& string : installedVersion->GetMultiProperty(prop)) + { + AddIfNotPresent(SystemReferenceString{ field, std::move(string) }); + } + } + + void GetNameAndPublisher( + IPackageVersion* installedVersion) + { + // Unfortunately the names and publishers are unique and not tied to each other strictly, so we need + // to go broad on the matches. Future work can hopefully make name and publisher operate more as a unit, + // but for now we have to search for the cartesian of these... + auto names = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Name); + auto publishers = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Publisher); + + for (size_t i = 0; i < names.size(); ++i) + { + for (size_t j = 0; j < publishers.size(); ++j) + { + AddIfNotPresent(SystemReferenceString{ + PackageMatchField::NormalizedNameAndPublisher, + names[i], + publishers[j] }); + } + } + } }; - // For a given package version, prepares the results for it. - PackageData GetSystemReferenceStrings(IPackageVersion* version) + // For a given package, prepares the results for it. + PackageData GetSystemReferenceStrings(IPackage* package) { PackageData result; - AddSystemReferenceStrings(version, result); + result.AddSystemReferenceStrings(package); return result; } @@ -695,66 +1025,51 @@ namespace AppInstaller::Repository for (auto const& versionKey : availablePackage->GetVersionKeys()) { auto packageVersion = availablePackage->GetVersion(versionKey); - AddSystemReferenceStrings(packageVersion.get(), result); + result.AddSystemReferenceStrings(packageVersion.get()); if (downloadManifests && manifestsDownloaded < c_downloadManifestsLimit) { auto manifest = packageVersion->GetManifest(); - AddSystemReferenceStringsFromManifest(manifest, result); + result.AddSystemReferenceStringsFromManifest(manifest); manifestsDownloaded++; } } return result; } - // Check for a package already in the result that should have been correlated already. - // If we find one, see if we should upgrade it's match criteria. - // If we don't, return package data for further use. - std::optional CheckForExistingResultFromTrackingPackageMatch(const ResultMatch& trackingMatch) + // Determines if the results contain the given installed package. + bool ContainsInstalledPackage(const IPackage* installedPackage) const { - std::shared_ptr trackingMatchPackage = OnlyAvailable(trackingMatch.Package); - for (auto& match : Matches) { - const std::shared_ptr& trackingPackage = match.Package->GetTrackingPackage(); - if (trackingPackage && trackingPackage->IsSame(trackingMatchPackage.get())) + if (match.Package->ContainsInstalledPackage(installedPackage)) { - if (ResultMatchComparator{}(trackingMatch, match)) - { - match.MatchCriteria = trackingMatch.MatchCriteria; - } - - return {}; + return true; } } - PackageData result; - for (auto const& versionKey : trackingMatchPackage->GetVersionKeys()) - { - auto packageVersion = trackingMatchPackage->GetVersion(versionKey); - AddSystemReferenceStrings(packageVersion.get(), result); - } - return result; + return false; } // Determines if the results contain the given installed package. - bool ContainsInstalledPackage(const IPackage* installedPackage) + std::shared_ptr FindInstalledPackage(const IPackage* installedPackage) const { for (auto& match : Matches) { - const std::shared_ptr& matchPackage = match.Package->GetInstalledPackage(); - if (matchPackage && matchPackage->IsSame(installedPackage)) + if (match.Package->ContainsInstalledPackage(installedPackage)) { - return true; + return match.Package; } } - return false; + return {}; } - // Destructively converts the result to the standard variant. - operator SearchResult() && + // *Destructively* converts the result to the standard variant. + SearchResult ConvertToSearchResult() { + FoldResults(); + SearchResult result; result.Matches.reserve(Matches.size()); @@ -812,95 +1127,218 @@ namespace AppInstaller::Repository return result; } - std::vector Matches; - bool Truncated = false; - std::vector Failures; - - private: - void AddSystemReferenceStrings(IPackageVersion* version, PackageData& data) + // Group results in an attempt to have a single result that covers all installed versions. + // This is expected to be called immediately after the installed search portion, + // when each result will contain a single installed version and some number of available packages. + // + // The folds that happen are: + // 1. When results have the same primary available package (the primary available package is set due to tracking data) + // 2. When a result has no primary available package, but another result does have a primary that matches one of the available + // a. Choose the latest primary if there are multiple + // 3. When multiple results have no primary available package and share the same available package set + // a. There are many potential additional rules that could be made here, but we will start with the simplest version. + // + // Potential improvements: + // 1. Attempting correlation of non-primary available packages to allow folding in more complex cases + // a. For example, if installed A has {source1:package1, source2:package2} and installed B has {source1:package1}, can we + // make sure that source1:package1 and source2:package2 are in fact "the same" to confidently say that installed A and B + // are side by side versions. + // 2. Attempt correlation by installed data only + // a. We can potentially detect multiple instances of the same installed item with the same correlation logic turned back on + // the installed source. This would allow for folding even when the package is not in any available source. + void FoldResults() { - GetSystemReferenceStrings( - version, - PackageVersionMultiProperty::PackageFamilyName, - PackageMatchField::PackageFamilyName, - data); + if (!ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) + { + return; + } - GetSystemReferenceStrings( - version, - PackageVersionMultiProperty::ProductCode, - PackageMatchField::ProductCode, - data); + // The key to uniquely identify the package in the map + struct InstalledResultFoldKey + { + InstalledResultFoldKey() = default; - GetSystemReferenceStrings( - version, - PackageVersionMultiProperty::UpgradeCode, - PackageMatchField::UpgradeCode, - data); + InstalledResultFoldKey(const std::shared_ptr& package) + { + std::shared_ptr latestAvailable = package->GetLatestVersion(); + if (latestAvailable) + { + SourceIdentifier = latestAvailable->GetSource().GetIdentifier(); + PackageIdentifier = latestAvailable->GetProperty(PackageVersionProperty::Id); + } + } - GetNameAndPublisher( - version, - data); - } + // Hash operation + size_t operator()(const InstalledResultFoldKey& value) const noexcept + { + std::hash hashString; + return hashString(value.SourceIdentifier) ^ (hashString(value.PackageIdentifier) << 1); + } - void AddSystemReferenceStringsFromManifest(const Manifest::Manifest& manifest, PackageData& data) - { - for (const auto& pfn : manifest.GetPackageFamilyNames()) - { - data.AddIfNotPresent(SystemReferenceString{ PackageMatchField::PackageFamilyName, Utility::LocIndString{ pfn } }); - } - for (const auto& productCode : manifest.GetProductCodes()) - { - data.AddIfNotPresent(SystemReferenceString{ PackageMatchField::ProductCode, Utility::LocIndString{ productCode } }); - } - for (const auto& upgradeCode : manifest.GetUpgradeCodes()) + bool operator==(const InstalledResultFoldKey& other) const noexcept + { + // Treat both empty as invalid and never equal + if (SourceIdentifier.empty() && PackageIdentifier.empty()) + { + return false; + } + + return SourceIdentifier == other.SourceIdentifier && PackageIdentifier == other.PackageIdentifier; + } + + std::string SourceIdentifier; + std::string PackageIdentifier; + }; + + // The data for a package in the map + struct InstalledResultFoldData { - data.AddIfNotPresent(SystemReferenceString{ PackageMatchField::UpgradeCode, Utility::LocIndString{ upgradeCode } }); - } - for (const auto& name : manifest.GetPackageNames()) + InstalledResultFoldData() = default; + explicit InstalledResultFoldData(size_t primaryPackageIndex) : PrimaryPackageIndex(primaryPackageIndex) {} + + std::optional PrimaryPackageIndex; + std::vector NonPrimaryPackageIndices; + }; + + std::unordered_map foldData; + + // Attempt to fold all primary package matches first. + // Packages without primaries will still be indexed into the hash table. + for (size_t i = 0; i < Matches.size(); ++i) { - for (const auto& publisher : manifest.GetPublishers()) + CompositeResultMatch& currentMatch = Matches[i]; + + // Check current match for fold target + if (currentMatch.Package->GetPrimaryAvailablePackage()) { - data.AddIfNotPresent(SystemReferenceString{ - PackageMatchField::NormalizedNameAndPublisher, - Utility::LocIndString{ name }, - Utility::LocIndString{ publisher } }); + InstalledResultFoldKey key{ currentMatch.Package->GetPrimaryAvailablePackage() }; + + auto itr = foldData.find(key); + if (itr != foldData.end()) + { + if (itr->second.PrimaryPackageIndex) + { + Matches[itr->second.PrimaryPackageIndex.value()].Package->FoldInstalledIn(currentMatch.Package); + currentMatch.Package.reset(); + } + else + { + itr->second.PrimaryPackageIndex = i; + } + } + else + { + foldData[key] = InstalledResultFoldData{ i }; + } + } + else + { + for (const auto& availablePackage : currentMatch.Package->GetAvailablePackages()) + { + InstalledResultFoldKey key{ availablePackage }; + + auto itr = foldData.find(key); + if (itr == foldData.end()) + { + itr = foldData.insert({ key, {} }).first; + } + + itr->second.NonPrimaryPackageIndices.emplace_back(i); + } } } - } - void GetSystemReferenceStrings( - IPackageVersion* installedVersion, - PackageVersionMultiProperty prop, - PackageMatchField field, - PackageData& data) - { - for (auto&& string : installedVersion->GetMultiProperty(prop)) + // After primary matches are folded, attempt to fold results without primary matches. + // The latest primary match will be preferred. + for (size_t i = 0; i < Matches.size(); ++i) { - data.AddIfNotPresent(SystemReferenceString{ field, std::move(string) }); - } - } + CompositeResultMatch& currentMatch = Matches[i]; - void GetNameAndPublisher( - IPackageVersion* installedVersion, - PackageData& data) - { - // Unfortunately the names and publishers are unique and not tied to each other strictly, so we need - // to go broad on the matches. Future work can hopefully make name and publisher operate more as a unit, - // but for now we have to search for the cartesian of these... - auto names = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Name); - auto publishers = installedVersion->GetMultiProperty(PackageVersionMultiProperty::Publisher); + // Skip any matches that we have already folded + if (!currentMatch.Package) + { + continue; + } - for (size_t i = 0; i < names.size(); ++i) - { - for (size_t j = 0; j < publishers.size(); ++j) + if (!currentMatch.Package->GetPrimaryAvailablePackage()) { - data.AddIfNotPresent(SystemReferenceString{ - PackageMatchField::NormalizedNameAndPublisher, - names[i], - publishers[j] }); + InstalledResultFoldData* latestPrimaryAvailable = nullptr; + std::vector availableFoldData; + + for (const auto& availablePackage : currentMatch.Package->GetAvailablePackages()) + { + auto& packageFoldData = foldData.at(availablePackage); + + if (packageFoldData.PrimaryPackageIndex) + { + if (!latestPrimaryAvailable || + Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package->GetTrackingPackageWriteTime() < Matches[packageFoldData.PrimaryPackageIndex.value()].Package->GetTrackingPackageWriteTime()) + { + latestPrimaryAvailable = &packageFoldData; + } + } + else + { + availableFoldData.emplace_back(&packageFoldData); + } + } + + if (latestPrimaryAvailable) + { + Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package->FoldInstalledIn(currentMatch.Package); + currentMatch.Package.reset(); + + // If the result with the primary is later, move it forward + if (latestPrimaryAvailable->PrimaryPackageIndex.value() > i) + { + currentMatch.Package = std::move(Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package); + Matches[latestPrimaryAvailable->PrimaryPackageIndex.value()].Package.reset(); + } + continue; + } + + // First, find the intersection of all results that contain all of the packages from this result. + std::vector candidateMatches; + for (size_t j = 0; j < availableFoldData.size(); ++j) + { + InstalledResultFoldData* packageFoldData = availableFoldData[j]; + + if (j == 0) + { + candidateMatches = packageFoldData->NonPrimaryPackageIndices; + } + else + { + std::vector temp; + std::set_intersection( + candidateMatches.begin(), candidateMatches.end(), + packageFoldData->NonPrimaryPackageIndices.begin(), packageFoldData->NonPrimaryPackageIndices.end(), + std::back_inserter(temp)); + candidateMatches = std::move(temp); + } + } + + // Now exclude both our own result and any that have a different (larger) number of available packages + candidateMatches.erase(std::remove_if(candidateMatches.begin(), candidateMatches.end(), + [&](size_t index) { return index == i || Matches[index].Package->GetAvailablePackages().size() != currentMatch.Package->GetAvailablePackages().size(); }), + candidateMatches.end()); + + // All of these remaining values should be folded in to our result + for (size_t foldTarget : candidateMatches) + { + currentMatch.Package->FoldInstalledIn(Matches[foldTarget].Package); + Matches[foldTarget].Package.reset(); + } } } + + // Get rid of the folded results; we reset the Package to indicate that it is no longer valid + Matches.erase(std::remove_if(Matches.begin(), Matches.end(), [&](const CompositeResultMatch& match) { return !match.Package; }), Matches.end()); } + + std::vector Matches; + bool Truncated = false; + std::vector Failures; }; std::shared_ptr GetTrackedPackageFromAvailableSource(CompositeResult& result, const Source& source, const Utility::LocIndString& identifier) @@ -1030,17 +1468,11 @@ namespace AppInstaller::Repository } std::shared_ptr compositePackage = std::make_shared(match.Package); - std::shared_ptr installedVersion; - auto installedPackage = compositePackage->GetInstalled(); - if (installedPackage) - { - installedVersion = installedPackage->GetLatestVersion(); - } - if (!installedVersion) + if (!installedPackage) { - // One would think that the installed version coming directly from our own installed source + // One would think that the installed package coming directly from our own installed source // would never be null, but it is sometimes. Rather than making users suffer through crashes // that break their entire experience, lets log a few things and then ignore this match. AICLI_LOG(Repo, Warning, << "CompositeSource: The installed version of the package '" << @@ -1048,98 +1480,86 @@ namespace AppInstaller::Repository continue; } - auto installedPackageData = result.GetSystemReferenceStrings(installedVersion.get()); + auto installedPackageData = result.GetSystemReferenceStrings(installedPackage.get()); // Create a search request to run against all available sources if (!installedPackageData.SystemReferenceStrings.empty()) { SearchRequest systemReferenceSearch = installedPackageData.CreateInclusionsSearchRequest(SearchPurpose::CorrelationToAvailable); - AICLI_LOG(Repo, Info, << "Finding available package from installed package using system reference search: " << systemReferenceSearch.ToString()); - - Source trackedSource; - std::shared_ptr trackingPackage; - std::shared_ptr trackingPackageVersion; - std::chrono::system_clock::time_point trackingPackageTime; - bool foundAvailablePackageFromTracking = false; + AICLI_LOG(Repo, Verbose, << "Finding available package from installed package using system reference search: " << systemReferenceSearch.ToString()); - // Check the tracking catalog first to see if there is a correlation there. - // TODO: When the issue with support for multiple available packages is fixed, this should move into - // the below available sources loop as we will check all sources at that point. + // Search sources and add to result for (const auto& source : m_availableSources) { + AICLI_LOG(Repo, Verbose, << " ... searching source: " << source.GetDetails().Name << " [" << source.GetIdentifier() << ']'); + + // Find the tracking result with the latest timestamp. auto trackingCatalog = source.GetTrackingCatalog(); SearchResult trackingResult = trackingCatalog.Search(systemReferenceSearch); - std::shared_ptr candidatePackage = GetMatchingPackage(trackingResult.Matches, - [&]() { - AICLI_LOG(Repo, Info, - << "Found multiple matches for installed package [" << installedVersion->GetProperty(PackageVersionProperty::Id) << - "] in tracking catalog for source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate tracking package could not be determined"); - }); + std::shared_ptr trackingPackage; + std::chrono::system_clock::time_point trackingPackageTime; + bool trackingSet = false; - // Determine the candidate package with the latest install time - if (candidatePackage) + for (const auto& trackingMatch : trackingResult.Matches) { - auto [candidateTime, candidateVersion] = GetLatestTrackingWriteTimeAndPackageVersion(candidatePackage); + auto candidateTime = GetLatestTrackingWriteTime(OnlyAvailable(trackingMatch.Package)); if (!trackingPackage || candidateTime > trackingPackageTime) { - trackedSource = source; - trackingPackage = OnlyAvailable(candidatePackage); - trackingPackageVersion = std::move(candidateVersion); + trackingPackage = OnlyAvailable(trackingMatch.Package); trackingPackageTime = candidateTime; } } - } - - // Directly search for the available package from tracking information. - if (trackingPackage) - { - auto availablePackage = GetTrackedPackageFromAvailableSource(result, trackedSource, trackingPackage->GetProperty(PackageProperty::Id)); - if (availablePackage) - { - compositePackage->AddAvailablePackage(std::move(availablePackage)); - foundAvailablePackageFromTracking = true; - } - compositePackage->SetTracking(std::move(trackedSource), std::move(trackingPackage), std::move(trackingPackageVersion)); - } - - // Search sources and add to result - for (const auto& source : m_availableSources) - { - // Do not attempt to correlate local packages against this source - if (!source.GetDetails().SupportInstalledSearchCorrelation) - { - continue; - } - // Skip the source that the tracking correlation result came from if we found one - if (foundAvailablePackageFromTracking && compositePackage->GetTrackingSource() == source) + if (trackingPackage && trackingPackageTime > compositePackage->GetTrackingPackageWriteTime()) { - continue; + AICLI_LOG(Repo, Verbose, << " ... setting latest tracking package to: " << trackingPackage->GetProperty(PackageProperty::Id)); + compositePackage->SetTracking(source, trackingPackage, trackingPackageTime); + trackingSet = true; } - SearchResult availableResult = result.SearchAndHandleFailures(source, systemReferenceSearch); - - if (availableResult.Matches.empty()) + // Attempt to correlate local packages against this source if supported. + SearchResult availableResult; + if (source.GetDetails().SupportInstalledSearchCorrelation) { - continue; + availableResult = result.SearchAndHandleFailures(source, systemReferenceSearch); } - // We will keep matching packages found from all sources, but generally we will use only the first one. auto availablePackage = GetMatchingPackage(availableResult.Matches, [&]() { AICLI_LOG(Repo, Info, - << "Found multiple matches for installed package [" << installedVersion->GetProperty(PackageVersionProperty::Id) << + << "Found multiple matches for installed package [" << installedPackage->GetProperty(PackageProperty::Id) << "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); }, [&] { AICLI_LOG(Repo, Warning, << " Appropriate available package could not be determined"); }); - // For non pinning cases. We found some matching packages here, don't keep going. - compositePackage->AddAvailablePackage(availablePackage); + if (trackingPackage) + { + auto trackingIdentifier = trackingPackage->GetProperty(PackageProperty::Id); + + // We always want to take the available search result if it exists as the package may have been updated. + if (availablePackage) + { + auto availableIdentifier = availablePackage->GetProperty(PackageProperty::Id); + if (!Utility::ICUCaseInsensitiveEquals(availableIdentifier, trackingIdentifier)) + { + AICLI_LOG(Repo, Verbose, << " ... overriding tracking package (" << trackingIdentifier << ") with available package (" << availableIdentifier << ")"); + } + } + else + { + AICLI_LOG(Repo, Verbose, << " ... using tracking package: " << trackingIdentifier); + availablePackage = GetTrackedPackageFromAvailableSource(result, source, trackingIdentifier); + } + } + + if (availablePackage) + { + AICLI_LOG(Repo, Verbose, << " ... adding available package: " << availablePackage->GetProperty(PackageProperty::Id)); + compositePackage->AddAvailablePackage(availablePackage, trackingSet); + } } } @@ -1150,68 +1570,21 @@ namespace AppInstaller::Repository // Optimization for the "everything installed" case, no need to allow for reverse correlations if (request.IsForEverything() && m_searchBehavior == CompositeSearchBehavior::Installed) { - return std::move(result); + return result.ConvertToSearchResult(); } } // Search available sources for (const auto& source : m_availableSources) { - // Search the tracking catalog as it can potentially get better correlations auto trackingCatalog = source.GetTrackingCatalog(); - SearchResult trackingResult = trackingCatalog.Search(request); - - for (auto&& match : trackingResult.Matches) - { - // Check for a package already in the result that should have been correlated already. - auto packageData = result.CheckForExistingResultFromTrackingPackageMatch(match); - - // If found existing package in the result, continue - if (!packageData) - { - continue; - } - - // If no package was found that was already in the results, do a correlation lookup with the installed - // source to create a new composite package entry if we find any packages there. - if (packageData && !packageData->SystemReferenceStrings.empty()) - { - SearchRequest systemReferenceSearch = packageData->CreateInclusionsSearchRequest(SearchPurpose::CorrelationToInstalled); - - AICLI_LOG(Repo, Info, << "Finding installed package from tracking package using system reference search: " << systemReferenceSearch.ToString()); - // Correlate against installed (allow exceptions out as we own the installed source) - SearchResult installedCrossRef = m_installedSource.Search(systemReferenceSearch); - - auto installedPackage = GetMatchingPackage(installedCrossRef.Matches, - [&]() { - AICLI_LOG(Repo, Info, - << "Found multiple matches for tracking package [" << match.Package->GetProperty(PackageProperty::Id) << - "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate installed package could not be determined"); - }); - - if (installedPackage && !result.ContainsInstalledPackage(installedPackage->GetInstalled().get())) - { - auto compositePackage = std::make_shared( - installedPackage, - GetTrackedPackageFromAvailableSource(result, source, match.Package->GetProperty(PackageProperty::Id))); - - auto [writeTime, trackingPackageVersion] = GetLatestTrackingWriteTimeAndPackageVersion(match.Package); - - compositePackage->SetTracking(source, OnlyAvailable(match.Package), std::move(trackingPackageVersion)); - - result.Matches.emplace_back(std::move(compositePackage), match.MatchCriteria); - } - } - } SearchResult availableResult = result.SearchAndHandleFailures(source, request); bool downloadManifests = source.QueryFeatureFlag(SourceFeatureFlag::ManifestMayContainAdditionalSystemReferenceStrings); for (auto&& match : availableResult.Matches) { - // Check for a package already in the result that should have been correlated already. + // Check for the package already in the result. // In cases that PackageData will be created, also download manifests for system reference strings // when search result is small (currently limiting to 1). auto packageData = result.CheckForExistingResultFromAvailablePackageMatch(match, downloadManifests && availableResult.Matches.size() == 1); @@ -1222,32 +1595,127 @@ namespace AppInstaller::Repository continue; } + // Use data from the tracking catalog as it can potentially get better correlations + auto trackingPackage = packageData->AddSystemReferenceStringsFromTrackingPackage(trackingCatalog, match.Package->GetProperty(PackageProperty::Id), source.GetDetails().Name); + // If no package was found that was already in the results, do a correlation lookup with the installed // source to create a new composite package entry if we find any packages there. bool foundInstalledMatch = false; - if (packageData && !packageData->SystemReferenceStrings.empty()) + if (!packageData->SystemReferenceStrings.empty()) { // Create a search request to run against the installed source SearchRequest systemReferenceSearch = packageData->CreateInclusionsSearchRequest(SearchPurpose::CorrelationToInstalled); - AICLI_LOG(Repo, Info, << "Finding installed package from available package using system reference search: " << systemReferenceSearch.ToString()); + AICLI_LOG(Repo, Verbose, << "Finding installed package from available package using system reference search: " << systemReferenceSearch.ToString()); // Correlate against installed (allow exceptions out as we own the installed source) SearchResult installedCrossRef = m_installedSource.Search(systemReferenceSearch); - auto installedPackage = GetMatchingPackage(installedCrossRef.Matches, - [&]() { - AICLI_LOG(Repo, Info, + if (ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SideBySide)) + { + for (const auto& installedMatch : installedCrossRef.Matches) + { + if (!IsStrongMatchField(installedMatch.MatchCriteria.Field)) + { + // For weak correlations, do an installed -> available check to ensure that there are no other strong correlations. + SearchResult correlationConfirmation; + if (source.GetDetails().SupportInstalledSearchCorrelation) + { + correlationConfirmation = result.SearchAndHandleFailures(source, result.GetSystemReferenceStrings(installedMatch.Package->GetInstalled().get()).CreateInclusionsSearchRequest(SearchPurpose::CorrelationToAvailable)); + } + + if (correlationConfirmation.Matches.empty()) + { + // We probably made the correlation due to tracking data, keep it. + } + else if (correlationConfirmation.Matches.size() > 1) + { + // There is contention for the correlation. + AICLI_LOG(Repo, Verbose, << " ... installed package [" << installedMatch.Package->GetProperty(PackageProperty::Id) << + "] had multiple correlations and is being ignored as a match for [" << match.Package->GetProperty(PackageProperty::Id) << "]"); + continue; + } + else if (!OnlyAvailable(correlationConfirmation.Matches[0].Package)->IsSame(OnlyAvailable(match.Package).get())) + { + // The only correlation is not to the current package. + AICLI_LOG(Repo, Verbose, << " ... installed package [" << installedMatch.Package->GetProperty(PackageProperty::Id) << + "] was found through available package [" << match.Package->GetProperty(PackageProperty::Id) << "], but only correlated to [" << + correlationConfirmation.Matches[0].Package->GetProperty(PackageProperty::Id) << "] and is being ignored"); + continue; + } + } + + // Now that we know we need to add this available package, determine how exactly + std::shared_ptr resultPackage = result.FindInstalledPackage(installedMatch.Package->GetInstalled().get()); + + if (resultPackage) + { + // Check for a package from the same source already present on the result package. + bool foundSameSource = false; + + for (const auto& availablePackage : resultPackage->GetAvailablePackages()) + { + if (availablePackage->GetSource() == source) + { + // TODO: May need to add more data so that we can choose the proper correlation, but it may also be very difficult to get through + // the gauntlet of other checks and arrive in this situation. + AICLI_LOG(Repo, Verbose, << " ... found [" << availablePackage->GetProperty(PackageProperty::Id) << + "] already correlated to [" << installedMatch.Package->GetProperty(PackageProperty::Id) << "] from the same source [" << + source.GetDetails().Name << "] as [" << match.Package->GetProperty(PackageProperty::Id) << "]; ignoring the second correlation"); + foundSameSource = true; + } + } + + if (foundSameSource) + { + continue; + } + } + else + { + result.Matches.emplace_back(std::make_shared(installedMatch.Package), match.MatchCriteria); + resultPackage = result.Matches.back().Package; + } + + bool setPrimary = false; + if (trackingPackage) + { + auto trackingPackageTime = GetLatestTrackingWriteTime(trackingPackage); + + if (trackingPackageTime > resultPackage->GetTrackingPackageWriteTime()) + { + resultPackage->SetTracking(source, std::move(trackingPackage), trackingPackageTime); + setPrimary = true; + } + } + + resultPackage->AddAvailablePackage(std::move(match.Package), setPrimary); + + foundInstalledMatch = true; + } + } + else + { + auto installedPackage = GetMatchingPackage(installedCrossRef.Matches, + [&]() { + AICLI_LOG(Repo, Info, << "Found multiple matches for available package [" << match.Package->GetProperty(PackageProperty::Id) << "] in source [" << source.GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); - }, [&] { - AICLI_LOG(Repo, Warning, << " Appropriate installed package could not be determined"); - }); + }, [&] { + AICLI_LOG(Repo, Warning, << " Appropriate installed package could not be determined"); + }); - if (installedPackage && !result.ContainsInstalledPackage(installedPackage->GetInstalled().get())) - { - // TODO: Needs a whole separate change to fix the fact that we don't support multiple available packages and what the different search behaviors mean - foundInstalledMatch = true; - result.Matches.emplace_back(std::make_shared(installedPackage, std::move(match.Package)), match.MatchCriteria); + if (installedPackage && !result.ContainsInstalledPackage(installedPackage->GetInstalled().get())) + { + // TODO: Needs a whole separate change to fix the fact that we don't support multiple available packages and what the different search behaviors mean + foundInstalledMatch = true; + auto compositePackage = std::make_shared(installedPackage, std::move(match.Package)); + if (trackingPackage) + { + auto trackingPackageTime = GetLatestTrackingWriteTime(trackingPackage); + compositePackage->SetTracking(source, std::move(trackingPackage), trackingPackageTime); + } + result.Matches.emplace_back(std::move(compositePackage), match.MatchCriteria); + } } } @@ -1267,7 +1735,7 @@ namespace AppInstaller::Repository result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); } - return std::move(result); + return result.ConvertToSearchResult(); } // An available search goes through each source, searching individually and then sorting the full result set. diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp index e15406eb58..99003d67ba 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -403,7 +403,19 @@ namespace AppInstaller::Repository::Microsoft // `Publisher.DisplayName`. We would need to ensure that there are no matches // against the rest of the data however (might happen if same package is // installed for multiple architectures/languages). - manifest.Id = productCode; + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SideBySide)) + { + char separator = '\\'; + + std::ostringstream stream; + stream << "ARP" << separator << scope << separator << architecture << separator << productCode; + + manifest.Id = stream.str(); + } + else + { + manifest.Id = productCode; + } manifest.Installers.emplace_back(); // TODO: This likely needs some cleanup applied, as it looks like INNO tends to append an "_is#" @@ -491,7 +503,7 @@ namespace AppInstaller::Repository::Microsoft try { // Use the ProductCode as a unique key for the path - manifestIdOpt = index.AddManifest(manifest, Utility::ConvertToUTF16(manifest.Installers[0].ProductCode)); + manifestIdOpt = index.AddManifest(manifest); } catch (...) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h index 45cf3103ba..672f8dba8f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -29,8 +29,7 @@ namespace AppInstaller::Repository::Microsoft return { filePath, disposition, std::move(indexFile) }; } - // Opens or creates a PinningIndex database on the default path. - // openDisposition is only used when opening an existing database. + // Opens the PinningIndex database on the default path if it exists. // Returns nullptr in case of error. static std::shared_ptr OpenIfExists(OpenDisposition openDisposition = OpenDisposition::Read); diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp index dd2408e5ef..8558e8650e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -9,6 +9,7 @@ #include #include +#include using namespace std::string_literals; using namespace std::string_view_literals; @@ -100,9 +101,17 @@ namespace AppInstaller::Repository::Microsoft } auto packageId = package.Id(); + Utility::NormalizedString fullName = Utility::ConvertToUTF8(packageId.FullName()); Utility::NormalizedString familyName = Utility::ConvertToUTF8(packageId.FamilyName()); - manifest.Id = familyName; + if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SideBySide)) + { + manifest.Id = "MSIX\\" + fullName; + } + else + { + manifest.Id = familyName; + } // Get version std::ostringstream strstr; @@ -142,11 +151,11 @@ namespace AppInstaller::Repository::Microsoft catch (const winrt::hresult_error& hre) { AICLI_LOG(Repo, Warning, << "winrt::hresult_error[0x" << Logging::SetHRFormat << hre.code() << ": " << - Utility::ConvertToUTF8(hre.message()) << "] exception thrown when getting DisplayName for " << familyName); + Utility::ConvertToUTF8(hre.message()) << "] exception thrown when getting DisplayName for " << fullName); } catch (...) { - AICLI_LOG(Repo, Warning, << "Unknown exception thrown when getting DisplayName for " << familyName); + AICLI_LOG(Repo, Warning, << "Unknown exception thrown when getting DisplayName for " << fullName); } } @@ -160,17 +169,28 @@ namespace AppInstaller::Repository::Microsoft try { // Use the full name as a unique key for the path - auto manifestId = index.AddManifest(manifest, std::filesystem::path{ packageId.FullName().c_str() }); + auto manifestId = index.AddManifest(manifest); index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msix)); + + auto architecture = Utility::ConvertToArchitectureEnum(packageId.Architecture()); + if (architecture) + { + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledArchitecture, + ToString(architecture.value())); + } } catch (const wil::ResultException& resultException) { if (HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) == resultException.GetErrorCode() && package.IsFramework()) { - // There may be multiple packages with same package family name for framework packages. - continue; + // This should not be needed with the SxS changes + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::SideBySide)) + { + // There may be multiple packages with same package family name for framework packages. + continue; + } } throw; @@ -183,7 +203,7 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Verbose, << "Creating PredefinedInstalledSource with filter [" << PredefinedInstalledSourceFactory::FilterToString(filter) << ']'); // Create an in memory index - SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, SQLite::Version::Latest()); + SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, SQLite::Version::Latest(), SQLiteIndex::CreateOptions::SupportPathless); // Put installed packages into the index if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP || diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp index f5508e4a3d..dd34ecbc57 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp @@ -105,7 +105,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement statement = builder.Prepare(m_connection); BindStatementForMatchType(statement, filter, bindIndex); statement.Execute(); - AICLI_LOG(Repo, Verbose, << "Search found " << m_connection.GetChanges() << " rows"); + AICLI_LOG(SQL, Verbose, << "Search found " << m_connection.GetChanges() << " rows"); } void SearchResultsTable::RemoveDuplicateManifestRows() @@ -128,7 +128,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 EndParenthetical(); builder.Execute(m_connection); - AICLI_LOG(Repo, Verbose, << "Removed " << m_connection.GetChanges() << " duplicate rows"); + AICLI_LOG(SQL, Verbose, << "Removed " << m_connection.GetChanges() << " duplicate rows"); } void SearchResultsTable::PrepareToFilter() @@ -170,7 +170,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement statement = builder.Prepare(m_connection); BindStatementForMatchType(statement, filter, bindIndex); statement.Execute(); - AICLI_LOG(Repo, Verbose, << "Filter kept " << m_connection.GetChanges() << " rows"); + AICLI_LOG(SQL, Verbose, << "Filter kept " << m_connection.GetChanges() << " rows"); } void SearchResultsTable::CompleteFilter() @@ -180,7 +180,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.DeleteFrom(GetQualifiedName()).Where(s_SearchResultsTable_Filter).Equals(false); builder.Execute(m_connection); - AICLI_LOG(Repo, Verbose, << "Filter deleted " << m_connection.GetChanges() << " rows"); + AICLI_LOG(SQL, Verbose, << "Filter deleted " << m_connection.GetChanges() << " rows"); } ISQLiteIndex::SearchResult SearchResultsTable::GetSearchResults(size_t limit) diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h index 1ad88150ad..36bf5774ee 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h @@ -242,7 +242,7 @@ namespace AppInstaller::Repository { PackageVersionKey() = default; - PackageVersionKey(Utility::NormalizedString sourceId, Utility::NormalizedString version, Utility::NormalizedString channel) : + PackageVersionKey(std::string sourceId, Utility::NormalizedString version, Utility::NormalizedString channel) : SourceId(std::move(sourceId)), Version(std::move(version)), Channel(std::move(channel)) {} // The source id that this version came from. diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h index fb24299526..663a4ec950 100644 --- a/src/AppInstallerRepositoryCore/pch.h +++ b/src/AppInstallerRepositoryCore/pch.h @@ -50,6 +50,7 @@ #include #include #include +#include #include #include diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 1818c3252a..2c7ba195ce 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -219,21 +219,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::optional<::AppInstaller::Utility::Architecture> GetUtilityArchitecture(winrt::Windows::System::ProcessorArchitecture architecture) { - switch (architecture) - { - case winrt::Windows::System::ProcessorArchitecture::X86: - return ::AppInstaller::Utility::Architecture::X86; - case winrt::Windows::System::ProcessorArchitecture::Arm: - return ::AppInstaller::Utility::Architecture::Arm; - case winrt::Windows::System::ProcessorArchitecture::X64: - return ::AppInstaller::Utility::Architecture::X64; - case winrt::Windows::System::ProcessorArchitecture::Neutral: - return ::AppInstaller::Utility::Architecture::Neutral; - case winrt::Windows::System::ProcessorArchitecture::Arm64: - return ::AppInstaller::Utility::Architecture::Arm64; - } - - return {}; + return ::AppInstaller::Utility::ConvertToArchitectureEnum(architecture); } std::optional GetWindowsSystemProcessorArchitecture(::AppInstaller::Utility::Architecture architecture) diff --git a/tools/DevInSandbox/InSandboxScript.ps1 b/tools/DevInSandbox/InSandboxScript.ps1 new file mode 100644 index 0000000000..8158343f60 --- /dev/null +++ b/tools/DevInSandbox/InSandboxScript.ps1 @@ -0,0 +1,31 @@ +Param( + [String[]] $DesktopAppInstallerDependencyPath +) + +$ProgressPreference = 'SilentlyContinue' + +$desktopPath = "C:\Users\WDAGUtilityAccount\Desktop" + +Write-Host @" +--> Installing WinGet + +"@ + +foreach($dependency in $DesktopAppInstallerDependencyPath) +{ + Write-Host @" + ----> Installing $dependency +"@ + Add-AppxPackage -Path $dependency +} + +Write-Host @" + ----> Enabling dev mode +"@ +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + +$devPackageManifestPath = Join-Path $desktopPath "DevPackage\AppxManifest.xml" +Write-Host @" + ----> Installing $devPackageManifestPath +"@ +Add-AppxPackage -Path $devPackageManifestPath -Register diff --git a/tools/DevInSandbox/Launch-DevPackageInSandbox.ps1 b/tools/DevInSandbox/Launch-DevPackageInSandbox.ps1 new file mode 100644 index 0000000000..f21a494791 --- /dev/null +++ b/tools/DevInSandbox/Launch-DevPackageInSandbox.ps1 @@ -0,0 +1,216 @@ +Param( + [Parameter(HelpMessage = "The directory where local dev build is located; only the release build works.")] + [String] $DevPackagePath +) + +$ErrorActionPreference = "Stop" + +# Validate that the local dev manifest exists + +if (-not $DevPackagePath) +{ + $DevPackagePath = Join-Path $PSScriptRoot "..\..\src\AppInstallerCLIPackage\bin\x64\Release\AppX" +} + +$DevPackagePath = [System.IO.Path]::GetFullPath($DevPackagePath) + +if ($DevPackagePath.ToLower().Contains("debug")) +{ + Write-Error -Category InvalidArgument -Message @" +The Debug dev package does not work for unknown reasons. +Use the Release build or figure out how to make debug work and fix the scripts. +"@ +} + +if (-not (Test-Path (Join-Path $DevPackagePath "AppxManifest.xml"))) +{ + Write-Error -Category InvalidArgument -Message @" +AppxManifest.xml does not exist in the path $DevPackagePath +Either build the local dev package, or provide the location using -DevPackagePath +"@ +} + +# Check if Windows Sandbox is enabled + +if (-Not (Get-Command 'WindowsSandbox' -ErrorAction SilentlyContinue)) +{ + Write-Error -Category NotInstalled -Message @' +Windows Sandbox does not seem to be available. Check the following URL for prerequisites and further details: +https://docs.microsoft.com/windows/security/threat-protection/windows-sandbox/windows-sandbox-overview + +You can run the following command in an elevated PowerShell for enabling Windows Sandbox: +$ Enable-WindowsOptionalFeature -Online -FeatureName 'Containers-DisposableClientVM' +'@ +} + +# Close Windows Sandbox + +function Close-WindowsSandbox { + $sandbox = Get-Process 'WindowsSandboxClient' -ErrorAction SilentlyContinue + if ($sandbox) + { + Write-Host '--> Closing Windows Sandbox' + + $sandboxServer = Get-Process 'WindowsSandbox' -ErrorAction SilentlyContinue + + $sandbox | Stop-Process + $sandbox | Wait-Process -Timeout 120 + + # Also wait for the server to close + if ($sandboxServer) + { + try + { + $sandboxServer | Wait-Process -Timeout 120 + } + catch [System.TimeoutException] + { + # Force stop the server if it does not automatically stop + $sandboxServer | Stop-Process + $sandboxServer | Wait-Process -Timeout 120 + } + + } + + Write-Host + } + Remove-Variable sandbox +} + +Close-WindowsSandbox + +# Initialize Temp Folder + +$tempFolderName = 'DevInSandboxStaging' +$tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $tempFolderName + +New-Item $tempFolder -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + +# Set dependencies + +$desktopInSandbox = 'C:\Users\WDAGUtilityAccount\Desktop' + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$WebClient = New-Object System.Net.WebClient + +# Hide the progress bar of Invoke-WebRequest +$oldProgressPreference = $ProgressPreference +$ProgressPreference = 'SilentlyContinue' + +$ProgressPreference = $oldProgressPreference + +$vcLibsUwp = @{ + fileName = 'Microsoft.VCLibs.x64.14.00.Desktop.appx' + url = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx' + hash = '9BFDE6CFCC530EF073AB4BC9C4817575F63BE1251DD75AAA58CB89299697A569' + folderInLocal = Join-Path ${env:ProgramFiles(x86)} "Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.VCLibs.Desktop\14.0\Appx\Retail\x64" +} + +$dependencies = @($vcLibsUwp) + +# Clean temp directory + +Get-ChildItem $tempFolder -Recurse -Exclude $dependencies.fileName | Remove-Item -Force -Recurse + +# Download dependencies + +Write-Host '--> Checking dependencies' + +foreach ($dependency in $dependencies) +{ + $dependency.pathInSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $dependency.fileName) + + # First see if the file exists locally to copy instead of downloading + if ($dependency.folderInLocal -ne $null) + { + $dependencyFilePath = Join-Path -Path $dependency.folderInLocal -ChildPath $dependency.fileName + if (Test-Path -Path $dependencyFilePath -PathType Leaf) + { + $dependencyFilePath + $tempFolder + Copy-Item -Path $dependencyFilePath -Destination $tempFolder -Force + continue + } + } + + # File does not exist locally, we need to download + + $dependency.file = Join-Path -Path $tempFolder -ChildPath $dependency.fileName + + # Only download if the file does not exist, or its hash does not match. + if (-Not ((Test-Path -Path $dependency.file -PathType Leaf) -And $dependency.hash -eq $(Get-FileHash $dependency.file).Hash)) + { + Write-Host @" + - Downloading: + $($dependency.url) +"@ + + try + { + $WebClient.DownloadFile($dependency.url, $dependency.file) + } + catch + { + #Pass the exception as an inner exception + throw [System.Net.WebException]::new("Error downloading $($dependency.url).",$_.Exception) + } + if (-not ($dependency.hash -eq $(Get-FileHash $dependency.file).Hash)) + { + throw [System.Activities.VersionMismatchException]::new('Dependency hash does not match the downloaded file') + } + } +} + +# Copy main script + +$mainPs1FileName = 'InSandboxScript.ps1' +Copy-Item (Join-Path $PSScriptRoot $mainPs1FileName) (Join-Path $tempFolder $mainPs1FileName) + +$dependenciesPathsInSandbox = "@('$($vcLibsUwp.pathInSandbox)')" +$bootstrapPs1Content = ".\$mainPs1FileName -DesktopAppInstallerDependencyPath @($dependenciesPathsInSandbox)" + +$bootstrapPs1FileName = 'Bootstrap.ps1' +$bootstrapPs1Content | Out-File (Join-Path $tempFolder $bootstrapPs1FileName) -Force + +# Create Windows Sandbox configuration file + +$bootstrapPs1InSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $bootstrapPs1FileName) +$tempFolderInSandbox = Join-Path -Path $desktopInSandbox -ChildPath $tempFolderName + +$devPackageInSandbox = Join-Path -Path $desktopInSandbox -ChildPath "DevPackage" +$devPackageXMLFragment = "" + +$devPackageXMLFragment = @" + + $DevPackagePath + $devPackageInSandbox + +"@ + +$sandboxTestWsbContent = @" + + + + $tempFolder + true + + $devPackageXMLFragment + + + PowerShell Start-Process PowerShell -WindowStyle Maximized -WorkingDirectory '$tempFolderInSandbox' -ArgumentList '-ExecutionPolicy Bypass -NoExit -NoLogo -File $bootstrapPs1InSandbox' + + +"@ + +$sandboxTestWsbFileName = 'SandboxTest.wsb' +$sandboxTestWsbFile = Join-Path -Path $tempFolder -ChildPath $sandboxTestWsbFileName +$sandboxTestWsbContent | Out-File $sandboxTestWsbFile -Force + +Write-Host @" +--> Starting Windows Sandbox + $sandboxTestWsbFile +"@ + +Write-Host + +WindowsSandbox $SandboxTestWsbFile