Simulate with OMC
This commit is contained in:
153
src/bedit_simulation/openmodelica.py
Normal file
153
src/bedit_simulation/openmodelica.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Run OMC directly or through a user-provided wrapper command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Event, Thread
|
||||
from typing import Any, TypeVar
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProcessResult:
|
||||
"""Captured output from one OMC invocation."""
|
||||
|
||||
command: tuple[str, ...]
|
||||
return_code: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
|
||||
|
||||
class OpenModelicaError(RuntimeError):
|
||||
"""Raised when OMC cannot start or reports a failure."""
|
||||
|
||||
|
||||
ProcessExecutor = Callable[
|
||||
[Sequence[str], Path, float | None, Mapping[str, str] | None],
|
||||
ProcessResult,
|
||||
]
|
||||
_Result = TypeVar("_Result")
|
||||
|
||||
|
||||
class OpenModelicaRunner:
|
||||
"""Execute ``.mos`` scripts using an OMC command or wrapper script.
|
||||
|
||||
``command`` may be ``"omc"``, a path to a wrapper script, or a shell-like
|
||||
command prefix such as ``"docker run ... omc"``. It is split with
|
||||
:func:`shlex.split` and is never passed through a shell.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command: str | Sequence[str] = "omc",
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
environment: Mapping[str, str] | None = None,
|
||||
executor: ProcessExecutor | None = None,
|
||||
) -> None:
|
||||
self.command = _command_parts(command)
|
||||
self.timeout = timeout
|
||||
self.environment = environment
|
||||
self._executor = executor or _execute_process
|
||||
|
||||
def _run(self, script: Path, working_directory: Path) -> ProcessResult:
|
||||
"""Run ``script`` in ``working_directory`` and return captured output."""
|
||||
command = (*self.command, str(script.resolve()))
|
||||
try:
|
||||
result = self._executor(
|
||||
command,
|
||||
working_directory,
|
||||
self.timeout,
|
||||
self.environment,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise OpenModelicaError(
|
||||
f"OpenModelica command {self.command[0]!r} was not found"
|
||||
) from None
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise OpenModelicaError(
|
||||
f"OpenModelica timed out after {error.timeout} seconds"
|
||||
) from error
|
||||
if result.return_code != 0:
|
||||
details = result.stderr.strip() or result.stdout.strip()
|
||||
suffix = f": {details}" if details else ""
|
||||
raise OpenModelicaError(
|
||||
f"OpenModelica exited with status {result.return_code}{suffix}"
|
||||
)
|
||||
return result
|
||||
|
||||
async def run(
|
||||
self,
|
||||
script: Path,
|
||||
working_directory: Path,
|
||||
) -> ProcessResult:
|
||||
"""Run OMC on an asyncio worker thread."""
|
||||
return await _run_on_worker(self._run, script, working_directory)
|
||||
|
||||
|
||||
def _command_parts(command: str | Sequence[str]) -> tuple[str, ...]:
|
||||
if isinstance(command, str):
|
||||
expanded = os.path.expandvars(os.path.expanduser(command))
|
||||
parts = tuple(shlex.split(expanded))
|
||||
else:
|
||||
parts = tuple(map(str, command))
|
||||
if not parts:
|
||||
raise ValueError("OpenModelica command cannot be empty")
|
||||
return parts
|
||||
|
||||
|
||||
def _execute_process(
|
||||
command: Sequence[str],
|
||||
working_directory: Path,
|
||||
timeout: float | None,
|
||||
environment: Mapping[str, str] | None,
|
||||
) -> ProcessResult:
|
||||
process_environment = None
|
||||
if environment is not None:
|
||||
process_environment = {**os.environ, **environment}
|
||||
completed = subprocess.run(
|
||||
list(command),
|
||||
cwd=working_directory,
|
||||
env=process_environment,
|
||||
timeout=timeout,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
return ProcessResult(
|
||||
command=tuple(command),
|
||||
return_code=completed.returncode,
|
||||
stdout=completed.stdout,
|
||||
stderr=completed.stderr,
|
||||
)
|
||||
|
||||
|
||||
async def _run_on_worker(
|
||||
operation: Callable[..., _Result],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> _Result:
|
||||
"""Run a blocking callable on a thread and await it without Qt."""
|
||||
finished = Event()
|
||||
results: list[_Result] = []
|
||||
errors: list[BaseException] = []
|
||||
|
||||
def invoke() -> None:
|
||||
try:
|
||||
results.append(operation(*args, **kwargs))
|
||||
except BaseException as error:
|
||||
errors.append(error)
|
||||
finally:
|
||||
finished.set()
|
||||
|
||||
Thread(target=invoke, name="bedit-openmodelica", daemon=True).start()
|
||||
while not finished.is_set():
|
||||
await asyncio.sleep(0.01)
|
||||
if errors:
|
||||
raise errors[0]
|
||||
return results[0]
|
||||
Reference in New Issue
Block a user