Run shutdown cleanup on the menu and dialog quit paths - #425
Conversation
…wn-cleanup # Conflicts: # tests/ui/test_main_window.py
There was a problem hiding this comment.
Pull request overview
This PR updates JABS’ quit/exit pathways so MainWindow.closeEvent reliably runs, ensuring shutdown cleanup (notably stopping background threads and shutting down the process pool) occurs even when quitting via the menu/Ctrl+Q or via the first-behavior prompt.
Changes:
- Route the Quit menu action to
MainWindow.quit_application()to triggercloseEventcleanup before exiting the event loop. - Ensure the first-behavior “Quit JABS” path closes windows before exiting, so cleanup runs first.
- Add/adjust UI tests to verify quit call order and the first-behavior quit path behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ui/test_main_window.py | Adds a test asserting quit_application() closes the window before quitting. |
| tests/ui/test_main_control_widget.py | Adds a test asserting the first-label “Quit JABS” path closes windows before exiting. |
| src/jabs/ui/main_window/menu_builder.py | Rewires Ctrl+Q/Quit action to call MainWindow.quit_application() instead of QCoreApplication.quit(). |
| src/jabs/ui/main_window/main_window.py | Introduces quit_application() to close the main window then quit the app. |
| src/jabs/ui/main_window/central_widget.py | Extends the “features not cached” dialog text to point to jabs-init --help. |
| src/jabs/ui/main_control_widget/main_control_widget.py | Updates first-label dialog cancel path to close all windows before exiting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.close() | ||
| QtWidgets.QApplication.quit() |
| QtWidgets.QApplication.closeAllWindows() | ||
| sys.exit(0) |
|
Both addressed, though the second one differently than suggested.
That prompted a related fix in the same spirit. Verified with a real window and an event filter that rejects the close:
The underlying concern is real, so I fixed it a better way: the prompt now closes the window that holds the widget ( window = self.window()
if window is not None and not window.close():
return
sys.exit(0)Tests cover both directions on both paths (accepted → quits/exits, declined → neither), and the existing quit-path smoke checks still pass unchanged: the real Quit menu action with a responsive scan exits 0 with the pool shut down, and the wedged-storage case still exits 0 after the forced stop. 1013 tests, ruff clean. |
Summary
Two of the three ways to quit JABS never delivered a close event, so
MainWindow.closeEvent— and the cleanup it performs — was skipped. With a background thread still running, the process aborts with SIGABRT at interpreter shutdown; the process pool shutdown was also being skipped.Stacked on #424 (both branches touch
main_window.py). Base retargets tomainonce that merges.The problem
menu_builder.py)QCoreApplication.quit, which leaves the event loop without closing anythingmain_control_widget.py)sys.exit(0)Measured with a background scan in flight (20 videos), the two bypassing paths exited with code 134 (SIGABRT) after
QThread: Destroyed while thread is still running, and_process_pool.shutdown()was never called.The gap predates the feature cache scan (
ProjectLoaderThreadis exposed the same way, and it runs longer), but the scan makes it easy to hit because it starts automatically when a project opens — the seconds right after opening are exactly when someone might hit Ctrl+Q, and the first-behavior prompt appears in that same window.The fix
MainWindow.quit_application():close()to deliver the close event synchronously, thenQtWidgets.QApplication.quit(). The Quit action now points at this.close()alone leaves the application running when another top-level window is open, and JABS creates some without a parent (TrainingReportDialog(..., parent=None)), so Ctrl+Q with a training report open would have closed the main window and left the app alive.QApplication.closeAllWindows()beforesys.exit(0), which delivers the close event synchronously so cleanup finishes first.Verification
Measured with a scan in flight (20 videos, 0.4 s each):
962 tests pass (7 added covering the quit call order and the first-behavior prompt path). Ruff clean.
Known remaining case
If a filesystem call is genuinely wedged, the bounded wait in
closeEventtimes out and the abort still happens on every path: no cooperative cancel can interrupt a blockedh5py.File()open. Left alone deliberately — a hung mount makes JABS unusable regardless, andQThread::terminate()is a worse trade.