Skip to content

bpo-40094: Add os.waitstatus_to_exitcode() #19201

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

Merged
merged 3 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3665,6 +3665,11 @@ written in Python, such as a mail server's external command delivery program.
subprocess was killed.) On Windows systems, the return value
contains the signed integer return code from the child process.

On Unix, :func:`waitstatus_to_exitcode` can be used to convert the ``close``
method result (exit status) into an exit code if it is not ``None``. On
Windows, the ``close`` method result is directly the exit code
(or ``None``).

This is implemented using :class:`subprocess.Popen`; see that class's
documentation for more powerful ways to manage and communicate with
subprocesses.
Expand Down Expand Up @@ -3968,6 +3973,10 @@ written in Python, such as a mail server's external command delivery program.
to using this function. See the :ref:`subprocess-replacements` section in
the :mod:`subprocess` documentation for some helpful recipes.

On Unix, :func:`waitstatus_to_exitcode` can be used to convert the result
(exit status) into an exit code. On Windows, the result is directly the exit
code.

.. audit-event:: os.system command os.system

.. availability:: Unix, Windows.
Expand Down Expand Up @@ -4008,8 +4017,16 @@ written in Python, such as a mail server's external command delivery program.
number is zero); the high bit of the low byte is set if a core file was
produced.

:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
exit code.

.. availability:: Unix.

.. seealso::

:func:`waitpid` can be used to wait for the completion of a specific
child process and has more options.

.. function:: waitid(idtype, id, options)

Wait for the completion of one or more child processes.
Expand Down Expand Up @@ -4105,6 +4122,9 @@ written in Python, such as a mail server's external command delivery program.
id is known, not necessarily a child process. The :func:`spawn\* <spawnl>`
functions called with :const:`P_NOWAIT` return suitable process handles.

:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
exit code.

.. versionchanged:: 3.5
If the system call is interrupted and the signal handler does not raise an
exception, the function now retries the system call instead of raising an
Expand All @@ -4120,6 +4140,9 @@ written in Python, such as a mail server's external command delivery program.
information. The option argument is the same as that provided to
:func:`waitpid` and :func:`wait4`.

:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
exitcode.

.. availability:: Unix.


Expand All @@ -4131,9 +4154,42 @@ written in Python, such as a mail server's external command delivery program.
resource usage information. The arguments to :func:`wait4` are the same
as those provided to :func:`waitpid`.

:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
exitcode.

.. availability:: Unix.


.. function:: waitstatus_to_exitcode(status)

Convert a wait status to an exit code.

On Unix:

* If the process exited normally (if ``WIFEXITED(status)`` is true),
return the process exit status (return ``WEXITSTATUS(status)``):
result greater than or equal to 0.
* If the process was terminated by a signal (if ``WIFSIGNALED(status)`` is
true), return ``-signum`` where *signum* is the number of the signal that
caused the process to terminate (return ``-WTERMSIG(status)``):
result less than 0.
* Otherwise, raise a :exc:`ValueError`.

On Windows, return *status* shifted right by 8 bits.

On Unix, if the process is being traced or if :func:`waitpid` was called
with :data:`WUNTRACED` option, the caller must first check if
``WIFSTOPPED(status)`` is true. This function must not be called if
``WIFSTOPPED(status)`` is true.

.. seealso::

:func:`WIFEXITED`, :func:`WEXITSTATUS`, :func:`WIFSIGNALED`,
:func:`WTERMSIG`, :func:`WIFSTOPPED`, :func:`WSTOPSIG` functions.

.. versionadded:: 3.9


.. data:: WNOHANG

The option for :func:`waitpid` to return immediately if no child process status
Expand Down
5 changes: 5 additions & 0 deletions Doc/library/pty.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ The :mod:`pty` module defines the following functions:
*select* throws an error on your platform when passed three empty lists. This
is a bug, documented in `issue 26228 <https://bugs.python.org/issue26228>`_.

Return the exit status value from :func:`os.waitpid` on the child process.

:func:`waitstatus_to_exitcode` can be used to convert the exit status into
an exit code.

.. audit-event:: pty.spawn argv pty.spawn

.. versionchanged:: 3.4
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ The :func:`os.putenv` and :func:`os.unsetenv` functions are now always
available.
(Contributed by Victor Stinner in :issue:`39395`.)

