Hooked up simulation to BEsim
This commit is contained in:
@@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -27,6 +29,10 @@ class OpenModelicaError(RuntimeError):
|
||||
"""Raised when OMC cannot start or reports a failure."""
|
||||
|
||||
|
||||
class SimulationCancelledError(RuntimeError):
|
||||
"""Raised when a running compiled simulation is cancelled."""
|
||||
|
||||
|
||||
ProcessExecutor = Callable[
|
||||
[Sequence[str], Path, float | None, Mapping[str, str] | None],
|
||||
ProcessResult,
|
||||
@@ -89,6 +95,58 @@ class OpenModelicaRunner:
|
||||
"""Run OMC on an asyncio worker thread."""
|
||||
return await _run_on_worker(self._run, script, working_directory)
|
||||
|
||||
def _run_cancellable(self, script: Path, working_directory: Path, cancel_event: Event) -> ProcessResult:
|
||||
command = (*self.command, str(script.resolve()))
|
||||
return self._run_cancellable_process(command, working_directory, cancel_event)
|
||||
|
||||
def _run_cancellable_process(self, command: Sequence[str], working_directory: Path, cancel_event: Event) -> ProcessResult:
|
||||
process_environment = None
|
||||
if self.environment is not None:
|
||||
process_environment = {**os.environ, **self.environment}
|
||||
try:
|
||||
process = subprocess.Popen(list(command), cwd=working_directory, env=process_environment, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=os.name != "nt")
|
||||
except OSError as exc:
|
||||
raise OpenModelicaError(f"simulation executable could not start: {exc}") from exc
|
||||
deadline = time.monotonic() + self.timeout if self.timeout is not None else None
|
||||
while True:
|
||||
if cancel_event.is_set():
|
||||
_terminate_process(process)
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
_kill_process(process)
|
||||
stdout, stderr = process.communicate()
|
||||
raise SimulationCancelledError("simulation was cancelled")
|
||||
if deadline is not None and time.monotonic() >= deadline:
|
||||
_kill_process(process)
|
||||
process.communicate()
|
||||
raise OpenModelicaError(f"simulation timed out after {self.timeout} seconds")
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=0.05)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
continue
|
||||
result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr)
|
||||
if result.return_code != 0:
|
||||
details = result.stderr.strip() or result.stdout.strip()
|
||||
suffix = f": {details}" if details else ""
|
||||
raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}")
|
||||
return result
|
||||
|
||||
|
||||
def _terminate_process(process: subprocess.Popen[str]) -> None:
|
||||
if os.name == "nt":
|
||||
process.terminate()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
|
||||
|
||||
def _kill_process(process: subprocess.Popen[str]) -> None:
|
||||
if os.name == "nt":
|
||||
process.kill()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
|
||||
|
||||
def _command_parts(command: str | Sequence[str]) -> tuple[str, ...]:
|
||||
if isinstance(command, str):
|
||||
@@ -140,7 +198,7 @@ async def _run_on_worker(
|
||||
def invoke() -> None:
|
||||
try:
|
||||
results.append(operation(*args, **kwargs))
|
||||
except BaseException as error:
|
||||
except Exception as error: # noqa: BLE001 - worker must relay operation failures
|
||||
errors.append(error)
|
||||
finally:
|
||||
finished.set()
|
||||
|
||||
Reference in New Issue
Block a user