PySFTP/Paramiko exceptions leaking into stderr

旧时模样 提交于 2020-01-13 06:50:08

问题


I am trying to catch paramiko exceptions but they are still written to stderr.

Is there a way to stop writing there?

EDIT: It happens even before paramiko gets involved:

import pysftp

try:
    pysftp.Connection(host="localhost")
except Exception as e:
    print(e)

Results in:

Example with proper SFTP params:

UPDATE:

$ pipenv graph
...
pysftp==0.2.9
  - paramiko [required: >=1.17, installed: 2.6.0]
...

$ pipenv run python
Python 3.7.3 (default, Jul 19 2019, 11:21:39)
[Clang 11.0.0 (clang-1100.0.28.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pysftp
>>> try:
...     pysftp.Connection(host="localhost")
... except Exception as e:
...     print(e)
...
No hostkey for host localhost found.
Exception ignored in: <function Connection.__del__ at 0x10f7e8268>
Traceback (most recent call last):
  File "/Users/andrei/Work/try/.venv/lib/python3.7/site-packages/pysftp/__init__.py", line 1013, in __del__
    self.close()
  File "/Users/andrei/Work/try/.venv/lib/python3.7/site-packages/pysftp/__init__.py", line 784, in close
    if self._sftp_live:
AttributeError: 'Connection' object has no attribute '_sftp_live'
>>>

回答1:


I want to start by pointing out that PySFTP ([PyPI]: PySFTP) has not been maintain for 3+ years (or has been moved to a different location - which so far is kept secret :) ).

I reproduced the problem. Below is a more verbose version of your code.

code00.py:

#!/usr/bin/env python3

import sys
import pysftp
import traceback


def main(argv):
    hostname = argv[0] if argv else "localhost"
    print("Attempting to connect to {0:s} ...".format(hostname))
    try:
        print("----------Before conn----------")
        conn = pysftp.Connection(host=hostname)
        print("----------After conn----------")
    except:
        print("----------Before exc print----------")
        traceback.print_exc()
        print("----------After exc print----------")
    finally:
        print("----------Finally----------")
    print("----------After try / except / finally----------")

if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    print("pysftp version: {0:s}\n".format(pysftp.__version__))
    main(sys.argv[1:])
    print("\nDone.")

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q058110732]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32
pysftp version: 0.2.9

Attempting to connect to localhost ...
----------Before conn----------
e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py:61: UserWarning: Failed to load HostKeys from C:\Users\cfati\.ssh\known_hosts.  You will need to explicitly load HostKeys (cnopts.hostkeys.load(filename)) or disableHostKey checking (cnopts.hostkeys = None).
  warnings.warn(wmsg, UserWarning)
----------Before exc print----------
Traceback (most recent call last):
  File "code00.py", line 13, in main
    conn = pysftp.Connection(host=hostname)
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 132, in __init__
    self._tconnect['hostkey'] = self._cnopts.get_hostkey(host)
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 71, in get_hostkey
    raise SSHException("No hostkey for host %s found." % host)
paramiko.ssh_exception.SSHException: No hostkey for host localhost found.
----------After exc print----------
Exception ignored in: <function Connection.__del__ at 0x000001CC720C80D0>
Traceback (most recent call last):
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 1013, in __del__
    self.close()
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 784, in close
    if self._sftp_live:
AttributeError: 'Connection' object has no attribute '_sftp_live'
----------Finally----------
----------After try / except / finally----------

Done.

It's a PySFTP bug:

  1. The Connection object is constructed (__new__)
  2. The initializer (__init__) is called
    1. Somewhere in the initializer an exception occurs
    2. The lines after the exception are not executed
  3. When the object is (automatically) garbage collected (when it goes out of scope, at the end of the except block), in its close method (called by the destructor (__del__)), some attributes are referenced
    1. But since those attributes initialization occurs after the line (raising the exception) from #2.2., they were never initialized, so their reference raises AttributeError

The fix is straightforward: initialize the attributes to some default values, at the initializer's beginning, so their reference doesn't represent a problem if the above scenario happens.

I noticed that you've already submitted an issue on BitBucket.

Considering that:

  • I'm not a BitBucket expert
  • I don't have permissions on [BitBucket]: dundeemt/pysftp - pysftp is an easy to use sftp module that utilizes paramiko and pycrypto. (I can neither push nor submit pull requests)

I created my own repo (out of the above one), and pushed the changes at: [BitBucket]: CristiFati0/pysftp - [Issue #144]: Exceptions leaking into stderr (one commit, so far).

Output (after manually applying the fix to the file installed by pip):

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q058110732]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32
pysftp version: 0.2.9

Attempting to connect to localhost ...
----------Before conn----------
e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py:61: UserWarning: Failed to load HostKeys from C:\Users\cfati\.ssh\known_hosts.  You will need to explicitly load HostKeys (cnopts.hostkeys.load(filename)) or disableHostKey checking (cnopts.hostkeys = None).
  warnings.warn(wmsg, UserWarning)
----------Before exc print----------
Traceback (most recent call last):
  File "code00.py", line 13, in main
    conn = pysftp.Connection(host=hostname)
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 135, in __init__
    self._tconnect['hostkey'] = self._cnopts.get_hostkey(host)
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 71, in get_hostkey
    raise SSHException("No hostkey for host %s found." % host)
paramiko.ssh_exception.SSHException: No hostkey for host localhost found.
----------After exc print----------
----------Finally----------
----------After try / except / finally----------

