From 8b4f9596d3e2b437317418b0af1aad68db056ee2 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 13 May 2026 21:32:28 -0500 Subject: multiprocess: claim primary listener when stale shm can't be reclaimed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walked into this when a download manager click silently popped "An instance of Mod Organizer is already running" with no MO2 actually visible. State on disk: SysV shm segment from a prior MO2 still there with nattch=0 (kernel won't reap until IPC_RMID, which only the dead creator could call) plus the orphaned /tmp/mo- socket file. The stale-recovery path was attaching, finding no listener via primaryAlive(), then trying to detach+create — which kept failing because the corpse segment still occupied the key — and falling all the way through to "re-attach as secondary". Result: the new launch flagged itself ephemeral, forwardToPrimary() had nothing to send, and the user got the ghost dialog. The shm is only advisory; the unix socket listener is the real lock. If primaryAlive() said no one's listening, we should run as primary even when we can't reclaim the orphaned segment. Also re-probe on listen() failure so a real race (another process becoming primary between our probe and our listen) demotes us cleanly instead of leaving us as a broken primary. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/src/multiprocess.cpp | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/src/multiprocess.cpp b/src/src/multiprocess.cpp index 3ac4c72..688e745 100644 --- a/src/src/multiprocess.cpp +++ b/src/src/multiprocess.cpp @@ -51,27 +51,17 @@ MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject* parent) m_OwnsSM = true; error = QSharedMemory::NoError; } else { - // Another attached client keeps refcount > 0; on Linux Qt only - // frees the segment when the last attacher detaches. Re-attach - // as a secondary so the constructor returns in a usable state - // and sendMessage() can still try to deliver the request via - // the unix socket — it would otherwise leave both ownership - // flags clear and silently no-op every later call. - const QString reclaimError = m_SharedMem.errorString(); - if (m_SharedMem.attach()) { - MOBase::log::warn( - "could not reclaim stale shared memory ({}), " - "running as secondary", - reclaimError); - m_Ephemeral = true; - error = QSharedMemory::NoError; - } else { - MOBase::log::warn( - "could not reclaim or re-attach stale shared memory " - "(reclaim: {}, reattach: {})", - reclaimError, m_SharedMem.errorString()); - error = m_SharedMem.error(); - } + // SHM reclaim failed — the SysV segment is orphaned because the + // previous primary died without IPC_RMID, and only the creator + // can remove it. The unix socket is the real liveness signal + // and we already proved no listener exists, so claim primary + // anyway. The orphan segment is harmless once we own the socket. + MOBase::log::warn( + "stale shared memory could not be reclaimed ({}), " + "claiming primary via socket listener", + m_SharedMem.errorString()); + m_OwnsSM = true; + error = QSharedMemory::NoError; } } } else { @@ -117,6 +107,15 @@ MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject* parent) if (!m_Server.listen(s_Key)) { MOBase::log::warn("QLocalServer listen failed: {}", m_Server.errorString().toStdString()); + // Another process may have started a listener between our probe and + // listen() (race during concurrent startup). Re-probe and demote + // ourselves to ephemeral if a real primary is now alive. + if (primaryAlive()) { + MOBase::log::info( + "another primary started concurrently, running as ephemeral"); + m_OwnsSM = false; + m_Ephemeral = true; + } } } } -- cgit v1.3.1