Update checker (#132)

(with some extra spice)

Maybe this should be a target for Android as well.

Signed-off-by: swurl <swurl@swurl.xyz>

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/132
Co-authored-by: swurl <swurl@swurl.xyz>
Co-committed-by: swurl <swurl@swurl.xyz>
This commit is contained in:
swurl 2025-05-28 02:23:51 +00:00 committed by crueter
parent 71df7b1451
commit 4235492079
12 changed files with 208 additions and 9 deletions

View file

@ -34,10 +34,9 @@ else
export EXTRA_CMAKE_FLAGS=(-DYUZU_USE_PRECOMPILED_HEADERS=OFF)
fi
# TODO(crueter): update checker
# if [ "$GITHUB_REF_TYPE" == "tag" ]; then
# export EXTRA_CMAKE_FLAGS=($EXTRA_CMAKE_FLAGS -DENABLE_QT_UPDATE_CHECKER=ON)
# fi
if [ "$RELEASE" == "1" ]; then
export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DENABLE_QT_UPDATE_CHECKER=ON)
fi
mkdir -p build && cd build
cmake .. -G Ninja \

View file

@ -93,7 +93,7 @@ jobs:
fetch-tags: true
- name: Build
run: TARGET=appimage ./.ci/linux/build.sh v3 8
run: TARGET=appimage RELEASE=1 ./.ci/linux/build.sh v3 8
- name: Package AppImage
run: ./.ci/linux/package.sh v3 &> /dev/null

View file

@ -34,9 +34,11 @@ cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "NOT ANDROID"
option(ENABLE_OPENGL "Enable OpenGL" ON)
mark_as_advanced(FORCE ENABLE_OPENGL)
option(ENABLE_QT "Enable the Qt frontend" ON)
option(ENABLE_QT "Enable the Qt frontend" ON)
option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
option(ENABLE_QT_UPDATE_CHECKER "Enable update checker for the Qt frontend" OFF)
CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF)
option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
@ -353,6 +355,9 @@ endif()
if (ENABLE_WEB_SERVICE)
find_package(cpp-jwt 1.4 CONFIG)
endif()
if (ENABLE_WEB_SERVICE OR ENABLE_QT_UPDATE_CHECKER)
find_package(httplib 0.12 MODULE COMPONENTS OpenSSL)
endif()

View file

@ -119,7 +119,7 @@ endif()
add_subdirectory(sirit)
# httplib
if (ENABLE_WEB_SERVICE AND NOT TARGET httplib::httplib)
if ((ENABLE_WEB_SERVICE OR ENABLE_QT_UPDATE_CHECKER) AND NOT TARGET httplib::httplib)
set(HTTPLIB_REQUIRE_OPENSSL ON)
add_subdirectory(cpp-httplib)
endif()

View file

