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