Skip to content

Commit df567aa

Browse files
committed
merge OMCSessionZMQ into OMCProcess; compatibility class for OMCSessionZMQ
1 parent 4c89099 commit df567aa

1 file changed

Lines changed: 193 additions & 102 deletions

File tree

OMPython/OMCSession.py

Lines changed: 193 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ class OMCPathReal(pathlib.PurePosixPath):
293293
errors as well as usage on a Windows system due to slightly different definitions (PureWindowsPath).
294294
"""
295295

296-
def __init__(self, *path, session: OMCSessionZMQ) -> None:
296+
def __init__(self, *path, session: OMCProcess) -> None:
297297
super().__init__(*path)
298298
self._session = session
299299

@@ -539,7 +539,117 @@ def get_cmd(self) -> list[str]:
539539

540540
class OMCSessionZMQ:
541541
"""
542-
This class is handling an OMC session.
542+
This class is handling an OMC session. It is a compatibility class for the new schema using OMCProcess* classes.
543+
"""
544+
545+
def __init__(
546+
self,
547+
timeout: float = 10.00,
548+
omhome: Optional[str] = None,
549+
omc_process: Optional[OMCProcess] = None,
550+
) -> None:
551+
"""
552+
Initialisation for OMCSessionZMQ
553+
"""
554+
warnings.warn(message="The class OMCSessionZMQ is depreciated and will be removed in future versions; "
555+
"please use OMCProcess* classes instead!",
556+
category=DeprecationWarning,
557+
stacklevel=2)
558+
559+
if omc_process is None:
560+
omc_process = OMCProcessLocal(omhome=omhome, timeout=timeout)
561+
elif not isinstance(omc_process, OMCProcess):
562+
raise OMCSessionException("Invalid definition of the OMC process!")
563+
self.omc_process = omc_process
564+
565+
def __del__(self):
566+
del self.omc_process
567+
568+
@staticmethod
569+
def escape_str(value: str) -> str:
570+
"""
571+
Escape a string such that it can be used as string within OMC expressions, i.e. escape all double quotes.
572+
"""
573+
return OMCProcess.escape_str(value=value)
574+
575+
def omcpath(self, *path) -> OMCPath:
576+
"""
577+
Create an OMCPath object based on the given path segments and the current OMC session.
578+
"""
579+
return self.omc_process.omcpath(path)
580+
581+
def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:
582+
"""
583+
Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all
584+
filesystem related access.
585+
"""
586+
return self.omc_process.omcpath_tempdir(tempdir_base=tempdir_base)
587+
588+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
589+
"""
590+
Modify data based on the selected OMCProcess implementation.
591+
592+
Needs to be implemented in the subclasses.
593+
"""
594+
return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data)
595+
596+
@staticmethod
597+
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
598+
"""
599+
Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to
600+
keep instances of over classes around.
601+
"""
602+
return OMCProcess.run_model_executable(cmd_run_data=cmd_run_data)
603+
604+
def sendExpression(self, command: str, parsed: bool = True) -> Any:
605+
"""
606+
Send an expression to the OMC server and return the result.
607+
608+
The complete error handling of the OMC result is done within this method using '"getMessagesStringInternal()'.
609+
Caller should only check for OMCSessionException.
610+
"""
611+
return self.omc_process.sendExpression(command=command, parsed=parsed)
612+
613+
614+
class PostInitCaller(type):
615+
"""
616+
Metaclass definition to define a new function __post_init__() which is called after all __init__() functions where
617+
executed. The workflow would read as follows:
618+
619+
On creating a class with the following inheritance Class2 => Class1 => Class0, where each class calls the __init__()
620+
functions of its parent, i.e. super().__init__(), as well as __post_init__() the call schema would be:
621+
622+
myclass = Class2()
623+
Class2.__init__()
624+
Class1.__init__()
625+
Class0.__init__()
626+
Class2.__post_init__() <= this is done due to the metaclass
627+
Class1.__post_init__()
628+
Class0.__post_init__()
629+
630+
References:
631+
* https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python
632+
* https://stackoverflow.com/questions/795190/how-to-perform-common-post-initialization-tasks-in-inherited-classes
633+
"""
634+
635+
def __call__(cls, *args, **kwargs):
636+
obj = type.__call__(cls, *args, **kwargs)
637+
obj.__post_init__()
638+
return obj
639+
640+
641+
class OMCProcessMeta(abc.ABCMeta, PostInitCaller):
642+
"""
643+
Helper class to get a combined metaclass of ABCMeta and PostInitCaller.
644+
645+
References:
646+
* https://stackoverflow.com/questions/11276037/resolving-metaclass-conflicts
647+
"""
648+
649+
650+
class OMCProcess(metaclass=OMCProcessMeta):
651+
"""
652+
Base class for an OMC session. This class contains common functionality for all OMC sessions.
543653
544654
The main method is sendExpression() which is used to send commands to the OMC process.
545655
@@ -561,22 +671,48 @@ class OMCSessionZMQ:
561671
def __init__(
562672
self,
563673
timeout: float = 10.00,
564-
omhome: Optional[str] = None,
565-
omc_process: Optional[OMCProcess] = None,
674+
**kwargs,
566675
) -> None:
567676
"""
568-
Initialisation for OMCSessionZMQ
677+
Initialisation for OMCProcess
569678
"""
570679

