ui: Add auto-updater feature for Windows

This commit is contained in:
Matt Borgerson 2021-06-10 09:03:01 -07:00 committed by mborgerson
parent 23766591c4
commit 90ddc5cce3
6 changed files with 468 additions and 3 deletions

View File

@ -37,6 +37,7 @@ xemu_ss.add(files(
'xemu-hud.cc',
'xemu-reporting.cc',
))
xemu_ss.add(when: 'CONFIG_WIN32', if_true: files('xemu-update.cc'))
imgui_flags = ['-DIMGUI_IMPL_OPENGL_LOADER_CUSTOM="epoxy/gl.h"']

View File

@ -35,6 +35,10 @@
#include "xemu-xbe.h"
#include "xemu-reporting.h"
#if defined(_WIN32)
#include "xemu-update.h"
#endif
#include "data/roboto_medium.ttf.h"
#include "imgui/imgui.h"
@ -570,6 +574,9 @@ private:
char eeprom_path[MAX_STRING_LEN];
int memory_idx;
bool short_animation;
#if defined(_WIN32)
bool check_for_update;
#endif
public:
SettingsWindow()
@ -622,6 +629,11 @@ public:
xemu_settings_get_bool(XEMU_SETTINGS_SYSTEM_SHORTANIM, &tmp_int);
short_animation = !!tmp_int;
#if defined(_WIN32)
xemu_settings_get_bool(XEMU_SETTINGS_MISC_CHECK_FOR_UPDATE, &tmp_int);
check_for_update = !!tmp_int;
#endif
dirty = false;
}
@ -633,6 +645,9 @@ public:
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_EEPROM_PATH, eeprom_path);
xemu_settings_set_int(XEMU_SETTINGS_SYSTEM_MEMORY, 64+memory_idx*64);
xemu_settings_set_bool(XEMU_SETTINGS_SYSTEM_SHORTANIM, short_animation);
#if defined(_WIN32)
xemu_settings_set_bool(XEMU_SETTINGS_MISC_CHECK_FOR_UPDATE, check_for_update);
#endif
xemu_settings_save();
xemu_queue_notification("Settings saved! Restart to apply updates.");
pending_restart = true;
@ -716,6 +731,15 @@ public:
}
ImGui::NextColumn();
#if defined(_WIN32)
ImGui::Dummy(ImVec2(0,0));
ImGui::NextColumn();
if (ImGui::Checkbox("Check for updates on startup", &check_for_update)) {
dirty = true;
}
ImGui::NextColumn();
#endif
ImGui::Columns(1);
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
@ -1536,6 +1560,140 @@ public:
}
};
#if defined(_WIN32)
class AutoUpdateWindow
{
protected:
Updater updater;
public:
bool is_open;
bool should_prompt_auto_update_selection;
AutoUpdateWindow()
{
is_open = false;
should_prompt_auto_update_selection = false;
}
~AutoUpdateWindow()
{
}
void save_auto_update_selection(bool preference)
{
xemu_settings_set_bool(XEMU_SETTINGS_MISC_CHECK_FOR_UPDATE, preference);
xemu_settings_save();
should_prompt_auto_update_selection = false;
}
void prompt_auto_update_selection()
{
ImGui::Text("Would you like xemu to check for updates on startup?");
ImGui::SetNextItemWidth(-1.0f);
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
ImGui::Separator();
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
float w = (130)*g_ui_scale;
float bw = w + (10)*g_ui_scale;
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-2*bw);
if (ImGui::Button("No", ImVec2(w, 0))) {
save_auto_update_selection(false);
is_open = false;
}
ImGui::SameLine();
if (ImGui::Button("Yes", ImVec2(w, 0))) {
save_auto_update_selection(true);
check_for_updates_and_prompt_if_available();
}
}
void check_for_updates_and_prompt_if_available()
{
updater.check_for_update([this](){
is_open |= updater.is_update_available();
});
}
void Draw()
{
if (!is_open) return;
ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_ui_scale, 0.0f));
if (!ImGui::Begin("Update", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
if (should_prompt_auto_update_selection) {
prompt_auto_update_selection();
ImGui::End();
return;
}
if (ImGui::IsWindowAppearing() && !updater.is_update_available()) {
updater.check_for_update();
}
const char *status_msg[] = {
"",
"An error has occured. Try again.",
"Checking for update...",
"Downloading update...",
"Update successful! Restart to launch updated version of xemu."
};
const char *available_msg[] = {
"Update availability unknown.",
"This version of xemu is up to date.",
"An updated version of xemu is available!",
};
if (updater.get_status() == UPDATER_IDLE) {
ImGui::Text(available_msg[updater.get_update_availability()]);
} else {
ImGui::Text(status_msg[updater.get_status()]);
}
if (updater.is_updating()) {
ImGui::ProgressBar(updater.get_update_progress_percentage()/100.0f,
ImVec2(-1.0f, 0.0f));
}
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
ImGui::Separator();
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
float w = (130)*g_ui_scale;
float bw = w + (10)*g_ui_scale;
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-bw);
if (updater.is_checking_for_update() || updater.is_updating()) {
if (ImGui::Button("Cancel", ImVec2(w, 0))) {
updater.cancel();
}
} else {
if (updater.is_pending_restart()) {
if (ImGui::Button("Restart", ImVec2(w, 0))) {
updater.restart_to_updated();
}
} else if (updater.is_update_available()) {
if (ImGui::Button("Update", ImVec2(w, 0))) {
updater.update();
}
} else {
if (ImGui::Button("Check for Update", ImVec2(w, 0))) {
updater.check_for_update();
}
}
}
ImGui::End();
}
};
#endif
static MonitorWindow monitor_window;
static DebugApuWindow apu_window;
static DebugVideoWindow video_window;
@ -1545,6 +1703,9 @@ static AboutWindow about_window;
static SettingsWindow settings_window;
static CompatibilityReporter compatibility_reporter_window;
static NotificationManager notification_manager;
#if defined(_WIN32)
static AutoUpdateWindow update_window;
#endif
static std::deque<const char *> g_errors;
class FirstBootWindow
@ -1776,12 +1937,16 @@ static void ShowMainMenu()
if (ImGui::BeginMenu("Help"))
{
ImGui::MenuItem("Report Compatibility", NULL, &compatibility_reporter_window.is_open);
if (ImGui::MenuItem("Help", NULL))
{
xemu_open_web_browser("https://github.com/mborgerson/xemu/wiki");
}
ImGui::MenuItem("Report Compatibility...", NULL, &compatibility_reporter_window.is_open);
#if defined(_WIN32)
ImGui::MenuItem("Check for Updates...", NULL, &update_window.is_open);
#endif
ImGui::Separator();
ImGui::MenuItem("About", NULL, &about_window.is_open);
ImGui::EndMenu();
@ -1911,6 +2076,18 @@ void xemu_hud_init(SDL_Window* window, void* sdl_gl_context)
g_sdl_window = window;
ImPlot::CreateContext();
#if defined(_WIN32)
int should_check_for_update;
xemu_settings_get_bool(XEMU_SETTINGS_MISC_CHECK_FOR_UPDATE, &should_check_for_update);
if (should_check_for_update == -1) {
update_window.should_prompt_auto_update_selection =
update_window.is_open = !xemu_settings_did_fail_to_load();
} else if (should_check_for_update) {
update_window.check_for_updates_and_prompt_if_available();
}
#endif
}
void xemu_hud_cleanup(void)
@ -2076,6 +2253,9 @@ void xemu_hud_render(void)
network_window.Draw();
compatibility_reporter_window.Draw();
notification_manager.Draw();
#if defined(_WIN32)
update_window.Draw();
#endif
// Very rudimentary error notification API
if (g_errors.size() > 0) {

View File

@ -68,6 +68,7 @@ struct xemu_settings {
// [misc]
char *user_token;
int check_for_update; // Boolean
};
struct enum_str_map {
@ -125,7 +126,8 @@ struct config_offset_table {
[XEMU_SETTINGS_NETWORK_LOCAL_ADDR] = { CONFIG_TYPE_STRING, "network", "local_addr", offsetof(struct xemu_settings, net_local_addr), { .default_str = "0.0.0.0:9368" } },
[XEMU_SETTINGS_NETWORK_REMOTE_ADDR] = { CONFIG_TYPE_STRING, "network", "remote_addr", offsetof(struct xemu_settings, net_remote_addr), { .default_str = "1.2.3.4:9368" } },
[XEMU_SETTINGS_MISC_USER_TOKEN] = { CONFIG_TYPE_STRING, "misc", "user_token", offsetof(struct xemu_settings, user_token), { .default_str = "" } },
[XEMU_SETTINGS_MISC_USER_TOKEN] = { CONFIG_TYPE_STRING, "misc", "user_token", offsetof(struct xemu_settings, user_token), { .default_str = "" } },
[XEMU_SETTINGS_MISC_CHECK_FOR_UPDATE] = { CONFIG_TYPE_BOOL, "misc", "check_for_update", offsetof(struct xemu_settings, check_for_update), { .default_bool = -1 } },
};
static const char *settings_path;
@ -312,6 +314,8 @@ static int config_parse_callback(void *user, const char *section, const char *na
int_val = 1;
} else if (strcmp(value, "false") == 0) {
int_val = 0;
} else if (strcmp(value, "") == 0) {
return 1;
} else {
fprintf(stderr, "Error parsing %s.%s as boolean. Got '%s'\n", section, name, value);
return 0;
@ -401,7 +405,11 @@ int xemu_settings_save(void)
} else if (config_items[i].type == CONFIG_TYPE_BOOL) {
int v;
xemu_settings_get_bool(i, &v);
fprintf(fd, "%s\n", !!(v) ? "true" : "false");
if (v == 0 || v == 1) {
fprintf(fd, "%s\n", !!(v) ? "true" : "false");
} else {
// Other values are considered unset
}
} else if (config_items[i].type == CONFIG_TYPE_ENUM) {
int v;
xemu_settings_get_enum(i, &v);

View File

@ -49,6 +49,7 @@ enum xemu_settings_keys {
XEMU_SETTINGS_NETWORK_LOCAL_ADDR,
XEMU_SETTINGS_NETWORK_REMOTE_ADDR,
XEMU_SETTINGS_MISC_USER_TOKEN,
XEMU_SETTINGS_MISC_CHECK_FOR_UPDATE,
XEMU_SETTINGS__COUNT,
XEMU_SETTINGS_INVALID = -1
};

195
ui/xemu-update.cc Normal file
View File

@ -0,0 +1,195 @@
/*
* xemu Automatic Update
*
* Copyright (C) 2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <SDL_filesystem.h>
#include "util/miniz/miniz.h"
#include "xemu-update.h"
#include "xemu-version.h"
#if defined(_WIN32)
const char *version_host = "raw.githubusercontent.com";
const char *version_uri = "/mborgerson/xemu/ppa-snapshot/XEMU_VERSION";
const char *download_host = "github.com";
const char *download_uri = "/mborgerson/xemu/releases/latest/download/xemu-win-release.zip";
#else
FIXME
#endif
#define CPPHTTPLIB_OPENSSL_SUPPORT 1
#include "httplib.h"
#define DPRINTF(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__);
Updater::Updater()
{
m_status = UPDATER_IDLE;
m_update_availability = UPDATE_AVAILABILITY_UNKNOWN;
m_update_percentage = 0;
m_latest_version = "Unknown";
m_should_cancel = false;
}
void Updater::check_for_update(UpdaterCallback on_complete)
{
if (m_status == UPDATER_IDLE || m_status == UPDATER_ERROR) {
m_on_complete = on_complete;
qemu_thread_create(&m_thread, "update_worker",
&Updater::checker_thread_worker_func,
this, QEMU_THREAD_JOINABLE);
}
}
void *Updater::checker_thread_worker_func(void *updater)
{
((Updater *)updater)->check_for_update_internal();
return NULL;
}
void Updater::check_for_update_internal()
{
httplib::SSLClient cli(version_host, 443);
cli.set_follow_location(true);
cli.set_timeout_sec(5);
auto res = cli.Get(version_uri, [this](uint64_t len, uint64_t total) {
m_update_percentage = len*100/total;
return !m_should_cancel;
});
if (m_should_cancel) {
m_should_cancel = false;
m_status = UPDATER_IDLE;
goto finished;
} else if (!res || res->status != 200) {
m_status = UPDATER_ERROR;
goto finished;
}
if (strcmp(xemu_version, res->body.c_str())) {
m_update_availability = UPDATE_AVAILABLE;
} else {
m_update_availability = UPDATE_NOT_AVAILABLE;
}
m_latest_version = res->body;
m_status = UPDATER_IDLE;
finished:
if (m_on_complete) {
m_on_complete();
}
}
void Updater::update()
{
if (m_status == UPDATER_IDLE || m_status == UPDATER_ERROR) {
m_status = UPDATER_UPDATING;
qemu_thread_create(&m_thread, "update_worker",
&Updater::update_thread_worker_func,
this, QEMU_THREAD_JOINABLE);
}
}
void *Updater::update_thread_worker_func(void *updater)
{
((Updater *)updater)->update_internal();
return NULL;
}
void Updater::update_internal()
{
httplib::SSLClient cli(download_host, 443);
cli.set_follow_location(true);
cli.set_timeout_sec(5);
auto res = cli.Get(download_uri, [this](uint64_t len, uint64_t total) {
m_update_percentage = len*100/total;
return !m_should_cancel;
});
if (m_should_cancel) {
m_should_cancel = false;
m_status = UPDATER_IDLE;
return;
} else if (!res || res->status != 200) {
m_status = UPDATER_ERROR;
return;
}
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
if (!mz_zip_reader_init_mem(&zip, res->body.data(), res->body.size(), 0)) {
DPRINTF("mz_zip_reader_init_mem failed\n");
m_status = UPDATER_ERROR;
return;
}
mz_uint num_files = mz_zip_reader_get_num_files(&zip);
for (mz_uint file_idx = 0; file_idx < num_files; file_idx++) {
mz_zip_archive_file_stat fstat;
if (!mz_zip_reader_file_stat(&zip, file_idx, &fstat)) {
DPRINTF("mz_zip_reader_file_stat failed for file #%d\n", file_idx);
goto errored;
}
if (fstat.m_filename[strlen(fstat.m_filename)-1] == '/') {
/* FIXME: mkdirs */
DPRINTF("FIXME: subdirs not handled yet\n");
goto errored;
}
char *dst_path = g_strdup_printf("%s%s", SDL_GetBasePath(), fstat.m_filename);
DPRINTF("extracting %s to %s\n", fstat.m_filename, dst_path);
if (!strcmp(fstat.m_filename, "xemu.exe")) {
// We cannot overwrite current executable, but we can move it
char *renamed_path = g_strdup_printf("%s%s", SDL_GetBasePath(), "xemu-previous.exe");
MoveFileExA(dst_path, renamed_path, MOVEFILE_REPLACE_EXISTING);
g_free(renamed_path);
}
if (!mz_zip_reader_extract_to_file(&zip, file_idx, dst_path, 0)) {
DPRINTF("mz_zip_reader_extract_to_file failed to create %s\n", dst_path);
g_free(dst_path);
goto errored;
}
g_free(dst_path);
}
m_status = UPDATER_UPDATE_SUCCESSFUL;
goto cleanup_zip;
errored:
m_status = UPDATER_ERROR;
cleanup_zip:
mz_zip_reader_end(&zip);
}
extern "C" {
extern char **gArgv;
}
void Updater::restart_to_updated()
{
char *target_exec = g_strdup_printf("%s%s", SDL_GetBasePath(), "xemu.exe");
DPRINTF("Restarting to updated executable %s\n", target_exec);
_execv(target_exec, gArgv);
DPRINTF("Launching updated executable failed\n");
exit(1);
}

