diff --git a/Source/Core/DolphinQt/Updater.cpp b/Source/Core/DolphinQt/Updater.cpp index 0827c0c10476..47eb57ef39a9 100644 --- a/Source/Core/DolphinQt/Updater.cpp +++ b/Source/Core/DolphinQt/Updater.cpp @@ -3,6 +3,7 @@ #include "DolphinQt/Updater.h" +#include #include #include @@ -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 choice = RunOnObject(m_parent, [&] { diff --git a/Source/Core/MacUpdater/MacUI.mm b/Source/Core/MacUpdater/MacUI.mm index 388f5a1da419..f60199034a45 100644 --- a/Source/Core/MacUpdater/MacUI.mm +++ b/Source/Core/MacUpdater/MacUI.mm @@ -140,8 +140,7 @@ void run_on_main(std::function fnc) } bool Platform::VersionCheck(const std::vector& 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 @@ void run_on_main(std::function fnc) 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 @@ void run_on_main(std::function fnc) 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 @@ void run_on_main(std::function fnc) 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]) { diff --git a/Source/Core/UICommon/AutoUpdate.cpp b/Source/Core/UICommon/AutoUpdate.cpp index b67c499c147b..a638ae33a614 100644 --- a/Source/Core/UICommon/AutoUpdate.cpp +++ b/Source/Core/UICommon/AutoUpdate.cpp @@ -3,6 +3,7 @@ #include "UICommon/AutoUpdate.h" +#include #include #include @@ -19,12 +20,13 @@ #ifdef _WIN32 #include +#else +#include +#include #endif #ifdef __APPLE__ #include -#include -#include #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()); - 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; diff --git a/Source/Core/UpdaterCommon/Platform.h b/Source/Core/UpdaterCommon/Platform.h index 546ceff98b78..e2580ec694c1 100644 --- a/Source/Core/UpdaterCommon/Platform.h +++ b/Source/Core/UpdaterCommon/Platform.h @@ -15,5 +15,5 @@ namespace Platform { bool VersionCheck(const std::vector& 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 diff --git a/Source/Core/UpdaterCommon/UI.h b/Source/Core/UpdaterCommon/UI.h index 86b5aa701305..3b441ea5684b 100644 --- a/Source/Core/UpdaterCommon/UI.h +++ b/Source/Core/UpdaterCommon/UI.h @@ -29,4 +29,6 @@ void Init(); void Sleep(int seconds); void WaitForPID(u32 pid); void LaunchApplication(std::string path); + +bool IsTestMode(); } // namespace UI diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.cpp b/Source/Core/UpdaterCommon/UpdaterCommon.cpp index f16354421da7..426943ce83ad 100644 --- a/Source/Core/UpdaterCommon/UpdaterCommon.cpp +++ b/Source/Core/UpdaterCommon/UpdaterCommon.cpp @@ -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 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 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 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(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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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 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 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 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 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 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 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 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(pid)); - fprintf(log_fp, "Completed! Proceeding with update.\n"); + LogToFile("Completed! Proceeding with update.\n"); } UI::SetVisible(true); diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.h b/Source/Core/UpdaterCommon/UpdaterCommon.h index 679a339f90b3..a636a0c32391 100644 --- a/Source/Core/UpdaterCommon/UpdaterCommon.h +++ b/Source/Core/UpdaterCommon/UpdaterCommon.h @@ -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 args); diff --git a/Source/Core/WinUpdater/Platform.cpp b/Source/Core/WinUpdater/Platform.cpp index f60a6621c026..bc8ccb843ef1 100644 --- a/Source/Core/WinUpdater/Platform.cpp +++ b/Source/Core/WinUpdater/Platform.cpp @@ -103,7 +103,10 @@ class BuildInfo 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 InitBuildInfos(const std::vector& 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 InitBuildInfos(const std::vector& 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 InitBuildInfos(const std::vector& 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& 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()) { diff --git a/Source/Core/WinUpdater/WinUI.cpp b/Source/Core/WinUpdater/WinUI.cpp index 1671f43457a2..8e3e7c375d38 100644 --- a/Source/Core/WinUpdater/WinUI.cpp +++ b/Source/Core/WinUpdater/WinUI.cpp @@ -3,12 +3,14 @@ #include "UpdaterCommon/UI.h" +#include #include #include #include #include #include +#include #include #include @@ -251,11 +253,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) diff --git a/Tools/test-updater.py b/Tools/test-updater.py new file mode 100644 index 000000000000..4cb5d07d4658 --- /dev/null +++ b/Tools/test-updater.py @@ -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)