Single omc session

This commit is contained in:
Joppe Blondel
2026-08-19 11:19:36 +02:00
parent 16d9cc7651
commit 6ebeb2607b
8 changed files with 113 additions and 18 deletions
+1
View File
@@ -11,6 +11,7 @@ requires-python = ">=3.10"
dependencies = [
"matplotlib>=3.8,<4",
"msgpack>=1.0,<2",
"OMPython>=3.6,<5",
"PySide6>=6.7,<7",
]
+4 -1
View File
@@ -21,6 +21,7 @@ from bedit_gui.services.application_settings import ApplicationSettings
from bedit_gui.services.clipboard import ClipboardService
from bedit_gui.views.main_window import MainWindow
from bedit_gui.versions import BEDIT_VERSION
from bedit_simulation import OpenModelicaRunner, compile_component_sync
def parse_arguments():
parser = argparse.ArgumentParser(exit_on_error=False)
@@ -46,6 +47,8 @@ def main() -> int:
app.setApplicationVersion(BEDIT_VERSION)
settings = ApplicationSettings()
omc_runner = OpenModelicaRunner()
app.aboutToQuit.connect(omc_runner.close)
document = Document(app)
window = MainWindow(settings.snap_to_grid_size)
window.ui.actionAbout.triggered.connect(lambda: QMessageBox.about(window, "About BEdit", f"BEdit {BEDIT_VERSION}"))
@@ -55,7 +58,7 @@ def main() -> int:
DocumentController(document, window)
settings_controller = SettingsController(window, settings)
SimulationSettingsController(document, window)
SimulationController(document, window)
SimulationController(document, window, compiler=lambda component, directory: compile_component_sync(component, directory, runner=omc_runner))
UndoController(document, window)
ViewMenuController(window)
document_tree_controller = DocumentTreeController(document, window)
@@ -13,6 +13,7 @@ from bedit_gui.services.application_logging import get_logger
from bedit_gui.simulation_models import CompiledModel, SimulationRoot
from bedit_gui.views.dialogs.simulation_settings_dialog import SimulationSettingsDialog
from bedit_gui.views.simulation_window import SimulationWindow
from bedit_simulation import OpenModelicaRunner
logger = get_logger(__name__)
@@ -20,9 +21,10 @@ logger = get_logger(__name__)
class SimulationFileController(QObject):
runtime_changed = Signal()
def __init__(self, window: SimulationWindow) -> None:
def __init__(self, window: SimulationWindow, runner: OpenModelicaRunner | None = None) -> None:
super().__init__(window)
self.window = window
self.runner = runner
self.root: SimulationRoot | None = None
self.compiled_model: CompiledModel | None = None
self.path: Path | None = None
@@ -147,14 +149,14 @@ class SimulationFileController(QObject):
def _compile_with_wait_cursor(self, path: Path, *, component: str | None, simulation: str | None) -> tuple[SimulationRoot, CompiledModel]:
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
return load_and_compile_bedit(path, component_selector=component, simulation_selector=simulation)
return load_and_compile_bedit(path, component_selector=component, simulation_selector=simulation, runner=self.runner)
finally:
QApplication.restoreOverrideCursor()
def _recompile_with_wait_cursor(self, root: SimulationRoot) -> CompiledModel:
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
return compile_simulation_root(root)
return compile_simulation_root(root, runner=self.runner)
finally:
QApplication.restoreOverrideCursor()
+7 -7
View File
@@ -7,7 +7,7 @@ from bedit_core.models import Component, ComponentID, Document, GraphImplementat
from bedit_gui.models import Simulation, SimulationMethod
from bedit_gui.services import document_files
from bedit_gui.simulation_models import CompiledModel, SimulationRoot
from bedit_simulation import compile_component_sync
from bedit_simulation import OpenModelicaRunner, compile_component_sync
def component_choices(document: Document) -> list[tuple[ComponentID, Component, str]]:
@@ -24,7 +24,7 @@ def component_choices(document: Document) -> list[tuple[ComponentID, Component,
return choices
def load_and_compile_bedit(path: str | Path, *, component_selector: str | None = None, simulation_selector: str | None = None, working_directory: str | Path | None = None, omc_command: str = "omc") -> tuple[SimulationRoot, CompiledModel]:
def load_and_compile_bedit(path: str | Path, *, component_selector: str | None = None, simulation_selector: str | None = None, working_directory: str | Path | None = None, omc_command: str = "omc", runner: OpenModelicaRunner | None = None) -> tuple[SimulationRoot, CompiledModel]:
"""Open a BEdit document, resolve one launch target, and compile it."""
if bool(component_selector) == bool(simulation_selector):
raise ValueError("specify exactly one component or simulation settings block")
@@ -57,16 +57,16 @@ def load_and_compile_bedit(path: str | Path, *, component_selector: str | None =
settings_name=settings_name,
settings=settings,
)
return root, _compile(component, build_directory, omc_command)
return root, _compile(component, build_directory, omc_command, runner)
def compile_simulation_root(root: SimulationRoot, *, working_directory: str | Path | None = None, omc_command: str = "omc") -> CompiledModel:
def compile_simulation_root(root: SimulationRoot, *, working_directory: str | Path | None = None, omc_command: str = "omc", runner: OpenModelicaRunner | None = None) -> CompiledModel:
if root.source_document is None:
raise ValueError("the simulation does not reference a BEdit source document and cannot be recompiled")
document = document_files.load(root.source_document)
_component_id, component, _component_path = _find_component(component_choices(document), str(root.component))
build_directory = Path(working_directory) if working_directory is not None else Path(tempfile.mkdtemp(prefix="bedit-compiled-"))
return _compile(component, build_directory, omc_command)
return _compile(component, build_directory, omc_command, runner)
def _find_component(choices: list[tuple[ComponentID, Component, str]], selector: str) -> tuple[ComponentID, Component, str]:
@@ -90,6 +90,6 @@ def _default_settings(component_id: ComponentID) -> Simulation:
)
def _compile(component: Component, working_directory: Path, omc_command: str) -> CompiledModel:
build = compile_component_sync(component, working_directory, omc_command=omc_command)
def _compile(component: Component, working_directory: Path, omc_command: str, runner: OpenModelicaRunner | None) -> CompiledModel:
build = compile_component_sync(component, working_directory, omc_command=omc_command, runner=runner)
return CompiledModel(build.model_name, str(build.executable.resolve()), str(build.executable.parent.resolve()), build.output, build.errors)
+4 -1
View File
@@ -14,6 +14,7 @@ from bedit_gui.simulation_models import CompiledModel
from bedit_gui.services.application_settings import SimulationApplicationSettings
from bedit_gui.views.simulation_window import SimulationWindow
from bedit_gui.versions import BESIM_VERSION
from bedit_simulation import OpenModelicaRunner
def parse_arguments(arguments: list[str] | None = None) -> argparse.Namespace:
@@ -46,7 +47,9 @@ def main(arguments: list[str] | None = None) -> int:
window = SimulationWindow()
window.ui.actionAbout.triggered.connect(lambda: QMessageBox.about(window, "About BEsim", f"BEsim {BESIM_VERSION}"))
window.ui.actionAbout_QT.triggered.connect(app.aboutQt)
controller = SimulationFileController(window)
omc_runner = OpenModelicaRunner()
app.aboutToQuit.connect(omc_runner.close)
controller = SimulationFileController(window, omc_runner)
settings = SimulationApplicationSettings()
LogController(window, settings.log_level)
+4 -4
View File
@@ -9,13 +9,13 @@ from .openmodelica import OpenModelicaRunner
from .simulation import ModelBuildResult, Simulation
async def compile_component(component: Component, working_directory: str | Path, *, omc_command: str = "omc", timeout: float | None = None) -> ModelBuildResult:
async def compile_component(component: Component, working_directory: str | Path, *, omc_command: str = "omc", timeout: float | None = None, runner: OpenModelicaRunner | None = None) -> ModelBuildResult:
"""Compose and compile a component without creating a GUI application."""
simulation = Simulation(OpenModelicaRunner(omc_command, timeout=timeout))
simulation = Simulation(runner or OpenModelicaRunner(omc_command, timeout=timeout))
await simulation.load(component)
return await simulation.compile(working_directory)
def compile_component_sync(component: Component, working_directory: str | Path, *, omc_command: str = "omc", timeout: float | None = None) -> ModelBuildResult:
def compile_component_sync(component: Component, working_directory: str | Path, *, omc_command: str = "omc", timeout: float | None = None, runner: OpenModelicaRunner | None = None) -> ModelBuildResult:
"""Synchronous entry point for scripts and non-async suite applications."""
return asyncio.run(compile_component(component, working_directory, omc_command=omc_command, timeout=timeout))
return asyncio.run(compile_component(component, working_directory, omc_command=omc_command, timeout=timeout, runner=runner))
+87 -2
View File
@@ -6,13 +6,15 @@ import asyncio
import os
import signal
import shlex
import shutil
import subprocess
import time
from collections.abc import Callable, Mapping, Sequence
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from threading import Event, Thread
from typing import Any, TypeVar
from threading import Event, Lock, Thread
from typing import Any, Protocol, Self, TypeVar
@dataclass(frozen=True)
@@ -48,6 +50,15 @@ ProcessExecutor = Callable[
_Result = TypeVar("_Result")
class InteractiveSession(Protocol):
"""Small part of the OMPython session API used by the runner."""
def sendExpression(self, command: str, parsed: bool = True) -> Any: ...
SessionFactory = Callable[[tuple[str, ...], float | None], InteractiveSession]
class OpenModelicaRunner:
"""Execute ``.mos`` scripts using an OMC command or wrapper script.
@@ -63,14 +74,22 @@ class OpenModelicaRunner:
timeout: float | None = None,
environment: Mapping[str, str] | None = None,
executor: ProcessExecutor | None = None,
session_factory: SessionFactory | None = None,
reuse_session: bool = True,
) -> None:
self.command = _command_parts(command)
self.timeout = timeout
self.environment = environment
self._executor = executor or _execute_process
self._session_factory = session_factory or _create_interactive_session
self._reuse_session = reuse_session and executor is None and environment is None and len(self.command) == 1
self._session: InteractiveSession | None = None
self._session_lock = Lock()
def _run(self, script: Path, working_directory: Path) -> ProcessResult:
"""Run ``script`` in ``working_directory`` and return captured output."""
if self._reuse_session:
return self._run_in_session(script, working_directory)
command = (*self.command, str(script.resolve()))
try:
result = self._executor(
@@ -95,6 +114,48 @@ class OpenModelicaRunner:
)
return result
def _run_in_session(self, script: Path, working_directory: Path) -> ProcessResult:
script_path = script.resolve()
expression = f"runScript({_modelica_string(str(script_path))})"
with self._session_lock:
try:
session = self._session
if session is None:
session = self._session_factory(self.command, self.timeout)
self._session = session
output = session.sendExpression(expression, parsed=False)
except Exception as exc: # OMPython exceptions are optional at import time
self._close_session_locked()
if isinstance(exc, OpenModelicaError):
raise
raise OpenModelicaError(f"OpenModelica session failed: {exc}") from exc
return ProcessResult(command=(*self.command, str(script_path)), return_code=0, stdout=str(output), stderr="")
@property
def session_running(self) -> bool:
"""Return whether this runner has started its reusable OMC session."""
with self._session_lock:
return self._session is not None
def close(self) -> None:
"""Stop the reusable OMC session, if it has been started."""
with self._session_lock:
self._close_session_locked()
def _close_session_locked(self) -> None:
session = self._session
self._session = None
if session is None:
return
with suppress(Exception):
session.sendExpression("quit()", parsed=False)
def __enter__(self) -> Self:
return self
def __exit__(self, *_args: object) -> None:
self.close()
async def run(
self,
script: Path,
@@ -167,6 +228,30 @@ def _command_parts(command: str | Sequence[str]) -> tuple[str, ...]:
return parts
def _modelica_string(value: str) -> str:
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def _create_interactive_session(command: tuple[str, ...], timeout: float | None) -> InteractiveSession:
if len(command) != 1:
raise OpenModelicaError("a reusable OpenModelica session requires a single OMC executable command")
executable = shutil.which(command[0])
if executable is None:
raise OpenModelicaError(f"OpenModelica command {command[0]!r} was not found")
try:
from OMPython import OMCSessionZMQ
except ImportError:
raise OpenModelicaError("reusable OpenModelica sessions require the OMPython package") from None
# Preserve wrapper symlinks: OMPython reconstructs ``<omhome>/bin/omc``.
omhome = str(Path(executable).parent.parent)
try:
if timeout is None:
return OMCSessionZMQ(omhome=omhome)
return OMCSessionZMQ(timeout=timeout, omhome=omhome)
except Exception as exc:
raise OpenModelicaError(f"OpenModelica session could not start: {exc}") from exc
def _execute_process(
command: Sequence[str],
working_directory: Path,
+1
View File
@@ -705,6 +705,7 @@ def _load_model_script(
])
return "\n".join(
[
"clear();",
f"cd({json.dumps(str(working_directory.resolve()))});",
*load_libraries,
f"loaded := loadFile({json.dumps(_MODEL_FILE)});",