mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-24 06:39:46 -06:00
Merge pull request #6463 from delroth/auto-update
Initial implementation of the Dolphin auto-updater for Windows
This commit is contained in:
@ -7,6 +7,7 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <curl/curl.h>
|
||||
#include <mutex>
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
@ -30,9 +31,14 @@ public:
|
||||
size_t size);
|
||||
|
||||
private:
|
||||
std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> m_curl{curl_easy_init(), curl_easy_cleanup};
|
||||
static std::mutex s_curl_was_inited_mutex;
|
||||
static bool s_curl_was_inited;
|
||||
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))
|
||||
{
|
||||
@ -65,6 +71,16 @@ HttpRequest::Response HttpRequest::Post(const std::string& url, const std::strin
|
||||
|
||||
HttpRequest::Impl::Impl(std::chrono::milliseconds timeout_ms)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(s_curl_was_inited_mutex);
|
||||
if (!s_curl_was_inited)
|
||||
{
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
s_curl_was_inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_curl.reset(curl_easy_init());
|
||||
if (!m_curl)
|
||||
return;
|
||||
|
||||
|
@ -81,6 +81,8 @@ var isStable = +("master" == branch || "stable" == branch);
|
||||
// Get environment information.
|
||||
var distributor = wshShell.ExpandEnvironmentStrings("%DOLPHIN_DISTRIBUTOR%");
|
||||
if (distributor == "%DOLPHIN_DISTRIBUTOR%") distributor = "None";
|
||||
var default_update_track = wshShell.ExpandEnvironmentStrings("%DOLPHIN_DEFAULT_UPDATE_TRACK%");
|
||||
if (default_update_track == "%DOLPHIN_DEFAULT_UPDATE_TRACK%") default_update_track = "";
|
||||
|
||||
// remove hash (and trailing "-0" if needed) from description
|
||||
describe = describe.replace(/(-0)?-[^-]+(-dirty)?$/, '$2');
|
||||
@ -90,7 +92,8 @@ var out_contents =
|
||||
"#define SCM_DESC_STR \"" + describe + "\"\n" +
|
||||
"#define SCM_BRANCH_STR \"" + branch + "\"\n" +
|
||||
"#define SCM_IS_MASTER " + isStable + "\n" +
|
||||
"#define SCM_DISTRIBUTOR_STR \"" + distributor + "\"\n";
|
||||
"#define SCM_DISTRIBUTOR_STR \"" + distributor + "\"\n" +
|
||||
"#define SCM_UPDATE_TRACK_STR \"" + default_update_track + "\"\n";
|
||||
|
||||
// check if file needs updating
|
||||
if (out_contents == GetFileContents(outfile))
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Common/NandPaths.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/scmrev.h"
|
||||
|
||||
#include "Core/Analytics.h"
|
||||
#include "Core/Boot/Boot.h"
|
||||
@ -90,6 +91,7 @@ void SConfig::SaveSettings()
|
||||
SaveNetworkSettings(ini);
|
||||
SaveBluetoothPassthroughSettings(ini);
|
||||
SaveUSBPassthroughSettings(ini);
|
||||
SaveAutoUpdateSettings(ini);
|
||||
|
||||
ini.Save(File::GetUserPath(F_DOLPHINCONFIG_IDX));
|
||||
|
||||
@ -369,6 +371,14 @@ void SConfig::SaveUSBPassthroughSettings(IniFile& ini)
|
||||
section->Set("Devices", devices_string);
|
||||
}
|
||||
|
||||
void SConfig::SaveAutoUpdateSettings(IniFile& ini)
|
||||
{
|
||||
IniFile::Section* section = ini.GetOrCreateSection("AutoUpdate");
|
||||
|
||||
section->Set("TrackForTesting", m_auto_update_track);
|
||||
section->Set("HashOverride", m_auto_update_hash_override);
|
||||
}
|
||||
|
||||
void SConfig::LoadSettings()
|
||||
{
|
||||
Config::Load();
|
||||
@ -390,6 +400,7 @@ void SConfig::LoadSettings()
|
||||
LoadAnalyticsSettings(ini);
|
||||
LoadBluetoothPassthroughSettings(ini);
|
||||
LoadUSBPassthroughSettings(ini);
|
||||
LoadAutoUpdateSettings(ini);
|
||||
}
|
||||
|
||||
void SConfig::LoadGeneralSettings(IniFile& ini)
|
||||
@ -669,6 +680,15 @@ void SConfig::LoadUSBPassthroughSettings(IniFile& ini)
|
||||
}
|
||||
}
|
||||
|
||||
void SConfig::LoadAutoUpdateSettings(IniFile& ini)
|
||||
{
|
||||
IniFile::Section* section = ini.GetOrCreateSection("AutoUpdate");
|
||||
|
||||
// TODO: Rename and default to SCM_UPDATE_TRACK_STR when ready for general consumption.
|
||||
section->Get("TrackForTesting", &m_auto_update_track, "");
|
||||
section->Get("HashOverride", &m_auto_update_hash_override, "");
|
||||
}
|
||||
|
||||
void SConfig::ResetRunningGameMetadata()
|
||||
{
|
||||
SetRunningGameMetadata("00000000", 0, 0, Core::TitleDatabase::TitleType::Other);
|
||||
|
@ -315,6 +315,10 @@ struct SConfig
|
||||
bool m_SSLDumpRootCA;
|
||||
bool m_SSLDumpPeerCert;
|
||||
|
||||
// Auto-update settings
|
||||
std::string m_auto_update_track;
|
||||
std::string m_auto_update_hash_override;
|
||||
|
||||
SConfig(const SConfig&) = delete;
|
||||
SConfig& operator=(const SConfig&) = delete;
|
||||
SConfig(SConfig&&) = delete;
|
||||
@ -348,6 +352,7 @@ private:
|
||||
void SaveAnalyticsSettings(IniFile& ini);
|
||||
void SaveBluetoothPassthroughSettings(IniFile& ini);
|
||||
void SaveUSBPassthroughSettings(IniFile& ini);
|
||||
void SaveAutoUpdateSettings(IniFile& ini);
|
||||
|
||||
void LoadGeneralSettings(IniFile& ini);
|
||||
void LoadInterfaceSettings(IniFile& ini);
|
||||
@ -362,6 +367,7 @@ private:
|
||||
void LoadAnalyticsSettings(IniFile& ini);
|
||||
void LoadBluetoothPassthroughSettings(IniFile& ini);
|
||||
void LoadUSBPassthroughSettings(IniFile& ini);
|
||||
void LoadAutoUpdateSettings(IniFile& ini);
|
||||
|
||||
void SetRunningGameMetadata(const std::string& game_id, u64 title_id, u16 revision,
|
||||
Core::TitleDatabase::TitleType type);
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Core/Analytics.h"
|
||||
@ -19,6 +20,7 @@
|
||||
#include "DolphinQt2/Resources.h"
|
||||
#include "DolphinQt2/Settings.h"
|
||||
#include "DolphinQt2/Translation.h"
|
||||
#include "UICommon/AutoUpdate.h"
|
||||
#include "UICommon/CommandLineParse.h"
|
||||
#include "UICommon/UICommon.h"
|
||||
|
||||
@ -49,6 +51,35 @@ static bool QtMsgAlertHandler(const char* caption, const char* text, bool yes_no
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: This should be replaced with something in a background thread, it performs a blocking
|
||||
// HTTP query. It also needs a proper UI, and many other things. But right now it needs to be
|
||||
// manually enabled through INI, so all these problems are ignored :)
|
||||
class QtAutoUpdateChecker : public AutoUpdateChecker
|
||||
{
|
||||
public:
|
||||
explicit QtAutoUpdateChecker(QWidget* parent) : m_parent(parent) {}
|
||||
protected:
|
||||
void OnUpdateAvailable(const NewVersionInformation& info) override
|
||||
{
|
||||
QMessageBox prompt(m_parent);
|
||||
|
||||
prompt.setIcon(QMessageBox::Question);
|
||||
prompt.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||||
prompt.setText(QString::fromUtf8("Update Dolphin to version %1?")
|
||||
.arg(QString::fromStdString(info.new_shortrev)));
|
||||
|
||||
const int answer = prompt.exec();
|
||||
if (answer == QMessageBox::Yes)
|
||||
{
|
||||
TriggerUpdate(info);
|
||||
m_parent->close();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QWidget* m_parent;
|
||||
};
|
||||
|
||||
// N.B. On Windows, this should be called from WinMain. Link against qtmain and specify
|
||||
// /SubSystem:Windows
|
||||
int main(int argc, char* argv[])
|
||||
@ -146,6 +177,9 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
QtAutoUpdateChecker updater(&win);
|
||||
updater.CheckForUpdate();
|
||||
|
||||
retval = app.exec();
|
||||
}
|
||||
|
||||
|
@ -279,6 +279,9 @@
|
||||
<ProjectReference Include="$(CoreDir)VideoCommon\VideoCommon.vcxproj">
|
||||
<Project>{3de9ee35-3e91-4f27-a014-2866ad8c3fe3}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Updater\Updater.vcxproj">
|
||||
<Project>{e4becbab-9c6e-41ab-bb56-f9d70ab6be03}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
139
Source/Core/UICommon/AutoUpdate.cpp
Normal file
139
Source/Core/UICommon/AutoUpdate.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "UICommon/AutoUpdate.h"
|
||||
|
||||
#include <picojson/picojson.h>
|
||||
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/HttpRequest.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/scmrev.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
bool SystemSupportsAutoUpdates()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
const char UPDATER_FILENAME[] = "Updater.exe";
|
||||
const char UPDATER_RELOC_FILENAME[] = "Updater.2.exe";
|
||||
const char UPDATER_LOG_FILE[] = "Updater.log";
|
||||
|
||||
std::wstring MakeUpdaterCommandLine(const std::map<std::string, std::string>& flags)
|
||||
{
|
||||
std::wstring cmdline = UTF8ToUTF16(UPDATER_FILENAME) + L" "; // Start with a fake argv[0].
|
||||
for (const auto& pair : flags)
|
||||
{
|
||||
std::string value = "--" + pair.first + "=" + pair.second;
|
||||
value = ReplaceAll(value, "\"", "\\\""); // Escape double quotes.
|
||||
value = "\"" + value + "\" ";
|
||||
cmdline += UTF8ToUTF16(value);
|
||||
}
|
||||
return cmdline;
|
||||
}
|
||||
|
||||
// Used to remove the relocated updater file once we don't need it anymore.
|
||||
void CleanupFromPreviousUpdate()
|
||||
{
|
||||
std::string reloc_updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME;
|
||||
File::Delete(reloc_updater_path);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void AutoUpdateChecker::CheckForUpdate()
|
||||
{
|
||||
// Don't bother checking if updates are not supported or not enabled.
|
||||
if (SConfig::GetInstance().m_auto_update_track.empty() || !SystemSupportsAutoUpdates())
|
||||
return;
|
||||
|
||||
#ifdef _WIN32
|
||||
CleanupFromPreviousUpdate();
|
||||
#endif
|
||||
|
||||
std::string version_hash = SConfig::GetInstance().m_auto_update_hash_override.empty() ?
|
||||
SCM_REV_STR :
|
||||
SConfig::GetInstance().m_auto_update_hash_override;
|
||||
std::string url = "https://dolphin-emu.org/update/check/v0/" +
|
||||
SConfig::GetInstance().m_auto_update_track + "/" + version_hash;
|
||||
|
||||
Common::HttpRequest req{std::chrono::seconds{10}};
|
||||
auto resp = req.Get(url);
|
||||
if (!resp)
|
||||
{
|
||||
ERROR_LOG(COMMON, "Auto-update request failed");
|
||||
return;
|
||||
}
|
||||
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||
INFO_LOG(COMMON, "Auto-update JSON response: %s", contents.c_str());
|
||||
|
||||
picojson::value json;
|
||||
std::string err = picojson::parse(json, contents);
|
||||
if (!err.empty())
|
||||
{
|
||||
ERROR_LOG(COMMON, "Invalid JSON received from auto-update service: %s", err.c_str());
|
||||
return;
|
||||
}
|
||||
picojson::object obj = json.get<picojson::object>();
|
||||
|
||||
if (obj["status"].get<std::string>() != "outdated")
|
||||
{
|
||||
INFO_LOG(COMMON, "Auto-update status: we are up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
NewVersionInformation nvi;
|
||||
nvi.this_manifest_url = obj["old"].get<picojson::object>()["manifest"].get<std::string>();
|
||||
nvi.next_manifest_url = obj["new"].get<picojson::object>()["manifest"].get<std::string>();
|
||||
nvi.content_store_url = obj["content-store"].get<std::string>();
|
||||
nvi.new_shortrev = obj["new"].get<picojson::object>()["name"].get<std::string>();
|
||||
nvi.new_hash = obj["new"].get<picojson::object>()["hash"].get<std::string>();
|
||||
// TODO: generate the HTML changelog from the JSON information.
|
||||
OnUpdateAvailable(nvi);
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInformation& info)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
std::map<std::string, std::string> updater_flags;
|
||||
updater_flags["this-manifest-url"] = info.this_manifest_url;
|
||||
updater_flags["next-manifest-url"] = info.next_manifest_url;
|
||||
updater_flags["content-store-url"] = info.content_store_url;
|
||||
updater_flags["parent-pid"] = std::to_string(GetCurrentProcessId());
|
||||
updater_flags["install-base-path"] = File::GetExeDirectory();
|
||||
updater_flags["log-file"] = File::GetExeDirectory() + DIR_SEP + UPDATER_LOG_FILE;
|
||||
|
||||
// Copy the updater so it can update itself if needed.
|
||||
std::string updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_FILENAME;
|
||||
std::string reloc_updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME;
|
||||
File::Copy(updater_path, reloc_updater_path);
|
||||
|
||||
// Run the updater!
|
||||
std::wstring command_line = MakeUpdaterCommandLine(updater_flags);
|
||||
STARTUPINFO sinfo = {sizeof(info)};
|
||||
PROCESS_INFORMATION pinfo;
|
||||
INFO_LOG(COMMON, "Updater command line: %s", UTF16ToUTF8(command_line).c_str());
|
||||
if (!CreateProcessW(UTF8ToUTF16(reloc_updater_path).c_str(),
|
||||
const_cast<wchar_t*>(command_line.c_str()), nullptr, nullptr, FALSE, 0,
|
||||
nullptr, nullptr, &sinfo, &pinfo))
|
||||
{
|
||||
ERROR_LOG(COMMON, "Could not start updater process: error=%d", GetLastError());
|
||||
}
|
||||
#endif
|
||||
}
|
38
Source/Core/UICommon/AutoUpdate.h
Normal file
38
Source/Core/UICommon/AutoUpdate.h
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// This class defines all the logic for Dolphin auto-update checking. UI-specific elements have to
|
||||
// be defined in a backend specific subclass.
|
||||
class AutoUpdateChecker
|
||||
{
|
||||
public:
|
||||
// Initiates a check for updates in the background. Calls the OnUpdateAvailable callback if an
|
||||
// update is available, does "nothing" otherwise.
|
||||
void CheckForUpdate();
|
||||
|
||||
struct NewVersionInformation
|
||||
{
|
||||
// Name (5.0-1234) and revision hash of the new version.
|
||||
std::string new_shortrev;
|
||||
std::string new_hash;
|
||||
|
||||
// The full changelog in HTML format.
|
||||
std::string changelog_html;
|
||||
|
||||
// Internals, to be passed to the updater binary.
|
||||
std::string this_manifest_url;
|
||||
std::string next_manifest_url;
|
||||
std::string content_store_url;
|
||||
};
|
||||
|
||||
// Starts the updater process, which will wait in the background until the current process exits.
|
||||
void TriggerUpdate(const NewVersionInformation& info);
|
||||
|
||||
protected:
|
||||
virtual void OnUpdateAvailable(const NewVersionInformation& info) = 0;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
set(SRCS
|
||||
AutoUpdate.cpp
|
||||
CommandLineParse.cpp
|
||||
Disassembler.cpp
|
||||
GameFile.cpp
|
||||
|
@ -42,8 +42,12 @@
|
||||
<ProjectReference Include="$(CoreDir)Core\Core.vcxproj">
|
||||
<Project>{E54CF649-140E-4255-81A5-30A673C1FB36}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\Externals\picojson\picojson.vcxproj">
|
||||
<Project>{2c0d058e-de35-4471-ad99-e68a2caf9e18}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="AutoUpdate.cpp" />
|
||||
<ClCompile Include="CommandLineParse.cpp" />
|
||||
<ClCompile Include="UICommon.cpp" />
|
||||
<ClCompile Include="Disassembler.cpp" />
|
||||
@ -55,6 +59,7 @@
|
||||
<ClCompile Include="GameFileCache.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="AutoUpdate.h" />
|
||||
<ClInclude Include="CommandLineParse.h" />
|
||||
<ClInclude Include="UICommon.h" />
|
||||
<ClInclude Include="Disassembler.h" />
|
||||
@ -70,4 +75,4 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
</Project>
|
689
Source/Core/Updater/Main.cpp
Normal file
689
Source/Core/Updater/Main.cpp
Normal file
@ -0,0 +1,689 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <OptionParser.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <ed25519/ed25519.h>
|
||||
#include <mbedtls/base64.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <optional>
|
||||
#include <shellapi.h>
|
||||
#include <vector>
|
||||
#include <zlib.h>
|
||||
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/HttpRequest.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Public key used to verify update manifests.
|
||||
const u8 UPDATE_PUB_KEY[] = {0x2a, 0xb3, 0xd1, 0xdc, 0x6e, 0xf5, 0x07, 0xf6, 0xa0, 0x6c, 0x7c,
|
||||
0x54, 0xdf, 0x54, 0xf4, 0x42, 0x80, 0xa6, 0x28, 0x8b, 0x6d, 0x70,
|
||||
0x14, 0xb5, 0x4c, 0x34, 0x95, 0x20, 0x4d, 0xd4, 0xd3, 0x5d};
|
||||
|
||||
const char UPDATE_TEMP_DIR[] = "TempUpdate";
|
||||
|
||||
// Where to log updater output.
|
||||
FILE* log_fp = stderr;
|
||||
|
||||
void FlushLog()
|
||||
{
|
||||
fflush(log_fp);
|
||||
fclose(log_fp);
|
||||
}
|
||||
|
||||
// Internal representation of options passed on the command-line.
|
||||
struct Options
|
||||
{
|
||||
std::string this_manifest_url;
|
||||
std::string next_manifest_url;
|
||||
std::string content_store_url;
|
||||
std::string install_base_path;
|
||||
std::optional<DWORD> parent_pid;
|
||||
std::optional<std::string> log_file;
|
||||
};
|
||||
|
||||
std::vector<std::string> CommandLineToUtf8Argv(PCWSTR command_line)
|
||||
{
|
||||
int nargs;
|
||||
LPWSTR* tokenized = CommandLineToArgvW(command_line, &nargs);
|
||||
if (!tokenized)
|
||||
return {};
|
||||
|
||||
std::vector<std::string> argv(nargs);
|
||||
for (int i = 0; i < nargs; ++i)
|
||||
{
|
||||
argv[i] = UTF16ToUTF8(tokenized[i]);
|
||||
}
|
||||
|
||||
LocalFree(tokenized);
|
||||
return argv;
|
||||
}
|
||||
|
||||
std::optional<Options> ParseCommandLine(PCWSTR command_line)
|
||||
{
|
||||
using optparse::OptionParser;
|
||||
|
||||
OptionParser parser = OptionParser().prog("updater.exe").description("Dolphin Updater binary");
|
||||
|
||||
parser.add_option("--this-manifest-url")
|
||||
.dest("this-manifest-url")
|
||||
.help("URL to the update manifest for the currently installed version.")
|
||||
.metavar("URL");
|
||||
parser.add_option("--next-manifest-url")
|
||||
.dest("next-manifest-url")
|
||||
.help("URL to the update manifest for the to-be-installed version.")
|
||||
.metavar("URL");
|
||||
parser.add_option("--content-store-url")
|
||||
.dest("content-store-url")
|
||||
.help("Base URL of the content store where files to download are stored.")
|
||||
.metavar("URL");
|
||||
parser.add_option("--install-base-path")
|
||||
.dest("install-base-path")
|
||||
.help("Base path of the Dolphin install to be updated.")
|
||||
.metavar("PATH");
|
||||
parser.add_option("--log-file")
|
||||
.dest("log-file")
|
||||
.help("File where to log updater debug output.")
|
||||
.metavar("PATH");
|
||||
parser.add_option("--parent-pid")
|
||||
.dest("parent-pid")
|
||||
.type("int")
|
||||
.help("(optional) PID of the parent process. The updater will wait for this process to "
|
||||
"complete before proceeding.")
|
||||
.metavar("PID");
|
||||
|
||||
std::vector<std::string> argv = CommandLineToUtf8Argv(command_line);
|
||||
optparse::Values options = parser.parse_args(argv);
|
||||
|
||||
Options opts;
|
||||
|
||||
// Required arguments.
|
||||
std::vector<std::string> required{"this-manifest-url", "next-manifest-url", "content-store-url",
|
||||
"install-base-path"};
|
||||
for (const auto& req : required)
|
||||
{
|
||||
if (!options.is_set(req))
|
||||
{
|
||||
parser.print_help();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
opts.this_manifest_url = options["this-manifest-url"];
|
||||
opts.next_manifest_url = options["next-manifest-url"];
|
||||
opts.content_store_url = options["content-store-url"];
|
||||
opts.install_base_path = options["install-base-path"];
|
||||
|
||||
// Optional arguments.
|
||||
if (options.is_set("parent-pid"))
|
||||
opts.parent_pid = (DWORD)options.get("parent-pid");
|
||||
if (options.is_set("log-file"))
|
||||
opts.log_file = options["log-file"];
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
std::optional<std::string> GzipInflate(const std::string& data)
|
||||
{
|
||||
z_stream zstrm;
|
||||
zstrm.zalloc = nullptr;
|
||||
zstrm.zfree = nullptr;
|
||||
zstrm.opaque = nullptr;
|
||||
zstrm.avail_in = static_cast<u32>(data.size());
|
||||
zstrm.next_in = reinterpret_cast<u8*>(const_cast<char*>(data.data()));
|
||||
|
||||
// 16 + MAX_WBITS means gzip. Don't ask me.
|
||||
inflateInit2(&zstrm, 16 + MAX_WBITS);
|
||||
|
||||
std::string out;
|
||||
char buffer[4096];
|
||||
int ret;
|
||||
|
||||
do
|
||||
{
|
||||
zstrm.avail_out = sizeof(buffer);
|
||||
zstrm.next_out = reinterpret_cast<u8*>(buffer);
|
||||
|
||||
ret = inflate(&zstrm, 0);
|
||||
out.append(buffer, sizeof(buffer) - zstrm.avail_out);
|
||||
} while (ret == Z_OK);
|
||||
|
||||
inflateEnd(&zstrm);
|
||||
|
||||
if (ret != Z_STREAM_END)
|
||||
{
|
||||
fprintf(log_fp, "Could not read the data as gzip: error %d.\n", ret);
|
||||
return {};
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
bool VerifySignature(const std::string& data, const std::string& b64_signature)
|
||||
{
|
||||
u8 signature[64]; // ed25519 sig size.
|
||||
size_t sig_size;
|
||||
|
||||
if (mbedtls_base64_decode(signature, sizeof(signature), &sig_size,
|
||||
reinterpret_cast<const u8*>(b64_signature.data()),
|
||||
b64_signature.size()) ||
|
||||
sig_size != sizeof(signature))
|
||||
{
|
||||
fprintf(log_fp, "Invalid base64: %s\n", b64_signature.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return ed25519_verify(signature, reinterpret_cast<const u8*>(data.data()), data.size(),
|
||||
UPDATE_PUB_KEY);
|
||||
}
|
||||
|
||||
struct Manifest
|
||||
{
|
||||
using Filename = std::string;
|
||||
using Hash = std::array<u8, 16>;
|
||||
std::map<Filename, Hash> entries;
|
||||
};
|
||||
|
||||
bool HexDecode(const std::string& hex, u8* buffer, size_t size)
|
||||
{
|
||||
if (hex.size() != size * 2)
|
||||
return false;
|
||||
|
||||
auto DecodeNibble = [](char c) -> std::optional<u8> {
|
||||
if (c >= '0' && c <= '9')
|
||||
return static_cast<u8>(c - '0');
|
||||
else if (c >= 'a' && c <= 'f')
|
||||
return static_cast<u8>(c - 'a' + 10);
|
||||
else if (c >= 'A' && c <= 'F')
|
||||
return static_cast<u8>(c - 'A' + 10);
|
||||
else
|
||||
return {};
|
||||
};
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
std::optional<u8> high = DecodeNibble(hex[2 * i]);
|
||||
std::optional<u8> low = DecodeNibble(hex[2 * i + 1]);
|
||||
|
||||
if (!high || !low)
|
||||
return false;
|
||||
|
||||
buffer[i] = (*high << 4) | *low;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HexEncode(const u8* buffer, size_t size)
|
||||
{
|
||||
std::string out(size * 2, '\0');
|
||||
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
out[2 * i] = "0123456789abcdef"[buffer[i] >> 4];
|
||||
out[2 * i + 1] = "0123456789abcdef"[buffer[i] & 0xF];
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<Manifest> ParseManifest(const std::string& manifest)
|
||||
{
|
||||
Manifest parsed;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < manifest.size())
|
||||
{
|
||||
size_t filename_end_pos = manifest.find('\t', pos);
|
||||
if (filename_end_pos == std::string::npos)
|
||||
{
|
||||
fprintf(log_fp, "Manifest entry %zu: could not find filename end.\n", parsed.entries.size());
|
||||
return {};
|
||||
}
|
||||
size_t hash_end_pos = manifest.find('\n', filename_end_pos);
|
||||
if (hash_end_pos == std::string::npos)
|
||||
{
|
||||
fprintf(log_fp, "Manifest entry %zu: could not find hash end.\n", parsed.entries.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string filename = manifest.substr(pos, filename_end_pos - pos);
|
||||
std::string hash = manifest.substr(filename_end_pos + 1, hash_end_pos - filename_end_pos - 1);
|
||||
if (hash.size() != 32)
|
||||
{
|
||||
fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(),
|
||||
hash.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
Manifest::Hash decoded_hash;
|
||||
if (!HexDecode(hash, decoded_hash.data(), decoded_hash.size()))
|
||||
{
|
||||
fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(),
|
||||
hash.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
parsed.entries[filename] = decoded_hash;
|
||||
pos = hash_end_pos + 1;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
std::optional<Manifest> FetchAndParseManifest(const std::string& url)
|
||||
{
|
||||
Common::HttpRequest http;
|
||||
|
||||
Common::HttpRequest::Response resp = http.Get(url);
|
||||
if (!resp)
|
||||
{
|
||||
fprintf(log_fp, "Manifest download failed.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||
if (!maybe_decompressed)
|
||||
return {};
|
||||
std::string decompressed = std::move(*maybe_decompressed);
|
||||
|
||||
// Split into manifest and signature.
|
||||
size_t boundary = decompressed.rfind("\n\n");
|
||||
if (boundary == std::string::npos)
|
||||
{
|
||||
fprintf(log_fp, "No signature was found in manifest.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string signature_block = decompressed.substr(boundary + 2); // 2 for "\n\n".
|
||||
decompressed.resize(boundary + 1); // 1 to keep the final "\n".
|
||||
|
||||
std::vector<std::string> signatures = SplitString(signature_block, '\n');
|
||||
bool found_valid_signature = false;
|
||||
for (const auto& signature : signatures)
|
||||
{
|
||||
if (VerifySignature(decompressed, signature))
|
||||
{
|
||||
found_valid_signature = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_valid_signature)
|
||||
{
|
||||
fprintf(log_fp, "Could not verify signature of the manifest.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
return ParseManifest(decompressed);
|
||||
}
|
||||
|
||||
// Represent the operations to be performed by the updater.
|
||||
struct TodoList
|
||||
{
|
||||
std::vector<Manifest::Hash> to_download;
|
||||
|
||||
struct UpdateOp
|
||||
{
|
||||
Manifest::Filename filename;
|
||||
std::optional<Manifest::Hash> old_hash;
|
||||
Manifest::Hash new_hash;
|
||||
};
|
||||
std::vector<UpdateOp> to_update;
|
||||
|
||||
struct DeleteOp
|
||||
{
|
||||
Manifest::Filename filename;
|
||||
Manifest::Hash old_hash;
|
||||
};
|
||||
std::vector<DeleteOp> to_delete;
|
||||
|
||||
void Log() const
|
||||
{
|
||||
if (to_update.size())
|
||||
{
|
||||
fprintf(log_fp, "Updating:\n");
|
||||
for (const auto& op : to_update)
|
||||
{
|
||||
std::string old_desc =
|
||||
op.old_hash ? HexEncode(op.old_hash->data(), op.old_hash->size()) : "(new)";
|
||||
fprintf(log_fp, " - %s: %s -> %s\n", op.filename.c_str(), old_desc.c_str(),
|
||||
HexEncode(op.new_hash.data(), op.new_hash.size()).c_str());
|
||||
}
|
||||
}
|
||||
if (to_delete.size())
|
||||
{
|
||||
fprintf(log_fp, "Deleting:\n");
|
||||
for (const auto& op : to_delete)
|
||||
{
|
||||
fprintf(log_fp, " - %s (%s)\n", op.filename.c_str(),
|
||||
HexEncode(op.old_hash.data(), op.old_hash.size()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest)
|
||||
{
|
||||
TodoList todo;
|
||||
|
||||
// Delete if present in this manifest but not in next manifest.
|
||||
for (const auto& entry : this_manifest.entries)
|
||||
{
|
||||
if (next_manifest.entries.find(entry.first) == next_manifest.entries.end())
|
||||
{
|
||||
TodoList::DeleteOp del;
|
||||
del.filename = entry.first;
|
||||
del.old_hash = entry.second;
|
||||
todo.to_delete.push_back(std::move(del));
|
||||
}
|
||||
}
|
||||
|
||||
// Download and update if present in next manifest with different hash from this manifest.
|
||||
for (const auto& entry : next_manifest.entries)
|
||||
{
|
||||
std::optional<Manifest::Hash> old_hash;
|
||||
|
||||
const auto& old_entry = this_manifest.entries.find(entry.first);
|
||||
if (old_entry != this_manifest.entries.end())
|
||||
old_hash = old_entry->second;
|
||||
|
||||
if (!old_hash || *old_hash != entry.second)
|
||||
{
|
||||
todo.to_download.push_back(entry.second);
|
||||
|
||||
TodoList::UpdateOp update;
|
||||
update.filename = entry.first;
|
||||
update.old_hash = old_hash;
|
||||
update.new_hash = entry.second;
|
||||
todo.to_update.push_back(std::move(update));
|
||||
}
|
||||
}
|
||||
|
||||
return todo;
|
||||
}
|
||||
|
||||
std::optional<std::string> FindOrCreateTempDir(const std::string& base_path)
|
||||
{
|
||||
std::string temp_path = base_path + DIR_SEP + UPDATE_TEMP_DIR;
|
||||
int counter = 0;
|
||||
|
||||
do
|
||||
{
|
||||
if (!File::Exists(temp_path))
|
||||
{
|
||||
if (File::CreateDir(temp_path))
|
||||
return temp_path;
|
||||
else
|
||||
{
|
||||
fprintf(log_fp, "Couldn't create temp directory.\n");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else if (File::IsDirectory(temp_path))
|
||||
{
|
||||
return temp_path;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try again with a counter appended to the path.
|
||||
std::string suffix = UPDATE_TEMP_DIR + std::to_string(counter);
|
||||
temp_path = base_path + DIR_SEP + suffix;
|
||||
}
|
||||
} while (counter++ < 10);
|
||||
|
||||
fprintf(log_fp, "Could not find an appropriate temp directory name. Giving up.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
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()));
|
||||
File::DeleteDir(temp_dir);
|
||||
}
|
||||
|
||||
Manifest::Hash ComputeHash(const std::string& contents)
|
||||
{
|
||||
std::array<u8, 32> full;
|
||||
mbedtls_sha256(reinterpret_cast<const u8*>(contents.data()), contents.size(), full.data(), false);
|
||||
|
||||
Manifest::Hash out;
|
||||
std::copy(full.begin(), full.begin() + 16, out.begin());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool DownloadContent(const std::vector<Manifest::Hash>& to_download,
|
||||
const std::string& content_base_url, const std::string& temp_path)
|
||||
{
|
||||
Common::HttpRequest req;
|
||||
for (const auto& h : to_download)
|
||||
{
|
||||
std::string hash_filename = HexEncode(h.data(), h.size());
|
||||
|
||||
// Add slashes where needed.
|
||||
std::string content_store_path = hash_filename;
|
||||
content_store_path.insert(4, "/");
|
||||
content_store_path.insert(2, "/");
|
||||
|
||||
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;
|
||||
|
||||
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||
if (!maybe_decompressed)
|
||||
return false;
|
||||
std::string decompressed = std::move(*maybe_decompressed);
|
||||
|
||||
// Check that the downloaded contents have the right hash.
|
||||
Manifest::Hash contents_hash = ComputeHash(decompressed);
|
||||
if (contents_hash != h)
|
||||
{
|
||||
fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string out = temp_path + DIR_SEP + hash_filename;
|
||||
if (!File::WriteStringToFile(decompressed, out))
|
||||
{
|
||||
fprintf(log_fp, "Could not write cache file %s.\n", out.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BackupFile(const std::string& path)
|
||||
{
|
||||
std::string backup_path = path + ".bak";
|
||||
fprintf(log_fp, "Backing up unknown pre-existing %s to .bak.\n", path.c_str());
|
||||
if (!File::Rename(path, backup_path))
|
||||
{
|
||||
fprintf(log_fp, "Cound not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
|
||||
const std::string& install_base_path, const std::string& temp_path)
|
||||
{
|
||||
for (const auto& op : to_update)
|
||||
{
|
||||
std::string path = install_base_path + DIR_SEP + op.filename;
|
||||
if (!File::CreateFullPath(path))
|
||||
{
|
||||
fprintf(log_fp, "Could not create directory structure for %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (File::Exists(path))
|
||||
{
|
||||
std::string contents;
|
||||
if (!File::ReadFileToString(path, contents))
|
||||
{
|
||||
fprintf(log_fp, "Could not read existing file %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
Manifest::Hash contents_hash = ComputeHash(contents);
|
||||
if (contents_hash == op.new_hash)
|
||||
{
|
||||
fprintf(log_fp, "File %s was already up to date. Partial update?\n", op.filename.c_str());
|
||||
continue;
|
||||
}
|
||||
else if (!op.old_hash || contents_hash != *op.old_hash)
|
||||
{
|
||||
if (!BackupFile(path))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can safely move the new contents to the location.
|
||||
std::string content_filename = HexEncode(op.new_hash.data(), op.new_hash.size());
|
||||
fprintf(log_fp, "Updating file %s from content %s...\n", op.filename.c_str(),
|
||||
content_filename.c_str());
|
||||
if (!File::Rename(temp_path + DIR_SEP + content_filename, path))
|
||||
{
|
||||
fprintf(log_fp, "Could not update file %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeleteObsoleteFiles(const std::vector<TodoList::DeleteOp>& to_delete,
|
||||
const std::string& install_base_path)
|
||||
{
|
||||
for (const auto& op : to_delete)
|
||||
{
|
||||
std::string path = install_base_path + DIR_SEP + op.filename;
|
||||
|
||||
if (!File::Exists(path))
|
||||
{
|
||||
fprintf(log_fp, "File %s is already missing.\n", op.filename.c_str());
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string contents;
|
||||
if (!File::ReadFileToString(path, contents))
|
||||
{
|
||||
fprintf(log_fp, "Could not read file planned for deletion: %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
Manifest::Hash contents_hash = ComputeHash(contents);
|
||||
if (contents_hash != op.old_hash)
|
||||
{
|
||||
if (!BackupFile(path))
|
||||
return false;
|
||||
}
|
||||
|
||||
File::Delete(path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PerformUpdate(const TodoList& todo, const std::string& install_base_path,
|
||||
const std::string& content_base_url, const std::string& temp_path)
|
||||
{
|
||||
fprintf(log_fp, "Starting download step...\n");
|
||||
if (!DownloadContent(todo.to_download, content_base_url, temp_path))
|
||||
return false;
|
||||
fprintf(log_fp, "Download step completed.\n");
|
||||
|
||||
fprintf(log_fp, "Starting update step...\n");
|
||||
if (!UpdateFiles(todo.to_update, install_base_path, temp_path))
|
||||
return false;
|
||||
fprintf(log_fp, "Update step completed.\n");
|
||||
|
||||
fprintf(log_fp, "Starting deletion step...\n");
|
||||
if (!DeleteObsoleteFiles(todo.to_delete, install_base_path))
|
||||
return false;
|
||||
fprintf(log_fp, "Deletion step completed.\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
|
||||
{
|
||||
std::optional<Options> maybe_opts = ParseCommandLine(pCmdLine);
|
||||
if (!maybe_opts)
|
||||
return 1;
|
||||
Options opts = std::move(*maybe_opts);
|
||||
|
||||
if (opts.log_file)
|
||||
{
|
||||
log_fp = _wfopen(UTF8ToUTF16(*opts.log_file).c_str(), L"w");
|
||||
if (!log_fp)
|
||||
log_fp = stderr;
|
||||
else
|
||||
atexit(FlushLog);
|
||||
}
|
||||
|
||||
fprintf(log_fp, "Updating from: %s\n", opts.this_manifest_url.c_str());
|
||||
fprintf(log_fp, "Updating to: %s\n", opts.next_manifest_url.c_str());
|
||||
fprintf(log_fp, "Install path: %s\n", opts.install_base_path.c_str());
|
||||
|
||||
if (!File::IsDirectory(opts.install_base_path))
|
||||
{
|
||||
fprintf(log_fp, "Cannot find install base path, or not a directory.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts.parent_pid)
|
||||
{
|
||||
fprintf(log_fp, "Waiting for parent PID %d to complete...\n", *opts.parent_pid);
|
||||
HANDLE parent_handle = OpenProcess(SYNCHRONIZE, FALSE, *opts.parent_pid);
|
||||
WaitForSingleObject(parent_handle, INFINITE);
|
||||
CloseHandle(parent_handle);
|
||||
fprintf(log_fp, "Completed! Proceeding with update.\n");
|
||||
}
|
||||
|
||||
Manifest this_manifest, next_manifest;
|
||||
{
|
||||
std::optional<Manifest> maybe_manifest = FetchAndParseManifest(opts.this_manifest_url);
|
||||
if (!maybe_manifest)
|
||||
{
|
||||
fprintf(log_fp, "Could not fetch current manifest. Aborting.\n");
|
||||
return 1;
|
||||
}
|
||||
this_manifest = std::move(*maybe_manifest);
|
||||
|
||||
maybe_manifest = FetchAndParseManifest(opts.next_manifest_url);
|
||||
if (!maybe_manifest)
|
||||
{
|
||||
fprintf(log_fp, "Could not fetch next manifest. Aborting.\n");
|
||||
return 1;
|
||||
}
|
||||
next_manifest = std::move(*maybe_manifest);
|
||||
}
|
||||
|
||||
TodoList todo = ComputeActionsToDo(this_manifest, next_manifest);
|
||||
todo.Log();
|
||||
|
||||
std::optional<std::string> maybe_temp_dir = FindOrCreateTempDir(opts.install_base_path);
|
||||
if (!maybe_temp_dir)
|
||||
return 1;
|
||||
std::string temp_dir = std::move(*maybe_temp_dir);
|
||||
|
||||
bool ok = PerformUpdate(todo, opts.install_base_path, opts.content_store_url, temp_dir);
|
||||
if (!ok)
|
||||
fprintf(log_fp, "Failed to apply the update.\n");
|
||||
|
||||
CleanUpTempDir(temp_dir, todo);
|
||||
return !ok;
|
||||
}
|
78
Source/Core/Updater/Updater.vcxproj
Normal file
78
Source/Core/Updater/Updater.vcxproj
Normal file
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{E4BECBAB-9C6E-41AB-BB56-F9D70AB6BE03}</ProjectGuid>
|
||||
<WindowsTargetPlatformVersion>10.0.15063.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="..\..\VSProps\Base.props" />
|
||||
<Import Project="..\..\VSProps\PCHUse.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup />
|
||||
<ItemDefinitionGroup>
|
||||
<Link>
|
||||
<AdditionalDependencies>iphlpapi.lib;winmm.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Externals\cpp-optparse\cpp-optparse.vcxproj">
|
||||
<Project>{c636d9d1-82fe-42b5-9987-63b7d4836341}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\Externals\curl\curl.vcxproj">
|
||||
<Project>{bb00605c-125f-4a21-b33b-7bf418322dcb}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\externals\ed25519\ed25519.vcxproj">
|
||||
<Project>{5bdf4b91-1491-4fb0-bc27-78e9a8e97dc3}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\Externals\mbedtls\mbedTLS.vcxproj">
|
||||
<Project>{bdb6578b-0691-4e80-a46c-df21639fd3b8}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\Externals\zlib\zlib.vcxproj">
|
||||
<Project>{ff213b23-2c26-4214-9f88-85271e557e87}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Common\Common.vcxproj">
|
||||
<Project>{2e6c348c-c75c-4d94-8d1e-9c1fcbf3efe4}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Main.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
<!--Copy the .exe to binary output folder-->
|
||||
<ItemGroup>
|
||||
<SourceFiles Include="$(TargetPath)" />
|
||||
</ItemGroup>
|
||||
<Target Name="AfterBuild" Inputs="@(SourceFiles)" Outputs="@(SourceFiles -> '$(BinaryOutputDir)%(Filename)%(Extension)')">
|
||||
<Message Text="Copy: @(SourceFiles) -> $(BinaryOutputDir)" Importance="High" />
|
||||
<Copy SourceFiles="@(SourceFiles)" DestinationFolder="$(BinaryOutputDir)" />
|
||||
</Target>
|
||||
</Project>
|
6
Source/Core/Updater/Updater.vcxproj.filters
Normal file
6
Source/Core/Updater/Updater.vcxproj.filters
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Main.cpp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
Reference in New Issue
Block a user