Skip to content

prevent close event while non-cancelable async tasks are running - #525

Merged
nschimme merged 1 commit into
MUME:masterfrom
nschimme:fix-save
Apr 20, 2026
Merged

prevent close event while non-cancelable async tasks are running#525
nschimme merged 1 commit into
MUME:masterfrom
nschimme:fix-save

Conversation

@nschimme

@nschimme nschimme commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Before this change, the game could close before the async saver had a chance to finish.

This commit:

  • Adds a 'cancelDisposition' to AsyncBase to define if a task is interruptible.
  • Updates MainWindow::closeEvent to ignore the close request if an active async task is marked as non-cancelable.
  • Ensures settings are written only after the close event is accepted.
  • Prevents redundant save prompts when a save task is already in progress.

Summary by Sourcery

Guard window closure while non-cancelable async tasks are running and improve async task cancellation handling during shutdown.

New Features:

  • Introduce a cancel disposition flag on async tasks to distinguish cancelable from non-cancelable operations.

Bug Fixes:

  • Prevent the application from closing while a non-cancelable async task, such as an async save, is still running.
  • Avoid prompting the user to save when a save operation is already in progress and cannot be canceled.

Enhancements:

  • Delay writing window settings until after the close event is fully accepted.
  • Route async task cancellation and progress dialog dismissal through safer helper methods that respect task cancelability.

@sourcery-ai

sourcery-ai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements non-cancelable async tasks and updates window-close behavior so the app will not close (or redundantly prompt) while a non-interruptible async operation, such as saving, is in progress, and only writes settings once the close has been accepted.

Sequence diagram for window close with non-cancelable async task

sequenceDiagram
    actor User
    participant MainWindow
    participant AsyncTask
    participant AsyncBase
    participant ProgressDialog

    User->>MainWindow: closeEvent(event)
    Note over MainWindow: First async check
    MainWindow->>AsyncTask: is_allowed_to_cancel()
    AsyncTask->>AsyncBase: is_allowed_to_cancel()
    AsyncBase-->>AsyncTask: cancelDisposition == Allow ? true : false
    AsyncTask-->>MainWindow: bool

    alt async task non_cancelable
        MainWindow-->>User: event->ignore() (window stays open)
    else async task cancelable or no task
        MainWindow->>MainWindow: maybeSave()
        alt maybeSave returns false
            MainWindow-->>User: event->ignore()
        else maybeSave returns true
            Note over MainWindow: Second async check
            MainWindow->>AsyncTask: is_allowed_to_cancel()
            AsyncTask->>AsyncBase: is_allowed_to_cancel()
            AsyncBase-->>AsyncTask: bool
            AsyncTask-->>MainWindow: bool
            alt now non_cancelable (e.g. save just scheduled)
                MainWindow-->>User: event->ignore()
            else cancelable
                MainWindow->>AsyncTask: request_cancel()
                AsyncTask->>AsyncBase: request_cancel()
                AsyncBase->>AsyncBase: is_allowed_to_cancel()
                AsyncBase->>ProgressCounter: requestCancel()

                MainWindow->>ProgressDialog: reject()
                MainWindow->>MainWindow: writeSettings()
                MainWindow-->>User: event->accept() (window closes)
            end
        end
    end
Loading

Class diagram for async cancel disposition and close handling

classDiagram
    class MainWindow {
        +void closeEvent(QCloseEvent *event)
        +bool maybeSave()
        +void writeSettings()
        -AsyncTask m_asyncTask
        -std::unique_ptr<QProgressDialog> m_progressDlg
    }

    class AsyncTask {
        -std::unique_ptr<AsyncBase> m_task
        +void begin(std::unique_ptr<AsyncBase> task)
        +void tick()
        +void request_cancel()
        +bool is_allowed_to_cancel() const
        +void reset()
    }

    class AsyncBase {
        +AsyncBase(std::shared_ptr<ProgressCounter> pc, CancelDispositionEnum cancelDisposition)
        +~AsyncBase()
        +PollResultEnum poll()
        +PollResultEnum poll(std::chrono::milliseconds timeout)
        +void request_cancel()
        +bool requested_cancel() const
        +bool is_allowed_to_cancel() const
        +void virt_request_cancel()
        +PollResultEnum virt_poll(std::chrono::milliseconds timeout)
        +const std::shared_ptr<ProgressCounter> progressCounter
        +const CancelDispositionEnum cancelDisposition
    }

    class AsyncHelper {
        +AsyncHelper(MainWindow &mw, QString name, std::unique_ptr<QSaveFile> pd, UniqueStorage ps, QString dialogText, CancelDispositionEnum allow_cancel)
        +MainWindow &mainWindow
        +QString fileName
    }

    class CancelDispositionEnum {
        <<enumeration>>
        Allow
        Disallow
    }

    class ProgressCounter {
        +void requestCancel()
        +bool requestedCancel() const
    }

    MainWindow o-- AsyncTask
    MainWindow o-- QProgressDialog
    AsyncTask *-- AsyncBase
    AsyncBase o-- ProgressCounter
    AsyncHelper --|> AsyncBase
    AsyncBase --> CancelDispositionEnum
