Skip to content
Permalink
Browse files
bpo-40094: Add os.waitstatus_to_exitcode() (GH-19201)
Add os.waitstatus_to_exitcode() function to convert a wait status to an
exitcode.

Suggest waitstatus_to_exitcode() usage in the documentation when
appropriate.

Use waitstatus_to_exitcode() in:

* multiprocessing, os, subprocess and _bootsubprocess modules;
* test.support.wait_process();
* setup.py: run_command();
* and many tests.
  • Loading branch information
vstinner committed Apr 1, 2020
1 parent 5dd8360 commit 65a796e5272f61b42792d3a8c69686558c1872c5
Show file tree
Hide file tree
Showing 18 changed files with 258 additions and 61 deletions.
@@ -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.
@@ -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.
@@ -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.
@@ -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
@@ -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.


@@ -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
@@ -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
@@ -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
-------

@@ -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:
@@ -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

@@ -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}")
@@ -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)
@@ -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):
@@ -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
@@ -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):
@@ -3442,18 +3442,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")
@@ -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):
@@ -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:
@@ -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:
@@ -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):
@@ -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():
@@ -0,0 +1,2 @@
Add :func:`os.waitstatus_to_exitcode` function:
convert a wait status to an exit code.

0 comments on commit 65a796e

Please sign in to comment.