@@ -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
540540class 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