ContextOrchestrator.cpp (23715B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "ExecutionContext.h" 5 #include "ContextOrchestrator.h" 6 #include "COMContext.h" 7 #include "Commands/COMCommand.h" 8 #include "Public/ShutdownMonitoring.h" 9 #include "winget/UserSettings.h" 10 #include <Commands/RootCommand.h> 11 12 namespace AppInstaller::CLI::Execution 13 { 14 namespace 15 { 16 // Operation command queue used by install, uninstall and repair commands. 17 constexpr static std::string_view OperationCommandQueueName = "operation"sv; 18 19 // Callback function used by worker threads in the queue. 20 // context must be a pointer to a queue item. 21 void CALLBACK OrchestratorQueueWorkCallback(PTP_CALLBACK_INSTANCE, PVOID context, PTP_WORK) 22 { 23 auto queueItem = reinterpret_cast<OrchestratorQueueItem*>(context); 24 auto queue = queueItem->GetCurrentQueue(); 25 if (queue) 26 { 27 queue->RunItem(queueItem->GetId()); 28 } 29 } 30 31 // Get command queue name based on command name. 32 std::string_view GetCommandQueueName(std::string_view commandName) 33 { 34 if (commandName == COMInstallCommand::CommandName || commandName == COMUninstallCommand::CommandName || commandName == COMRepairCommand::CommandName) 35 { 36 return OperationCommandQueueName; 37 } 38 39 return commandName; 40 } 41 } 42 43 ContextOrchestrator& ContextOrchestrator::Instance() 44 { 45 static ContextOrchestrator s_instance; 46 return s_instance; 47 } 48 49 ContextOrchestrator::ContextOrchestrator() : ContextOrchestrator(std::thread::hardware_concurrency()) {} 50 51 ContextOrchestrator::ContextOrchestrator(unsigned int hardwareConcurrency) 52 { 53 ProgressCallback progress; 54 m_installingWriteableSource = Repository::Source(Repository::PredefinedSource::Installing); 55 m_installingWriteableSource.Open(progress); 56 57 // Decide how many threads to use for each command. 58 // We always allow only one install at a time. 59 // For download, if we can find the number of supported concurrent threads, 60 // use that as the maximum (up to 3); otherwise use a single thread. 61 const UINT32 maxDownloadThreads = 3; 62 const UINT32 operationThreads = 1; 63 const UINT32 downloadThreads = std::min(hardwareConcurrency > 1 ? hardwareConcurrency - 1 : 1, maxDownloadThreads); 64 65 AddCommandQueue(COMDownloadCommand::CommandName, downloadThreads); 66 AddCommandQueue(OperationCommandQueueName, operationThreads); 67 } 68 69 void ContextOrchestrator::AddCommandQueue(std::string_view commandName, UINT32 allowedThreads) 70 { 71 std::lock_guard<std::mutex> lockQueue{ m_queueLock }; 72 m_commandQueues.emplace(commandName, std::make_unique<OrchestratorQueue>(*this, commandName, allowedThreads)); 73 } 74 75 _Requires_lock_held_(m_queueLock) 76 std::shared_ptr<OrchestratorQueueItem> ContextOrchestrator::FindById(const OrchestratorQueueItemId& comparisonQueueItemId) 77 { 78 for (const auto& queue : m_commandQueues) 79 { 80 auto item = queue.second->FindById(comparisonQueueItemId); 81 if (item) 82 { 83 return item; 84 } 85 } 86 87 return {}; 88 } 89 90 void ContextOrchestrator::EnqueueAndRunItem(const std::shared_ptr<OrchestratorQueueItem>& item) 91 { 92 std::lock_guard<std::mutex> lockQueue{ m_queueLock }; 93 94 if (item->IsOnFirstCommand()) 95 { 96 // Directly error on attempting to enqueue first time 97 THROW_HR_IF(ToHRESULT(m_disabledReason), !m_enabled); 98 99 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INSTALL_ALREADY_RUNNING), FindById(item->GetId())); 100 101 // Log the beginning of the item 102 item->GetContext().GetThreadGlobals().GetTelemetryLogger().LogCommand(item->GetItemCommandName()); 103 } 104 else if (!m_enabled) 105 { 106 // On subsequent command enqueues, cancel and complete the item 107 item->GetContext().Cancel(m_disabledReason, true); 108 item->HandleItemCompletion(*this); 109 } 110 111 std::string commandQueueName{ GetCommandQueueName(item->GetNextCommand().Name()) }; 112 m_commandQueues.at(commandQueueName)->EnqueueAndRunItem(item); 113 } 114 115 void ContextOrchestrator::RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state) 116 { 117 std::lock_guard<std::mutex> lockQueue{ m_queueLock }; 118 for (const auto& queue : m_commandQueues) 119 { 120 if (queue.second->RemoveItemInState(item, state, true)) 121 { 122 return; 123 } 124 } 125 } 126 127 void ContextOrchestrator::CancelQueueItem(const OrchestratorQueueItem& item) 128 { 129 // Always cancel the item, even if it isn't running yet, to get the terminationHR set correctly. 130 item.GetContext().Cancel(CancelReason::Abort, true); 131 132 RemoveItemInState(item, OrchestratorQueueItemState::Queued); 133 } 134 135 std::shared_ptr<OrchestratorQueueItem> ContextOrchestrator::GetQueueItem(const OrchestratorQueueItemId& queueItemId) 136 { 137 std::lock_guard<std::mutex> lock{ m_queueLock }; 138 139 return FindById(queueItemId); 140 } 141 142 void ContextOrchestrator::AddItemManifestToInstallingSource(const OrchestratorQueueItem& queueItem) 143 { 144 if (queueItem.IsApplicableForInstallingSource()) 145 { 146 const auto& manifest = queueItem.GetContext().Get<Execution::Data::Manifest>(); 147 m_installingWriteableSource.AddPackageVersion(manifest, std::filesystem::path{ manifest.Id + '.' + manifest.Version }); 148 } 149 } 150 151 void ContextOrchestrator::RemoveItemManifestFromInstallingSource(const OrchestratorQueueItem& queueItem) 152 { 153 if (queueItem.IsApplicableForInstallingSource()) 154 { 155 const auto& manifest = queueItem.GetContext().Get<Execution::Data::Manifest>(); 156 m_installingWriteableSource.RemovePackageVersion(manifest, std::filesystem::path{ manifest.Id + '.' + manifest.Version }); 157 } 158 } 159 160 void ContextOrchestrator::RegisterForShutdownSynchronization() 161 { 162 static std::once_flag registerComponentOnceFlag; 163 std::call_once(registerComponentOnceFlag, 164 [&]() 165 { 166 using namespace ShutdownMonitoring; 167 168 ServerShutdownSynchronization::ComponentSystem component; 169 component.BlockNewWork = StaticDisable; 170 component.BeginShutdown = StaticCancelQueuedItems; 171 component.Wait = StaticWaitForRunningItems; 172 173 ServerShutdownSynchronization::AddComponent(component); 174 }); 175 } 176 177 void ContextOrchestrator::StaticDisable(CancelReason reason) 178 { 179 Instance().Disable(reason); 180 } 181 182 void ContextOrchestrator::StaticCancelQueuedItems(CancelReason reason) 183 { 184 Instance().CancelQueuedItems(reason); 185 } 186 187 void ContextOrchestrator::StaticWaitForRunningItems() 188 { 189 Instance().WaitForRunningItems(); 190 } 191 192 void ContextOrchestrator::Disable(CancelReason reason) 193 { 194 std::lock_guard<std::mutex> lock{ m_queueLock }; 195 m_enabled = false; 196 m_disabledReason = reason; 197 } 198 199 void ContextOrchestrator::CancelQueuedItems(CancelReason reason) 200 { 201 std::lock_guard<std::mutex> lock{ m_queueLock }; 202 for (const auto& queue : m_commandQueues) 203 { 204 queue.second->CancelAllItems(reason); 205 } 206 } 207 208 void ContextOrchestrator::WaitForRunningItems() 209 { 210 std::lock_guard<std::mutex> lock{ m_queueLock }; 211 for (const auto& queue : m_commandQueues) 212 { 213 queue.second->WaitForEmptyQueue(); 214 } 215 } 216 217 bool ContextOrchestrator::WaitForRunningItems(DWORD timeoutMilliseconds) 218 { 219 std::lock_guard<std::mutex> lock{ m_queueLock }; 220 for (const auto& queue : m_commandQueues) 221 { 222 if (!queue.second->WaitForEmptyQueue(timeoutMilliseconds)) 223 { 224 return false; 225 } 226 } 227 228 return true; 229 } 230 231 std::string ContextOrchestrator::GetStatusString() 232 { 233 std::ostringstream stream; 234 235 std::lock_guard<std::mutex> lock{ m_queueLock }; 236 237 if (!m_enabled) 238 { 239 stream << "Disabled due to " << ToIntegral(m_disabledReason) << std::endl; 240 } 241 242 for (const auto& queue : m_commandQueues) 243 { 244 stream << queue.second->GetStatusString(); 245 } 246 247 return stream.str(); 248 } 249 250 _Requires_lock_held_(m_itemLock) 251 std::deque<std::shared_ptr<OrchestratorQueueItem>>::iterator OrchestratorQueue::FindIteratorById(const OrchestratorQueueItemId& comparisonQueueItemId) 252 { 253 return std::find_if(m_queueItems.begin(), m_queueItems.end(), [&comparisonQueueItemId](const std::shared_ptr<OrchestratorQueueItem>& item) {return (item->GetId().IsSame(comparisonQueueItemId)); }); 254 } 255 256 _Requires_lock_held_(m_itemLock) 257 std::shared_ptr<OrchestratorQueueItem> OrchestratorQueue::FindById(const OrchestratorQueueItemId& comparisonQueueItemId) 258 { 259 auto itr = FindIteratorById(comparisonQueueItemId); 260 if (itr != m_queueItems.end()) 261 { 262 return *itr; 263 } 264 265 return {}; 266 } 267 268 void OrchestratorQueue::EnqueueItem(const std::shared_ptr<OrchestratorQueueItem>& item) 269 { 270 { 271 std::lock_guard<std::mutex> lockQueue{ m_itemLock }; 272 m_queueItems.push_back(item); 273 m_queueEmpty.ResetEvent(); 274 } 275 276 // Add the package to the Installing source so that it can be queried using the Source interface. 277 // Only do this the first time the item is queued. 278 if (item->IsOnFirstCommand()) 279 { 280 try 281 { 282 m_orchestrator.AddItemManifestToInstallingSource(*item); 283 } 284 catch (...) 285 { 286 std::lock_guard<std::mutex> lockQueue{ m_itemLock }; 287 auto itr = FindIteratorById(item->GetId()); 288 if (itr != m_queueItems.end()) 289 { 290 m_queueItems.erase(itr); 291 292 if (m_queueItems.empty()) 293 { 294 m_queueEmpty.SetEvent(); 295 } 296 } 297 throw; 298 } 299 } 300 301 { 302 std::lock_guard<std::mutex> lockQueue{ m_itemLock }; 303 item->SetState(OrchestratorQueueItemState::Queued); 304 } 305 } 306 307 OrchestratorQueue::OrchestratorQueue(ContextOrchestrator& orchestrator, std::string_view commandName, UINT32 allowedThreads) : 308 m_orchestrator(orchestrator), m_commandName(commandName), m_allowedThreads(allowedThreads) 309 { 310 m_threadPool.reset(CreateThreadpool(nullptr)); 311 THROW_LAST_ERROR_IF_NULL(m_threadPool); 312 m_threadPoolCleanupGroup.reset(CreateThreadpoolCleanupGroup()); 313 THROW_LAST_ERROR_IF_NULL(m_threadPoolCleanupGroup); 314 InitializeThreadpoolEnvironment(&m_threadPoolCallbackEnviron); 315 SetThreadpoolCallbackPool(&m_threadPoolCallbackEnviron, m_threadPool.get()); 316 SetThreadpoolCallbackCleanupGroup(&m_threadPoolCallbackEnviron, m_threadPoolCleanupGroup.get(), nullptr); 317 318 SetThreadpoolThreadMaximum(m_threadPool.get(), m_allowedThreads); 319 THROW_LAST_ERROR_IF(!SetThreadpoolThreadMinimum(m_threadPool.get(), 1)); 320 } 321 322 OrchestratorQueue::~OrchestratorQueue() 323 { 324 CloseThreadpoolCleanupGroupMembers(m_threadPoolCleanupGroup.get(), false, nullptr); 325 } 326 327 void OrchestratorQueue::EnqueueAndRunItem(const std::shared_ptr<OrchestratorQueueItem>& item) 328 { 329 EnqueueItem(item); 330 331 item->SetCurrentQueue(this); 332 auto work = CreateThreadpoolWork(OrchestratorQueueWorkCallback, item.get(), &m_threadPoolCallbackEnviron); 333 SubmitThreadpoolWork(work); 334 } 335 336 void OrchestratorQueue::RunItem(const OrchestratorQueueItemId& itemId) 337 { 338 try 339 { 340 std::shared_ptr<OrchestratorQueueItem> item; 341 bool isCancelled = false; 342 343 // Try to find the item in the queue. 344 { 345 std::lock_guard<std::mutex> lockQueue{ m_itemLock }; 346 item = FindById(itemId); 347 348 if (!item) 349 { 350 // Item should be in the queue; this shouldn't happen. 351 return; 352 } 353 354 // Only run if the item is queued and not cancelled. 355 if (item->GetState() == OrchestratorQueueItemState::Queued) 356 { 357 // Mark it as running so that it cannot be cancelled by other threads. 358 item->SetState(OrchestratorQueueItemState::Running); 359 } 360 else if (item->GetState() == OrchestratorQueueItemState::Cancelled) 361 { 362 isCancelled = true; 363 } 364 } 365 366 if (isCancelled) 367 { 368 // Do this separate from above block as the Remove function needs to manage the lock. 369 RemoveItemInState(*item, OrchestratorQueueItemState::Cancelled, true); 370 } 371 372 // Get the item's command and execute it. 373 HRESULT exceptionHR = S_OK; 374 try 375 { 376 std::unique_ptr<Command> command = item->PopNextCommand(); 377 378 std::unique_ptr<AppInstaller::ThreadLocalStorage::PreviousThreadGlobals> setThreadGlobalsToPreviousState = item->GetContext().SetForCurrentThread(); 379 380 command->ValidateArguments(item->GetContext().Args); 381 382 item->GetContext().EnableSignalTerminationHandler(); 383 384 ::AppInstaller::CLI::ExecuteWithoutLoggingSuccess(item->GetContext(), command.get()); 385 } 386 WINGET_CATCH_STORE(exceptionHR, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); 387 388 if (FAILED(exceptionHR)) 389 { 390 // Set the termination hr directly from any exception that escaped so that the context always 391 // has the result of the operation no matter how it failed. 392 item->GetContext().SetTerminationHR(exceptionHR); 393 } 394 395 item->GetContext().EnableSignalTerminationHandler(false); 396 397 if (FAILED(item->GetContext().GetTerminationHR()) || item->IsComplete()) 398 { 399 if (SUCCEEDED(item->GetContext().GetTerminationHR())) 400 { 401 item->GetContext().GetThreadGlobals().GetTelemetryLogger().LogCommandSuccess(item->GetItemCommandName()); 402 } 403 404 RemoveItemInState(*item, OrchestratorQueueItemState::Running, true); 405 } 406 else 407 { 408 // Remove item from this queue and add it to the queue for the next command. 409 RemoveItemInState(*item, OrchestratorQueueItemState::Running, false); 410 m_orchestrator.EnqueueAndRunItem(item); 411 } 412 } 413 catch (...) 414 { 415 } 416 } 417 418 void OrchestratorQueue::CancelAllItems(CancelReason reason) 419 { 420 std::lock_guard<std::mutex> lockQueue{ m_itemLock }; 421 422 for (auto itr = m_queueItems.begin(); itr != m_queueItems.end(); itr++) 423 { 424 auto& item = *itr; 425 426 item->GetContext().Cancel(reason, true); 427 428 // This mimics ContextOrchestrator::CancelQueueItem, which speeds up the process of cancelling queued items 429 if (item->GetState() == OrchestratorQueueItemState::Queued) 430 { 431 item->SetState(OrchestratorQueueItemState::Cancelled); 432 item->HandleItemCompletion(m_orchestrator); 433 } 434 } 435 } 436 437 void OrchestratorQueue::WaitForEmptyQueue() 438 { 439 m_queueEmpty.wait(); 440 } 441 442 bool OrchestratorQueue::WaitForEmptyQueue(DWORD timeoutMilliseconds) 443 { 444 return m_queueEmpty.wait(timeoutMilliseconds); 445 } 446 447 std::string OrchestratorQueue::GetStatusString() 448 { 449 std::ostringstream stream; 450 stream << m_commandName << '[' << m_allowedThreads << "]\n"; 451 452 std::map<OrchestratorQueueItemState, size_t> stateCounts; 453 stateCounts[OrchestratorQueueItemState::NotQueued] = 0; 454 stateCounts[OrchestratorQueueItemState::Queued] = 0; 455 stateCounts[OrchestratorQueueItemState::Running] = 0; 456 stateCounts[OrchestratorQueueItemState::Cancelled] = 0; 457 458 { 459 std::lock_guard<std::mutex> lock{ m_itemLock }; 460 461 for (const auto& item : m_queueItems) 462 { 463 stateCounts[item->GetState()] += 1; 464 } 465 } 466 467 for (const auto& stateCount : stateCounts) 468 { 469 stream << " " << ToString(stateCount.first) << " : " << stateCount.second << std::endl; 470 } 471 472 return stream.str(); 473 } 474 475 bool OrchestratorQueue::RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state, bool isGlobalRemove) 476 { 477 // OrchestratorQueueItemState::Running items should only be removed by the thread that ran the item. 478 // Queued items can be removed by any thread. 479 // NotQueued items should not be removed since, if found in the queue, they are in the process of being queued by another thread. 480 bool foundItem = false; 481 482 { 483 std::lock_guard<std::mutex> lockQueue{ m_itemLock }; 484 485 // Look for the item. It's ok if the item is not found since multiple listeners may try to remove the same item. 486 auto itr = FindIteratorById(item.GetId()); 487 if (itr != m_queueItems.end() && (*itr)->GetState() == state) 488 { 489 foundItem = true; 490 491 // The item must only be removed from the queue by the thread that runs 492 // it, because the callback uses it. If any other thread tries to remove 493 // it, we simply mark it as cancelled. 494 if (state == OrchestratorQueueItemState::Running || state == OrchestratorQueueItemState::Cancelled) 495 { 496 (*itr)->SetCurrentQueue(nullptr); 497 m_queueItems.erase(itr); 498 499 if (m_queueItems.empty()) 500 { 501 m_queueEmpty.SetEvent(); 502 } 503 } 504 else if (state == OrchestratorQueueItemState::Queued) 505 { 506 (*itr)->SetState(OrchestratorQueueItemState::Cancelled); 507 } 508 } 509 } 510 511 if (foundItem && isGlobalRemove) 512 { 513 item.HandleItemCompletion(m_orchestrator); 514 } 515 516 return foundItem; 517 } 518 519 bool OrchestratorQueueItemId::IsSame(const OrchestratorQueueItemId& comparedId) const 520 { 521 return ((GetPackageId() == comparedId.GetPackageId()) && 522 (GetSourceId() == comparedId.GetSourceId())); 523 } 524 525 std::string_view OrchestratorQueueItem::GetItemCommandName() const 526 { 527 // The goal is that these should match the winget.exe commands for easy correlation. 528 switch (m_operationType) 529 { 530 case PackageOperationType::Search: return "root:search"sv; 531 case PackageOperationType::Install: return "root:install"sv; 532 case PackageOperationType::Upgrade: return "root:upgrade"sv; 533 case PackageOperationType::Uninstall: return "root:uninstall"sv; 534 case PackageOperationType::Download: return "root:download"sv; 535 case PackageOperationType::Repair: return "root:repair"sv; 536 default: return "unknown"; 537 } 538 } 539 540 void OrchestratorQueueItem::HandleItemCompletion(ContextOrchestrator& orchestrator) const 541 { 542 orchestrator.RemoveItemManifestFromInstallingSource(*this); 543 GetCompletedEvent().SetEvent(); 544 } 545 546 std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForInstall(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context, bool isUpgrade) 547 { 548 std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), isUpgrade ? PackageOperationType::Upgrade : PackageOperationType::Install); 549 item->AddCommand(std::make_unique<::AppInstaller::CLI::COMDownloadCommand>(RootCommand::CommandName)); 550 item->AddCommand(std::make_unique<::AppInstaller::CLI::COMInstallCommand>(RootCommand::CommandName)); 551 return item; 552 } 553 554 std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForUninstall(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context) 555 { 556 std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Uninstall); 557 item->AddCommand(std::make_unique<::AppInstaller::CLI::COMUninstallCommand>(RootCommand::CommandName)); 558 return item; 559 } 560 561 std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForSearch(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context) 562 { 563 std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Search); 564 return item; 565 } 566 567 std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForDownload(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context) 568 { 569 std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Download); 570 item->AddCommand(std::make_unique<::AppInstaller::CLI::COMDownloadCommand>(RootCommand::CommandName)); 571 return item; 572 } 573 574 std::unique_ptr<OrchestratorQueueItem> OrchestratorQueueItemFactory::CreateItemForRepair(std::wstring packageId, std::wstring sourceId, std::unique_ptr<COMContext> context) 575 { 576 std::unique_ptr<OrchestratorQueueItem> item = std::make_unique<OrchestratorQueueItem>(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context), PackageOperationType::Repair); 577 item->AddCommand(std::make_unique<::AppInstaller::CLI::COMRepairCommand>(RootCommand::CommandName)); 578 return item; 579 } 580 581 std::string_view ToString(OrchestratorQueueItemState state) 582 { 583 switch (state) 584 { 585 case OrchestratorQueueItemState::NotQueued: return "NotQueued"; 586 case OrchestratorQueueItemState::Queued: return "Queued"; 587 case OrchestratorQueueItemState::Running: return "Running"; 588 case OrchestratorQueueItemState::Cancelled: return "Cancelled"; 589 default: return "Unknown"; 590 } 591 } 592 }