ES-DE/es-app/src/ApplicationUpdater.cpp
Leon Styhre 63b02c2ad0 Simplified ApplicationUpdater by only using the release number when checking for updates
Also added more error checking by checking for blank name, filename, url and md5 keys in latest_release.json
2023-07-05 17:20:56 +02:00

461 lines
17 KiB
C++

// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// ApplicationUpdater.cpp
//
// Checks for application updates.
// Used in conjunction with GuiApplicationUpdater.
//
#include "ApplicationUpdater.h"
#include "EmulationStation.h"
#include "Log.h"
#include "Settings.h"
#include "resources/ResourceManager.h"
#include "utils/StringUtil.h"
#include "utils/TimeUtil.h"
#include "rapidjson/document.h"
#include "rapidjson/error/en.h"
#include <SDL2/SDL_timer.h>
#include <algorithm>
#include <deque>
#define LOCAL_TESTING_FILE false
#define MAX_DOWNLOAD_TIME 1
ApplicationUpdater::ApplicationUpdater()
: mPackageType {PackageType::UNKNOWN}
, mTimer {0}
, mMaxTime {0}
, mAbortDownload {false}
, mApplicationShutdown {false}
, mCheckedForUpdate {false}
, mNewVersion {false}
{
mUrl = "https://gitlab.com/api/v4/projects/18817634/repository/files/latest_release.json/"
"raw?ref=master";
#if defined(_WIN64)
if (Settings::getInstance()->getBool("PortableMode"))
mPackageType = PackageType::WINDOWS_PORTABLE;
else
mPackageType = PackageType::WINDOWS_INSTALLER;
#elif defined(MACOS_APPLE_CPU)
mPackageType = PackageType::MACOS_APPLE;
#elif defined(MACOS_INTEL_CPU)
mPackageType = PackageType::MACOS_INTEL;
#elif defined(STEAM_DECK)
mPackageType = PackageType::LINUX_STEAM_DECK_APPIMAGE;
#elif defined(APPIMAGE_BUILD)
mPackageType = PackageType::LINUX_APPIMAGE;
#elif defined(LINUX_DEB_PACKAGE)
mPackageType = PackageType::LINUX_DEB;
#endif
}
ApplicationUpdater::~ApplicationUpdater()
{
// This is needed if getResults() was never called.
mApplicationShutdown = true;
if (mThread)
mThread->join();
}
ApplicationUpdater& ApplicationUpdater::getInstance()
{
static ApplicationUpdater instance;
return instance;
}
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<std::thread>(&ApplicationUpdater::updaterThread, this);
}
else {
LOG(LogInfo) << "Skipping application update check as not enough time has passed "
"since the last run (configured to check \""
<< updateFrequency << "\")";
}
}
void ApplicationUpdater::updaterThread()
{
if (!downloadFile())
compareVersions();
mRequest.reset();
}
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<HttpReq>(std::make_unique<HttpReq>(mUrl));
while (mTimer < mMaxTime || !mAbortDownload) {
SDL_Delay(10);
try {
update();
}
catch (std::runtime_error& e) {
mLogWarning = "ApplicationUpdater: Couldn't download \"latest_release.json\": " +
std::string {e.what()};
return true;
}
if (mStatus == ASYNC_DONE || mApplicationShutdown)
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.HasParseError() && doc.HasMember("message") && doc["message"].IsString()) {
mLogWarning = "ApplicationUpdater: Couldn't download \"latest_release.json\", received "
"server response \"" +
std::string {doc["message"].GetString()} + "\"";
return true;
}
LOG(LogDebug)
<< "ApplicationUpdater::downloadFile(): Downloaded \"latest_release.json\" in "
<< mTimer - startTime << " milliseconds";
try {
parseFile();
}
catch (std::runtime_error& e) {
mLogError = "ApplicationUpdater: Couldn't parse \"latest_release.json\": " +
std::string {e.what()};
return true;
}
}
else if (mApplicationShutdown) {
return true;
}
else if (mTimer - startTime - 10 > MAX_DOWNLOAD_TIME * 1000) {
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);
rapidjson::Document doc;
#if (LOCAL_TESTING_FILE)
LOG(LogWarning) << "ApplicationUpdater: Using local \"latest_release.json\" testing file";
const std::string localReleaseFile {Utils::FileSystem::getHomePath() +
"/.emulationstation/latest_release.json"};
if (!Utils::FileSystem::exists(localReleaseFile))
throw std::runtime_error("Local testing file not found");
const ResourceData& localReleaseFileData {
ResourceManager::getInstance().getFileData(localReleaseFile)};
doc.Parse(reinterpret_cast<const char*>(localReleaseFileData.ptr.get()),
localReleaseFileData.length);
#else
const std::string fileContents {mRequest->getContent()};
doc.Parse(&fileContents[0], fileContents.length());
#endif
if (doc.HasParseError())
throw std::runtime_error(rapidjson::GetParseError_En(doc.GetParseError()));
const std::vector<std::string> 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<int>(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 (package.name == "")
throw std::runtime_error(
"Invalid file contents, package \"name\" key is blank");
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 (package.filename == "")
throw std::runtime_error(
"Invalid file contents, package \"filename\" key is blank");
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 (package.url == "")
throw std::runtime_error(
"Invalid file contents, package \"url\" key is blank");
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 (package.md5 == "")
throw std::runtime_error(
"Invalid file contents, package \"md5\" key is blank");
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<Release*> releaseTypes {&mStableRelease};
if (mPrerelease.releaseNum != "") {
#if defined(IS_PRERELEASE)
releaseTypes.emplace_front(&mPrerelease);
#else
if (Settings::getInstance()->getBool("ApplicationUpdaterPrereleases"))
releaseTypes.emplace_front(&mPrerelease);
#endif
}
mNewVersion = false;
for (auto& releaseType : releaseTypes) {
// That these keys are blank is not technically wrong as the prerelease is optional,
// therefore we just check them here and not in the parseFile() function.
if (releaseType->version == "" || releaseType->releaseNum == "" || releaseType->date == "")
continue;
mNewVersion = (std::stoi(releaseType->releaseNum) > PROGRAM_RELEASE_NUMBER);
if (mNewVersion) {
for (auto& package : releaseType->packages) {
if (mPackageType == PackageType::WINDOWS_PORTABLE &&
package.name == "WindowsPortable")
mPackage = package;
else if (mPackageType == PackageType::WINDOWS_INSTALLER &&
package.name == "WindowsInstaller")
mPackage = package;
else if (mPackageType == PackageType::MACOS_APPLE && package.name == "macOSApple")
mPackage = package;
else if (mPackageType == PackageType::MACOS_INTEL && package.name == "macOSIntel")
mPackage = package;
else if (mPackageType == PackageType::LINUX_DEB && package.name == "LinuxDEB")
mPackage = package;
else if (mPackageType == PackageType::LINUX_APPIMAGE &&
package.name == "LinuxAppImage")
mPackage = package;
else if (mPackageType == PackageType::LINUX_STEAM_DECK_APPIMAGE &&
package.name == "LinuxSteamDeckAppImage")
mPackage = package;
}
mPackage.version = releaseType->version;
// Cut the message to 280 characters so we don't make the message box exceedingly large.
mPackage.message = mPackage.message.substr(0, 280);
mLogInfo = "A new ";
mLogInfo.append(releaseType == &mStableRelease ? "stable release" : "prerelease")
.append(" is available for download at https://es-de.org: ")
.append(releaseType->version)
.append(" (r")
.append(releaseType->releaseNum)
.append("), release date: ")
.append(releaseType->date);
mResults.append("New ");
if (releaseType == &mPrerelease) {
mResults.append("prerelease available:\n")
.append(releaseType->version)
.append(" (")
.append(releaseType->date)
.append(")");
}
else {
mResults.append("release available: ").append(releaseType->version);
}
if (mPackage.name != "LinuxAppImage" && mPackage.name != "LinuxSteamDeckAppImage")
mResults.append("\nIt can be downloaded from\n").append("https://es-de.org");
// mResults.append("\nFor more information visit\n").append("https://es-de.org");
if (mPackage.message != "")
mResults.append("\n").append(mPackage.message);
mResults = Utils::String::toUpper(mResults);
break;
}
}
if (!mNewVersion) {
mLogInfo = "No application updates available";
}
mCheckedForUpdate = true;
}
bool ApplicationUpdater::getResults()
{
mAbortDownload = true;
if (mThread) {
mThread->join();
mThread.reset();
if (mCheckedForUpdate) {
Settings::getInstance()->setString(
"ApplicationUpdaterLastCheck",
Utils::Time::DateTime(Utils::Time::now()).getIsoString());
Settings::getInstance()->saveFile();
}
}
// We output these messages here instead of immediately when they occur so that they will
// always be printed at the end of the application startup.
if (mLogError != "") {
LOG(LogError) << mLogError;
}
if (mLogWarning != "") {
LOG(LogWarning) << mLogWarning;
}
if (mLogInfo != "") {
LOG(LogInfo) << mLogInfo;
}
if (mNewVersion && mPackage.name == "") {
LOG(LogWarning)
<< "ApplicationUpdater: Couldn't find a package type matching current platform";
}
return mNewVersion;
}