From 1aa1ec9ee9b2c0d96a9f8ca1c42c69b3dec27f4a Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 4 Feb 2026 19:54:52 -0500 Subject: [PATCH] Fix Updater Logic & Remove version.txt requirement w/ refined SCM --- src/citron/updater/updater_service.cpp | 329 ++++++++++++++++--------- src/citron/updater/updater_service.h | 40 ++- 2 files changed, 241 insertions(+), 128 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index f43902ad9..1b737cb2a 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -1,30 +1,30 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "citron/updater/updater_service.h" #include "citron/uisettings.h" -#include "common/logging/log.h" +#include "citron/updater/updater_service.h" #include "common/fs/path_util.h" +#include "common/logging/log.h" #include "common/scm_rev.h" #include -#include -#include -#include -#include -#include +#include +#include #include #include #include -#include +#include +#include +#include #include #include -#include -#include -#include -#include #include +#include #include +#include +#include +#include +#include #ifdef CITRON_ENABLE_LIBARCHIVE #include @@ -35,17 +35,33 @@ #include #ifdef _WIN32 -#include #include +#include + #endif namespace Updater { -const std::string STABLE_UPDATE_URL = "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"; +const std::string STABLE_UPDATE_URL = + "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::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; if (std::regex_search(version_string, match, re) && match.size() > 1) { return match[1].str(); @@ -65,7 +81,6 @@ QByteArray GetFileChecksum(const std::filesystem::path& file_path) { return QByteArray(); } - UpdaterService::UpdaterService(QObject* parent) : QObject(parent) { network_manager = std::make_unique(this); InitializeSSL(); @@ -91,22 +106,27 @@ void UpdaterService::InitializeSSL() { // Check if SSL is supported if (!QSslSocket::supportsSsl()) { LOG_WARNING(Frontend, "SSL support not available"); - LOG_WARNING(Frontend, "Build-time SSL version: {}", QSslSocket::sslLibraryBuildVersionString().toStdString()); - LOG_WARNING(Frontend, "Runtime SSL version: {}", QSslSocket::sslLibraryVersionString().toStdString()); + LOG_WARNING(Frontend, "Build-time SSL version: {}", + QSslSocket::sslLibraryBuildVersionString().toStdString()); + LOG_WARNING(Frontend, "Runtime SSL version: {}", + QSslSocket::sslLibraryVersionString().toStdString()); #ifdef _WIN32 // 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 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)); #endif return; } - LOG_INFO(Frontend, "SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString()); + LOG_INFO(Frontend, "SSL library version: {}", + QSslSocket::sslLibraryVersionString().toStdString()); QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); auto certs = QSslConfiguration::systemCaCertificates(); @@ -126,22 +146,27 @@ void UpdaterService::CheckForUpdates() { return; } QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString(); - std::string update_url = (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL; + QString channel = + 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, "Checking for updates from: {}", update_url); QUrl url{QString::fromStdString(update_url)}; QNetworkRequest request{url}; request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); request.setRawHeader("Accept", QByteArrayLiteral("application/json")); - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); current_reply = network_manager->get(request); connect(current_reply, &QNetworkReply::finished, this, [this, channel]() { - if (!current_reply) return; + if (!current_reply) + return; if (current_reply->error() == QNetworkReply::NoError) { ParseUpdateResponse(current_reply->readAll(), channel); } 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 = nullptr; @@ -149,7 +174,8 @@ void UpdaterService::CheckForUpdates() { } void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) { - if (!QSslSocket::supportsSsl()) return; + if (!QSslSocket::supportsSsl()) + return; QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfig.setProtocol(QSsl::SecureProtocols); @@ -173,7 +199,8 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) { #ifdef _WIN32 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); return; } @@ -181,15 +208,18 @@ void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) { QUrl url(QString::fromStdString(download_url)); QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); 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::errorOccurred, this, &UpdaterService::OnDownloadError); } void UpdaterService::CancelUpdate() { - if (!update_in_progress.load()) return; + if (!update_in_progress.load()) + return; cancel_requested.store(true); if (current_reply) { current_reply->abort(); @@ -201,21 +231,12 @@ void UpdaterService::CancelUpdate() { std::string UpdaterService::GetCurrentVersion() const { 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. - 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 - } + const std::string build_version = Common::g_build_version; - // Otherwise (channel is Stable), we prioritize version.txt. + // First priority: version.txt (only relevant for Stable installations) std::filesystem::path search_path; #ifdef __linux__ const char* appimage_path_env = qgetenv("APPIMAGE").constData(); @@ -235,22 +256,37 @@ std::string UpdaterService::GetCurrentVersion() const { std::string version_from_file; std::getline(file, version_from_file); 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. - // This allows a nightly build to correctly check for a stable update. - std::string build_version = Common::g_build_version; - if (!build_version.empty()) { - std::string hash = ExtractCommitHash(build_version); - if (!hash.empty()) { - return hash; + // If the user's setting is Nightly, we prioritize the commit hash. + if (channel == QStringLiteral("Nightly")) { + if (!build_version.empty()) { + std::string hash = ExtractCommitHash(build_version); + if (!hash.empty()) { + 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 { @@ -270,14 +306,17 @@ void UpdaterService::OnDownloadFinished() { QByteArray downloaded_data = current_reply->readAll(); 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) - 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(); QFile file(QString::fromStdString(download_path.string())); 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); return; } @@ -293,7 +332,8 @@ void UpdaterService::OnDownloadFinished() { emit UpdateInstallProgress(10, QStringLiteral("Extracting update archive...")); std::filesystem::path extract_path = temp_download_path / "extracted"; 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); return; } @@ -305,7 +345,9 @@ void UpdaterService::OnDownloadFinished() { return; } 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); CleanupFiles(); }); @@ -344,13 +386,17 @@ void UpdaterService::OnDownloadFinished() { } else { // Create the backup copy of the old AppImage 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::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) { LOG_ERROR(Frontend, "Failed to copy AppImage to backup location: {}", ec.message()); } 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.close(); - if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | - QFileDevice::ReadGroup | QFileDevice::ExeGroup | - QFileDevice::ReadOther | QFileDevice::ExeOther)) { + if (!new_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | + QFileDevice::ExeOwner | QFileDevice::ReadGroup | + QFileDevice::ExeGroup | QFileDevice::ReadOther | + QFileDevice::ExeOther)) { emit UpdateError(QStringLiteral("Failed to make the new AppImage executable.")); std::filesystem::remove(new_appimage_path, ec); update_in_progress.store(false); @@ -397,7 +444,8 @@ void UpdaterService::OnDownloadFinished() { } 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); #endif } @@ -430,42 +478,50 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response, const QStri if (channel == QStringLiteral("Stable")) { latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString(); } 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; update_info.version = latest_version; 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(); for (const QJsonValue& asset_value : assets) { QJsonObject asset_obj = asset_value.toObject(); QString asset_name = asset_obj.value(QStringLiteral("name")).toString(); - #if defined(__linux__) +#if defined(__linux__) if (asset_name.endsWith(QStringLiteral(".AppImage"))) { DownloadOption option; 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); } - #elif defined(_WIN32) +#elif defined(_WIN32) // 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; 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); } - #endif - +#endif } 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; emit UpdateCheckCompleted(update_info.is_newer_version, update_info); return; @@ -487,28 +543,34 @@ bool UpdaterService::CompareVersions(const std::string& current, const std::stri } #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 struct archive* a = archive_read_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_filter_all(a); archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); 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); struct archive_entry* entry; 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); 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; size_t size; la_int64_t offset; 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_finish_entry(ext); @@ -524,12 +586,18 @@ bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, c } #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); - std::string sevenzip_cmd = "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; - if (std::system(sevenzip_cmd.c_str()) == 0) 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; + std::string sevenzip_cmd = + "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; + if (std::system(sevenzip_cmd.c_str()) == 0) + 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."); return false; } @@ -548,12 +616,15 @@ bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) { std::filesystem::path staging_path = app_directory / "update_staging"; EnsureDirectoryExists(staging_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()) { - 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::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"; @@ -585,14 +656,16 @@ bool UpdaterService::CreateBackup() { std::filesystem::remove_all(backup_dir); } std::filesystem::create_directories(backup_dir); - std::vector backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", "*.pdb"}; + std::vector backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", + "*.pdb"}; for (const auto& entry : std::filesystem::directory_iterator(app_directory)) { if (entry.is_regular_file()) { std::string filename = entry.path().filename().string(); std::string extension = entry.path().extension().string(); bool should_backup = false; 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; break; } @@ -613,11 +686,13 @@ bool UpdaterService::CreateBackup() { bool UpdaterService::RestoreBackup() { try { 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)) { if (entry.is_regular_file()) { 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"); @@ -648,9 +723,15 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi std::string app_path_str = app_directory.string(); std::string exe_path_str = (app_directory / "citron.exe").string(); - for (auto& ch : staging_path_str) if (ch == '/') ch = '\\'; - for (auto& ch : app_path_str) if (ch == '/') ch = '\\'; - for (auto& ch : exe_path_str) if (ch == '/') ch = '\\'; + for (auto& ch : staging_path_str) + if (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 << "setlocal enabledelayedexpansion\n"; @@ -665,7 +746,8 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi script << "if not errorlevel 1 (\n"; script << " set /a wait_count+=1\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 << " )\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 script << "echo Removing read-only attributes from existing files...\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_cmd.exe\" attrib -R \"" << app_path_str << "\\citron_cmd.exe\" >nul 2>&1\n\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_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+) script << "echo Copying update files...\n"; script << "set /a copy_retries=0\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 << "REM Robocopy returns 0-7 for success, 8+ for errors\n"; script << "if !robocopy_exit! geq 8 (\n"; @@ -786,10 +871,13 @@ bool UpdaterService::LaunchUpdateHelper() { bool launched = QProcess::startDetached(QStringLiteral("cmd.exe"), arguments); 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; } 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; } } catch (const std::exception& e) { @@ -808,14 +896,16 @@ bool UpdaterService::CleanupFiles() { std::vector backup_dirs; if (std::filesystem::exists(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()); } } } if (backup_dirs.size() > 3) { - std::sort(backup_dirs.begin(), backup_dirs.end(), - [](const auto& a, const auto& b) { return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); }); + std::sort(backup_dirs.begin(), backup_dirs.end(), [](const auto& a, const auto& b) { + return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); + }); for (size_t i = 3; i < backup_dirs.size(); ++i) { std::filesystem::remove_all(backup_dirs[i]); } @@ -829,7 +919,9 @@ bool UpdaterService::CleanupFiles() { } 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 { @@ -856,7 +948,8 @@ bool UpdaterService::HasStagedUpdate(const std::filesystem::path& app_directory) #ifdef _WIN32 std::filesystem::path staging_path = app_directory / "update_staging"; 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 return false; #endif @@ -867,15 +960,19 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director try { std::filesystem::path staging_path = app_directory / "update_staging"; 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()); 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); 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()) { - 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; if (std::filesystem::exists(dest_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::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); @@ -897,7 +995,8 @@ bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_director if (!version.empty()) { std::filesystem::path version_file = app_directory / "version.txt"; std::ofstream vfile(version_file); - if (vfile.is_open()) vfile << version; + if (vfile.is_open()) + vfile << version; } std::filesystem::remove_all(staging_path); LOG_INFO(Frontend, "Update applied successfully. Version: {}", version); diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index bfa18aa90..96ab8870a 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -3,21 +3,24 @@ #pragma once -#include -#include #include -#include #include +#include #include +#include +#include + -#include #include +#include + namespace Updater { // Declarations for helper functions QString FormatDateTimeString(const std::string& iso_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); struct DownloadOption { @@ -39,7 +42,16 @@ class UpdaterService : public QObject { Q_OBJECT 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); ~UpdaterService() override; @@ -53,9 +65,9 @@ public: static bool HasStagedUpdate(const std::filesystem::path& app_directory); static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); - #ifdef _WIN32 +#ifdef _WIN32 bool LaunchUpdateHelper(); - #endif +#endif signals: void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); @@ -75,16 +87,18 @@ private: void ParseUpdateResponse(const QByteArray& response, const QString& channel); bool CompareVersions(const std::string& current, const std::string& latest) const; - #ifdef _WIN32 - bool ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); - #ifndef CITRON_ENABLE_LIBARCHIVE - bool ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); - #endif +#ifdef _WIN32 + bool ExtractArchive(const std::filesystem::path& archive_path, + const std::filesystem::path& extract_path); +#ifndef CITRON_ENABLE_LIBARCHIVE + bool ExtractArchiveWindows(const std::filesystem::path& archive_path, + const std::filesystem::path& extract_path); +#endif bool InstallUpdate(const std::filesystem::path& update_path); bool CreateBackup(); bool RestoreBackup(); bool CreateUpdateHelperScript(const std::filesystem::path& staging_path); - #endif +#endif bool CleanupFiles(); std::filesystem::path GetTempDirectory() const; std::filesystem::path GetApplicationDirectory() const;