from __future__ import annotations import argparse import json import os import signal import sys from contextlib import redirect_stderr, redirect_stdout from bec_lib.logger import bec_logger from bec_lib.service_config import ServiceConfig from qtpy.QtCore import QSize, Qt from qtpy.QtGui import QIcon from qtpy.QtWidgets import QApplication import bec_widgets from bec_widgets.applications.launch_window import LaunchWindow from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.utils.bec_dispatcher import BECDispatcher logger = bec_logger.logger MODULE_PATH = os.path.dirname(bec_widgets.__file__) class SimpleFileLikeFromLogOutputFunc: def __init__(self, log_func): self._log_func = log_func self._buffer = [] def write(self, buffer): self._buffer.append(buffer) def flush(self): lines, _, remaining = "".join(self._buffer).rpartition("\n") if lines: self._log_func(lines) self._buffer = [remaining] @property def encoding(self): return "utf-8" def close(self): return class GUIServer: """ This class is used to start the BEC GUI and is the main entry point for launching BEC Widgets in a subprocess. """ def __init__(self, args): self.config = args.config self.gui_id = args.id self.gui_class = args.gui_class self.gui_class_id = args.gui_class_id self.hide = args.hide self.app: QApplication | None = None self.launcher_window: LaunchWindow | None = None self.dispatcher: BECDispatcher | None = None def start(self): """ Start the GUI server. """ bec_logger.level = bec_logger.LOGLEVEL.INFO if self.hide: # pylint: disable=protected-access bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR bec_logger._update_sinks() with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)): # type: ignore with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)): # type: ignore self._run() def _get_service_config(self) -> ServiceConfig: if self.config: try: config = json.loads(self.config) service_config = ServiceConfig(config=config) except (json.JSONDecodeError, TypeError): service_config = ServiceConfig(config_path=config) else: # if no config is provided, use the default config service_config = ServiceConfig() return service_config def _run(self): """ Run the GUI server. """ self.app = QApplication(sys.argv) self.app.setApplicationName("BEC") self.app.gui_id = self.gui_id # type: ignore self.setup_bec_icon() service_config = self._get_service_config() self.dispatcher = BECDispatcher(config=service_config, gui_id=self.gui_id) # self.dispatcher.start_cli_server(gui_id=self.gui_id) self.launcher_window = LaunchWindow(gui_id=f"{self.gui_id}:launcher") self.launcher_window.setAttribute(Qt.WA_ShowWithoutActivating) # type: ignore self.app.aboutToQuit.connect(self.shutdown) self.app.setQuitOnLastWindowClosed(False) if self.gui_class: # If the server is started with a specific gui class, we launch it. # This will automatically hide the launcher. self.launcher_window.launch(self.gui_class, name=self.gui_class_id) def sigint_handler(*args): # display message, for people to let it terminate gracefully print("Caught SIGINT, exiting") # Widgets should be all closed. with RPCRegister.delayed_broadcast(): for widget in QApplication.instance().topLevelWidgets(): # type: ignore widget.close() if self.app: self.app.quit() signal.signal(signal.SIGINT, sigint_handler) signal.signal(signal.SIGTERM, sigint_handler) sys.exit(self.app.exec()) def setup_bec_icon(self): """ Set the BEC icon for the application """ if self.app is None: return icon = QIcon() icon.addFile( os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"), size=QSize(48, 48), ) self.app.setWindowIcon(icon) def shutdown(self): """ Shutdown the GUI server. """ if self.dispatcher: self.dispatcher.stop_cli_server() self.dispatcher.disconnect_all() def main(): """ Main entry point for subprocesses that start a GUI server. """ parser = argparse.ArgumentParser(description="BEC Widgets CLI Server") parser.add_argument("--id", type=str, default="test", help="The id of the server") parser.add_argument( "--gui_class", type=str, help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea", ) parser.add_argument( "--gui_class_id", type=str, default="bec", help="The id of the gui class that is added to the QApplication", ) parser.add_argument("--config", type=str, help="Config file or config string.") parser.add_argument("--hide", action="store_true", help="Hide on startup") args = parser.parse_args() server = GUIServer(args) server.start() if __name__ == "__main__": # import sys # sys.argv = ["bec_widgets", "--gui_class", "MainWindow"] main()