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/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/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 cb581116ec..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); @@ -101,6 +81,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()) { @@ -118,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); @@ -304,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) @@ -360,6 +345,7 @@ namespace AppInstaller::ShutdownMonitoring components = m_components; } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: BlockNewWork"); for (const auto& component : components) { if (component.BlockNewWork) @@ -368,6 +354,7 @@ namespace AppInstaller::ShutdownMonitoring } } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: BeginShutdown"); for (const auto& component : components) { if (component.BeginShutdown) @@ -376,6 +363,7 @@ namespace AppInstaller::ShutdownMonitoring } } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: Wait"); for (const auto& component : components) { if (component.Wait) @@ -384,6 +372,7 @@ namespace AppInstaller::ShutdownMonitoring } } + AICLI_LOG(CLI, Verbose, << "ServerShutdownSynchronization :: ShutdownCompleteCallback"); ShutdownCompleteCallback callback = m_callback; if (callback) { 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/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/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/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/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/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/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); } 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 }; } } } 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); 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 + 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)); } ///