80
ui/xemu-update.h Normal file
View File

@ -0,0 +1,80 @@
/*
* xemu Automatic Update
*
* Copyright (C) 2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef XEMU_UPDATE_H
#define XEMU_UPDATE_H
#include <string>
#include <stdint.h>
#include <functional>
extern "C" {
#include "qemu/osdep.h"
#include "qemu-common.h"
#include "qemu/thread.h"
}
typedef enum {
UPDATE_AVAILABILITY_UNKNOWN,
UPDATE_NOT_AVAILABLE,
UPDATE_AVAILABLE
} UpdateAvailability;
typedef enum {
UPDATER_IDLE,
UPDATER_ERROR,
UPDATER_CHECKING_FOR_UPDATE,
UPDATER_UPDATING,
UPDATER_UPDATE_SUCCESSFUL
} UpdateStatus;
using UpdaterCallback = std::function<void(void)>;
class Updater {
private:
UpdateAvailability m_update_availability;
int m_update_percentage;
QemuThread m_thread;
std::string m_latest_version;
bool m_should_cancel;
UpdateStatus m_status;
UpdaterCallback m_on_complete;
public:
Updater();
UpdateStatus get_status() { return m_status; }
UpdateAvailability get_update_availability() { return m_update_availability; }
bool is_errored() { return m_status == UPDATER_ERROR; }
bool is_pending_restart() { return m_status == UPDATER_UPDATE_SUCCESSFUL; }
bool is_update_available() { return m_update_availability == UPDATE_AVAILABLE; }
bool is_checking_for_update() { return m_status == UPDATER_CHECKING_FOR_UPDATE; }
bool is_updating() { return m_status == UPDATER_UPDATING; }
std::string get_update_version() { return m_latest_version; }
void cancel() { m_should_cancel = true; }
void update();
void update_internal();
void check_for_update(UpdaterCallback on_complete = nullptr);
void check_for_update_internal();
int get_update_progress_percentage() { return m_update_percentage; }
static void *update_thread_worker_func(void *updater);
static void *checker_thread_worker_func(void *updater);
void restart_to_updated(void);
};
#endif