diff --git a/Source/Core/Common/HttpRequest.cpp b/Source/Core/Common/HttpRequest.cpp index da489ef18d..edb3286b19 100644 --- a/Source/Core/Common/HttpRequest.cpp +++ b/Source/Core/Common/HttpRequest.cpp @@ -6,9 +6,10 @@ #include #include -#include #include +#include + #include "Common/Logging/Log.h" #include "Common/ScopeGuard.h" #include "Common/StringUtil.h" @@ -24,23 +25,27 @@ public: POST, }; - explicit Impl(std::chrono::milliseconds timeout_ms); + explicit Impl(std::chrono::milliseconds timeout_ms, ProgressCallback callback); bool IsValid() const; Response Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload, size_t size); + static int CurlProgressCallback(Impl* impl, double dlnow, double dltotal, double ulnow, + double ultotal); + private: static std::mutex s_curl_was_inited_mutex; static bool s_curl_was_inited; + ProgressCallback m_callback; std::unique_ptr m_curl{nullptr, curl_easy_cleanup}; }; std::mutex HttpRequest::Impl::s_curl_was_inited_mutex; bool HttpRequest::Impl::s_curl_was_inited = false; -HttpRequest::HttpRequest(std::chrono::milliseconds timeout_ms) - : m_impl(std::make_unique(timeout_ms)) +HttpRequest::HttpRequest(std::chrono::milliseconds timeout_ms, ProgressCallback callback) + : m_impl(std::make_unique(timeout_ms, callback)) { } @@ -69,7 +74,15 @@ HttpRequest::Response HttpRequest::Post(const std::string& url, const std::strin reinterpret_cast(payload.data()), payload.size()); } -HttpRequest::Impl::Impl(std::chrono::milliseconds timeout_ms) +int HttpRequest::Impl::CurlProgressCallback(Impl* impl, double dlnow, double dltotal, double ulnow, + double ultotal) +{ + // Abort if callback isn't true + return !impl->m_callback(dlnow, dltotal, ulnow, ultotal); +} + +HttpRequest::Impl::Impl(std::chrono::milliseconds timeout_ms, ProgressCallback callback) + : m_callback(callback) { { std::lock_guard lk(s_curl_was_inited_mutex); @@ -84,6 +97,14 @@ HttpRequest::Impl::Impl(std::chrono::milliseconds timeout_ms) if (!m_curl) return; + curl_easy_setopt(m_curl.get(), CURLOPT_NOPROGRESS, m_callback == nullptr); + + if (m_callback) + { + curl_easy_setopt(m_curl.get(), CURLOPT_PROGRESSDATA, this); + curl_easy_setopt(m_curl.get(), CURLOPT_PROGRESSFUNCTION, CurlProgressCallback); + } + // libcurl may not have been built with async DNS support, so we disable // signal handlers to avoid a possible and likely crash if a resolve times out. curl_easy_setopt(m_curl.get(), CURLOPT_NOSIGNAL, true); @@ -99,7 +120,7 @@ bool HttpRequest::Impl::IsValid() const return m_curl != nullptr; } -static size_t CurlCallback(char* data, size_t size, size_t nmemb, void* userdata) +static size_t CurlWriteCallback(char* data, size_t size, size_t nmemb, void* userdata) { auto* buffer = static_cast*>(userdata); const size_t actual_size = size * nmemb; @@ -133,7 +154,7 @@ HttpRequest::Response HttpRequest::Impl::Fetch(const std::string& url, Method me curl_easy_setopt(m_curl.get(), CURLOPT_HTTPHEADER, list); std::vector buffer; - curl_easy_setopt(m_curl.get(), CURLOPT_WRITEFUNCTION, CurlCallback); + curl_easy_setopt(m_curl.get(), CURLOPT_WRITEFUNCTION, CurlWriteCallback); curl_easy_setopt(m_curl.get(), CURLOPT_WRITEDATA, &buffer); const char* type = method == Method::POST ? "POST" : "GET"; diff --git a/Source/Core/Common/HttpRequest.h b/Source/Core/Common/HttpRequest.h index 2a6590c1ca..92bae2307e 100644 --- a/Source/Core/Common/HttpRequest.h +++ b/Source/Core/Common/HttpRequest.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -18,7 +19,12 @@ namespace Common class HttpRequest final { public: - explicit HttpRequest(std::chrono::milliseconds timeout_ms = std::chrono::milliseconds{3000}); + // Return false to abort the request + using ProgressCallback = + std::function; + + explicit HttpRequest(std::chrono::milliseconds timeout_ms = std::chrono::milliseconds{3000}, + ProgressCallback callback = nullptr); ~HttpRequest(); bool IsValid() const; diff --git a/Source/Core/Updater/Main.cpp b/Source/Core/Updater/Main.cpp index 6731f7334d..d13019b8f6 100644 --- a/Source/Core/Updater/Main.cpp +++ b/Source/Core/Updater/Main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,8 @@ #include "Common/HttpRequest.h" #include "Common/StringUtil.h" +#include "Updater/UI.h" + namespace { // Public key used to verify update manifests. @@ -286,6 +289,7 @@ std::optional ParseManifest(const std::string& manifest) return parsed; } +// Not showing a progress bar here because this part is just too quick std::optional FetchAndParseManifest(const std::string& url) { Common::HttpRequest http; @@ -336,7 +340,12 @@ std::optional FetchAndParseManifest(const std::string& url) // Represent the operations to be performed by the updater. struct TodoList { - std::vector to_download; + struct DownloadOp + { + Manifest::Filename filename; + Manifest::Hash hash; + }; + std::vector to_download; struct UpdateOp { @@ -405,7 +414,11 @@ TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest) if (!old_hash || *old_hash != entry.second) { - todo.to_download.push_back(entry.second); + TodoList::DownloadOp download; + download.filename = entry.first; + download.hash = entry.second; + + todo.to_download.push_back(std::move(download)); TodoList::UpdateOp update; update.filename = entry.first; @@ -454,8 +467,8 @@ std::optional FindOrCreateTempDir(const std::string& base_path) void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo) { // This is best-effort cleanup, we ignore most errors. - for (const auto& hash : todo.to_download) - File::Delete(temp_dir + DIR_SEP + HexEncode(hash.data(), hash.size())); + for (const auto& download : todo.to_download) + File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size())); File::DeleteDir(temp_dir); } @@ -469,13 +482,25 @@ Manifest::Hash ComputeHash(const std::string& contents) return out; } -bool DownloadContent(const std::vector& to_download, +bool ProgressCallback(double total, double now, double, double) +{ + UI::SetProgress(static_cast(now), static_cast(total)); + return true; +} + +bool DownloadContent(const std::vector& to_download, const std::string& content_base_url, const std::string& temp_path) { - Common::HttpRequest req(std::chrono::seconds(30)); - for (const auto& h : to_download) + Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback); + + for (size_t i = 0; i < to_download.size(); i++) { - std::string hash_filename = HexEncode(h.data(), h.size()); + auto& download = to_download[i]; + + std::string hash_filename = HexEncode(download.hash.data(), download.hash.size()); + UI::SetDescription("Downloading " + download.filename + "... (File " + std::to_string(i + 1) + + " of " + std::to_string(to_download.size()) + ")"); + UI::SetMarquee(false); // Add slashes where needed. std::string content_store_path = hash_filename; @@ -484,10 +509,14 @@ bool DownloadContent(const std::vector& to_download, std::string url = content_base_url + content_store_path; fprintf(log_fp, "Downloading %s ...\n", url.c_str()); + auto resp = req.Get(url); if (!resp) return false; + UI::SetMarquee(true); + UI::SetDescription("Verifying " + download.filename + "..."); + std::string contents(reinterpret_cast(resp->data()), resp->size()); std::optional maybe_decompressed = GzipInflate(contents); if (!maybe_decompressed) @@ -496,7 +525,7 @@ bool DownloadContent(const std::vector& to_download, // Check that the downloaded contents have the right hash. Manifest::Hash contents_hash = ComputeHash(decompressed); - if (contents_hash != h) + if (contents_hash != download.hash) { fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str()); return false; @@ -631,9 +660,11 @@ void FatalError(const std::string& message) MessageBox(nullptr, (L"A fatal error occured and the updater cannot continue:\n " + wide_message).c_str(), L"Error", MB_ICONERROR); - fprintf(log_fp, "%s\n", message.c_str()); -} + fprintf(log_fp, "%s\n", message.c_str()); + + UI::Stop(); +} } // namespace int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) @@ -671,6 +702,11 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine fprintf(log_fp, "Completed! Proceeding with update.\n"); } + std::thread thread(UI::MessageLoop); + thread.detach(); + + UI::SetDescription("Fetching and parsing manifests..."); + Manifest this_manifest, next_manifest; { std::optional maybe_manifest = FetchAndParseManifest(opts.this_manifest_url); @@ -690,6 +726,8 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine next_manifest = std::move(*maybe_manifest); } + UI::SetDescription("Computing what to do..."); + TodoList todo = ComputeActionsToDo(this_manifest, next_manifest); todo.Log(); @@ -698,16 +736,29 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine return 1; std::string temp_dir = std::move(*maybe_temp_dir); + UI::SetDescription("Performing Update..."); + bool ok = PerformUpdate(todo, opts.install_base_path, opts.content_store_url, temp_dir); if (!ok) FatalError("Failed to apply the update."); CleanUpTempDir(temp_dir, todo); + UI::ResetProgress(); + UI::SetMarquee(false); + UI::SetProgress(100, 100); + UI::SetDescription("Done!"); + + // Let the user process that we are done. + Sleep(1000); + if (opts.binary_to_restart) { ShellExecuteW(nullptr, L"open", UTF8ToUTF16(*opts.binary_to_restart).c_str(), L"", nullptr, SW_SHOW); } + + UI::Stop(); + return !ok; } diff --git a/Source/Core/Updater/UI.cpp b/Source/Core/Updater/UI.cpp new file mode 100644 index 0000000000..7f4e5cb1b2 --- /dev/null +++ b/Source/Core/Updater/UI.cpp @@ -0,0 +1,146 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "Updater/UI.h" + +#include + +#include + +#include "Common/Flag.h" +#include "Common/StringUtil.h" + +namespace +{ +HWND window_handle = nullptr; +HWND label_handle = nullptr; +HWND progressbar_handle = nullptr; + +Common::Flag running; +Common::Flag request_stop; +}; // namespace + +constexpr int PROGRESSBAR_FLAGS = WS_VISIBLE | WS_CHILD | PBS_SMOOTH | PBS_SMOOTHREVERSE; + +namespace UI +{ +bool Init() +{ + InitCommonControls(); + + WNDCLASS wndcl = {}; + wndcl.lpfnWndProc = DefWindowProcW; + wndcl.hbrBackground = GetSysColorBrush(COLOR_MENU); + wndcl.lpszClassName = L"UPDATER"; + + if (!RegisterClass(&wndcl)) + return false; + + window_handle = + CreateWindow(L"UPDATER", L"Dolphin Updater", WS_VISIBLE | WS_CLIPCHILDREN, CW_USEDEFAULT, + CW_USEDEFAULT, 500, 100, nullptr, nullptr, GetModuleHandle(nullptr), 0); + + if (!window_handle) + return false; + + label_handle = CreateWindow(L"STATIC", NULL, WS_VISIBLE | WS_CHILD, 5, 5, 500, 25, window_handle, + nullptr, nullptr, 0); + + if (!label_handle) + return false; + + // Get the default system font + NONCLIENTMETRICS metrics = {}; + metrics.cbSize = sizeof(NONCLIENTMETRICS); + + if (!SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0)) + return false; + + SendMessage(label_handle, WM_SETFONT, + reinterpret_cast(CreateFontIndirect(&metrics.lfMessageFont)), 0); + + progressbar_handle = CreateWindow(PROGRESS_CLASS, NULL, PROGRESSBAR_FLAGS, 5, 25, 470, 25, + window_handle, nullptr, nullptr, 0); + + if (!progressbar_handle) + return false; + + return true; +} + +void Destroy() +{ + DestroyWindow(window_handle); + DestroyWindow(label_handle); + DestroyWindow(progressbar_handle); +} + +void SetMarquee(bool marquee) +{ + SetWindowLong(progressbar_handle, GWL_STYLE, PROGRESSBAR_FLAGS | (marquee ? PBS_MARQUEE : 0)); + SendMessage(progressbar_handle, PBM_SETMARQUEE, marquee, 0); +} + +void ResetProgress() +{ + SendMessage(progressbar_handle, PBM_SETPOS, 0, 0); + SetMarquee(true); +} + +void SetProgress(int current, int total) +{ + SendMessage(progressbar_handle, PBM_SETRANGE32, 0, total); + SendMessage(progressbar_handle, PBM_SETPOS, current, 0); +} + +void IncrementProgress(int amount) +{ + SendMessage(progressbar_handle, PBM_DELTAPOS, amount, 0); +} + +void SetDescription(const std::string& text) +{ + SetWindowText(label_handle, UTF8ToUTF16(text).c_str()); +} + +void MessageLoop() +{ + request_stop.Clear(); + running.Set(); + + if (!Init()) + { + running.Clear(); + MessageBox(nullptr, L"Window init failed!", L"", MB_ICONERROR); + } + + SetMarquee(true); + + while (!request_stop.IsSet()) + { + MSG msg; + while (PeekMessage(&msg, window_handle, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + running.Clear(); + + Destroy(); +} + +void Stop() +{ + if (!running.IsSet()) + return; + + request_stop.Set(); + + while (running.IsSet()) + { + } +} +}; // namespace UI diff --git a/Source/Core/Updater/UI.h b/Source/Core/Updater/UI.h new file mode 100644 index 0000000000..3e22ceb0c8 --- /dev/null +++ b/Source/Core/Updater/UI.h @@ -0,0 +1,19 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +namespace UI +{ +void MessageLoop(); +void Stop(); + +void SetDescription(const std::string& text); +void SetMarquee(bool marquee); +void ResetProgress(); +void SetProgress(int current, int total); +void IncrementProgress(int amount); +} // namespace UI diff --git a/Source/Core/Updater/Updater.exe.manifest b/Source/Core/Updater/Updater.exe.manifest new file mode 100644 index 0000000000..5116462f29 --- /dev/null +++ b/Source/Core/Updater/Updater.exe.manifest @@ -0,0 +1,22 @@ + + + +Dolphin updater + + + + + + \ No newline at end of file diff --git a/Source/Core/Updater/Updater.vcxproj b/Source/Core/Updater/Updater.vcxproj index a13fe1413a..748e507ec4 100644 --- a/Source/Core/Updater/Updater.vcxproj +++ b/Source/Core/Updater/Updater.vcxproj @@ -38,7 +38,7 @@ - iphlpapi.lib;winmm.lib;ws2_32.lib;%(AdditionalDependencies) + iphlpapi.lib;winmm.lib;ws2_32.lib;comctl32.lib;%(AdditionalDependencies) @@ -63,6 +63,7 @@ + @@ -71,6 +72,12 @@ + + + + + + diff --git a/Source/Core/Updater/Updater.vcxproj.filters b/Source/Core/Updater/Updater.vcxproj.filters index d3e9077da4..f39ac62aae 100644 --- a/Source/Core/Updater/Updater.vcxproj.filters +++ b/Source/Core/Updater/Updater.vcxproj.filters @@ -2,5 +2,12 @@ + + + + + + + \ No newline at end of file