Repository: ansys/pyfluent

Issue:
Higher-level exceptions
Investigate PyFluent behaviour to find scenarios where we could catch and rethrow to provide improved error information to users. See this post: https://softwareengineering.stackexchange.com/a/231059.

Note some usage examples where the first example could reraise an exception closer to the user's context:

```
>>> pf.connect_to_fluent(ip="1.2.3.4",  port=5555)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\launcher\launcher.py", line 349, in connect_to_fluent
    fluent_connection = FluentConnection(
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\fluent_connection.py", line 417, in __init__
    self.health_check.check_health()
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\services\health_check.py", line 62, in check_health
    response = self._stub.Check(request, metadata=self._metadata)
  File "C:\ANSYSDev\pyfluent\.env-local-2\lib\site-packages\grpc\_interceptor.py", line 277, in __call__
    response, ignored_call = self._with_call(
  File "C:\ANSYSDev\pyfluent\.env-local-2\lib\site-packages\grpc\_interceptor.py", line 329, in _with_call
    call = self._interceptor.intercept_unary_unary(
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\services\interceptors.py", line 124, in intercept_unary_unary
    return self._intercept_call(continuation, client_call_details, request)
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\services\interceptors.py", line 114, in _intercept_call
    raise new_ex from None
RuntimeError: failed to connect to all addresses; last error: UNKNOWN: ipv4:1.2.3.4:5555: tcp handshaker shutdown
>>>
>>> pf.connect_to_fluent(ip="1.2.3.4")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\launcher\launcher.py", line 348, in connect_to_fluent
    ip, port, password = _get_server_info(server_info_file_name, ip, port, password)
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\launcher\server_info.py", line 56, in _get_server_info
    raise IpPortNotProvided()
ansys.fluent.core.launcher.error_handler.IpPortNotProvided: Provide either 'ip' and 'port' or 'server_info_file_name'.
>>>
>>> pf.connect_to_fluent(port=3333)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\launcher\launcher.py", line 348, in connect_to_fluent
    ip, port, password = _get_server_info(server_info_file_name, ip, port, password)
  File "C:\ANSYSDev\pyfluent\src\ansys\fluent\core\launcher\server_info.py", line 56, in _get_server_info
    raise IpPortNotProvided()
ansys.fluent.core.launcher.error_handler.IpPortNotProvided: Provide either 'ip' and 'port' or 'server_info_file_name'.
>>>
```

Discussion:

Solution patch:
diff --git a/pyproject.toml b/pyproject.toml
index 07ab9956c0..161e80c108 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,7 +21,7 @@ classifiers = [
 ]
 dependencies = [
     "ansys-api-fluent>=0.3.33",
-    "ansys-platform-instancemanagement~=1.0",
+    "ansys-platform-instancemanagement~=1.1",
     "ansys-tools-filetransfer>=0.1,<0.3",
     "ansys-units>=0.3.3,<0.5",
     "docker>=7.1.0",
diff --git a/src/ansys/fluent/core/launcher/error_handler.py b/src/ansys/fluent/core/launcher/error_handler.py
index c598f91c11..8034e96398 100644
--- a/src/ansys/fluent/core/launcher/error_handler.py
+++ b/src/ansys/fluent/core/launcher/error_handler.py
@@ -29,6 +29,14 @@ class IpPortNotProvided(ValueError):
         super().__init__("Provide either 'ip' and 'port' or 'server_info_file_name'.")
 
 
+class InvalidIpPort(ValueError):
+    """Raised when IP address and port are invalid."""
+
+    def __init__(self):
+        """Initialize InvalidIpPort."""
+        super().__init__("Provide a valid 'ip' and 'port'.")
+
+
 class UnexpectedKeywordArgument(TypeError):
     """Raised when a valid keyword argument is not specified."""
 
diff --git a/src/ansys/fluent/core/launcher/server_info.py b/src/ansys/fluent/core/launcher/server_info.py
index 44c3f5c308..446c499604 100644
--- a/src/ansys/fluent/core/launcher/server_info.py
+++ b/src/ansys/fluent/core/launcher/server_info.py
@@ -6,7 +6,7 @@ import tempfile
 
 from ansys.fluent.core.fluent_connection import PortNotProvided
 from ansys.fluent.core.launcher import launcher_utils
-from ansys.fluent.core.launcher.error_handler import IpPortNotProvided
+from ansys.fluent.core.launcher.error_handler import InvalidIpPort, IpPortNotProvided
 from ansys.fluent.core.session import _parse_server_info_file
 
 
@@ -45,6 +45,27 @@ def _get_server_info_file_names(use_tmpdir=True) -> tuple[str, str]:
         return file_name, file_name
 
 
+def _check_ip_port(ip: str, port: int):
+    """Check if a port is open on a given IP address."""
+
+    if not (ip and port):
+        raise IpPortNotProvided()
+
+    if not port:
+        raise PortNotProvided()
+
+    import socket
+
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.settimeout(2)
+    try:
+        result = sock.connect_ex((ip, port))
+        if result != 0:
+            raise InvalidIpPort()
+    finally:
+        sock.close()
+
+
 def _get_server_info(
     server_info_file_name: str,
     ip: str | None = None,
@@ -65,7 +86,6 @@ def _get_server_info(
         ip = ip or os.getenv("PYFLUENT_FLUENT_IP", "127.0.0.1")
         port = port or os.getenv("PYFLUENT_FLUENT_PORT")
 
-    if not port:
-        raise PortNotProvided()
+    _check_ip_port(ip=ip, port=port)
 
     return ip, port, password


Test patch:
diff --git a/tests/test_launcher.py b/tests/test_launcher.py
index 759b5fdc69..a3f2d65218 100644
--- a/tests/test_launcher.py
+++ b/tests/test_launcher.py
@@ -13,6 +13,7 @@ from ansys.fluent.core.exceptions import DisallowedValuesError, InvalidArgument
 from ansys.fluent.core.launcher import launcher_utils
 from ansys.fluent.core.launcher.error_handler import (
     GPUSolverSupportError,
+    InvalidIpPort,
     LaunchFluentError,
     _raise_non_gui_exception_in_windows,
 )
@@ -534,3 +535,8 @@ def test_container_ports():
     with pyfluent.launch_fluent(container_dict=container_dict) as session:
         session._container.reload()
         assert len(session._container.ports) == 2
+
+
+def test_correct_ip_port():
+    with pytest.raises(InvalidIpPort):
+        pyfluent.connect_to_fluent(ip="1.2.3.4", port=5555)
diff --git a/tests/test_session.py b/tests/test_session.py
index 35e6bf3419..269351afcb 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -435,12 +435,6 @@ def test_recover_grpc_error_from_launch_error(monkeypatch: pytest.MonkeyPatch):
     assert ex.value.__context__.__context__.code() == grpc.StatusCode.UNAVAILABLE
 
 
-def test_recover_grpc_error_from_connection_error():
-    with pytest.raises(RuntimeError) as ex:
-        pyfluent.connect_to_fluent(ip="127.0.0.1", port=50000, password="abcdefg")
-    assert ex.value.__context__.code() == grpc.StatusCode.UNAVAILABLE
-
-
 def test_solver_methods(new_solver_session):
     solver = new_solver_session