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:
Zephyron
2025-07-11 16:22:28 +10:00
parent d709c878bc
commit 9ef1c76a2c
10 changed files with 1692 additions and 0 deletions

View File

@@ -210,6 +210,11 @@ add_executable(citron
startup_checks.h startup_checks.h
uisettings.cpp uisettings.cpp
uisettings.h 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.cpp
util/clickable_label.h util/clickable_label.h
util/controller_navigation.cpp 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 Boost::headers glad Qt${QT_MAJOR_VERSION}::Widgets)
target_link_libraries(citron PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) 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) target_link_libraries(citron PRIVATE Vulkan::Headers)
if (NOT WIN32) if (NOT WIN32)
target_include_directories(citron PRIVATE ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS}) target_include_directories(citron PRIVATE ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})

View File

@@ -161,6 +161,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "citron/play_time_manager.h" #include "citron/play_time_manager.h"
#include "citron/startup_checks.h" #include "citron/startup_checks.h"
#include "citron/uisettings.h" #include "citron/uisettings.h"
#include "citron/updater/updater_dialog.h"
#include "citron/util/clickable_label.h" #include "citron/util/clickable_label.h"
#include "citron/vk_device_info.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 // Show one-time "callout" messages to the user
ShowTelemetryCallout(); 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 // make sure menubar has the arrow cursor instead of inheriting from this
ui->menubar->setCursor(QCursor()); ui->menubar->setCursor(QCursor());
statusBar()->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_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware); connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware);
connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys); connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys);
connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates);
connect_menu(ui->action_About, &GMainWindow::OnAbout); connect_menu(ui->action_About, &GMainWindow::OnAbout);
} }
@@ -5347,6 +5352,60 @@ int main(int argc, char* argv[]) {
return result; 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() { void GMainWindow::OnToggleGridView() {
game_list->ToggleViewMode(); game_list->ToggleViewMode();
} }

View File

@@ -59,6 +59,7 @@ class QtControllerSelectorDialog;
class QtProfileSelectionDialog; class QtProfileSelectionDialog;
class QtSoftwareKeyboardDialog; class QtSoftwareKeyboardDialog;
class QtNXWebEngineView; class QtNXWebEngineView;
class UpdaterDialog;
enum class StartGameType { enum class StartGameType {
Normal, // Can use custom configuration Normal, // Can use custom configuration
@@ -383,6 +384,8 @@ private slots:
void OnInstallFirmware(); void OnInstallFirmware();
void OnInstallDecryptionKeys(); void OnInstallDecryptionKeys();
void OnAbout(); void OnAbout();
void OnCheckForUpdates();
void CheckForUpdatesAutomatically();
void OnToggleFilterBar(); void OnToggleFilterBar();
void OnToggleGridView(); void OnToggleGridView();
void OnToggleStatusBar(); void OnToggleStatusBar();

View File

@@ -187,6 +187,8 @@
<addaction name="action_Open_Quickstart_Guide"/> <addaction name="action_Open_Quickstart_Guide"/>
<addaction name="action_Open_FAQ"/> <addaction name="action_Open_FAQ"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_Check_For_Updates"/>
<addaction name="separator"/>
<addaction name="action_About"/> <addaction name="action_About"/>
</widget> </widget>
<addaction name="menu_File"/> <addaction name="menu_File"/>
@@ -240,6 +242,11 @@
<string>&amp;Verify Installed Contents</string> <string>&amp;Verify Installed Contents</string>
</property> </property>
</action> </action>
<action name="action_Check_For_Updates">
<property name="text">
<string>Check for &amp;Updates...</string>
</property>
</action>
<action name="action_About"> <action name="action_About">
<property name="text"> <property name="text">
<string>&amp;About citron</string> <string>&amp;About citron</string>

View 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"

View 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;
};

View 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>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;No changelog available.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

View 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"

View 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

View File

@@ -617,6 +617,9 @@ struct Values {
Category::WebService}; Category::WebService};
Setting<std::string> citron_token{linkage, std::string(), "citron_token", 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 // Add-Ons
std::map<u64, std::vector<std::string>> disabled_addons; std::map<u64, std::vector<std::string>> disabled_addons;
}; };