aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-27 14:58:24 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-27 14:58:24 -0600
commit7d633783c3f5bde724c14e93b6bb1aa261c80737 (patch)
tree762854fabd4a05169a9fcefafaa4209a48438ed3 /src
parent11404dc2bbfb9033344ff360e72b66372edd0d9b (diff)
Fix process tracking for Wine/Proton game processes
Process tracking failed with Proton-GE because launcher executables (nvse_loader.exe, skse_loader.exe) exit after spawning the actual game (FalloutNV.exe, SkyrimSE.exe). The tracker saw the launcher exit and declared the game done, unmounting VFS while the game was still running. Fix: when the tracked process exits, fall back to waiting for wineserver which stays alive as long as ANY wine process in the prefix is running. This is the most robust way to detect when a game has truly exited, regardless of how many launcher layers are involved. Also: - Use "waitforexitandrun" proton verb instead of "run" (matches umu) - Reduce process tracking log spam (log once on first find, not every 50ms poll cycle) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/processrunner.cpp77
-rw-r--r--src/src/protonlauncher.cpp6
2 files changed, 72 insertions, 11 deletions
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp
index 2446f86..9d444f1 100644
--- a/src/src/processrunner.cpp
+++ b/src/src/processrunner.cpp
@@ -22,6 +22,7 @@
#include <dirent.h>
#include <fstream>
#include <signal.h>
+#include <sys/stat.h>
#include <sys/wait.h>
#include <unordered_map>
#include <unordered_set>
@@ -514,6 +515,38 @@ QStringList readProcCmdline(pid_t pid) {
return parts;
}
+// Find a wineserver process owned by the current user. Wineserver stays
+// alive as long as any Wine process in the prefix is running, making it
+// the most reliable way to detect when a game has truly exited — even when
+// launcher .exe's (nvse_loader, skse_loader, etc.) exit before the actual game.
+pid_t findWineserver() {
+ const uid_t myUid = ::getuid();
+ DIR *proc = opendir("/proc");
+ if (!proc) return 0;
+
+ pid_t result = 0;
+ struct dirent *entry = nullptr;
+ while ((entry = readdir(proc)) != nullptr) {
+ if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) continue;
+ const char *name = entry->d_name;
+ if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) continue;
+
+ const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10));
+ const QString comm = readProcComm(pid);
+ if (comm != "wineserver") continue;
+
+ // Verify it's owned by us.
+ struct stat st;
+ if (::stat(QString("/proc/%1").arg(pid).toStdString().c_str(), &st) == 0 &&
+ st.st_uid == myUid) {
+ result = pid;
+ break;
+ }
+ }
+ closedir(proc);
+ return result;
+}
+
// Check whether any of the expected executable names appear in a process's
// comm or cmdline. Wine processes often show "wine64-preload" or "start.exe"
// in /proc/comm while the actual game executable only appears in cmdline.
@@ -673,12 +706,8 @@ pid_t findTrackedProcess(pid_t rootPid, const QStringList &expected,
if (best > 0 && trackedNameOut) {
*trackedNameOut = bestName;
- log::debug("findTrackedProcess: found best={}, name={}", best,
- bestName.toStdString());
- } else {
- log::debug("findTrackedProcess: no tracked process found for rootPid={}",
- rootPid);
}
+ // Logging moved to caller to avoid spamming every 50ms poll cycle.
return best;
}
@@ -731,24 +760,41 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
QString displayName = readProcComm(pid);
const pid_t tracked = findTrackedProcess(pid, expected, &trackedName);
if (tracked > 0) {
+ if (!seenTrackedProcess || tracked != lastTrackedPid) {
+ log::info("tracking game process {}: {}", tracked, trackedName.toStdString());
+ }
seenTrackedProcess = true;
lastTrackedPid = tracked;
displayPid = tracked;
displayName = trackedName;
} else if (seenTrackedProcess) {
// The tracked process is no longer a descendant of the root PID.
- // This can happen when the root (proton) exits and wine/game processes
- // get reparented to PID 1. Before declaring the game exited, check
- // if the last tracked PID is still alive.
+ // This can happen when:
+ // a) The root (proton) exits and wine/game processes get reparented
+ // b) A launcher .exe (nvse_loader, skse_loader) exits after spawning
+ // the actual game (FalloutNV.exe, SkyrimSE.exe)
+ //
+ // Before declaring the game exited, check if the last tracked PID is
+ // still alive. If not, fall back to waiting for wineserver — it stays
+ // alive as long as ANY wine process in the prefix is running.
if (lastTrackedPid > 0 && ::kill(lastTrackedPid, 0) == 0) {
displayPid = lastTrackedPid;
displayName = readProcComm(lastTrackedPid);
- log::debug("tracked process {} reparented but still alive", lastTrackedPid);
} else {
+ const pid_t ws = findWineserver();
+ if (ws > 0) {
+ log::info("tracked process exited, waiting for wineserver {}", ws);
+ lastTrackedPid = ws;
+ useKillPoll = true;
+ displayPid = ws;
+ displayName = QStringLiteral("wineserver");
+ continue;
+ }
if (exitCode != nullptr) {
*exitCode = 0;
}
- log::debug("tracked child process {} for root {} exited", lastTrackedPid, pid);
+ log::debug("tracked child process {} for root {} exited (no wineserver)",
+ lastTrackedPid, pid);
return ProcessRunner::Completed;
}
}
@@ -767,6 +813,17 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
: pid;
if (::kill(pollPid, 0) != 0) {
if (errno == ESRCH) {
+ // The polled process exited. If it was the game (not wineserver),
+ // try falling back to wineserver — the real game may still be running
+ // (e.g. nvse_loader exits after spawning FalloutNV.exe).
+ if (seenTrackedProcess) {
+ const pid_t ws = findWineserver();
+ if (ws > 0 && ws != pollPid) {
+ log::info("polled process {} exited, falling back to wineserver {}", pollPid, ws);
+ lastTrackedPid = ws;
+ continue;
+ }
+ }
if (exitCode != nullptr) {
*exitCode = 0;
}
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp
index dbe9e20..d845b6f 100644
--- a/src/src/protonlauncher.cpp
+++ b/src/src/protonlauncher.cpp
@@ -349,7 +349,11 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
protonScript = QDir(m_protonPath).filePath("proton");
}
- const QStringList protonArgs = QStringList() << "run" << m_binary << m_arguments;
+ // Use "waitforexitandrun" instead of "run": this tells Proton to wait for
+ // any existing wineserver to shut down first, then launch the game. This is
+ // what umu-launcher uses and ensures the previous session is fully cleaned up
+ // before starting a new one.
+ const QStringList protonArgs = QStringList() << "waitforexitandrun" << m_binary << m_arguments;
QString program;
QStringList arguments;