ansys-pyensight-core 0.8.9__py3-none-any.whl → 0.8.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ansys-pyensight-core might be problematic. Click here for more details.

@@ -12,7 +12,7 @@ __ansys_version__ = DEFAULT_ANSYS_VERSION
12
12
  __ansys_version_str__ = f"{2000+(int(__ansys_version__) // 10)} R{int(__ansys_version__) % 10}"
13
13
 
14
14
  from ansys.pyensight.core.dockerlauncher import DockerLauncher
15
- from ansys.pyensight.core.launch_ensight import launch_ensight
15
+ from ansys.pyensight.core.launch_ensight import launch_ensight, launch_libuserd
16
16
  from ansys.pyensight.core.launcher import Launcher
17
17
  from ansys.pyensight.core.listobj import ensobjlist
18
18
  from ansys.pyensight.core.locallauncher import LocalLauncher
@@ -0,0 +1,216 @@
1
+ """ This module provides a list of common utilities shared between different PyEnSight modules."""
2
+
3
+ import random
4
+ import re
5
+ import socket
6
+ import time
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
8
+
9
+ from ansys.pyensight.core import enshell_grpc
10
+ import urllib3
11
+
12
+ try:
13
+ from simple_upload_server.client import Client
14
+
15
+ simple_upload_server_is_available = True # pragma: no cover
16
+ except Exception:
17
+ simple_upload_server_is_available = False
18
+
19
+ if TYPE_CHECKING:
20
+ from docker import DockerClient
21
+
22
+
23
+ def find_unused_ports(count: int, avoid: Optional[List[int]] = None) -> Optional[List[int]]:
24
+ """Find "count" unused ports on the host system
25
+
26
+ A port is considered unused if it does not respond to a "connect" attempt. Walk
27
+ the ports from 'start' to 'end' looking for unused ports and avoiding any ports
28
+ in the 'avoid' list. Stop once the desired number of ports have been
29
+ found. If an insufficient number of ports were found, return None.
30
+
31
+ Parameters
32
+ ----------
33
+ count: int :
34
+ Number of unused ports to find
35
+ avoid: Optional[List[int]] :
36
+ An optional list of ports not to check
37
+
38
+ Returns
39
+ -------
40
+ The detected ports or None on failure
41
+
42
+ """
43
+ if avoid is None:
44
+ avoid = []
45
+ ports = list()
46
+
47
+ # pick a starting port number
48
+ start = random.randint(1024, 64000)
49
+ # We will scan for 65530 ports unless end is specified
50
+ port_mod = 65530
51
+ end = start + port_mod - 1
52
+ # walk the "virtual" port range
53
+ for base_port in range(start, end + 1):
54
+ # Map to physical port range
55
+ # There have been some issues with 65534+ so we stop at 65530
56
+ port = base_port % port_mod
57
+ # port 0 is special
58
+ if port == 0: # pragma: no cover
59
+ continue # pragma: no cover
60
+ # avoid admin ports
61
+ if port < 1024: # pragma: no cover
62
+ continue # pragma: no cover
63
+ # are we supposed to skip this one?
64
+ if port in avoid: # pragma: no cover
65
+ continue # pragma: no cover
66
+ # is anyone listening?
67
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
68
+ result = sock.connect_ex(("127.0.0.1", port))
69
+ if result != 0:
70
+ ports.append(port)
71
+ else:
72
+ sock.close() # pragma: no cover
73
+ if len(ports) >= count:
74
+ return ports
75
+ # in case we failed...
76
+ if len(ports) < count: # pragma: no cover
77
+ return None # pragma: no cover
78
+ return ports # pragma: no cover
79
+
80
+
81
+ def get_host_port(uri: str) -> Tuple[str, int]:
82
+ """Get the host port for the input uri
83
+
84
+ Parameters
85
+ ----------
86
+
87
+ uri: str
88
+ The Uri to inspect
89
+
90
+ Returns
91
+ -------
92
+ (tuple):
93
+ A tuple containing the host and the port of the input uri
94
+ """
95
+ parse_results = urllib3.util.parse_url(uri)
96
+ port = (
97
+ parse_results.port
98
+ if parse_results.port
99
+ else (443 if re.search("^https|wss$", parse_results.scheme) else None)
100
+ )
101
+ return (parse_results.host, port)
102
+
103
+
104
+ def get_file_service(pim_instance: Any) -> Optional[Any]: # pragma: no cover
105
+ """Get the file service object for the input pim instance.
106
+
107
+ Parameters
108
+ ----------
109
+
110
+ pim_instance:
111
+ the PIM instance to get the service from.
112
+
113
+ Returns
114
+ -------
115
+
116
+ pim_file_service:
117
+ the PIM file service object
118
+ """
119
+ if simple_upload_server_is_available is False:
120
+ return None
121
+ if pim_instance is None:
122
+ return None
123
+
124
+ if "http-simple-upload-server" in pim_instance.services:
125
+ pim_file_service = Client(
126
+ token="token",
127
+ url=pim_instance.services["http-simple-upload-server"].uri,
128
+ headers=pim_instance.services["http-simple-upload-server"].headers,
129
+ )
130
+ return pim_file_service
131
+ return None
132
+
133
+
134
+ def populate_service_host_port( # pragma: no cover
135
+ pim_instance: Any, service_host_port: Dict[str, Tuple[str, int]], webui: bool = False
136
+ ) -> Dict[str, Tuple[str, int]]:
137
+ """Populate the service host port dictionary with the services available in the PIM instance.
138
+
139
+ Parameters
140
+ ----------
141
+ pim_instance:
142
+ the PIM instance to get the servicea from.
143
+ service_host_port: dict
144
+ the dictionary to be updated with the services from the PIM instance
145
+ webui: bool
146
+ if True retrieve also the webUI service
147
+
148
+ Returns
149
+ -------
150
+ service_host_port: dict
151
+ the dictionary updated with the services from the PIM instance
152
+ """
153
+ if not set(("grpc_private", "http", "ws")).issubset(pim_instance.services):
154
+ raise RuntimeError(
155
+ "If channel is specified, the PIM instance must have a list of length 3 "
156
+ + "containing the appropriate service URIs. It does not."
157
+ )
158
+ service_host_port["grpc_private"] = get_host_port(pim_instance.services["grpc_private"].uri)
159
+ service_host_port["http"] = get_host_port(pim_instance.services["http"].uri)
160
+ service_host_port["ws"] = get_host_port(pim_instance.services["ws"].uri)
161
+ service_host_port["grpc"] = ("127.0.0.1", -1)
162
+ if webui:
163
+ service_host_port["webui"] = get_host_port(pim_instance.services["webui"].uri)
164
+ return service_host_port
165
+
166
+
167
+ def launch_enshell_interface(
168
+ enshell_grpc_channel: Any, grpc_port: int, timeout: float
169
+ ) -> enshell_grpc.EnShellGRPC:
170
+ """Launch the EnShell gRPC Interface.
171
+
172
+ Parameters
173
+ ----------
174
+ enshell_grpc_channel:
175
+ An eventual gRPC channel already available, like in the PIM case
176
+ grpc_port: int
177
+ the gRPC port to connect to
178
+ timeout: float
179
+ a timeout to wait for the gRPC connection
180
+
181
+ Returns
182
+ -------
183
+ enshell: enshell_grpc.EnShellGRPC
184
+ the enshell gRPC interface
185
+ """
186
+ if enshell_grpc_channel: # pragma: no cover
187
+ enshell = enshell_grpc.EnShellGRPC() # pragma: no cover
188
+ enshell.connect_existing_channel(enshell_grpc_channel) # pragma: no cover
189
+ else:
190
+ enshell = enshell_grpc.EnShellGRPC(port=grpc_port)
191
+ time_start = time.time()
192
+ while time.time() - time_start < timeout: # pragma: no cover
193
+ if enshell.is_connected():
194
+ break
195
+ try:
196
+ enshell.connect(timeout=timeout)
197
+ except OSError: # pragma: no cover
198
+ pass # pragma: no cover
199
+ return enshell
200
+
201
+
202
+ def pull_image(docker_client: "DockerClient", image_name: str) -> None:
203
+ """Pull the input docker image using the input Docker Client
204
+
205
+ Parameters
206
+ ----------
207
+ docker_client: DockerClient
208
+ the current DockerClient to pull the image with
209
+ image_name: str
210
+ the image to pull
211
+ """
212
+ try:
213
+ if docker_client is not None: # pragma: no cover
214
+ docker_client.images.pull(image_name)
215
+ except Exception:
216
+ raise RuntimeError(f"Can't pull Docker image: {image_name}")
@@ -16,14 +16,10 @@ Examples:
16
16
  """
17
17
  import logging
18
18
  import os.path
19
- import re
20
19
  import subprocess
21
- import time
22
- from typing import Any, Dict, Optional
20
+ from typing import TYPE_CHECKING, Any, Dict, Optional
23
21
  import uuid
24
22
 
25
- import urllib3
26
-
27
23
  try:
28
24
  import grpc
29
25
  except ModuleNotFoundError: # pragma: no cover
@@ -31,24 +27,27 @@ except ModuleNotFoundError: # pragma: no cover
31
27
  except Exception: # pragma: no cover
32
28
  raise RuntimeError("Cannot initialize grpc")
33
29
 
34
- from ansys.pyensight.core.launcher import Launcher
35
- import ansys.pyensight.core.session
36
- from ansys.pyensight.core.session import Session
37
-
38
30
  try:
39
- from ansys.pyensight.core import enshell_grpc
31
+ import docker
40
32
  except ModuleNotFoundError: # pragma: no cover
41
- raise RuntimeError("The enshell_grpc must be installed for DockerLauncher")
33
+ raise RuntimeError("The docker module must be installed for DockerLauncher")
42
34
  except Exception: # pragma: no cover
43
- raise RuntimeError("Cannot initialize enshell_grpc")
44
-
45
-
46
- try:
47
- from simple_upload_server.client import Client
35
+ raise RuntimeError("Cannot initialize Docker")
36
+
37
+ from ansys.pyensight.core.common import (
38
+ find_unused_ports,
39
+ get_file_service,
40
+ launch_enshell_interface,
41
+ populate_service_host_port,
42
+ pull_image,
43
+ )
44
+ from ansys.pyensight.core.launcher import Launcher
45
+ import ansys.pyensight.core.session
46
+ from ansys.pyensight.core.session import Session
48
47
 
49
- simple_upload_server_is_available = True # pragma: no cover
50
- except Exception:
51
- simple_upload_server_is_available = False
48
+ if TYPE_CHECKING:
49
+ from docker import DockerClient
50
+ from docker.models.containers import Container
52
51
 
53
52
 
54
53
  class DockerLauncher(Launcher):
@@ -117,8 +116,8 @@ class DockerLauncher(Launcher):
117
116
  self._enshell_grpc_channel = channel
118
117
  self._service_uris: Dict[Any, str] = {}
119
118
  self._image_name: Optional[str] = None
120
- self._docker_client: Optional[Any] = None
121
- self._container = None
119
+ self._docker_client: Optional["DockerClient"] = None
120
+ self._container: Optional["Container"] = None
122
121
  self._enshell: Optional[Any] = None
123
122
  self._pim_instance: Optional[Any] = pim_instance
124
123
 
@@ -143,27 +142,20 @@ class DockerLauncher(Launcher):
143
142
  )
144
143
 
145
144
  if self._enshell_grpc_channel and self._pim_instance:
146
- if not set(("grpc_private", "http", "ws")).issubset(self._pim_instance.services):
145
+ service_set = ["grpc_private", "http", "ws"]
146
+ # if self._launch_webui:
147
+ # service_set.append("webui")
148
+ if not set(service_set).issubset(self._pim_instance.services):
147
149
  raise RuntimeError(
148
150
  "If channel is specified, the PIM instance must have a list of length 3 "
149
151
  + "containing the appropriate service URIs. It does not."
150
152
  )
151
- self._service_host_port = {}
152
153
  # grab the URIs for the 3 required services passed in from PIM
153
- self._service_host_port["grpc_private"] = self._get_host_port(
154
- self._pim_instance.services["grpc_private"].uri
155
- )
156
- self._service_host_port["http"] = self._get_host_port(
157
- self._pim_instance.services["http"].uri
158
- )
159
- self._service_host_port["ws"] = self._get_host_port(
160
- self._pim_instance.services["ws"].uri
154
+ self._service_host_port = populate_service_host_port(
155
+ self._pim_instance, {}, webui=self._launch_webui
161
156
  )
162
- # for parity, add 'grpc' as a placeholder even though using PIM sets up the gRPC channel.
163
- # this isn't used in this situation.
164
- self._service_host_port["grpc"] = ("127.0.0.1", -1)
165
157
  # attach to the file service if available
166
- self._get_file_service()
158
+ self._pim_file_service = get_file_service(self._pim_instance)
167
159
  # if using PIM, we have a query parameter to append to http requests
168
160
  if self._pim_instance is not None:
169
161
  d = {"instance_name": self._pim_instance.name}
@@ -173,7 +165,10 @@ class DockerLauncher(Launcher):
173
165
 
174
166
  # EnShell gRPC port, EnSight gRPC port, HTTP port, WSS port
175
167
  # skip 1999 as that internal to the container is used to the container for the VNC connection
176
- ports = self._find_unused_ports(4, avoid=[1999])
168
+ num_ports = 4
169
+ if self._launch_webui:
170
+ num_ports = 5
171
+ ports = find_unused_ports(num_ports, avoid=[1999])
177
172
  if ports is None: # pragma: no cover
178
173
  raise RuntimeError(
179
174
  "Unable to allocate local ports for EnSight session"
@@ -183,6 +178,8 @@ class DockerLauncher(Launcher):
183
178
  self._service_host_port["grpc_private"] = ("127.0.0.1", ports[1])
184
179
  self._service_host_port["http"] = ("127.0.0.1", ports[2])
185
180
  self._service_host_port["ws"] = ("127.0.0.1", ports[3])
181
+ if self._launch_webui:
182
+ self._service_host_port["webui"] = ("127.0.0.1", ports[4])
186
183
 
187
184
  # get the optional user-specified image name
188
185
  # Note: the default name needs to change over time... TODO
@@ -193,14 +190,7 @@ class DockerLauncher(Launcher):
193
190
  self._image_name = docker_image_name
194
191
 
195
192
  # Load up Docker from the user's environment
196
- try:
197
- import docker
198
-
199
- self._docker_client = docker.from_env()
200
- except ModuleNotFoundError:
201
- raise RuntimeError("The docker module must be installed for DockerLauncher")
202
- except Exception:
203
- raise RuntimeError("Cannot initialize Docker")
193
+ self._docker_client = docker.from_env()
204
194
 
205
195
  def ansys_version(self) -> Optional[str]:
206
196
  """Get the Ansys version (three-digit string) found in the Docker container.
