[PATCH v2] ash: fix wait -n behavior for multiple PIDs and pipe jobs
Luiz Angelo Daros de Luca via busybox <[email protected]>
| Newsgroups | gmane.linux.busybox |
|---|---|
| Message-ID | <[email protected]> |
- "wait -n $p1 $p2" should return when the FIRST of the listed jobs finishes, not wait for all of them (Bugzilla 15970). - "wait -n" with a pipe job returns only when the entire job completes (e.g., `(sleep 1; exit 1) | (sleep 2; exit 2) & wait -n` returns 2). - "wait -n" with no arguments now correctly finds older finished jobs instead of blocking on the newest running job (e.g., `(exit 42) & sleep 1; sleep 5 & wait -n` now returns 42 immediately instead of blocking for 5 seconds and returning 0). - "wait -n" with no arguments now correctly returns 127 if all finished jobs have already been waited for (e.g., `(exit 42) & wait; wait -n` now returns 127 instead of 0). - Added comprehensive test cases: wait8..12.tests, with wait8 and wait9 from Dominique Martinet previous patch series. Link: https://www.mail-archive.com/[email protected]/msg30397.html Link: https://bugs.busybox.net/show_bug.cgi?id=15970 Co-developed-by: Dominique Martinet <[email protected]> Signed-off-by: Luiz Angelo Daros de Luca <[email protected]> --- v1->v2: - Fixed wait -n state management: In the "wait without IDs" loop, the code now uses continue instead of returning immediately after a process exits. Returning immediately caused the job to never be marked as waited, leading to subsequent wait -n calls incorrectly returning the same status. By continuing the loop, the shell performs its standard job-scanning logic, which correctly marks the job as waited and then returns the status. - Added wait8.tests and wait9.tests from Dominique patches. - Renamed existing tests for consistency (wait_n1 -> wait10, wait_n2 -> wait11) - Added wait12.tests specifically for wait -n $p1 $p2 (multiple positional arguments) shell/ash.c | 181 ++++++++++++++++++++++----- shell/ash_test/ash-misc/wait10.right | 16 +++ shell/ash_test/ash-misc/wait10.tests | 62 +++++++++ shell/ash_test/ash-misc/wait11.right | 3 + shell/ash_test/ash-misc/wait11.tests | 5 + shell/ash_test/ash-misc/wait12.right | 3 + shell/ash_test/ash-misc/wait12.tests | 18 +++ shell/ash_test/ash-misc/wait8.right | 6 + shell/ash_test/ash-misc/wait8.tests | 61 +++++++++ shell/ash_test/ash-misc/wait9.right | 6 + shell/ash_test/ash-misc/wait9.tests | 21 ++++ 11 files changed, 350 insertions(+), 32 deletions(-) create mode 100644 shell/ash_test/ash-misc/wait10.right create mode 100755 shell/ash_test/ash-misc/wait10.tests create mode 100644 shell/ash_test/ash-misc/wait11.right create mode 100755 shell/ash_test/ash-misc/wait11.tests create mode 100644 shell/ash_test/ash-misc/wait12.right create mode 100755 shell/ash_test/ash-misc/wait12.tests create mode 100644 shell/ash_test/ash-misc/wait8.right create mode 100755 shell/ash_test/ash-misc/wait8.tests create mode 100644 shell/ash_test/ash-misc/wait9.right create mode 100755 shell/ash_test/ash-misc/wait9.tests diff --git a/shell/ash.c b/shell/ash.c index fb887f31b..61811bc08 100644 --- a/shell/ash.c +++ b/shell/ash.c @@ -3844,7 +3844,8 @@ struct job { #endif waited: 1, /* true if this entry has been waited for */ used: 1, /* true if this entry is in used */ - changed: 1; /* true if status has changed */ + changed: 1, /* true if status has changed */ + wait_n: 1; /* wait -n is interested in this job */ struct job *prev_job; /* previous job */ }; @@ -4462,9 +4463,17 @@ static int waitone(int block, struct job *job) #if BASH_WAIT_N if (want_jobexitstatus) { - pid = -1; - if (thisjob && thisjob->state == JOBDONE) - pid = thisjob->ps[thisjob->nprocs - 1].ps_status; + if (pid > 0) { + /* Reaped a child. If its job is done, return its status. + * If not done yet, return -2 (indicates "reaped, but job still running"). + */ + int exit_status = -2; + if (thisjob && thisjob->state == JOBDONE) + exit_status = thisjob->ps[thisjob->nprocs - 1].ps_status; + pid = exit_status; + } else if (pid == 0) { + pid = -1; /* No child reaped, prevent returning 0 (success) */ + } } #endif if (thisjob && thisjob == job) { @@ -4493,10 +4502,30 @@ static int dowait(int block, struct job *jp) if (block == DOWAIT_NONBLOCK && !gotchld) return 1; + /* rpid tracks if we reaped a child (pid > 0) in every waitone call. + * If any waitone call returns 0 (e.g. signal received but no child reaped), + * rpid will become 0. It stays 1 if all calls reaped a child or if the + * loop was not entered. + */ rpid = 1; +#if BASH_WAIT_N + again: +#endif do { pid = waitone(block, jp); +#if BASH_WAIT_N + if (block & DOWAIT_JOBSTATUS) { + if (pid >= 0) + return pid; + if (pid == -2) + goto again; + if (block == DOWAIT_NONBLOCK || pending_sig) + return -1; + goto again; + } +#endif + /* rpid remains 1 only if all waitone calls returned pid > 0 */ rpid &= !!pid; if (!pid || (jp && jp->state != JOBRUNNING)) @@ -4808,33 +4837,69 @@ stoppedjobs(void) static int FAST_FUNC waitcmd(int argc UNUSED_PARAM, char **argv) { - struct job *job; int retval; struct job *jp; #if BASH_WAIT_N int status; char one = nextopt("n"); + bool any_waitable = false; #else nextopt(nullstr); #endif retval = 0; argv = argptr; + /* wait without IDs */ if (!argv[0]) { /* wait for all jobs / one job if -n */ for (;;) { - jp = curjob; #if BASH_WAIT_N - if (one && !jp) - /* exitcode of "wait -n" with nothing to wait for is 127, not 0 */ - retval = 127; + bool found_running = false; #endif + jp = curjob; while (1) { - if (!jp) /* no running procs */ + if (!jp) { /* no more jobs */ +#if BASH_WAIT_N + if (one) { + if (found_running) + /* if we have a running job, we can wait for it. + * exit while(1), will call dowait aftewards + */ + break; + /* without jobs, exitcode of "wait -n" is 127, not 0 */ + retval = 127; + } +#endif goto ret; - if (jp->state == JOBRUNNING) - break; - jp->waited = 1; + } + + /* non-running jobs are first in the list. For standard "wait", + * when we reach a running one, we can stop scanning and block. + */ +#if BASH_WAIT_N + if (one) { + if (jp->state != JOBRUNNING) { + /* when we have a non-running job not reported yet, we can + * return "wait -n" immediately + */ + if (!jp->waited) { + jp->waited = 1; + retval = getstatus(jp); + goto ret_n; + } + } else { + found_running = true; + } + } else +#endif + { + if (jp->state == JOBRUNNING) + break; + jp->waited = 1; + } + /* check the remaining jobs for a running job (to wait for) + * or a non-running job to mark as waited or return if '-n' + */ jp = jp->prev_job; } /* man bash: @@ -4862,40 +4927,92 @@ waitcmd(int argc UNUSED_PARAM, char **argv) * should wait for 2 seconds. Not 1 or 3. */ if (status != -1 && !WIFSTOPPED(status)) { - retval = WEXITSTATUS(status); - if (WIFSIGNALED(status)) - retval = 128 | WTERMSIG(status); - goto ret; + continue; } } #endif } } - retval = 127; +#if BASH_WAIT_N + if (one) + retval = 127; +#endif + + /* wait with IDs */ do { - if (**argv != '%') { + if (**argv == '%') { + jp = getjob(*argv, 0); + } else { pid_t pid = number(*argv); - job = curjob; - while (1) { - if (!job) - goto repeat; - if (job->ps[job->nprocs - 1].ps_pid == pid) - break; - job = job->prev_job; + jp = curjob; + while (jp && jp->ps[jp->nprocs - 1].ps_pid != pid) + jp = jp->prev_job; + } + +#if BASH_WAIT_N + if (one) { + if (jp) { + if (jp->state != JOBRUNNING) { + /* when we have a non-running job, we can + * return "wait -n" immediately. + * + * Bash returns the job status if the non-running job + * exists, even if it was already waited. It only returns + * 127 if the job was reaped. In Bash, the job cleanup + * moment might be influenced by whether it runs as a + * command (-c), a script or interactive. + */ + jp->waited = 1; + retval = getstatus(jp); + goto ret_n; + } else { + any_waitable = true; + jp->wait_n = 1; + } } - } else { - job = getjob(*argv, 0); + continue; } +#endif + if (!jp) + continue; + /* loop until process terminated or stopped */ - dowait(DOWAIT_CHILD_OR_SIG, job); + dowait(DOWAIT_CHILD_OR_SIG, jp); if (pending_sig) goto sigout; - job->waited = 1; - retval = getstatus(job); - repeat: ; + jp->waited = 1; + retval = getstatus(jp); } while (*++argv); +#if BASH_WAIT_N + if (one) { + if (!any_waitable) + goto ret_n; /* retval is 127 */ + + /* Wait for any job to finish */ + for (;;) { + if (dowait(DOWAIT_CHILD_OR_SIG | DOWAIT_JOBSTATUS, NULL) == -1) { + retval = 127; + break; + } + if (pending_sig) + goto sigout; + /* Check if any of our marked jobs is done */ + for (jp = curjob; jp; jp = jp->prev_job) { + if (jp->wait_n && jp->state == JOBDONE && !jp->waited) { + jp->waited = 1; + retval = getstatus(jp); + goto ret_n; + } + } + } + ret_n: + /* Unmark all jobs */ + for (jp = curjob; jp; jp = jp->prev_job) jp->wait_n = 0; + } +#endif + ret: return retval; sigout: diff --git a/shell/ash_test/ash-misc/wait10.right b/shell/ash_test/ash-misc/wait10.right new file mode 100644 index 000000000..edae8d87d --- /dev/null +++ b/shell/ash_test/ash-misc/wait10.right @@ -0,0 +1,16 @@ +Wait 1 returned: 42. (expects 42) +p1 active: 0 (expects 0) +p2 active: 1 (expects 1) +Wait 2 returned: 41 (expects 41) +Wait 3 returned: 41 (expects 41) +Wait 4 returned: 127 (expects 127) +Wait 5 returned: 42 (expects 42) +Wait 6 returned: 42 (expects 42) +Wait 7 returned: 42 (expects 42) +Wait 8 returned: 42 (expects 42) +Wait 9 returned: 42 (expects 42) +Wait 10 returned: 42 (expects 42) +Wait 11 returned: 127 (expects 127) +Wait 12 returned: 42 (expects 42) +p7 active: 0 (expects 0) +Wait 13 returned: 127 (expects 127) diff --git a/shell/ash_test/ash-misc/wait10.tests b/shell/ash_test/ash-misc/wait10.tests new file mode 100755 index 000000000..8864649b7 --- /dev/null +++ b/shell/ash_test/ash-misc/wait10.tests @@ -0,0 +1,62 @@ +# Wait for any of p1, p2. p2 finishes first. +(sleep 2; exit 41) & p1=$! +(sleep 1; exit 42) & p2=$! +wait -n $p1 $p2 +echo "Wait 1 returned: $?. (expects 42)" +kill -0 $p1 2>/dev/null; echo "p1 active: $? (expects 0)" +kill -0 $p2 2>/dev/null; echo "p2 active: $? (expects 1)" + +# wait $p1 +wait $p1 +echo "Wait 2 returned: $? (expects 41)" + +# wait -n $p1 again. +# bash as script returns 41 while command (-c) returns 127 +wait -n $p1 +echo "Wait 3 returned: $? (expects 41)" + +# wait -n (no args) with only already-waited jobs should return 127 +wait -n +echo "Wait 4 returned: $? (expects 127)" +# Test: wait -n with a job that finished before wait was called +(sleep 1; exit 42) & p3=$! +sleep 2 +# p3 is done but not reaped. wait -n should return 42. +wait -n $p3 +echo "Wait 5 returned: $? (expects 42)" +# wait -n $p3 again should return 42 +wait -n $p3 +echo "Wait 6 returned: $? (expects 42)" +# wait $p3 again should return 42 +wait $p3 +echo "Wait 7 returned: $? (expects 42)" +# wait -n $p3 should return 42 +wait -n $p3 +echo "Wait 8 returned: $? (expects 42)" + +# Test: wait -n with multiple already finished jobs +(sleep 1; exit 42) & p4=$! +(sleep 1; exit 42) & p5=$! +sleep 2 +# Both p4 and p5 are done. +wait -n +echo "Wait 9 returned: $? (expects 42)" +wait -n +echo "Wait 10 returned: $? (expects 42)" +wait -n +echo "Wait 11 returned: $? (expects 127)" + +# Test: wait -n with a buried finished job (p6 done, p7 running) +# This used to deadlock. +(sleep 1; exit 42) & p6=$! +(sleep 5; exit 41) & p7=$! +sleep 2 +wait -n +echo "Wait 12 returned: $? (expects 42)" +# p7 should still be running +kill -0 $p7 2>/dev/null; echo "p7 active: $? (expects 0)" +wait $p7 + +# Test: wait -n with no jobs at all +wait -n +echo "Wait 13 returned: $? (expects 127)" diff --git a/shell/ash_test/ash-misc/wait11.right b/shell/ash_test/ash-misc/wait11.right new file mode 100644 index 000000000..95e266646 --- /dev/null +++ b/shell/ash_test/ash-misc/wait11.right @@ -0,0 +1,3 @@ +P1 done +P2 done +Wait returned: 0 diff --git a/shell/ash_test/ash-misc/wait11.tests b/shell/ash_test/ash-misc/wait11.tests new file mode 100755 index 000000000..e41b90d85 --- /dev/null +++ b/shell/ash_test/ash-misc/wait11.tests @@ -0,0 +1,5 @@ +# Testing wait -n with a pipe +# It should wait for the WHOLE job. +(sleep 1; echo "P1 done" >&2) | (sleep 3; echo "P2 done" >&2) & +wait -n +echo "Wait returned: $?" diff --git a/shell/ash_test/ash-misc/wait12.right b/shell/ash_test/ash-misc/wait12.right new file mode 100644 index 000000000..20e4b1cfb --- /dev/null +++ b/shell/ash_test/ash-misc/wait12.right @@ -0,0 +1,3 @@ +First wait -n returned: 42 (expects 42) +Second wait -n returned: 41 (expects 41) +Third wait -n returned: 43 (expects 43) diff --git a/shell/ash_test/ash-misc/wait12.tests b/shell/ash_test/ash-misc/wait12.tests new file mode 100755 index 000000000..11162f6b0 --- /dev/null +++ b/shell/ash_test/ash-misc/wait12.tests @@ -0,0 +1,18 @@ +# Test wait -n with multiple PIDs +# It should return when ANY of the specified jobs finishes. + +(sleep 2; exit 41) & p1=$! +(sleep 1; exit 42) & p2=$! +(sleep 3; exit 43) & p3=$! + +# Should wait for p2 (1s) and return 42 +wait -n $p1 $p2 +echo "First wait -n returned: $? (expects 42)" + +# p1 should still be running. wait -n for p1 or p3 should return when p1 finishes (after another 1s) +wait -n $p1 $p3 +echo "Second wait -n returned: $? (expects 41)" + +# wait -n for p3 (still running) +wait -n $p3 +echo "Third wait -n returned: $? (expects 43)" diff --git a/shell/ash_test/ash-misc/wait8.right b/shell/ash_test/ash-misc/wait8.right new file mode 100644 index 000000000..e3ebde532 --- /dev/null +++ b/shell/ash_test/ash-misc/wait8.right @@ -0,0 +1,6 @@ +Test1 (wait with zero exit status of bg process) +Test2 (wait with non-zero exit status of bg process) +Test3 (pipeline wait for whole job) +Ok:0 +Test4 (wait exit code with signal) +Test5 (wait -n exits 127 if nothing to wait for) diff --git a/shell/ash_test/ash-misc/wait8.tests b/shell/ash_test/ash-misc/wait8.tests new file mode 100755 index 000000000..85fd55c2c --- /dev/null +++ b/shell/ash_test/ash-misc/wait8.tests @@ -0,0 +1,61 @@ +# wait -n tests + +# should never wait for this one +sleep 5 && echo "Background1: BUG!" & #A +bg=$! + +# normal process +echo "Test1 (wait with zero exit status of bg process)" +sleep 1 & #B +wait -n #B, A still 4s to end +rc=$? +if [ "$rc" != 0 ]; then + echo "Test1 exit code was not 0: $rc" +fi + +# non-zero return code +echo "Test2 (wait with non-zero exit status of bg process)" +sh -c 'sleep 1; false' & #C +pid2=$! +wait -n #C, #A still 3s to end +rc=$? +if [ "$rc" != 1 ]; then + echo "Test2 exit code was not 1: $rc" +fi + +wait $pid2 # asking to wait for a waited pid is prone to racing condition as it can both return C exit status, if the process was already cleaned, or 0 if already cleaned (or 127 if -n). A is still running +rc=$? +if [ "$rc" != 1 ]; then + echo "Test2 exit code was not 1 on recheck: $rc" +fi + +# pipeline... not too fast +echo "Test3 (pipeline wait for whole job)" +d1=$(date +%s) +sleep 2 | sleep 1 & #D +wait -n #D, A is still 2s to end +rc=$? +if [ "$rc" != 0 ]; then + echo "Test3 exit code was not 0: $rc" +fi +d2=$(date +%s) +if [ "$d2" -lt "$((d1 + 2))" ]; then + echo "Test3 finished in less than 2s" +fi + +kill $bg # kill A +echo Ok:$? + +echo "Test4 (wait exit code with signal)" +wait -n #A +rc=$? +if [ "$rc" != 143 ]; then + echo "Test4 exit code was not 143 (128+SIGTERM): $rc" +fi + +echo "Test5 (wait -n exits 127 if nothing to wait for)" +wait -n #none +rc=$? +if [ "$rc" != 127 ]; then + echo "wait -n with nothing to wait was not 127: $rc" +fi diff --git a/shell/ash_test/ash-misc/wait9.right b/shell/ash_test/ash-misc/wait9.right new file mode 100644 index 000000000..23db7738d --- /dev/null +++ b/shell/ash_test/ash-misc/wait9.right @@ -0,0 +1,6 @@ +wait -n +from USR1 +prints after USR1 +wait +from USR1 +prints after USR1 (2) diff --git a/shell/ash_test/ash-misc/wait9.tests b/shell/ash_test/ash-misc/wait9.tests new file mode 100755 index 000000000..3dda2e133 --- /dev/null +++ b/shell/ash_test/ash-misc/wait9.tests @@ -0,0 +1,21 @@ +# tests wait and signals + +trap 'echo from USR1' USR1 + +echo "wait -n" +{ sleep 1; kill -USR1 $$; sleep 1; echo prints after USR1; } & +wait -n +rc=$? +[ "$rc" = 138 ] || echo "rc not 138: $?" +wait -n +rc=$? +[ "$rc" = 0 ] || echo "rc not 0: $?" + +echo "wait" +{ sleep 1; kill -USR1 $$; sleep 1; echo "prints after USR1 (2)"; } & +wait +rc=$? +[ "$rc" = 138 ] || echo "rc not 138: $?" +wait +rc=$? +[ "$rc" = 0 ] || echo "rc not 0: $?" -- 2.53.0