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: Check spyder-remote-services version compatibility (Remote client) #22860

Merged
merged 7 commits into from
Nov 27, 2024
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
Loading