mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-11-06 01:33:26 +01:00
feat: close main app on server client restart requests
This commit is contained in:
1 parent
e290738343
commit
10e203f311
2 files changed
+111
-1
No files matched your search
@@ -1,5 +1,7 @@
|
||||
from bec_widgets.applications.startup_profiler import startup_profiler # isort: skip
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QTimer
|
||||
from qtpy.QtGui import QAction # type: ignore
|
||||
@@ -14,6 +16,7 @@ from bec_widgets.applications.views.device_manager_view.device_manager_view impo
|
||||
from bec_widgets.applications.views.dock_area_view.dock_area_view import DockAreaView
|
||||
from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.guided_tour import GuidedTour
|
||||
from bec_widgets.utils.launcher_ready import notify_launcher_ready
|
||||
from bec_widgets.utils.name_utils import sanitize_namespace
|
||||
@@ -70,6 +73,19 @@ class BECMainApp(BECMainWindow):
|
||||
self._setup_guided_tour()
|
||||
startup_profiler.mark("guided tour")
|
||||
|
||||
self.bec_dispatcher.connect_slot(self._on_client_restart, MessageEndpoints.client_restart())
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def _on_client_restart(self, msg: dict, _meta: dict) -> None:
|
||||
"""Close the application when the server requests clients to restart."""
|
||||
reason = msg.get("reason")
|
||||
detail = f": {reason}" if reason else ""
|
||||
bec_logger.logger.info(f"Client restart requested by server{detail}. Exiting...")
|
||||
# Include hidden windows and continue if a launcher rejects its close event.
|
||||
for widget in self.app.topLevelWidgets():
|
||||
widget.close()
|
||||
self.app.quit()
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
if self._launcher_ready_notified:
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import QRect
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import QRect, Qt, QThread
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.applications.main_app import BECMainApp
|
||||
from bec_widgets.applications.views.view import ViewBase
|
||||
from bec_widgets.tests.client_mocks import mocked_client
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.containers.dock_area.profile_utils import (
|
||||
is_experimental_features_enabled,
|
||||
@@ -14,6 +19,95 @@ from bec_widgets.widgets.containers.dock_area.profile_utils import (
|
||||
ANIM_TEST_DURATION = 60 # ms
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reason", ["", "server maintenance"])
|
||||
@pytest.mark.parametrize("visible", [False, True], ids=["hidden", "visible"])
|
||||
def test_server_restart_closes_main_app_and_other_windows(
|
||||
qtbot, mocked_client, monkeypatch, tmp_path, reason, visible
|
||||
):
|
||||
class OtherWindow(QWidget):
|
||||
closed = False
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.closed = True
|
||||
super().closeEvent(event)
|
||||
|
||||
monkeypatch.setenv("BECWIDGETS_PROFILE_DIR", str(tmp_path))
|
||||
app = create_widget(qtbot, BECMainApp, client=mocked_client)
|
||||
# Keep the window alive so qtbot can close it again during teardown.
|
||||
app.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
|
||||
other_window = create_widget(qtbot, OtherWindow)
|
||||
if visible:
|
||||
app.show()
|
||||
other_window.show()
|
||||
dispatcher = app.bec_dispatcher
|
||||
quit_threads = []
|
||||
monkeypatch.setattr(app.app, "quit", lambda: quit_threads.append(QThread.currentThread()))
|
||||
|
||||
restart_slot = next(
|
||||
slot for slot in dispatcher._registered_slots if slot.cb == app._on_client_restart
|
||||
)
|
||||
assert MessageEndpoints.client_restart().endpoint in restart_slot.topics
|
||||
message = messages.ClientRestartMessage(reason=reason)
|
||||
# Deliver like the Redis worker, through the registered Qt dispatcher callback.
|
||||
worker = threading.Thread(target=restart_slot, args=(message.content, message.metadata))
|
||||
worker.start()
|
||||
worker.join(timeout=1)
|
||||
assert not worker.is_alive()
|
||||
|
||||
qtbot.waitUntil(lambda: len(quit_threads) == 1)
|
||||
assert quit_threads == [app.app.thread()]
|
||||
assert app._destroyed
|
||||
assert other_window.closed
|
||||
assert not other_window.isVisible()
|
||||
assert not any(slot.cb == app._on_client_restart for slot in dispatcher._registered_slots)
|
||||
|
||||
|
||||
def test_closed_main_app_ignores_pending_restart(qtbot, mocked_client, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("BECWIDGETS_PROFILE_DIR", str(tmp_path))
|
||||
app = create_widget(qtbot, BECMainApp, client=mocked_client)
|
||||
app.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
|
||||
quit_calls = []
|
||||
monkeypatch.setattr(app.app, "quit", lambda: quit_calls.append(True))
|
||||
restart_slot = next(
|
||||
slot for slot in app.bec_dispatcher._registered_slots if slot.cb == app._on_client_restart
|
||||
)
|
||||
|
||||
app.close()
|
||||
restart_slot({"reason": "server maintenance"}, {})
|
||||
|
||||
assert quit_calls == []
|
||||
assert not restart_slot._active
|
||||
|
||||
|
||||
def test_server_restart_continues_after_rejected_close(qtbot, mocked_client, monkeypatch, tmp_path):
|
||||
class RejectingWindow(QWidget):
|
||||
reject_close = True
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.reject_close:
|
||||
event.ignore()
|
||||
else:
|
||||
super().closeEvent(event)
|
||||
|
||||
monkeypatch.setenv("BECWIDGETS_PROFILE_DIR", str(tmp_path))
|
||||
app = create_widget(qtbot, BECMainApp, client=mocked_client)
|
||||
app.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
|
||||
app.show()
|
||||
rejecting_window = create_widget(qtbot, RejectingWindow)
|
||||
rejecting_window.show()
|
||||
quit_calls = []
|
||||
monkeypatch.setattr(app.app, "quit", lambda: quit_calls.append(True))
|
||||
# Qt visits windows in an unspecified order; put the rejecting window first.
|
||||
with monkeypatch.context() as patch:
|
||||
patch.setattr(app.app, "topLevelWidgets", lambda: [rejecting_window, app])
|
||||
try:
|
||||
app._on_client_restart({}, {})
|
||||
assert app._destroyed
|
||||
assert quit_calls == [True]
|
||||
finally:
|
||||
rejecting_window.reject_close = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def viewbase(qtbot):
|
||||
v = ViewBase(content=QWidget())
|
||||
|
||||
Reference in new issue
Block a user