680+
# store variables
571681
self._timeout = timeout
682+
# generate a random string for this session
683+
self._random_string = uuid.uuid4().hex
684+
# get a temporary directory
685+
self._temp_dir = pathlib.Path(tempfile.gettempdir())
572686

573-
if omc_process is None:
574-
omc_process = OMCProcessLocal(omhome=omhome, timeout=timeout)
575-
elif not isinstance(omc_process, OMCProcess):
576-
raise OMCSessionException("Invalid definition of the OMC process!")
577-
self.omc_process = omc_process
687+
# omc process
688+
self._omc_process: Optional[subprocess.Popen] = None
689+
# omc ZMQ port to use
690+
self._omc_port: Optional[str] = None
691+
# omc port and log file
692+
self._omc_filebase = f"openmodelica.{self._random_string}"
693+
# ZMQ socket to communicate with OMC
694+
self._omc_zmq: Optional[zmq.Socket[bytes]] = None
695+
696+
# setup log file - this file must be closed in the destructor
697+
logfile = self._temp_dir / (self._omc_filebase + ".log")
698+
self._omc_loghandle: Optional[io.TextIOWrapper] = None
699+
try:
700+
self._omc_loghandle = open(file=logfile, mode="w+", encoding="utf-8")
701+
except OSError as ex:
702+
raise OMCSessionException(f"Cannot open log file {logfile}.") from ex
578703

579-
port = self.omc_process.get_port()
704+
# variables to store compiled re expressions use in self.sendExpression()
705+
self._re_log_entries: Optional[re.Pattern[str]] = None
706+
self._re_log_raw: Optional[re.Pattern[str]] = None
707+
708+
self._re_portfile_path = re.compile(pattern=r'\nDumped server port in file: (.*?)($|\n)',
709+
flags=re.MULTILINE | re.DOTALL)
710+
711+
def __post_init__(self) -> None:
712+
"""
713+
Create the connection to the OMC server using ZeroMQ.
714+
"""
715+
port = self.get_port()
580716
if not isinstance(port, str):
581717
raise OMCSessionException(f"Invalid content for port: {port}")
582718

@@ -587,22 +723,36 @@ def __init__(
587723
omc.setsockopt(zmq.IMMEDIATE, True) # Queue messages only to completed connections
588724
omc.connect(port)
589725

590-
self.omc_zmq: Optional[zmq.Socket[bytes]] = omc
591-
592-
# variables to store compiled re expressions use in self.sendExpression()
593-
self._re_log_entries: Optional[re.Pattern[str]] = None
594-
self._re_log_raw: Optional[re.Pattern[str]] = None
726+
self._omc_zmq = omc
595727

596728
def __del__(self):
597-
if isinstance(self.omc_zmq, zmq.Socket):
729+
if isinstance(self._omc_zmq, zmq.Socket):
598730
try:
599731
self.sendExpression("quit()")
600732
except OMCSessionException:
601733
pass
734+
finally:
735+
self._omc_zmq = None
602736

603-
del self.omc_zmq
737+
if self._omc_loghandle is not None:
738+
try:
739+
self._omc_loghandle.close()
740+
except (OSError, IOError):
741+
pass
742+
finally:
743+
self._omc_loghandle = None
604744

605-
self.omc_zmq = None
745+
if isinstance(self._omc_process, subprocess.Popen):
746+
try:
747+
self._omc_process.wait(timeout=2.0)
748+
except subprocess.TimeoutExpired:
749+
if self._omc_process:
750+
logger.warning("OMC did not exit after being sent the quit() command; "
751+
"killing the process with pid=%s", self._omc_process.pid)
752+
self._omc_process.kill()
753+
self._omc_process.wait()
754+
finally:
755+
self._omc_process = None
606756

607757
@staticmethod
608758
def escape_str(value: str) -> str:
@@ -618,7 +768,7 @@ def omcpath(self, *path) -> OMCPath:
618768

619769
# fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement
620770
if sys.version_info < (3, 12):
621-
if isinstance(self.omc_process, OMCProcessLocal):
771+
if isinstance(self, OMCProcessLocal):
622772
# noinspection PyArgumentList
623773
return OMCPath(*path)
624774
raise OMCSessionException("OMCPath is supported for Python < 3.12 only if OMCProcessLocal is used!")
@@ -655,14 +805,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:
655805

656806
return tempdir
657807

658-
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
659-
"""
660-
Modify data based on the selected OMCProcess implementation.
661-
662-
Needs to be implemented in the subclasses.
663-
"""
664-
return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data)
665-
666808
@staticmethod
667809
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
668810
"""
@@ -715,29 +857,41 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
715857
The complete error handling of the OMC result is done within this method using '"getMessagesStringInternal()'.
716858
Caller should only check for OMCSessionException.
717859
"""
718-
if self.omc_zmq is None:
719-
raise OMCSessionException("No OMC running. Create a new instance of OMCProcess!")
860+
861+
# this is needed if the class is not fully initialized or in the process of deletion
862+
if hasattr(self, '_timeout'):
863+
timeout = self._timeout
864+
else:
865+
timeout = 1.0
866+
867+
if self._omc_zmq is None:
868+
raise OMCSessionException("No OMC running. Please create a new instance of OMCProcess!")
720869

