Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve error handling robustness for os.execvpe #47

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

ryanpetrello
Copy link

@ryanpetrello ryanpetrello commented Jul 2, 2018

see: pexpect/pexpect#512

Improved behavior (with this PR):

image

@ryanpetrello
Copy link
Author

cc @anwilli5 if you're still out there, I'd love your thoughts on this PR, since I think you wrote the original implementation at #1

@anwilli5
Copy link
Contributor

anwilli5 commented Jul 3, 2018

Your PR seems reasonable to me. It'd probably be safe to simplify the exception description string to something like '{}:0:{}'.format(cls_name, str(err))

The reason the code caught OSError specifically is because the Python documentation for the os library says:

Note: All functions in this module raise OSError in the case of invalid or inaccessible file names and paths, or other arguments that have the correct type, but are not accepted by the operating system.

and, from the documentation for the exec functions:

Errors will be reported as OSError exceptions.

It looks like those statements aren't as concrete as I interpreted them to be!

@ryanpetrello
Copy link
Author

ryanpetrello commented Jul 3, 2018

It seems to me that we want to keep the special case for OSError because it allows the errno to be restored explicitly when the exception is rebuilt by the parent. Otherwise, using a hard coded 0 all the time loses that context for OSError. This works because the constructor signature for OSError is simple, and because OSError is a builtin. For other exceptions, falling back to plain old Exception with a nicely formatted message is probably enough to get the point across - we don’t have to worry about importing code, function signatures, side effects of constructing new exception objects, etc...

@ryanpetrello
Copy link
Author

@takluyver any thoughts on this one?

@takluyver
Copy link
Member

Thanks, this seems like a sensible improvement.

For the unicode error, I'd suggest that we also check for that before forking, i.e. by encoding environment variables and argv in advance. That avoids the complexity of a fork and the machinery to send an error back through a pipe. It could also give clearer error messages (e.g. which thing is causing a unicode error?).

Possibly have a look at the code in the subprocess module. I think this 'error pipe' mechanism is borrowed from there.

@ryanpetrello ryanpetrello force-pushed the fix-execvpe-unicode-errors branch 2 times, most recently from e0bf942 to 978ada4 Compare February 18, 2019 17:25
@ryanpetrello ryanpetrello force-pushed the fix-execvpe-unicode-errors branch from 978ada4 to 3e4bbba Compare February 18, 2019 17:32
@ryanpetrello
Copy link
Author

@takluyver,

I'm a bit delayed in my response, but I recently updated a project I work on to support py3, and ran into this all over again. I've updated this PR w/ the changes you requested - any chance you have some time to look them over?

@ryanpetrello
Copy link
Author

@takluyver,

Sorry to nag, any chance you've had some time to look this one over?

@ryanpetrello
Copy link
Author

@takluyver this is a pretty notable error for Python 3 users. Is there any chance we could get it merged?

@Red-M
Copy link
Member

Red-M commented Mar 26, 2019

I can merge this but I am very unfamiliar with this code base.

Copy link
Member

@takluyver takluyver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it's taken me so long to get to this. My interest in Pexpect stuff waned a bit. I think the idea looks good, but I've left some comments inline.

@@ -206,6 +206,19 @@ def spawn(
if not isinstance(argv, (list, tuple)):
raise TypeError("Expected a list or tuple for argv, got %r" % argv)

# env vars must be utf-8 encoded strings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always UTF-8? Or should we be trying to apply a locale-dependent encoding?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On reflection, unless this comment makes you go "aha, yes, I know just how we should do this!", let's stick to UTF-8 encoding and see if anyone complains. It might not be 100% right, but it's easy to understand, and people who really need something else can still pass environment variables as bytes.

if isinstance(env, dict):
_text_type = str
if not PY3:
_text_type = unicode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put this compatibility bit in the existing if PY3: block at the top of the file.

_text_type = unicode
for k, v in env.items():
if isinstance(v, _text_type):
env[k] = v.encode('utf-8')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC modifying a dict while iterating over it is generally considered a bad idea. Maybe it's better to create a new dict env_b instead.

tosend = 'OSError:{}:{}'.format(err.errno, str(err))
else:
cls_name = err.__class__.__name__
tosend = 'Exception:0:{}: {}'.format(cls_name, str(err))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the exception handling around preexec_fn(), maybe this should do '{}:0:{}' (e.g. 'UnicodeEncodeError:0:blah'.

@ryanpetrello
Copy link
Author

ryanpetrello commented Sep 10, 2019

@takluyver I actually ran into another version of this bug today, which can occur if you specify an env var that has an integer or float as a env var value:

~ python
>>> import pexpect
>>> pexpect.spawn(
...     'sleep', ['1'], cwd='/tmp/', env={'FOO': 1}, ignore_sighup=True,
...     encoding='utf-8', echo=False
... )

# <python hangs>
Traceback (most recent call last):
awx_1        |   File "/venv/awx/lib64/python3.6/site-packages/pexpect/pty_spawn.py", line 303, in _spawn
awx_1        |     cwd=self.cwd, **kwargs)
awx_1        |   File "/venv/awx/lib64/python3.6/site-packages/pexpect/pty_spawn.py", line 314, in _spawnpty
awx_1        |     return ptyprocess.PtyProcess.spawn(args, **kwargs)
awx_1        |   File "/venv/awx/lib64/python3.6/site-packages/ptyprocess/ptyprocess.py", line 353, in spawn
awx_1        |     raise exception
awx_1        | Exception: TypeError: expected str, bytes or os.PathLike object, not float

This PR also fixes that case.

Do you have very strong feelings about the review feedback on this PR? I've found that it definitely addresses the bugs I've found (which are pretty serious, given that they cause pexpect's spawn to hang forever).

@takluyver
Copy link
Member

Do you have strong feelings against any of my feedback? I generally expect that when I make suggestions as a project maintainer, they'll get a response, even if that's "I think it's actually better this way because..."

If you're too busy, I'll try to get round to applying my suggestions myself. But I'm pretty busy too, as you might have noticed 😉 .

@ryanpetrello
Copy link
Author

ryanpetrello commented Sep 30, 2019

Do you have strong feelings against any of my feedback?

@takluyver nope, not especially - just also a lack of time on my end w/ the endless volunteer and open source work I'm doing 😄

Whichever one of us finds time to get to this first 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants