from __future__ import annotations import sys import tempfile from collections.abc import Callable from pathlib import Path from PySide6.QtCore import QObject, QProcess, Qt from PySide6.QtWidgets import QApplication, QMessageBox from bedit_core.models import Component 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 logger = get_logger(__name__) Compiler = Callable[[Component, str | Path], ModelBuildResult] 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) class SimulationController(QObject): """Compile the active BEdit simulation and launch the simulator process.""" def __init__(self, document: Document, window: MainWindow, compiler: Compiler = compile_component_sync, launcher: Launcher = _launch_simulator) -> None: super().__init__(window) self.document = document self.window = window self.compiler = compiler self.launcher = launcher self.compiled_root: SimulationRoot | None = None self.compiled_model: CompiledModel | None = None window.ui.actionCompile_Model.triggered.connect(self.compile_model) window.ui.actionOpen_Simulation_Window.triggered.connect(self.open_simulation) def compile_model(self) -> SimulationRoot | None: database = self.document.simulation_database() settings = database.simulations.get(database.active_simulation) if database.active_simulation is not None else None if settings is None: QMessageBox.warning(self.window, "No active simulation", "Select and accept a simulation settings block first.") return None match = next((choice for choice in component_choices(self.document.model) if choice[0] == settings.component), None) if match is None: QMessageBox.critical(self.window, "Could not compile", "The active simulation references a missing component.") return None component_id, component, component_path = match build_directory = Path(tempfile.mkdtemp(prefix="bedit-compiled-")) QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) self.window.statusBar().showMessage(f"Compiling {component_path}…") logger.info("Compiling model: %s", component_path) try: self.document.infer_causality(component) build = self.compiler(component, build_directory) except (OSError, RuntimeError, ValueError) as exc: logger.error("Could not compile model %s:\n%s", component_path, exc) QMessageBox.critical(self.window, "Could not compile", str(exc)) self.window.statusBar().showMessage("Compilation failed") return None finally: QApplication.restoreOverrideCursor() self.compiled_model = CompiledModel(build.model_name, str(build.executable.resolve()), str(build.executable.parent.resolve()), build.output, build.errors) self.compiled_root = SimulationRoot( format_version=1, source_document=str(self.document.path.resolve()) if self.document.path is not None else None, source_document_id=str(self.document.model.id), component=component_id, component_path=component_path, settings_name=settings.name, settings=settings, ) self.window.statusBar().showMessage(f"Compiled {component_path}") logger.info("Compiled model %s: %s", build.model_name, self.compiled_model.executable) if build.output.strip(): logger.info("Compiler output:\n%s", build.output.strip()) if build.errors.strip(): logger.warning("Compiler errors:\n%s", build.errors.strip()) return self.compiled_root def open_simulation(self) -> None: root = self.compile_model() if root is None or self.compiled_model is None: return transfer_directory = Path(tempfile.mkdtemp(prefix="bedit-simulator-launch-")) transfer_path = transfer_directory / "simulation.bes" try: simulation_files.save(root, transfer_path) if not self.launcher(transfer_path, self.compiled_model): raise RuntimeError("the simulator process could not be started") except (OSError, RuntimeError, ValueError) as exc: QMessageBox.critical(self.window, "Could not open simulator", str(exc))