Single omc session
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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 +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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)});",
|
||||
|
||||
Reference in New Issue
Block a user