@@ -222,11 +212,7 @@ class DockerLauncher(Launcher):
222
212
  If the Docker image couldn't be pulled.
223
213
 
224
214
  """
225
- try:
226
- if self._docker_client is not None: # pragma: no cover
227
- self._docker_client.images.pull(self._image_name)
228
- except Exception:
229
- raise RuntimeError(f"Can't pull Docker image: {self._image_name}")
215
+ pull_image(self._docker_client, self._image_name)
230
216
 
231
217
  def _get_container_env(self) -> Dict:
232
218
  # Create the environmental variables
@@ -251,6 +237,10 @@ class DockerLauncher(Launcher):
251
237
  if "ENSIGHT_ANSYS_APIP_CONFIG" in os.environ:
252
238
  container_env["ENSIGHT_ANSYS_APIP_CONFIG"] = os.environ["ENSIGHT_ANSYS_APIP_CONFIG"]
253
239
 
240
+ if self._launch_webui:
241
+ container_env["SIMBA_WEBSERVER_TOKEN"] = self._secret_key
242
+ container_env["FLUENT_WEBSERVER_TOKEN"] = self._secret_key
243
+
254
244
  return container_env
255
245
 
256
246
  def start(self) -> "Session":
@@ -299,7 +289,13 @@ class DockerLauncher(Launcher):
299
289
  + "/tcp": str(self._service_host_port["http"][1]),
300
290
  str(self._service_host_port["ws"][1]) + "/tcp": str(self._service_host_port["ws"][1]),
301
291
  }
302
-
292
+ if self._launch_webui:
293
+ ports_to_map.update(
294
+ {
295
+ str(self._service_host_port["webui"][1])
296
+ + "/tcp": str(self._service_host_port["webui"][1])
297
+ }
298
+ )
303
299
  # The data directory to map into the container
304
300
  data_volume = None
305
301
  if self._data_directory:
@@ -310,13 +306,6 @@ class DockerLauncher(Launcher):
310
306
  #
311
307
  enshell_cmd = "-app -v 3 -grpc_server " + str(grpc_port)
312
308
 
313
- try:
314
- import docker
315
- except ModuleNotFoundError: # pragma: no cover
316
- raise RuntimeError("The docker module must be installed for DockerLauncher")
317
- except Exception: # pragma: no cover
318
- raise RuntimeError("Cannot initialize Docker")
319
-
320
309
  use_egl = self._use_egl()
321
310
 
322
311
  logging.debug("Starting Container...\n")
@@ -389,6 +378,33 @@ class DockerLauncher(Launcher):
389
378
  logging.debug("Container started.\n")
390
379
  return self.connect()
391
380
 
381
+ def launch_webui(self, container_env_str):
382
+ # Run websocketserver
383
+ cmd = f"cpython /ansys_inc/v{self._ansys_version}/CEI/"
384
+ cmd += f"nexus{self._ansys_version}/nexus_launcher/webui_launcher.py"
385
+ # websocket port - this needs to come first since we now have
386
+ # --add_header as a optional arg that can take an arbitrary
387
+ # number of optional headers.
388
+ webui_port = self._service_host_port["webui"][1]
389
+ grpc_port = self._service_host_port["grpc_private"][1]
390
+ http_port = self._service_host_port["http"][1]
391
+ ws_port = self._service_host_port["ws"][1]
392
+ cmd += f" --server-listen-port {webui_port}"
393
+ cmd += (
394
+ f" --server-web-roots /ansys_inc/v{self._ansys_version}/CEI/nexus{self._ansys_version}/"
395
+ )
396
+ cmd += f"ansys{self._ansys_version}/ensight/WebUI/web/ui/"
397
+ cmd += f" --ensight-grpc-port {grpc_port}"
398
+ cmd += f" --ensight-html-port {http_port}"
399
+ cmd += f" --ensight-ws-port {ws_port}"
400
+ cmd += f" --ensight-session-directory {self._session_directory}"
401
+ cmd += f" --ensight-secret-key {self._secret_key}"
402
+ logging.debug(f"Starting WebUI: {cmd}\n")
403
+ ret = self._enshell.start_other(cmd, extra_env=container_env_str)
404
+ if ret[0] != 0: # pragma: no cover
405
+ self.stop() # pragma: no cover
406
+ raise RuntimeError(f"Error starting WebUI: {cmd}\n") # pragma: no cover
407
+
392
408
  def connect(self):
393
409
  """Create and bind a :class:`Session<ansys.pyensight.core.Session>` instance
