Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ee3a7e2
remote-connection server API changed.
franknli Aug 7, 2025
83c2a1d
chore: adding changelog file 237.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Aug 7, 2025
b5e11dd
internal_wbexit became available at 25.2
franknli Aug 8, 2025
5b0f1b4
Merge remote-tracking branch 'refs/remotes/origin/fli/handle_api_chan…
franknli Aug 8, 2025
3f2a3e0
auto exiting server
franknli Aug 8, 2025
ede2107
fix code style
franknli Aug 8, 2025
2ecfe5a
fix code style
franknli Aug 8, 2025
401ea66
feat: ask user to allow remote host
jorgepiloto Aug 12, 2025
2c26e98
merge main
franknli Sep 12, 2025
4713800
merge main
franknli Sep 14, 2025
b74da83
correct previous commits
franknli Sep 14, 2025
3204efb
security options
franknli Sep 16, 2025
33353be
chore: adding changelog file 254.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Sep 16, 2025
94f7c37
fix code style
franknli Sep 16, 2025
82a4da9
Merge remote-tracking branch 'refs/remotes/origin/fli/handle_api_chan…
franknli Sep 16, 2025
94b2522
code style
franknli Sep 16, 2025
d128dc3
style
franknli Sep 16, 2025
60168f7
handle older install
franknli Sep 17, 2025
3d10b89
code style
franknli Sep 17, 2025
00c5b80
fix long line
franknli Sep 17, 2025
f6f7cc0
fix unit tests
franknli Sep 17, 2025
dc4db24
update comment
franknli Sep 17, 2025
c5501c8
fix unit test
franknli Sep 17, 2025
02ce3f5
code style
franknli Sep 17, 2025
22a226b
typo
franknli Oct 1, 2025
bcbc7d6
merge main
franknli Oct 1, 2025
b26883f
fix command line quotes
franknli Oct 3, 2025
31549b2
chore: adding changelog file 260.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Oct 3, 2025
93f10f2
fix code style
franknli Oct 3, 2025
c456f5a
merge pull
franknli Oct 3, 2025
d4e5b2e
merge main
franknli Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/237.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
remote-connection server API changed.
1 change: 1 addition & 0 deletions doc/changelog.d/254.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fli/handle api change for remote connection
1 change: 1 addition & 0 deletions doc/changelog.d/260.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fli/handle api change for remote connection
41 changes: 34 additions & 7 deletions src/ansys/workbench/core/public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

"""Module for public API on PyWorkbench."""

import atexit
import logging
import tempfile

Expand All @@ -40,16 +41,18 @@ class ClientWrapper(WorkbenchClient):
Path to a writable directory on the client computer.
host : str, default: None
Server computer's name or IP address.
security : str, default: 'default'
Transport mode used for connection security.
"""

def __init__(self, port, client_workdir=None, host=None):
def __init__(self, port, client_workdir=None, host=None, security="default"):
"""Create a PyWorkbench client that connects to a Workbench server."""
if host is None:
host = "localhost"
if client_workdir is None:
client_workdir = tempfile.gettempdir()
super().__init__(client_workdir, host, port)
super()._connect()
super()._connect(security)

def exit(self):
"""Disconnect from the server."""
Expand All @@ -73,6 +76,8 @@ class LaunchWorkbench(ClientWrapper):
server_workdir : str, None
Path to a writable directory on the server computer. The default is ``None``,
in which case the user preference for the Workbench temporary file folder is used.
use_insecure_connection : bool, default: False
whether to use insecure connection between the server and clients
host : str, None
Server computer's name or IP address. The default is ``None`` for launching on the
local computer.
Expand Down Expand Up @@ -102,6 +107,7 @@ def __init__(
version=None,
client_workdir=None,
server_workdir=None,
use_insecure_connection=False,
host=None,
username=None,
password=None,
Expand All @@ -110,10 +116,19 @@ def __init__(
version = "252"

self._launcher = Launcher()
port = self._launcher.launch(version, show_gui, server_workdir, host, username, password)
port, security = self._launcher.launch(
version, show_gui, server_workdir, use_insecure_connection, host, username, password
)
if port is None or port <= 0:
raise Exception("Failed to launch Ansys Workbench service.")
super().__init__(port, client_workdir, host)
if use_insecure_connection:
print(
"Using insecure connection is not recommended. "
"Please see the documentation for your installed "
"product for additional information."
)
super().__init__(port, client_workdir, host, security)
atexit.register(self.exit)
self._exited = False

def exit(self):
Expand All @@ -134,6 +149,7 @@ def launch_workbench(
version=None,
client_workdir=None,
server_workdir=None,
use_insecure_connection=False,
host=None,
username=None,
password=None,
Expand All @@ -155,6 +171,8 @@ def launch_workbench(
server_workdir : str, None
Path to a writable directory on the server computer. The default is ``None``,
in which case the user preference for the Workbench temporary file folder is used.
use_insecure_connection : bool, default: False
whether to use insecure connection between the server and clients
host : str, None
Server computer's name or IP address. The default is ``None`` for launching on the
local computer.
Expand All @@ -179,11 +197,18 @@ def launch_workbench(

"""
return LaunchWorkbench(
show_gui, version, client_workdir, server_workdir, host, username, password
show_gui,
version,
client_workdir,
server_workdir,
use_insecure_connection,
host,
username,
password,
)


