Updater: Implement UI
This commit is contained in:
parent
f700fcae2e
commit
429dc54159
|
@ -14,6 +14,7 @@
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@
|
||||||
#include "Common/HttpRequest.h"
|
#include "Common/HttpRequest.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
|
|
||||||
|
#include "Updater/UI.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// Public key used to verify update manifests.
|
// Public key used to verify update manifests.
|
||||||
|
@ -286,6 +289,7 @@ std::optional<Manifest> ParseManifest(const std::string& manifest)
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not showing a progress bar here because this part is just too quick
|
||||||
std::optional<Manifest> FetchAndParseManifest(const std::string& url)
|
std::optional<Manifest> FetchAndParseManifest(const std::string& url)
|
||||||
{
|
{
|
||||||
Common::HttpRequest http;
|
Common::HttpRequest http;
|
||||||
|
@ -336,7 +340,12 @@ std::optional<Manifest> FetchAndParseManifest(const std::string& url)
|
||||||
// Represent the operations to be performed by the updater.
|
// Represent the operations to be performed by the updater.
|
||||||
struct TodoList
|
struct TodoList
|
||||||
{
|
{
|
||||||
std::vector<Manifest::Hash> to_download;
|
struct DownloadOp
|
||||||
|
{
|
||||||
|
Manifest::Filename filename;
|
||||||
|
Manifest::Hash hash;
|
||||||
|
};
|
||||||
|
std::vector<DownloadOp> to_download;
|
||||||
|
|
||||||
struct UpdateOp
|
struct UpdateOp
|
||||||
{
|
{
|
||||||
|
@ -405,7 +414,11 @@ TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest)
|
||||||
|
|
||||||
if (!old_hash || *old_hash != entry.second)
|
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;
|
TodoList::UpdateOp update;
|
||||||
update.filename = entry.first;
|
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)
|
void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo)
|
||||||
{
|
{
|
||||||
// This is best-effort cleanup, we ignore most errors.
|
// This is best-effort cleanup, we ignore most errors.
|
||||||
for (const auto& hash : todo.to_download)
|
for (const auto& download : todo.to_download)
|
||||||
File::Delete(temp_dir + DIR_SEP + HexEncode(hash.data(), hash.size()));
|
File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size()));
|
||||||
File::DeleteDir(temp_dir);
|
File::DeleteDir(temp_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,13 +482,25 @@ Manifest::Hash ComputeHash(const std::string& contents)
|
||||||
return out;
|
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)
|
const std::string& content_base_url, const std::string& temp_path)
|
||||||
{
|
{
|
||||||
Common::HttpRequest req(std::chrono::seconds(30));
|
Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback);
|
||||||
for (const auto& h : to_download)
|
|
||||||
|
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.
|
// Add slashes where needed.
|
||||||
std::string content_store_path = hash_filename;
|
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;
|
std::string url = content_base_url + content_store_path;
|
||||||
fprintf(log_fp, "Downloading %s ...\n", url.c_str());
|
fprintf(log_fp, "Downloading %s ...\n", url.c_str());
|
||||||
|
|
||||||
auto resp = req.Get(url);
|
auto resp = req.Get(url);
|
||||||
if (!resp)
|
if (!resp)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
UI::SetMarquee(true);
|
||||||
|
UI::SetDescription("Verifying " + download.filename + "...");
|
||||||
|
|
||||||
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||||
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||||
if (!maybe_decompressed)
|
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.
|
// Check that the downloaded contents have the right hash.
|
||||||
Manifest::Hash contents_hash = ComputeHash(decompressed);
|
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());
|
fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str());
|
||||||
return false;
|
return false;
|
||||||
|
@ -631,9 +660,11 @@ void FatalError(const std::string& message)
|
||||||
MessageBox(nullptr,
|
MessageBox(nullptr,
|
||||||
(L"A fatal error occured and the updater cannot continue:\n " + wide_message).c_str(),
|
(L"A fatal error occured and the updater cannot continue:\n " + wide_message).c_str(),
|
||||||
L"Error", MB_ICONERROR);
|
L"Error", MB_ICONERROR);
|
||||||
fprintf(log_fp, "%s\n", message.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fprintf(log_fp, "%s\n", message.c_str());
|
||||||
|
|
||||||
|
UI::Stop();
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
|
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");
|
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;
|
Manifest this_manifest, next_manifest;
|
||||||
{
|
{
|
||||||
std::optional<Manifest> maybe_manifest = FetchAndParseManifest(opts.this_manifest_url);
|
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);
|
next_manifest = std::move(*maybe_manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UI::SetDescription("Computing what to do...");
|
||||||
|
|
||||||
TodoList todo = ComputeActionsToDo(this_manifest, next_manifest);
|
TodoList todo = ComputeActionsToDo(this_manifest, next_manifest);
|
||||||
todo.Log();
|
todo.Log();
|
||||||
|
|
||||||
|
@ -698,16 +736,29 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine
|
||||||
return 1;
|
return 1;
|
||||||
std::string temp_dir = std::move(*maybe_temp_dir);
|
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);
|
bool ok = PerformUpdate(todo, opts.install_base_path, opts.content_store_url, temp_dir);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
FatalError("Failed to apply the update.");
|
FatalError("Failed to apply the update.");
|
||||||
|
|
||||||
CleanUpTempDir(temp_dir, todo);
|
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)
|
if (opts.binary_to_restart)
|
||||||
{
|
{
|
||||||
ShellExecuteW(nullptr, L"open", UTF8ToUTF16(*opts.binary_to_restart).c_str(), L"", nullptr,
|
ShellExecuteW(nullptr, L"open", UTF8ToUTF16(*opts.binary_to_restart).c_str(), L"", nullptr,
|
||||||
SW_SHOW);
|
SW_SHOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UI::Stop();
|
||||||
|
|
||||||
return !ok;
|
return !ok;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -38,7 +38,7 @@
|
||||||
<PropertyGroup />
|
<PropertyGroup />
|
||||||
<ItemDefinitionGroup>
|
<ItemDefinitionGroup>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>iphlpapi.lib;winmm.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>iphlpapi.lib;winmm.lib;ws2_32.lib;comctl32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -63,6 +63,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="Main.cpp" />
|
<ClCompile Include="Main.cpp" />
|
||||||
|
<ClCompile Include="UI.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
<ImportGroup Label="ExtensionTargets">
|
<ImportGroup Label="ExtensionTargets">
|
||||||
|
@ -71,6 +72,12 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<SourceFiles Include="$(TargetPath)" />
|
<SourceFiles Include="$(TargetPath)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="UI.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Manifest Include="Updater.exe.manifest" />
|
||||||
|
</ItemGroup>
|
||||||
<Target Name="AfterBuild" Inputs="@(SourceFiles)" Outputs="@(SourceFiles -> '$(BinaryOutputDir)%(Filename)%(Extension)')">
|
<Target Name="AfterBuild" Inputs="@(SourceFiles)" Outputs="@(SourceFiles -> '$(BinaryOutputDir)%(Filename)%(Extension)')">
|
||||||
<Message Text="Copy: @(SourceFiles) -> $(BinaryOutputDir)" Importance="High" />
|
<Message Text="Copy: @(SourceFiles) -> $(BinaryOutputDir)" Importance="High" />
|
||||||
<Copy SourceFiles="@(SourceFiles)" DestinationFolder="$(BinaryOutputDir)" />
|
<Copy SourceFiles="@(SourceFiles)" DestinationFolder="$(BinaryOutputDir)" />
|
||||||
|
|
|
@ -2,5 +2,12 @@
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="Main.cpp" />
|
<ClCompile Include="Main.cpp" />
|
||||||
|
<ClCompile Include="UI.cpp" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="UI.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Manifest Include="Updater.exe.manifest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
Loading…
Reference in New Issue