Skip to content

Commit 84507f2

Browse files
authored
Merge branch 'master' into check_lintime
2 parents f8aa836 + 7ba2bea commit 84507f2

4 files changed

Lines changed: 103 additions & 83 deletions

File tree

.github/workflows/Test.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ jobs:
1414
timeout-minutes: 30
1515
strategy:
1616
matrix:
17-
python-version: ['3.10', '3.12', '3.13']
17+
# test for:
18+
# * oldest supported version
19+
# * latest available Python version
20+
python-version: ['3.10', '3.14']
21+
# * Linux using ubuntu-latest
22+
# * Windows using windows-latest
1823
os: ['ubuntu-latest', 'windows-latest']
24+
# * OM stable - latest stable version
25+
# * OM nightly - latest nightly build
1926
omc-version: ['stable', 'nightly']
2027

2128
steps:

OMPython/ModelicaSystem.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1800,7 +1800,7 @@ def linearize(
18001800
)
18011801

18021802
if self._inputs:
1803-
for key, data in self._inputs.items():
1803+
for data in self._inputs.values():
18041804
if data is not None:
18051805
for value in data:
18061806
if value[0] < float(self._simulate_options["startTime"]):

OMPython/OMCSession.py

Lines changed: 91 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,6 @@ def getClassComment(self, className):
171171
logger.warning("Method 'getClassComment(%s)' failed; OMTypedParser error: %s",
172172
className, ex.msg)
173173
return 'No description available'
174-
except OMCSessionException:
175-
raise
176174

177175
def getNthComponent(self, className, comp_id):
178176
""" returns with (type, name, description) """
@@ -201,8 +199,6 @@ def getParameterNames(self, className):
201199
logger.warning('OMPython error: %s', ex)
202200
# FIXME: OMC returns with a different structure for empty parameter set
203201
return []
204-
except OMCSessionException:
205-
raise
206202

207203
def getParameterValue(self, className, parameterName):
208204
try:
@@ -211,8 +207,6 @@ def getParameterValue(self, className, parameterName):
211207
logger.warning("Method 'getParameterValue(%s, %s)' failed; OMTypedParser error: %s",
212208
className, parameterName, ex.msg)
213209
return ""
214-
except OMCSessionException:
215-
raise
216210

217211
def getComponentModifierNames(self, className, componentName):
218212
return self._ask(question='getComponentModifierNames', opt=[className, componentName])
@@ -488,8 +482,6 @@ class OMCSessionRunData:
488482
cmd_model_executable: Optional[str] = None
489483
# additional library search path; this is mainly needed if OMCProcessLocal is run on Windows
490484
cmd_library_path: Optional[str] = None
491-
# command timeout
492-
cmd_timeout: Optional[float] = 10.0
493485

494486
# working directory to be used on the *local* system
495487
cmd_cwd_local: Optional[str] = None
@@ -564,13 +556,12 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunD
564556
"""
565557
return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data)
566558

567-
@staticmethod
568-
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
559+
def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int:
569560
"""
570561
Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to
571562
keep instances of over classes around.
572563
"""
573-
return OMCSession.run_model_executable(cmd_run_data=cmd_run_data)
564+
return self.omc_process.run_model_executable(cmd_run_data=cmd_run_data)
574565

575566
def execute(self, command: str):
576567
return self.omc_process.execute(command=command)
@@ -667,12 +658,12 @@ def __init__(
667658
self._omc_zmq: Optional[zmq.Socket[bytes]] = None
668659

669660
# setup log file - this file must be closed in the destructor
670-
logfile = self._temp_dir / (self._omc_filebase + ".log")
661+
self._omc_logfile = self._temp_dir / (self._omc_filebase + ".log")
671662
self._omc_loghandle: Optional[io.TextIOWrapper] = None
672663
try:
673-
self._omc_loghandle = open(file=logfile, mode="w+", encoding="utf-8")
664+
self._omc_loghandle = open(file=self._omc_logfile, mode="w+", encoding="utf-8")
674665
except OSError as ex:
675-
raise OMCSessionException(f"Cannot open log file {logfile}.") from ex
666+
raise OMCSessionException(f"Cannot open log file {self._omc_logfile}.") from ex
676667

677668
# variables to store compiled re expressions use in self.sendExpression()
678669
self._re_log_entries: Optional[re.Pattern[str]] = None
@@ -685,6 +676,9 @@ def __post_init__(self) -> None:
685676
"""
686677
Create the connection to the OMC server using ZeroMQ.
687678
"""
679+
# set_timeout() is used to define the value of _timeout as it includes additional checks
680+
self.set_timeout(timeout=self._timeout)
681+
688682
port = self.get_port()
689683
if not isinstance(port, str):
690684
raise OMCSessionException(f"Invalid content for port: {port}")
@@ -727,6 +721,44 @@ def __del__(self):
727721
finally:
728722
self._omc_process = None
729723

724+
def _timeout_loop(
725+
self,
726+
timeout: Optional[float] = None,
727+
timestep: float = 0.1,
728+
):
729+
"""
730+
Helper (using yield) for while loops to check OMC startup / response. The loop is executed as long as True is
731+
returned, i.e. the first False will stop the while loop.
732+
"""
733+
734+
if timeout is None:
735+
timeout = self._timeout
736+
if timeout <= 0:
737+
raise OMCSessionException(f"Invalid timeout: {timeout}")
738+
739+
timer = 0.0
740+
yield True
741+
while True:
742+
timer += timestep
743+
if timer > timeout:
744+
break
745+
time.sleep(timestep)
746+
yield True
747+
yield False
748+
749+
def set_timeout(self, timeout: Optional[float] = None) -> float:
750+
"""
751+
Set the timeout to be used for OMC communication (OMCSession).
752+
753+
The defined value is set and the current value is returned. If None is provided as argument, nothing is changed.
754+
"""
755+
retval = self._timeout
756+
if timeout is not None:
757+
if timeout <= 0.0:
758+
raise OMCSessionException(f"Invalid timeout value: {timeout}!")
759+
self._timeout = timeout
760+
return retval
761+
730762
@staticmethod
731763
def escape_str(value: str) -> str:
732764
"""
@@ -778,11 +810,9 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:
778810