394
410
  to the created EnSight gRPC connection started by EnShell.
@@ -408,24 +424,12 @@ class DockerLauncher(Launcher):
408
424
  #
409
425
  #
410
426
  # Start up the EnShell gRPC interface
427
+ self._enshell = launch_enshell_interface(
428
+ self._enshell_grpc_channel, self._service_host_port["grpc"][1], self._timeout
429
+ )
411
430
  log_dir = "/data"
412
431
  if self._enshell_grpc_channel: # pragma: no cover
413
- self._enshell = enshell_grpc.EnShellGRPC()
414
- self._enshell.connect_existing_channel(self._enshell_grpc_channel)
415
432
  log_dir = "/work"
416
- else:
417
- logging.debug(
418
- f"Connecting to EnShell over gRPC port: {self._service_host_port['grpc'][1]}...\n"
419
- )
420
- self._enshell = enshell_grpc.EnShellGRPC(port=self._service_host_port["grpc"][1])
421
- time_start = time.time()
422
- while time.time() - time_start < self._timeout: # pragma: no cover
423
- if self._enshell.is_connected():
424
- break
425
- try:
426
- self._enshell.connect(timeout=self._timeout)
427
- except OSError: # pragma: no cover
428
- pass # pragma: no cover
429
433
 
430
434
  if not self._enshell.is_connected(): # pragma: no cover
