aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/src/mainwindow.cpp9
-rw-r--r--src/src/organizercore.cpp168
-rw-r--r--src/src/slrmanager.cpp139
-rw-r--r--src/src/slrmanager.h13
4 files changed, 288 insertions, 41 deletions
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp
index 431a9db..11f2cde 100644
--- a/src/src/mainwindow.cpp
+++ b/src/src/mainwindow.cpp
@@ -2264,7 +2264,6 @@ void MainWindow::readSettings()
s.geometry().restoreState(ui->splitter);
s.geometry().restoreState(ui->categoriesSplitter);
ui->menuBar->show();
- ui->toolBar->show();
s.geometry().restoreVisibility(ui->statusBar);
FilterWidget::setOptions(s.interface().filterOptions());
@@ -2421,15 +2420,11 @@ void MainWindow::on_startButton_clicked()
});
// Pre-check: Proton launches always use SLR. Native Linux executables skip
- // this branch entirely. downloadSlr() short-circuits on BUILD_ID match, so
- // once SLR is current this is a single HTTP GET; no UI is shown unless the
- // runtime actually needs (re)downloading.
+ // this branch entirely. Startup update checks offer SLR updates separately;
+ // launch only blocks when no managed runtime exists yet.
if (selectedExecutable->useProton()) {
const bool freshInstall = !isSlrInstalled();
// Only show the heavyweight progress dialog on a fresh install.
- // Up-to-date checks happen quietly in the background here so we don't
- // block launch on a remote BUILD_ID fetch — the up-to-date case is
- // handled at startup by OrganizerCore::checkForSlrUpdates().
if (freshInstall) {
auto* progress = new QProgressDialog(
tr("Downloading Steam Linux Runtime (~200 MB)...\n"
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 64772fa..fec0dfd 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -58,15 +58,20 @@
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
+#include <QFutureWatcher>
#include <QMessageBox>
#include <QNetworkInterface>
+#include <QPointer>
#include <QProcess>
+#include <QProgressDialog>
+#include <QPushButton>
#include <QRegularExpression>
#include <QTemporaryFile>
#include <QTextStream>
#include <QTimer>
#include <QUrl>
#include <QWidget>
+#include <QtConcurrent/QtConcurrentRun>
#include <QtDebug>
#include <QtGlobal> // for qUtf8Printable, etc
@@ -76,6 +81,7 @@
#include <cstring> // for memset, wcsrchr
#include <boost/algorithm/string/predicate.hpp>
+#include <atomic>
#include <exception>
#include <functional>
#include <memory>
@@ -96,6 +102,7 @@ static env::CoreDumpTypes g_coreDumpType = env::CoreDumpTypes::Mini;
namespace
{
+std::atomic<bool> s_slrUpdateCheckInProgress{false};
QString uniqueFilePath(const QDir& dir, const QString& fileName)
{
@@ -127,6 +134,104 @@ QString uniqueFilePath(const QDir& dir, const QString& fileName)
: QStringLiteral("%1_%2.%3").arg(base, timestamp, suffix));
}
+void installSlrUpdate(QWidget* parent, const SlrUpdateInfo& info);
+
+void promptSlrUpdate(QWidget* parent, const SlrUpdateInfo& info)
+{
+ if (parent == nullptr) {
+ parent = qApp->activeWindow();
+ }
+
+ QMessageBox box(parent);
+ box.setIcon(QMessageBox::Information);
+ box.setWindowTitle(QObject::tr("Steam Linux Runtime Update"));
+ box.setText(QObject::tr("A Steam Linux Runtime update is available."));
+ box.setInformativeText(
+ QObject::tr("Current build: %1\nLatest build: %2\n\n"
+ "You can update now, be reminded later, or skip this build.")
+ .arg(info.localBuildId.isEmpty() ? QObject::tr("unknown") : info.localBuildId,
+ info.remoteBuildId));
+
+ QPushButton* updateButton =
+ box.addButton(QObject::tr("Update Now"), QMessageBox::AcceptRole);
+ box.addButton(QObject::tr("Remind Me Later"), QMessageBox::RejectRole);
+ QPushButton* skipButton =
+ box.addButton(QObject::tr("Skip This Version"), QMessageBox::DestructiveRole);
+ box.setDefaultButton(updateButton);
+
+ box.exec();
+
+ if (box.clickedButton() == updateButton) {
+ installSlrUpdate(parent, info);
+ } else if (box.clickedButton() == skipButton) {
+ QSettings globalSettings = GlobalSettings::settings();
+ globalSettings.setValue(QStringLiteral("SteamLinuxRuntime/skipped_build_id"),
+ info.remoteBuildId);
+ }
+}
+
+void installSlrUpdate(QWidget* parent, const SlrUpdateInfo& info)
+{
+ if (parent == nullptr) {
+ parent = qApp->activeWindow();
+ }
+
+ auto* progress = new QProgressDialog(
+ QObject::tr("Updating Steam Linux Runtime...\n"
+ "Current build: %1\nLatest build: %2")
+ .arg(info.localBuildId.isEmpty() ? QObject::tr("unknown") : info.localBuildId,
+ info.remoteBuildId),
+ QObject::tr("Cancel"), 0, 0, parent);
+ progress->setWindowTitle(QObject::tr("Steam Linux Runtime"));
+ progress->setWindowModality(Qt::WindowModal);
+ progress->setMinimumDuration(0);
+
+ auto* cancelFlag = new int(0);
+ QObject::connect(progress, &QProgressDialog::canceled, progress, [cancelFlag] {
+ *cancelFlag = 1;
+ });
+
+ auto* watcher = new QFutureWatcher<QString>(progress);
+ QObject::connect(watcher, &QFutureWatcher<QString>::finished, progress,
+ [watcher, progress, cancelFlag] {
+ progress->close();
+ const QString err = watcher->result();
+ watcher->deleteLater();
+ progress->deleteLater();
+
+ if (*cancelFlag != 0) {
+ delete cancelFlag;
+ return;
+ }
+
+ if (!err.isEmpty()) {
+ MOBase::log::warn("SLR update failed: {}", err);
+ QMessageBox::warning(
+ qApp->activeWindow(),
+ QObject::tr("Steam Linux Runtime"),
+ QObject::tr("Steam Linux Runtime update failed:\n%1\n\n"
+ "The existing runtime was kept.")
+ .arg(err));
+ } else {
+ QSettings globalSettings = GlobalSettings::settings();
+ globalSettings.remove(
+ QStringLiteral("SteamLinuxRuntime/skipped_build_id"));
+ MOBase::log::info("Steam Linux Runtime updated successfully");
+ QMessageBox::information(
+ qApp->activeWindow(),
+ QObject::tr("Steam Linux Runtime"),
+ QObject::tr("Steam Linux Runtime updated successfully."));
+ }
+ delete cancelFlag;
+ });
+
+ int* cancelPtr = cancelFlag;
+ watcher->setFuture(QtConcurrent::run([cancelPtr]() -> QString {
+ return downloadSlr(nullptr, nullptr, cancelPtr);
+ }));
+ progress->show();
+}
+
} // namespace
template <typename InputIterator>
@@ -470,11 +575,9 @@ void OrganizerCore::checkForUpdates()
void OrganizerCore::checkForSlrUpdates()
{
- // SLR auto-update: only relevant if SLR is already installed. The first-
- // install case is handled at game launch in MainWindow, where the user gets
- // a progress dialog. Here we silently background-fetch a newer steamrt4
- // image when one exists; downloadSlr() short-circuits on BUILD_ID match so
- // the cost is one HTTP GET when already up-to-date.
+ // SLR updates are offered at startup but remain user-controlled. The first
+ // install case is still handled at game launch, where Proton cannot proceed
+ // without a runtime.
if (!m_Settings.checkForUpdates()) {
return;
}
@@ -485,11 +588,56 @@ void OrganizerCore::checkForSlrUpdates()
return;
}
- // Fire and forget on a detached thread. No UI; result is logged.
- std::thread([]() {
- const QString err = downloadSlr(nullptr, nullptr, nullptr);
- if (!err.isEmpty()) {
- MOBase::log::warn("SLR update check failed: {}", err);
+ bool expected = false;
+ if (!s_slrUpdateCheckInProgress.compare_exchange_strong(expected, true)) {
+ return;
+ }
+
+ QPointer<OrganizerCore> self(this);
+ std::thread([self]() {
+ const SlrUpdateInfo info = checkSlrUpdate();
+ if (!self) {
+ s_slrUpdateCheckInProgress = false;
+ return;
+ }
+
+ const bool invoked = QMetaObject::invokeMethod(self, [self, info]() {
+ if (!self) {
+ s_slrUpdateCheckInProgress = false;
+ return;
+ }
+
+ s_slrUpdateCheckInProgress = false;
+
+ if (!info.error.isEmpty()) {
+ MOBase::log::warn("SLR update check failed: {}", info.error);
+ return;
+ }
+
+ if (!info.updateAvailable) {
+ MOBase::log::debug("Steam Linux Runtime is up to date ({})",
+ info.localBuildId);
+ return;
+ }
+
+ QSettings globalSettings = GlobalSettings::settings();
+ const QString skippedBuild =
+ globalSettings.value(QStringLiteral("SteamLinuxRuntime/skipped_build_id"))
+ .toString();
+ if (skippedBuild == info.remoteBuildId) {
+ MOBase::log::debug("Skipping SLR update prompt for ignored build {}",
+ info.remoteBuildId);
+ return;
+ }
+
+ QWidget* parent = nullptr;
+ if (self->m_UserInterface) {
+ parent = self->m_UserInterface->mainWindow();
+ }
+ promptSlrUpdate(parent, info);
+ }, Qt::QueuedConnection);
+ if (!invoked) {
+ s_slrUpdateCheckInProgress = false;
}
}).detach();
}
diff --git a/src/src/slrmanager.cpp b/src/src/slrmanager.cpp
index 9fa529e..6c863f4 100644
--- a/src/src/slrmanager.cpp
+++ b/src/src/slrmanager.cpp
@@ -3,9 +3,11 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
+#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QProcess>
+#include <QTemporaryDir>
#include <QTimer>
#include <QEventLoop>
#include <uibase/log.h>
@@ -44,7 +46,13 @@ QByteArray httpGet(const QString& url, const int* cancelFlag,
const QString& destFile = {})
{
QNetworkAccessManager mgr;
- QNetworkReply* reply = mgr.get(QNetworkRequest(QUrl(url)));
+ QNetworkRequest request{QUrl(url)};
+ request.setRawHeader("User-Agent", "Fluorine-Manager/slr");
+ request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
+ QNetworkRequest::NoLessSafeRedirectPolicy);
+ request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
+
+ QNetworkReply* reply = mgr.get(request);
QEventLoop loop;
QFile outFile;
@@ -91,6 +99,9 @@ QByteArray httpGet(const QString& url, const int* cancelFlag,
outFile.close();
if (reply->error() != QNetworkReply::NoError) {
+ MOBase::log::warn("SLR download request failed: {} ({})",
+ url,
+ reply->errorString());
reply->deleteLater();
if (!destFile.isEmpty())
QFile::remove(destFile);
@@ -101,6 +112,70 @@ QByteArray httpGet(const QString& url, const int* cancelFlag,
return inMemoryBuf;
}
+QString readLocalBuildId()
+{
+ QFile f(localBuildIdPath());
+ if (!f.open(QIODevice::ReadOnly)) {
+ return {};
+ }
+ return QString::fromUtf8(f.readAll()).trimmed();
+}
+
+bool writeLocalBuildId(const QString& buildId)
+{
+ QFile f(localBuildIdPath());
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ return false;
+ }
+ f.write(buildId.toUtf8());
+ f.write("\n");
+ return true;
+}
+
+bool makeExecutable(const QString& path)
+{
+ return QFile::setPermissions(path,
+ QFileDevice::ReadOwner | QFileDevice::WriteOwner |
+ QFileDevice::ExeOwner | QFileDevice::ReadGroup |
+ QFileDevice::ExeGroup | QFileDevice::ReadOther |
+ QFileDevice::ExeOther);
+}
+
+bool replaceRuntimeAtomically(const QString& stagedRuntimeDir, QString& err)
+{
+ const QString installDir = slrInstallDir();
+ const QString extractedDir = installDir + "/" + EXTRACTED_DIR;
+ const QString backupDir = installDir + "/" + EXTRACTED_DIR + ".previous";
+
+ if (!QFileInfo::exists(stagedRuntimeDir + "/run")) {
+ err = QStringLiteral("staged runtime is missing run script");
+ return false;
+ }
+ makeExecutable(stagedRuntimeDir + "/run");
+
+ QDir().mkpath(installDir);
+ QDir(backupDir).removeRecursively();
+
+ bool hadPrevious = QFileInfo::exists(extractedDir);
+ if (hadPrevious && !QDir().rename(extractedDir, backupDir)) {
+ err = QStringLiteral("failed to move existing runtime aside");
+ return false;
+ }
+
+ if (!QDir().rename(stagedRuntimeDir, extractedDir)) {
+ if (hadPrevious) {
+ QDir().rename(backupDir, extractedDir);
+ }
+ err = QStringLiteral("failed to install staged runtime");
+ return false;
+ }
+
+ if (hadPrevious) {
+ QDir(backupDir).removeRecursively();
+ }
+ return true;
+}
+
} // namespace
bool isSlrInstalled()
@@ -198,6 +273,24 @@ QString getSlrRunScript()
return isSlrInstalled() ? slrRunScriptPath() : QString();
}
+SlrUpdateInfo checkSlrUpdate(const int* cancelFlag)
+{
+ SlrUpdateInfo info;
+ info.installed = isSlrInstalled();
+ info.localBuildId = readLocalBuildId();
+
+ const QByteArray remoteBuildIdRaw = httpGet(
+ QStringLiteral("%1/BUILD_ID.txt").arg(QLatin1String(BASE_URL)), cancelFlag);
+ if (remoteBuildIdRaw.isEmpty()) {
+ info.error = QStringLiteral("Failed to fetch SLR BUILD_ID");
+ return info;
+ }
+
+ info.remoteBuildId = QString::fromUtf8(remoteBuildIdRaw).trimmed();
+ info.updateAvailable = info.installed && info.localBuildId != info.remoteBuildId;
+ return info;
+}
+
QString downloadSlr(const std::function<void(float)>& progressCb,
const std::function<void(const QString&)>& statusCb,
const int* cancelFlag)
@@ -208,20 +301,12 @@ QString downloadSlr(const std::function<void(float)>& progressCb,
// 1. Check for updates.
status(QStringLiteral("Checking Steam Linux Runtime version..."));
- const QByteArray remoteBuildIdRaw = httpGet(
- QStringLiteral("%1/BUILD_ID.txt").arg(QLatin1String(BASE_URL)), cancelFlag);
- if (remoteBuildIdRaw.isEmpty())
- return QStringLiteral("Failed to fetch SLR BUILD_ID");
-
- const QString remoteBuildId = QString::fromUtf8(remoteBuildIdRaw).trimmed();
+ const SlrUpdateInfo updateInfo = checkSlrUpdate(cancelFlag);
+ if (!updateInfo.error.isEmpty())
+ return updateInfo.error;
- // Read local BUILD_ID.
- QString localBuildId;
- {
- QFile f(localBuildIdPath());
- if (f.open(QIODevice::ReadOnly))
- localBuildId = QString::fromUtf8(f.readAll()).trimmed();
- }
+ const QString remoteBuildId = updateInfo.remoteBuildId;
+ const QString localBuildId = updateInfo.localBuildId;
if (localBuildId == remoteBuildId && isSlrInstalled()) {
MOBase::log::info("Steam Linux Runtime is already up to date");
@@ -241,7 +326,12 @@ QString downloadSlr(const std::function<void(float)>& progressCb,
const QString installDir = slrInstallDir();
QDir().mkpath(installDir);
- const QString archivePath = installDir + "/" + ARCHIVE_NAME;
+
+ QTemporaryDir stagingRoot(installDir + "/slr-update-XXXXXX");
+ if (!stagingRoot.isValid()) {
+ return QStringLiteral("Failed to create SLR staging directory");
+ }
+ const QString archivePath = stagingRoot.filePath(ARCHIVE_NAME);
// 2. Download.
status(QStringLiteral("Downloading Steam Linux Runtime (steamrt4, ~200 MB)..."));
@@ -254,12 +344,9 @@ QString downloadSlr(const std::function<void(float)>& progressCb,
// 3. Extract.
status(QStringLiteral("Extracting Steam Linux Runtime..."));
- const QString extractedDir = installDir + "/" + EXTRACTED_DIR;
- if (QFileInfo::exists(extractedDir))
- QDir(extractedDir).removeRecursively();
QProcess tar;
- tar.setWorkingDirectory(installDir);
+ tar.setWorkingDirectory(stagingRoot.path());
tar.start(QStringLiteral("tar"), {QStringLiteral("xJf"), archivePath});
tar.waitForFinished(600000);
QFile::remove(archivePath);
@@ -267,19 +354,23 @@ QString downloadSlr(const std::function<void(float)>& progressCb,
if (tar.exitStatus() != QProcess::NormalExit || tar.exitCode() != 0)
return QStringLiteral("tar extraction failed (exit code %1)").arg(tar.exitCode());
- if (!QFileInfo::exists(slrRunScriptPath()))
+ const QString stagedRuntimeDir = stagingRoot.filePath(EXTRACTED_DIR);
+ if (!QFileInfo::exists(stagedRuntimeDir + "/run"))
return QStringLiteral("Extraction succeeded but run script not found");
+ QString replaceError;
+ if (!replaceRuntimeAtomically(stagedRuntimeDir, replaceError)) {
+ return replaceError;
+ }
+
// 4. Inject xrandr into the container (steamrt4 ships without it, but
// Proton-GE and several protonfixes invoke xrandr during launch).
status(QStringLiteral("Injecting xrandr into runtime..."));
installXrandrAssets(cancelFlag, statusCb);
// 5. Save BUILD_ID.
- {
- QFile f(localBuildIdPath());
- if (f.open(QIODevice::WriteOnly))
- f.write(remoteBuildId.toUtf8());
+ if (!writeLocalBuildId(remoteBuildId)) {
+ MOBase::log::warn("Failed to write SLR BUILD_ID marker");
}
MOBase::log::info("Steam Linux Runtime installed successfully");
diff --git a/src/src/slrmanager.h b/src/src/slrmanager.h
index 6a842b9..149708a 100644
--- a/src/src/slrmanager.h
+++ b/src/src/slrmanager.h
@@ -4,12 +4,25 @@
#include <QString>
#include <functional>
+struct SlrUpdateInfo
+{
+ bool installed = false;
+ bool updateAvailable = false;
+ QString localBuildId;
+ QString remoteBuildId;
+ QString error;
+};
+
/// Returns true if the SLR `run` script is present and executable.
__attribute__((visibility("default"))) bool isSlrInstalled();
/// Returns the path to the SLR `run` script, or empty if not installed.
__attribute__((visibility("default"))) QString getSlrRunScript();
+/// Check the remote SLR BUILD_ID without downloading or replacing anything.
+__attribute__((visibility("default")))
+SlrUpdateInfo checkSlrUpdate(const int* cancelFlag = nullptr);
+
/// Download and install SteamLinuxRuntime_4 (steamrt4, ~200 MB).
/// Skips if already at the latest version (BUILD_ID check).
/// Returns empty string on success, or an error message.