mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 10:43:33 +00:00
Add update channel support (Stable/Nightly) to updater system
- Add STABLE_UPDATE_URL and NIGHTLY_UPDATE_URL constants - Update CheckForUpdates() to read channel from QSettings - Implement channel-aware version checking - Nightly: Always use commit hash from build version - Stable: Prioritize version.txt file, fallback to commit hash - Update ParseUpdateResponse() to handle different API formats - Stable: Parse tag_name from Gitea API - Nightly: Extract commit hash from GitHub release name - Update Linux AppImage updates to manage version.txt based on channel - Remove update_url parameter from CheckForUpdates() methods - Update main.cpp to use new CheckForUpdates() signature The updater now supports both Stable and Nightly channels with appropriate version checking and update source selection for each. Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -6187,12 +6187,10 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
void GMainWindow::OnCheckForUpdates() {
|
void GMainWindow::OnCheckForUpdates() {
|
||||||
#ifdef CITRON_USE_AUTO_UPDATER
|
#ifdef CITRON_USE_AUTO_UPDATER
|
||||||
std::string update_url = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases";
|
|
||||||
|
|
||||||
auto* updater_dialog = new Updater::UpdaterDialog(this);
|
auto* updater_dialog = new Updater::UpdaterDialog(this);
|
||||||
updater_dialog->setAttribute(Qt::WA_DeleteOnClose);
|
updater_dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
updater_dialog->show();
|
updater_dialog->show();
|
||||||
updater_dialog->CheckForUpdates(update_url);
|
updater_dialog->CheckForUpdates();
|
||||||
#else
|
#else
|
||||||
QMessageBox::information(this, tr("Updates"),
|
QMessageBox::information(this, tr("Updates"),
|
||||||
tr("The automatic updater is not enabled in this build."));
|
tr("The automatic updater is not enabled in this build."));
|
||||||
@@ -6208,8 +6206,6 @@ void GMainWindow::CheckForUpdatesAutomatically() {
|
|||||||
|
|
||||||
LOG_INFO(Frontend, "Checking for updates automatically...");
|
LOG_INFO(Frontend, "Checking for updates automatically...");
|
||||||
|
|
||||||
std::string update_url = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases";
|
|
||||||
|
|
||||||
auto* updater_service = new Updater::UpdaterService(this);
|
auto* updater_service = new Updater::UpdaterService(this);
|
||||||
|
|
||||||
connect(updater_service, &Updater::UpdaterService::UpdateCheckCompleted, this,
|
connect(updater_service, &Updater::UpdaterService::UpdateCheckCompleted, this,
|
||||||
@@ -6249,7 +6245,7 @@ void GMainWindow::CheckForUpdatesAutomatically() {
|
|||||||
updater_service->deleteLater();
|
updater_service->deleteLater();
|
||||||
});
|
});
|
||||||
|
|
||||||
updater_service->CheckForUpdates(update_url);
|
updater_service->CheckForUpdates();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ UpdaterDialog::UpdaterDialog(QWidget* parent)
|
|||||||
|
|
||||||
UpdaterDialog::~UpdaterDialog() = default;
|
UpdaterDialog::~UpdaterDialog() = default;
|
||||||
|
|
||||||
void UpdaterDialog::CheckForUpdates(const std::string& update_url) {
|
void UpdaterDialog::CheckForUpdates() {
|
||||||
ShowCheckingState();
|
ShowCheckingState();
|
||||||
updater_service->CheckForUpdates(update_url);
|
updater_service->CheckForUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdaterDialog::OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info) {
|
void UpdaterDialog::OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace Updater {
|
|||||||
explicit UpdaterDialog(QWidget* parent = nullptr);
|
explicit UpdaterDialog(QWidget* parent = nullptr);
|
||||||
~UpdaterDialog() override;
|
~UpdaterDialog() override;
|
||||||
|
|
||||||
void CheckForUpdates(const std::string& update_url);
|
void CheckForUpdates();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info);
|
void OnUpdateCheckCompleted(bool has_update, const Updater::UpdateInfo& update_info);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <QSslSocket>
|
#include <QSslSocket>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
#ifdef CITRON_ENABLE_LIBARCHIVE
|
#ifdef CITRON_ENABLE_LIBARCHIVE
|
||||||
#include <archive.h>
|
#include <archive.h>
|
||||||
@@ -39,7 +40,9 @@
|
|||||||
|
|
||||||
namespace Updater {
|
namespace Updater {
|
||||||
|
|
||||||
// Helper function to extract a commit hash from a string using std::regex.
|
const std::string STABLE_UPDATE_URL = "https://git.citron-emu.org/api/v1/repos/Citron/Emulator/releases";
|
||||||
|
const std::string NIGHTLY_UPDATE_URL = "https://api.github.com/repos/Zephyron-Dev/Citron-CI/releases";
|
||||||
|
|
||||||
std::string ExtractCommitHash(const std::string& version_string) {
|
std::string ExtractCommitHash(const std::string& version_string) {
|
||||||
std::regex re("\\b([0-9a-fA-F]{7,40})\\b");
|
std::regex re("\\b([0-9a-fA-F]{7,40})\\b");
|
||||||
std::smatch match;
|
std::smatch match;
|
||||||
@@ -116,15 +119,15 @@ void UpdaterService::InitializeSSL() {
|
|||||||
LOG_INFO(Frontend, "SSL initialized successfully");
|
LOG_INFO(Frontend, "SSL initialized successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdaterService::CheckForUpdates(const std::string& update_url) {
|
void UpdaterService::CheckForUpdates() {
|
||||||
if (update_in_progress.load()) {
|
if (update_in_progress.load()) {
|
||||||
emit UpdateError(QStringLiteral("Update operation already in progress"));
|
emit UpdateError(QStringLiteral("Update operation already in progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (update_url.empty()) {
|
QSettings settings;
|
||||||
emit UpdateError(QStringLiteral("Update URL not configured"));
|
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||||
return;
|
std::string update_url = (channel == QStringLiteral("Nightly")) ? NIGHTLY_UPDATE_URL : STABLE_UPDATE_URL;
|
||||||
}
|
LOG_INFO(Frontend, "Selected update channel: {}", channel.toStdString());
|
||||||
LOG_INFO(Frontend, "Checking for updates from: {}", update_url);
|
LOG_INFO(Frontend, "Checking for updates from: {}", update_url);
|
||||||
QUrl url{QString::fromStdString(update_url)};
|
QUrl url{QString::fromStdString(update_url)};
|
||||||
QNetworkRequest request{url};
|
QNetworkRequest request{url};
|
||||||
@@ -132,10 +135,10 @@ void UpdaterService::CheckForUpdates(const std::string& update_url) {
|
|||||||
request.setRawHeader("Accept", QByteArrayLiteral("application/json"));
|
request.setRawHeader("Accept", QByteArrayLiteral("application/json"));
|
||||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
current_reply = network_manager->get(request);
|
current_reply = network_manager->get(request);
|
||||||
connect(current_reply, &QNetworkReply::finished, this, [this]() {
|
connect(current_reply, &QNetworkReply::finished, this, [this, channel]() {
|
||||||
if (!current_reply) return;
|
if (!current_reply) return;
|
||||||
if (current_reply->error() == QNetworkReply::NoError) {
|
if (current_reply->error() == QNetworkReply::NoError) {
|
||||||
ParseUpdateResponse(current_reply->readAll());
|
ParseUpdateResponse(current_reply->readAll(), channel);
|
||||||
} else {
|
} else {
|
||||||
emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString()));
|
emit UpdateError(QStringLiteral("Update check failed: %1").arg(current_reply->errorString()));
|
||||||
}
|
}
|
||||||
@@ -196,29 +199,56 @@ void UpdaterService::CancelUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string UpdaterService::GetCurrentVersion() const {
|
std::string UpdaterService::GetCurrentVersion() const {
|
||||||
std::string build_version = Common::g_build_version;
|
QSettings settings;
|
||||||
if (!build_version.empty()) {
|
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||||
std::string hash = ExtractCommitHash(build_version);
|
|
||||||
if (!hash.empty()) {
|
// If the user's setting is Nightly, we must ignore version.txt and only use the commit hash.
|
||||||
return hash;
|
if (channel == QStringLiteral("Nightly")) {
|
||||||
|
std::string build_version = Common::g_build_version;
|
||||||
|
if (!build_version.empty()) {
|
||||||
|
std::string hash = ExtractCommitHash(build_version);
|
||||||
|
if (!hash.empty()) {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return ""; // Fallback if no hash is found
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path version_file = app_directory / CITRON_VERSION_FILE;
|
// Otherwise (channel is Stable), we prioritize version.txt.
|
||||||
|
std::filesystem::path search_path;
|
||||||
|
#ifdef __linux__
|
||||||
|
const char* appimage_path_env = qgetenv("APPIMAGE").constData();
|
||||||
|
if (appimage_path_env && strlen(appimage_path_env) > 0) {
|
||||||
|
search_path = std::filesystem::path(appimage_path_env).parent_path();
|
||||||
|
} else {
|
||||||
|
search_path = app_directory;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
search_path = app_directory;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::filesystem::path version_file = search_path / CITRON_VERSION_FILE;
|
||||||
if (std::filesystem::exists(version_file)) {
|
if (std::filesystem::exists(version_file)) {
|
||||||
std::ifstream file(version_file);
|
std::ifstream file(version_file);
|
||||||
if (file.is_open()) {
|
if (file.is_open()) {
|
||||||
std::string version_from_file;
|
std::string version_from_file;
|
||||||
std::getline(file, version_from_file);
|
std::getline(file, version_from_file);
|
||||||
if (!version_from_file.empty()) {
|
if (!version_from_file.empty()) {
|
||||||
std::string hash = ExtractCommitHash(version_from_file);
|
return version_from_file;
|
||||||
if (!hash.empty()){
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for Stable channel: If version.txt is missing, use the commit hash.
|
||||||
|
// This allows a nightly build to correctly check for a stable update.
|
||||||
|
std::string build_version = Common::g_build_version;
|
||||||
|
if (!build_version.empty()) {
|
||||||
|
std::string hash = ExtractCommitHash(build_version);
|
||||||
|
if (!hash.empty()) {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,17 +257,19 @@ bool UpdaterService::IsUpdateInProgress() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UpdaterService::OnDownloadFinished() {
|
void UpdaterService::OnDownloadFinished() {
|
||||||
if (cancel_requested.load()) {
|
if (cancel_requested.load() || !current_reply) {
|
||||||
update_in_progress.store(false);
|
update_in_progress.store(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!current_reply || current_reply->error() != QNetworkReply::NoError) {
|
if (current_reply->error() != QNetworkReply::NoError) {
|
||||||
if(current_reply) emit UpdateError(QStringLiteral("Download failed: %1").arg(current_reply->errorString()));
|
emit UpdateError(QStringLiteral("Download failed: %1").arg(current_reply->errorString()));
|
||||||
update_in_progress.store(false);
|
update_in_progress.store(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray downloaded_data = current_reply->readAll();
|
QByteArray downloaded_data = current_reply->readAll();
|
||||||
|
QSettings settings;
|
||||||
|
QString channel = settings.value(QStringLiteral("updater/channel"), QStringLiteral("Stable")).toString();
|
||||||
|
|
||||||
// This logic has been simplified for clarity. The checksum part can be re-added later.
|
// This logic has been simplified for clarity. The checksum part can be re-added later.
|
||||||
|
|
||||||
@@ -311,16 +343,29 @@ void UpdaterService::OnDownloadFinished() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the old AppImage with the new one.
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::rename(new_appimage_path, original_appimage_path, ec);
|
std::filesystem::rename(new_appimage_path, original_appimage_path, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
LOG_ERROR(Frontend, "Failed to replace old AppImage: {}", ec.message());
|
LOG_ERROR(Frontend, "Failed to replace old AppImage: {}", ec.message());
|
||||||
emit UpdateError(QStringLiteral("Failed to replace old AppImage. Please close the application and replace it manually."));
|
emit UpdateError(QStringLiteral("Failed to replace old AppImage."));
|
||||||
update_in_progress.store(false);
|
update_in_progress.store(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::filesystem::path version_file_path = original_appimage_path.parent_path() / CITRON_VERSION_FILE;
|
||||||
|
if (channel == QStringLiteral("Stable")) {
|
||||||
|
LOG_INFO(Frontend, "Writing stable version marker: {}", current_update_info.version);
|
||||||
|
std::ofstream version_file(version_file_path);
|
||||||
|
if (version_file.is_open()) {
|
||||||
|
version_file << current_update_info.version;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_INFO(Frontend, "Nightly update, removing stable version marker if it exists.");
|
||||||
|
if (std::filesystem::exists(version_file_path)) {
|
||||||
|
std::filesystem::remove(version_file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO(Frontend, "AppImage updated successfully.");
|
LOG_INFO(Frontend, "AppImage updated successfully.");
|
||||||
emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application."));
|
emit UpdateCompleted(UpdateResult::Success, QStringLiteral("Update successful. Please restart the application."));
|
||||||
update_in_progress.store(false);
|
update_in_progress.store(false);
|
||||||
@@ -329,8 +374,8 @@ void UpdaterService::OnDownloadFinished() {
|
|||||||
|
|
||||||
void UpdaterService::OnDownloadProgress(qint64 bytes_received, qint64 bytes_total) {
|
void UpdaterService::OnDownloadProgress(qint64 bytes_received, qint64 bytes_total) {
|
||||||
if (bytes_total > 0) {
|
if (bytes_total > 0) {
|
||||||
int percentage = static_cast<int>((bytes_received * 100) / bytes_total);
|
emit UpdateDownloadProgress(static_cast<int>((bytes_received * 100) / bytes_total),
|
||||||
emit UpdateDownloadProgress(percentage, bytes_received, bytes_total);
|
bytes_received, bytes_total);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,63 +386,51 @@ void UpdaterService::OnDownloadError(QNetworkReply::NetworkError) {
|
|||||||
update_in_progress.store(false);
|
update_in_progress.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdaterService::ParseUpdateResponse(const QByteArray& response) {
|
void UpdaterService::ParseUpdateResponse(const QByteArray& response, const QString& channel) {
|
||||||
QJsonParseError error;
|
QJsonParseError error;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(response, &error);
|
QJsonDocument doc = QJsonDocument::fromJson(response, &error);
|
||||||
if (error.error != QJsonParseError::NoError) {
|
if (error.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||||
emit UpdateError(QStringLiteral("Failed to parse JSON: %1").arg(error.errorString()));
|
emit UpdateError(QStringLiteral("Failed to parse update response."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!doc.isArray()) {
|
|
||||||
emit UpdateError(QStringLiteral("JSON response is not an array."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString platform_identifier;
|
|
||||||
#if defined(_WIN32)
|
|
||||||
platform_identifier = QStringLiteral("windows");
|
|
||||||
#elif defined(__linux__)
|
|
||||||
platform_identifier = QStringLiteral("linux");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (const QJsonValue& release_value : doc.array()) {
|
for (const QJsonValue& release_value : doc.array()) {
|
||||||
QJsonObject release_obj = release_value.toObject();
|
QJsonObject release_obj = release_value.toObject();
|
||||||
QString release_name = release_obj.value(QStringLiteral("name")).toString();
|
std::string latest_version;
|
||||||
|
if (channel == QStringLiteral("Stable")) {
|
||||||
|
latest_version = release_obj.value(QStringLiteral("tag_name")).toString().toStdString();
|
||||||
|
} else {
|
||||||
|
latest_version = ExtractCommitHash(release_obj.value(QStringLiteral("name")).toString().toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
if (release_name.toLower().contains(platform_identifier)) {
|
if (latest_version.empty()) continue;
|
||||||
std::string latest_hash = ExtractCommitHash(release_name.toStdString());
|
|
||||||
|
|
||||||
if (latest_hash.empty()) {
|
UpdateInfo update_info;
|
||||||
continue;
|
update_info.version = latest_version;
|
||||||
}
|
update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString();
|
||||||
|
update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString();
|
||||||
|
|
||||||
UpdateInfo update_info;
|
QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray();
|
||||||
update_info.version = latest_hash;
|
for (const QJsonValue& asset_value : assets) {
|
||||||
update_info.changelog = release_obj.value(QStringLiteral("body")).toString().toStdString();
|
QJsonObject asset_obj = asset_value.toObject();
|
||||||
update_info.release_date = release_obj.value(QStringLiteral("published_at")).toString().toStdString();
|
QString asset_name = asset_obj.value(QStringLiteral("name")).toString();
|
||||||
|
#if defined(__linux__)
|
||||||
QJsonArray assets = release_obj.value(QStringLiteral("assets")).toArray();
|
if (asset_name.endsWith(QStringLiteral(".AppImage"))) {
|
||||||
for (const QJsonValue& asset_value : assets) {
|
#else
|
||||||
QJsonObject asset_obj = asset_value.toObject();
|
if (asset_name.endsWith(QStringLiteral(".zip"))) {
|
||||||
QString asset_name = asset_obj.value(QStringLiteral("name")).toString();
|
|
||||||
#if defined(_WIN32)
|
|
||||||
if (asset_name.endsWith(QStringLiteral(".zip"))) {
|
|
||||||
#elif defined(__linux__)
|
|
||||||
if (asset_name.endsWith(QStringLiteral(".AppImage"))) {
|
|
||||||
#endif
|
#endif
|
||||||
DownloadOption option;
|
DownloadOption option;
|
||||||
option.name = asset_name.toStdString();
|
option.name = asset_name.toStdString();
|
||||||
option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString();
|
option.url = asset_obj.value(QStringLiteral("browser_download_url")).toString().toStdString();
|
||||||
update_info.download_options.push_back(option);
|
update_info.download_options.push_back(option);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!update_info.download_options.empty()) {
|
if (!update_info.download_options.empty()) {
|
||||||
update_info.is_newer_version = CompareVersions(GetCurrentVersion(), update_info.version);
|
update_info.is_newer_version = CompareVersions(GetCurrentVersion(), update_info.version);
|
||||||
current_update_info = update_info;
|
current_update_info = update_info;
|
||||||
emit UpdateCheckCompleted(update_info.is_newer_version, update_info);
|
emit UpdateCheckCompleted(update_info.is_newer_version, update_info);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit UpdateError(QStringLiteral("Could not find a recent update for your platform."));
|
emit UpdateError(QStringLiteral("Could not find a recent update for your platform."));
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public:
|
|||||||
explicit UpdaterService(QObject* parent = nullptr);
|
explicit UpdaterService(QObject* parent = nullptr);
|
||||||
~UpdaterService() override;
|
~UpdaterService() override;
|
||||||
|
|
||||||
void CheckForUpdates(const std::string& update_url);
|
void CheckForUpdates();
|
||||||
void DownloadAndInstallUpdate(const std::string& download_url);
|
void DownloadAndInstallUpdate(const std::string& download_url);
|
||||||
void CancelUpdate();
|
void CancelUpdate();
|
||||||
std::string GetCurrentVersion() const;
|
std::string GetCurrentVersion() const;
|
||||||
@@ -72,7 +72,7 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void InitializeSSL();
|
void InitializeSSL();
|
||||||
void ConfigureSSLForRequest(QNetworkRequest& request);
|
void ConfigureSSLForRequest(QNetworkRequest& request);
|
||||||
void ParseUpdateResponse(const QByteArray& response);
|
void ParseUpdateResponse(const QByteArray& response, const QString& channel);
|
||||||
bool CompareVersions(const std::string& current, const std::string& latest) const;
|
bool CompareVersions(const std::string& current, const std::string& latest) const;
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|||||||
Reference in New Issue
Block a user