431
435
  self.stop() # pragma: no cover
@@ -581,10 +585,13 @@ class DockerLauncher(Launcher):
581
585
  timeout=self._timeout,
582
586
  sos=use_sos,
583
587
  rest_api=self._enable_rest_api,
588
+ webui_port=self._service_host_port["webui"][1] if self._launch_webui else None,
584
589
  )
585
590
  session.launcher = self
586
591
  self._sessions.append(session)
587
592
 
593
+ if self._launch_webui:
594
+ self.launch_webui(container_env_str)
588
595
  logging.debug("Return session.\n")
589
596
 
590
597
  return session
@@ -619,32 +626,10 @@ class DockerLauncher(Launcher):
619
626
  self._pim_instance = None
620
627
  super().stop()
621
628
 
622
- def _get_file_service(self) -> None: # pragma: no cover
623
- if simple_upload_server_is_available is False:
624
- return
625
- if self._pim_instance is None:
626
- return
627
-
628
- if "http-simple-upload-server" in self._pim_instance.services:
629
- self._pim_file_service = Client(
630
- token="token",
631
- url=self._pim_instance.services["http-simple-upload-server"].uri,
632
- headers=self._pim_instance.services["http-simple-upload-server"].headers,
633
- )
634
-
635
629
  def file_service(self) -> Optional[Any]:
