-
Notifications
You must be signed in to change notification settings - Fork 268
FIX: Connect to existing student version #7918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2603962
8c352ca
7c24b83
3a848b6
2a85972
dcb2185
1f7b6cd
68f7182
394d8be
3c63d24
82be8b1
0ee530f
448dafe
e53bce6
feeeb7b
41658c8
125d13a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Connect to existing student version |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ | |
| from ansys.aedt.core.generic.general_methods import _is_version_format_valid | ||
| from ansys.aedt.core.generic.general_methods import _normalize_version_to_string | ||
| from ansys.aedt.core.generic.general_methods import active_sessions | ||
| from ansys.aedt.core.generic.general_methods import all_active_sessions | ||
| from ansys.aedt.core.generic.general_methods import com_active_sessions | ||
| from ansys.aedt.core.generic.general_methods import grpc_active_sessions | ||
| from ansys.aedt.core.generic.general_methods import inside_desktop_ironpython_console | ||
|
|
@@ -437,10 +438,7 @@ def launch_aedt( | |
| timeout = settings.desktop_launch_timeout | ||
| start = time.time() | ||
| while timeout > 0: | ||
| if is_grpc_session_active( | ||
| port, | ||
| host, | ||
| ): | ||
| if is_grpc_session_active(port, host, student_version): | ||
| break | ||
| timeout -= 1 | ||
| time.sleep(1) | ||
|
|
@@ -799,7 +797,7 @@ def __init__( | |
| ) | ||
| self.__close_on_exit_arg = close_on_exit | ||
| self.__machine = machine if machine else None | ||
| self.__port = port | ||
| self.__port = port if port is not None else 0 | ||
| self.__is_grpc_api = True | ||
| self.__student_version = False | ||
| self.__aedt_version_string = "" | ||
|
|
@@ -1030,8 +1028,6 @@ def port(self) -> int: | |
| >>> d.port | ||
|
|
||
| """ | ||
| if not self.__port: | ||
| self._assign_port() | ||
| return self.__port | ||
|
|
||
| @port.setter | ||
|
|
@@ -3085,39 +3081,75 @@ def _check_machine(self) -> None: | |
| self.machine = "127.0.0.1" | ||
|
|
||
| @pyaedt_function_handler() | ||
| def _validate_port(self, port, machine=None): | ||
| def _validate_port( | ||
| self, | ||
| ): | ||
| """Validate the specified gRPC port. | ||
|
|
||
| On top of checking the port, this method also determines if a new AEDT session | ||
| needs to be launched. | ||
| """ | ||
| self.logger.debug(f"Validating specified gRPC port: {port}") | ||
| if port == 0: | ||
| return port | ||
| active_ports = is_grpc_session_active(port, machine) | ||
| if self.new_desktop and active_ports: | ||
| self.logger.warning(f"Port {port} is already in use. Finding a new free port.") | ||
| return _find_free_port() | ||
| elif not settings.remote_rpc_session and not self.new_desktop and not active_ports: | ||
| self.logger.warning(f"No active AEDT gRPC session found on port {port}. Opening a new AEDT session.") | ||
| self.logger.debug(f"Validating specified gRPC port: {self.port}") | ||
|
|
||
| if self.port == 0: # Checking if available session is there or eventually assign new port | ||
| self._assign_port() | ||
| return self.port | ||
| all_sessions = all_active_sessions() | ||
| version = self.aedt_version_id[2:4] + self.aedt_version_id[5] | ||
| version += "_nongraphical" if self.non_graphical else "_graphical" | ||
| version += "_student" if self.student_version else "" | ||
|
|
||
| version_neg = self.aedt_version_id[2:4] + self.aedt_version_id[5] | ||
| version_neg += "_nongraphical" if not self.non_graphical else "_graphical" | ||
| version_neg += "_student" if self.student_version else "" | ||
|
|
||
| if self.new_desktop: | ||
| for el in all_sessions.values(): | ||
| if self.port in el.values(): | ||
| self.logger.warning(f"Port {self.port} is already in use. Finding a new free port.") | ||
| self.port = _find_free_port() | ||
| break | ||
| return self.port | ||
| elif settings.remote_rpc_session: # remote session -> no port check | ||
| self.logger.warning(f"Remote session found on port {self.port}. Using it.") | ||
| self.new_desktop = False | ||
| return self.port | ||
| elif version in all_sessions and self.port in all_sessions[version].values(): | ||
| self.logger.info(f"Port {self.port} session has been found.") | ||
| return self.port | ||
| elif version_neg in all_sessions and self.port in all_sessions[version_neg].values(): | ||
| mode = "graphical" if self.non_graphical else "non_graphical" | ||
| self.logger.warning(f"Port {self.port} is already in use in {mode} mode. Using it.") | ||
| self.non_graphical = not self.non_graphical | ||
| return self.port | ||
| else: | ||
| for el in all_sessions.values(): | ||
| if self.port in el.values(): | ||
| self.logger.warning( | ||
| f"Port {self.port} is already in use by another AEDT version. Finding a new free port." | ||
| ) | ||
| self.new_desktop = True | ||
| self.port = _find_free_port() | ||
| return self.port | ||
| # No active sessions found, open a new AEDT session | ||
| self.new_desktop = True | ||
| return port | ||
| return self.port | ||
|
|
||
| @pyaedt_function_handler() | ||
| def _assign_port(self): | ||
| self.__port = 0 | ||
| self.port = 0 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same remark as above, I would keep the |
||
| if settings.remote_rpc_session: | ||
| self.logger.warning( | ||
| "Remote AEDT connection without specified port. Trying to use the port from the RPyC connection." | ||
| ) | ||
| try: | ||
| self.__port = settings.remote_rpc_session.port | ||
| except Exception: | ||
| self.port = settings.remote_rpc_session.port | ||
| except Exception: # pragma: no cover | ||
| self.logger.debug("Failed to retrieve port from RPyC connection") | ||
| raise Exception("Failed to retrieve port from RPyC connection") | ||
|
|
||
| if settings.use_multi_desktop or self.new_desktop: | ||
| self.__port = _find_free_port() | ||
| elif settings.use_multi_desktop or self.new_desktop: | ||
| self.port = _find_free_port() | ||
| self.logger.info(f"New AEDT session is starting on gRPC port {self.port}.") | ||
|
|
||
| else: | ||
|
|
@@ -3127,15 +3159,15 @@ def _assign_port(self): | |
| non_graphical=self.non_graphical, | ||
| ) | ||
| if sessions: | ||
| self.__port = sessions[0] | ||
| self.port = sessions[0] | ||
| if len(sessions) == 1: | ||
| self.logger.info(f"Found active AEDT gRPC session on port {self.port}.") | ||
| else: | ||
| self.logger.warning( | ||
| f"Multiple AEDT gRPC sessions are found. Setting the active session on port {self.port}." | ||
| ) | ||
| else: | ||
| self.__port = _find_free_port() | ||
| self.port = _find_free_port() | ||
| self.logger.info(f"New AEDT session is starting on gRPC port {self.port}.") | ||
| self.new_desktop = True | ||
|
|
||
|
|
@@ -3285,17 +3317,8 @@ def __init_grpc(self): | |
| lock_file = self._on_ci_generate_lock_file() | ||
|
|
||
| # Validate port availability/compatibility | ||
| try: | ||
| self.__port = self._validate_port(self.port) | ||
| except Exception: | ||
| # NOTE: When we can't validate the port and are not in a | ||
| # remote RPC session, we try to launch a new instance by default. | ||
| self.logger.warning(f"Could not validate port {self.port}") | ||
| if not settings.remote_rpc_session: | ||
| self.logger.info("Opening a new AEDT session.") | ||
| self.new_desktop = True | ||
| self._validate_port() | ||
|
|
||
| self.__port = self._validate_port(self.port, self.machine) | ||
| is_launched = True | ||
| # Launch new AEDT instance if needed | ||
| if self.new_desktop: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -32,6 +32,7 @@ | |||||
| import itertools | ||||||
| import logging | ||||||
| import os | ||||||
| import pathlib | ||||||
| import platform | ||||||
| import re | ||||||
| import shutil | ||||||
|
|
@@ -54,6 +55,7 @@ | |||||
| from ansys.aedt.core.aedt_logger import pyaedt_logger | ||||||
| from ansys.aedt.core.base import PyAedtBase | ||||||
| from ansys.aedt.core.generic.numbers_utils import _units_assignment | ||||||
| from ansys.aedt.core.generic.numbers_utils import is_number | ||||||
| from ansys.aedt.core.generic.settings import settings | ||||||
| from ansys.aedt.core.internal.errors import AEDTRuntimeError | ||||||
| from ansys.aedt.core.internal.errors import GrpcApiError | ||||||
|
|
@@ -1292,7 +1294,9 @@ def _is_port_occupied(port, host=None): | |||||
|
|
||||||
|
|
||||||
| @pyaedt_function_handler() | ||||||
| def is_grpc_session_active(port: int, machine: str | None = None) -> bool: | ||||||
| def is_grpc_session_active( | ||||||
| port: int, machine: str | None = None, student_version: bool = False, version=None, non_graphical=None | ||||||
| ) -> bool: | ||||||
| """Check if a gRPC session is active on the specified port. | ||||||
|
|
||||||
| This function verifies whether an AEDT session is actively listening on | ||||||
|
|
@@ -1310,6 +1314,13 @@ def is_grpc_session_active(port: int, machine: str | None = None) -> bool: | |||||
| The gRPC port number to check. | ||||||
| machine : str, optional | ||||||
| Specific machine IP address. | ||||||
| student_version : bool, optional | ||||||
| Whether to search for student version sessions (ansysedtsv). The default is ``False``. | ||||||
| When ``True``, searches for ``ansysedtsv.exe`` or ``ansysedtsv`` processes. | ||||||
| version : str, optional | ||||||
| Specific AEDT version. | ||||||
| non_graphical : bool, optional | ||||||
| Whether to search for graphical or non-graphical version. The default is ``None``. | ||||||
|
|
||||||
| Returns | ||||||
| ------- | ||||||
|
|
@@ -1338,7 +1349,12 @@ def is_grpc_session_active(port: int, machine: str | None = None) -> bool: | |||||
| if machine and machine not in ["localhost", "127.0.0.1", "::ffff:127.0.0.1", socket.gethostname()]: | ||||||
| return _is_port_occupied(port, machine) | ||||||
|
|
||||||
| return True if port in active_sessions().values() else False | ||||||
| return ( | ||||||
| True | ||||||
| if port | ||||||
| in active_sessions(version=version, student_version=student_version, non_graphical=non_graphical).values() | ||||||
| else False | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| @pyaedt_function_handler() | ||||||
|
|
@@ -1492,6 +1508,109 @@ def active_sessions( | |||||
| return return_dict_filtered | ||||||
|
|
||||||
|
|
||||||
| @pyaedt_function_handler() | ||||||
| def all_active_sessions() -> dict[str, dict]: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| """Get information for active AEDT sessions. | ||||||
|
|
||||||
| This function detects running AEDT processes and identifies their gRPC ports or | ||||||
| marks them as COM sessions. It works on both Windows and Linux platforms by using | ||||||
| multiple detection strategies to ensure reliable session discovery. | ||||||
|
|
||||||
| Detection Strategy (in order of execution): | ||||||
| 1. **Process Discovery**: Searches for AEDT processes (ansysedt.exe or ansysedtsv.exe). | ||||||
| 2. **Command-Line Parsing**: Extracts gRPC port from ``-grpcsrv`` command-line argument. | ||||||
| 3. **Unix gRPC Analysis** (Linux only): Uses ``ss -Hnlp`` to find ports from socket files. | ||||||
| 4. **TCP Connection Analysis**: Falls back to checking active TCP connections via psutil. | ||||||
|
|
||||||
| Port Detection Results: | ||||||
| - Positive integer, gRPC session on that port. | ||||||
| - ``-1``: COM session (no gRPC server running). | ||||||
|
|
||||||
|
|
||||||
| Returns | ||||||
| ------- | ||||||
| dict[str, dict] | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Dictionary mapping AEDT process IDs to their corresponding ports. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Port is set to ``-1`` if the session is using COM instead of gRPC. | ||||||
|
|
||||||
| Examples | ||||||
| -------- | ||||||
| Get all active AEDT sessions (any version, any mode): | ||||||
|
|
||||||
|
|
||||||
| """ | ||||||
| # Step 1: Determine target process names based on version type and operating system | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Steps do not align with the docstring description and misses the second step :p |
||||||
| # Student version uses different executable names (ansysedtsv vs ansysedt) | ||||||
|
|
||||||
| targets = ["ansysedtsv.exe", "ansysedt.exe"] | ||||||
|
|
||||||
| # Step 3: Get all matching AEDT processes from the system | ||||||
| # Returns list of tuples: [(pid, command_line_args), ...] | ||||||
|
|
||||||
| target_processes = _get_target_processes(targets) | ||||||
|
|
||||||
| # Step 4: AEDT processes launched | ||||||
| return_dict = {pid: -1 for pid, _ in target_processes} | ||||||
|
|
||||||
| # Step 5: On Linux, try to resolve unknown ports using Unix socket analysis | ||||||
| # In Linux, running AEDT locally uses Unix domain sockets with filenames containing port numbers | ||||||
| # Example socket: AnsysEMUDS-50051.sock | ||||||
| if is_linux and any(port == -1 for port in return_dict.values()): | ||||||
| try: | ||||||
| # Run 'ss -Hnlp' command to get Unix socket information | ||||||
| sockets = _run_ss() # Returns {pid: port} mapping from socket filenames | ||||||
|
|
||||||
| # Update return_dict with discovered ports | ||||||
| for pid, port in sockets.items(): | ||||||
| # Only update if PID is in our results and port is still unknown (-1) | ||||||
| if pid in return_dict and return_dict[pid] == -1: | ||||||
| return_dict[pid] = port | ||||||
| except Exception as e: | ||||||
| # Log but don't fail - we have other detection methods | ||||||
| pyaedt_logger.debug(f"Failed to analyze Unix sockets for port detection: {str(e)}") | ||||||
|
|
||||||
| # Get all TCP connections for our AEDT processes | ||||||
| connections = _check_psutil_connections(list(return_dict.keys())) | ||||||
| return_dict_filtered = {} | ||||||
| for pid, port in return_dict.items(): | ||||||
| cmdline = "" | ||||||
| if pid in connections and len(connections[pid]) > 0 and "cmdline" in connections[pid][0]: | ||||||
| cmdline = connections[pid][0]["cmdline"] | ||||||
|
|
||||||
| version = [i[1:] for i in pathlib.Path(cmdline).parts if i.startswith("v") and is_number(i[1:])] | ||||||
|
|
||||||
| if version: | ||||||
| version = version[0] | ||||||
| flag_present = "nongraphical" if "-ng" in cmdline else "graphical" | ||||||
| version += f"_{flag_present}" | ||||||
|
|
||||||
| if "ansysedtsv" in cmdline: | ||||||
| version += "_student" | ||||||
|
|
||||||
| if version not in return_dict_filtered: | ||||||
| return_dict_filtered[version] = {pid: port} | ||||||
| else: | ||||||
| return_dict_filtered[version][pid] = port | ||||||
| else: | ||||||
| pyaedt_logger.debug( | ||||||
| f"Failed to retrieve AEDT version, the version should be included in the command line: {cmdline}." | ||||||
| ) | ||||||
|
|
||||||
| # Step 6: Fallback method - Try to find ports by checking TCP network connections | ||||||
| for version, sessions in return_dict_filtered.items(): | ||||||
| if any(port == -1 for port in sessions.values()): | ||||||
| for pid in [i for i, v in sessions.items() if v == -1]: | ||||||
| version_number = version.replace("_student", "").replace("_nongraphical", "").replace("_graphical", "") | ||||||
| sessions[pid] = _check_connection_grpc_port( | ||||||
| connections, | ||||||
| pid, | ||||||
| version_number, | ||||||
| True if "nongraphical" in version else False, | ||||||
| ) | ||||||
|
Comment on lines
+1599
to
+1609
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how this step interact with the returned value, isn't there something missing here ? |
||||||
|
|
||||||
| return return_dict_filtered | ||||||
|
|
||||||
|
|
||||||
| @pyaedt_function_handler() | ||||||
| def com_active_sessions( | ||||||
| version: str | None = None, student_version: bool | None = False, non_graphical: bool | None = False | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we shoudl directly play with
__portinstead of theportproperty. That way we are sure that any change in the property (like this PR) wouldn't affect the inner logic. Open to discussion though :)