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.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. 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