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

Cannot set exit code in atexit callback #71222

Open
Melebius mannequin opened this issue May 16, 2016 · 19 comments
Open

Cannot set exit code in atexit callback #71222

Melebius mannequin opened this issue May 16, 2016 · 19 comments
Labels
3.10 only security fixes 3.11 only security fixes docs Documentation in the Doc dir extension-modules C modules in the Modules dir interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@Melebius
Copy link
Mannequin

Melebius mannequin commented May 16, 2016

BPO 27035
Nosy @glyph, @bitdancer, @Bluehorn, @gwk, @Melebius, @pablogsal, @chrahunt, @kakshay21

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2016-05-16.07:57:08.094>
labels = ['extension-modules', 'interpreter-core', 'type-bug', '3.10', '3.11']
title = 'Cannot set exit code in atexit callback'
updated_at = <Date 2021-08-19.09:10:43.702>
user = 'https://github.com/Melebius'

bugs.python.org fields:

activity = <Date 2021-08-19.09:10:43.702>
actor = 'Mike Hommey'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Extension Modules', 'Interpreter Core']
creation = <Date 2016-05-16.07:57:08.094>
creator = 'Melebius'
dependencies = []
files = []
hgrepos = []
issue_num = 27035
keywords = []
message_count = 19.0
messages = ['265678', '265696', '271736', '271740', '289145', '307052', '334237', '334241', '334242', '334243', '334263', '334264', '334285', '334286', '367957', '367992', '367993', '399896', '399897']
nosy_count = 9.0
nosy_names = ['glyph', 'r.david.murray', 'torsten', 'gwk', 'Melebius', 'pablogsal', 'chrahunt', 'kakshay', 'Mike Hommey']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue27035'
versions = ['Python 3.10', 'Python 3.11']

@Melebius
Copy link
Mannequin Author

Melebius mannequin commented May 16, 2016

