Skip to content

Commit ce1d0a3

Browse files
author
Andy Chu
committed
[builtin/wait] wait builtin can be interrupted.
Addresses issue #858. Also improve test case. Found the same bug in dash! bash/mksh/zsh have special cases that allow you to interrupt the 'wait' builtin. Also make note of bug with Ctrl-C / SIGINT, due to KeyboardInterrupt.
1 parent 212af20 commit ce1d0a3

File tree

5 files changed

+85
-44
lines changed

5 files changed

+85
-44
lines changed

core/process.py

+46-17
Original file line numberDiff line numberDiff line change
@@ -883,19 +883,20 @@ def Start(self, why):
883883

884884
return pid
885885

886-
def Wait(self, waiter):
887-
# type: (Waiter) -> int
886+
def Wait(self, waiter, eintr_retry=True):
887+
# type: (Waiter, bool) -> int
888888
"""Wait for this process to finish."""
889889
while True:
890-
if not waiter.WaitForOne():
890+
if not waiter.WaitForOne(eintr_retry):
891891
break
892892
if self.state != job_state_e.Running:
893893
break
894894
return self.status
895895

896896
def JobWait(self, waiter):
897897
# type: (Waiter) -> job_status_t
898-
exit_code = self.Wait(waiter)
898+
# wait builtin can be interrupted
899+
exit_code = self.Wait(waiter, eintr_retry=False)
899900
return job_status.Proc(exit_code)
900901

901902
def WhenStopped(self):
@@ -1011,8 +1012,8 @@ def LastPid(self):
10111012
"""
10121013
return self.pids[-1]
10131014

1014-
def Wait(self, waiter):
1015-
# type: (Waiter) -> List[int]
1015+
def Wait(self, waiter, eintr_retry=True):
1016+
# type: (Waiter, bool) -> List[int]
10161017
"""Wait for this pipeline to finish.
10171018
10181019
Called by the 'wait' builtin.
@@ -1022,7 +1023,7 @@ def Wait(self, waiter):
10221023
assert self.procs, "no procs for Wait()"
10231024
while True:
10241025
#log('WAIT pipeline')
1025-
if not waiter.WaitForOne():
1026+
if not waiter.WaitForOne(eintr_retry):
10261027
break
10271028
if self.state != job_state_e.Running:
10281029
#log('Pipeline DONE')
@@ -1032,7 +1033,8 @@ def Wait(self, waiter):
10321033

10331034
def JobWait(self, waiter):
10341035
# type: (Waiter) -> job_status_t
1035-
pipe_status = self.Wait(waiter)
1036+
# wait builtin can be interrupted
1037+
pipe_status = self.Wait(waiter, eintr_retry=False)
10361038
return job_status.Pipeline(pipe_status)
10371039

10381040
def Run(self, waiter, fd_state):
@@ -1288,31 +1290,58 @@ def __init__(self, job_state, exec_opts, tracer):
12881290
self.tracer = tracer
12891291
self.last_status = 127 # wait -n error code
12901292

1291-
def WaitForOne(self):
1292-
# type: () -> bool
1293+
def WaitForOne(self, eintr_retry):
1294+
# type: (bool) -> bool
12931295
"""Wait until the next process returns (or maybe Ctrl-C).
12941296
1297+
Args:
1298+
Should be True to prevent zombies
1299+
12951300
Returns:
12961301
True if we got a notification, or False if there was nothing to wait for.
12971302
12981303
In the interactive shell, we return True if we get a Ctrl-C, so the
12991304
caller will try again.
1305+
1306+
Callers:
1307+
wait -n -- WaitForOne() just once
1308+
wait -- WaitForOne() in a loop
1309+
wait $! -- job.JobWait()
1310+
1311+
Comparisons:
1312+
bash: jobs.c waitchld() Has a special case macro(!) CHECK_WAIT_INTR for
1313+
the wait builtin
1314+
1315+
dash: jobs.c waitproc() uses sigfillset(), sigprocmask(), etc. Runs in a
1316+
loop while (gotsigchld), but that might be a hack for System V!
1317+
1318+
You could imagine a clean API like posix::wait_for_one()
1319+
1320+
wait_result =
1321+
ECHILD -- nothing to wait for
1322+
| Done(int pid, int status) -- process done
1323+
| EINTR(bool sigint) -- may or may not retry
1324+
1325+
But I think we want to keep KeyboardInterrupt as an exception for now.
13001326
"""
13011327
# This is a list of async jobs
13021328
try:
1303-
# -1 makes it like wait(), which waits for any process.
1304-
# NOTE: WUNTRACED is necessary to get stopped jobs. What about
1305-
# WCONTINUED?
1329+
# Notes:
1330+
# - The arg -1 makes it like wait(), which waits for any process.
1331+
# - WUNTRACED is necessary to get stopped jobs. What about WCONTINUED?
1332+
# - Unlike other syscalls, we do NOT try on EINTR, because the 'wait'
1333+
# builtin should be interruptable. This doesn't appear to cause any
1334+
# problems for other WaitForOne callers?
13061335
pid, status = posix.waitpid(-1, WUNTRACED)
13071336
except OSError as e:
13081337
#log('wait() error: %s', e)
13091338
if e.errno == errno_.ECHILD:
13101339
return False # nothing to wait for caller should stop
1340+
elif e.errno == errno_.EINTR: # Bug #858 fix
1341+
return eintr_retry
13111342
else:
1312-
# We should never get here. EINTR was handled by the 'posix'
1313-
# module. The only other error is EINVAL, which doesn't apply to
1314-
# this call.
1315-
raise
1343+
# The signature of waitpid() means this shouldn't happen
1344+
raise AssertionError()
13161345
except KeyboardInterrupt:
13171346
# NOTE: Another way to handle this is to disable SIGINT when a process is
13181347
# running. Not sure if there's any real difference. bash and dash

