Added restart server widget
CI for debye_bec / test (push) Successful in 58s
CI for debye_bec / test (pull_request) Successful in 1m0s

This commit is contained in:
x01da
2026-08-19 12:50:40 +02:00
parent b6be408ad3
commit 853001825f
7 changed files with 318 additions and 1 deletions
+28
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
logger = bec_logger.logger
@@ -14,6 +15,7 @@ logger = bec_logger.logger
_Widgets = {
"DataViewer": "DataViewer",
"DigitalTwin": "DigitalTwin",
"RestartServer": "RestartServer",
"ScanControlXAS": "ScanControlXAS",
}
@@ -66,7 +68,33 @@ class DigitalTwin(RPCBase):
"""
class RestartServer(RPCBase):
"""Main widget of server restart widget"""
_IMPORT_MODULE = "debye_bec.bec_widgets.widgets.restart_server.restart_server"
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
@rpc_call
def attach(self):
"""
None
"""
@rpc_call
def detach(self):
"""
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
"""
class ScanControlXAS(RPCBase):
"""Main widget of Scan Control XAS"""
_IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas"
@rpc_call
@@ -7,10 +7,19 @@ from __future__ import annotations
designer_plugins = {
"DataViewer": ("debye_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"),
"DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"),
"RestartServer": (
"debye_bec.bec_widgets.widgets.restart_server.restart_server",
"RestartServer",
),
"ScanControlXAS": (
"debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas",
"ScanControlXAS",
),
}
widget_icons = {"DataViewer": "find_in_page", "DigitalTwin": "lightbulb", "ScanControlXAS": "tune"}
widget_icons = {
"DataViewer": "find_in_page",
"DigitalTwin": "lightbulb",
"RestartServer": "restart_alt",
"ScanControlXAS": "tune",
}
@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from .restart_server_plugin import RestartServerPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(RestartServerPlugin())
if __name__ == "__main__": # pragma: no cover
main()
@@ -0,0 +1,207 @@
"""
Restart Server: Custom BEC widget to restart server.
"""
import sys
from bec_lib import bec_logger
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
from PySide6QtAds import CDockWidget
from qtpy.QtCore import QThread, QTimer, Signal
# pylint: disable=E0611
from qtpy.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLabel, QVBoxLayout, QWidget
logger = bec_logger.logger
class RestartServer(BECWidget, QWidget):
"""
Main widget of server restart widget.
This widget has no visible UI of its own - it only drives the confirmation/
progress dialog. The surrounding dock panel is hidden as soon as we can find
it, so the user only ever sees the dialog, never an empty "Restart Server" tab.
"""
PLUGIN = True
ICON_NAME = "restart_alt"
def __init__(self, *arg, parent=None, **kwargs):
super().__init__(parent=parent, *arg, **kwargs)
self.get_bec_shortcuts()
self.dialog = None
self._worker = None
self._dock_widget = None
QTimer.singleShot(0, self._start)
def _start(self):
"""Hide the containing dock panel, then show the confirmation dialog."""
self._dock_widget = self._find_containing_dock()
if self._dock_widget is not None:
self._dock_widget.toggleView(False)
self.prompt_restart()
def _find_containing_dock(self):
"""Walk up the parent chain to find the wrapping CDockWidget, if any."""
widget = self.parentWidget()
while widget is not None:
if CDockWidget is not None and isinstance(widget, CDockWidget):
return widget
widget = widget.parentWidget()
return None
def prompt_restart(self):
"""Prompt the user for confirmation, then restart the BEC server if confirmed."""
self.dialog = RestartConfirmationDialog(parent=self)
if self.dialog.exec_() == QDialog.Accepted:
self._start_restart()
else:
self._close_containing_dock()
def _start_restart(self):
"""Kick off the restart on a background thread and show progress feedback."""
self.dialog.show_restarting()
self.dialog.finished.connect(self._close_containing_dock)
self._worker = _RestartWorker(self.client, parent=self)
self._worker.finished_ok.connect(self._on_restart_finished)
self._worker.failed.connect(self._on_restart_failed)
self._worker.finished.connect(self._worker.deleteLater)
self._worker.start()
def _on_restart_finished(self):
self.dialog.show_done()
def _on_restart_failed(self, error_msg: str):
self.dialog.show_error(error_msg)
def _close_containing_dock(self):
"""Actually close (and typically delete) the dock panel this widget lives in.
Reuses the reference found in `_start()` rather than re-walking the
parent chain, since toggleView(False) doesn't reparent anything.
"""
if self._dock_widget is not None:
self._dock_widget.closeDockWidget()
return
# Fallback: no dock wrapper found (e.g. standalone use)
self.close()
class _RestartWorker(QThread):
"""Runs the blocking server-restart call off the GUI thread."""
finished_ok = Signal()
failed = Signal(str)
def __init__(self, client, parent=None):
super().__init__(parent)
self._client = client
def run(self):
try:
self._client._request_server_restart()
except Exception as exc: # pylint: disable=broad-except
logger.exception("Failed to restart BEC server")
self.failed.emit(str(exc))
else:
self.finished_ok.emit()
class RestartConfirmationDialog(QDialog):
"""Dialog asking the user to confirm a restart of the BEC server, then shows progress
and waits for the user to acknowledge completion before closing."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Restart BEC Server")
self.setModal(True)
self.layout = QVBoxLayout(self)
self.message = QLabel(
"Are you sure you want to restart the BEC server?\n"
"This will interrupt any running scans!"
)
self.layout.addWidget(self.message)
self.button_box = QDialogButtonBox()
self.cancel_button = self.button_box.addButton("Cancel", QDialogButtonBox.RejectRole)
self.restart_button = self.button_box.addButton("Restart now", QDialogButtonBox.AcceptRole)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
# Log panel is created lazily, only once the restart actually begins, so
# opening/cancelling the confirmation dialog never touches the log stream.
self._log_panel: LogPanel | None = None
# Regardless of how the dialog ends (accept/reject/close), tear the log
# panel down. finished() fires reliably in all of those cases; it's a
# no-op if the log panel was never created.
self.finished.connect(self._teardown_log_panel)
def show_restarting(self):
"""Switch the dialog into a non-blocking 'in progress' state, with a live
view of the server logs so the user can see what's happening.
"""
self.message.setText("<b>BEC server is restarting, please wait...</b>")
self.button_box.hide()
self._setup_log_panel()
self.resize(750, 450)
self.show()
def _setup_log_panel(self):
"""Embed the standard LogPanel widget"""
if self._log_panel is not None:
return
self._log_panel = LogPanel(parent=self, show_toolbar=False)
self.layout.removeWidget(self.button_box)
self.layout.addWidget(self._log_panel)
self.layout.addWidget(self.button_box)
def _teardown_log_panel(self, *_):
"""Clean up the log panel so it stops receiving updates once this dialog
is gone. LogPanel.cleanup() detaches it from the shared log queue and
handles its own BECWidget/RPC teardown."""
if self._log_panel is None:
return
self._log_panel.cleanup()
self._log_panel = None
def show_done(self):
"""Show a success state and require the user to explicitly close the dialog."""
self.message.setText("<b>Server restarted successfully.</b>")
self._show_close_button("Close")
def show_error(self, error_msg: str):
"""Show a failure state and require the user to explicitly close the dialog."""
self.message.setText(f"<b>Restart failed:</b>\n{error_msg}")
self._show_close_button("Close")
def _show_close_button(self, label: str):
"""Replace the button row with a single acknowledgement button.
Clicking it calls accept(), which emits the dialog's `finished` signal -
that's what RestartServer listens for to know it's safe to close the dock.
"""
self.button_box.clear()
self.close_button = self.button_box.addButton(label, QDialogButtonBox.AcceptRole)
self.button_box.accepted.connect(self.accept)
self.button_box.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
apply_theme("light")
dispatcher = BECDispatcher(gui_id="restart_server")
win = RestartServer()
win.show()
sys.exit(app.exec_())
@@ -0,0 +1 @@
{'files': ['restart_server.py']}
@@ -0,0 +1,57 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from bec_widgets.utils.bec_designer import designer_material_icon
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from .restart_server import RestartServer
DOM_XML = """
<ui language='c++'>
<widget class='RestartServer' name='restart_server'>
</widget>
</ui>
"""
class RestartServerPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = RestartServer(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return designer_material_icon(RestartServer.ICON_NAME)
def includeFile(self):
return "restart_server"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "RestartServer"
def toolTip(self):
return "RestartServer"
def whatsThis(self):
return self.toolTip()