diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index 46151481c..2b6ef9194 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -246,10 +246,12 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath) // Close the main window synchronously so file locks are released before the NSIS installer // (already launched) starts replacing files. This also flushes settings and shuts down the // tabs, but only when the close is actually accepted: MainWindow may veto it for a running - // card DB update, an open game, or an unsaved deck. In that case keep running so the user - // can resolve the blocker, and tell them the installer is already waiting. + // card DB update, an open game, or an unsaved deck, and closeForUpdate() also reports a + // close already in progress (reached from a nested event loop while a shutdown prompt is + // up). Only quit when the shutdown really ran - otherwise keep running so the user can + // resolve the blocker, and tell them the installer is already waiting. if (auto *window = qobject_cast(parent())) { - if (window->close()) { + if (window->closeForUpdate()) { QTimer::singleShot(0, qApp, &QCoreApplication::quit); } else { QMessageBox::warning(this, tr("Update"), diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 595d38d5b..86a54e21b 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -851,7 +851,6 @@ void MainWindow::actShow() void MainWindow::closeEvent(QCloseEvent *event) { // workaround Qt bug where closeEvent gets called twice - static bool bClosingDown = false; if (bClosingDown) { return; } @@ -880,6 +879,18 @@ void MainWindow::closeEvent(QCloseEvent *event) tabSupervisor->deleteLater(); } +bool MainWindow::closeForUpdate() +{ + // A shutdown is already being handled (e.g. this is reached from a nested event loop while + // closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return + // true without any shutdown having happened, so report faithfully instead: the caller must keep + // the process alive until the user resolves whatever is blocking the close. + if (bClosingDown) { + return false; + } + return close(); +} + void MainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 9e45a4c3e..7762f09b0 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -167,6 +167,7 @@ private: LagMonitor lagMonitor; ///< watches the main thread for event loop stalls LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph bool bHasActivated, askedForDbUpdater; + bool bClosingDown = false; ///< guards closeEvent() against re-entrancy bool skipStartupAutoConnect = false; bool startupAutoConnectAttempted = false; QProcess *cardUpdateProcess; @@ -204,6 +205,13 @@ public: return tabSupervisor; } + /** + * @brief Closes the window so an update installer can replace the running binaries. + * Returns true only if the shutdown actually ran (settings flushed, tabs shut down); + * false if the close was vetoed by the user or is already in progress. + */ + bool closeForUpdate(); + protected: void closeEvent(QCloseEvent *event) override; void changeEvent(QEvent *event) override;