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:
@ -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):
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user