636
630
  """Get the PIM file service object if available."""
637
631
  return self._pim_file_service
638
632
 
639
- def _get_host_port(self, uri: str) -> tuple:
640
- parse_results = urllib3.util.parse_url(uri)
641
- port = (
642
- parse_results.port
643
- if parse_results.port
644
- else (443 if re.search("^https|wss$", parse_results.scheme) else None)
645
- )
646
- return (parse_results.host, port)
647
-
648
633
  def _is_system_egl_capable(self) -> bool:
649
634
  """Check if the system is EGL capable.
650
635
 
@@ -355,6 +355,38 @@ class EnShellGRPC(object):
355
355
  else:
356
356
  return self.run_command_with_env(command_string, ensight_env) # pragma: no cover
357
357
 
358
+ def start_ensight_server(
359
+ self, ensight_args: Optional[str] = None, ensight_env: Optional[str] = None
360
+ ):
361
+ """Tell EnShell to start the EnSight server.
362
+
363
+ The string will be sent to EnShell via the EnShellService::run_command()
364
+ gRPC call. An IOError exception may be thrown
365
+ if there's a gRPC communication problem. The response
366
+ is the tuple of the EnShell return code and return string.
367
+
368
+ Parameters
369
+ ----------
370
+ ensight_args : Optional[str], optional
371
+ ensight_args arguments for the ensight command line, by default None
372
+
373
+ Returns
374
+ -------
375
+ Tuple
376
+ A tuple of (int, string) for (returnCode, returnString)
377
+ """
378
+ self.connect()
379
+ command_string = (
380
+ f"start_app OTHER /ansys_inc/v{self.ansys_version()}/CEI/bin/ensight_server"
381
+ )
382
+ if ensight_args and (ensight_args != ""):
383
+ command_string += " " + ensight_args
384
+
385
+ if ensight_env is None or ensight_env == "": # pragma: no cover
386
+ return self.run_command(command_string)
387
+ else:
388
+ return self.run_command_with_env(command_string, ensight_env) # pragma: no cover
389
+
358
390
  # @brief
359
391
  #
360
392
  # @param cmd The command line