def connect_workbench(port, client_workdir=None, host=None):
def connect_workbench(port, client_workdir=None, host=None, security="mtls"):
"""Create a PyWorkbench client that connects to an already running Workbench server.

Parameters
Expand All @@ -195,6 +220,8 @@ def connect_workbench(port, client_workdir=None, host=None):
in which case the system temp directory is used.
host : str, default: None
Server computer's name or IP address. The default is ``None`` for the local computer.
security : str among 'mtls', 'wnua', 'insecure', default: 'mtls'
Transport mode used for connection security. The default is `mtls`.

Returns
-------
Expand All @@ -209,7 +236,7 @@ def connect_workbench(port, client_workdir=None, host=None):
>>> from ansys.workbench.core import connect_workbench
>>> wb = connect_workbench(port = 32588)
"""
return ClientWrapper(port, client_workdir, host)
return ClientWrapper(port, client_workdir, host, security)


__all__ = ["launch_workbench", "connect_workbench"]
51 changes: 49 additions & 2 deletions src/ansys/workbench/core/workbench_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

"""Workbench client module for PyWorkbench."""

from enum import Enum
import glob
import json
import logging
Expand All @@ -39,6 +40,12 @@
from ansys.workbench.core.example_data import ExampleData


class SecurityType(str, Enum):
"""Enum containing the security types for server connection."""

(INSECURE, MTLS, WNUA) = ("insecure", "mtls", "wnua")


