diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-01 12:57:45 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-01 12:57:45 -0600 |
| commit | eaabd444c0f54b58e3179c6312b867d1e63e54c1 (patch) | |
| tree | d61a43cf319819ed8c6f424c28a69a482d31af7d /src | |
| parent | 94596962a17f8989bfa20bbf98d1b5707fd773d2 (diff) | |
Fix wineserver tracking matching unrelated wine prefixes
findWineserver() was scanning /proc for ANY wineserver owned by the
current user. If the user had other wine processes running (e.g. winecfg
under ~/.wine), Fluorine would latch onto that unrelated wineserver and
never declare the game finished.
Now reads WINEPREFIX from the launched process's /proc/<pid>/environ and
passes it to findWineserver(), which filters candidates by matching
their WINEPREFIX against the game's prefix path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/processrunner.cpp | 68 |
1 files changed, 57 insertions, 11 deletions
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index 9d444f1..724886b 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -515,11 +515,33 @@ 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() {
+// Read a specific environment variable from /proc/<pid>/environ.
+// The environ file is NUL-separated KEY=VALUE pairs.
+QString readProcEnvVar(pid_t pid, const char *varName) {
+ QFile f(QString("/proc/%1/environ").arg(pid));
+ if (!f.open(QIODevice::ReadOnly)) {
+ return {};
+ }
+
+ const QByteArray data = f.readAll();
+ const QByteArray prefix = QByteArray(varName) + '=';
+ for (const QByteArray &entry : data.split('\0')) {
+ if (entry.startsWith(prefix)) {
+ return QString::fromUtf8(entry.mid(prefix.size()));
+ }
+ }
+ return {};
+}
+
+// Find a wineserver process owned by the current user that belongs to the
+// given WINEPREFIX. When expectedPrefix is empty, returns the first
+// wineserver owned by us (legacy behaviour).
+//
+// 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 QString &expectedPrefix = {}) {
const uid_t myUid = ::getuid();
DIR *proc = opendir("/proc");
if (!proc) return 0;
@@ -537,11 +559,26 @@ pid_t findWineserver() { // 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;
+ if (::stat(QString("/proc/%1").arg(pid).toStdString().c_str(), &st) != 0 ||
+ st.st_uid != myUid) {
+ continue;
+ }
+
+ // If a prefix filter was given, verify this wineserver belongs to it.
+ // A wineserver without WINEPREFIX in its environ is using the default
+ // ~/.wine prefix, which is never the game prefix — skip it too.
+ if (!expectedPrefix.isEmpty()) {
+ const QString wsPrefix = readProcEnvVar(pid, "WINEPREFIX");
+ if (wsPrefix.isEmpty() ||
+ QDir(wsPrefix).canonicalPath() != QDir(expectedPrefix).canonicalPath()) {
+ log::debug("skipping wineserver {} (prefix '{}' != expected '{}')",
+ pid, wsPrefix.toStdString(), expectedPrefix.toStdString());
+ continue;
+ }
}
+
+ result = pid;
+ break;
}
closedir(proc);
return result;
@@ -730,6 +767,15 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, return ProcessRunner::Error;
}
+ // Capture the WINEPREFIX from the launched process so we can filter
+ // wineserver lookups to the correct prefix. Without this, Fluorine
+ // would track ANY wineserver owned by the user (e.g. one running
+ // winecfg under ~/.wine while the game uses a different prefix).
+ const QString winePrefix = readProcEnvVar(pid, "WINEPREFIX");
+ if (!winePrefix.isEmpty()) {
+ log::debug("process {} has WINEPREFIX='{}'", pid, winePrefix.toStdString());
+ }
+
// startDetached() creates a non-child process, so waitpid() will fail with
// ECHILD. Detect this on the first call and switch to kill(pid, 0) polling
// which works for any process owned by the same user.
@@ -781,7 +827,7 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, displayPid = lastTrackedPid;
displayName = readProcComm(lastTrackedPid);
} else {
- const pid_t ws = findWineserver();
+ const pid_t ws = findWineserver(winePrefix);
if (ws > 0) {
log::info("tracked process exited, waiting for wineserver {}", ws);
lastTrackedPid = ws;
@@ -817,7 +863,7 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, // 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();
+ const pid_t ws = findWineserver(winePrefix);
if (ws > 0 && ws != pollPid) {
log::info("polled process {} exited, falling back to wineserver {}", pollPid, ws);
lastTrackedPid = ws;
|
