Hello community,
here is the log from the commit of package python-asyncssh for openSUSE:Factory
checked in at 2020-07-10 14:13:14
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
and /work/SRC/openSUSE:Factory/.python-asyncssh.new.3060 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asyncssh"
Fri Jul 10 14:13:14 2020 rev:12 rq:819833 version:2.2.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes
2020-03-03 10:20:27.615174225 +0100
+++
/work/SRC/openSUSE:Factory/.python-asyncssh.new.3060/python-asyncssh.changes
2020-07-10 14:13:19.499616871 +0200
@@ -1,0 +2,16 @@
+Thu Jul 9 22:36:54 UTC 2020 - Ondřej Súkup <[email protected]>
+
+- update to 2.2.1
+ * Added optional timeout parameter to SSHClientProcess.wait()
+ and SSHClientConnection.run() methods.
+ * Created subclasses for SFTPError exceptions, allowing applications
+ to more easily have distinct exception handling for different errors.
+ * Fixed an issue in SFTP parallel I/O related to handling low-level
+ connection failures
+ * Fixed an issue with SFTP file copy where a local file could sometimes
+ be left open if an attempt to close a remote file failed.
+ * Fixed an issue in the handling of boolean return values when
+ SSHServer.server_requested() returns a coroutine
+ * Fixed an issue with passing tuples to the SFTP copy functions.
+
+-------------------------------------------------------------------
Old:
----
asyncssh-2.2.0.tar.gz
New:
----
asyncssh-2.2.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.S2Y6Wi/_old 2020-07-10 14:13:20.379619761 +0200
+++ /var/tmp/diff_new_pack.S2Y6Wi/_new 2020-07-10 14:13:20.383619774 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-asyncssh
-Version: 2.2.0
+Version: 2.2.1
Release: 0
Summary: Asynchronous SSHv2 client and server library
License: EPL-2.0 OR GPL-2.0-or-later
++++++ asyncssh-2.2.0.tar.gz -> asyncssh-2.2.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/PKG-INFO new/asyncssh-2.2.1/PKG-INFO
--- old/asyncssh-2.2.0/PKG-INFO 2020-03-01 00:59:29.000000000 +0100
+++ new/asyncssh-2.2.1/PKG-INFO 2020-04-18 19:20:47.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 2.2.0
+Version: 2.2.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/__init__.py
new/asyncssh-2.2.1/asyncssh/__init__.py
--- old/asyncssh-2.2.0/asyncssh/__init__.py 2020-03-01 00:16:54.000000000
+0100
+++ new/asyncssh-2.2.1/asyncssh/__init__.py 2020-04-18 18:36:42.000000000
+0200
@@ -65,6 +65,7 @@
from .process import SSHClientProcess, SSHServerProcess
from .process import SSHCompletedProcess, ProcessError
+from .process import TimeoutError # pylint: disable=redefined-builtin
from .process import DEVNULL, PIPE, STDOUT
from .public_key import SSHKey, SSHKeyPair, SSHCertificate
@@ -85,6 +86,9 @@
from .server import SSHServer
from .sftp import SFTPClient, SFTPClientFile, SFTPServer, SFTPError
+from .sftp import SFTPEOFError, SFTPNoSuchFile, SFTPPermissionDenied
+from .sftp import SFTPFailure, SFTPBadMessage, SFTPNoConnection
+from .sftp import SFTPConnectionLost, SFTPOpUnsupported
from .sftp import SFTPAttrs, SFTPVFSAttrs, SFTPName
from .sftp import SEEK_SET, SEEK_CUR, SEEK_END
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/connection.py
new/asyncssh-2.2.1/asyncssh/connection.py
--- old/asyncssh-2.2.0/asyncssh/connection.py 2020-03-01 00:16:54.000000000
+0100
+++ new/asyncssh-2.2.1/asyncssh/connection.py 2020-04-18 18:36:42.000000000
+0200
@@ -3114,7 +3114,7 @@
return transport, transport.get_protocol()
# pylint: enable=redefined-builtin
- async def run(self, *args, check=False, **kwargs):
+ async def run(self, *args, check=False, timeout=None, **kwargs):
"""Run a command on the remote system and collect its output
This method is a coroutine wrapper around :meth:`create_process`
@@ -3133,21 +3133,30 @@
In addition to the argument below, all arguments to
:meth:`create_process` are supported and have the same meaning.
+ If a timeout is specified and it expires before the process
+ exits, the :exc:`TimeoutError` exception will be raised. By
+ default, no timeout is set and this call will wait indefinitely.
+
:param check: (optional)
Whether or not to raise :exc:`ProcessError` when a non-zero
exit status is returned
+ :param timeout:
+ Amount of time in seconds to wait for process to exit or
+ `None` to wait indefinitely
:type check: `bool`
+ :type timeout: `int`, `float`, or `None`
:returns: :class:`SSHCompletedProcess`
:raises: | :exc:`ChannelOpenError` if the session can't be opened
| :exc:`ProcessError` if checking non-zero exit status
+ | :exc:`TimeoutError` if the timeout expires before exit
"""
process = await self.create_process(*args, **kwargs)
- return await process.wait(check)
+ return await process.wait(check, timeout)
async def create_connection(self, session_factory, remote_host,
remote_port,
orig_host='', orig_port=0, *, encoding=None,
@@ -4343,17 +4352,6 @@
result = self._owner.server_requested(listen_host, listen_port)
- if not result:
- self.logger.info('Request for TCP listener on %s denied by '
- 'application', (listen_host, listen_port))
-
- self._report_global_response(False)
- return
-
- if result is True:
- result = self.forward_local_port(listen_host, listen_port,
- listen_host, listen_port)
-
self.create_task(self._finish_port_forward(result, listen_host,
listen_port))
@@ -4364,21 +4362,33 @@
if inspect.isawaitable(listener):
listener = await listener
- if listen_port == 0:
- listen_port = listener.get_port()
- result = UInt32(listen_port)
- else:
- result = True
-
- self._local_listeners[listen_host, listen_port] = listener
-
- self.logger.info('Created TCP listener on %s',
- (listen_host, listen_port))
-
- self._report_global_response(result)
+ if listener is True:
+ listener = await self.forward_local_port(
+ listen_host, listen_port, listen_host, listen_port)
except OSError:
self.logger.debug1('Failed to create TCP listener')
self._report_global_response(False)
+ return
+
+ if not listener:
+ self.logger.info('Request for TCP listener on %s denied by '
+ 'application', (listen_host, listen_port))
+
+ self._report_global_response(False)
+ return
+
+ if listen_port == 0:
+ listen_port = listener.get_port()
+ result = UInt32(listen_port)
+ else:
+ result = True
+
+ self._local_listeners[listen_host, listen_port] = listener
+
+ self.logger.info('Created TCP listener on %s',
+ (listen_host, listen_port))
+
+ self._report_global_response(result)
def _process_cancel_tcpip_forward_global_request(self, packet):
"""Process a request to cancel TCP/IP port forwarding"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/misc.py
new/asyncssh-2.2.1/asyncssh/misc.py
--- old/asyncssh-2.2.0/asyncssh/misc.py 2020-03-01 00:16:54.000000000 +0100
+++ new/asyncssh-2.2.1/asyncssh/misc.py 2020-04-18 18:36:42.000000000 +0200
@@ -622,7 +622,7 @@
def construct_disc_error(code, reason, lang):
- """Map discussion error code to appropriate DisconnectError exception"""
+ """Map disconnect error code to appropriate DisconnectError exception"""
try:
return _disc_error_map[code](reason, lang)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/process.py
new/asyncssh-2.2.1/asyncssh/process.py
--- old/asyncssh-2.2.0/asyncssh/process.py 2019-11-30 19:27:24.000000000
+0100
+++ new/asyncssh-2.2.1/asyncssh/process.py 2020-04-18 18:36:42.000000000
+0200
@@ -454,7 +454,7 @@
process to execute (if any)
subsystem The subsystem the client requested the `str` or `None`
process to open (if any)
- exit_status The exit status returned, or -1 if an `int`
+ exit_status The exit status returned, or -1 if an `int` or `None`
exit signal is sent
exit_signal The exit signal sent (if any) in the `tuple` or `None`
form of a tuple containing the signal
@@ -462,7 +462,7 @@
occurred, a message associated with the
signal, and the language the message
was in
- returncode The exit status returned, or negative `int`
+ returncode The exit status returned, or negative `int` or `None`
of the signal number when an exit
signal is sent
stdout The output sent by the process to `str` or `bytes`
@@ -474,7 +474,8 @@
"""
def __init__(self, env, command, subsystem, exit_status,
- exit_signal, returncode, stdout, stderr):
+ exit_signal, returncode, stdout, stderr,
+ reason='', lang=DEFAULT_LANG):
self.env = env
self.command = command
self.subsystem = subsystem
@@ -489,14 +490,30 @@
reason = 'Process exited with signal %s%s%s' % \
(signal, ': ' + msg if msg else '',
' (core dumped)' if core_dumped else '')
- else:
+ elif exit_status:
reason = 'Process exited with non-zero exit status %s' % \
exit_status
- lang = DEFAULT_LANG
super().__init__(exit_status, reason, lang)
+# pylint: disable=redefined-builtin
+class TimeoutError(ProcessError, asyncio.TimeoutError):
+ """SSH Process timeout error
+
+ This exception is raised when a timeout occurs when calling the
+ :meth:`wait <SSHClientProcess.wait>` method on :class:`SSHClientProcess`
+ or the :meth:`run <SSHClientConnection.run>` method on
+ :class:`SSHClientConnection`. It is a subclass of :class:`ProcessError`
+ and contains all of the fields documented there, including any output
+ received on stdout and stderr prior to when the timeout occurred. It
+ is also a subclass of :class:`asyncio.TimeoutError`, for code that
+ might be expecting that.
+
+ """
+# pylint: enable=redefined-builtin
+
+
class SSHCompletedProcess(Record):
"""Results from running an SSH process
@@ -1168,7 +1185,7 @@
self._chan.kill()
- async def wait(self, check=False):
+ async def wait(self, check=False, timeout=None):
"""Wait for process to exit
This method is a coroutine which waits for the process to
@@ -1180,18 +1197,37 @@
status from the process with trigger the :exc:`ProcessError`
exception to be raised.
+ If a timeout is specified and it expires before the process
+ exits, the :exc:`TimeoutError` exception will be raised. By
+ default, no timeout is set and this call will wait indefinitely.
+
:param check:
Whether or not to raise an error on non-zero exit status
+ :param timeout:
+ Amount of time in seconds to wait for process to exit, or
+ `None` to wait indefinitely
:type check: `bool`
+ :type timeout: `int`, `float`, or `None`
:returns: :class:`SSHCompletedProcess`
- :raises: :exc:`ProcessError` if check is set to `True`
- and the process returns a non-zero exit status
+ :raises: | :exc:`ProcessError` if check is set to `True`
+ and the process returns a non-zero exit status
+ | :exc:`TimeoutError` if the timeout expires
+ before the process exits
"""
- stdout_data, stderr_data = await self.communicate()
+ try:
+ stdout_data, stderr_data = \
+ await asyncio.wait_for(self.communicate(), timeout)
+ except asyncio.TimeoutError:
+ stdout_data, stderr_data = self.collect_output()
+
+ raise TimeoutError(self.env, self.command, self.subsystem,
+ self.exit_status, self.exit_signal,
+ self.returncode, stdout_data,
+ stderr_data) from None
if check and self.exit_status:
raise ProcessError(self.env, self.command, self.subsystem,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/scp.py
new/asyncssh-2.2.1/asyncssh/scp.py
--- old/asyncssh-2.2.0/asyncssh/scp.py 2020-03-01 00:16:54.000000000 +0100
+++ new/asyncssh-2.2.1/asyncssh/scp.py 2020-04-18 18:36:42.000000000 +0200
@@ -31,12 +31,34 @@
import sys
from .constants import DEFAULT_LANG
-from .constants import FX_BAD_MESSAGE, FX_CONNECTION_LOST, FX_FAILURE
from .misc import plural
from .sftp import LocalFile, match_glob
-from .sftp import SFTP_BLOCK_SIZE, SFTPAttrs, SFTPError, SFTPServerFile
+from .sftp import SFTP_BLOCK_SIZE, SFTPAttrs, SFTPServerFile
+from .sftp import SFTPError, SFTPFailure, SFTPBadMessage, SFTPConnectionLost
+
+
+def _scp_error(exc_class, reason, path=None, fatal=False,
+ suppress_send=False, lang=DEFAULT_LANG):
+ """Construct SCP version of SFTPError exception"""
+
+ if isinstance(reason, bytes):
+ reason = reason.decode('utf-8', errors='replace')
+
+ if isinstance(path, bytes):
+ path = path.decode('utf-8', errors='replace')
+
+ if path:
+ reason = reason + ': ' + path
+
+ exc = exc_class(reason, lang)
+
+ # pylint: disable=attribute-defined-outside-init
+ exc.fatal = fatal
+ exc.suppress_send = suppress_send
+
+ return exc
def _parse_cd_args(args):
@@ -46,7 +68,8 @@
permissions, size, name = args.split(None, 2)
return int(permissions, 8), int(size), name
except ValueError:
- raise SCPError(FX_BAD_MESSAGE, 'Invalid copy or dir request') from None
+ raise _scp_error(SFTPBadMessage,
+ 'Invalid copy or dir request') from None
def _parse_t_args(args):
@@ -56,7 +79,7 @@
mtime, _, atime, _ = args.split()
return int(atime), int(mtime)
except ValueError:
- raise SCPError(FX_BAD_MESSAGE, 'Invalid time request') from None
+ raise _scp_error(SFTPBadMessage, 'Invalid time request') from None
async def _parse_path(path, **kwargs):
@@ -115,25 +138,6 @@
return reader, writer
-class SCPError(SFTPError):
- """SCP error"""
-
- def __init__(self, code, reason, path=None, fatal=False,
- suppress_send=False, lang=DEFAULT_LANG):
- if isinstance(reason, bytes):
- reason = reason.decode('utf-8', errors='replace')
-
- if isinstance(path, bytes):
- path = path.decode('utf-8', errors='replace')
-
- if path:
- reason = reason + ': ' + path
-
- super().__init__(code, reason, lang)
- self.fatal = fatal
- self.suppress_send = suppress_send
-
-
class _SCPArgParser(argparse.ArgumentParser):
"""A parser for SCP arguments"""
@@ -185,14 +189,14 @@
reason = await self._reader.readline()
if not result or not reason.endswith(b'\n'):
- raise SCPError(FX_CONNECTION_LOST, 'Connection lost',
- fatal=True, suppress_send=True)
+ raise _scp_error(SFTPConnectionLost, 'Connection lost',
+ fatal=True, suppress_send=True)
if result not in b'\x01\x02':
reason = result + reason
- return SCPError(FX_FAILURE, reason[:-1], fatal=result != b'\x01',
- suppress_send=True)
+ return _scp_error(SFTPFailure, reason[:-1], fatal=result !=
b'\x01',
+ suppress_send=True)
self.logger.debug1('Received SCP OK')
@@ -288,8 +292,8 @@
"""Handle an SCP error"""
if isinstance(exc, BrokenPipeError):
- exc = SCPError(FX_CONNECTION_LOST, 'Connection lost',
- fatal=True, suppress_send=True)
+ exc = _scp_error(SFTPConnectionLost, 'Connection lost',
+ fatal=True, suppress_send=True)
if not getattr(exc, 'suppress_send', False):
self.send_error(exc)
@@ -362,7 +366,7 @@
data = await file_obj.read(blocklen, offset)
if not data:
- raise SCPError(FX_FAILURE, 'Unexpected EOF')
+ raise _scp_error(SFTPFailure, 'Unexpected EOF')
except (OSError, SFTPError) as exc:
local_exc = exc
@@ -418,7 +422,7 @@
elif stat.S_ISREG(attrs.permissions):
await self._send_file(srcpath, dstpath, attrs)
else:
- raise SCPError(FX_FAILURE, 'Not a regular file', srcpath)
+ raise _scp_error(SFTPFailure, 'Not a regular file', srcpath)
except (OSError, SFTPError, ValueError) as exc:
self.handle_error(exc)
@@ -474,8 +478,8 @@
data = await self.recv_data(blocklen)
if not data:
- raise SCPError(FX_CONNECTION_LOST, 'Connection lost',
- fatal=True, suppress_send=True)
+ raise _scp_error(SFTPConnectionLost, 'Connection lost',
+ fatal=True, suppress_send=True)
if not local_exc:
try:
@@ -507,13 +511,14 @@
"""Receive a directory over SCP"""
if not self._recurse:
- raise SCPError(FX_BAD_MESSAGE, 'Directory received without
recurse')
+ raise _scp_error(SFTPBadMessage,
+ 'Directory received without recurse')
self.logger.info(' Starting receive of directory %s', dstpath)
if await self._fs.exists(dstpath):
if not await self._fs.isdir(dstpath):
- raise SCPError(FX_FAILURE, 'Not a directory', dstpath)
+ raise _scp_error(SFTPFailure, 'Not a directory', dstpath)
else:
await self._fs.mkdir(dstpath)
@@ -536,8 +541,8 @@
try:
if action in b'\x01\x02':
- raise SCPError(FX_FAILURE, args, fatal=action != b'\x01',
- suppress_send=True)
+ raise _scp_error(SFTPFailure, args, fatal=action !=
b'\x01',
+ suppress_send=True)
elif action == b'T':
if self._preserve:
attrs.atime, attrs.mtime = _parse_t_args(args)
@@ -569,7 +574,7 @@
finally:
attrs = SFTPAttrs()
else:
- raise SCPError(FX_BAD_MESSAGE, 'Unknown request')
+ raise _scp_error(SFTPBadMessage, 'Unknown request')
except (OSError, SFTPError) as exc:
self.handle_error(exc)
@@ -581,8 +586,8 @@
dstpath = dstpath.encode('utf-8')
if self._must_be_dir and not await self._fs.isdir(dstpath):
- self.handle_error(SCPError(FX_FAILURE, 'Not a directory',
- dstpath))
+ self.handle_error(_scp_error(SFTPFailure, 'Not a directory',
+ dstpath))
else:
await self._recv_files(b'', dstpath)
except (OSError, SFTPError, ValueError) as exc:
@@ -614,7 +619,7 @@
"""Handle an SCP error"""
if isinstance(exc, BrokenPipeError):
- exc = SCPError(FX_CONNECTION_LOST, 'Connection lost', fatal=True)
+ exc = _scp_error(SFTPConnectionLost, 'Connection lost', fatal=True)
self.logger.debug1('Handling SCP error: %s', exc)
@@ -652,8 +657,8 @@
data = await self._source.recv_data(blocklen)
if not data:
- raise SCPError(FX_CONNECTION_LOST, 'Connection lost',
- fatal=True)
+ raise _scp_error(SFTPConnectionLost, 'Connection lost',
+ fatal=True)
await self._sink.send_data(data)
offset += len(data)
@@ -690,7 +695,7 @@
self._sink.send_request(action, args)
if action in b'\x01\x02':
- exc = SCPError(FX_FAILURE, args, fatal=action != b'\x01')
+ exc = _scp_error(SFTPFailure, args, fatal=action != b'\x01')
self._handle_error(exc)
continue
@@ -729,7 +734,7 @@
elif action == b'T':
attrs.atime, attrs.mtime = _parse_t_args(args)
else:
- raise SCPError(FX_BAD_MESSAGE, 'Unknown SCP action')
+ raise _scp_error(SFTPBadMessage, 'Unknown SCP action')
async def run(self):
"""Start SCP remote-to-remote transfer"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/server.py
new/asyncssh-2.2.1/asyncssh/server.py
--- old/asyncssh-2.2.0/asyncssh/server.py 2019-10-27 01:29:42.000000000
+0200
+++ new/asyncssh-2.2.1/asyncssh/server.py 2020-04-18 18:36:42.000000000
+0200
@@ -783,11 +783,10 @@
:returns: One of the following:
- * An :class:`SSHListener` object or a coroutine
- which returns an :class:`SSHListener` or `False`
- if the listener can't be opened
+ * An :class:`SSHListener` object
* `True` to set up standard port forwarding
* `False` to reject the request
+ * A coroutine object which returns one of the above
"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/sftp.py
new/asyncssh-2.2.1/asyncssh/sftp.py
--- old/asyncssh-2.2.0/asyncssh/sftp.py 2019-11-30 19:27:24.000000000 +0100
+++ new/asyncssh-2.2.1/asyncssh/sftp.py 2020-04-18 18:36:42.000000000 +0200
@@ -139,7 +139,7 @@
try:
os.chown(path, attrs.uid, attrs.gid)
except AttributeError: # pragma: no cover
- raise NotImplementedError
+ raise NotImplementedError from None
if attrs.permissions is not None:
os.chmod(path, stat.S_IMODE(attrs.permissions))
@@ -230,7 +230,7 @@
await _glob(fs, basedir, patlist, names)
if not names:
- raise SFTPError(FX_NO_SUCH_FILE, 'No matches found')
+ raise SFTPNoSuchFile('No matches found')
else:
await fs.stat(pattern)
names.append(pattern)
@@ -241,7 +241,7 @@
if error_handler:
error_handler(exc)
else:
- raise exc
+ raise
return names
@@ -433,7 +433,7 @@
for task in done:
exc = task.exception()
- if exc and exc.code != FX_EOF:
+ if exc and not isinstance(exc, SFTPEOFError):
exceptions.append(exc)
if exceptions:
@@ -541,7 +541,7 @@
data = await self._src.read(size, offset)
if not data:
- exc = SFTPError(FX_FAILURE, 'Unexpected EOF during file copy')
+ exc = SFTPFailure('Unexpected EOF during file copy')
# pylint: disable=attribute-defined-outside-init
exc.filename = self._srcpath
@@ -564,11 +564,12 @@
async def cleanup(self):
"""Clean up parallel copy"""
- if self._src: # pragma: no branch
- await self._src.close()
-
- if self._dst: # pragma: no branch
- await self._dst.close()
+ try:
+ if self._src: # pragma: no branch
+ await self._src.close()
+ finally:
+ if self._dst: # pragma: no branch
+ await self._dst.close()
class SFTPError(Error):
@@ -583,7 +584,7 @@
codes <DisconnectReasons>`
:param reason:
A human-readable reason for the disconnect
- :param lang:
+ :param lang: (optional)
The language the reason is in
:type code: `int`
:type reason: `str`
@@ -592,6 +593,177 @@
"""
+class SFTPEOFError(SFTPError):
+ """SFTP EOF error
+
+ This exception is raised when end of file is reached when
+ reading a file or directory.
+
+ :param reason: (optional)
+ Details about the EOF
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason='', lang=DEFAULT_LANG):
+ super().__init__(FX_EOF, reason, lang)
+
+
+class SFTPNoSuchFile(SFTPError):
+ """SFTP no such file
+
+ This exception is raised when the requested file is not found.
+
+ :param reason:
+ Details about the missing file
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_NO_SUCH_FILE, reason, lang)
+
+
+class SFTPPermissionDenied(SFTPError):
+ """SFTP permission denied
+
+ This exception is raised when the permissions are not available
+ to perform the requested operation.
+
+ :param reason:
+ Details about the invalid permissions
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_PERMISSION_DENIED, reason, lang)
+
+
+class SFTPFailure(SFTPError):
+ """SFTP failure
+
+ This exception is raised when an unexpected SFTP failure occurs.
+
+ :param reason:
+ Details about the failure
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_FAILURE, reason, lang)
+
+
+class SFTPBadMessage(SFTPError):
+ """SFTP bad message
+
+ This exception is raised when an invalid SFTP message is
+ received.
+
+ :param reason:
+ Details about the invalid message
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_BAD_MESSAGE, reason, lang)
+
+
+class SFTPNoConnection(SFTPError):
+ """SFTP no connection
+
+ This exception is raised when an SFTP request is made on a
+ closed SSH connection.
+
+ :param reason:
+ Details about the closed connection
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_NO_CONNECTION, reason, lang)
+
+
+class SFTPConnectionLost(SFTPError):
+ """SFTP connection lost
+
+ This exception is raised when the SSH connection is lost or
+ closed while making an SFTP request.
+
+ :param reason:
+ Details about the connection failure
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_CONNECTION_LOST, reason, lang)
+
+
+class SFTPOpUnsupported(SFTPError):
+ """SFTP operation unsupported
+
+ This exception is raised when the requested SFTP operation
+ is not supported.
+
+ :param reason:
+ Details about the unsupported operation
+ :param lang: (optional)
+ The language the reason is in
+ :type reason: `str`
+ :type lang: `str`
+
+ """
+
+ def __init__(self, reason, lang=DEFAULT_LANG):
+ super().__init__(FX_OP_UNSUPPORTED, reason, lang)
+
+
+_sftp_error_map = {
+ FX_EOF: SFTPEOFError,
+ FX_NO_SUCH_FILE: SFTPNoSuchFile,
+ FX_PERMISSION_DENIED: SFTPPermissionDenied,
+ FX_FAILURE: SFTPFailure,
+ FX_BAD_MESSAGE: SFTPBadMessage,
+ FX_NO_CONNECTION: SFTPNoConnection,
+ FX_CONNECTION_LOST: SFTPConnectionLost,
+ FX_OP_UNSUPPORTED: SFTPOpUnsupported
+}
+
+
+def _construct_sftp_error(code, reason, lang):
+ """Map SFTP error code to appropriate SFTPError exception"""
+
+ try:
+ return _sftp_error_map[code](reason, lang)
+ except KeyError:
+ return SFTPError(code, '%s (error %d)' % (reason, code), lang)
+
+
class SFTPAttrs(Record):
"""SFTP file attributes
@@ -681,7 +853,7 @@
attrs = cls()
if flags & FILEXFER_ATTR_UNDEFINED:
- raise SFTPError(FX_BAD_MESSAGE, 'Unsupported attribute flags')
+ raise SFTPBadMessage('Unsupported attribute flags')
if flags & FILEXFER_ATTR_SIZE:
attrs.size = packet.get_uint64()
@@ -901,7 +1073,7 @@
try:
self._writer.write(UInt32(len(payload)) + payload)
except ConnectionError as exc:
- raise SFTPError(FX_CONNECTION_LOST, str(exc)) from None
+ raise SFTPConnectionLost(str(exc)) from None
self.log_sent_packet(pkttype, pktid, payload)
@@ -928,7 +1100,7 @@
await self._process_packet(pkttype, pktid, packet)
except PacketDecodeError as exc:
- await self._cleanup(SFTPError(FX_BAD_MESSAGE, str(exc)))
+ await self._cleanup(SFTPBadMessage(str(exc)))
except EOFError:
await self._cleanup(None)
except (OSError, Error) as exc:
@@ -957,7 +1129,7 @@
async def _cleanup(self, exc):
"""Clean up this SFTP client session"""
- req_exc = exc or SFTPError(FX_CONNECTION_LOST, 'Connection closed')
+ req_exc = exc or SFTPConnectionLost('Connection closed')
for waiter in self._requests.values():
if not waiter.cancelled(): # pragma: no branch
@@ -975,8 +1147,7 @@
try:
waiter = self._requests.pop(pktid)
except KeyError:
- await self._cleanup(SFTPError(FX_BAD_MESSAGE,
- 'Invalid response id'))
+ await self._cleanup(SFTPBadMessage('Invalid response id'))
else:
if not waiter.cancelled(): # pragma: no branch
waiter.set_result((pkttype, packet))
@@ -985,7 +1156,7 @@
"""Send an SFTP request"""
if not self._writer:
- raise SFTPError(FX_NO_CONNECTION, 'Connection not open')
+ raise SFTPNoConnection('Connection not open')
pktid = self._next_pktid
self._next_pktid = (self._next_pktid + 1) & 0xffffffff
@@ -1010,15 +1181,14 @@
return_type = self._return_types.get(pkttype)
if resptype not in (FXP_STATUS, return_type):
- raise SFTPError(FX_BAD_MESSAGE,
- 'Unexpected response type: %s' % resptype)
+ raise SFTPBadMessage('Unexpected response type: %s' % resptype)
result = self._packet_handlers[resptype](self, resp)
if result is not None or return_type is None:
return result
else:
- raise SFTPError(FX_BAD_MESSAGE, 'Unexpected FX_OK response')
+ raise SFTPBadMessage('Unexpected FX_OK response')
def _process_status(self, packet):
"""Process an incoming SFTP status response"""
@@ -1030,8 +1200,7 @@
reason = packet.get_string().decode('utf-8')
lang = packet.get_string().decode('ascii')
except UnicodeDecodeError:
- raise SFTPError(FX_BAD_MESSAGE,
- 'Invalid status message') from None
+ raise SFTPBadMessage('Invalid status message') from None
else:
# Some servers may not always send reason and lang (usually
# when responding with FX_OK). Tolerate this, automatically
@@ -1046,7 +1215,7 @@
self.logger.debug1('Received OK')
return None
else:
- raise SFTPError(code, reason, lang)
+ raise _construct_sftp_error(code, reason, lang)
def _process_handle(self, packet):
"""Process an incoming SFTP handle response"""
@@ -1131,13 +1300,12 @@
self.log_received_packet(resptype, None, resp)
if resptype != FXP_VERSION:
- raise SFTPError(FX_BAD_MESSAGE, 'Expected version message')
+ raise SFTPBadMessage('Expected version message')
version = resp.get_uint32()
if version != _SFTP_VERSION:
- raise SFTPError(FX_BAD_MESSAGE,
- 'Unsupported version: %d' % version)
+ raise SFTPBadMessage('Unsupported version: %d' % version)
self._version = version
@@ -1148,9 +1316,9 @@
data = resp.get_string()
extensions.append((name, data))
except PacketDecodeError as exc:
- raise SFTPError(FX_BAD_MESSAGE, str(exc))
+ raise SFTPBadMessage(str(exc)) from None
except (asyncio.IncompleteReadError, Error) as exc:
- raise SFTPError(FX_FAILURE, str(exc))
+ raise SFTPFailure(str(exc)) from None
self.logger.debug1('Received version=%d%s', version,
', extensions:' if extensions else '')
@@ -1267,7 +1435,7 @@
return vfsattrs
else:
- raise SFTPError(FX_OP_UNSUPPORTED, 'statvfs not supported')
+ raise SFTPOpUnsupported('statvfs not supported')
async def fstatvfs(self, handle):
"""Make an SFTP fstatvfs request"""
@@ -1284,7 +1452,7 @@
return vfsattrs
else:
- raise SFTPError(FX_OP_UNSUPPORTED, 'fstatvfs not supported')
+ raise SFTPOpUnsupported('fstatvfs not supported')
async def remove(self, path):
"""Make an SFTP remove request"""
@@ -1312,7 +1480,7 @@
return await self._make_request(b'[email protected]',
String(oldpath), String(newpath))
else:
- raise SFTPError(FX_OP_UNSUPPORTED, 'POSIX rename not supported')
+ raise SFTPOpUnsupported('POSIX rename not supported')
async def opendir(self, path):
"""Make an SFTP opendir request"""
@@ -1379,7 +1547,7 @@
return await self._make_request(b'[email protected]',
String(oldpath), String(newpath))
else:
- raise SFTPError(FX_OP_UNSUPPORTED, 'link not supported')
+ raise SFTPOpUnsupported('link not supported')
async def fsync(self, handle):
"""Make an SFTP fsync request"""
@@ -1390,7 +1558,7 @@
return await self._make_request(b'[email protected]',
String(handle))
else:
- raise SFTPError(FX_OP_UNSUPPORTED, 'fsync not supported')
+ raise SFTPOpUnsupported('fsync not supported')
def exit(self):
"""Handle a request to close the SFTP session"""
@@ -1500,9 +1668,8 @@
else:
data = await self._handler.read(self._handle, offset, size)
self._offset = offset + len(data)
- except SFTPError as exc:
- if exc.code != FX_EOF:
- raise
+ except SFTPEOFError:
+ pass
if self._encoding:
data = data.decode(self._encoding, self._errors)
@@ -1813,8 +1980,8 @@
if self._path_encoding:
path = path.encode(self._path_encoding, self._path_errors)
else:
- raise SFTPError(FX_BAD_MESSAGE, 'Path must be bytes when '
- 'encoding is not set')
+ raise SFTPBadMessage('Path must be bytes when '
+ 'encoding is not set')
return path
@@ -1829,8 +1996,7 @@
try:
path = path.decode(self._path_encoding, self._path_errors)
except UnicodeDecodeError:
- raise SFTPError(FX_BAD_MESSAGE,
- 'Unable to decode name') from None
+ raise SFTPBadMessage('Unable to decode name') from None
return path
@@ -1857,11 +2023,8 @@
try:
return (await statfunc(path)).permissions
- except SFTPError as exc:
- if exc.code in (FX_NO_SUCH_FILE, FX_PERMISSION_DENIED):
- return 0
- else:
- raise
+ except (SFTPNoSuchFile, SFTPPermissionDenied):
+ return 0
async def _glob(self, fs, patterns, error_handler):
"""Begin a new glob pattern match"""
@@ -1899,8 +2062,8 @@
try:
if stat.S_ISDIR(srcattrs.permissions):
if not recurse:
- raise SFTPError(FX_FAILURE, '%s is a directory' %
- srcpath.decode('utf-8', errors='replace'))
+ raise SFTPFailure('%s is a directory' %
+ srcpath.decode('utf-8',
errors='replace'))
self.logger.info(' Starting copy of directory %s to %s',
srcpath, dstpath)
@@ -1958,11 +2121,21 @@
else:
raise
- async def _begin_copy(self, srcfs, dstfs, srcpaths, dstpath, preserve,
- recurse, follow_symlinks, block_size, max_requests,
- progress_handler, error_handler):
+ async def _begin_copy(self, srcfs, dstfs, srcpaths, dstpath, copy_type,
+ expand_glob, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler):
"""Begin a new file upload, download, or copy"""
+ if isinstance(srcpaths, tuple):
+ srcpaths = list(srcpaths)
+
+ self.logger.info('Starting SFTP %s of %s to %s',
+ copy_type, srcpaths, dstpath)
+
+ if expand_glob:
+ srcpaths = await self._glob(srcfs, srcpaths, error_handler)
+
dst_isdir = dstpath is None or (await dstfs.isdir(dstpath))
if dstpath:
@@ -1971,8 +2144,8 @@
if isinstance(srcpaths, (str, bytes, PurePath)):
srcpaths = [srcpaths]
elif not dst_isdir:
- raise SFTPError(FX_FAILURE, '%s must be a directory' %
- dstpath.decode('utf-8', errors='replace'))
+ raise SFTPFailure('%s must be a directory' %
+ dstpath.decode('utf-8', errors='replace'))
for srcfile in srcpaths:
srcfile = srcfs.encode(srcfile)
@@ -2085,12 +2258,10 @@
"""
- self.logger.info('Starting SFTP get of %s to %s',
- remotepaths, localpath)
-
- await self._begin_copy(self, LocalFile, remotepaths, localpath,
- preserve, recurse, follow_symlinks, block_size,
- max_requests, progress_handler, error_handler)
+ await self._begin_copy(self, LocalFile, remotepaths, localpath, 'get',
+ False, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler)
async def put(self, localpaths, remotepath=None, *, preserve=False,
recurse=False, follow_symlinks=False,
@@ -2188,12 +2359,10 @@
"""
- self.logger.info('Starting SFTP put of %s to %s',
- localpaths, remotepath)
-
- await self._begin_copy(LocalFile, self, localpaths, remotepath,
- preserve, recurse, follow_symlinks, block_size,
- max_requests, progress_handler, error_handler)
+ await self._begin_copy(LocalFile, self, localpaths, remotepath, 'put',
+ False, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler)
async def copy(self, srcpaths, dstpath=None, *, preserve=False,
recurse=False, follow_symlinks=False,
@@ -2291,12 +2460,10 @@
"""
- self.logger.info('Starting SFTP remote copy of %s to %s',
- srcpaths, dstpath)
-
- await self._begin_copy(self, self, srcpaths, dstpath, preserve,
- recurse, follow_symlinks, block_size,
- max_requests, progress_handler, error_handler)
+ await self._begin_copy(self, self, srcpaths, dstpath, 'remote copy',
+ False, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler)
async def mget(self, remotepaths, localpath=None, *, preserve=False,
recurse=False, follow_symlinks=False,
@@ -2313,14 +2480,10 @@
"""
- self.logger.info('Starting SFTP mget of %s to %s',
- remotepaths, localpath)
-
- matches = await self._glob(self, remotepaths, error_handler)
-
- await self._begin_copy(self, LocalFile, matches, localpath,
- preserve, recurse, follow_symlinks, block_size,
- max_requests, progress_handler, error_handler)
+ await self._begin_copy(self, LocalFile, remotepaths, localpath, 'mget',
+ True, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler)
async def mput(self, localpaths, remotepath=None, *, preserve=False,
recurse=False, follow_symlinks=False,
@@ -2337,14 +2500,10 @@
"""
- self.logger.info('Starting SFTP mput of %s to %s',
- localpaths, remotepath)
-
- matches = await self._glob(LocalFile, localpaths, error_handler)
-
- await self._begin_copy(LocalFile, self, matches, remotepath,
- preserve, recurse, follow_symlinks, block_size,
- max_requests, progress_handler, error_handler)
+ await self._begin_copy(LocalFile, self, localpaths, remotepath, 'mput',
+ True, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler)
async def mcopy(self, srcpaths, dstpath=None, *, preserve=False,
recurse=False, follow_symlinks=False,
@@ -2361,14 +2520,10 @@
"""
- self.logger.info('Starting SFTP remote mcopy of %s to %s',
- srcpaths, dstpath)
-
- matches = await self._glob(self, srcpaths, error_handler)
-
- await self._begin_copy(self, self, matches, dstpath, preserve,
- recurse, follow_symlinks, block_size,
- max_requests, progress_handler, error_handler)
+ await self._begin_copy(self, self, srcpaths, dstpath, 'remote mcopy',
+ True, preserve, recurse, follow_symlinks,
+ block_size, max_requests, progress_handler,
+ error_handler)
async def glob(self, patterns, error_handler=None):
"""Match remote files against glob patterns
@@ -2924,9 +3079,8 @@
try:
while True:
names.extend((await self._handler.readdir(handle)))
- except SFTPError as exc:
- if exc.code != FX_EOF:
- raise
+ except SFTPEOFError:
+ pass
finally:
await self._handler.close(handle)
@@ -3015,7 +3169,7 @@
names = await self._handler.realpath(fullpath)
if len(names) > 1:
- raise SFTPError(FX_BAD_MESSAGE, 'Too many names returned')
+ raise SFTPBadMessage('Too many names returned')
return self.decode(names[0].filename, isinstance(path, (str,
PurePath)))
@@ -3066,7 +3220,7 @@
names = await self._handler.readlink(linkpath)
if len(names) > 1:
- raise SFTPError(FX_BAD_MESSAGE, 'Too many names returned')
+ raise SFTPBadMessage('Too many names returned')
return self.decode(names[0].filename, isinstance(path, str))
@@ -3201,8 +3355,8 @@
handler = self._packet_handlers.get(pkttype)
if not handler:
- raise SFTPError(FX_OP_UNSUPPORTED,
- 'Unsupported request type: %s' % pkttype)
+ raise SFTPOpUnsupported('Unsupported request type: %s' %
+ pkttype)
return_type = self._return_types.get(pkttype, FXP_STATUS)
result = await handler(self, packet)
@@ -3333,7 +3487,7 @@
if self._dir_handles.pop(handle, None) is not None:
return
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
async def _process_read(self, packet):
"""Process an incoming SFTP read request"""
@@ -3357,9 +3511,9 @@
if result:
return result
else:
- raise SFTPError(FX_EOF, '')
+ raise SFTPEOFError
else:
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
async def _process_write(self, packet):
"""Process an incoming SFTP write request"""
@@ -3382,7 +3536,7 @@
return result
else:
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
async def _process_lstat(self, packet):
"""Process an incoming SFTP lstat request"""
@@ -3417,7 +3571,7 @@
return result
else:
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
async def _process_setstat(self, packet):
"""Process an incoming SFTP setstat request"""
@@ -3455,7 +3609,7 @@
return result
else:
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
async def _process_opendir(self, packet):
"""Process an incoming SFTP opendir request"""
@@ -3512,7 +3666,7 @@
del names[:_MAX_READDIR_NAMES]
return result
else:
- raise SFTPError(FX_EOF, '')
+ raise SFTPEOFError
async def _process_remove(self, packet):
"""Process an incoming SFTP remove request"""
@@ -3694,7 +3848,7 @@
return result
else:
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
async def _process_link(self, packet):
"""Process an incoming SFTP hard link request"""
@@ -3731,7 +3885,7 @@
return result
else:
- raise SFTPError(FX_FAILURE, 'Invalid file handle')
+ raise SFTPFailure('Invalid file handle')
_packet_handlers = {
FXP_OPEN: _process_open,
@@ -3778,15 +3932,14 @@
data = packet.get_string()
extensions.append((name, data))
except PacketDecodeError as exc:
- await self._cleanup(SFTPError(FX_BAD_MESSAGE, str(exc)))
+ await self._cleanup(SFTPBadMessage(str(exc)))
return
except Error as exc:
await self._cleanup(exc)
return
if pkttype != FXP_INIT:
- await self._cleanup(SFTPError(FX_BAD_MESSAGE,
- 'Expected init message'))
+ await self._cleanup(SFTPBadMessage('Expected init message'))
return
self.logger.debug1('Received init, version=%d%s', version,
@@ -4066,7 +4219,7 @@
elif path.startswith(self._chroot + b'/'):
return path[len(self._chroot):]
else:
- raise SFTPError(FX_NO_SUCH_FILE, 'File not found')
+ raise SFTPNoSuchFile('File not found')
else:
return path
@@ -4413,7 +4566,7 @@
newpath = _to_local_path(self.map_path(newpath))
if os.path.exists(newpath):
- raise SFTPError(FX_FAILURE, 'File already exists')
+ raise SFTPFailure('File already exists')
os.rename(oldpath, newpath)
@@ -4503,7 +4656,7 @@
try:
return os.statvfs(_to_local_path(self.map_path(path)))
except AttributeError: # pragma: no cover
- raise SFTPError(FX_OP_UNSUPPORTED, 'statvfs not supported')
+ raise SFTPOpUnsupported('statvfs not supported') from None
def fstatvfs(self, file_obj):
"""Return attributes of the file system containing an open file
@@ -4522,7 +4675,7 @@
try:
return os.statvfs(file_obj.fileno())
except AttributeError: # pragma: no cover
- raise SFTPError(FX_OP_UNSUPPORTED, 'fstatvfs not supported')
+ raise SFTPOpUnsupported('fstatvfs not supported') from None
def link(self, oldpath, newpath):
"""Create a hard link
@@ -4604,11 +4757,8 @@
return 0
else:
raise
- except SFTPError as exc:
- if exc.code in (FX_NO_SUCH_FILE, FX_PERMISSION_DENIED):
- return 0
- else:
- raise
+ except (SFTPNoSuchFile, SFTPPermissionDenied):
+ return 0
async def exists(self, path):
"""Return if a path exists"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh/version.py
new/asyncssh-2.2.1/asyncssh/version.py
--- old/asyncssh-2.2.0/asyncssh/version.py 2020-03-01 00:17:18.000000000
+0100
+++ new/asyncssh-2.2.1/asyncssh/version.py 2020-04-18 18:37:35.000000000
+0200
@@ -26,4 +26,4 @@
__url__ = 'http://asyncssh.timeheart.net'
-__version__ = '2.2.0'
+__version__ = '2.2.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/asyncssh.egg-info/PKG-INFO
new/asyncssh-2.2.1/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-2.2.0/asyncssh.egg-info/PKG-INFO 2020-03-01
00:59:29.000000000 +0100
+++ new/asyncssh-2.2.1/asyncssh.egg-info/PKG-INFO 2020-04-18
19:20:46.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 2.2.0
+Version: 2.2.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/tests/test_forward.py
new/asyncssh-2.2.1/tests/test_forward.py
--- old/asyncssh-2.2.0/tests/test_forward.py 2019-11-16 21:33:08.000000000
+0100
+++ new/asyncssh-2.2.1/tests/test_forward.py 2020-04-18 18:36:42.000000000
+0200
@@ -167,6 +167,18 @@
return listen_host != 'fail'
+class _TCPAsyncConnectionServer(_TCPConnectionServer):
+ """Server for testing async direct and forwarded TCP connections"""
+
+ async def server_requested(self, listen_host, listen_port):
+ """Handle a request to create a new socket listener"""
+
+ if listen_host == 'open':
+ return _EchoPortListener(self._conn)
+ else:
+ return listen_host != 'fail'
+
+
class _UNIXConnectionServer(Server):
"""Server for testing direct and forwarded UNIX domain connections"""
@@ -647,6 +659,17 @@
await listener.wait_closed()
+class _TestAsyncTCPForwarding(_TestTCPForwarding):
+ """Unit tests for AsyncSSH TCP connection forwarding with async return"""
+
+ @classmethod
+ async def start_server(cls):
+ """Start an SSH server which supports TCP connection forwarding"""
+
+ return await cls.create_server(
+ _TCPAsyncConnectionServer,
authorized_client_keys='authorized_keys')
+
+
@unittest.skipIf(sys.platform == 'win32',
'skip UNIX domain socket tests on Windows')
class _TestUNIXForwarding(_CheckForwarding):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/tests/test_process.py
new/asyncssh-2.2.1/tests/test_process.py
--- old/asyncssh-2.2.0/tests/test_process.py 2019-11-30 19:27:24.000000000
+0100
+++ new/asyncssh-2.2.1/tests/test_process.py 2020-04-18 18:36:42.000000000
+0200
@@ -84,6 +84,10 @@
except asyncssh.TerminalSizeChanged as exc:
process.exit_with_signal('ABRT', False,
'%sx%s' % (exc.width, exc.height))
+ elif action == 'timeout':
+ process.channel.set_encoding('utf-8')
+ process.stdout.write('Sleeping')
+ await asyncio.sleep(1)
else:
process.exit(255)
@@ -317,6 +321,18 @@
self.assertEqual(exc.exception.returncode, 1)
@asynctest
+ async def test_raise_on_timeout(self):
+ """Test raising an exception on timeout"""
+
+ async with self.connect() as conn:
+ with self.assertRaises(asyncssh.ProcessError) as exc:
+ await conn.run('timeout', timeout=0.1)
+
+ self.assertEqual(exc.exception.command, 'timeout')
+ self.assertEqual(exc.exception.reason, '')
+ self.assertEqual(exc.exception.stdout, 'Sleeping')
+
+ @asynctest
async def test_exit_signal(self):
"""Test checking exit signal"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.2.0/tests/test_sftp.py
new/asyncssh-2.2.1/tests/test_sftp.py
--- old/asyncssh-2.2.0/tests/test_sftp.py 2019-11-16 21:33:08.000000000
+0100
+++ new/asyncssh-2.2.1/tests/test_sftp.py 2020-04-18 18:36:42.000000000
+0200
@@ -34,12 +34,12 @@
import asyncssh
-from asyncssh import SFTPError, SFTPAttrs, SFTPVFSAttrs, SFTPName, SFTPServer
+from asyncssh import SFTPError, SFTPFailure, SFTPPermissionDenied
+from asyncssh import SFTPAttrs, SFTPVFSAttrs, SFTPName, SFTPServer
from asyncssh import SEEK_CUR, SEEK_END
from asyncssh import FXP_INIT, FXP_VERSION, FXP_OPEN, FXP_READ
from asyncssh import FXP_WRITE, FXP_STATUS, FXP_HANDLE, FXP_DATA
-from asyncssh import FILEXFER_ATTR_UNDEFINED
-from asyncssh import FX_OK, FX_PERMISSION_DENIED, FX_FAILURE
+from asyncssh import FILEXFER_ATTR_UNDEFINED, FX_OK
from asyncssh import scp
from asyncssh.packet import SSHPacket, String, UInt32
@@ -153,7 +153,7 @@
"""Return an error for reads past 64 KB in a file"""
if offset >= 65536:
- raise SFTPError(FX_FAILURE, 'I/O error')
+ raise SFTPFailure('I/O error')
else:
return super().read(file_obj, offset, size)
@@ -161,7 +161,7 @@
"""Return an error for writes past 64 KB in a file"""
if offset >= 65536:
- raise SFTPError(FX_FAILURE, 'I/O error')
+ raise SFTPFailure('I/O error')
else:
super().write(file_obj, offset, data)
@@ -290,9 +290,9 @@
return SFTPAttrs.from_local(super().stat(path))
except OSError as exc:
if exc.errno == errno.EACCES:
- raise SFTPError(FX_PERMISSION_DENIED, exc.strerror)
+ raise SFTPPermissionDenied(exc.strerror)
else:
- raise SFTPError(FX_FAILURE, exc.strerror)
+ raise SFTPError(99, exc.strerror)
class _AsyncSFTPServer(SFTPServer):
@@ -649,6 +649,26 @@
async def test_multiple_copy(self, sftp):
"""Test copying multiple files over SFTP"""
+ for method in ('get', 'put', 'copy'):
+ for seq in (list, tuple):
+ with self.subTest(method=method):
+ try:
+ self._create_file('src1', 'xxx')
+ self._create_file('src2', 'yyy')
+ os.mkdir('dst')
+
+ await getattr(sftp, method)(seq(('src1', 'src2')),
+ 'dst')
+
+ self._check_file('src1', 'dst/src1')
+ self._check_file('src2', 'dst/src2')
+ finally:
+ remove('src1 src2 dst')
+
+ @sftp_test
+ async def test_multiple_copy_glob(self, sftp):
+ """Test copying multiple files via glob over SFTP"""
+
for method in ('mget', 'mput', 'mcopy'):
with self.subTest(method=method):
try:
@@ -1093,7 +1113,7 @@
# pylint: disable=unused-argument
- raise SFTPError(FX_FAILURE, 'I/O error')
+ raise SFTPFailure('I/O error')
try:
os.mkdir('dir')
@@ -2354,6 +2374,25 @@
remove('chroot/link1 chroot/link2')
+class _TestSFTPUnknownError(_CheckSFTP):
+ """Unit test for SFTP server returning unknown error"""
+
+ @classmethod
+ async def start_server(cls):
+ """Start an SFTP server which returns unknown error"""
+
+ return await cls.create_server(sftp_factory=_SFTPAttrsSFTPServer)
+
+ @sftp_test
+ async def test_stat_error(self, sftp):
+ """Test error when getting attributes of a file on an SFTP server"""
+
+ with self.assertRaises(SFTPError) as exc:
+ await sftp.stat('file')
+
+ self.assertEqual(exc.exception.code, 99)
+
+
class _TestSFTPIOError(_CheckSFTP):
"""Unit test for SFTP server returning file I/O error"""