0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

feat: asynchronous .start() for GUI

This commit is contained in:
2024-11-11 20:19:44 +01:00
parent 1f71d8e5ed
commit 2047e484d5
2 changed files with 68 additions and 19 deletions

View File

@ -92,11 +92,11 @@ def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger
process will not be captured.
"""
# pylint: disable=subprocess-run-check
command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__]
command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__, "--hide"]
if config:
if isinstance(config, dict):
config = json.dumps(config)
command.extend(["--config", config])
command.extend(["--config", str(config)])
env_dict = os.environ.copy()
env_dict["PYTHONUNBUFFERED"] = "1"
@ -126,9 +126,17 @@ def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger
return process, process_output_processing_thread
class RepeatTimer(threading.Timer):
def run(self):
while not self.finished.wait(self.interval):
self.function(*self.args, **self.kwargs)
class BECGuiClientMixin:
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._gui_started_timer = None
self._gui_started_event = threading.Event()
self._process = None
self._process_output_processing_thread = None
self.auto_updates = self._get_update_script()
@ -182,29 +190,57 @@ class BECGuiClientMixin:
return
self.auto_updates.msg_queue.put(msg)
def show(self) -> None:
def _gui_post_startup(self):
if self.auto_updates is None:
AutoUpdates.create_default_dock = True
AutoUpdates.enabled = True
self.auto_updates = AutoUpdates(gui=self)
if self.auto_updates.create_default_dock:
self.auto_updates.start_default_dock()
fig = self.auto_updates.get_default_figure()
self._gui_started_event.set()
self.show_all()
def start_server(self, wait=False) -> None:
"""
Show the figure.
Start the GUI server, and execute callback when it is launched
"""
if self._process is None or self._process.poll() is not None:
logger.success("GUI starting...")
self._gui_started_event.clear()
self._start_update_script()
self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id, self.__class__, self._client._service_config.config, logger=logger
)
while not self.gui_is_alive():
print("Waiting for GUI to start...")
time.sleep(1)
logger.success(f"GUI started with id: {self._gui_id}")
def gui_started_callback(callback):
try:
if callable(callback):
callback()
finally:
threading.current_thread().cancel()
self._gui_started_timer = RepeatTimer(
1, lambda: self.gui_is_alive() and gui_started_callback(self._gui_post_startup)
)
self._gui_started_timer.start()
if wait:
self._gui_started_event.wait()
def close(self) -> None:
"""
Close the gui window.
"""
if self._gui_started_timer is not None:
self._gui_started_timer.cancel()
self._gui_started_timer.join()
if self._process is None:
return
self._client.shutdown()
if self._process:
logger.success("Stopping GUI...")
self._process.terminate()
if self._process_output_processing_thread:
self._process_output_processing_thread.join()
@ -212,6 +248,7 @@ class BECGuiClientMixin:
self._process = None
if self.auto_updates is not None:
self.auto_updates.shutdown()
self.auto_updates = None
class RPCResponseTimeoutError(Exception):

View File

@ -1,3 +1,4 @@
from contextlib import contextmanager
from unittest import mock
import pytest
@ -40,7 +41,7 @@ def test_rpc_call_accepts_device_as_input(cli_figure):
def test_client_utils_start_plot_process(config, call_config):
with mock.patch("bec_widgets.cli.client_utils.subprocess.Popen") as mock_popen:
_start_plot_process("gui_id", BECFigure, config)
command = ["bec-gui-server", "--id", "gui_id", "--gui_class", "BECFigure"]
command = ["bec-gui-server", "--id", "gui_id", "--gui_class", "BECFigure", "--hide"]
if call_config:
command.extend(["--config", call_config])
mock_popen.assert_called_once_with(
@ -59,17 +60,28 @@ def test_client_utils_passes_client_config_to_server(bec_dispatcher):
changes to the client config (either through config files or plugins) are
reflected in the server.
"""
mixin = BECGuiClientMixin()
mixin._client = bec_dispatcher.client
mixin._gui_id = "gui_id"
mixin.gui_is_alive = mock.MagicMock()
mixin.gui_is_alive.side_effect = [True]
with mock.patch("bec_widgets.cli.client_utils._start_plot_process") as mock_start_plot:
with mock.patch.object(mixin, "_start_update_script") as mock_start_update:
@contextmanager
def bec_client_mixin():
mixin = BECGuiClientMixin()
mixin._client = bec_dispatcher.client
mixin._gui_id = "gui_id"
mixin.gui_is_alive = mock.MagicMock()
mixin.gui_is_alive.side_effect = [True]
try:
with mock.patch.object(mixin, "_start_update_script"):
yield mixin
finally:
mixin.close()
with bec_client_mixin() as mixin:
with mock.patch("bec_widgets.cli.client_utils._start_plot_process") as mock_start_plot:
mock_start_plot.return_value = [mock.MagicMock(), mock.MagicMock()]
mixin.show()
mixin.start_server(
wait=False
) # the started event will not be set, wait=True would block forever
mock_start_plot.assert_called_once_with(
"gui_id", BECGuiClientMixin, mixin._client._service_config.config, logger=mock.ANY
)
mock_start_update.assert_called_once()
mixin._start_update_script.assert_called_once()