From 270351b0335e522bd80794ae435d57736a653b06 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 18 Feb 2023 12:42:19 +0100 Subject: [PATCH] Added an application updater that checks for new releases on startup. --- CMakeLists.txt | 71 +++-- es-app/CMakeLists.txt | 2 + es-app/src/ApplicationUpdater.cpp | 403 ++++++++++++++++++++++++++++ es-app/src/ApplicationUpdater.h | 67 +++++ es-app/src/guis/GuiMenu.cpp | 67 ++++- es-app/src/main.cpp | 44 ++- es-app/src/views/ViewController.cpp | 9 + es-app/src/views/ViewController.h | 1 + es-core/src/HttpReq.cpp | 1 + es-core/src/HttpReq.h | 1 + es-core/src/Settings.cpp | 6 + es-core/src/Settings.h | 1 + 12 files changed, 653 insertions(+), 20 deletions(-) create mode 100644 es-app/src/ApplicationUpdater.cpp create mode 100644 es-app/src/ApplicationUpdater.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1283ec37d..1920346ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,20 +32,22 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/CMake/Utils ${CMAKE_CURRENT_SOURCE_DIR}/CMake/Packages) # Define the options. -option(GL "Set to ON if targeting Desktop OpenGL" ${GL}) -option(GLES "Set to ON if targeting OpenGL ES" ${GLES}) -option(APPIMAGE_BUILD "Set to ON when building as an AppImage" ${APPIMAGE_BUILD}) -option(FLATPAK_BUILD "Set to ON when building as a Flatpak" ${FLATPAK_BUILD}) -option(STEAM_DECK "Set to ON to enable a Valve Steam Deck specific build" ${STEAM_DECK}) -option(RETRODECK "Set to ON to enable a RetroDECK specific build" ${RETRODECK}) -option(RPI "Set to ON to enable a Raspberry Pi specific build" ${RPI}) -option(BUNDLED_CERTS "Set to ON to use bundled TLS/SSL certificates" ${BUNDLED_CERTS}) -option(CEC "Set to ON to enable CEC" ${CEC}) -option(VIDEO_HW_DECODING "Set to ON to enable FFmpeg HW decoding" ${VIDEO_HW_DECODING}) -option(CLANG_TIDY "Set to ON to build using the clang-tidy static analyzer" ${CLANG_TIDY}) -option(ASAN "Set to ON to build with AddressSanitizer" ${ASAN}) -option(TSAN "Set to ON to build with ThreadSanitizer" ${TSAN}) -option(UBSAN "Set to ON to build with UndefinedBehaviorSanitizer" ${UBSAN}) +option(GL "Set to ON if targeting Desktop OpenGL" ON) +option(GLES "Set to ON if targeting OpenGL ES" OFF) +option(APPLICATION_UPDATER "Set to OFF to build without the application updater" ON) +option(APPIMAGE_BUILD "Set to ON when building as an AppImage" OFF) +option(AUR_BUILD "Set to ON when building for the AUR" OFF) +option(FLATPAK_BUILD "Set to ON when building as a Flatpak" OFF) +option(STEAM_DECK "Set to ON to enable a Valve Steam Deck specific build" OFF) +option(RETRODECK "Set to ON to enable a RetroDECK specific build" OFF) +option(RPI "Set to ON to enable a Raspberry Pi specific build" OFF) +option(BUNDLED_CERTS "Set to ON to use bundled TLS/SSL certificates" OFF) +option(CEC "Set to ON to enable CEC" OFF) +option(VIDEO_HW_DECODING "Set to ON to enable FFmpeg HW decoding" OFF) +option(CLANG_TIDY "Set to ON to build using the clang-tidy static analyzer" OFF) +option(ASAN "Set to ON to build with AddressSanitizer" OFF) +option(TSAN "Set to ON to build with ThreadSanitizer" OFF) +option(UBSAN "Set to ON to build with UndefinedBehaviorSanitizer" OFF) if(CLANG_TIDY) find_program(CLANG_TIDY_BINARY NAMES clang-tidy) @@ -287,7 +289,7 @@ if(GLES) message("-- Building with OpenGL ES renderer") endif() -if (APPIMAGE_BUILD AND FLATPAK_BUILD) +if(APPIMAGE_BUILD AND FLATPAK_BUILD) message(FATAL_ERROR "-- APPIMAGE_BUILD and FLATPAK_BUILD can't be combined") endif() @@ -307,6 +309,10 @@ if(FLATPAK_BUILD) message("-- Building as a Flatpak") endif() +if(AUR_BUILD) + message("-- Building for the AUR") +endif() + if(STEAM_DECK AND RETRODECK) message(FATAL_ERROR "-- STEAM_DECK and RETRODECK can't be combined") endif() @@ -339,13 +345,44 @@ if(VIDEO_HW_DECODING) message("-- Building with FFmpeg HW decoding") endif() +if(AUR_BUILD OR FLATPAK_BUILD OR RETRODECK OR RPI) + set(APPLICATION_UPDATER OFF) +endif() + +if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR CMAKE_SYSTEM_NAME MATCHES NetBSD OR CMAKE_SYSTEM_NAME MATCHES OpenBSD) + set(APPLICATION_UPDATER OFF) +endif() + +if(APPLICATION_UPDATER) + add_compile_definitions(APPLICATION_UPDATER) +else() + message("-- Building without application updater") +endif() + +# This is needed to identify the package type for the application updater. +if(CMAKE_SYSTEM_NAME MATCHES Linux) + if(LINUX_CPACK_GENERATOR MATCHES DEB) + add_compile_definitions(LINUX_DEB_PACKAGE) + elseif(LINUX_CPACK_GENERATOR MATCHES RPM) + add_compile_definitions(LINUX_RPM_PACKAGE) + endif() +endif() + +if(APPLE) + if(CMAKE_SYSTEM_PROCESSOR MATCHES arm) + add_compile_definitions(MACOS_APPLE_CPU) + else() + add_compile_definitions(MACOS_INTEL_CPU) + endif() +endif() + if(APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 10.14) add_compile_definitions(LEGACY_MACOS) endif() -# If it's an alpha, beta or dev build, then display the build date in the main menu. +# Affects the application updater and is used for displaying version info in the main menu. if(ES_VERSION MATCHES alpha OR ES_VERSION MATCHES beta OR ES_VERSION MATCHES dev) - add_compile_definitions(MENU_BUILD_DATE) + add_compile_definitions(IS_PRERELEASE) endif() # GLM library options. diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 442ef62d9..34a8463fc 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -10,6 +10,7 @@ project(emulationstation-de) set(ES_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationUpdater.h ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemsManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.h ${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.h @@ -58,6 +59,7 @@ set(ES_HEADERS ) set(ES_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationUpdater.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemsManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp diff --git a/es-app/src/ApplicationUpdater.cpp b/es-app/src/ApplicationUpdater.cpp new file mode 100644 index 000000000..48d268e7c --- /dev/null +++ b/es-app/src/ApplicationUpdater.cpp @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// ApplicationUpdater.cpp +// +// Checks for application updates. +// In the future updates will also be downloaded and possibly installed. +// + +#include "ApplicationUpdater.h" + +#include "EmulationStation.h" +#include "Log.h" +#include "Settings.h" +#include "utils/StringUtil.h" +#include "utils/TimeUtil.h" + +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" + +#include + +#include +#include + +#define MAX_DOWNLOAD_TIME 1 + +ApplicationUpdater::ApplicationUpdater() + : mTimer {0} + , mMaxTime {0} + , mAbortDownload {false} + , mCheckedForUpdate {false} +{ + mUrl = "https://gitlab.com/api/v4/projects/18817634/repository/files/latest_release.json/" + "raw?ref=master"; +} + +ApplicationUpdater::~ApplicationUpdater() +{ + // This is needed if getResults() was never called. + if (mThread) + mThread->join(); +} + +void ApplicationUpdater::checkForUpdates() +{ + const std::string updateFrequency { + Settings::getInstance()->getString("ApplicationUpdaterFrequency")}; + if (updateFrequency == "never") + return; + + const std::string lastCheck {Settings::getInstance()->getString("ApplicationUpdaterLastCheck")}; + unsigned int frequencyDays {0}; + bool checkForUpdate {false}; + + if (updateFrequency == "daily") + frequencyDays = 1; + else if (updateFrequency == "weekly") + frequencyDays = 7; + else if (updateFrequency == "monthly") + frequencyDays = 30; + + // Frequency set to "always" or it's the first time we check for updates. + if (frequencyDays == 0 || lastCheck == "") { + checkForUpdate = true; + } + else { + const Utils::Time::DateTime now {Utils::Time::now()}; + const Utils::Time::DateTime lastTime {lastCheck}; + const Utils::Time::Duration dur {now.getTime() - lastTime.getTime()}; + if (dur.getDays() >= frequencyDays) + checkForUpdate = true; + } + + if (checkForUpdate) { + LOG(LogInfo) << "Checking for application updates..."; + mThread = std::make_unique(&ApplicationUpdater::updaterThread, this); + } +} + +void ApplicationUpdater::updaterThread() +{ + if (!downloadFile()) { + compareVersions(); + } +} + +bool ApplicationUpdater::downloadFile() +{ + const unsigned int startTime {SDL_GetTicks()}; + mTimer = startTime; + mMaxTime = mTimer + (MAX_DOWNLOAD_TIME * 1000); + + mStatus = ASYNC_IN_PROGRESS; + mRequest = std::unique_ptr(std::make_unique(mUrl)); + + while (mTimer < mMaxTime || !mAbortDownload) { + SDL_Delay(10); + try { + update(); + } + catch (std::runtime_error& e) { + LOG(LogWarning) << "ApplicationUpdater: Couldn't download \"latest_release.json\": " + << e.what(); + return true; + } + if (mStatus == ASYNC_DONE) + break; + mTimer = SDL_GetTicks(); + }; + + if (mStatus == ASYNC_DONE) { + rapidjson::Document doc; + const std::string& fileContents {mRequest->getContent()}; + doc.Parse(&fileContents[0], fileContents.length()); + if (doc.HasMember("error") && doc["error"].IsString()) { + LOG(LogWarning) + << "ApplicationUpdater: Couldn't download \"latest_release.json\", received " + "server error response \"" + << doc["error"].GetString() << "\""; + return true; + } + LOG(LogDebug) + << "ApplicationUpdater::downloadFile(): Downloaded \"latest_release.json\" in " + << mTimer - startTime << " milliseconds"; + try { + parseFile(); + } + catch (std::runtime_error& e) { + LOG(LogError) << "ApplicationUpdater: Couldn't parse \"latest_release.json\": " + << e.what(); + return true; + } + } + else if (mAbortDownload) { + LOG(LogWarning) << "ApplicationUpdater: Aborted download of \"latest_release.json\" after " + << mTimer - startTime << " milliseconds as the application has started up"; + return true; + } + else { + LOG(LogWarning) << "ApplicationUpdater: Couldn't download \"latest_release.json\" within " + << MAX_DOWNLOAD_TIME << " second time limit"; + return true; + } + + return false; +} + +void ApplicationUpdater::update() +{ + HttpReq::Status reqStatus {mRequest->status()}; + if (reqStatus == HttpReq::REQ_SUCCESS) { + mStatus = ASYNC_DONE; + return; + } + + // Not ready yet. + if (reqStatus == HttpReq::REQ_IN_PROGRESS) + return; + + // Everything else is some sort of error. + std::string errorMessage {"Network error (status: "}; + errorMessage.append(std::to_string(reqStatus)).append(") - ").append(mRequest->getErrorMsg()); + throw std::runtime_error(errorMessage); +} + +void ApplicationUpdater::parseFile() +{ + assert(mRequest->status() == HttpReq::REQ_SUCCESS); + + const std::string fileContents {mRequest->getContent()}; + rapidjson::Document doc; + doc.Parse(&fileContents[0], fileContents.length()); + + if (doc.HasParseError()) + throw std::runtime_error(rapidjson::GetParseError_En(doc.GetParseError())); + + const std::vector releaseTypes {"stable", "prerelease"}; + + for (auto& releaseType : releaseTypes) { + Release release; + if (doc.HasMember(releaseType.c_str())) { + release.releaseType = releaseType.c_str(); + const rapidjson::Value& releaseTypeEntry {doc[releaseType.c_str()]}; + + if (releaseTypeEntry.HasMember("version") && releaseTypeEntry["version"].IsString()) + release.version = releaseTypeEntry["version"].GetString(); + else + throw std::runtime_error("Invalid file structure, \"version\" key missing"); + + // There may not be a prerelease available. + if (releaseType == "prerelease" && release.version == "") + continue; + + if (releaseTypeEntry.HasMember("release") && releaseTypeEntry["release"].IsString()) + release.releaseNum = releaseTypeEntry["release"].GetString(); + else + throw std::runtime_error("Invalid file structure, \"release\" key missing"); + + if (releaseTypeEntry.HasMember("date") && releaseTypeEntry["date"].IsString()) + release.date = releaseTypeEntry["date"].GetString(); + else + throw std::runtime_error("Invalid file structure, \"date\" key missing"); + + if (releaseTypeEntry.HasMember("packages") && releaseTypeEntry["packages"].IsArray()) { + const rapidjson::Value& packages {releaseTypeEntry["packages"]}; + for (int i {0}; i < static_cast(packages.Size()); ++i) { + Package package; + const rapidjson::Value& packageEntry {packages[i]}; + + if (packageEntry.HasMember("name") && packageEntry["name"].IsString()) + package.name = packageEntry["name"].GetString(); + else + throw std::runtime_error( + "Invalid file structure, package \"name\" key missing"); + + if (packageEntry.HasMember("filename") && packageEntry["filename"].IsString()) + package.filename = packageEntry["filename"].GetString(); + else + throw std::runtime_error( + "Invalid file structure, package \"filename\" key missing"); + + if (packageEntry.HasMember("url") && packageEntry["url"].IsString()) + package.url = packageEntry["url"].GetString(); + else + throw std::runtime_error( + "Invalid file structure, package \"url\" key missing"); + + if (packageEntry.HasMember("md5") && packageEntry["md5"].IsString()) + package.md5 = packageEntry["md5"].GetString(); + else + throw std::runtime_error( + "Invalid file structure, package \"md5\" key missing"); + + if (packageEntry.HasMember("message") && packageEntry["message"].IsString()) + package.message = packageEntry["message"].GetString(); + else + throw std::runtime_error( + "Invalid file structure, package \"message\" key missing"); + + release.packages.emplace_back(package); + } + } + else { + throw std::runtime_error("Invalid file structure"); + } + if (releaseType == "stable") + mStableRelease = std::move(release); + else + mPrerelease = std::move(release); + } + else { + throw std::runtime_error("Invalid file structure, release type \"" + releaseType + + "\" missing"); + } + } + if (mPrerelease.version == "") { + LOG(LogDebug) << "ApplicationUpdater::parseFile(): Latest stable release is " + << mStableRelease.version << " (r" << mStableRelease.releaseNum + << "), no prerelease currently available"; + } + else { + LOG(LogDebug) << "ApplicationUpdater::parseFile(): Latest stable release is " + << mStableRelease.version << " (r" << mStableRelease.releaseNum + << ") and latest prerelease is " << mPrerelease.version << " (r" + << mPrerelease.releaseNum << ")"; + } +} + +void ApplicationUpdater::compareVersions() +{ + std::deque releaseTypes {&mStableRelease}; + + if (mPrerelease.version != "") { +#if defined(IS_PRERELEASE) + releaseTypes.emplace_front(&mPrerelease); +#else + if (Settings::getInstance()->getBool("ApplicationUpdaterPrereleases")) + releaseTypes.emplace_front(&mPrerelease); +#endif + } + + for (auto& releaseType : releaseTypes) { + bool newVersion {false}; + // If the version does not follow the semantic versioning scheme then always consider it to + // be a new release as perhaps the version scheme will be changed sometime in the future. + if (count_if(releaseType->version.cbegin(), releaseType->version.cend(), + [](char c) { return c == '.'; }) != 2) { + newVersion = true; + } + else { + std::vector fileVersion { + Utils::String::delimitedStringToVector(releaseType->version, ".")}; + + const size_t dashPos {fileVersion.back().find('-')}; + if (dashPos != std::string::npos) + fileVersion.back() = fileVersion.back().substr(0, dashPos); + + int versionWeight {0}; + + if (std::stoi(fileVersion.at(0)) > PROGRAM_VERSION_MAJOR) + versionWeight += 8; + else if (std::stoi(fileVersion.at(0)) < PROGRAM_VERSION_MAJOR) + versionWeight -= 8; + + if (std::stoi(fileVersion.at(1)) > PROGRAM_VERSION_MINOR) + versionWeight += 4; + else if (std::stoi(fileVersion.at(1)) < PROGRAM_VERSION_MINOR) + versionWeight -= 4; + + if (std::stoi(fileVersion.at(2)) > PROGRAM_VERSION_MAINTENANCE) + versionWeight += 2; + else if (std::stoi(fileVersion.at(2)) < PROGRAM_VERSION_MAINTENANCE) + versionWeight -= 2; + + // If versions match precisely then fall back to using the release number. + if (versionWeight == 0 && std::stoi(releaseType->releaseNum) > PROGRAM_RELEASE_NUMBER) + ++versionWeight; + + if (versionWeight > 0) + newVersion = true; + } + if (newVersion) { + std::string message; + + for (auto& package : releaseType->packages) { +#if defined(_WIN64) + if (Settings::getInstance()->getBool("PortableMode")) { + if (package.name == "WindowsPortable") + message = package.message; + } + else { + if (package.name == "WindowsInstaller") + message = package.message; + } +#elif defined(MACOS_APPLE_CPU) + if (package.name == "macOSApple") + message = package.message; +#elif defined(MACOS_INTEL_CPU) + if (package.name == "macOSIntel") + message = package.message; +#elif defined(STEAM_DECK) + if (package.name == "LinuxSteamDeckAppImage") + message = package.message; +#elif defined(APPIMAGE_BUILD) + if (package.name == "LinuxAppImage") + message = package.message; +#elif defined(LINUX_DEB_PACKAGE) + if (package.name == "LinuxDEB") + message = package.message; +#elif defined(LINUX_RPM_PACKAGE) + if (package.name == "LinuxRPM") + message = package.message; +#endif + auto tempVar = package; + } + + // Cut the message to 280 characters so we don't make the message box exceedingly large. + message = message.substr(0, 280); + + LOG(LogInfo) << "ApplicationUpdater: A new " + << (releaseType == &mStableRelease ? "stable release" : "prerelease") + << " is available for download at https://es-de.org: " + << releaseType->version << " (r" << releaseType->releaseNum + << "), release date: " << releaseType->date; + + mResults.append("New ") + .append(releaseType == &mStableRelease ? "release " : "prerelease ") + .append("available!\n") + .append(releaseType->version) + .append(" (") + .append(releaseType->date) + .append(")\n") + .append("can now be downloaded from\n") + .append("https://es-de.org/"); + + if (message != "") + mResults.append("\n").append(message); + + mResults = Utils::String::toUpper(mResults); + break; + } + } + mCheckedForUpdate = true; +} + +void ApplicationUpdater::getResults(std::string& results) +{ + mAbortDownload = true; + + if (mThread) { + mThread->join(); + mThread.reset(); + if (mCheckedForUpdate) { + if (mResults != "") + results = mResults; + Settings::getInstance()->setString( + "ApplicationUpdaterLastCheck", + Utils::Time::DateTime(Utils::Time::now()).getIsoString()); + Settings::getInstance()->saveFile(); + } + } +} diff --git a/es-app/src/ApplicationUpdater.h b/es-app/src/ApplicationUpdater.h new file mode 100644 index 000000000..a3f456ddc --- /dev/null +++ b/es-app/src/ApplicationUpdater.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// ApplicationUpdater.h +// +// Checks for application updates. +// In the future updates will also be downloaded and possibly installed. +// + +#ifndef ES_APP_APPLICATION_UPDATER_H +#define ES_APP_APPLICATION_UPDATER_H + +#include "AsyncHandle.h" +#include "HttpReq.h" + +#include +#include +#include +#include + +class ApplicationUpdater : public AsyncHandle +{ +public: + ApplicationUpdater(); + ~ApplicationUpdater(); + + void checkForUpdates(); + void updaterThread(); + bool downloadFile(); + void update() override; + void parseFile(); + void compareVersions(); + void getResults(std::string& results); + +private: + struct Package { + std::string name; + std::string filename; + std::string url; + std::string md5; + std::string message; + }; + + struct Release { + std::string releaseType; + std::string version; + std::string releaseNum; + std::string date; + std::vector packages; + }; + + std::string mUrl; + std::string mResults; + unsigned int mTimer; + unsigned int mMaxTime; + std::atomic mAbortDownload; + bool mCheckedForUpdate; + + std::unique_ptr mThread; + std::unique_ptr mRequest; + AsyncHandleStatus mStatus; + + Release mStableRelease; + Release mPrerelease; +}; + +#endif // ES_APP_APPLICATION_UPDATER_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index e5fbca78e..e3aea18a8 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1408,6 +1408,48 @@ void GuiMenu::openOtherOptions() } }); +#if defined(APPLICATION_UPDATER) + // Application updater frequency. + auto applicationUpdaterFrequency = std::make_shared>( + getHelpStyle(), "APPLICATION UPDATES", false); + const std::string& selectedUpdaterFrequency { + Settings::getInstance()->getString("ApplicationUpdaterFrequency")}; + applicationUpdaterFrequency->add("ALWAYS", "always", selectedUpdaterFrequency == "always"); + applicationUpdaterFrequency->add("DAILY", "daily", selectedUpdaterFrequency == "daily"); + applicationUpdaterFrequency->add("WEEKLY", "weekly", selectedUpdaterFrequency == "weekly"); + applicationUpdaterFrequency->add("MONTHLY", "monthly", selectedUpdaterFrequency == "monthly"); + applicationUpdaterFrequency->add("NEVER", "never", selectedUpdaterFrequency == "never"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set updater frequency to "always" in this case. + if (applicationUpdaterFrequency->getSelectedObjects().size() == 0) + applicationUpdaterFrequency->selectEntry(0); + s->addWithLabel("CHECK FOR APPLICATION UPDATES", applicationUpdaterFrequency); + s->addSaveFunc([applicationUpdaterFrequency, s] { + if (applicationUpdaterFrequency->getSelected() != + Settings::getInstance()->getString("ApplicationUpdaterFrequency")) { + Settings::getInstance()->setString("ApplicationUpdaterFrequency", + applicationUpdaterFrequency->getSelected()); + s->setNeedsSaving(); + } + }); +#endif + +#if defined(APPLICATION_UPDATER) && !defined(IS_PRERELEASE) + // Whether to include prereleases when checking for application updates. + auto applicationUpdaterPrereleases = std::make_shared(); + applicationUpdaterPrereleases->setState( + Settings::getInstance()->getBool("ApplicationUpdaterPrereleases")); + s->addWithLabel("INCLUDE PRERELEASES IN UPDATE CHECKS", applicationUpdaterPrereleases); + s->addSaveFunc([applicationUpdaterPrereleases, s] { + if (applicationUpdaterPrereleases->getState() != + Settings::getInstance()->getBool("ApplicationUpdaterPrereleases")) { + Settings::getInstance()->setBool("ApplicationUpdaterPrereleases", + applicationUpdaterPrereleases->getState()); + s->setNeedsSaving(); + } + }); +#endif + #if defined(_WIN64) // Hide taskbar during the program session. auto hide_taskbar = std::make_shared(); @@ -1591,6 +1633,29 @@ void GuiMenu::openOtherOptions() }); #endif +#if defined(APPLICATION_UPDATER) && !defined(IS_PRERELEASE) + auto applicationUpdaterFrequencyFunc = [applicationUpdaterFrequency, + applicationUpdaterPrereleases](const std::string&) { + if (applicationUpdaterFrequency->getSelected() == "never") { + applicationUpdaterPrereleases->setEnabled(false); + applicationUpdaterPrereleases->setOpacity(DISABLED_OPACITY); + applicationUpdaterPrereleases->getParent() + ->getChild(applicationUpdaterPrereleases->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + else { + applicationUpdaterPrereleases->setEnabled(true); + applicationUpdaterPrereleases->setOpacity(1.0f); + applicationUpdaterPrereleases->getParent() + ->getChild(applicationUpdaterPrereleases->getChildIndex() - 1) + ->setOpacity(1.0f); + } + }; + + applicationUpdaterFrequencyFunc(std::string()); + applicationUpdaterFrequency->setCallback(applicationUpdaterFrequencyFunc); +#endif + s->setSize(mSize); mWindow->pushGui(s); } @@ -1673,7 +1738,7 @@ void GuiMenu::addVersionInfo() mVersion.setFont(Font::get(FONT_SIZE_SMALL)); mVersion.setColor(0x5E5E5EFF); -#if defined(MENU_BUILD_DATE) +#if defined(IS_PRERELEASE) mVersion.setText("EMULATIONSTATION-DE V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + " (Built " + __DATE__ + ")"); #else diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 8837fb7a6..43c942993 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -17,6 +17,7 @@ // environment and starts listening to SDL events. // +#include "ApplicationUpdater.h" #include "AudioManager.h" #include "CollectionSystemsManager.h" #include "EmulationStation.h" @@ -62,6 +63,9 @@ namespace Window* window {nullptr}; int lastTime {0}; +#if defined(APPLICATION_UPDATER) + bool noUpdateCheck {false}; +#endif bool forceInputConfig {false}; bool createSystemDirectories {false}; bool settingsNeedSaving {false}; @@ -361,6 +365,11 @@ bool parseArgs(int argc, char* argv[]) else if (strcmp(argv[i], "--no-splash") == 0) { Settings::getInstance()->setBool("SplashScreen", false); } +#if defined(APPLICATION_UPDATER) + else if (strcmp(argv[i], "--no-update-check") == 0) { + noUpdateCheck = true; + } +#endif else if (strcmp(argv[i], "--gamelist-only") == 0) { Settings::getInstance()->setBool("ParseGamelistOnly", true); settingsNeedSaving = true; @@ -418,6 +427,9 @@ bool parseArgs(int argc, char* argv[]) " --anti-aliasing [0, 2 or 4] Set MSAA anti-aliasing to disabled, 2x or 4x\n" #endif " --no-splash Don't show the splash screen during startup\n" +#if defined(APPLICATION_UPDATER) +" --no-update-check Don't check for application updates during startup\n" +#endif " --gamelist-only Skip automatic game ROM search, only read from gamelist.xml\n" " --ignore-gamelist Ignore the gamelist.xml files\n" " --show-hidden-files Show hidden files and folders\n" @@ -587,6 +599,10 @@ int main(int argc, char* argv[]) << PROGRAM_RELEASE_NUMBER << "), built " << PROGRAM_BUILT_STRING; if (portableMode) { LOG(LogInfo) << "Running in portable mode"; + Settings::getInstance()->setBool("PortableMode", true); + } + else { + Settings::getInstance()->setBool("PortableMode", false); } // Always close the log on exit. @@ -676,6 +692,15 @@ int main(int argc, char* argv[]) renderer = Renderer::getInstance(); window = Window::getInstance(); + +#if defined(APPLICATION_UPDATER) + std::unique_ptr applicationUpdater; + if (!noUpdateCheck) { + applicationUpdater = std::make_unique(); + applicationUpdater->checkForUpdates(); + } +#endif + ViewController::getInstance(); CollectionSystemsManager::getInstance(); Screensaver screensaver; @@ -743,9 +768,14 @@ int main(int argc, char* argv[]) } if (!SystemData::sStartupExitSignal) { - if (loadSystemsStatus == loadSystemsReturnCode::LOADING_OK) + std::string updaterResults; + if (loadSystemsStatus == loadSystemsReturnCode::LOADING_OK) { ThemeData::themeLoadedLogOutput(); - +#if defined(APPLICATION_UPDATER) + if (!noUpdateCheck) + applicationUpdater->getResults(updaterResults); +#endif + } // Open the input configuration GUI if the force flag was passed from the command line. if (!loadSystemsStatus) { if (forceInputConfig) { @@ -762,12 +792,22 @@ int main(int argc, char* argv[]) lastTime = SDL_GetTicks(); +#if defined(APPLICATION_UPDATER) + if (!noUpdateCheck) + applicationUpdater.reset(); +#endif + LOG(LogInfo) << "Application startup time: " << std::chrono::duration_cast( std::chrono::system_clock::now() - applicationStartTime) .count() << " ms"; +#if defined(APPLICATION_UPDATER) + if (updaterResults != "") + ViewController::getInstance()->updateAvailableDialog(updaterResults); +#endif + // Main application loop. if (!SystemData::sStartupExitSignal) { diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 98c2a65df..bf4e5a545 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -208,6 +208,15 @@ void ViewController::invalidAlternativeEmulatorDialog() "INTERFACE IN THE 'OTHER SETTINGS' MENU")); } +void ViewController::updateAvailableDialog(const std::string& message) +{ + mWindow->pushGui(new GuiMsgBox(getHelpStyle(), message, "OK", nullptr, "", nullptr, "", nullptr, + true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.85f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); +} + void ViewController::goToStart(bool playTransition) { // Needed to avoid segfaults during emergency shutdown. diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 87ff2294d..db127ea9e 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -36,6 +36,7 @@ public: void invalidSystemsFileDialog(); void noGamesDialog(); void invalidAlternativeEmulatorDialog(); + void updateAvailableDialog(const std::string& message); // Try to completely populate the GamelistView map. // Caches things so there's no pauses during transitions. diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index f720ed02a..fbbfff1f6 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -6,6 +6,7 @@ // HTTP request functions. // Used by Scraper, GamesDBJSONScraper, GamesDBJSONScraperResources and // ScreenScraper to download game information and media files. +// Also used by ApplicationUpdater to check for application updates. // #include "HttpReq.h" diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index ca45609a3..8bef62a3a 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -6,6 +6,7 @@ // HTTP request functions. // Used by Scraper, GamesDBJSONScraper, GamesDBJSONScraperResources and // ScreenScraper to download game information and media files. +// Also used by ApplicationUpdater to check for application updates. // #ifndef ES_CORE_HTTP_REQ_H diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 7bd9482ee..cd63f8936 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -5,6 +5,7 @@ // // Functions to read from and write to the configuration file es_settings.xml. // The default values for the application settings are defined here as well. +// This class is not thread safe. // #include "Settings.h" @@ -40,6 +41,7 @@ namespace "Debug", // --debug // These options are only used internally during the application session: + "PortableMode", "DebugGrid", "DebugText", "DebugImage", @@ -253,6 +255,8 @@ void Settings::setDefaults() mStringMap["KeyboardQuitShortcut"] = {"AltF4", "AltF4"}; #endif mStringMap["SaveGamelistsMode"] = {"always", "always"}; + mStringMap["ApplicationUpdaterFrequency"] = {"always", "always"}; + mBoolMap["ApplicationUpdaterPrereleases"] = {false, false}; #if defined(_WIN64) mBoolMap["HideTaskbar"] = {false, false}; #endif @@ -322,6 +326,8 @@ void Settings::setDefaults() // mStringMap["ApplicationVersion"] = {"", ""}; + mStringMap["ApplicationUpdaterLastCheck"] = {"", ""}; + mBoolMap["PortableMode"] = {false, false}; mBoolMap["DebugGrid"] = {false, false}; mBoolMap["DebugText"] = {false, false}; mBoolMap["DebugImage"] = {false, false}; diff --git a/es-core/src/Settings.h b/es-core/src/Settings.h index 9ded6f693..c35d871a2 100644 --- a/es-core/src/Settings.h +++ b/es-core/src/Settings.h @@ -5,6 +5,7 @@ // // Functions to read from and write to the configuration file es_settings.xml. // The default values for the application settings are defined here as well. +// This class is not thread safe. // #ifndef ES_CORE_SETTINGS_H