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
55 changes: 55 additions & 0 deletions trio/_subprocess/linux_waitpid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import attr
import functools
import os
import outcome
from typing import Any

from .. import _core
from .._sync import Event
from .._threads import run_sync_in_worker_thread


@attr.s
class WaitpidState:
pid = attr.ib()
event = attr.ib(default=attr.Factory(Event))
outcome = attr.ib(default=None)


# https://github.com/python-trio/trio/issues/618
class StubLimiter:
def release_on_behalf_of(self, x):
pass

async def acquire_on_behalf_of(self, x):
pass


waitpid_limiter = StubLimiter()


# adapted from
# https://github.com/python-trio/trio/issues/4#issuecomment-398967572
async def _task(state: WaitpidState) -> None:
"""The waitpid thread runner task. This must be spawned as a system
task."""
partial = functools.partial(
os.waitpid, # function
state.pid, # pid
0 # no options
)

tresult = await run_sync_in_worker_thread(
outcome.capture, partial, cancellable=True, limiter=waitpid_limiter
)
state.outcome = tresult
state.event.set()


async def waitpid(pid: int) -> Any:
"""Waits for a child process with the specified PID to finish running."""
waiter = WaitpidState(pid=pid)
_core.spawn_system_task(_task, waiter)

await waiter.event.wait()
return waiter.outcome.unwrap()
Copy link
Member

Choose a reason for hiding this comment

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

I guess we'll eventually need to split this up into two functions, but that's fine, no particular reason to do that now.

39 changes: 39 additions & 0 deletions trio/tests/subprocess/test_waitpid_linux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import sys

import os
import pytest
import signal

from ... import _core
from ..._subprocess.linux_waitpid import waitpid

pytestmark = pytest.mark.skipif(
sys.platform != "linux", reason="linux waitpid only works on linux"
)


async def test_waitpid():
pid = os.spawnvp(os.P_NOWAIT, "/bin/false", ("false",))
result = await waitpid(pid)
# exit code is a 16-bit int: (code, signal)
Copy link
Member

Choose a reason for hiding this comment

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

In theory it's a bit more complicated than that, there's a "was a core dumped?" flag in there and the ability to distinguish between stop signals and termination signals (see).

In practice this is a pretty pedantic distinction. If we want to be really POSIX-ly correct, though, I guess the tests should make assertions like

assert os.WIFEXITED(code) and os.WEXITSTATUS(code) == 1
assert os.WIFEXITED(code) and os.WEXITSTATUS(code) == 0
assert os.WIFSIGNALED(code) and os.WTERMSIG(code) == signal.SIGKILL

?

assert result[0] == pid
assert os.WIFEXITED(result[1]) and os.WEXITSTATUS(result[1]) == 1

pid2 = os.spawnvp(os.P_NOWAIT, "/bin/true", ("true",))
result = await waitpid(pid2)
assert result[0] == pid2
assert os.WIFEXITED(result[1]) and os.WEXITSTATUS(result[1]) == 0

pid3 = os.spawnvp(os.P_NOWAIT, "/bin/sleep", ("/bin/sleep", "5"))
os.kill(pid3, signal.SIGKILL)
result = await waitpid(pid3)
assert result[0] == pid3
status = os.WTERMSIG(result[1])
assert os.WIFSIGNALED(result[1]) and status == 9


async def test_waitpid_no_process():
with pytest.raises(ChildProcessError):
# this PID does exist, but it's ourselves
# which doesn't work
await waitpid(os.getpid())