Merge pull request #6546 from spycrab/updater_ui

Implement Updater UI
This commit is contained in:
Léo Lam 2018-03-28 20:04:32 +02:00 committed by GitHub
commit 03a6a9b240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 299 additions and 20 deletions

View File

@ -6,9 +6,10 @@
#include <chrono>
#include <cstddef>
#include <curl/curl.h>
#include <mutex>
#include <curl/curl.h>
#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<CURL, decltype(&curl_easy_cleanup)> 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<Impl>(timeout_ms))
HttpRequest::HttpRequest(std::chrono::milliseconds timeout_ms, ProgressCallback callback)
: m_impl(std::make_unique<Impl>(timeout_ms, callback))
{
}
@ -69,7 +74,15 @@ HttpRequest::Response HttpRequest::Post(const std::string& url, const std::strin
reinterpret_cast<const u8*>(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<std::mutex> 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<std::vector<u8>*>(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<u8> 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";

View File

@ -5,6 +5,7 @@
#pragma once
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <optional>
@ -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<bool(double dlnow, double dltotal, double ulnow, double ultotal)>;
explicit HttpRequest(std::chrono::milliseconds timeout_ms = std::chrono::milliseconds{3000},
ProgressCallback callback = nullptr);
~HttpRequest();
bool IsValid() const;

View File

@ -14,6 +14,7 @@
#include <mbedtls/sha256.h>
#include <optional>
#include <shellapi.h>
#include <thread>
#include <vector>
#include <zlib.h>
@ -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<Manifest> ParseManifest(const std::string& manifest)
return parsed;
}
// Not showing a progress bar here because this part is just too quick
std::optional<Manifest> FetchAndParseManifest(const std::string& url)
{
Common::HttpRequest http;
@ -336,7 +340,12 @@ std::optional<Manifest> FetchAndParseManifest(const std::string& url)
// Represent the operations to be performed by the updater.
struct TodoList
{
std::vector<Manifest::Hash> to_download;
struct DownloadOp
{
Manifest::Filename filename;
Manifest::Hash hash;
};
std::vector<DownloadOp> 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<std::string> 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<Manifest::Hash>& to_download,
bool ProgressCallback(double total, double now, double, double)
{
UI::SetProgress(static_cast<int>(now), static_cast<int>(total));
return true;
}
bool DownloadContent(const std::vector<TodoList::DownloadOp>& 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<Manifest::Hash>& 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<char*>(resp->data()), resp->size());
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
if (!maybe_decompressed)
@ -496,7 +525,7 @@ bool DownloadContent(const std::vector<Manifest::Hash>& 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<Manifest> 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;
}

146
Source/Core/Updater/UI.cpp Normal file
View File

@ -0,0 +1,146 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "Updater/UI.h"
#include <CommCtrl.h>
#include <string>
#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<WPARAM>(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

19
Source/Core/Updater/UI.h Normal file
View File

@ -0,0 +1,19 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <Windows.h>
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

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="amd64"
name="DolphinTeam.DolphinEmuUpdater"
type="win32"
/>
<description>Dolphin updater</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>

View File

@ -38,7 +38,7 @@
<PropertyGroup />
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>iphlpapi.lib;winmm.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>iphlpapi.lib;winmm.lib;ws2_32.lib;comctl32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
@ -63,6 +63,7 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="Main.cpp" />
<ClCompile Include="UI.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
@ -71,6 +72,12 @@
<ItemGroup>
<SourceFiles Include="$(TargetPath)" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="UI.h" />
</ItemGroup>
<ItemGroup>
<Manifest Include="Updater.exe.manifest" />
</ItemGroup>
<Target Name="AfterBuild" Inputs="@(SourceFiles)" Outputs="@(SourceFiles -> '$(BinaryOutputDir)%(Filename)%(Extension)')">
<Message Text="Copy: @(SourceFiles) -&gt; $(BinaryOutputDir)" Importance="High" />
<Copy SourceFiles="@(SourceFiles)" DestinationFolder="$(BinaryOutputDir)" />

View File

@ -2,5 +2,12 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="Main.cpp" />
<ClCompile Include="UI.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="UI.h" />
</ItemGroup>
<ItemGroup>
<Manifest Include="Updater.exe.manifest" />
</ItemGroup>
</Project>