diff options
| author | SulfurNitride <lukew19@proton.me> | 2026-06-24 22:37:19 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-24 22:37:19 -0500 |
| commit | e4fefc7f186a77174c93d6894b5c9252785b98cc (patch) | |
| tree | 1cfd7d2b23cfeb8b8792ea93621600013974527f /src | |
| parent | 1be46fbb193010d6a5d1ea79167eca08a99e8d4c (diff) | |
| parent | e490b76e6ca5c5bbc52429a2cf16b7fe91fffc09 (diff) | |
Merge pull request #108 from tristan-iu/openmw-support
OpenMW support
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/organizercore.cpp | 19 | ||||
| -rw-r--r-- | src/src/processrunner.cpp | 50 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 56 |
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() |
