aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-16 13:45:46 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-16 13:45:46 -0500
commitf39c7e06d2465258371412c4adac48ba72fddbb9 (patch)
tree5ad48d246e7f09b7a5fcec926c027dd986055bd6
parent62fd9437c7d750cde9c0ae91c4eb9ed4d69ecf35 (diff)
ui: recover from stale FUSE mounts without bothering the user
Two complementary changes against ENOTCONN — "Transport endpoint is not connected" — from a wedged FUSE mount left by a previous crashed or unclean session. MOApplication::notify already caught filesystem_error and called FuseConnector::tryCleanupStaleMount on the affected path, but it still went on to show the user a hard error dialog about the exception. If we've already cleared the wedged mount the workflow that triggered the iterator (refresh, restore backup, etc.) just needs to be retried — the dialog is noise on a state we just recovered from. Probe stat() after cleanup; if the path is no longer wedged, log + swallow. OrganizerCore::afterRun's post-game chmod loop used the throwing recursive_directory_iterator constructor with no error_code, so a single ENOTCONN under the game dir unwound out of afterRun and left the rest of the post-run sync (save/INI/plugin sync-back, load-order refresh, FinishedRun callback) un-run. Switch to error_code-based iteration; on ENOTCONN, call tryCleanupStaleMount on the game dir and continue cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--src/src/moapplication.cpp42
-rw-r--r--src/src/organizercore.cpp28
2 files changed, 57 insertions, 13 deletions
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp
index dda52e7..7216e7c 100644
--- a/src/src/moapplication.cpp
+++ b/src/src/moapplication.cpp
@@ -36,6 +36,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <cerrno>
#include <filesystem>
+#include <sys/stat.h>
#include "shared/appconfig.h"
#include "shared/util.h"
#include "thread_utils.h"
@@ -745,16 +746,39 @@ bool MOApplication::notify(QObject* receiver, QEvent* event)
receiver->objectName(), event->type(), fe.what());
// ENOTCONN = stale FUSE mount. Attempt recovery so MO2 can continue.
+ // If we manage to clear the wedged mount, suppress the user-facing
+ // dialog — the iteration that failed will be retried by whatever
+ // workflow triggered it (refresh, restore, etc.) and showing a hard
+ // error on a state we just recovered from is just noise.
if (fe.code().value() == ENOTCONN) {
- const auto& p1 = fe.path1();
- if (!p1.empty()) {
- log::warn("ENOTCONN on '{}' — attempting stale mount cleanup", p1.string());
- FuseConnector::tryCleanupStaleMount(QString::fromStdString(p1.string()));
- }
- const auto& p2 = fe.path2();
- if (!p2.empty()) {
- log::warn("ENOTCONN on '{}' — attempting stale mount cleanup", p2.string());
- FuseConnector::tryCleanupStaleMount(QString::fromStdString(p2.string()));
+ bool recovered = false;
+ auto attemptCleanup = [&recovered](const std::filesystem::path& p) {
+ if (p.empty()) {
+ return;
+ }
+ const QString qpath = QString::fromStdString(p.string());
+ log::warn("ENOTCONN on '{}' — attempting stale mount cleanup",
+ p.string());
+ FuseConnector::tryCleanupStaleMount(qpath);
+ // Probe the path: if stat() no longer returns ENOTCONN, we cleared
+ // it. Even if the path is now missing (ENOENT), that's recovery
+ // from MO2's perspective — the iterator can succeed (or skip).
+ struct stat st;
+ const bool stillWedged =
+ ::stat(qpath.toLocal8Bit().constData(), &st) != 0 &&
+ errno == ENOTCONN;
+ if (!stillWedged) {
+ recovered = true;
+ }
+ };
+ attemptCleanup(fe.path1());
+ attemptCleanup(fe.path2());
+
+ if (recovered) {
+ log::info(
+ "stale FUSE mount recovered; suppressing error dialog. "
+ "The triggering operation will need to be retried.");
+ return false;
}
}
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 0ce5d90..d42b1ac 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -2760,17 +2760,37 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode)
// Restore write permissions on the game directory. In rare cases
// (crashes, unclean Wine shutdown) file permissions can be changed to
// read-only, preventing subsequent launches from working.
+ //
+ // Use error_code-based iteration: a previous run may have left a stale
+ // FUSE mount under the game dir (zombie Wine process holding open
+ // handles), and the throwing iterator would unwind out of afterRun()
+ // leaving the rest of the post-run sync untouched.
{
const auto t0 = std::chrono::steady_clock::now();
const QString gameDir = managedGame()->gameDirectory().absolutePath();
namespace fs = std::filesystem;
- std::error_code ec;
+ std::error_code permsEc;
+ std::error_code iterEc;
for (auto it = fs::recursive_directory_iterator(
- gameDir.toStdString(), fs::directory_options::skip_permission_denied);
- it != fs::recursive_directory_iterator(); ++it) {
+ gameDir.toStdString(),
+ fs::directory_options::skip_permission_denied, iterEc);
+ !iterEc && it != fs::recursive_directory_iterator();
+ it.increment(iterEc)) {
fs::permissions(it->path(),
fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec,
- fs::perm_options::add, ec);
+ fs::perm_options::add, permsEc);
+ }
+ if (iterEc) {
+ if (iterEc.value() == ENOTCONN) {
+ log::warn(
+ "afterRun: stale FUSE mount encountered under '{}' "
+ "(errno={}); skipping remaining permission restoration",
+ gameDir.toStdString(), iterEc.value());
+ FuseConnector::tryCleanupStaleMount(gameDir);
+ } else {
+ log::warn("afterRun: directory iteration aborted at error: {}",
+ iterEc.message());
+ }
}
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();