diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/downloadlistview.cpp | 28 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 78 | ||||
| -rw-r--r-- | src/src/organizercore.h | 4 | ||||
| -rw-r--r-- | src/src/processrunner.cpp | 9 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 60 | ||||
| -rw-r--r-- | src/src/protonlauncher.h | 14 | ||||
| -rw-r--r-- | src/src/spawn.cpp | 4 | ||||
| -rw-r--r-- | src/src/spawn.h | 7 |
8 files changed, 178 insertions, 26 deletions
diff --git a/src/src/downloadlistview.cpp b/src/src/downloadlistview.cpp index 0920980..cfb9468 100644 --- a/src/src/downloadlistview.cpp +++ b/src/src/downloadlistview.cpp @@ -415,20 +415,24 @@ void DownloadListView::issueDeleteSelected() auto* proxy = qobject_cast<QSortFilterProxyModel*>(model());
if (!proxy) return;
- QList<int> sourceRows;
+ // Capture filenames (not indices): removeDownload() runs refreshList()
+ // internally, which rebuilds m_ActiveDownloads from disk enumeration and
+ // can reorder entries, so stale indices from before the first delete may
+ // point at the wrong file or be out of range.
+ QStringList fileNames;
for (const auto& idx : selectionModel()->selectedRows()) {
int row = proxy->mapToSource(idx).row();
auto state = m_Manager->getState(row);
if (state >= DownloadManager::STATE_READY) {
- sourceRows.append(row);
+ fileNames.append(m_Manager->getFileName(row));
}
}
- if (sourceRows.isEmpty()) return;
+ if (fileNames.isEmpty()) return;
const auto r = MOBase::TaskDialog(this, tr("Delete downloads"))
.main(tr("Are you sure you want to delete %1 download(s)?")
- .arg(sourceRows.size()))
+ .arg(fileNames.size()))
.icon(QMessageBox::Question)
.button({tr("Move to the Recycle Bin"), QMessageBox::Yes})
.button({tr("Cancel"), QMessageBox::Cancel})
@@ -436,10 +440,18 @@ void DownloadListView::issueDeleteSelected() if (r != QMessageBox::Yes) return;
- // Delete in reverse order so indices remain valid
- std::sort(sourceRows.begin(), sourceRows.end(), std::greater<int>());
- for (int row : sourceRows) {
- emit removeDownload(row, true);
+ for (const QString& name : fileNames) {
+ const int total = m_Manager->numTotalDownloads();
+ int row = -1;
+ for (int i = 0; i < total; ++i) {
+ if (m_Manager->getFileName(i) == name) {
+ row = i;
+ break;
+ }
+ }
+ if (row >= 0) {
+ emit removeDownload(row, true);
+ }
}
}
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index d236abe..c1cb9aa 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -40,6 +40,7 @@ #include <uibase/filesystemutilities.h>
#include <uibase/utility.h>
#include "fluorineconfig.h"
+#include "protonlauncher.h"
#include "wineprefix.h"
#include <chrono>
@@ -2416,9 +2417,12 @@ bool OrganizerCore::checkGameRegistryKey() bool OrganizerCore::beforeRun(
const QFileInfo& binary, const QDir& cwd, const QString& arguments,
const QString& profileName, const QString& customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries)
+ const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries,
+ QString* saveBindMountSource, QString* saveBindMountTarget)
{
saveCurrentProfile();
+ if (saveBindMountSource) saveBindMountSource->clear();
+ if (saveBindMountTarget) saveBindMountTarget->clear();
// need to wait until directory structure is ready
if (m_DirectoryUpdate) {
@@ -2620,14 +2624,43 @@ bool OrganizerCore::beforeRun( if (m_CurrentProfile->localSavesEnabled()) {
const QString profileSavesDir =
QDir(m_CurrentProfile->absolutePath()).filePath("saves");
- log::info("Save deploy target: '{}' -> '{}'", profileSavesDir,
- absoluteSaveDir);
- if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) {
- log::warn("Failed to deploy profile saves from '{}' to prefix '{}'",
- profileSavesDir, prefixPathStr);
+
+ const bool useBindMount =
+ saveBindMountSource && saveBindMountTarget &&
+ ProtonLauncher::unprivilegedBindMountSupported();
+
+ if (useBindMount) {
+ // Clean up any stale symlink from previous symlink-based runs:
+ // mount --bind won't traverse a symlink as the target.
+ if (QFileInfo(absoluteSaveDir).isSymLink()) {
+ QFile::remove(absoluteSaveDir);
+ }
+ const QFileInfo leafInfo(absoluteSaveDir);
+ const QString lowerSaveDir =
+ QDir(leafInfo.dir().absolutePath())
+ .filePath(leafInfo.fileName().toLower());
+ if (lowerSaveDir != absoluteSaveDir &&
+ QFileInfo(lowerSaveDir).isSymLink()) {
+ QFile::remove(lowerSaveDir);
+ }
+
+ // Both endpoints must exist before the kernel can bind them.
+ QDir().mkpath(profileSavesDir);
+ QDir().mkpath(absoluteSaveDir);
+
+ *saveBindMountSource = profileSavesDir;
+ *saveBindMountTarget = absoluteSaveDir;
+ log::info("Save bind mount: '{}' -> '{}'", profileSavesDir,
+ absoluteSaveDir);
} else {
- log::debug("Deployed profile saves '{}' -> '{}' in prefix '{}'",
- profileSavesDir, absoluteSaveDir, prefixPathStr);
+ log::info("Save deploy target: '{}' -> '{}'", profileSavesDir,
+ absoluteSaveDir);
+ if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir,
+ true)) {
+ log::warn("Failed to deploy profile saves from '{}' to "
+ "prefix '{}'",
+ profileSavesDir, prefixPathStr);
+ }
}
// Ensure the prefix INI points to __MO_Saves so the game reads
@@ -2717,16 +2750,27 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) if (m_CurrentProfile->localSavesEnabled()) {
const QString profileSavesDir =
QDir(m_CurrentProfile->absolutePath()).filePath("saves");
- log::info("Save sync target: '{}' <- '{}'", profileSavesDir,
- absoluteSaveDir);
- if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) {
- log::warn("Failed to sync saves back from prefix '{}' to '{}'",
- prefixPathStr, profileSavesDir);
- }
- // Remove the __MO_Saves symlinks and revert the prefix INI so a
- // vanilla launch outside MO2 uses the default Saves dir.
- prefix.undeployProfileSaves(absoluteSaveDir);
+ // Bind-mount path: writes already landed live in the profile via
+ // the per-launch mount namespace, which teardown automatically
+ // when the game exited. Nothing to sync and nothing to undeploy.
+ // The symlink path below only runs on kernels without
+ // unprivileged user namespaces.
+ const bool usedBindMount =
+ !QFileInfo(absoluteSaveDir).isSymLink() &&
+ ProtonLauncher::unprivilegedBindMountSupported();
+ if (!usedBindMount) {
+ log::info("Save sync target: '{}' <- '{}'", profileSavesDir,
+ absoluteSaveDir);
+ if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) {
+ log::warn("Failed to sync saves back from prefix '{}' to '{}'",
+ prefixPathStr, profileSavesDir);
+ }
+
+ // Remove the __MO_Saves symlinks and revert the prefix INI so a
+ // vanilla launch outside MO2 uses the default Saves dir.
+ prefix.undeployProfileSaves(absoluteSaveDir);
+ }
const QStringList iniFiles = managedGame()->iniFiles();
if (!iniFiles.isEmpty()) {
const QString prefixIni =
diff --git a/src/src/organizercore.h b/src/src/organizercore.h index 834b8a7..dcf07c7 100644 --- a/src/src/organizercore.h +++ b/src/src/organizercore.h @@ -310,7 +310,9 @@ public: bool beforeRun(const QFileInfo& binary, const QDir& cwd, const QString& arguments,
const QString& profileName, const QString& customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries);
+ const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries,
+ QString* saveBindMountSource = nullptr,
+ QString* saveBindMountTarget = nullptr);
#ifndef _WIN32
bool checkGameRegistryKey();
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index 1a50849..4457cdb 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -1356,10 +1356,19 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() { // saves profile, sets up usvfs, notifies plugins, etc.; can return false if
// a plugin doesn't want the program to run (such as when checkFNIS fails to
// run FNIS and the user clicks cancel)
+#ifdef _WIN32
if (!m_core.beforeRun(m_sp.binary, m_sp.currentDirectory, m_sp.arguments,
m_profileName, m_customOverwrite, m_forcedLibraries)) {
return Error;
}
+#else
+ if (!m_core.beforeRun(m_sp.binary, m_sp.currentDirectory, m_sp.arguments,
+ m_profileName, m_customOverwrite, m_forcedLibraries,
+ &m_sp.saveBindMountSource,
+ &m_sp.saveBindMountTarget)) {
+ return Error;
+ }
+#endif
// parent widget used for any dialog popped up while checking for things
QWidget *parent = (m_ui ? m_ui->mainWindow() : nullptr);
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index c1b6b64..cd96afa 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -417,6 +417,37 @@ ProtonLauncher& ProtonLauncher::setUseTerminal(bool useTerminal) return *this; } +ProtonLauncher& ProtonLauncher::setSavesBindMount(const QString& source, + const QString& target) +{ + m_bindMountSource = source.trimmed(); + m_bindMountTarget = target.trimmed(); + return *this; +} + +bool ProtonLauncher::unprivilegedBindMountSupported() +{ + // Need both an `unshare` binary and a kernel that lets unprivileged users + // enter a user namespace with CAP_SYS_ADMIN (which is what makes + // `mount --bind` work without root). Debian stable + some hardened + // kernels disable this via a sysctl; treat missing/zero as "no". + if (QStandardPaths::findExecutable("unshare").isEmpty()) { + return false; + } + + QFile f(QStringLiteral("/proc/sys/kernel/unprivileged_userns_clone")); + if (f.open(QIODevice::ReadOnly)) { + const QByteArray v = f.readAll().trimmed(); + // File exists and explicitly says "0" -> disabled. Any other value + // (including "1") or an unreadable/missing file -> assume enabled, which + // is the modern-kernel default. + if (v == "0") { + return false; + } + } + return true; +} + std::pair<bool, qint64> ProtonLauncher::launch() const { qint64 pid = -1; @@ -606,6 +637,35 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const env.insert(it.key(), it.value()); } + // If a saves bind mount was requested (and the kernel supports + // unprivileged user namespaces), wrap the whole invocation in + // `unshare --user --mount -r` so the mount lives only inside the game's + // process tree and is torn down automatically on exit. Must happen + // AFTER wrapProgram but BEFORE startWithEnv so that SLR/pressure-vessel + // inherits the mount from the outer namespace. + if (!m_bindMountSource.isEmpty() && !m_bindMountTarget.isEmpty() && + unprivilegedBindMountSupported()) { + const QString unshareBin = QStandardPaths::findExecutable("unshare"); + QStringList newArgs; + newArgs << "--user" << "--mount" << "-r" << "--" + << "/bin/sh" << "-c" + << "mount --bind \"$1\" \"$2\" && shift 2 && exec \"$@\"" + << "_mo2bind" + << m_bindMountSource + << m_bindMountTarget + << program; + newArgs.append(arguments); + program = unshareBin; + arguments = newArgs; + MOBase::log::info("Saves bind mount: '{}' -> '{}'", m_bindMountSource, + m_bindMountTarget); + } else if (!m_bindMountSource.isEmpty()) { + MOBase::log::warn("Saves bind mount requested but unprivileged user " + "namespaces unavailable; game will write to prefix " + "'{}' directly", + m_bindMountTarget); + } + MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary); MOBase::log::info("Final command: '{}' {}", program, arguments.join(" ").toStdString()); diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 13be35a..c9b5de8 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -26,6 +26,18 @@ public: ProtonLauncher& addEnvVar(const QString& key, const QString& value); ProtonLauncher& setUseTerminal(bool useTerminal); + // Bind-mount `source` over `target` inside a per-launch user+mount + // namespace so the game's view of `target` redirects to `source`. Used to + // make `<prefix>/__MO_Saves` resolve to the profile's saves dir without + // symlinks (which Wine can accidentally replace with a real directory). + // Both paths must already exist before launch; the mount is torn down + // automatically when the game process tree exits. + ProtonLauncher& setSavesBindMount(const QString& source, const QString& target); + + // True iff the running kernel supports unprivileged user namespaces with + // CAP_SYS_ADMIN so that `setSavesBindMount` will actually take effect. + static bool unprivilegedBindMountSupported(); + // Launch dispatch: Proton -> Direct std::pair<bool, qint64> launch() const; @@ -47,6 +59,8 @@ private: QMap<QString, QString> m_envVars; QMap<QString, QString> m_wrapperEnvVars; bool m_useTerminal = false; + QString m_bindMountSource; + QString m_bindMountTarget; }; #endif // PROTONLAUNCHER_H diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 76c1787..59bb7d6 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -686,6 +686,10 @@ int spawn(const SpawnParameters &sp, pid_t &processId) { if (!wrapper.isEmpty()) {
launcher.setWrapper(wrapper);
}
+
+ if (!sp.saveBindMountSource.isEmpty() && !sp.saveBindMountTarget.isEmpty()) {
+ launcher.setSavesBindMount(sp.saveBindMountSource, sp.saveBindMountTarget);
+ }
} else {
MOBase::log::info("Proton disabled for this executable, launching directly");
}
diff --git a/src/src/spawn.h b/src/src/spawn.h index 378c20f..da124b3 100644 --- a/src/src/spawn.h +++ b/src/src/spawn.h @@ -65,6 +65,13 @@ struct SpawnParameters #else
int stdOut = -1;
int stdErr = -1;
+ // When both are set and unprivileged user namespaces are available,
+ // spawn() wraps the launch so `saveBindMountTarget` becomes a live view
+ // of `saveBindMountSource` for the duration of the game process tree.
+ // Used to redirect `<prefix>/__MO_Saves` to the profile's saves dir
+ // without symlinks, which Wine can accidentally replace.
+ QString saveBindMountSource;
+ QString saveBindMountTarget;
#endif
};
|
