From 5628d07b9cbe550b5da9fc43396a98b46d46906f Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:36:49 +0000 Subject: [PATCH 01/18] fix: Autoupdater --- src/citron/main.cpp | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index eb2cf8f11..270bddcf9 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -6091,25 +6091,17 @@ int main(int argc, char* argv[]) { } void GMainWindow::OnCheckForUpdates() { -#ifdef CITRON_USE_AUTO_UPDATER -#ifdef _WIN32 - // Use HTTP URL to bypass SSL issues (will be redirected to HTTPS but handled by updater) - // TODO: Fix SSL libraries and revert to https://releases.citron-emu.org/api/check - std::string update_url = "http://releases.citron-emu.org/api/check"; + #ifdef CITRON_USE_AUTO_UPDATER + std::string update_url = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases"; - // Create and show the updater dialog - auto* updater_dialog = new UpdaterDialog(this); + auto* updater_dialog = new Updater::UpdaterDialog(this); updater_dialog->setAttribute(Qt::WA_DeleteOnClose); updater_dialog->show(); updater_dialog->CheckForUpdates(update_url); -#else - QMessageBox::information(this, tr("Updates"), - tr("The update dialog is only available on Windows in this build.")); -#endif -#else + #else QMessageBox::information(this, tr("Updates"), tr("The automatic updater is not enabled in this build.")); -#endif + #endif } void GMainWindow::CheckForUpdatesAutomatically() { @@ -6122,7 +6114,7 @@ void GMainWindow::CheckForUpdatesAutomatically() { LOG_INFO(Frontend, "Checking for updates automatically..."); // Use HTTP URL to bypass SSL issues - std::string update_url = "http://releases.citron-emu.org/api/check"; + std::string update_url = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases"; // Create updater service for silent background check auto* updater_service = new Updater::UpdaterService(this); From d5f158934129babec7532eeffb137fa7696e7e1b Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:38:02 +0000 Subject: [PATCH 02/18] fix: Autoupdater --- src/citron/updater/updater_dialog.h | 131 ++++++++++------------------ 1 file changed, 48 insertions(+), 83 deletions(-) diff --git a/src/citron/updater/updater_dialog.h b/src/citron/updater/updater_dialog.h index 48489208e..d8d2afd89 100644 --- a/src/citron/updater/updater_dialog.h +++ b/src/citron/updater/updater_dialog.h @@ -5,101 +5,66 @@ #include #include - -#include -#include -#include -#include #include - -#ifdef _WIN32 #include "citron/updater/updater_service.h" -#else -// Forward declarations for non-Windows platforms -namespace Updater { -struct UpdateInfo; -class UpdaterService; -} -#endif namespace Ui { -class UpdaterDialog; + class UpdaterDialog; } -class UpdaterDialog : public QDialog { - Q_OBJECT +namespace Updater { -public: - explicit UpdaterDialog(QWidget* parent = nullptr); - ~UpdaterDialog() override; + class UpdaterDialog : public QDialog { + Q_OBJECT - // Check for updates using the given URL - void CheckForUpdates(const std::string& update_url); + public: + explicit UpdaterDialog(QWidget* parent = nullptr); + ~UpdaterDialog() override; - // Show update available dialog - void ShowUpdateAvailable(const Updater::UpdateInfo& update_info); + void CheckForUpdates(const std::string& update_url); - // Show update checking dialog - void ShowUpdateChecking(); + private slots: + void OnUpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); + void OnUpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); + void OnUpdateInstallProgress(int percentage, const QString& current_file); + void OnUpdateCompleted(Updater::UpdaterService::UpdateResult result, const QString& message); + void OnUpdateError(const QString& error_message); + void OnDownloadButtonClicked(); + void OnCancelButtonClicked(); + void OnCloseButtonClicked(); + void OnRestartButtonClicked(); -private slots: - void OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info); - void OnUpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); - void OnUpdateInstallProgress(int percentage, const QString& current_file); -#ifdef _WIN32 - void OnUpdateCompleted(Updater::UpdaterService::UpdateResult result, const QString& message); -#else - void OnUpdateCompleted(int result, const QString& message); -#endif - void OnUpdateError(const QString& error_message); + private: + void SetupUI(); + void ShowCheckingState(); + void ShowNoUpdateState(); + void ShowUpdateAvailableState(); + void ShowDownloadingState(); + void ShowInstallingState(); + void ShowCompletedState(); + void ShowErrorState(); + void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); + void UpdateInstallProgress(int percentage, const QString& current_file); + QString FormatBytes(qint64 bytes) const; + QString GetUpdateMessage(Updater::UpdaterService::UpdateResult result) const; - void OnDownloadButtonClicked(); - void OnCancelButtonClicked(); - void OnCloseButtonClicked(); - void OnRestartButtonClicked(); + enum class State { + Checking, + NoUpdate, + UpdateAvailable, + Downloading, + Installing, + Completed, + Error + }; -private: - void SetupUI(); - void ShowCheckingState(); - void ShowNoUpdateState(); - void ShowUpdateAvailableState(); - void ShowDownloadingState(); - void ShowInstallingState(); - void ShowCompletedState(); - void ShowErrorState(); - - void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); - void UpdateInstallProgress(int percentage, const QString& current_file); - - QString FormatBytes(qint64 bytes) const; -#ifdef _WIN32 - QString GetUpdateMessage(Updater::UpdaterService::UpdateResult result) const; -#else - QString GetUpdateMessage(int result) const; -#endif - -private: -#ifdef _WIN32 - std::unique_ptr ui; - std::unique_ptr updater_service; - - Updater::UpdateInfo current_update_info; - - // UI state - enum class State { - Checking, - NoUpdate, - UpdateAvailable, - Downloading, - Installing, - Completed, - Error + std::unique_ptr ui; + std::unique_ptr updater_service; + UpdateInfo current_update_info; + State current_state; + qint64 total_download_size; + qint64 downloaded_bytes; + QTimer* progress_timer; }; - State current_state = State::Checking; - // Progress tracking - qint64 total_download_size = 0; - qint64 downloaded_bytes = 0; - QTimer* progress_timer; -#endif // _WIN32 -}; \ No newline at end of file +} // namespace Updater From 1a29259e33da19f9739d79e1e3b352cc5e388d79 Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:39:04 +0000 Subject: [PATCH 03/18] fix: Autoupdater --- src/citron/updater/updater_dialog.cpp | 225 ++++++++------------------ 1 file changed, 66 insertions(+), 159 deletions(-) diff --git a/src/citron/updater/updater_dialog.cpp b/src/citron/updater/updater_dialog.cpp index 42a93926d..63b36ff75 100644 --- a/src/citron/updater/updater_dialog.cpp +++ b/src/citron/updater/updater_dialog.cpp @@ -2,17 +2,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "citron/updater/updater_dialog.h" - -#ifdef _WIN32 #include "ui_updater_dialog.h" #include -#include #include #include -#include -#include +#include #include +#include +#include + +namespace Updater { UpdaterDialog::UpdaterDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique()), @@ -23,16 +23,16 @@ UpdaterDialog::UpdaterDialog(QWidget* parent) ui->setupUi(this); // Set up connections - connect(updater_service.get(), &Updater::UpdaterService::UpdateCheckCompleted, - this, &UpdaterDialog::OnUpdateCheckCompleted); - connect(updater_service.get(), &Updater::UpdaterService::UpdateDownloadProgress, - this, &UpdaterDialog::OnUpdateDownloadProgress); - connect(updater_service.get(), &Updater::UpdaterService::UpdateInstallProgress, - this, &UpdaterDialog::OnUpdateInstallProgress); - connect(updater_service.get(), &Updater::UpdaterService::UpdateCompleted, - this, &UpdaterDialog::OnUpdateCompleted); - connect(updater_service.get(), &Updater::UpdaterService::UpdateError, - this, &UpdaterDialog::OnUpdateError); + connect(updater_service.get(), &Updater::UpdaterService::UpdateCheckCompleted, this, + &UpdaterDialog::OnUpdateCheckCompleted); + connect(updater_service.get(), &Updater::UpdaterService::UpdateDownloadProgress, this, + &UpdaterDialog::OnUpdateDownloadProgress); + connect(updater_service.get(), &Updater::UpdaterService::UpdateInstallProgress, this, + &UpdaterDialog::OnUpdateInstallProgress); + connect(updater_service.get(), &Updater::UpdaterService::UpdateCompleted, this, + &UpdaterDialog::OnUpdateCompleted); + connect(updater_service.get(), &Updater::UpdaterService::UpdateError, this, + &UpdaterDialog::OnUpdateError); // Set up UI connections connect(ui->downloadButton, &QPushButton::clicked, this, &UpdaterDialog::OnDownloadButtonClicked); @@ -48,9 +48,8 @@ UpdaterDialog::UpdaterDialog(QWidget* parent) if (current_state == State::Downloading) { ui->downloadInfoLabel->setText( QStringLiteral("Downloaded: %1 / %2") - .arg(FormatBytes(downloaded_bytes)) - .arg(FormatBytes(total_download_size)) - ); + .arg(FormatBytes(downloaded_bytes)) + .arg(FormatBytes(total_download_size))); } }); } @@ -62,15 +61,6 @@ void UpdaterDialog::CheckForUpdates(const std::string& update_url) { updater_service->CheckForUpdates(update_url); } -void UpdaterDialog::ShowUpdateAvailable(const Updater::UpdateInfo& update_info) { - current_update_info = update_info; - ShowUpdateAvailableState(); -} - -void UpdaterDialog::ShowUpdateChecking() { - ShowCheckingState(); -} - void UpdaterDialog::OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info) { if (has_update) { current_update_info = update_info; @@ -80,7 +70,8 @@ void UpdaterDialog::OnUpdateCheckCompleted(bool has_update, const Updater::Updat } } -void UpdaterDialog::OnUpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total) { +void UpdaterDialog::OnUpdateDownloadProgress(int percentage, qint64 bytes_received, + qint64 bytes_total) { downloaded_bytes = bytes_received; total_download_size = bytes_total; @@ -100,20 +91,21 @@ void UpdaterDialog::OnUpdateInstallProgress(int percentage, const QString& curre ui->downloadInfoLabel->setText(current_file); } -void UpdaterDialog::OnUpdateCompleted(Updater::UpdaterService::UpdateResult result, const QString& message) { +void UpdaterDialog::OnUpdateCompleted(Updater::UpdaterService::UpdateResult result, + const QString& message) { progress_timer->stop(); switch (result) { - case Updater::UpdaterService::UpdateResult::Success: - ShowCompletedState(); - break; - case Updater::UpdaterService::UpdateResult::Cancelled: - close(); - break; - default: - ShowErrorState(); - ui->statusLabel->setText(GetUpdateMessage(result) + QStringLiteral("\n\n") + message); - break; + case Updater::UpdaterService::UpdateResult::Success: + ShowCompletedState(); + break; + case Updater::UpdaterService::UpdateResult::Cancelled: + close(); + break; + default: + ShowErrorState(); + ui->statusLabel->setText(GetUpdateMessage(result) + QStringLiteral("\n\n") + message); + break; } } @@ -141,22 +133,16 @@ void UpdaterDialog::OnCloseButtonClicked() { } void UpdaterDialog::OnRestartButtonClicked() { - // Ask for confirmation int ret = QMessageBox::question(this, QStringLiteral("Restart Citron"), QStringLiteral("Are you sure you want to restart Citron now?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes); + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (ret == QMessageBox::Yes) { - // Get the current executable path QString program = QApplication::applicationFilePath(); QStringList arguments = QApplication::arguments(); - arguments.removeFirst(); // Remove the program name + arguments.removeFirst(); - // Start the new instance QProcess::startDetached(program, arguments); - - // Close the current instance QApplication::quit(); } } @@ -164,41 +150,31 @@ void UpdaterDialog::OnRestartButtonClicked() { void UpdaterDialog::SetupUI() { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); setFixedSize(size()); - - // Set current version ui->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion())); - ShowCheckingState(); } void UpdaterDialog::ShowCheckingState() { current_state = State::Checking; - ui->titleLabel->setText(QStringLiteral("Checking for updates...")); ui->statusLabel->setText(QStringLiteral("Please wait while we check for available updates...")); - ui->updateInfoGroup->setVisible(false); ui->changelogGroup->setVisible(false); ui->progressGroup->setVisible(false); - ui->downloadButton->setVisible(false); ui->cancelButton->setVisible(true); ui->closeButton->setVisible(false); ui->restartButton->setVisible(false); - ui->cancelButton->setText(QStringLiteral("Cancel")); } void UpdaterDialog::ShowNoUpdateState() { current_state = State::NoUpdate; - ui->titleLabel->setText(QStringLiteral("No updates available")); ui->statusLabel->setText(QStringLiteral("You are running the latest version of Citron.")); - ui->updateInfoGroup->setVisible(true); ui->changelogGroup->setVisible(false); ui->progressGroup->setVisible(false); - ui->downloadButton->setVisible(false); ui->cancelButton->setVisible(false); ui->closeButton->setVisible(true); @@ -207,105 +183,84 @@ void UpdaterDialog::ShowNoUpdateState() { void UpdaterDialog::ShowUpdateAvailableState() { current_state = State::UpdateAvailable; - ui->titleLabel->setText(QStringLiteral("Update available")); ui->statusLabel->setText(QStringLiteral("A new version of Citron is available for download.")); - - // Fill in update information ui->latestVersionValue->setText(QString::fromStdString(current_update_info.version)); ui->releaseDateValue->setText(QString::fromStdString(current_update_info.release_date)); - - // Show changelog if available if (!current_update_info.changelog.empty()) { ui->changelogText->setPlainText(QString::fromStdString(current_update_info.changelog)); ui->changelogGroup->setVisible(true); } else { ui->changelogGroup->setVisible(false); } - ui->updateInfoGroup->setVisible(true); ui->progressGroup->setVisible(false); - ui->downloadButton->setVisible(true); ui->cancelButton->setVisible(true); ui->closeButton->setVisible(false); ui->restartButton->setVisible(false); - ui->cancelButton->setText(QStringLiteral("Later")); } void UpdaterDialog::ShowDownloadingState() { current_state = State::Downloading; - ui->titleLabel->setText(QStringLiteral("Downloading update...")); - ui->statusLabel->setText(QStringLiteral("Please wait while the update is being downloaded and installed.")); - + ui->statusLabel->setText( + QStringLiteral("Please wait while the update is being downloaded and installed.")); ui->updateInfoGroup->setVisible(false); ui->changelogGroup->setVisible(false); ui->progressGroup->setVisible(true); - ui->progressLabel->setText(QStringLiteral("Preparing download...")); ui->progressBar->setValue(0); ui->downloadInfoLabel->setText(QStringLiteral("")); - ui->downloadButton->setVisible(false); ui->cancelButton->setVisible(true); ui->closeButton->setVisible(false); ui->restartButton->setVisible(false); - ui->cancelButton->setText(QStringLiteral("Cancel")); - progress_timer->start(); } void UpdaterDialog::ShowInstallingState() { current_state = State::Installing; - ui->titleLabel->setText(QStringLiteral("Installing update...")); - ui->statusLabel->setText(QStringLiteral("Please wait while the update is being installed. Do not close the application.")); - + ui->statusLabel->setText(QStringLiteral( + "Please wait while the update is being installed. Do not close the application.")); ui->progressLabel->setText(QStringLiteral("Installing...")); ui->downloadInfoLabel->setText(QStringLiteral("")); - ui->cancelButton->setVisible(false); } void UpdaterDialog::ShowCompletedState() { current_state = State::Completed; - ui->titleLabel->setText(QStringLiteral("Update ready!")); - ui->statusLabel->setText(QStringLiteral("The update has been downloaded and prepared successfully. The update will be applied when you restart Citron.")); - + ui->statusLabel->setText(QStringLiteral("The update has been downloaded and prepared " + "successfully. The update will be applied when you " + "restart Citron.")); ui->progressGroup->setVisible(false); - ui->downloadButton->setVisible(false); ui->cancelButton->setVisible(false); ui->closeButton->setVisible(true); ui->restartButton->setVisible(true); - ui->progressBar->setValue(100); } void UpdaterDialog::ShowErrorState() { current_state = State::Error; - ui->titleLabel->setText(QStringLiteral("Update failed")); - // statusLabel text is set by the caller - ui->updateInfoGroup->setVisible(false); ui->changelogGroup->setVisible(false); ui->progressGroup->setVisible(false); - ui->downloadButton->setVisible(false); ui->cancelButton->setVisible(false); ui->closeButton->setVisible(true); ui->restartButton->setVisible(false); } -void UpdaterDialog::UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total) { +void UpdaterDialog::UpdateDownloadProgress(int percentage, qint64 bytes_received, + qint64 bytes_total) { downloaded_bytes = bytes_received; total_download_size = bytes_total; - ui->progressBar->setValue(percentage); ui->progressLabel->setText(QStringLiteral("Downloading update... %1%").arg(percentage)); } @@ -317,90 +272,42 @@ void UpdaterDialog::UpdateInstallProgress(int percentage, const QString& current } QString UpdaterDialog::FormatBytes(qint64 bytes) const { - const QStringList units = {QStringLiteral("B"), QStringLiteral("KB"), QStringLiteral("MB"), QStringLiteral("GB")}; + const QStringList units = {QStringLiteral("B"), QStringLiteral("KB"), QStringLiteral("MB"), + QStringLiteral("GB")}; double size = bytes; int unit = 0; - while (size >= 1024.0 && unit < units.size() - 1) { size /= 1024.0; unit++; } - - return QStringLiteral("%1 %2").arg(QString::number(size, 'f', unit == 0 ? 0 : 1)).arg(units[unit]); + return QStringLiteral("%1 %2") + .arg(QString::number(size, 'f', unit == 0 ? 0 : 1)) + .arg(units[unit]); } QString UpdaterDialog::GetUpdateMessage(Updater::UpdaterService::UpdateResult result) const { switch (result) { - case Updater::UpdaterService::UpdateResult::Success: - return QStringLiteral("Update completed successfully!"); - case Updater::UpdaterService::UpdateResult::Failed: - return QStringLiteral("Update failed due to an unknown error."); - case Updater::UpdaterService::UpdateResult::Cancelled: - return QStringLiteral("Update was cancelled."); - case Updater::UpdaterService::UpdateResult::NetworkError: - return QStringLiteral("Update failed due to a network error."); - case Updater::UpdaterService::UpdateResult::ExtractionError: - return QStringLiteral("Failed to extract the update archive."); - case Updater::UpdaterService::UpdateResult::PermissionError: - return QStringLiteral("Update failed due to insufficient permissions."); - case Updater::UpdaterService::UpdateResult::InvalidArchive: - return QStringLiteral("The downloaded update archive is invalid."); - case Updater::UpdaterService::UpdateResult::NoUpdateAvailable: - return QStringLiteral("No update is available."); - default: - return QStringLiteral("Unknown error occurred."); + case Updater::UpdaterService::UpdateResult::Success: + return QStringLiteral("Update completed successfully!"); + case Updater::UpdaterService::UpdateResult::Failed: + return QStringLiteral("Update failed due to an unknown error."); + case Updater::UpdaterService::UpdateResult::Cancelled: + return QStringLiteral("Update was cancelled."); + case Updater::UpdaterService::UpdateResult::NetworkError: + return QStringLiteral("Update failed due to a network error."); + case Updater::UpdaterService::UpdateResult::ExtractionError: + return QStringLiteral("Failed to extract the update archive."); + case Updater::UpdaterService::UpdateResult::PermissionError: + return QStringLiteral("Update failed due to insufficient permissions."); + case Updater::UpdaterService::UpdateResult::InvalidArchive: + return QStringLiteral("The downloaded update archive is invalid."); + case Updater::UpdaterService::UpdateResult::NoUpdateAvailable: + return QStringLiteral("No update is available."); + default: + return QStringLiteral("Unknown error occurred."); } } -#include "updater_dialog.moc" - -#else // _WIN32 - -// Forward declarations for non-Windows platforms -namespace Updater { -struct UpdateInfo {}; -class UpdaterService { -public: - enum class UpdateResult { Success }; -}; -} - -// Stub implementations for non-Windows platforms -UpdaterDialog::UpdaterDialog(QWidget* parent) : QDialog(parent) {} -UpdaterDialog::~UpdaterDialog() = default; -void UpdaterDialog::CheckForUpdates(const std::string&) {} -void UpdaterDialog::ShowUpdateAvailable(const Updater::UpdateInfo&) {} -void UpdaterDialog::ShowUpdateChecking() {} -void UpdaterDialog::OnUpdateCheckCompleted(bool, const Updater::UpdateInfo&) {} -void UpdaterDialog::OnUpdateDownloadProgress(int, qint64, qint64) {} -void UpdaterDialog::OnUpdateInstallProgress(int, const QString&) {} -#ifdef _WIN32 -void UpdaterDialog::OnUpdateCompleted(Updater::UpdaterService::UpdateResult, const QString&) {} -#else -void UpdaterDialog::OnUpdateCompleted(int, const QString&) {} -#endif -void UpdaterDialog::OnUpdateError(const QString&) {} -void UpdaterDialog::OnDownloadButtonClicked() {} -void UpdaterDialog::OnCancelButtonClicked() {} -void UpdaterDialog::OnCloseButtonClicked() {} -void UpdaterDialog::OnRestartButtonClicked() {} -void UpdaterDialog::SetupUI() {} -void UpdaterDialog::ShowCheckingState() {} -void UpdaterDialog::ShowNoUpdateState() {} -void UpdaterDialog::ShowUpdateAvailableState() {} -void UpdaterDialog::ShowDownloadingState() {} -void UpdaterDialog::ShowInstallingState() {} -void UpdaterDialog::ShowCompletedState() {} -void UpdaterDialog::ShowErrorState() {} -void UpdaterDialog::UpdateDownloadProgress(int, qint64, qint64) {} -void UpdaterDialog::UpdateInstallProgress(int, const QString&) {} -QString UpdaterDialog::FormatBytes(qint64) const { return QString(); } -#ifdef _WIN32 -QString UpdaterDialog::GetUpdateMessage(Updater::UpdaterService::UpdateResult) const { return QString(); } -#else -QString UpdaterDialog::GetUpdateMessage(int) const { return QString(); } -#endif +} // namespace Updater #include "updater_dialog.moc" - -#endif // _WIN32 From f5029f62a1a85cdaeb94370164df862e627b720b Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:39:53 +0000 Subject: [PATCH 04/18] fix: Autoupdater --- src/citron/updater/updater_service.cpp | 678 +++++++------------------ 1 file changed, 175 insertions(+), 503 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index ef075e794..d9f046cfe 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -15,17 +15,11 @@ #include #include #include -#include +#include #include #include -#include #include #include -#include -#include -#include -#include -#include #ifdef CITRON_ENABLE_LIBARCHIVE #include @@ -44,27 +38,13 @@ namespace Updater { UpdaterService::UpdaterService(QObject* parent) : QObject(parent) { network_manager = std::make_unique(this); - - // Initialize SSL support InitializeSSL(); - - // Initialize paths app_directory = GetApplicationDirectory(); temp_download_path = GetTempDirectory(); backup_path = GetBackupDirectory(); - - // Create necessary directories EnsureDirectoryExists(temp_download_path); EnsureDirectoryExists(backup_path); - LOG_INFO(Frontend, "UpdaterService initialized"); - LOG_INFO(Frontend, "App directory: {}", app_directory.string()); - LOG_INFO(Frontend, "Temp directory: {}", temp_download_path.string()); - LOG_INFO(Frontend, "Backup directory: {}", backup_path.string()); - - // Log SSL support status - LOG_INFO(Frontend, "SSL support available: {}", QSslSocket::supportsSsl() ? "Yes" : "No"); - LOG_INFO(Frontend, "SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString()); } UpdaterService::~UpdaterService() { @@ -72,66 +52,25 @@ UpdaterService::~UpdaterService() { current_reply->abort(); current_reply->deleteLater(); } - - // Cleanup temporary files CleanupFiles(); } void UpdaterService::InitializeSSL() { - // Log OpenSSL library information LOG_INFO(Frontend, "Attempting to initialize SSL support..."); - - // On Windows, check for OpenSSL libraries -#ifdef _WIN32 - // Get the application directory - QString appDir = QCoreApplication::applicationDirPath(); - - // Check for OpenSSL libraries using proper path construction - QString sslLib = QDir(appDir).filePath(QStringLiteral("libssl-3-x64.dll")); - QString cryptoLib = QDir(appDir).filePath(QStringLiteral("libcrypto-3-x64.dll")); - - LOG_INFO(Frontend, "Looking for SSL libraries in: {}", appDir.toStdString()); - LOG_INFO(Frontend, "SSL library path: {}", sslLib.toStdString()); - LOG_INFO(Frontend, "Crypto library path: {}", cryptoLib.toStdString()); - - // Check if files exist - if (QFile::exists(sslLib) && QFile::exists(cryptoLib)) { - LOG_INFO(Frontend, "OpenSSL library files found"); - } else { - LOG_WARNING(Frontend, "OpenSSL library files not found at expected locations"); - } -#endif - - // Check if SSL is supported - bool sslSupported = QSslSocket::supportsSsl(); - LOG_INFO(Frontend, "SSL support available: {}", sslSupported ? "Yes" : "No"); - - if (!sslSupported) { - LOG_WARNING(Frontend, "SSL support not available after initialization"); - LOG_INFO(Frontend, "Build-time SSL library version: {}", QSslSocket::sslLibraryBuildVersionString().toStdString()); + if (!QSslSocket::supportsSsl()) { + LOG_WARNING(Frontend, "SSL support not available"); return; } - - // Set up SSL configuration QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - - // Use system certificates if available auto certs = QSslConfiguration::systemCaCertificates(); if (!certs.isEmpty()) { sslConfig.setCaCertificates(certs); - LOG_INFO(Frontend, "Loaded {} system CA certificates", certs.size()); } else { LOG_WARNING(Frontend, "No system CA certificates available"); } - - // Configure SSL protocol sslConfig.setProtocol(QSsl::SecureProtocols); - - // Set as default QSslConfiguration::setDefaultConfiguration(sslConfig); - LOG_INFO(Frontend, "SSL initialized successfully"); - LOG_INFO(Frontend, "Runtime SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString()); } void UpdaterService::CheckForUpdates(const std::string& update_url) { @@ -139,113 +78,34 @@ void UpdaterService::CheckForUpdates(const std::string& update_url) { emit UpdateError(QStringLiteral("Update operation already in progress")); return; } - if (update_url.empty()) { emit UpdateError(QStringLiteral("Update URL not configured")); return; } - LOG_INFO(Frontend, "Checking for updates from: {}", update_url); - - // Try HTTPS first, fallback to HTTP if SSL not available - QString requestUrl = QString::fromStdString(update_url); - bool ssl_available = QSslSocket::supportsSsl(); - - if (!ssl_available && requestUrl.startsWith(QStringLiteral("https://"))) { - LOG_WARNING(Frontend, "SSL not supported, trying HTTP fallback"); - requestUrl.replace(QStringLiteral("https://"), QStringLiteral("http://")); - LOG_INFO(Frontend, "Using HTTP fallback URL: {}", requestUrl.toStdString()); - } - - QUrl url{requestUrl}; + QUrl url{QString::fromStdString(update_url)}; QNetworkRequest request{url}; request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); request.setRawHeader("Accept", QByteArrayLiteral("application/json")); - - // Only enable automatic redirect following if SSL is available - // This prevents TLS initialization failures when redirecting HTTP -> HTTPS - if (ssl_available) { - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - } else { - // Disable automatic redirects when SSL is not available - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); - LOG_INFO(Frontend, "SSL not available, disabling automatic redirects"); - } - - // Configure SSL for HTTPS requests (if SSL is available) - if (requestUrl.startsWith(QStringLiteral("https://"))) { - ConfigureSSLForRequest(request); - } - + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); current_reply = network_manager->get(request); - - connect(current_reply, &QNetworkReply::finished, this, [this, ssl_available]() { - // Handle manual redirects when SSL is not available - if (!ssl_available && current_reply->error() == QNetworkReply::NoError) { - QVariant redirect_url = current_reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - if (redirect_url.isValid()) { - QString redirect_str = redirect_url.toString(); - if (redirect_str.startsWith(QStringLiteral("https://"))) { - LOG_ERROR(Frontend, "Server redirected HTTP to HTTPS, but SSL is not available"); - emit UpdateError(QStringLiteral("SSL not available - cannot follow HTTPS redirect. Please check your Qt installation.")); - } else { - LOG_INFO(Frontend, "Following redirect to: {}", redirect_str.toStdString()); - // Follow the redirect manually - QUrl new_url = QUrl(redirect_str); - QNetworkRequest new_request(new_url); - new_request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); - new_request.setRawHeader("Accept", QByteArrayLiteral("application/json")); - new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); - - current_reply->deleteLater(); - current_reply = network_manager->get(new_request); - - // Reconnect handlers for the new request - connect(current_reply, &QNetworkReply::finished, this, [this]() { - if (current_reply->error() == QNetworkReply::NoError) { - ParseUpdateResponse(current_reply->readAll()); - } else { - emit UpdateError(QStringLiteral("Failed to check for updates: %1").arg(current_reply->errorString())); - } - current_reply->deleteLater(); - current_reply = nullptr; - }); - - connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError); - } - return; - } - } - - // Normal response handling + connect(current_reply, &QNetworkReply::finished, this, [this]() { + if (!current_reply) return; if (current_reply->error() == QNetworkReply::NoError) { ParseUpdateResponse(current_reply->readAll()); } else { - emit UpdateError(QStringLiteral("Failed to check for updates: %1").arg(current_reply->errorString())); + emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString())); } current_reply->deleteLater(); current_reply = nullptr; }); - - connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError); } void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) { - if (!QSslSocket::supportsSsl()) { - LOG_WARNING(Frontend, "SSL not supported, request may fail for HTTPS URLs"); - return; - } - + if (!QSslSocket::supportsSsl()) return; QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - - // For now, use permissive SSL verification for compatibility - // In production, this should be QSslSocket::VerifyPeer - sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); - - // Set secure protocols + sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); // Should be VerifyPeer in production sslConfig.setProtocol(QSsl::SecureProtocols); - - // Apply SSL configuration request.setSslConfiguration(sslConfig); } @@ -254,9 +114,8 @@ void UpdaterService::DownloadAndInstallUpdate(const UpdateInfo& update_info) { emit UpdateError(QStringLiteral("Update operation already in progress")); return; } - if (update_info.download_url.empty()) { - emit UpdateError(QStringLiteral("Invalid download URL")); + emit UpdateError(QStringLiteral("Invalid download URL.")); return; } @@ -264,135 +123,54 @@ void UpdaterService::DownloadAndInstallUpdate(const UpdateInfo& update_info) { cancel_requested.store(false); current_update_info = update_info; - LOG_INFO(Frontend, "Starting download of update: {}", update_info.version); - LOG_INFO(Frontend, "Download URL: {}", update_info.download_url); + LOG_INFO(Frontend, "Starting update download from {}", update_info.download_url); - // Create backup before starting update +#ifdef _WIN32 if (!CreateBackup()) { emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup")); update_in_progress.store(false); return; } +#endif - // Prepare download URL with HTTP fallback if needed - QString downloadUrl = QString::fromStdString(update_info.download_url); - bool ssl_available = QSslSocket::supportsSsl(); - - if (!ssl_available && downloadUrl.startsWith(QStringLiteral("https://"))) { - LOG_WARNING(Frontend, "SSL not supported, trying HTTP fallback for download"); - downloadUrl.replace(QStringLiteral("https://"), QStringLiteral("http://")); - LOG_INFO(Frontend, "Using HTTP fallback download URL: {}", downloadUrl.toStdString()); - } - - // Start download - QUrl downloadQUrl{downloadUrl}; - QNetworkRequest request{downloadQUrl}; - request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); - - // Only enable automatic redirect following if SSL is available - if (ssl_available) { - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - } else { - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); - LOG_INFO(Frontend, "SSL not available, disabling automatic redirects for download"); - } - - // Configure SSL for the download request (if SSL is available) - if (downloadUrl.startsWith(QStringLiteral("https://"))) { - ConfigureSSLForRequest(request); - } - + QUrl url(QString::fromStdString(update_info.download_url)); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); current_reply = network_manager->get(request); - connect(current_reply, &QNetworkReply::downloadProgress, this, &UpdaterService::OnDownloadProgress); - connect(current_reply, &QNetworkReply::finished, this, [this, ssl_available]() { - // Handle manual redirects when SSL is not available - if (!ssl_available && current_reply->error() == QNetworkReply::NoError) { - QVariant redirect_url = current_reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - if (redirect_url.isValid()) { - QString redirect_str = redirect_url.toString(); - if (redirect_str.startsWith(QStringLiteral("https://"))) { - LOG_ERROR(Frontend, "Server redirected HTTP to HTTPS for download, but SSL is not available"); - emit UpdateCompleted(UpdateResult::NetworkError, - QStringLiteral("SSL not available - cannot follow HTTPS redirect for download. Please check your Qt installation.")); - update_in_progress.store(false); - return; - } else { - LOG_INFO(Frontend, "Following download redirect to: {}", redirect_str.toStdString()); - // Follow the redirect manually - QUrl new_url = QUrl(redirect_str); - QNetworkRequest new_request(new_url); - new_request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); - new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); - - current_reply->deleteLater(); - current_reply = network_manager->get(new_request); - - // Reconnect handlers for the new request - connect(current_reply, &QNetworkReply::downloadProgress, this, &UpdaterService::OnDownloadProgress); - connect(current_reply, &QNetworkReply::finished, this, &UpdaterService::OnDownloadFinished); - connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError); - } - return; - } - } - - // Normal download finished handling - OnDownloadFinished(); - }); + 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(); } - LOG_INFO(Frontend, "Update cancelled by user"); emit UpdateCompleted(UpdateResult::Cancelled, QStringLiteral("Update cancelled by user")); - update_in_progress.store(false); } std::string UpdaterService::GetCurrentVersion() const { - // Try to read version from version.txt file first (updated versions) std::filesystem::path version_file = app_directory / CITRON_VERSION_FILE; - if (std::filesystem::exists(version_file)) { std::ifstream file(version_file); if (file.is_open()) { std::string version; std::getline(file, version); - if (!version.empty()) { - return version; - } + if (!version.empty()) return version; } } - - // Use build version from the build system std::string build_version = Common::g_build_version; if (!build_version.empty()) { - // Create version.txt file if it doesn't exist try { std::ofstream vfile(version_file); - if (vfile.is_open()) { - vfile << build_version; - vfile.close(); - LOG_INFO(Frontend, "Created version.txt with build version: {}", build_version); - } - } catch (const std::exception& e) { - LOG_WARNING(Frontend, "Failed to create version.txt: {}", e.what()); - } + if (vfile.is_open()) vfile << build_version; + } catch (...) {} return build_version; } - - // Final fallback to application version return QCoreApplication::applicationVersion().toStdString(); } @@ -405,61 +183,87 @@ void UpdaterService::OnDownloadFinished() { update_in_progress.store(false); return; } - - if (current_reply->error() != QNetworkReply::NoError) { - emit UpdateCompleted(UpdateResult::NetworkError, - QStringLiteral("Download failed: %1").arg(current_reply->errorString())); + if (!current_reply || current_reply->error() != QNetworkReply::NoError) { + if(current_reply) emit UpdateError(QStringLiteral("Download failed: %1").arg(current_reply->errorString())); update_in_progress.store(false); return; } - // Save downloaded file - QString filename = QStringLiteral("citron_update_%1.zip").arg(QString::fromStdString(current_update_info.version)); - std::filesystem::path download_path = temp_download_path / filename.toStdString(); + QByteArray downloaded_data = current_reply->readAll(); +#if defined(_WIN32) + 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")); update_in_progress.store(false); return; } - - file.write(current_reply->readAll()); + file.write(downloaded_data); file.close(); - LOG_INFO(Frontend, "Download completed: {}", download_path.string()); - // Start extraction and installation QTimer::singleShot(100, this, [this, download_path]() { if (cancel_requested.load()) { update_in_progress.store(false); return; } - 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")); update_in_progress.store(false); return; } - emit UpdateInstallProgress(70, QStringLiteral("Installing update...")); - if (!InstallUpdate(extract_path)) { RestoreBackup(); emit UpdateCompleted(UpdateResult::Failed, QStringLiteral("Failed to install update")); update_in_progress.store(false); return; } - emit UpdateInstallProgress(100, QStringLiteral("Update completed successfully!")); emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update installed successfully. Please restart the application.")); - update_in_progress.store(false); CleanupFiles(); }); +#elif defined(__linux__) + LOG_INFO(Frontend, "AppImage download completed."); + QString current_appimage_path_str = QCoreApplication::applicationFilePath(); + std::filesystem::path current_appimage_path = current_appimage_path_str.toStdString(); + std::filesystem::path new_appimage_path = current_appimage_path.string() + ".new"; + + QFile new_file(QString::fromStdString(new_appimage_path.string())); + if (!new_file.open(QIODevice::WriteOnly)) { + emit UpdateError(QStringLiteral("Failed to save new AppImage version.")); + update_in_progress.store(false); + return; + } + 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)) { + emit UpdateError(QStringLiteral("Failed to make the new AppImage executable.")); + update_in_progress.store(false); + return; + } + + std::error_code ec; + std::filesystem::rename(new_appimage_path, current_appimage_path, ec); + if (ec) { + LOG_ERROR(Frontend, "Failed to replace old AppImage: {}", ec.message()); + emit UpdateError(QStringLiteral("Failed to replace old AppImage. Please close the application and replace it manually.")); + update_in_progress.store(false); + return; + } + + LOG_INFO(Frontend, "AppImage updated successfully."); + emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application.")); + update_in_progress.store(false); +#endif } void UpdaterService::OnDownloadProgress(qint64 bytes_received, qint64 bytes_total) { @@ -469,237 +273,163 @@ void UpdaterService::OnDownloadProgress(qint64 bytes_received, qint64 bytes_tota } } -void UpdaterService::OnDownloadError(QNetworkReply::NetworkError error) { - QString error_message = QStringLiteral("Network error: %1").arg(current_reply->errorString()); - LOG_ERROR(Frontend, "Download error: {}", error_message.toStdString()); - emit UpdateCompleted(UpdateResult::NetworkError, error_message); +void UpdaterService::OnDownloadError(QNetworkReply::NetworkError) { + if (current_reply) { + emit UpdateError(QStringLiteral("Network error: %1").arg(current_reply->errorString())); + } update_in_progress.store(false); } void UpdaterService::ParseUpdateResponse(const QByteArray& response) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(response, &error); - if (error.error != QJsonParseError::NoError) { - emit UpdateError(QStringLiteral("Invalid JSON response: %1").arg(error.errorString())); + emit UpdateError(QStringLiteral("Failed to parse JSON: %1").arg(error.errorString())); + return; + } + if (!doc.isArray()) { + emit UpdateError(QStringLiteral("JSON response is not an array.")); return; } - QJsonObject json = doc.object(); - UpdateInfo update_info; + QString platform_identifier; +#if defined(_WIN32) + platform_identifier = QStringLiteral("windows"); +#elif defined(__linux__) + platform_identifier = QStringLiteral("linux"); +#endif - update_info.version = json.value(QStringLiteral("version")).toString().toStdString(); - update_info.download_url = json.value(QStringLiteral("download_url")).toString().toStdString(); - update_info.changelog = json.value(QStringLiteral("changelog")).toString().toStdString(); - update_info.release_date = json.value(QStringLiteral("release_date")).toString().toStdString(); + for (const QJsonValue& release_value : doc.array()) { + QJsonObject release_obj = release_value.toObject(); + if (release_obj.value(QStringLiteral("name")).toString().toLower().contains(platform_identifier)) { + UpdateInfo update_info; + update_info.version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString(); + update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString(); + update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString(); - std::string current_version = GetCurrentVersion(); - update_info.is_newer_version = CompareVersions(current_version, update_info.version); + 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(_WIN32) + if (asset_name.endsWith(QStringLiteral(".zip"))) { + update_info.download_url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + break; + } +#elif defined(__linux__) + if (asset_name.endsWith(QStringLiteral(".AppImage"))) { + update_info.download_url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + break; + } +#endif + } - LOG_INFO(Frontend, "Update check completed - Current: {}, Latest: {}, Has update: {}", - current_version, update_info.version, update_info.is_newer_version); - - emit UpdateCheckCompleted(update_info.is_newer_version, update_info); + if (!update_info.download_url.empty()) { + update_info.is_newer_version = CompareVersions(GetCurrentVersion(), update_info.version); + emit UpdateCheckCompleted(update_info.is_newer_version, update_info); + return; + } + } + } + emit UpdateError(QStringLiteral("Could not find a recent update for your platform.")); } bool UpdaterService::CompareVersions(const std::string& current, const std::string& latest) const { - // Simple version comparison (assumes semantic versioning like 1.2.3) - std::regex version_regex(R"((\d+)\.(\d+)\.(\d+)(?:-(.+))?)"); - std::smatch current_match, latest_match; - - if (!std::regex_match(current, current_match, version_regex) || - !std::regex_match(latest, latest_match, version_regex)) { - // Fallback to string comparison if regex fails + auto get_build_num = [](const std::string& version_str) -> int { + size_t pos = version_str.find_last_of('-'); + if (pos != std::string::npos && pos + 1 < version_str.length()) { + try { + return std::stoi(version_str.substr(pos + 1)); + } catch (...) { return 0; } + } + return 0; + }; + int current_build = get_build_num(current); + int latest_build = get_build_num(latest); + if (current_build == 0 || latest_build == 0) { return latest > current; } - - // Compare major, minor, patch versions - for (int i = 1; i <= 3; ++i) { - int current_num = std::stoi(current_match[i].str()); - int latest_num = std::stoi(latest_match[i].str()); - - if (latest_num > current_num) return true; - if (latest_num < current_num) return false; - } - - return false; // Versions are equal + return latest_build > current_build; } +#ifdef _WIN32 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(); - struct archive_entry* entry; - int r; - - if (!a || !ext) { - LOG_ERROR(Frontend, "Failed to create archive objects"); - return false; - } - - // Configure archive reader for 7z + if (!a || !ext) return false; archive_read_support_format_7zip(a); archive_read_support_filter_all(a); - - // Configure archive writer archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); archive_write_disk_set_standard_lookup(ext); - - r = archive_read_open_filename(a, archive_path.string().c_str(), 10240); - if (r != ARCHIVE_OK) { - LOG_ERROR(Frontend, "Failed to open archive: {}", archive_error_string(a)); - archive_read_free(a); - archive_write_free(ext); - return false; - } - - // Create extraction directory + if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) return false; EnsureDirectoryExists(extract_path); - - // Extract files + struct archive_entry* entry; while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { - if (cancel_requested.load()) { - break; - } - - // Set the extraction path + 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()); - - r = archive_write_header(ext, entry); - if (r != ARCHIVE_OK) { - LOG_WARNING(Frontend, "Failed to write header for: {}", archive_entry_pathname(entry)); - continue; - } - - // Copy file data + 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); } - archive_read_close(a); archive_read_free(a); archive_write_close(ext); archive_write_free(ext); - return !cancel_requested.load(); #else -#ifdef _WIN32 - // Windows fallback: use system 7zip or PowerShell return ExtractArchiveWindows(archive_path, extract_path); -#else - LOG_ERROR(Frontend, "Archive extraction requires libarchive on this platform."); - (void)archive_path; - (void)extract_path; - return false; -#endif #endif } -#if defined(_WIN32) && !defined(CITRON_ENABLE_LIBARCHIVE) +#if !defined(CITRON_ENABLE_LIBARCHIVE) bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { - // Create extraction directory EnsureDirectoryExists(extract_path); - - // Try 7zip first (most common on Windows) std::string sevenzip_cmd = "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; - - LOG_INFO(Frontend, "Attempting extraction with 7zip: {}", sevenzip_cmd); - - int result = std::system(sevenzip_cmd.c_str()); - if (result == 0) { - LOG_INFO(Frontend, "Archive extracted successfully with 7zip"); - return true; - } - - // Fallback to PowerShell for zip files (won't work for 7z) - std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" + - archive_path.string() + "\\\" -DestinationPath \\\"" + - extract_path.string() + "\\\" -Force\""; - - LOG_INFO(Frontend, "Attempting extraction with PowerShell: {}", powershell_cmd); - - result = std::system(powershell_cmd.c_str()); - if (result == 0) { - LOG_INFO(Frontend, "Archive extracted successfully with PowerShell"); - return true; - } - - // Both extraction methods failed - LOG_ERROR(Frontend, "Failed to extract archive automatically. Please install 7-Zip or ensure PowerShell is available."); - - // For now, return false - in a real implementation, you might want to: - // 1. Show a dialog asking user to install 7-Zip - // 2. Provide manual extraction instructions - // 3. Download and install 7-Zip automatically + 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; } #endif bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) { try { - // Check if there's a single directory in the update path (common with archives) std::filesystem::path source_path = update_path; - std::vector top_level_items; for (const auto& entry : std::filesystem::directory_iterator(update_path)) { top_level_items.push_back(entry.path()); } - - // If there's only one top-level directory, use it as the source if (top_level_items.size() == 1 && std::filesystem::is_directory(top_level_items[0])) { source_path = top_level_items[0]; - LOG_INFO(Frontend, "Found single directory in archive: {}", source_path.filename().string()); } - - // Create a staging directory for the update std::filesystem::path staging_path = app_directory / "update_staging"; EnsureDirectoryExists(staging_path); - - // Copy all files to staging directory first (this avoids file-in-use issues) 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 staging_dest = staging_path / relative_path; - - // Create destination directory if it doesn't exist std::filesystem::create_directories(staging_dest.parent_path()); - - // Copy to staging directory - std::filesystem::copy_file(entry.path(), staging_dest, - std::filesystem::copy_options::overwrite_existing); - - LOG_DEBUG(Frontend, "Staged file: {} -> {}", entry.path().string(), staging_dest.string()); + std::filesystem::copy_file(entry.path(), staging_dest, std::filesystem::copy_options::overwrite_existing); } } - - // Create update manifest for post-restart installation std::filesystem::path manifest_file = staging_path / "update_manifest.txt"; std::ofstream manifest(manifest_file); if (manifest.is_open()) { manifest << "UPDATE_VERSION=" << current_update_info.version << "\n"; manifest << "UPDATE_TIMESTAMP=" << std::time(nullptr) << "\n"; manifest << "APP_DIRECTORY=" << app_directory.string() << "\n"; - manifest.close(); } - - LOG_INFO(Frontend, "Update staged successfully. Files prepared in: {}", staging_path.string()); - LOG_INFO(Frontend, "Update will be applied after application restart."); - + LOG_INFO(Frontend, "Update staged successfully."); return true; } catch (const std::exception& e) { LOG_ERROR(Frontend, "Failed to install update: {}", e.what()); @@ -710,39 +440,27 @@ bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) { bool UpdaterService::CreateBackup() { try { std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion()); - if (std::filesystem::exists(backup_dir)) { std::filesystem::remove_all(backup_dir); } - std::filesystem::create_directories(backup_dir); - - // Backup essential files (executable, dlls, etc.) - 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(); - - // Check if file should be backed up 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; } } - if (should_backup) { std::filesystem::copy_file(entry.path(), backup_dir / filename); } } } - LOG_INFO(Frontend, "Backup created: {}", backup_dir.string()); return true; } catch (const std::exception& e) { @@ -754,20 +472,13 @@ bool UpdaterService::CreateBackup() { bool UpdaterService::RestoreBackup() { try { std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion()); - - if (!std::filesystem::exists(backup_dir)) { - LOG_ERROR(Frontend, "Backup directory not found: {}", backup_dir.string()); - 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"); return true; } catch (const std::exception& e) { @@ -775,39 +486,30 @@ bool UpdaterService::RestoreBackup() { return false; } } +#endif bool UpdaterService::CleanupFiles() { try { - // Remove temporary files if (std::filesystem::exists(temp_download_path)) { - for (const auto& entry : std::filesystem::directory_iterator(temp_download_path)) { - if (entry.path().extension() == ".7z" || - entry.path().extension() == ".zip" || - entry.path().filename() == "extracted") { - std::filesystem::remove_all(entry.path()); + std::filesystem::remove_all(temp_download_path); + } +#ifdef _WIN32 + 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_")) { + backup_dirs.push_back(entry.path()); } } } - - // Remove old backups (keep only the latest 3) - std::vector backup_dirs; - for (const auto& entry : std::filesystem::directory_iterator(backup_path)) { - 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 std::filesystem::path& a, const std::filesystem::path& b) { - return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); - }); - + [](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]); } } - +#endif return true; } catch (const std::exception& e) { LOG_ERROR(Frontend, "Failed to cleanup files: {}", e.what()); @@ -816,7 +518,7 @@ bool UpdaterService::CleanupFiles() { } std::filesystem::path UpdaterService::GetTempDirectory() const { - return std::filesystem::temp_directory_path() / "citron_updater"; + return std::filesystem::path(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / "citron_updater"; } std::filesystem::path UpdaterService::GetApplicationDirectory() const { @@ -840,94 +542,64 @@ bool UpdaterService::EnsureDirectoryExists(const std::filesystem::path& path) co } 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 } bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_directory) { +#ifdef _WIN32 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()); - - // Create backup directory for current files - std::filesystem::path backup_path = app_directory / "backup_before_update"; - if (std::filesystem::exists(backup_path)) { - std::filesystem::remove_all(backup_path); - } - std::filesystem::create_directories(backup_path); - - // Copy files from staging to application directory + 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); + 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; // Skip manifest file - } - + 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 dest_path = app_directory / relative_path; - - // Backup existing file if it exists if (std::filesystem::exists(dest_path)) { - std::filesystem::path backup_dest = backup_path / relative_path; + std::filesystem::path backup_dest = backup_path_dir / relative_path; std::filesystem::create_directories(backup_dest.parent_path()); std::filesystem::copy_file(dest_path, backup_dest); } - - // Create destination directory and copy new file std::filesystem::create_directories(dest_path.parent_path()); - std::filesystem::copy_file(entry.path(), dest_path, - std::filesystem::copy_options::overwrite_existing); - - LOG_DEBUG(Frontend, "Updated file: {}", dest_path.string()); + std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing); } } - - // Read and apply version from manifest std::ifstream manifest(manifest_file); - std::string line; - std::string version; - + std::string line, version; while (std::getline(manifest, line)) { if (line.starts_with("UPDATE_VERSION=")) { - version = line.substr(15); // Remove "UPDATE_VERSION=" + version = line.substr(15); break; } } - manifest.close(); - - // Update version file if (!version.empty()) { std::filesystem::path version_file = app_directory / "version.txt"; std::ofstream vfile(version_file); - if (vfile.is_open()) { - vfile << version; - vfile.close(); - } + if (vfile.is_open()) vfile << version; } - - // Clean up staging directory std::filesystem::remove_all(staging_path); - LOG_INFO(Frontend, "Update applied successfully. Version: {}", version); return true; - } catch (const std::exception& e) { LOG_ERROR(Frontend, "Failed to apply staged update: {}", e.what()); return false; } +#else + return false; +#endif } } // namespace Updater -#ifdef _WIN32 + #include "updater_service.moc" -#endif \ No newline at end of file From 06500c65457ebe96ad054dca9f21be9e9807efe3 Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:42:46 +0000 Subject: [PATCH 05/18] fix: Autoupdater --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 57526c324..3f06f08ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,7 +95,7 @@ set(DEFAULT_ENABLE_OPENSSL ON) if (ANDROID OR WIN32 OR APPLE) set(DEFAULT_ENABLE_OPENSSL OFF) endif() -option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL}) +option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ON) if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL) set(vvl_version "vulkan-sdk-1.4.328.1") From 339d78cd081a04de02549635b8790ede79f5e623 Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:43:27 +0000 Subject: [PATCH 06/18] fix: Autoupdater --- src/citron/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index ebf5e99aa..fffe2cf99 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -429,7 +429,7 @@ else() target_link_libraries(citron PRIVATE common core input_common frontend_common network video_core) endif() -target_link_libraries(citron PRIVATE Boost::headers glad Qt6::Widgets) +target_link_libraries(citron PRIVATE Boost::headers glad Qt6::Widgets Qt6::Network) target_link_libraries(citron PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) # Add libarchive for updater functionality From f8a96f4964850931ddb0f345eda97b06c5d34c7a Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 10:44:01 +0000 Subject: [PATCH 07/18] fix: Autoupdater --- src/citron/main.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citron/main.h b/src/citron/main.h index a994a583b..729bc6aae 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -59,7 +59,7 @@ class QtControllerSelectorDialog; class QtProfileSelectionDialog; class QtSoftwareKeyboardDialog; class QtNXWebEngineView; -class UpdaterDialog; +namespace Updater { class UpdaterDialog; } enum class StartGameType { Normal, Global }; From 7df644499acce03ad51b61c713736499f211a413 Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 11:29:21 +0000 Subject: [PATCH 08/18] fix: Auto Updater --- src/citron/updater/updater_service.h | 170 +++++++++++---------------- 1 file changed, 70 insertions(+), 100 deletions(-) diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index 07c0cdda9..edd912fa7 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -3,123 +3,93 @@ #pragma once -#include -#include +#include #include #include -#include -#include -#include -#include #include -#include -#include -#include +#include +#include + +// Forward declare to keep headers clean +class QNetworkAccessManager; namespace Updater { -struct UpdateInfo { - std::string version; - std::string download_url; - std::string changelog; - std::string release_date; - bool is_newer_version = false; -}; - -class UpdaterService : public QObject { - Q_OBJECT - -public: - enum class UpdateResult { - Success, - Failed, - Cancelled, - NetworkError, - ExtractionError, - PermissionError, - InvalidArchive, - NoUpdateAvailable + struct DownloadOption { + std::string name; + std::string url; }; - explicit UpdaterService(QObject* parent = nullptr); - ~UpdaterService() override; + struct UpdateInfo { + std::string version; + std::vector download_options; // Used for both Windows .zip and Linux .AppImage + std::string changelog; + std::string release_date; + bool is_newer_version = false; + }; - // Check for updates from the configured URL - void CheckForUpdates(const std::string& update_url); + class UpdaterService : public QObject { + Q_OBJECT - // Download and install update - void DownloadAndInstallUpdate(const UpdateInfo& update_info); + public: + enum class UpdateResult { Success, Failed, Cancelled, NetworkError, ExtractionError, PermissionError, InvalidArchive, NoUpdateAvailable }; - // Cancel current operation - void CancelUpdate(); + explicit UpdaterService(QObject* parent = nullptr); + ~UpdaterService() override; - // Get current application version - std::string GetCurrentVersion() const; + void CheckForUpdates(const std::string& update_url); + void DownloadAndInstallUpdate(const std::string& download_url); + void CancelUpdate(); + std::string GetCurrentVersion() const; + bool IsUpdateInProgress() const; - // Check if update is in progress - bool IsUpdateInProgress() const; + static bool HasStagedUpdate(const std::filesystem::path& app_directory); + static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); - // Static methods for startup update handling - static bool HasStagedUpdate(const std::filesystem::path& app_directory); - static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); + signals: + void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); + void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); + void UpdateInstallProgress(int percentage, const QString& current_file); + void UpdateCompleted(UpdateResult result, const QString& message); + void UpdateError(const QString& error_message); -signals: - void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); - void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); - void UpdateInstallProgress(int percentage, const QString& current_file); - void UpdateCompleted(UpdateResult result, const QString& message); - void UpdateError(const QString& error_message); + private slots: + void OnDownloadFinished(); + void OnDownloadProgress(qint64 bytes_received, qint64 bytes_total); + void OnDownloadError(QNetworkReply::NetworkError error); -private slots: - void OnDownloadFinished(); - void OnDownloadProgress(qint64 bytes_received, qint64 bytes_total); - void OnDownloadError(QNetworkReply::NetworkError error); + private: + void InitializeSSL(); + void ConfigureSSLForRequest(QNetworkRequest& request); + void ParseUpdateResponse(const QByteArray& response); + bool CompareVersions(const std::string& current, const std::string& latest) const; -private: - // Network operations - void ParseUpdateResponse(const QByteArray& response); - 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 + bool InstallUpdate(const std::filesystem::path& update_path); + bool CreateBackup(); + bool RestoreBackup(); + #endif + bool CleanupFiles(); + std::filesystem::path GetTempDirectory() const; + std::filesystem::path GetApplicationDirectory() const; + std::filesystem::path GetBackupDirectory() const; + bool EnsureDirectoryExists(const std::filesystem::path& path) const; - // SSL configuration - void InitializeSSL(); - void ConfigureSSLForRequest(QNetworkRequest& request); + std::unique_ptr network_manager; + QNetworkReply* current_reply = nullptr; + std::atomic update_in_progress{false}; + std::atomic cancel_requested{false}; + UpdateInfo current_update_info; + std::filesystem::path app_directory; + std::filesystem::path temp_download_path; + std::filesystem::path backup_path; - // File operations - bool ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path); -#if defined(_WIN32) && !defined(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 CleanupFiles(); + static constexpr const char* CITRON_VERSION_FILE = "version.txt"; + static constexpr const char* BACKUP_DIRECTORY = "backup"; + }; - // Helper functions - std::filesystem::path GetTempDirectory() const; - std::filesystem::path GetApplicationDirectory() const; - std::filesystem::path GetBackupDirectory() const; - bool EnsureDirectoryExists(const std::filesystem::path& path) const; - - // Network components - std::unique_ptr network_manager; - QNetworkReply* current_reply = nullptr; - - // Update state - std::atomic update_in_progress{false}; - std::atomic cancel_requested{false}; - UpdateInfo current_update_info; - - // File paths - std::filesystem::path temp_download_path; - std::filesystem::path backup_path; - std::filesystem::path app_directory; - - // Constants - static constexpr const char* CITRON_VERSION_FILE = "version.txt"; - static constexpr const char* UPDATE_MANIFEST_FILE = "update_manifest.json"; - static constexpr const char* BACKUP_DIRECTORY = "backup"; - static constexpr const char* TEMP_DIRECTORY = "temp"; - static constexpr size_t MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024; // 500MB limit -}; - -} // namespace Updater \ No newline at end of file +} // namespace Updater From ada780f3f8ee9b28313e16c9dad7b685ae533adc Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 11:30:19 +0000 Subject: [PATCH 09/18] fix: Auto Updater --- src/citron/updater/updater_service.cpp | 32 ++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index d9f046cfe..cd2729736 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -109,21 +110,20 @@ void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) { request.setSslConfiguration(sslConfig); } -void UpdaterService::DownloadAndInstallUpdate(const UpdateInfo& update_info) { +void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) { if (update_in_progress.load()) { emit UpdateError(QStringLiteral("Update operation already in progress")); return; } - if (update_info.download_url.empty()) { + if (download_url.empty()) { emit UpdateError(QStringLiteral("Invalid download URL.")); return; } update_in_progress.store(true); cancel_requested.store(false); - current_update_info = update_info; - LOG_INFO(Frontend, "Starting update download from {}", update_info.download_url); + LOG_INFO(Frontend, "Starting update download from {}", download_url); #ifdef _WIN32 if (!CreateBackup()) { @@ -133,7 +133,7 @@ void UpdaterService::DownloadAndInstallUpdate(const UpdateInfo& update_info) { } #endif - QUrl url(QString::fromStdString(update_info.download_url)); + QUrl url(QString::fromStdString(download_url)); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); current_reply = network_manager->get(request); @@ -313,19 +313,33 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response) { QString asset_name = asset_obj.value(QStringLiteral("name")).toString(); #if defined(_WIN32) if (asset_name.endsWith(QStringLiteral(".zip"))) { - update_info.download_url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + DownloadOption option; + option.name = asset_name.toStdString(); + option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + update_info.download_options.push_back(option); break; } #elif defined(__linux__) if (asset_name.endsWith(QStringLiteral(".AppImage"))) { - update_info.download_url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); - break; + DownloadOption option; + QString friendly_name = asset_name; + + friendly_name.remove(QRegularExpression(QStringLiteral(R"(^citron-linux-\d*-x86_64-?)"), QRegularExpression::CaseInsensitiveOption)); + friendly_name.remove(QStringLiteral(".AppImage")); + if (friendly_name.isEmpty()) { + option.name = "AppImage"; + } else { + option.name = friendly_name.toUpper().toStdString(); + } + option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + update_info.download_options.push_back(option); } #endif } - if (!update_info.download_url.empty()) { + if (!update_info.download_options.empty()) { 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; } From c1dda00eea296cb8a40d168db3a3ec79d3401d5d Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 11:30:59 +0000 Subject: [PATCH 10/18] fix: Auto Updater --- src/citron/updater/updater_dialog.cpp | 55 ++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/citron/updater/updater_dialog.cpp b/src/citron/updater/updater_dialog.cpp index 63b36ff75..b27ab1e5e 100644 --- a/src/citron/updater/updater_dialog.cpp +++ b/src/citron/updater/updater_dialog.cpp @@ -116,8 +116,27 @@ void UpdaterDialog::OnUpdateError(const QString& error_message) { } void UpdaterDialog::OnDownloadButtonClicked() { - ShowDownloadingState(); - updater_service->DownloadAndInstallUpdate(current_update_info); + std::string download_url; + +#ifdef __linux__ + if (ui->appImageSelector->isVisible() && !current_update_info.download_options.empty()) { + int current_index = ui->appImageSelector->currentIndex(); + if (current_index >= 0 && static_cast(current_index) < current_update_info.download_options.size()) { + download_url = current_update_info.download_options[current_index].url; + } + } +#endif + + if (download_url.empty() && !current_update_info.download_options.empty()) { + download_url = current_update_info.download_options[0].url; + } + + if (!download_url.empty()) { + ShowDownloadingState(); + updater_service->DownloadAndInstallUpdate(download_url); + } else { + OnUpdateError(QStringLiteral("No download URL could be found for the update.")); + } } void UpdaterDialog::OnCancelButtonClicked() { @@ -151,6 +170,8 @@ void UpdaterDialog::SetupUI() { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); setFixedSize(size()); ui->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion())); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); ShowCheckingState(); } @@ -166,6 +187,8 @@ void UpdaterDialog::ShowCheckingState() { ui->closeButton->setVisible(false); ui->restartButton->setVisible(false); ui->cancelButton->setText(QStringLiteral("Cancel")); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); } void UpdaterDialog::ShowNoUpdateState() { @@ -179,6 +202,8 @@ void UpdaterDialog::ShowNoUpdateState() { ui->cancelButton->setVisible(false); ui->closeButton->setVisible(true); ui->restartButton->setVisible(false); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); } void UpdaterDialog::ShowUpdateAvailableState() { @@ -193,6 +218,24 @@ void UpdaterDialog::ShowUpdateAvailableState() { } else { ui->changelogGroup->setVisible(false); } + +#ifdef __linux__ + if (current_update_info.download_options.size() > 1) { + ui->appImageSelector->clear(); + for (const auto& option : current_update_info.download_options) { + ui->appImageSelector->addItem(QString::fromStdString(option.name)); + } + ui->appImageSelectorLabel->setVisible(true); + ui->appImageSelector->setVisible(true); + } else { + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); + } +#else + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); +#endif + ui->updateInfoGroup->setVisible(true); ui->progressGroup->setVisible(false); ui->downloadButton->setVisible(true); @@ -218,6 +261,8 @@ void UpdaterDialog::ShowDownloadingState() { ui->closeButton->setVisible(false); ui->restartButton->setVisible(false); ui->cancelButton->setText(QStringLiteral("Cancel")); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); progress_timer->start(); } @@ -229,6 +274,8 @@ void UpdaterDialog::ShowInstallingState() { ui->progressLabel->setText(QStringLiteral("Installing...")); ui->downloadInfoLabel->setText(QStringLiteral("")); ui->cancelButton->setVisible(false); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); } void UpdaterDialog::ShowCompletedState() { @@ -243,6 +290,8 @@ void UpdaterDialog::ShowCompletedState() { ui->closeButton->setVisible(true); ui->restartButton->setVisible(true); ui->progressBar->setValue(100); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); } void UpdaterDialog::ShowErrorState() { @@ -255,6 +304,8 @@ void UpdaterDialog::ShowErrorState() { ui->cancelButton->setVisible(false); ui->closeButton->setVisible(true); ui->restartButton->setVisible(false); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); } void UpdaterDialog::UpdateDownloadProgress(int percentage, qint64 bytes_received, From ee7aebca9b26b4f78c5380c579699b920ebd35d9 Mon Sep 17 00:00:00 2001 From: collecting Date: Fri, 24 Oct 2025 11:31:39 +0000 Subject: [PATCH 11/18] fix: Auto Updater --- src/citron/updater/updater_dialog.ui | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/citron/updater/updater_dialog.ui b/src/citron/updater/updater_dialog.ui index 791e807fd..d9368388e 100644 --- a/src/citron/updater/updater_dialog.ui +++ b/src/citron/updater/updater_dialog.ui @@ -192,6 +192,22 @@ p, li { white-space: pre-wrap; } + + + + + + + Select AppImage: + + + + + + + + + @@ -250,4 +266,4 @@ p, li { white-space: pre-wrap; } - \ No newline at end of file + From ab3429ed1c69b68419d9882256c7b4986f38ff8c Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 01:02:37 +0000 Subject: [PATCH 12/18] Edit updater_dialog.h --- src/citron/updater/updater_dialog.h | 93 ++++++++++++++--------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/src/citron/updater/updater_dialog.h b/src/citron/updater/updater_dialog.h index d8d2afd89..fa517e345 100644 --- a/src/citron/updater/updater_dialog.h +++ b/src/citron/updater/updater_dialog.h @@ -3,68 +3,63 @@ #pragma once -#include #include -#include +#include #include "citron/updater/updater_service.h" +// Forward declare QString for the helper function. +class QString; + namespace Ui { - class UpdaterDialog; +class UpdaterDialog; } namespace Updater { - class UpdaterDialog : public QDialog { - Q_OBJECT +// Add the declaration for the date formatting helper function. +QString FormatDateTimeString(const std::string& iso_string); - public: - explicit UpdaterDialog(QWidget* parent = nullptr); - ~UpdaterDialog() override; +class UpdaterDialog : public QDialog { + Q_OBJECT - void CheckForUpdates(const std::string& update_url); +public: + explicit UpdaterDialog(QWidget* parent = nullptr); + ~UpdaterDialog() override; - private slots: - void OnUpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); - void OnUpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); - void OnUpdateInstallProgress(int percentage, const QString& current_file); - void OnUpdateCompleted(Updater::UpdaterService::UpdateResult result, const QString& message); - void OnUpdateError(const QString& error_message); - void OnDownloadButtonClicked(); - void OnCancelButtonClicked(); - void OnCloseButtonClicked(); - void OnRestartButtonClicked(); + void CheckForUpdates(const std::string& update_url); - private: - void SetupUI(); - void ShowCheckingState(); - void ShowNoUpdateState(); - void ShowUpdateAvailableState(); - void ShowDownloadingState(); - void ShowInstallingState(); - void ShowCompletedState(); - void ShowErrorState(); - void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); - void UpdateInstallProgress(int percentage, const QString& current_file); - QString FormatBytes(qint64 bytes) const; - QString GetUpdateMessage(Updater::UpdaterService::UpdateResult result) const; +private slots: + void OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info); + void OnUpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); + void OnUpdateInstallProgress(int percentage, const QString& current_file); + void OnUpdateCompleted(Updater::UpdaterService::UpdateResult result, const QString& message); + void OnUpdateError(const QString& error_message); + void OnDownloadButtonClicked(); + void OnCancelButtonClicked(); + void OnCloseButtonClicked(); + void OnRestartButtonClicked(); - enum class State { - Checking, - NoUpdate, - UpdateAvailable, - Downloading, - Installing, - Completed, - Error - }; +private: + enum class State { Checking, NoUpdate, UpdateAvailable, Downloading, Installing, Completed, Error }; - std::unique_ptr ui; - std::unique_ptr updater_service; - UpdateInfo current_update_info; - State current_state; - qint64 total_download_size; - qint64 downloaded_bytes; - QTimer* progress_timer; - }; + void SetupUI(); + void ShowCheckingState(); + void ShowNoUpdateState(const Updater::UpdateInfo& update_info); + void ShowUpdateAvailableState(); + void ShowDownloadingState(); + void ShowInstallingState(); + void ShowCompletedState(); + void ShowErrorState(); + QString FormatBytes(qint64 bytes) const; + QString GetUpdateMessage(Updater::UpdaterService::UpdateResult result) const; + + std::unique_ptr ui; + std::unique_ptr updater_service; + UpdateInfo current_update_info; + State current_state; + qint64 total_download_size; + qint64 downloaded_bytes; + QTimer* progress_timer; +}; } // namespace Updater From 66fc50646f1c57fcd44b0567971b2139dd17b560 Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 01:03:30 +0000 Subject: [PATCH 13/18] Edit updater_dialog.cpp --- src/citron/updater/updater_dialog.cpp | 53 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/citron/updater/updater_dialog.cpp b/src/citron/updater/updater_dialog.cpp index b27ab1e5e..58716ee8f 100644 --- a/src/citron/updater/updater_dialog.cpp +++ b/src/citron/updater/updater_dialog.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,21 @@ namespace Updater { +// Helper function to format the date and time +QString FormatDateTimeString(const std::string& iso_string) { + if (iso_string.empty() || iso_string == "Unknown") { + return QStringLiteral("Unknown"); + } + // Parse the ISO 8601 date string provided by the GitHub API. + QDateTime date_time = QDateTime::fromString(QString::fromStdString(iso_string), Qt::ISODate); + if (!date_time.isValid()) { + return QString::fromStdString(iso_string); // Fallback to original if parsing fails. + } + // Convert from UTC (which the 'Z' indicates) to the user's local time + // and format it in a friendly, readable way. + return date_time.toLocalTime().toString(QStringLiteral("MMMM d, yyyy 'at' hh:mm AP")); +} + UpdaterDialog::UpdaterDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique()), updater_service(std::make_unique(this)), @@ -66,7 +82,7 @@ void UpdaterDialog::OnUpdateCheckCompleted(bool has_update, const Updater::Updat current_update_info = update_info; ShowUpdateAvailableState(); } else { - ShowNoUpdateState(); + ShowNoUpdateState(update_info); } } @@ -168,7 +184,10 @@ void UpdaterDialog::OnRestartButtonClicked() { void UpdaterDialog::SetupUI() { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - setFixedSize(size()); + + // MODIFIED: Use setMinimumSize to allow the window to be resized larger. + setMinimumSize(size()); + ui->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion())); ui->appImageSelectorLabel->setVisible(false); ui->appImageSelector->setVisible(false); @@ -191,11 +210,17 @@ void UpdaterDialog::ShowCheckingState() { ui->appImageSelector->setVisible(false); } -void UpdaterDialog::ShowNoUpdateState() { +void UpdaterDialog::ShowNoUpdateState(const Updater::UpdateInfo& update_info) { current_state = State::NoUpdate; ui->titleLabel->setText(QStringLiteral("No updates available")); ui->statusLabel->setText(QStringLiteral("You are running the latest version of Citron.")); ui->updateInfoGroup->setVisible(true); + + ui->latestVersionValue->setText(QString::fromStdString(update_info.version)); + + // MODIFIED: Use the new helper function to format the release date. + ui->releaseDateValue->setText(FormatDateTimeString(update_info.release_date)); + ui->changelogGroup->setVisible(false); ui->progressGroup->setVisible(false); ui->downloadButton->setVisible(false); @@ -211,9 +236,13 @@ void UpdaterDialog::ShowUpdateAvailableState() { ui->titleLabel->setText(QStringLiteral("Update available")); ui->statusLabel->setText(QStringLiteral("A new version of Citron is available for download.")); ui->latestVersionValue->setText(QString::fromStdString(current_update_info.version)); - ui->releaseDateValue->setText(QString::fromStdString(current_update_info.release_date)); + + // MODIFIED: Use the new helper function to format the release date. + ui->releaseDateValue->setText(FormatDateTimeString(current_update_info.release_date)); + if (!current_update_info.changelog.empty()) { - ui->changelogText->setPlainText(QString::fromStdString(current_update_info.changelog)); + // MODIFIED: Use setMarkdown to render the changelog with formatting and links. + ui->changelogText->setMarkdown(QString::fromStdString(current_update_info.changelog)); ui->changelogGroup->setVisible(true); } else { ui->changelogGroup->setVisible(false); @@ -308,20 +337,6 @@ void UpdaterDialog::ShowErrorState() { ui->appImageSelector->setVisible(false); } -void UpdaterDialog::UpdateDownloadProgress(int percentage, qint64 bytes_received, - qint64 bytes_total) { - downloaded_bytes = bytes_received; - total_download_size = bytes_total; - ui->progressBar->setValue(percentage); - ui->progressLabel->setText(QStringLiteral("Downloading update... %1%").arg(percentage)); -} - -void UpdaterDialog::UpdateInstallProgress(int percentage, const QString& current_file) { - ui->progressBar->setValue(percentage); - ui->progressLabel->setText(QStringLiteral("Installing update... %1%").arg(percentage)); - ui->downloadInfoLabel->setText(current_file); -} - QString UpdaterDialog::FormatBytes(qint64 bytes) const { const QStringList units = {QStringLiteral("B"), QStringLiteral("KB"), QStringLiteral("MB"), QStringLiteral("GB")}; From d329e75fd588fea7995699bb8b5c7df95b326761 Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 01:04:12 +0000 Subject: [PATCH 14/18] Edit updater_service.h --- src/citron/updater/updater_service.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index edd912fa7..0638718bf 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -15,6 +15,8 @@ class QNetworkAccessManager; namespace Updater { + std::string ExtractCommitHash(const std::string& version_string); + struct DownloadOption { std::string name; std::string url; From 7a7cd0004bd5f30d2b73d717b15a6f1a69059e51 Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 01:05:17 +0000 Subject: [PATCH 15/18] Edit updater_service.cpp --- src/citron/updater/updater_service.cpp | 105 ++++++++++++++++++------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index cd2729736..f8e29cf66 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -37,6 +37,18 @@ namespace Updater { +// Helper function to extract a commit hash from a string using std::regex. +// This ensures that both local and remote version strings are normalized to a clean hash. +std::string ExtractCommitHash(const std::string& version_string) { + // This regex finds a sequence of 7 to 40 hexadecimal characters, case-insensitive. + std::regex re("\\b([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 ""; // Return empty if no hash is found +} + UpdaterService::UpdaterService(QObject* parent) : QObject(parent) { network_manager = std::make_unique(this); InitializeSSL(); @@ -154,24 +166,44 @@ void UpdaterService::CancelUpdate() { } std::string UpdaterService::GetCurrentVersion() const { + // Prioritize the compiled-in build version first, as it's the most reliable source. + std::string build_version = Common::g_build_version; + + if (build_version.empty()) { + LOG_WARNING(Frontend, "Updater Debug: Common::g_build_version is EMPTY."); + } else { + LOG_INFO(Frontend, "Updater Debug: Raw Common::g_build_version is '{}'", build_version); + } + + if (!build_version.empty()) { + std::string hash = ExtractCommitHash(build_version); + if (!hash.empty()) { + LOG_INFO(Frontend, "Updater Debug: Extracted hash '{}' from SCM_REV.", hash); + return hash; + } else { + LOG_WARNING(Frontend, "Updater Debug: FAILED to extract hash from Common::g_build_version."); + } + } + + // Fallback to the version file. std::filesystem::path version_file = app_directory / CITRON_VERSION_FILE; if (std::filesystem::exists(version_file)) { std::ifstream file(version_file); if (file.is_open()) { - std::string version; - std::getline(file, version); - if (!version.empty()) return version; + std::string version_from_file; + std::getline(file, version_from_file); + if (!version_from_file.empty()) { + std::string hash = ExtractCommitHash(version_from_file); + if (!hash.empty()){ + LOG_INFO(Frontend, "Found current version from file: {}", hash); + return hash; + } + } } } - std::string build_version = Common::g_build_version; - if (!build_version.empty()) { - try { - std::ofstream vfile(version_file); - if (vfile.is_open()) vfile << build_version; - } catch (...) {} - return build_version; - } - return QCoreApplication::applicationVersion().toStdString(); + + LOG_WARNING(Frontend, "Could not determine a valid commit hash for the current version."); + return ""; // Return empty if no reliable version is found. } bool UpdaterService::IsUpdateInProgress() const { @@ -280,6 +312,7 @@ void UpdaterService::OnDownloadError(QNetworkReply::NetworkError) { update_in_progress.store(false); } +// This function is updated to parse the commit hash from the release name. void UpdaterService::ParseUpdateResponse(const QByteArray& response) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(response, &error); @@ -301,9 +334,19 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response) { for (const QJsonValue& release_value : doc.array()) { QJsonObject release_obj = release_value.toObject(); - if (release_obj.value(QStringLiteral("name")).toString().toLower().contains(platform_identifier)) { + QString release_name = release_obj.value(QStringLiteral("name")).toString(); + + if (release_name.toLower().contains(platform_identifier)) { + // MODIFIED: Use the helper function to extract the commit hash from the remote release name. + std::string latest_hash = ExtractCommitHash(release_name.toStdString()); + + if (latest_hash.empty()) { + continue; // Skip this release if no commit hash is found in its name. + } + UpdateInfo update_info; - update_info.version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString(); + // MODIFIED: The version is now the clean, captured commit hash. + update_info.version = latest_hash; update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString(); update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString(); @@ -323,7 +366,6 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response) { if (asset_name.endsWith(QStringLiteral(".AppImage"))) { DownloadOption option; QString friendly_name = asset_name; - friendly_name.remove(QRegularExpression(QStringLiteral(R"(^citron-linux-\d*-x86_64-?)"), QRegularExpression::CaseInsensitiveOption)); friendly_name.remove(QStringLiteral(".AppImage")); if (friendly_name.isEmpty()) { @@ -338,32 +380,35 @@ void UpdaterService::ParseUpdateResponse(const QByteArray& response) { } if (!update_info.download_options.empty()) { + // MODIFIED: GetCurrentVersion() now returns a clean hash, making this comparison reliable. 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; + return; // Exit after finding the first valid release for the platform. } } } emit UpdateError(QStringLiteral("Could not find a recent update for your platform.")); } +// MODIFIED: This function now compares clean commit hashes instead of complex version strings. bool UpdaterService::CompareVersions(const std::string& current, const std::string& latest) const { - auto get_build_num = [](const std::string& version_str) -> int { - size_t pos = version_str.find_last_of('-'); - if (pos != std::string::npos && pos + 1 < version_str.length()) { - try { - return std::stoi(version_str.substr(pos + 1)); - } catch (...) { return 0; } - } - return 0; - }; - int current_build = get_build_num(current); - int latest_build = get_build_num(latest); - if (current_build == 0 || latest_build == 0) { - return latest > current; + if (current.empty()) { + // NEW: If we don't know our current version, we should assume an update is available + // to allow for recovery from a missing version file. + LOG_WARNING(Frontend, "Current version is unknown, assuming update is available."); + return true; } - return latest_build > current_build; + + if (latest.empty()) { + LOG_WARNING(Frontend, "Latest version from remote is empty, cannot compare."); + return false; + } + + // MODIFIED: For commit hashes, a simple string inequality check is sufficient and reliable. + bool is_newer = (current != latest); + LOG_INFO(Frontend, "Comparing versions. Current: '{}', Latest: '{}'. Is newer: {}", current, latest, is_newer); + return is_newer; } #ifdef _WIN32 From 05da09d070da026ede13fa7c69170ab05b899a87 Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 01:05:44 +0000 Subject: [PATCH 16/18] Edit GenerateSCMRev.cmake --- CMakeModules/GenerateSCMRev.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CMakeModules/GenerateSCMRev.cmake b/CMakeModules/GenerateSCMRev.cmake index 3f5ce0f34..a568e4ea3 100644 --- a/CMakeModules/GenerateSCMRev.cmake +++ b/CMakeModules/GenerateSCMRev.cmake @@ -53,4 +53,11 @@ if (BUILD_REPOSITORY) endif() endif() +# Add a fallback for local builds +# If BUILD_VERSION is still "0", it means the CI-specific logic was not executed. +# In that case, we should use the git description which contains the commit hash. +if (BUILD_VERSION STREQUAL "0") + set(BUILD_VERSION ${GIT_DESC}) +endif() + configure_file(scm_rev.cpp.in scm_rev.cpp @ONLY) From 8050dc0cd2795f5c2256e8ba3b91681307d3889d Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 02:12:01 +0000 Subject: [PATCH 17/18] Edit CMakeLists.txt --- src/citron/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index fffe2cf99..3d87b9515 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -265,6 +265,8 @@ if (CITRON_USE_AUTO_UPDATER) $<$:_WIN32> CITRON_USE_AUTO_UPDATER ) + + target_link_libraries(citron PRIVATE Qt6::Network) endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") From 1175bf35b7ec44514930a42b588a25bdf0a1f887 Mon Sep 17 00:00:00 2001 From: collecting Date: Sat, 25 Oct 2025 02:13:00 +0000 Subject: [PATCH 18/18] Edit updater_service.h --- src/citron/updater/updater_service.h | 137 ++++++++++++++------------- 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index 0638718bf..d60c3ad2b 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -10,88 +10,91 @@ #include #include -// Forward declare to keep headers clean -class QNetworkAccessManager; +#include + +#include namespace Updater { - std::string ExtractCommitHash(const std::string& version_string); +// Declaration for the helper function. +QString FormatDateTimeString(const std::string& iso_string); +std::string ExtractCommitHash(const std::string& version_string); - struct DownloadOption { - std::string name; - std::string url; - }; +struct DownloadOption { + std::string name; + std::string url; +}; - struct UpdateInfo { - std::string version; - std::vector download_options; // Used for both Windows .zip and Linux .AppImage - std::string changelog; - std::string release_date; - bool is_newer_version = false; - }; +struct UpdateInfo { + std::string version; + std::vector download_options; + std::string changelog; + std::string release_date; + bool is_newer_version = false; +}; - class UpdaterService : public QObject { - Q_OBJECT +class UpdaterService : public QObject { + Q_OBJECT - public: - enum class UpdateResult { Success, Failed, Cancelled, NetworkError, ExtractionError, PermissionError, InvalidArchive, NoUpdateAvailable }; +public: + enum class UpdateResult { Success, Failed, Cancelled, NetworkError, ExtractionError, PermissionError, InvalidArchive, NoUpdateAvailable }; - explicit UpdaterService(QObject* parent = nullptr); - ~UpdaterService() override; + explicit UpdaterService(QObject* parent = nullptr); + ~UpdaterService() override; - void CheckForUpdates(const std::string& update_url); - void DownloadAndInstallUpdate(const std::string& download_url); - void CancelUpdate(); - std::string GetCurrentVersion() const; - bool IsUpdateInProgress() const; + void CheckForUpdates(const std::string& update_url); + void DownloadAndInstallUpdate(const std::string& download_url); + void CancelUpdate(); + std::string GetCurrentVersion() const; + bool IsUpdateInProgress() const; - static bool HasStagedUpdate(const std::filesystem::path& app_directory); - static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); + static bool HasStagedUpdate(const std::filesystem::path& app_directory); + static bool ApplyStagedUpdate(const std::filesystem::path& app_directory); - signals: - void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); - void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); - void UpdateInstallProgress(int percentage, const QString& current_file); - void UpdateCompleted(UpdateResult result, const QString& message); - void UpdateError(const QString& error_message); +signals: + void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); + void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total); + void UpdateInstallProgress(int percentage, const QString& current_file); + void UpdateCompleted(UpdateResult result, const QString& message); + void UpdateError(const QString& error_message); - private slots: - void OnDownloadFinished(); - void OnDownloadProgress(qint64 bytes_received, qint64 bytes_total); - void OnDownloadError(QNetworkReply::NetworkError error); +private slots: + void OnDownloadFinished(); + void OnDownloadProgress(qint64 bytes_received, qint64 bytes_total); + void OnDownloadError(QNetworkReply::NetworkError error); - private: - void InitializeSSL(); - void ConfigureSSLForRequest(QNetworkRequest& request); - void ParseUpdateResponse(const QByteArray& response); - bool CompareVersions(const std::string& current, const std::string& latest) const; +private: + void InitializeSSL(); + void ConfigureSSLForRequest(QNetworkRequest& request); + void ParseUpdateResponse(const QByteArray& response); + 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 - bool InstallUpdate(const std::filesystem::path& update_path); - bool CreateBackup(); - bool RestoreBackup(); - #endif - bool CleanupFiles(); - std::filesystem::path GetTempDirectory() const; - std::filesystem::path GetApplicationDirectory() const; - std::filesystem::path GetBackupDirectory() const; - bool EnsureDirectoryExists(const std::filesystem::path& path) 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 + bool InstallUpdate(const std::filesystem::path& update_path); + bool CreateBackup(); + bool RestoreBackup(); + #endif + bool CleanupFiles(); + std::filesystem::path GetTempDirectory() const; + std::filesystem::path GetApplicationDirectory() const; + std::filesystem::path GetBackupDirectory() const; + bool EnsureDirectoryExists(const std::filesystem::path& path) const; - std::unique_ptr network_manager; - QNetworkReply* current_reply = nullptr; - std::atomic update_in_progress{false}; - std::atomic cancel_requested{false}; - UpdateInfo current_update_info; - std::filesystem::path app_directory; - std::filesystem::path temp_download_path; - std::filesystem::path backup_path; + std::unique_ptr network_manager; + QNetworkReply* current_reply = nullptr; + std::atomic update_in_progress{false}; + std::atomic cancel_requested{false}; + UpdateInfo current_update_info; + std::filesystem::path app_directory; + std::filesystem::path temp_download_path; + std::filesystem::path backup_path; - static constexpr const char* CITRON_VERSION_FILE = "version.txt"; - static constexpr const char* BACKUP_DIRECTORY = "backup"; - }; + static constexpr const char* CITRON_VERSION_FILE = "version.txt"; + static constexpr const char* BACKUP_DIRECTORY = "backup"; +}; } // namespace Updater