From c331f32500a78f018eb929291b6c339320af397c Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sun, 4 Dec 2022 10:26:16 +0100 Subject: [PATCH] tmp --- external-deps/spyder-kernels-server/README.md | 1 + external-deps/spyder-kernels-server/setup.py | 73 +++++ .../spyder_kernels_server/__init__.py | 2 + .../spyder_kernels_server/__main__.py | 56 ++++ .../spyder_kernels_server/_version.py | 12 + .../spyder_kernels_server/kernel_client.py | 0 .../spyder_kernels_server/kernel_comm.py | 1 - .../spyder_kernels_server/kernel_manager.py | 2 +- .../spyder_kernels_server/kernel_server.py | 174 ++++++++++++ .../ipythonconsole/utils/kernel_handler.py | 265 ++++-------------- .../plugins/ipythonconsole/widgets/client.py | 42 --- .../ipythonconsole/widgets/main_widget.py | 15 +- .../plugins/ipythonconsole/widgets/mixins.py | 39 ++- .../plugins/ipythonconsole/widgets/shell.py | 9 +- spyder/plugins/maininterpreter/confpage.py | 92 +++++- 15 files changed, 511 insertions(+), 272 deletions(-) create mode 100644 external-deps/spyder-kernels-server/README.md create mode 100644 external-deps/spyder-kernels-server/setup.py create mode 100644 external-deps/spyder-kernels-server/spyder_kernels_server/__init__.py create mode 100644 external-deps/spyder-kernels-server/spyder_kernels_server/__main__.py create mode 100644 external-deps/spyder-kernels-server/spyder_kernels_server/_version.py rename spyder/plugins/ipythonconsole/utils/client.py => external-deps/spyder-kernels-server/spyder_kernels_server/kernel_client.py (100%) rename spyder/plugins/ipythonconsole/comms/kernelcomm.py => external-deps/spyder-kernels-server/spyder_kernels_server/kernel_comm.py (99%) rename spyder/plugins/ipythonconsole/utils/manager.py => external-deps/spyder-kernels-server/spyder_kernels_server/kernel_manager.py (98%) create mode 100644 external-deps/spyder-kernels-server/spyder_kernels_server/kernel_server.py diff --git a/external-deps/spyder-kernels-server/README.md b/external-deps/spyder-kernels-server/README.md new file mode 100644 index 00000000000..30404ce4c54 --- /dev/null +++ b/external-deps/spyder-kernels-server/README.md @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/external-deps/spyder-kernels-server/setup.py b/external-deps/spyder-kernels-server/setup.py new file mode 100644 index 00000000000..916e66fba76 --- /dev/null +++ b/external-deps/spyder-kernels-server/setup.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +"""Jupyter Kernels for the Spyder consoles.""" + +# Standard library imports +import ast +import io +import os + +# Third party imports +from setuptools import find_packages, setup + +HERE = os.path.abspath(os.path.dirname(__file__)) + +with io.open('README.md', encoding='utf-8') as f: + LONG_DESCRIPTION = f.read() + + +def get_version(module='spyder_kernels_server'): + """Get version.""" + with open(os.path.join(HERE, module, '_version.py'), 'r') as f: + data = f.read() + lines = data.split('\n') + for line in lines: + if line.startswith('VERSION_INFO'): + version_tuple = ast.literal_eval(line.split('=')[-1].strip()) + version = '.'.join(map(str, version_tuple)) + break + return version + + +REQUIREMENTS = [ + 'spyder-kernels', +] + +setup( + name='spyder-kernels-server', + version=get_version(), + keywords='spyder jupyter kernel ipython console', + url='https://github.com/spyder-ide/spyder-kernels', + download_url="https://www.spyder-ide.org/#fh5co-download", + license='MIT', + author='Spyder Development Team', + author_email="spyderlib@googlegroups.com", + description="Jupyter kernels for Spyder's console", + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', + packages=find_packages(exclude=['docs', '*tests']), + install_requires=REQUIREMENTS, + # extras_require={'test': TEST_REQUIREMENTS}, + include_package_data=True, + python_requires='>=3.7', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Framework :: Jupyter', + 'Framework :: IPython', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Software Development :: Interpreters', + ] +) diff --git a/external-deps/spyder-kernels-server/spyder_kernels_server/__init__.py b/external-deps/spyder-kernels-server/spyder_kernels_server/__init__.py new file mode 100644 index 00000000000..633f866158a --- /dev/null +++ b/external-deps/spyder-kernels-server/spyder_kernels_server/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- + diff --git a/external-deps/spyder-kernels-server/spyder_kernels_server/__main__.py b/external-deps/spyder-kernels-server/spyder_kernels_server/__main__.py new file mode 100644 index 00000000000..0d69363c8c8 --- /dev/null +++ b/external-deps/spyder-kernels-server/spyder_kernels_server/__main__.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- +import sys +import zmq +import json +from spyder_kernels_server.kernel_server import KernelServer + + +def main(port): + if len(sys.argv) > 1: + port = sys.argv[1] + + context = zmq.Context() + socket = context.socket(zmq.REP) + socket.bind("tcp://*:%s" % port) + print(f"Server running on port {port}") + kernel_server = KernelServer() + shutdown = False + + while not shutdown: + # Wait for next request from client + message = socket.recv_pyobj() + print(message) + cmd = message[0] + if cmd == "shutdown": + socket.send_pyobj(["shutting_down"]) + shutdown = True + kernel_server.shutdown() + + elif cmd == "open_kernel": + try: + cf = kernel_server.open_kernel(message[1]) + print(cf) + with open(cf, "br") as f: + cf = (cf, json.load(f)) + + except Exception as e: + cf = ("error", e) + socket.send_pyobj(["new_kernel", *cf]) + + elif cmd == "close_kernel": + socket.send_pyobj(["closing_kernel"]) + try: + kernel_server.close_kernel(message[1]) + except Exception: + pass + + +if __name__ == "__main__": + port = "5556" + main(port) diff --git a/external-deps/spyder-kernels-server/spyder_kernels_server/_version.py b/external-deps/spyder-kernels-server/spyder_kernels_server/_version.py new file mode 100644 index 00000000000..b9e5bdfe787 --- /dev/null +++ b/external-deps/spyder-kernels-server/spyder_kernels_server/_version.py @@ -0,0 +1,12 @@ +# +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +"""Version File.""" + +VERSION_INFO = (1, 0, 0, 'dev0') +__version__ = '.'.join(map(str, VERSION_INFO)) diff --git a/spyder/plugins/ipythonconsole/utils/client.py b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_client.py similarity index 100% rename from spyder/plugins/ipythonconsole/utils/client.py rename to external-deps/spyder-kernels-server/spyder_kernels_server/kernel_client.py diff --git a/spyder/plugins/ipythonconsole/comms/kernelcomm.py b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_comm.py similarity index 99% rename from spyder/plugins/ipythonconsole/comms/kernelcomm.py rename to external-deps/spyder-kernels-server/spyder_kernels_server/kernel_comm.py index c8636ec1f47..cc9f1fd6dce 100644 --- a/spyder/plugins/ipythonconsole/comms/kernelcomm.py +++ b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_comm.py @@ -15,7 +15,6 @@ from qtpy.QtCore import QEventLoop, QObject, QTimer, Signal from spyder_kernels.comms.commbase import CommBase -from spyder.py3compat import TimeoutError logger = logging.getLogger(__name__) TIMEOUT_KERNEL_START = 30 diff --git a/spyder/plugins/ipythonconsole/utils/manager.py b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_manager.py similarity index 98% rename from spyder/plugins/ipythonconsole/utils/manager.py rename to external-deps/spyder-kernels-server/spyder_kernels_server/kernel_manager.py index 0beabef9531..a24c9c4c614 100644 --- a/spyder/plugins/ipythonconsole/utils/manager.py +++ b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_manager.py @@ -30,7 +30,7 @@ class SpyderKernelManager(QtKernelManager): """ client_class = DottedObjectName( - 'spyder.plugins.ipythonconsole.utils.client.SpyderKernelClient') + 'spyder_kernels_server.kernel_client.SpyderKernelClient') def __init__(self, *args, **kwargs): self.shutting_down = False diff --git a/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_server.py b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_server.py new file mode 100644 index 00000000000..b2db62395e1 --- /dev/null +++ b/external-deps/spyder-kernels-server/spyder_kernels_server/kernel_server.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +"""Jupyter Kernels for the Spyder consoles.""" + +# Standard library imports +import os +import os.path as osp +from subprocess import PIPE +import uuid + +from threading import Thread + + +# Third-party imports +from jupyter_core.paths import jupyter_runtime_dir + +from spyder_kernels_server.kernel_manager import SpyderKernelManager +from spyder_kernels_server.kernel_comm import KernelComm + + +PERMISSION_ERROR_MSG = ( + "The directory {} is not writable and it is required to create IPython " + "consoles. Please make it writable." +) + +# kernel_comm needs a qthread +class StdThread(Thread): + """Poll for changes in std buffers.""" + def __init__(self, std_buffer, buffer_key, kernel_comm): + self._std_buffer = std_buffer + + self.buffer_key = buffer_key + self.kernel_comm = kernel_comm + super().__init__() + + def run(self): + txt = True + while txt: + txt = self._std_buffer.read1() + if txt: + # Needs to be on control so the message is sent to currently + # executing shell + self.kernel_comm.remote_call( + interrupt=True + ).print_remote( + txt.decode(), + self.buffer_key + ) + + +class ShutdownThread(Thread): + def __init__(self, kernel_dict): + self.kernel_dict = kernel_dict + super().__init__() + + def run(self): + """Shutdown kernel.""" + kernel_manager = self.kernel_dict["kernel"] + + if not kernel_manager.shutting_down: + kernel_manager.shutting_down = True + try: + kernel_manager.shutdown_kernel() + except Exception: + # kernel was externally killed + pass + if "stdout" in self.kernel_dict: + self.kernel_dict["stdout" ].join() + if "stderr" in self.kernel_dict: + self.kernel_dict["stderr" ].join() + + +class KernelServer: + + def __init__(self): + self._kernel_list = {} + + @staticmethod + def new_connection_file(): + """ + Generate a new connection file + + Taken from jupyter_client/console_app.py + Licensed under the BSD license + """ + # Check if jupyter_runtime_dir exists (Spyder addition) + if not osp.isdir(jupyter_runtime_dir()): + try: + os.makedirs(jupyter_runtime_dir()) + except (IOError, OSError): + return None + cf = "" + while not cf: + ident = str(uuid.uuid4()).split("-")[-1] + cf = os.path.join(jupyter_runtime_dir(), "kernel-%s.json" % ident) + cf = cf if not os.path.exists(cf) else "" + return cf + + + def open_kernel(self, kernel_spec): + """ + Create a new kernel. + + Might raise all kinds of exceptions + """ + connection_file = self.new_connection_file() + if connection_file is None: + raise RuntimeError( + PERMISSION_ERROR_MSG.format(jupyter_runtime_dir()) + ) + + # Kernel manager + kernel_manager = SpyderKernelManager( + connection_file=connection_file, + config=None, + autorestart=True, + ) + + kernel_manager._kernel_spec = kernel_spec + + kernel_manager.start_kernel( + stderr=PIPE, + stdout=PIPE, + env=kernel_spec.env, + ) + + kernel_key = connection_file + self._kernel_list[kernel_key] = { + "kernel": kernel_manager + } + + kernel_client = kernel_manager.client() + kernel_client.start_channels() + kernel_comm = KernelComm() + kernel_comm.open_comm(kernel_client) + self.connect_std_pipes(kernel_key, kernel_comm) + + + return connection_file + + def connect_std_pipes(self, kernel_key, kernel_comm): + """Connect to std pipes.""" + + kernel_manager = self._kernel_list[kernel_key]["kernel"] + stdout = kernel_manager.provisioner.process.stdout + stderr = kernel_manager.provisioner.process.stderr + + if stdout: + stdout_thread = StdThread( + stdout, "stdout", kernel_comm) + stdout_thread.start() + self._kernel_list[kernel_key]["stdout"] = stdout_thread + if stderr: + stderr_thread = StdThread( + stderr, "stderr", kernel_comm) + stderr_thread.start() + self._kernel_list[kernel_key]["stderr"] = stderr_thread + + def close_kernel(self, kernel_key): + """Close kernel""" + kernel_manager = self._kernel_list[kernel_key]["kernel"] + kernel_manager.stop_restarter() + shutdown_thread = ShutdownThread(self._kernel_list.pop(kernel_key)) + shutdown_thread.start() + + def shutdown(self): + for kernel_key in self._kernel_list: + self.close_kernel(kernel_key) diff --git a/spyder/plugins/ipythonconsole/utils/kernel_handler.py b/spyder/plugins/ipythonconsole/utils/kernel_handler.py index 26935b795f4..488d244f4ea 100644 --- a/spyder/plugins/ipythonconsole/utils/kernel_handler.py +++ b/spyder/plugins/ipythonconsole/utils/kernel_handler.py @@ -9,14 +9,10 @@ # Standard library imports import ast import os -import os.path as osp -from subprocess import PIPE -from threading import Lock import uuid # Third-party imports -from jupyter_core.paths import jupyter_runtime_dir -from qtpy.QtCore import QObject, QThread, Signal, Slot +from qtpy.QtCore import QObject, Signal from zmq.ssh import tunnel as zmqtunnel # Local imports @@ -24,9 +20,8 @@ from spyder.plugins.ipythonconsole import ( SPYDER_KERNELS_MIN_VERSION, SPYDER_KERNELS_MAX_VERSION, SPYDER_KERNELS_VERSION, SPYDER_KERNELS_CONDA, SPYDER_KERNELS_PIP) -from spyder.plugins.ipythonconsole.comms.kernelcomm import KernelComm -from spyder.plugins.ipythonconsole.utils.manager import SpyderKernelManager -from spyder.plugins.ipythonconsole.utils.client import SpyderKernelClient +from spyder_kernels_server.kernel_comm import KernelComm +from spyder_kernels_server.kernel_client import SpyderKernelClient from spyder.plugins.ipythonconsole.utils.ssh import openssh_tunnel from spyder.utils.programs import check_version_range @@ -89,39 +84,12 @@ class KernelConnectionState: Closed = 'closed' -class StdThread(QThread): - """Poll for changes in std buffers.""" - sig_out = Signal(str) - - def __init__(self, parent, std_buffer): - super().__init__(parent) - self._std_buffer = std_buffer - self._closing = False - - def run(self): - txt = True - while txt: - txt = self._std_buffer.read1() - if txt: - self.sig_out.emit(txt.decode()) - - class KernelHandler(QObject): """ A class to handle the kernel in several ways and store kernel connection information. """ - sig_stdout = Signal(str) - """ - A stdout message was received on the process stdout. - """ - - sig_stderr = Signal(str) - """ - A stderr message was received on the process stderr. - """ - sig_fault = Signal(str) """ A fault message was received. @@ -137,10 +105,15 @@ class KernelHandler(QObject): The kernel raised an error while connecting. """ + sig_request_close = Signal(str) + """ + This kernel would like to be closed + """ + def __init__( self, connection_file, - kernel_manager=None, + kernel_spec=None, kernel_client=None, known_spyder_kernel=False, hostname=None, @@ -150,7 +123,7 @@ def __init__( super().__init__() # Connection Informations self.connection_file = connection_file - self.kernel_manager = kernel_manager + self.kernel_spec = kernel_spec self.kernel_client = kernel_client self.known_spyder_kernel = known_spyder_kernel self.hostname = hostname @@ -165,18 +138,12 @@ def __init__( self.handle_comm_ready) # Internal - self._shutdown_thread = None - self._shutdown_lock = Lock() - self._stdout_thread = None - self._stderr_thread = None self._fault_args = None - self._init_stderr = "" - self._init_stdout = "" self._spyder_kernel_info_uuid = None self._shellwidget_connected = False # Start kernel - self.connect_std_pipes() + # self.connect_std_pipes() self.kernel_client.start_channels() self.check_kernel_info() @@ -191,13 +158,13 @@ def connect(self): elif self.connection_state == KernelConnectionState.Error: self.sig_kernel_connection_error.emit() - # Show initial io - if self._init_stderr: - self.sig_stderr.emit(self._init_stderr) - self._init_stderr = None - if self._init_stdout: - self.sig_stdout.emit(self._init_stdout) - self._init_stdout = None + # # Show initial io + # if self._init_stderr: + # self.sig_stderr.emit(self._init_stderr) + # self._init_stderr = None + # if self._init_stdout: + # self.sig_stdout.emit(self._init_stdout) + # self._init_stdout = None def check_kernel_info(self): """Send request to check kernel info.""" @@ -282,81 +249,6 @@ def handle_comm_ready(self): self.connection_state = KernelConnectionState.SpyderKernelReady self.sig_kernel_is_ready.emit() - def connect_std_pipes(self): - """Connect to std pipes.""" - self.close_std_threads() - - # Connect new threads - if self.kernel_manager is None: - return - - stdout = self.kernel_manager.provisioner.process.stdout - stderr = self.kernel_manager.provisioner.process.stderr - - if stdout: - self._stdout_thread = StdThread(self, stdout) - self._stdout_thread.sig_out.connect(self.handle_stdout) - self._stdout_thread.start() - if stderr: - self._stderr_thread = StdThread(self, stderr) - self._stderr_thread.sig_out.connect(self.handle_stderr) - self._stderr_thread.start() - - def disconnect_std_pipes(self): - """Disconnect old std pipes.""" - if self._stdout_thread and not self._stdout_thread._closing: - self._stdout_thread.sig_out.disconnect(self.handle_stdout) - self._stdout_thread._closing = True - if self._stderr_thread and not self._stderr_thread._closing: - self._stderr_thread.sig_out.disconnect(self.handle_stderr) - self._stderr_thread._closing = True - - def close_std_threads(self): - """Close std threads.""" - if self._stdout_thread is not None: - self._stdout_thread.wait() - self._stdout_thread = None - if self._stderr_thread is not None: - self._stderr_thread.wait() - self._stderr_thread = None - - @Slot(str) - def handle_stderr(self, err): - """Handle stderr""" - if self._shellwidget_connected: - self.sig_stderr.emit(err) - else: - self._init_stderr += err - - @Slot(str) - def handle_stdout(self, out): - """Handle stdout""" - if self._shellwidget_connected: - self.sig_stdout.emit(out) - else: - self._init_stdout += out - - @staticmethod - def new_connection_file(): - """ - Generate a new connection file - - Taken from jupyter_client/console_app.py - Licensed under the BSD license - """ - # Check if jupyter_runtime_dir exists (Spyder addition) - if not osp.isdir(jupyter_runtime_dir()): - try: - os.makedirs(jupyter_runtime_dir()) - except (IOError, OSError): - return None - cf = "" - while not cf: - ident = str(uuid.uuid4()).split("-")[-1] - cf = os.path.join(jupyter_runtime_dir(), "kernel-%s.json" % ident) - cf = cf if not os.path.exists(cf) else "" - return cf - @staticmethod def tunnel_to_kernel( connection_info, hostname, sshkey=None, password=None, timeout=10 @@ -380,45 +272,34 @@ def tunnel_to_kernel( return tuple(lports) @classmethod - def new_from_spec(cls, kernel_spec): + def new_from_spec( + cls, kernel_spec, connection_file, connection_info, + hostname=None, sshkey=None, password=None + ): """ Create a new kernel. - - Might raise all kinds of exceptions """ - connection_file = cls.new_connection_file() - if connection_file is None: - raise RuntimeError( - PERMISSION_ERROR_MSG.format(jupyter_runtime_dir()) + kernel_client = SpyderKernelClient() + kernel_client.load_connection_info(connection_info) + kernel_client = cls.tunnel_kernel_client( + kernel_client, + hostname, + sshkey, + password ) - # Kernel manager - kernel_manager = SpyderKernelManager( - connection_file=connection_file, - config=None, - autorestart=True, - ) - - kernel_manager._kernel_spec = kernel_spec - - kernel_manager.start_kernel( - stderr=PIPE, - stdout=PIPE, - env=kernel_spec.env, - ) - - # Kernel client - kernel_client = kernel_manager.client() - # Increase time (in seconds) to detect if a kernel is alive. # See spyder-ide/spyder#3444. kernel_client.hb_channel.time_to_dead = 25.0 return cls( connection_file=connection_file, - kernel_manager=kernel_manager, + kernel_spec=kernel_spec, kernel_client=kernel_client, known_spyder_kernel=True, + hostname=hostname, + sshkey=sshkey, + password=password, ) @classmethod @@ -426,22 +307,6 @@ def from_connection_file( cls, connection_file, hostname=None, sshkey=None, password=None ): """Create kernel for given connection file.""" - return cls( - connection_file, - hostname=hostname, - sshkey=sshkey, - password=password, - kernel_client=cls.init_kernel_client( - connection_file, - hostname, - sshkey, - password - ) - ) - - @classmethod - def init_kernel_client(cls, connection_file, hostname, sshkey, password): - """Create kernel client.""" kernel_client = SpyderKernelClient( connection_file=connection_file ) @@ -459,6 +324,24 @@ def init_kernel_client(cls, connection_file, hostname, sshkey, password): + str(e) ) + kernel_client = cls.tunnel_kernel_client( + kernel_client, + hostname, + sshkey, + password + ) + + return cls( + connection_file, + hostname=hostname, + sshkey=sshkey, + password=password, + kernel_client=kernel_client + ) + + @classmethod + def tunnel_kernel_client(cls, kernel_client, hostname, sshkey, password): + """Create kernel client.""" if hostname is not None: try: connection_info = dict( @@ -490,20 +373,8 @@ def close(self, shutdown_kernel=True, now=False): """Close kernel""" self.close_comm() - if shutdown_kernel and self.kernel_manager is not None: - km = self.kernel_manager - km.stop_restarter() - self.disconnect_std_pipes() - - if now: - km.shutdown_kernel(now=True) - self.after_shutdown() - else: - shutdown_thread = QThread(None) - shutdown_thread.run = self._thread_shutdown_kernel - shutdown_thread.start() - shutdown_thread.finished.connect(self.after_shutdown) - self._shutdown_thread = shutdown_thread + if shutdown_kernel and self.kernel_spec is not None: + self.sig_request_close.emit(self.connection_file) if ( self.kernel_client is not None @@ -515,34 +386,6 @@ def after_shutdown(self): """Cleanup after shutdown""" self.close_std_threads() self.kernel_comm.remove(only_closing=True) - self._shutdown_thread = None - - def _thread_shutdown_kernel(self): - """Shutdown kernel.""" - with self._shutdown_lock: - # Avoid calling shutdown_kernel on the same manager twice - # from different threads to avoid crash. - if self.kernel_manager.shutting_down: - return - self.kernel_manager.shutting_down = True - try: - self.kernel_manager.shutdown_kernel() - except Exception: - # kernel was externally killed - pass - - def wait_shutdown_thread(self): - """Wait shutdown thread.""" - thread = self._shutdown_thread - if thread is None: - return - if thread.isRunning(): - try: - thread.kernel_manager._kill_kernel() - except Exception: - pass - thread.quit() - thread.wait() def copy(self): """Copy kernel.""" @@ -558,7 +401,7 @@ def copy(self): return self.__class__( connection_file=self.connection_file, - kernel_manager=self.kernel_manager, + kernel_spec=self.kernel_spec, known_spyder_kernel=self.known_spyder_kernel, hostname=self.hostname, sshkey=self.sshkey, diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py index 58c906467ee..d4e18e2c741 100644 --- a/spyder/plugins/ipythonconsole/widgets/client.py +++ b/spyder/plugins/ipythonconsole/widgets/client.py @@ -320,8 +320,6 @@ def connect_kernel(self, kernel_handler, first_connect=True): self.kernel_handler = kernel_handler # Connect standard streams. - kernel_handler.sig_stderr.connect(self.print_stderr) - kernel_handler.sig_stdout.connect(self.print_stdout) kernel_handler.sig_fault.connect(self.print_fault) kernel_handler.sig_kernel_is_ready.connect( self._when_kernel_is_ready) @@ -336,51 +334,11 @@ def disconnect_kernel(self, shutdown_kernel): if not kernel_handler: return - kernel_handler.sig_stderr.disconnect(self.print_stderr) - kernel_handler.sig_stdout.disconnect(self.print_stdout) kernel_handler.sig_fault.disconnect(self.print_fault) self.shellwidget.disconnect_kernel(shutdown_kernel) self.kernel_handler = None - @Slot(str) - def print_stderr(self, stderr): - """Print stderr written in PIPE.""" - if not stderr: - return - - if self.is_benign_error(stderr): - return - - if self.shellwidget.isHidden(): - error_text = '%s' % stderr - # Avoid printing the same thing again - if self.error_text != error_text: - if self.error_text: - # Append to error text - error_text = self.error_text + error_text - self.show_kernel_error(error_text) - - if self.shellwidget._starting: - self.shellwidget.banner = ( - stderr + '\n' + self.shellwidget.banner) - else: - self.shellwidget._append_plain_text( - stderr, before_prompt=True) - - @Slot(str) - def print_stdout(self, stdout): - """Print stdout written in PIPE.""" - if not stdout: - return - - if self.shellwidget._starting: - self.shellwidget.banner = ( - stdout + '\n' + self.shellwidget.banner) - else: - self.shellwidget._append_plain_text( - stdout, before_prompt=True) - def connect_shellwidget_signals(self): """Configure shellwidget after kernel is connected.""" # Set exit callback diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py index 57a9a7efc68..3699d00e7f8 100644 --- a/spyder/plugins/ipythonconsole/widgets/main_widget.py +++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py @@ -38,7 +38,8 @@ from spyder.plugins.ipythonconsole.widgets import ( ClientWidget, ConsoleRestartDialog, COMPLETION_WIDGET_TYPE, KernelConnectionDialog, PageControlWidget) -from spyder.plugins.ipythonconsole.widgets.mixins import CachedKernelMixin +from spyder.plugins.ipythonconsole.widgets.mixins import ( + CachedKernelMixin, KernelConnectorMixin) from spyder.py3compat import PY38_OR_MORE from spyder.utils import encoding, programs, sourcecode from spyder.utils.misc import get_error_match, remove_backslashes @@ -114,7 +115,9 @@ class IPythonConsoleWidgetTabsContextMenuSections: # --- Widgets # ---------------------------------------------------------------------------- -class IPythonConsoleWidget(PluginMainWidget, CachedKernelMixin): +class IPythonConsoleWidget( + PluginMainWidget, CachedKernelMixin, KernelConnectorMixin +): """ IPython Console plugin @@ -1402,7 +1405,7 @@ def create_client_for_kernel(self, connection_file, hostname, sshkey, related_clients = [] for cl in self.clients: - if connection_file in cl.connection_file: + if cl.connection_file and connection_file in cl.connection_file: if ( cl.kernel_handler is not None and hostname == cl.kernel_handler.hostname and @@ -1772,8 +1775,8 @@ def restart_kernel(self, client=None, ask_before_restart=True): if client is None: return - km = client.kernel_handler.kernel_manager - if km is None: + ks = client.kernel_handler.kernel_spec + if ks is None: client.shellwidget._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True @@ -1798,7 +1801,7 @@ def restart_kernel(self, client=None, ask_before_restart=True): # Get new kernel try: - kernel_handler = self.get_cached_kernel(km._kernel_spec) + kernel_handler = self.get_cached_kernel(ks) except Exception as e: client.show_kernel_error(e) return diff --git a/spyder/plugins/ipythonconsole/widgets/mixins.py b/spyder/plugins/ipythonconsole/widgets/mixins.py index 7c5b829d77c..792a42f51a4 100644 --- a/spyder/plugins/ipythonconsole/widgets/mixins.py +++ b/spyder/plugins/ipythonconsole/widgets/mixins.py @@ -7,14 +7,47 @@ """ IPython Console mixins. """ +import zmq # Local imports from spyder.plugins.ipythonconsole.utils.kernel_handler import KernelHandler +class KernelConnectorMixin: + """Needs https://github.com/jupyter/jupyter_client/pull/835""" + def __init__(self): + super().__init__() + self.port = "5556" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REQ) + self.socket.connect("tcp://localhost:%s" % self.port) + + def new_kernel(self, kernel_spec): + """Get a new kernel""" + self.socket.send_pyobj(["open_kernel", kernel_spec]) + cmd, connection_file, connection_info = self.socket.recv_pyobj() + if connection_file == "error": + raise connection_info + + hostname, sshkey, password = None, None, None + + kernel_handler = KernelHandler.new_from_spec( + kernel_spec, connection_file, connection_info, + hostname, sshkey, password + ) + + kernel_handler.sig_request_close.connect(self.close_kernel) + return kernel_handler + + def close_kernel(self, connection_file): + self.socket.send_pyobj(["close_kernel", connection_file]) + # Wait for confirmation + self.socket.recv_pyobj() + + + class CachedKernelMixin: """Cached kernel mixin.""" - def __init__(self): super().__init__() self._cached_kernel_properties = None @@ -56,7 +89,7 @@ def check_cached_kernel_spec(self, kernel_spec): def get_cached_kernel(self, kernel_spec, cache=True): """Get a new kernel, and cache one for next time.""" # Cache another kernel for next time. - new_kernel_handler = KernelHandler.new_from_spec(kernel_spec) + new_kernel_handler = self.new_kernel(kernel_spec) if not cache: # remove/don't use cache if requested @@ -81,6 +114,6 @@ def get_cached_kernel(self, kernel_spec, cache=True): ) if cached_kernel_handler is None: - return KernelHandler.new_from_spec(kernel_spec) + return self.new_kernel(kernel_spec) return cached_kernel_handler diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py index 757e55d6b1d..053c197df24 100644 --- a/spyder/plugins/ipythonconsole/widgets/shell.py +++ b/spyder/plugins/ipythonconsole/widgets/shell.py @@ -150,7 +150,6 @@ def __init__(self, ipyclient, additional_options, interpreter_versions, self.set_bracket_matcher_color_scheme(self.syntax_style) self.shutting_down = False - self.kernel_manager = None self.kernel_client = None self._init_kernel_setup = False handlers.update({ @@ -197,7 +196,6 @@ def connect_kernel(self, kernel_handler, first_connect=True): kernel_client.stopped_channels.connect(self.notify_deleted) self.kernel_client = kernel_client - self.kernel_manager = kernel_handler.kernel_manager self.kernel_handler = kernel_handler if first_connect: @@ -254,7 +252,6 @@ def disconnect_kernel(self, shutdown_kernel=True, will_reconnect=True): self.reset_kernel_state() self.kernel_client = None - self.kernel_manager = None self.kernel_handler = None def handle_kernel_is_ready(self): @@ -332,7 +329,7 @@ def call_kernel(self, interrupt=False, blocking=False, callback=None, @property def is_external_kernel(self): """Check if this is an external kernel.""" - return self.kernel_manager is None + return self.kernel_handler.kernel_spec is None def setup_spyder_kernel(self): """Setup spyder kernel""" @@ -728,7 +725,7 @@ def _perform_reset(self, message): # kernels. # See spyder-ide/spyder#9505. try: - kernel_env = self.kernel_manager._kernel_spec.env + kernel_env = self.kernel_handler.kernel_spec.env except AttributeError: kernel_env = {} @@ -1098,7 +1095,7 @@ def _banner_default(self): def _kernel_restarted_message(self, died=True): msg = _("Kernel died, restarting") if died else _("Kernel restarting") - if died and self.kernel_manager is None: + if died and self.is_external_kernel: # The kernel might never restart, show position of fault file msg += ( "\n" + _("Its crash file is located at:") + " " diff --git a/spyder/plugins/maininterpreter/confpage.py b/spyder/plugins/maininterpreter/confpage.py index 0f4c3ff795d..4e5bbeda0ed 100644 --- a/spyder/plugins/maininterpreter/confpage.py +++ b/spyder/plugins/maininterpreter/confpage.py @@ -12,8 +12,11 @@ import sys # Third party imports -from qtpy.QtWidgets import (QButtonGroup, QGroupBox, QInputDialog, QLabel, - QLineEdit, QMessageBox, QPushButton, QVBoxLayout) +from qtpy.QtWidgets import ( + QButtonGroup, QGroupBox, QInputDialog, QLabel, + QLineEdit, QMessageBox, QPushButton, QVBoxLayout, QRadioButton, + QHBoxLayout, QGridLayout, QSpacerItem) +from qtpy.compat import getopenfilename # Local imports from spyder.api.translations import get_translation @@ -23,6 +26,7 @@ from spyder.utils.conda import get_list_conda_envs_cache from spyder.utils.misc import get_python_executable from spyder.utils.pyenv import get_list_pyenv_envs_cache +from spyder.config.base import get_home_dir # Localization _ = get_translation('spyder') @@ -66,6 +70,84 @@ def initialize(self): def setup_page(self): newcb = self.create_checkbox + + # Remote kernel groupbox + self.rm_group = QGroupBox(_("Use a remote kernel server (via SSH)")) + + # SSH connection + hn_label = QLabel(_('Hostname:')) + self.hn = QLineEdit() + pn_label = QLabel(_('Port:')) + self.pn = QLineEdit() + self.pn.setMaximumWidth(75) + + un_label = QLabel(_('Username:')) + self.un = QLineEdit() + + # SSH authentication + auth_group = QGroupBox(_("Authentication method:")) + self.pw_radio = QRadioButton() + pw_label = QLabel(_('Password:')) + self.kf_radio = QRadioButton() + kf_label = QLabel(_('SSH keyfile:')) + + self.pw = QLineEdit() + self.pw.setEchoMode(QLineEdit.Password) + self.pw_radio.toggled.connect(self.pw.setEnabled) + self.kf_radio.toggled.connect(self.pw.setDisabled) + + self.kf = QLineEdit() + kf_open_btn = QPushButton(_('Browse')) + kf_open_btn.clicked.connect(self.select_ssh_key) + kf_layout = QHBoxLayout() + kf_layout.addWidget(self.kf) + kf_layout.addWidget(kf_open_btn) + + kfp_label = QLabel(_('Passphase:')) + self.kfp = QLineEdit() + self.kfp.setPlaceholderText(_('Optional')) + self.kfp.setEchoMode(QLineEdit.Password) + + self.kf_radio.toggled.connect(self.kf.setEnabled) + self.kf_radio.toggled.connect(self.kfp.setEnabled) + self.kf_radio.toggled.connect(kf_open_btn.setEnabled) + self.kf_radio.toggled.connect(kfp_label.setEnabled) + self.pw_radio.toggled.connect(self.kf.setDisabled) + self.pw_radio.toggled.connect(self.kfp.setDisabled) + self.pw_radio.toggled.connect(kf_open_btn.setDisabled) + self.pw_radio.toggled.connect(kfp_label.setDisabled) + + # SSH layout + ssh_layout = QGridLayout() + ssh_layout.addWidget(hn_label, 0, 0, 1, 2) + ssh_layout.addWidget(self.hn, 0, 2) + ssh_layout.addWidget(pn_label, 0, 3) + ssh_layout.addWidget(self.pn, 0, 4) + ssh_layout.addWidget(un_label, 1, 0, 1, 2) + ssh_layout.addWidget(self.un, 1, 2, 1, 3) + + # SSH authentication layout + auth_layout = QGridLayout() + auth_layout.addWidget(self.pw_radio, 1, 0) + auth_layout.addWidget(pw_label, 1, 1) + auth_layout.addWidget(self.pw, 1, 2) + auth_layout.addWidget(self.kf_radio, 2, 0) + auth_layout.addWidget(kf_label, 2, 1) + auth_layout.addLayout(kf_layout, 2, 2) + auth_layout.addWidget(kfp_label, 3, 1) + auth_layout.addWidget(self.kfp, 3, 2) + auth_group.setLayout(auth_layout) + + # Remote kernel layout + rm_layout = QVBoxLayout() + rm_layout.addLayout(ssh_layout) + rm_layout.addSpacerItem(QSpacerItem(0, 8)) + rm_layout.addWidget(auth_group) + self.rm_group.setLayout(rm_layout) + self.rm_group.setCheckable(True) + self.rm_group.toggled.connect(self.pw_radio.setChecked) + + # Python executable Group pyexec_group = QGroupBox(_("Python interpreter")) pyexec_bg = QButtonGroup(pyexec_group) @@ -104,6 +186,7 @@ def setup_page(self): self.def_exec_radio.toggled.connect(self.cus_exec_combo.setDisabled) self.cus_exec_radio.toggled.connect(self.cus_exec_combo.setEnabled) pyexec_layout.addWidget(self.cus_exec_combo) + pyexec_layout.addWidget(self.rm_group) pyexec_group.setLayout(pyexec_layout) self.pyexec_edit = self.cus_exec_combo.combobox.lineEdit() @@ -159,6 +242,11 @@ def setup_page(self): vlayout.addStretch(1) self.setLayout(vlayout) + def select_ssh_key(self): + kf = getopenfilename(self, _('Select SSH keyfile'), + get_home_dir(), '*.pem;;*')[0] + self.kf.setText(kf) + def warn_python_compatibility(self, pyexec): if not osp.isfile(pyexec): return