Skip to content

Commit 5040aab

Browse files
committed
Improve signal handling and graceful shutdown
Enhance Qt app signal handling to support SIGTERM and SIGBREAK, and make Ctrl+C shutdown more robust. Adds a quitting flag and a two-stage interrupt: first interrupt triggers window close and schedules app.quit, second interrupt forces immediate exit (os._exit(130)). Parents the keepalive QTimer to the QApplication, sets a 100ms interval, and safely stops any previous timer to avoid duplicates. Adds logging and exception handling around window close and timer cleanup, and uses QTimer.singleShot to ensure Qt leaves its event loop even if closeEvent cleanup is asynchronous.
1 parent 51ab19e commit 5040aab

1 file changed

Lines changed: 49 additions & 11 deletions

File tree

dlclivegui/main.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,70 @@
2727

2828
def _maybe_allow_keyboard_interrupt(app: QApplication) -> None:
2929
"""
30-
Gracefully handle Ctrl+C (SIGINT) by closing the main window and quitting Qt.
30+
Gracefully handle Ctrl+C/SIGTERM by closing the main window and quitting Qt.
31+
32+
Notes:
33+
- The small timer keeps Python signal handling responsive while Qt owns the event loop.
34+
- First Ctrl+C tries graceful cleanup via closeEvent().
35+
- Second Ctrl+C exits immediately with code 130.
3136
"""
37+
quitting = {"requested": False}
3238

3339
def _request_quit() -> None:
40+
if quitting["requested"]:
41+
return
42+
43+
quitting["requested"] = True
3444
logging.info("Keyboard interrupt received, closing application...")
45+
3546
win = getattr(app, "_main_window", None)
47+
3648
if win is not None:
37-
# Trigger your existing closeEvent cleanup (camera stop, threads, timers, etc.)
38-
win.close()
39-
else:
40-
app.quit()
49+
try:
50+
# Trigger existing closeEvent cleanup:
51+
# camera stop, controller shutdown, timers, DLC shutdown, etc.
52+
win.close()
53+
except Exception:
54+
logging.exception("Error while closing main window after Ctrl+C")
55+
56+
# Explicitly ask Qt to leave app.exec().
57+
# Do this even after win.close(), because closeEvent cleanup can be async
58+
# and relying only on quitOnLastWindowClosed can be fragile.
59+
QTimer.singleShot(0, app.quit)
60+
61+
def _force_exit() -> None:
62+
logging.warning("Second interrupt received, forcing process exit.")
63+
os._exit(130)
4164

4265
def _sigint_handler(_signum, _frame) -> None:
66+
if quitting["requested"]:
67+
_force_exit()
4368
QTimer.singleShot(0, _request_quit)
4469

4570
signal.signal(signal.SIGINT, _sigint_handler)
4671

47-
# Keepalive timer to allow Python to handle signals while Qt is running.
48-
sig_timer = QTimer()
49-
sig_timer.setInterval(100) # 50–200ms typical; keep low overhead
72+
# Ctrl+Break on Windows.
73+
if hasattr(signal, "SIGBREAK"):
74+
signal.signal(signal.SIGBREAK, _sigint_handler)
75+
76+
# Useful when process is terminated from shells/process managers.
77+
if hasattr(signal, "SIGTERM"):
78+
signal.signal(signal.SIGTERM, _sigint_handler)
79+
80+
# Parent the timer to app so Qt owns its lifetime.
81+
sig_timer = QTimer(app)
82+
sig_timer.setInterval(100)
5083
sig_timer.timeout.connect(lambda: None)
5184
sig_timer.start()
5285

53-
if hasattr(app, "_sig_timer"):
54-
app._sig_timer.stop() # Stop any existing timer to avoid duplicates
55-
app._sig_timer = sig_timer # Store on app to keep it alive and allow cleanup on exit
86+
old_timer = getattr(app, "_sig_timer", None)
87+
if old_timer is not None:
88+
try:
89+
old_timer.stop()
90+
except Exception:
91+
pass
92+
93+
app._sig_timer = sig_timer
5694

5795

5896
def configure_logging(debug: bool = False) -> None:

0 commit comments

Comments
 (0)