From 95add984d147223256630f762824f768d98d9485 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 15 Oct 2025 17:07:10 -0700 Subject: [PATCH 1/6] Move to latest 7.4 PS SDK (1.12) (#5812) CP of #5811 ## Change Update to the latest 7.4 PowerShell SDK. Updates the SqlClient version as well since it is a required dependency. --- src/Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 97b31dfc38..a085a62cdd 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -14,7 +14,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -36,4 +36,4 @@ - \ No newline at end of file + From 99821c17b2058e2962d2ff3da7706dcf6c88a701 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Tue, 4 Nov 2025 13:56:24 -0800 Subject: [PATCH 2/6] Make Repair-WGPM a COM-aware cmdlet and rework version retrieval (CP to 1.12) (#5858) CP of #5842 ## Issue A previous change introduced a COM API to retrieve the WinGet version. The PowerShell methods to get the version were updated to try using it before invoking the existing method (run `winget --version`). This caused Repair-WGPM to use COM for the first time (IFF a version specifier was provided [which includes `-Latest`]). This caused the two linked issues: 1. #5826 :: In .NET Framework (Windows PowerShell), the .winmd file must be found in order to generate the COM type information at runtime. This is required when jit'ing the new version API, used only when a version specifier is provided. This doesn't affect .NET Core (PowerShell 7) because exceptions are swallowed to support backward compat and the types are all pre-generated by CsWinRT. Only commands deriving from a specific type were doing the initialization required. 2. #5827 :: Calling a COM API means that the server is active, making attempts to install the package fail due to an in-use error. This required `-Force` to be provided, again only if a version specifier was provided. ## Change The larger part of this change reworks the existing assert and repair state machine to better re-use the call to `winget --version` that is actually attempting to probe for WinGet CLI functionality. We keep that result around and use it when comparing to the supplied target version rather than attempting to retrieve the version again. If the version is not correct, we attach it to the exception that is thrown so that we can re-use it once again during the attempt to install the different version. Since the first attempt to call `winget --version` has to succeed in order to get to the code that would check the current version, we can successfully avoid the COM call in this path every time. Ultimately this means that if WinGet is already installed properly, attempting to change the version with Repair only gets the version once instead of the previous 3 times. The final portion of the change updates the base command for Repair and Assert to the one that provides the COM initialization. While this shouldn't be necessary with the other portion, it is preferable to supply `-Force` as a workaround than to simply wait for a resolution if the type cannot be loaded. --- .../Commands/WinGetPackageManagerCommand.cs | 12 ++++--- .../Common/WinGetIntegrity.cs | 12 ++++--- .../Exceptions/WinGetIntegrityException.cs | 8 ++++- .../Helpers/WinGetVersion.cs | 32 ++++++++++++++----- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index cdf5f16d51..b08e70ce60 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -21,7 +21,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// /// Used by Repair-WinGetPackageManager and Assert-WinGetPackageManager. /// - public sealed class WinGetPackageManagerCommand : BaseCommand + public sealed class WinGetPackageManagerCommand : ManagementDeploymentCommand { private const string EnvPath = "env:PATH"; @@ -132,7 +132,7 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers switch (currentCategory) { case IntegrityCategory.UnexpectedVersion: - await this.InstallDifferentVersionAsync(new WinGetVersion(expectedVersion), allUsers, force); + await this.InstallDifferentVersionAsync(new WinGetVersion(expectedVersion), e.InstalledVersion, allUsers, force); break; case IntegrityCategory.NotInPath: this.RepairEnvPath(); @@ -167,9 +167,13 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers } } - private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, bool allUsers, bool force) + private async Task InstallDifferentVersionAsync(WinGetVersion toInstallVersion, WinGetVersion? installedVersion, bool allUsers, bool force) { - var installedVersion = WinGetVersion.InstalledWinGetVersion(this); + if (installedVersion == null) + { + installedVersion = WinGetVersion.InstalledWinGetVersion(this); + } + bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0; string message = $"Installed WinGet version '{installedVersion.TagVersion}' " + diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs index 9669da8dbc..d9ff12bb56 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -39,13 +39,14 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers return; } + WinGetCLICommandResult? versionResult = null; + try { // Start by calling winget without its WindowsApp PFN path. // If it succeeds and the exit code is 0 then we are good. - var wingetCliWrapper = new WingetCLIWrapper(false); - var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); - result.VerifyExitCode(); + versionResult = WinGetVersion.RunWinGetVersionFromCLI(pwshCmdlet, false); + versionResult.VerifyExitCode(); } catch (Win32Exception e) { @@ -68,7 +69,7 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers { // This assumes caller knows that the version exist. WinGetVersion expectedWinGetVersion = new WinGetVersion(expectedVersion); - var installedVersion = WinGetVersion.InstalledWinGetVersion(pwshCmdlet); + var installedVersion = WinGetVersion.InstalledWinGetVersion(pwshCmdlet, versionResult); if (expectedWinGetVersion.CompareTo(installedVersion) != 0) { throw new WinGetIntegrityException( @@ -76,7 +77,8 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers string.Format( Resources.IntegrityUnexpectedVersionMessage, installedVersion.TagVersion, - expectedVersion)); + expectedVersion)) + { InstalledVersion = installedVersion }; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs index 63a7939038..4ffee8283d 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -9,6 +9,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions using System; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Resources; /// @@ -53,6 +54,11 @@ public WinGetIntegrityException(IntegrityCategory category, string message) /// public IntegrityCategory Category { get; } + /// + /// Gets or sets the installed version. + /// + internal WinGetVersion? InstalledVersion { get; set; } + private static string GetMessage(IntegrityCategory category) => category switch { IntegrityCategory.Failure => Resources.IntegrityFailureMessage, diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs index 19d4704dd5..1961df90f1 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs @@ -64,23 +64,39 @@ public WinGetVersion(string version) /// public bool IsPrerelease { get; } + /// + /// Runs the winget version command. + /// + /// PowerShell cmdlet. + /// Use full path or not. + /// The command result. + public static WinGetCLICommandResult RunWinGetVersionFromCLI(PowerShellCmdlet pwshCmdlet, bool fullPath = true) + { + var wingetCliWrapper = new WingetCLIWrapper(fullPath); + return wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); + } + /// /// Gets the version of the installed winget. /// /// PowerShell cmdlet. + /// A command result from running previously. /// The WinGetVersion. - public static WinGetVersion InstalledWinGetVersion(PowerShellCmdlet pwshCmdlet) + public static WinGetVersion InstalledWinGetVersion(PowerShellCmdlet pwshCmdlet, WinGetCLICommandResult? versionResult = null) { - // Try getting the version through COM if it is available (user might have an older build installed) - string? comVersion = PackageManagerWrapper.Instance.GetVersion(); - if (comVersion != null) + if (versionResult == null || versionResult.ExitCode != 0) { - return new WinGetVersion(comVersion); + // Try getting the version through COM if it is available (user might have an older build installed) + string? comVersion = PackageManagerWrapper.Instance.GetVersion(); + if (comVersion != null) + { + return new WinGetVersion(comVersion); + } + + versionResult = RunWinGetVersionFromCLI(pwshCmdlet); } - var wingetCliWrapper = new WingetCLIWrapper(); - var result = wingetCliWrapper.RunCommand(pwshCmdlet, "--version"); - return new WinGetVersion(result.StdOut.Replace(Environment.NewLine, string.Empty)); + return new WinGetVersion(versionResult.StdOut.Replace(Environment.NewLine, string.Empty)); } /// From c736beddc295096def4ae4d54a94a4b7df0001c8 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 5 Nov 2025 16:25:45 -0800 Subject: [PATCH 3/6] Unregister signal handler (CP to 1.12) (#5862) ## Change Remove the CTRL handler when we destruct. CP of #5861 --- src/AppInstallerCLICore/ShutdownMonitoring.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AppInstallerCLICore/ShutdownMonitoring.cpp b/src/AppInstallerCLICore/ShutdownMonitoring.cpp index cb581116ec..c7122bf66f 100644 --- a/src/AppInstallerCLICore/ShutdownMonitoring.cpp +++ b/src/AppInstallerCLICore/ShutdownMonitoring.cpp @@ -101,6 +101,8 @@ namespace AppInstaller::ShutdownMonitoring TerminationSignalHandler::~TerminationSignalHandler() { + SetConsoleCtrlHandler(StaticCtrlHandlerFunction, FALSE); + // std::thread requires that any managed thread (joinable) be joined or detached before destructing if (m_windowThread.joinable()) { From 53044ac3d822bfcb8809e5a78a85b61d6ab3ad87 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Mon, 10 Nov 2025 14:16:03 -0800 Subject: [PATCH 4/6] Support associating export units with packages in subdirectories (1.12) (#5866) CP of #5859 ## Change The primary motivation is to support directories below the install location to contain configuration units that we will associate with the package. This is achieved by refactoring the association logic from a Package x Unit loop into a tree structure that is colored by package install locations. This also has the benefit of making a O(N^2) algorithm into an O(N). Units are first inserted into the tree based on their file path. Then the install location of each package is recorded onto that tree as well. Finally, during the export of each package, all resources at the install location and any that are descended from it but not under another package are included. --- .github/actions/spelling/expect.txt | 3 + .../Workflows/ConfigurationFlow.cpp | 118 +++++++++++++--- src/AppInstallerCLICore/pch.h | 1 + .../ConfigureCommand.cs | 2 +- .../ConfigureExportCommand.cs | 10 +- src/AppInstallerCLITests/Downloader.cpp | 4 +- src/AppInstallerCLITests/Filesystem.cpp | 110 +++++++++++++++ .../AppInstallerSharedLib.vcxproj | 1 + .../AppInstallerSharedLib.vcxproj.filters | 3 + .../Public/winget/PathTree.h | 129 ++++++++++++++++++ src/AppInstallerTestExeInstaller/main.cpp | 60 +++++--- 11 files changed, 396 insertions(+), 45 deletions(-) create mode 100644 src/AppInstallerSharedLib/Public/winget/PathTree.h diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 9f28a6b5b3..2e41a48dcf 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -47,6 +47,7 @@ awgpm awgs azurewebsites Baz +bbb bcp BEBOM BEFACEF @@ -74,6 +75,7 @@ buildtrees cancelledbyuser casemap casemappings +ccc cch centralus certmgr @@ -395,6 +397,7 @@ packageinusebyapplication PACL PARAMETERMAP pathparts +pathtree Patil pbstr pcb diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 055e8fca84..7c23fcab3c 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include using namespace AppInstaller::CLI::Execution; @@ -1678,6 +1679,80 @@ namespace AppInstaller::CLI::Workflow } } + // Contains a tree of all unit processors by their path. + struct UnitProcessorTree + { + private: + struct SourceAndPackage + { + PackageCollection::Source Source; + PackageCollection::Package Package; + }; + + struct Node + { + // Packages whose installed location is at this node + std::vector Packages; + + // Units whose location is at this node. + std::vector Units; + }; + + Filesystem::PathTree m_pathTree; + + Node& FindNodeForFilePath(const winrt::hstring& filePath) + { + std::filesystem::path path{ std::wstring{ filePath } }; + return m_pathTree.FindOrInsert(path.parent_path()); + } + + public: + UnitProcessorTree(std::vector&& unitProcessors) + { + for (auto&& unit : unitProcessors) + { + IConfigurationUnitProcessorDetails3 unitProcessor3; + if (unit.try_as(unitProcessor3)) + { + winrt::hstring unitPath = unitProcessor3.Path(); + AICLI_LOG(Config, Verbose, << "Found unit `" << Utility::ConvertToUTF8(unit.UnitType()) << "` at: " << Utility::ConvertToUTF8(unitPath)); + Node& node = FindNodeForFilePath(unitPath); + node.Units.emplace_back(std::move(unit)); + } + } + } + + void PlacePackage(const PackageCollection::Source& source, const PackageCollection::Package& package) + { + Node* node = m_pathTree.Find(package.InstalledLocation); + if (node) + { + node->Packages.emplace_back(SourceAndPackage{ source, package }); + } + } + + std::vector GetResourcesForPackage(const PackageCollection::Package& package) const + { + std::vector result; + + m_pathTree.VisitIf( + package.InstalledLocation, + [&](const Node& node) + { + for (const auto& unit : node.Units) + { + result.emplace_back(unit); + } + }, + [](const Node& node) + { + return node.Packages.empty(); + }); + + return result; + } + }; + void ProcessPackagesForConfigurationExportAll(Execution::Context& context) { ConfigurationContext& configContext = context.Get(); @@ -1718,6 +1793,17 @@ namespace AppInstaller::CLI::Workflow } } + // Build a tree of the unit processors and place packages onto it to indicate nearest ownership. + UnitProcessorTree unitProcessorTree{ std::move(unitProcessors) }; + + for (const auto& source : context.Get().Sources) + { + for (const auto& package : source.Packages) + { + unitProcessorTree.PlacePackage(source, package); + } + } + for (const auto& source : context.Get().Sources) { // Create WinGetSource unit for non well known source. @@ -1730,34 +1816,28 @@ namespace AppInstaller::CLI::Workflow for (const auto& package : source.Packages) { + AICLI_LOG(Config, Verbose, << "Exporting package `" << package.Id << "` at: " << package.InstalledLocation); + auto packageUnit = anon::CreateWinGetPackageUnit(package, source, context.Args.Contains(Args::Type::IncludeVersions), sourceUnit, packageUnitType); configContext.Set().Units().Append(packageUnit); // Try package settings export. - for (auto itr = unitProcessors.begin(); itr != unitProcessors.end(); /* itr incremented in the logic */) + auto unitsForPackage = unitProcessorTree.GetResourcesForPackage(package); + for (const auto& unit : unitsForPackage) { - IConfigurationUnitProcessorDetails3 unitProcessor3; - itr->try_as(unitProcessor3); - if (Filesystem::IsParentPath(std::filesystem::path{ std::wstring{ unitProcessor3.Path() } }, package.InstalledLocation)) - { - ConfigurationUnit configUnit = anon::CreateConfigurationUnitFromUnitType( - unitProcessor3.UnitType(), - Utility::ConvertToUTF8(packageUnit.Identifier())); + winrt::hstring unitType = unit.UnitType(); + AICLI_LOG(Config, Verbose, << " exporting unit `" << Utility::ConvertToUTF8(unitType)); - auto exportedUnits = anon::ExportUnit(context, configUnit); - anon::AddDependentUnit(exportedUnits, packageUnit); + ConfigurationUnit configUnit = anon::CreateConfigurationUnitFromUnitType( + unitType, + Utility::ConvertToUTF8(packageUnit.Identifier())); - for (auto exportedUnit : exportedUnits) - { - configContext.Set().Units().Append(exportedUnit); - } + auto exportedUnits = anon::ExportUnit(context, configUnit); + anon::AddDependentUnit(exportedUnits, packageUnit); - // Remove the unit processor from the list after export. - itr = unitProcessors.erase(itr); - } - else + for (const auto& exportedUnit : exportedUnits) { - itr++; + configContext.Set().Units().Append(exportedUnit); } } } diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index c0daddfd06..e13f7ed751 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs index f4048cff45..f90da50627 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -364,7 +364,7 @@ public void ConfigureFromTestRepo_DSCv3() public void ConfigureFindUnitProcessors() { // Find all unit processors. - var result = TestCommon.RunAICLICommand("test config-find-unit-processors", string.Empty, timeOut: 120000); + var result = TestCommon.RunAICLICommand("test config-find-unit-processors", string.Empty, timeOut: 300000); Assert.AreEqual(0, result.ExitCode); Assert.True(result.StdOut.Contains("Microsoft/OSInfo")); diff --git a/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs index 58a3085e3f..cfc30554b8 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs @@ -31,7 +31,9 @@ public void BaseSetup() var installDir = TestCommon.GetRandomTestDir(); TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestPackageExport -v 1.0.0.0 --silent -l {installDir}"); this.previousPathValue = System.Environment.GetEnvironmentVariable("PATH"); - System.Environment.SetEnvironmentVariable("PATH", this.previousPathValue + ";" + installDir); + + // The installer puts DSCv3 resources in both locations + System.Environment.SetEnvironmentVariable("PATH", this.previousPathValue + ";" + installDir + ";" + installDir + "\\SubDirectory"); DSCv3ResourceTestBase.EnsureTestResourcePresence(); } @@ -146,7 +148,7 @@ public void ExportAll() { var exportDir = TestCommon.GetRandomTestDir(); var exportFile = Path.Combine(exportDir, "exported.yml"); - var result = TestCommon.RunAICLICommand(Command, $"--all -o {exportFile}", timeOut: 1200000); + var result = TestCommon.RunAICLICommand(Command, $"--all --verbose -o {exportFile}", timeOut: 1200000); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(File.Exists(exportFile)); @@ -175,6 +177,10 @@ public void ExportAll() Assert.True(showResult.StdOut.Contains("AppInstallerTest/TestResource")); Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_AppInstallerTest.TestPackageExport")); Assert.True(showResult.StdOut.Contains("data: TestData")); + + Assert.True(showResult.StdOut.Contains("AppInstallerTest/TestResource.SubDirectory")); + Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_AppInstallerTest.TestPackageExport")); + Assert.True(showResult.StdOut.Contains("data: TestData")); } /// diff --git a/src/AppInstallerCLITests/Downloader.cpp b/src/AppInstallerCLITests/Downloader.cpp index 0965222446..4c65c05bb1 100644 --- a/src/AppInstallerCLITests/Downloader.cpp +++ b/src/AppInstallerCLITests/Downloader.cpp @@ -82,7 +82,7 @@ TEST_CASE("HttpStream_ReadLastFullPage", "[HttpStream]") for (size_t i = 0; i < 10; ++i) { - stream = GetReadOnlyStreamFromURI("https://cdn.winget.microsoft.com/cache/source2.msix"); + stream = GetReadOnlyStreamFromURI("https://aka.ms/win32-x64-user-stable"); stat = { 0 }; REQUIRE(stream->Stat(&stat, STATFLAG_NONAME) == S_OK); @@ -96,7 +96,7 @@ TEST_CASE("HttpStream_ReadLastFullPage", "[HttpStream]") } { - INFO("https://cdn.winget.microsoft.com/cache/source2.msix gave back a 0 byte file"); + INFO("https://aka.ms/win32-x64-user-stable gave back a 0 byte file"); REQUIRE(stream); } diff --git a/src/AppInstallerCLITests/Filesystem.cpp b/src/AppInstallerCLITests/Filesystem.cpp index f98934de82..38fda9fff6 100644 --- a/src/AppInstallerCLITests/Filesystem.cpp +++ b/src/AppInstallerCLITests/Filesystem.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "TestCommon.h" #include +#include #include using namespace AppInstaller::Utility; @@ -113,3 +114,112 @@ TEST_CASE("GetExecutablePathForProcess", "[filesystem]") REQUIRE(thisExecutable.has_extension()); REQUIRE(thisExecutable.filename() == L"AppInstallerCLITests.exe"); } + +TEST_CASE("PathTree_InsertAndFind", "[filesystem][pathtree]") +{ + PathTree pathTree; + + std::filesystem::path path1 = L"C:\\test"; + std::filesystem::path path1sub = L"C:\\test\\sub"; + std::filesystem::path path2 = L"C:\\diff"; + std::filesystem::path path3 = L"D:\\test"; + + REQUIRE(nullptr == pathTree.Find(path1)); + pathTree.FindOrInsert(path1) = true; + + REQUIRE(nullptr != pathTree.Find(path1)); + REQUIRE(*pathTree.Find(path1)); + + REQUIRE(nullptr == pathTree.Find(path1sub)); + REQUIRE(nullptr == pathTree.Find(path2)); + REQUIRE(nullptr == pathTree.Find(path3)); +} + +TEST_CASE("PathTree_InsertAndFind_Negative", "[filesystem][pathtree]") +{ + PathTree pathTree; + pathTree.FindOrInsert(L"C:\\a\\aa\\aaa"); + + REQUIRE(nullptr == pathTree.Find({})); + REQUIRE_THROWS_HR(pathTree.FindOrInsert({}), E_INVALIDARG); +} + +size_t CountVisited(const PathTree& pathTree, const std::filesystem::path& path, std::function predicate) +{ + size_t result = 0; + pathTree.VisitIf(path, [&](const bool&) { ++result; }, predicate); + return result; +} + +TEST_CASE("PathTree_VisitIf_Count", "[filesystem][pathtree]") +{ + PathTree pathTree; + + pathTree.FindOrInsert(L"C:\\a\\aa\\aaa") = true; + pathTree.FindOrInsert(L"C:\\a\\aa\\bbb") = true; + pathTree.FindOrInsert(L"C:\\a\\aa\\ccc") = false; + pathTree.FindOrInsert(L"C:\\a\\aa") = true; + + pathTree.FindOrInsert(L"C:\\a\\bb\\aaa") = false; + pathTree.FindOrInsert(L"C:\\a\\bb\\bbb") = true; + pathTree.FindOrInsert(L"C:\\a\\bb\\ccc") = false; + pathTree.FindOrInsert(L"C:\\a\\bb") = true; + + pathTree.FindOrInsert(L"C:\\a\\cc\\aaa") = true; + pathTree.FindOrInsert(L"C:\\a\\cc\\bbb") = false; + pathTree.FindOrInsert(L"C:\\a\\cc\\ccc") = false; + pathTree.FindOrInsert(L"C:\\a\\cc") = false; + + pathTree.FindOrInsert(L"C:\\a") = true; + pathTree.FindOrInsert(L"C:\\b") = false; + pathTree.FindOrInsert(L"D:\\a") = false; + + auto always = [](const bool&) { return true; }; + auto never = [](const bool&) { return false; }; + auto if_input = [](const bool& b) { return b; }; + + REQUIRE(0 == CountVisited(pathTree, {}, always)); + + REQUIRE(15 == CountVisited(pathTree, L"C:\\", always)); + REQUIRE(2 == CountVisited(pathTree, L"D:\\", always)); + REQUIRE(0 == CountVisited(pathTree, L"E:\\", always)); + + REQUIRE(1 == CountVisited(pathTree, L"C:\\", never)); + REQUIRE(1 == CountVisited(pathTree, L"D:\\", never)); + REQUIRE(0 == CountVisited(pathTree, L"E:\\", never)); + + REQUIRE(7 == CountVisited(pathTree, L"C:\\", if_input)); + REQUIRE(6 == CountVisited(pathTree, L"C:\\a", if_input)); + REQUIRE(2 == CountVisited(pathTree, L"C:\\a\\cc", if_input)); + REQUIRE(1 == CountVisited(pathTree, L"D:\\", if_input)); + REQUIRE(0 == CountVisited(pathTree, L"E:\\", if_input)); +} + +TEST_CASE("PathTree_VisitIf_Correct", "[filesystem][pathtree]") +{ + PathTree> pathTree; + + pathTree.FindOrInsert(L"C:\\a\\aa\\aaa") = { true, true }; + pathTree.FindOrInsert(L"C:\\a\\aa\\bbb") = { true, true }; + pathTree.FindOrInsert(L"C:\\a\\aa\\ccc") = { false, false }; + pathTree.FindOrInsert(L"C:\\a\\aa") = { true, true }; + + pathTree.FindOrInsert(L"C:\\a\\bb\\aaa") = { false, false }; + pathTree.FindOrInsert(L"C:\\a\\bb\\bbb") = { true, true }; + pathTree.FindOrInsert(L"C:\\a\\bb\\ccc") = { false, false }; + pathTree.FindOrInsert(L"C:\\a\\bb") = { true, true }; + + pathTree.FindOrInsert(L"C:\\a\\cc\\aaa") = { true, true }; + pathTree.FindOrInsert(L"C:\\a\\cc\\bbb") = { false, false }; + pathTree.FindOrInsert(L"C:\\a\\cc\\ccc") = { false, false }; + pathTree.FindOrInsert(L"C:\\a\\cc") = { false, false }; + + pathTree.FindOrInsert(L"C:\\a") = { true, true }; + pathTree.FindOrInsert(L"C:\\b") = { false, false }; + pathTree.FindOrInsert(L"C:") = { true, false }; + + auto check_input = [](const std::pair& p) { REQUIRE(p.first); }; + auto if_input = [](const std::pair& p) { return p.second; }; + + pathTree.VisitIf(L"C:", check_input, if_input); +} diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj index 8afee3c685..511de1d87b 100644 --- a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj +++ b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -346,6 +346,7 @@ + diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters index 36613a4c92..4c865ad740 100644 --- a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters +++ b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -146,6 +146,9 @@ Public\winget + + Public\winget + diff --git a/src/AppInstallerSharedLib/Public/winget/PathTree.h b/src/AppInstallerSharedLib/Public/winget/PathTree.h new file mode 100644 index 0000000000..202d9cd5d8 --- /dev/null +++ b/src/AppInstallerSharedLib/Public/winget/PathTree.h @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include +#include + +namespace AppInstaller::Filesystem +{ + // Container that holds a map of items addressable by path. + template + struct PathTree + { + using value_t = Value; + + PathTree() = default; + + private: + struct Node + { + value_t Value{}; + std::map Children; + }; + + public: + // Returns the value for the given path, inserting it if necessary. + value_t& FindOrInsert(const std::filesystem::path& path) + { + return FindNode(path, true)->Value; + } + + // Finds the value for the given path; returns null if not found. + value_t* Find(const std::filesystem::path& path) + { + Node* node = FindNode(path, false); + return node ? &node->Value : nullptr; + } + + // Finds the value for the given path; returns null if not found. + const value_t* Find(const std::filesystem::path& path) const + { + const Node* node = FindNode(path); + return node ? &node->Value : nullptr; + } + + // Invokes the `visit` function for each value in the tree starting at `initialPath` (unconditionally) + // and recursively continuing on to children for whom the predicate returns true. + void VisitIf(const std::filesystem::path& initialPath, std::function visit, std::function predicate) const + { + const Node* node = FindNode(initialPath); + if (node) + { + std::queue nodes; + nodes.push(node); + + while (!nodes.empty()) + { + const Node* currentNode = nodes.front(); + nodes.pop(); + + visit(currentNode->Value); + + for (const auto& child : currentNode->Children) + { + if (predicate(child.second.Value)) + { + nodes.push(&child.second); + } + } + } + } + } + + private: + // Finds the node for the given path, creating as needed if requested. + Node* FindNode(const std::filesystem::path& path, bool createIfNeeded) + { + if (path.empty()) + { + if (createIfNeeded) + { + THROW_HR(E_INVALIDARG); + } + else + { + return nullptr; + } + } + + const auto& nodePath = std::filesystem::weakly_canonical(path); + Node* currentNode = &m_rootNode; + + for (const auto& pathPart : nodePath) + { + auto& children = currentNode->Children; + + if (createIfNeeded) + { + currentNode = &children[pathPart]; + } + else + { + auto itr = children.find(pathPart); + + if (itr != children.end()) + { + currentNode = &itr->second; + } + else + { + // Not found and should not create + return nullptr; + } + } + } + + return currentNode; + } + + // Finds the node for the given path; returns null if not found. + const Node* FindNode(const std::filesystem::path& path) const + { + return const_cast(this)->FindNode(path, false); + } + + Node m_rootNode; + }; +} diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index a67f12eab4..f0e86644cd 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ std::wstring_view RegistrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersio std::wstring_view DefaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; std::wstring_view DefaultDisplayName = L"AppInstallerTestExeInstaller"; std::wstring_view DefaultDisplayVersion = L"1.0.0.0"; +std::wstring_view DscSubDirectoryName = L"SubDirectory"; void WriteModifyRepairScript(std::wofstream& script, const path& repairCompletedTextFilePath, bool isModifyScript) { std::wstring scriptName = isModifyScript ? L"Modify" : L"Uninstaller"; @@ -48,17 +50,16 @@ void WriteUninstallerScript( std::wofstream& uninstallerScript, const path& uninstallerOutputTextFilePath, const std::wstring& registryKey, - const path& modifyScriptPath, - const path& repairCompletedTextFilePath, - const path& dscResourceExecutablePath, - const path& dscResourceManifestPath) { + std::initializer_list paths) { uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; uninstallerScript << "REG DELETE " << registryKey << " /f\n"; - uninstallerScript << "if exist \"" << modifyScriptPath.wstring() << "\" del \"" << modifyScriptPath.wstring() << "\"\n"; - uninstallerScript << "if exist \"" << repairCompletedTextFilePath.wstring() << "\" del \"" << repairCompletedTextFilePath.wstring() << "\"\n"; - uninstallerScript << "if exist \"" << dscResourceExecutablePath.wstring() << "\" del \"" << dscResourceExecutablePath.wstring() << "\"\n"; - uninstallerScript << "if exist \"" << dscResourceManifestPath.wstring() << "\" del \"" << dscResourceManifestPath.wstring() << "\"\n"; + + for (const auto& path : paths) + { + std::wstring pathString = path.wstring(); + uninstallerScript << "if exist \"" << pathString << "\" del \"" << pathString << "\"\n"; + } } path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID, bool useHKLM) @@ -74,15 +75,6 @@ path GenerateUninstaller(std::wostream& out, const path& installDirectory, const path repairCompletedTextFilePath = installDirectory; repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; - path modifyScriptPath = installDirectory; - modifyScriptPath /= "ModifyTestExe.bat"; - - path dscResourceExecutablePath = installDirectory; - dscResourceExecutablePath /= "AppInstallerTestResource.exe"; - - path dscResourceManifestPath = installDirectory; - dscResourceManifestPath /= "AppInstallerTest.dsc.resource.json"; - std::wstring registryKey{ useHKLM ? L"HKEY_LOCAL_MACHINE\\" : L"HKEY_CURRENT_USER\\" }; registryKey += RegistrySubkey; if (!productID.empty()) @@ -99,7 +91,15 @@ path GenerateUninstaller(std::wostream& out, const path& installDirectory, const uninstallerScript << L"for %%A in (%*) do (\n"; WriteModifyRepairScript(uninstallerScript, repairCompletedTextFilePath, false /*isModifyScript*/); uninstallerScript << ")\n"; - WriteUninstallerScript(uninstallerScript, uninstallerOutputTextFilePath, registryKey, modifyScriptPath, repairCompletedTextFilePath, dscResourceExecutablePath, dscResourceManifestPath); + WriteUninstallerScript(uninstallerScript, uninstallerOutputTextFilePath, registryKey, + { + installDirectory / "ModifyTestExe.bat", + repairCompletedTextFilePath, + installDirectory / "AppInstallerTestResource.exe", + installDirectory / "AppInstallerTest.dsc.resource.json", + installDirectory / DscSubDirectoryName / "AppInstallerTestResource.exe", + installDirectory / DscSubDirectoryName / "AppInstallerTest.dsc.resource.json", + }); uninstallerScript.close(); @@ -128,9 +128,14 @@ path GenerateModifyPath(const path& installDirectory) return modifyScriptPath; } -void GenerateDSCv3ProviderFiles(const path& installDirectory) +void GenerateDSCv3ProviderFiles(const path& installDirectory, const std::wstring_view subDirectory) { path dscResourceExecutablePath = installDirectory; + if (!subDirectory.empty()) + { + dscResourceExecutablePath /= subDirectory; + std::filesystem::create_directories(dscResourceExecutablePath); + } dscResourceExecutablePath /= "AppInstallerTestResource.exe"; WCHAR currentExecutable[MAX_PATH]; @@ -139,6 +144,10 @@ void GenerateDSCv3ProviderFiles(const path& installDirectory) copy_file(currentExecutablePath, dscResourceExecutablePath); path dscResourceManifestPath = installDirectory; + if (!subDirectory.empty()) + { + dscResourceManifestPath /= subDirectory; + } dscResourceManifestPath /= "AppInstallerTest.dsc.resource.json"; std::wstring DscResourceJsonContent = @@ -205,7 +214,15 @@ void GenerateDSCv3ProviderFiles(const path& installDirectory) } } }, - "type" : "AppInstallerTest/TestResource", + "type" : "AppInstallerTest/TestResource)"; + + if (!subDirectory.empty()) + { + DscResourceJsonContent += '.'; + DscResourceJsonContent += subDirectory; + } + + DscResourceJsonContent += LR"(", "version" : "1.0.0" } )"; @@ -417,7 +434,8 @@ void HandleInstallationOperation( if (generateDscResourceFiles) { - GenerateDSCv3ProviderFiles(installDirectory); + GenerateDSCv3ProviderFiles(installDirectory, {}); + GenerateDSCv3ProviderFiles(installDirectory, DscSubDirectoryName); } path uninstallerPath = GenerateUninstaller(out, installDirectory, productCode, useHKLM); From f805d9bee1aabaa780e45b7619efb96d52096564 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 6 Jan 2026 12:40:29 -0600 Subject: [PATCH 5/6] [v1.12] Fix Font feature property name (#5947) Backports the same changes as https://github.com/microsoft/winget-cli/pull/5946 for whenever the next cut of 1.12 is made --- doc/ReleaseNotes.md | 1 + src/AppInstallerCommonCore/ExperimentalFeature.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 3157b744f6..b70594ffef 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -8,6 +8,7 @@ * Manifest validation no longer fails using `UTF-8 BOM` encoding when the schema header is on the first line * Upgrading a portable package with dev mode disabled will no longer remove the package from the PATH variable. * Fixed source open failure when there were multiple sources but less than two non-explicit sources. +* Corrected property of `Font` experimental feature to accurately reflect `fonts` as the required setting value ## Font Support Font Install and Uninstall via manifest and package source for user and machine scopes has been added. diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index caeb32d0fe..6996f834f6 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -76,8 +76,8 @@ namespace AppInstaller::Settings case Feature::Resume: return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Font: - return ExperimentalFeature{ "Font", "Font", "https://aka.ms/winget-settings", Feature::Font }; - + return ExperimentalFeature{ "Font", "fonts", "https://aka.ms/winget-settings", Feature::Font }; + default: THROW_HR(E_UNEXPECTED); } From 2c7a2fc2fccfb60e4b4de7dbd363e19f1a477365 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 14 Jan 2026 22:46:20 -0800 Subject: [PATCH 6/6] Fixes for updating winget from winget (1.12) (#5978) CP of #5972 onto 1.12 ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/5978) --- .../Commands/TestCommand.cpp | 24 +- src/AppInstallerCLICore/ExecutionContext.h | 6 - .../Public/ShutdownMonitoring.h | 13 +- .../ShutdownMonitoring.cpp | 49 ++-- .../AppShutdownTests.cs | 4 +- .../AppInstallerCLITests.vcxproj | 6 + src/AppInstallerCLITests/CompositeSource.cpp | 177 +++++++++++++- .../Manifest-MSIX-in-AppsAndFeatures.yaml | 42 ++++ .../TestData/Manifest-MSIX-in-Archive.yaml | 34 +++ src/AppInstallerCLITests/YamlManifest.cpp | 19 ++ src/AppInstallerCommonCore/MSStore.cpp | 228 ++++++++++-------- .../Manifest/ManifestCommon.cpp | 16 +- .../Manifest/ManifestValidation.cpp | 2 +- .../Manifest/ManifestYamlPopulator.cpp | 35 +-- .../Public/winget/MSStore.h | 1 - .../Public/winget/ManifestCommon.h | 3 + .../CompositeSource.cpp | 1 + 17 files changed, 482 insertions(+), 178 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/Manifest-MSIX-in-AppsAndFeatures.yaml create mode 100644 src/AppInstallerCLITests/TestData/Manifest-MSIX-in-Archive.yaml diff --git a/src/AppInstallerCLICore/Commands/TestCommand.cpp b/src/AppInstallerCLICore/Commands/TestCommand.cpp index 2063b0d3cf..ebcbcf0bf4 100644 --- a/src/AppInstallerCLICore/Commands/TestCommand.cpp +++ b/src/AppInstallerCLICore/Commands/TestCommand.cpp @@ -31,7 +31,7 @@ namespace AppInstaller::CLI HRESULT WaitForShutdown(Execution::Context& context) { LogAndReport(context, "Waiting for app shutdown event"); - if (!ShutdownMonitoring::TerminationSignalHandler::Instance()->WaitForAppShutdownEvent()) + if (!ShutdownMonitoring::ServerShutdownSynchronization::WaitForShutdown(300000)) { LogAndReport(context, "Failed getting app shutdown event"); return APPINSTALLER_CLI_ERROR_INTERNAL_ERROR; @@ -82,6 +82,21 @@ namespace AppInstaller::CLI return hr; } + void AppShutdownTestSystemBlockNewWork(CancelReason reason) + { + AICLI_LOG(CLI, Info, << "AppShutdownTestSystemBlockNewWork :: " << reason); + } + + void AppShutdownTestSystemBeginShutdown(CancelReason reason) + { + AICLI_LOG(CLI, Info, << "AppShutdownTestSystemBeginShutdown :: " << reason); + } + + void AppShutdownTestSystemWait() + { + AICLI_LOG(CLI, Info, << "AppShutdownTestSystemWait"); + } + void EnsureDSCv3Processor(Execution::Context& context) { auto& configurationSet = context.Get().Set(); @@ -354,6 +369,13 @@ namespace AppInstaller::CLI { HRESULT hr = E_FAIL; + ShutdownMonitoring::ServerShutdownSynchronization::ComponentSystem appShutdownTestSystem{}; + appShutdownTestSystem.BlockNewWork = AppShutdownTestSystemBlockNewWork; + appShutdownTestSystem.BeginShutdown = AppShutdownTestSystemBeginShutdown; + appShutdownTestSystem.Wait = AppShutdownTestSystemWait; + + ShutdownMonitoring::ServerShutdownSynchronization::AddComponent(appShutdownTestSystem); + // Only package context and admin won't create the window message. if (!Runtime::IsRunningInPackagedContext() || !Runtime::IsRunningAsAdmin()) { diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index 3bda124a46..339245d0d5 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -80,12 +80,6 @@ namespace AppInstaller::CLI::Execution DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); -#ifndef AICLI_DISABLE_TEST_HOOKS - HWND GetWindowHandle(); - - bool WaitForAppShutdownEvent(); -#endif - // Callback to log data actions. void ContextEnumBasedVariantMapActionCallback(const void* map, Data data, EnumBasedVariantMapAction action); diff --git a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h index 77599c3021..912903f0f8 100644 --- a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h +++ b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h @@ -3,10 +3,10 @@ #pragma once #include #include -#include #include #include #include +#include namespace AppInstaller::ShutdownMonitoring { @@ -32,9 +32,6 @@ namespace AppInstaller::ShutdownMonitoring #ifndef AICLI_DISABLE_TEST_HOOKS // Gets the window handle for the message window. HWND GetWindowHandle() const; - - // Waits for the shutdown event. - bool WaitForAppShutdownEvent() const; #endif private: @@ -52,17 +49,11 @@ namespace AppInstaller::ShutdownMonitoring void CreateWindowAndStartMessageLoop(); -#ifndef AICLI_DISABLE_TEST_HOOKS - wil::unique_event m_appShutdownEvent; -#endif - std::mutex m_listenersLock; std::vector m_listeners; wil::unique_event m_messageQueueReady; wil::unique_hwnd m_windowHandle; std::thread m_windowThread; - winrt::Windows::ApplicationModel::PackageCatalog m_catalog = nullptr; - decltype(winrt::Windows::ApplicationModel::PackageCatalog{ nullptr }.PackageUpdating(winrt::auto_revoke, nullptr)) m_updatingEvent; }; // Coordinates shutdown across server components @@ -91,7 +82,7 @@ namespace AppInstaller::ShutdownMonitoring static void AddComponent(const ComponentSystem& component); // Waits for the shutdown to complete. - static void WaitForShutdown(); + static bool WaitForShutdown(std::optional timeout = std::nullopt); // Listens for a termination signal. void Cancel(CancelReason reason, bool force) override; diff --git a/src/AppInstallerCLICore/ShutdownMonitoring.cpp b/src/AppInstallerCLICore/ShutdownMonitoring.cpp index c7122bf66f..bd03ab1f3a 100644 --- a/src/AppInstallerCLICore/ShutdownMonitoring.cpp +++ b/src/AppInstallerCLICore/ShutdownMonitoring.cpp @@ -63,30 +63,10 @@ namespace AppInstaller::ShutdownMonitoring { return m_windowHandle.get(); } - - bool TerminationSignalHandler::WaitForAppShutdownEvent() const - { - return m_appShutdownEvent.wait(60000); - } #endif TerminationSignalHandler::TerminationSignalHandler() { -#ifndef AICLI_DISABLE_TEST_HOOKS - m_appShutdownEvent.create(); -#endif - - if (Runtime::IsRunningInPackagedContext()) - { - // Create package update listener - m_catalog = winrt::Windows::ApplicationModel::PackageCatalog::OpenForCurrentPackage(); - m_updatingEvent = m_catalog.PackageUpdating( - winrt::auto_revoke, [this](winrt::Windows::ApplicationModel::PackageCatalog, winrt::Windows::ApplicationModel::PackageUpdatingEventArgs) - { - this->StartAppShutdown(); - }); - } - // Create message only window. m_messageQueueReady.create(); m_windowThread = std::thread(&TerminationSignalHandler::CreateWindowAndStartMessageLoop, this); @@ -120,10 +100,6 @@ namespace AppInstaller::ShutdownMonitoring { AICLI_LOG(CLI, Info, << "Initiating shutdown procedure"); -#ifndef AICLI_DISABLE_TEST_HOOKS - m_appShutdownEvent.SetEvent(); -#endif - // Lifetime manager sends CTRL-C after the WM_QUERYENDSESSION is processed. // If we disable the CTRL-C handler, the default handler will kill us. InformListeners(CancelReason::AppShutdown, true); @@ -306,20 +282,27 @@ namespace AppInstaller::ShutdownMonitoring instance.m_components.push_back(component); } - void ServerShutdownSynchronization::WaitForShutdown() + bool ServerShutdownSynchronization::WaitForShutdown(std::optional timeout) { ServerShutdownSynchronization& instance = Instance(); + if (timeout) + { + return instance.m_shutdownComplete.wait(timeout.value()); + } + else { - std::lock_guard lock{ instance.m_threadLock }; - if (!instance.m_shutdownThread.joinable()) { - AICLI_LOG(Core, Warning, << "Attempt to wait for shutdown when shutdown has not been initiated."); - return; + std::lock_guard lock{ instance.m_threadLock }; + if (!instance.m_shutdownThread.joinable()) + { + AICLI_LOG(Core, Warning, << "Attempt to wait for shutdown when shutdown has not been initiated."); + return false; + } } - } - instance.m_shutdownComplete.wait(); + return instance.m_shutdownComplete.wait(); + } } void ServerShutdownSynchronization::Cancel(CancelReason reason, bool) @@ -362,6 +345,7 @@ namespace AppInstaller::ShutdownMonitoring components = m_components; } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: BlockNewWork"); for (const auto& component : components) { if (component.BlockNewWork) @@ -370,6 +354,7 @@ namespace AppInstaller::ShutdownMonitoring } } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: BeginShutdown"); for (const auto& component : components) { if (component.BeginShutdown) @@ -378,6 +363,7 @@ namespace AppInstaller::ShutdownMonitoring } } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: Wait"); for (const auto& component : components) { if (component.Wait) @@ -386,6 +372,7 @@ namespace AppInstaller::ShutdownMonitoring } } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: ShutdownCompleteCallback"); ShutdownCompleteCallback callback = m_callback; if (callback) { diff --git a/src/AppInstallerCLIE2ETests/AppShutdownTests.cs b/src/AppInstallerCLIE2ETests/AppShutdownTests.cs index 2864c4e991..7c83c13549 100644 --- a/src/AppInstallerCLIE2ETests/AppShutdownTests.cs +++ b/src/AppInstallerCLIE2ETests/AppShutdownTests.cs @@ -24,6 +24,7 @@ public class AppShutdownTests /// Runs winget test appshutdown and register the application to force a WM_QUERYENDSESSION message. /// [Test] + [Ignore("This test relied on a signal to terminate that was determined to be problematic. We may need OS fixes to test it when elevated.")] public void RegisterApplicationTest() { if (!TestSetup.Parameters.PackagedContext) @@ -95,9 +96,6 @@ public void RegisterApplicationTest() Task.WaitAll(new Task[] { testCmdTask, registerTask }, 360000); - // Assert.True(registerTask.Result); - TestContext.Out.Write(testCmdTask.Result.StdOut); - // The ctrl-c command terminates the batch file before the exit code file gets created. // Look for the output. Assert.True(testCmdTask.Result.StdOut.Contains("Succeeded waiting for app shutdown event")); diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 7bbd210747..970d0a628f 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -1053,6 +1053,12 @@ true + + true + + + true + diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp index 24659ea796..94d3523d70 100644 --- a/src/AppInstallerCLITests/CompositeSource.cpp +++ b/src/AppInstallerCLITests/CompositeSource.cpp @@ -73,47 +73,79 @@ Manifest::Manifest MakeDefaultManifest(std::string_view version = "1.0"sv) return result; } -struct TestPackageHelper +struct TestManifestHelper { - TestPackageHelper(bool isInstalled, std::shared_ptr source = {}) : - m_isInstalled(isInstalled), m_manifest(MakeDefaultManifest()), m_source(source) {} + TestManifestHelper() : m_manifest(MakeDefaultManifest()) {} - TestPackageHelper& WithId(const std::string& id) + TestManifestHelper& WithId(const std::string& id) { m_manifest.Id = id; return *this; } - TestPackageHelper& WithVersion(std::string_view version) + TestManifestHelper& WithVersion(std::string_view version) { m_manifest.Version = version; return *this; } - TestPackageHelper& WithChannel(const std::string& channel) + TestManifestHelper& WithChannel(const std::string& channel) { m_manifest.Channel = channel; return *this; } - TestPackageHelper& WithDefaultName(std::string_view name) + TestManifestHelper& WithDefaultName(std::string_view name) { m_manifest.DefaultLocalization.Add(std::string{ name }); return *this; } - TestPackageHelper& WithPFN(const std::string& pfn) + TestManifestHelper& WithPFN(const std::string& pfn) { m_manifest.Installers[0].PackageFamilyName = pfn; return *this; } - TestPackageHelper& WithPC(const std::string& pc) + TestManifestHelper& WithPC(const std::string& pc) { m_manifest.Installers[0].ProductCode = pc; return *this; } + TestManifestHelper& WithType(Manifest::InstallerTypeEnum type) + { + m_manifest.Installers[0].BaseInstallerType = type; + return *this; + } + + TestManifestHelper& WithDisplayVersion(std::string_view version) + { + if (m_manifest.Installers[0].AppsAndFeaturesEntries.empty()) + { + m_manifest.Installers[0].AppsAndFeaturesEntries.emplace_back(); + } + m_manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayVersion = version; + return *this; + } + + operator const Manifest::Manifest& () const + { + return m_manifest; + } + +private: + Manifest::Manifest m_manifest; +}; + +struct TestPackageHelper +{ + TestPackageHelper(bool isInstalled, std::shared_ptr source = {}) : + m_isInstalled(isInstalled), m_source(source) + { + m_manifestHelpers.emplace_back(); + } + TestPackageHelper& HideSRS(bool value = true) { m_hideSystemReferenceStrings = value; @@ -126,11 +158,18 @@ struct TestPackageHelper { if (m_isInstalled) { - m_package = TestCompositePackage::Make(m_manifest, TestCompositePackage::MetadataMap{}, std::vector(), m_source); + m_package = TestCompositePackage::Make(this->operator const Manifest::Manifest&(), m_metadata, std::vector(), m_source); } else { - m_package = TestCompositePackage::Make(std::vector{ m_manifest }, m_source, m_hideSystemReferenceStrings); + std::vector manifests; + + for (const auto& helper : m_manifestHelpers) + { + manifests.emplace_back(helper.operator const AppInstaller::Manifest::Manifest &()); + } + + m_package = TestCompositePackage::Make(manifests, m_source, m_hideSystemReferenceStrings); } } @@ -142,17 +181,89 @@ struct TestPackageHelper return ToPackage(); } + TestPackageHelper& WithId(const std::string& id) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithId(id); + return *this; + } + + TestPackageHelper& WithVersion(std::string_view version) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithVersion(version); + return *this; + } + + TestPackageHelper& WithChannel(const std::string& channel) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithChannel(channel); + return *this; + } + + TestPackageHelper& WithDefaultName(std::string_view name) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithDefaultName(name); + return *this; + } + + TestPackageHelper& WithPFN(const std::string& pfn) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithPFN(pfn); + return *this; + } + + TestPackageHelper& WithPC(const std::string& pc) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithPC(pc); + return *this; + } + + TestPackageHelper& WithType(Manifest::InstallerTypeEnum type) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithType(type); + return *this; + } + + TestPackageHelper& WithDisplayVersion(std::string_view version) + { + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + m_manifestHelpers[0].WithDisplayVersion(version); + return *this; + } + + TestPackageHelper& WithMetadata(PackageVersionMetadata metadata, const std::string& value) + { + THROW_HR_IF(E_UNEXPECTED, !m_isInstalled); + m_metadata[metadata] = value; + return *this; + } + operator const Manifest::Manifest& () const { - return m_manifest; + THROW_HR_IF(E_UNEXPECTED, m_manifestHelpers.size() != 1); + return m_manifestHelpers[0]; + } + + TestManifestHelper& MakeManifest() + { + THROW_HR_IF(E_UNEXPECTED, m_isInstalled); + m_manifestHelpers.emplace_back(); + return m_manifestHelpers.back(); } private: bool m_isInstalled; - Manifest::Manifest m_manifest; + std::vector m_manifestHelpers; std::shared_ptr m_source; std::shared_ptr m_package; bool m_hideSystemReferenceStrings = false; + TestCompositePackage::MetadataMap m_metadata; }; // A helper to create the sources used by the majority of tests in this file. @@ -1831,3 +1942,43 @@ TEST_CASE("CompositeSource_SxS_Available_TwoVersions_SameAvailable", "[Composite REQUIRE(availablePackages.size() == 1); REQUIRE(availablePackages[0]->IsSame(availablePackage->Available[0].get())); } + +TEST_CASE("CompositeSource_MappedVersions_ProperSorting", "[CompositeSource]") +{ + std::string installedID = "Installed.Id"; + std::string availableID = "Available.Id"; + auto type = Manifest::InstallerTypeEnum::Exe; + std::string pfn = "MY_PFN"; + std::string version1 = "1000.0"; + std::string version2 = "2000.0"; + std::string versionMapped1 = "1.0"; + std::string versionMapped2 = "2.0"; + + CompositeTestSetup setup; + + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithId(installedID).WithPFN(pfn).WithVersion(version1).WithMetadata(PackageVersionMetadata::InstalledType, "exe"), Criteria()); + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithId(installedID).WithPFN(pfn).WithVersion(version2).WithMetadata(PackageVersionMetadata::InstalledType, "exe"), Criteria()); + + setup.Available->SearchFunction = [&](const SearchRequest&) + { + auto package = setup.MakeAvailable(); + package.WithId(availableID).WithType(type).WithPFN(pfn).WithVersion(versionMapped1).WithDisplayVersion(version1); + package.MakeManifest().WithId(availableID).WithType(type).WithPFN(pfn).WithVersion(versionMapped2).WithDisplayVersion(version2); + + SearchResult result; + result.Matches.emplace_back(package, Criteria()); + return result; + }; + + SearchResult result = setup.Search(true); + + 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(installedVersions[0].Version == versionMapped2); + REQUIRE(installedVersions[1].Version == versionMapped1); +} diff --git a/src/AppInstallerCLITests/TestData/Manifest-MSIX-in-AppsAndFeatures.yaml b/src/AppInstallerCLITests/TestData/Manifest-MSIX-in-AppsAndFeatures.yaml new file mode 100644 index 0000000000..4a0503d09f --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-MSIX-in-AppsAndFeatures.yaml @@ -0,0 +1,42 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.12.0.schema.json + +PackageIdentifier: Microsoft.ArchiveContainingMSIX +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: Archive with nested MSIX +Description: A manifest containing an archive with a nested MSIX installer +InstallerLocale: en-US +InstallerType: exe +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +AppsAndFeaturesEntries: + - DisplayName: DisplayName + InstallerType: msix +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log= + InstallLocation: /dir= + Upgrade: /upgrade + Repair: /repair + +Installers: + - Architecture: x86 + InstallerLocale: en-GB + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + +ManifestType: singleton +ManifestVersion: 1.12.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-MSIX-in-Archive.yaml b/src/AppInstallerCLITests/TestData/Manifest-MSIX-in-Archive.yaml new file mode 100644 index 0000000000..02f5072321 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-MSIX-in-Archive.yaml @@ -0,0 +1,34 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.12.0.schema.json + +PackageIdentifier: Microsoft.ArchiveContainingMSIX +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: Archive with nested MSIX +Description: A manifest containing an archive with a nested MSIX installer +InstallerLocale: en-US +InstallerType: zip +NestedInstallerType: msix +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +NestedInstallerFiles: + - RelativeFilePath: RelativeFilePath + PortableCommandAlias: PortableCommandAlias + +Installers: + - Architecture: x86 + InstallerLocale: en-GB + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + +ManifestType: singleton +ManifestVersion: 1.12.0 diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index ce1b23b757..fac1eee06e 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -2085,3 +2085,22 @@ TEST_CASE("ShadowManifest_NotVerifiedPublisher", "[ShadowManifest]") TempFile mergedManifestFile{ "merged.yaml" }; REQUIRE_THROWS_MATCHES(YamlParser::CreateFromPath(multiFileDirectory, validateOption, mergedManifestFile), ManifestException, ManifestExceptionMatcher("Field usage requires verified publishers. [Icons]")); } + +TEST_CASE("Manifest_PackageFamilyNameInheritance", "[ManifestValidation]") +{ + std::filesystem::path testManifest; + + SECTION("MSIX inside Archive") + { + testManifest = "Manifest-MSIX-in-Archive.yaml"; + } + SECTION("MSIX in AppsAndFeatures") + { + testManifest = "Manifest-MSIX-in-AppsAndFeatures.yaml"; + } + + auto manifest = YamlParser::CreateFromPath(TestDataFile(testManifest), GetTestManifestValidateOption()); + + REQUIRE(!manifest.Installers.empty()); + REQUIRE(!manifest.Installers[0].PackageFamilyName.empty()); +} diff --git a/src/AppInstallerCommonCore/MSStore.cpp b/src/AppInstallerCommonCore/MSStore.cpp index 81b3b21db1..6b8f458072 100644 --- a/src/AppInstallerCommonCore/MSStore.cpp +++ b/src/AppInstallerCommonCore/MSStore.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace AppInstaller::MSStore { @@ -176,6 +177,133 @@ namespace AppInstaller::MSStore return S_FALSE; } + + // Used to detect a signal that a package update is being requested so that we can early out + // on an attempt to update ourself. This is only needed for elevated processes because the + // standard shutdown signals are not sent to elevated processes in the same manner. + struct PackageUpdateMonitor + { + PackageUpdateMonitor() + { + if (Runtime::IsRunningAsAdmin() && Runtime::IsRunningInPackagedContext()) + { + m_catalog = winrt::Windows::ApplicationModel::PackageCatalog::OpenForCurrentPackage(); + m_updatingEvent = m_catalog.PackageUpdating( + winrt::auto_revoke, [this](winrt::Windows::ApplicationModel::PackageCatalog, winrt::Windows::ApplicationModel::PackageUpdatingEventArgs args) + { + // Deployment always sends a value of 0 before doing any work and a value of 100 when completely done. + constexpr double minProgress = 0; + auto progress = args.Progress(); + if (progress > minProgress) + { + m_isUpdating = true; + } + }); + } + } + + bool IsUpdating() const + { + return m_isUpdating; + } + + private: + winrt::Windows::ApplicationModel::PackageCatalog m_catalog = nullptr; + decltype(winrt::Windows::ApplicationModel::PackageCatalog{ nullptr }.PackageUpdating(winrt::auto_revoke, nullptr)) m_updatingEvent; + std::atomic_bool m_isUpdating = false; + }; + + HRESULT WaitForOperation(const std::wstring& productId, bool isSilentMode, IVectorView& installItems, IProgressCallback& progress, const PackageUpdateMonitor& monitor) + { + auto cancelIfOperationFailed = wil::scope_exit( + [&]() + { + try + { + AppInstallManager installManager; + installManager.Cancel(productId); + } + CATCH_LOG(); + }); + + for (auto const& installItem : installItems) + { + AICLI_LOG(Core, Info, << + "Started MSStore package execution. ProductId: " << Utility::ConvertToUTF8(installItem.ProductId()) << + " PackageFamilyName: " << Utility::ConvertToUTF8(installItem.PackageFamilyName())); + + if (isSilentMode) + { + installItem.InstallInProgressToastNotificationMode(AppInstallationToastNotificationMode::NoToast); + installItem.CompletedInstallToastNotificationMode(AppInstallationToastNotificationMode::NoToast); + } + } + + HRESULT errorCode = S_OK; + + // We are aggregating all AppInstallItem progresses into one. + // Averaging every progress for now until we have a better way to find overall progress. + uint64_t overallProgressMax = 100 * static_cast(installItems.Size()); + uint64_t currentProgress = 0; + + while (currentProgress < overallProgressMax) + { + currentProgress = 0; + + for (auto const& installItem : installItems) + { + const auto& status = installItem.GetCurrentStatus(); + currentProgress += static_cast(status.PercentComplete()); + + errorCode = status.ErrorCode(); + + if (!SUCCEEDED(errorCode)) + { + return errorCode; + } + } + + // It may take a while for Store client to pick up the install request. + // So we show indefinite progress here to avoid a progress bar stuck at 0. + if (currentProgress > 0) + { + progress.OnProgress(currentProgress, overallProgressMax, ProgressType::Percent); + } + + if (progress.IsCancelledBy(CancelReason::User)) + { + for (auto const& installItem : installItems) + { + installItem.Cancel(); + } + } + + // If app shutdown then we have 30s to keep installing, keep going and hope for the best. + else if (progress.IsCancelledBy(CancelReason::AppShutdown) || monitor.IsUpdating()) + { + for (auto const& installItem : installItems) + { + // Insert spiderman meme. + if (installItem.ProductId() == std::wstring{ s_AppInstallerProductId }) + { + AICLI_LOG(Core, Info, << "Asked to shutdown while installing AppInstaller."); + progress.OnProgress(overallProgressMax, overallProgressMax, ProgressType::Percent); + cancelIfOperationFailed.release(); + return S_OK; + } + } + } + + Sleep(100); + } + + if (SUCCEEDED(errorCode)) + { + cancelIfOperationFailed.release(); + } + + return errorCode; + } } HRESULT MSStoreOperation::StartAndWaitForOperation(IProgressCallback& progress) @@ -195,6 +323,8 @@ namespace AppInstaller::MSStore HRESULT MSStoreOperation::InstallPackage(IProgressCallback& progress) { + PackageUpdateMonitor monitor; + AppInstallManager installManager; AppInstallOptions installOptions; @@ -248,11 +378,13 @@ namespace AppInstaller::MSStore installOptions).get(); } - return WaitForOperation(installItems, progress); + return WaitForOperation(m_productId, m_isSilentMode, installItems, progress, monitor); } HRESULT MSStoreOperation::UpdatePackage(IProgressCallback& progress) { + PackageUpdateMonitor monitor; + AppInstallManager installManager; AppUpdateOptions updateOptions; updateOptions.AllowForcedAppRestart(m_force); @@ -300,98 +432,6 @@ namespace AppInstaller::MSStore installItems = winrt::single_threaded_vector(std::move(installItemVector)).GetView(); } - return WaitForOperation(installItems, progress); - } - - HRESULT MSStoreOperation::WaitForOperation(IVectorView& installItems, IProgressCallback& progress) - { - auto cancelIfOperationFailed = wil::scope_exit( - [&]() - { - try - { - AppInstallManager installManager; - installManager.Cancel(m_productId); - } - CATCH_LOG(); - }); - - for (auto const& installItem : installItems) - { - AICLI_LOG(Core, Info, << - "Started MSStore package execution. ProductId: " << Utility::ConvertToUTF8(installItem.ProductId()) << - " PackageFamilyName: " << Utility::ConvertToUTF8(installItem.PackageFamilyName())); - - if (m_isSilentMode) - { - installItem.InstallInProgressToastNotificationMode(AppInstallationToastNotificationMode::NoToast); - installItem.CompletedInstallToastNotificationMode(AppInstallationToastNotificationMode::NoToast); - } - } - - HRESULT errorCode = S_OK; - - // We are aggregating all AppInstallItem progresses into one. - // Averaging every progress for now until we have a better way to find overall progress. - uint64_t overallProgressMax = 100 * static_cast(installItems.Size()); - uint64_t currentProgress = 0; - - while (currentProgress < overallProgressMax) - { - currentProgress = 0; - - for (auto const& installItem : installItems) - { - const auto& status = installItem.GetCurrentStatus(); - currentProgress += static_cast(status.PercentComplete()); - - errorCode = status.ErrorCode(); - - if (!SUCCEEDED(errorCode)) - { - return errorCode; - } - } - - // It may take a while for Store client to pick up the install request. - // So we show indefinite progress here to avoid a progress bar stuck at 0. - if (currentProgress > 0) - { - progress.OnProgress(currentProgress, overallProgressMax, ProgressType::Percent); - } - - if (progress.IsCancelledBy(CancelReason::User)) - { - for (auto const& installItem : installItems) - { - installItem.Cancel(); - } - } - - // If app shutdown then we have 30s to keep installing, keep going and hope for the best. - else if (progress.IsCancelledBy(CancelReason::AppShutdown)) - { - for (auto const& installItem : installItems) - { - // Insert spiderman meme. - if (installItem.ProductId() == std::wstring{ s_AppInstallerProductId }) - { - AICLI_LOG(Core, Info, << "Asked to shutdown while installing AppInstaller."); - progress.OnProgress(overallProgressMax, overallProgressMax, ProgressType::Percent); - cancelIfOperationFailed.release(); - return S_OK; - } - } - } - - Sleep(100); - } - - if (SUCCEEDED(errorCode)) - { - cancelIfOperationFailed.release(); - } - - return errorCode; + return WaitForOperation(m_productId, m_isSilentMode, installItems, progress, monitor); } } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp index 1fdddaec90..544c82e36c 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -885,6 +885,19 @@ namespace AppInstaller::Manifest return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); } + bool DoAnyAppsAndFeaturesEntriesUsePackageFamilyName(const std::vector& entries) + { + for (const AppsAndFeaturesEntry& entry : entries) + { + if (DoesInstallerTypeUsePackageFamilyName(entry.InstallerType)) + { + return true; + } + } + + return false; + } + bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType) { return ( @@ -919,7 +932,8 @@ namespace AppInstaller::Manifest installerType == InstallerTypeEnum::Msi || installerType == InstallerTypeEnum::Nullsoft || installerType == InstallerTypeEnum::Wix || - installerType == InstallerTypeEnum::Burn + installerType == InstallerTypeEnum::Burn || + installerType == InstallerTypeEnum::Msix ); } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index 586d8b7deb..c9f87c516a 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -197,7 +197,7 @@ namespace AppInstaller::Manifest // Validate system reference strings if they are set at the installer level // Allow PackageFamilyName to be declared with non msix installers to support nested installer scenarios. But still report as warning to notify user of this uncommon case. - if (!installer.PackageFamilyName.empty() && !DoesInstallerTypeUsePackageFamilyName(installer.EffectiveInstallerType())) + if (!installer.PackageFamilyName.empty() && !(DoesInstallerTypeUsePackageFamilyName(installer.EffectiveInstallerType()) || DoAnyAppsAndFeaturesEntriesUsePackageFamilyName(installer.AppsAndFeaturesEntries))) { resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportPackageFamilyName, "InstallerType", std::string{ InstallerTypeToString(installer.EffectiveInstallerType()) }, ValidationError::Level::Warning); } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp index 2d46f60403..aef14781b6 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -1140,22 +1140,7 @@ namespace AppInstaller::Manifest auto errors = ValidateAndProcessFields(entry, InstallerFieldInfos, VariantManifestPtr(&installer)); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - // Copy in system reference strings from the root if not set in the installer and appropriate - if (installer.PackageFamilyName.empty() && DoesInstallerTypeUsePackageFamilyName(installer.EffectiveInstallerType())) - { - installer.PackageFamilyName = m_manifest.get().DefaultInstallerInfo.PackageFamilyName; - } - - if (installer.ProductCode.empty() && DoesInstallerTypeUseProductCode(installer.EffectiveInstallerType())) - { - installer.ProductCode = m_manifest.get().DefaultInstallerInfo.ProductCode; - } - - if (installer.AppsAndFeaturesEntries.empty() && DoesInstallerTypeWriteAppsAndFeaturesEntry(installer.EffectiveInstallerType())) - { - installer.AppsAndFeaturesEntries = m_manifest.get().DefaultInstallerInfo.AppsAndFeaturesEntries; - } - + // Set installer type back before attempting to use it in any of the EffectiveInstallerType calls below if (IsArchiveType(installer.BaseInstallerType)) { if (installer.NestedInstallerFiles.empty()) @@ -1169,6 +1154,24 @@ namespace AppInstaller::Manifest } } + // Copy in system reference strings from the root if not set in the installer and appropriate + if (installer.AppsAndFeaturesEntries.empty() && DoesInstallerTypeWriteAppsAndFeaturesEntry(installer.EffectiveInstallerType())) + { + installer.AppsAndFeaturesEntries = m_manifest.get().DefaultInstallerInfo.AppsAndFeaturesEntries; + } + + if (installer.PackageFamilyName.empty() && + (DoesInstallerTypeUsePackageFamilyName(installer.EffectiveInstallerType()) || + DoAnyAppsAndFeaturesEntriesUsePackageFamilyName(installer.AppsAndFeaturesEntries))) + { + installer.PackageFamilyName = m_manifest.get().DefaultInstallerInfo.PackageFamilyName; + } + + if (installer.ProductCode.empty() && DoesInstallerTypeUseProductCode(installer.EffectiveInstallerType())) + { + installer.ProductCode = m_manifest.get().DefaultInstallerInfo.ProductCode; + } + // If there are no dependencies on installer use default ones if (!installer.Dependencies.HasAny()) { diff --git a/src/AppInstallerCommonCore/Public/winget/MSStore.h b/src/AppInstallerCommonCore/Public/winget/MSStore.h index 55540b28bc..8dd67491a4 100644 --- a/src/AppInstallerCommonCore/Public/winget/MSStore.h +++ b/src/AppInstallerCommonCore/Public/winget/MSStore.h @@ -39,7 +39,6 @@ namespace AppInstaller::MSStore private: HRESULT InstallPackage(IProgressCallback& progress); HRESULT UpdatePackage(IProgressCallback& progress); - HRESULT WaitForOperation(winrt::Windows::Foundation::Collections::IVectorView& installItems, IProgressCallback& progress); MSStoreOperationType m_type; std::wstring m_productId; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index 39c6ff9685..f30f4ae8d6 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -434,6 +434,9 @@ namespace AppInstaller::Manifest // Gets a value indicating whether the given installer uses the PackageFamilyName system reference. bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); + // Gets a value indicating whether any of the ARP entries uses the PackageFamilyName system reference. + bool DoAnyAppsAndFeaturesEntriesUsePackageFamilyName(const std::vector& entries); + // Gets a value indicating whether the given installer uses the ProductCode system reference. bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index d8f81eb923..78c0ad5e22 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -550,6 +550,7 @@ namespace AppInstaller::Repository if (Manifest::DoesInstallerTypeSupportArpVersionRange(key.InstalledType)) { key.Version = GetMappedInstalledVersion(key.InstalledVersion->GetProperty(PackageVersionProperty::Version), availablePackage); + key.VersionAndChannel = Utility::VersionAndChannel{ key.Version, key.Channel }; } } }