aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <lukew19@proton.me>2026-06-24 22:37:19 -0500
committerGitHub <noreply@github.com>2026-06-24 22:37:19 -0500
commite4fefc7f186a77174c93d6894b5c9252785b98cc (patch)
tree1cfd7d2b23cfeb8b8792ea93621600013974527f /src
parent1be46fbb193010d6a5d1ea79167eca08a99e8d4c (diff)
parente490b76e6ca5c5bbc52429a2cf16b7fe91fffc09 (diff)
Merge pull request #108 from tristan-iu/openmw-support
OpenMW support
Diffstat (limited to 'src')
-rw-r--r--src/src/organizercore.cpp19
-rw-r--r--src/src/processrunner.cpp50
-rw-r--r--src/src/protonlauncher.cpp56
3 files changed, 98 insertions, 27 deletions
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 1ab4166..107a607 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -963,7 +963,14 @@ void OrganizerCore::prepareVFS()
m_USVFS.setRootBuilderEnabled(vfsRootBuilder, storageDir.toStdString());
}
- m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
+ // Games that manage their own VFS (e.g. OpenMW via openmw.cfg) opt out of the
+ // FUSE mount; Fluorine must not overlay Data Files for them.
+ if (managedGame() == nullptr || managedGame()->usesVFS()) {
+ m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
+ } else {
+ log::debug("prepareVFS: skipping FUSE mount; managed game manages its own "
+ "VFS (usesVFS=false)");
+ }
}
void OrganizerCore::unmountVFS()
@@ -2841,8 +2848,14 @@ bool OrganizerCore::beforeRun(
}
try {
- m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
- m_USVFS.updateForcedLibraries(forcedLibraries);
+ // OpenMW and other self-managed-VFS games skip the FUSE mount (usesVFS()).
+ if (managedGame() == nullptr || managedGame()->usesVFS()) {
+ m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
+ m_USVFS.updateForcedLibraries(forcedLibraries);
+ } else {
+ log::debug("beforeRun: skipping FUSE mount; managed game manages its own "
+ "VFS (usesVFS=false)");
+ }
} catch (const FuseConnectorException& e) {
log::error("VFS mount failed: {}", e.what());
return false;
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp
index 36108b8..01ed905 100644
--- a/src/src/processrunner.cpp
+++ b/src/src/processrunner.cpp
@@ -595,8 +595,16 @@ DWORD exitCodeFromWaitStatus(int status)
return 0;
}
+// killTreeOnUnlock: when the user force-unlocks/cancels the lock dialog, SIGKILL
+// the launched process tree (and the wineserver) so the wineprefix/FUSE VFS can
+// be torn down cleanly. This is correct for Proton games and for native games
+// that still rely on the FUSE VFS (e.g. Stardew Valley). It must be FALSE for
+// native, non-VFS games like OpenMW (usesVFS()==false): there is nothing to tear
+// down, and killing the tree would take down a launcher-spawned engine the user
+// is actively using. See OrganizerCore::managedGame()->usesVFS() at the caller.
ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
- UILocker::Session* ls, const QStringList& expected)
+ UILocker::Session* ls, const QStringList& expected,
+ bool killTreeOnUnlock)
{
if (pid <= 0) {
return ProcessRunner::Error;
@@ -777,6 +785,23 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
case UILocker::ForceUnlocked:
case UILocker::Cancelled: {
const bool cancelled = (UILocker::Session::result() == UILocker::Cancelled);
+
+ // The teardown below exists only to clean up the wineprefix and let the
+ // FUSE VFS unmount. A native, non-VFS game (OpenMW, usesVFS()==false)
+ // has neither, and its launcher spawns the real engine as a child the
+ // user is still playing — so force-unlock here must release the lock
+ // without killing the process tree.
+ if (!killTreeOnUnlock) {
+ log::debug("waiting for {} {} by user; non-VFS game, releasing lock "
+ "but leaving the process tree running",
+ displayPid, cancelled ? "cancelled" : "force unlocked");
+ if (exitCode != nullptr) {
+ *exitCode = 0;
+ }
+ return cancelled ? ProcessRunner::Cancelled
+ : ProcessRunner::ForceUnlocked;
+ }
+
log::debug("waiting for {} {} by user, terminating", displayPid,
cancelled ? "cancelled" : "force unlocked");
@@ -825,14 +850,17 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
UILocker::Session* ls,
- const QStringList& expected)
+ const QStringList& expected,
+ bool killTreeOnUnlock)
{
- return waitForPid(handleToPid(initialProcess), exitCode, ls, expected);
+ return waitForPid(handleToPid(initialProcess), exitCode, ls, expected,
+ killTreeOnUnlock);
}
ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProcesses,
UILocker::Session* ls,
- const QStringList& expected)
+ const QStringList& expected,
+ bool killTreeOnUnlock)
{
if (initialProcesses.empty()) {
return ProcessRunner::Completed;
@@ -840,7 +868,8 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces
for (HANDLE h : initialProcesses) {
DWORD ignored = 0;
- const auto r = waitForPid(handleToPid(h), &ignored, ls, expected);
+ const auto r = waitForPid(handleToPid(h), &ignored, ls, expected,
+ killTreeOnUnlock);
if (r != ProcessRunner::Completed) {
return r;
}
@@ -1324,9 +1353,18 @@ ProcessRunner::Results ProcessRunner::postRun()
}
}
+ // Only tear down the launched process tree on force-unlock for games that
+ // have a wineprefix/FUSE VFS to clean up. Native, non-VFS games (OpenMW)
+ // must keep running when the user releases the lock — same usesVFS() gate as
+ // the FUSE mount in OrganizerCore (default true = unchanged for every other
+ // game).
+ const auto* game = m_core.managedGame();
+ const bool killTreeOnUnlock = (game == nullptr) || game->usesVFS();
+
auto r = Error;
withLock([&](auto& ls) {
- r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables);
+ r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables,
+ killTreeOnUnlock);
});
if (shouldRefresh(r)) {
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp
index 0c876ec..f6059e3 100644
--- a/src/src/protonlauncher.cpp
+++ b/src/src/protonlauncher.cpp
@@ -457,7 +457,8 @@ void wrapInTerminal(QString& program, QStringList& arguments)
bool startWithEnv(const QString& program, const QStringList& arguments,
const QString& workingDir,
- const QProcessEnvironment& environment, qint64& pid)
+ const QProcessEnvironment& environment, qint64& pid,
+ bool forwardAllChannels = false)
{
auto* process = new QProcess();
process->setProgram(program);
@@ -468,23 +469,38 @@ bool startWithEnv(const QString& program, const QStringList& arguments,
}
process->setProcessEnvironment(environment);
- process->setProcessChannelMode(QProcess::ForwardedOutputChannel);
- // Filter noisy Wine/Proton stderr (GStreamer warnings, etc.) while
- // forwarding everything else to our stderr.
- QObject::connect(process, &QProcess::readyReadStandardError, process, [process]() {
- const QByteArray data = process->readAllStandardError();
- for (const QByteArray& line : data.split('\n')) {
- if (line.isEmpty())
- continue;
- if (line.contains("GStreamer-WARNING") || line.contains("Failed to load plugin") ||
- line.contains("ProtonFixes[") || line.contains("wineserver: NTSync") ||
- line.contains("[MANGOHUD]") || line.contains("radv is not a conformant"))
- continue;
- std::fwrite(line.constData(), 1, line.size(), stderr);
- std::fputc('\n', stderr);
- }
- });
+ if (forwardAllChannels) {
+ // Forward BOTH stdout and stderr straight to our own fds, with no
+ // QProcess-managed pipe. This is required for native launches that hand
+ // off to a longer-lived child: e.g. openmw-launcher spawns the OpenMW
+ // engine, the engine inherits the launcher's stderr, then the launcher
+ // exits. With a piped stderr (ForwardedOutputChannel below), the QProcess
+ // is destroyed on `finished` and closes the pipe's read end — the engine's
+ // next write to stderr would hit a broken pipe and die with SIGPIPE (no
+ // coredump). Forwarding to our real stderr fd (which lives as long as
+ // Fluorine does) makes the child immune. Native games don't emit the
+ // Proton/Wine stderr noise the filter below targets, so we lose nothing.
+ process->setProcessChannelMode(QProcess::ForwardedChannels);
+ } else {
+ process->setProcessChannelMode(QProcess::ForwardedOutputChannel);
+
+ // Filter noisy Wine/Proton stderr (GStreamer warnings, etc.) while
+ // forwarding everything else to our stderr.
+ QObject::connect(process, &QProcess::readyReadStandardError, process, [process]() {
+ const QByteArray data = process->readAllStandardError();
+ for (const QByteArray& line : data.split('\n')) {
+ if (line.isEmpty())
+ continue;
+ if (line.contains("GStreamer-WARNING") || line.contains("Failed to load plugin") ||
+ line.contains("ProtonFixes[") || line.contains("wineserver: NTSync") ||
+ line.contains("[MANGOHUD]") || line.contains("radv is not a conformant"))
+ continue;
+ std::fwrite(line.constData(), 1, line.size(), stderr);
+ std::fputc('\n', stderr);
+ }
+ });
+ }
QObject::connect(process,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
@@ -1022,7 +1038,11 @@ bool ProtonLauncher::launchDirect(qint64& pid) const
wrapInTerminal(program, arguments);
}
- return startWithEnv(program, arguments, m_workingDir, env, pid);
+ // Native direct launch: forward both channels so a launcher-spawned engine
+ // (openmw-launcher -> openmw) can't be SIGPIPE'd when the launcher exits and
+ // the QProcess closes its stderr pipe. See startWithEnv for the full rationale.
+ return startWithEnv(program, arguments, m_workingDir, env, pid,
+ /*forwardAllChannels=*/true);
}
bool ProtonLauncher::ensureSteamRunning()