I want to set exit code of my script in a function registered in the atexit module. (See https://stackoverflow.com/q/37178636/711006.) Calling sys.exit() in that function results in the following error:

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "atexit_test.py", line 3, in myexit
    sys.exit(2)
SystemExit: 2

Despite the printed error, the exit code is set to 0. (This might be related with bpo-1257.)

This problem seems to affect Python 3.x. I experienced it with Python 3.5.1 on Windows 7 x64 and I am able to reproduce it with Python 3.4.3 on Linux (x64). Python 2.7.6 on the same Linux machine works as expected: Exits without additional messages and the desired exit code is set.

A simple test case:

def myexit():
  import sys
  sys.exit(2)

import atexit
atexit.register(myexit)

@Melebius Melebius mannequin added type-bug An unexpected behavior, bug, or error extension-modules C modules in the Modules dir labels May 16, 2016
@bitdancer
Copy link
Member

Calling sys.exit in an atexit function strikes me as a really bad idea. It feels to me like it breaks the contract of what atexit is designed for, which is to run multiple handlers at exit. If you call sys.exit you are in some sense restarting the exit processing, which feels broken to me. It would feel equally broken, however, to call _exit, since that would mean the rest of the handlers in the chain would not be run.

In any case, I personally would *expect* an exit called in an atexit handler to have no effect, since an exit is already in progress and we've just taken a detour to run the handlers. I can see the argument for the reverse, however, so it isn't obvious to me what the correct answer is.

I do note that the OP in bpo-1257 prefers to use the sys.exit return code if an atexit handler raises an error, which argues for not replacing it if the atexit handler raises SystemExit with a different RC. It would be more consistent with the proposed handling of atexit errors.

@gwk
Copy link
Mannequin

gwk mannequin commented Jul 31, 2016

The documentation for atexit.register clearly states that a SystemExit raised inside of the registered function is a special case:

'''
If an exception is raised during execution of the exit handlers, a traceback is printed (unless SystemExit is raised) and the exception information is saved. After all exit handlers have had a chance to run the last exception to be raised is re-raised.
'''

Python 2.7.11 behaves as described; Python 3.5.2 does not.

I believe there is a clear argument for allowing atexit functions to set an exit status code: Ultimately, it is the responsibility of the application programmer to return an appropriate code for all execution paths, and it is up to the programmer to decide what is appropriate. I can easily imagine cases where the atexit function encounters a critical error and the appropriate behavior is to return an error status to the parent process.

In many large systems, returning the correct code is the most critical behavior of a process, and so if atexit prevents the programmer from doing so then its utility is greatly diminished.

I disagree that calling _exit is "equally broken". Calling _exit completely breaks the atexit unwinding contract, and if an error code is necessary, then this is exactly what I am forced to do!

In my mind the correct behavior would be that the process exit code is determined from the last exception that occurs in the exit process.

  • If the program begins exiting with 0, and then an atexit handler raises SystemExit(1), then code 1 should be returned.
  • If the program begins exiting with 1, and then an atexit handler raises SystemExit(0), then code 0 should be returned (this may seem strange, but handlers can do all manner of strange things!).
  • If successive handlers raise multiple exceptions, the last one determines the code.
  • If the program is exiting with any code, and an exception other than SystemExit is raised, then we should return the code that would result from raising that exception in normal execution (usually 1).

I expect this last case to be most contentious, because it changes behavior for both python2 and python3. However I think it is desirable because it gives the handlers precise capabilities (and responsibilities) regarding process status. The point of atexit is to allow modules to execute code in a deferred manner; the design already specifies a 'last exception wins' policy, and the problem is that we are unnecessarily suppressing the exit code that would result from that last exception.

@bitdancer
Copy link
Member

Well, changing something like this in 2.7 is off the table in any case. This would be a "feature release only" type of change if there is agreement that it is a good idea.

@bitdancer bitdancer added interpreter-core (Objects, Python, Grammar, and Parser dirs) type-feature A feature request or enhancement and removed type-bug An unexpected behavior, bug, or error labels Jul 31, 2016
@glyph
Copy link
Mannequin

glyph mannequin commented Mar 6, 2017

I just bumped into this myself. If this really is only fixable in a major release, there ought to at least be a minor release for the *documentation* to update it to be correct.

@Bluehorn
Copy link
Mannequin

Bluehorn mannequin commented Nov 27, 2017

As this bug report clearly states this worked as documented in Python 2.7 and stopped working sometime in the Python 3 series.

I just ran into this while porting some code to Python 3 which uses an atexit handler to wind down some resources on process exit. This sometimes gets stuck and instead of hanging indefinitely the cleanup is aborted if it takes longer than a few seconds.

As this is actually an error, the registered exit function raises SystemExit to modify the exit code in this case. This used to work fine but does not anymore...

Observe:

## Python 2.7 works fine

$ sudo docker run python:2.7 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
124

## Python 3.2 (oldest Python 3 on docker hub) swallows the exit code (but prints it)

$ sudo docker run python:3.2 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0

## Same for 3.3 up to 3.6

$ sudo docker run python:3.3 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0
$ sudo docker run python:3.5 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0

$ sudo docker run python:3.6 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0

## Python 3.7 swallows the exit code *and does not even print it*:

$ /opt/python-dev/bin/python3.7 -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
0

@Bluehorn Bluehorn mannequin added type-bug An unexpected behavior, bug, or error and removed type-feature A feature request or enhancement labels Nov 27, 2017
@chrahunt chrahunt mannequin added the 3.7 (EOL) end of life label Jan 21, 2019
@kakshay21
Copy link
Mannequin

kakshay21 mannequin commented Jan 22, 2019

Can I work on this?
I noticed the same behaviour as python3.7 in python3.8 from master branch.

@pablogsal
Copy link
Member

I think this is a documentation issue today. The docs say:

If an exception is raised during execution of the exit handlers, a traceback
is printed (unless SystemExit is raised) and the exception
information is saved. After all exit handlers have had a chance to run the
last exception to be raised is re-raised.

Which is true except for two things:

  • SystemExit is not covered by the paragraph (it just says that SystemExit will not print a traceback but what actually happens is that is ignored).
  • The last exception is not re-raised (as it was on Python2.7).

I think we should update the docs to reflect that SystemExit is ignored and to remove that the last exception is re-raised.

@pablogsal
Copy link
Member

The behaviour regarding printing SytemExit was changed by Serhiy in 3fd54d4 and in bpo-28994.

@pablogsal
Copy link
Member

@ Kumar Do you want to make a PR fixing the docs?

@kakshay21
Copy link
Mannequin

kakshay21 mannequin commented Jan 23, 2019

Sure, I would love to!

@gwk
Copy link
Mannequin

gwk mannequin commented Jan 23, 2019

I agree that regardless of the underlying issue, the docs should match the behavior. Additionally, I hope the docs will note the exact release at which the behavior changed.

@serhiy-storchaka do you have any opinion on this? I took a brief look at your commit cited above, and it appears that you were trying to suppress an unwanted stack trace display. Did you intend for the exit status behavior to also change?

I stand by my original assessment, as a somewhat dissatisfied user of the atexit feature. However I also acknowledge that at this point, CPython already has a subtle backwards-compatibility issue and further change might not be so welcome. I'm happy to discuss this further if people are interested, but I'm not going to lobby hard :)

