// Copyright Epic Games, Inc. All Rights Reserved. #include "Installer/DownloadService.h" #include "HAL/ThreadSafeBool.h" #include "Misc/ScopeLock.h" #include "Core/AsyncHelpers.h" #include "Tasks/Task.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "Common/StatsCollector.h" #include "Installer/InstallerAnalytics.h" #include "Common/HttpManager.h" #include "Common/FileSystem.h" #include "Stats/Stats.h" #include "Containers/Ticker.h" DECLARE_LOG_CATEGORY_EXTERN(LogDownloadService, Warning, All); DEFINE_LOG_CATEGORY(LogDownloadService); namespace BuildPatchServices { // 2MB buffer for reading from disk/network. const int32 FileReaderBufferSize = 2097152; // Empty array representation for no data. const TArray NoData; struct FFileRequest { UE::Tasks::FTask Task; UE::Tasks::FCancellationToken ShouldCancel; }; class FHttpDownload : public IDownload { public: FHttpDownload(FHttpRequestPtr InHttpRequest, bool bInSuccess) : HttpRequest(MoveTemp(InHttpRequest)) , HttpResponse(HttpRequest ? HttpRequest->GetResponse() : nullptr) , bSuccess(bInSuccess) {} // IDownload interface begin. virtual bool RequestSuccessful() const override { return bSuccess; } virtual bool ResponseSuccessful() const override { return EHttpResponseCodes::IsOk(GetResponseCode()); } virtual int32 GetResponseCode() const override { return HttpResponse.IsValid() ? HttpResponse->GetResponseCode() : INDEX_NONE; } virtual const TArray& GetData() const override { return HttpResponse.IsValid() ? HttpResponse->GetContent() : NoData; } // IDownload interface end. private: // Make sure to hold on the the request pointer because the response could reference it FHttpRequestPtr HttpRequest; FHttpResponsePtr HttpResponse; bool bSuccess = false; }; class FFileDownload : public IDownload { public: FFileDownload(TArray InDataArray, bool bInSuccess) : DataArray(MoveTemp(InDataArray)) , bSuccess(bInSuccess) {} // IDownload interface begin. virtual bool RequestSuccessful() const override { return bSuccess; } virtual bool ResponseSuccessful() const override { return EHttpResponseCodes::IsOk(GetResponseCode()); } virtual int32 GetResponseCode() const override { return RequestSuccessful() ? EHttpResponseCodes::Ok : EHttpResponseCodes::NotFound; } virtual const TArray& GetData() const override { return DataArray; } // IDownload interface end. private: TArray DataArray; bool bSuccess = false; }; struct FDownloadDelegates { public: FDownloadDelegates() = default; FDownloadDelegates(const FDownloadCompleteDelegate& InOnCompleteDelegate, const FDownloadProgressDelegate& InOnProgressDelegate); public: FDownloadCompleteDelegate OnCompleteDelegate; FDownloadProgressDelegate OnProgressDelegate; }; FDownloadDelegates::FDownloadDelegates(const FDownloadCompleteDelegate& InOnCompleteDelegate, const FDownloadProgressDelegate& InOnProgressDelegate) : OnCompleteDelegate(InOnCompleteDelegate) , OnProgressDelegate(InOnProgressDelegate) { } /** * Use Pimpl pattern for binding thread safe shared ptr delegates for the http module, without having to enforce that * this service should be made using TShared* reference controllers. */ class FDownloadServiceImpl : public TSharedFromThis { public: FDownloadServiceImpl(IHttpManager* HttpManager, IFileSystem* FileSystem, IDownloadServiceStat* DownloadServiceStat, IInstallerAnalytics* InstallerAnalytics); ~FDownloadServiceImpl(); void CancelAllRequests(); // IDownloadService interface begin. int32 RequestFileWithHeaders(const FString& FileUri, const TMap& RequestHeaders, const FDownloadCompleteDelegate& OnCompleteDelegate, const FDownloadProgressDelegate& OnProgressDelegate); void RequestCancel(int32 RequestId); void RequestAbandon(int32 RequestId); // IDownloadService interface end. private: UE::Tasks::FTask MakeFileLoadTask(int32 RequestId, const FString& FileUri, FFileRequest* FileRequest); IDownloadServiceStat::FDownloadRecord MakeDownloadRecord(int32 RequestId, FString Uri); private: IHttpManager* HttpManager = nullptr; IFileSystem* FileSystem = nullptr; IDownloadServiceStat* DownloadServiceStat = nullptr; IInstallerAnalytics* InstallerAnalytics = nullptr; std::atomic RequestIdCounter; FCriticalSection ActiveRequestsCS; TMap> ActiveHttpRequests; TMap> ActiveFileRequests; FCriticalSection RequestDelegatesCS; TMap RequestDelegates; }; class FDownloadService : public IDownloadService { public: FDownloadService(IHttpManager* HttpManager, IFileSystem* FileSystem, IDownloadServiceStat* DownloadServiceStat, IInstallerAnalytics* InstallerAnalytics) : Impl(MakeShared(HttpManager, FileSystem, DownloadServiceStat, InstallerAnalytics)) {} ~FDownloadService() { Impl->CancelAllRequests(); } // IDownloadService interface begin. virtual int32 RequestFile(const FString& FileUri, const FDownloadCompleteDelegate& OnCompleteDelegate, const FDownloadProgressDelegate& OnProgressDelegate) override { TMap RequestHeaders; return Impl->RequestFileWithHeaders(FileUri, RequestHeaders, OnCompleteDelegate, OnProgressDelegate); } int32 RequestFileWithHeaders(const FString& FileUri, const TMap& RequestHeaders, const FDownloadCompleteDelegate& OnCompleteDelegate, const FDownloadProgressDelegate& OnProgressDelegate) { return Impl->RequestFileWithHeaders(FileUri, RequestHeaders, OnCompleteDelegate, OnProgressDelegate); } virtual void RequestCancel(int32 RequestId) override { Impl->RequestCancel(RequestId); } virtual void RequestAbandon(int32 RequestId) override { Impl->RequestAbandon(RequestId); } // IDownloadService interface end. private: TSharedRef Impl; }; FDownloadServiceImpl::FDownloadServiceImpl(IHttpManager* InHttpManager, IFileSystem* InFileSystem, IDownloadServiceStat* InDownloadServiceStat, IInstallerAnalytics* InInstallerAnalytics) : HttpManager(InHttpManager) , FileSystem(InFileSystem) , DownloadServiceStat(InDownloadServiceStat) , InstallerAnalytics(InInstallerAnalytics) {} FDownloadServiceImpl::~FDownloadServiceImpl() { } void FDownloadServiceImpl::CancelAllRequests() { // Cancel all HTTP requests and wait for all file downloads threads to exit. { FScopeLock ScopeLock(&ActiveRequestsCS); for (const TPair>& ActiveHttpRequest : ActiveHttpRequests) { ActiveHttpRequest.Value->CancelRequest(); } for (const TPair>& ActiveFileRequest : ActiveFileRequests) { ActiveFileRequest.Value->ShouldCancel.Cancel(); } } // callbacks and removal handled by completion delegates } int32 FDownloadServiceImpl::RequestFileWithHeaders(const FString& FileUri, const TMap& RequestHeaders, const FDownloadCompleteDelegate& OnCompleteDelegate, const FDownloadProgressDelegate& OnProgressDelegate) { const int32 RequestId = ++RequestIdCounter; // Save the delegates. { FScopeLock ScopeLock(&RequestDelegatesCS); RequestDelegates.Emplace(RequestId, FDownloadDelegates(OnCompleteDelegate, OnProgressDelegate)); } const bool bIsHttpRequest = FileUri.StartsWith(TEXT("http"), ESearchCase::IgnoreCase); if (bIsHttpRequest) { // Kick off http request. FHttpRequestPtr HttpRequest; { FScopeLock ScopeLock(&ActiveRequestsCS); HttpRequest = ActiveHttpRequests.Emplace(RequestId, HttpManager->CreateRequest()); } HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->SetURL(FileUri); HttpRequest->SetVerb(TEXT("GET")); for (const TPair& Header : RequestHeaders) { HttpRequest->SetHeader(Header.Key, Header.Value); } HttpRequest->OnRequestProgress64().BindSPLambda(this, [this, RequestId](FHttpRequestPtr Request, uint64 BytesSent, uint64 BytesReceived) { #if USING_ADDRESS_SANITISER // Force use the Request parameter to work around some MSVC-specific compiler issue in ASan. UE_LOG(LogDownloadService, Verbose, TEXT("[FDownloadServiceImpl::RequestFile] %s %s received %d bytes"), *Request->GetVerb(), *Request->GetURL(), BytesReceived); #endif { FScopeLock ScopeLock(&RequestDelegatesCS); if (FDownloadDelegates* MaybeDelegates = RequestDelegates.Find(RequestId)) { MaybeDelegates->OnProgressDelegate.ExecuteIfBound(RequestId, BytesReceived); } } DownloadServiceStat->OnDownloadProgress(RequestId, BytesReceived); }); HttpRequest->OnProcessRequestComplete().BindSPLambda(this, [this, DownloadRecord = MakeDownloadRecord(RequestId, FileUri)](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSucceeded) mutable { InstallerAnalytics->TrackRequest(Request); DownloadRecord.bSuccess = bSucceeded; DownloadRecord.ResponseCode = Response.IsValid() ? Response->GetResponseCode() : INDEX_NONE; DownloadRecord.SpeedRecord.CyclesEnd = FStatsCollector::GetCycles(); DownloadRecord.SpeedRecord.Size = Response.IsValid() ? Response->GetContent().Num() : 0; FDownloadDelegates Delegates; bool bFoundDelegates = false; { FScopeLock ScopeLock(&RequestDelegatesCS); bFoundDelegates = RequestDelegates.RemoveAndCopyValue(DownloadRecord.RequestId, Delegates); } if (bFoundDelegates) { TSharedRef CompletedDownload = MakeShared(Request, bSucceeded); Delegates.OnCompleteDelegate.ExecuteIfBound(DownloadRecord.RequestId, CompletedDownload); } { FScopeLock ScopeLock(&ActiveRequestsCS); ActiveHttpRequests.Remove(DownloadRecord.RequestId); } DownloadServiceStat->OnDownloadComplete(DownloadRecord); }); HttpRequest->ProcessRequest(); } else { // Load file from drive/network. TUniquePtr FileRequest = MakeUnique(); FileRequest->Task = MakeFileLoadTask(RequestId, FileUri, FileRequest.Get()); { FScopeLock ScopeLock(&ActiveRequestsCS); ActiveFileRequests.Add(RequestId, MoveTemp(FileRequest)); } } DownloadServiceStat->OnDownloadStarted(RequestId, FileUri); return RequestId; } void FDownloadServiceImpl::RequestCancel(int32 RequestId) { FScopeLock ScopeLock(&ActiveRequestsCS); if (FHttpRequestRef* MaybeRequest = ActiveHttpRequests.Find(RequestId)) { (*MaybeRequest)->CancelRequest(); } if (TUniquePtr* MaybeRequest = ActiveFileRequests.Find(RequestId)) { (*MaybeRequest)->ShouldCancel.Cancel(); } // callbacks and removal handled by completion delegates } void FDownloadServiceImpl::RequestAbandon(int32 RequestId) { { FScopeLock ScopeLock(&RequestDelegatesCS); RequestDelegates.Remove(RequestId); } RequestCancel(RequestId); } UE::Tasks::FTask FDownloadServiceImpl::MakeFileLoadTask(const int32 RequestId, const FString& FileUri, FFileRequest* FileRequest) { return UE::Tasks::Launch(UE_SOURCE_LOCATION, [this, WeakThis = AsWeak(), RequestId, FileUri, FileRequest] { TSharedPtr PinThis = WeakThis.Pin(); if (!PinThis) { return; } TArray FileDataArray; bool bSuccess = !FileRequest->ShouldCancel.IsCanceled(); IDownloadServiceStat::FDownloadRecord DownloadRecord = MakeDownloadRecord(RequestId, FileUri); if (bSuccess) { TUniquePtr Reader = FileSystem->CreateFileReader(*FileUri); bSuccess = Reader.IsValid(); if (bSuccess) { const int64 FileSize = Reader->TotalSize(); FileDataArray.Reset(); FileDataArray.AddUninitialized(FileSize); int64 BytesRead = 0; bool bIsCanceled = false; while (BytesRead < FileSize && !FileRequest->ShouldCancel.IsCanceled()) { bIsCanceled = FileRequest->ShouldCancel.IsCanceled(); if (bIsCanceled) { bSuccess = false; break; } const int64 ReadLen = FMath::Min(FileReaderBufferSize, FileSize - BytesRead); Reader->Serialize(FileDataArray.GetData() + BytesRead, ReadLen); BytesRead += ReadLen; { FScopeLock ScopeLock(&RequestDelegatesCS); if (FDownloadDelegates* MaybeDelegates = RequestDelegates.Find(RequestId)) { MaybeDelegates->OnProgressDelegate.ExecuteIfBound(RequestId, BytesRead); } } DownloadServiceStat->OnDownloadProgress(RequestId, BytesRead); } DownloadRecord.SpeedRecord.Size = BytesRead; bSuccess = bSuccess && Reader->Close() && BytesRead == FileSize; } DownloadRecord.SpeedRecord.CyclesEnd = FStatsCollector::GetCycles(); DownloadRecord.bSuccess = bSuccess; DownloadRecord.ResponseCode = bSuccess ? EHttpResponseCodes::Ok : EHttpResponseCodes::NotFound; } if (!bSuccess) { FileDataArray.Empty(); } FDownloadDelegates Delegates; bool bFoundDelegates = false; { FScopeLock ScopeLock(&RequestDelegatesCS); bFoundDelegates = RequestDelegates.RemoveAndCopyValue(RequestId, Delegates); } if (bFoundDelegates) { TSharedRef CompletedDownload = MakeShared(MoveTemp(FileDataArray), bSuccess); Delegates.OnCompleteDelegate.ExecuteIfBound(RequestId, CompletedDownload); } { FScopeLock ScopeLock(&ActiveRequestsCS); ActiveFileRequests.Remove(RequestId); // FileRequest is dangling at this point } DownloadServiceStat->OnDownloadComplete(DownloadRecord); }, UE::Tasks::ETaskPriority::BackgroundHigh); } IDownloadServiceStat::FDownloadRecord FDownloadServiceImpl::MakeDownloadRecord(int32 RequestId, FString Uri) { IDownloadServiceStat::FDownloadRecord DownloadRecord; DownloadRecord.RequestId = RequestId; DownloadRecord.Uri = MoveTemp(Uri); DownloadRecord.bSuccess = false; DownloadRecord.ResponseCode = INDEX_NONE; DownloadRecord.SpeedRecord.CyclesStart = FStatsCollector::GetCycles(); DownloadRecord.SpeedRecord.CyclesEnd = DownloadRecord.SpeedRecord.CyclesStart; DownloadRecord.SpeedRecord.Size = 0; return DownloadRecord; } IDownloadService* FDownloadServiceFactory::Create(IHttpManager* HttpManager, IFileSystem* FileSystem, IDownloadServiceStat* DownloadServiceStat, IInstallerAnalytics* InstallerAnalytics) { check(HttpManager != nullptr); check(FileSystem != nullptr); check(DownloadServiceStat != nullptr); check(InstallerAnalytics != nullptr); return new FDownloadService(HttpManager, FileSystem, DownloadServiceStat, InstallerAnalytics); } }