Skip to content

Commit

Permalink
Merge pull request #22223 from hlouzada/fix-windows-kernel-tunneling-…
Browse files Browse the repository at this point in the history
…error

PR: Fix Windows tunneling error when connecting to kernels (Remote client)
  • Loading branch information
ccordoba12 authored Jul 16, 2024
2 parents 3b75314 + d5f8088 commit e1fc1a9
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 385 deletions.
6 changes: 5 additions & 1 deletion requirements/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
channels:
- conda-forge
dependencies:
- paramiko >=2.4.0
# This is a dummy dependency for Windows because it's also declared in
# main.yml. But we have it here in case we need to add Windows-only deps
# in the future (without this we'd have to remove code from CIs and tests
# and restore it again if we were to have Windows-only deps again).
- aiohttp >=3.9.3
2 changes: 0 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,6 @@ def run(self):
'keyring>=17.0.0',
'nbconvert>=4.0',
'numpydoc>=0.6.0',
# Required to get SSH connections to remote kernels
'paramiko>=2.4.0;platform_system=="Windows"',
'parso>=0.7.0,<0.9.0',
'pexpect>=4.4.0',
'pickleshare>=0.4',
Expand Down
6 changes: 0 additions & 6 deletions spyder/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
KEYRING_REQVER = '>=17.0.0'
NBCONVERT_REQVER = '>=4.0'
NUMPYDOC_REQVER = '>=0.6.0'
PARAMIKO_REQVER = '>=2.4.0'
PARSO_REQVER = '>=0.7.0,<0.9.0'
PEXPECT_REQVER = '>=4.4.0'
PICKLESHARE_REQVER = '>=0.4'
Expand Down Expand Up @@ -163,11 +162,6 @@
'package_name': "numpydoc",
'features': _("Improve code completion for objects that use Numpy docstrings"),
'required_version': NUMPYDOC_REQVER},
{'modname': "paramiko",
'package_name': "paramiko",
'features': _("Connect to remote kernels through SSH"),
'required_version': PARAMIKO_REQVER,
'display': os.name == 'nt'},
{'modname': "parso",
'package_name': "parso",
'features': _("Python parser that supports error recovery and "
Expand Down
104 changes: 104 additions & 0 deletions spyder/plugins/ipythonconsole/utils/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,77 @@

"""Kernel Client subclass."""

# Standard library imports
import socket

# Third party imports
import asyncssh
from qtpy.QtCore import Signal
from qtconsole.client import QtKernelClient, QtZMQSocketChannel
from traitlets import Type

# Local imports
from spyder.api.asyncdispatcher import AsyncDispatcher
from spyder.api.translations import _


class KernelClientTunneler:
"""Class to handle SSH tunneling for a kernel connection."""

def __init__(self, ssh_connection, *, _close_conn_on_exit=False):
self.ssh_connection = ssh_connection
self._port_forwarded = {}
self._close_conn_on_exit = _close_conn_on_exit

def __del__(self):
"""Close all port forwarders and the connection if required."""
for forwarder in self._port_forwarded.values():
forwarder.close()

if self._close_conn_on_exit:
self.ssh_connection.close()

@classmethod
@AsyncDispatcher.dispatch(loop="asyncssh", early_return=False)
async def new_connection(cls, *args, **kwargs):
"""Create a new SSH connection."""
return cls(
await asyncssh.connect(*args, **kwargs, known_hosts=None),
_close_conn_on_exit=True,
)

@classmethod
def from_connection(cls, ssh_connection):
"""Create a new KernelTunnelHandler from an existing connection."""
return cls(ssh_connection)

@AsyncDispatcher.dispatch(loop="asyncssh", early_return=False)
async def forward_port(self, remote_host, remote_port):
"""Forward a port through the SSH connection."""
local = self._get_free_port()
try:
self._port_forwarded[(remote_host, remote_port)] = (
await self.ssh_connection.forward_local_port(
"", local, remote_host, remote_port
)
)
except asyncssh.Error as err:
raise RuntimeError(
_(
"It was not possible to open an SSH tunnel for the "
"remote kernel. Please check your credentials and the "
"server connection status."
)
) from err
return local

@staticmethod
def _get_free_port():
"""Request a free port from the OS."""
with socket.socket() as s:
s.bind(("", 0))
return s.getsockname()[1]


class SpyderKernelClient(QtKernelClient):
# Enable receiving messages on control channel.
Expand All @@ -25,3 +91,41 @@ def _handle_kernel_info_reply(self, rep):
super()._handle_kernel_info_reply(rep)
spyder_kernels_info = rep["content"].get("spyder_kernels_info", None)
self.sig_spyder_kernel_info.emit(spyder_kernels_info)

def tunnel_to_kernel(
self, hostname=None, sshkey=None, password=None, ssh_connection=None
):
"""Tunnel to remote kernel."""
if ssh_connection is not None:
self.__tunnel_handler = KernelClientTunneler.from_connection(
ssh_connection
)
elif sshkey is not None:
self.__tunnel_handler = KernelClientTunneler.new_connection(
tunnel=hostname,
password=password,
client_keys=[sshkey],
)
else:
self.__tunnel_handler = KernelClientTunneler.new_connection(
tunnel=hostname,
password=password,
)

(
self.shell_port,
self.iopub_port,
self.stdin_port,
self.hb_port,
self.control_port,
) = (
self.__tunnel_handler.forward_port(self.ip, port)
for port in (
self.shell_port,
self.iopub_port,
self.stdin_port,
self.hb_port,
self.control_port,
)
)
self.ip = "127.0.0.1" # Tunneled to localhost
Loading

0 comments on commit e1fc1a9

Please sign in to comment.