Skip to content
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

PR: Fix windows kernel tunneling error #22223

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
101 changes: 101 additions & 0 deletions spyder/plugins/ipythonconsole/utils/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,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 ClientKernelTunneler:
"""Class to handle SSH tunneling for a kernel connection."""

def __init__(self, connection, *, _close_conn_on_exit=False):
self.connection = 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.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, conn):
"""Create a new KernelTunnelHandler from an existing connection."""
return cls(conn)

@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.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 +90,39 @@ 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 = ClientKernelTunneler.from_connection(ssh_connection)
elif sshkey is not None:
self.__tunnel_handler = ClientKernelTunneler.new_connection(
tunnel=hostname,
password=password,
client_keys=[sshkey],
)
else:
self.__tunnel_handler = ClientKernelTunneler.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