diff --git a/requirements/build.txt b/requirements/build.txt index b52fb0e25a1..68fbc165041 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -4,3 +4,4 @@ requests>=2.20.1 twine>=4.0.1 pyansys-docker>=5.0.4 junitparser>=2.8.0 +urllib3>=1.26.10 \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index 6b4b878152a..953462154d2 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,3 +6,5 @@ pyansys-docker>=5.0.4 pytest-mock>=3.10.0 numpy>=1.13.0 junitparser>=2.8.0 +requests<2.29.0 +urllib3>=1.26.10 \ No newline at end of file diff --git a/setup.py b/setup.py index 286ae62d6d2..cb0d8e68e12 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ "protobuf>=3.9.1", "grpcio>=1.23.0", "requests>=2.20.1", + "urllib3>=1.26.10", "dill>=0.3.5.1", ] diff --git a/src/ansys/pyensight/__init__.py b/src/ansys/pyensight/__init__.py index 9ccc8adbdb8..c9ff8153c92 100644 --- a/src/ansys/pyensight/__init__.py +++ b/src/ansys/pyensight/__init__.py @@ -15,3 +15,10 @@ from ansys.pyensight.dockerlauncher import DockerLauncher except Exception: pass + +try: + from ansys.pyensight.dockerlauncherenshell import DockerLauncherEnShell +except Exception: + pass + +from ansys.pyensight.launch_ensight import launch_ensight diff --git a/src/ansys/pyensight/dockerlauncher.py b/src/ansys/pyensight/dockerlauncher.py index b8322d65cc6..048523e4029 100644 --- a/src/ansys/pyensight/dockerlauncher.py +++ b/src/ansys/pyensight/dockerlauncher.py @@ -10,7 +10,7 @@ launcher = DockerLauncher(data_directory="D:\\data") launcher.pull() session = launcher.start() - launcher.stop() + session.close() """ import os.path @@ -39,6 +39,11 @@ class DockerLauncher(pyensight.Launcher): In some cases where the EnSight session can take a significant amount of timme to start up, this is the number of seconds to wait before failing the connection. The default is 120.0. + use_egl: + If True, EGL hardware accelerated graphics will be used. The platform + must be able to support it. + use_sos: + If None, don't use SOS. Otherwise, it's the number of EnSight Servers to use (int). Examples: :: @@ -47,7 +52,7 @@ class DockerLauncher(pyensight.Launcher): launcher = DockerLauncher(data_directory="D:\\data") launcher.pull() session = launcher.start() - launcher.stop() + session.close() """ @@ -55,12 +60,13 @@ def __init__( self, data_directory: str, docker_image_name: Optional[str] = None, - use_dev: bool = False, - timeout: float = 120.0, + use_dev: Optional[bool] = False, + timeout: Optional[float] = 120.0, + use_egl: Optional[bool] = False, + use_sos: Optional[int] = None, ) -> None: - super().__init__() + super().__init__(timeout=timeout, use_egl=use_egl, use_sos=use_sos) - self._timeout = timeout self._data_directory: str = data_directory self._container = None @@ -118,7 +124,7 @@ def pull(self) -> None: except Exception: raise RuntimeError(f"Can't pull Docker image: {self._image_name}") - def start(self, host: str = "127.0.0.1", use_egl: bool = False) -> "pyensight.Session": + def start(self, host: str = "127.0.0.1") -> "pyensight.Session": """Start an EnSight session using the local Docker ensight image Launch a copy of EnSight in the container that supports the gRPC interface. Create and bind a Session instance to the created gRPC session. Return that session. @@ -126,8 +132,6 @@ def start(self, host: str = "127.0.0.1", use_egl: bool = False) -> "pyensight.Se Args: host: Optional hostname on which the EnSight gRPC service is running - use_egl: - Specify True if EnSight should try to use EGL. Beta flag. Returns: pyensight Session object instance @@ -180,7 +184,7 @@ def start(self, host: str = "127.0.0.1", use_egl: bool = False) -> "pyensight.Se # FIXME_MFK: probably need a unique name for our container # in case the user launches multiple sessions egl_env = os.environ.get("PYENSIGHT_FORCE_ENSIGHT_EGL") - use_egl = use_egl or egl_env or self._has_egl() + use_egl = self._use_egl or egl_env or self._has_egl() if use_egl: self._container = self._docker_client.containers.run( self._image_name, @@ -250,6 +254,9 @@ def start(self, host: str = "127.0.0.1", use_egl: bool = False) -> "pyensight.Se if use_egl: cmd2 += " -egl" + if self._use_sos: + cmd2 += " -sos -nservers " + str(int(self._use_sos)) + cmd2 += " -grpc_server " + str(ports[0]) vnc_url = f"vnc://%%3Frfb_port={ports[1]}%%26use_auth=0" diff --git a/src/ansys/pyensight/dockerlauncherenshell.py b/src/ansys/pyensight/dockerlauncherenshell.py new file mode 100644 index 00000000000..4d13120680b --- /dev/null +++ b/src/ansys/pyensight/dockerlauncherenshell.py @@ -0,0 +1,450 @@ +"""dockerlauncherenshell module + +The docker launcher enshell module provides pyensight with the ability to launch an +EnSight session using a local Docker installation via EnShell. + +Examples: + :: + + from ansys.pyensight import DockerLauncherEnShell + launcher = DockerLauncherEnShell(data_directory="D:\\data") + launcher.pull() + session = launcher.start() + session.close() + +""" +import os.path +import subprocess +from typing import Any, Optional +import uuid + +import urllib3 + +try: + import grpc +except ModuleNotFoundError: + raise RuntimeError("The grpc module must be installed for DockerLauncherEnShell") +except Exception: + raise RuntimeError("Cannot initialize grpc") + +from ansys import pyensight + +try: + from enshell_remote import enshell_grpc +except ModuleNotFoundError: + raise RuntimeError("The enshell_remote module must be installed for DockerLauncherEnShell") +except Exception: + raise RuntimeError("Cannot initialize grpc") + + +class DockerLauncherEnShell(pyensight.Launcher): + """Create a Session instance by launching a local Docker copy of EnSight via EnShell + + Launch a Docker copy of EnSight locally via EnShell that supports the gRPC interface. Create and + bind a Session instance to the created gRPC session from EnSight (not EnShell). Return that session. + + Args: + data_directory: + Host directory to make into the container at /data + docker_image_name: + Optional Docker Image name to use + use_dev: + Option to use the latest ensight_dev Docker Image; overridden by docker_image_name if specified. + timeout: + In some cases where the EnSight session can take a significant amount of + timme to start up, this is the number of seconds to wait before failing + the connection. The default is 120.0. + use_egl: + If True, EGL hardware accelerated graphics will be used. The platform + must be able to support it. + use_sos: + If None, don't use SOS. Otherwise, it's the number of EnSight Servers to use (int). + channel: + Existing gRPC channel to a running EnShell instance such as provided by PIM + pim_instance: + The PyPIM instance if using PIM (internal) + + Examples: + :: + + from ansys.pyensight import DockerLauncherEnShell + launcher = DockerLauncherEnShell(data_directory="D:\\data") + launcher.pull() + session = launcher.start() + session.close() + + """ + + def __init__( + self, + data_directory: Optional[str] = None, + docker_image_name: Optional[str] = None, + use_dev: Optional[bool] = False, + timeout: Optional[float] = 120.0, + use_egl: Optional[bool] = False, + use_sos: Optional[int] = None, + channel: Optional[grpc.Channel] = None, + pim_instance: Optional[Any] = None, + ) -> None: + super().__init__(timeout=timeout, use_egl=use_egl, use_sos=use_sos) + + self._data_directory = data_directory + self._enshell_grpc_channel = channel + self._service_uris = {} + self._image_name = None + self._docker_client = None + self._container = None + self._enshell = None + self._pim_instance = pim_instance + + # EnSight session secret key + self._secret_key: str = str(uuid.uuid1()) + # temporary directory + # it's in the ephemeral container, so just use "ensight's" + # home directory within the container + self._session_directory: str = "/home/ensight" + # the Ansys / EnSight version we found in the container + # to be reassigned later + self._ansys_version = None + + if self._enshell_grpc_channel: + if not set(("grpc_private", "http", "ws")).issubset(self._pim_instance.services): + raise RuntimeError( + "If channel is specified, the PIM instance must have a list of length 3 " + + "containing the appropriate service URIs. It does not." + ) + self._service_host_port = {} + # grab the URIs for the 3 required services passed in from PIM + self._service_host_port["grpc_private"] = self._get_host_port( + self._pim_instance.services["grpc_private"].uri + ) + self._service_host_port["http"] = self._get_host_port( + self._pim_instance.services["http"].uri + ) + self._service_host_port["ws"] = self._get_host_port( + self._pim_instance.services["ws"].uri + ) + # for parity, add 'grpc' as a placeholder even though pim use sets up the grpc channel. + # this isn't used in this situation. + self._service_host_port["grpc"] = ("127.0.0.1", -1) + return + + # EnShell gRPC port, EnSight gRPC port, HTTP port, WSS port + # skip 1999 as we'll use that internal to the Container for the VNC connection + ports = self._find_unused_ports(4, avoid=[1999]) + if ports is None: + raise RuntimeError("Unable to allocate local ports for EnSight session") + self._service_host_port = {} + self._service_host_port["grpc"] = ("127.0.0.1", ports[0]) + self._service_host_port["grpc_private"] = ("127.0.0.1", ports[1]) + self._service_host_port["http"] = ("127.0.0.1", ports[2]) + self._service_host_port["ws"] = ("127.0.0.1", ports[3]) + + # get the optional user specified image name + # Note: the default name will need to change over time... TODO + self._image_name: str = "ghcr.io/ansys-internal/ensight" + if use_dev: + self._image_name = "ghcr.io/ansys-internal/ensight_dev" + if docker_image_name: + self._image_name = docker_image_name + + # Load up Docker from the user's environment + try: + import docker + + self._docker_client = docker.from_env() + except ModuleNotFoundError: + raise RuntimeError("The pyansys-docker module must be installed for DockerLauncher") + except Exception: + raise RuntimeError("Cannot initialize Docker") + + def ansys_version(self) -> str: + """Returns the Ansys version as a 3 digit number string as found in the Docker container. + + Returns: + Ansys 3-digit version as a string, or None if not found or not start()'ed + + """ + return self._ansys_version + + def pull(self) -> None: + """Pulls the Docker image. + + Returns: + None + + Raises: + RuntimeError: + if Docker couldn't pull the image. + """ + try: + self._docker_client.images.pull(self._image_name) + except Exception: + raise RuntimeError(f"Can't pull Docker image: {self._image_name}") + + def start(self) -> "pyensight.Session": + """Start EnShell by running a local Docker EnSight Image. + Then, connect to the EnShell in the Container over gRPC. Once connected, + have EnShell launch a copy of EnSight and WSS in the Container. + Create and bind a Session instance to the created EnSight gRPC connection. + Return the Session. + + Args: + + Returns: + pyensight Session object instance + + Raises: + RuntimeError: + variety of error conditions. + """ + + # Launch the EnSight Docker container locally as a detached container + # initially running EnShell over the first gRPC port. Then launch EnSight + # and other apps. + + # Create the environmental variables + local_env = os.environ.copy() + local_env["ENSIGHT_SECURITY_TOKEN"] = self._secret_key + local_env["WEBSOCKETSERVER_SECURITY_TOKEN"] = self._secret_key + # local_env["ENSIGHT_SESSION_TEMPDIR"] = self._session_directory + + # Environment to pass into the container + container_env = { + "ENSIGHT_SECURITY_TOKEN": self._secret_key, + "WEBSOCKETSERVER_SECURITY_TOKEN": self._secret_key, + "ENSIGHT_SESSION_TEMPDIR": self._session_directory, + "ANSYSLMD_LICENSE_FILE": os.environ["ANSYSLMD_LICENSE_FILE"], + } + + # Ports to map between the host and the container + # If we're here in the code, then we're not using PIM + # and we're not really using URIs where the hostname + # is anything other than 127.0.0.1, so, we only need + # to grab the port numbers. + grpc_port = self._service_host_port["grpc"][1] + ports_to_map = { + str(self._service_host_port["grpc"][1]) + + "/tcp": str(self._service_host_port["grpc"][1]), + str(self._service_host_port["grpc_private"][1]) + + "/tcp": str(self._service_host_port["grpc_private"][1]), + str(self._service_host_port["http"][1]) + + "/tcp": str(self._service_host_port["http"][1]), + str(self._service_host_port["ws"][1]) + "/tcp": str(self._service_host_port["ws"][1]), + } + + # The data directory to map into the container + data_volume = None + if self._data_directory: + data_volume = {self._data_directory: {"bind": "/data", "mode": "rw"}} + + # FIXME_MFK: probably need a unique name for our container + # in case the user launches multiple sessions + egl_env = os.environ.get("PYENSIGHT_FORCE_ENSIGHT_EGL") + self._use_egl or egl_env or self._has_egl() + # FIXME_MFK: fix egl and remove the next line + self._use_egl = False + + # Start the container in detached mode with EnShell as a + # gRPC server as the command + # + import docker + + enshell_cmd = "-app -grpc_server " + str(grpc_port) + + # print("Starting Container...\n") + if data_volume: + if self._use_egl: + self._container = self._docker_client.containers.run( + self._image_name, + command=enshell_cmd, + volumes=data_volume, + environment=container_env, + device_requests=[docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])], + ports=ports_to_map, + tty=True, + detach=True, + ) + else: + # print(f"Running container {self._image_name} with cmd {enshellCmd}\n") + # print(f"ports to map: {ports_to_map}\n") + self._container = self._docker_client.containers.run( + self._image_name, + command=enshell_cmd, + volumes=data_volume, + environment=container_env, + ports=ports_to_map, + tty=True, + detach=True, + ) + # print(f"_container = {str(self._container)}\n") + else: + if self._use_egl: + self._container = self._docker_client.containers.run( + self._image_name, + command=enshell_cmd, + environment=container_env, + device_requests=[docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])], + ports=ports_to_map, + tty=True, + detach=True, + ) + else: + # print(f"Running container {self._image_name} with cmd {enshellCmd}\n") + # print(f"ports to map: {ports_to_map}\n") + self._container = self._docker_client.containers.run( + self._image_name, + command=enshell_cmd, + environment=container_env, + ports=ports_to_map, + tty=True, + detach=True, + ) + # print(f"_container = {str(self._container)}\n") + # print("Container started.\n") + return self.connect() + + def connect(self): + """Internal method. Create and bind a Session instance to the created gRPC EnSight + session as started by EnShell. Return that session. + + Args: + + Returns: + pyensight Session object instance + + Raises: + RuntimeError: + variety of error conditions. + """ + # + # + # Start up the EnShell gRPC interface + if self._enshell_grpc_channel: + self._enshell = enshell_grpc.EnShellGRPC() + self._enshell.connect_existing_channel(self._enshell_grpc_channel) + else: + # print(f"Connecting to EnShell over gRPC port: {self._service_host_port['grpc'][1]}...\n") + self._enshell = enshell_grpc.EnShellGRPC(port=self._service_host_port["grpc"][1]) + self._enshell.connect(self._timeout) + + if not self._enshell.is_connected(): + self.stop() + raise RuntimeError("Can't connect to EnShell over gRPC.") + + # print("Connected to EnShell. Getting CEI_HOME and Ansys version...\n") + + # Build up the command to run ensight via the EnShell gRPC interface + + self._cei_home = self._enshell.cei_home() + self._ansys_version = self._enshell.ansys_version() + # print("CEI_HOME=", self._cei_home) + # print("Ansys Version=", self._ansys_version) + + # print("Got them. Starting EnSight...\n") + + # Run EnSight + ensight_env = None + if self._use_egl: + ensight_env = ( + "export LD_PRELOAD=/usr/local/lib64/libGL.so.1:/usr/local/lib64/libEGL.so.1 ;" + ) + + ensight_args = "-batch -v 3" + + if self._use_egl: + ensight_args += " -egl" + + if self._use_sos: + ensight_args += " -sos -nservers " + str(int(self._use_sos)) + + ensight_args += " -grpc_server " + str(self._service_host_port["grpc_private"][1]) + + vnc_url = "vnc://%%3Frfb_port=1999%%26use_auth=0" + ensight_args += " -vnc " + vnc_url + + # print(f"Starting EnSight with args: {ensight_args}\n") + ret = self._enshell.start_ensight(ensight_args, ensight_env) + if ret[0] != 0: + self.stop() + raise RuntimeError(f"Error starting EnSight with args: {ensight_args}") + + # print("EnSight started. Starting wss...\n") + + # Run websocketserver + wss_cmd = "cpython /ansys_inc/v" + self._ansys_version + "/CEI/nexus" + wss_cmd += self._ansys_version + "/nexus_launcher/websocketserver.py" + wss_cmd += " --http_directory " + self._session_directory + # http port + wss_cmd += " --http_port " + str(self._service_host_port["http"][1]) + # vnc port + wss_cmd += " --client_port 1999" + # EnVision sessions + wss_cmd += " --local_session envision 5" + # websocket port + wss_cmd += " " + str(self._service_host_port["ws"][1]) + + # print(f"Starting WSS: {wss_cmd}\n") + ret = self._enshell.start_other(wss_cmd) + if ret[0] != 0: + self.stop() + raise RuntimeError(f"Error starting WSS: {wss_cmd}\n") + + # print("wss started. Making session...\n") + + # build the session instance + # WARNING: assuming the host is the same for grpc_private, http, and ws + # This may not be true in the future if using PIM. + # revise Session to handle three different hosts if necessary. + session = pyensight.Session( + host=self._service_host_port["grpc"][0], + grpc_port=self._service_host_port["grpc_private"][1], + html_port=self._service_host_port["http"][1], + ws_port=self._service_host_port["ws"][1], + install_path=None, + secret_key=self._secret_key, + timeout=self._timeout, + ) + session.launcher = self + self._sessions.append(session) + + # print("Return session.\n") + + return session + + def stop(self) -> None: + """Release any additional resources allocated during launching""" + if self._enshell.is_connected(): + try: + self._enshell.stop_server() + except Exception: + pass + self._enshell = None + # + if self._container: + try: + self._container.stop() + except Exception: + pass + try: + self._container.remove() + except Exception: + pass + self._container = None + + if self._pim_instance is not None: + self._pim_instance.delete() + self._pim_instance = None + + def _has_egl(self) -> bool: + if self._is_windows(): + return False + try: + subprocess.check_output("nvidia-smi") + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + def _get_host_port(self, uri: str) -> tuple: + parse_results = urllib3.util.parse_url(uri) + return (parse_results.host, parse_results.port) diff --git a/src/ansys/pyensight/launch_ensight.py b/src/ansys/pyensight/launch_ensight.py new file mode 100644 index 00000000000..bd33aaffe8a --- /dev/null +++ b/src/ansys/pyensight/launch_ensight.py @@ -0,0 +1,178 @@ +"""launch_ensight module + +The launch_ensight module provides pyensight with the ability to launch an +EnSight session using PyPIM. This leverages the DockerLauncherEnShell module. + +Examples: + :: + + from ansys.pyensight import launch_ensight + session = launch_ensight() + # do pyensight stuff with the session + session.close() +""" + +from typing import Optional + +from ansys.pyensight.locallauncher import LocalLauncher +from ansys.pyensight.session import Session + +pim_is_available = False +try: + import ansys.platform.instancemanagement as pypim + + pim_is_available = True +except Exception: + pass +# print(f"pim_is_available: {pim_is_available}\n") + +docker_is_available = False +try: + from ansys.pyensight.dockerlauncherenshell import DockerLauncherEnShell + + docker_is_available = True +except Exception: + pass +# print(f"docker_is_available: {docker_is_available}\n") + + +if pim_is_available: + + def _launch_ensight_with_pim( + product_version: str = None, + use_egl: Optional[bool] = False, + use_sos: Optional[int] = None, + ) -> "Session": + """Internal function. + Start via PyPIM the EnSight Docker container with EnShell as the ENTRYPOINT. + Create and bind a Session instance to the created gRPC session. Return that session. + + Args: + product_version : str, optional + Version of the product. For example, "232". The default is "None", in which case + use_egl: + If True, EGL hardware accelerated graphics will be used. The platform + must be able to support it. + use_sos: + If None, don't use SOS. Otherwise, it's the number of EnSight Servers to use (int). + + Returns: + pyensight Session object instance + """ + + pim = pypim.connect() + instance = pim.create_instance( + product_name="ensight", + product_version=product_version, + ) + instance.wait_for_ready() + # use defaults as specified by PIM + channel = instance.build_grpc_channel( + options=[ + ("grpc.max_receive_message_length", -1), + ("grpc.max_send_message_length", -1), + ("grpc.testing.fixed_reconnect_backoff_ms", 1100), + ] + ) + + launcher = DockerLauncherEnShell( + use_egl=use_egl, + use_sos=use_sos, + channel=channel, + pim_instance=instance, + ) + return launcher.connect() + + +def launch_ensight( + product_version: Optional[str] = None, + use_pim: Optional[bool] = True, + use_docker: Optional[bool] = True, + data_directory: Optional[str] = None, + docker_image_name: Optional[str] = None, + use_dev: Optional[bool] = False, + ansys_installation: Optional[str] = None, + application: Optional[str] = "ensight", + batch: Optional[bool] = True, + use_egl: Optional[bool] = False, + use_sos: Optional[int] = None, + timeout: Optional[float] = 120.0, +) -> "Session": + """Start an EnSight session via EnShell using the Docker EnSight Image. + Return that session. + + Args: + product_version : str, optional + Select an installed version of ANSYS. The string must be in a format like + "232" (for 2023 R2). The default is "None", in which case the newest installed + version is used. + use_pim : bool, optional + If True, then PyPIM is used to launch EnSight. + use_docker : bool, optional + If True, use DockerLaucherEnShell. If use_pim is True, this option is ignored. + data_directory: + Host directory to make into the Docker container at /data + Only used if use_docker is True. + docker_image_name: + Optional Docker Image name to use + use_dev: + Option to use the latest ensight_dev Docker Image; overridden by docker_image_name if specified. + ansys_installation: + Location of the ANSYS installation, including the version + directory Default: None (causes common locations to be scanned). + If use_pim is True, this option is ignored. If use_docker is True, this option is ignored. + application: + The application to be launched. By default, "ensight", but + "envision" is also available. + batch: + By default, the EnSight/EnVision instance will run in batch mode. + If batch is set to False, the full GUI will be presented. + Only used if use_pim and use_docker are False. + use_egl: + If True, EGL hardware accelerated graphics will be used. The platform + must be able to support it. + use_sos: + If None, don't use SOS. Otherwise, it's the number of EnSight Servers to use (int). + timeout: + In some cases where the EnSight session can take a significant amount of + time to start up, this is the number of seconds to wait before failing + the connection. The default is 120.0. + + Returns: + pyensight Session object instance + + Raises: + RuntimeError: + variety of error conditions. + """ + + # print(f"pim_is_available: {pim_is_available} use_pim: {use_pim}\n") + if pim_is_available and use_pim: + if pypim.is_configured(): + return _launch_ensight_with_pim( + product_version=product_version, use_egl=use_egl, use_sos=use_sos + ) + + # not using PIM, but use Docker + # print(f"docker_is_available: {docker_is_available} use_docker: {use_docker}\n") + if docker_is_available and use_docker: + launcher = DockerLauncherEnShell( + data_directory=data_directory, + docker_image_name=docker_image_name, + use_dev=use_dev, + timeout=timeout, + use_egl=use_egl, + use_sos=use_sos, + ) + return launcher.start() + + # use local installation of EnSight + launcher = LocalLauncher( + ansys_installation=ansys_installation, + application=application, + batch=batch, + timeout=timeout, + use_egl=use_egl, + use_sos=use_sos, + ) + return launcher.start() diff --git a/src/ansys/pyensight/launcher.py b/src/ansys/pyensight/launcher.py index 648f0948c4a..dcc3062e063 100644 --- a/src/ansys/pyensight/launcher.py +++ b/src/ansys/pyensight/launcher.py @@ -36,9 +36,28 @@ class Launcher: A Launcher instance is used to start/end an EnSight session. Specific subclasses handle different types of launching semantics. + + Args: + timeout: + In some cases where the EnSight session can take a significant amount of + timme to start up, this is the number of seconds to wait before failing + the connection. The default is 120.0. + use_egl: + If True, EGL hardware accelerated graphics will be used. The platform + must be able to support it. + use_sos: + If None, don't use SOS. Otherwise, it's the number of EnSight Servers to use (int). """ - def __init__(self) -> None: + def __init__( + self, + timeout: float = 120.0, + use_egl: bool = False, + use_sos: Optional[int] = None, + ) -> None: + self._timeout = timeout + self._use_egl = use_egl + self._use_sos = use_sos self._sessions = [] self._session_directory: str = "." diff --git a/src/ansys/pyensight/locallauncher.py b/src/ansys/pyensight/locallauncher.py index e2044a3031a..2c1d9432186 100644 --- a/src/ansys/pyensight/locallauncher.py +++ b/src/ansys/pyensight/locallauncher.py @@ -35,11 +35,16 @@ class LocalLauncher(pyensight.Launcher): "envision" is also available. batch: By default, the EnSight/EnVision instance will run in batch mode. - If batch is set to True, the full GUI will be presented. + If batch is set to True, the full GUI will not be presented. timeout: In some cases where the EnSight session can take a significant amount of timme to start up, this is the number of seconds to wait before failing the connection. The default is 120.0. + use_egl: + If True, EGL hardware accelerated graphics will be used. The platform + must be able to support it. + use_sos: + If None, don't use SOS. Otherwise, it's the number of EnSight Servers to use (int). Examples: :: @@ -52,11 +57,13 @@ class LocalLauncher(pyensight.Launcher): def __init__( self, ansys_installation: Optional[str] = None, - application: str = "ensight", - batch: bool = True, - timeout: float = 120.0, + application: Optional[str] = "ensight", + batch: Optional[bool] = True, + timeout: Optional[float] = 120.0, + use_egl: Optional[bool] = False, + use_sos: Optional[int] = None, ) -> None: - super().__init__() + super().__init__(timeout=timeout, use_egl=use_egl, use_sos=use_sos) # get the user selected installation directory self._install_path: str = self.get_cei_install_directory(ansys_installation) @@ -73,8 +80,6 @@ def __init__( self._ports = None # Are we running the instance in batch self._batch = batch - # The gRPC timeout - self._timeout = timeout @property def application(self): @@ -83,7 +88,7 @@ def application(self): """ return self._application - def start(self, use_egl: bool = False) -> "pyensight.Session": + def start(self) -> "pyensight.Session": """Start an EnSight session using the local ensight install Launch a copy of EnSight locally that supports the gRPC interface. Create and bind a Session instance to the created gRPC session. Return that session. @@ -91,8 +96,6 @@ def start(self, use_egl: bool = False) -> "pyensight.Session": Args: host: Optional hostname on which the EnSight gRPC service is running - use_egl: - Specify True if EnSight should try to use EGL. Returns: pyensight Session object instance @@ -130,11 +133,15 @@ def start(self, use_egl: bool = False) -> "pyensight.Session": vnc_url = f"vnc://%%3Frfb_port={self._ports[1]}%%26use_auth=0" cmd.extend(["-vnc", vnc_url]) egl_env = os.environ.get("PYENSIGHT_FORCE_ENSIGHT_EGL") - use_egl = use_egl or egl_env or self._has_egl() + use_egl = self._use_egl or egl_env or self._has_egl() if is_windows: cmd[0] += ".bat" if use_egl: cmd.append("-egl") + if self._use_sos: + cmd.append("-sos") + cmd.append("-nservers") + cmd.append(str(int(self._use_sos))) # cmd.append("-minimize_console") self._ensight_pid = subprocess.Popen( cmd, diff --git a/tests/conftest.py b/tests/conftest.py index 9fe95aaa879..893a8bbd8a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ Global fixtures go here. """ import atexit +import subprocess from unittest import mock import pytest @@ -31,8 +32,26 @@ def local_launcher_session(pytestconfig: pytest.Config) -> "ansys.pyensight.Sess session.close() +def cleanup_docker(request) -> None: + # Stop and remove 'ensight' and 'ensight_dev' containers. This needs to be deleted + # once we address the issue in the pyensight code by giving unique names to the containers + try: + subprocess.run(["docker", "stop", "ensight"]) + subprocess.run(["docker", "rm", "ensight"]) + except Exception: + # There might not be a running ensight container. That is fine, just continue + pass + try: + subprocess.run(["docker", "stop", "ensight_dev"]) + subprocess.run(["docker", "rm", "ensight_dev"]) + except Exception: + # There might not be a running ensight_dev container. That is fine, just continue + pass + + @pytest.fixture def docker_launcher_session() -> "ansys.pyensight.Session": + cleanup_docker() launcher = DockerLauncher(data_directory=".", use_dev=True) launcher.pull() session = launcher.start()