@ -266,6 +266,12 @@ file(GLOB COMPAT_LIST
file(GLOB_RECURSE ICONS ${PROJECT_SOURCE_DIR}/dist/icons/*)
file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/qt_themes/*)
if (ENABLE_QT_UPDATE_CHECKER)
target_link_libraries(yuzu PRIVATE httplib::httplib)
target_sources(yuzu PRIVATE update_checker.cpp)
target_compile_definitions(yuzu PUBLIC ENABLE_QT_UPDATE_CHECKER)
endif()
if (ENABLE_QT_TRANSLATION)
set(YUZU_QT_LANGUAGES "${PROJECT_SOURCE_DIR}/dist/languages" CACHE PATH "Path to the translation bundle for the Qt frontend")
option(GENERATE_QT_TRANSLATION "Generate en.ts as the translation source file" OFF)

View file

@ -13,6 +13,7 @@
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/main.h"
#include "ui_qt_amiibo_settings.h"
#include "web_service/web_result.h"
#ifdef ENABLE_WEB_SERVICE
#include "web_service/web_backend.h"
#endif

View file

@ -296,6 +296,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(UISettings, controller_applet_disabled, tr("Disable controller applet"),
tr("Forcibly disables the use of the controller applet by guests.\nWhen a guest "
"attempts to open the controller applet, it is immediately closed."));
INSERT(UISettings,
check_for_updates,
tr("Check for updates"),
tr("Whether or not to check for updates upon startup."));
// Linux
INSERT(Settings, enable_gamemode, tr("Enable Gamemode"), QString());

View file

@ -52,6 +52,10 @@
#include "yuzu/multiplayer/state.h"
#include "yuzu/util/controller_navigation.h"
#ifdef ENABLE_QT_UPDATE_CHECKER
#include "yuzu/update_checker.h"
#endif
#ifdef YUZU_ROOM
#include "dedicated_room/yuzu_room.h"
#endif
@ -313,6 +317,7 @@ GMainWindow::GMainWindow(bool has_broken_vulkan)
provider{std::make_unique<FileSys::ManualContentProvider>()} {
Common::FS::CreateEdenPaths();
this->config = std::make_unique<QtConfig>();
#ifdef __unix__
SetupSigInterrupts();
SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
@ -410,6 +415,27 @@ GMainWindow::GMainWindow(bool has_broken_vulkan)
show();
#ifdef ENABLE_QT_UPDATE_CHECKER
if (UISettings::values.check_for_updates) {
update_future = QtConcurrent::run([]() -> QString {
const bool is_prerelease =
((strstr(Common::g_build_fullname, "pre-alpha") != NULL) ||
(strstr(Common::g_build_fullname, "alpha") != NULL) ||
(strstr(Common::g_build_fullname, "beta") != NULL) ||
(strstr(Common::g_build_fullname, "rc") != NULL));
const std::optional<std::string> latest_release_tag =
UpdateChecker::GetLatestRelease(is_prerelease);
if (latest_release_tag && latest_release_tag.value() != Common::g_build_fullname) {
return QString::fromStdString(latest_release_tag.value());
}
return QString{};
});
QObject::connect(&update_watcher, &QFutureWatcher<QString>::finished, this,
&GMainWindow::OnEmulatorUpdateAvailable);
update_watcher.setFuture(update_future);
}
#endif
system->SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
provider.get());
@ -4767,6 +4793,28 @@ void GMainWindow::MigrateConfigFiles() {
}
}
#ifdef ENABLE_QT_UPDATE_CHECKER
void GMainWindow::OnEmulatorUpdateAvailable() {
QString version_string = update_future.result();
if (version_string.isEmpty())
return;
QMessageBox update_prompt(this);
update_prompt.setWindowTitle(tr("Update Available"));
update_prompt.setIcon(QMessageBox::Information);
update_prompt.addButton(QMessageBox::Yes);
update_prompt.addButton(QMessageBox::Ignore);
update_prompt.setText(tr("Update %1 for Eden is available.\nWould you like to download it?")
.arg(version_string));
update_prompt.exec();
if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) {
QDesktopServices::openUrl(
QUrl(QString::fromStdString("https://github.com/eden-emulator/Releases/releases/tag/") +
version_string));
}
}
#endif
void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_view title_version,
std::string_view gpu_vendor) {
const auto branch_name = std::string(Common::g_scm_branch);

View file

@ -13,7 +13,6 @@
#include <QTimer>
#include <QTranslator>
#include "common/announce_multiplayer_room.h"
#include "common/common_types.h"
#include "configuration/qt_config.h"
#include "frontend_common/content_manager.h"
@ -21,14 +20,19 @@
#include "user_data_migration.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/hotkeys.h"
#include "yuzu/util/controller_navigation.h"
#ifdef __unix__
#include <QDBusObjectPath>
#include <QVariant>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QtDBus>
#endif
#ifdef ENABLE_QT_UPDATE_CHECKER
#include <QFuture>
#include <QFutureWatcher>
#endif
class QtConfig;
class ClickableLabel;
class EmuThread;
@ -414,6 +418,10 @@ private slots:
void OnEmulationStopped();
void OnEmulationStopTimeExpired();
#ifdef ENABLE_QT_UPDATE_CHECKER
void OnEmulatorUpdateAvailable();
#endif
private:
QString GetGameListErrorRemoving(InstalledEntryType type) const;
void RemoveBaseContent(u64 program_id, InstalledEntryType type);
@ -483,6 +491,11 @@ private:
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
#ifdef ENABLE_QT_UPDATE_CHECKER
QFuture<QString> update_future;
QFutureWatcher<QString> update_watcher;
#endif
MultiplayerState* multiplayer_state = nullptr;
GRenderWindow* render_window;

View file

@ -141,6 +141,7 @@ struct Values {
true,
true};
Setting<bool> disable_web_applet{linkage, true, "disable_web_applet", Category::Ui};
Setting<bool> check_for_updates{linkage, true, "check_for_updates", Category::UiGeneral};
// Discord RPC
Setting<bool> enable_discord_presence{linkage, false, "enable_discord_presence", Category::Ui};

109
src/yuzu/update_checker.cpp Normal file
View file

@ -0,0 +1,109 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "update_checker.h"
#include "common/logging/log.h"
#include <fmt/format.h>
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
std::optional<std::string> UpdateChecker::GetResponse(std::string url, std::string path)
{
constexpr std::size_t timeout_seconds = 15;
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
client->set_connection_timeout(timeout_seconds);
client->set_read_timeout(timeout_seconds);
client->set_write_timeout(timeout_seconds);
if (client == nullptr) {
LOG_ERROR(Frontend, "Invalid URL {}{}", url, path);
return {};
}
httplib::Request request{
.method = "GET",
.path = path,
};
client->set_follow_location(true);
const auto result = client->send(request);
if (!result) {
LOG_ERROR(Frontend, "GET to {}{} returned null", url, path);
return {};
}
const auto& response = result.value();
if (response.status >= 400) {
LOG_ERROR(Frontend, "GET to {}{} returned error status code: {}", url, path, response.status);
return {};
}
if (!response.headers.contains("content-type")) {
LOG_ERROR(Frontend, "GET to {}{} returned no content", url, path);
return {};
}
return response.body;
}
std::optional<std::string> UpdateChecker::GetLatestRelease(bool include_prereleases)
{
constexpr auto update_check_url = "http://api.github.com";
std::string update_check_path = "/repos/eden-emulator/Releases";
try {
if (include_prereleases) { // This can return either a prerelease or a stable release,
// whichever is more recent.
const auto update_check_tags_path = update_check_path + "/tags";
const auto update_check_releases_path = update_check_path + "/releases";
const auto tags_response = GetResponse(update_check_url, update_check_tags_path);
const auto releases_response = GetResponse(update_check_url, update_check_releases_path);
if (!tags_response || !releases_response)
return {};
const std::string latest_tag
= nlohmann::json::parse(tags_response.value()).at(0).at("name");
const bool latest_tag_has_release = releases_response.value().find(
fmt::format("\"{}\"", latest_tag))
!= std::string::npos;
// If there is a newer tag, but that tag has no associated release, don't prompt the
// user to update.
if (!latest_tag_has_release)
return {};
return latest_tag;
} else { // This is a stable release, only check for other stable releases.
update_check_path += "/releases/latest";
const auto response = GetResponse(update_check_url, update_check_path);
if (!response)
return {};
const std::string latest_tag = nlohmann::json::parse(response.value()).at("tag_name");
return latest_tag;
}
} catch (nlohmann::detail::out_of_range&) {
LOG_ERROR(Frontend,
"Parsing JSON response from {}{} failed during update check: "
"nlohmann::detail::out_of_range",
update_check_url,
update_check_path);
return {};
} catch (nlohmann::detail::type_error&) {
LOG_ERROR(Frontend,
"Parsing JSON response from {}{} failed during update check: "
"nlohmann::detail::type_error",
update_check_url,
update_check_path);
return {};
}
}

13
src/yuzu/update_checker.h Normal file
View file

@ -0,0 +1,13 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <optional>
#include <string>
namespace UpdateChecker {
std::optional<std::string> GetResponse(std::string url, std::string path);
std::optional<std::string> GetLatestRelease(bool);
} // namespace UpdateChecker