class WorkbenchClient:
"""Functions of a PyWorkbench client.

Expand Down Expand Up @@ -79,13 +86,53 @@ def __exit__(self, exc_type, exc_value, traceback):
"""Disconnect from the server when exiting a context."""
self._disconnect()

def _connect(self):
def _connect(self, server_security):
"""Connect to the server."""
hnp = self._server_host + ":" + str(self._server_port)
self.channel = grpc.insecure_channel(hnp)
match server_security:
case SecurityType.INSECURE:
self.channel = grpc.insecure_channel(hnp)
case SecurityType.MTLS:
ssl_creds = self._get_ssl_creds()
self.channel = grpc.secure_channel(hnp, ssl_creds)
case SecurityType.WNUA:
self.channel = grpc.insecure_channel(
hnp, options=(("grpc.default_authority", "localhost"),)
)
case _:
raise RuntimeError(f"Unknown security type: {server_security}")
self.stub = WorkbenchServiceStub(self.channel)
logging.info(f"connected to the WB server at {hnp}")

def _get_ssl_creds(self):
# TLS certificates location
if os.environ.get("ANSYS_GRPC_CERTIFICATES"):
certs_folder = os.environ.get("ANSYS_GRPC_CERTIFICATES")
else:
certs_folder = "certs"

# verify the existence of TLS certificates
client_cert = f"{certs_folder}/client.crt"
client_key = f"{certs_folder}/client.key"
ca_cert = f"{certs_folder}/ca.crt"

missing = [f for f in (client_cert, client_key, ca_cert) if not os.path.exists(f)]
if missing:
raise RuntimeError(f"Missing required TLS file(s) for mutual TLS: {', '.join(missing)}")

# create TLS credential
with open(client_cert, "rb") as f:
certificate_chain = f.read()
with open(client_key, "rb") as f:
private_key = f.read()
with open(ca_cert, "rb") as f:
root_certificates = f.read()
return grpc.ssl_channel_credentials(
root_certificates=root_certificates,
private_key=private_key,
certificate_chain=certificate_chain,
)

def _disconnect(self):
"""Disconnect from the server."""
if self.channel:
Expand Down
52 changes: 44 additions & 8 deletions src/ansys/workbench/core/workbench_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def launch(
version,
show_gui=True,
server_workdir=None,
use_insecure_connection=False,
host=None,
username=None,
password=None,
Expand All @@ -91,6 +92,8 @@ def launch(
server_workdir : str, default: None
Path to a writable directory on the server. The default is ``None``,
in which case the user preference for the Workbench temporary file folder is used.
use_insecure_connection : bool, default: False
whether to use insecure connection between the server and clients
host : str, default: None
Name or IP address of the server. The default is ``None``, which launches Workbench
on the local computer.
Expand Down Expand Up @@ -127,10 +130,16 @@ def launch(

if host and (not username or not password):
raise Exception(
"Username and passwork must be specified "
"Username and password must be specified "
"to launch PyWorkbench on a remote machine."
)

security = "mtls"
if use_insecure_connection:
security = "insecure"
elif not host and self._wmi:
security = "wnua"

if self._wmi:
try:
if not host:
Expand Down Expand Up @@ -167,17 +176,44 @@ def launch(
args.append("--start-and-wait")
args.append("-nowindow")
args.append("-E")

# create command string
prefix = uuid.uuid4().hex
cmd = "StartServer(EnvironmentPrefix='"
cmd += prefix + "'"
cmd1 = "StartServer(EnvironmentPrefix='"
cmd1 += prefix + "'"
if server_workdir is not None:
# use forward slash only to avoid escaping as command line argument
server_workdir = server_workdir.replace("\\", "/")
cmd += ",WorkingDirectory='" + server_workdir + "'"
cmd += ")"
cmd1 += ",WorkingDirectory='" + server_workdir + "'"
cmd2 = str(cmd1)
cmd1 += ")"
cmd2 += ",Security='" + security + "'"
if host is not None:
cmd2 += ",AllowRemoteConnection=True"
cmd2 += ")"

if self._wmi:
quote_or_not = '"' # quotes needed when constructing command line
else:
quote_or_not = ""
cmd = (
quote_or_not
+ cmd2
+ """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin("""
+ """'Ansys.RemoteWB.Addin').Version.Major > 1 else """
+ cmd1
+ quote_or_not
)
args.append(cmd)

command_line = " ".join(args)

# security precaution statement
if host is not None:
print("""The server started will allow remote access connections to be
established, possibly permitting control of the machine and any data which resides on it.
It is highly recommended to only utilize these features on a trusted, secure network.""")

successful = False
process = None
if self._wmi:
Expand Down Expand Up @@ -206,7 +242,7 @@ def launch(
logging.info(f"Workbench is launched successfully with process ID {self._process_id}.")
else:
logging.error("Workbench failed to launch on the host.")
return 0
return 0, security

# retrieve server port once WB is fully up running
port = None
Expand All @@ -232,9 +268,9 @@ def launch(
time.sleep(10)
if not port or int(port) <= 0:
logging.error("Failed to retrieve the port used by Workbench service.")
return 0
return 0, security
logging.info("Workbench service uses port: " + port)
return int(port)
return int(port), security

def __getenv(self, key):
value = None
Expand Down
Loading