0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31: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. process will not be captured.
""" """
# pylint: disable=subprocess-run-check # 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 config:
if isinstance(config, dict): if isinstance(config, dict):
config = json.dumps(config) config = json.dumps(config)
command.extend(["--config", config]) command.extend(["--config", str(config)])
env_dict = os.environ.copy() env_dict = os.environ.copy()
env_dict["PYTHONUNBUFFERED"] = "1" 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 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: class BECGuiClientMixin:
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self._gui_started_timer = None
self._gui_started_event = threading.Event()
self._process = None self._process = None
self._process_output_processing_thread = None self._process_output_processing_thread = None
self.auto_updates = self._get_update_script() self.auto_updates = self._get_update_script()
@ -182,29 +190,57 @@ class BECGuiClientMixin:
return return
self.auto_updates.msg_queue.put(msg) 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: 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._start_update_script()
self._process, self._process_output_processing_thread = _start_plot_process( self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id, self.__class__, self._client._service_config.config, logger=logger self._gui_id, self.__class__, self._client._service_config.config, logger=logger
) )
while not self.gui_is_alive():
print("Waiting for GUI to start...") def gui_started_callback(callback):
time.sleep(1) try:
logger.success(f"GUI started with id: {self._gui_id}") 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: def close(self) -> None:
""" """
Close the gui window. 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: if self._process is None:
return return
self._client.shutdown()
if self._process: if self._process:
logger.success("Stopping GUI...")
self._process.terminate() self._process.terminate()
if self._process_output_processing_thread: if self._process_output_processing_thread:
self._process_output_processing_thread.join() self._process_output_processing_thread.join()
@ -212,6 +248,7 @@ class BECGuiClientMixin:
self._process = None self._process = None
if self.auto_updates is not None: if self.auto_updates is not None:
self.auto_updates.shutdown() self.auto_updates.shutdown()
self.auto_updates = None
class RPCResponseTimeoutError(Exception): class RPCResponseTimeoutError(Exception):

View File

@ -1,3 +1,4 @@
from contextlib import contextmanager
from unittest import mock from unittest import mock
import pytest 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): def test_client_utils_start_plot_process(config, call_config):
with mock.patch("bec_widgets.cli.client_utils.subprocess.Popen") as mock_popen: with mock.patch("bec_widgets.cli.client_utils.subprocess.Popen") as mock_popen:
_start_plot_process("gui_id", BECFigure, config) _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: if call_config:
command.extend(["--config", call_config]) command.extend(["--config", call_config])
mock_popen.assert_called_once_with( 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 changes to the client config (either through config files or plugins) are
reflected in the server. 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: @contextmanager
with mock.patch.object(mixin, "_start_update_script") as mock_start_update: 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()] 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( mock_start_plot.assert_called_once_with(
"gui_id", BECGuiClientMixin, mixin._client._service_config.config, logger=mock.ANY "gui_id", BECGuiClientMixin, mixin._client._service_config.config, logger=mock.ANY
) )
mock_start_update.assert_called_once() mixin._start_update_script.assert_called_once()