From c8373c3423b4b046495bd02834ff0e2f96ca5d19 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sun, 2 Aug 2026 12:05:08 +0200 Subject: [PATCH] Resulable besim handof so recompiling model does not give second besim window --- .../controllers/simulation_controller.py | 3 + .../controllers/simulation_file_controller.py | 13 +++- .../simulation_handoff_controller.py | 57 +++++++++++++++ .../controllers/simulation_run_controller.py | 4 ++ src/bedit_gui/services/simulation_handoff.py | 72 +++++++++++++++++++ src/bedit_gui/simulation_application.py | 2 + 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/bedit_gui/controllers/simulation_handoff_controller.py create mode 100644 src/bedit_gui/services/simulation_handoff.py diff --git a/src/bedit_gui/controllers/simulation_controller.py b/src/bedit_gui/controllers/simulation_controller.py index 4479ed0..d898bd3 100644 --- a/src/bedit_gui/controllers/simulation_controller.py +++ b/src/bedit_gui/controllers/simulation_controller.py @@ -13,6 +13,7 @@ from bedit_gui.documents import Document from bedit_gui.services import simulation_files from bedit_gui.services.application_logging import get_logger from bedit_gui.services.simulation_loader import component_choices +from bedit_gui.services.simulation_handoff import send_simulation_handoff from bedit_gui.simulation_models import CompiledModel, SimulationRoot from bedit_gui.views.main_window import MainWindow from bedit_simulation import ModelBuildResult, compile_component_sync @@ -24,6 +25,8 @@ Launcher = Callable[[Path, CompiledModel], bool] def _launch_simulator(path: Path, compiled_model: CompiledModel) -> bool: + if send_simulation_handoff(path, compiled_model): + return True arguments = ["-m", "bedit_gui.simulation_application", "--handoff", "--model-name", compiled_model.model_name, "--executable", compiled_model.executable, "--working-directory", compiled_model.working_directory, str(path)] launched = QProcess.startDetached(sys.executable, arguments) return launched[0] if isinstance(launched, tuple) else bool(launched) diff --git a/src/bedit_gui/controllers/simulation_file_controller.py b/src/bedit_gui/controllers/simulation_file_controller.py index cd56971..d7ad0d9 100644 --- a/src/bedit_gui/controllers/simulation_file_controller.py +++ b/src/bedit_gui/controllers/simulation_file_controller.py @@ -1,5 +1,6 @@ from __future__ import annotations +from copy import deepcopy from pathlib import Path from PySide6.QtCore import QObject, Signal, Qt @@ -39,8 +40,9 @@ class SimulationFileController(QObject): self.runtime_changed.emit() logger.info("Created new simulation") - def open(self, path: str | Path, *, component: str | None = None, simulation: str | None = None, backed_by_file: bool = True, compiled_model: CompiledModel | None = None) -> None: + def open(self, path: str | Path, *, component: str | None = None, simulation: str | None = None, backed_by_file: bool = True, compiled_model: CompiledModel | None = None, preserve_plots: bool = False) -> None: file_path = Path(path) + plot_tabs = deepcopy(self.root.plot_tabs) if preserve_plots and self.root is not None else None try: if file_path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(file_path): self.root = simulation_files.load(file_path) @@ -52,11 +54,20 @@ class SimulationFileController(QObject): except (OSError, RuntimeError, TypeError, ValueError): logger.exception("Could not open simulation %s", file_path) raise + if preserve_plots and self.root is not None: + if plot_tabs is not None: + self.root.plot_tabs = plot_tabs + self.root.current_end_time = self.root.settings.start_time + self.root.results.clear() self._log_compilation() self._update_window() self.runtime_changed.emit() logger.info("Opened simulation: %s", file_path) + def open_handoff(self, path: str | Path, compiled_model: CompiledModel) -> None: + self.open(path, backed_by_file=False, compiled_model=compiled_model, preserve_plots=True) + logger.info("Accepted simulation handoff and reset to time %s", self.root.current_end_time if self.root is not None else "unknown") + def open_dialog(self) -> None: filename, _ = QFileDialog.getOpenFileName(self.window, "Open Simulation", "", "Simulation and BEdit files (*.bes *.beb *.json)") if not filename: diff --git a/src/bedit_gui/controllers/simulation_handoff_controller.py b/src/bedit_gui/controllers/simulation_handoff_controller.py new file mode 100644 index 0000000..c64530b --- /dev/null +++ b/src/bedit_gui/controllers/simulation_handoff_controller.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from pathlib import Path + +from PySide6.QtCore import QObject +from PySide6.QtWidgets import QMessageBox + +from bedit_gui.controllers.simulation_file_controller import SimulationFileController +from bedit_gui.controllers.simulation_run_controller import SimulationRunController +from bedit_gui.services.application_logging import get_logger +from bedit_gui.services.simulation_handoff import SimulationHandoffServer +from bedit_gui.simulation_models import CompiledModel +from bedit_gui.views.simulation_window import SimulationWindow + +logger = get_logger(__name__) + + +class SimulationHandoffController(QObject): + def __init__(self, window: SimulationWindow, files: SimulationFileController, runs: SimulationRunController) -> None: + super().__init__(window) + self.window = window + self.files = files + self.runs = runs + self._pending: tuple[Path, CompiledModel] | None = None + self.server = SimulationHandoffServer(self) + self.server.handoff_received.connect(self.receive) + runs.run_completed.connect(self._run_finished) + runs.run_cancelled.connect(self._run_finished) + runs.run_failed.connect(self._run_finished) + + def receive(self, path: Path, compiled_model: CompiledModel) -> None: + self._pending = (path, compiled_model) + if self.runs.is_running: + logger.info("Received a new model; stopping the active simulation before handoff") + self.runs.stop() + return + self._apply_pending() + + def _run_finished(self, *_args: object) -> None: + if self._pending is not None: + self._apply_pending() + + def _apply_pending(self) -> None: + pending = self._pending + self._pending = None + if pending is None: + return + path, compiled_model = pending + try: + self.files.open_handoff(path, compiled_model) + except (OSError, RuntimeError, TypeError, ValueError) as exc: + logger.exception("Could not accept simulation handoff from %s", path) + QMessageBox.critical(self.window, "Could not open simulation", str(exc)) + return + self.window.show() + self.window.raise_() + self.window.activateWindow() diff --git a/src/bedit_gui/controllers/simulation_run_controller.py b/src/bedit_gui/controllers/simulation_run_controller.py index a0d85cf..f1d4bac 100644 --- a/src/bedit_gui/controllers/simulation_run_controller.py +++ b/src/bedit_gui/controllers/simulation_run_controller.py @@ -44,6 +44,10 @@ class SimulationRunController(QObject): self.run_failed.connect(self._failed) self._load_session() + @property + def is_running(self) -> bool: + return self._running + def start(self) -> None: if self._running or self.session is None: return diff --git a/src/bedit_gui/services/simulation_handoff.py b/src/bedit_gui/services/simulation_handoff.py new file mode 100644 index 0000000..b9f2498 --- /dev/null +++ b/src/bedit_gui/services/simulation_handoff.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from PySide6.QtCore import QObject, Signal +from PySide6.QtNetwork import QLocalServer, QLocalSocket + +from bedit_gui.simulation_models import CompiledModel + +SERVER_NAME = "bedit-besim-handoff-v1" + + +def send_simulation_handoff(path: str | Path, compiled_model: CompiledModel, *, server_name: str = SERVER_NAME, timeout_ms: int = 500) -> bool: + socket = QLocalSocket() + socket.connectToServer(server_name) + if not socket.waitForConnected(timeout_ms): + return False + payload = json.dumps({"path": str(Path(path).resolve()), "compiled_model": compiled_model.to_data()}, separators=(",", ":")).encode("utf-8") + b"\n" + if socket.write(payload) != len(payload) or not socket.waitForBytesWritten(timeout_ms): + socket.abort() + return False + socket.disconnectFromServer() + return True + + +class SimulationHandoffServer(QObject): + handoff_received = Signal(object, object) + + def __init__(self, parent: QObject | None = None, *, server_name: str = SERVER_NAME) -> None: + super().__init__(parent) + self.server = QLocalServer(self) + self._buffers: dict[QLocalSocket, bytearray] = {} + self.server.newConnection.connect(self._accept_connections) + if not self.server.listen(server_name): + probe = QLocalSocket() + probe.connectToServer(server_name) + if probe.waitForConnected(200): + probe.disconnectFromServer() + return + QLocalServer.removeServer(server_name) + if not self.server.listen(server_name): + raise RuntimeError(f"could not listen for BEsim handoffs: {self.server.errorString()}") + + def _accept_connections(self) -> None: + while self.server.hasPendingConnections(): + socket = self.server.nextPendingConnection() + if socket is None: + continue + self._buffers[socket] = bytearray() + socket.readyRead.connect(lambda active=socket: self._read(active)) + socket.disconnected.connect(lambda active=socket: self._discard(active)) + self._read(socket) + + def _read(self, socket: QLocalSocket) -> None: + buffer = self._buffers.get(socket) + if buffer is None: + return + buffer.extend(socket.readAll().data()) + while b"\n" in buffer: + raw_message, _, remaining = buffer.partition(b"\n") + buffer[:] = remaining + try: + message = json.loads(raw_message.decode("utf-8")) + path = Path(str(message["path"])) + compiled_model = CompiledModel.from_data(message["compiled_model"]) + except (KeyError, TypeError, ValueError, json.JSONDecodeError, UnicodeDecodeError): + continue + self.handoff_received.emit(path, compiled_model) + + def _discard(self, socket: QLocalSocket) -> None: + self._buffers.pop(socket, None) diff --git a/src/bedit_gui/simulation_application.py b/src/bedit_gui/simulation_application.py index 16bd78d..5381ccc 100644 --- a/src/bedit_gui/simulation_application.py +++ b/src/bedit_gui/simulation_application.py @@ -9,6 +9,7 @@ from bedit_gui.controllers.simulation_file_controller import SimulationFileContr from bedit_gui.controllers.log_controller import LogController from bedit_gui.controllers.simulation_plot_controller import SimulationPlotController from bedit_gui.controllers.simulation_run_controller import SimulationRunController +from bedit_gui.controllers.simulation_handoff_controller import SimulationHandoffController from bedit_gui.simulation_models import CompiledModel from bedit_gui.services.application_settings import SimulationApplicationSettings from bedit_gui.views.simulation_window import SimulationWindow @@ -51,6 +52,7 @@ def main(arguments: list[str] | None = None) -> int: LogController(window, settings.log_level) run_controller = SimulationRunController(window, controller) plot_controller = SimulationPlotController(window, controller) + SimulationHandoffController(window, controller, run_controller) run_controller.simulation_state_changed.connect(plot_controller.refresh_results) window.showMaximized()