diff --git a/pyproject.toml b/pyproject.toml index 081181879..99af4418f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ tests = [ "matplotlib>=3.5.3", ] doc = [ - "ansys-sphinx-theme==1.0.8", + "ansys-sphinx-theme==1.0.11", "matplotlib==3.9.2", "numpydoc==1.7.0", "pypandoc==1.13", diff --git a/src/ansys/optislang/core/osl_server.py b/src/ansys/optislang/core/osl_server.py index 1a3cfa6b3..bba411ace 100644 --- a/src/ansys/optislang/core/osl_server.py +++ b/src/ansys/optislang/core/osl_server.py @@ -26,6 +26,8 @@ from abc import ABC, abstractmethod from typing import NamedTuple, Optional +from ansys.optislang.core import utils + class OslVersion(NamedTuple): """optiSLang version. @@ -55,6 +57,11 @@ def __init__(self): # pragma: no cover """``OslServer`` class is an abstract base class and cannot be instantiated.""" pass + @property + def is_remote(self) -> bool: + """Determines whether the optiSLang server does not run on localhost.""" + return self.host is not None and self.port is not None and not utils.is_localhost(self.host) + @property @abstractmethod def host(self) -> Optional[str]: # pragma no cover diff --git a/src/ansys/optislang/core/tcp/nodes.py b/src/ansys/optislang/core/tcp/nodes.py index 122a75e44..cbf6aceb3 100644 --- a/src/ansys/optislang/core/tcp/nodes.py +++ b/src/ansys/optislang/core/tcp/nodes.py @@ -2140,6 +2140,9 @@ def get_inner_output_slots( def get_omdb_files(self) -> Tuple[File]: """Get paths to omdb files. + This method is supported only when the client runs on the same file + system as the server, i.e., the server is not remote. + Returns ------- Tuple[File] @@ -2153,7 +2156,14 @@ def get_omdb_files(self) -> Tuple[File]: Raised when a command or query fails. TimeoutError Raised when the timeout float value expires. + RuntimeError + Raised when the server is remote. """ + if self._osl_server.is_remote: + raise RuntimeError( + "Paths to omdb files cannot be provided when connected to the remote server." + ) + statuses_info = self._get_status_info() wdirs = [Path(status_info["working dir"]) for status_info in statuses_info] omdb_files = [] diff --git a/src/ansys/optislang/core/tcp/osl_server.py b/src/ansys/optislang/core/tcp/osl_server.py index 20ccb41a3..de0bc1fca 100644 --- a/src/ansys/optislang/core/tcp/osl_server.py +++ b/src/ansys/optislang/core/tcp/osl_server.py @@ -42,6 +42,7 @@ from deprecated.sphinx import deprecated +from ansys.optislang.core import utils from ansys.optislang.core.encoding import force_bytes, force_text from ansys.optislang.core.errors import ( ConnectionEstablishedError, @@ -799,7 +800,7 @@ def timeout(self, timeout) -> None: @property def host_addresses(self) -> List[str]: """Local IP addresses associated with self.__listener_socket.""" - addresses = [i[4][0] for i in socket.getaddrinfo(socket.gethostname(), None)] + addresses = utils.get_localhost_addresses() # Explicitly add localhost to workaround potential networking issues addresses.append("127.0.0.1") return addresses diff --git a/src/ansys/optislang/core/utils.py b/src/ansys/optislang/core/utils.py index bb76c661e..e5da2c580 100644 --- a/src/ansys/optislang/core/utils.py +++ b/src/ansys/optislang/core/utils.py @@ -25,9 +25,11 @@ import collections from enum import Enum +import ipaddress import os from pathlib import Path import re +import socket import sys from typing import ( DefaultDict, @@ -454,3 +456,43 @@ def iter_awp_roots() -> Iterator[Tuple[int, Path]]: def is_iron_python(): """Whether current platform is IronPython.""" return sys.platform == "cli" + + +def get_localhost_addresses() -> List[str]: + """Get addresses of the localhost machine. + + Returns + ------- + List[str] + List of addresses of the localhost machine. + """ + return [i[4][0] for i in socket.getaddrinfo(socket.gethostname(), None)] + + +def is_localhost(host: str) -> bool: + """Determine whether the host is localhost. + + Parameters + ---------- + host : str + IPv4/v6 address or domain name of the host. + + Returns + ------- + bool + ``True`` if the provided ``host`` is localhost; ``False`` otherwise. + """ + try: + addrinfo = socket.getaddrinfo(host=host, port=None) + except socket.gaierror: + return False + + localhost_addresses = get_localhost_addresses() + for _, _, _, _, sockaddr in addrinfo: + try: + ip = sockaddr[0] + if not ipaddress.ip_address(ip).is_loopback and ip not in localhost_addresses: + return False + except ValueError: + return False + return True diff --git a/tests/tcp/test_tcp_osl_server.py b/tests/tcp/test_tcp_osl_server.py index 5c75290a7..851b4e82d 100644 --- a/tests/tcp/test_tcp_osl_server.py +++ b/tests/tcp/test_tcp_osl_server.py @@ -335,6 +335,8 @@ def test_tcp_osl_properties(osl_server_process: OslServerProcess): tcp_osl_server.timeout = 20 assert tcp_osl_server.timeout == 20 + assert not tcp_osl_server.is_remote + with pytest.raises(ValueError): tcp_osl_server.timeout = -5 with pytest.raises(ValueError): diff --git a/tests/test_utils.py b/tests/test_utils.py index 18129dc15..936016eeb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -21,12 +21,28 @@ # SOFTWARE. from enum import Enum +from socket import getaddrinfo, gethostname import pytest from ansys.optislang.core import utils +@pytest.fixture +def localhost_addresses(): + """Get addresses of the local machine excluding loopback addresses.""" + addresses = [] + for _, _, _, _, sockaddr in getaddrinfo(gethostname(), None): + addresses.append(sockaddr[0]) + return addresses + + +@pytest.fixture +def loopback_addresses(): + """Get loopback addresses.""" + return ["127.0.0.0", "127.0.0.1", "127.255.255.255", "::1", "localhost"] + + def test_enum_from_str(): class MyEnum(Enum): ONE = 0 @@ -42,3 +58,27 @@ class MyEnum(Enum): assert utils.enum_from_str("one", MyEnum) == MyEnum.ONE assert utils.enum_from_str("ONE", MyEnum) == MyEnum.ONE assert utils.enum_from_str("ONX", MyEnum, ["X", "E"]) == MyEnum.ONE + + +def test_get_localhost_addresses(localhost_addresses): + for address in utils.get_localhost_addresses(): + assert address in localhost_addresses + + +def test_is_localhost(loopback_addresses, localhost_addresses): + random_addresses = [ + "192.168.101.1", + "10.0.10.1", + "130.10.20.1", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "fe80::1ff:fe23:4567:890a%3", + ] + + for loopback_address in loopback_addresses: + assert utils.is_localhost(loopback_address) == True + + for localhost_address in localhost_addresses: + assert utils.is_localhost(localhost_address) == True + + for random_address in random_addresses: + assert utils.is_localhost(random_address) == (random_address in localhost_address)