Resulable besim handof so recompiling model does not give second besim window
This commit is contained in:
@@ -13,6 +13,7 @@ from bedit_gui.documents import Document
|
|||||||
from bedit_gui.services import simulation_files
|
from bedit_gui.services import simulation_files
|
||||||
from bedit_gui.services.application_logging import get_logger
|
from bedit_gui.services.application_logging import get_logger
|
||||||
from bedit_gui.services.simulation_loader import component_choices
|
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.simulation_models import CompiledModel, SimulationRoot
|
||||||
from bedit_gui.views.main_window import MainWindow
|
from bedit_gui.views.main_window import MainWindow
|
||||||
from bedit_simulation import ModelBuildResult, compile_component_sync
|
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:
|
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)]
|
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)
|
launched = QProcess.startDetached(sys.executable, arguments)
|
||||||
return launched[0] if isinstance(launched, tuple) else bool(launched)
|
return launched[0] if isinstance(launched, tuple) else bool(launched)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, Signal, Qt
|
from PySide6.QtCore import QObject, Signal, Qt
|
||||||
@@ -39,8 +40,9 @@ class SimulationFileController(QObject):
|
|||||||
self.runtime_changed.emit()
|
self.runtime_changed.emit()
|
||||||
logger.info("Created new simulation")
|
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)
|
file_path = Path(path)
|
||||||
|
plot_tabs = deepcopy(self.root.plot_tabs) if preserve_plots and self.root is not None else None
|
||||||
try:
|
try:
|
||||||
if file_path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(file_path):
|
if file_path.suffix.lower() == ".bes" or simulation_files.is_simulation_json(file_path):
|
||||||
self.root = simulation_files.load(file_path)
|
self.root = simulation_files.load(file_path)
|
||||||
@@ -52,11 +54,20 @@ class SimulationFileController(QObject):
|
|||||||
except (OSError, RuntimeError, TypeError, ValueError):
|
except (OSError, RuntimeError, TypeError, ValueError):
|
||||||
logger.exception("Could not open simulation %s", file_path)
|
logger.exception("Could not open simulation %s", file_path)
|
||||||
raise
|
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._log_compilation()
|
||||||
self._update_window()
|
self._update_window()
|
||||||
self.runtime_changed.emit()
|
self.runtime_changed.emit()
|
||||||
logger.info("Opened simulation: %s", file_path)
|
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:
|
def open_dialog(self) -> None:
|
||||||
filename, _ = QFileDialog.getOpenFileName(self.window, "Open Simulation", "", "Simulation and BEdit files (*.bes *.beb *.json)")
|
filename, _ = QFileDialog.getOpenFileName(self.window, "Open Simulation", "", "Simulation and BEdit files (*.bes *.beb *.json)")
|
||||||
if not filename:
|
if not filename:
|
||||||
|
|||||||
57
src/bedit_gui/controllers/simulation_handoff_controller.py
Normal file
57
src/bedit_gui/controllers/simulation_handoff_controller.py
Normal file
@@ -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()
|
||||||
@@ -44,6 +44,10 @@ class SimulationRunController(QObject):
|
|||||||
self.run_failed.connect(self._failed)
|
self.run_failed.connect(self._failed)
|
||||||
self._load_session()
|
self._load_session()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
return self._running
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
if self._running or self.session is None:
|
if self._running or self.session is None:
|
||||||
return
|
return
|
||||||
|
|||||||
72
src/bedit_gui/services/simulation_handoff.py
Normal file
72
src/bedit_gui/services/simulation_handoff.py
Normal file
@@ -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)
|
||||||
@@ -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.log_controller import LogController
|
||||||
from bedit_gui.controllers.simulation_plot_controller import SimulationPlotController
|
from bedit_gui.controllers.simulation_plot_controller import SimulationPlotController
|
||||||
from bedit_gui.controllers.simulation_run_controller import SimulationRunController
|
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.simulation_models import CompiledModel
|
||||||
from bedit_gui.services.application_settings import SimulationApplicationSettings
|
from bedit_gui.services.application_settings import SimulationApplicationSettings
|
||||||
from bedit_gui.views.simulation_window import SimulationWindow
|
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)
|
LogController(window, settings.log_level)
|
||||||
run_controller = SimulationRunController(window, controller)
|
run_controller = SimulationRunController(window, controller)
|
||||||
plot_controller = SimulationPlotController(window, controller)
|
plot_controller = SimulationPlotController(window, controller)
|
||||||
|
SimulationHandoffController(window, controller, run_controller)
|
||||||
run_controller.simulation_state_changed.connect(plot_controller.refresh_results)
|
run_controller.simulation_state_changed.connect(plot_controller.refresh_results)
|
||||||
|
|
||||||
window.showMaximized()
|
window.showMaximized()
|
||||||
|
|||||||
Reference in New Issue
Block a user