Skip to content

Commit

Permalink
Mostieri/webui (#422)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariostieriansys authored Sep 13, 2024
1 parent 901f38a commit 372fef8
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 6 deletions.
60 changes: 56 additions & 4 deletions src/ansys/pyensight/core/dockerlauncher.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def __init__(
self._image_name: Optional[str] = None
self._docker_client: Optional[Any] = None
self._container = None
self._enshell: Optional[Any] = None
self._enshell: Optional["enshell_grpc.EnShellGRPC"] = None
self._pim_instance: Optional[Any] = pim_instance

# EnSight session secret key
Expand All @@ -143,7 +143,10 @@ def __init__(
)

if self._enshell_grpc_channel and self._pim_instance:
if not set(("grpc_private", "http", "ws")).issubset(self._pim_instance.services):
service_set = ["grpc_private", "http", "ws"]
# if self._launch_webui:
# service_set.append("webui")
if not set(service_set).issubset(self._pim_instance.services):
raise RuntimeError(
"If channel is specified, the PIM instance must have a list of length 3 "
+ "containing the appropriate service URIs. It does not."
Expand All @@ -159,6 +162,10 @@ def __init__(
self._service_host_port["ws"] = self._get_host_port(
self._pim_instance.services["ws"].uri
)
if self._launch_webui:
self._service_host_port["webui"] = self._get_host_port(
self._pim_instance.services["webui"].uri
)
# for parity, add 'grpc' as a placeholder even though using PIM sets up the gRPC channel.
# this isn't used in this situation.
self._service_host_port["grpc"] = ("127.0.0.1", -1)
Expand All @@ -173,7 +180,10 @@ def __init__(

# EnShell gRPC port, EnSight gRPC port, HTTP port, WSS port
# skip 1999 as that internal to the container is used to the container for the VNC connection
ports = self._find_unused_ports(4, avoid=[1999])
num_ports = 4
if self._launch_webui:
num_ports = 5
ports = self._find_unused_ports(num_ports, avoid=[1999])
if ports is None: # pragma: no cover
raise RuntimeError(
"Unable to allocate local ports for EnSight session"
Expand All @@ -183,6 +193,8 @@ def __init__(
self._service_host_port["grpc_private"] = ("127.0.0.1", ports[1])
self._service_host_port["http"] = ("127.0.0.1", ports[2])
self._service_host_port["ws"] = ("127.0.0.1", ports[3])
if self._launch_webui:
self._service_host_port["webui"] = ("127.0.0.1", ports[4])

# get the optional user-specified image name
# Note: the default name needs to change over time... TODO
Expand Down Expand Up @@ -251,6 +263,10 @@ def _get_container_env(self) -> Dict:
if "ENSIGHT_ANSYS_APIP_CONFIG" in os.environ:
container_env["ENSIGHT_ANSYS_APIP_CONFIG"] = os.environ["ENSIGHT_ANSYS_APIP_CONFIG"]

if self._launch_webui:
container_env["SIMBA_WEBSERVER_TOKEN"] = self._secret_key
container_env["FLUENT_WEBSERVER_TOKEN"] = self._secret_key

return container_env

def start(self) -> "Session":
Expand Down Expand Up @@ -299,7 +315,13 @@ def start(self) -> "Session":
+ "/tcp": str(self._service_host_port["http"][1]),
str(self._service_host_port["ws"][1]) + "/tcp": str(self._service_host_port["ws"][1]),
}

if self._launch_webui:
ports_to_map.update(
{
str(self._service_host_port["webui"][1])
+ "/tcp": str(self._service_host_port["webui"][1])
}
)
# The data directory to map into the container
data_volume = None
if self._data_directory:
Expand Down Expand Up @@ -389,6 +411,33 @@ def start(self) -> "Session":
logging.debug("Container started.\n")
return self.connect()

def launch_webui(self, container_env_str):
# Run websocketserver
cmd = f"cpython /ansys_inc/v{self._ansys_version}/CEI/"
cmd += f"nexus{self._ansys_version}/nexus_launcher/webui_launcher.py"
# websocket port - this needs to come first since we now have
# --add_header as a optional arg that can take an arbitrary
# number of optional headers.
webui_port = self._service_host_port["webui"][1]
grpc_port = self._service_host_port["grpc_private"][1]
http_port = self._service_host_port["http"][1]
ws_port = self._service_host_port["ws"][1]
cmd += f" --server-listen-port {webui_port}"
cmd += (
f" --server-web-roots /ansys_inc/v{self._ansys_version}/CEI/nexus{self._ansys_version}/"
)
cmd += f"ansys{self._ansys_version}/ensight/WebUI/web/ui/"
cmd += f" --ensight-grpc-port {grpc_port}"
cmd += f" --ensight-html-port {http_port}"
cmd += f" --ensight-ws-port {ws_port}"
cmd += f" --ensight-session-directory {self._session_directory}"
cmd += f" --ensight-secret-key {self._secret_key}"
logging.debug(f"Starting WebUI: {cmd}\n")
ret = self._enshell.start_other(cmd, extra_env=container_env_str)
if ret[0] != 0: # pragma: no cover
self.stop() # pragma: no cover
raise RuntimeError(f"Error starting WebUI: {cmd}\n") # pragma: no cover

def connect(self):
"""Create and bind a :class:`Session<ansys.pyensight.core.Session>` instance
to the created EnSight gRPC connection started by EnShell.
Expand Down Expand Up @@ -581,10 +630,13 @@ def connect(self):
timeout=self._timeout,
sos=use_sos,
rest_api=self._enable_rest_api,
webui_port=self._service_host_port["webui"][1] if self._launch_webui else None,
)
session.launcher = self
self._sessions.append(session)

if self._launch_webui:
self.launch_webui(container_env_str)
logging.debug("Return session.\n")

return session
Expand Down
4 changes: 4 additions & 0 deletions src/ansys/pyensight/core/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class Launcher:
Additional command line options to be used to launch EnSight.
Please note, when using DockerLauncher, arguments that contain spaces
are not supported.
launch_web_ui : bool, optional
Whether to launch the webUI from EnSight
"""

def __init__(
Expand All @@ -72,6 +74,7 @@ def __init__(
use_sos: Optional[int] = None,
enable_rest_api: bool = False,
additional_command_line_options: Optional[List] = None,
launch_webui: bool = False,
) -> None:
self._timeout = timeout
self._use_egl_param_val: bool = use_egl
Expand All @@ -92,6 +95,7 @@ def __init__(
# a dict of any optional launcher specific query parameters for URLs
self._query_parameters: Dict[str, str] = {}
self._additional_command_line_options = additional_command_line_options
self._launch_webui = launch_webui

@property
def session_directory(self) -> str:
Expand Down
44 changes: 42 additions & 2 deletions src/ansys/pyensight/core/locallauncher.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import logging
import os.path
import platform
import re
import shutil
import subprocess
import tempfile
Expand Down Expand Up @@ -89,6 +90,7 @@ def __init__(
# launched process ids
self._ensight_pid = None
self._websocketserver_pid = None
self._webui_pid = None
# and ports
self._ports = None
# Are we running the instance in batch
Expand All @@ -99,6 +101,36 @@ def application(self):
"""Type of app to launch. Options are ``ensight`` and ``envision``."""
return self._application

def launch_webui(self, cpython, webui_script, popen_common):
cmd = [cpython]
cmd += [webui_script]
version = re.findall(r"nexus(\d+)", webui_script)[0]
path_to_webui = self._install_path
path_to_webui = os.path.join(
path_to_webui, f"nexus{version}", f"ansys{version}", "ensight", "WebUI", "web", "ui"
)
cmd += ["--server-listen-port", str(self._ports[5])]
cmd += ["--server-web-roots", path_to_webui]
cmd += ["--ensight-grpc-port", str(self._ports[0])]
cmd += ["--ensight-html-port", str(self._ports[2])]
cmd += ["--ensight-ws-port", str(self._ports[3])]
cmd += ["--ensight-session-directory", self._session_directory]
cmd += ["--ensight-secret-key", self._secret_key]
if "PYENSIGHT_DEBUG" in os.environ:
try:
if int(os.environ["PYENSIGHT_DEBUG"]) > 0:
del popen_common["stdout"]
del popen_common["stderr"]
except (ValueError, KeyError):
pass
popen_common["env"].update(
{
"SIMBA_WEBSERVER_TOKEN": self._secret_key,
"FLUENT_WEBSERVER_TOKEN": self._secret_key,
}
)
self._webui_pid = subprocess.Popen(cmd, **popen_common).pid

def start(self) -> "pyensight.Session":
"""Start an EnSight session using the local EnSight installation.
Expand Down Expand Up @@ -126,7 +158,10 @@ def start(self) -> "pyensight.Session":

# gRPC port, VNC port, websocketserver ws, websocketserver html
to_avoid = self._find_ports_used_by_other_pyensight_and_ensight()
self._ports = self._find_unused_ports(5, avoid=to_avoid)
num_ports = 5
if self._launch_webui:
num_ports = 6
self._ports = self._find_unused_ports(num_ports, avoid=to_avoid)
if self._ports is None:
raise RuntimeError("Unable to allocate local ports for EnSight session")
is_windows = self._is_windows()
Expand Down Expand Up @@ -211,11 +246,12 @@ def start(self) -> "pyensight.Session":
except Exception:
pass
websocket_script = found_scripts[idx]

webui_script = websocket_script.replace("websocketserver.py", "webui_launcher.py")
# build the commandline
cmd = [os.path.join(self._install_path, "bin", "cpython"), websocket_script]
if is_windows:
cmd[0] += ".bat"
ensight_python = cmd[0]
cmd.extend(["--http_directory", self.session_directory])
# http port
cmd.extend(["--http_port", str(self._ports[2])])
Expand Down Expand Up @@ -256,9 +292,13 @@ def start(self) -> "pyensight.Session":
timeout=self._timeout,
sos=use_sos,
rest_api=self._enable_rest_api,
webui_port=self._ports[5] if self._launch_webui else None,
)
session.launcher = self
self._sessions.append(session)

if self._launch_webui:
self.launch_webui(ensight_python, webui_script, popen_common)
return session

def stop(self) -> None:
Expand Down
30 changes: 30 additions & 0 deletions src/ansys/pyensight/core/renderable.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
This module provides the interface for creating objects in the EnSight session
that can be displayed via HTML over the websocket server interface.
"""
import hashlib
import os
import shutil
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, no_type_check
import uuid
import warnings
import webbrowser

import requests
Expand Down Expand Up @@ -812,3 +814,31 @@ def _periodic_script(self) -> str:
html = html.replace("REVURL_ITEMID", revision_uri)
html = html.replace("ITEMID", self._guid)
return html


class RenderableFluidsWebUI(Renderable):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._session.ensight_version_check("2025 R1")
warnings.warn("The webUI is still under active development and should be considered beta.")
self._rendertype = "webui"
self._generate_url()
self.update()

def _generate_url(self) -> None:
sha256_hash = hashlib.sha256()
sha256_hash.update(self._session._secret_key.encode())
token = sha256_hash.hexdigest()
optional_query = self._get_query_parameters_str()
port = self._session._webui_port
if "instance_name" in self._session._launcher._get_query_parameters():
instance_name = self._session._launcher._get_query_parameters()["instance_name"]
# If using PIM, the port needs to be the 443 HTTPS Port;
port = self._session.html_port
# In the webUI code there's already a workflow to pass down the query parameter
# ans_instance_id, just use it
instance_name = self._session._launcher._get_query_parameters()["instance_name"]
optional_query = f"?ans_instance_id={instance_name}"
url = f"{self._http_protocol}://{self._session.html_hostname}:{port}"
url += f"{optional_query}#{token}"
self._url = url
5 changes: 5 additions & 0 deletions src/ansys/pyensight/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ansys.pyensight.core.renderable import (
RenderableDeepPixel,
RenderableEVSN,
RenderableFluidsWebUI,
RenderableImage,
RenderableMP4,
RenderableSGEO,
Expand Down Expand Up @@ -136,6 +137,7 @@ def __init__(
timeout: float = 120.0,
rest_api: bool = False,
sos: bool = False,
webui_port: Optional[int] = None,
) -> None:
# every session instance needs a unique name that can be used as a cache key
self._session_name = str(uuid.uuid1())
Expand All @@ -161,6 +163,7 @@ def __init__(
self._grpc_port = grpc_port
self._halt_ensight_on_close = True
self._callbacks: Dict[str, Tuple[int, Any]] = dict()
self._webui_port = webui_port
# if the caller passed a session directory we will assume they are
# creating effectively a proxy Session and create a (stub) launcher
if session_directory is not None:
Expand Down Expand Up @@ -927,6 +930,8 @@ def show(
# Undocumented. Available only internally
elif what == "webensight":
render = RenderableVNCAngular(self, **kwargs)
elif what == "webui":
render = RenderableFluidsWebUI(self, **kwargs)

if render is None:
raise RuntimeError("Unable to generate requested visualization")
Expand Down

0 comments on commit 372fef8

Please sign in to comment.