updater: add test for update flow

currently windows-only
This commit is contained in:
Shawn Hoffman
2023-03-09 18:23:12 -08:00
parent de0bc06856
commit 0a8725e4a9
7 changed files with 295 additions and 20 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

@ -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

@ -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

@ -38,6 +38,10 @@
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;
@ -163,8 +167,9 @@ bool VerifySignature(const std::string& data, const std::string& b64_signature)
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()

View File

@ -194,9 +194,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 +216,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()
@ -287,11 +291,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)
{

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>
@ -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)