0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

fix: ignore GUI server output (any output will go to log file)

If a logger is given to log `_start_log_process`, the server stdout and
stderr streams will be redirected as log entries with levels DEBUG or ERROR
in their parent process
This commit is contained in:
2024-06-17 09:03:15 +02:00
committed by wyzula_j
parent 3644f344da
commit ce374163ca
2 changed files with 38 additions and 29 deletions

View File

@ -13,6 +13,7 @@ from functools import wraps
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
@ -31,6 +32,8 @@ messages = lazy_import("bec_lib.messages")
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",)) MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",)) BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
logger = bec_logger.logger
def rpc_call(func): def rpc_call(func):
""" """
@ -63,27 +66,34 @@ def rpc_call(func):
return wrapper return wrapper
def _get_output(process) -> None: def _get_output(process, logger) -> None:
log_func = {process.stdout: logger.debug, process.stderr: logger.error}
stream_buffer = {process.stdout: [], process.stderr: []}
try: try:
os.set_blocking(process.stdout.fileno(), False) os.set_blocking(process.stdout.fileno(), False)
os.set_blocking(process.stderr.fileno(), False) os.set_blocking(process.stderr.fileno(), False)
while process.poll() is None: while process.poll() is None:
readylist, _, _ = select.select([process.stdout, process.stderr], [], [], 1) readylist, _, _ = select.select([process.stdout, process.stderr], [], [], 1)
if process.stdout in readylist: for stream in (process.stdout, process.stderr):
output = process.stdout.read(1024) buf = stream_buffer[stream]
if stream in readylist:
buf.append(stream.read(4096))
output, _, remaining = "".join(buf).rpartition("\n")
if output: if output:
print(output, end="") log_func[stream](output)
if process.stderr in readylist: buf.clear()
error_output = process.stderr.read(1024) buf.append(remaining)
if error_output:
print(error_output, end="", file=sys.stderr)
except Exception as e: except Exception as e:
print(f"Error reading process output: {str(e)}") print(f"Error reading process output: {str(e)}")
def _start_plot_process(gui_id, gui_class, config) -> None: def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
""" """
Start the plot in a new process. Start the plot in a new process.
Logger must be a logger object with "debug" and "error" functions,
or it can be left to "None" as default. None means output from the
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__]
@ -92,16 +102,28 @@ def _start_plot_process(gui_id, gui_class, config) -> None:
env_dict = os.environ.copy() env_dict = os.environ.copy()
env_dict["PYTHONUNBUFFERED"] = "1" env_dict["PYTHONUNBUFFERED"] = "1"
if logger is None:
stdout_redirect = subprocess.DEVNULL
stderr_redirect = subprocess.DEVNULL
else:
stdout_redirect = subprocess.PIPE
stderr_redirect = subprocess.PIPE
process = subprocess.Popen( process = subprocess.Popen(
command, command,
text=True, text=True,
start_new_session=True, start_new_session=True,
stdout=subprocess.PIPE, stdout=stdout_redirect,
stderr=subprocess.PIPE, stderr=stderr_redirect,
env=env_dict, env=env_dict,
) )
process_output_processing_thread = threading.Thread(target=_get_output, args=(process,)) if logger is None:
process_output_processing_thread.start() process_output_processing_thread = None
else:
process_output_processing_thread = threading.Thread(
target=_get_output, args=(process, logger)
)
process_output_processing_thread.start()
return process, process_output_processing_thread return process, process_output_processing_thread
@ -113,7 +135,6 @@ class BECGuiClientMixin:
self.auto_updates = self._get_update_script() self.auto_updates = self._get_update_script()
self._target_endpoint = MessageEndpoints.scan_status() self._target_endpoint = MessageEndpoints.scan_status()
self._selected_device = None self._selected_device = None
self.stderr_output = []
def _get_update_script(self) -> AutoUpdates | None: def _get_update_script(self) -> AutoUpdates | None:
eps = imd.entry_points(group="bec.widgets.auto_updates") eps = imd.entry_points(group="bec.widgets.auto_updates")
@ -185,19 +206,10 @@ class BECGuiClientMixin:
self._client.shutdown() self._client.shutdown()
if self._process: if self._process:
self._process.terminate() self._process.terminate()
self._process_output_processing_thread.join() if self._process_output_processing_thread:
self._process_output_processing_thread.join()
self._process = None self._process = None
def print_log(self) -> None:
"""
Print the log of the plot process.
"""
if self._process is None:
return
print("".join(self.stderr_output))
# Flush list
self.stderr_output.clear()
class RPCResponseTimeoutError(Exception): class RPCResponseTimeoutError(Exception):
"""Exception raised when an RPC response is not received within the expected time.""" """Exception raised when an RPC response is not received within the expected time."""

View File

@ -29,9 +29,7 @@ def gui_id():
@contextmanager @contextmanager
def plot_server(gui_id, klass, client_lib): def plot_server(gui_id, klass, client_lib):
dispatcher = BECDispatcher(client=client_lib) # Has to init singleton with fixture client dispatcher = BECDispatcher(client=client_lib) # Has to init singleton with fixture client
process, output_thread = _start_plot_process( process, _ = _start_plot_process(gui_id, klass, client_lib._client._service_config.config_path)
gui_id, klass, client_lib._client._service_config.config_path
)
try: try:
while client_lib._client.connector.get(MessageEndpoints.gui_heartbeat(gui_id)) is None: while client_lib._client.connector.get(MessageEndpoints.gui_heartbeat(gui_id)) is None:
time.sleep(0.3) time.sleep(0.3)
@ -39,7 +37,6 @@ def plot_server(gui_id, klass, client_lib):
finally: finally:
process.terminate() process.terminate()
process.wait() process.wait()
output_thread.join()
dispatcher.disconnect_all() dispatcher.disconnect_all()
dispatcher.reset_singleton() dispatcher.reset_singleton()