From 8a2b1fb65d6fe635173ba882c6b741e040669a3c Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 26 Dec 2025 02:22:59 +0000 Subject: [PATCH] feat(updater): Improved Updater System w/ Automation & SCM Logic Changes Signed-off-by: Collecting --- src/citron/updater/updater_service.cpp | 853 +++++++------------------ 1 file changed, 231 insertions(+), 622 deletions(-) diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index f43902ad9..e3add434f 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include @@ -45,36 +44,26 @@ const std::string STABLE_UPDATE_URL = "https://git.citron-emu.org/api/v1/repos/C const std::string NIGHTLY_UPDATE_URL = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases"; std::string ExtractCommitHash(const std::string& version_string) { - std::regex re("\\b([0-9a-fA-F]{7,40})\\b"); + std::regex re("([0-9a-fA-F]{7,40})"); std::smatch match; - if (std::regex_search(version_string, match, re) && match.size() > 1) { - return match[1].str(); + if (std::regex_search(version_string, match, re)) { + return match[0].str(); } return ""; } -// Helper function to calculate the SHA256 hash of a file. -QByteArray GetFileChecksum(const std::filesystem::path& file_path) { - QFile file(QString::fromStdString(file_path.string())); - if (file.open(QIODevice::ReadOnly)) { - QCryptographicHash hash(QCryptographicHash::Sha256); - if (hash.addData(&file)) { - return hash.result(); - } - } - return QByteArray(); -} - - UpdaterService::UpdaterService(QObject* parent) : QObject(parent) { network_manager = std::make_unique(this); InitializeSSL(); - app_directory = GetApplicationDirectory(); - temp_download_path = GetTempDirectory(); - backup_path = GetBackupDirectory(); - EnsureDirectoryExists(temp_download_path); + + app_directory = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); + temp_download_path = std::filesystem::path(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / "citron_updater"; + backup_path = app_directory / BACKUP_DIRECTORY; + + if (!std::filesystem::exists(temp_download_path)) { + std::filesystem::create_directories(temp_download_path); + } EnsureDirectoryExists(backup_path); - LOG_INFO(Frontend, "UpdaterService initialized"); } UpdaterService::~UpdaterService() { @@ -86,171 +75,73 @@ UpdaterService::~UpdaterService() { } void UpdaterService::InitializeSSL() { - LOG_INFO(Frontend, "Attempting to initialize SSL support..."); - - // Check if SSL is supported - if (!QSslSocket::supportsSsl()) { - LOG_WARNING(Frontend, "SSL support not available"); - LOG_WARNING(Frontend, "Build-time SSL version: {}", QSslSocket::sslLibraryBuildVersionString().toStdString()); - LOG_WARNING(Frontend, "Runtime SSL version: {}", QSslSocket::sslLibraryVersionString().toStdString()); - -#ifdef _WIN32 - // Try to provide helpful information about missing DLLs - std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); - std::filesystem::path crypto_dll = app_dir / "libcrypto-3-x64.dll"; - std::filesystem::path ssl_dll = app_dir / "libssl-3-x64.dll"; - - LOG_WARNING(Frontend, "libcrypto-3-x64.dll exists: {}", std::filesystem::exists(crypto_dll)); - LOG_WARNING(Frontend, "libssl-3-x64.dll exists: {}", std::filesystem::exists(ssl_dll)); -#endif - return; - } - - LOG_INFO(Frontend, "SSL library version: {}", QSslSocket::sslLibraryVersionString().toStdString()); - + if (!QSslSocket::supportsSsl()) return; QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - auto certs = QSslConfiguration::systemCaCertificates(); - if (!certs.isEmpty()) { - sslConfig.setCaCertificates(certs); - } else { - LOG_WARNING(Frontend, "No system CA certificates available"); - } sslConfig.setProtocol(QSsl::SecureProtocols); QSslConfiguration::setDefaultConfiguration(sslConfig); - LOG_INFO(Frontend, "SSL initialized successfully"); } -void UpdaterService::CheckForUpdates() { - if (update_in_progress.load()) { - emit UpdateError(QStringLiteral("Update operation already in progress")); - return; - } - QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString(); - std::string update_url = (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL; - LOG_INFO(Frontend, "Selected update channel: {}", channel.toStdString()); - LOG_INFO(Frontend, "Checking for updates from: {}", update_url); - QUrl url{QString::fromStdString(update_url)}; - QNetworkRequest request{url}; +void UpdaterService::CheckForUpdates(const QString& override_channel) { + if (update_in_progress.load()) return; + + QString channel = override_channel.isEmpty() ? + QSettings().value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString() : override_channel; + + std::string update_url = (channel.toLower() == QStringLiteral("nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL; + + QNetworkRequest request{QUrl(QString::fromStdString(update_url))}; request.setRawHeader("User-Agent", QByteArrayLiteral("Citron-Updater/1.0")); - request.setRawHeader("Accept", QByteArrayLiteral("application/json")); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + current_reply = network_manager->get(request); connect(current_reply, &QNetworkReply::finished, this, [this, channel]() { if (!current_reply) return; if (current_reply->error() == QNetworkReply::NoError) { ParseUpdateResponse(current_reply->readAll(), channel); } else { - emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString())); + emit UpdateError(current_reply->errorString()); } current_reply->deleteLater(); current_reply = nullptr; }); } -void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) { - if (!QSslSocket::supportsSsl()) return; - QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); - sslConfig.setProtocol(QSsl::SecureProtocols); - request.setSslConfiguration(sslConfig); -} - void UpdaterService::DownloadAndInstallUpdate(const std::string& download_url) { - if (update_in_progress.load()) { - emit UpdateError(QStringLiteral("Update operation already in progress")); - return; - } - if (download_url.empty()) { - emit UpdateError(QStringLiteral("Invalid download URL.")); - return; - } - + if (update_in_progress.load()) return; update_in_progress.store(true); cancel_requested.store(false); - LOG_INFO(Frontend, "Starting update download from {}", download_url); - #ifdef _WIN32 if (!CreateBackup()) { - emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup")); + emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup of citron.exe")); update_in_progress.store(false); return; } #endif - QUrl url(QString::fromStdString(download_url)); - QNetworkRequest request(url); + QNetworkRequest request{QUrl(QString::fromStdString(download_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, &UpdaterService::OnDownloadFinished); - connect(current_reply, &QNetworkReply::errorOccurred, this, &UpdaterService::OnDownloadError); } void UpdaterService::CancelUpdate() { 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")); + if (current_reply) current_reply->abort(); update_in_progress.store(false); } -std::string UpdaterService::GetCurrentVersion() const { - QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); +std::string UpdaterService::GetCurrentVersion(const QString& channel) const { + QString active = channel.isEmpty() ? + QSettings().value(QStringLiteral("updater/channel"), QStringLiteral("Nightly")).toString() : channel; - // If the user's setting is Nightly, we must ignore version.txt and only use the commit hash. - if (channel == QStringLiteral("Nightly")) { - std::string build_version = Common::g_build_version; - if (!build_version.empty()) { - std::string hash = ExtractCommitHash(build_version); - if (!hash.empty()) { - return hash; - } - } - return ""; // Fallback if no hash is found + if (active.toLower() == QStringLiteral("nightly")) { + return Common::g_citron_hash; } - - // Otherwise (channel is Stable), we prioritize version.txt. - std::filesystem::path search_path; -#ifdef __linux__ - const char* appimage_path_env = qgetenv("APPIMAGE").constData(); - if (appimage_path_env && strlen(appimage_path_env) > 0) { - search_path = std::filesystem::path(appimage_path_env).parent_path(); - } else { - search_path = app_directory; - } -#else - search_path = app_directory; -#endif - - std::filesystem::path version_file = search_path / CITRON_VERSION_FILE; - if (std::filesystem::exists(version_file)) { - std::ifstream file(version_file); - if (file.is_open()) { - std::string version_from_file; - std::getline(file, version_from_file); - if (!version_from_file.empty()) { - return version_from_file; - } - } - } - - // Fallback for Stable channel: If version.txt is missing, use the commit hash. - // This allows a nightly build to correctly check for a stable update. - std::string build_version = Common::g_build_version; - if (!build_version.empty()) { - std::string hash = ExtractCommitHash(build_version); - if (!hash.empty()) { - return hash; - } - } - - return ""; + return Common::g_citron_version; } bool UpdaterService::IsUpdateInProgress() const { @@ -262,410 +153,279 @@ void UpdaterService::OnDownloadFinished() { update_in_progress.store(false); return; } + if (current_reply->error() != QNetworkReply::NoError) { - emit UpdateError(QStringLiteral("Download failed: %1").arg(current_reply->errorString())); + emit UpdateError(current_reply->errorString()); update_in_progress.store(false); return; } - QByteArray downloaded_data = current_reply->readAll(); - QSettings settings; - QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString(); + QByteArray 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; + std::filesystem::path zip_path = temp_download_path / "citron_update.zip"; + QFile f(QString::fromStdString(zip_path.string())); + if (f.open(QIODevice::WriteOnly)) { + f.write(data); + f.close(); } - file.write(downloaded_data); - file.close(); - LOG_INFO(Frontend, "Download completed: {}", download_path.string()); - QTimer::singleShot(100, this, [this, download_path]() { - if (cancel_requested.load()) { - update_in_progress.store(false); - return; - } - emit UpdateInstallProgress(10, QStringLiteral("Extracting update archive...")); + // Process extraction on a slight delay to ensure file handles are clear + QTimer::singleShot(100, this, [this, zip_path]() { 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")); + + if (!ExtractArchive(zip_path, extract_path)) { + RestoreBackup(); + emit UpdateCompleted(UpdateResult::ExtractionError, QStringLiteral("Failed to extract update. Ensure you have permissions.")); 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")); + emit UpdateCompleted(UpdateResult::Failed, QStringLiteral("Failed to stage update files.")); 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.")); + + emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update staged successfully.")); update_in_progress.store(false); - CleanupFiles(); }); + #elif defined(__linux__) - - LOG_INFO(Frontend, "AppImage download completed."); - - const char* appimage_path_env = qgetenv("APPIMAGE").constData(); - if (!appimage_path_env || strlen(appimage_path_env) == 0) { - emit UpdateError(QStringLiteral("Failed to update: Not running from an AppImage.")); + const char* appimage_env = qgetenv("APPIMAGE").constData(); + if (!appimage_env || strlen(appimage_env) == 0) { + emit UpdateError(QStringLiteral("Not running from an AppImage. Manual update required.")); update_in_progress.store(false); return; } - std::filesystem::path original_appimage_path = appimage_path_env; - std::filesystem::path appimage_dir = original_appimage_path.parent_path(); - std::error_code ec; + std::filesystem::path original_path = appimage_env; - // Check if backups are enabled before doing anything. + // Backup logic if (UISettings::values.updater_enable_backups.GetValue()) { - const std::string& custom_backup_path = UISettings::values.updater_backup_path.GetValue(); - std::filesystem::path backup_dir; + std::filesystem::path backup_dir = UISettings::values.updater_backup_path.GetValue().empty() ? + (original_path.parent_path() / "backup") : std::filesystem::path(UISettings::values.updater_backup_path.GetValue()); - if (!custom_backup_path.empty()) { - // User has specified a custom path. - backup_dir = custom_backup_path; - } else { - // Default behavior: create 'backup' folder next to the AppImage. - backup_dir = appimage_dir / "backup"; - } - - // Create the backup directory + std::error_code ec; std::filesystem::create_directories(backup_dir, ec); + std::filesystem::copy_file(original_path, backup_dir / ("citron-backup-" + GetCurrentVersion() + ".AppImage"), + std::filesystem::copy_options::overwrite_existing, ec); + } + + std::filesystem::path new_path = original_path.string() + ".new"; + QFile n_file(QString::fromStdString(new_path.string())); + if (n_file.open(QIODevice::WriteOnly)) { + n_file.write(data); + n_file.close(); + + // chmod +x + n_file.setPermissions(QFileDevice::ReadOwner|QFileDevice::WriteOwner|QFileDevice::ExeOwner| + QFileDevice::ReadGroup|QFileDevice::ExeGroup| + QFileDevice::ReadOther|QFileDevice::ExeOther); + + std::error_code ec; + std::filesystem::rename(new_path, original_path, ec); if (ec) { - LOG_ERROR(Frontend, "Failed to create backup directory: {}", ec.message()); - } else { - // Create the backup copy of the old AppImage - std::string current_version = GetCurrentVersion(); - std::string backup_filename = "citron-backup-" + (current_version.empty() ? "unknown" : current_version) + ".AppImage"; - std::filesystem::path backup_filepath = backup_dir / backup_filename; - std::filesystem::copy_file(original_appimage_path, backup_filepath, std::filesystem::copy_options::overwrite_existing, ec); - if (ec) { - LOG_ERROR(Frontend, "Failed to copy AppImage to backup location: {}", ec.message()); - } else { - LOG_INFO(Frontend, "Created backup of old AppImage at: {}", backup_filepath.string()); - } + emit UpdateError(QString::fromStdString("Failed to replace AppImage: " + ec.message())); + update_in_progress.store(false); + return; } } - std::filesystem::path new_appimage_path = original_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.")); - std::filesystem::remove(new_appimage_path, ec); - update_in_progress.store(false); - return; - } - - std::filesystem::rename(new_appimage_path, original_appimage_path, ec); - if (ec) { - LOG_ERROR(Frontend, "Failed to replace old AppImage: {}", ec.message()); - emit UpdateError(QStringLiteral("Failed to replace old AppImage.")); - update_in_progress.store(false); - return; - } - - std::filesystem::path version_file_path = appimage_dir / CITRON_VERSION_FILE; - if (channel == QStringLiteral("Stable")) { - LOG_INFO(Frontend, "Writing stable version marker: {}", current_update_info.version); - std::ofstream version_file(version_file_path); - if (version_file.is_open()) { - version_file << current_update_info.version; - } - } else { - LOG_INFO(Frontend, "Nightly update, removing stable version marker if it exists."); - if (std::filesystem::exists(version_file_path)) { - std::filesystem::remove(version_file_path, ec); - } - } - - LOG_INFO(Frontend, "AppImage updated successfully."); - emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application.")); + emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Success")); update_in_progress.store(false); #endif } -void UpdaterService::OnDownloadProgress(qint64 bytes_received, qint64 bytes_total) { - if (bytes_total > 0) { - emit UpdateDownloadProgress(static_cast((bytes_received * 100) / bytes_total), - bytes_received, bytes_total); - } -} - -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, const QString& channel) { - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(response, &error); - if (error.error != QJsonParseError::NoError || !doc.isArray()) { - emit UpdateError(QStringLiteral("Failed to parse update response.")); - return; - } + QJsonDocument doc = QJsonDocument::fromJson(response); + if (!doc.isArray()) return; - for (const QJsonValue& release_value : doc.array()) { - QJsonObject release_obj = release_value.toObject(); - std::string latest_version; - if (channel == QStringLiteral("Stable")) { - latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString(); + std::string current_variant = Common::g_citron_variant; + + for (const QJsonValue& rel_val : doc.array()) { + QJsonObject rel_obj = rel_val.toObject(); + + std::string latest_v; + if (channel.toLower() == QStringLiteral("stable")) { + latest_v = rel_obj.value(QStringLiteral("tag_name")).toString().toStdString(); } else { - latest_version = ExtractCommitHash(release_obj.value(QStringLiteral("name")).toString().toStdString()); + // Nightly names are usually "Nightly Build - [hash]" + latest_v = ExtractCommitHash(rel_obj.value(QStringLiteral("name")).toString().toStdString()); } - if (latest_version.empty()) continue; + if (latest_v.empty()) continue; - UpdateInfo update_info; - update_info.version = latest_version; - update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString(); - update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString(); + UpdateInfo info; + info.version = latest_v; + info.changelog = rel_obj.value(QStringLiteral("body")).toString().toStdString(); + info.release_date = rel_obj.value(QStringLiteral("published_at")).toString().toStdString(); - QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray(); - for (const QJsonValue& asset_value : assets) { - QJsonObject asset_obj = asset_value.toObject(); - QString asset_name = asset_obj.value(QStringLiteral("name")).toString(); + QJsonArray assets = rel_obj.value(QStringLiteral("assets")).toArray(); + std::vector os_valid; + std::vector variant_matched; - #if defined(__linux__) - if (asset_name.endsWith(QStringLiteral(".AppImage"))) { - DownloadOption option; - option.name = asset_name.toStdString(); - option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); - update_info.download_options.push_back(option); + for (const QJsonValue& asset_val : assets) { + QJsonObject asset_obj = asset_val.toObject(); + QString name = asset_obj.value(QStringLiteral("name")).toString(); + DownloadOption opt; + opt.name = name.toStdString(); + opt.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString(); + +#if defined(__linux__) + if (!name.endsWith(QStringLiteral(".AppImage"), Qt::CaseInsensitive)) continue; + os_valid.push_back(opt); + + // Architecture matching + if (current_variant.find("aarch64") != std::string::npos && name.contains(QStringLiteral("aarch64"), Qt::CaseInsensitive)) { + variant_matched.push_back(opt); + } else if (current_variant.find("v3") != std::string::npos && name.contains(QStringLiteral("v3"), Qt::CaseInsensitive)) { + variant_matched.push_back(opt); + } else if (!name.contains(QStringLiteral("v3"), Qt::CaseInsensitive) && !name.contains(QStringLiteral("aarch64"), Qt::CaseInsensitive)) { + // Fallback for generic x86_64 + if (current_variant.find("x86_64") != std::string::npos && current_variant.find("v3") == std::string::npos) { + variant_matched.push_back(opt); + } } - #elif defined(_WIN32) - // For Windows, find the .zip file but explicitly skip PGO builds. - if (asset_name.endsWith(QStringLiteral(".zip")) && !asset_name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) { - 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); - } - #endif +#elif defined(_WIN32) + if (!name.endsWith(QStringLiteral(".zip"), Qt::CaseInsensitive) || name.contains(QStringLiteral("PGO"), Qt::CaseInsensitive)) continue; + os_valid.push_back(opt); + // Windows Variant Matching (v3 vs generic) + if (current_variant.find("v3") != std::string::npos && name.contains(QStringLiteral("v3"), Qt::CaseInsensitive)) { + variant_matched.push_back(opt); + } else if (current_variant.find("v3") == std::string::npos && !name.contains(QStringLiteral("v3"), Qt::CaseInsensitive)) { + variant_matched.push_back(opt); + } +#endif } - 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); + // Priority: Match Variant -> Any OS Valid -> Fail + info.download_options = variant_matched.empty() ? os_valid : variant_matched; + + if (!info.download_options.empty()) { + std::string current_v = GetCurrentVersion(channel); + info.is_newer_version = (current_v != info.version); + + current_update_info = info; + emit UpdateCheckCompleted(info.is_newer_version, 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 { - if (current.empty()) { - return true; - } - if (latest.empty()) { - return false; - } - - bool is_newer = (current != latest); - return is_newer; } #ifdef _WIN32 -bool UpdaterService::ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { +bool UpdaterService::ExtractArchive(const std::filesystem::path& a_p, const std::filesystem::path& e_p) { #ifdef CITRON_ENABLE_LIBARCHIVE struct archive* a = archive_read_new(); struct archive* ext = archive_write_disk_new(); if (!a || !ext) return false; - archive_read_support_format_7zip(a); + + archive_read_support_format_all(a); archive_read_support_filter_all(a); - archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); - archive_write_disk_set_standard_lookup(ext); - if (archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) return false; - EnsureDirectoryExists(extract_path); + archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL); + + if (archive_read_open_filename(a, a_p.string().c_str(), 10240) != ARCHIVE_OK) return false; + + std::filesystem::create_directories(e_p); struct archive_entry* entry; while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { - if (cancel_requested.load()) break; - std::filesystem::path entry_path = extract_path / archive_entry_pathname(entry); - archive_entry_set_pathname(entry, entry_path.string().c_str()); - if (archive_write_header(ext, entry) != ARCHIVE_OK) continue; - 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; - archive_write_data_block(ext, buff, size, offset); + std::filesystem::path full_path = e_p / archive_entry_pathname(entry); + archive_entry_set_pathname(entry, full_path.string().c_str()); + archive_write_header(ext, entry); + + const void* buff; size_t sz; la_int64_t off; + while (archive_read_data_block(a, &buff, &sz, &off) == ARCHIVE_OK) { + archive_write_data_block(ext, buff, sz, off); } 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(); + return true; #else - return ExtractArchiveWindows(archive_path, extract_path); + // Windows PowerShell Fallback if LibArchive is missing + QString cmd = QString("powershell -Command \"Expand-Archive -Path '%1' -DestinationPath '%2' -Force\"") + .arg(QString::fromStdString(a_p.string())) + .arg(QString::fromStdString(e_p.string())); + return QProcess::execute(cmd) == 0; #endif } -#if !defined(CITRON_ENABLE_LIBARCHIVE) -bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path) { - EnsureDirectoryExists(extract_path); - std::string sevenzip_cmd = "7z x \"" + archive_path.string() + "\" -o\"" + extract_path.string() + "\" -y"; - if (std::system(sevenzip_cmd.c_str()) == 0) return true; - std::string powershell_cmd = "powershell -Command \"Expand-Archive -Path \\\"" + archive_path.string() + "\\\" -DestinationPath \\\"" + extract_path.string() + "\\\" -Force\""; - if (std::system(powershell_cmd.c_str()) == 0) return true; - LOG_ERROR(Frontend, "Failed to extract archive automatically."); - return false; -} -#endif - -bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) { +bool UpdaterService::InstallUpdate(const std::filesystem::path& u_p) { try { - 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 (top_level_items.size() == 1 && std::filesystem::is_directory(top_level_items[0])) { - source_path = top_level_items[0]; - } - std::filesystem::path staging_path = app_directory / "update_staging"; - EnsureDirectoryExists(staging_path); - for (const auto& entry : std::filesystem::recursive_directory_iterator(source_path)) { - if (cancel_requested.load()) return false; + std::filesystem::path staging = app_directory / "update_staging"; + std::filesystem::create_directories(staging); + + // Clean staging area first + std::error_code ec; + std::filesystem::remove_all(staging, ec); + std::filesystem::create_directories(staging); + + for (const auto& entry : std::filesystem::recursive_directory_iterator(u_p)) { 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; - std::filesystem::create_directories(staging_dest.parent_path()); - std::filesystem::copy_file(entry.path(), staging_dest, std::filesystem::copy_options::overwrite_existing); + std::filesystem::path rel = std::filesystem::relative(entry.path(), u_p); + std::filesystem::path dest = staging / rel; + std::filesystem::create_directories(dest.parent_path()); + std::filesystem::copy_file(entry.path(), dest, std::filesystem::copy_options::overwrite_existing); } } - 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"; - } - - // Create the update helper script for deferred update application - if (!CreateUpdateHelperScript(staging_path)) { - LOG_ERROR(Frontend, "Failed to create update helper script"); - return false; - } - - LOG_INFO(Frontend, "Update staged successfully."); - return true; - } catch (const std::exception& e) { - LOG_ERROR(Frontend, "Failed to install update: {}", e.what()); - return false; - } + return CreateUpdateHelperScript(staging); + } catch (...) { return false; } } 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); - std::vector backup_patterns = {"citron.exe", "citron_cmd.exe", "*.dll", "*.pdb"}; - for (const auto& entry : std::filesystem::directory_iterator(app_directory)) { - if (entry.is_regular_file()) { - std::string filename = entry.path().filename().string(); - std::string extension = entry.path().extension().string(); - bool should_backup = false; - for (const auto& pattern : backup_patterns) { - if (pattern == filename || (pattern.starts_with("*") && pattern.substr(1) == extension)) { - should_backup = true; - break; - } - } - if (should_backup) { - std::filesystem::copy_file(entry.path(), backup_dir / filename); - } - } - } - LOG_INFO(Frontend, "Backup created: {}", backup_dir.string()); + std::filesystem::path b_file = backup_path / "citron.exe.bak"; + std::filesystem::copy_file(app_directory / "citron.exe", b_file, std::filesystem::copy_options::overwrite_existing); return true; - } catch (const std::exception& e) { - LOG_ERROR(Frontend, "Failed to create backup: {}", e.what()); - return false; - } + } catch (...) { return false; } } bool UpdaterService::RestoreBackup() { try { - std::filesystem::path backup_dir = backup_path / ("backup_" + GetCurrentVersion()); - 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::path b_file = backup_path / "citron.exe.bak"; + if (std::filesystem::exists(b_file)) { + std::filesystem::copy_file(b_file, app_directory / "citron.exe", std::filesystem::copy_options::overwrite_existing); + return true; } - LOG_INFO(Frontend, "Backup restored successfully"); - return true; - } catch (const std::exception& e) { - LOG_ERROR(Frontend, "Failed to restore backup: {}", e.what()); - return false; - } + } catch (...) {} + return false; } bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& staging_path) { try { - std::filesystem::path script_path = staging_path / "apply_update.bat"; - LOG_INFO(Frontend, "Creating update helper script at: {}", script_path.string()); - - if (!std::filesystem::exists(staging_path)) { - LOG_ERROR(Frontend, "Staging path does not exist: {}", staging_path.string()); - return false; - } - - std::ofstream script(script_path, std::ios::out | std::ios::trunc); - if (!script.is_open()) { - LOG_ERROR(Frontend, "Failed to open file for writing: {}", script_path.string()); - return false; - } + std::ofstream script(staging_path / "apply_update.bat"); + if (!script.is_open()) return false; std::string staging_path_str = staging_path.string(); std::string app_path_str = app_directory.string(); std::string exe_path_str = (app_directory / "citron.exe").string(); + // Ensure Windows-style backslashes for the batch file for (auto& ch : staging_path_str) if (ch == '/') ch = '\\'; for (auto& ch : app_path_str) if (ch == '/') ch = '\\'; for (auto& ch : exe_path_str) if (ch == '/') ch = '\\'; script << "@echo off\n"; script << "setlocal enabledelayedexpansion\n"; - script << "REM Citron Auto-Updater Helper Script\n\n"; + script << "title Citron Auto-Updater\n"; + script << "color 0B\n"; + script << "echo =======================================\n"; + script << "echo Citron Emulator Updater \n"; + script << "echo =======================================\n\n"; script << "echo Waiting for Citron to close...\n"; - // Wait for citron.exe to close, with a longer timeout + // Wait for process to exit with a 60s timeout script << "set /a wait_count=0\n"; script << ":wait_loop\n"; script << "tasklist /FI \"IMAGENAME eq citron.exe\" | find /I \"citron.exe\" >nul 2>&1\n"; script << "if not errorlevel 1 (\n"; script << " set /a wait_count+=1\n"; script << " if !wait_count! gtr 60 (\n"; - script << " echo Warning: Citron process still running after 60 seconds, proceeding anyway...\n"; + script << " echo [WARNING] Citron is taking a long time to close. Attempting to proceed...\n"; script << " goto wait_done\n"; script << " )\n"; script << " timeout /t 1 /nobreak >nul\n"; @@ -673,55 +433,47 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi script << ")\n"; script << ":wait_done\n"; script << "timeout /t 2 /nobreak >nul\n"; - script << "echo Citron has closed. Preparing update...\n\n"; - // Remove read-only attributes from all files in the destination directory - script << "echo Removing read-only attributes from existing files...\n"; + // Permission fix + script << "echo Preparing permissions...\n"; script << "attrib -R \"" << app_path_str << "\\*.*\" /S /D >nul 2>&1\n"; - script << "if exist \"" << app_path_str << "\\citron.exe\" attrib -R \"" << app_path_str << "\\citron.exe\" >nul 2>&1\n"; - script << "if exist \"" << app_path_str << "\\citron_cmd.exe\" attrib -R \"" << app_path_str << "\\citron_cmd.exe\" >nul 2>&1\n\n"; - // Use robocopy for more reliable copying (available on Windows Vista+) - script << "echo Copying update files...\n"; + // Reliable Robocopy with error checking + script << "echo Applying update files...\n"; script << "set /a copy_retries=0\n"; script << ":copy_loop\n"; + // /E: recursive, /IS: include same, /IT: include tweaked, /R:3: retries, /W:1: wait between retries script << "robocopy \"" << staging_path_str << "\" \"" << app_path_str << "\" /E /IS /IT /R:3 /W:1 /NP /NFL /NDL >nul 2>&1\n"; script << "set /a robocopy_exit=!errorlevel!\n"; - script << "REM Robocopy returns 0-7 for success, 8+ for errors\n"; + + // Robocopy success is anything below 8 script << "if !robocopy_exit! geq 8 (\n"; script << " set /a copy_retries+=1\n"; script << " if !copy_retries! lss 3 (\n"; - script << " echo Copy attempt !copy_retries! failed, retrying...\n"; + script << " echo [RETRY] Copy failed (Error !robocopy_exit!). Retrying in 2s...\n"; script << " timeout /t 2 /nobreak >nul\n"; - script << " REM Try removing read-only again\n"; - script << " attrib -R \"" << app_path_str << "\\*.*\" /S /D >nul 2>&1\n"; script << " goto copy_loop\n"; script << " ) else (\n"; - script << " echo ERROR: Failed to copy update files after 3 attempts.\n"; - script << " echo Error code: !robocopy_exit!\n"; - script << " echo.\n"; - script << " echo Update failed. Please restart Citron manually.\n"; - script << " echo You may need to run this script as Administrator.\n"; + script << " echo [ERROR] Update failed to copy files. Error code: !robocopy_exit!\n"; + script << " echo Please ensure no other programs are using Citron files.\n"; script << " pause\n"; script << " exit /b 1\n"; script << " )\n"; script << ")\n\n"; - // Verify critical files were copied + // Post-install verification script << "if not exist \"" << exe_path_str << "\" (\n"; - script << " echo ERROR: citron.exe was not copied successfully.\n"; - script << " echo Update failed. Please restart Citron manually.\n"; + script << " echo [ERROR] Critical file citron.exe is missing after update!\n"; script << " pause\n"; script << " exit /b 1\n"; script << ")\n\n"; script << "echo Update applied successfully!\n"; script << "echo Restarting Citron...\n"; - script << "timeout /t 1 /nobreak >nul\n"; script << "start \"\" \"" << exe_path_str << "\"\n\n"; - // Clean up staging directory with retry - script << "REM Cleaning up staging directory...\n"; + // Cleanup with retry loop + script << "echo Cleaning up temporary files...\n"; script << "set /a cleanup_retries=0\n"; script << ":cleanup_loop\n"; script << "rd /s /q \"" << staging_path_str << "\" >nul 2>&1\n"; @@ -733,184 +485,41 @@ bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& stagi script << " )\n"; script << ")\n\n"; - script << "REM Delete this script (with retry)\n"; - script << "set /a del_retries=0\n"; - script << ":del_script_loop\n"; + // Self-delete script script << "del \"%~f0\" >nul 2>&1\n"; - script << "if exist \"%~f0\" (\n"; - script << " set /a del_retries+=1\n"; - script << " if !del_retries! lss 3 (\n"; - script << " timeout /t 1 /nobreak >nul\n"; - script << " goto del_script_loop\n"; - script << " )\n"; - script << ")\n"; script << "exit /b 0\n"; - script.flush(); script.close(); - - LOG_INFO(Frontend, "Update helper script created successfully: {}", script_path.string()); return true; - } catch (const std::exception& e) { - LOG_ERROR(Frontend, "Exception creating update helper script: {}", e.what()); + } catch (...) { return false; } } bool UpdaterService::LaunchUpdateHelper() { - try { - std::filesystem::path staging_path = app_directory / "update_staging"; - std::filesystem::path script_path = staging_path / "apply_update.bat"; + std::filesystem::path sc = app_directory / "update_staging" / "apply_update.bat"; + if (!std::filesystem::exists(sc)) return false; - if (!std::filesystem::exists(script_path)) { - LOG_ERROR(Frontend, "Update helper script not found at: {}", script_path.string()); - return false; - } - - // Verify the script is readable - std::ifstream test_read(script_path); - if (!test_read.good()) { - LOG_ERROR(Frontend, "Update helper script exists but cannot be read"); - return false; - } - test_read.close(); - - // Launch the batch script as a detached process - QString script_path_str = QString::fromStdString(script_path.string()); - QStringList arguments; - arguments << QStringLiteral("/C"); - arguments << script_path_str; - - // Use cmd.exe to run the batch file - // Note: We don't hide the window so users can see progress and any errors - bool launched = QProcess::startDetached(QStringLiteral("cmd.exe"), arguments); - - if (launched) { - LOG_INFO(Frontend, "Update helper script launched successfully from: {}", script_path.string()); - return true; - } else { - LOG_ERROR(Frontend, "Failed to launch update helper script. QProcess::startDetached returned false"); - return false; - } - } catch (const std::exception& e) { - LOG_ERROR(Frontend, "Exception launching update helper: {}", e.what()); - return false; - } + // We launch via cmd /c and detach so the script survives Citron's death + return QProcess::startDetached("cmd.exe", QStringList() << "/C" << QString::fromStdString(sc.string())); } #endif +void UpdaterService::OnDownloadProgress(qint64 r, qint64 t) { + if (t > 0) emit UpdateDownloadProgress(static_cast((r * 100) / t), r, t); +} + +bool UpdaterService::EnsureDirectoryExists(const std::filesystem::path& p) const { + std::error_code ec; + return std::filesystem::create_directories(p, ec) || std::filesystem::exists(p); +} + bool UpdaterService::CleanupFiles() { - try { - if (std::filesystem::exists(temp_download_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()); - } - } - } - if (backup_dirs.size() > 3) { - std::sort(backup_dirs.begin(), backup_dirs.end(), - [](const auto& a, const auto& b) { return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b); }); - 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()); - return false; + std::error_code ec; + if (std::filesystem::exists(temp_download_path)) { + std::filesystem::remove_all(temp_download_path, ec); } -} - -std::filesystem::path UpdaterService::GetTempDirectory() const { - return std::filesystem::path(QStandardPaths::writableLocation(QStandardPaths::TempLocation).toStdString()) / "citron_updater"; -} - -std::filesystem::path UpdaterService::GetApplicationDirectory() const { - return std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); -} - -std::filesystem::path UpdaterService::GetBackupDirectory() const { - return GetApplicationDirectory() / BACKUP_DIRECTORY; -} - -bool UpdaterService::EnsureDirectoryExists(const std::filesystem::path& path) const { - try { - if (!std::filesystem::exists(path)) { - std::filesystem::create_directories(path); - } - return true; - } catch (const std::exception& e) { - LOG_ERROR(Frontend, "Failed to create directory {}: {}", path.string(), e.what()); - return false; - } -} - -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); -#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; - LOG_INFO(Frontend, "Applying staged update from: {}", staging_path.string()); - std::filesystem::path backup_path_dir = app_directory / "backup_before_update"; - if (std::filesystem::exists(backup_path_dir)) std::filesystem::remove_all(backup_path_dir); - std::filesystem::create_directories(backup_path_dir); - for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) { - if (entry.path().filename() == "update_manifest.txt") continue; - if (entry.is_regular_file()) { - std::filesystem::path relative_path = std::filesystem::relative(entry.path(), staging_path); - std::filesystem::path dest_path = app_directory / relative_path; - if (std::filesystem::exists(dest_path)) { - std::filesystem::path backup_dest = backup_path_dir / relative_path; - std::filesystem::create_directories(backup_dest.parent_path()); - std::filesystem::copy_file(dest_path, backup_dest); - } - std::filesystem::create_directories(dest_path.parent_path()); - std::filesystem::copy_file(entry.path(), dest_path, std::filesystem::copy_options::overwrite_existing); - } - } - std::ifstream manifest(manifest_file); - std::string line, version; - while (std::getline(manifest, line)) { - if (line.starts_with("UPDATE_VERSION=")) { - version = line.substr(15); - break; - } - } - if (!version.empty()) { - std::filesystem::path version_file = app_directory / "version.txt"; - std::ofstream vfile(version_file); - if (vfile.is_open()) vfile << version; - } - 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 + return !ec; } } // namespace Updater - -#include "updater_service.moc"