Skip to content

Commit d590e64

Browse files
authored
[OMCSessionZMQ] merge into OMCProcess (#381)
* merge OMCSessionZMQ into OMCProcess; compatibility class for OMCSessionZMQ * [OMCSessionZMQ] fix omcpath() * [OMCSessionZMQ] add missing execute()
1 parent 4c89099 commit d590e64

File tree

1 file changed

+196
-102
lines changed

1 file changed

+196
-102
lines changed

OMPython/OMCSession.py

Lines changed: 196 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,120 @@ 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 execute(self, command: str):
605+
return self.omc_process.execute(command=command)
606+
607+
def sendExpression(self, command: str, parsed: bool = True) -> Any:
608+
"""
609+
Send an expression to the OMC server and return the result.
610+
611+
The complete error handling of the OMC result is done within this method using '"getMessagesStringInternal()'.
612+
Caller should only check for OMCSessionException.
613+
"""
614+
return self.omc_process.sendExpression(command=command, parsed=parsed)
615+
616+
617+
class PostInitCaller(type):
618+
"""
619+
Metaclass definition to define a new function __post_init__() which is called after all __init__() functions where
620+
executed. The workflow would read as follows:
621+
622+
On creating a class with the following inheritance Class2 => Class1 => Class0, where each class calls the __init__()
623+
functions of its parent, i.e. super().__init__(), as well as __post_init__() the call schema would be:
624+
625+
myclass = Class2()
626+
Class2.__init__()
627+
Class1.__init__()
628+
Class0.__init__()
629+
Class2.__post_init__() <= this is done due to the metaclass
630+
Class1.__post_init__()
631+
Class0.__post_init__()
632+
633+
References:
634+
* https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python
635+
* https://stackoverflow.com/questions/795190/how-to-perform-common-post-initialization-tasks-in-inherited-classes
636+
"""
637+
638+
def __call__(cls, *args, **kwargs):
639+
obj = type.__call__(cls, *args, **kwargs)
640+
obj.__post_init__()
641+
return obj
642+
643+
644+
class OMCProcessMeta(abc.ABCMeta, PostInitCaller):
645+
"""
646+
Helper class to get a combined metaclass of ABCMeta and PostInitCaller.
647+
648+
References:
649+
* https://stackoverflow.com/questions/11276037/resolving-metaclass-conflicts
650+
"""
651+
652+
653+
class OMCProcess(metaclass=OMCProcessMeta):
654+
"""
655+
Base class for an OMC session. This class contains common functionality for all OMC sessions.
543656
544657
The main method is sendExpression() which is used to send commands to the OMC process.
545658
@@ -561,22 +674,48 @@ class OMCSessionZMQ:
561674
def __init__(
562675
self,
563676
timeout: float = 10.00,
564-
omhome: Optional[str] = None,
565-
omc_process: Optional[OMCProcess] = None,
677+
**kwargs,
566678
) -> None:
567679
"""
568-
Initialisation for OMCSessionZMQ
680+
Initialisation for OMCProcess
569681
"""
570682

683+
# store variables
571684
self._timeout = timeout
685+
# generate a random string for this session
686+
self._random_string = uuid.uuid4().hex
687+
# get a temporary directory
688+
self._temp_dir = pathlib.Path(tempfile.gettempdir())
572689

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
690+
# omc process
691+
self._omc_process: Optional[subprocess.Popen] = None
692+
# omc ZMQ port to use
693+
self._omc_port: Optional[str] = None
694+
# omc port and log file
695+
self._omc_filebase = f"openmodelica.{self._random_string}"
696+
# ZMQ socket to communicate with OMC
697+
self._omc_zmq: Optional[zmq.Socket[bytes]] = None
698+
699+
# setup log file - this file must be closed in the destructor
700+
logfile = self._temp_dir / (self._omc_filebase + ".log")
701+
self._omc_loghandle: Optional[io.TextIOWrapper] = None
702+
try:
703+
self._omc_loghandle = open(file=logfile, mode="w+", encoding="utf-8")
704+
except OSError as ex:
705+
raise OMCSessionException(f"Cannot open log file {logfile}.") from ex
578706

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

@@ -587,22 +726,36 @@ def __init__(
587726
omc.setsockopt(zmq.IMMEDIATE, True) # Queue messages only to completed connections
588727
omc.connect(port)
589728

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
729+
self._omc_zmq = omc
595730

596731
def __del__(self):
597-
if isinstance(self.omc_zmq, zmq.Socket):
732+
if isinstance(self._omc_zmq, zmq.Socket):
598733
try:
599734
self.sendExpression("quit()")
600735
except OMCSessionException:
601736
pass
737+
finally:
738+
self._omc_zmq = None
602739

603-
del self.omc_zmq
740+
if self._omc_loghandle is not None:
741+
try:
742+
self._omc_loghandle.close()
743+
except (OSError, IOError):
744+
pass
745+
finally:
746+
self._omc_loghandle = None
604747

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

607760
@staticmethod
608761
def escape_str(value: str) -> str:
@@ -618,7 +771,7 @@ def omcpath(self, *path) -> OMCPath:
618771

619772
# fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement
620773
if sys.version_info < (3, 12):
621-
if isinstance(self.omc_process, OMCProcessLocal):
774+
if isinstance(self, OMCProcessLocal):
622775
# noinspection PyArgumentList
623776
return OMCPath(*path)
624777
raise OMCSessionException("OMCPath is supported for Python < 3.12 only if OMCProcessLocal is used!")
@@ -655,14 +808,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:
655808

656809
return tempdir
657810

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-
666811
@staticmethod
667812
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
668813
"""
@@ -715,29 +860,41 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
715860
The complete error handling of the OMC result is done within this method using '"getMessagesStringInternal()'.
716861
Caller should only check for OMCSessionException.
717862
"""
718-
if self.omc_zmq is None:
719-
raise OMCSessionException("No OMC running. Create a new instance of OMCProcess!")
863+
864+
# this is needed if the class is not fully initialized or in the process of deletion
865+
if hasattr(self, '_timeout'):
866+
timeout = self._timeout
867+
else:
868+
timeout = 1.0
869+
870+
if self._omc_zmq is None:
871+
raise OMCSessionException("No OMC running. Please create a new instance of OMCProcess!")
720872

721873
logger.debug("sendExpression(%r, parsed=%r)", command, parsed)
722874

723875
attempts = 0
724876
while True:
725877
try:
726-
self.omc_zmq.send_string(str(command), flags=zmq.NOBLOCK)
878+
self._omc_zmq.send_string(str(command), flags=zmq.NOBLOCK)
727879
break
728880
except zmq.error.Again:
729881
pass
730882
attempts += 1
731883
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)
884+
# in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked
885+
try:
886+
log_content = self.get_log()
887+
except OMCSessionException:
888+
log_content = 'log not available'
889+
raise OMCSessionException(f"No connection with OMC (timeout={timeout}). "
890+
f"Log-file says: \n{log_content}")
891+
time.sleep(timeout / 50.0)
735892
if command == "quit()":
736-
self.omc_zmq.close()
737-
self.omc_zmq = None
893+
self._omc_zmq.close()
894+
self._omc_zmq = None
738895
return None
739896

740-
result = self.omc_zmq.recv_string()
897+
result = self._omc_zmq.recv_string()
741898

742899
if result.startswith('Error occurred building AST'):
743900
raise OMCSessionException(f"OMC error: {result}")
@@ -755,8 +912,8 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
755912
return result
756913

757914
# always check for error
758-
self.omc_zmq.send_string('getMessagesStringInternal()', flags=zmq.NOBLOCK)
759-
error_raw = self.omc_zmq.recv_string()
915+
self._omc_zmq.send_string('getMessagesStringInternal()', flags=zmq.NOBLOCK)
916+
error_raw = self._omc_zmq.recv_string()
760917
# run error handling only if there is something to check
761918
msg_long_list = []
762919
has_error = False
@@ -839,69 +996,6 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
839996
except (TypeError, UnboundLocalError) as ex2:
840997
raise OMCSessionException("Cannot parse OMC result") from ex2
841998

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-
905999
def get_port(self) -> Optional[str]:
9061000
"""
9071001
Get the port to connect to the OMC process.

0 commit comments

Comments
 (0)