Merge pull request 'Fix Updater Logic & Remove version.txt requirement w/ refined SCM' (#126) from fix/updater into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/126
This commit is contained in:
Collecting
2026-02-05 01:58:31 +01:00
2 changed files with 241 additions and 128 deletions

View File

@@ -1,30 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/updater/updater_service.h"
#include "citron/uisettings.h" #include "citron/uisettings.h"
#include "common/logging/log.h" #include "citron/updater/updater_service.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/scm_rev.h" #include "common/scm_rev.h"
#include <QApplication> #include <QApplication>
#include <QStandardPaths> #include <QCoreApplication>
#include <QJsonDocument> #include <QCryptographicHash>
#include <QJsonObject>
#include <QJsonArray>
#include <QRegularExpression>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QTimer> #include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QSslConfiguration>
#include <QCoreApplication>
#include <QSslSocket>
#include <QCryptographicHash>
#include <QProcess> #include <QProcess>
#include <QRegularExpression>
#include <QSettings> #include <QSettings>
#include <QSslConfiguration>
#include <QSslSocket>
#include <QStandardPaths>
#include <QTimer>
#ifdef CITRON_ENABLE_LIBARCHIVE #ifdef CITRON_ENABLE_LIBARCHIVE
#include <archive.h> #include <archive.h>
@@ -35,17 +35,33 @@
#include <regex> #include <regex>
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h>
#include <shellapi.h> #include <shellapi.h>
#include <windows.h>
#endif #endif
namespace Updater { namespace Updater {
const std::string STABLE_UPDATE_URL = "https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases"; const std::string STABLE_UPDATE_URL =
const std::string NIGHTLY_UPDATE_URL = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases"; "https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases";
const std::string NIGHTLY_UPDATE_URL =
"https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases";
std::string ExtractCommitHash(const std::string& version_string) { std::string ExtractCommitHash(const std::string& version_string) {
std::regex re("\\b([0-9a-fA-F]{7,40})\\b"); // Hashes in git describe often start with 'g'.
// We match 7-40 hex characters, optionally preceded by 'g'.
std::regex re("(?:\\b|[gG])([0-9a-fA-F]{7,40})\\b");
std::smatch match;
if (std::regex_search(version_string, match, re) && match.size() > 1) {
return match[1].str();
}
return "";
}
std::string ExtractVersionTag(const std::string& version_string) {
// Matches tag parts like v1.2.3 or 2026.02.1 at the start of the string.
// We stop at the first hyphen to avoid the -count-gHASH part.
std::regex re("^v?([0-9.]+)");
std::smatch match; std::smatch match;
if (std::regex_search(version_string, match, re) && match.size() > 1) { if (std::regex_search(version_string, match, re) && match.size() > 1) {
return match[1].str(); return match[1].str();
@@ -65,7 +81,6 @@ QByteArray GetFileChecksum(const std::filesystem::path& file_path) {
return QByteArray(); return QByteArray();
} }
UpdaterService::UpdaterService(QObject* parent) : QObject(parent) { UpdaterService::UpdaterService(QObject* parent) : QObject(parent) {
network_manager = std::make_unique<QNetworkAccessManager>(this); network_manager = std::make_unique<QNetworkAccessManager>(this);
InitializeSSL(); InitializeSSL();
@@ -91,22 +106,27 @@ void UpdaterService::InitializeSSL() {
// Check if SSL is supported // Check if SSL is supported
if (!QSslSocket::supportsSsl()) { if (!QSslSocket::supportsSsl()) {
LOG_WARNING(Frontend, "SSL support not available"); LOG_WARNING(Frontend, "SSL support not available");
LOG_WARNING(Frontend, "Build-time SSL version: {}", QSslSocket::sslLibraryBuildVersionString().toStdString()); LOG_WARNING(Frontend, "Build-time SSL version: {}",
LOG_WARNING(Frontend, "Runtime SSL version: {}", QSslSocket::sslLibraryVersionString().toStdString()); QSslSocket::sslLibraryBuildVersionString().toStdString());
LOG_WARNING(Frontend, "Runtime SSL version: {}",
QSslSocket::sslLibraryVersionString().toStdString());
#ifdef _WIN32 #ifdef _WIN32
// Try to provide helpful information about missing DLLs // Try to provide helpful information about missing DLLs
std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); std::filesystem::path app_dir =
std::filesystem::path(QCoreApplication::applicationDirPath().toStdString());
std::filesystem::path crypto_dll = app_dir / "libcrypto-3-x64.dll"; std::filesystem::path crypto_dll = app_dir / "libcrypto-3-x64.dll";
std::filesystem::path ssl_dll = app_dir / "libssl-3-x64.dll"; std::filesystem::path ssl_dll = app_dir / "libssl-3-x64.dll";
LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}", std::filesystem::exists(crypto_dll)); LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}",
std::filesystem::exists(crypto_dll));
LOG_WARNING(Frontend, "libssl-3-x64.dll exists: {}", std::filesystem::exists(ssl_dll)); LOG_WARNING(Frontend, "libssl-3-x64.dll exists: {}", std::filesystem::exists(ssl_dll));
#endif #endif
return; return;
} }
LOG_INFO(Frontend, "SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString()); LOG_INFO(Frontend, "SSL library version: {}",
QSslSocket::sslLibraryVersionString().toStdString());
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
auto certs = QSslConfiguration::systemCaCertificates(); auto certs = QSslConfiguration::systemCaCertificates();
@@ -126,22 +146,27 @@ void UpdaterService::CheckForUpdates() {
return; return;
} }
QSettings settings; QSettings settings;
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString(); QString channel =
std::string update_url = (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL; settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString();
std::string update_url =
(channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL;
LOG_INFO(Frontend, "Selected update channel: {}", channel.toStdString()); LOG_INFO(Frontend, "Selected update channel: {}", channel.toStdString());
LOG_INFO(Frontend, "Checking for updates from: {}", update_url); LOG_INFO(Frontend, "Checking for updates from: {}", update_url);
QUrl url{QString::fromStdString(update_url)}; QUrl url{QString::fromStdString(update_url)};
QNetworkRequest request{url}; QNetworkRequest request{url};
request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0"));
request.setRawHeader("Accept", QByteArrayLiteral("application/json")); request.setRawHeader("Accept", QByteArrayLiteral("application/json"));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
current_reply = network_manager->get(request); current_reply = network_manager->get(request);
connect(current_reply, &QNetworkReply::finished, this, [this, channel]() { connect(current_reply, &QNetworkReply::finished, this, [this, channel]() {
if (!current_reply) return; if (!current_reply)
return;
if (current_reply->error() == QNetworkReply::NoError) { if (current_reply->error() == QNetworkReply::NoError) {
ParseUpdateResponse(current_reply->readAll(), channel); ParseUpdateResponse(current_reply->readAll(), channel);
} else { } else {
emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString())); emit UpdateError(
QStringLiteral("Update check failed: %1").arg(current_reply->errorString()));
} }
current_reply->deleteLater(); current_reply->deleteLater();
current_reply = nullptr; current_reply = nullptr;
@@ -149,7 +174,8 @@ void UpdaterService::CheckForUpdates() {
} }
void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) { void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) {
if (!QSslSocket::supportsSsl()) return; if (!QSslSocket::supportsSsl())
return;
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
sslConfig.setProtocol(QSsl::SecureProtocols); sslConfig.setProtocol(QSsl::SecureProtocols);
@@ -173,7 +199,8 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) {
#ifdef _WIN32 #ifdef _WIN32
if (!CreateBackup()) { if (!CreateBackup()) {
emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup")); emit UpdateCompleted(UpdateResult::PermissionError,
QStringLiteral("Failed to create backup"));
update_in_progress.store(false); update_in_progress.store(false);
return; return;
} }
@@ -181,15 +208,18 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) {
QUrl url(QString::fromStdString(download_url)); QUrl url(QString::fromStdString(download_url));
QNetworkRequest request(url); QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
current_reply = network_manager->get(request); current_reply = network_manager->get(request);
connect(current_reply, &QNetworkReply::downloadProgress, this, &UpdaterService::OnDownloadProgress); connect(current_reply, &QNetworkReply::downloadProgress, this,
&UpdaterService::OnDownloadProgress);
connect(current_reply, &QNetworkReply::finished, this, &UpdaterService::OnDownloadFinished); connect(current_reply, &QNetworkReply::finished, this, &UpdaterService::OnDownloadFinished);
connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError); connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError);
} }
void UpdaterService::CancelUpdate() { void UpdaterService::CancelUpdate() {
if (!update_in_progress.load()) return; if (!update_in_progress.load())
return;
cancel_requested.store(true); cancel_requested.store(true);
if (current_reply) { if (current_reply) {
current_reply->abort(); current_reply->abort();
@@ -201,21 +231,12 @@ void UpdaterService::CancelUpdate() {
std::string UpdaterService::GetCurrentVersion() const { std::string UpdaterService::GetCurrentVersion() const {
QSettings settings; QSettings settings;
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); QString channel =
settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
// If the user's setting is Nightly, we must ignore version.txt and only use the commit hash. const std::string build_version = Common::g_build_version;
if (channel == QStringLiteral("Nightly")) {
std::string build_version = Common::g_build_version;
if (!build_version.empty()) {
std::string hash = ExtractCommitHash(build_version);
if (!hash.empty()) {
return hash;
}
}
return ""; // Fallback if no hash is found
}
// Otherwise (channel is Stable), we prioritize version.txt. // First priority: version.txt (only relevant for Stable installations)
std::filesystem::path search_path; std::filesystem::path search_path;
#ifdef __linux__ #ifdef __linux__
const char* appimage_path_env = qgetenv("APPIMAGE").constData(); const char* appimage_path_env = qgetenv("APPIMAGE").constData();
@@ -235,22 +256,37 @@ std::string UpdaterService::GetCurrentVersion() const {
std::string version_from_file; std::string version_from_file;
std::getline(file, version_from_file); std::getline(file, version_from_file);
if (!version_from_file.empty()) { if (!version_from_file.empty()) {
return version_from_file; // Trim trailing metadata/whitespace from version.txt (e.g. "1.0.0 (Release)")
return version_from_file.substr(0, version_from_file.find_first_of(" \t\r\n"));
} }
} }
} }
// Fallback for Stable channel: If version.txt is missing, use the commit hash. // If the user's setting is Nightly, we prioritize the commit hash.
// This allows a nightly build to correctly check for a stable update. if (channel == QStringLiteral("Nightly")) {
std::string build_version = Common::g_build_version; if (!build_version.empty()) {
if (!build_version.empty()) { std::string hash = ExtractCommitHash(build_version);
std::string hash = ExtractCommitHash(build_version); if (!hash.empty()) {
if (!hash.empty()) { return hash;
return hash; }
}
} else {
// Otherwise (channel is Stable), we try to extract the tag from build_version if
// version.txt is missing. This happens when a Nightly user checks for Stable updates.
std::string tag = ExtractVersionTag(build_version);
if (!tag.empty()) {
return tag;
} }
} }
return ""; // Common fallback: try to extract a hash if we haven't found a tag yet,
// otherwise just return the full build version.
std::string hash_fallback = ExtractCommitHash(build_version);
if (!hash_fallback.empty()) {
return hash_fallback;
}
return build_version;
} }
bool UpdaterService::IsUpdateInProgress() const { bool UpdaterService::IsUpdateInProgress() const {
@@ -270,14 +306,17 @@ void UpdaterService::OnDownloadFinished() {
QByteArray downloaded_data = current_reply->readAll(); QByteArray downloaded_data = current_reply->readAll();
QSettings settings; QSettings settings;
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); QString channel =
settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
#if defined(_WIN32) #if defined(_WIN32)
QString filename = QStringLiteral("citron_update_%1.zip").arg(QString::fromStdString(current_update_info.version)); QString filename = QStringLiteral("citron_update_%1.zip")
.arg(QString::fromStdString(current_update_info.version));
std::filesystem::path download_path = temp_download_path / filename.toStdString(); std::filesystem::path download_path = temp_download_path / filename.toStdString();
QFile file(QString::fromStdString(download_path.string())); QFile file(QString::fromStdString(download_path.string()));
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
emit UpdateCompleted(UpdateResult::Failed, QStringLiteral("Failed to save downloaded file")); emit UpdateCompleted(UpdateResult::Failed,
QStringLiteral("Failed to save downloaded file"));
update_in_progress.store(false); update_in_progress.store(false);
return; return;
} }
@@ -293,7 +332,8 @@ void UpdaterService::OnDownloadFinished() {
emit UpdateInstallProgress(10, QStringLiteral("Extracting update archive...")); emit UpdateInstallProgress(10, QStringLiteral("Extracting update archive..."));
std::filesystem::path extract_path = temp_download_path / "extracted"; std::filesystem::path extract_path = temp_download_path / "extracted";
if (!ExtractArchive(download_path, extract_path)) { if (!ExtractArchive(download_path, extract_path)) {
emit UpdateCompleted(UpdateResult::ExtractionError, QStringLiteral("Failed to extract update archive")); emit UpdateCompleted(UpdateResult::ExtractionError,
QStringLiteral("Failed to extract update archive"));
update_in_progress.store(false); update_in_progress.store(false);
return; return;
} }
@@ -305,7 +345,9 @@ void UpdaterService::OnDownloadFinished() {
return; return;
} }
emit UpdateInstallProgress(100, QStringLiteral("Update completed successfully!")); emit UpdateInstallProgress(100, QStringLiteral("Update completed successfully!"));
emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update installed successfully. Please restart the application.")); emit UpdateCompleted(
UpdateResult::Success,
QStringLiteral("Update installed successfully. Please restart the application."));
update_in_progress.store(false); update_in_progress.store(false);
CleanupFiles(); CleanupFiles();
}); });
@@ -344,13 +386,17 @@ void UpdaterService::OnDownloadFinished() {
} else { } else {
// Create the backup copy of the old AppImage // Create the backup copy of the old AppImage
std::string current_version = GetCurrentVersion(); std::string current_version = GetCurrentVersion();
std::string backup_filename = "citron-backup-" + (current_version.empty() ? "unknown" : current_version) + ".AppImage"; std::string backup_filename = "citron-backup-" +
(current_version.empty() ? "unknown" : current_version) +
".AppImage";
std::filesystem::path backup_filepath = backup_dir / backup_filename; std::filesystem::path backup_filepath = backup_dir / backup_filename;
std::filesystem::copy_file(original_appimage_path, backup_filepath, std::filesystem::copy_options::overwrite_existing, ec); std::filesystem::copy_file(original_appimage_path, backup_filepath,
std::filesystem::copy_options::overwrite_existing, ec);
if (ec) { if (ec) {
LOG_ERROR(Frontend, "Failed to copy AppImage to backup location: {}", ec.message()); LOG_ERROR(Frontend, "Failed to copy AppImage to backup location: {}", ec.message());
} else { } else {
LOG_INFO(Frontend, "Created backup of old AppImage at: {}", backup_filepath.string()); LOG_INFO(Frontend, "Created backup of old AppImage at: {}",
backup_filepath.string());
} }
} }
} }
@@ -365,9 +411,10 @@ void UpdaterService::OnDownloadFinished() {
new_file.write(downloaded_data); new_file.write(downloaded_data);
new_file.close(); new_file.close();
if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
QFileDevice::ReadGroup | QFileDevice::ExeGroup | QFileDevice::ExeOwner | QFileDevice::ReadGroup |
QFileDevice::ReadOther | QFileDevice::ExeOther)) { QFileDevice::ExeGroup | QFileDevice::ReadOther |
QFileDevice::ExeOther)) {
emit UpdateError(QStringLiteral("Failed to make the new AppImage executable.")); emit UpdateError(QStringLiteral("Failed to make the new AppImage executable."));
std::filesystem::remove(new_appimage_path, ec); std::filesystem::remove(new_appimage_path, ec);
update_in_progress.store(false); update_in_progress.store(false);
@@ -397,7 +444,8 @@ void UpdaterService::OnDownloadFinished() {
} }
LOG_INFO(Frontend, "AppImage updated successfully."); LOG_INFO(Frontend, "AppImage updated successfully.");
emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application.")); emit UpdateCompleted(UpdateResult::Success,
QStringLiteral("Update successful. Please restart the application."));
update_in_progress.store(false); update_in_progress.store(false);
#endif #endif
} }
@@ -430,42 +478,50 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response, const QStri
if (channel == QStringLiteral("Stable")) { if (channel == QStringLiteral("Stable")) {
latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString(); latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString();
} else { } else {
latest_version = ExtractCommitHash(release_obj.value(QStringLiteral("name")).toString().toStdString()); latest_version = ExtractCommitHash(
release_obj.value(QStringLiteral("name")).toString().toStdString());
} }
if (latest_version.empty()) continue; if (latest_version.empty())
continue;
UpdateInfo update_info; UpdateInfo update_info;
update_info.version = latest_version; update_info.version = latest_version;
update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString(); update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString();
update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString(); update_info.release_date =
release_obj.value(QStringLiteral("published_at")).toString().toStdString();
QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray(); QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray();
for (const QJsonValue& asset_value : assets) { for (const QJsonValue& asset_value : assets) {
QJsonObject asset_obj = asset_value.toObject(); QJsonObject asset_obj = asset_value.toObject();
QString asset_name = asset_obj.value(QStringLiteral("name")).toString(); QString asset_name = asset_obj.value(QStringLiteral("name")).toString();
#if defined(__linux__) #if defined(__linux__)
if (asset_name.endsWith(QStringLiteral(".AppImage"))) { if (asset_name.endsWith(QStringLiteral(".AppImage"))) {
DownloadOption option; DownloadOption option;
option.name = asset_name.toStdString(); option.name = asset_name.toStdString();
option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); option.url = asset_obj.value(QStringLiteral("browser_download_url"))
.toString()
.toStdString();
update_info.download_options.push_back(option); update_info.download_options.push_back(option);
} }
#elif defined(_WIN32) #elif defined(_WIN32)
// For Windows, find the .zip file but explicitly skip PGO builds. // For Windows, find the .zip file but explicitly skip PGO builds.
if (asset_name.endsWith(QStringLiteral(".zip")) && !asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) { if (asset_name.endsWith(QStringLiteral(".zip")) &&
!asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) {
DownloadOption option; DownloadOption option;
option.name = asset_name.toStdString(); option.name = asset_name.toStdString();
option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); option.url = asset_obj.value(QStringLiteral("browser_download_url"))
.toString()
.toStdString();
update_info.download_options.push_back(option); update_info.download_options.push_back(option);
} }
#endif #endif
} }
if (!update_info.download_options.empty()) { if (!update_info.download_options.empty()) {
update_info.is_newer_version = CompareVersions(GetCurrentVersion(), update_info.version); update_info.is_newer_version =
CompareVersions(GetCurrentVersion(), update_info.version);
current_update_info = update_info; current_update_info = update_info;
emit UpdateCheckCompleted(update_info.is_newer_version, update_info); emit UpdateCheckCompleted(update_info.is_newer_version, update_info);
return; return;
@@ -487,28 +543,34 @@ bool UpdaterService::CompareVersions(const std::string& current, const std::stri
} }
#ifdef _WIN32 #ifdef _WIN32
bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path,
const std::filesystem::path& extract_path) {
#ifdef CITRON_ENABLE_LIBARCHIVE #ifdef CITRON_ENABLE_LIBARCHIVE
struct archive* a = archive_read_new(); struct archive* a = archive_read_new();
struct archive* ext = archive_write_disk_new(); struct archive* ext = archive_write_disk_new();
if (!a || !ext) return false; if (!a || !ext)
return false;
archive_read_support_format_7zip(a); archive_read_support_format_7zip(a);
archive_read_support_filter_all(a); archive_read_support_filter_all(a);
archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM);
archive_write_disk_set_standard_lookup(ext); archive_write_disk_set_standard_lookup(ext);
if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) return false; if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK)
return false;
EnsureDirectoryExists(extract_path); EnsureDirectoryExists(extract_path);
struct archive_entry* entry; struct archive_entry* entry;
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
if (cancel_requested.load()) break; if (cancel_requested.load())
break;
std::filesystem::path entry_path = extract_path / archive_entry_pathname(entry); std::filesystem::path entry_path = extract_path / archive_entry_pathname(entry);
archive_entry_set_pathname(entry, entry_path.string().c_str()); archive_entry_set_pathname(entry, entry_path.string().c_str());
if (archive_write_header(ext, entry) != ARCHIVE_OK) continue; if (archive_write_header(ext, entry) != ARCHIVE_OK)
continue;
const void* buff; const void* buff;
size_t size; size_t size;
la_int64_t offset; la_int64_t offset;
while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) { while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) {
if (cancel_requested.load()) break; if (cancel_requested.load())
break;
archive_write_data_block(ext, buff, size, offset); archive_write_data_block(ext, buff, size, offset);
} }
archive_write_finish_entry(ext); archive_write_finish_entry(ext);
@@ -524,12 +586,18 @@ bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, c
} }
#if !defined(CITRON_ENABLE_LIBARCHIVE) #if !defined(CITRON_ENABLE_LIBARCHIVE)
bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path,
const std::filesystem::path& extract_path) {
EnsureDirectoryExists(extract_path); EnsureDirectoryExists(extract_path);
std::string sevenzip_cmd = "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; std::string sevenzip_cmd =
if (std::system(sevenzip_cmd.c_str()) == 0) return true; "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y";
std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" + archive_path.string() + "\\\" -DestinationPath \\\"" + extract_path.string() + "\\\" -Force\""; if (std::system(sevenzip_cmd.c_str()) == 0)
if (std::system(powershell_cmd.c_str()) == 0) return true; return true;
std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" +
archive_path.string() + "\\\" -DestinationPath \\\"" +
extract_path.string() + "\\\" -Force\"";
if (std::system(powershell_cmd.c_str()) == 0)
return true;
LOG_ERROR(Frontend, "Failed to extract archive automatically."); LOG_ERROR(Frontend, "Failed to extract archive automatically.");
return false; return false;
} }
@@ -548,12 +616,15 @@ bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) {
std::filesystem::path staging_path = app_directory / "update_staging"; std::filesystem::path staging_path = app_directory / "update_staging";
EnsureDirectoryExists(staging_path); EnsureDirectoryExists(staging_path);
for (const auto& entry : std::filesystem::recursive_directory_iterator(source_path)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(source_path)) {
if (cancel_requested.load()) return false; if (cancel_requested.load())
return false;
if (entry.is_regular_file()) { if (entry.is_regular_file()) {
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), source_path); std::filesystem::path relative_path =
std::filesystem::relative(entry.path(), source_path);
std::filesystem::path staging_dest = staging_path / relative_path; std::filesystem::path staging_dest = staging_path / relative_path;
std::filesystem::create_directories(staging_dest.parent_path()); std::filesystem::create_directories(staging_dest.parent_path());
std::filesystem::copy_file(entry.path(), staging_dest, std::filesystem::copy_options::overwrite_existing); std::filesystem::copy_file(entry.path(), staging_dest,
std::filesystem::copy_options::overwrite_existing);
} }
} }
std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
@@ -585,14 +656,16 @@ bool UpdaterService::CreateBackup() {
std::filesystem::remove_all(backup_dir); std::filesystem::remove_all(backup_dir);
} }
std::filesystem::create_directories(backup_dir); std::filesystem::create_directories(backup_dir);
std::vector<std::string> backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", "*.pdb"}; std::vector<std::string> backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll",
"*.pdb"};
for (const auto& entry : std::filesystem::directory_iterator(app_directory)) { for (const auto& entry : std::filesystem::directory_iterator(app_directory)) {
if (entry.is_regular_file()) { if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string(); std::string filename = entry.path().filename().string();
std::string extension = entry.path().extension().string(); std::string extension = entry.path().extension().string();
bool should_backup = false; bool should_backup = false;
for (const auto& pattern : backup_patterns) { for (const auto& pattern : backup_patterns) {
if (pattern == filename || (pattern.starts_with("*") && pattern.substr(1) == extension)) { if (pattern == filename ||
(pattern.starts_with("*") && pattern.substr(1) == extension)) {
should_backup = true; should_backup = true;
break; break;
} }
@@ -613,11 +686,13 @@ bool UpdaterService::CreateBackup() {
bool UpdaterService::RestoreBackup() { bool UpdaterService::RestoreBackup() {
try { try {
std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion()); std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion());
if (!std::filesystem::exists(backup_dir)) return false; if (!std::filesystem::exists(backup_dir))
return false;
for (const auto& entry : std::filesystem::directory_iterator(backup_dir)) { for (const auto& entry : std::filesystem::directory_iterator(backup_dir)) {
if (entry.is_regular_file()) { if (entry.is_regular_file()) {
std::filesystem::path dest_path = app_directory / entry.path().filename(); std::filesystem::path dest_path = app_directory / entry.path().filename();
std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing); std::filesystem::copy_file(entry.path(), dest_path,
std::filesystem::copy_options::overwrite_existing);
} }
} }
LOG_INFO(Frontend, "Backup restored successfully"); LOG_INFO(Frontend, "Backup restored successfully");
@@ -648,9 +723,15 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi
std::string app_path_str = app_directory.string(); std::string app_path_str = app_directory.string();
std::string exe_path_str = (app_directory / "citron.exe").string(); std::string exe_path_str = (app_directory / "citron.exe").string();
for (auto& ch : staging_path_str) if (ch == '/') ch = '\\'; for (auto& ch : staging_path_str)
for (auto& ch : app_path_str) if (ch == '/') ch = '\\'; if (ch == '/')
for (auto& ch : exe_path_str) if (ch == '/') ch = '\\'; ch = '\\';
for (auto& ch : app_path_str)
if (ch == '/')
ch = '\\';
for (auto& ch : exe_path_str)
if (ch == '/')
ch = '\\';
script << "@echo off\n"; script << "@echo off\n";
script << "setlocal enabledelayedexpansion\n"; script << "setlocal enabledelayedexpansion\n";
@@ -665,7 +746,8 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi
script << "if not errorlevel 1 (\n"; script << "if not errorlevel 1 (\n";
script << " set /a wait_count+=1\n"; script << " set /a wait_count+=1\n";
script << " if !wait_count! gtr 60 (\n"; script << " if !wait_count! gtr 60 (\n";
script << " echo Warning: Citron process still running after 60 seconds, proceeding anyway...\n"; script << " echo Warning: Citron process still running after 60 seconds, proceeding "
"anyway...\n";
script << " goto wait_done\n"; script << " goto wait_done\n";
script << " )\n"; script << " )\n";
script << " timeout /t 1 /nobreak >nul\n"; script << " timeout /t 1 /nobreak >nul\n";
@@ -678,14 +760,17 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi
// Remove read-only attributes from all files in the destination directory // Remove read-only attributes from all files in the destination directory
script << "echo Removing read-only attributes from existing files...\n"; script << "echo Removing read-only attributes from existing files...\n";
script << "attrib -R \"" << app_path_str << "\\*.*\" /S /D >nul 2>&1\n"; script << "attrib -R \"" << app_path_str << "\\*.*\" /S /D >nul 2>&1\n";
script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str << "\\citron.exe\" >nul 2>&1\n"; script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str
script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str << "\\citron_cmd.exe\" >nul 2>&1\n\n"; << "\\citron.exe\" >nul 2>&1\n";
script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str
<< "\\citron_cmd.exe\" >nul 2>&1\n\n";
// Use robocopy for more reliable copying (available on Windows Vista+) // Use robocopy for more reliable copying (available on Windows Vista+)
script << "echo Copying update files...\n"; script << "echo Copying update files...\n";
script << "set /a copy_retries=0\n"; script << "set /a copy_retries=0\n";
script << ":copy_loop\n"; script << ":copy_loop\n";
script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str << "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n"; script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str
<< "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n";
script << "set /a robocopy_exit=!errorlevel!\n"; script << "set /a robocopy_exit=!errorlevel!\n";
script << "REM Robocopy returns 0-7 for success, 8+ for errors\n"; script << "REM Robocopy returns 0-7 for success, 8+ for errors\n";
script << "if !robocopy_exit! geq 8 (\n"; script << "if !robocopy_exit! geq 8 (\n";
@@ -786,10 +871,13 @@ bool UpdaterService::LaunchUpdateHelper() {
bool launched = QProcess::startDetached(QStringLiteral("cmd.exe"), arguments); bool launched = QProcess::startDetached(QStringLiteral("cmd.exe"), arguments);
if (launched) { if (launched) {
LOG_INFO(Frontend, "Update helper script launched successfully from: {}", script_path.string()); LOG_INFO(Frontend, "Update helper script launched successfully from: {}",
script_path.string());
return true; return true;
} else { } else {
LOG_ERROR(Frontend, "Failed to launch update helper script. QProcess::startDetached returned false"); LOG_ERROR(
Frontend,
"Failed to launch update helper script. QProcess::startDetached returned false");
return false; return false;
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
@@ -808,14 +896,16 @@ bool UpdaterService::CleanupFiles() {
std::vector<std::filesystem::path> backup_dirs; std::vector<std::filesystem::path> backup_dirs;
if (std::filesystem::exists(backup_path)) { if (std::filesystem::exists(backup_path)) {
for (const auto& entry : std::filesystem::directory_iterator(backup_path)) { for (const auto& entry : std::filesystem::directory_iterator(backup_path)) {
if (entry.is_directory() && entry.path().filename().string().starts_with("backup_")) { if (entry.is_directory() &&
entry.path().filename().string().starts_with("backup_")) {
backup_dirs.push_back(entry.path()); backup_dirs.push_back(entry.path());
} }
} }
} }
if (backup_dirs.size() > 3) { if (backup_dirs.size() > 3) {
std::sort(backup_dirs.begin(), backup_dirs.end(), std::sort(backup_dirs.begin(), backup_dirs.end(), [](const auto& a, const auto& b) {
[](const auto& a, const auto& b) { return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); }); return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b);
});
for (size_t i = 3; i < backup_dirs.size(); ++i) { for (size_t i = 3; i < backup_dirs.size(); ++i) {
std::filesystem::remove_all(backup_dirs[i]); std::filesystem::remove_all(backup_dirs[i]);
} }
@@ -829,7 +919,9 @@ bool UpdaterService::CleanupFiles() {
} }
std::filesystem::path UpdaterService::GetTempDirectory() const { std::filesystem::path UpdaterService::GetTempDirectory() const {
return std::filesystem::path(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / "citron_updater"; return std::filesystem::path(
QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) /
"citron_updater";
} }
std::filesystem::path UpdaterService::GetApplicationDirectory() const { std::filesystem::path UpdaterService::GetApplicationDirectory() const {
@@ -856,7 +948,8 @@ bool UpdaterService::HasStagedUpdate(const std::filesystem::path& app_directory)
#ifdef _WIN32 #ifdef _WIN32
std::filesystem::path staging_path = app_directory / "update_staging"; std::filesystem::path staging_path = app_directory / "update_staging";
std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
return std::filesystem::exists(staging_path) && std::filesystem::exists(manifest_file) && std::filesystem::is_directory(staging_path); return std::filesystem::exists(staging_path) && std::filesystem::exists(manifest_file) &&
std::filesystem::is_directory(staging_path);
#else #else
return false; return false;
#endif #endif
@@ -867,15 +960,19 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director
try { try {
std::filesystem::path staging_path = app_directory / "update_staging"; std::filesystem::path staging_path = app_directory / "update_staging";
std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file)) return false; if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file))
return false;
LOG_INFO(Frontend, "Applying staged update from: {}", staging_path.string()); LOG_INFO(Frontend, "Applying staged update from: {}", staging_path.string());
std::filesystem::path backup_path_dir = app_directory / "backup_before_update"; std::filesystem::path backup_path_dir = app_directory / "backup_before_update";
if (std::filesystem::exists(backup_path_dir)) std::filesystem::remove_all(backup_path_dir); if (std::filesystem::exists(backup_path_dir))
std::filesystem::remove_all(backup_path_dir);
std::filesystem::create_directories(backup_path_dir); std::filesystem::create_directories(backup_path_dir);
for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) {
if (entry.path().filename() == "update_manifest.txt") continue; if (entry.path().filename() == "update_manifest.txt")
continue;
if (entry.is_regular_file()) { if (entry.is_regular_file()) {
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), staging_path); std::filesystem::path relative_path =
std::filesystem::relative(entry.path(), staging_path);
std::filesystem::path dest_path = app_directory / relative_path; std::filesystem::path dest_path = app_directory / relative_path;
if (std::filesystem::exists(dest_path)) { if (std::filesystem::exists(dest_path)) {
std::filesystem::path backup_dest = backup_path_dir / relative_path; std::filesystem::path backup_dest = backup_path_dir / relative_path;
@@ -883,7 +980,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director
std::filesystem::copy_file(dest_path, backup_dest); std::filesystem::copy_file(dest_path, backup_dest);
} }
std::filesystem::create_directories(dest_path.parent_path()); std::filesystem::create_directories(dest_path.parent_path());
std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing); std::filesystem::copy_file(entry.path(), dest_path,
std::filesystem::copy_options::overwrite_existing);
} }
} }
std::ifstream manifest(manifest_file); std::ifstream manifest(manifest_file);
@@ -897,7 +995,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director
if (!version.empty()) { if (!version.empty()) {
std::filesystem::path version_file = app_directory / "version.txt"; std::filesystem::path version_file = app_directory / "version.txt";
std::ofstream vfile(version_file); std::ofstream vfile(version_file);
if (vfile.is_open()) vfile << version; if (vfile.is_open())
vfile << version;
} }
std::filesystem::remove_all(staging_path); std::filesystem::remove_all(staging_path);
LOG_INFO(Frontend, "Update applied successfully. Version: {}", version); LOG_INFO(Frontend, "Update applied successfully. Version: {}", version);

View File

@@ -3,21 +3,24 @@
#pragma once #pragma once
#include <QObject>
#include <string>
#include <filesystem> #include <filesystem>
#include <QNetworkReply>
#include <memory> #include <memory>
#include <string>
#include <vector> #include <vector>
#include <QNetworkReply>
#include <QObject>
#include <QString>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QString>
namespace Updater { namespace Updater {
// Declarations for helper functions // Declarations for helper functions
QString FormatDateTimeString(const std::string& iso_string); QString FormatDateTimeString(const std::string& iso_string);
std::string ExtractCommitHash(const std::string& version_string); std::string ExtractCommitHash(const std::string& version_string);
std::string ExtractVersionTag(const std::string& version_string);
QByteArray GetFileChecksum(const std::filesystem::path& file_path); QByteArray GetFileChecksum(const std::filesystem::path& file_path);
struct DownloadOption { struct DownloadOption {
@@ -39,7 +42,16 @@ class UpdaterService : public QObject {
Q_OBJECT Q_OBJECT
public: public:
enum class UpdateResult { Success, Failed, Cancelled, NetworkError, ExtractionError, PermissionError, InvalidArchive, NoUpdateAvailable }; enum class UpdateResult {
Success,
Failed,
Cancelled,
NetworkError,
ExtractionError,
PermissionError,
InvalidArchive,
NoUpdateAvailable
};
explicit UpdaterService(QObject* parent = nullptr); explicit UpdaterService(QObject* parent = nullptr);
~UpdaterService() override; ~UpdaterService() override;
@@ -53,9 +65,9 @@ public:
static bool HasStagedUpdate(const std::filesystem::path& app_directory); static bool HasStagedUpdate(const std::filesystem::path& app_directory);
static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); static bool ApplyStagedUpdate(const std::filesystem::path& app_directory);
#ifdef _WIN32 #ifdef _WIN32
bool LaunchUpdateHelper(); bool LaunchUpdateHelper();
#endif #endif
signals: signals:
void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info);
@@ -75,16 +87,18 @@ private:
void ParseUpdateResponse(const QByteArray& response, const QString& channel); void ParseUpdateResponse(const QByteArray& response, const QString& channel);
bool CompareVersions(const std::string& current, const std::string& latest) const; bool CompareVersions(const std::string& current, const std::string& latest) const;
#ifdef _WIN32 #ifdef _WIN32
bool ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); bool ExtractArchive(const std::filesystem::path& archive_path,
#ifndef CITRON_ENABLE_LIBARCHIVE const std::filesystem::path& extract_path);
bool ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); #ifndef CITRON_ENABLE_LIBARCHIVE
#endif bool ExtractArchiveWindows(const std::filesystem::path& archive_path,
const std::filesystem::path& extract_path);
#endif
bool InstallUpdate(const std::filesystem::path& update_path); bool InstallUpdate(const std::filesystem::path& update_path);
bool CreateBackup(); bool CreateBackup();
bool RestoreBackup(); bool RestoreBackup();
bool CreateUpdateHelperScript(const std::filesystem::path& staging_path); bool CreateUpdateHelperScript(const std::filesystem::path& staging_path);
#endif #endif
bool CleanupFiles(); bool CleanupFiles();
std::filesystem::path GetTempDirectory() const; std::filesystem::path GetTempDirectory() const;
std::filesystem::path GetApplicationDirectory() const; std::filesystem::path GetApplicationDirectory() const;