Hello community, here is the log from the commit of package python-pexpect for openSUSE:Factory checked in at 2018-04-20 17:30:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pexpect (Old) and /work/SRC/openSUSE:Factory/.python-pexpect.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pexpect" Fri Apr 20 17:30:45 2018 rev:24 rq:598545 version:4.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pexpect/python-pexpect.changes 2018-02-14 09:45:25.601362343 +0100 +++ /work/SRC/openSUSE:Factory/.python-pexpect.new/python-pexpect.changes 2018-04-20 17:33:00.346845842 +0200 @@ -1,0 +2,39 @@ +Thu Apr 19 11:58:18 UTC 2018 - [email protected] + +- Fix typo and make sure the tests will pass + +------------------------------------------------------------------- +Sat Apr 14 17:14:36 UTC 2018 - [email protected] + +- specfile: + * enable tests + +- update to version 4.5.0: + * spawn and fdspawn now have a use_poll parameter. If this is True, + they will use select.poll() instead of select.select(). poll() + allows file descriptors above 1024, but it must be explicitly + enabled due to compatibility concerns (PR #474). + * The pxssh.login() method has several new and changed options: + + The option password_regex allows changing the password prompt + regex, for servers that include password: in a banner before + reaching a prompt (PR #468). + + login() now allows for setting up SSH tunnels to be requested + once logged in to the remote server. This option is ssh_tunnels + (PR #473). The structure should be like this: + { + 'local': ['2424:localhost:22'], # Local SSH tunnels + 'remote': ['2525:localhost:22'], # Remote SSH tunnels + 'dynamic': [8888], # Dynamic/SOCKS tunnels + } + + + The option spawn_local_ssh=False allows subsequent logins from + the remote session and treats the session as if it was local (PR + #472). + + Setting sync_original_prompt=False will prevent changing the + prompt to something unique, in case the remote server is + sensitive to new lines at login (PR #468). + + If ssh_key=True is passed, the SSH client forces forwarding the + authentication agent to the remote server instead of providing a + key (PR #473). + +------------------------------------------------------------------- Old: ---- pexpect-4.4.0.tar.gz New: ---- pexpect-4.5.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pexpect.spec ++++++ --- /var/tmp/diff_new_pack.AWQRq2/_old 2018-04-20 17:33:00.946824084 +0200 +++ /var/tmp/diff_new_pack.AWQRq2/_new 2018-04-20 17:33:00.950823939 +0200 @@ -16,26 +16,25 @@ # -%bcond_with tests - %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-pexpect -Version: 4.4.0 +Version: 4.5.0 Release: 0 Summary: Pure Python Expect-like module License: ISC Group: Development/Libraries/Python -Url: http://pexpect.readthedocs.org/en/latest/ +URL: http://pexpect.readthedocs.org/en/latest/ Source: https://files.pythonhosted.org/packages/source/p/pexpect/pexpect-%{version}.tar.gz BuildRequires: %{python_module devel} BuildRequires: %{python_module ptyprocess} +BuildRequires: %{python_module pytest} BuildRequires: fdupes +# For man validation +BuildRequires: man +# For test command calls +BuildRequires: openssl BuildRequires: python-rpm-macros -%if %{with tests} -BuildRequires: %{python_module pytest} -%endif Requires: python-ptyprocess -BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildArch: noarch %python_subpackages @@ -44,11 +43,11 @@ controlling them; and responding to expected patterns in their output. %prep -%setup -n pexpect-%{version} +%setup -q -n pexpect-%{version} # Fix wrong-script-interpreter -find examples -type f -name "*.py" -exec sed -i "s|#!/usr/bin/env python||" {} \; -find examples -type f -name "*.cgi" -exec sed -i "s|##!/usr/bin/env python|##!/usr/bin/python|" {} \; +find examples -type f -name "*.py" -exec sed -i "s|#!%{_bindir}/env python||" {} \; +find examples -type f -name "*.cgi" -exec sed -i "s|##!%{_bindir}/env python|##!%{_bindir}/python|" {} \; %build %python_build @@ -57,15 +56,12 @@ %python_install %python_expand %fdupes %{buildroot}%{$python_sitelib} -%if %{with tests} %check export LANG=en_US.UTF-8 -%python_expand py.test-%{$python_bun_suffix} -%endif +%python_expand py.test-%{$python_bin_suffix} %files %{python_files} -%defattr(-,root,root,-) -%doc LICENSE +%license LICENSE %doc doc/ %doc examples/ %{python_sitelib}/pexpect/ ++++++ pexpect-4.4.0.tar.gz -> pexpect-4.5.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/PKG-INFO new/pexpect-4.5.0/PKG-INFO --- old/pexpect-4.4.0/PKG-INFO 2018-02-10 13:45:15.000000000 +0100 +++ new/pexpect-4.5.0/PKG-INFO 2018-04-13 20:20:48.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pexpect -Version: 4.4.0 +Version: 4.5.0 Summary: Pexpect allows easy control of interactive console applications. Home-page: https://pexpect.readthedocs.io/ Author: Noah Spurrier; Thomas Kluyver; Jeff Quast diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/doc/conf.py new/pexpect-4.5.0/doc/conf.py --- old/pexpect-4.4.0/doc/conf.py 2018-02-10 13:34:25.000000000 +0100 +++ new/pexpect-4.5.0/doc/conf.py 2018-04-11 21:03:57.000000000 +0200 @@ -52,7 +52,7 @@ # built documents. # # The short X.Y version. -version = '4.4' +version = '4.5' # The full version, including alpha/beta/rc tags. release = version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/doc/history.rst new/pexpect-4.5.0/doc/history.rst --- old/pexpect-4.4.0/doc/history.rst 2018-02-10 13:32:20.000000000 +0100 +++ new/pexpect-4.5.0/doc/history.rst 2018-04-13 19:49:35.000000000 +0200 @@ -4,6 +4,36 @@ Releases -------- +Version 4.5 +``````````` + +* :class:`~.spawn` and :class:`~.fdspawn` now have a ``use_poll`` parameter. + If this is True, they will use :func:`select.poll` instead of :func:`select.select`. + ``poll()`` allows file descriptors above 1024, but it must be explicitly + enabled due to compatibility concerns (:ghpull:`474`). +* The :meth:`.pxssh.login` method has several new and changed options: + + * The option ``password_regex`` allows changing + the password prompt regex, for servers that include ``password:`` in a banner + before reaching a prompt (:ghpull:`468`). + * :meth:`~.pxssh.login` now allows for setting up SSH tunnels to be requested once + logged in to the remote server. This option is ``ssh_tunnels`` (:ghpull:`473`). + The structure should be like this:: + + { + 'local': ['2424:localhost:22'], # Local SSH tunnels + 'remote': ['2525:localhost:22'], # Remote SSH tunnels + 'dynamic': [8888], # Dynamic/SOCKS tunnels + } + + * The option ``spawn_local_ssh=False`` allows subsequent logins from the + remote session and treats the session as if it was local (:ghpull:`472`). + * Setting ``sync_original_prompt=False`` will prevent changing the prompt to + something unique, in case the remote server is sensitive to new lines at login + (:ghpull:`468`). + * If ``ssh_key=True`` is passed, the SSH client forces forwarding the authentication + agent to the remote server instead of providing a key (:ghpull:`473`). + Version 4.4 ``````````` @@ -295,4 +325,3 @@ to call the new fork Pexpected. Noah Spurrier agreed to let them use the name Pexpect, so Pexpect versions 3 and above are based on this fork, which now lives `here on Github <https://github.com/pexpect/pexpect>`_. - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/doc/overview.rst new/pexpect-4.5.0/doc/overview.rst --- old/pexpect-4.4.0/doc/overview.rst 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/doc/overview.rst 2018-04-13 19:51:22.000000000 +0200 @@ -261,7 +261,6 @@ .. seealso:: - `winpexpect <https://pypi.python.org/pypi/winpexpect>`__ and - `wexpect <https://gist.github.com/anthonyeden/8488763>`__ + `winpexpect <https://pypi.python.org/pypi/winpexpect>`__ and `wexpect <https://gist.github.com/anthonyeden/8488763>`__ Two unmaintained pexpect-like modules for Windows, which work with a hidden console. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/__init__.py new/pexpect-4.5.0/pexpect/__init__.py --- old/pexpect-4.4.0/pexpect/__init__.py 2018-02-10 13:34:36.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/__init__.py 2018-04-11 21:03:57.000000000 +0200 @@ -75,7 +75,7 @@ from .pty_spawn import spawn, spawnu from .run import run, runu -__version__ = '4.4.0' +__version__ = '4.5.0' __revision__ = '' __all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'spawnu', 'run', 'runu', 'which', 'split_command_line', '__version__', '__revision__'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/_async.py new/pexpect-4.5.0/pexpect/_async.py --- old/pexpect-4.4.0/pexpect/_async.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/_async.py 2018-04-11 21:03:57.000000000 +0200 @@ -9,6 +9,7 @@ # async stuff. previously_read = expecter.spawn.buffer expecter.spawn._buffer = expecter.spawn.buffer_type() + expecter.spawn._before = expecter.spawn.buffer_type() idx = expecter.new_data(previously_read) if idx is not None: return idx diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/expect.py new/pexpect-4.5.0/pexpect/expect.py --- old/pexpect-4.4.0/pexpect/expect.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/expect.py 2018-04-11 21:03:57.000000000 +0200 @@ -16,6 +16,7 @@ pos = spawn._buffer.tell() spawn._buffer.write(data) + spawn._before.write(data) # determine which chunk of data to search; if a windowsize is # specified, this is the *new* data + the preceding <windowsize> bytes @@ -27,11 +28,11 @@ window = spawn.buffer index = searcher.search(window, len(data)) if index >= 0: - value = spawn.buffer spawn._buffer = spawn.buffer_type() - spawn._buffer.write(value[searcher.end:]) - spawn.before = value[: searcher.start] - spawn.after = value[searcher.start: searcher.end] + spawn._buffer.write(window[searcher.end:]) + spawn.before = spawn._before.getvalue()[0:-(len(window) - searcher.start)] + spawn._before = spawn.buffer_type() + spawn.after = window[searcher.start: searcher.end] spawn.match = searcher.match spawn.match_index = index # Found a match @@ -45,6 +46,7 @@ spawn.before = spawn.buffer spawn._buffer = spawn.buffer_type() + spawn._before = spawn.buffer_type() spawn.after = EOF index = self.searcher.eof_index if index >= 0: @@ -96,6 +98,7 @@ try: incoming = spawn.buffer spawn._buffer = spawn.buffer_type() + spawn._before = spawn.buffer_type() while True: idx = self.new_data(incoming) # Keep reading until exception or return. @@ -158,7 +161,7 @@ '''This returns a human-readable string that represents the state of the object.''' - ss = [(ns[0], ' %d: "%s"' % ns) for ns in self._strings] + ss = [(ns[0], ' %d: %r' % ns) for ns in self._strings] ss.append((-1, 'searcher_string:')) if self.eof_index >= 0: ss.append((self.eof_index, ' %d: EOF' % self.eof_index)) @@ -258,13 +261,7 @@ # (n, repr(s.pattern))) for n, s in self._searches] ss = list() for n, s in self._searches: - try: - ss.append((n, ' %d: re.compile("%s")' % (n, s.pattern))) - except UnicodeEncodeError: - # for test cases that display __str__ of searches, dont throw - # another exception just because stdout is ascii-only, using - # repr() - ss.append((n, ' %d: re.compile(%r)' % (n, s.pattern))) + ss.append((n, ' %d: re.compile(%r)' % (n, s.pattern))) ss.append((-1, 'searcher_re:')) if self.eof_index >= 0: ss.append((self.eof_index, ' %d: EOF' % self.eof_index)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/fdpexpect.py new/pexpect-4.5.0/pexpect/fdpexpect.py --- old/pexpect-4.4.0/pexpect/fdpexpect.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/fdpexpect.py 2018-04-13 19:35:52.000000000 +0200 @@ -23,7 +23,7 @@ from .spawnbase import SpawnBase from .exceptions import ExceptionPexpect, TIMEOUT -from .utils import select_ignore_interrupts +from .utils import select_ignore_interrupts, poll_ignore_interrupts import os __all__ = ['fdspawn'] @@ -34,7 +34,7 @@ for patterns, or to control a modem or serial device. ''' def __init__ (self, fd, args=None, timeout=30, maxread=2000, searchwindowsize=None, - logfile=None, encoding=None, codec_errors='strict'): + logfile=None, encoding=None, codec_errors='strict', use_poll=False): '''This takes a file descriptor (an int) or an object that support the fileno() method (returning an int). All Python file-like objects support fileno(). ''' @@ -58,6 +58,7 @@ self.own_fd = False self.closed = False self.name = '<file descriptor %d>' % fd + self.use_poll = use_poll def close (self): """Close the file descriptor. @@ -88,7 +89,7 @@ def terminate (self, force=False): # pragma: no cover '''Deprecated and invalid. Just raises an exception.''' raise ExceptionPexpect('This method is not valid for file descriptors.') - + # These four methods are left around for backwards compatibility, but not # documented as part of fdpexpect. You're encouraged to use os.write # directly. @@ -96,19 +97,19 @@ "Write to fd, return number of bytes written" s = self._coerce_send_string(s) self._log(s, 'send') - + b = self._encoder.encode(s, final=False) return os.write(self.child_fd, b) - + def sendline(self, s): "Write to fd with trailing newline, return number of bytes written" s = self._coerce_send_string(s) return self.send(s + self.linesep) - + def write(self, s): "Write to fd, return None" self.send(s) - + def writelines(self, sequence): "Call self.write() for each item in sequence" for s in sequence: @@ -136,7 +137,12 @@ rlist = [self.child_fd] wlist = [] xlist = [] - rlist, wlist, xlist = select_ignore_interrupts(rlist, wlist, xlist, timeout) + if self.use_poll: + rlist = poll_ignore_interrupts(rlist, timeout) + else: + rlist, wlist, xlist = select_ignore_interrupts( + rlist, wlist, xlist, timeout + ) if self.child_fd not in rlist: raise TIMEOUT('Timeout exceeded.') return super(fdspawn, self).read_nonblocking(size) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/pty_spawn.py new/pexpect-4.5.0/pexpect/pty_spawn.py --- old/pexpect-4.4.0/pexpect/pty_spawn.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/pty_spawn.py 2018-04-13 19:35:52.000000000 +0200 @@ -12,7 +12,9 @@ from .exceptions import ExceptionPexpect, EOF, TIMEOUT from .spawnbase import SpawnBase -from .utils import which, split_command_line, select_ignore_interrupts +from .utils import ( + which, split_command_line, select_ignore_interrupts, poll_ignore_interrupts +) @contextmanager def _wrap_ptyprocess_err(): @@ -34,7 +36,8 @@ def __init__(self, command, args=[], timeout=30, maxread=2000, searchwindowsize=None, logfile=None, cwd=None, env=None, ignore_sighup=False, echo=True, preexec_fn=None, - encoding=None, codec_errors='strict', dimensions=None): + encoding=None, codec_errors='strict', dimensions=None, + use_poll=False): '''This is the constructor. The command parameter may be a string that includes a command and any arguments to the command. For example:: @@ -171,7 +174,7 @@ using setecho(False) followed by waitnoecho(). However, for some platforms such as Solaris, this is not possible, and should be disabled immediately on spawn. - + If preexec_fn is given, it will be called in the child process before launching the given command. This is useful to e.g. reset inherited signal handlers. @@ -179,6 +182,9 @@ The dimensions attribute specifies the size of the pseudo-terminal as seen by the subprocess, and is specified as a two-entry tuple (rows, columns). If this is unspecified, the defaults in ptyprocess will apply. + + The use_poll attribute enables using select.poll() over select.select() + for socket handling. This is handy if your system could have > 1024 fds ''' super(spawn, self).__init__(timeout=timeout, maxread=maxread, searchwindowsize=searchwindowsize, logfile=logfile, encoding=encoding, codec_errors=codec_errors) @@ -196,6 +202,7 @@ self.name = '<pexpect factory incomplete>' else: self._spawn(command, args, preexec_fn, dimensions) + self.use_poll = use_poll def __str__(self): '''This returns a human-readable string that represents the state of @@ -439,7 +446,10 @@ # If isalive() is false, then I pretend that this is the same as EOF. if not self.isalive(): # timeout of 0 means "poll" - r, w, e = select_ignore_interrupts([self.child_fd], [], [], 0) + if self.use_poll: + r = poll_ignore_interrupts([self.child_fd], timeout) + else: + r, w, e = select_ignore_interrupts([self.child_fd], [], [], 0) if not r: self.flag_eof = True raise EOF('End Of File (EOF). Braindead platform.') @@ -447,12 +457,19 @@ # Irix takes a long time before it realizes a child was terminated. # FIXME So does this mean Irix systems are forced to always have # FIXME a 2 second delay when calling read_nonblocking? That sucks. - r, w, e = select_ignore_interrupts([self.child_fd], [], [], 2) + if self.use_poll: + r = poll_ignore_interrupts([self.child_fd], timeout) + else: + r, w, e = select_ignore_interrupts([self.child_fd], [], [], 2) if not r and not self.isalive(): self.flag_eof = True raise EOF('End Of File (EOF). Slow platform.') - - r, w, e = select_ignore_interrupts([self.child_fd], [], [], timeout) + if self.use_poll: + r = poll_ignore_interrupts([self.child_fd], timeout) + else: + r, w, e = select_ignore_interrupts( + [self.child_fd], [], [], timeout + ) if not r: if not self.isalive(): @@ -764,14 +781,20 @@ return os.read(fd, 1000) - def __interact_copy(self, escape_character=None, - input_filter=None, output_filter=None): + def __interact_copy( + self, escape_character=None, input_filter=None, output_filter=None + ): '''This is used by the interact() method. ''' while self.isalive(): - r, w, e = select_ignore_interrupts([self.child_fd, self.STDIN_FILENO], [], []) + if self.use_poll: + r = poll_ignore_interrupts([self.child_fd, self.STDIN_FILENO]) + else: + r, w, e = select_ignore_interrupts( + [self.child_fd, self.STDIN_FILENO], [], [] + ) if self.child_fd in r: try: data = self.__interact_read(self.child_fd) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/pxssh.py new/pexpect-4.5.0/pexpect/pxssh.py --- old/pexpect-4.4.0/pexpect/pxssh.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/pxssh.py 2018-04-11 21:03:57.000000000 +0200 @@ -23,6 +23,8 @@ from pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn import time import os +import sys +import re __all__ = ['ExceptionPxssh', 'pxssh'] @@ -31,6 +33,22 @@ '''Raised for pxssh exceptions. ''' +if sys.version_info > (3, 0): + from shlex import quote +else: + _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search + + def quote(s): + """Return a shell-escaped version of the string *s*.""" + if not s: + return "''" + if _find_unsafe(s) is None: + return s + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + s.replace("'", "'\"'\"'") + "'" + class pxssh (spawn): '''This class extends pexpect.spawn to specialize setting up SSH connections. This adds methods for login, logout, and expecting the shell @@ -91,11 +109,16 @@ username = raw_input('username: ') password = getpass.getpass('password: ') s.login (hostname, username, password) + + `debug_command_string` is only for the test suite to confirm that the string + generated for SSH is correct, using this will not allow you to do + anything other than get a string back from `pxssh.pxssh.login()`. ''' def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None, logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True, - options={}, encoding=None, codec_errors='strict'): + options={}, encoding=None, codec_errors='strict', + debug_command_string=False): spawn.__init__(self, None, timeout=timeout, maxread=maxread, searchwindowsize=searchwindowsize, logfile=logfile, @@ -131,6 +154,8 @@ # Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying! #self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'" self.force_password = False + + self.debug_command_string = debug_command_string # User defined SSH options, eg, # ssh.otions = dict(StrictHostKeyChecking="no",UserKnownHostsFile="/dev/null") @@ -195,7 +220,7 @@ can take 12 seconds. Low latency connections are more likely to fail with a low sync_multiplier. Best case sync time gets worse with a high sync multiplier (500 ms with default). ''' - + # All of these timing pace values are magic. # I came up with these based on what seemed reliable for # connecting to a heavily loaded machine I have. @@ -227,10 +252,14 @@ ### TODO: This is getting messy and I'm pretty sure this isn't perfect. ### TODO: I need to draw a flow chart for this. + ### TODO: Unit tests for SSH tunnels, remote SSH command exec, disabling original prompt sync def login (self, server, username, password='', terminal_type='ansi', original_prompt=r"[#$]", login_timeout=10, port=None, auto_prompt_reset=True, ssh_key=None, quiet=True, - sync_multiplier=1, check_local_ip=True): + sync_multiplier=1, check_local_ip=True, + password_regex=r'(?i)(?:password:)|(?:passphrase for key)', + ssh_tunnels={}, spawn_local_ssh=True, + sync_original_prompt=True): '''This logs the user into the given server. It uses @@ -255,7 +284,24 @@ uses a unique prompt in the :meth:`prompt` method. If the original prompt is not reset then this will disable the :meth:`prompt` method unless you manually set the :attr:`PROMPT` attribute. + + Set ``password_regex`` if there is a MOTD message with `password` in it. + Changing this is like playing in traffic, don't (p)expect it to match straight + away. + + If you require to connect to another SSH server from the your original SSH + connection set ``spawn_local_ssh`` to `False` and this will use your current + session to do so. Setting this option to `False` and not having an active session + will trigger an error. + + Set ``ssh_key`` to `True` to force passing the current SSH authentication socket to the + to the desired ``hostname``. ''' + + session_regex_array = ["(?i)are you sure you want to continue connecting", original_prompt, password_regex, "(?i)permission denied", "(?i)terminal type", TIMEOUT] + session_init_regex_array = [] + session_init_regex_array.extend(session_regex_array) + session_init_regex_array.extend(["(?i)connection closed by remote host", EOF]) ssh_options = ''.join([" -o '%s=%s'" % (o, v) for (o, v) in self.options.items()]) if quiet: @@ -267,17 +313,52 @@ if port is not None: ssh_options = ssh_options + ' -p %s'%(str(port)) if ssh_key is not None: - try: - os.path.isfile(ssh_key) - except: - raise ExceptionPxssh('private ssh key does not exist') - ssh_options = ssh_options + ' -i %s' % (ssh_key) + # Allow forwarding our SSH key to the current session + if ssh_key==True: + ssh_options = ssh_options + ' -A' + else: + try: + if spawn_local_ssh: + os.path.isfile(ssh_key) + except: + raise ExceptionPxssh('private ssh key does not exist') + ssh_options = ssh_options + ' -i %s' % (ssh_key) + + # SSH tunnels, make sure you know what you're putting into the lists + # under each heading. Do not expect these to open 100% of the time, + # The port you're requesting might be bound. + # + # The structure should be like this: + # { 'local': ['2424:localhost:22'], # Local SSH tunnels + # 'remote': ['2525:localhost:22'], # Remote SSH tunnels + # 'dynamic': [8888] } # Dynamic/SOCKS tunnels + if ssh_tunnels!={} and isinstance({},type(ssh_tunnels)): + tunnel_types = { + 'local':'L', + 'remote':'R', + 'dynamic':'D' + } + for tunnel_type in tunnel_types: + cmd_type = tunnel_types[tunnel_type] + if tunnel_type in ssh_tunnels: + tunnels = ssh_tunnels[tunnel_type] + for tunnel in tunnels: + if spawn_local_ssh==False: + tunnel = quote(str(tunnel)) + ssh_options = ssh_options + ' -' + cmd_type + ' ' + str(tunnel) cmd = "ssh %s -l %s %s" % (ssh_options, username, server) + if self.debug_command_string: + return(cmd) + + # Are we asking for a local ssh command or to spawn one in another session? + if spawn_local_ssh: + spawn._spawn(self, cmd) + else: + self.sendline(cmd) # This does not distinguish between a remote server 'password' prompt # and a local ssh 'passphrase' prompt (for unlocking a private key). - spawn._spawn(self, cmd) - i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password:)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT, "(?i)connection closed by remote host", EOF], timeout=login_timeout) + i = self.expect(session_init_regex_array, timeout=login_timeout) # First phase if i==0: @@ -285,13 +366,13 @@ # This is what you get if SSH does not have the remote host's # public key stored in the 'known_hosts' cache. self.sendline("yes") - i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password:)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT]) + i = self.expect(session_regex_array) if i==2: # password or passphrase self.sendline(password) - i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password:)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT]) + i = self.expect(session_regex_array) if i==4: self.sendline(terminal_type) - i = self.expect(["(?i)are you sure you want to continue connecting", original_prompt, "(?i)(?:password:)|(?:passphrase for key)", "(?i)permission denied", "(?i)terminal type", TIMEOUT]) + i = self.expect(session_regex_array) if i==7: self.close() raise ExceptionPxssh('Could not establish connection to host') @@ -331,9 +412,10 @@ else: # Unexpected self.close() raise ExceptionPxssh('unexpected login response') - if not self.sync_original_prompt(sync_multiplier): - self.close() - raise ExceptionPxssh('could not synchronize with original prompt') + if sync_original_prompt: + if not self.sync_original_prompt(sync_multiplier): + self.close() + raise ExceptionPxssh('could not synchronize with original prompt') # We appear to be in. # set shell prompt to something unique. if auto_prompt_reset: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/spawnbase.py new/pexpect-4.5.0/pexpect/spawnbase.py --- old/pexpect-4.4.0/pexpect/spawnbase.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/spawnbase.py 2018-04-11 21:03:57.000000000 +0200 @@ -262,7 +262,7 @@ # the input is 'foobar' index = p.expect(['foobar', 'foo']) # returns 0('foobar') if all input is available at once, - # but returs 1('foo') if parts of the final 'bar' arrive late + # but returns 1('foo') if parts of the final 'bar' arrive late When a match is found for the given pattern, the class instance attribute *match* becomes an re.MatchObject result. Should an EOF diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/pexpect/utils.py new/pexpect-4.5.0/pexpect/utils.py --- old/pexpect-4.4.0/pexpect/utils.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/pexpect/utils.py 2018-04-13 19:35:52.000000000 +0200 @@ -154,3 +154,34 @@ # something else caused the select.error, so # this actually is an exception. raise + + +def poll_ignore_interrupts(fds, timeout=None): + '''Simple wrapper around poll to register file descriptors and + ignore signals.''' + + if timeout is not None: + end_time = time.time() + timeout + + poller = select.poll() + for fd in fds: + poller.register(fd) + + while True: + try: + timeout_ms = None if timeout is None else timeout * 1000 + results = poller.poll(timeout_ms) + return [afd for afd, _ in results] + except InterruptedError: + err = sys.exc_info()[1] + if err.args[0] == errno.EINTR: + # if we loop back we have to subtract the + # amount of time we already waited. + if timeout is not None: + timeout = end_time - time.time() + if timeout < 0: + return [] + else: + # something else caused the select.error, so + # this actually is an exception. + raise diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/setup.cfg new/pexpect-4.5.0/setup.cfg --- old/pexpect-4.4.0/setup.cfg 2016-05-21 13:43:33.000000000 +0200 +++ new/pexpect-4.5.0/setup.cfg 2018-04-11 21:03:57.000000000 +0200 @@ -1,4 +1,4 @@ -[pytest] +[tool:pytest] norecursedirs = .git [bdist_wheel] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/setup.py new/pexpect-4.5.0/setup.py --- old/pexpect-4.4.0/setup.py 2018-02-10 13:43:58.000000000 +0100 +++ new/pexpect-4.5.0/setup.py 2018-04-11 21:03:57.000000000 +0200 @@ -34,7 +34,7 @@ Windows. """ -setup (name='pexpect', +setup(name='pexpect', version=version, packages=['pexpect'], package_data={'pexpect': ['bashrc.sh']}, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/tests/log new/pexpect-4.5.0/tests/log --- old/pexpect-4.4.0/tests/log 2016-08-21 22:02:45.000000000 +0200 +++ new/pexpect-4.5.0/tests/log 1970-01-01 01:00:00.000000000 +0100 @@ -1,20 +0,0 @@ -\,ESC -P,ESC -\,ESC -P,ESC -\,ESC -P,ESC -\,ESC -P,ESC -\,ESC -r,SEMICOLON -\,ESC -P,ESC -\,ESC -P,ESC -\,ESC -P,ESC -\,ESC -P,ESC -\,ESC -r,SEMICOLON diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/tests/test_expect.py new/pexpect-4.5.0/tests/test_expect.py --- old/pexpect-4.4.0/tests/test_expect.py 2018-02-10 13:16:58.000000000 +0100 +++ new/pexpect-4.5.0/tests/test_expect.py 2018-04-11 21:03:57.000000000 +0200 @@ -408,6 +408,17 @@ p.buffer = b'Testing' p.sendeof () + def test_before_across_chunks(self): + # https://github.com/pexpect/pexpect/issues/478 + child = pexpect.spawn( + '''/bin/bash -c "openssl rand -base64 {} | head -500 | nl --number-format=rz --number-width=5 2>&1 ; echo 'PATTERN!!!'"'''.format(1024 * 1024 * 2), + searchwindowsize=128 + ) + child.expect(['PATTERN']) + assert len(child.before.splitlines()) == 500 + assert child.after == b'PATTERN' + assert child.buffer == b'!!!\r\n' + def _before_after(self, p): p.timeout = 5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/tests/test_misc.py new/pexpect-4.5.0/tests/test_misc.py --- old/pexpect-4.4.0/tests/test_misc.py 2016-07-09 23:34:08.000000000 +0200 +++ new/pexpect-4.5.0/tests/test_misc.py 2018-04-11 21:03:57.000000000 +0200 @@ -52,6 +52,15 @@ return 'skip' assert child.isatty() + def test_isatty_poll(self): + " Test isatty() is True after spawning process on most platforms. " + child = pexpect.spawn('cat', use_poll=True) + if not child.isatty() and sys.platform.lower().startswith('sunos'): + if hasattr(unittest, 'SkipTest'): + raise unittest.SkipTest("Not supported on this platform.") + return 'skip' + assert child.isatty() + def test_read(self): " Test spawn.read by calls of various size. " child = pexpect.spawn('cat') @@ -65,6 +74,19 @@ remaining = child.read().replace(_CAT_EOF, b'') self.assertEqual(remaining, b'abc\r\n') + def test_read_poll(self): + " Test spawn.read by calls of various size. " + child = pexpect.spawn('cat', use_poll=True) + child.sendline("abc") + child.sendeof() + self.assertEqual(child.read(0), b'') + self.assertEqual(child.read(1), b'a') + self.assertEqual(child.read(1), b'b') + self.assertEqual(child.read(1), b'c') + self.assertEqual(child.read(2), b'\r\n') + remaining = child.read().replace(_CAT_EOF, b'') + self.assertEqual(remaining, b'abc\r\n') + def test_readline_bin_echo(self): " Test spawn('echo'). " # given, @@ -148,7 +170,7 @@ p.sendline(b'alpha') p.expect(b'<out>alpha') assert p.isalive() - + assert not p.isalive() def test_terminate(self): @@ -271,7 +293,7 @@ expected_output = '{0}:'.format(searcher.__name__) idx = 0 for word in given_words: - expected_output += fmt.format(idx, '"{0}"'.format(word)) + expected_output += fmt.format(idx, "'{0}'".format(word)) idx += 1 if plus is not None: if plus == pexpect.EOF: @@ -343,4 +365,3 @@ unittest.main() suite = unittest.makeSuite(TestCaseMisc,'test') - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/tests/test_pxssh.py new/pexpect-4.5.0/tests/test_pxssh.py --- old/pexpect-4.4.0/tests/test_pxssh.py 2016-08-21 21:57:33.000000000 +0200 +++ new/pexpect-4.5.0/tests/test_pxssh.py 2018-04-11 21:03:57.000000000 +0200 @@ -57,6 +57,56 @@ else: assert False, 'should have raised exception, pxssh.ExceptionPxssh' + def test_ssh_tunnel_string(self): + ssh = pxssh.pxssh(debug_command_string=True) + tunnels = { 'local': ['2424:localhost:22'],'remote': ['2525:localhost:22'], + 'dynamic': [8888] } + confirmation_strings = 0 + confirmation_array = ['-R 2525:localhost:22','-L 2424:localhost:22','-D 8888'] + string = ssh.login('server', 'me', password='s3cret', ssh_tunnels=tunnels) + for confirmation in confirmation_array: + if confirmation in string: + confirmation_strings+=1 + + if confirmation_strings!=len(confirmation_array): + assert False, 'String generated from tunneling is incorrect.' + + def test_remote_ssh_tunnel_string(self): + ssh = pxssh.pxssh(debug_command_string=True) + tunnels = { 'local': ['2424:localhost:22'],'remote': ['2525:localhost:22'], + 'dynamic': [8888] } + confirmation_strings = 0 + confirmation_array = ['-R 2525:localhost:22','-L 2424:localhost:22','-D 8888'] + string = ssh.login('server', 'me', password='s3cret', ssh_tunnels=tunnels, spawn_local_ssh=False) + for confirmation in confirmation_array: + if confirmation in string: + confirmation_strings+=1 + + if confirmation_strings!=len(confirmation_array): + assert False, 'String generated from remote tunneling is incorrect.' + + def test_ssh_key_string(self): + ssh = pxssh.pxssh(debug_command_string=True) + confirmation_strings = 0 + confirmation_array = [' -A'] + string = ssh.login('server', 'me', password='s3cret', ssh_key=True) + for confirmation in confirmation_array: + if confirmation in string: + confirmation_strings+=1 + + if confirmation_strings!=len(confirmation_array): + assert False, 'String generated from forcing the SSH agent sock is incorrect.' + + confirmation_strings = 0 + confirmation_array = [' -i True'] + string = ssh.login('server', 'me', password='s3cret', ssh_key='True') + for confirmation in confirmation_array: + if confirmation in string: + confirmation_strings+=1 + + if confirmation_strings!=len(confirmation_array): + assert False, 'String generated from adding an SSH key is incorrect.' + if __name__ == '__main__': unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pexpect-4.4.0/tests/test_socket.py new/pexpect-4.5.0/tests/test_socket.py --- old/pexpect-4.4.0/tests/test_socket.py 2016-07-09 23:34:08.000000000 +0200 +++ new/pexpect-4.5.0/tests/test_socket.py 2018-04-11 21:03:57.000000000 +0200 @@ -225,7 +225,7 @@ session.expect(pexpect.EOF) self.assertEqual(session.before, b'') - def test_fd_isalive (self): + def test_fd_isalive(self): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((self.host, self.port)) session = fdpexpect.fdspawn(sock.fileno(), timeout=10) @@ -233,13 +233,28 @@ sock.close() assert not session.isalive(), "Should not be alive after close()" - def test_fd_isatty (self): + def test_fd_isalive_poll(self): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.host, self.port)) + session = fdpexpect.fdspawn(sock.fileno(), timeout=10, use_poll=True) + assert session.isalive() + sock.close() + assert not session.isalive(), "Should not be alive after close()" + + def test_fd_isatty(self): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((self.host, self.port)) session = fdpexpect.fdspawn(sock.fileno(), timeout=10) assert not session.isatty() session.close() + def test_fd_isatty_poll(self): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.host, self.port)) + session = fdpexpect.fdspawn(sock.fileno(), timeout=10, use_poll=True) + assert not session.isatty() + session.close() + def test_fileobj(self): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((self.host, self.port))
