Skip to content

Commit

Permalink
Backport PR #22860 on branch 6.x (PR: Check spyder-remote-services
Browse files Browse the repository at this point in the history
…version compatibility (Remote client)) (#23089)
  • Loading branch information
meeseeksmachine authored Nov 27, 2024
1 parent 6367a64 commit 1e40941
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 32 deletions.
7 changes: 7 additions & 0 deletions spyder/plugins/remoteclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@
Remote Client Plugin.
"""

# Required version of spyder-remote-services
SPYDER_REMOTE_MIN_VERSION = "0.1.3"
SPYDER_REMOTE_MAX_VERSION = '1.0.0'
SPYDER_REMOTE_VERSION = (
f'>={SPYDER_REMOTE_MIN_VERSION},<{SPYDER_REMOTE_MAX_VERSION}'
)
65 changes: 44 additions & 21 deletions spyder/plugins/remoteclient/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
import socket

import asyncssh
from packaging.version import Version

from spyder.api.translations import _
from spyder.config.base import get_debug_level
from spyder.plugins.remoteclient import (
SPYDER_REMOTE_MAX_VERSION,
SPYDER_REMOTE_MIN_VERSION,
)
from spyder.plugins.remoteclient.api.jupyterhub import JupyterAPI
from spyder.plugins.remoteclient.api.protocol import (
ConnectionInfo,
Expand All @@ -26,6 +31,7 @@
from spyder.plugins.remoteclient.api.ssh import SpyderSSHClient
from spyder.plugins.remoteclient.utils.installation import (
get_installer_command,
get_server_version_command,
SERVER_ENV,
)

Expand Down Expand Up @@ -99,6 +105,10 @@ def __emit_connection_status(self, status, message):
)
)

def __emit_version_mismatch(self, version: str):
if self._plugin is not None:
self._plugin.sig_version_mismatch.emit(self.config_id, version)

@property
def _api_token(self):
return self._server_info.get("token")
Expand Down Expand Up @@ -409,36 +419,47 @@ async def __start_remote_server(self):
return False

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."""
"""Check remote server version."""
if not self.ssh_is_connected:
self._logger.error("SSH connection is not open")
return False
return ""

commnad = get_server_version_command(self.options["platform"])

try:
await self._ssh_connection.run(
self.CHECK_SERVER_COMMAND, check=True
output = await self._ssh_connection.run(
commnad, check=True
)
except asyncssh.ProcessError as err:
self._logger.warning(
f"spyder-remote-server is not installed: {err.stderr}"
)
return False
# Server is not installed
self._logger.warning(f"Error checking server version: {err.stderr}")
return await self.install_remote_server()
except asyncssh.TimeoutError:
self._logger.error("Checking server version timed out")
return False

version = output.stdout.splitlines()[-1].strip()

if Version(version) >= Version(SPYDER_REMOTE_MAX_VERSION):
self._logger.error(
"Checking if spyder-remote-server is installed timed out"
f"Server version mismatch: {version} is greater than "
f"the maximum supported version {SPYDER_REMOTE_MAX_VERSION}"
)
self.__emit_version_mismatch(version)
self.__emit_connection_status(
status=ConnectionStatus.Error,
message=_("Error connecting to the remote server"),
)
return False

self._logger.debug(
f"spyder-remote-server is installed on {self.peer_host}"
)
if Version(version) < Version(SPYDER_REMOTE_MIN_VERSION):
self._logger.error(
f"Server version mismatch: {version} is lower than "
f"the minimum supported version {SPYDER_REMOTE_MIN_VERSION}"
)
return await self.install_remote_server()

self._logger.info(f"Supported Server version: {version}")

return True

Expand Down Expand Up @@ -468,8 +489,10 @@ async def __install_remote_server(self):
self._logger.debug(
f"Installing spyder-remote-server on {self.peer_host}"
)
command = get_installer_command(self.options["platform"])
if not command:

try:
command = get_installer_command(self.options["platform"])
except NotImplementedError as e:
self._logger.error(
f"Cannot install spyder-remote-server on "
f"{self.options['platform']} automatically. Please install it "
Expand Down
5 changes: 0 additions & 5 deletions spyder/plugins/remoteclient/api/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@
from asyncssh.auth import PasswordChangeResponse
from asyncssh.public_key import KeyPairListArg

from spyder.api.translations import _
from spyder.plugins.remoteclient.api.protocol import (
ConnectionInfo,
ConnectionStatus,
)

_logger = logging.getLogger(__name__)

Expand Down
5 changes: 5 additions & 0 deletions spyder/plugins/remoteclient/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class RemoteClient(SpyderPluginV2):
sig_connection_lost = Signal(str)
sig_connection_status_changed = Signal(dict)

sig_version_mismatch = Signal(str, str)

_sig_kernel_started = Signal(object, dict)

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -112,6 +114,9 @@ def on_initialize(self):
self.sig_client_message_logged.connect(
container.sig_client_message_logged
)
self.sig_version_mismatch.connect(
container.on_server_version_mismatch
)
self._sig_kernel_started.connect(container.on_kernel_started)

def on_first_registration(self):
Expand Down
36 changes: 36 additions & 0 deletions spyder/plugins/remoteclient/tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Third party imports
import pytest
from flaky import flaky
from qtpy.QtWidgets import QMessageBox

# Local imports
from spyder.plugins.remoteclient.plugin import RemoteClient
Expand Down Expand Up @@ -166,5 +167,40 @@ def test_restart_server(
)


class TestVersionCheck:
def test_wrong_version(
self,
remote_client: RemoteClient,
remote_client_id: str,
monkeypatch,
qtbot,
):
monkeypatch.setattr(
"spyder.plugins.remoteclient.api.client.SPYDER_REMOTE_MAX_VERSION",
"0.0.1",
)
monkeypatch.setattr(
"spyder.plugins.remoteclient.widgets.container.SPYDER_REMOTE_MAX_VERSION",
"0.0.1",
)

def mock_critical(parent, title, text, buttons):
assert "spyder-remote-services" in text
assert "0.0.1" in text
assert "is newer than" in text
return QMessageBox.Ok

monkeypatch.setattr(
"spyder.plugins.remoteclient.widgets.container.QMessageBox.critical",
mock_critical,
)

with qtbot.waitSignal(
remote_client.sig_version_mismatch,
timeout=180000,
):
remote_client.start_remote_server(remote_client_id)


if __name__ == "__main__":
pytest.main()
16 changes: 10 additions & 6 deletions spyder/plugins/remoteclient/utils/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@

from spyder.plugins.ipythonconsole import SPYDER_KERNELS_VERSION
from spyder.config.base import running_remoteclient_tests
from spyder.plugins.remoteclient import SPYDER_REMOTE_VERSION


SERVER_ENTRY_POINT = "spyder-server"
SERVER_ENV = "spyder-remote"
PACKAGE_NAME = "spyder-remote-services"
PACKAGE_VERSION = "0.1.3"

ENCODING = "utf-8"

SCRIPT_URL = (
f"https://raw.githubusercontent.com/spyder-ide/{PACKAGE_NAME}/master/scripts"
)


def get_installer_command(platform: str) -> str:
if platform == "win":
raise NotImplementedError("Windows is not supported yet")
Expand All @@ -28,5 +25,12 @@ def get_installer_command(platform: str) -> str:

return (
f'"${{SHELL}}" <(curl -L {SCRIPT_URL}/installer.sh) '
f'"{PACKAGE_VERSION}" "{SPYDER_KERNELS_VERSION}"'
f'"{SPYDER_REMOTE_VERSION}" "{SPYDER_KERNELS_VERSION}"'
)


def get_server_version_command(platform: str) -> str:
return (
f"${{HOME}}/.local/bin/micromamba run -n {SERVER_ENV} python -c "
"'import spyder_remote_services as sprs; print(sprs.__version__)'"
)
28 changes: 28 additions & 0 deletions spyder/plugins/remoteclient/widgets/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

# Third-party imports
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QMessageBox

# Local imports
from spyder.api.translations import _
from spyder.api.widgets.main_container import PluginMainContainer
from spyder.plugins.ipythonconsole.utils.kernel_handler import KernelHandler
from spyder.plugins.remoteclient import SPYDER_REMOTE_MAX_VERSION
from spyder.plugins.remoteclient.api import (
MAX_CLIENT_MESSAGES,
RemoteClientActions,
Expand Down Expand Up @@ -262,6 +264,32 @@ def on_kernel_started(self, ipyclient, kernel_info):
# Connect client to the kernel
ipyclient.connect_kernel(kernel_handler)

def on_server_version_mismatch(self, config_id, version: str):
"""
Actions to take when there's a mismatch between the
spyder-remote-services version installed in the server and the one
supported by Spyder.
"""
auth_method = self.get_conf(f"{config_id}/auth_method")
server_name = self.get_conf(f"{config_id}/{auth_method}/name")

QMessageBox.critical(
self,
_("Remote server error"),
_(
"The version of <tt>spyder-remote-services</tt> on the "
"remote host <b>{server}</b> (<b>{srs_version}</b>) is newer "
"than the latest Spyder supports (<b>{max_version}</b>)."
"<br><br>"
"Please update Spyder to be able to connect to this host."
).format(
server=server_name,
srs_version=version,
max_version=SPYDER_REMOTE_MAX_VERSION,
),
QMessageBox.Ok,
)

# ---- Private API
# -------------------------------------------------------------------------
def _show_connection_dialog(self):
Expand Down

0 comments on commit 1e40941

Please sign in to comment.