diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/settingsdialogupdates.cpp | 263 | ||||
| -rw-r--r-- | src/src/settingsdialogupdates.h | 7 |
2 files changed, 246 insertions, 24 deletions
diff --git a/src/src/settingsdialogupdates.cpp b/src/src/settingsdialogupdates.cpp index c5441d1..b903975 100644 --- a/src/src/settingsdialogupdates.cpp +++ b/src/src/settingsdialogupdates.cpp @@ -5,18 +5,35 @@ #include "ui_settingsdialog.h" #include <fluorine_build_info.h> +#include <log.h> +#include <QApplication> #include <QCheckBox> #include <QComboBox> +#include <QCoreApplication> +#include <QDir> +#include <QFile> +#include <QFileInfo> #include <QFormLayout> #include <QGroupBox> #include <QHBoxLayout> #include <QLabel> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QProcess> +#include <QProgressBar> #include <QPushButton> +#include <QStandardPaths> #include <QTabWidget> +#include <QTimer> +#include <QUrl> #include <QVBoxLayout> #include <QWidget> +#include <sys/types.h> +#include <unistd.h> + #include <algorithm> UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) @@ -25,7 +42,7 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) // Build the tab contents programmatically so we don't have to hand-edit // settingsdialog.ui — the Updates section used to live on the General // tab; moving it to its own tab keeps General uncluttered and gives us - // room for a "Check for updates now" button + live status line. + // room for "Check now" + "Install & restart" buttons with progress. QWidget* page = new QWidget(ui->tabWidget); auto* layout = new QVBoxLayout(page); @@ -33,23 +50,19 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) auto* infoGroup = new QGroupBox(tr("Current build"), page); auto* infoLayout = new QFormLayout(infoGroup); - const QString currentVersion = - QStringLiteral(FLUORINE_DISPLAY_VERSION); - const QString channel = - QStringLiteral(FLUORINE_BUILD_CHANNEL); - const QString commit = QStringLiteral(FLUORINE_BUILD_COMMIT); - const QString timestamp = QStringLiteral(FLUORINE_BUILD_TIMESTAMP); + const QString currentVersion = QStringLiteral(FLUORINE_DISPLAY_VERSION); + const QString channel = QStringLiteral(FLUORINE_BUILD_CHANNEL); + const QString commit = QStringLiteral(FLUORINE_BUILD_COMMIT); + const QString timestamp = QStringLiteral(FLUORINE_BUILD_TIMESTAMP); m_currentVersionLabel = new QLabel(currentVersion, infoGroup); infoLayout->addRow(tr("Version:"), m_currentVersionLabel); QString buildLine = QStringLiteral("channel=%1").arg(channel); - if (!commit.isEmpty()) { + if (!commit.isEmpty()) buildLine += QStringLiteral(" commit=%1").arg(commit); - } - if (!timestamp.isEmpty()) { + if (!timestamp.isEmpty()) buildLine += QStringLiteral(" timestamp=%1").arg(timestamp); - } m_buildInfoLabel = new QLabel(buildLine, infoGroup); m_buildInfoLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); infoLayout->addRow(tr("Build:"), m_buildInfoLabel); @@ -64,8 +77,8 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) prefsGroup); m_checkForUpdates->setToolTip( tr("Query GitHub for a newer Fluorine Manager build when the app " - "starts. Nothing is installed automatically — you'll see a log " - "entry and can download the new build from the release page.")); + "starts. Nothing is installed automatically — you'll see a notice " + "here and can install from the button below.")); prefsLayout->addWidget(m_checkForUpdates); auto* channelRow = new QHBoxLayout(); @@ -75,11 +88,6 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) QStringLiteral("stable")); m_channelBox->addItem(tr("Beta (rolling build from main)"), QStringLiteral("beta")); - m_channelBox->setToolTip( - tr("Stable: only tagged v* releases. Beta: the rolling `beta` " - "release that's replaced on every successful CI build. Beta " - "builds embed a commit hash and timestamp so the updater can " - "tell whether you're already on the latest one.")); channelRow->addWidget(m_channelBox, 1); prefsLayout->addLayout(channelRow); @@ -92,10 +100,19 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) auto* buttonRow = new QHBoxLayout(); m_checkNowButton = new QPushButton(tr("Check for updates now"), actionGroup); + m_installButton = + new QPushButton(tr("Install update && restart"), actionGroup); + m_installButton->setEnabled(false); buttonRow->addWidget(m_checkNowButton); + buttonRow->addWidget(m_installButton); buttonRow->addStretch(1); actionLayout->addLayout(buttonRow); + m_progressBar = new QProgressBar(actionGroup); + m_progressBar->setVisible(false); + m_progressBar->setRange(0, 100); + actionLayout->addWidget(m_progressBar); + m_statusLabel = new QLabel(actionGroup); m_statusLabel->setTextFormat(Qt::RichText); m_statusLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); @@ -106,7 +123,7 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) layout->addWidget(actionGroup); layout->addStretch(1); - // Insert as the second tab (right after General) so it's easy to find. + // Insert after General. const int insertIndex = std::max(1, std::min(ui->tabWidget->count(), 1)); ui->tabWidget->insertTab(insertIndex, page, tr("Updates")); @@ -122,11 +139,14 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) QObject::connect(m_checkNowButton, &QPushButton::clicked, &d, [this]() { onCheckNow(); }); + QObject::connect(m_installButton, &QPushButton::clicked, &d, + [this]() { onInstall(); }); + QObject::connect(m_updater, &FluorineUpdater::updateAvailable, &d, [this](const FluorineUpdater::ReleaseInfo& info) { - const QString url = info.htmlUrl.isEmpty() - ? QString() - : info.htmlUrl; + m_pendingUpdate = info; + m_updatePending = true; + const QString url = info.htmlUrl.isEmpty() ? QString() : info.htmlUrl; QString line = tr("<b>Update available:</b> %1") .arg(info.name.isEmpty() ? info.tagName : info.name); @@ -137,9 +157,18 @@ UpdatesSettingsTab::UpdatesSettingsTab(Settings& s, SettingsDialog& d) } m_statusLabel->setText(line); m_checkNowButton->setEnabled(true); + m_installButton->setEnabled(!info.downloadUrl.isEmpty()); + if (info.downloadUrl.isEmpty()) { + m_installButton->setToolTip( + tr("This release has no .tar.gz asset attached.")); + } else { + m_installButton->setToolTip(QString()); + } }); QObject::connect(m_updater, &FluorineUpdater::upToDate, &d, [this](const FluorineUpdater::ReleaseInfo&) { + m_updatePending = false; + m_installButton->setEnabled(false); m_statusLabel->setText( tr("You're on the latest build for this channel.")); m_checkNowButton->setEnabled(true); @@ -157,8 +186,6 @@ void UpdatesSettingsTab::update() settings().setCheckForUpdates(m_checkForUpdates->isChecked()); const QString channel = m_channelBox->currentData().toString(); settings().setFluorineUpdateChannel(channel); - // Keep the legacy usePrereleases flag aligned so the Nexus-side pre- - // release toggle mirrors the Fluorine channel selection. settings().setUsePrereleases(channel == QStringLiteral("beta")); } @@ -166,9 +193,197 @@ void UpdatesSettingsTab::onCheckNow() { m_statusLabel->setText(tr("Checking…")); m_checkNowButton->setEnabled(false); + m_installButton->setEnabled(false); const QString channel = m_channelBox->currentData().toString(); const FluorineUpdater::Channel c = FluorineUpdater::channelFromString( channel, FluorineUpdater::buildChannel()); m_updater->checkForUpdates(c); } + +void UpdatesSettingsTab::onInstall() +{ + if (!m_updatePending || m_pendingUpdate.downloadUrl.isEmpty()) { + return; + } + + const QString downloadUrl = m_pendingUpdate.downloadUrl; + + // Staging dir for the downloaded tarball. Separate from the live bin/ + // so we can fall back cleanly if anything fails mid-extract. + const QString dataRoot = + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + + QStringLiteral("/fluorine"); + const QString stagingDir = dataRoot + QStringLiteral("/update-staging"); + QDir().mkpath(stagingDir); + const QString tarPath = stagingDir + QStringLiteral("/download.tar.gz"); + const QString extractDir = stagingDir + QStringLiteral("/extract"); + + QFile::remove(tarPath); + QDir(extractDir).removeRecursively(); + QDir().mkpath(extractDir); + + m_installButton->setEnabled(false); + m_checkNowButton->setEnabled(false); + m_progressBar->setVisible(true); + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + m_statusLabel->setText(tr("Downloading update…")); + + auto* nam = new QNetworkAccessManager(m_installButton); + QNetworkRequest req{QUrl(downloadUrl)}; + req.setRawHeader("User-Agent", "Fluorine-Manager/updater"); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + QNetworkReply* reply = nam->get(req); + + auto* outFile = new QFile(tarPath, reply); + if (!outFile->open(QIODevice::WriteOnly)) { + m_statusLabel->setText( + tr("<i>Install failed:</i> cannot open '%1' for writing") + .arg(tarPath)); + m_progressBar->setVisible(false); + m_installButton->setEnabled(true); + m_checkNowButton->setEnabled(true); + reply->deleteLater(); + return; + } + + QObject::connect(reply, &QNetworkReply::readyRead, reply, + [reply, outFile]() { + outFile->write(reply->readAll()); + }); + QObject::connect( + reply, &QNetworkReply::downloadProgress, m_progressBar, + [this](qint64 received, qint64 total) { + if (total > 0) { + m_progressBar->setRange(0, 100); + m_progressBar->setValue( + static_cast<int>((received * 100) / total)); + } else { + m_progressBar->setRange(0, 0); // indeterminate + } + }); + QObject::connect( + reply, &QNetworkReply::finished, m_installButton, + [this, reply, outFile, tarPath, extractDir, stagingDir]() { + outFile->close(); + const QNetworkReply::NetworkError err = reply->error(); + const QString errString = reply->errorString(); + reply->deleteLater(); + + if (err != QNetworkReply::NoError) { + m_statusLabel->setText( + tr("<i>Download failed:</i> %1").arg(errString)); + m_progressBar->setVisible(false); + m_installButton->setEnabled(true); + m_checkNowButton->setEnabled(true); + return; + } + + m_statusLabel->setText(tr("Extracting update…")); + m_progressBar->setRange(0, 0); // indeterminate while tar runs + + auto* tar = new QProcess(m_installButton); + tar->setWorkingDirectory(extractDir); + tar->setProgram(QStringLiteral("tar")); + tar->setArguments({QStringLiteral("xzf"), tarPath}); + QObject::connect( + tar, + QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), + m_installButton, + [this, tar, tarPath, extractDir, stagingDir]( + int code, QProcess::ExitStatus st) { + tar->deleteLater(); + if (st != QProcess::NormalExit || code != 0) { + m_statusLabel->setText( + tr("<i>Extraction failed:</i> tar exited with %1") + .arg(code)); + m_progressBar->setVisible(false); + m_installButton->setEnabled(true); + m_checkNowButton->setEnabled(true); + return; + } + + // The tarball is packed as `fluorine-manager/…`. Locate the + // top-level dir (picks up a rename too, just in case). + QDir extract(extractDir); + const QStringList tops = extract.entryList( + QDir::Dirs | QDir::NoDotAndDotDot); + if (tops.isEmpty()) { + m_statusLabel->setText( + tr("<i>Install failed:</i> extracted archive is empty")); + m_progressBar->setVisible(false); + m_installButton->setEnabled(true); + m_checkNowButton->setEnabled(true); + return; + } + const QString newBundle = + extract.absoluteFilePath(tops.first()); + const QString newLauncher = + newBundle + QStringLiteral("/fluorine-manager"); + if (!QFileInfo::exists(newLauncher)) { + m_statusLabel->setText( + tr("<i>Install failed:</i> launcher not found in " + "extracted archive")); + m_progressBar->setVisible(false); + m_installButton->setEnabled(true); + m_checkNowButton->setEnabled(true); + return; + } + + // Write a tiny helper script that waits for the current + // Fluorine process to exit (the fluorine-manager launcher + // refuses to re-sync itself while we're still holding bin/ + // open) then execs the new launcher, which performs its + // own sync into ~/.local/share/fluorine/bin/. + const QString helperPath = + stagingDir + QStringLiteral("/install.sh"); + const qint64 currentPid = ::getpid(); + QFile helper(helperPath); + if (!helper.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + m_statusLabel->setText( + tr("<i>Install failed:</i> cannot write install helper")); + m_progressBar->setVisible(false); + m_installButton->setEnabled(true); + m_checkNowButton->setEnabled(true); + return; + } + const QString script = + QStringLiteral( + "#!/usr/bin/env bash\n" + "set -u\n" + "OLD_PID=%1\n" + "NEW_LAUNCHER=%2\n" + "for _ in $(seq 1 200); do\n" + " if ! kill -0 \"$OLD_PID\" 2>/dev/null; then break; fi\n" + " sleep 0.1\n" + "done\n" + "exec \"$NEW_LAUNCHER\"\n") + .arg(QString::number(currentPid), newLauncher); + helper.write(script.toUtf8()); + helper.close(); + QFile::setPermissions( + helperPath, + QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | + QFile::ReadGroup | QFile::ExeGroup | + QFile::ReadOther | QFile::ExeOther); + + m_statusLabel->setText( + tr("Update staged. Restarting Fluorine Manager…")); + + // Detach the helper so it survives our exit. + QProcess::startDetached(QStringLiteral("/usr/bin/env"), + {QStringLiteral("bash"), helperPath}); + + MOBase::log::info( + "update installer: spawned helper to restart into {}", + newLauncher); + + // Give the signal a beat to propagate, then quit cleanly. + QTimer::singleShot(250, qApp, + []() { QCoreApplication::quit(); }); + }); + tar->start(); + }); +} diff --git a/src/src/settingsdialogupdates.h b/src/src/settingsdialogupdates.h index 9712beb..3a656db 100644 --- a/src/src/settingsdialogupdates.h +++ b/src/src/settingsdialogupdates.h @@ -1,6 +1,7 @@ #ifndef SETTINGSDIALOGUPDATES_H #define SETTINGSDIALOGUPDATES_H +#include "fluorineupdater.h" #include "settingsdialog.h" #include <QCoreApplication> @@ -8,6 +9,7 @@ class QCheckBox; class QComboBox; class QLabel; +class QProgressBar; class QPushButton; class FluorineUpdater; @@ -25,14 +27,19 @@ public: private: void onCheckNow(); + void onInstall(); QCheckBox* m_checkForUpdates = nullptr; QComboBox* m_channelBox = nullptr; QLabel* m_currentVersionLabel = nullptr; QLabel* m_buildInfoLabel = nullptr; QPushButton* m_checkNowButton = nullptr; + QPushButton* m_installButton = nullptr; QLabel* m_statusLabel = nullptr; + QProgressBar* m_progressBar = nullptr; FluorineUpdater* m_updater = nullptr; + FluorineUpdater::ReleaseInfo m_pendingUpdate{}; + bool m_updatePending = false; }; #endif // SETTINGSDIALOGUPDATES_H |
