mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 11:03:56 +00:00
Replace direct file overwriting with a staging-based update mechanism to resolve "file in use" errors during self-updates. **Changes:** - Stage updates to temporary directory instead of direct installation - Apply staged updates on next application startup (before files are loaded) - Add static methods for startup update detection and application - Create update manifest to track staged update metadata - Backup existing files before applying updates - Update UI messaging to reflect staged update workflow **Problem solved:** The previous direct file replacement approach failed when trying to overwrite DLLs and executables that were loaded into memory by the running process. This two-stage approach stages files safely, then applies them on restart when no files are in use. **Workflow:** 1. Download and extract update to staging directory 2. Create manifest with update metadata 3. Show "Update ready" message to user 4. On next app startup: detect staged update, apply it, show success message 5. Clean up staging directory after successful application This ensures reliable self-updates without file access conflicts. Signed-off-by: Zephyron <zephyron@citron-emu.org>
907 lines
35 KiB
C++
907 lines
35 KiB
C++
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "citron/updater/updater_service.h"
|
|
#include "common/logging/log.h"
|
|
#include "common/fs/path_util.h"
|
|
|
|
#include <QApplication>
|
|
#include <QStandardPaths>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QTimer>
|
|
#include <QMessageBox>
|
|
#include <QNetworkRequest>
|
|
#include <QSslConfiguration>
|
|
#include <QThread>
|
|
#include <QCoreApplication>
|
|
#include <QSslSocket>
|
|
#include <QSslCertificate>
|
|
#include <QSslKey>
|
|
#include <QFile>
|
|
#include <QDir>
|
|
#include <QStandardPaths>
|
|
|
|
#ifdef CITRON_ENABLE_LIBARCHIVE
|
|
#include <archive.h>
|
|
#include <archive_entry.h>
|
|
#endif
|
|
|
|
#include <fstream>
|
|
#include <regex>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#endif
|
|
|
|
namespace Updater {
|
|
|
|
UpdaterService::UpdaterService(QObject* parent) : QObject(parent) {
|
|
network_manager = std::make_unique<QNetworkAccessManager>(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() {
|
|
if (current_reply) {
|
|
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());
|
|
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) {
|
|
if (update_in_progress.load()) {
|
|
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};
|
|
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);
|
|
}
|
|
|
|
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
|
|
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);
|
|
}
|
|
|
|
void UpdaterService::ConfigureSSLForRequest(QNetworkRequest& request) {
|
|
if (!QSslSocket::supportsSsl()) {
|
|
LOG_WARNING(Frontend, "SSL not supported, request may fail for HTTPS URLs");
|
|
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.setProtocol(QSsl::SecureProtocols);
|
|
|
|
// Apply SSL configuration
|
|
request.setSslConfiguration(sslConfig);
|
|
}
|
|
|
|
void UpdaterService::DownloadAndInstallUpdate(const UpdateInfo& update_info) {
|
|
if (update_in_progress.load()) {
|
|
emit UpdateError(QStringLiteral("Update operation already in progress"));
|
|
return;
|
|
}
|
|
|
|
if (update_info.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 download of update: {}", update_info.version);
|
|
LOG_INFO(Frontend, "Download URL: {}", update_info.download_url);
|
|
|
|
// Create backup before starting update
|
|
if (!CreateBackup()) {
|
|
emit UpdateCompleted(UpdateResult::PermissionError, QStringLiteral("Failed to create backup"));
|
|
update_in_progress.store(false);
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
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::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"));
|
|
|
|
update_in_progress.store(false);
|
|
}
|
|
|
|
std::string UpdaterService::GetCurrentVersion() const {
|
|
// Try to read version from version.txt 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to application version
|
|
return QCoreApplication::applicationVersion().toStdString();
|
|
}
|
|
|
|
bool UpdaterService::IsUpdateInProgress() const {
|
|
return update_in_progress.load();
|
|
}
|
|
|
|
void UpdaterService::OnDownloadFinished() {
|
|
if (cancel_requested.load()) {
|
|
update_in_progress.store(false);
|
|
return;
|
|
}
|
|
|
|
if (current_reply->error() != QNetworkReply::NoError) {
|
|
emit UpdateCompleted(UpdateResult::NetworkError,
|
|
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();
|
|
|
|
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.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();
|
|
});
|
|
}
|
|
|
|
void UpdaterService::OnDownloadProgress(qint64 bytes_received, qint64 bytes_total) {
|
|
if (bytes_total > 0) {
|
|
int percentage = static_cast<int>((bytes_received * 100) / bytes_total);
|
|
emit UpdateDownloadProgress(percentage, bytes_received, bytes_total);
|
|
}
|
|
}
|
|
|
|
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);
|
|
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()));
|
|
return;
|
|
}
|
|
|
|
QJsonObject json = doc.object();
|
|
UpdateInfo update_info;
|
|
|
|
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();
|
|
|
|
std::string current_version = GetCurrentVersion();
|
|
update_info.is_newer_version = CompareVersions(current_version, update_info.version);
|
|
|
|
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);
|
|
}
|
|
|
|
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
|
|
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
|
|
}
|
|
|
|
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
|
|
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
|
|
EnsureDirectoryExists(extract_path);
|
|
|
|
// Extract files
|
|
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
|
|
if (cancel_requested.load()) {
|
|
break;
|
|
}
|
|
|
|
// Set the extraction path
|
|
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
|
|
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);
|
|
}
|
|
|
|
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
|
|
// Windows fallback: use system 7zip or PowerShell
|
|
return ExtractArchiveWindows(archive_path, extract_path);
|
|
#endif
|
|
}
|
|
|
|
#ifndef 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
|
|
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<std::filesystem::path> 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 (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());
|
|
}
|
|
}
|
|
|
|
// 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.");
|
|
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
LOG_ERROR(Frontend, "Failed to install update: {}", e.what());
|
|
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);
|
|
|
|
// Backup essential files (executable, dlls, etc.)
|
|
std::vector<std::string> 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)) {
|
|
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) {
|
|
LOG_ERROR(Frontend, "Failed to create backup: {}", e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
LOG_INFO(Frontend, "Backup restored successfully");
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
LOG_ERROR(Frontend, "Failed to restore backup: {}", e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove old backups (keep only the latest 3)
|
|
std::vector<std::filesystem::path> 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);
|
|
});
|
|
|
|
for (size_t i = 3; i < backup_dirs.size(); ++i) {
|
|
std::filesystem::remove_all(backup_dirs[i]);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
LOG_ERROR(Frontend, "Failed to cleanup files: {}", e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::filesystem::path UpdaterService::GetTempDirectory() const {
|
|
return std::filesystem::temp_directory_path() / "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) {
|
|
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);
|
|
}
|
|
|
|
bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_directory) {
|
|
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());
|
|
|
|
// 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
|
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) {
|
|
if (entry.path().filename() == "update_manifest.txt") {
|
|
continue; // Skip manifest file
|
|
}
|
|
|
|
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::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());
|
|
}
|
|
}
|
|
|
|
// Read and apply version from manifest
|
|
std::ifstream manifest(manifest_file);
|
|
std::string line;
|
|
std::string version;
|
|
|
|
while (std::getline(manifest, line)) {
|
|
if (line.starts_with("UPDATE_VERSION=")) {
|
|
version = line.substr(15); // Remove "UPDATE_VERSION="
|
|
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();
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
} // namespace Updater
|
|
|
|
#include "updater_service.moc" |