Done.

Needless to say, other unhandled exceptions might be raised for different scenarios.

@EDIT0

Apparently, ther's more to this than meets the eye. Besides the PySFTP bug described above, there are 2 more things that pollute stderr.

1. The warning

In my case (as I am on Win and don't have any native SSH tool installed) it pops up every time (unless I create / copy some valid known_hosts file in my home dir), but on Nix systems it most likely won't.

Anyway, the fix for this one (if wanted) is easy, simply suppress the UserWarning for example by setting the %PYTHONWARNINGS% env var to ignore::UserWarning (can also be achieved from code - like in next item's case).

2. Paramiko exceptions

I was able to reproduce this by manually modifying transport.py (and raising socket.timeout).

paramiko.Transport which is initialized by pysftp.Connection._start_transport (called by the initializer) does its work in a thread (by subclassing threading.Thread). Any exception raised in that thread, can't be caught by the calling thread (ours). This is a Python limitation scheduled to be addressed in v3.8 ([Python.Bugs]: threading.Thread should have way to catch an exception thrown within).

For this one, there is a (lame) workaround (gainarie): redirecting stderr. Of course there are other workarounds, but they would imply modifying Paramiko, so I'd advice against them.

Below is an example that redirects stderr to stdout (but you can choose any other file - including /dev/null (or nul on Win)). It does it from code (but it can also be done from interpreter command line) so that it only affects the desired (hot) area(s).

code01.py:

#!/usr/bin/env python3

import sys
import pysftp
import paramiko
import traceback
import threading


_sys_stderr = sys.stderr  # For restoring purposes


def main(argv):
    hostname = argv[0] if argv else "localhost"
    print("Attempting to connect to {0:s} ...".format(hostname))
    try:
        cnopts = pysftp.CnOpts()
        cnopts.hostkeys = None
        print("---------- STATS: {0:s} {1:d} ----------".format(__file__, threading.get_ident()))
        print("---------- Before conn ----------")
        sys.stderr.write("DUMMY TEXT before sent to stderr\n")
        sys.stderr = sys.stdout  # @TODO - cfati: decomment so that everything from stderr is redirected to stdout
        conn = pysftp.Connection(host=hostname, port=22001, username="usr", password="pwd", cnopts=cnopts,)
        print("---------- After conn ----------")
    except:
        sys.stderr = _sys_stderr
        print("---------- Before exc tb ----------")
        traceback.print_exc(file=sys.stdout)
        print("---------- After exc tb ----------")
    finally:
        sys.stderr = _sys_stderr
        print("---------- Finally ----------")
        sys.stderr.write("DUMMY TEXT after sent to stderr\n")
    print("---------- After try / except / finally ----------")


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    print("pysftp version: {0:s}\nparamiko version: {1:s}".format(pysftp.__version__, paramiko.__version__))
    main(sys.argv[1:])
    print("\nDone.")

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q058110732]> sopr.bat
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***

[prompt]> dir /b
code00.py
code01.py

[prompt]> :: Suppress warning
[prompt]> set PYTHONWARNINGS=ignore::UserWarning

[prompt]> :: Redirect stdout and stderr to different files, so it is obvious which is which
[prompt]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code01.py 1>1.out 2>1.err

[prompt]> type 1.err
DUMMY TEXT before sent to stderr
DUMMY TEXT after sent to stderr

[prompt]> type 1.out
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32
pysftp version: 0.2.9
paramiko version: 2.6.0
Attempting to connect to localhost ...
---------- STATS: code01.py 23016 ----------
---------- Before conn ----------
---------- STATS: e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py 45616 ----------
Exception: Error reading SSH protocol banner
Traceback (most recent call last):
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2212, in _check_banner
    raise socket.timeout()
socket.timeout

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2039, in run
    self._check_banner()
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2218, in _check_banner
    "Error reading SSH protocol banner" + str(e)
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner

---------- Before exc tb ----------
Traceback (most recent call last):
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2212, in _check_banner
    raise socket.timeout()
socket.timeout

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "code01.py", line 23, in main
    conn = pysftp.Connection(host=hostname, port=22001, username="usr", password="pwd", cnopts=cnopts,)
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 144, in __init__
    self._transport.connect(**self._tconnect)
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 1291, in connect
    self.start_client()
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 660, in start_client
    raise e
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2039, in run
    self._check_banner()
  File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2218, in _check_banner
    "Error reading SSH protocol banner" + str(e)
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner
---------- After exc tb ----------
---------- Finally ----------
---------- After try / except / finally ----------

Done.

And how it looks from PyCharm (had to stretch it to the max to fit the whole thing):




回答2:


I would skip pysftp and just use paramiko as it support SFTP just fine by itself:

import paramiko

host,port = "example.com",22
transport = paramiko.Transport((host,port))
username,password = "bar","foo"
transport.connect(None,username,password)
sftp = paramiko.SFTPClient.from_transport(transport)

Then here you can again try to catch the exception at exactly where it occurs and see how you fare.

Or if you need to authenticate with only password:

client = pk.SSHClient()
client.set_missing_host_key_policy(pk.AutoAddPolicy())
client.connect(hostname=ip, port=22, username=userName, password=pwd)
ftp_client = client.open_sftp()


来源:https://stackoverflow.com/questions/58110732/pysftp-paramiko-exceptions-leaking-into-stderr

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!