Add :func:`os.waitstatus_to_exitcode` function:
convert a wait status to an exit code.
(Contributed by Victor Stinner in :issue:`40094`.)

pathlib
-------

Expand Down
13 changes: 2 additions & 11 deletions Lib/_bootsubprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@
import os


def _waitstatus_to_exitcode(status):
if os.WIFEXITED(status):
return os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
return -os.WTERMSIG(status)
else:
raise ValueError(f"invalid wait status: {status!r}")


# distutils.spawn used by distutils.command.build_ext
# calls subprocess.Popen().wait()
class Popen:
Expand All @@ -37,7 +28,7 @@ def wait(self):
else:
# Parent process
_, status = os.waitpid(pid, 0)
self.returncode = _waitstatus_to_exitcode(status)
self.returncode = os.waitstatus_to_exitcode(status)

return self.returncode

Expand Down Expand Up @@ -87,7 +78,7 @@ def check_output(cmd, **kwargs):
try:
# system() spawns a shell
status = os.system(cmd)
exitcode = _waitstatus_to_exitcode(status)
exitcode = os.waitstatus_to_exitcode(status)
if exitcode:
raise ValueError(f"Command {cmd!r} returned non-zero "
f"exit status {exitcode!r}")
Expand Down
10 changes: 2 additions & 8 deletions Lib/multiprocessing/forkserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,8 @@ def sigchld_handler(*_unused):
break
child_w = pid_to_fd.pop(pid, None)
if child_w is not None:
if os.WIFSIGNALED(sts):
returncode = -os.WTERMSIG(sts)
else:
if not os.WIFEXITED(sts):
raise AssertionError(
"Child {0:n} status is {1:n}".format(
pid,sts))
returncode = os.WEXITSTATUS(sts)
returncode = os.waitstatus_to_exitcode(sts)

# Send exit code to client process
try:
write_signed(child_w, returncode)
Expand Down
6 changes: 1 addition & 5 deletions Lib/multiprocessing/popen_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,7 @@ def poll(self, flag=os.WNOHANG):
# e.errno == errno.ECHILD == 10
return None
if pid == self.pid:
if os.WIFSIGNALED(sts):
self.returncode = -os.WTERMSIG(sts)
else:
assert os.WIFEXITED(sts), "Status is {:n}".format(sts)
self.returncode = os.WEXITSTATUS(sts)
self.returncode = os.waitstatus_to_exitcode(sts)
return self.returncode

def wait(self, timeout=None):
Expand Down
8 changes: 2 additions & 6 deletions Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,12 +864,8 @@ def _spawnvef(mode, file, args, env, func):
wpid, sts = waitpid(pid, 0)
if WIFSTOPPED(sts):
continue
elif WIFSIGNALED(sts):
return -WTERMSIG(sts)
elif WIFEXITED(sts):
return WEXITSTATUS(sts)
else:
raise OSError("Not stopped, signaled or exited???")

return waitstatus_to_exitcode(sts)

