From bfc5059853f7a9c272275be173608bbc2513adc7 Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Sun, 9 Jun 2024 22:36:14 -0300 Subject: [PATCH 1/3] feat: refactor server starting logic and getting server info to use with jupyter_server api --- spyder/plugins/remoteclient/api/client.py | 553 ++++++++++------------ 1 file changed, 259 insertions(+), 294 deletions(-) diff --git a/spyder/plugins/remoteclient/api/client.py b/spyder/plugins/remoteclient/api/client.py index 6f87f7da564..ec54c5a1449 100644 --- a/spyder/plugins/remoteclient/api/client.py +++ b/spyder/plugins/remoteclient/api/client.py @@ -6,22 +6,22 @@ from __future__ import annotations import asyncio +import json import logging import socket -import aiohttp import asyncssh from spyder.api.translations import _ +from spyder.plugins.remoteclient.api.jupyterhub import JupyterAPI from spyder.plugins.remoteclient.api.protocol import ( ConnectionInfo, ConnectionStatus, KernelConnectionInfo, KernelInfo, - SSHClientOptions, RemoteClientLog, + SSHClientOptions, ) -from spyder.plugins.remoteclient.api.jupyterhub import JupyterHubAPI from spyder.plugins.remoteclient.api.ssh import SpyderSSHClient from spyder.plugins.remoteclient.utils.installation import ( get_installer_command, @@ -51,23 +51,24 @@ class SpyderRemoteClient: _extra_options = ["platform", "id"] - START_SERVER_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server --jupyterhub" + START_SERVER_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server --jupyter-server" CHECK_SERVER_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server -h" - GET_SERVER_PORT_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server --get-running-port" - GET_SERVER_PID_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server --get-running-pid" - GET_SERVER_TOKEN_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server --get-running-token" + GET_SERVER_INFO_COMMAND = "/${HOME}/.local/bin/micromamba run -n spyder-remote spyder-remote-server --get-running-info" def __init__(self, conf_id, options: SSHClientOptions, _plugin=None): self._config_id = conf_id self.options = options self._plugin = _plugin - self.ssh_connection: asyncssh.SSHClientConnection = None - self.remote_server_process: asyncssh.SSHClientProcess = None - self.port_forwarder: asyncssh.SSHListener = None - self.server_port: int = None - self.local_port: int = None - self._api_token: str = None + self.__server_started = asyncio.Event() + self.__connection_established = asyncio.Event() + self.__starting_server = False + self.__creating_connection = False + + self._ssh_connection: asyncssh.SSHClientConnection = None + self._remote_server_process: asyncssh.SSHClientProcess = None + self._port_forwarder: asyncssh.SSHListener = None + self._server_info = {} self._logger = logging.getLogger( f"{__name__}.{self.__class__.__name__}({self.config_id})" @@ -75,73 +76,42 @@ def __init__(self, conf_id, options: SSHClientOptions, _plugin=None): if self._plugin is not None: self._logger.addHandler(SpyderRemoteClientLoggerHandler(self)) - async def close(self): - """Closes the remote server and the SSH connection.""" + def __emit_connection_status(self, status, message): if self._plugin is not None: self._plugin.sig_connection_status_changed.emit( ConnectionInfo( id=self.config_id, - status=ConnectionStatus.Stopping, - message=_( - "We're closing the connection. Please be patient" - ), + status=status, + message=message ) ) - await self.close_port_forwarder() - await self.stop_remote_server() - await self.close_ssh_connection() - @property - def client_factory(self): - """Return the client factory.""" - if self._plugin is None: - return lambda: asyncssh.SSHClient() - - return lambda: SpyderSSHClient(self) - - async def get_server_pid(self): - """Check if the remote server is running.""" - if self.ssh_connection is None: - self._logger.debug("ssh connection was not established") - return None + def _api_token(self): + return self._server_info.get('token') - try: - output = await self.ssh_connection.run( - self.GET_SERVER_PID_COMMAND, check=True - ) - except asyncssh.ProcessError as err: - self._logger.debug(f"Error getting server pid: {err.stderr}") - return None - except asyncssh.TimeoutError: - self._logger.error("Getting server pid timed out") - return None - except asyncssh.misc.ChannelOpenError: - self._logger.error( - "The connection is closed, so it's not possible to get the " - "server pid" - ) - return None + @property + def server_port(self): + return self._server_info.get('port') - try: - pid = int(output.stdout.strip("PID: ")) - except ValueError: - self._logger.debug( - f"Server pid not found in output: {output.stdout}" - ) - return None + @property + def server_pid(self): + return self._server_info.get('pid') - return pid + @property + def server_started(self): + return self.__server_started.is_set() and not self.__starting_server @property def ssh_is_connected(self): """Check if SSH connection is open.""" - return self.ssh_connection is not None + return (self.__connection_established.is_set() and + not self.__creating_connection) @property def port_is_forwarded(self): """Check if local port is forwarded.""" - return self.port_forwarder is not None + return self._port_forwarder is not None @property def config_id(self): @@ -178,55 +148,132 @@ def api_token(self): @property def peer_host(self): if not self.ssh_is_connected: - return + return None - return self.ssh_connection.get_extra_info("peername")[0] + return self._ssh_connection.get_extra_info("peername")[0] @property def peer_port(self): if not self.ssh_is_connected: - return + return None - return self.ssh_connection.get_extra_info("peername")[1] + return self._ssh_connection.get_extra_info("peername")[1] @property def peer_username(self): if not self.ssh_is_connected: - return + return None - return self.ssh_connection.get_extra_info("username") + return self._ssh_connection.get_extra_info("username") + + @property + def client_factory(self): + """Return the client factory.""" + if self._plugin is None: + return lambda: asyncssh.SSHClient() + + return lambda: SpyderSSHClient(self) + + async def close(self): + """Closes the remote server and the SSH connection.""" + self.__emit_connection_status( + ConnectionStatus.Stopping, + _("We're closing the connection. Please be patient"), + ) + + await self.close_port_forwarder() + await self.stop_remote_server() + await self.close_ssh_connection() + + async def get_server_info(self): + """Check if the remote server is running.""" + if self._ssh_connection is None: + self._logger.debug("ssh connection was not established") + return None + + try: + output = await self._ssh_connection.run( + self.GET_SERVER_INFO_COMMAND, check=True + ) + except asyncssh.TimeoutError: + self._logger.error("Getting server info timed out") + return None + except asyncssh.misc.ChannelOpenError: + self._logger.error( + "The connection is closed, so it's not possible to get the " + "server info" + ) + return None + except asyncssh.ProcessError as err: + self._logger.debug(f"Error getting server infp: {err.stderr}") + return None + + try: + info = json.loads(output.stdout.splitlines()[-1]) + except json.JSONDecodeError: + self._logger.debug( + f"Error parsing server info, received: {output.stdout}" + ) + return None + + return info # -- Connection and server management async def connect_and_install_remote_server(self) -> bool: """Connect to the remote server and install the server.""" + if self.__creating_connection: + await self.__connection_established.wait() + if await self.create_new_connection(): + if self.__starting_server: + await self.__server_started.wait() + return await self.install_remote_server() return False async def connect_and_start_server(self) -> bool: """Connect to the remote server and start the server.""" + if self.__creating_connection: + await self.__connection_established.wait() + if await self.create_new_connection(): + if self.__starting_server: + await self.__server_started.wait() + return await self.start_remote_server() return False + async def ensure_connection_and_server(self) -> bool: + if self.ssh_is_connected and not self.server_started: + return await self.ensure_server() + + if not self.ssh_is_connected: + return await self.connect_and_ensure_server() + + return True + async def connect_and_ensure_server(self) -> bool: """ Connect to the remote server and ensure it is installed and running. """ + if self.__creating_connection: + await self.__connection_established.wait() + if await self.create_new_connection(): return await self.ensure_server() return False - async def ensure_server(self) -> bool: + async def ensure_server(self, check_installed=True) -> bool: """Ensure remote server is installed and running.""" - if not self.ssh_is_connected: - self._logger.error("SSH connection is not open") - return False + + if self.__starting_server: + await self.__server_started.wait() if ( + check_installed and not await self.check_server_installed() and not await self.install_remote_server() ): @@ -235,72 +282,109 @@ async def ensure_server(self) -> bool: return await self.start_remote_server() async def start_remote_server(self): + self.__starting_server = True + try: + if await self._start_remote_server(): + self.__server_started.set() + return True + finally: + self.__starting_server = False + + self.__server_started.clear() + return False + + async def _start_remote_server(self): """Start remote server.""" if not self.ssh_is_connected: self._logger.error("SSH connection is not open") return False - if await self.get_server_pid(): + if (info := await self.get_server_info()): self._logger.warning( f"Remote server is already running for {self.peer_host}" ) - self._logger.debug("Checking API token") - if self._api_token != ( - new_api_token := await self.__extract_api_token() - ): - self._api_token = new_api_token + + self._logger.debug("Checking server info") + if self._server_info != info: + self._server_info = info self._logger.info( - f"Remote server is running for {self.peer_host} with new " - f"API token" + "Different server info, updating info " + f"for {self.peer_host}" ) + if await self.forward_local_port(): + self.__emit_connection_status( + ConnectionStatus.Active, + _("The connection was established successfully") + ) + return True - self._logger.debug("Checking server port") - if self.server_port != ( - new_server_port := await self.__extract_server_port() - ): - self.server_port = new_server_port - self._logger.info( - f"Remote server is running for {self.peer_host} at " - f"port {self.server_port}" + self._logger.error( + "Error forwarding local port, server might not be " + "reachable" + ) + self.__emit_connection_status( + ConnectionStatus.Error, + _("It was not possible to forward the local port") ) - return await self.forward_local_port() - self._logger.info( - f"Remote server is already running for {self.peer_host} at " - f"port {self.server_port}" + self.__emit_connection_status( + ConnectionStatus.Active, + _("The connection was established successfully") ) + return True self._logger.debug(f"Starting remote server for {self.peer_host}") try: - self.remote_server_process = ( - await self.ssh_connection.create_process( + self._remote_server_process = ( + await self._ssh_connection.create_process( self.START_SERVER_COMMAND, stderr=asyncssh.STDOUT, ) ) except (OSError, asyncssh.Error, ValueError) as e: self._logger.error(f"Error starting remote server: {e}") - self.remote_server_process = None + self._remote_server_process = None + self.__emit_connection_status( + ConnectionStatus.Error, + _("Error starting the remote server") + ) return False - self.server_port = await self.__extract_server_port() - self._api_token = await self.__extract_api_token() + _time = 0 + while (info := await self.get_server_info()) is None and _time < 5: + await asyncio.sleep(1) + _time += 1 + + if info is None: + self._logger.error("Faield to get server info") + self.__emit_connection_status( + ConnectionStatus.Error, + _("Error getting server info") + ) + return False + + self._server_info = info self._logger.info( f"Remote server started for {self.peer_host} at port " f"{self.server_port}" ) - self._plugin.sig_connection_status_changed.emit( - ConnectionInfo( - id=self.config_id, - status=ConnectionStatus.Active, - message=_("The connection was established successfully"), + if await self.forward_local_port(): + self.__emit_connection_status( + ConnectionStatus.Active, + _("The connection was established successfully") ) + + self._logger.error( + "Error forwarding local port." ) - return await self.forward_local_port() + self.__emit_connection_status( + ConnectionStatus.Error, + _("It was not possible to forward the local port") + ) async def check_server_installed(self) -> bool: """Check if remote server is installed.""" @@ -309,7 +393,7 @@ async def check_server_installed(self) -> bool: return False try: - await self.ssh_connection.run( + await self._ssh_connection.run( self.CHECK_SERVER_COMMAND, check=True ) except asyncssh.ProcessError as err: @@ -348,7 +432,7 @@ async def install_remote_server(self): return False try: - await self.ssh_connection.run(command, check=True) + await self._ssh_connection.run(command, check=True) except asyncssh.ProcessError as err: self._logger.error(f"Instalation script failed: {err.stderr}") return False @@ -363,6 +447,18 @@ async def install_remote_server(self): return True async def create_new_connection(self) -> bool: + self.__creating_connection = True + try: + if await self._create_new_connection(): + self.__connection_established.set() + return True + finally: + self.__creating_connection = False + + self.__connection_established.clear() + return False + + async def _create_new_connection(self) -> bool: """Creates a new SSH connection to the remote server machine. Args @@ -382,16 +478,10 @@ async def create_new_connection(self) -> bool: ) await self.close_ssh_connection() - if self._plugin is not None: - self._plugin.sig_connection_status_changed.emit( - ConnectionInfo( - id=self.config_id, - status=ConnectionStatus.Connecting, - message=_( - "We're establishing the connection. Please be patient" - ), - ) - ) + self.__emit_connection_status( + ConnectionStatus.Connecting, + _("We're establishing the connection. Please be patient") + ) conect_kwargs = { k: v @@ -400,118 +490,22 @@ async def create_new_connection(self) -> bool: } self._logger.debug("Opening SSH connection") try: - self.ssh_connection = await asyncssh.connect( + self._ssh_connection = await asyncssh.connect( **conect_kwargs, client_factory=self.client_factory ) except (OSError, asyncssh.Error) as e: self._logger.error(f"Failed to open ssh connection: {e}") - if self._plugin is not None: - self._plugin.sig_connection_status_changed.emit( - ConnectionInfo( - id=self.config_id, - status=ConnectionStatus.Error, - message=_( - "It was not possible to open a connection to this " - "machine" - ), - ) - ) - + self.__emit_connection_status( + ConnectionStatus.Error, + _("It was not possible to open a connection to this machine") + ) return False self._logger.info(f"SSH connection opened for {self.peer_host}") return True - async def __extract_server_port(self, _retries=5) -> int | None: - """Extract server port from server stdout. - - Returns - ------- - int | None - The server port if found, None otherwise. - - Raises - ------ - ValueError - If the server port is not found in the server stdout. - """ - self._logger.debug("Extracting server port from server stdout") - - tries = 0 - port = None - while port is None and tries < _retries: - await asyncio.sleep(0.5) - try: - output = await self.ssh_connection.run( - self.GET_SERVER_PORT_COMMAND, check=True - ) - except asyncssh.ProcessError as err: - self._logger.error(f"Error getting server port: {err.stderr}") - return None - except asyncssh.TimeoutError: - self._logger.error("Getting server port timed out") - return None - - try: - port = int(output.stdout.strip("Port: ")) - except ValueError: - self._logger.debug( - f"Server port not found in output: {output.stdout}, " - f"retrying ({tries + 1}/{_retries})" - ) - port = None - tries += 1 - - self._logger.debug(f"Server port extracted: {port}") - - return port - - async def __extract_api_token(self, _retries=5) -> str: - """Extract server token from server stdout. - - Returns - ------- - int | None - The server token if found, None otherwise. - - Raises - ------ - ValueError - If the server token is not found in the server stdout. - """ - self._logger.debug("Extracting server token from server stdout") - - tries = 0 - token = None - while token is None and tries < _retries: - await asyncio.sleep(0.5) - try: - output = await self.ssh_connection.run( - self.GET_SERVER_TOKEN_COMMAND, check=True - ) - except asyncssh.ProcessError as err: - self._logger.error(f"Error getting server token: {err.stderr}") - return None - except asyncssh.TimeoutError: - self._logger.error("Getting server token timed out") - return None - - try: - token = output.stdout.strip("Token: ").splitlines()[0] - except ValueError: - self._logger.debug( - f"Server token not found in output: {output.stdout}, " - f"retrying ({tries + 1}/{_retries})" - ) - token = None - tries += 1 - - self._logger.debug(f"Server port extracted: {token}") - - return token - async def forward_local_port(self): """Forward local port.""" if not self.server_port: @@ -526,7 +520,7 @@ async def forward_local_port(self): f"Forwarding an free local port to remote port {self.server_port}" ) - if self.port_forwarder: + if self._port_forwarder: self._logger.warning( f"Port forwarder is already open for host {self.peer_host} " f"with local port {self.local_port} and remote port " @@ -536,18 +530,20 @@ async def forward_local_port(self): local_port = self.get_free_port() - self.port_forwarder = await self.ssh_connection.forward_local_port( + server_host = self._server_info['hostname'] + + self._port_forwarder = await self._ssh_connection.forward_local_port( "", local_port, - self.options["host"], + server_host, self.server_port, ) self.local_port = local_port self._logger.debug( - f"Forwarded local port {local_port} to remote port " - f"{self.server_port}" + f"Forwarded local port {local_port} to remote server at " + f"{server_host}:{self.server_port}" ) return True @@ -559,9 +555,9 @@ async def close_port_forwarder(self): f"Closing port forwarder for host {self.peer_host} with local " f"port {self.local_port}" ) - self.port_forwarder.close() - await self.port_forwarder.wait_closed() - self.port_forwarder = None + self._port_forwarder.close() + await self._port_forwarder.wait_closed() + self._port_forwarder = None self._logger.debug( f"Port forwarder closed for host {self.peer_host} with local " f"port {self.local_port}" @@ -569,35 +565,35 @@ async def close_port_forwarder(self): async def stop_remote_server(self): """Close remote server.""" - pid = await self.get_server_pid() - if not pid: + if not self.server_started: self._logger.warning( f"Remote server is not running for {self.peer_host}" ) return False + pid = self._server_info['pid'] + # bug in jupyterhub, need to send SIGINT twice self._logger.debug( f"Stopping remote server for {self.peer_host} with pid {pid}" ) try: - await self.ssh_connection.run(f"kill -INT {pid}", check=True) + await self._ssh_connection.run(f"kill -INT {pid}", check=True) except asyncssh.ProcessError as err: self._logger.error(f"Error stopping remote server: {err.stderr}") - return False - - await asyncio.sleep(3) - - await self.ssh_connection.run(f"kill -INT {pid}", check=False) + else: + await asyncio.sleep(1) + await self._ssh_connection.run(f"kill -INT {pid}", check=False) if ( - self.remote_server_process - and not self.remote_server_process.is_closing() + self._remote_server_process + and not self._remote_server_process.is_closing() ): - self.remote_server_process.terminate() - await self.remote_server_process.wait_closed() + self._remote_server_process.terminate() + await self._remote_server_process.wait_closed() - self.remote_server_process = None + self.__server_started.clear() + self._remote_server_process = None self._logger.info(f"Remote server process closed for {self.peer_host}") return True @@ -605,18 +601,15 @@ async def close_ssh_connection(self): """Close SSH connection.""" if self.ssh_is_connected: self._logger.debug(f"Closing SSH connection for {self.peer_host}") - self.ssh_connection.close() - await self.ssh_connection.wait_closed() - self.ssh_connection = None + self._ssh_connection.close() + await self._ssh_connection.wait_closed() + self._ssh_connection = None self._logger.info("SSH connection closed") - if self._plugin is not None: - self._plugin.sig_connection_status_changed.emit( - ConnectionInfo( - id=self.config_id, - status=ConnectionStatus.Inactive, - message=_("The connection was closed successfully") - ) - ) + self.__connection_established.clear() + self.__emit_connection_status( + ConnectionStatus.Inactive, + _("The connection was closed successfully") + ) # --- Kernel Management async def start_new_kernel_ensure_server( @@ -634,12 +627,7 @@ async def start_new_kernel_ensure_server( KernelConnectionInfo The kernel connection information. """ - if self.ssh_is_connected and not await self.get_server_pid(): - await self.ensure_server() - elif ( - not self.ssh_is_connected - and not await self.connect_and_ensure_server() - ): + if not await self.ensure_connection_and_server(): self._logger.error( "Cannot launch kernel, remote server is not running" ) @@ -664,82 +652,59 @@ async def start_new_kernel_ensure_server( async def start_new_kernel(self, kernel_spec=None) -> KernelInfo: """Start new kernel.""" - async with JupyterHubAPI( + async with JupyterAPI( self.server_url, api_token=self.api_token - ) as hub: - async with await hub.ensure_server( - self.peer_username, - self.JUPYTER_SERVER_TIMEOUT, - create_user=True, - ) as jupyter: - response = await jupyter.create_kernel(kernel_spec=kernel_spec) + ) as jupyter: + response = await jupyter.create_kernel(kernel_spec=kernel_spec) self._logger.info(f"Kernel started with ID {response['id']}") return response async def list_kernels(self) -> list[KernelInfo]: """List kernels.""" - async with JupyterHubAPI( + async with JupyterAPI( self.server_url, api_token=self.api_token - ) as hub: - async with await hub.ensure_server( - self.peer_username, self.JUPYTER_SERVER_TIMEOUT - ) as jupyter: - response = await jupyter.list_kernels() + ) as jupyter: + response = await jupyter.list_kernels() self._logger.info(f"Kernels listed for {self.peer_host}") return response async def get_kernel_info(self, kernel_id) -> KernelInfo: """Get kernel info.""" - async with JupyterHubAPI( + async with JupyterAPI( self.server_url, api_token=self.api_token - ) as hub: - async with await hub.ensure_server( - self.peer_username, self.JUPYTER_SERVER_TIMEOUT - ) as jupyter: - response = await jupyter.get_kernel(kernel_id=kernel_id) + ) as jupyter: + response = await jupyter.get_kernel(kernel_id=kernel_id) self._logger.info(f"Kernel info retrieved for ID {kernel_id}") return response async def terminate_kernel(self, kernel_id) -> bool: """Terminate kernel.""" - async with JupyterHubAPI( + async with JupyterAPI( self.server_url, api_token=self.api_token - ) as hub: - try: - async with await hub.ensure_server( - self.peer_username, self.JUPYTER_SERVER_TIMEOUT - ) as jupyter: - response = await jupyter.delete_kernel(kernel_id=kernel_id) - except aiohttp.client_exceptions.ClientConnectionError: - return True + ) as jupyter: + response = await jupyter.delete_kernel(kernel_id=kernel_id) self._logger.info(f"Kernel terminated for ID {kernel_id}") return response async def interrupt_kernel(self, kernel_id) -> bool: """Interrupt kernel.""" - async with JupyterHubAPI( + async with JupyterAPI( self.server_url, api_token=self.api_token - ) as hub: - async with await hub.ensure_server( - self.peer_username, self.JUPYTER_SERVER_TIMEOUT - ) as jupyter: - response = await jupyter.interrupt_kernel(kernel_id=kernel_id) + ) as jupyter: + response = await jupyter.interrupt_kernel(kernel_id=kernel_id) self._logger.info(f"Kernel interrupted for ID {kernel_id}") return response async def restart_kernel(self, kernel_id) -> bool: """Restart kernel.""" - async with JupyterHubAPI( + async with JupyterAPI( self.server_url, api_token=self.api_token - ) as hub: - async with await hub.ensure_server( - self.peer_username, self.JUPYTER_SERVER_TIMEOUT - ) as jupyter: - response = await jupyter.restart_kernel(kernel_id=kernel_id) + ) as jupyter: + response = await jupyter.restart_kernel(kernel_id=kernel_id) self._logger.info(f"Kernel restarted for ID {kernel_id}") return response From 182397c4c8f6196c992be6c3443da46a7252bd33 Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Tue, 11 Jun 2024 20:35:25 -0300 Subject: [PATCH 2/3] fix: improve logic of ensuring server and connection --- spyder/plugins/remoteclient/api/client.py | 86 +++++++++++++++-------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/spyder/plugins/remoteclient/api/client.py b/spyder/plugins/remoteclient/api/client.py index ec54c5a1449..8c60a5b71b2 100644 --- a/spyder/plugins/remoteclient/api/client.py +++ b/spyder/plugins/remoteclient/api/client.py @@ -60,8 +60,10 @@ def __init__(self, conf_id, options: SSHClientOptions, _plugin=None): self.options = options self._plugin = _plugin + self.__server_installed = asyncio.Event() self.__server_started = asyncio.Event() self.__connection_established = asyncio.Event() + self.__installing_server = False self.__starting_server = False self.__creating_connection = False @@ -221,31 +223,22 @@ async def get_server_info(self): # -- Connection and server management async def connect_and_install_remote_server(self) -> bool: """Connect to the remote server and install the server.""" - if self.__creating_connection: - await self.__connection_established.wait() - if await self.create_new_connection(): - if self.__starting_server: - await self.__server_started.wait() - return await self.install_remote_server() return False async def connect_and_start_server(self) -> bool: """Connect to the remote server and start the server.""" - if self.__creating_connection: - await self.__connection_established.wait() - if await self.create_new_connection(): - if self.__starting_server: - await self.__server_started.wait() - return await self.start_remote_server() return False async def ensure_connection_and_server(self) -> bool: + """ + Ensure the SSH connection is open and the remote server is running. + """ if self.ssh_is_connected and not self.server_started: return await self.ensure_server() @@ -258,33 +251,42 @@ async def connect_and_ensure_server(self) -> bool: """ Connect to the remote server and ensure it is installed and running. """ - if self.__creating_connection: - await self.__connection_established.wait() - - if await self.create_new_connection(): + if await self.create_new_connection() and not self.server_started: return await self.ensure_server() + if self.server_started: + return True + return False - async def ensure_server(self, check_installed=True) -> bool: - """Ensure remote server is installed and running.""" + async def ensure_connection(self) -> bool: + """Ensure the SSH connection is open.""" + if self.ssh_is_connected: + return True - if self.__starting_server: - await self.__server_started.wait() + return await self.create_new_connection() + + async def ensure_server(self, *, check_installed=True) -> bool: + """Ensure remote server is installed and running.""" + if self.server_started: + return True if ( - check_installed and - not await self.check_server_installed() - and not await self.install_remote_server() + check_installed and not await self.ensure_server_installed() ): return False return await self.start_remote_server() async def start_remote_server(self): + """Start remote server.""" + if self.__starting_server: + await self.__server_started.wait() + return True + self.__starting_server = True try: - if await self._start_remote_server(): + if await self.__start_remote_server(): self.__server_started.set() return True finally: @@ -293,7 +295,7 @@ async def start_remote_server(self): self.__server_started.clear() return False - async def _start_remote_server(self): + async def __start_remote_server(self): """Start remote server.""" if not self.ssh_is_connected: self._logger.error("SSH connection is not open") @@ -386,6 +388,13 @@ async def _start_remote_server(self): _("It was not possible to forward the local port") ) + async def ensure_server_installed(self) -> bool: + """Ensure remote server is installed.""" + if not await self.check_server_installed(): + return await self.install_remote_server() + + return True + async def check_server_installed(self) -> bool: """Check if remote server is installed.""" if not self.ssh_is_connected: @@ -413,7 +422,24 @@ async def check_server_installed(self) -> bool: return True - async def install_remote_server(self): + async def install_remote_server(self) -> bool: + """Install remote server.""" + if self.__installing_server: + await self.__server_installed.wait() + return True + + self.__installing_server = True + try: + if await self.__install_remote_server(): + self.__server_installed.set() + return True + finally: + self.__installing_server = False + + self.__server_installed.clear() + return False + + async def __install_remote_server(self): """Install remote server.""" if not self.ssh_is_connected: self._logger.error("SSH connection is not open") @@ -447,9 +473,13 @@ async def install_remote_server(self): return True async def create_new_connection(self) -> bool: + if self.__creating_connection: + await self.__connection_established.wait() + return True + self.__creating_connection = True try: - if await self._create_new_connection(): + if await self.__create_new_connection(): self.__connection_established.set() return True finally: @@ -458,7 +488,7 @@ async def create_new_connection(self) -> bool: self.__connection_established.clear() return False - async def _create_new_connection(self) -> bool: + async def __create_new_connection(self) -> bool: """Creates a new SSH connection to the remote server machine. Args From ece13dbd9b915d71c932f16d64b3a7d3260dcf89 Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Tue, 11 Jun 2024 21:10:01 -0300 Subject: [PATCH 3/3] fix: correctly return after forwarding local port --- spyder/plugins/remoteclient/api/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spyder/plugins/remoteclient/api/client.py b/spyder/plugins/remoteclient/api/client.py index 8c60a5b71b2..658218ff8a2 100644 --- a/spyder/plugins/remoteclient/api/client.py +++ b/spyder/plugins/remoteclient/api/client.py @@ -378,6 +378,7 @@ async def __start_remote_server(self): ConnectionStatus.Active, _("The connection was established successfully") ) + return True self._logger.error( "Error forwarding local port." @@ -387,6 +388,7 @@ async def __start_remote_server(self): ConnectionStatus.Error, _("It was not possible to forward the local port") ) + return False async def ensure_server_installed(self) -> bool: """Ensure remote server is installed."""