demo/bug-858-trap.sh

100644100755
+24-7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,28 @@ action() {
22
echo "from signal"
33
}
44

5-
trap "action" USR1
6-
5+
trap 'action' USR1
76
echo "Run: kill -USR1 $$; date"
8-
while true; do
9-
date
10-
sleep 5 &
11-
wait $!
12-
done
7+
8+
# bash/zsh/mksh: wait is interrupted by USR1
9+
# dash: wait is NOT interrupted!
10+
11+
async_test() {
12+
while true; do
13+
date
14+
sleep 5 &
15+
wait $!
16+
echo 'next'
17+
done
18+
}
19+
20+
# bash/dash/zsh/mksh: signal handler run after sleep
21+
sync_test() {
22+
while true; do
23+
date
24+
sleep 5
25+
echo 'next'
26+
done
27+
}
28+
29+
"$@"

demo/trap.sh

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
#
33
# Demo of traps.
44

5+
# BUG: OSH doesn't run the trap because of KeyboardInterrupt!
6+
7+
# BUG interactively too:
8+
#
9+
# osh$ trap 'echo INT' SIGINT; sleep 5 & wait
10+
511
sigint-batch() {
612
trap 'echo [got SIGINT]' INT
7-
echo "Registered SIGINT trap. Run 'kill -INT $$' to see a message."
13+
echo "Registered SIGINT trap. Hit Ctrl-C or Run 'kill -INT $$' to see a message."
814
sleep 5
915
}
1016

native/posixmodule.c

+6-17
Original file line numberDiff line numberDiff line change
@@ -1668,6 +1668,7 @@ PyDoc_STRVAR_remove(posix_waitpid__doc__,
16681668
"waitpid(pid, options) -> (pid, status)\n\n\
16691669
Wait for completion of a given child process.");
16701670

1671+
// NO PATCH HERE FOR EINTR !!! See core/process.py.
16711672
static PyObject *
16721673
posix_waitpid(PyObject *self, PyObject *args)
16731674
{
@@ -1678,23 +1679,11 @@ posix_waitpid(PyObject *self, PyObject *args)
16781679

16791680
if (!PyArg_ParseTuple(args, PARSE_PID "i:waitpid", &pid, &options))
16801681
return NULL;
1681-
while (1) {
1682-
Py_BEGIN_ALLOW_THREADS
1683-
pid = waitpid(pid, &status, options);
1684-
Py_END_ALLOW_THREADS
1685-
1686-
if (pid >= 0) { // success
1687-
break;
1688-
} else {
1689-
if (PyErr_CheckSignals()) {
1690-
return NULL; // Propagate KeyboardInterrupt
1691-
}
1692-
if (errno != EINTR) { // e.g. ECHILD
1693-
return posix_error();
1694-
}
1695-
}
1696-
// Otherwise, try again on EINTR.
1697-
}
1682+
Py_BEGIN_ALLOW_THREADS
1683+
pid = waitpid(pid, &status, options);
1684+
Py_END_ALLOW_THREADS
1685+
if (pid == -1)
1686+
return posix_error();
16981687

16991688
return Py_BuildValue("Ni", PyLong_FromPid(pid), WAIT_STATUS_INT(status));
17001689
}

osh/builtin_process.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def _Run(self, cmd_val):
141141
#
142142
#log('wait next')
143143

144-
if self.waiter.WaitForOne():
144+
if self.waiter.WaitForOne(False):
145145
return self.waiter.last_status
146146
else:
147147
return 127 # nothing to wait for
@@ -155,7 +155,7 @@ def _Run(self, cmd_val):
155155
# we don't get ECHILD.
156156
# Not sure it matters since you can now Ctrl-C it.
157157

158-
if not self.waiter.WaitForOne():
158+
if not self.waiter.WaitForOne(False):
159159
break # nothing to wait for
160160
i += 1
161161
if self.job_state.NoneAreRunning():

0 commit comments

Comments
 (0)