def spawnv(mode, file, args):
"""spawnv(mode, file, args) -> integer
Expand Down
18 changes: 6 additions & 12 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1838,23 +1838,17 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
raise child_exception_type(err_msg)


def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
_WTERMSIG=os.WTERMSIG, _WIFEXITED=os.WIFEXITED,
_WEXITSTATUS=os.WEXITSTATUS, _WIFSTOPPED=os.WIFSTOPPED,
_WSTOPSIG=os.WSTOPSIG):
def _handle_exitstatus(self, sts,
waitstatus_to_exitcode=os.waitstatus_to_exitcode,
_WIFSTOPPED=os.WIFSTOPPED,
_WSTOPSIG=os.WSTOPSIG):
"""All callers to this function MUST hold self._waitpid_lock."""
# This method is called (indirectly) by __del__, so it cannot
# refer to anything outside of its local scope.
if _WIFSIGNALED(sts):
self.returncode = -_WTERMSIG(sts)
elif _WIFEXITED(sts):
self.returncode = _WEXITSTATUS(sts)
elif _WIFSTOPPED(sts):
if _WIFSTOPPED(sts):
self.returncode = -_WSTOPSIG(sts)
else:
# Should never happen
raise SubprocessError("Unknown child exit status!")

self.returncode = waitstatus_to_exitcode(sts)

def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid,
_WNOHANG=os.WNOHANG, _ECHILD=errno.ECHILD):
Expand Down
9 changes: 1 addition & 8 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3440,18 +3440,11 @@ def wait_process(pid, *, exitcode, timeout=None):

sleep = min(sleep * 2, max_sleep)
time.sleep(sleep)

if os.WIFEXITED(status):
exitcode2 = os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
exitcode2 = -os.WTERMSIG(status)
else:
raise ValueError(f"invalid wait status: {status!r}")
else:
# Windows implementation
pid2, status = os.waitpid(pid, 0)
exitcode2 = (status >> 8)

exitcode2 = os.waitstatus_to_exitcode(status)
if exitcode2 != exitcode:
raise AssertionError(f"process {pid} exited with code {exitcode2}, "
f"but exit code {exitcode} is expected")
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -2794,6 +2794,35 @@ def test_waitpid(self):
pid = os.spawnv(os.P_NOWAIT, FakePath(args[0]), args)
support.wait_process(pid, exitcode=0)

def test_waitstatus_to_exitcode(self):
exitcode = 23
filename = support.TESTFN
self.addCleanup(support.unlink, filename)

with open(filename, "w") as fp:
print(f'import sys; sys.exit({exitcode})', file=fp)
fp.flush()
args = [sys.executable, filename]
pid = os.spawnv(os.P_NOWAIT, args[0], args)

pid2, status = os.waitpid(pid, 0)
self.assertEqual(os.waitstatus_to_exitcode(status), exitcode)
self.assertEqual(pid2, pid)

# Skip the test on Windows
@unittest.skipUnless(hasattr(signal, 'SIGKILL'), 'need signal.SIGKILL')
def test_waitstatus_to_exitcode_kill(self):
signum = signal.SIGKILL
args = [sys.executable, '-c',
f'import time; time.sleep({support.LONG_TIMEOUT})']
pid = os.spawnv(os.P_NOWAIT, args[0], args)

os.kill(pid, signum)

pid2, status = os.waitpid(pid, 0)
self.assertEqual(os.waitstatus_to_exitcode(status), -signum)
self.assertEqual(pid2, pid)


class SpawnTests(unittest.TestCase):
def create_args(self, *, with_env=False, use_bytes=False):
Expand Down
5 changes: 3 additions & 2 deletions Lib/test/test_popen.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ def test_popen(self):

def test_return_code(self):
self.assertEqual(os.popen("exit 0").close(), None)
status = os.popen("exit 42").close()
if os.name == 'nt':
self.assertEqual(os.popen("exit 42").close(), 42)
self.assertEqual(status, 42)
else:
self.assertEqual(os.popen("exit 42").close(), 42 << 8)
self.assertEqual(os.waitstatus_to_exitcode(status), 42)

def test_contextmanager(self):
with os.popen("echo hello") as f:
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_pty.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ def test_fork(self):
## raise TestFailed("Unexpected output from child: %r" % line)

(pid, status) = os.waitpid(pid, 0)
res = status >> 8
debug("Child (%d) exited with status %d (%d)." % (pid, res, status))
res = os.waitstatus_to_exitcode(status)
debug("Child (%d) exited with code %d (status %d)." % (pid, res, status))
if res == 1:
self.fail("Child raised an unexpected exception in os.setsid()")
elif res == 2:
Expand Down
3 changes: 1 addition & 2 deletions Lib/test/test_wait3.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ def wait_impl(self, cpid, *, exitcode):
time.sleep(0.1)

self.assertEqual(spid, cpid)
self.assertEqual(status, exitcode << 8,
"cause = %d, exit = %d" % (status&0xff, status>>8))
self.assertEqual(os.waitstatus_to_exitcode(status), exitcode)
self.assertTrue(rusage)

def test_wait3_rusage_initialized(self):
Expand Down
3 changes: 1 addition & 2 deletions Lib/test/test_wait4.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ def wait_impl(self, cpid, *, exitcode):
break
time.sleep(0.1)
self.assertEqual(spid, cpid)
self.assertEqual(status, exitcode << 8,
"cause = %d, exit = %d" % (status&0xff, status>>8))
self.assertEqual(os.waitstatus_to_exitcode(status), exitcode)
self.assertTrue(rusage)

def tearDownModule():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`os.waitstatus_to_exitcode` function:
convert a wait status to an exit code.
Loading