ansys-pyensight-core 0.11.0__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.
- ansys/pyensight/core/__init__.py +41 -0
- ansys/pyensight/core/common.py +341 -0
- ansys/pyensight/core/deep_pixel_view.html +98 -0
- ansys/pyensight/core/dockerlauncher.py +1124 -0
- ansys/pyensight/core/dvs.py +872 -0
- ansys/pyensight/core/enscontext.py +345 -0
- ansys/pyensight/core/enshell_grpc.py +641 -0
- ansys/pyensight/core/ensight_grpc.py +874 -0
- ansys/pyensight/core/ensobj.py +515 -0
- ansys/pyensight/core/launch_ensight.py +296 -0
- ansys/pyensight/core/launcher.py +388 -0
- ansys/pyensight/core/libuserd.py +2110 -0
- ansys/pyensight/core/listobj.py +280 -0
- ansys/pyensight/core/locallauncher.py +579 -0
- ansys/pyensight/core/py.typed +0 -0
- ansys/pyensight/core/renderable.py +880 -0
- ansys/pyensight/core/session.py +1923 -0
- ansys/pyensight/core/sgeo_poll.html +24 -0
- ansys/pyensight/core/utils/__init__.py +21 -0
- ansys/pyensight/core/utils/adr.py +111 -0
- ansys/pyensight/core/utils/dsg_server.py +1220 -0
- ansys/pyensight/core/utils/export.py +606 -0
- ansys/pyensight/core/utils/omniverse.py +769 -0
- ansys/pyensight/core/utils/omniverse_cli.py +614 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
- ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
- ansys/pyensight/core/utils/parts.py +1221 -0
- ansys/pyensight/core/utils/query.py +487 -0
- ansys/pyensight/core/utils/readers.py +300 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/support.py +128 -0
- ansys/pyensight/core/utils/variables.py +2019 -0
- ansys/pyensight/core/utils/views.py +674 -0
- ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
- ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
- ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
- ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
"""Docker Launcher module.
|
|
24
|
+
|
|
25
|
+
The Docker Launcher module provides PyEnSight with the ability to launch a
|
|
26
|
+
:class:`Session<ansys.pyensight.core.Session>` instance using a local Docker
|
|
27
|
+
installation or a `PIM <https://github.com/ansys/pypim`_-launched Docker installation.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
::
|
|
31
|
+
|
|
32
|
+
from ansys.pyensight.core import DockerLauncher
|
|
33
|
+
launcher = DockerLauncher(data_directory="D:\\data")
|
|
34
|
+
launcher.pull()
|
|
35
|
+
session = launcher.start()
|
|
36
|
+
session.close()
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
import io
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
import re
|
|
43
|
+
import subprocess
|
|
44
|
+
import tarfile
|
|
45
|
+
import time
|
|
46
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
47
|
+
import uuid
|
|
48
|
+
import warnings
|
|
49
|
+
|
|
50
|
+
import requests
|
|
51
|
+
import urllib3
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import grpc
|
|
55
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
56
|
+
raise RuntimeError("The grpc module must be installed for DockerLauncher")
|
|
57
|
+
except Exception: # pragma: no cover
|
|
58
|
+
raise RuntimeError("Cannot initialize grpc")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
import docker
|
|
62
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
63
|
+
raise RuntimeError("The docker module must be installed for DockerLauncher")
|
|
64
|
+
except Exception: # pragma: no cover
|
|
65
|
+
raise RuntimeError("Cannot initialize Docker")
|
|
66
|
+
|
|
67
|
+
from ansys.pyensight.core.common import (
|
|
68
|
+
GRPC_WARNING_MESSAGE,
|
|
69
|
+
find_unused_ports,
|
|
70
|
+
get_file_service,
|
|
71
|
+
grpc_version_check,
|
|
72
|
+
launch_enshell_interface,
|
|
73
|
+
populate_service_host_port,
|
|
74
|
+
pull_image,
|
|
75
|
+
)
|
|
76
|
+
from ansys.pyensight.core.launcher import Launcher
|
|
77
|
+
import ansys.pyensight.core.session
|
|
78
|
+
from ansys.pyensight.core.session import Session
|
|
79
|
+
|
|
80
|
+
if TYPE_CHECKING:
|
|
81
|
+
from docker import DockerClient
|
|
82
|
+
from docker.models.containers import Container
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _iter_tar_members_from_stream(stream_iter):
|
|
86
|
+
"""
|
|
87
|
+
Wrap docker-py's stream iterator into a file-like object that tarfile can read
|
|
88
|
+
in streaming mode ('r|*'). This prevents buffering the entire archive.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
class _GenReader(io.RawIOBase):
|
|
92
|
+
def __init__(self, chunks_iter):
|
|
93
|
+
self._iter = iter(chunks_iter)
|
|
94
|
+
self._buf = bytearray()
|
|
95
|
+
self._closed = False
|
|
96
|
+
|
|
97
|
+
def readable(self):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def readinto(self, b):
|
|
101
|
+
# Fill 'b' with data from internal buffer; fetch from iterator if needed.
|
|
102
|
+
if self._closed:
|
|
103
|
+
return 0
|
|
104
|
+
while len(self._buf) < len(b):
|
|
105
|
+
try:
|
|
106
|
+
next_chunk = next(self._iter)
|
|
107
|
+
if not next_chunk:
|
|
108
|
+
break
|
|
109
|
+
self._buf.extend(next_chunk)
|
|
110
|
+
except StopIteration:
|
|
111
|
+
break
|
|
112
|
+
n = min(len(b), len(self._buf))
|
|
113
|
+
if n:
|
|
114
|
+
b[:n] = self._buf[:n]
|
|
115
|
+
del self._buf[:n]
|
|
116
|
+
return n
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
def close(self):
|
|
120
|
+
self._closed = True
|
|
121
|
+
super().close()
|
|
122
|
+
|
|
123
|
+
gen_reader = _GenReader(stream_iter)
|
|
124
|
+
# Tarfile in stream mode; yields members one-by-one
|
|
125
|
+
tf = tarfile.open(fileobj=gen_reader, mode="r|*")
|
|
126
|
+
return tf, gen_reader
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def find_ansys_version_dir(container) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Stream the /ansys_inc archive until we find a path matching:
|
|
132
|
+
ansys_inc/v###/CEI/BUILDINFO.txt
|
|
133
|
+
Returns 'v###' or None if not found.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
stream, stat = container.get_archive("/ansys_inc")
|
|
137
|
+
except docker.errors.APIError:
|
|
138
|
+
# /ansys_inc might not exist
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
tf, gen_reader = _iter_tar_members_from_stream(stream)
|
|
142
|
+
|
|
143
|
+
version_dir = ""
|
|
144
|
+
version_re = re.compile(r"^ansys_inc/(v\d{3})")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
while True:
|
|
148
|
+
member = tf.next()
|
|
149
|
+
if member is None:
|
|
150
|
+
break # end of stream
|
|
151
|
+
# Tar headers use forward slashes; names are relative to the requested path
|
|
152
|
+
name = member.name
|
|
153
|
+
m = version_re.match(name)
|
|
154
|
+
if m:
|
|
155
|
+
version_dir = str(m.group(1))
|
|
156
|
+
break # we can stop early
|
|
157
|
+
finally:
|
|
158
|
+
# Close the streaming reader to avoid pulling the remainder of the archive
|
|
159
|
+
try:
|
|
160
|
+
tf.close()
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
try:
|
|
164
|
+
gen_reader.close()
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
return version_dir
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def read_buildinfo(container, version_dir: str) -> bytes:
|
|
172
|
+
"""
|
|
173
|
+
Fetch just the BUILDINFO.txt file as a small tar and extract its bytes.
|
|
174
|
+
"""
|
|
175
|
+
target_path = f"/ansys_inc/{version_dir}/CEI/BUILDINFO.txt"
|
|
176
|
+
stream, stat = container.get_archive(target_path)
|
|
177
|
+
|
|
178
|
+
# The returned tar contains the single file; extract into memory
|
|
179
|
+
file_bytes = io.BytesIO()
|
|
180
|
+
# Accumulate minimally; typically tiny
|
|
181
|
+
for chunk in stream:
|
|
182
|
+
file_bytes.write(chunk)
|
|
183
|
+
file_bytes.seek(0)
|
|
184
|
+
text = b""
|
|
185
|
+
with tarfile.open(fileobj=file_bytes, mode="r:*") as tf:
|
|
186
|
+
for member in tf.getmembers():
|
|
187
|
+
if member.isfile():
|
|
188
|
+
f = tf.extractfile(member)
|
|
189
|
+
if f:
|
|
190
|
+
text = f.read()
|
|
191
|
+
return text
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class DockerLauncher(Launcher):
|
|
195
|
+
"""Creates a ``Session`` instance using a copy of EnSight hosted in a Docker container.
|
|
196
|
+
|
|
197
|
+
This class allows you to either attach to a `PIM <https://github.com/ansys/pypim>`_-launched
|
|
198
|
+
Docker container hosting EnSight or launch a local Docker container hosting EnSight.
|
|
199
|
+
gRPC connections to EnShell and EnSight are established. A PyEnSight ``Session`` instance
|
|
200
|
+
is returned upon successful launch and connection.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
data_directory : str, optional
|
|
205
|
+
Host directory to make into the container at ``/data``.
|
|
206
|
+
The default is ``None``.
|
|
207
|
+
docker_image_name : str, optional
|
|
208
|
+
Name of the Docker image to use. The default is ``None``.
|
|
209
|
+
use_dev : bool, optional
|
|
210
|
+
Whether to use the latest ``ensight_dev`` Docker image. The
|
|
211
|
+
default is ``False``. However, this value is overridden if the
|
|
212
|
+
name of a Docker image is specified for the ``docker_image_name``
|
|
213
|
+
parameter.
|
|
214
|
+
channel :
|
|
215
|
+
Existing gRPC channel to a running ``EnShell`` instance provided by PIM.
|
|
216
|
+
pim_instance :
|
|
217
|
+
PyPIM instance if using PIM (internal). The default is ``None``.
|
|
218
|
+
timeout : float, optional
|
|
219
|
+
Number of seconds to try a gRPC connection before giving up.
|
|
220
|
+
This parameter is defined on the parent ``Launcher`` class,
|
|
221
|
+
where the default is ``120``.
|
|
222
|
+
use_egl : bool, optional
|
|
223
|
+
Whether to use EGL hardware for accelerated graphics. The platform
|
|
224
|
+
must be able to support this hardware. This parameter is defined on
|
|
225
|
+
the parent ``Launcher`` class, where the default is ``False``.
|
|
226
|
+
use_sos : int, optional
|
|
227
|
+
Number of EnSight servers to use for SOS (Server of Server) mode.
|
|
228
|
+
This parameter is defined on the parent ``Launcher`` class, where
|
|
229
|
+
the default is ``None``, in which case SOS mode is not used.
|
|
230
|
+
additional_command_line_options: list, optional
|
|
231
|
+
Additional command line options to be used to launch EnSight.
|
|
232
|
+
Arguments that contain spaces are not supported.
|
|
233
|
+
grpc_use_tcp_sockets :
|
|
234
|
+
If using gRPC, and if True, then allow TCP Socket based connections
|
|
235
|
+
instead of only local connections.
|
|
236
|
+
grpc_allow_network_connections :
|
|
237
|
+
If using gRPC and using TCP Socket based connections, listen on all networks.
|
|
238
|
+
grpc_disable_tls :
|
|
239
|
+
If using gRPC and using TCP Socket based connections, disable TLS.
|
|
240
|
+
grpc_uds_pathname :
|
|
241
|
+
If using gRPC and using Unix Domain Socket based connections, explicitly
|
|
242
|
+
set the pathname to the shared UDS file instead of using the default.
|
|
243
|
+
|
|
244
|
+
Examples
|
|
245
|
+
--------
|
|
246
|
+
>>> from ansys.pyensight.core import DockerLauncher
|
|
247
|
+
>>> launcher = DockerLauncher(data_directory="D:\\data")
|
|
248
|
+
>>> launcher.pull()
|
|
249
|
+
>>> session = launcher.start()
|
|
250
|
+
>>> session.close()
|
|
251
|
+
|
|
252
|
+
WARNING:
|
|
253
|
+
Overriding the default values for these options: grpc_use_tcp_sockets, grpc_allow_network_connections,
|
|
254
|
+
and grpc_disable_tls
|
|
255
|
+
can possibly permit control of this computer and any data which resides on it.
|
|
256
|
+
Modification of this configuration is not recommended. Please see the
|
|
257
|
+
documentation for your installed product for additional information.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(
|
|
261
|
+
self,
|
|
262
|
+
data_directory: Optional[str] = None,
|
|
263
|
+
docker_image_name: Optional[str] = None,
|
|
264
|
+
use_dev: bool = False,
|
|
265
|
+
channel: Optional[grpc.Channel] = None,
|
|
266
|
+
pim_instance: Optional[Any] = None,
|
|
267
|
+
grpc_use_tcp_sockets: Optional[bool] = False,
|
|
268
|
+
grpc_allow_network_connections: Optional[bool] = False,
|
|
269
|
+
grpc_disable_tls: Optional[bool] = False,
|
|
270
|
+
grpc_uds_pathname: Optional[str] = None,
|
|
271
|
+
**kwargs,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Initialize DockerLauncher."""
|
|
274
|
+
super().__init__(**kwargs)
|
|
275
|
+
|
|
276
|
+
self._data_directory = data_directory
|
|
277
|
+
self._enshell_grpc_channel = channel
|
|
278
|
+
self._grpc_use_tcp_sockets = grpc_use_tcp_sockets
|
|
279
|
+
self._grpc_allow_network_connections = grpc_allow_network_connections
|
|
280
|
+
self._grpc_disable_tls = grpc_disable_tls
|
|
281
|
+
self._grpc_uds_pathname = grpc_uds_pathname
|
|
282
|
+
self._service_uris: Dict[Any, str] = {}
|
|
283
|
+
self._image_name: Optional[str] = None
|
|
284
|
+
self._docker_client: Optional["DockerClient"] = None
|
|
285
|
+
self._container: Optional["Container"] = None
|
|
286
|
+
self._enshell: Optional[Any] = None
|
|
287
|
+
self._pim_instance: Optional[Any] = pim_instance
|
|
288
|
+
|
|
289
|
+
# EnSight session secret key
|
|
290
|
+
self._secret_key: str = str(uuid.uuid1())
|
|
291
|
+
# temporary directory
|
|
292
|
+
# it's in the ephemeral container, so just use "ensight's"
|
|
293
|
+
# home directory within the container
|
|
294
|
+
self._session_directory: str = "/home/ensight"
|
|
295
|
+
# the Ansys / EnSight version we found in the container
|
|
296
|
+
# to be reassigned later
|
|
297
|
+
self._ansys_version: Optional[str] = None
|
|
298
|
+
# EnShell's log file
|
|
299
|
+
self._enshell_log_file = None
|
|
300
|
+
# pointer to the file service object if available
|
|
301
|
+
self._pim_file_service = None
|
|
302
|
+
|
|
303
|
+
logging.debug(
|
|
304
|
+
f"DockerLauncher data_dir={self._data_directory}\n"
|
|
305
|
+
+ f" image_name={self._image_name}\n"
|
|
306
|
+
+ f" use_dev={use_dev}\n"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if self._enshell_grpc_channel and self._pim_instance:
|
|
310
|
+
service_set = ["grpc_private", "http", "ws"]
|
|
311
|
+
# if self._launch_webui:
|
|
312
|
+
# service_set.append("webui")
|
|
313
|
+
if not set(service_set).issubset(self._pim_instance.services):
|
|
314
|
+
raise RuntimeError(
|
|
315
|
+
"If channel is specified, the PIM instance must have a list of length 3 "
|
|
316
|
+
+ "containing the appropriate service URIs. It does not."
|
|
317
|
+
)
|
|
318
|
+
# grab the URIs for the 3 required services passed in from PIM
|
|
319
|
+
self._service_host_port = populate_service_host_port(
|
|
320
|
+
self._pim_instance, {}, webui=self._launch_webui
|
|
321
|
+
)
|
|
322
|
+
# attach to the file service if available
|
|
323
|
+
self._pim_file_service = get_file_service(self._pim_instance)
|
|
324
|
+
# if using PIM, we have a query parameter to append to http requests
|
|
325
|
+
if self._pim_instance is not None:
|
|
326
|
+
d = {"instance_name": self._pim_instance.name}
|
|
327
|
+
self._add_query_parameters(d)
|
|
328
|
+
#
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
# EnShell gRPC port, EnSight gRPC port, HTTP port, WSS port
|
|
332
|
+
# skip 1999 as that internal to the container is used to the container for the VNC connection
|
|
333
|
+
num_ports = 4
|
|
334
|
+
if self._launch_webui:
|
|
335
|
+
num_ports += 1
|
|
336
|
+
if self._vtk_ws_port:
|
|
337
|
+
num_ports += 1
|
|
338
|
+
ports = find_unused_ports(num_ports, avoid=[1999])
|
|
339
|
+
if ports is None: # pragma: no cover
|
|
340
|
+
raise RuntimeError(
|
|
341
|
+
"Unable to allocate local ports for EnSight session"
|
|
342
|
+
) # pragma: no cover
|
|
343
|
+
self._service_host_port = {}
|
|
344
|
+
self._service_host_port["grpc"] = ("127.0.0.1", ports[0])
|
|
345
|
+
self._service_host_port["grpc_private"] = ("127.0.0.1", ports[1])
|
|
346
|
+
self._service_host_port["http"] = ("127.0.0.1", ports[2])
|
|
347
|
+
self._service_host_port["ws"] = ("127.0.0.1", ports[3])
|
|
348
|
+
if self._launch_webui:
|
|
349
|
+
self._service_host_port["webui"] = ("127.0.0.1", ports[4])
|
|
350
|
+
# This option for the moment is only needed for testing purposes
|
|
351
|
+
if self._vtk_ws_port:
|
|
352
|
+
port = ports[5] if self._launch_webui else ports[4]
|
|
353
|
+
self._service_host_port["vtk_ws"] = ("127.0.0.1", port)
|
|
354
|
+
|
|
355
|
+
# get the optional user-specified image name
|
|
356
|
+
# Note: the default name needs to change over time... TODO
|
|
357
|
+
self._image_name = "ghcr.io/ansys-internal/ensight"
|
|
358
|
+
if use_dev:
|
|
359
|
+
self._image_name = "ghcr.io/ansys-internal/ensight_dev"
|
|
360
|
+
if docker_image_name:
|
|
361
|
+
self._image_name = docker_image_name
|
|
362
|
+
|
|
363
|
+
# Load up Docker from the user's environment
|
|
364
|
+
self._docker_client = docker.from_env()
|
|
365
|
+
|
|
366
|
+
def ansys_version(self) -> Optional[str]:
|
|
367
|
+
"""Get the Ansys version (three-digit string) found in the Docker container.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
str
|
|
372
|
+
Ansys version or ``None`` if not found or not started.
|
|
373
|
+
|
|
374
|
+
"""
|
|
375
|
+
return self._ansys_version
|
|
376
|
+
|
|
377
|
+
def pull(self) -> None:
|
|
378
|
+
"""Pull the Docker image.
|
|
379
|
+
|
|
380
|
+
Raises
|
|
381
|
+
------
|
|
382
|
+
RuntimeError
|
|
383
|
+
If the Docker image couldn't be pulled.
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
pull_image(self._docker_client, self._image_name)
|
|
387
|
+
|
|
388
|
+
def _get_container_env(self) -> Dict:
|
|
389
|
+
# Create the environmental variables
|
|
390
|
+
# Environment to pass into the container
|
|
391
|
+
container_env = {
|
|
392
|
+
"ENSIGHT_SECURITY_TOKEN": self._secret_key,
|
|
393
|
+
"WEBSOCKETSERVER_SECURITY_TOKEN": self._secret_key,
|
|
394
|
+
"ENSIGHT_SESSION_TEMPDIR": self._session_directory,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# If for some reason, the ENSIGHT_ANSYS_LAUNCH is set previously,
|
|
398
|
+
# honor that value, otherwise set it to "pyensight". This allows
|
|
399
|
+
# for an environmental setup to set the value to something else
|
|
400
|
+
# (e.g. their "app").
|
|
401
|
+
if "ENSIGHT_ANSYS_LAUNCH" not in os.environ:
|
|
402
|
+
container_env["ENSIGHT_ANSYS_LAUNCH"] = "container"
|
|
403
|
+
else:
|
|
404
|
+
container_env["ENSIGHT_ANSYS_LAUNCH"] = os.environ["ENSIGHT_ANSYS_LAUNCH"]
|
|
405
|
+
|
|
406
|
+
if self._pim_instance is None:
|
|
407
|
+
container_env["ANSYSLMD_LICENSE_FILE"] = os.environ["ANSYSLMD_LICENSE_FILE"]
|
|
408
|
+
if "ENSIGHT_ANSYS_APIP_CONFIG" in os.environ:
|
|
409
|
+
container_env["ENSIGHT_ANSYS_APIP_CONFIG"] = os.environ["ENSIGHT_ANSYS_APIP_CONFIG"]
|
|
410
|
+
|
|
411
|
+
if self._launch_webui:
|
|
412
|
+
container_env["SIMBA_WEBSERVER_TOKEN"] = self._secret_key
|
|
413
|
+
container_env["FLUENT_WEBSERVER_TOKEN"] = self._secret_key
|
|
414
|
+
|
|
415
|
+
return container_env
|
|
416
|
+
|
|
417
|
+
def start(self) -> "Session":
|
|
418
|
+
"""Start EnShell by running a local Docker EnSight image.
|
|
419
|
+
|
|
420
|
+
Once this method starts EnShell, it connects over gRPC to EnShell running
|
|
421
|
+
in the Docker container. Once connected, EnShell launches a copy of EnSight
|
|
422
|
+
and WSS in the container. It then creates and binds a
|
|
423
|
+
:class:`Session<ansys.pyensight.core.Session>` instance to the created
|
|
424
|
+
EnSight gRPC connection and returns this instance.
|
|
425
|
+
|
|
426
|
+
Returns
|
|
427
|
+
-------
|
|
428
|
+
object
|
|
429
|
+
PyEnSight ``Session`` object instance.
|
|
430
|
+
|
|
431
|
+
Raises
|
|
432
|
+
------
|
|
433
|
+
RuntimeError
|
|
434
|
+
Variety of error conditions.
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
tmp_session = super().start()
|
|
438
|
+
if tmp_session:
|
|
439
|
+
return tmp_session
|
|
440
|
+
|
|
441
|
+
# Launch the EnSight Docker container locally as a detached container
|
|
442
|
+
# initially running EnShell over the first gRPC port. Then launch EnSight
|
|
443
|
+
# and other apps.
|
|
444
|
+
|
|
445
|
+
# get the environment to pass to the container
|
|
446
|
+
container_env = self._get_container_env()
|
|
447
|
+
|
|
448
|
+
# Ports to map between the host and the container
|
|
449
|
+
# If we're here in the code, then we're not using PIM
|
|
450
|
+
# and we're not really using URIs where the hostname
|
|
451
|
+
# is anything other than 127.0.0.1, so, we only need
|
|
452
|
+
# to grab the port numbers.
|
|
453
|
+
grpc_port = self._service_host_port["grpc"][1]
|
|
454
|
+
ports_to_map = {
|
|
455
|
+
str(self._service_host_port["grpc"][1])
|
|
456
|
+
+ "/tcp": str(self._service_host_port["grpc"][1]),
|
|
457
|
+
str(self._service_host_port["grpc_private"][1])
|
|
458
|
+
+ "/tcp": str(self._service_host_port["grpc_private"][1]),
|
|
459
|
+
str(self._service_host_port["http"][1])
|
|
460
|
+
+ "/tcp": str(self._service_host_port["http"][1]),
|
|
461
|
+
str(self._service_host_port["ws"][1]) + "/tcp": str(self._service_host_port["ws"][1]),
|
|
462
|
+
}
|
|
463
|
+
if self._launch_webui:
|
|
464
|
+
ports_to_map.update(
|
|
465
|
+
{
|
|
466
|
+
str(self._service_host_port["webui"][1])
|
|
467
|
+
+ "/tcp": str(self._service_host_port["webui"][1])
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
if self._service_host_port.get("vtk_ws"):
|
|
471
|
+
ports_to_map.update(
|
|
472
|
+
{
|
|
473
|
+
str(self._service_host_port["vtk_ws"][1])
|
|
474
|
+
+ "/tcp": str(self._service_host_port["vtk_ws"][1])
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
# The data directory to map into the container
|
|
478
|
+
data_volume = None
|
|
479
|
+
if self._data_directory:
|
|
480
|
+
data_volume = {self._data_directory: {"bind": "/data", "mode": "rw"}}
|
|
481
|
+
|
|
482
|
+
# Start the container in detached mode with EnShell as a
|
|
483
|
+
# gRPC server as the command
|
|
484
|
+
#
|
|
485
|
+
enshell_cmd = "-app -v 3 -grpc_server " + str(grpc_port)
|
|
486
|
+
if self._grpc_use_tcp_sockets:
|
|
487
|
+
enshell_cmd += " -grpc_use_tcp_sockets"
|
|
488
|
+
if self._grpc_allow_network_connections:
|
|
489
|
+
enshell_cmd += " -grpc_allow_network_connections"
|
|
490
|
+
if self._grpc_disable_tls:
|
|
491
|
+
enshell_cmd += " -grpc_disable_tls"
|
|
492
|
+
if self._grpc_uds_pathname:
|
|
493
|
+
enshell_cmd += " -grpc_uds_pathname " + self._grpc_uds_pathname
|
|
494
|
+
|
|
495
|
+
use_egl = self._use_egl()
|
|
496
|
+
|
|
497
|
+
logging.debug("Starting Container...\n")
|
|
498
|
+
if data_volume:
|
|
499
|
+
if use_egl: # pragma: no cover
|
|
500
|
+
if self._docker_client:
|
|
501
|
+
self._container = self._docker_client.containers.run( # pragma: no cover
|
|
502
|
+
self._image_name,
|
|
503
|
+
command=enshell_cmd,
|
|
504
|
+
volumes=data_volume,
|
|
505
|
+
environment=container_env,
|
|
506
|
+
device_requests=[
|
|
507
|
+
docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])
|
|
508
|
+
],
|
|
509
|
+
ports=ports_to_map,
|
|
510
|
+
tty=True,
|
|
511
|
+
detach=True,
|
|
512
|
+
auto_remove=True,
|
|
513
|
+
remove=True,
|
|
514
|
+
user="ensight:ensight",
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
logging.debug(f"Running container {self._image_name} with cmd {enshell_cmd}\n")
|
|
518
|
+
logging.debug(f"ports to map: {ports_to_map}\n")
|
|
519
|
+
if self._docker_client:
|
|
520
|
+
self._container = self._docker_client.containers.run(
|
|
521
|
+
self._image_name,
|
|
522
|
+
command=enshell_cmd,
|
|
523
|
+
volumes=data_volume,
|
|
524
|
+
environment=container_env,
|
|
525
|
+
ports=ports_to_map,
|
|
526
|
+
tty=True,
|
|
527
|
+
detach=True,
|
|
528
|
+
auto_remove=True,
|
|
529
|
+
remove=True,
|
|
530
|
+
user="ensight:ensight",
|
|
531
|
+
)
|
|
532
|
+
logging.debug(f"_container = {str(self._container)}\n")
|
|
533
|
+
else:
|
|
534
|
+
if use_egl: # pragma: no cover
|
|
535
|
+
if self._docker_client: # pragma: no cover
|
|
536
|
+
self._container = self._docker_client.containers.run(
|
|
537
|
+
self._image_name,
|
|
538
|
+
command=enshell_cmd,
|
|
539
|
+
environment=container_env,
|
|
540
|
+
device_requests=[
|
|
541
|
+
docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])
|
|
542
|
+
],
|
|
543
|
+
ports=ports_to_map,
|
|
544
|
+
tty=True,
|
|
545
|
+
detach=True,
|
|
546
|
+
auto_remove=True,
|
|
547
|
+
remove=True,
|
|
548
|
+
user="ensight:ensight",
|
|
549
|
+
)
|
|
550
|
+
else: # pragma: no cover
|
|
551
|
+
logging.debug(
|
|
552
|
+
f"Running container {self._image_name} with cmd {enshell_cmd}\n"
|
|
553
|
+
) # pragma: no cover
|
|
554
|
+
logging.debug(f"ports to map: {ports_to_map}\n") # pragma: no cover
|
|
555
|
+
if self._docker_client: # pragma: no cover
|
|
556
|
+
self._container = self._docker_client.containers.run( # pragma: no cover
|
|
557
|
+
self._image_name,
|
|
558
|
+
command=enshell_cmd,
|
|
559
|
+
environment=container_env,
|
|
560
|
+
ports=ports_to_map,
|
|
561
|
+
tty=True,
|
|
562
|
+
detach=True,
|
|
563
|
+
auto_remove=True,
|
|
564
|
+
remove=True,
|
|
565
|
+
user="ensight:ensight",
|
|
566
|
+
)
|
|
567
|
+
# logging.debug(f"_container = {str(self._container)}\n")
|
|
568
|
+
logging.debug("Container started.\n")
|
|
569
|
+
self._has_grpc_changes = self._grpc_version_check()
|
|
570
|
+
if not self._has_grpc_changes:
|
|
571
|
+
warnings.warn(GRPC_WARNING_MESSAGE)
|
|
572
|
+
return self.connect()
|
|
573
|
+
|
|
574
|
+
def _list_ansys_versions_from_image(self) -> List[str]:
|
|
575
|
+
"""List available Ansys version directories from the image using archives only.
|
|
576
|
+
|
|
577
|
+
Returns
|
|
578
|
+
-------
|
|
579
|
+
list of str
|
|
580
|
+
Version numbers like ["261", "260"]. Empty list if none found.
|
|
581
|
+
"""
|
|
582
|
+
# Stream the tar headers and avoid downloading file contents
|
|
583
|
+
if not self._container:
|
|
584
|
+
return []
|
|
585
|
+
try:
|
|
586
|
+
bits, _ = self._container.get_archive("/ansys_inc")
|
|
587
|
+
|
|
588
|
+
class _ChunkStream:
|
|
589
|
+
def __init__(self, gen):
|
|
590
|
+
self._gen = gen
|
|
591
|
+
self._buf = b""
|
|
592
|
+
self._closed = False
|
|
593
|
+
|
|
594
|
+
def read(self, size=-1):
|
|
595
|
+
if self._closed:
|
|
596
|
+
return b""
|
|
597
|
+
if size is None or size < 0:
|
|
598
|
+
# tarfile may request large reads; provide buffered then next chunk
|
|
599
|
+
if self._buf:
|
|
600
|
+
data = self._buf
|
|
601
|
+
self._buf = b""
|
|
602
|
+
return data
|
|
603
|
+
try:
|
|
604
|
+
return next(self._gen)
|
|
605
|
+
except StopIteration:
|
|
606
|
+
self._closed = True
|
|
607
|
+
return b""
|
|
608
|
+
# ensure at least size bytes
|
|
609
|
+
while len(self._buf) < size and not self._closed:
|
|
610
|
+
try:
|
|
611
|
+
self._buf += next(self._gen)
|
|
612
|
+
except StopIteration:
|
|
613
|
+
self._closed = True
|
|
614
|
+
break
|
|
615
|
+
data, self._buf = self._buf[:size], self._buf[size:]
|
|
616
|
+
return data
|
|
617
|
+
|
|
618
|
+
def close(self):
|
|
619
|
+
self._closed = True
|
|
620
|
+
|
|
621
|
+
stream = _ChunkStream(bits)
|
|
622
|
+
# Use streaming mode to iterate headers only
|
|
623
|
+
import tarfile
|
|
624
|
+
|
|
625
|
+
tar = tarfile.open(mode="r|*", fileobj=stream) # type: ignore[arg-type]
|
|
626
|
+
versions: List[str] = []
|
|
627
|
+
prefix = "ansys_inc/"
|
|
628
|
+
for m in tar: # streamed iteration over members
|
|
629
|
+
# Only consider directory entries at top-level under ansys_inc
|
|
630
|
+
name = m.name
|
|
631
|
+
if not name.startswith(prefix):
|
|
632
|
+
continue
|
|
633
|
+
rel = name[len(prefix) :]
|
|
634
|
+
if not rel:
|
|
635
|
+
continue
|
|
636
|
+
# Normalize to have trailing slash for dirs
|
|
637
|
+
is_dir = m.isdir() or name.endswith("/")
|
|
638
|
+
if not is_dir:
|
|
639
|
+
# Skip files; we don't want file payloads (stream mode avoids reading them)
|
|
640
|
+
continue
|
|
641
|
+
# Immediate children have exactly one '/'
|
|
642
|
+
if rel.count("/") == 1 and re.fullmatch(r"v\d{3}/", rel):
|
|
643
|
+
versions.append(rel[1:4])
|
|
644
|
+
# If we have collected a reasonable number, we can optionally break
|
|
645
|
+
# but generally keep scanning headers; no file bodies are read.
|
|
646
|
+
tar.close()
|
|
647
|
+
stream.close()
|
|
648
|
+
return sorted(set(versions), key=lambda s: int(s), reverse=True)
|
|
649
|
+
except Exception:
|
|
650
|
+
return []
|
|
651
|
+
|
|
652
|
+
def _get_buildinfo_from_image(self) -> str:
|
|
653
|
+
"""Read /ansys_inc/vXXX/CEI/BUILDINFO.txt from the image using archives only.
|
|
654
|
+
|
|
655
|
+
Returns
|
|
656
|
+
-------
|
|
657
|
+
str | None
|
|
658
|
+
The file content, or None if unavailable.
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
version_dir = find_ansys_version_dir(self._container)
|
|
662
|
+
if not version_dir:
|
|
663
|
+
return ""
|
|
664
|
+
content = read_buildinfo(self._container, version_dir)
|
|
665
|
+
if not content:
|
|
666
|
+
return ""
|
|
667
|
+
try:
|
|
668
|
+
text = content.decode("utf-8", errors="replace")
|
|
669
|
+
except Exception:
|
|
670
|
+
text = content.decode("latin1", errors="replace")
|
|
671
|
+
return text
|
|
672
|
+
|
|
673
|
+
def launch_webui(self, container_env_str):
|
|
674
|
+
# Run websocketserver
|
|
675
|
+
cmd = f"/ansys_inc/v{self._ansys_version}/FluidsOne/server/linx64/fluidsone"
|
|
676
|
+
cmd += " --main-run-mode post"
|
|
677
|
+
# websocket port - this needs to come first since we now have
|
|
678
|
+
# --add_header as a optional arg that can take an arbitrary
|
|
679
|
+
# number of optional headers.
|
|
680
|
+
webui_port = self._service_host_port["webui"][1]
|
|
681
|
+
grpc_port = self._service_host_port["grpc_private"][1]
|
|
682
|
+
http_port = self._service_host_port["http"][1]
|
|
683
|
+
ws_port = self._service_host_port["ws"][1]
|
|
684
|
+
cmd += f" --server-listen-port {webui_port}"
|
|
685
|
+
cmd += f" --server-web-roots /ansys_inc/v{self._ansys_version}/FluidsOne/web/ui"
|
|
686
|
+
cmd += f" --ensight-grpc-port {grpc_port}"
|
|
687
|
+
cmd += f" --ensight-html-port {http_port}"
|
|
688
|
+
cmd += f" --ensight-ws-port {ws_port}"
|
|
689
|
+
cmd += f" --ensight-session-directory {self._session_directory}"
|
|
690
|
+
cmd += f" --ensight-secret-key {self._secret_key}"
|
|
691
|
+
cmd += " --main-show-gui 'False'"
|
|
692
|
+
logging.debug(f"Starting WebUI: {cmd}\n")
|
|
693
|
+
ret = self._enshell.start_other(cmd, extra_env=container_env_str)
|
|
694
|
+
if ret[0] != 0: # pragma: no cover
|
|
695
|
+
self.stop() # pragma: no cover
|
|
696
|
+
raise RuntimeError(f"Error starting WebUI: {cmd}\n") # pragma: no cover
|
|
697
|
+
|
|
698
|
+
def _exec_run(self, commands: Union[str, List[str]]):
|
|
699
|
+
"""Wrapper around the container exec run to try up to 5 times."""
|
|
700
|
+
counter = 0
|
|
701
|
+
if not self._container:
|
|
702
|
+
raise RuntimeError("Exec run can be called only when the container is up.")
|
|
703
|
+
while counter < 10:
|
|
704
|
+
try:
|
|
705
|
+
return self._container.exec_run(commands)
|
|
706
|
+
except (requests.exceptions.ConnectionError, urllib3.exceptions.ProtocolError) as exc:
|
|
707
|
+
counter += 1
|
|
708
|
+
time.sleep(0.5)
|
|
709
|
+
if counter == 10:
|
|
710
|
+
raise exc
|
|
711
|
+
|
|
712
|
+
def _get_build_info(self):
|
|
713
|
+
# The unit test has no container
|
|
714
|
+
if not self._container:
|
|
715
|
+
return "mock"
|
|
716
|
+
res = self._exec_run(["sh", "-lc", "ls -1 /ansys_inc 2>/dev/null"])
|
|
717
|
+
entries = [
|
|
718
|
+
e.strip() for e in res.output.decode("utf-8", "replace").splitlines() if e.strip()
|
|
719
|
+
]
|
|
720
|
+
vdir = None
|
|
721
|
+
for e in entries:
|
|
722
|
+
if re.fullmatch(r"v\d{3}", e):
|
|
723
|
+
vdir = e
|
|
724
|
+
break
|
|
725
|
+
path = f"/ansys_inc/{vdir}/CEI/BUILDINFO.txt"
|
|
726
|
+
res2 = self._exec_run(["cat", path])
|
|
727
|
+
return res2.output.decode("utf-8", errors="replace")
|
|
728
|
+
|
|
729
|
+
def _grpc_version_check(self):
|
|
730
|
+
text = self._get_buildinfo_from_image()
|
|
731
|
+
if text == "mock":
|
|
732
|
+
return True
|
|
733
|
+
internal_version, ensight_full_version = self._get_versionfrom_buildinfo(text)
|
|
734
|
+
return grpc_version_check(internal_version, ensight_full_version)
|
|
735
|
+
|
|
736
|
+
def connect(self):
|
|
737
|
+
"""Create and bind a :class:`Session<ansys.pyensight.core.Session>` instance
|
|
738
|
+
to the created EnSight gRPC connection started by EnShell.
|
|
739
|
+
|
|
740
|
+
This method is an internal method.
|
|
741
|
+
|
|
742
|
+
Returns
|
|
743
|
+
-------
|
|
744
|
+
obj
|
|
745
|
+
:class:`Session<ansys.pyensight.core.Session>` instance
|
|
746
|
+
|
|
747
|
+
Raises
|
|
748
|
+
------
|
|
749
|
+
RuntimeError:
|
|
750
|
+
Variety of error conditions.
|
|
751
|
+
"""
|
|
752
|
+
#
|
|
753
|
+
#
|
|
754
|
+
# Start up the EnShell gRPC interface
|
|
755
|
+
self._enshell = launch_enshell_interface(
|
|
756
|
+
self._enshell_grpc_channel,
|
|
757
|
+
self._service_host_port["grpc"][1],
|
|
758
|
+
self._timeout,
|
|
759
|
+
self._secret_key,
|
|
760
|
+
grpc_allow_network_connections=self._grpc_allow_network_connections,
|
|
761
|
+
grpc_disable_tls=self._grpc_disable_tls,
|
|
762
|
+
grpc_uds_pathname=self._grpc_uds_pathname,
|
|
763
|
+
grpc_use_tcp_sockets=self._grpc_use_tcp_sockets,
|
|
764
|
+
disable_grpc_options=not self._has_grpc_changes,
|
|
765
|
+
)
|
|
766
|
+
log_dir = "/data"
|
|
767
|
+
if self._enshell_grpc_channel: # pragma: no cover
|
|
768
|
+
log_dir = "/work"
|
|
769
|
+
|
|
770
|
+
if not self._enshell.is_connected(): # pragma: no cover
|
|
771
|
+
self.stop() # pragma: no cover
|
|
772
|
+
raise RuntimeError("Can't connect to EnShell over gRPC.") # pragma: no cover
|
|
773
|
+
|
|
774
|
+
cmd = "set_no_reroute_log"
|
|
775
|
+
ret = self._enshell.run_command(cmd)
|
|
776
|
+
logging.debug(f"enshell cmd: {cmd} ret: {ret}\n")
|
|
777
|
+
if ret[0] != 0:
|
|
778
|
+
self.stop()
|
|
779
|
+
raise RuntimeError(f"Error sending EnShell command: {cmd} ret: {ret}")
|
|
780
|
+
|
|
781
|
+
files_to_try = [log_dir + "/enshell.log", "/home/ensight/enshell.log"]
|
|
782
|
+
for f in files_to_try: # pragma: no cover
|
|
783
|
+
cmd = "set_debug_log " + f
|
|
784
|
+
ret = self._enshell.run_command(cmd)
|
|
785
|
+
if ret[0] == 0:
|
|
786
|
+
self._enshell_log_file = f
|
|
787
|
+
break
|
|
788
|
+
else:
|
|
789
|
+
logging.debug(f"enshell error; cmd: {cmd} ret: {ret}\n")
|
|
790
|
+
|
|
791
|
+
if self._enshell_log_file is not None: # pragma: no cover
|
|
792
|
+
logging.debug(f"enshell log file {self._enshell_log_file}\n")
|
|
793
|
+
|
|
794
|
+
cmd = "verbose 3"
|
|
795
|
+
ret = self._enshell.run_command(cmd)
|
|
796
|
+
logging.debug(f"enshell cmd: {cmd} ret: {ret}\n")
|
|
797
|
+
if ret[0] != 0: # pragma: no cover
|
|
798
|
+
self.stop() # pragma: no cover
|
|
799
|
+
raise RuntimeError(
|
|
800
|
+
f"Error sending EnShell command: {cmd} ret: {ret}"
|
|
801
|
+
) # pragma: no cover
|
|
802
|
+
|
|
803
|
+
logging.debug("Connected to EnShell. Getting CEI_HOME and Ansys version...\n")
|
|
804
|
+
logging.debug(f" _enshell: {self._enshell}\n\n")
|
|
805
|
+
# Build up the command to run ensight via the EnShell gRPC interface
|
|
806
|
+
|
|
807
|
+
self._cei_home = self._enshell.cei_home()
|
|
808
|
+
self._ansys_version = self._enshell.ansys_version()
|
|
809
|
+
print("CEI_HOME=", self._cei_home)
|
|
810
|
+
print("Ansys Version=", self._ansys_version)
|
|
811
|
+
|
|
812
|
+
logging.debug("Got them. Starting EnSight...\n")
|
|
813
|
+
|
|
814
|
+
use_egl = self._use_egl()
|
|
815
|
+
|
|
816
|
+
# get the environment to pass to the container
|
|
817
|
+
container_env_str = ""
|
|
818
|
+
if self._pim_instance is not None:
|
|
819
|
+
container_env = self._get_container_env()
|
|
820
|
+
for i in container_env.items():
|
|
821
|
+
container_env_str += f"{i[0]}={i[1]}\n"
|
|
822
|
+
|
|
823
|
+
# Run EnSight
|
|
824
|
+
ensight_env_vars = None
|
|
825
|
+
if container_env_str != "": # pragma: no cover
|
|
826
|
+
ensight_env_vars = container_env_str # pragma: no cover
|
|
827
|
+
|
|
828
|
+
if use_egl:
|
|
829
|
+
if ensight_env_vars is None: # pragma: no cover
|
|
830
|
+
ensight_env_vars = (
|
|
831
|
+
"LD_PRELOAD=/usr/local/lib64/libGL.so.1:/usr/local/lib64/libEGL.so.1"
|
|
832
|
+
)
|
|
833
|
+
else:
|
|
834
|
+
ensight_env_vars += ( # pragma: no cover
|
|
835
|
+
"LD_PRELOAD=/usr/local/lib64/libGL.so.1:/usr/local/lib64/libEGL.so.1"
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
ensight_args = "-batch -v 3"
|
|
839
|
+
|
|
840
|
+
if use_egl:
|
|
841
|
+
ensight_args += " -egl"
|
|
842
|
+
|
|
843
|
+
if self._use_sos:
|
|
844
|
+
ensight_args += " -sos -nservers " + str(int(self._use_sos))
|
|
845
|
+
|
|
846
|
+
ensight_args += " -grpc_server " + str(self._service_host_port["grpc_private"][1])
|
|
847
|
+
|
|
848
|
+
if self._grpc_use_tcp_sockets:
|
|
849
|
+
ensight_args += " -grpc_use_tcp_sockets"
|
|
850
|
+
if self._grpc_allow_network_connections:
|
|
851
|
+
ensight_args += " -grpc_allow_network_connections"
|
|
852
|
+
if self._grpc_disable_tls:
|
|
853
|
+
ensight_args += " -grpc_disable_tls"
|
|
854
|
+
# Can't specify the same name; the default ought to be fine within the Docker Image
|
|
855
|
+
# if self._grpc_uds_pathname:
|
|
856
|
+
# enshell_cmd += " -grpc_uds_pathname "+self._grpc_uds_pathname
|
|
857
|
+
|
|
858
|
+
vnc_url = "vnc://%%3Frfb_port=1999%%26use_auth=0"
|
|
859
|
+
ensight_args += " -vnc " + vnc_url
|
|
860
|
+
if self._liben_rest:
|
|
861
|
+
ensight_args += " -rest_server " + str(self._service_host_port["http"][1])
|
|
862
|
+
if self._additional_command_line_options:
|
|
863
|
+
ensight_args += " "
|
|
864
|
+
ensight_args += " ".join(self._additional_command_line_options)
|
|
865
|
+
|
|
866
|
+
logging.debug(f"Starting EnSight with args: {ensight_args}\n")
|
|
867
|
+
if self._liben_rest:
|
|
868
|
+
ensight_location = "/ansys_inc/v" + self._ansys_version + "/CEI/bin/ensight "
|
|
869
|
+
ensight_args = ensight_location + ensight_args
|
|
870
|
+
ret = self._enshell.start_other(ensight_args, extra_env=ensight_env_vars)
|
|
871
|
+
else:
|
|
872
|
+
ret = self._enshell.start_ensight(ensight_args, ensight_env_vars)
|
|
873
|
+
if ret[0] != 0: # pragma: no cover
|
|
874
|
+
self.stop() # pragma: no cover
|
|
875
|
+
raise RuntimeError(
|
|
876
|
+
f"Error starting EnSight with args: {ensight_args}"
|
|
877
|
+
) # pragma: no cover
|
|
878
|
+
|
|
879
|
+
logging.debug("EnSight started. Starting wss...\n")
|
|
880
|
+
|
|
881
|
+
# Run websocketserver
|
|
882
|
+
if not self._liben_rest:
|
|
883
|
+
logging.debug(
|
|
884
|
+
"WebSocketserver script not being launched. WS server must be launched manually.\n\n"
|
|
885
|
+
)
|
|
886
|
+
wss_cmd = "cpython /ansys_inc/v" + self._ansys_version + "/CEI/nexus"
|
|
887
|
+
wss_cmd += self._ansys_version + "/nexus_launcher/websocketserver.py"
|
|
888
|
+
# websocket port - this needs to come first since we now have
|
|
889
|
+
# --add_header as a optional arg that can take an arbitrary
|
|
890
|
+
# number of optional headers.
|
|
891
|
+
if int(self._ansys_version) > 252 and self._do_not_start_ws:
|
|
892
|
+
wss_cmd += " -1"
|
|
893
|
+
else:
|
|
894
|
+
wss_cmd += " " + str(self._service_host_port["ws"][1])
|
|
895
|
+
#
|
|
896
|
+
wss_cmd += " --http_directory " + self._session_directory
|
|
897
|
+
# http port
|
|
898
|
+
wss_cmd += " --http_port " + str(self._service_host_port["http"][1])
|
|
899
|
+
# vnc port
|
|
900
|
+
if int(self._ansys_version) > 252 and self._rest_ws_separate_loops:
|
|
901
|
+
wss_cmd += " --separate_loops"
|
|
902
|
+
wss_cmd += f" --security_token {self._secret_key}"
|
|
903
|
+
wss_cmd += " --client_port 1999"
|
|
904
|
+
# optional PIM instance header
|
|
905
|
+
if self._pim_instance is not None:
|
|
906
|
+
# Add the PIM instance header. wss needs to return this optional
|
|
907
|
+
# header in each http response. It's how the Ansys Lab proxy
|
|
908
|
+
# knows how to map back to this particular container's IP and port.
|
|
909
|
+
wss_cmd += " --add_header instance_name=" + self._pim_instance.name
|
|
910
|
+
# EnSight REST API
|
|
911
|
+
if self._enable_rest_api:
|
|
912
|
+
# grpc port
|
|
913
|
+
wss_cmd += " --grpc_port " + str(self._service_host_port["grpc_private"][1])
|
|
914
|
+
if self._grpc_use_tcp_sockets:
|
|
915
|
+
wss_cmd += " --grpc_use_tcp_sockets"
|
|
916
|
+
if self._grpc_allow_network_connections:
|
|
917
|
+
wss_cmd += " --grpc_allow_network_connections"
|
|
918
|
+
if self._grpc_disable_tls:
|
|
919
|
+
wss_cmd += " --grpc_disable_tls"
|
|
920
|
+
if self._grpc_uds_pathname:
|
|
921
|
+
wss_cmd += " --grpc_uds_pathname"
|
|
922
|
+
wss_cmd += " " + self._grpc_uds_pathname
|
|
923
|
+
# EnVision sessions
|
|
924
|
+
wss_cmd += " --local_session envision 5"
|
|
925
|
+
|
|
926
|
+
wss_env_vars = None
|
|
927
|
+
if container_env_str != "": # pragma: no cover
|
|
928
|
+
wss_env_vars = container_env_str # pragma: no cover
|
|
929
|
+
|
|
930
|
+
logging.debug(f"Starting WSS: {wss_cmd}\n")
|
|
931
|
+
ret = self._enshell.start_other(wss_cmd, extra_env=wss_env_vars)
|
|
932
|
+
if ret[0] != 0: # pragma: no cover
|
|
933
|
+
self.stop() # pragma: no cover
|
|
934
|
+
raise RuntimeError(f"Error starting WSS: {wss_cmd}\n") # pragma: no cover
|
|
935
|
+
|
|
936
|
+
logging.debug("wss started. Making session...\n")
|
|
937
|
+
|
|
938
|
+
# build the session instance
|
|
939
|
+
# WARNING: assuming the host is the same for grpc_private, http, and ws
|
|
940
|
+
# This may not be true in the future if using PIM.
|
|
941
|
+
# revise Session to handle three different hosts if necessary.
|
|
942
|
+
use_sos = False
|
|
943
|
+
if self._use_sos:
|
|
944
|
+
use_sos = True
|
|
945
|
+
if self._pim_instance is None:
|
|
946
|
+
ws_port = self._service_host_port["ws"][1]
|
|
947
|
+
else:
|
|
948
|
+
ws_port = self._service_host_port["http"][1] # pragma: no cover
|
|
949
|
+
session = ansys.pyensight.core.session.Session(
|
|
950
|
+
host=self._service_host_port["grpc_private"][0],
|
|
951
|
+
grpc_port=self._service_host_port["grpc_private"][1],
|
|
952
|
+
grpc_use_tcp_sockets=self._grpc_use_tcp_sockets,
|
|
953
|
+
grpc_allow_network_connections=self._grpc_allow_network_connections,
|
|
954
|
+
grpc_disable_tls=self._grpc_disable_tls,
|
|
955
|
+
grpc_uds_pathname=self._grpc_uds_pathname,
|
|
956
|
+
html_hostname=self._service_host_port["http"][0],
|
|
957
|
+
html_port=self._service_host_port["http"][1],
|
|
958
|
+
ws_port=ws_port,
|
|
959
|
+
install_path=None,
|
|
960
|
+
secret_key=self._secret_key,
|
|
961
|
+
timeout=self._timeout,
|
|
962
|
+
sos=use_sos,
|
|
963
|
+
rest_api=self._enable_rest_api,
|
|
964
|
+
webui_port=self._service_host_port["webui"][1] if self._launch_webui else None,
|
|
965
|
+
disable_grpc_options=not self._has_grpc_changes,
|
|
966
|
+
)
|
|
967
|
+
session.launcher = self
|
|
968
|
+
self._sessions.append(session)
|
|
969
|
+
|
|
970
|
+
if self._launch_webui:
|
|
971
|
+
self.launch_webui(container_env_str)
|
|
972
|
+
logging.debug("Return session.\n")
|
|
973
|
+
|
|
974
|
+
return session
|
|
975
|
+
|
|
976
|
+
def close(self, session):
|
|
977
|
+
"""Shut down the launched EnSight session.
|
|
978
|
+
|
|
979
|
+
This method closes all associated sessions and then stops the
|
|
980
|
+
launched EnSight instance.
|
|
981
|
+
|
|
982
|
+
Parameters
|
|
983
|
+
----------
|
|
984
|
+
session : ``pyensight.Session``
|
|
985
|
+
Session to close.
|
|
986
|
+
|
|
987
|
+
Raises
|
|
988
|
+
------
|
|
989
|
+
RuntimeError
|
|
990
|
+
If the session was not launched by this launcher.
|
|
991
|
+
|
|
992
|
+
"""
|
|
993
|
+
if self._enshell:
|
|
994
|
+
if self._enshell.is_connected(): # pragma: no cover
|
|
995
|
+
logging.debug("Killing WSS\n")
|
|
996
|
+
command = 'pkill -f "websocketserver.py"'
|
|
997
|
+
kill_env_vars = None
|
|
998
|
+
container_env_str = ""
|
|
999
|
+
if self._pim_instance is not None:
|
|
1000
|
+
container_env = self._get_container_env()
|
|
1001
|
+
for i in container_env.items():
|
|
1002
|
+
container_env_str += f"{i[0]}={i[1]}\n"
|
|
1003
|
+
if container_env_str: # pragma: no cover
|
|
1004
|
+
kill_env_vars = container_env_str # pragma: no cover
|
|
1005
|
+
ret = self._enshell.start_other(command, extra_env=kill_env_vars)
|
|
1006
|
+
if ret[0] != 0: # pragma: no cover
|
|
1007
|
+
pass
|
|
1008
|
+
return super().close(session)
|
|
1009
|
+
|
|
1010
|
+
def stop(self) -> None:
|
|
1011
|
+
"""Release any additional resources allocated during launching."""
|
|
1012
|
+
if self._enshell:
|
|
1013
|
+
if self._enshell.is_connected(): # pragma: no cover
|
|
1014
|
+
try:
|
|
1015
|
+
logging.debug("Killing WSS\n")
|
|
1016
|
+
command = 'pkill -f "websocketserver.py"'
|
|
1017
|
+
kill_env_vars = None
|
|
1018
|
+
container_env_str = ""
|
|
1019
|
+
if self._pim_instance is not None:
|
|
1020
|
+
container_env = self._get_container_env()
|
|
1021
|
+
for i in container_env.items():
|
|
1022
|
+
container_env_str += f"{i[0]}={i[1]}\n"
|
|
1023
|
+
if container_env_str: # pragma: no cover
|
|
1024
|
+
kill_env_vars = container_env_str # pragma: no cover
|
|
1025
|
+
ret = self._enshell.start_other(command, extra_env=kill_env_vars)
|
|
1026
|
+
if ret[0] != 0: # pragma: no cover
|
|
1027
|
+
pass
|
|
1028
|
+
logging.debug("Stopping EnShell.\n")
|
|
1029
|
+
self._enshell.stop_server()
|
|
1030
|
+
except Exception: # pragma: no cover
|
|
1031
|
+
pass # pragma: no cover
|
|
1032
|
+
self._enshell = None
|
|
1033
|
+
#
|
|
1034
|
+
if self._container:
|
|
1035
|
+
try:
|
|
1036
|
+
logging.debug("Stopping the Docker Container.\n")
|
|
1037
|
+
self._container.stop()
|
|
1038
|
+
except Exception:
|
|
1039
|
+
pass
|
|
1040
|
+
try:
|
|
1041
|
+
logging.debug("Removing the Docker Container.\n")
|
|
1042
|
+
self._container.remove(force=True)
|
|
1043
|
+
except Exception:
|
|
1044
|
+
pass
|
|
1045
|
+
self._container = None
|
|
1046
|
+
|
|
1047
|
+
if self._pim_instance is not None:
|
|
1048
|
+
logging.debug("Deleting the PIM instance.\n")
|
|
1049
|
+
self._pim_instance.delete()
|
|
1050
|
+
self._pim_instance = None
|
|
1051
|
+
super().stop()
|
|
1052
|
+
|
|
1053
|
+
def file_service(self) -> Optional[Any]:
|
|
1054
|
+
"""Get the PIM file service object if available."""
|
|
1055
|
+
return self._pim_file_service
|
|
1056
|
+
|
|
1057
|
+
def _is_system_egl_capable(self) -> bool:
|
|
1058
|
+
"""Check if the system is EGL capable.
|
|
1059
|
+
|
|
1060
|
+
Returns
|
|
1061
|
+
-------
|
|
1062
|
+
bool
|
|
1063
|
+
``True`` if the system is EGL capable, ``False`` otherwise.
|
|
1064
|
+
"""
|
|
1065
|
+
if self._is_windows():
|
|
1066
|
+
return False # pragma: no cover
|
|
1067
|
+
|
|
1068
|
+
# FIXME: MFK, need to figure out how we'd do this
|
|
1069
|
+
# with a PIM managed system such as Ansys Lab
|
|
1070
|
+
if self._pim_instance is not None:
|
|
1071
|
+
return False
|
|
1072
|
+
|
|
1073
|
+
try: # pragma: no cover
|
|
1074
|
+
subprocess.check_output("nvidia-smi")
|
|
1075
|
+
return True
|
|
1076
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
1077
|
+
return False
|
|
1078
|
+
|
|
1079
|
+
def enshell_log_contents(self) -> Optional[str]:
|
|
1080
|
+
"""Get the contents of the EnShell log if possible.
|
|
1081
|
+
|
|
1082
|
+
Returns
|
|
1083
|
+
-------
|
|
1084
|
+
str
|
|
1085
|
+
Contents of the log or ``None``.
|
|
1086
|
+
"""
|
|
1087
|
+
if self._enshell_log_file is None: # pragma: no cover
|
|
1088
|
+
return None # pragma: no cover
|
|
1089
|
+
|
|
1090
|
+
if self._container is not None:
|
|
1091
|
+
try:
|
|
1092
|
+
# docker containers allow copying from a container to the host
|
|
1093
|
+
# a file, files, or a directory. it arrives as a tar file.
|
|
1094
|
+
# we're grabbing just the log file, but we still need to
|
|
1095
|
+
# extract it from the tar file and then put the contents
|
|
1096
|
+
# in the string we're returning
|
|
1097
|
+
from io import BytesIO
|
|
1098
|
+
import tarfile
|
|
1099
|
+
|
|
1100
|
+
bits, stat = self._container.get_archive(self._enshell_log_file)
|
|
1101
|
+
file_obj = BytesIO()
|
|
1102
|
+
for chunk in bits:
|
|
1103
|
+
file_obj.write(chunk)
|
|
1104
|
+
file_obj.seek(0)
|
|
1105
|
+
tar = tarfile.open(mode="r", fileobj=file_obj)
|
|
1106
|
+
member = tar.getmembers()
|
|
1107
|
+
text = tar.extractfile(member[0])
|
|
1108
|
+
s = text.read().decode("utf-8")
|
|
1109
|
+
text.close()
|
|
1110
|
+
return s
|
|
1111
|
+
except Exception as e: # pragma: no cover
|
|
1112
|
+
logging.debug(f"Error getting EnShell log: {e}\n") # pragma: no cover
|
|
1113
|
+
return None # pragma: no cover
|
|
1114
|
+
|
|
1115
|
+
fs = self.file_service() # pragma: no cover
|
|
1116
|
+
if fs is not None: # pragma: no cover
|
|
1117
|
+
try:
|
|
1118
|
+
fs.download_file("enshell.log", ".")
|
|
1119
|
+
f = open("enshell.log")
|
|
1120
|
+
s = f.read()
|
|
1121
|
+
f.close()
|
|
1122
|
+
return s
|
|
1123
|
+
except Exception:
|
|
1124
|
+
return None
|