779811
return tempdir
780812

781-
@staticmethod
782-
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
813+
def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int:
783814
"""
784-
Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to
785-
keep instances of over classes around.
815+
Run the command defined in cmd_run_data.
786816
"""
787817

788818
my_env = os.environ.copy()
@@ -799,7 +829,7 @@ def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
799829
text=True,
800830
env=my_env,
801831
cwd=cmd_run_data.cmd_cwd_local,
802-
timeout=cmd_run_data.cmd_timeout,
832+
timeout=self._timeout,
803833
check=True,
804834
)
805835
stdout = cmdres.stdout.strip()
@@ -833,34 +863,28 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
833863
Caller should only check for OMCSessionException.
834864
"""
835865

836-
# this is needed if the class is not fully initialized or in the process of deletion
837-
if hasattr(self, '_timeout'):
838-
timeout = self._timeout
839-
else:
840-
timeout = 1.0
841-
842866
if self._omc_zmq is None:
843867
raise OMCSessionException("No OMC running. Please create a new instance of OMCSession!")
844868

845869
logger.debug("sendExpression(%r, parsed=%r)", command, parsed)
846870

847-
attempts = 0
848-
while True:
871+
loop = self._timeout_loop(timestep=0.05)
872+
while next(loop):
849873
try:
850874
self._omc_zmq.send_string(str(command), flags=zmq.NOBLOCK)
851875
break
852876
except zmq.error.Again:
853877
pass
854-
attempts += 1
855-
if attempts >= 50:
856-
# in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked
857-
try:
858-
log_content = self.get_log()
859-
except OMCSessionException:
860-
log_content = 'log not available'
861-
raise OMCSessionException(f"No connection with OMC (timeout={timeout}). "
862-
f"Log-file says: \n{log_content}")
863-
time.sleep(timeout / 50.0)
878+
else:
879+
# in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked
880+
try:
881+
log_content = self.get_log()
882+
except OMCSessionException:
883+
log_content = 'log not available'
884+
885+
logger.error(f"OMC did not start. Log-file says:\n{log_content}")
886+
raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}).")
887+
864888
if command == "quit()":
865889
self._omc_zmq.close()
866890
self._omc_zmq = None
@@ -956,7 +980,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
956980
raise OMCSessionException(f"OMC error occurred for 'sendExpression({command}, {parsed}):\n"
957981
f"{msg_long_str}")
958982

959-
if parsed is False:
983+
if not parsed:
960984
return result
961985

962986
try:
@@ -1105,25 +1129,20 @@ def _omc_port_get(self) -> str:
11051129
port = None
11061130

11071131
# See if the omc server is running
1108-
attempts = 0
1109-
while True:
1132+
loop = self._timeout_loop(timestep=0.1)
1133+
while next(loop):
11101134
omc_portfile_path = self._get_portfile_path()
1111-
11121135
if omc_portfile_path is not None and omc_portfile_path.is_file():
11131136
# Read the port file
11141137
with open(file=omc_portfile_path, mode='r', encoding="utf-8") as f_p:
11151138
port = f_p.readline()
11161139
break
1117-
11181140
if port is not None:
11191141
break
1120-
1121-
attempts += 1
1122-
if attempts == 80.0:
1123-
raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}). "
1124-
f"Could not open file {omc_portfile_path}. "
1125-
f"Log-file says:\n{self.get_log()}")
1126-
time.sleep(self._timeout / 80.0)
1142+
else:
1143+
logger.error(f"OMC server did not start. Log-file says:\n{self.get_log()}")
1144+
raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}, "
1145+
f"logfile={repr(self._omc_logfile)}).")
11271146

11281147
logger.info(f"Local OMC Server is up and running at ZMQ port {port} "
11291148
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")
@@ -1204,8 +1223,8 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]:
12041223
if sys.platform == 'win32':
12051224
raise NotImplementedError("Docker not supported on win32!")
12061225

1207-
docker_process = None
1208-
for _ in range(0, 40):
1226+
loop = self._timeout_loop(timestep=0.2)
1227+
while next(loop):
12091228
docker_top = subprocess.check_output(["docker", "top", docker_cid]).decode().strip()
12101229
docker_process = None
12111230
for line in docker_top.split("\n"):
@@ -1216,10 +1235,11 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]:
12161235
except psutil.NoSuchProcess as ex:
12171236
raise OMCSessionException(f"Could not find PID {docker_top} - "
12181237
"is this a docker instance spawned without --pid=host?") from ex
1219-
12201238
if docker_process is not None:
12211239
break
1222-
time.sleep(self._timeout / 40.0)
1240+
else:
1241+
logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}")
1242+
raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).")
12231243

12241244
return docker_process
12251245

@@ -1241,8 +1261,8 @@ def _omc_port_get(self) -> str:
12411261
raise OMCSessionException(f"Invalid docker container ID: {self._docker_container_id}")
12421262

12431263
# See if the omc server is running
1244-
attempts = 0
1245-
while True:
1264+
loop = self._timeout_loop(timestep=0.1)
1265+
while next(loop):
12461266
omc_portfile_path = self._get_portfile_path()
12471267
if omc_portfile_path is not None:
12481268
try:
@@ -1253,16 +1273,12 @@ def _omc_port_get(self) -> str:
12531273
port = output.decode().strip()
12541274
except subprocess.CalledProcessError:
12551275
pass
1256-
12571276
if port is not None:
12581277
break
1259-
1260-
attempts += 1
1261-
if attempts == 80.0:
1262-
raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}). "
1263-
f"Could not open port file {omc_portfile_path}. "
1264-
f"Log-file says:\n{self.get_log()}")
1265-
time.sleep(self._timeout / 80.0)
1278+
else:
1279+
logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}")
1280+
raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}, "
1281+
f"logfile={repr(self._omc_logfile)}).")
12661282

12671283
logger.info(f"Docker based OMC Server is up and running at port {port}")
12681284

@@ -1430,25 +1446,24 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DockerPopen, str]:
14301446
raise OMCSessionException(f"Invalid content for docker container ID file path: {docker_cid_file}")
14311447

14321448
docker_cid = None
1433-
for _ in range(0, 40):
1449+
loop = self._timeout_loop(timestep=0.1)
1450+
while next(loop):
14341451
try:
14351452
with open(file=docker_cid_file, mode="r", encoding="utf-8") as fh:
14361453
docker_cid = fh.read().strip()
14371454
except IOError:
14381455
pass
1439-
if docker_cid:
1456+
if docker_cid is not None:
14401457
break
1441-
time.sleep(self._timeout / 40.0)
1442-
1443-
if docker_cid is None:
1458+
else:
14441459
logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}")
14451460
raise OMCSessionException(f"Docker did not start (timeout={self._timeout} might be too short "
14461461
"especially if you did not docker pull the image before this command).")
14471462

14481463
docker_process = self._docker_process_get(docker_cid=docker_cid)
14491464
if docker_process is None:
1450-
raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}. "
1451-
f"Log-file says:\n{self.get_log()}")
1465+
logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}")
1466+
raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}.")
14521467

14531468
return omc_process, docker_process, docker_cid
14541469

@@ -1600,12 +1615,11 @@ def _omc_process_get(self) -> subprocess.Popen:
16001615
return omc_process
16011616

16021617
def _omc_port_get(self) -> str:
1603-
omc_portfile_path: Optional[pathlib.Path] = None
16041618
port = None
16051619

16061620
# See if the omc server is running
1607-
attempts = 0
1608-
while True:
1621+
loop = self._timeout_loop(timestep=0.1)
1622+
while next(loop):
16091623
try:
16101624
omc_portfile_path = self._get_portfile_path()
16111625
if omc_portfile_path is not None:
@@ -1616,16 +1630,12 @@ def _omc_port_get(self) -> str:
16161630
port = output.decode().strip()
16171631
except subprocess.CalledProcessError:
16181632
pass
1619-
16201633
if port is not None:
16211634
break
1622-
1623-
attempts += 1
1624-
if attempts == 80.0:
1625-
raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}). "
1626-
f"Could not open port file {omc_portfile_path}. "
1627-
f"Log-file says:\n{self.get_log()}")
1628-
time.sleep(self._timeout / 80.0)
1635+
else:
1636+
logger.error(f"WSL based OMC server did not start. Log-file says:\n{self.get_log()}")
1637+
raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}, "
1638+
f"logfile={repr(self._omc_logfile)}).")
16291639

16301640
logger.info(f"WSL based OMC Server is up and running at ZMQ port {port} "
16311641
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")

0 commit comments

Comments
 (0)