From e9b25d4ec0d15f7e33a8ea96f63105906b9b1839 Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Date: Wed, 4 Oct 2023 16:14:42 -0700 Subject: [PATCH] Honor 429 Retry-After (#3718) (#3730) Change This pull request honors 429 Retry-After for pre indexed sources and throws ServiceUnavailableException when the response is 429 or 503. If there's a Retry-After header and its value is more than 60s or the date is more than 60s into the future Save the next time it can be updated in the source details and not try again for that command. Subsequent updates or open that source will be skipped if the time lapsed hasn't passed. If there's a Retry-After header and its value is less than 60s or the date value is less than 60 seconds into the future Retry once after that amount of time. If no Retry-After header Retry after a random amount of time. For open, the failed to update source message will show up. For update, a warning with Unavailable will show up. The next available time will show up in the logs. --- .github/actions/spelling/expect.txt | 2 + .../package-manager/winget/returnCodes.md | 1 + src/AppInstallerCLICore/Resources.h | 1 + .../Workflows/DownloadFlow.cpp | 28 +++- .../Workflows/SourceFlow.cpp | 12 +- .../Shared/Strings/en-us/winget.resw | 3 + src/AppInstallerCLITests/HttpClientHelper.cpp | 2 +- src/AppInstallerCommonCore/Downloader.cpp | 148 ++++++++++++++++-- .../HttpStream/HttpClientWrapper.cpp | 32 +++- .../HttpStream/HttpRandomAccessStream.cpp | 31 ++-- .../HttpStream/HttpRandomAccessStream.h | 6 +- src/AppInstallerCommonCore/MsixInfo.cpp | 1 + src/AppInstallerCommonCore/Progress.cpp | 18 +++ .../Public/AppInstallerDownloader.h | 23 +++ .../Public/AppInstallerProgress.h | 2 + .../PreIndexedPackageSourceFactory.cpp | 43 +++-- .../Microsoft/SQLiteIndexSource.cpp | 22 ++- .../Public/winget/RepositorySource.h | 3 + .../RepositorySource.cpp | 118 +++++++++++--- .../Rest/Schema/HttpClientHelper.cpp | 18 ++- src/AppInstallerRepositoryCore/SourceList.cpp | 34 +++- src/AppInstallerRepositoryCore/SourceList.h | 5 +- src/AppInstallerSharedLib/Errors.cpp | 2 + .../Public/AppInstallerErrors.h | 1 + src/WinGetUtil/pch.h | 2 + 25 files changed, 471 insertions(+), 87 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e02b4b6b9b..f04cb4280c 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -244,6 +244,7 @@ LOWORD LPARAM LPBYTE LPCWSTR +lpdw LPDWORD lpfn LPGRPICONDIR @@ -481,6 +482,7 @@ ucasemap UChars ucnv uec +UNAVAIL uninitialize unins uninstallation diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md index 92f8cd8456..6f01c97940 100644 --- a/doc/windows/package-manager/winget/returnCodes.md +++ b/doc/windows/package-manager/winget/returnCodes.md @@ -120,6 +120,7 @@ ms.localizationpriority: medium | 0x8A15006A | -1978335126 | APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED | Application shutdown signal received | | 0x8A15006B | -1978335125 | APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES | Failed to download package dependencies. | | 0x8A15006C | -1978335124 | APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED | Failed to download package. Download for offline installation is prohibited. | +| 0x8A15006D | -1978335123 | APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE | A required service is busy or unavailable. Try again later. | ## Install errors. diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 39750ce1e5..9523df2126 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -533,6 +533,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(TooManyArgError); WINGET_DEFINE_RESOURCE_STRINGID(TooManyBehaviorsError); WINGET_DEFINE_RESOURCE_STRINGID(UnableToPurgeInstallDirectory); + WINGET_DEFINE_RESOURCE_STRINGID(Unavailable); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp index aa876a2147..86e0395cef 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -307,7 +307,8 @@ namespace AppInstaller::CLI::Workflow std::optional> hash; - const int MaxRetryCount = 2; + constexpr int MaxRetryCount = 2; + constexpr std::chrono::seconds maximumWaitTimeAllowed = 60s; for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) { bool success = false; @@ -323,6 +324,31 @@ namespace AppInstaller::CLI::Workflow success = true; } + catch (const ServiceUnavailableException& sue) + { + if (retryCount < MaxRetryCount - 1) + { + auto waitSecondsForRetry = sue.RetryAfter(); + if (waitSecondsForRetry > maximumWaitTimeAllowed) + { + throw; + } + + bool waitCompleted = context.Reporter.ExecuteWithProgress([&waitSecondsForRetry](IProgressCallback& progress) + { + return ProgressCallback::Wait(progress, waitSecondsForRetry); + }); + + if (!waitCompleted) + { + break; + } + } + else + { + throw; + } + } catch (...) { if (retryCount < MaxRetryCount - 1) diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp index b412b54921..c0a9a6fb6c 100644 --- a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -198,9 +198,17 @@ namespace AppInstaller::CLI::Workflow Repository::Source source{ sd.Name }; context.Reporter.Info() << Resource::String::SourceUpdateOne(Utility::LocIndView{ sd.Name }) << std::endl; auto updateFunction = [&](IProgressCallback& progress)->std::vector { return source.Update(progress); }; - if (!context.Reporter.ExecuteWithProgress(updateFunction).empty()) + auto sourceDetails = context.Reporter.ExecuteWithProgress(updateFunction); + if (!sourceDetails.empty()) { - context.Reporter.Info() << Resource::String::Cancelled << std::endl; + if (std::chrono::system_clock::now() < sourceDetails[0].DoNotUpdateBefore) + { + context.Reporter.Warn() << Resource::String::Unavailable << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::Cancelled << std::endl; + } } else { diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 8cf06488f7..58e6fd4a50 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2080,4 +2080,7 @@ Please specify one of them using the --source option to proceed. Enable Windows Package Manager Configuration + + Unavailable + \ No newline at end of file diff --git a/src/AppInstallerCLITests/HttpClientHelper.cpp b/src/AppInstallerCLITests/HttpClientHelper.cpp index 750070494e..dfa6026931 100644 --- a/src/AppInstallerCLITests/HttpClientHelper.cpp +++ b/src/AppInstallerCLITests/HttpClientHelper.cpp @@ -24,7 +24,7 @@ TEST_CASE("ExtractJsonResponse_UnsupportedMimeType", "[RestSource][RestSearch]") TEST_CASE("ValidateAndExtractResponse_ServiceUnavailable", "[RestSource]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::ServiceUnavailable) }; - REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::ServiceUnavailable)); + REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE); } TEST_CASE("ValidateAndExtractResponse_NotFound", "[RestSource]") diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index dfff527c8a..748cce6de9 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -16,12 +16,80 @@ using namespace AppInstaller::Runtime; using namespace AppInstaller::Settings; using namespace AppInstaller::Filesystem; +using namespace AppInstaller::Utility::HttpStream; using namespace winrt::Windows::Web::Http; using namespace winrt::Windows::Web::Http::Headers; using namespace winrt::Windows::Web::Http::Filters; namespace AppInstaller::Utility { + namespace + { + // Gets the retry after value in terms of a delay in seconds + std::chrono::seconds GetRetryAfter(const HttpDateOrDeltaHeaderValue& retryAfter) + { + if (retryAfter) + { + auto delta = retryAfter.Delta(); + if (delta) + { + return std::chrono::duration_cast(delta.GetTimeSpan()); + } + + auto dateTimeRef = retryAfter.Date(); + if (dateTimeRef) + { + auto dateTime = dateTimeRef.GetDateTime(); + auto now = winrt::clock::now(); + + if (dateTime > now) + { + return std::chrono::duration_cast(dateTime - now); + } + } + } + + return 0s; + } + + std::chrono::seconds GetRetryAfter(const wil::unique_hinternet& urlFile) + { + std::wstring retryAfter = {}; + DWORD length = 0; + if (!HttpQueryInfoW(urlFile.get(), + HTTP_QUERY_RETRY_AFTER, + &retryAfter, + &length, + nullptr)) + { + auto lastError = GetLastError(); + if (lastError == ERROR_INSUFFICIENT_BUFFER) + { + // lpdwBufferLength contains the size, in bytes, of a buffer large enough to receive the requested information + // without the nul char. not the exact buffer size. + auto size = static_cast(length) / sizeof(wchar_t); + retryAfter.resize(size + 1); + if (HttpQueryInfoW(urlFile.get(), + HTTP_QUERY_RETRY_AFTER, + &retryAfter[0], + &length, + nullptr)) + { + // because the buffer can be bigger remove possible null chars + retryAfter.erase(retryAfter.find(L'\0')); + return AppInstaller::Utility::GetRetryAfter(retryAfter); + } + } + else + { + AICLI_LOG(Core, Error, << "Error retrieving Retry-After header: " << GetLastError()); + } + } + + return 0s; + } + } + std::optional> WinINetDownloadToStream( const std::string& url, std::ostream& dest, @@ -57,14 +125,25 @@ namespace AppInstaller::Utility DWORD requestStatus = 0; DWORD cbRequestStatus = sizeof(requestStatus); - THROW_LAST_ERROR_IF_MSG(!HttpQueryInfo(urlFile.get(), + THROW_LAST_ERROR_IF_MSG(!HttpQueryInfoW(urlFile.get(), HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &requestStatus, &cbRequestStatus, nullptr), "Query download request status failed."); - if (requestStatus != HTTP_STATUS_OK) + constexpr DWORD TooManyRequest = 429; + + switch (requestStatus) + { + case HTTP_STATUS_OK: + // All good + break; + case TooManyRequest: + case HTTP_STATUS_SERVICE_UNAVAIL: { + THROW_EXCEPTION(ServiceUnavailableException(GetRetryAfter(urlFile))); + } + default: AICLI_LOG(Core, Error, << "Download request failed. Returned status: " << requestStatus); THROW_HR_MSG(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, requestStatus), "Download request status is not success."); } @@ -75,7 +154,7 @@ namespace AppInstaller::Utility LONGLONG contentLength = 0; DWORD cbContentLength = sizeof(contentLength); - HttpQueryInfo( + HttpQueryInfoW( urlFile.get(), HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER64, &contentLength, @@ -160,9 +239,19 @@ namespace AppInstaller::Utility HttpResponseMessage response = client.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead).get(); - THROW_HR_IF( - MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode()), - response.StatusCode() != HttpStatusCode::Ok); + switch (response.StatusCode()) + { + case HttpStatusCode::Ok: + // All good + break; + case HttpStatusCode::TooManyRequests: + case HttpStatusCode::ServiceUnavailable: + { + THROW_EXCEPTION(ServiceUnavailableException(GetRetryAfter(response.Headers().RetryAfter()))); + } + default: + THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode())); + } std::map result; @@ -409,12 +498,27 @@ namespace AppInstaller::Utility { // Get an IStream from the input uri and try to create package or bundler reader. winrt::Windows::Foundation::Uri uri(Utility::ConvertToUTF16(uriStr)); - auto randomAccessStream = HttpStream::HttpRandomAccessStream::CreateAsync(uri).get(); - ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); - THROW_IF_FAILED(CreateStreamOverRandomAccessStream( - rasAsIUnknown, - IID_PPV_ARGS(inputStream.ReleaseAndGetAddressOf()))); + winrt::com_ptr httpRandomAccessStream = winrt::make_self(); + + try + { + auto randomAccessStream = httpRandomAccessStream->InitializeAsync(uri).get(); + + ::IUnknown* rasAsIUnknown = (::IUnknown*)winrt::get_abi(randomAccessStream); + THROW_IF_FAILED(CreateStreamOverRandomAccessStream( + rasAsIUnknown, + IID_PPV_ARGS(inputStream.ReleaseAndGetAddressOf()))); + } + catch (const winrt::hresult_error& hre) + { + if (hre.code() == APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE) + { + THROW_EXCEPTION(AppInstaller::Utility::ServiceUnavailableException(httpRandomAccessStream->RetryAfter())); + } + + throw; + } } else { @@ -425,4 +529,26 @@ namespace AppInstaller::Utility return inputStream; } + + std::chrono::seconds GetRetryAfter(const std::wstring& retryAfter) + { + try + { + winrt::hstring hstringValue{ retryAfter }; + HttpDateOrDeltaHeaderValue headerValue = nullptr; + HttpDateOrDeltaHeaderValue::TryParse(hstringValue, headerValue); + return GetRetryAfter(headerValue); + } + catch (...) + { + AICLI_LOG(Core, Error, << "Retry-After value not supported: " << Utility::ConvertToUTF8(retryAfter)); + } + + return 0s; + } + + std::chrono::seconds GetRetryAfter(const HttpResponseMessage& response) + { + return GetRetryAfter(response.Headers().RetryAfter()); + } } diff --git a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp index 3f5a536b8e..8ee90117f9 100644 --- a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp +++ b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp @@ -5,6 +5,7 @@ #include "Public/AppInstallerStrings.h" #include "HttpClientWrapper.h" #include "Public/AppInstallerRuntime.h" +#include "Public/AppInstallerDownloader.h" using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Security::Cryptography; @@ -47,9 +48,19 @@ namespace AppInstaller::Utility::HttpStream HttpResponseMessage response = co_await m_httpClient.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead); - THROW_HR_IF( - MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode()), - response.StatusCode() != HttpStatusCode::Ok); + switch (response.StatusCode()) + { + case HttpStatusCode::Ok: + // All good + break; + case HttpStatusCode::TooManyRequests: + case HttpStatusCode::ServiceUnavailable: + { + THROW_EXCEPTION(ServiceUnavailableException(GetRetryAfter(response))); + } + default: + THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode())); + } // Get the length from the response if (response.Content().Headers().HasKey(L"Content-Length")) @@ -106,6 +117,21 @@ namespace AppInstaller::Utility::HttpStream HttpResponseMessage response = co_await m_httpClient.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead); HttpContentHeaderCollection contentHeaders = response.Content().Headers(); + switch (response.StatusCode()) + { + case HttpStatusCode::Ok: + case HttpStatusCode::PartialContent: + // All good + break; + case HttpStatusCode::TooManyRequests: + case HttpStatusCode::ServiceUnavailable: + { + THROW_EXCEPTION(ServiceUnavailableException(GetRetryAfter(response))); + } + default: + THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode())); + } + if (response.StatusCode() != HttpStatusCode::PartialContent && startPosition != 0) { // throw HRESULT used for range-request error diff --git a/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.cpp b/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.cpp index 0b4799aff0..a2ab0a7821 100644 --- a/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.cpp +++ b/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "HttpRandomAccessStream.h" +#include "Public/AppInstallerDownloader.h" using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Storage::Streams; @@ -12,16 +13,23 @@ using namespace winrt::Windows::Storage::Streams; // The HRESULTs will be mapped to UI error code by the appropriate component namespace AppInstaller::Utility::HttpStream { - IAsyncOperation HttpRandomAccessStream::CreateAsync(const Uri& uri) + IAsyncOperation HttpRandomAccessStream::InitializeAsync(const Uri& uri) { - winrt::com_ptr stream = winrt::make_self(); - - stream->m_httpHelper = co_await HttpClientWrapper::CreateAsync(uri); - stream->m_size = stream->m_httpHelper->GetFullFileSize(); - stream->m_httpLocalCache = std::make_unique(); - - co_return stream.as(); - + auto strong_this{ get_strong() }; + + try + { + strong_this->m_httpHelper = co_await HttpClientWrapper::CreateAsync(uri); + strong_this->m_size = strong_this->m_httpHelper->GetFullFileSize(); + strong_this->m_httpLocalCache = std::make_unique(); + } + catch (const ServiceUnavailableException& e) + { + strong_this->m_retryAfter = e.RetryAfter(); + throw; + } + + co_return strong_this.as(); } uint64_t HttpRandomAccessStream::Size() const @@ -86,4 +94,9 @@ namespace AppInstaller::Utility::HttpStream co_return result; } + + std::chrono::seconds HttpRandomAccessStream::RetryAfter() const + { + return m_retryAfter; + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.h b/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.h index 88ae571783..6947be6578 100644 --- a/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.h +++ b/src/AppInstallerCommonCore/HttpStream/HttpRandomAccessStream.h @@ -5,6 +5,8 @@ #include "HttpClientWrapper.h" #include "HttpLocalCache.h" +using namespace std::chrono_literals; + namespace AppInstaller::Utility::HttpStream { // Provides an implementation of a random access stream over HTTP that supports @@ -17,7 +19,7 @@ namespace AppInstaller::Utility::HttpStream winrt::Windows::Storage::Streams::IInputStream> { public: - static winrt::Windows::Foundation::IAsyncOperation CreateAsync( + winrt::Windows::Foundation::IAsyncOperation InitializeAsync( const winrt::Windows::Foundation::Uri& uri); uint64_t Size() const; void Size(uint64_t value); @@ -32,11 +34,13 @@ namespace AppInstaller::Utility::HttpStream winrt::Windows::Storage::Streams::IBuffer buffer, uint32_t count, winrt::Windows::Storage::Streams::InputStreamOptions options); + std::chrono::seconds RetryAfter() const; private: std::shared_ptr m_httpHelper; std::unique_ptr m_httpLocalCache; unsigned long long m_size = 0; unsigned long long m_requestedPosition = 0; + std::chrono::seconds m_retryAfter = 0s; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/MsixInfo.cpp b/src/AppInstallerCommonCore/MsixInfo.cpp index 6256bb8eea..c27d606136 100644 --- a/src/AppInstallerCommonCore/MsixInfo.cpp +++ b/src/AppInstallerCommonCore/MsixInfo.cpp @@ -531,6 +531,7 @@ namespace AppInstaller::Msix MsixInfo::MsixInfo(std::string_view uriStr) { m_stream = Utility::GetReadOnlyStreamFromURI(uriStr); + if (GetBundleReader(m_stream.Get(), &m_bundleReader)) { m_isBundle = true; diff --git a/src/AppInstallerCommonCore/Progress.cpp b/src/AppInstallerCommonCore/Progress.cpp index 55caaf7542..4288256507 100644 --- a/src/AppInstallerCommonCore/Progress.cpp +++ b/src/AppInstallerCommonCore/Progress.cpp @@ -64,6 +64,24 @@ namespace AppInstaller } } + bool ProgressCallback::Wait(IProgressCallback& progress, std::chrono::milliseconds millisecondsToWait) + { + wil::unique_event calledEvent; + calledEvent.create(); + + auto cancellationFunc = progress.SetCancellationFunction([&calledEvent]() + { + calledEvent.SetEvent(); + }); + + if (calledEvent.wait(static_cast(millisecondsToWait.count()))) + { + return false; + } + + return true; + } + void ProgressCallback::Cancel(CancelReason reason) { m_cancelReason = reason; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h index 8434c6e981..aa202cbb7c 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include #include +#include + #include #include +#include #include #include #include @@ -14,6 +18,8 @@ #include #include +using namespace std::chrono_literals; + namespace AppInstaller::Utility { // The type of data being downloaded; determines what code should @@ -34,6 +40,17 @@ namespace AppInstaller::Utility std::string ContentId; }; + // An exception that indicates that a remote service is too busy/unavailable and may contain data on when to try again. + struct ServiceUnavailableException : public wil::ResultException + { + ServiceUnavailableException(std::chrono::seconds retryAfter = 0s) : wil::ResultException(APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE), m_retryAfter(retryAfter) {} + + std::chrono::seconds RetryAfter() const { return m_retryAfter; } + + private: + std::chrono::seconds m_retryAfter; + }; + // Downloads a file from the given URL and places it in the given location. // url: The url to be downloaded from. http->https redirection is allowed. // dest: The stream to be downloaded to. @@ -83,4 +100,10 @@ namespace AppInstaller::Utility // Function to read-only create a stream from a uri string (url address or file system path) Microsoft::WRL::ComPtr GetReadOnlyStreamFromURI(std::string_view uriStr); + + // Gets the retry after value in terms of a delay in seconds. + std::chrono::seconds GetRetryAfter(const std::wstring& retryAfter); + + // Gets the retry after value in terms of a delay in seconds. + std::chrono::seconds GetRetryAfter(const winrt::Windows::Web::Http::HttpResponseMessage& response); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerProgress.h b/src/AppInstallerCommonCore/Public/AppInstallerProgress.h index 5e8e07b874..9ba643dc34 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerProgress.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerProgress.h @@ -78,6 +78,8 @@ namespace AppInstaller ProgressCallback() = default; ProgressCallback(IProgressSink* sink); + static bool Wait(IProgressCallback& progress, std::chrono::milliseconds ms); + void BeginProgress() override; void OnProgress(uint64_t current, uint64_t maximum, ProgressType type) override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index fc863abd8d..e8a5e63e29 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -112,7 +112,7 @@ namespace AppInstaller::Repository::Microsoft std::string alternateLocation = GetAlternatePackageLocation(details, s_PreIndexedPackageSourceFactory_PackageFileName); // Try getting the primary location's info - HRESULT primaryHR = S_OK; + std::exception_ptr primaryException; try { @@ -125,7 +125,8 @@ namespace AppInstaller::Repository::Microsoft { throw; } - primaryHR = LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageUpdateCheck failed on primary location"); + LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageUpdateCheck failed on primary location"); + primaryException = std::current_exception(); } // Try alternate location @@ -138,7 +139,7 @@ namespace AppInstaller::Repository::Microsoft } CATCH_LOG_MSG("PreIndexedPackageUpdateCheck failed on alternate location"); - THROW_HR(primaryHR); + std::rethrow_exception(primaryException); } const std::string& PackageLocation() const { return m_packageLocation; } @@ -152,31 +153,27 @@ namespace AppInstaller::Repository::Microsoft { if (Utility::IsUrlRemote(packageLocation)) { - try + std::map headers = Utility::GetHeaders(packageLocation); + auto itr = headers.find(std::string{ s_PreIndexedPackageSourceFactory_PackageVersionHeader }); + if (itr != headers.end()) { - std::map headers = Utility::GetHeaders(packageLocation); - auto itr = headers.find(std::string{ s_PreIndexedPackageSourceFactory_PackageVersionHeader }); - if (itr != headers.end()) - { - AICLI_LOG(Repo, Verbose, << "Header indicates version is: " << itr->second); - return { itr->second }; - } + AICLI_LOG(Repo, Verbose, << "Header indicates version is: " << itr->second); + return { itr->second }; + } - // We did not find the header we were looking for, log the ones we did find - AICLI_LOG(Repo, Verbose, << "Did not find " << s_PreIndexedPackageSourceFactory_PackageVersionHeader << " in:\n" << [&]() + // We did not find the header we were looking for, log the ones we did find + AICLI_LOG(Repo, Verbose, << "Did not find " << s_PreIndexedPackageSourceFactory_PackageVersionHeader << " in:\n" << [&]() + { + std::ostringstream headerLog; + for (const auto& header : headers) { - std::ostringstream headerLog; - for (const auto& header : headers) - { - headerLog << " " << header.first << " : " << header.second << '\n'; - } - return std::move(headerLog).str(); - }()); - } - CATCH_LOG(); + headerLog << " " << header.first << " : " << header.second << '\n'; + } + return std::move(headerLog).str(); + }()); } - AICLI_LOG(Repo, Verbose, << "Falling back to reading the package data"); + AICLI_LOG(Repo, Verbose, << "Reading package data to determine version"); Msix::MsixInfo info{ packageLocation }; auto manifest = info.GetAppPackageManifests(); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp index f79a708eb6..b8c6b4dcbe 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -142,7 +142,8 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Downloading manifest"); ProgressCallback emptyCallback; - const int MaxRetryCount = 2; + constexpr int MaxRetryCount = 2; + constexpr std::chrono::seconds maximumWaitTimeAllowed = 10s; for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) { try @@ -157,6 +158,25 @@ namespace AppInstaller::Repository::Microsoft break; } + catch (const ServiceUnavailableException& sue) + { + if (retryCount < MaxRetryCount - 1) + { + auto waitSecondsForRetry = sue.RetryAfter(); + if (waitSecondsForRetry > maximumWaitTimeAllowed) + { + throw; + } + + // TODO: Get real progress callback to allow cancelation. + auto ms = std::chrono::duration_cast(waitSecondsForRetry); + Sleep(static_cast(ms.count())); + } + else + { + throw; + } + } catch (...) { if (retryCount < MaxRetryCount - 1) diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index 4e8d882830..d528f99b51 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -119,6 +119,9 @@ namespace AppInstaller::Repository // The last time that this source was updated. std::chrono::system_clock::time_point LastUpdateTime = {}; + // Stores the earliest time that a background update should be attempted. + std::chrono::system_clock::time_point DoNotUpdateBefore = {}; + // Whether the source supports InstalledSource correlation. bool SupportInstalledSearchCorrelation = true; diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index a99e954181..efdacdb49d 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -34,52 +34,111 @@ namespace AppInstaller::Repository return ISourceFactory::GetForType(details.Type)->Create(details); } + std::chrono::milliseconds GetMillisecondsToWait(std::chrono::seconds retryAfter, size_t randomMultiplier = 1) + { + if (retryAfter != 0s) + { + return std::chrono::duration_cast(retryAfter); + } + else + { + // Add a bit of randomness to the retry wait time + std::default_random_engine randomEngine(std::random_device{}()); + std::uniform_int_distribution distribution(2000, 10000); + + return std::chrono::milliseconds(distribution(randomEngine) * randomMultiplier); + } + } + + bool IsUpdateSuppressed(const SourceDetails& details) + { + return std::chrono::system_clock::now() < details.DoNotUpdateBefore; + } + + struct AddOrUpdateResult + { + bool UpdateChecked = false; + bool MetadataWritten = false; + }; + template - bool AddOrUpdateFromDetails(SourceDetails& details, MemberFunc member, IProgressCallback& progress) + AddOrUpdateResult AddOrUpdateFromDetails(SourceDetails& details, MemberFunc member, IProgressCallback& progress) { - bool result = false; + AddOrUpdateResult result; + auto factory = ISourceFactory::GetForType(details.Type); + // If we are instructed to wait longer than this, just fail rather than retrying. + constexpr std::chrono::seconds maximumWaitTimeAllowed = 60s; + std::chrono::seconds waitSecondsForRetry = 0s; + // Attempt; if it fails, wait a short time and retry. try { - result = (factory.get()->*member)(details, progress); - if (result) + result.UpdateChecked = (factory.get()->*member)(details, progress); + if (result.UpdateChecked) { details.LastUpdateTime = std::chrono::system_clock::now(); + result.MetadataWritten = true; } return result; } + catch (const Utility::ServiceUnavailableException& sue) + { + waitSecondsForRetry = sue.RetryAfter(); + + // Do not retry if the server tell us to wait more than the max time allowed. + if (waitSecondsForRetry > maximumWaitTimeAllowed) + { + details.DoNotUpdateBefore = std::chrono::system_clock::now() + waitSecondsForRetry; + AICLI_LOG(Repo, Info, << "Source `" << details.Name << "` unavailable first try, setting DoNotUpdateBefore to " << details.DoNotUpdateBefore); + result.MetadataWritten = true; + return result; + } + } CATCH_LOG(); - AICLI_LOG(Repo, Info, << "Source add/update failed, waiting a bit and retrying: " << details.Name); + std::chrono::milliseconds millisecondsToWait = GetMillisecondsToWait(waitSecondsForRetry); - // Add a bit of randomness to the retry wait time - std::default_random_engine randomEngine(std::random_device{}()); - std::uniform_int_distribution distribution(2000, 10000); + AICLI_LOG(Repo, Info, << "Source add/update failed, waiting " << millisecondsToWait.count() << " milliseconds and retrying: " << details.Name); - std::this_thread::sleep_for(std::chrono::milliseconds(distribution(randomEngine))); + if (!ProgressCallback::Wait(progress, millisecondsToWait)) + { + AICLI_LOG(Repo, Info, << "Source second try cancelled."); + return {}; + } - // If this one fails, maybe the problem is persistent. - result = (factory.get()->*member)(details, progress); - if (result) + try + { + // If this one fails, maybe the problem is persistent. + result.UpdateChecked = (factory.get()->*member)(details, progress); + if (result.UpdateChecked) + { + details.LastUpdateTime = std::chrono::system_clock::now(); + result.MetadataWritten = true; + } + } + catch (const Utility::ServiceUnavailableException& sue) { - details.LastUpdateTime = std::chrono::system_clock::now(); + details.DoNotUpdateBefore = std::chrono::system_clock::now() + GetMillisecondsToWait(sue.RetryAfter(), 3); + AICLI_LOG(Repo, Info, << "Source `" << details.Name << "` unavailable second try, setting DoNotUpdateBefore to " << details.DoNotUpdateBefore); + result.MetadataWritten = true; } + return result; } - bool AddSourceFromDetails(SourceDetails& details, IProgressCallback& progress) + AddOrUpdateResult AddSourceFromDetails(SourceDetails& details, IProgressCallback& progress) { return AddOrUpdateFromDetails(details, &ISourceFactory::Add, progress); } - bool UpdateSourceFromDetails(SourceDetails& details, IProgressCallback& progress) + AddOrUpdateResult UpdateSourceFromDetails(SourceDetails& details, IProgressCallback& progress) { return AddOrUpdateFromDetails(details, &ISourceFactory::Update, progress); } - bool BackgroundUpdateSourceFromDetails(SourceDetails& details, IProgressCallback& progress) + AddOrUpdateResult BackgroundUpdateSourceFromDetails(SourceDetails& details, IProgressCallback& progress) { return AddOrUpdateFromDetails(details, &ISourceFactory::BackgroundUpdate, progress); } @@ -104,6 +163,13 @@ namespace AppInstaller::Repository return false; } + // Do not update if we are still before the update block time. + if (IsUpdateSuppressed(details)) + { + AICLI_LOG(Repo, Info, << "Background update is suppressed until: " << details.DoNotUpdateBefore); + return false; + } + constexpr static TimeSpan s_ZeroMins = 0min; TimeSpan autoUpdateTime; if (backgroundUpdateInterval.has_value()) @@ -639,7 +705,9 @@ namespace AppInstaller::Repository { // TODO: Consider adding a context callback to indicate we are doing the same action // to avoid the progress bar fill up multiple times. - if (BackgroundUpdateSourceFromDetails(details, progress)) + AddOrUpdateResult updateResult = BackgroundUpdateSourceFromDetails(details, progress); + + if (updateResult.MetadataWritten) { if (sourceList == nullptr) { @@ -647,10 +715,11 @@ namespace AppInstaller::Repository } auto detailsInternal = sourceList->GetSource(details.Name); - detailsInternal->LastUpdateTime = details.LastUpdateTime; + detailsInternal->CopyMetadataFieldsFrom(details); sourceList->SaveMetadata(*detailsInternal); } - else + + if (!updateResult.UpdateChecked) { AICLI_LOG(Repo, Error, << "Failed to update source: " << details.Name); result.emplace_back(details); @@ -747,7 +816,7 @@ namespace AppInstaller::Repository sourceDetails.Origin = SourceOrigin::User; } - bool result = AddSourceFromDetails(sourceDetails, progress); + bool result = AddSourceFromDetails(sourceDetails, progress).UpdateChecked; if (result) { sourceList.AddSource(sourceDetails); @@ -777,13 +846,16 @@ namespace AppInstaller::Repository { // TODO: Consider adding a context callback to indicate we are doing the same action // to avoid the progress bar fill up multiple times. - if (UpdateSourceFromDetails(details, progress)) + AddOrUpdateResult updateResult = UpdateSourceFromDetails(details, progress); + + if (updateResult.MetadataWritten) { auto detailsInternal = sourceList.GetSource(details.Name); - detailsInternal->LastUpdateTime = details.LastUpdateTime; + detailsInternal->CopyMetadataFieldsFrom(details); sourceList.SaveMetadata(*detailsInternal); } - else + + if (!updateResult.UpdateChecked) { AICLI_LOG(Repo, Error, << "Failed to update source: " << details.Name); result.emplace_back(details); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp index c0c0ea0510..2309d0f08c 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp @@ -29,6 +29,17 @@ namespace AppInstaller::Repository::Rest::Schema THROW_HR_IF(APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH, !pinningConfiguration.Validate(certContext.get())); } + + std::chrono::seconds GetRetryAfter(const web::http::http_headers& headers) + { + auto retryAfterHeader = headers.find(web::http::header_names::retry_after); + if (retryAfterHeader != headers.end()) + { + return AppInstaller::Utility::GetRetryAfter(retryAfterHeader->second.c_str()); + } + + return 0s; + } } HttpClientHelper::HttpClientHelper(std::shared_ptr stage) : m_defaultRequestHandlerStage(std::move(stage)) {} @@ -135,7 +146,6 @@ namespace AppInstaller::Repository::Rest::Schema case web::http::status_codes::NotFound: THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); - break; case web::http::status_codes::NoContent: result = {}; @@ -143,11 +153,13 @@ namespace AppInstaller::Repository::Rest::Schema case web::http::status_codes::BadRequest: THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR); - break; + + case web::http::status_codes::TooManyRequests: + case web::http::status_codes::ServiceUnavailable: + THROW_EXCEPTION(AppInstaller::Utility::ServiceUnavailableException(GetRetryAfter(response.headers()))); default: THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.status_code())); - break; } return result; diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp index fa27a558f0..c934e75847 100644 --- a/src/AppInstallerRepositoryCore/SourceList.cpp +++ b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -28,12 +28,12 @@ namespace AppInstaller::Repository constexpr std::string_view s_MetadataYaml_Sources = "Sources"sv; constexpr std::string_view s_MetadataYaml_Source_Name = "Name"sv; constexpr std::string_view s_MetadataYaml_Source_LastUpdate = "LastUpdate"sv; + constexpr std::string_view s_MetadataYaml_Source_DoNotUpdateBefore = "DoNotUpdateBefore"sv; constexpr std::string_view s_MetadataYaml_Source_AcceptedAgreementsIdentifier = "AcceptedAgreementsIdentifier"sv; constexpr std::string_view s_MetadataYaml_Source_AcceptedAgreementFields = "AcceptedAgreementFields"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Name = "winget"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Arg = "https://cdn.winget.microsoft.com/cache"sv; - constexpr std::string_view s_Source_WingetCommunityDefault_AlternateArg = "https://winget-cache-pme-cxfsgwfxarb8hwg0.z01.azurefd.net/cache"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Data = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Identifier = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; @@ -43,7 +43,6 @@ namespace AppInstaller::Repository constexpr std::string_view s_Source_DesktopFrameworks_Name = "microsoft.builtin.desktop.frameworks"sv; constexpr std::string_view s_Source_DesktopFrameworks_Arg = "https://cdn.winget.microsoft.com/platform"sv; - constexpr std::string_view s_Source_DesktopFrameworks_AlternateArg = "https://winget-cache-pme-cxfsgwfxarb8hwg0.z01.azurefd.net/platform"sv; constexpr std::string_view s_Source_DesktopFrameworks_Data = "Microsoft.Winget.Platform.Source_8wekyb3d8bbwe"sv; constexpr std::string_view s_Source_DesktopFrameworks_Identifier = "Microsoft.Winget.Platform.Source_8wekyb3d8bbwe"sv; @@ -205,11 +204,26 @@ namespace AppInstaller::Repository void SourceDetailsInternal::CopyMetadataFieldsTo(SourceDetailsInternal& target) { - target.LastUpdateTime = LastUpdateTime; + if (LastUpdateTime > target.LastUpdateTime) + { + target.LastUpdateTime = LastUpdateTime; + } + + if (DoNotUpdateBefore > target.DoNotUpdateBefore) + { + target.DoNotUpdateBefore = DoNotUpdateBefore; + } + target.AcceptedAgreementFields = AcceptedAgreementFields; target.AcceptedAgreementsIdentifier = AcceptedAgreementsIdentifier; } + void SourceDetailsInternal::CopyMetadataFieldsFrom(const SourceDetails& source) + { + LastUpdateTime = source.LastUpdateTime; + DoNotUpdateBefore = source.DoNotUpdateBefore; + } + std::string_view GetWellKnownSourceName(WellKnownSource source) { switch (source) @@ -286,10 +300,6 @@ namespace AppInstaller::Repository details.Name = s_Source_WingetCommunityDefault_Name; details.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); details.Arg = s_Source_WingetCommunityDefault_Arg; - if (Settings::User().Get()) - { - details.AlternateArg = s_Source_WingetCommunityDefault_AlternateArg; - } details.Data = s_Source_WingetCommunityDefault_Data; details.Identifier = s_Source_WingetCommunityDefault_Identifier; details.TrustLevel = SourceTrustLevel::Trusted | SourceTrustLevel::StoreOrigin; @@ -340,7 +350,6 @@ namespace AppInstaller::Repository details.Name = s_Source_DesktopFrameworks_Name; details.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); details.Arg = s_Source_DesktopFrameworks_Arg; - details.AlternateArg = s_Source_DesktopFrameworks_AlternateArg; details.Data = s_Source_DesktopFrameworks_Data; details.Identifier = s_Source_DesktopFrameworks_Identifier; details.TrustLevel = SourceTrustLevel::Trusted | SourceTrustLevel::StoreOrigin; @@ -710,9 +719,17 @@ namespace AppInstaller::Repository details.Origin = SourceOrigin::Metadata; std::string_view name = m_metadataStream.GetName(); if (!TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_Name, details.Name)) { return false; } + int64_t lastUpdateInEpoch{}; if (!TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_LastUpdate, lastUpdateInEpoch)) { return false; } details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(lastUpdateInEpoch); + + int64_t doNotUpdateBeforeInEpoch{}; + if (TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_DoNotUpdateBefore, doNotUpdateBeforeInEpoch, false)) + { + details.DoNotUpdateBefore = Utility::ConvertUnixEpochToSystemClock(doNotUpdateBeforeInEpoch); + } + TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_AcceptedAgreementsIdentifier, details.AcceptedAgreementsIdentifier, false); TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_AcceptedAgreementFields, details.AcceptedAgreementFields, false); return true; @@ -731,6 +748,7 @@ namespace AppInstaller::Repository out << YAML::BeginMap; out << YAML::Key << s_MetadataYaml_Source_Name << YAML::Value << details.Name; out << YAML::Key << s_MetadataYaml_Source_LastUpdate << YAML::Value << Utility::ConvertSystemClockToUnixEpoch(details.LastUpdateTime); + out << YAML::Key << s_MetadataYaml_Source_DoNotUpdateBefore << YAML::Value << Utility::ConvertSystemClockToUnixEpoch(details.DoNotUpdateBefore); out << YAML::Key << s_MetadataYaml_Source_AcceptedAgreementsIdentifier << YAML::Value << details.AcceptedAgreementsIdentifier; out << YAML::Key << s_MetadataYaml_Source_AcceptedAgreementFields << YAML::Value << details.AcceptedAgreementFields; out << YAML::EndMap; diff --git a/src/AppInstallerRepositoryCore/SourceList.h b/src/AppInstallerRepositoryCore/SourceList.h index f31fefe04b..b8808be3c7 100644 --- a/src/AppInstallerRepositoryCore/SourceList.h +++ b/src/AppInstallerRepositoryCore/SourceList.h @@ -19,9 +19,12 @@ namespace AppInstaller::Repository SourceDetailsInternal() = default; SourceDetailsInternal(const SourceDetails& details) : SourceDetails(details) {} - // Copies the metadata fields from this to target. + // Copies the metadata fields to this target. void CopyMetadataFieldsTo(SourceDetailsInternal& target); + // Copies the metadata fields from this source. This only include partial metadata. + void CopyMetadataFieldsFrom(const SourceDetails& source); + // If true, this is a tombstone, marking the deletion of a source at a lower priority origin. bool IsTombstone = false; diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp index 6ae31a654d..56a5b25d29 100644 --- a/src/AppInstallerSharedLib/Errors.cpp +++ b/src/AppInstallerSharedLib/Errors.cpp @@ -230,6 +230,8 @@ namespace AppInstaller return "Failed to download package dependencies."; case APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED: return "Failed to download package. Download for offline installation is prohibited."; + case APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE: + return "A required service is busy or unavailable. Try again later."; // Install errors case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index 334d19b82c..ddc7360df8 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -121,6 +121,7 @@ #define APPINSTALLER_CLI_ERROR_APPTERMINATION_RECEIVED ((HRESULT)0x8A15006A) #define APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES ((HRESULT)0x8A15006B) #define APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED ((HRESULT)0x8A15006C) +#define APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE ((HRESULT)0x8A15006D) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/WinGetUtil/pch.h b/src/WinGetUtil/pch.h index 0de0f60cd9..99d149eb3a 100644 --- a/src/WinGetUtil/pch.h +++ b/src/WinGetUtil/pch.h @@ -17,3 +17,5 @@ #include #include #include + +#include