721870
logger.debug("sendExpression(%r, parsed=%r)", command, parsed)
722871

723872
attempts = 0
724873
while True:
725874
try:
726-
self.omc_zmq.send_string(str(command), flags=zmq.NOBLOCK)
875+
self._omc_zmq.send_string(str(command), flags=zmq.NOBLOCK)
727876
break
728877
except zmq.error.Again:
729878
pass
730879
attempts += 1
731880
if attempts >= 50:
732-
raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}). "
733-
f"Log-file says: \n{self.omc_process.get_log()}")
734-
time.sleep(self._timeout / 50.0)
881+
# in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked
882+
try:
883+
log_content = self.get_log()
884+
except OMCSessionException:
885+
log_content = 'log not available'
886+
raise OMCSessionException(f"No connection with OMC (timeout={timeout}). "
887+
f"Log-file says: \n{log_content}")
888+
time.sleep(timeout / 50.0)
735889
if command == "quit()":
736-
self.omc_zmq.close()
737-
self.omc_zmq = None
890+
self._omc_zmq.close()
891+
self._omc_zmq = None
738892
return None
739893

740-
result = self.omc_zmq.recv_string()
894+
result = self._omc_zmq.recv_string()
741895

742896
if result.startswith('Error occurred building AST'):
743897
raise OMCSessionException(f"OMC error: {result}")
@@ -755,8 +909,8 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
755909
return result
756910

757911
# always check for error
758-
self.omc_zmq.send_string('getMessagesStringInternal()', flags=zmq.NOBLOCK)
759-
error_raw = self.omc_zmq.recv_string()
912+
self._omc_zmq.send_string('getMessagesStringInternal()', flags=zmq.NOBLOCK)
913+
error_raw = self._omc_zmq.recv_string()
760914
# run error handling only if there is something to check
761915
msg_long_list = []
762916
has_error = False
@@ -839,69 +993,6 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
839993
except (TypeError, UnboundLocalError) as ex2:
840994
raise OMCSessionException("Cannot parse OMC result") from ex2
841995

842-
843-
class OMCProcess(metaclass=abc.ABCMeta):
844-
"""
845-
Metaclass to be used by all OMCProcess* implementations. The main task is the evaluation of the port to be used to
846-
connect to the selected OMC process (method get_port()). Besides that, any implementation should define the method
847-
omc_run_data_update() to finalize the definition of an OMC simulation.
848-
"""
849-
850-
def __init__(
851-
self,
852-
timeout: float = 10.00,
853-
**kwargs,
854-
) -> None:
855-
super().__init__(**kwargs)
856-
857-
# store variables
858-
self._timeout = timeout
859-
860-
# omc process
861-
self._omc_process: Optional[subprocess.Popen] = None
862-
# omc ZMQ port to use
863-
self._omc_port: Optional[str] = None
864-
865-
# generate a random string for this session
866-
self._random_string = uuid.uuid4().hex
867-
868-
# omc port and log file
869-
self._omc_filebase = f"openmodelica.{self._random_string}"
870-
871-
# get a temporary directory
872-
self._temp_dir = pathlib.Path(tempfile.gettempdir())
873-
874-
# setup log file - this file must be closed in the destructor
875-
logfile = self._temp_dir / (self._omc_filebase + ".log")
876-
self._omc_loghandle: Optional[io.TextIOWrapper] = None
877-
try:
878-
self._omc_loghandle = open(file=logfile, mode="w+", encoding="utf-8")
879-
except OSError as ex:
880-
raise OMCSessionException(f"Cannot open log file {logfile}.") from ex
881-
882-
self._re_portfile_path = re.compile(pattern=r'\nDumped server port in file: (.*?)($|\n)',
883-
flags=re.MULTILINE | re.DOTALL)
884-
885-
def __del__(self):
886-
if self._omc_loghandle is not None:
887-
try:
888-
self._omc_loghandle.close()
889-
except (OSError, IOError):
890-
pass
891-
self._omc_loghandle = None
892-
893-
if isinstance(self._omc_process, subprocess.Popen):
894-
try:
895-
self._omc_process.wait(timeout=2.0)
896-
except subprocess.TimeoutExpired:
897-
if self._omc_process:
898-
logger.warning("OMC did not exit after being sent the quit() command; "
899-
"killing the process with pid=%s", self._omc_process.pid)
900-
self._omc_process.kill()
901-
self._omc_process.wait()
902-
finally:
903-
self._omc_process = None
904-
905996
def get_port(self) -> Optional[str]:
906997
"""
907998
Get the port to connect to the OMC process.

0 commit comments

Comments
 (0)