Merge pull request #11636 from shuffle2/updater-test

Add test for Updater
This commit is contained in:
Pierre Bourdon 2023-03-13 15:47:37 +01:00 committed by GitHub
commit a6b2655631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 371 additions and 86 deletions

View File

@ -3,6 +3,7 @@
#include "DolphinQt/Updater.h"
#include <cstdlib>
#include <utility>
#include <QCheckBox>
@ -41,6 +42,16 @@ void Updater::CheckForUpdate()
void Updater::OnUpdateAvailable(const NewVersionInformation& info)
{
if (std::getenv("DOLPHIN_UPDATE_SERVER_URL"))
{
TriggerUpdate(info, AutoUpdateChecker::RestartMode::RESTART_AFTER_UPDATE);
RunOnObject(m_parent, [this] {
m_parent->close();
return 0;
});
return;
}
bool later = false;
std::optional<int> choice = RunOnObject(m_parent, [&] {

View File

@ -140,8 +140,7 @@ void UI::Init()
}
bool Platform::VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
const std::string& install_base_path, const std::string& temp_dir,
FILE* log_fp)
const std::string& install_base_path, const std::string& temp_dir)
{
const auto op_it = std::find_if(to_update.cbegin(), to_update.cend(), [&](const auto& op) {
return op.filename == "Dolphin.app/Contents/Info.plist";
@ -155,7 +154,7 @@ bool Platform::VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
NSData* data = [NSData dataWithContentsOfFile:[NSString stringWithCString:plist_path.c_str()]];
if (!data)
{
fprintf(log_fp, "Failed to read %s, skipping platform version check.\n", plist_path.c_str());
LogToFile("Failed to read %s, skipping platform version check.\n", plist_path.c_str());
return true;
}
@ -167,13 +166,13 @@ bool Platform::VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
error:&error];
if (error)
{
fprintf(log_fp, "Failed to parse %s, skipping platform version check.\n", plist_path.c_str());
LogToFile("Failed to parse %s, skipping platform version check.\n", plist_path.c_str());
return true;
}
NSString* min_version_str = info_dict[@"LSMinimumSystemVersion"];
if (!min_version_str)
{
fprintf(log_fp, "LSMinimumSystemVersion key missing, skipping platform version check.\n");
LogToFile("LSMinimumSystemVersion key missing, skipping platform version check.\n");
return true;
}
@ -181,9 +180,8 @@ bool Platform::VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
NSOperatingSystemVersion next_version{
[components[0] integerValue], [components[1] integerValue], [components[2] integerValue]};
fprintf(log_fp, "Platform version check: next_version=%ld.%ld.%ld\n",
(long)next_version.majorVersion, (long)next_version.minorVersion,
(long)next_version.patchVersion);
LogToFile("Platform version check: next_version=%ld.%ld.%ld\n", (long)next_version.majorVersion,
(long)next_version.minorVersion, (long)next_version.patchVersion);
if (![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:next_version])
{

View File

@ -3,6 +3,7 @@
#include "UICommon/AutoUpdate.h"
#include <cstdlib>
#include <string>
#include <fmt/format.h>
@ -19,12 +20,13 @@
#ifdef _WIN32
#include <Windows.h>
#else
#include <sys/types.h>
#include <unistd.h>
#endif
#ifdef __APPLE__
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#if defined(_WIN32) || defined(__APPLE__)
@ -160,6 +162,23 @@ static std::string GetPlatformID()
#endif
}
static std::string GetUpdateServerUrl()
{
auto server_url = std::getenv("DOLPHIN_UPDATE_SERVER_URL");
if (server_url)
return server_url;
return "https://dolphin-emu.org";
}
static u32 GetOwnProcessId()
{
#ifdef _WIN32
return GetCurrentProcessId();
#else
return getpid();
#endif
}
void AutoUpdateChecker::CheckForUpdate(std::string_view update_track,
std::string_view hash_override, const CheckType check_type)
{
@ -172,7 +191,7 @@ void AutoUpdateChecker::CheckForUpdate(std::string_view update_track,
#endif
std::string_view version_hash = hash_override.empty() ? Common::GetScmRevGitStr() : hash_override;
std::string url = fmt::format("https://dolphin-emu.org/update/check/v1/{}/{}/{}", update_track,
std::string url = fmt::format("{}/update/check/v1/{}/{}/{}", GetUpdateServerUrl(), update_track,
version_hash, GetPlatformID());
const bool is_manual_check = check_type == CheckType::Manual;
@ -215,7 +234,15 @@ void AutoUpdateChecker::CheckForUpdate(std::string_view update_track,
// TODO: generate the HTML changelog from the JSON information.
nvi.changelog_html = GenerateChangelog(obj["changelog"].get<picojson::array>());
OnUpdateAvailable(nvi);
if (std::getenv("DOLPHIN_UPDATE_TEST_DONE"))
{
// We are at end of updater test flow, send a message to server, which will kill us.
req.Get(fmt::format("{}/update-test-done/{}", GetUpdateServerUrl(), GetOwnProcessId()));
}
else
{
OnUpdateAvailable(nvi);
}
}
void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInformation& info,
@ -234,11 +261,7 @@ void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInforma
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;
#ifdef _WIN32
updater_flags["parent-pid"] = std::to_string(GetCurrentProcessId());
#else
updater_flags["parent-pid"] = std::to_string(getpid());
#endif
updater_flags["parent-pid"] = std::to_string(GetOwnProcessId());
updater_flags["install-base-path"] = File::GetExeDirectory();
updater_flags["log-file"] = File::GetUserPath(D_LOGS_IDX) + UPDATER_LOG_FILE;

View File

@ -15,5 +15,5 @@
namespace Platform
{
bool VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
const std::string& install_base_path, const std::string& temp_dir, FILE* log_fp);
const std::string& install_base_path, const std::string& temp_dir);
} // namespace Platform

View File

@ -29,4 +29,6 @@ void Init();
void Sleep(int seconds);
void WaitForPID(u32 pid);
void LaunchApplication(std::string path);
bool IsTestMode();
} // namespace UI

View File

@ -34,13 +34,28 @@
// Refer to docs/autoupdate_overview.md for a detailed overview of the autoupdate process
// Where to log updater output.
static FILE* log_fp = stderr;
// Public key used to verify update manifests.
const std::array<u8, 32> 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};
// The private key for UPDATE_PUB_KEY_TEST is in Tools/test-updater.py
const std::array<u8, 32> UPDATE_PUB_KEY_TEST = {
0x0c, 0x5f, 0xdc, 0xd1, 0x15, 0x71, 0xfb, 0x86, 0x4f, 0x9e, 0x6d, 0xe6, 0x65, 0x39, 0x43, 0xe1,
0x9e, 0xe0, 0x9b, 0x28, 0xc9, 0x1a, 0x60, 0xb7, 0x67, 0x1c, 0xf3, 0xf6, 0xca, 0x1b, 0xdd, 0x1a};
// Where to log updater output.
static FILE* log_fp = stderr;
void LogToFile(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
vfprintf(log_fp, fmt, args);
fflush(log_fp);
va_end(args);
}
bool ProgressCallback(double total, double now, double, double)
{
@ -120,7 +135,7 @@ std::optional<std::string> GzipInflate(const std::string& data)
if (ret != Z_STREAM_END)
{
fprintf(log_fp, "Could not read the data as gzip: error %d.\n", ret);
LogToFile("Could not read the data as gzip: error %d.\n", ret);
return {};
}
@ -148,12 +163,13 @@ bool VerifySignature(const std::string& data, const std::string& b64_signature)
b64_signature.size()) ||
sig_size != sizeof(signature))
{
fprintf(log_fp, "Invalid base64: %s\n", b64_signature.c_str());
LogToFile("Invalid base64: %s\n", b64_signature.c_str());
return false;
}
const auto& pub_key = UI::IsTestMode() ? UPDATE_PUB_KEY_TEST : UPDATE_PUB_KEY;
return ed25519_verify(signature, reinterpret_cast<const u8*>(data.data()), data.size(),
UPDATE_PUB_KEY.data());
pub_key.data());
}
void FlushLog()
@ -166,22 +182,22 @@ void TodoList::Log() const
{
if (to_update.size())
{
fprintf(log_fp, "Updating:\n");
LogToFile("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());
LogToFile(" - %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");
LogToFile("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());
LogToFile(" - %s (%s)\n", op.filename.c_str(),
HexEncode(op.old_hash.data(), op.old_hash.size()).c_str());
}
}
}
@ -215,7 +231,7 @@ bool DownloadContent(const std::vector<TodoList::DownloadOp>& to_download,
content_store_path.insert(2, "/");
std::string url = content_base_url + content_store_path;
fprintf(log_fp, "Downloading %s ...\n", url.c_str());
LogToFile("Downloading %s ...\n", url.c_str());
auto resp = req.Get(url);
if (!resp)
@ -234,14 +250,14 @@ bool DownloadContent(const std::vector<TodoList::DownloadOp>& to_download,
Manifest::Hash contents_hash = ComputeHash(decompressed);
if (contents_hash != download.hash)
{
fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str());
LogToFile("Wrong hash on downloaded content %s.\n", url.c_str());
return false;
}
const std::string out = temp_path + DIR_SEP + hash_filename;
if (!File::WriteStringToFile(out, decompressed))
{
fprintf(log_fp, "Could not write cache file %s.\n", out.c_str());
LogToFile("Could not write cache file %s.\n", out.c_str());
return false;
}
}
@ -252,7 +268,7 @@ bool PlatformVersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
const std::string& install_base_path, const std::string& temp_dir)
{
UI::SetDescription("Checking platform...");
return Platform::VersionCheck(to_update, install_base_path, temp_dir, log_fp);
return Platform::VersionCheck(to_update, install_base_path, temp_dir);
}
TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest)
@ -310,10 +326,10 @@ void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo)
bool BackupFile(const std::string& path)
{
std::string backup_path = path + ".bak";
fprintf(log_fp, "Backing up existing %s to .bak.\n", path.c_str());
LogToFile("Backing up 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());
LogToFile("Cound not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str());
return false;
}
return true;
@ -328,7 +344,7 @@ bool DeleteObsoleteFiles(const std::vector<TodoList::DeleteOp>& to_delete,
if (!File::Exists(path))
{
fprintf(log_fp, "File %s is already missing.\n", op.filename.c_str());
LogToFile("File %s is already missing.\n", op.filename.c_str());
continue;
}
else
@ -336,7 +352,7 @@ bool DeleteObsoleteFiles(const std::vector<TodoList::DeleteOp>& to_delete,
std::string contents;
if (!File::ReadFileToString(path, contents))
{
fprintf(log_fp, "Could not read file planned for deletion: %s.\n", op.filename.c_str());
LogToFile("Could not read file planned for deletion: %s.\n", op.filename.c_str());
return false;
}
Manifest::Hash contents_hash = ComputeHash(contents);
@ -365,7 +381,7 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& 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());
LogToFile("Could not create directory structure for %s.\n", op.filename.c_str());
return false;
}
@ -385,7 +401,7 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
if (S_ISLNK(file_stats.st_mode))
{
fprintf(log_fp, "%s is symlink, skipping\n", path.c_str());
LogToFile("%s is symlink, skipping\n", path.c_str());
continue;
}
@ -408,13 +424,13 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
std::string contents;
if (!File::ReadFileToString(path, contents))
{
fprintf(log_fp, "Could not read existing file %s.\n", op.filename.c_str());
LogToFile("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());
LogToFile("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 || is_self)
@ -426,8 +442,8 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
// 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());
LogToFile("Updating file %s from content %s...\n", op.filename.c_str(),
content_filename.c_str());
#ifdef __APPLE__
// macOS caches the code signature of Mach-O executables when they're first loaded.
// Unfortunately, there is a quirk in the kernel with how it handles the cache: if the file is
@ -440,8 +456,7 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
const std::string temporary_file = temp_path + DIR_SEP + "temporary_file";
if (!File::CopyRegularFile(temp_path + DIR_SEP + content_filename, temporary_file))
{
fprintf(log_fp, "Could not copy %s to %s.\n", content_filename.c_str(),
temporary_file.c_str());
LogToFile("Could not copy %s to %s.\n", content_filename.c_str(), temporary_file.c_str());
return false;
}
@ -450,7 +465,7 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
if (!File::CopyRegularFile(temp_path + DIR_SEP + content_filename, path))
#endif
{
fprintf(log_fp, "Could not update file %s.\n", op.filename.c_str());
LogToFile("Could not update file %s.\n", op.filename.c_str());
return false;
}
@ -465,32 +480,32 @@ bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
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");
LogToFile("Starting download step...\n");
if (!DownloadContent(todo.to_download, content_base_url, temp_path))
return false;
fprintf(log_fp, "Download step completed.\n");
LogToFile("Download step completed.\n");
fprintf(log_fp, "Starting platform version check step...\n");
LogToFile("Starting platform version check step...\n");
if (!PlatformVersionCheck(todo.to_update, install_base_path, temp_path))
return false;
fprintf(log_fp, "Platform version check step completed.\n");
LogToFile("Platform version check step completed.\n");
fprintf(log_fp, "Starting update step...\n");
LogToFile("Starting update step...\n");
if (!UpdateFiles(todo.to_update, install_base_path, temp_path))
return false;
fprintf(log_fp, "Update step completed.\n");
LogToFile("Update step completed.\n");
fprintf(log_fp, "Starting deletion step...\n");
LogToFile("Starting deletion step...\n");
if (!DeleteObsoleteFiles(todo.to_delete, install_base_path))
return false;
fprintf(log_fp, "Deletion step completed.\n");
LogToFile("Deletion step completed.\n");
return true;
}
void FatalError(const std::string& message)
{
fprintf(log_fp, "%s\n", message.c_str());
LogToFile("%s\n", message.c_str());
UI::SetVisible(true);
UI::Error(message);
@ -506,13 +521,13 @@ std::optional<Manifest> ParseManifest(const std::string& manifest)
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());
LogToFile("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());
LogToFile("Manifest entry %zu: could not find hash end.\n", parsed.entries.size());
return {};
}
@ -520,16 +535,14 @@ std::optional<Manifest> ParseManifest(const std::string& manifest)
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());
LogToFile("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());
LogToFile("Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), hash.c_str());
return {};
}
@ -548,7 +561,7 @@ std::optional<Manifest> FetchAndParseManifest(const std::string& url)
Common::HttpRequest::Response resp = http.Get(url);
if (!resp)
{
fprintf(log_fp, "Manifest download failed.\n");
LogToFile("Manifest download failed.\n");
return {};
}
@ -562,7 +575,7 @@ std::optional<Manifest> FetchAndParseManifest(const std::string& url)
size_t boundary = decompressed.rfind("\n\n");
if (boundary == std::string::npos)
{
fprintf(log_fp, "No signature was found in manifest.\n");
LogToFile("No signature was found in manifest.\n");
return {};
}
@ -581,7 +594,7 @@ std::optional<Manifest> FetchAndParseManifest(const std::string& url)
}
if (!found_valid_signature)
{
fprintf(log_fp, "Could not verify signature of the manifest.\n");
LogToFile("Could not verify signature of the manifest.\n");
return {};
}
@ -692,9 +705,9 @@ bool RunUpdater(std::vector<std::string> args)
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());
LogToFile("Updating from: %s\n", opts.this_manifest_url.c_str());
LogToFile("Updating to: %s\n", opts.next_manifest_url.c_str());
LogToFile("Install path: %s\n", opts.install_base_path.c_str());
if (!File::IsDirectory(opts.install_base_path))
{
@ -704,13 +717,13 @@ bool RunUpdater(std::vector<std::string> args)
if (opts.parent_pid)
{
fprintf(log_fp, "Waiting for parent PID %d to complete...\n", *opts.parent_pid);
LogToFile("Waiting for parent PID %d to complete...\n", *opts.parent_pid);
auto pid = opts.parent_pid.value();
UI::WaitForPID(static_cast<u32>(pid));
fprintf(log_fp, "Completed! Proceeding with update.\n");
LogToFile("Completed! Proceeding with update.\n");
}
UI::SetVisible(true);

View File

@ -49,6 +49,7 @@ struct TodoList
void Log() const;
};
void LogToFile(const char* fmt, ...);
std::string HexEncode(const u8* buffer, size_t size);
Manifest::Hash ComputeHash(const std::string& contents);
bool RunUpdater(std::vector<std::string> args);

View File

@ -103,7 +103,10 @@ private:
auto key_it = map.find(key);
if (key_it == map.end())
continue;
key_it->second = line.substr(equals_index + 1);
auto val_start = equals_index + 1;
auto eol = line.find('\r', val_start);
auto val_size = (eol == line.npos) ? line.npos : eol - val_start;
key_it->second = line.substr(val_start, val_size);
}
}
Map map;
@ -194,9 +197,12 @@ static bool VCRuntimeUpdate(const BuildInfo& build_info)
Common::ScopeGuard redist_deleter([&] { File::Delete(redist_path_u8); });
// The installer also supports /passive and /quiet. We pass neither to allow the user to see and
// interact with the installer.
// The installer also supports /passive and /quiet. We normally pass neither (the
// exception being test automation) to allow the user to see and interact with the installer.
std::wstring cmdline = redist_path.filename().wstring() + L" /install /norestart";
if (UI::IsTestMode())
cmdline += L" /passive /quiet";
STARTUPINFOW startup_info{.cb = sizeof(startup_info)};
PROCESS_INFORMATION process_info;
if (!CreateProcessW(redist_path.c_str(), cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr,
@ -213,7 +219,8 @@ static bool VCRuntimeUpdate(const BuildInfo& build_info)
CloseHandle(process_info.hProcess);
// NOTE: Some nonzero exit codes can still be considered success (e.g. if installation was
// bypassed because the same version already installed).
return has_exit_code && exit_code == EXIT_SUCCESS;
return has_exit_code &&
(exit_code == ERROR_SUCCESS || exit_code == ERROR_SUCCESS_REBOOT_REQUIRED);
}
static BuildVersion CurrentOSVersion()
@ -241,7 +248,7 @@ static VersionCheckResult OSVersionCheck(const BuildInfo& build_info)
std::optional<BuildInfos> InitBuildInfos(const std::vector<TodoList::UpdateOp>& to_update,
const std::string& install_base_path,
const std::string& temp_dir, FILE* log_fp)
const std::string& temp_dir)
{
const auto op_it = std::find_if(to_update.cbegin(), to_update.cend(),
[&](const auto& op) { return op.filename == "build_info.txt"; });
@ -255,7 +262,7 @@ std::optional<BuildInfos> InitBuildInfos(const std::vector<TodoList::UpdateOp>&
if (!File::ReadFileToString(build_info_path, build_info_content) ||
op.new_hash != ComputeHash(build_info_content))
{
fprintf(log_fp, "Failed to read %s\n.", build_info_path.c_str());
LogToFile("Failed to read %s\n.", build_info_path.c_str());
return {};
}
BuildInfos build_infos;
@ -266,7 +273,7 @@ std::optional<BuildInfos> InitBuildInfos(const std::vector<TodoList::UpdateOp>&
if (File::ReadFileToString(build_info_path, build_info_content))
{
if (op.old_hash != ComputeHash(build_info_content))
fprintf(log_fp, "Using modified existing BuildInfo %s.\n", build_info_path.c_str());
LogToFile("Using modified existing BuildInfo %s.\n", build_info_path.c_str());
build_infos.current = Platform::BuildInfo(build_info_content);
}
return build_infos;
@ -287,11 +294,16 @@ bool CheckBuildInfo(const BuildInfos& build_infos)
// Check if application being launched needs more recent version of VC Redist. If so, download
// latest updater and execute it.
auto vc_check = VCRuntimeVersionCheck(build_infos);
if (vc_check.status != VersionCheckStatus::NothingToDo)
const auto is_test_mode = UI::IsTestMode();
if (vc_check.status != VersionCheckStatus::NothingToDo || is_test_mode)
{
// Don't bother checking status of the install itself, just check if we actually see the new
// version.
VCRuntimeUpdate(build_infos.next);
auto update_ok = VCRuntimeUpdate(build_infos.next);
if (!update_ok && is_test_mode)
{
// For now, only check return value when test automation is running.
// The vc_redist exe may return other non-zero status that we don't check for, yet.
return false;
}
vc_check = VCRuntimeVersionCheck(build_infos);
if (vc_check.status == VersionCheckStatus::UpdateRequired)
{
@ -305,9 +317,9 @@ bool CheckBuildInfo(const BuildInfos& build_infos)
}
bool VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
const std::string& install_base_path, const std::string& temp_dir, FILE* log_fp)
const std::string& install_base_path, const std::string& temp_dir)
{
auto build_infos = InitBuildInfos(to_update, install_base_path, temp_dir, log_fp);
auto build_infos = InitBuildInfos(to_update, install_base_path, temp_dir);
// If there's no build info, it means the check should be skipped.
if (!build_infos.has_value())
{

View File

@ -3,12 +3,14 @@
#include "UpdaterCommon/UI.h"
#include <cstdlib>
#include <string>
#include <thread>
#include <Windows.h>
#include <CommCtrl.h>
#include <ShObjIdl.h>
#include <ShlObj.h>
#include <shellapi.h>
#include <wrl/client.h>
@ -253,11 +255,34 @@ void Stop()
ui_thread.join();
}
bool IsTestMode()
{
return std::getenv("DOLPHIN_UPDATE_SERVER_URL") != nullptr;
}
void LaunchApplication(std::string path)
{
// Indirectly start the application via explorer. This effectively drops admin priviliges because
// explorer is running as current user.
ShellExecuteW(nullptr, nullptr, L"explorer.exe", UTF8ToWString(path).c_str(), nullptr, SW_SHOW);
const auto wpath = UTF8ToWString(path);
if (IsUserAnAdmin())
{
// Indirectly start the application via explorer. This effectively drops admin privileges
// because explorer is running as current user.
ShellExecuteW(nullptr, nullptr, L"explorer.exe", wpath.c_str(), nullptr, SW_SHOW);
}
else
{
std::wstring cmdline = wpath;
STARTUPINFOW startup_info{.cb = sizeof(startup_info)};
PROCESS_INFORMATION process_info;
if (IsTestMode())
SetEnvironmentVariableA("DOLPHIN_UPDATE_TEST_DONE", "1");
if (CreateProcessW(wpath.c_str(), cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr,
&startup_info, &process_info))
{
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
}
}
}
void Sleep(int sleep)

200
Tools/test-updater.py Normal file
View File

@ -0,0 +1,200 @@
#!/usr/bin/env python3
# requirements: pycryptodome
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from hashlib import sha256
from pathlib import Path
import base64
import configparser
import gzip
import http.server
import json
import os
import shutil
import socketserver
import subprocess
import sys
import tempfile
import threading
import time
UPDATE_KEY_TEST = ECC.construct(
curve="Ed25519",
seed=bytes.fromhex(
"543a581db60008bbb978a464e136d686dbc9d594119e928b5276bece3d583d81"
),
)
HTTP_SERVER_ADDR = ("localhost", 8042)
DOLPHIN_UPDATE_SERVER_URL = f"http://{HTTP_SERVER_ADDR[0]}:{HTTP_SERVER_ADDR[1]}"
class Manifest:
def __init__(self, path: Path):
self.path = path
self.entries = {}
for p in self.path.glob("**/*.*"):
if not p.is_file():
continue
digest = sha256(p.read_bytes()).digest()[:0x10].hex()
self.entries[digest] = p.relative_to(self.path).as_posix()
def get_signed(self):
manifest = "".join(
f"{name}\t{digest}\n" for digest, name in self.entries.items()
)
manifest = manifest.encode("utf-8")
sig = eddsa.new(UPDATE_KEY_TEST, "rfc8032").sign(manifest)
manifest += b"\n" + base64.b64encode(sig) + b"\n"
return gzip.compress(manifest)
def get_path(self, digest):
return self.path.joinpath(self.entries.get(digest))
class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path.startswith("/update/check/v1/updater-test"):
self.send_response(200)
self.end_headers()
self.wfile.write(
bytes(
json.dumps(
{
"status": "outdated",
"content-store": DOLPHIN_UPDATE_SERVER_URL + "/content/",
"changelog": [],
"old": {"manifest": DOLPHIN_UPDATE_SERVER_URL + "/old"},
"new": {
"manifest": DOLPHIN_UPDATE_SERVER_URL + "/new",
"name": "updater-test",
"hash": bytes(range(32)).hex(),
},
}
),
"utf-8",
)
)
elif self.path == "/old":
self.send_response(200)
self.end_headers()
self.wfile.write(self.current.get_signed())
elif self.path == "/new":
self.send_response(200)
self.end_headers()
self.wfile.write(self.next.get_signed())
elif self.path.startswith("/content/"):
self.send_response(200)
self.end_headers()
digest = "".join(self.path[len("/content/") :].split("/"))
path = self.next.get_path(digest)
self.wfile.write(gzip.compress(path.read_bytes()))
elif self.path.startswith("/update-test-done/"):
self.send_response(200)
self.end_headers()
HTTPRequestHandler.dolphin_pid = int(self.path[len("/update-test-done/") :])
self.done.set()
def http_server():
with socketserver.TCPServer(HTTP_SERVER_ADDR, HTTPRequestHandler) as httpd:
httpd.serve_forever()
def create_entries_in_ini(ini_path: Path, entries: dict):
config = configparser.ConfigParser()
if ini_path.exists():
config.read(ini_path)
else:
ini_path.parent.mkdir(parents=True, exist_ok=True)
for section, options in entries.items():
if not config.has_section(section):
config.add_section(section)
for option, value in options.items():
config.set(section, option, value)
with ini_path.open("w") as f:
config.write(f)
if __name__ == "__main__":
dolphin_bin_path = Path(sys.argv[1])
threading.Thread(target=http_server, daemon=True).start()
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_dir = Path(tmp_dir)
tmp_dolphin = tmp_dir.joinpath("dolphin")
print(f"install to {tmp_dolphin}")
shutil.copytree(dolphin_bin_path.parent, tmp_dolphin)
tmp_dolphin.joinpath("portable.txt").touch()
create_entries_in_ini(
tmp_dolphin.joinpath("User/Config/Dolphin.ini"),
{
"Analytics": {"Enabled": "False", "PermissionAsked": "True"},
"AutoUpdate": {"UpdateTrack": "updater-test"},
},
)
tmp_dolphin_next = tmp_dir.joinpath("dolphin_next")
print(f"install next to {tmp_dolphin_next}")
# XXX copies from just-created dir so Dolphin.ini is kept
shutil.copytree(tmp_dolphin, tmp_dolphin_next)
tmp_dolphin_next.joinpath("updater-test-file").write_text("test")
with tmp_dolphin_next.joinpath("build_info.txt").open("a") as f:
print("test", file=f)
for ext in ("exe", "dll"):
for path in tmp_dolphin_next.glob("**/*." + ext):
data = bytearray(path.read_bytes())
richpos = data[:0x200].find(b"Rich")
if richpos < 0:
continue
data[richpos : richpos + 4] = b"DOLP"
path.write_bytes(data)
HTTPRequestHandler.current = Manifest(tmp_dolphin)
HTTPRequestHandler.next = Manifest(tmp_dolphin_next)
HTTPRequestHandler.done = threading.Event()
tmp_env = os.environ
tmp_env.update({"DOLPHIN_UPDATE_SERVER_URL": DOLPHIN_UPDATE_SERVER_URL})
tmp_dolphin_bin = tmp_dolphin.joinpath(dolphin_bin_path.name)
result = subprocess.run(tmp_dolphin_bin, env=tmp_env)
assert result.returncode == 0
assert HTTPRequestHandler.done.wait(60 * 2)
# works fine but raises exceptions...
try:
os.kill(HTTPRequestHandler.dolphin_pid, 0)
except:
pass
try:
os.waitpid(HTTPRequestHandler.dolphin_pid, 0)
except:
pass
failed = False
for path in tmp_dolphin_next.glob("**/*.*"):
if not path.is_file():
continue
path_rel = path.relative_to(tmp_dolphin_next)
if path_rel.parts[0] == "User":
continue
new_path = tmp_dolphin.joinpath(path_rel)
if not new_path.exists():
print(f"missing: {new_path}")
failed = True
continue
if (
sha256(new_path.read_bytes()).digest()
!= sha256(path.read_bytes()).digest()
):
print(f"bad digest: {new_path} {path}")
failed = True
continue
assert not failed
print(tmp_dolphin.joinpath("User/Logs/Updater.log").read_text())
# while True: time.sleep(1)