winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 2536bde0f6e6f502fd0193e0580787ccd4960091
parent cab941a8c458ca8b8685bd359c504f3e15ef1d06
Author: sreadingMSFT <74242768+sreadingMSFT@users.noreply.github.com>
Date:   Tue, 24 Aug 2021 02:01:02 -0700

Fix GetInstallProgress to return null directly when an install is not in progress for that package (#1385)

* Return null asyncoperation from GetInstallProgress

* Fix header.

* Remove double lock from contextorchestrator.

* Fix spelling expectations
Diffstat:
M.github/actions/spelling/expect.txt | 1+
Msrc/AppInstallerCLICore/ContextOrchestrator.cpp | 42++++++++++++++++++++++++++++++------------
Msrc/AppInstallerCLICore/ContextOrchestrator.h | 3++-
Msrc/Microsoft.Management.Deployment/Helpers.cpp | 14+++++++-------
Msrc/Microsoft.Management.Deployment/Helpers.h | 2+-
Msrc/Microsoft.Management.Deployment/PackageManager.cpp | 163++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
6 files changed, 152 insertions(+), 73 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -70,6 +70,7 @@ contosainstaller contoso contractversion count'th +countof countryregion createmanifestmetadata cstdint diff --git a/src/AppInstallerCLICore/ContextOrchestrator.cpp b/src/AppInstallerCLICore/ContextOrchestrator.cpp @@ -42,14 +42,21 @@ namespace AppInstaller::CLI::Execution void ContextOrchestrator::EnqueueItem(std::shared_ptr<OrchestratorQueueItem> item) { - std::lock_guard<std::mutex> lock{ m_queueLock }; + { + std::lock_guard<std::mutex> lockQueue{ m_queueLock }; - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INSTALL_ALREADY_RUNNING), FindById(item->GetId())); - m_queueItems.push_back(item); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INSTALL_ALREADY_RUNNING), FindById(item->GetId())); + m_queueItems.push_back(item); + } // Add the package to the Installing source so that it can be queried using the ISource interface. const auto& manifest = item->GetContext().Get<Execution::Data::Manifest>(); m_installingWriteableSource->AddPackageVersion(manifest, std::filesystem::path{ manifest.Id + '.' + manifest.Version }); + + { + std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + item->SetState(OrchestratorQueueItemState::Queued); + } } void ContextOrchestrator::EnqueueAndRunItem(std::shared_ptr<OrchestratorQueueItem> item) @@ -62,7 +69,7 @@ namespace AppInstaller::CLI::Execution std::shared_ptr<OrchestratorQueueItem> ContextOrchestrator::GetNextItem() { - std::lock_guard<std::mutex> lock{ m_queueLock }; + std::lock_guard<std::mutex> lockQueue{ m_queueLock }; if (m_queueItems.empty()) { @@ -73,10 +80,10 @@ namespace AppInstaller::CLI::Execution // Check if item can be dequeued. // Since only one item can be installed at a time currently the logic is very simple, - // and can just check if the first item is already running. This logic will need to become + // and can just check if the first item is ready to run. This logic will need to become // more complicated if multiple operation types (e.g. Download & Install) are added that can // run simultaneously. - if (item->GetState() == OrchestratorQueueItemState::Running) + if (item->GetState() != OrchestratorQueueItemState::Queued) { return {}; } @@ -121,21 +128,32 @@ namespace AppInstaller::CLI::Execution void ContextOrchestrator::RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state) { - std::lock_guard<std::mutex> lock{ m_queueLock }; + // OrchestratorQueueItemState::Running items should only be removed by the thread that ran the item. + // Queued items can be removed by any thread. + // NotQueued items should not be removed since, if found in the queue, they are in the process of being queued by another thread. + bool foundItem = false; - // 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 = std::find(m_queueItems.begin(), m_queueItems.end(), item); - auto itr = FindIteratorById(item.GetId()); - if (itr != m_queueItems.end() && (*itr)->GetState() == state) { - m_queueItems.erase(itr); + std::lock_guard<std::mutex> lockQueue{ m_queueLock }; + // 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()); + if (itr != m_queueItems.end() && (*itr)->GetState() == state) + { + foundItem = true; + m_queueItems.erase(itr); + } + } + + if (foundItem) + { const auto& manifest = item.GetContext().Get<Execution::Data::Manifest>(); m_installingWriteableSource->RemovePackageVersion(manifest, std::filesystem::path{ manifest.Id + '.' + manifest.Version }); item.GetCompletedEvent().SetEvent(); } } + void ContextOrchestrator::CancelQueueItem(const OrchestratorQueueItem& item) { // Always cancel the item, even if it isn't running yet, to get the terminationHR set correctly. diff --git a/src/AppInstallerCLICore/ContextOrchestrator.h b/src/AppInstallerCLICore/ContextOrchestrator.h @@ -14,6 +14,7 @@ namespace AppInstaller::CLI::Execution { enum class OrchestratorQueueItemState { + NotQueued, Queued, Running }; @@ -40,7 +41,7 @@ namespace AppInstaller::CLI::Execution const wil::unique_event& GetCompletedEvent() const { return m_completedEvent; } const OrchestratorQueueItemId& GetId() const { return m_id; } private: - OrchestratorQueueItemState m_state = OrchestratorQueueItemState::Queued; + OrchestratorQueueItemState m_state = OrchestratorQueueItemState::NotQueued; std::unique_ptr<COMContext> m_context; wil::unique_event m_completedEvent{ wil::EventOptions::ManualReset }; OrchestratorQueueItemId m_id; diff --git a/src/Microsoft.Management.Deployment/Helpers.cpp b/src/Microsoft.Management.Deployment/Helpers.cpp @@ -13,7 +13,7 @@ using namespace std::string_view_literals; namespace winrt::Microsoft::Management::Deployment::implementation { - std::optional<DWORD> GetCallerProcessId() + std::pair<HRESULT, DWORD> GetCallerProcessId() { RPC_STATUS rpcStatus = RPC_S_OK; RPC_CALL_ATTRIBUTES callAttributes = {}; @@ -25,9 +25,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation !((rpcStatus == RPC_S_OK) && HandleToULong(callAttributes.ClientPID) == GetCurrentProcessId())) { DWORD callerProcessId = HandleToULong(callAttributes.ClientPID); - return callerProcessId; + return { S_OK, callerProcessId }; } - return {}; + return { E_ACCESSDENIED, 0 }; } std::wstring_view GetStringForCapability(Capability capability) @@ -57,14 +57,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation HRESULT EnsureComCallerHasCapability(Capability requiredCapability) { - auto callerProcessId = GetCallerProcessId(); - RETURN_HR_IF(E_ACCESSDENIED, !callerProcessId.has_value()); - HRESULT hr = EnsureProcessHasCapability(requiredCapability, callerProcessId.value()); + auto [hr, callerProcessId] = GetCallerProcessId(); + RETURN_IF_FAILED(hr); + hr = EnsureProcessHasCapability(requiredCapability, callerProcessId); // The Windows.Management.Deployment API has set the precedent that packageManagement is a superset of packageQuery // and packageQuery does not need to be declared separately. if (FAILED(hr) && requiredCapability == Capability::PackageQuery) { - return EnsureProcessHasCapability(Capability::PackageManagement, callerProcessId.value()); + return EnsureProcessHasCapability(Capability::PackageManagement, callerProcessId); } return hr; } diff --git a/src/Microsoft.Management.Deployment/Helpers.h b/src/Microsoft.Management.Deployment/Helpers.h @@ -10,6 +10,6 @@ namespace winrt::Microsoft::Management::Deployment::implementation HRESULT EnsureProcessHasCapability(Capability requiredCapability, DWORD callerProcessId); HRESULT EnsureComCallerHasCapability(Capability requiredCapability); - std::optional<DWORD> GetCallerProcessId(); + std::pair<HRESULT, DWORD> GetCallerProcessId(); std::wstring TryGetCallerProcessInfo(DWORD callerProcessId); } \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -233,7 +233,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation return {}; } } - + Microsoft::Management::Deployment::PackageVersionInfo GetPackageVersionInfo(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::InstallOptions options) { Microsoft::Management::Deployment::PackageVersionInfo packageVersionInfo{ nullptr }; @@ -255,8 +255,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation } std::unique_ptr<::AppInstaller::COMContext> CreateContextFromInstallOptions( - winrt::Microsoft::Management::Deployment::CatalogPackage package, - winrt::Microsoft::Management::Deployment::InstallOptions options, + winrt::Microsoft::Management::Deployment::CatalogPackage package, + winrt::Microsoft::Management::Deployment::InstallOptions options, std::wstring callerProcessInfoString) { std::unique_ptr<::AppInstaller::COMContext> context = std::make_unique<::AppInstaller::COMContext>(); @@ -366,45 +366,29 @@ namespace winrt::Microsoft::Management::Deployment::implementation } return nullptr; } - winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::InstallResult, winrt::Microsoft::Management::Deployment::InstallProgress> GetInstallOperation( - bool addToQueue, - winrt::Microsoft::Management::Deployment::CatalogPackage package, - winrt::Microsoft::Management::Deployment::InstallOptions options, - winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo) + bool canCancelQueueItem, + std::shared_ptr<Execution::OrchestratorQueueItem> queueItemParam, + winrt::Microsoft::Management::Deployment::CatalogPackage package = nullptr, + winrt::Microsoft::Management::Deployment::InstallOptions options = nullptr, + std::wstring callerProcessInfoString = {}) { winrt::hresult terminationHR = S_OK; hstring correlationData = (options) ? options.CorrelationData() : L""; ::Workflow::ExecutionStage executionStage = ::Workflow::ExecutionStage::Initial; - #define WINGET_RETURN_INSTALL_RESULT_IF(installResult, boolVal) { if(boolVal) { co_return installResult; }} - #define WINGET_RETURN_INSTALL_RESULT_HR(hr) { WINGET_RETURN_INSTALL_RESULT_IF(GetInstallResult(executionStage, hr, correlationData, false), true) } - #define WINGET_RETURN_INSTALL_RESULT_HR_IF(hr, boolVal) { if(boolVal) { WINGET_RETURN_INSTALL_RESULT_HR(hr) }} - #define WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(hr) { WINGET_RETURN_INSTALL_RESULT_HR_IF(hr, FAILED(hr)) } - - // options and catalog can both be null, package must be set. - WINGET_RETURN_INSTALL_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package); - try { + // re-scope the parameter to inside the try block to avoid lifetime management issues. + std::shared_ptr<Execution::OrchestratorQueueItem> queueItem = std::move(queueItemParam); + auto report_progress{ co_await winrt::get_progress_token() }; auto cancellationToken{ co_await winrt::get_cancellation_token() }; + // co_await does not guarantee that it's on a background thread, so do so explicitly. + co_await winrt::resume_background(); - wil::unique_event progressEvent{ wil::EventOptions::None }; - - std::shared_ptr<Execution::OrchestratorQueueItem> queueItem = nullptr; - if (addToQueue) + if (queueItem == nullptr) { - // Check for permissions and get caller info for telemetry. - // This must be done before any co_awaits since it requires info from the rpc caller thread. - std::optional<DWORD> callerProcessId = GetCallerProcessId(); - WINGET_RETURN_INSTALL_RESULT_HR_IF(E_ACCESSDENIED, !callerProcessId.has_value()); - WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(EnsureProcessHasCapability(Capability::PackageManagement, callerProcessId.value())); - std::wstring callerProcessInfoString = TryGetCallerProcessInfo(callerProcessId.value()); - - // co_await does not guarantee that it's on a background thread, so do so explicitly. - co_await winrt::resume_background(); - Microsoft::Management::Deployment::PackageVersionInfo packageVersionInfo = GetPackageVersionInfo(package, options); std::unique_ptr<::AppInstaller::COMContext> comContext = CreateContextFromInstallOptions(package, options, callerProcessInfoString); queueItem = Execution::OrchestratorQueueItemFactory::CreateItemForInstall(std::wstring{ package.Id() }, std::wstring{ packageVersionInfo.PackageCatalog().Info().Id() }, std::move(comContext)); @@ -413,20 +397,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation InstallProgress queuedProgress{ PackageInstallProgressState::Queued, 0, 0, 0 }; report_progress(queuedProgress); } - else { - WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); - - queueItem = GetExistingQueueItemForPackage(package, catalogInfo); - WINGET_RETURN_INSTALL_RESULT_IF(nullptr, queueItem == nullptr); - // correlation data is not passed in when retrieving an existing queue item, so get it from the existing context. correlationData = hstring(queueItem->GetContext().GetCorrelationJson()); - - // co_await does not guarantee that it's on a background thread, so do so explicitly. - co_await winrt::resume_background(); } + wil::unique_event progressEvent{ wil::EventOptions::None }; + wil::unique_event cancelWaitEvent{ wil::EventOptions::None }; + std::atomic<winrt::Microsoft::Management::Deployment::InstallProgress> installProgress; queueItem->GetContext().AddProgressCallbackFunction([&installProgress, &progressEvent]( ::AppInstaller::ReportType reportType, @@ -439,27 +417,41 @@ namespace winrt::Microsoft::Management::Deployment::implementation if (installProgressOptional.has_value()) { installProgress = installProgressOptional.value(); - ::SetEvent(progressEvent.get()); + progressEvent.SetEvent(); } return; } ); - cancellationToken.callback([&queueItem] + + std::weak_ptr<Execution::OrchestratorQueueItem> weakQueueItem(queueItem); + cancellationToken.callback([weakQueueItem, &canCancelQueueItem, &cancelWaitEvent] { - // The cancellation of the AsyncOperation on the client triggers Cancel which causes the Execute to end. - Execution::ContextOrchestrator::Instance().CancelQueueItem(*queueItem); + if (canCancelQueueItem) + { + auto strongQueueItem = weakQueueItem.lock(); + if (strongQueueItem) { + // The cancellation of the AsyncOperation on the client triggers Cancel which causes the Execute to end. + Execution::ContextOrchestrator::Instance().CancelQueueItem(*strongQueueItem); + } + } + else + { + cancelWaitEvent.SetEvent(); + } }); // Wait for completion or progress events. // Waiting for both on the same thread ensures that progress is never reported after the async operation itself has completed. bool completionEventFired = false; - HANDLE operationEvents[2]; + bool cancelWaitEventFired = false; + HANDLE operationEvents[3]; operationEvents[0] = progressEvent.get(); operationEvents[1] = queueItem->GetCompletedEvent().get(); - while (!completionEventFired) + operationEvents[2] = cancelWaitEvent.get(); + while (!completionEventFired && !cancelWaitEventFired) { DWORD dwEvent = WaitForMultipleObjects( - 2 /* number of events */, + _countof(operationEvents) /* number of events */, operationEvents /* event array */, FALSE /* bWaitAll, FALSE to wake on any event */, INFINITE /* wait until operation completion */); @@ -481,30 +473,97 @@ namespace winrt::Microsoft::Management::Deployment::implementation completionEventFired = true; break; + // operationEvents[2] was signaled, operation is cancelled + case WAIT_OBJECT_0 + 2: + cancelWaitEventFired = true; + break; + // Return value is invalid. default: THROW_LAST_ERROR(); } } - // The install command has finished, check for success/failure and how far it got. - terminationHR = queueItem->GetContext().GetTerminationHR(); - executionStage = queueItem->GetContext().GetExecutionStage(); + if (completionEventFired) + { + // The install command has finished, check for success/failure and how far it got. + terminationHR = queueItem->GetContext().GetTerminationHR(); + executionStage = queueItem->GetContext().GetExecutionStage(); + } } WINGET_CATCH_STORE(terminationHR, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); // TODO - RebootRequired not yet populated, msi arguments not returned from Execute. - WINGET_RETURN_INSTALL_RESULT_HR(terminationHR); + co_return GetInstallResult(executionStage, terminationHR, correlationData, false); } + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::InstallResult, winrt::Microsoft::Management::Deployment::InstallProgress> GetEmptyAsynchronousResultForInstallOperation( + HRESULT hr, + hstring correlationData) + { + // If a function uses co_await or co_return (i.e. if it is a co_routine), it cannot use return directly. + // This helper helps a function that is not a coroutine itself to return errors asynchronously. + co_return GetInstallResult(::Workflow::ExecutionStage::Initial, hr, correlationData, false); + } + +#define WINGET_RETURN_INSTALL_RESULT_HR_IF(hr, boolVal) { if(boolVal) { return GetEmptyAsynchronousResultForInstallOperation(hr, correlationData); }} +#define WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(hr) { WINGET_RETURN_INSTALL_RESULT_HR_IF(hr, FAILED(hr)) } + winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::InstallResult, winrt::Microsoft::Management::Deployment::InstallProgress> PackageManager::InstallPackageAsync(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::InstallOptions options) { - return GetInstallOperation(true, package, options, nullptr); + hstring correlationData = (options) ? options.CorrelationData() : L""; + + // options and catalog can both be null, package must be set. + WINGET_RETURN_INSTALL_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package); + + HRESULT hr = S_OK; + std::wstring callerProcessInfoString; + try + { + // Check for permissions and get caller info for telemetry. + // This must be done before any co_awaits since it requires info from the rpc caller thread. + auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); + WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(hrGetCallerId); + WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(EnsureProcessHasCapability(Capability::PackageManagement, callerProcessId)); + callerProcessInfoString = TryGetCallerProcessInfo(callerProcessId); + } + WINGET_CATCH_STORE(hr, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); + WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(hr); + + return GetInstallOperation(true /*canCancelQueueItem*/, nullptr /*queueItem*/, package, options, std::move(callerProcessInfoString)); } winrt::Windows::Foundation::IAsyncOperationWithProgress<winrt::Microsoft::Management::Deployment::InstallResult, winrt::Microsoft::Management::Deployment::InstallProgress> PackageManager::GetInstallProgress(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PackageCatalogInfo catalogInfo) { - return GetInstallOperation(false, package, nullptr, catalogInfo); + hstring correlationData; + WINGET_RETURN_INSTALL_RESULT_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS, !package); + + HRESULT hr = S_OK; + std::shared_ptr<Execution::OrchestratorQueueItem> queueItem = nullptr; + bool canCancelQueueItem = false; + try + { + // Check for permissions + // This must be done before any co_awaits since it requires info from the rpc caller thread. + auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); + WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(hrGetCallerId); + canCancelQueueItem = SUCCEEDED(EnsureProcessHasCapability(Capability::PackageManagement, callerProcessId)); + if (!canCancelQueueItem) + { + WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(EnsureProcessHasCapability(Capability::PackageQuery, callerProcessId)); + } + + // Get the queueItem synchronously. + queueItem = GetExistingQueueItemForPackage(package, catalogInfo); + if (queueItem == nullptr) + { + return nullptr; + } + } + WINGET_CATCH_STORE(hr, APPINSTALLER_CLI_ERROR_COMMAND_FAILED); + WINGET_RETURN_INSTALL_RESULT_HR_IF_FAILED(hr); + + return GetInstallOperation(canCancelQueueItem, std::move(queueItem)); } CoCreatableCppWinRtClass(PackageManager);