Skip to content
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
4 changes: 4 additions & 0 deletions newsfragments/1315.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The `trio.Process.returncode` attribute is now automatically updated
as needed, instead of only when you call `~trio.Process.poll` or
`~trio.Process.wait`. Also, ``repr(process_object)`` now always
contains up-to-date information about the process status.
47 changes: 27 additions & 20 deletions trio/_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,30 +133,37 @@ def __init__(self, popen, stdin, stdout, stderr):
self.pid = self._proc.pid

def __repr__(self):
if self.returncode is None:
returncode = self.returncode
if returncode is None:
status = "running with PID {}".format(self.pid)
else:
if self.returncode < 0:
status = "exited with signal {}".format(-self.returncode)
if returncode < 0:
status = "exited with signal {}".format(-returncode)
else:
status = "exited with status {}".format(self.returncode)
status = "exited with status {}".format(returncode)
return "<trio.Process {!r}: {}>".format(self.args, status)

@property
def returncode(self):
"""The exit status of the process (an integer), or ``None`` if it is
not yet known to have exited.
"""The exit status of the process (an integer), or ``None`` if it's
still running.

By convention, a return code of zero indicates success. On
UNIX, negative values indicate termination due to a signal,
e.g., -11 if terminated by signal 11 (``SIGSEGV``). On
Windows, a process that exits due to a call to
:meth:`Process.terminate` will have an exit status of 1.

Accessing this attribute does not check for termination;
use :meth:`poll` or :meth:`wait` for that.
Unlike the standard library `subprocess.Popen.returncode`, you don't
have to call `poll` or `wait` to update this attribute; it's
automatically updated as needed, and will always give you the latest
information.

"""
return self._proc.returncode
result = self._proc.poll()
if result is not None:
self._close_pidfd()
return result

async def aclose(self):
"""Close any pipes we have to the process (both input and output)
Expand All @@ -175,7 +182,7 @@ async def aclose(self):
try:
await self.wait()
finally:
if self.returncode is None:
if self._proc.returncode is None:
self.kill()
with trio.CancelScope(shield=True):
await self.wait()
Expand Down Expand Up @@ -205,20 +212,20 @@ async def wait(self):
# actually block for a tiny fraction of a second.
self._proc.wait()
self._close_pidfd()
assert self.returncode is not None
return self.returncode
assert self._proc.returncode is not None
return self._proc.returncode

def poll(self):
"""Check if the process has exited yet.
"""Returns the exit status of the process (an integer), or ``None`` if
it's still running.

Note that on Trio (unlike the standard library `subprocess.Popen`),
``process.poll()`` and ``process.returncode`` always give the same
result. See `returncode` for more details. This method is only
included to make it easier to port code from `subprocess`.

Returns:
The exit status of the process, or ``None`` if it is still
running; see :attr:`returncode`.
"""
result = self._proc.poll()
if result is not None:
self._close_pidfd()
return result
return self.returncode

def send_signal(self, sig):
"""Send signal ``sig`` to the process.
Expand Down
11 changes: 11 additions & 0 deletions trio/tests/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async def test_basic():
assert repr(proc) == repr_template.format(
"running with PID {}".format(proc.pid)
)
assert proc._pidfd is None
assert proc.returncode == 0
assert repr(proc) == repr_template.format("exited with status 0")

Expand All @@ -58,6 +59,16 @@ async def test_basic():
)


async def test_auto_update_returncode():
p = await open_process(SLEEP(9999))
assert p.returncode is None
p.kill()
p._proc.wait()
assert p.returncode is not None
assert p._pidfd is None
assert p.returncode is not None


async def test_multi_wait():
async with await open_process(SLEEP(10)) as proc:
# Check that wait (including multi-wait) tolerates being cancelled
Expand Down