diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 13:45:46 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 13:45:46 -0500 |
| commit | f39c7e06d2465258371412c4adac48ba72fddbb9 (patch) | |
| tree | 5ad48d246e7f09b7a5fcec926c027dd986055bd6 | |
| parent | 62fd9437c7d750cde9c0ae91c4eb9ed4d69ecf35 (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.cpp | 42 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 28 |
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(); |
