[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.