@Melebius
Copy link
Mannequin Author

Melebius mannequin commented Jan 24, 2019

I completely support @gwk’s opinion expressed in his comments. My original intention has been to set the exit code in an atexit callback. I tried a way and found that it was working in Python 2.7 but not in 3.x (without notice), so I filed this bug report. (See also the Stack Overflow link in my first post.)

I don’t find this just a documentation issue since it prevents the user from setting the exit code, although (as correctly stated by @gwk): “Ultimately, it is the responsibility of the application programmer to return an appropriate code for all execution paths” and “returning the correct code is the most critical behavior of a process”.

My use case is a testing library. The user calls assertions from my library and when their script finishes, my library is responsible for deciding whether the test has passed (and finishing the log). Before porting the library to Python (3.5 initially), I used to indicate successfulness by the exit code. With Python 3.x, I am forced to either:

  1. print a result message and let the parent process parse it (implemented currently), or
  2. force the user of my library to include something like
    sys.exit(testlib.result())
    as the last line of their scripts which I find annoying and error-prone.

I cannot decide whether calling sys.exit() in an atexit callback means breaking the contract as @r.david.murray states. However, the user shall be able to set the exit code of their application when it finishes and atexit should support some way to do that.

@pablogsal
Copy link
Member

You can always set the exit code calling sys.exit from outside the atexit handlers. I concur with R. David Murray in that calling sys.exit and expect anything sounds like a bad idea and an abuse if the atexit system.

A normal application would call sys.exit, some cleanup code will be called in the atexit handlers and finally the program will exit with the code originally set in the first call.

How and when your application will be calling sys.exit is an application architecture problem, and IMHO it should not be a CPython provided functionality to guarantee that you can do this in the atexit handlers.

It also violates the contract that right now is in the documentation: SystemExit exceptions are not printed or reraiaed in atexit handlers. Changing this (specially the second part) will be backwards incompatible, although that is a second order argument.

@glyph
Copy link
Mannequin

glyph mannequin commented May 3, 2020

This bug has been filed several times:

bpo-1257
bpo-11654

and it's tempting to simply close this as a dup, but this ticket mentions the documentation, which is slightly confusing:

https://docs.python.org/3.8/library/atexit.html#atexit.register

It's not *wrong* exactly; 3.8's behavior matches the letter of the documentation, but "the last exception to be raised is re-raised" *implies* a change in exit code, since that is what would normally happen if an exception were re-raised at the top level.

So would it be a good idea to change the documentation?

@gwk
Copy link
Mannequin

gwk mannequin commented May 3, 2020

I think we should change the documentation to expand the parenthetical " (unless SystemExit is raised)" to a complete explanation of that special case.

@glyph
Copy link
Mannequin

glyph mannequin commented May 3, 2020

gwk, I absolutely agree; at this point that's the main thing I'm looking for.

But it would be great if whoever adds that documentation could include the rationale for this behavior too. It still seems "obvious" to me that it should change the exit code, and every time I bump into this behavior I am freshly confused and have to re-read these bugs. If I had a better intuition for what the error-handling *model* of atexit was, I think it might stick to memory a bit better.

@wyz23x2 wyz23x2 mannequin added 3.10 only security fixes 3.11 only security fixes and removed 3.7 (EOL) end of life labels Jul 26, 2021
@MikeHommey
Copy link
Mannequin

MikeHommey mannequin commented Aug 19, 2021

I think we should change the documentation to expand the parenthetical " (unless SystemExit is raised)" to a complete explanation of that special case.

That would not be enough, since the case for other exceptions would still be ambiguous, as the described behavior ("After all exit handlers have had a chance to run the last exception to be raised is re-raised.") would imply the exit code would be altered, like when exceptions are raised in normal context. In 2.7 the only exception that _did_ change the exit code was SystemExit.

@MikeHommey
Copy link
Mannequin

MikeHommey mannequin commented Aug 19, 2021

In 2.7 the only exception that _did_ change the exit code was SystemExit.

(and only if it was the last thrown exception)

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
@iritkatriel iritkatriel added the docs Documentation in the Doc dir label Jun 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.10 only security fixes 3.11 only security fixes docs Documentation in the Doc dir extension-modules C modules in the Modules dir interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants