diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1a979647..d6a3d654 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -130,7 +130,6 @@ def __init__( session: OMCSessionZMQ, runpath: OMCPath, modelname: Optional[str] = None, - timeout: Optional[float] = None, ) -> None: if modelname is None: raise ModelicaSystemError("Missing model name!") @@ -138,7 +137,6 @@ def __init__( self._session = session self._runpath = runpath self._model_name = modelname - self._timeout = timeout # dictionaries of command line arguments for the model executable self._args: dict[str, str | None] = {} @@ -278,7 +276,6 @@ def definition(self) -> OMCSessionRunData: cmd_model_name=self._model_name, cmd_args=self.get_cmd_args(), cmd_result_path=result_file, - cmd_timeout=self._timeout, ) omc_run_data_updated = self._session.omc_run_data_update( @@ -575,7 +572,6 @@ def buildModel(self, variableFilter: Optional[str] = None): session=self._session, runpath=self.getWorkDirectory(), modelname=self._model_name, - timeout=5.0, ) # ... by running it - output help for command help om_cmd.arg_set(key="help", val="help") @@ -1058,7 +1054,6 @@ def simulate_cmd( result_file: OMCPath, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, - timeout: Optional[float] = None, ) -> ModelicaSystemCmd: """ This method prepares the simulates model according to the simulation options. It returns an instance of @@ -1075,7 +1070,6 @@ def simulate_cmd( result_file simflags simargs - timeout Returns ------- @@ -1086,7 +1080,6 @@ def simulate_cmd( session=self._session, runpath=self.getWorkDirectory(), modelname=self._model_name, - timeout=timeout, ) # always define the result file to use @@ -1136,7 +1129,6 @@ def simulate( resultfile: Optional[str | os.PathLike] = None, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, - timeout: Optional[float] = None, ) -> None: """Simulate the model according to simulation options. @@ -1147,7 +1139,6 @@ def simulate( simflags: String of extra command line flags for the model binary. This argument is deprecated, use simargs instead. simargs: Dict with simulation runtime flags. - timeout: Maximum execution time in seconds. Examples: mod.simulate() @@ -1175,7 +1166,6 @@ def simulate( result_file=self._result_file, simflags=simflags, simargs=simargs, - timeout=timeout, ) # delete resultfile ... @@ -1759,7 +1749,6 @@ def linearize( lintime: Optional[float] = None, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, - timeout: Optional[float] = None, ) -> LinearizationResult: """Linearize the model according to linearization options. @@ -1770,7 +1759,6 @@ def linearize( simflags: String of extra command line flags for the model binary. This argument is deprecated, use simargs instead. simargs: A dict with command line flags and possible options; example: "simargs={'csvInput': 'a.csv'}" - timeout: Maximum execution time in seconds. Returns: A LinearizationResult object is returned. This allows several @@ -1792,7 +1780,6 @@ def linearize( session=self._session, runpath=self.getWorkDirectory(), modelname=self._model_name, - timeout=timeout, ) override_content = ( @@ -1971,7 +1958,6 @@ def __init__( # simulation specific input # TODO: add more settings (simulation options, input options, ...) simargs: Optional[dict[str, Optional[str | dict[str, str] | numbers.Number]]] = None, - timeout: Optional[int] = None, # DoE specific inputs resultpath: Optional[str | os.PathLike] = None, parameters: Optional[dict[str, list[str] | list[int] | list[float]]] = None, @@ -2000,7 +1986,6 @@ def __init__( self._model_name = model_name self._simargs = simargs - self._timeout = timeout if resultpath is None: self._resultpath = self.session().omcpath_tempdir() @@ -2103,7 +2088,6 @@ def prepare(self) -> int: self._mod.setParameters(sim_param_non_structural) mscmd = self._mod.simulate_cmd( result_file=resultfile, - timeout=self._timeout, ) if self._simargs is not None: mscmd.args_set(args=self._simargs) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 73d4b0c4..757a08a1 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -517,8 +517,6 @@ class OMCSessionRunData: cmd_model_executable: Optional[str] = None # additional library search path; this is mainly needed if OMCProcessLocal is run on Windows cmd_library_path: Optional[str] = None - # command timeout - cmd_timeout: Optional[float] = 10.0 # working directory to be used on the *local* system cmd_cwd_local: Optional[str] = None @@ -604,6 +602,17 @@ def __del__(self): self.omc_zmq = None + def set_timeout(self, timeout: Optional[float] = None) -> float: + """ + Set the timeout to be used for OMC communication (OMCSession). + """ + retval = self._timeout + if timeout is not None: + if timeout <= 0.0: + raise OMCSessionException(f"Invalid timeout value: {timeout}!") + self._timeout = timeout + return retval + @staticmethod def escape_str(value: str) -> str: """ @@ -663,11 +672,9 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunD """ return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data) - @staticmethod - def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: + def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: """ - Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to - keep instances of over classes around. + Run the command defined in cmd_run_data. """ my_env = os.environ.copy() @@ -684,7 +691,7 @@ def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: text=True, env=my_env, cwd=cmd_run_data.cmd_cwd_local, - timeout=cmd_run_data.cmd_timeout, + timeout=self._timeout, check=True, ) stdout = cmdres.stdout.strip() @@ -720,18 +727,17 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: logger.debug("sendExpression(%r, parsed=%r)", command, parsed) - attempts = 0 - while True: + loop = self.omc_process._timeout_loop(timestep=0.05) + while next(loop): try: self.omc_zmq.send_string(str(command), flags=zmq.NOBLOCK) break except zmq.error.Again: pass - attempts += 1 - if attempts >= 50: - raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}). " - f"Log-file says: \n{self.omc_process.get_log()}") - time.sleep(self._timeout / 50.0) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.omc_process.get_log()}") + raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}).") + if command == "quit()": self.omc_zmq.close() self.omc_zmq = None @@ -902,6 +908,31 @@ def __del__(self): finally: self._omc_process = None + def _timeout_loop( + self, + timeout: Optional[float] = None, + timestep: float = 0.1, + ): + """ + Helper (using yield) for while loops to check OMC startup / response. The loop is executed as long as True is + returned, i.e. the first False will stop the while loop. + """ + + if timeout is None: + timeout = self._timeout + if timeout <= 0: + raise OMCSessionException(f"Invalid timeout: {timeout}") + + timer = 0.0 + yield True + while True: + timer += timestep + if timer > timeout: + break + time.sleep(timestep) + yield True + yield False + def get_port(self) -> Optional[str]: """ Get the port to connect to the OMC process. @@ -1023,25 +1054,19 @@ def _omc_port_get(self) -> str: port = None # See if the omc server is running - attempts = 0 - while True: + loop = self._timeout_loop(timestep=0.1) + while next(loop): omc_portfile_path = self._get_portfile_path() - if omc_portfile_path is not None and omc_portfile_path.is_file(): # Read the port file with open(file=omc_portfile_path, mode='r', encoding="utf-8") as f_p: port = f_p.readline() break - if port is not None: break - - attempts += 1 - if attempts == 80.0: - raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}). " - f"Could not open file {omc_portfile_path}. " - f"Log-file says:\n{self.get_log()}") - time.sleep(self._timeout / 80.0) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}).") logger.info(f"Local OMC Server is up and running at ZMQ port {port} " f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") @@ -1123,7 +1148,8 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: raise NotImplementedError("Docker not supported on win32!") docker_process = None - for _ in range(0, 40): + loop = self._timeout_loop(timestep=0.2) + while next(loop): docker_top = subprocess.check_output(["docker", "top", docker_cid]).decode().strip() docker_process = None for line in docker_top.split("\n"): @@ -1134,10 +1160,11 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: except psutil.NoSuchProcess as ex: raise OMCSessionException(f"Could not find PID {docker_top} - " "is this a docker instance spawned without --pid=host?") from ex - if docker_process is not None: break - time.sleep(self._timeout / 40.0) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") return docker_process @@ -1159,8 +1186,8 @@ def _omc_port_get(self) -> str: raise OMCSessionException(f"Invalid docker container ID: {self._docker_container_id}") # See if the omc server is running - attempts = 0 - while True: + loop = self._timeout_loop(timestep=0.1) + while next(loop): omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: try: @@ -1171,16 +1198,11 @@ def _omc_port_get(self) -> str: port = output.decode().strip() except subprocess.CalledProcessError: pass - if port is not None: break - - attempts += 1 - if attempts == 80.0: - raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}). " - f"Could not open port file {omc_portfile_path}. " - f"Log-file says:\n{self.get_log()}") - time.sleep(self._timeout / 80.0) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") logger.info(f"Docker based OMC Server is up and running at port {port}") @@ -1348,25 +1370,24 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DockerPopen, str]: raise OMCSessionException(f"Invalid content for docker container ID file path: {docker_cid_file}") docker_cid = None - for _ in range(0, 40): + loop = self._timeout_loop(timestep=0.1) + while next(loop): try: with open(file=docker_cid_file, mode="r", encoding="utf-8") as fh: docker_cid = fh.read().strip() except IOError: pass - if docker_cid: + if docker_cid is not None: break - time.sleep(self._timeout / 40.0) - - if docker_cid is None: + else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"Docker did not start (timeout={self._timeout} might be too short " "especially if you did not docker pull the image before this command).") docker_process = self._docker_process_get(docker_cid=docker_cid) if docker_process is None: - raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}. " - f"Log-file says:\n{self.get_log()}") + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}.") return omc_process, docker_process, docker_cid @@ -1522,8 +1543,8 @@ def _omc_port_get(self) -> str: port = None # See if the omc server is running - attempts = 0 - while True: + loop = self._timeout_loop(timestep=0.1) + while next(loop): try: omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: @@ -1534,16 +1555,11 @@ def _omc_port_get(self) -> str: port = output.decode().strip() except subprocess.CalledProcessError: pass - if port is not None: break - - attempts += 1 - if attempts == 80.0: - raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}). " - f"Could not open port file {omc_portfile_path}. " - f"Log-file says:\n{self.get_log()}") - time.sleep(self._timeout / 80.0) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}).") logger.info(f"WSL based OMC Server is up and running at ZMQ port {port} " f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")