commit df76964b0cbfd40bb2d8a430537961576b333aa1 parent 0c4145ca63068d561c865912fb83ae0b77efc77f Author: KEINOS <github+fork-qiita-news@keinos.com> Date: Wed, 13 Aug 2025 22:27:07 +0000 Merge remote-tracking branch 'upstream/master' Diffstat:
42 files changed, 2108 insertions(+), 346 deletions(-)
diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -209,6 +209,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "targets", "targets", "{A0B4 EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PureLib", "PureLib\PureLib.vcxitems", "{A33223D2-550B-4D99-A53D-488B1F68683E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetTestCommon", "WinGetTestCommon\WinGetTestCommon.csproj", "{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetMCPServer", "WinGetMCPServer\WinGetMCPServer.csproj", "{33745E4A-39E2-676F-7E23-50FB43848D25}" EndProject Global @@ -968,6 +972,24 @@ Global {9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x64.Build.0 = ReleaseStatic|Any CPU {9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x86.ActiveCfg = ReleaseStatic|Any CPU {9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x86.Build.0 = ReleaseStatic|Any CPU + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|ARM64.ActiveCfg = Debug|arm64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|ARM64.Build.0 = Debug|arm64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x64.ActiveCfg = Debug|x64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x64.Build.0 = Debug|x64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x86.ActiveCfg = Debug|x86 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x86.Build.0 = Debug|x86 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|ARM64.ActiveCfg = Release|arm64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|x64.ActiveCfg = Release|x64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|x86.ActiveCfg = Release|x86 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|ARM64.ActiveCfg = Release|arm64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|ARM64.Build.0 = Release|arm64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x64.ActiveCfg = Release|x64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x64.Build.0 = Release|x64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x86.ActiveCfg = Release|x86 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x86.Build.0 = Release|x86 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|ARM64.ActiveCfg = Release|arm64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|x64.ActiveCfg = Release|x64 + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|x86.ActiveCfg = Release|x86 {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|ARM64.ActiveCfg = Debug|arm64 {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|ARM64.Build.0 = Debug|arm64 {33745E4A-39E2-676F-7E23-50FB43848D25}.Debug|x64.ActiveCfg = Debug|x64 @@ -994,7 +1016,12 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {6CB84692-5994-407D-B9BD-9216AF77FE83} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {6E36DDD7-1602-474E-B1D7-D0A7E1D5AD86} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {3C0269FA-E582-4CA7-9E33-3881A005CA0C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {3E2CBA31-CEBA-4D63-BF52-49C0718E19EA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C1624B2F-2BF6-4E28-92FA-1BF85C6B62A8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {3B8466CF-4FDD-4329-9C80-91321C4AAC99} = {EA8CD934-0702-4911-A2C5-A40600E616DE} {1622DA16-914F-4F57-A259-D5169003CC8C} = {6D7776A8-42FE-46DD-B0F8-712F35EA0C79} {3BAF989F-7F65-465B-ACE8-BAFE42D1017E} = {EA8CD934-0702-4911-A2C5-A40600E616DE} @@ -1002,11 +1029,13 @@ Global {952B513F-8A00-4D74-9271-925AFB3C6252} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {2ACDE176-F13F-42FA-8159-C34FA3D37837} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {1A47951F-5C7A-4D6D-BB5F-D77484437940} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {68808357-902B-406C-8C19-E8E26A69DE8A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {409CD681-22A4-469D-88AE-CB5E4836E07A} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {B0BBBD92-943B-408F-B2B2-DBBAB4A22D23} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {31ED69A8-5310-45A9-953F-56C351D2C3E1} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {8E43F982-40D5-4DF1-9044-C08047B5F43B} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {EE43C990-7789-4A60-B077-BF0ED3D093A1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {1F56BECB-D65D-4BBA-8788-6671B251392A} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {167F634B-A3AD-494E-8E67-B888103E35FF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {C54F80ED-B736-49B0-9BD3-662F57024D01} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} @@ -1016,6 +1045,7 @@ Global {76B26B2C-602A-4AD0-9736-4162D3FCA92A} = {1A5D7A7D-5CB2-47D5-B40D-4E61CAEDC798} {A0B4F808-B190-41C4-97CB-C8EA1932F84F} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {A33223D2-550B-4D99-A53D-488B1F68683E} = {60618CAC-2995-4DF9-9914-45C6FC02C995} + {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -347,6 +347,7 @@ <ClInclude Include="ContextOrchestrator.h" /> <ClInclude Include="COMContext.h" /> <ClInclude Include="Public\ConfigurationSetProcessorFactoryRemoting.h" /> + <ClInclude Include="Public\ShutdownMonitoring.h" /> <ClInclude Include="Sixel.h" /> <ClInclude Include="Workflows\ConfigurationFlow.h" /> <ClInclude Include="Workflows\DependenciesFlow.h" /> @@ -421,6 +422,7 @@ <ClCompile Include="ConfigurationWingetDscModuleUnitValidation.cpp" /> <ClCompile Include="ConfigureExportCommand.cpp" /> <ClCompile Include="ContextOrchestrator.cpp" /> + <ClCompile Include="ShutdownMonitoring.cpp" /> <ClCompile Include="Sixel.cpp" /> <ClCompile Include="Workflows\ConfigurationFlow.cpp" /> <ClCompile Include="Workflows\DependenciesFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -293,6 +293,9 @@ <ClInclude Include="Commands\DscAdminSettingsResource.h"> <Filter>Commands\Configuration</Filter> </ClInclude> + <ClInclude Include="Public\ShutdownMonitoring.h"> + <Filter>Public</Filter> + </ClInclude> <ClInclude Include="Commands\McpCommand.h"> <Filter>Commands</Filter> </ClInclude> @@ -556,6 +559,9 @@ <ClCompile Include="Commands\DscAdminSettingsResource.cpp"> <Filter>Commands\Configuration</Filter> </ClCompile> + <ClCompile Include="ShutdownMonitoring.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="Commands\McpCommand.cpp"> <Filter>Commands</Filter> </ClCompile> diff --git a/src/AppInstallerCLICore/Commands/TestCommand.cpp b/src/AppInstallerCLICore/Commands/TestCommand.cpp @@ -8,6 +8,7 @@ #include "AppInstallerRuntime.h" #include "TableOutput.h" #include "Public/ConfigurationSetProcessorFactoryRemoting.h" +#include "Public/ShutdownMonitoring.h" #include "Workflows/ConfigurationFlow.h" #include "Workflows/MSStoreInstallerHandler.h" #include <winrt/Microsoft.Management.Configuration.h> @@ -19,16 +20,16 @@ namespace AppInstaller::CLI { namespace { - void LogAndReport(Execution::Context& context, const std::string& message) + void LogAndReport(Execution::Context& context, std::string_view message) { - AICLI_LOG(CLI, Info, << message); context.Reporter.Info() << message << std::endl; + AICLI_LOG(CLI, Info, << message); } HRESULT WaitForShutdown(Execution::Context& context) { LogAndReport(context, "Waiting for app shutdown event"); - if (!Execution::WaitForAppShutdownEvent()) + if (!ShutdownMonitoring::TerminationSignalHandler::Instance().WaitForAppShutdownEvent()) { LogAndReport(context, "Failed getting app shutdown event"); return APPINSTALLER_CLI_ERROR_INTERNAL_ERROR; @@ -40,7 +41,7 @@ namespace AppInstaller::CLI HRESULT AppShutdownWindowMessage(Execution::Context& context) { - auto windowHandle = Execution::GetWindowHandle(); + auto windowHandle = ShutdownMonitoring::TerminationSignalHandler::Instance().GetWindowHandle(); if (windowHandle == NULL) { diff --git a/src/AppInstallerCLICore/ContextOrchestrator.cpp b/src/AppInstallerCLICore/ContextOrchestrator.cpp @@ -5,6 +5,7 @@ #include "ContextOrchestrator.h" #include "COMContext.h" #include "Commands/COMCommand.h" +#include "Public/ShutdownMonitoring.h" #include "winget/UserSettings.h" #include <Commands/RootCommand.h> @@ -45,7 +46,9 @@ namespace AppInstaller::CLI::Execution return s_instance; } - ContextOrchestrator::ContextOrchestrator() + ContextOrchestrator::ContextOrchestrator() : ContextOrchestrator(std::thread::hardware_concurrency()) {} + + ContextOrchestrator::ContextOrchestrator(unsigned int hardwareConcurrency) { ProgressCallback progress; m_installingWriteableSource = Repository::Source(Repository::PredefinedSource::Installing); @@ -55,10 +58,9 @@ namespace AppInstaller::CLI::Execution // We always allow only one install at a time. // For download, if we can find the number of supported concurrent threads, // use that as the maximum (up to 3); otherwise use a single thread. - const auto supportedConcurrentThreads = std::thread::hardware_concurrency(); const UINT32 maxDownloadThreads = 3; const UINT32 operationThreads = 1; - const UINT32 downloadThreads = std::min(supportedConcurrentThreads > 1 ? supportedConcurrentThreads - 1 : 1, maxDownloadThreads); + const UINT32 downloadThreads = std::min(hardwareConcurrency > 1 ? hardwareConcurrency - 1 : 1, maxDownloadThreads); AddCommandQueue(COMDownloadCommand::CommandName, downloadThreads); AddCommandQueue(OperationCommandQueueName, operationThreads); @@ -66,7 +68,8 @@ namespace AppInstaller::CLI::Execution void ContextOrchestrator::AddCommandQueue(std::string_view commandName, UINT32 allowedThreads) { - m_commandQueues.emplace(commandName, std::make_unique<OrchestratorQueue>(commandName, allowedThreads)); + std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + m_commandQueues.emplace(commandName, std::make_unique<OrchestratorQueue>(*this, commandName, allowedThreads)); } _Requires_lock_held_(m_queueLock) @@ -84,17 +87,26 @@ namespace AppInstaller::CLI::Execution return {}; } - void ContextOrchestrator::EnqueueAndRunItem(std::shared_ptr<OrchestratorQueueItem> item) + void ContextOrchestrator::EnqueueAndRunItem(const std::shared_ptr<OrchestratorQueueItem>& item) { std::lock_guard<std::mutex> lockQueue{ m_queueLock }; if (item->IsOnFirstCommand()) { + // Directly error on attempting to enqueue first time + THROW_HR_IF(ToHRESULT(m_disabledReason), !m_enabled); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INSTALL_ALREADY_RUNNING), FindById(item->GetId())); // Log the beginning of the item item->GetContext().GetThreadGlobals().GetTelemetryLogger().LogCommand(item->GetItemCommandName()); } + else if (!m_enabled) + { + // On subsequent command enqueues, cancel and complete the item + item->GetContext().Cancel(m_disabledReason, true); + item->HandleItemCompletion(*this); + } std::string commandQueueName{ GetCommandQueueName(item->GetNextCommand().Name()) }; m_commandQueues.at(commandQueueName)->EnqueueAndRunItem(item); @@ -145,13 +157,103 @@ namespace AppInstaller::CLI::Execution } } - _Requires_lock_held_(m_queueLock) + void ContextOrchestrator::RegisterForShutdownSynchronization() + { + static std::once_flag registerComponentOnceFlag; + std::call_once(registerComponentOnceFlag, + [&]() + { + using namespace ShutdownMonitoring; + + ServerShutdownSynchronization::ComponentSystem component; + component.BlockNewWork = StaticDisable; + component.BeginShutdown = StaticCancelQueuedItems; + component.Wait = StaticWaitForRunningItems; + + ServerShutdownSynchronization::AddComponent(component); + }); + } + + void ContextOrchestrator::StaticDisable(CancelReason reason) + { + Instance().Disable(reason); + } + + void ContextOrchestrator::StaticCancelQueuedItems(CancelReason reason) + { + Instance().CancelQueuedItems(reason); + } + + void ContextOrchestrator::StaticWaitForRunningItems() + { + Instance().WaitForRunningItems(); + } + + void ContextOrchestrator::Disable(CancelReason reason) + { + std::lock_guard<std::mutex> lock{ m_queueLock }; + m_enabled = false; + m_disabledReason = reason; + } + + void ContextOrchestrator::CancelQueuedItems(CancelReason reason) + { + std::lock_guard<std::mutex> lock{ m_queueLock }; + for (const auto& queue : m_commandQueues) + { + queue.second->CancelAllItems(reason); + } + } + + void ContextOrchestrator::WaitForRunningItems() + { + std::lock_guard<std::mutex> lock{ m_queueLock }; + for (const auto& queue : m_commandQueues) + { + queue.second->WaitForEmptyQueue(); + } + } + + bool ContextOrchestrator::WaitForRunningItems(DWORD timeoutMilliseconds) + { + std::lock_guard<std::mutex> lock{ m_queueLock }; + for (const auto& queue : m_commandQueues) + { + if (!queue.second->WaitForEmptyQueue(timeoutMilliseconds)) + { + return false; + } + } + + return true; + } + + std::string ContextOrchestrator::GetStatusString() + { + std::ostringstream stream; + + std::lock_guard<std::mutex> lock{ m_queueLock }; + + if (!m_enabled) + { + stream << "Disabled due to " << ToIntegral(m_disabledReason) << std::endl; + } + + for (const auto& queue : m_commandQueues) + { + stream << queue.second->GetStatusString(); + } + + return stream.str(); + } + + _Requires_lock_held_(m_itemLock) std::deque<std::shared_ptr<OrchestratorQueueItem>>::iterator OrchestratorQueue::FindIteratorById(const OrchestratorQueueItemId& comparisonQueueItemId) { return std::find_if(m_queueItems.begin(), m_queueItems.end(), [&comparisonQueueItemId](const std::shared_ptr<OrchestratorQueueItem>& item) {return (item->GetId().IsSame(comparisonQueueItemId)); }); } - _Requires_lock_held_(m_queueLock) + _Requires_lock_held_(m_itemLock) std::shared_ptr<OrchestratorQueueItem> OrchestratorQueue::FindById(const OrchestratorQueueItemId& comparisonQueueItemId) { auto itr = FindIteratorById(comparisonQueueItemId); @@ -163,11 +265,12 @@ namespace AppInstaller::CLI::Execution return {}; } - void OrchestratorQueue::EnqueueItem(std::shared_ptr<OrchestratorQueueItem> item) + void OrchestratorQueue::EnqueueItem(const std::shared_ptr<OrchestratorQueueItem>& item) { { - std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + std::lock_guard<std::mutex> lockQueue{ m_itemLock }; m_queueItems.push_back(item); + m_queueEmpty.ResetEvent(); } // Add the package to the Installing source so that it can be queried using the Source interface. @@ -176,28 +279,33 @@ namespace AppInstaller::CLI::Execution { try { - ContextOrchestrator::Instance().AddItemManifestToInstallingSource(*item); + m_orchestrator.AddItemManifestToInstallingSource(*item); } catch (...) { - std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + std::lock_guard<std::mutex> lockQueue{ m_itemLock }; auto itr = FindIteratorById(item->GetId()); if (itr != m_queueItems.end()) { m_queueItems.erase(itr); + + if (m_queueItems.empty()) + { + m_queueEmpty.SetEvent(); + } } throw; } } { - std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + std::lock_guard<std::mutex> lockQueue{ m_itemLock }; item->SetState(OrchestratorQueueItemState::Queued); } } - OrchestratorQueue::OrchestratorQueue(std::string_view commandName, UINT32 allowedThreads) : - m_commandName(commandName), m_allowedThreads(allowedThreads) + OrchestratorQueue::OrchestratorQueue(ContextOrchestrator& orchestrator, std::string_view commandName, UINT32 allowedThreads) : + m_orchestrator(orchestrator), m_commandName(commandName), m_allowedThreads(allowedThreads) { m_threadPool.reset(CreateThreadpool(nullptr)); THROW_LAST_ERROR_IF_NULL(m_threadPool); @@ -216,7 +324,7 @@ namespace AppInstaller::CLI::Execution CloseThreadpoolCleanupGroupMembers(m_threadPoolCleanupGroup.get(), false, nullptr); } - void OrchestratorQueue::EnqueueAndRunItem(std::shared_ptr<OrchestratorQueueItem> item) + void OrchestratorQueue::EnqueueAndRunItem(const std::shared_ptr<OrchestratorQueueItem>& item) { EnqueueItem(item); @@ -234,7 +342,7 @@ namespace AppInstaller::CLI::Execution // Try to find the item in the queue. { - std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + std::lock_guard<std::mutex> lockQueue{ m_itemLock }; item = FindById(itemId); if (!item) @@ -299,7 +407,7 @@ namespace AppInstaller::CLI::Execution { // Remove item from this queue and add it to the queue for the next command. RemoveItemInState(*item, OrchestratorQueueItemState::Running, false); - ContextOrchestrator::Instance().EnqueueAndRunItem(item); + m_orchestrator.EnqueueAndRunItem(item); } } catch (...) @@ -307,6 +415,63 @@ namespace AppInstaller::CLI::Execution } } + void OrchestratorQueue::CancelAllItems(CancelReason reason) + { + std::lock_guard<std::mutex> lockQueue{ m_itemLock }; + + for (auto itr = m_queueItems.begin(); itr != m_queueItems.end(); itr++) + { + auto& item = *itr; + + item->GetContext().Cancel(reason, true); + + // This mimics ContextOrchestrator::CancelQueueItem, which speeds up the process of cancelling queued items + if (item->GetState() == OrchestratorQueueItemState::Queued) + { + item->SetState(OrchestratorQueueItemState::Cancelled); + item->HandleItemCompletion(m_orchestrator); + } + } + } + + void OrchestratorQueue::WaitForEmptyQueue() + { + m_queueEmpty.wait(); + } + + bool OrchestratorQueue::WaitForEmptyQueue(DWORD timeoutMilliseconds) + { + return m_queueEmpty.wait(timeoutMilliseconds); + } + + std::string OrchestratorQueue::GetStatusString() + { + std::ostringstream stream; + stream << m_commandName << '[' << m_allowedThreads << "]\n"; + + std::map<OrchestratorQueueItemState, size_t> stateCounts; + stateCounts[OrchestratorQueueItemState::NotQueued] = 0; + stateCounts[OrchestratorQueueItemState::Queued] = 0; + stateCounts[OrchestratorQueueItemState::Running] = 0; + stateCounts[OrchestratorQueueItemState::Cancelled] = 0; + + { + std::lock_guard<std::mutex> lock{ m_itemLock }; + + for (const auto& item : m_queueItems) + { + stateCounts[item->GetState()] += 1; + } + } + + for (const auto& stateCount : stateCounts) + { + stream << " " << ToString(stateCount.first) << " : " << stateCount.second << std::endl; + } + + return stream.str(); + } + bool OrchestratorQueue::RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state, bool isGlobalRemove) { // OrchestratorQueueItemState::Running items should only be removed by the thread that ran the item. @@ -315,7 +480,7 @@ namespace AppInstaller::CLI::Execution bool foundItem = false; { - std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + std::lock_guard<std::mutex> lockQueue{ m_itemLock }; // Look for the item. It's ok if the item is not found since multiple listeners may try to remove the same item. auto itr = FindIteratorById(item.GetId()); @@ -330,6 +495,11 @@ namespace AppInstaller::CLI::Execution { (*itr)->SetCurrentQueue(nullptr); m_queueItems.erase(itr); + + if (m_queueItems.empty()) + { + m_queueEmpty.SetEvent(); + } } else if (state == OrchestratorQueueItemState::Queued) { @@ -340,8 +510,7 @@ namespace AppInstaller::CLI::Execution if (foundItem && isGlobalRemove) { - ContextOrchestrator::Instance().RemoveItemManifestFromInstallingSource(item); - item.GetCompletedEvent().SetEvent(); + item.HandleItemCompletion(m_orchestrator); } return foundItem; @@ -368,6 +537,12 @@ namespace AppInstaller::CLI::Execution } } + void OrchestratorQueueItem::HandleItemCompletion(ContextOrchestrator& orchestrator) const + { + orchestrator.RemoveItemManifestFromInstallingSource(*this); + GetCompletedEvent().SetEvent(); + } + std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForInstall(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context, bool isUpgrade) { std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), isUpgrade ? PackageOperationType::Upgrade : PackageOperationType::Install); @@ -402,4 +577,16 @@ namespace AppInstaller::CLI::Execution item->AddCommand(std::make_unique<::AppInstaller::CLI::COMRepairCommand>(RootCommand::CommandName)); return item; } + + std::string_view ToString(OrchestratorQueueItemState state) + { + switch (state) + { + case OrchestratorQueueItemState::NotQueued: return "NotQueued"; + case OrchestratorQueueItemState::Queued: return "Queued"; + case OrchestratorQueueItemState::Running: return "Running"; + case OrchestratorQueueItemState::Cancelled: return "Cancelled"; + default: return "Unknown"; + } + } } diff --git a/src/AppInstallerCLICore/ContextOrchestrator.h b/src/AppInstallerCLICore/ContextOrchestrator.h @@ -9,7 +9,8 @@ #include "CompletionData.h" #include "Command.h" #include "COMContext.h" - +#include <wil/resource.h> +#include <string> #include <string_view> namespace AppInstaller::CLI::Execution @@ -26,6 +27,8 @@ namespace AppInstaller::CLI::Execution Cancelled }; + std::string_view ToString(OrchestratorQueueItemState state); + struct OrchestratorQueueItemId { OrchestratorQueueItemId(std::wstring packageId, std::wstring sourceId) : m_packageId(std::move(packageId)), m_sourceId(std::move(sourceId)) {} @@ -50,6 +53,8 @@ namespace AppInstaller::CLI::Execution Repair, }; + struct ContextOrchestrator; + struct OrchestratorQueueItem { OrchestratorQueueItem(OrchestratorQueueItemId id, std::unique_ptr<COMContext> context, PackageOperationType operationType) : @@ -81,6 +86,8 @@ namespace AppInstaller::CLI::Execution PackageOperationType GetPackageOperationType() const { return m_operationType; } std::string_view GetItemCommandName() const; + void HandleItemCompletion(ContextOrchestrator& orchestrator) const; + private: OrchestratorQueueItemState m_state = OrchestratorQueueItemState::NotQueued; std::unique_ptr<COMContext> m_context; @@ -109,9 +116,10 @@ namespace AppInstaller::CLI::Execution struct ContextOrchestrator { ContextOrchestrator(); + ContextOrchestrator(unsigned int hardwareConcurrency); static ContextOrchestrator& Instance(); - void EnqueueAndRunItem(std::shared_ptr<OrchestratorQueueItem> queueItem); + void EnqueueAndRunItem(const std::shared_ptr<OrchestratorQueueItem>& queueItem); void CancelQueueItem(const OrchestratorQueueItem& item); std::shared_ptr<OrchestratorQueueItem> GetQueueItem(const OrchestratorQueueItemId& queueItemId); @@ -119,8 +127,27 @@ namespace AppInstaller::CLI::Execution void AddItemManifestToInstallingSource(const OrchestratorQueueItem& queueItem); void RemoveItemManifestFromInstallingSource(const OrchestratorQueueItem& queueItem); + // Functions for ServerShutdownSynchronization::ComponentSystem registration + static void RegisterForShutdownSynchronization(); + static void StaticDisable(CancelReason reason); + static void StaticCancelQueuedItems(CancelReason reason); + static void StaticWaitForRunningItems(); + + void Disable(CancelReason reason); + void CancelQueuedItems(CancelReason reason); + void WaitForRunningItems(); + + // Waits for running items to complete; waits up to full time out in *each* queue. + // Returns true to indicate all queues are empty before the timeout. + bool WaitForRunningItems(DWORD timeoutMilliseconds); + + // Gets a string that represents the current state of the orchestrator. + std::string GetStatusString(); + private: std::mutex m_queueLock; + bool m_enabled = true; + CancelReason m_disabledReason = CancelReason::None; void AddCommandQueue(std::string_view commandName, UINT32 allowedThreads); void RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state); @@ -136,14 +163,14 @@ namespace AppInstaller::CLI::Execution // The queue allows multiple items to run at the same time, up to a limit. struct OrchestratorQueue { - OrchestratorQueue(std::string_view commandName, UINT32 allowedThreads); + OrchestratorQueue(ContextOrchestrator& orchestrator, std::string_view commandName, UINT32 allowedThreads); ~OrchestratorQueue(); // Name of the command this queue can execute std::string_view CommandName() const { return m_commandName; } // Enqueues an item to be run when there are threads available. - void EnqueueAndRunItem(std::shared_ptr<OrchestratorQueueItem> item); + void EnqueueAndRunItem(const std::shared_ptr<OrchestratorQueueItem>& item); // Removes an item by id, provided that it is in the given state. // Returns true if an item was removed. @@ -151,19 +178,33 @@ namespace AppInstaller::CLI::Execution bool RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state, bool isGlobalRemove); // Finds an item by id, if it is in the queue. - _Requires_lock_held_(m_queueLock) + _Requires_lock_held_(m_itemLock) std::shared_ptr<OrchestratorQueueItem> FindById(const OrchestratorQueueItemId& queueItemId); // Runs a single item from the queue. void RunItem(const OrchestratorQueueItemId& itemId); + // Cancels and "removes" all items in the queue. + void CancelAllItems(CancelReason reason); + + // Waits until the empty queue event is signaled. + void WaitForEmptyQueue(); + + // Waits until the empty queue event is signaled. + // Returns true to indicate the queue is empty before the timeout. + bool WaitForEmptyQueue(DWORD timeoutMilliseconds); + + // Gets a string that represents the current state of the queue. + std::string GetStatusString(); + private: // Enqueues an item. - void EnqueueItem(std::shared_ptr<OrchestratorQueueItem> item); + void EnqueueItem(const std::shared_ptr<OrchestratorQueueItem>& item); - _Requires_lock_held_(m_queueLock) + _Requires_lock_held_(m_itemLock) std::deque<std::shared_ptr<OrchestratorQueueItem>>::iterator FindIteratorById(const OrchestratorQueueItemId& comparisonQueueItemId); + ContextOrchestrator& m_orchestrator; std::string_view m_commandName; // Number of threads allowed to run items in this queue. @@ -177,7 +218,8 @@ namespace AppInstaller::CLI::Execution wil::unique_any<PTP_POOL, decltype(CloseThreadpool), CloseThreadpool> m_threadPool; wil::unique_any<PTP_CLEANUP_GROUP, decltype(CloseThreadpoolCleanupGroup), CloseThreadpoolCleanupGroup> m_threadPoolCleanupGroup; - std::mutex m_queueLock; + std::mutex m_itemLock; std::deque<std::shared_ptr<OrchestratorQueueItem>> m_queueItems; + wil::slim_event_manual_reset m_queueEmpty{ true }; }; } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -9,7 +9,8 @@ #include "Commands/InstallCommand.h" #include "COMContext.h" #include <AppInstallerFileLogger.h> -#include <winget/OutputDebugStringLogger.h> +#include <winget/OutputDebugStringLogger.h> +#include "Public/ShutdownMonitoring.h" #ifndef AICLI_DISABLE_TEST_HOOKS #include <winget/Debugging.h> @@ -58,11 +59,33 @@ namespace AppInstaller::CLI #endif std::_Exit(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); + } + + wil::slim_event_manual_reset& GetMainWaitEvent() + { + static wil::slim_event_manual_reset s_mainWait; + return s_mainWait; + } + + void WaitOnMainWaitEvent() + { + GetMainWaitEvent().wait(5000); + } + + void RegisterShutdownBlocker() + { + ShutdownMonitoring::ServerShutdownSynchronization::ComponentSystem main{}; + main.Wait = WaitOnMainWaitEvent; + ShutdownMonitoring::ServerShutdownSynchronization::AddComponent(main); } } int CoreMain(int argc, wchar_t const** argv) try { + // This prevents the OS package management from terminating the CLI process before it has had a chance to gracefully exit. + RegisterShutdownBlocker(); + auto signalMainExit = wil::scope_exit([]() { GetMainWaitEvent().SetEvent(); }); + std::signal(SIGABRT, abort_signal_handler); init_apartment(); diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -6,6 +6,7 @@ #include "COMContext.h" #include "Command.h" #include "ExecutionContext.h" +#include "Public/ShutdownMonitoring.h" #include <winget/Checkpoint.h> #include <winget/Reboot.h> #include <winget/UserSettings.h> @@ -19,262 +20,6 @@ namespace AppInstaller::CLI::Execution namespace { - // Type to contain the CTRL signal and window messages handler. - struct SignalTerminationHandler - { - static SignalTerminationHandler& Instance() - { - static SignalTerminationHandler s_instance; - return s_instance; - } - - void AddContext(Context* context) - { - std::lock_guard<std::mutex> lock{ m_contextsLock }; - - auto itr = std::find(m_contexts.begin(), m_contexts.end(), context); - THROW_HR_IF(E_NOT_VALID_STATE, itr != m_contexts.end()); - m_contexts.push_back(context); - } - - void RemoveContext(Context* context) - { - std::lock_guard<std::mutex> lock{ m_contextsLock }; - - auto itr = std::find(m_contexts.begin(), m_contexts.end(), context); - THROW_HR_IF(E_NOT_VALID_STATE, itr == m_contexts.end()); - m_contexts.erase(itr); - } - - void StartAppShutdown() - { - // Lifetime manager sends CTRL-C after the WM_QUERYENDSESSION is processed. - // If we disable the CTRL-C handler, the default handler will kill us. - TerminateContexts(CancelReason::AppShutdown, true); - -#ifndef AICLI_DISABLE_TEST_HOOKS - m_appShutdownEvent.SetEvent(); -#endif - } - -#ifndef AICLI_DISABLE_TEST_HOOKS - HWND GetWindowHandle() { return m_windowHandle.get(); } - - bool WaitForAppShutdownEvent() - { - return m_appShutdownEvent.wait(60000); - } -#endif - - private: - SignalTerminationHandler() - { - 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) - { - // There are 3 events being hit with 0%, 1% and 38% - // Typically the window message is received between the first two. - constexpr double minProgress = 0; - auto progress = args.Progress(); - if (progress > minProgress) - { - SignalTerminationHandler::Instance().StartAppShutdown(); - } - }); - } - else - { - // Create message only window. - m_messageQueueReady.create(); - m_windowThread = std::thread(&SignalTerminationHandler::CreateWindowAndStartMessageLoop, this); - if (!m_messageQueueReady.wait(100)) - { - AICLI_LOG(CLI, Warning, << "Timeout creating winget window"); - } - } - - // Set up ctrl-c handler. - LOG_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(StaticCtrlHandlerFunction, TRUE)); - -#ifndef AICLI_DISABLE_TEST_HOOKS - m_appShutdownEvent.create(); -#endif - } - - ~SignalTerminationHandler() - { - // At this point the thread is gone, but it will get angry - // if there's no call to join. - if (m_windowThread.joinable()) - { - m_windowThread.join(); - } - } - - static BOOL WINAPI StaticCtrlHandlerFunction(DWORD ctrlType) - { - return Instance().CtrlHandlerFunction(ctrlType); - } - - static LRESULT WINAPI WindowMessageProcedure(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) - { - AICLI_LOG(CLI, Verbose, << "Received window message type: " << uMsg); - switch (uMsg) - { - case WM_QUERYENDSESSION: - SignalTerminationHandler::Instance().StartAppShutdown(); - return TRUE; - case WM_ENDSESSION: - case WM_CLOSE: - DestroyWindow(hWnd); - break; - case WM_DESTROY: - PostQuitMessage(0); - break; - default: - return DefWindowProc(hWnd, uMsg, wParam, lParam); - } - return FALSE; - } - - BOOL CtrlHandlerFunction(DWORD ctrlType) - { - // TODO: Move this to be logged per active context when we have thread static globals - AICLI_LOG(CLI, Info, << "Got CTRL type: " << ctrlType); - - switch (ctrlType) - { - case CTRL_C_EVENT: - case CTRL_BREAK_EVENT: - return TerminateContexts(CancelReason::CtrlCSignal, false); - // According to MSDN, we should never receive these due to having gdi32/user32 loaded in our process. - // But handle them as a force terminate anyway. - case CTRL_CLOSE_EVENT: - case CTRL_LOGOFF_EVENT: - case CTRL_SHUTDOWN_EVENT: - return TerminateContexts(CancelReason::CtrlCSignal, true); - default: - return FALSE; - } - } - - // Terminates the currently attached contexts. - // Returns FALSE if no contexts attached; TRUE otherwise. - BOOL TerminateContexts(CancelReason reason, bool force) - { - if (m_contexts.empty()) - { - return FALSE; - } - - { - std::lock_guard<std::mutex> lock{ m_contextsLock }; - for (auto& context : m_contexts) - { - context->Cancel(reason, force); - } - } - - return TRUE; - } - - void CreateWindowAndStartMessageLoop() - { - PCWSTR windowClass = L"wingetWindow"; - HINSTANCE hInstance = GetModuleHandle(NULL); - if (hInstance == NULL) - { - LOG_LAST_ERROR_MSG("Failed getting module handle"); - return; - } - - WNDCLASSEX wcex = {}; - wcex.cbSize = sizeof(wcex); - - wcex.style = CS_NOCLOSE; - wcex.lpfnWndProc = SignalTerminationHandler::WindowMessageProcedure; - wcex.cbClsExtra = 0; - wcex.cbWndExtra = 0; - wcex.hInstance = hInstance; - wcex.lpszClassName = windowClass; - - if (!RegisterClassEx(&wcex)) - { - LOG_LAST_ERROR_MSG("Failed registering window class"); - return; - } - - m_windowHandle = wil::unique_hwnd(CreateWindow( - windowClass, - L"WingetMessageOnlyWindow", - WS_OVERLAPPEDWINDOW, - 0, /* x */ - 0, /* y */ - 0, /* nWidth */ - 0, /* nHeight */ - NULL, /* hWndParent */ - NULL, /* hMenu */ - hInstance, - NULL)); /* lpParam */ - - if (m_windowHandle == nullptr) - { - LOG_LAST_ERROR_MSG("Failed creating window"); - return; - } - - ShowWindow(m_windowHandle.get(), SW_HIDE); - - // Force message queue to be created. - MSG msg; - PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); - m_messageQueueReady.SetEvent(); - - // Message loop - BOOL getMessageResult; - while ((getMessageResult = GetMessage(&msg, m_windowHandle.get(), 0, 0)) != 0) - { - if (getMessageResult == -1) - { - LOG_LAST_ERROR(); - } - else - { - DispatchMessage(&msg); - } - } - } - -#ifndef AICLI_DISABLE_TEST_HOOKS - wil::unique_event m_appShutdownEvent; -#endif - - std::mutex m_contextsLock; - std::vector<Context*> m_contexts; - 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; - }; - - void SetSignalTerminationHandlerContext(bool add, Context* context) - { - THROW_HR_IF(E_POINTER, context == nullptr); - - if (add) - { - SignalTerminationHandler::Instance().AddContext(context); - } - else - { - SignalTerminationHandler::Instance().RemoveContext(context); - } - } - bool ShouldRemoveCheckpointDatabase(HRESULT hr) { switch (hr) @@ -356,7 +101,7 @@ namespace AppInstaller::CLI::Execution void Context::EnableSignalTerminationHandler(bool enabled) { - SetSignalTerminationHandlerContext(enabled, this); + ShutdownMonitoring::TerminationSignalHandler::EnableListener(enabled, this); m_disableSignalTerminationHandlerOnExit = enabled; } @@ -443,15 +188,7 @@ namespace AppInstaller::CLI::Execution void Context::Cancel(CancelReason reason, bool bypassUser) { - HRESULT hr = E_ABORT; - if (reason == CancelReason::CtrlCSignal) - { - hr = APPINSTALLER_CLI_ERROR_CTRL_SIGNAL_RECEIVED; - } - else if (reason == CancelReason::AppShutdown) - { - hr = APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED; - } + HRESULT hr = ToHRESULT(reason); Terminate(hr); Reporter.CancelInProgressTask(bypassUser, reason); @@ -487,16 +224,6 @@ namespace AppInstaller::CLI::Execution { return (m_shouldExecuteWorkflowTask ? m_shouldExecuteWorkflowTask(task) : true); } - - HWND GetWindowHandle() - { - return SignalTerminationHandler::Instance().GetWindowHandle(); - } - - bool WaitForAppShutdownEvent() - { - return SignalTerminationHandler::Instance().WaitForAppShutdownEvent(); - } #endif void ContextEnumBasedVariantMapActionCallback(const void* map, Data data, EnumBasedVariantMapAction action) diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -7,6 +7,7 @@ #include "ExecutionContextData.h" #include "CompletionData.h" #include "CheckpointManager.h" +#include <AppInstallerProgress.h> #include <winget/Checkpoint.h> #include <string_view> @@ -91,7 +92,7 @@ namespace AppInstaller::CLI::Execution // The context within which all commands execute. // Contains input/output via Execution::Reporter and // arguments via Execution::Args. - struct Context : EnumBasedVariantMap<Data, details::DataMapping, ContextEnumBasedVariantMapActionCallback> + struct Context : EnumBasedVariantMap<Data, details::DataMapping, ContextEnumBasedVariantMapActionCallback>, ICancellable { Context() = default; Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} @@ -139,7 +140,7 @@ namespace AppInstaller::CLI::Execution // Cancel the context; this terminates it as well as informing any in progress task to stop cooperatively. // Multiple attempts with CancelReason::CancelSignal may cause the process to simply exit. // The bypassUser indicates whether the user should be asked for cancellation (does not currently have any effect). - void Cancel(CancelReason reason, bool bypassUser = false); + void Cancel(CancelReason reason, bool bypassUser = false) override; // Gets context flags ContextFlag GetFlags() const diff --git a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <Windows.h> +#include <AppInstallerProgress.h> +#include <winrt/Windows.ApplicationModel.h> +#include <wil/resource.h> +#include <mutex> + +namespace AppInstaller::ShutdownMonitoring +{ + // Type to contain the CTRL signal and window messages handler. + struct TerminationSignalHandler + { + // Gets the singleton handler. + static TerminationSignalHandler& Instance(); + + // Add a termination listener. + void AddListener(ICancellable* cancellable); + + // Remove a termination listener. + void RemoveListener(ICancellable* cancellable); + + // Add or remove the listener based on `enabled`. + static void EnableListener(bool enabled, ICancellable* cancellable); + +#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: + TerminationSignalHandler(); + + ~TerminationSignalHandler(); + + void StartAppShutdown(); + + static BOOL WINAPI StaticCtrlHandlerFunction(DWORD ctrlType); + + static LRESULT WINAPI WindowMessageProcedure(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + BOOL CtrlHandlerFunction(DWORD ctrlType); + + // Terminates the currently attached contexts. + // Returns FALSE if no contexts attached; TRUE otherwise. + BOOL InformListeners(CancelReason reason, bool force); + + void CreateWindowAndStartMessageLoop(); + +#ifndef AICLI_DISABLE_TEST_HOOKS + wil::unique_event m_appShutdownEvent; +#endif + + std::mutex m_listenersLock; + std::vector<ICancellable*> 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 + struct ServerShutdownSynchronization : public ICancellable + { + using ShutdownCompleteCallback = void (*)(); + + // Initializes the monitoring system and sets up a callback to be invoked when shutdown is completed. + static void Initialize(ShutdownCompleteCallback callback); + + // "Interface" for a single component to synchronize with. + struct ComponentSystem + { + // Initiate the shutdown process. + // Components are expected to set flags to prevent any further work from beginning and return as quickly as possible. + void (*BlockNewWork)(CancelReason reason) = nullptr; + + // Components are expected to cancel active or pending work as asynchronously as possible. + void (*BeginShutdown)(CancelReason reason) = nullptr; + + // Components wait until all active and pending work have completed their + void (*Wait)() = nullptr; + }; + + // Adds a component to the system. + static void AddComponent(const ComponentSystem& component); + + // Waits for the shutdown to complete. + static void WaitForShutdown(); + + // Listens for a termination signal. + void Cancel(CancelReason reason, bool force) override; + + private: + ServerShutdownSynchronization(); + ~ServerShutdownSynchronization(); + + static ServerShutdownSynchronization& Instance(); + + // Runs the actual shutdown process and invokes the callback. + void SynchronizeShutdown(CancelReason reason); + + ShutdownCompleteCallback m_callback = nullptr; + std::mutex m_componentsLock; + std::vector<ComponentSystem> m_components; + std::mutex m_threadLock; + std::thread m_shutdownThread; + wil::slim_event_manual_reset m_shutdownComplete; + }; +} diff --git a/src/AppInstallerCLICore/ShutdownMonitoring.cpp b/src/AppInstallerCLICore/ShutdownMonitoring.cpp @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/ShutdownMonitoring.h" +#include <AppInstallerErrors.h> +#include <AppInstallerLogging.h> +#include <AppInstallerRuntime.h> + +namespace AppInstaller::ShutdownMonitoring +{ + TerminationSignalHandler& TerminationSignalHandler::Instance() + { + static TerminationSignalHandler s_instance; + return s_instance; + } + + void TerminationSignalHandler::AddListener(ICancellable* cancellable) + { + std::lock_guard<std::mutex> lock{ m_listenersLock }; + + auto itr = std::find(m_listeners.begin(), m_listeners.end(), cancellable); + THROW_HR_IF(E_NOT_VALID_STATE, itr != m_listeners.end()); + m_listeners.push_back(cancellable); + } + + void TerminationSignalHandler::RemoveListener(ICancellable* cancellable) + { + std::lock_guard<std::mutex> lock{ m_listenersLock }; + + auto itr = std::find(m_listeners.begin(), m_listeners.end(), cancellable); + THROW_HR_IF(E_NOT_VALID_STATE, itr == m_listeners.end()); + m_listeners.erase(itr); + } + + void TerminationSignalHandler::EnableListener(bool enabled, ICancellable* cancellable) + { + if (enabled) + { + Instance().AddListener(cancellable); + } + else + { + Instance().RemoveListener(cancellable); + } + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + HWND TerminationSignalHandler::GetWindowHandle() const + { + 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); + if (!m_messageQueueReady.wait(100)) + { + AICLI_LOG(CLI, Warning, << "Timeout creating winget window"); + } + + // Set up ctrl-c handler. + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(StaticCtrlHandlerFunction, TRUE)); + } + + TerminationSignalHandler::~TerminationSignalHandler() + { + // std::thread requires that any managed thread (joinable) be joined or detached before destructing + if (m_windowThread.joinable()) + { + m_windowThread.detach(); + } + } + + void TerminationSignalHandler::StartAppShutdown() + { + 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); + } + + BOOL WINAPI TerminationSignalHandler::StaticCtrlHandlerFunction(DWORD ctrlType) + { + return Instance().CtrlHandlerFunction(ctrlType); + } + + LRESULT WINAPI TerminationSignalHandler::WindowMessageProcedure(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + switch (uMsg) + { + case WM_QUERYENDSESSION: + AICLI_LOG(CLI, Verbose, << "Received WM_QUERYENDSESSION"); + Instance().StartAppShutdown(); + return TRUE; + case WM_ENDSESSION: + case WM_CLOSE: + AICLI_LOG(CLI, Verbose, << "Received window message type: " << uMsg); + // We delay as long as needed during the WM_ENDSESSION as we will be terminated on return. + ServerShutdownSynchronization::WaitForShutdown(); + DestroyWindow(hWnd); + break; + case WM_DESTROY: + PostQuitMessage(0); + break; + default: + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + return FALSE; + } + + BOOL TerminationSignalHandler::CtrlHandlerFunction(DWORD ctrlType) + { + // TODO: Move this to be logged per active context when we have thread static globals + AICLI_LOG(CLI, Info, << "Got CTRL type: " << ctrlType); + + switch (ctrlType) + { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + return InformListeners(CancelReason::CtrlCSignal, false); + // According to MSDN, we should never receive these due to having gdi32/user32 loaded in our process. + // But handle them as a force terminate anyway. + case CTRL_CLOSE_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_SHUTDOWN_EVENT: + return InformListeners(CancelReason::CtrlCSignal, true); + default: + return FALSE; + } + } + + // Terminates the currently attached contexts. + // Returns FALSE if no contexts attached; TRUE otherwise. + BOOL TerminationSignalHandler::InformListeners(CancelReason reason, bool force) + { + std::lock_guard<std::mutex> lock{ m_listenersLock }; + + if (m_listeners.empty()) + { + return FALSE; + } + + for (auto& listener : m_listeners) + { + listener->Cancel(reason, force); + } + + return TRUE; + } + + void TerminationSignalHandler::CreateWindowAndStartMessageLoop() + { + PCWSTR windowClass = L"wingetWindow"; + HINSTANCE hInstance = GetModuleHandle(NULL); + if (hInstance == NULL) + { + LOG_LAST_ERROR_MSG("Failed getting module handle"); + return; + } + + WNDCLASSEX wcex = {}; + wcex.cbSize = sizeof(wcex); + + wcex.style = CS_NOCLOSE; + wcex.lpfnWndProc = TerminationSignalHandler::WindowMessageProcedure; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.lpszClassName = windowClass; + + if (!RegisterClassEx(&wcex)) + { + LOG_LAST_ERROR_MSG("Failed registering window class"); + return; + } + + m_windowHandle = wil::unique_hwnd(CreateWindow( + windowClass, + L"WingetMessageOnlyWindow", + WS_OVERLAPPEDWINDOW, + 0, /* x */ + 0, /* y */ + 0, /* nWidth */ + 0, /* nHeight */ + NULL, /* hWndParent */ + NULL, /* hMenu */ + hInstance, + NULL)); /* lpParam */ + + if (m_windowHandle == nullptr) + { + LOG_LAST_ERROR_MSG("Failed creating window"); + return; + } + + ShowWindow(m_windowHandle.get(), SW_HIDE); + + // Force message queue to be created. + MSG msg; + PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); + m_messageQueueReady.SetEvent(); + + // Message loop + BOOL getMessageResult; + while ((getMessageResult = GetMessage(&msg, m_windowHandle.get(), 0, 0)) != 0) + { + if (getMessageResult == -1) + { + LOG_LAST_ERROR(); + } + else + { + DispatchMessage(&msg); + } + } + } + + void ServerShutdownSynchronization::Initialize(ShutdownCompleteCallback callback) + { + Instance().m_callback = callback; + } + + void ServerShutdownSynchronization::AddComponent(const ComponentSystem& component) + { + ServerShutdownSynchronization& instance = Instance(); + std::lock_guard<std::mutex> lock{ instance.m_componentsLock }; + + for (const auto& item : instance.m_components) + { + if (item.BlockNewWork == component.BlockNewWork || + item.BeginShutdown == component.BeginShutdown || + item.Wait == component.Wait) + { + return; + } + } + + instance.m_components.push_back(component); + } + + void ServerShutdownSynchronization::WaitForShutdown() + { + ServerShutdownSynchronization& instance = Instance(); + + { + std::lock_guard<std::mutex> 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; + } + } + + instance.m_shutdownComplete.wait(); + } + + void ServerShutdownSynchronization::Cancel(CancelReason reason, bool) + { + std::lock_guard<std::mutex> lock{ m_threadLock }; + + if (!m_shutdownThread.joinable()) + { + m_shutdownThread = std::thread(&ServerShutdownSynchronization::SynchronizeShutdown, this, reason); + } + } + + ServerShutdownSynchronization::ServerShutdownSynchronization() + { + TerminationSignalHandler::Instance().AddListener(this); + } + + ServerShutdownSynchronization::~ServerShutdownSynchronization() + { + TerminationSignalHandler::Instance().RemoveListener(this); + if (m_shutdownThread.joinable()) + { + m_shutdownThread.detach(); + } + } + + ServerShutdownSynchronization& ServerShutdownSynchronization::Instance() + { + static ServerShutdownSynchronization s_instance; + return s_instance; + } + + void ServerShutdownSynchronization::SynchronizeShutdown(CancelReason reason) try + { + auto setShutdownComplete = wil::scope_exit([this]() { this->m_shutdownComplete.SetEvent(); }); + + std::vector<ComponentSystem> components; + { + std::lock_guard<std::mutex> lock{ m_componentsLock }; + components = m_components; + } + + for (const auto& component : components) + { + if (component.BlockNewWork) + { + component.BlockNewWork(reason); + } + } + + for (const auto& component : components) + { + if (component.BeginShutdown) + { + component.BeginShutdown(reason); + } + } + + for (const auto& component : components) + { + if (component.Wait) + { + component.Wait(); + } + } + + ShutdownCompleteCallback callback = m_callback; + if (callback) + { + callback(); + } + } + CATCH_LOG(); +} diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -27,6 +27,7 @@ #include <functional> #include <iterator> #include <memory> +#include <mutex> #include <numeric> #include <optional> #include <set> @@ -39,6 +40,7 @@ #include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Management.Deployment.h> #include <winrt/Windows.UI.ViewManagement.h> +#include <winrt/Windows.ApplicationModel.h> #include <winrt/Windows.ApplicationModel.Resources.h> #include <winrt/Windows.ApplicationModel.Resources.Core.h> #include <winrt/Windows.ApplicationModel.Store.Preview.InstallControl.h> @@ -47,6 +49,7 @@ #pragma warning( push ) #pragma warning ( disable : 6001 6285 6340 6388 ) +#include <wil/resource.h> #include <wil/result.h> #include <wil/result_macros.h> #include <wil/safecast.h> diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -65,6 +65,7 @@ <ReferenceOutputAssembly>False</ReferenceOutputAssembly> </ProjectReference> <ProjectReference Include="..\WinGetSourceCreator\WinGetSourceCreator.csproj" /> + <ProjectReference Include="..\WinGetTestCommon\WinGetTestCommon.csproj" /> <ProjectReference Include="..\WinGetUtil\WinGetUtil.vcxproj"> <ReferenceOutputAssembly>False</ReferenceOutputAssembly> </ProjectReference> diff --git a/src/AppInstallerCLIE2ETests/AppShutdownTests.cs b/src/AppInstallerCLIE2ETests/AppShutdownTests.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="AppShutdownTests.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -72,7 +72,7 @@ namespace AppInstallerCLIE2ETests // This just waits for the app termination event. var testCmdTask = new Task<TestCommon.RunCommandResult>(() => { - return TestCommon.RunAICLICommand("test", "appshutdown", timeOut: 300000, throwOnTimeout: false); + return TestCommon.RunAICLICommand("test", "appshutdown --verbose", timeOut: 300000, throwOnTimeout: false); }); // Register the app with the updated version. @@ -96,4 +96,4 @@ namespace AppInstallerCLIE2ETests Assert.True(testCmdTask.Result.StdOut.Contains("Succeeded waiting for app shutdown event")); } } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -152,6 +152,7 @@ namespace AppInstallerCLIE2ETests public const int OPC_E_ZIP_MISSING_END_OF_CENTRAL_DIRECTORY = unchecked((int)0x8051100F); public const int ERROR_OLD_WIN_VERSION = unchecked((int)0x8007047E); public const int HTTP_E_STATUS_NOT_FOUND = unchecked((int)0x80190194); + public const int E_ABORT = unchecked((int)0x80004004); // AICLI custom HRESULTs public const int ERROR_INTERNAL_ERROR = unchecked((int)0x8A150001); diff --git a/src/AppInstallerCLIE2ETests/Interop/Shutdown.cs b/src/AppInstallerCLIE2ETests/Interop/Shutdown.cs @@ -0,0 +1,134 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Shutdown.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests.Interop +{ + using System; + using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; + using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment.Projection; + using NUnit.Framework; + using WinGetTestCommon; + + /// <summary> + /// Shutdown testing. + /// </summary> + [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))] + public class Shutdown : BaseInterop + { + /// <summary> + /// Initializes a new instance of the <see cref="Shutdown"/> class. + /// </summary> + /// <param name="initializer">Initializer.</param> + public Shutdown(IInstanceInitializer initializer) + : base(initializer) + { + } + + /// <summary> + /// Checks that shutdown will proceed even though an object is active. + /// </summary> + [Test] + public void NoActiveOperations() + { + var packageManager = this.TestFactory.CreatePackageManager(); + + var servers = WinGetServerInstance.GetInstances(); + Assert.AreEqual(1, servers.Count); + + var server = servers[0]; + Assert.IsTrue(server.HasWindow); + + // This is the call pattern from Windows + this.SendMessageAndLog(server, WindowMessage.QueryEndSession); + this.SendMessageAndLog(server, WindowMessage.EndSession); + this.SendMessageAndLog(server, WindowMessage.Close); + + Assert.IsTrue(server.Process.WaitForExit(5000)); + } + + /// <summary> + /// Checks that shutdown will proceed even though an operation is active. + /// </summary> + /// <returns>The task.</returns> + [Test] + public async Task ActiveInstallOperation() + { + var packageManager = this.TestFactory.CreatePackageManager(); + var testSource = packageManager.GetPackageCatalogByName(Constants.TestSourceName); + var installDir = TestCommon.GetRandomTestDir(); + + var servers = WinGetServerInstance.GetInstances(); + Assert.AreEqual(1, servers.Count); + + var server = servers[0]; + Assert.IsTrue(server.HasWindow); + + // Find package + var searchResult = this.FindOnePackage(testSource, PackageMatchField.Name, PackageFieldMatchOption.Equals, "InapplicableOsVersion"); + + // Configure installation + var installOptions = this.TestFactory.CreateInstallOptions(); + installOptions.PackageInstallMode = PackageInstallMode.Silent; + installOptions.PreferredInstallLocation = installDir; + + // Install + var installOperation = packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); + + // This is the call pattern from Windows + this.SendMessageAndLog(server, WindowMessage.QueryEndSession); + this.SendMessageAndLog(server, WindowMessage.EndSession); + this.SendMessageAndLog(server, WindowMessage.Close); + + Assert.IsTrue(server.Process.WaitForExit(5000)); + + InstallResult installResult = null; + Exception exception = null; + + try + { + installResult = await installOperation; + } + catch (Exception ex) + { + exception = ex; + } + + // We just expect some kind of signal to indicate the failed attempt. + if (installResult != null) + { + Assert.AreNotEqual(InstallResultStatus.Ok, installResult.Status); + } + else + { + Assert.NotNull(exception); + } + + Assert.False(TestCommon.VerifyTestExeInstalledAndCleanup(installDir)); + } + + private void SendMessageAndLog(WinGetServerInstance server, WindowMessage message) + { + TestContext.Out.WriteLine($"Sending message {message} to process {server.Process.Id}..."); + try + { + if (server.SendMessage(message)) + { + TestContext.Out.WriteLine("... succeeded."); + } + else + { + TestContext.Out.WriteLine("... failed."); + } + } + catch (Exception e) + { + TestContext.Out.WriteLine($"... had exception: {e.Message}"); + } + } + } +} diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -229,6 +229,7 @@ <ClCompile Include="Command.cpp" /> <ClCompile Include="Completion.cpp" /> <ClCompile Include="CompositeSource.cpp" /> + <ClCompile Include="ContextOrchestrator.cpp" /> <ClCompile Include="Correlation.cpp" /> <ClCompile Include="CustomHeader.cpp" /> <ClCompile Include="DateTime.cpp" /> @@ -1055,4 +1056,4 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.props'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets'))" /> </Target> -</Project> +</Project>+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -377,6 +377,9 @@ <ClCompile Include="ManifestComparator.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="ContextOrchestrator.cpp"> + <Filter>Source Files\CLI</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/ContextOrchestrator.cpp b/src/AppInstallerCLITests/ContextOrchestrator.cpp @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <ContextOrchestrator.h> +#include <winget/ManifestYamlParser.h> + +using namespace TestCommon; +using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::Manifest; + +static constexpr DWORD c_DefaultWaitInMs = 5000; + +struct TestCOMContext : public COMContext +{ + TestCOMContext() = default; + + std::function<void()> DownloadCallback; + + void InvokeDownload() + { + if (DownloadCallback) + { + DownloadCallback(); + } + } + + std::function<void()> OperationCallback; + + void InvokeOperation() + { + if (OperationCallback) + { + OperationCallback(); + } + } +}; + +struct TestDownloadCommand : public Command +{ + TestDownloadCommand() : Command("download", "tests") {} + + Resource::LocString ShortDescription() const override { return {}; } + Resource::LocString LongDescription() const override { return {}; } + void ValidateArguments(Execution::Args&) const override {} + + void Execute(Context& context) const override + { + static_cast<TestCOMContext*>(&context)->InvokeDownload(); + } +}; + +struct TestOperationCommand : public Command +{ + TestOperationCommand() : Command("operation", "tests") {} + + Resource::LocString ShortDescription() const override { return {}; } + Resource::LocString LongDescription() const override { return {}; } + void ValidateArguments(Execution::Args&) const override {} + + void Execute(Context& context) const override + { + static_cast<TestCOMContext*>(&context)->InvokeOperation(); + } +}; + +struct TestQueueItem +{ + TestCOMContext* Context = nullptr; + std::shared_ptr<OrchestratorQueueItem> QueueItem; +}; + +TestQueueItem CreateTestItem(std::optional<std::string> packageName = std::nullopt) +{ + TestQueueItem result; + + std::unique_ptr<TestCOMContext> context = std::make_unique<TestCOMContext>(); + // Forcibly initialize the thread globals objects + context->GetThreadGlobals().SetForCurrentThread(); + + TestDataFile testManifest("Manifest-Good.yaml"); + auto manifest = YamlParser::CreateFromPath(testManifest); + + if (packageName) + { + manifest.Id = packageName.value(); + } + + context->Add<Data::Manifest>(std::move(manifest)); + + result.Context = context.get(); + + // Marking it an uninstall removes the extra work adding the items to the installing index + result.QueueItem = std::make_shared<OrchestratorQueueItem>(OrchestratorQueueItemId(AppInstaller::Utility::ConvertToUTF16(packageName.value_or("package")), L"source"), std::move(context), PackageOperationType::Uninstall); + + result.QueueItem->AddCommand(std::make_unique<TestDownloadCommand>()); + result.QueueItem->AddCommand(std::make_unique<TestOperationCommand>()); + + return result; +} + +// Runs an item through the orchestrator to ensure the basic functionality +TEST_CASE("ContextOrchestrator_UnitTestExecution", "[context_orchestrator]") +{ + ContextOrchestrator orchestrator; + + auto testItem = CreateTestItem(); + + wil::slim_event_manual_reset operationEvent; + testItem.Context->OperationCallback = [&]() { operationEvent.SetEvent(); }; + + orchestrator.EnqueueAndRunItem(testItem.QueueItem); + + REQUIRE(operationEvent.wait(c_DefaultWaitInMs)); + REQUIRE(testItem.QueueItem->GetCompletedEvent().wait(c_DefaultWaitInMs)); + REQUIRE(orchestrator.WaitForRunningItems(c_DefaultWaitInMs)); +} + +TEST_CASE("ContextOrchestrator_Disabled_NewEnqueue", "[context_orchestrator]") +{ + ContextOrchestrator orchestrator; + auto testItem = CreateTestItem(); + + auto reason = AppInstaller::CancelReason::AppShutdown; + orchestrator.Disable(reason); + REQUIRE_THROWS_HR(orchestrator.EnqueueAndRunItem(testItem.QueueItem), AppInstaller::ToHRESULT(reason)); +} + +TEST_CASE("ContextOrchestrator_Disabled_QueueTransition", "[context_orchestrator]") +{ + ContextOrchestrator orchestrator; + + auto testItem = CreateTestItem(); + + wil::slim_event_manual_reset downloadEvent; + testItem.Context->DownloadCallback = [&]() { downloadEvent.wait(); }; + + orchestrator.EnqueueAndRunItem(testItem.QueueItem); + + auto reason = AppInstaller::CancelReason::AppShutdown; + orchestrator.Disable(reason); + + downloadEvent.SetEvent(); + + REQUIRE(testItem.QueueItem->GetCompletedEvent().wait(c_DefaultWaitInMs)); + REQUIRE(testItem.Context->IsTerminated()); + // Context translates our shutdown HRs to E_ABORT + REQUIRE(E_ABORT == testItem.Context->GetTerminationHR()); +} + +// While item in { Queued, Running } in both queues, cancel everything +TEST_CASE("ContextOrchestrator_CancelAllItems", "[context_orchestrator]") +{ + // Limit to one thread for downloads so we can get a queued item + ContextOrchestrator orchestrator{ 1 }; + + auto downloadQueued = CreateTestItem("downloadQueued"); + auto downloadRunning = CreateTestItem("downloadRunning"); + wil::slim_event_manual_reset downloadBegunEvent; + wil::slim_event_manual_reset downloadWaitingEvent; + downloadRunning.Context->DownloadCallback = [&]() + { + downloadBegunEvent.SetEvent(); + downloadWaitingEvent.wait(); + }; + + auto operationQueued = CreateTestItem("operationQueued"); + auto operationRunning = CreateTestItem("operationRunning"); + wil::slim_event_manual_reset operationBegunEvent; + wil::slim_event_manual_reset operationWaitingEvent; + operationRunning.Context->OperationCallback = [&]() + { + operationBegunEvent.SetEvent(); + operationWaitingEvent.wait(); + }; + + orchestrator.EnqueueAndRunItem(operationRunning.QueueItem); + orchestrator.EnqueueAndRunItem(operationQueued.QueueItem); + orchestrator.EnqueueAndRunItem(downloadRunning.QueueItem); + orchestrator.EnqueueAndRunItem(downloadQueued.QueueItem); + + operationBegunEvent.wait(c_DefaultWaitInMs); + downloadBegunEvent.wait(c_DefaultWaitInMs); + + INFO("Pre-shutdown state: \n" << orchestrator.GetStatusString()); + + auto reason = AppInstaller::CancelReason::AppShutdown; + orchestrator.Disable(reason); + orchestrator.CancelQueuedItems(reason); + + operationWaitingEvent.SetEvent(); + downloadWaitingEvent.SetEvent(); + + if (!orchestrator.WaitForRunningItems(c_DefaultWaitInMs)) + { + INFO("Post-wait state: \n" << orchestrator.GetStatusString()); + FAIL("Timed out waiting for orchestrator to empty"); + } + + auto checkQueueItem = [](TestQueueItem& item) + { + REQUIRE(item.QueueItem->GetCompletedEvent().wait(0)); + REQUIRE(item.Context->IsTerminated()); + REQUIRE(E_ABORT == item.Context->GetTerminationHR()); + }; + + checkQueueItem(downloadQueued); + checkQueueItem(downloadRunning); + checkQueueItem(operationQueued); + checkQueueItem(operationRunning); +} diff --git a/src/AppInstallerCommonCore/Progress.cpp b/src/AppInstallerCommonCore/Progress.cpp @@ -2,9 +2,27 @@ // Licensed under the MIT License. #include "pch.h" #include "AppInstallerProgress.h" +#include <AppInstallerErrors.h> namespace AppInstaller { + HRESULT ToHRESULT(CancelReason reason) + { + HRESULT hr = E_ABORT; + + switch (reason) + { + case CancelReason::CtrlCSignal: + hr = APPINSTALLER_CLI_ERROR_CTRL_SIGNAL_RECEIVED; + break; + case CancelReason::AppShutdown: + hr = APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED; + break; + } + + return hr; + } + ProgressCallback::ProgressCallback(IProgressSink* sink) : m_sink(sink) { } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerProgress.h b/src/AppInstallerCommonCore/Public/AppInstallerProgress.h @@ -40,6 +40,17 @@ namespace AppInstaller DEFINE_ENUM_FLAG_OPERATORS(CancelReason); + // Gets the HRESULT associated with the given reason. + HRESULT ToHRESULT(CancelReason reason); + + // Interface that provides a callback to inform of cancellation. + struct ICancellable + { + // Inform of cancellation with provided reason. + // When `force` is true, it is expected to happen regardless of user intent. + virtual void Cancel(CancelReason reason, bool force) = 0; + }; + // Interface that only receives progress, and does not participate in cancellation. // This allows a sink be simple, and let ProgressCallback handle the complications // of cancel state. diff --git a/src/AppInstallerSharedLib/Public/winget/AsyncTokens.h b/src/AppInstallerSharedLib/Public/winget/AsyncTokens.h @@ -16,6 +16,7 @@ namespace AppInstaller::WinRT virtual bool IsCancelled() const noexcept = 0; virtual void Callback(winrt::delegate<>&& callback) const noexcept = 0; + virtual void Cancel() noexcept = 0; }; // Type containing winrt cancellation token wrapper. @@ -36,6 +37,13 @@ namespace AppInstaller::WinRT m_token.callback(std::move(callback)); } + void Cancel() noexcept override + { + // This is a bit of a hack, but the cancellation_token provides no access to the underlying promise for the purpose of cancellation. + static_assert(sizeof(Token) == sizeof(Promise*), "We expect that the cancellation_token has only 1 member and it is a Promise*"); + (*reinterpret_cast<Promise**>(&m_token))->Cancel(); + } + private: Token m_token; }; @@ -52,7 +60,7 @@ namespace AppInstaller::WinRT template <typename Promise> AsyncCancellation(winrt::impl::cancellation_token<Promise>&& token) { - m_token = std::make_unique<details::AsyncCancellationT<Promise>>(std::move(token)); + m_token = std::make_shared<details::AsyncCancellationT<Promise>>(std::move(token)); } // Returns true if the operation has been cancelled, false if not. @@ -80,8 +88,13 @@ namespace AppInstaller::WinRT } } + std::weak_ptr<details::AsyncCancellationTypeErasure> GetWeak() + { + return m_token; + } + private: - std::unique_ptr<details::AsyncCancellationTypeErasure> m_token; + std::shared_ptr<details::AsyncCancellationTypeErasure> m_token; }; namespace details @@ -167,6 +180,13 @@ namespace AppInstaller::WinRT m_token = std::make_unique<details::AsyncProgressEventHandlerT<ResultT, ProgressT>>(std::move(progress)); } + // Create a cancellation only object. + template <typename Promise> + AsyncProgress(winrt::impl::cancellation_token<Promise>&& cancellation) : + AsyncCancellation(std::move(cancellation)) + { + } + // Sends progress if this object is not empty. void Progress(ProgressT const& progress) const { diff --git a/src/AppInstallerSharedLib/Public/winget/IConfigurationStaticsInternals.h b/src/AppInstallerSharedLib/Public/winget/IConfigurationStaticsInternals.h @@ -21,5 +21,11 @@ namespace AppInstaller::WinRT public: virtual /* [local] */ HRESULT STDMETHODCALLTYPE SetExperimentalState( UINT32 state) = 0; + + virtual /* [local] */ HRESULT STDMETHODCALLTYPE BlockNewWorkForShutdown() = 0; + + virtual /* [local] */ HRESULT STDMETHODCALLTYPE BeginShutdown() = 0; + + virtual /* [local] */ HRESULT STDMETHODCALLTYPE WaitForShutdown() = 0; }; } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs b/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs @@ -7,6 +7,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures { using System; + using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using Microsoft.Management.Configuration.Processor; @@ -62,7 +63,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures throw new DirectoryNotFoundException(this.ExternalModulesPath); } - this.ConfigurationStatics = new ConfigurationStaticFunctions().As<IConfigurationStatics2>(); + this.RecreateStatics(); } /// <summary> @@ -88,7 +89,16 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures /// <summary> /// Gets the configuration statics object to use. /// </summary> - internal IConfigurationStatics2 ConfigurationStatics { get; private init; } + internal IConfigurationStatics2 ConfigurationStatics { get; private set; } + + /// <summary> + /// Creates a new statics object for use by the tests. + /// </summary> + [MemberNotNull("ConfigurationStatics")] + public void RecreateStatics() + { + this.ConfigurationStatics = new ConfigurationStaticFunctions().As<IConfigurationStatics2>(); + } /// <summary> /// Creates a runspace adding the test module path. diff --git a/src/Microsoft.Management.Configuration.UnitTests/Microsoft.Management.Configuration.UnitTests.csproj b/src/Microsoft.Management.Configuration.UnitTests/Microsoft.Management.Configuration.UnitTests.csproj @@ -49,6 +49,9 @@ <Private>True</Private> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </ProjectReference> + <ProjectReference Include="..\WinGetTestCommon\WinGetTestCommon.csproj"> + <Private>True</Private> + </ProjectReference> </ItemGroup> <Import Project="..\targets\ReferenceEmbeddedCsWinRTProject.targets" /> diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ShutdownTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ShutdownTests.cs @@ -0,0 +1,185 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ShutdownTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using WinGetTestCommon; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Unit tests for running test on the processor. + /// </summary> + [Collection("UnitTestCollection")] + [OutOfProc] + public class ShutdownTests : ConfigurationProcessorTestBase + { + /// <summary> + /// Initializes a new instance of the <see cref="ShutdownTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ShutdownTests(UnitTestFixture fixture, ITestOutputHelper log) + : base(fixture, log) + { + } + + /// <summary> + /// Initiates a shutdown on the process when it is running a synchronous operation. + /// </summary> + [Fact] + public void ShutdownSynchronization_SyncCall() + { + this.Fixture.RecreateStatics(); + + ConfigurationSet configurationSet = this.ConfigurationSet(); + ConfigurationUnit configurationUnitWaits = this.ConfigurationUnit(); + ConfigurationUnit configurationUnitWorks = this.ConfigurationUnit(); + configurationSet.Units = new ConfigurationUnit[] { configurationUnitWaits, configurationUnitWorks }; + + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + TestConfigurationSetProcessor setProcessor = factory.CreateTestProcessor(configurationSet); + TestConfigurationUnitProcessor unitProcessor = setProcessor.CreateTestProcessor(configurationUnitWaits); + + ManualResetEvent isWaiting = new ManualResetEvent(false); + ManualResetEvent waitingOn = new ManualResetEvent(false); + unitProcessor.TestSettingsDelegate = () => + { + isWaiting.Set(); + waitingOn.WaitOne(); + return new TestSettingsResultInstance(configurationUnitWaits) { TestResult = ConfigurationTestResult.Positive }; + }; + + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + TestConfigurationSetResult? result = null; + Exception? exception = null; + + ManualResetEvent syncCallDone = new ManualResetEvent(false); + Thread thread = new Thread(() => + { + try + { + result = processor.TestSet(configurationSet); + } + catch (Exception ex) + { + exception = ex; + } + + syncCallDone.Set(); + }); + thread.Start(); + + Assert.True(isWaiting.WaitOne(5000)); + + var servers = WinGetServerInstance.GetInstances(); + Assert.Single(servers); + + var server = servers[0]; + Assert.True(server.HasWindow); + + // This is the call pattern from Windows + this.SendMessageAndLog(server, WindowMessage.QueryEndSession); + + Thread thread2 = new Thread(() => + { + // Release the wait after initiating the shutdown, but before waiting on it + waitingOn.Set(); + }); + thread2.Start(); + + this.SendMessageAndLog(server, WindowMessage.EndSession); + this.SendMessageAndLog(server, WindowMessage.Close); + + Assert.True(syncCallDone.WaitOne(5000)); + + Assert.NotNull(exception); + + Assert.True(server.Process.WaitForExit(5000)); + } + + /// <summary> + /// Initiates a shutdown on the process when it is running an asynchronous operation. + /// </summary> + [Fact] + public void ShutdownSynchronization_AsyncCall() + { + this.Fixture.RecreateStatics(); + + ConfigurationSet configurationSet = this.ConfigurationSet(); + ConfigurationUnit configurationUnitWaits = this.ConfigurationUnit(); + ConfigurationUnit configurationUnitWorks = this.ConfigurationUnit(); + configurationSet.Units = new ConfigurationUnit[] { configurationUnitWaits, configurationUnitWorks }; + + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + TestConfigurationSetProcessor setProcessor = factory.CreateTestProcessor(configurationSet); + TestConfigurationUnitProcessor unitProcessor = setProcessor.CreateTestProcessor(configurationUnitWaits); + + ManualResetEvent isWaiting = new ManualResetEvent(false); + ManualResetEvent waitingOn = new ManualResetEvent(false); + unitProcessor.TestSettingsDelegate = () => + { + isWaiting.Set(); + waitingOn.WaitOne(); + return new TestSettingsResultInstance(configurationUnitWaits) { TestResult = ConfigurationTestResult.Positive }; + }; + + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + var operation = processor.TestSetAsync(configurationSet); + Assert.True(isWaiting.WaitOne(5000)); + + var servers = WinGetServerInstance.GetInstances(); + Assert.Single(servers); + + var server = servers[0]; + Assert.True(server.HasWindow); + + // This is the call pattern from Windows + this.SendMessageAndLog(server, WindowMessage.QueryEndSession); + + Thread thread2 = new Thread(() => + { + // Release the wait after initiating the shutdown, but before waiting on it + waitingOn.Set(); + }); + thread2.Start(); + + this.SendMessageAndLog(server, WindowMessage.EndSession); + this.SendMessageAndLog(server, WindowMessage.Close); + + Assert.ThrowsAny<Exception>(() => operation.GetAwaiter().GetResult()); + + Assert.True(server.Process.WaitForExit(5000)); + } + + private void SendMessageAndLog(WinGetServerInstance server, WindowMessage message) + { + this.Log.WriteLine($"Sending message {message} to process {server.Process.Id}..."); + try + { + if (server.SendMessage(message)) + { + this.Log.WriteLine("... succeeded."); + } + else + { + this.Log.WriteLine("... failed."); + } + } + catch (Exception e) + { + this.Log.WriteLine($"... had exception: {e.Message}"); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -358,7 +358,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation co_return GetSetDetailsImpl(localSet, detailFlags, { co_await winrt::get_progress_token(), co_await winrt::get_cancellation_token()}); } - Windows::Foundation::Collections::IVector<Configuration::ConfigurationSet> ConfigurationProcessor::GetConfigurationHistoryImpl(AppInstaller::WinRT::AsyncCancellation cancellation) + Windows::Foundation::Collections::IVector<Configuration::ConfigurationSet> ConfigurationProcessor::GetConfigurationHistoryImpl(ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -378,7 +378,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::GetConfigurationSetDetailsResult ConfigurationProcessor::GetSetDetailsImpl( const Configuration::ConfigurationSet& configurationSet, ConfigurationUnitDetailFlags detailFlags, - AppInstaller::WinRT::AsyncProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> progress) + ShutdownAwareAsyncProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> progress) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -480,7 +480,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::ApplyConfigurationSetResult ConfigurationProcessor::ApplySetImpl( const Configuration::ConfigurationSet& configurationSet, ApplyConfigurationSetFlags flags, - AppInstaller::WinRT::AsyncProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> progress) + ShutdownAwareAsyncProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> progress) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -561,7 +561,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (sequencer.Enqueue(configurationSet)) { updateState(ConfigurationSetState::Pending); - sequencer.Wait(progress); + sequencer.Wait(progress.GetCancellation()); } } @@ -654,7 +654,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::TestConfigurationSetResult ConfigurationProcessor::TestSetImpl( const Configuration::ConfigurationSet& configurationSet, - AppInstaller::WinRT::AsyncProgress<TestConfigurationSetResult, TestConfigurationUnitResult> progress) + ShutdownAwareAsyncProgress<TestConfigurationSetResult, TestConfigurationUnitResult> progress) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -727,7 +727,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::GetConfigurationUnitSettingsResult ConfigurationProcessor::GetUnitSettingsImpl( const ConfigurationUnit& unit, - AppInstaller::WinRT::AsyncCancellation cancellation) + ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -790,7 +790,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::GetAllConfigurationUnitSettingsResult ConfigurationProcessor::GetAllUnitSettingsImpl( const ConfigurationUnit& unit, - AppInstaller::WinRT::AsyncCancellation cancellation) + ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -861,7 +861,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::GetAllConfigurationUnitsResult ConfigurationProcessor::GetAllUnitsImpl( const ConfigurationUnit& unit, - AppInstaller::WinRT::AsyncCancellation cancellation) + ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -978,7 +978,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::IVector<IConfigurationUnitProcessorDetails> ConfigurationProcessor::FindUnitProcessorsImpl( const FindUnitProcessorsOptions& findOptions, - AppInstaller::WinRT::AsyncCancellation cancellation) + ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -1019,7 +1019,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::ApplyConfigurationUnitResult ConfigurationProcessor::ApplyUnitImpl( const ConfigurationUnit& unit, - AppInstaller::WinRT::AsyncCancellation cancellation) + ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); @@ -1085,7 +1085,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::TestConfigurationUnitResult ConfigurationProcessor::TestUnitImpl( const ConfigurationUnit& unit, - AppInstaller::WinRT::AsyncCancellation cancellation) + ShutdownAwareAsyncCancellation cancellation) { auto threadGlobals = m_threadGlobals.SetForCurrentThread(); diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -9,6 +9,7 @@ #include "ConfigurationStatus.h" #include "Database/ConfigurationDatabase.h" #include <winget/AsyncTokens.h> +#include "ShutdownSynchronization.h" #include <winget/ILifetimeWatcher.h> #include <functional> @@ -111,35 +112,35 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationChange(const Configuration::ConfigurationSet& set, const Configuration::ConfigurationChangeData& data); private: - Windows::Foundation::Collections::IVector<Configuration::ConfigurationSet> GetConfigurationHistoryImpl(AppInstaller::WinRT::AsyncCancellation cancellation = {}); + Windows::Foundation::Collections::IVector<Configuration::ConfigurationSet> GetConfigurationHistoryImpl(ShutdownAwareAsyncCancellation cancellation = {}); GetConfigurationSetDetailsResult GetSetDetailsImpl( const Configuration::ConfigurationSet& configurationSet, ConfigurationUnitDetailFlags detailFlags, - AppInstaller::WinRT::AsyncProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> progress = {}); + ShutdownAwareAsyncProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> progress = {}); GetConfigurationUnitDetailsResult GetUnitDetailsImpl(const ConfigurationUnit& unit, ConfigurationUnitDetailFlags detailFlags); ApplyConfigurationSetResult ApplySetImpl( const Configuration::ConfigurationSet& configurationSet, ApplyConfigurationSetFlags flags, - AppInstaller::WinRT::AsyncProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> progress = {}); + ShutdownAwareAsyncProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> progress = {}); TestConfigurationSetResult TestSetImpl( const Configuration::ConfigurationSet& configurationSet, - AppInstaller::WinRT::AsyncProgress<TestConfigurationSetResult, TestConfigurationUnitResult> progress = {}); + ShutdownAwareAsyncProgress<TestConfigurationSetResult, TestConfigurationUnitResult> progress = {}); - GetConfigurationUnitSettingsResult GetUnitSettingsImpl(const ConfigurationUnit& unit, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + GetConfigurationUnitSettingsResult GetUnitSettingsImpl(const ConfigurationUnit& unit, ShutdownAwareAsyncCancellation cancellation = {}); - GetAllConfigurationUnitSettingsResult GetAllUnitSettingsImpl(const ConfigurationUnit& unit, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + GetAllConfigurationUnitSettingsResult GetAllUnitSettingsImpl(const ConfigurationUnit& unit, ShutdownAwareAsyncCancellation cancellation = {}); - Configuration::GetAllConfigurationUnitsResult GetAllUnitsImpl(const ConfigurationUnit& unit, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + Configuration::GetAllConfigurationUnitsResult GetAllUnitsImpl(const ConfigurationUnit& unit, ShutdownAwareAsyncCancellation cancellation = {}); - Windows::Foundation::Collections::IVector<Configuration::IConfigurationUnitProcessorDetails> FindUnitProcessorsImpl(const Configuration::FindUnitProcessorsOptions& findOptions, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + Windows::Foundation::Collections::IVector<Configuration::IConfigurationUnitProcessorDetails> FindUnitProcessorsImpl(const Configuration::FindUnitProcessorsOptions& findOptions, ShutdownAwareAsyncCancellation cancellation = {}); - Configuration::ApplyConfigurationUnitResult ApplyUnitImpl(const ConfigurationUnit& unit, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + Configuration::ApplyConfigurationUnitResult ApplyUnitImpl(const ConfigurationUnit& unit, ShutdownAwareAsyncCancellation cancellation = {}); - Configuration::TestConfigurationUnitResult TestUnitImpl(const ConfigurationUnit& unit, AppInstaller::WinRT::AsyncCancellation cancellation = {}); + Configuration::TestConfigurationUnitResult TestUnitImpl(const ConfigurationUnit& unit, ShutdownAwareAsyncCancellation cancellation = {}); IConfigurationGroupProcessor GetSetGroupProcessor(const Configuration::ConfigurationSet& configurationSet); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetApplyProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationSetApplyProcessor.h @@ -6,7 +6,7 @@ #include "ApplyGroupSettingsResult.h" #include "ApplyConfigurationUnitResult.h" #include "ConfigurationUnitResultInformation.h" -#include <winget/AsyncTokens.h> +#include "ShutdownSynchronization.h" #include <map> #include <string> @@ -22,7 +22,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation using ConfigurationSetChangeData = Configuration::ConfigurationSetChangeData; using result_type = decltype(make_self<wil::details::module_count_wrapper<implementation::ApplyGroupSettingsResult>>()); - using progress_type = AppInstaller::WinRT::AsyncProgress<IApplyGroupSettingsResult, IApplyGroupMemberSettingsResult>; + using progress_type = ShutdownAwareAsyncProgress<IApplyGroupSettingsResult, IApplyGroupMemberSettingsResult>; ConfigurationSetApplyProcessor(const ConfigurationSet& configurationSet, IConfigurationSetProcessor setProcessor, progress_type&& progress); diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp @@ -7,7 +7,8 @@ #include "ConfigurationSet.h" #include "ConfigurationProcessor.h" #include "ConfigurationParameter.h" -#include "FindUnitProcessorsOptions.h" +#include "FindUnitProcessorsOptions.h" +#include "ShutdownSynchronization.h" #include <AppInstallerStrings.h> #include <winget/ConfigurationSetProcessorHandlers.h> @@ -62,5 +63,23 @@ namespace winrt::Microsoft::Management::Configuration::implementation { m_state = static_cast<AppInstaller::WinRT::ConfigurationStaticsInternalsStateFlags>(state); return S_OK; + } + + HRESULT STDMETHODCALLTYPE ConfigurationStaticFunctions::BlockNewWorkForShutdown() + { + ShutdownSynchronization::Instance().BlockNewWork(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ConfigurationStaticFunctions::BeginShutdown() + { + ShutdownSynchronization::Instance().CancelAllWork(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ConfigurationStaticFunctions::WaitForShutdown() + { + ShutdownSynchronization::Instance().Wait(); + return S_OK; } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.h b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.h @@ -20,7 +20,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::FindUnitProcessorsOptions CreateFindUnitProcessorsOptions(); // IConfigurationStaticsInternals - HRESULT STDMETHODCALLTYPE SetExperimentalState(UINT32 state); + HRESULT STDMETHODCALLTYPE SetExperimentalState(UINT32 state); + HRESULT STDMETHODCALLTYPE BlockNewWorkForShutdown(); + HRESULT STDMETHODCALLTYPE BeginShutdown(); + HRESULT STDMETHODCALLTYPE WaitForShutdown(); private: // By default, enable all state so that in-proc usage contains it. diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -238,6 +238,7 @@ <ClInclude Include="OpenConfigurationSetResult.h" /> <ClInclude Include="ParsingMacros.h" /> <ClInclude Include="pch.h" /> + <ClInclude Include="ShutdownSynchronization.h" /> <ClInclude Include="Telemetry\Telemetry.h" /> <ClInclude Include="Telemetry\TraceLogging.h" /> <ClInclude Include="TestConfigurationSetResult.h" /> @@ -297,6 +298,7 @@ <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" /> + <ClCompile Include="ShutdownSynchronization.cpp" /> <ClCompile Include="Telemetry\Telemetry.cpp" /> <ClCompile Include="Telemetry\TraceLogging.cpp" /> <ClCompile Include="TestConfigurationSetResult.cpp" /> diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -162,6 +162,9 @@ <ClCompile Include="FindUnitProcessorsOptions.cpp"> <Filter>API Source</Filter> </ClCompile> + <ClCompile Include="ShutdownSynchronization.cpp"> + <Filter>Internals</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h" /> @@ -333,6 +336,9 @@ <ClInclude Include="FindUnitProcessorsOptions.h"> <Filter>API Headers</Filter> </ClInclude> + <ClInclude Include="ShutdownSynchronization.h"> + <Filter>Internals</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" /> diff --git a/src/Microsoft.Management.Configuration/ShutdownSynchronization.cpp b/src/Microsoft.Management.Configuration/ShutdownSynchronization.cpp @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ShutdownSynchronization.h" + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + ShutdownAwareAsyncCancellation::ShutdownAwareAsyncCancellation() + { + m_defaultPromise = std::make_unique<ShutdownAwareAsyncCancellationPromise>(); + m_cancellation = std::make_unique<AppInstaller::WinRT::AsyncCancellation>(winrt::impl::cancellation_token<ShutdownAwareAsyncCancellationPromise>{ m_defaultPromise.get() }); + RegisterWithShutdownSynchronization(); + } + + ShutdownAwareAsyncCancellation::~ShutdownAwareAsyncCancellation() + { + if (m_cancellation) + { + ShutdownSynchronization::Instance().RegisterWorkEnd(m_cancellation->GetWeak()); + } + } + + bool ShutdownAwareAsyncCancellation::IsCancelled() const noexcept + { + return m_cancellation->IsCancelled(); + } + + void ShutdownAwareAsyncCancellation::ThrowIfCancelled() const + { + m_cancellation->ThrowIfCancelled(); + } + + void ShutdownAwareAsyncCancellation::Callback(winrt::delegate<>&& callback) const noexcept + { + m_cancellation->Callback(std::move(callback)); + } + + void ShutdownAwareAsyncCancellation::RegisterWithShutdownSynchronization() + { + ShutdownSynchronization::Instance().RegisterWorkBegin(m_cancellation->GetWeak()); + } + + Windows::Foundation::AsyncStatus ShutdownAwareAsyncCancellationPromise::Status() noexcept + { + return m_status.load(std::memory_order_acquire); + } + + void ShutdownAwareAsyncCancellationPromise::cancellation_callback(winrt::delegate<>&& cancel) noexcept + { + { + slim_lock_guard const guard(m_lock); + + if (m_status.load(std::memory_order_relaxed) != Windows::Foundation::AsyncStatus::Canceled) + { + m_cancel = std::move(cancel); + return; + } + } + + if (cancel) + { + cancel(); + } + } + + bool ShutdownAwareAsyncCancellationPromise::enable_cancellation_propagation(bool) noexcept + { + THROW_HR(E_NOTIMPL); + } + + void ShutdownAwareAsyncCancellationPromise::Cancel() noexcept + { + winrt::delegate<> cancel; + + { + slim_lock_guard const guard(m_lock); + + if (m_status.load(std::memory_order_relaxed) == Windows::Foundation::AsyncStatus::Started) + { + m_status.store(Windows::Foundation::AsyncStatus::Canceled, std::memory_order_relaxed); + cancel = std::move(m_cancel); + } + } + + if (cancel) + { + cancel(); + } + } + + ShutdownSynchronization& ShutdownSynchronization::Instance() + { + static ShutdownSynchronization s_instance; + return s_instance; + } + + void ShutdownSynchronization::BlockNewWork() + { + m_disabled = true; + } + + void ShutdownSynchronization::RegisterWorkBegin(CancellableWeakPtr&& ptr) + { + if (m_disabled) + { + THROW_HR(E_ABORT); + } + + std::lock_guard<std::mutex> lock{ m_workLock }; + m_work.emplace(std::move(ptr)); + m_noActiveWork.ResetEvent(); + } + + void ShutdownSynchronization::RegisterWorkEnd(CancellableWeakPtr&& ptr) + { + std::lock_guard<std::mutex> lock{ m_workLock }; + + auto itr = m_work.find(ptr); + if (itr != m_work.end()) + { + m_work.erase(itr); + + if (m_work.empty()) + { + m_noActiveWork.SetEvent(); + } + } + } + + void ShutdownSynchronization::CancelAllWork() + { + std::lock_guard<std::mutex> lock{ m_workLock }; + + for (auto itr = m_work.begin(); itr != m_work.end(); ++itr) + { + if (auto locked = itr->lock()) + { + locked->Cancel(); + } + else + { + m_work.erase(itr); + } + } + + if (m_work.empty()) + { + m_noActiveWork.SetEvent(); + } + } + + void ShutdownSynchronization::Wait() + { + for (;;) + { + { + std::lock_guard<std::mutex> lock{ m_workLock }; + + // Check for any inactive work before waiting + for (auto itr = m_work.begin(); itr != m_work.end(); ++itr) + { + if (!itr->lock()) + { + m_work.erase(itr); + } + } + + if (m_work.empty()) + { + break; + } + } + + if (m_noActiveWork.wait(250)) + { + break; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration/ShutdownSynchronization.h b/src/Microsoft.Management.Configuration/ShutdownSynchronization.h @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/AsyncTokens.h> +#include <wil/resource.h> +#include <atomic> +#include <set> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Promise type implementation for cancellation_token + struct ShutdownAwareAsyncCancellationPromise + { + Windows::Foundation::AsyncStatus Status() noexcept; + void cancellation_callback(winrt::delegate<>&& cancel) noexcept; + bool enable_cancellation_propagation(bool value) noexcept; + void Cancel() noexcept; + + private: + slim_mutex m_lock; + winrt::delegate<> m_cancel; + std::atomic<Windows::Foundation::AsyncStatus> m_status{ Windows::Foundation::AsyncStatus::Started }; + }; + + // An AsyncCancellation that registers with ShutdownSynchronization. + struct ShutdownAwareAsyncCancellation + { + // Creates a cancellable object without an external cancellation token. + ShutdownAwareAsyncCancellation(); + + // Create a cancellation object from the winrt token. + template <typename Promise> + ShutdownAwareAsyncCancellation(winrt::impl::cancellation_token<Promise>&& token) + { + m_cancellation = std::make_unique<AppInstaller::WinRT::AsyncCancellation>(std::move(token)); + RegisterWithShutdownSynchronization(); + } + + // Removes the shutdown registration. + ~ShutdownAwareAsyncCancellation(); + + // Returns true if the operation has been cancelled, false if not. + bool IsCancelled() const noexcept; + + // Throws the appropriate exception if the operation has been cancelled. + void ThrowIfCancelled() const; + + // Sets a callback that will be invoked on cancellation. + void Callback(winrt::delegate<>&& callback) const noexcept; + + protected: + void RegisterWithShutdownSynchronization(); + + std::unique_ptr<ShutdownAwareAsyncCancellationPromise> m_defaultPromise; + std::unique_ptr<AppInstaller::WinRT::AsyncCancellation> m_cancellation; + }; + + // An AsyncProgress that registers with ShutdownSynchronization. + template <typename ResultT, typename ProgressT> + struct ShutdownAwareAsyncProgress + { + // Creates a cancellable object without an external cancellation token. + ShutdownAwareAsyncProgress() + { + m_defaultPromise = std::make_unique<ShutdownAwareAsyncCancellationPromise>(); + m_progress = std::make_unique<AppInstaller::WinRT::AsyncProgress<ResultT, ProgressT>>(winrt::impl::cancellation_token<ShutdownAwareAsyncCancellationPromise>{ m_defaultPromise.get() }); + RegisterWithShutdownSynchronization(); + } + + // Create a progress object from the winrt token. + template <typename Promise> + ShutdownAwareAsyncProgress(winrt::impl::progress_token<Promise, ProgressT>&& progress, winrt::impl::cancellation_token<Promise>&& cancellation) + { + m_progress = std::make_unique<AppInstaller::WinRT::AsyncProgress<ResultT, ProgressT>>(std::move(progress), std::move(cancellation)); + RegisterWithShutdownSynchronization(); + } + + // Create a progress object from an EventHandler. + template <typename Promise> + ShutdownAwareAsyncProgress(winrt::Windows::Foundation::EventHandler<ProgressT>&& progress, winrt::impl::cancellation_token<Promise>&& cancellation) + { + m_progress = std::make_unique<AppInstaller::WinRT::AsyncProgress<ResultT, ProgressT>>(std::move(progress), std::move(cancellation)); + RegisterWithShutdownSynchronization(); + } + + ShutdownAwareAsyncProgress(const ShutdownAwareAsyncProgress&) = delete; + ShutdownAwareAsyncProgress& operator=(const ShutdownAwareAsyncProgress&) = delete; + + ShutdownAwareAsyncProgress(ShutdownAwareAsyncProgress&&) = default; + ShutdownAwareAsyncProgress& operator=(ShutdownAwareAsyncProgress&&) = default; + + // Removes the shutdown registration. + ~ShutdownAwareAsyncProgress() + { + if (m_progress) + { + ShutdownSynchronization::Instance().RegisterWorkEnd(m_progress->GetWeak()); + } + } + + AppInstaller::WinRT::AsyncCancellation& GetCancellation() + { + return *m_progress; + } + + // Returns true if the operation has been cancelled, false if not. + bool IsCancelled() const noexcept + { + return m_progress->IsCancelled(); + } + + // Throws the appropriate exception if the operation has been cancelled. + void ThrowIfCancelled() const + { + m_progress->ThrowIfCancelled(); + } + + // Sets a callback that will be invoked on cancellation. + void Callback(winrt::delegate<>&& callback) const noexcept + { + m_progress->Callback(std::move(callback)); + } + + // Sends progress if this object is not empty. + void Progress(ProgressT const& progress) const + { + m_progress->Progress(progress); + } + + // Sets the result onto the progress object if it is not empty. + void Result(ResultT const& result) const + { + m_progress->Result(result); + } + + protected: + void RegisterWithShutdownSynchronization() + { + ShutdownSynchronization::Instance().RegisterWorkBegin(m_progress->GetWeak()); + } + + std::unique_ptr<ShutdownAwareAsyncCancellationPromise> m_defaultPromise; + std::unique_ptr<AppInstaller::WinRT::AsyncProgress<ResultT, ProgressT>> m_progress; + }; + + struct ShutdownSynchronization + { + using CancellableWeakPtr = std::weak_ptr<AppInstaller::WinRT::details::AsyncCancellationTypeErasure>; + + ShutdownSynchronization() = default; + + static ShutdownSynchronization& Instance(); + + // Signals that new work should be blocked. + void BlockNewWork(); + + // Call to register the begin and end of work. + void RegisterWorkBegin(CancellableWeakPtr&& ptr); + void RegisterWorkEnd(CancellableWeakPtr&& ptr); + + // Cancels all currently registered work. + void CancelAllWork(); + + // Waits for outstanding work to be completed. + void Wait(); + + private: + std::atomic_bool m_disabled{ false }; + std::mutex m_workLock; + std::set<CancellableWeakPtr, std::owner_less<CancellableWeakPtr>> m_work; + wil::slim_event_manual_reset m_noActiveWork{ true }; + }; +} diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -149,6 +149,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + PackageManager::PackageManager() + { + Execution::ContextOrchestrator::RegisterForShutdownSynchronization(); + } + winrt::Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Management::Deployment::PackageCatalogReference> PackageManager::GetPackageCatalogs() { LogStartupIfApplicable(); diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h @@ -17,7 +17,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation [uuid(WINGET_OUTOFPROC_COM_CLSID_PackageManager)] struct PackageManager : PackageManagerT<PackageManager> { - PackageManager() = default; + PackageManager(); winrt::Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Management::Deployment::PackageCatalogReference> GetPackageCatalogs(); winrt::Microsoft::Management::Deployment::PackageCatalogReference GetPredefinedPackageCatalog(winrt::Microsoft::Management::Deployment::PredefinedPackageCatalog const& predefinedPackageCatalog); diff --git a/src/WinGetTestCommon/WinGetServerInstance.cs b/src/WinGetTestCommon/WinGetServerInstance.cs @@ -0,0 +1,196 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetServerInstance.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetTestCommon +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.InteropServices; + + /// <summary> + /// Represents an instance of a Windows Package Manager (WinGet) server. + /// </summary> + public class WinGetServerInstance + { + /// <summary> + /// The name of the executable for the COM server. + /// </summary> + public const string ServerExecutableName = "WindowsPackageManagerServer"; + + /// <summary> + /// The package family name for the development package. + /// </summary> + public const string DevelopmentPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe"; + + /// <summary> + /// The window name for the COM server message window. + /// </summary> + public const string TargetWindowName = "WingetMessageOnlyWindow"; + + /// <summary> + /// Gets the process for the server. + /// </summary> + public required Process Process { get; init; } + + /// <summary> + /// Gets a value indicating whether the current server has an associated window. + /// </summary> + public bool HasWindow + { + get + { + return EnumerateWindowHandles(TargetWindowName).Count > 0; + } + } + + /// <summary> + /// Sends a specified message to a window. + /// </summary> + /// <param name="message">The message to be sent to the window.</param> + /// <returns>True to indicate that the message was sent and processed within the timeout; false otherwise.</returns> + public bool SendMessage(WindowMessage message) + { + const int TRUE = 0x1; + const int ENDSESSION_CLOSEAPP = 0x1; + const uint SMTO_ABORTIFHUNG = 0x0002; + const uint TIMEOUT_MS = 5000; + + var windowHandles = EnumerateWindowHandles(TargetWindowName); + + if (windowHandles.Count > 1) + { + throw new InvalidOperationException($"Target process has more than one window named `{TargetWindowName}`"); + } + + foreach (var hWnd in windowHandles) + { + IntPtr result; + bool success; + switch (message) + { + case WindowMessage.Close: + success = SendMessageTimeout(hWnd, (uint)message, IntPtr.Zero, IntPtr.Zero, SMTO_ABORTIFHUNG, TIMEOUT_MS, out result) != IntPtr.Zero; + break; + case WindowMessage.QueryEndSession: + success = SendMessageTimeout(hWnd, (uint)message, IntPtr.Zero, (IntPtr)ENDSESSION_CLOSEAPP, SMTO_ABORTIFHUNG, TIMEOUT_MS, out result) != IntPtr.Zero; + break; + case WindowMessage.EndSession: + success = SendMessageTimeout(hWnd, (uint)message, (IntPtr)TRUE, (IntPtr)ENDSESSION_CLOSEAPP, SMTO_ABORTIFHUNG, TIMEOUT_MS, out result) != IntPtr.Zero; + break; + default: + throw new NotImplementedException("Unexpected window message"); + } + + return success; + } + + return false; + } + + /// <summary> + /// Retrieves an array of all available WinGet server instances. + /// </summary> + /// <returns> + /// An array of <see cref="WinGetServerInstance"/> objects representing the available server instances. + /// The array will be empty if no instances are available. + /// </returns> + public static List<WinGetServerInstance> GetInstances() + { + Process[] processes = Process.GetProcessesByName(ServerExecutableName); + List<WinGetServerInstance> result = new List<WinGetServerInstance>(); + + foreach (Process process in processes) + { + try + { + string? familyName = GetProcessPackageFamilyName(process); + if (familyName == DevelopmentPackageFamilyName) + { + result.Add(new WinGetServerInstance { Process = process }); + } + } + catch + { + // Ignore processes that we can't access or that aren't packaged + } + } + + return result; + } + + private static string? GetProcessPackageFamilyName(Process process) + { + const int ERROR_INSUFFICIENT_BUFFER = 122; + int length = 0; + int result = GetPackageFamilyName(process.Handle, ref length, null); + if (result == ERROR_INSUFFICIENT_BUFFER) + { + var sb = new System.Text.StringBuilder(length); + result = GetPackageFamilyName(process.Handle, ref length, sb); + if (result == 0) + { + return sb.ToString(); + } + } + return null; + } + + private List<IntPtr> EnumerateWindowHandles(string windowName) + { + List<IntPtr> windowHandles = new List<IntPtr>(); + int processId = Process.Id; + + bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam) + { + GetWindowThreadProcessId(hWnd, out int windowProcessId); + if (windowProcessId == processId) + { + // Get the window title + var sb = new System.Text.StringBuilder(256); + int length = GetWindowText(hWnd, sb, sb.Capacity); + if (length > 0 && sb.ToString() == windowName) + { + windowHandles.Add(hWnd); + } + } + return true; + } + + EnumWindows(EnumWindowsProc, IntPtr.Zero); + return windowHandles; + } + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProcDelegate lpEnumFunc, IntPtr lParam); + + private delegate bool EnumWindowsProcDelegate(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SendMessageTimeout( + IntPtr hWnd, + uint Msg, + IntPtr wParam, + IntPtr lParam, + uint fuFlags, + uint uTimeout, + out IntPtr lpdwResult + ); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern int GetPackageFamilyName( + IntPtr hProcess, + ref int packageFamilyNameLength, + System.Text.StringBuilder? packageFamilyName + ); + } +} diff --git a/src/WinGetTestCommon/WinGetTestCommon.csproj b/src/WinGetTestCommon/WinGetTestCommon.csproj @@ -0,0 +1,10 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0-windows</TargetFramework> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\WinGetTestCommon\</OutDir> + <Platforms>x64;x86;arm64</Platforms> + <OutputType>Library</OutputType> + <Nullable>enable</Nullable> + </PropertyGroup> +</Project> diff --git a/src/WinGetTestCommon/WindowMessage.cs b/src/WinGetTestCommon/WindowMessage.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WindowMessage.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace WinGetTestCommon +{ + /// <summary> + /// Represents the Windows messages that can be sent or received by a window. + /// </summary> + public enum WindowMessage + { + /// <summary> + /// WM_CLOSE + /// </summary> + Close = 0x0010, + + /// <summary> + /// WM_QUERYENDSESSION + /// </summary> + QueryEndSession = 0x0011, + + /// <summary> + /// WM_ENDSESSION + /// </summary> + EndSession = 0x0016, + } +} diff --git a/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp b/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp @@ -11,6 +11,7 @@ #include <AppInstallerStrings.h> #include <winget/ConfigurationSetProcessorHandlers.h> #include <ConfigurationSetProcessorFactoryRemoting.h> +#include <ShutdownMonitoring.h> #include <winget/ILifetimeWatcher.h> #include <winget/IConfigurationStaticsInternals.h> #include <winget/GroupPolicy.h> @@ -29,6 +30,43 @@ namespace ConfigurationShim namespace { static std::atomic_bool s_canBeCreated{ true }; + + auto GetInternalStatics() + { + return winrt::Microsoft::Management::Configuration::ConfigurationStaticFunctions().as<AppInstaller::WinRT::IConfigurationStaticsInternals>(); + } + + void BlockNewWorkForShutdown(AppInstaller::CancelReason) + { + GetInternalStatics()->BlockNewWorkForShutdown(); + } + + void BeginShutdown(AppInstaller::CancelReason) + { + GetInternalStatics()->BeginShutdown(); + } + + void WaitForShutdown() + { + GetInternalStatics()->WaitForShutdown(); + } + + void RegisterForShutdownSynchronization() + { + static std::once_flag registerComponentOnceFlag; + std::call_once(registerComponentOnceFlag, + [&]() + { + using namespace AppInstaller::ShutdownMonitoring; + + ServerShutdownSynchronization::ComponentSystem component; + component.BlockNewWork = BlockNewWorkForShutdown; + component.BeginShutdown = BeginShutdown; + component.Wait = WaitForShutdown; + + ServerShutdownSynchronization::AddComponent(component); + }); + } } CLSID CLSID_ConfigurationObjectLifetimeWatcher = { 0x89a8f1d4,0x1e24,0x46a4,{0x9f,0x6c,0x65,0x78,0xb0,0x47,0xf2,0xf7} }; @@ -57,6 +95,7 @@ namespace ConfigurationShim if (IsConfigurationAvailable()) { m_statics = winrt::Microsoft::Management::Configuration::ConfigurationStaticFunctions().as<winrt::Microsoft::Management::Configuration::IConfigurationStatics3>(); + RegisterForShutdownSynchronization(); } } diff --git a/src/WindowsPackageManager/main.cpp b/src/WindowsPackageManager/main.cpp @@ -15,6 +15,7 @@ #include <AppInstallerTelemetry.h> #include <AppInstallerErrors.h> #include <winget/GroupPolicy.h> +#include <ShutdownMonitoring.h> #include <ComClsids.h> using namespace winrt::Microsoft::Management::Deployment; @@ -54,6 +55,7 @@ extern "C" WINDOWS_PACKAGE_MANAGER_API WindowsPackageManagerServerModuleCreate(WindowsPackageManagerServerModuleTerminationCallback callback) try { + AppInstaller::ShutdownMonitoring::ServerShutdownSynchronization::Initialize(callback); ::Microsoft::WRL::Module<::Microsoft::WRL::ModuleType::OutOfProc>::Create(callback); return S_OK; }