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