mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 11:03:56 +00:00
feat: Add automatic update system with background checking
Implement a complete auto-updater system for Citron with the following features: - Add UpdaterService class for handling update checks, downloads, and installations - Add UpdaterDialog with progress tracking and user interaction - Support both .zip and .7z archives with Windows PowerShell fallback - Automatic background update checking on startup (3-second delay) - Silent notifications when updates are available - Manual "Check for Updates" menu option in Help menu - User setting to enable/disable automatic update checks (enabled by default) - Graceful error handling with detailed logging - Restart functionality after successful updates - Libarchive integration for cross-platform archive support The system uses HTTP URLs to bypass SSL library compatibility issues and provides a smooth user experience with minimal interruption during automatic checks. Manual updates show a full dialog with progress tracking and changelog information. Fixes update distribution workflow and keeps users informed of new releases. Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -210,6 +210,11 @@ add_executable(citron
|
||||
startup_checks.h
|
||||
uisettings.cpp
|
||||
uisettings.h
|
||||
updater/updater_service.cpp
|
||||
updater/updater_service.h
|
||||
updater/updater_dialog.cpp
|
||||
updater/updater_dialog.h
|
||||
updater/updater_dialog.ui
|
||||
util/clickable_label.cpp
|
||||
util/clickable_label.h
|
||||
util/controller_navigation.cpp
|
||||
@@ -385,6 +390,12 @@ target_link_libraries(citron PRIVATE common core input_common frontend_common ne
|
||||
target_link_libraries(citron PRIVATE Boost::headers glad Qt${QT_MAJOR_VERSION}::Widgets)
|
||||
target_link_libraries(citron PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
|
||||
|
||||
# Add libarchive for updater functionality
|
||||
if (TARGET LibArchive::LibArchive)
|
||||
target_link_libraries(citron PRIVATE LibArchive::LibArchive)
|
||||
target_compile_definitions(citron PRIVATE CITRON_ENABLE_LIBARCHIVE)
|
||||
endif()
|
||||
|
||||
target_link_libraries(citron PRIVATE Vulkan::Headers)
|
||||
if (NOT WIN32)
|
||||
target_include_directories(citron PRIVATE ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
|
||||
|
||||
@@ -161,6 +161,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
||||
#include "citron/play_time_manager.h"
|
||||
#include "citron/startup_checks.h"
|
||||
#include "citron/uisettings.h"
|
||||
#include "citron/updater/updater_dialog.h"
|
||||
#include "citron/util/clickable_label.h"
|
||||
#include "citron/vk_device_info.h"
|
||||
|
||||
@@ -431,6 +432,9 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
|
||||
// Show one-time "callout" messages to the user
|
||||
ShowTelemetryCallout();
|
||||
|
||||
// Check for updates automatically after a short delay (non-blocking)
|
||||
QTimer::singleShot(3000, this, &GMainWindow::CheckForUpdatesAutomatically);
|
||||
|
||||
// make sure menubar has the arrow cursor instead of inheriting from this
|
||||
ui->menubar->setCursor(QCursor());
|
||||
statusBar()->setCursor(QCursor());
|
||||
@@ -1609,6 +1613,7 @@ void GMainWindow::ConnectMenuEvents() {
|
||||
connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
|
||||
connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware);
|
||||
connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys);
|
||||
connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates);
|
||||
connect_menu(ui->action_About, &GMainWindow::OnAbout);
|
||||
}
|
||||
|
||||
@@ -5347,6 +5352,60 @@ int main(int argc, char* argv[]) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void GMainWindow::OnCheckForUpdates() {
|
||||
// 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";
|
||||
|
||||
// Create and show the updater dialog
|
||||
auto* updater_dialog = new UpdaterDialog(this);
|
||||
updater_dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
updater_dialog->show();
|
||||
updater_dialog->CheckForUpdates(update_url);
|
||||
}
|
||||
|
||||
void GMainWindow::CheckForUpdatesAutomatically() {
|
||||
// Check if automatic updates are enabled
|
||||
if (!Settings::values.enable_auto_update_check.GetValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
// Create updater service for silent background check
|
||||
auto* updater_service = new Updater::UpdaterService(this);
|
||||
|
||||
// Connect to update check result
|
||||
connect(updater_service, &Updater::UpdaterService::UpdateCheckCompleted, this,
|
||||
[this, updater_service](bool has_update, const Updater::UpdateInfo& update_info) {
|
||||
if (has_update) {
|
||||
// Show a subtle notification that an update is available
|
||||
QMessageBox::information(this, tr("Update Available"),
|
||||
tr("A new version of Citron is available: %1\n\n"
|
||||
"Click Help → Check for Updates to download it.")
|
||||
.arg(QString::fromStdString(update_info.version)));
|
||||
}
|
||||
updater_service->deleteLater();
|
||||
});
|
||||
|
||||
// Connect to error handling
|
||||
connect(updater_service, &Updater::UpdaterService::UpdateCompleted, this,
|
||||
[updater_service](Updater::UpdaterService::UpdateResult result, const QString& message) {
|
||||
if (result == Updater::UpdaterService::UpdateResult::NetworkError ||
|
||||
result == Updater::UpdaterService::UpdateResult::Failed) {
|
||||
// Silent fail for automatic checks - just log the error
|
||||
LOG_WARNING(Frontend, "Automatic update check failed: {}", message.toStdString());
|
||||
}
|
||||
updater_service->deleteLater();
|
||||
});
|
||||
|
||||
// Start the silent update check
|
||||
updater_service->CheckForUpdates(update_url);
|
||||
}
|
||||
|
||||
void GMainWindow::OnToggleGridView() {
|
||||
game_list->ToggleViewMode();
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class QtControllerSelectorDialog;
|
||||
class QtProfileSelectionDialog;
|
||||
class QtSoftwareKeyboardDialog;
|
||||
class QtNXWebEngineView;
|
||||
class UpdaterDialog;
|
||||
|
||||
enum class StartGameType {
|
||||
Normal, // Can use custom configuration
|
||||
@@ -383,6 +384,8 @@ private slots:
|
||||
void OnInstallFirmware();
|
||||
void OnInstallDecryptionKeys();
|
||||
void OnAbout();
|
||||
void OnCheckForUpdates();
|
||||
void CheckForUpdatesAutomatically();
|
||||
void OnToggleFilterBar();
|
||||
void OnToggleGridView();
|
||||
void OnToggleStatusBar();
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
<addaction name="action_Open_Quickstart_Guide"/>
|
||||
<addaction name="action_Open_FAQ"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Check_For_Updates"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_About"/>
|
||||
</widget>
|
||||
<addaction name="menu_File"/>
|
||||
@@ -240,6 +242,11 @@
|
||||
<string>&Verify Installed Contents</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Check_For_Updates">
|
||||
<property name="text">
|
||||
<string>Check for &Updates...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_About">
|
||||
<property name="text">
|
||||
<string>&About citron</string>
|
||||
|
||||
353
src/citron/updater/updater_dialog.cpp
Normal file
353
src/citron/updater/updater_dialog.cpp
Normal file
@@ -0,0 +1,353 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "citron/updater/updater_dialog.h"
|
||||
#include "ui_updater_dialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
#include <QTimer>
|
||||
#include <QProcess>
|
||||
|
||||
UpdaterDialog::UpdaterDialog(QWidget* parent)
|
||||
: QDialog(parent), ui(std::make_unique<Ui::UpdaterDialog>()),
|
||||
updater_service(std::make_unique<Updater::UpdaterService>(this)),
|
||||
current_state(State::Checking), total_download_size(0), downloaded_bytes(0),
|
||||
progress_timer(new QTimer(this)) {
|
||||
|
||||
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);
|
||||
|
||||
// Set up UI connections
|
||||
connect(ui->downloadButton, &QPushButton::clicked, this, &UpdaterDialog::OnDownloadButtonClicked);
|
||||
connect(ui->cancelButton, &QPushButton::clicked, this, &UpdaterDialog::OnCancelButtonClicked);
|
||||
connect(ui->closeButton, &QPushButton::clicked, this, &UpdaterDialog::OnCloseButtonClicked);
|
||||
connect(ui->restartButton, &QPushButton::clicked, this, &UpdaterDialog::OnRestartButtonClicked);
|
||||
|
||||
SetupUI();
|
||||
|
||||
// Set up progress timer for smooth updates
|
||||
progress_timer->setInterval(100); // Update every 100ms
|
||||
connect(progress_timer, &QTimer::timeout, this, [this]() {
|
||||
if (current_state == State::Downloading) {
|
||||
ui->downloadInfoLabel->setText(
|
||||
QStringLiteral("Downloaded: %1 / %2")
|
||||
.arg(FormatBytes(downloaded_bytes))
|
||||
.arg(FormatBytes(total_download_size))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
UpdaterDialog::~UpdaterDialog() = default;
|
||||
|
||||
void UpdaterDialog::CheckForUpdates(const std::string& update_url) {
|
||||
ShowCheckingState();
|
||||
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;
|
||||
ShowUpdateAvailableState();
|
||||
} else {
|
||||
ShowNoUpdateState();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterDialog::OnUpdateDownloadProgress(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));
|
||||
|
||||
if (!progress_timer->isActive()) {
|
||||
progress_timer->start();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterDialog::OnUpdateInstallProgress(int percentage, const QString& current_file) {
|
||||
progress_timer->stop();
|
||||
|
||||
ui->progressBar->setValue(percentage);
|
||||
ui->progressLabel->setText(QStringLiteral("Installing update... %1%").arg(percentage));
|
||||
ui->downloadInfoLabel->setText(current_file);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterDialog::OnUpdateError(const QString& error_message) {
|
||||
progress_timer->stop();
|
||||
ShowErrorState();
|
||||
ui->statusLabel->setText(QStringLiteral("Update failed: ") + error_message);
|
||||
}
|
||||
|
||||
void UpdaterDialog::OnDownloadButtonClicked() {
|
||||
ShowDownloadingState();
|
||||
updater_service->DownloadAndInstallUpdate(current_update_info);
|
||||
}
|
||||
|
||||
void UpdaterDialog::OnCancelButtonClicked() {
|
||||
if (updater_service->IsUpdateInProgress()) {
|
||||
updater_service->CancelUpdate();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterDialog::OnCloseButtonClicked() {
|
||||
close();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (ret == QMessageBox::Yes) {
|
||||
// Get the current executable path
|
||||
QString program = QApplication::applicationFilePath();
|
||||
QStringList arguments = QApplication::arguments();
|
||||
arguments.removeFirst(); // Remove the program name
|
||||
|
||||
// Start the new instance
|
||||
QProcess::startDetached(program, arguments);
|
||||
|
||||
// Close the current instance
|
||||
QApplication::quit();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
ui->restartButton->setVisible(false);
|
||||
}
|
||||
|
||||
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->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->progressLabel->setText(QStringLiteral("Installing..."));
|
||||
ui->downloadInfoLabel->setText(QStringLiteral(""));
|
||||
|
||||
ui->cancelButton->setVisible(false);
|
||||
}
|
||||
|
||||
void UpdaterDialog::ShowCompletedState() {
|
||||
current_state = State::Completed;
|
||||
|
||||
ui->titleLabel->setText(QStringLiteral("Update completed!"));
|
||||
ui->statusLabel->setText(QStringLiteral("The update has been installed successfully. Please restart Citron to use the new version."));
|
||||
|
||||
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) {
|
||||
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")};
|
||||
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]);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
#include "updater_dialog.moc"
|
||||
86
src/citron/updater/updater_dialog.h
Normal file
86
src/citron/updater/updater_dialog.h
Normal file
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <QDialog>
|
||||
#include <QProgressBar>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTextBrowser>
|
||||
#include <QTimer>
|
||||
|
||||
#include "citron/updater/updater_service.h"
|
||||
|
||||
namespace Ui {
|
||||
class UpdaterDialog;
|
||||
}
|
||||
|
||||
class UpdaterDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UpdaterDialog(QWidget* parent = nullptr);
|
||||
~UpdaterDialog() override;
|
||||
|
||||
// Check for updates using the given URL
|
||||
void CheckForUpdates(const std::string& update_url);
|
||||
|
||||
// Show update available dialog
|
||||
void ShowUpdateAvailable(const Updater::UpdateInfo& update_info);
|
||||
|
||||
// Show update checking dialog
|
||||
void ShowUpdateChecking();
|
||||
|
||||
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();
|
||||
|
||||
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:
|
||||
std::unique_ptr<Ui::UpdaterDialog> ui;
|
||||
std::unique_ptr<Updater::UpdaterService> updater_service;
|
||||
|
||||
Updater::UpdateInfo current_update_info;
|
||||
|
||||
// UI state
|
||||
enum class State {
|
||||
Checking,
|
||||
NoUpdate,
|
||||
UpdateAvailable,
|
||||
Downloading,
|
||||
Installing,
|
||||
Completed,
|
||||
Error
|
||||
};
|
||||
State current_state = State::Checking;
|
||||
|
||||
// Progress tracking
|
||||
qint64 total_download_size = 0;
|
||||
qint64 downloaded_bytes = 0;
|
||||
QTimer* progress_timer;
|
||||
};
|
||||
253
src/citron/updater/updater_dialog.ui
Normal file
253
src/citron/updater/updater_dialog.ui
Normal file
@@ -0,0 +1,253 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>UpdaterDialog</class>
|
||||
<widget class="QDialog" name="UpdaterDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Citron Updater</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>:/icons/default/citron.ico</normaloff>:/icons/default/citron.ico</iconset>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="titleLabel">
|
||||
<property name="text">
|
||||
<string>Checking for updates...</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>14</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="statusLabel">
|
||||
<property name="text">
|
||||
<string>Please wait while we check for available updates...</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="updateInfoGroup">
|
||||
<property name="title">
|
||||
<string>Update Information</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="currentVersionLabel">
|
||||
<property name="text">
|
||||
<string>Current Version:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="currentVersionValue">
|
||||
<property name="text">
|
||||
<string>Unknown</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="latestVersionLabel">
|
||||
<property name="text">
|
||||
<string>Latest Version:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="latestVersionValue">
|
||||
<property name="text">
|
||||
<string>Unknown</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="releaseDateLabel">
|
||||
<property name="text">
|
||||
<string>Release Date:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="releaseDateValue">
|
||||
<property name="text">
|
||||
<string>Unknown</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="changelogGroup">
|
||||
<property name="title">
|
||||
<string>Changelog</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="changelogLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="changelogText">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>150</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="html">
|
||||
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
|
||||
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">No changelog available.</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="progressGroup">
|
||||
<property name="title">
|
||||
<string>Progress</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="progressLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="progressLabel">
|
||||
<property name="text">
|
||||
<string>Preparing...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="downloadInfoLabel">
|
||||
<property name="text">
|
||||
<string></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttonLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="downloadButton">
|
||||
<property name="text">
|
||||
<string>Download Update</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="closeButton">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="restartButton">
|
||||
<property name="text">
|
||||
<string>Restart Citron</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
796
src/citron/updater/updater_service.cpp
Normal file
796
src/citron/updater/updater_service.cpp
Normal file
@@ -0,0 +1,796 @@
|
||||
// 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 {
|
||||
// Copy all files from update path to application directory
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(update_path)) {
|
||||
if (cancel_requested.load()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.is_regular_file()) {
|
||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), update_path);
|
||||
std::filesystem::path dest_path = app_directory / relative_path;
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
std::filesystem::create_directories(dest_path.parent_path());
|
||||
|
||||
// Copy file
|
||||
std::filesystem::copy_file(entry.path(), dest_path,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
|
||||
LOG_DEBUG(Frontend, "Installed file: {}", dest_path.string());
|
||||
}
|
||||
}
|
||||
|
||||
// Update version file
|
||||
std::filesystem::path version_file = app_directory / CITRON_VERSION_FILE;
|
||||
std::ofstream file(version_file);
|
||||
if (file.is_open()) {
|
||||
file << current_update_info.version;
|
||||
file.close();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Updater
|
||||
|
||||
#include "updater_service.moc"
|
||||
121
src/citron/updater/updater_service.h
Normal file
121
src/citron/updater/updater_service.h
Normal file
@@ -0,0 +1,121 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QProgressDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
explicit UpdaterService(QObject* parent = nullptr);
|
||||
~UpdaterService() override;
|
||||
|
||||
// Check for updates from the configured URL
|
||||
void CheckForUpdates(const std::string& update_url);
|
||||
|
||||
// Download and install update
|
||||
void DownloadAndInstallUpdate(const UpdateInfo& update_info);
|
||||
|
||||
// Cancel current operation
|
||||
void CancelUpdate();
|
||||
|
||||
// Get current application version
|
||||
std::string GetCurrentVersion() const;
|
||||
|
||||
// Check if update is in progress
|
||||
bool IsUpdateInProgress() const;
|
||||
|
||||
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:
|
||||
// Network operations
|
||||
void ParseUpdateResponse(const QByteArray& response);
|
||||
bool CompareVersions(const std::string& current, const std::string& latest) const;
|
||||
|
||||
// SSL configuration
|
||||
void InitializeSSL();
|
||||
void ConfigureSSLForRequest(QNetworkRequest& request);
|
||||
|
||||
// File operations
|
||||
bool ExtractArchive(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path);
|
||||
#ifndef CITRON_ENABLE_LIBARCHIVE
|
||||
bool ExtractArchiveWindows(const std::filesystem::path& archive_path, const std::filesystem::path& extract_path);
|
||||
#endif
|
||||
bool InstallUpdate(const std::filesystem::path& update_path);
|
||||
bool CreateBackup();
|
||||
bool RestoreBackup();
|
||||
bool CleanupFiles();
|
||||
|
||||
// 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<QNetworkAccessManager> network_manager;
|
||||
QNetworkReply* current_reply = nullptr;
|
||||
|
||||
// Update state
|
||||
std::atomic<bool> update_in_progress{false};
|
||||
std::atomic<bool> 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
|
||||
@@ -617,6 +617,9 @@ struct Values {
|
||||
Category::WebService};
|
||||
Setting<std::string> citron_token{linkage, std::string(), "citron_token", Category::WebService};
|
||||
|
||||
// Updater
|
||||
Setting<bool> enable_auto_update_check{linkage, true, "enable_auto_update_check", Category::WebService};
|
||||
|
||||
// Add-Ons
|
||||
std::map<u64, std::vector<std::string>> disabled_addons;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user