Loading

File-Level Changes

Change Details Files
Gate window closing on async task cancelability and move settings persistence after close acceptance.
  • Add logging at the start of MainWindow::closeEvent for traceability.
  • If an async task exists and is not allowed to cancel, immediately ignore the close event without prompting to save.
  • After maybeSave(), if an async task exists and is not allowed to cancel, ignore the close event to protect newly scheduled non-cancelable tasks.
  • When an async task is cancelable and working, request its cancellation and reject the progress dialog via a safe pointer check.
  • Call writeSettings() only after all close conditions pass, just before accepting the event.
src/mainwindow/mainwindow.cpp
Introduce cancel-disposition semantics to async tasks and plumb them through AsyncBase/AsyncTask/AsyncHelper.
  • Add a CancelDispositionEnum-backed cancelDisposition member to AsyncBase and extend its constructor to accept the desired disposition.
  • Add is_allowed_to_cancel() to AsyncBase and AsyncTask, returning true only when the disposition allows cancellation.
  • Guard AsyncBase::request_cancel() so it is a no-op if the task is not allowed to cancel.
  • Switch AsyncTask methods from raw pointer access to deref(m_task) for safety and consistency.
  • Update AsyncHelper to pass the allow_cancel disposition through to the AsyncBase constructor.
src/mainwindow/mainwindow-async.cpp
src/mainwindow/mainwindow.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The closeEvent logic now has two separate blocks checking m_asyncTask cancelability and ignoring the event; consider centralizing this into a single helper (e.g., canCloseForAsyncState()) to avoid duplicated conditions and keep future changes to the close/async interaction in one place.
  • AsyncBase::request_cancel() silently returns when the task is non-cancelable; you might want to either return a bool indicating whether a cancel was actually requested or log/rename the method so callers are not misled into thinking a cancel always occurs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The closeEvent logic now has two separate blocks checking m_asyncTask cancelability and ignoring the event; consider centralizing this into a single helper (e.g., canCloseForAsyncState()) to avoid duplicated conditions and keep future changes to the close/async interaction in one place.
- AsyncBase::request_cancel() silently returns when the task is non-cancelable; you might want to either return a bool indicating whether a cancel was actually requested or log/rename the method so callers are not misled into thinking a cancel always occurs.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Before this change, the game could close before the async saver had a
chance to finish.

This commit:
- Adds a 'cancelDisposition' to AsyncBase to define if a task is
  interruptible.
- Updates MainWindow::closeEvent to ignore the close request if an active
  async task is marked as non-cancelable.
- Ensures settings are written only after the close event is accepted.
- Prevents redundant save prompts when a save task is already in progress.
@nschimme
nschimme merged commit f0f3b68 into MUME:master Apr 20, 2026
17 of 18 checks passed
@nschimme
nschimme deleted the fix-save branch April 20, 2026 16:20
@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.39%. Comparing base (dd0c268) to head (ba36a33).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
src/mainwindow/mainwindow.cpp 0.00% 17 Missing ⚠️
src/mainwindow/mainwindow-async.cpp 0.00% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #525      +/-   ##
==========================================
- Coverage   25.40%   25.39%   -0.02%     
==========================================
  Files         519      519              
  Lines       43109    43130      +21     
  Branches     4699     4705       +6     
==========================================
  Hits        10952    10952              
- Misses      32157    32178      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant