Skip to content

Commit

Permalink
Import SSH-related from datalad-ria
Browse files Browse the repository at this point in the history
These have been developed months ago, and fix critical aspects of SSH
execution on windows clients that prevent any RIA functionality to be
working on windows. Given the uncertainty of a datalad-ria release,
I am importing them here.

Hopefully fixes datalad#654
  • Loading branch information
mih committed Apr 17, 2024
1 parent 11ab3a1 commit 0a2a2b4
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 0 deletions.
4 changes: 4 additions & 0 deletions datalad_next/patches/enabled.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
run,
update,
ora_remote,
# the following patches have been taken verbatim from datalad-ria
ssh_exec,
sshremoteio,
sshconnector,
)
65 changes: 65 additions & 0 deletions datalad_next/patches/ssh_exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Enable SSH-based remote command execution on Windows
This change introduces a replacement for core's
``datalad/support/sshconnector.py:BaseSSHConnection._exec_ssh()``
with a dedicated handling of ``stdin`` for Windows.
The OpenSSH client in Windows modifies its ``stdin``-descriptor in such a way, that it becomes unusable for the python process, if the ``stdin``-descriptor is shared between the ``python``-process and the ``ssh``-process. As a result, all read-operations that the ``python``-process performs on ``stdin`` will block and leave the python-process "hanging".
This change passes an explicit, empty, byte-string as ``stdin`` to the
SSH client call, in order to avoid any interaction of SSH with the
parent process's ``stdin`` descriptor.
"""

import logging

from datalad.support.sshconnector import (
StdOutErrCapture,
NoCapture,
)

from datalad_next.patches import apply_patch
from datalad_next.utils import on_windows

# use same logger as -core
lgr = logging.getLogger('datalad.support.sshconnector')


# This method interface/original implementation is taken from
# datalad-core@58b8e06317fe1a03290aed80526bff1e2d5b7797
# datalad/support/sshconnector.py:BaseSSHConnection
def _exec_ssh(self, ssh_cmd, cmd, options=None, stdin=None, log_output=True):
cmd = self._adjust_cmd_for_bundle_execution(cmd)

for opt in options or []:
ssh_cmd.extend(["-o", opt])

# THIS IS THE PATCH
if on_windows and stdin is None:
# SSH on windows requires a special stdin handling. If we'd let
# stdin=None do its normal thing, the Python process would hang,
# because it looses touch with its own file descriptor.
# See https://github.com/datalad/datalad-ria/issues/68
stdin = b''

# build SSH call, feed remote command as a single last argument
# whatever it contains will go to the remote machine for execution
# we cannot perform any sort of escaping, because it will limit
# what we can do on the remote, e.g. concatenate commands with '&&'
ssh_cmd += [self.sshri.as_str()] + [cmd]

lgr.debug("%s is used to run %s", self, ssh_cmd)

# TODO: pass expect parameters from above?
# Hard to explain to toplevel users ... So for now, just set True
out = self.runner.run(
ssh_cmd,
protocol=StdOutErrCapture if log_output else NoCapture,
stdin=stdin)
return out['stdout'], out['stderr']


apply_patch(
'datalad.support.sshconnector', 'BaseSSHConnection', '_exec_ssh',
_exec_ssh,
)
124 changes: 124 additions & 0 deletions datalad_next/patches/sshconnector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Provide proper arguments for scp-command calls in `SSHConnection`
The original code has errors in the methods ``BaseSSHConnection.put``
``BaseSSHConnection.get``. Both methods use ``self.sshri.hostname`` to
determine the target for an ``scp``-command. They should instead use
``self.sshri.as_str()`` in order to include a user specification into the
target.
The changes in this patch use ``self.sshri.as_str()`` to provide the correct
targets for ``scp``-commands.
"""

import logging

from datalad.support.sshconnector import (
StdOutErrCapture,
ensure_list,
)
from datalad_next.patches import apply_patch


# use same logger as -core
lgr = logging.getLogger('datalad.support.sshconnector')


# The method 'BaseSSHConnection_get' is a patched version of
# 'datalad/support/sshconnector.py:BaseSSHConnection.get'
# from datalad@e0b357d9b8ca5f432638c23c0cb7c373028c8e52
def BaseSSHConnection_get(self, source, destination, recursive=False, preserve_attrs=False):
"""Copies source file/folder from remote to a local destination.
Note: this method performs escaping of filenames to an extent that
moderately weird ones should work (spaces, quotes, pipes, other
characters with special shell meaning), but more complicated cases
might require appropriate external preprocessing of filenames.
Parameters
----------
source : str or list
file/folder path(s) to copy from the remote host
destination : str
file/folder path to copy to on the local host
recursive : bool
flag to enable recursive copying of given sources
preserve_attrs : bool
preserve modification times, access times, and modes from the
original file
Returns
-------
str
stdout, stderr of the copy operation.
"""
# make sure we have an open connection, will test if action is needed
# by itself
self.open()
scp_cmd = self._get_scp_command_spec(recursive, preserve_attrs)
# add source filepath(s) to scp command, prefixed with the remote host
# PATCH in the line below: replaces `self.sshri.hostname` with `self.sshri.as_str()`
scp_cmd += ["%s:%s" % (self.sshri.as_str(), self._quote_filename(s))
for s in ensure_list(source)]
# add destination path
scp_cmd += [destination]
out = self.runner.run(scp_cmd, protocol=StdOutErrCapture)
return out['stdout'], out['stderr']


# The method 'BaseSSHConnection_put' is a patched version of
# 'datalad/support/sshconnector.py:BaseSSHConnection.put'
# from datalad@e0b357d9b8ca5f432638c23c0cb7c373028c8e52
def BaseSSHConnection_put(self, source, destination, recursive=False, preserve_attrs=False):
"""Copies source file/folder to destination on the remote.
Note: this method performs escaping of filenames to an extent that
moderately weird ones should work (spaces, quotes, pipes, other
characters with special shell meaning), but more complicated cases
might require appropriate external preprocessing of filenames.
Parameters
----------
source : str or list
file/folder path(s) to copy from on local
destination : str
file/folder path to copy to on remote
recursive : bool
flag to enable recursive copying of given sources
preserve_attrs : bool
preserve modification times, access times, and modes from the
original file
Returns
-------
str
stdout, stderr of the copy operation.
"""
# make sure we have an open connection, will test if action is needed
# by itself
self.open()
scp_cmd = self._get_scp_command_spec(recursive, preserve_attrs)
# add source filepath(s) to scp command
scp_cmd += ensure_list(source)
# add destination path
scp_cmd += ['%s:%s' % (
# PATCH in the line below: replaces `self.sshri.hostname` with `self.sshri.as_str()`
self.sshri.as_str(),
self._quote_filename(destination),
)]
out = self.runner.run(scp_cmd, protocol=StdOutErrCapture)
return out['stdout'], out['stderr']


apply_patch(
modname='datalad.support.sshconnector',
objname='BaseSSHConnection',
attrname='get',
patch=BaseSSHConnection_get,
)

apply_patch(
modname='datalad.support.sshconnector',
objname='BaseSSHConnection',
attrname='put',
patch=BaseSSHConnection_put,
)
Loading

0 comments on commit 0a2a2b4

Please sign in to comment.