[Toybox] toybox: xargs: add actual support for -P max-proc flag

Mark Salyzyn salyzyn at android.com
Fri Jul 31 12:30:27 PDT 2020


-P flag was fake just to provide compatibility.  Add support for -P
maximum process count to actually create parallelism.  Does not
support SIGUSR1 or SIGUSR2 increment/decrement signals as publicly
documented for other variants.  If max-proc is 0, run as many
processes as possible simultaneously.

Signed-off-by: Mark Salyzyn <salyzyn at android.com>
---
 tests/xargs.test   | 15 ++++++++++
 toys/posix/xargs.c | 70 ++++++++++++++++++++++++++++++++--------------
 2 files changed, 64 insertions(+), 21 deletions(-)

diff --git a/tests/xargs.test b/tests/xargs.test
index afed8a17..dc3c7b32 100644
--- a/tests/xargs.test
+++ b/tests/xargs.test
@@ -61,6 +61,21 @@ testing "big input forces split" \
   'for i in $(seq 1 100);do echo $X;done|{ xargs >/dev/null && echo yes;}' \
   "yes\n" "" ""
 
+# -P max-proc
+testing "max-proc=1" "xargs -n 1 -P 1" "one\ntwo\nthree\n" "" "one\ntwo\nthree\n"
+testing "max-proc=2" "xargs -n 1 -P 2" "y\ny\ny\n" "" "y\ny\ny\n"
+testing "max-proc=3" "xargs -n 1 -P 3" "y\ny\ny\n" "" "y\ny\ny\n"
+# 3 seconds vs 2 seconds vs 1 second parallel sleep
+testing "parallel sleep" "
+  xargs_sleep() {
+    ( yes | head -3 | tr y 1 | time -p xargs -n 1 \${*} sleep ) 2>&1 |
+      sed -n 's/^real *\([0-9]*\)[.]\(.*\)/\1\2/p'
+  } &&
+  A=\`xargs_sleep\` &&
+  B=\`xargs_sleep -P 2\` &&
+  C=\`xargs_sleep -P 0\` &&
+  [ \${A} -gt \${B} -a \${B} -gt \${C} ] && echo OK || echo FAIL" "OK\n" "" ""
+
 # TODO: what exactly is -x supposed to do? why does coreutils output "one"?
 #testing "-x" "xargs -x -s 9 || echo expected" "one\nexpected\n" "" "one\ntwo\nthree"
 
diff --git a/toys/posix/xargs.c b/toys/posix/xargs.c
index 79e7dea5..a1707ea7 100644
--- a/toys/posix/xargs.c
+++ b/toys/posix/xargs.c
@@ -8,15 +8,15 @@
  * TODO: -I	Insert mode
  * TODO: -L	Max number of lines of input per command
  * TODO: -x	Exit if can't fit everything in one command
- * TODO: -P NUM	Run up to NUM processes at once
+ * TODO: -P NUM	Support SIGUSR1 and SIGUSR2
 
-USE_XARGS(NEWTOY(xargs, "^E:P#optrn#<1(max-args)s#0[!0E]", TOYFLAG_USR|TOYFLAG_BIN))
+USE_XARGS(NEWTOY(xargs, "^E:P#<0=1optrn#<1(max-args)s#0[!0E]", TOYFLAG_USR|TOYFLAG_BIN))
 
 config XARGS
   bool "xargs"
   default y
   help
-    usage: xargs [-0prt] [-s NUM] [-n NUM] [-E STR] COMMAND...
+    usage: xargs [-0prt] [-s NUM] [-n NUM] [-P NUM] [-E STR] COMMAND...
 
     Run command line one or more times, appending arguments from stdin.
 
@@ -27,6 +27,7 @@ config XARGS
     -n	Max number of arguments per command
     -o	Open tty for COMMAND's stdin (default /dev/null)
     -p	Prompt for y/n from tty before running each command
+    -P	Max number of parallel processes (default 1)
     -r	Don't run command with empty input (otherwise always run command once)
     -s	Size in bytes per command line
     -t	Trace, print command line to stderr
@@ -42,6 +43,7 @@ GLOBALS(
   long entries, bytes;
   char delim;
   FILE *tty;
+  long np;
 )
 
 // If !entry count TT.bytes and TT.entries, stopping at max.
@@ -91,10 +93,47 @@ static char *handle_entries(char *data, char **entry)
   return 0;
 }
 
+static void wait_for_pid_flush() {
+  int status;
+
+  while (TT.np > 0 && waitpid(-1, &status, 0)) --TT.np;
+}
+
+// Handle process completions and report.
+//
+// pid: process to wait for, or 0 non-blocking all, -1 blocking all
+// return pid, negative pid if return code requires immediate exit
+static pid_t wait_for_pid(pid_t pid) {
+  int status;
+
+  if (TT.np <= 0) return 0;
+  pid = waitpid(pid > 0 ? pid : -1, &status, pid ? 0 : WNOHANG);
+  if (pid == 0) return pid;
+  --TT.np;
+
+  // xargs is yet another weird collection of exit value special cases,
+  // different from all the others.
+  if (WIFEXITED(status)) {
+    if (WEXITSTATUS(status) == 126 || WEXITSTATUS(status) == 127) {
+      toys.exitval = WEXITSTATUS(status);
+      wait_for_pid_flush();
+      return -pid;
+    } else if (WEXITSTATUS(status) >= 1 && WEXITSTATUS(status) <= 125) {
+      toys.exitval = 123;
+    } else if (WEXITSTATUS(status) == 255) {
+      error_msg("%s: exited with status 255; aborting", toys.optargs[0]);
+      toys.exitval = 124;
+      wait_for_pid_flush();
+      return -pid;
+    }
+  } else toys.exitval = 127;
+  return pid;
+}
+
 void xargs_main(void)
 {
   struct double_list *dlist = 0, *dtemp;
-  int entries, bytes, done = 0, status;
+  int entries, bytes, done = 0;
   char *data = 0, **out;
   pid_t pid = 0;
 
@@ -150,7 +189,7 @@ void xargs_main(void)
 
     if (!TT.entries) {
       if (data) error_exit("argument too long");
-      else if (pid) return;
+      else if (pid) break;
       else if (FLAG(r)) continue;
     }
 
@@ -174,27 +213,15 @@ void xargs_main(void)
       } else fprintf(stderr, "\n");
     }
 
+    while ((pid = wait_for_pid((TT.P > 0 && TT.np >= TT.P) ? -1 : 0)) > 0);
+    if (pid < 0) break;
+    ++TT.np;
     if (!(pid = XVFORK())) {
       close(0);
       xopen_stdio(FLAG(o) ? "/dev/tty" : "/dev/null", O_RDONLY);
       xexec(out);
     }
-    waitpid(pid, &status, 0);
-
-    // xargs is yet another weird collection of exit value special cases,
-    // different from all the others.
-    if (WIFEXITED(status)) {
-      if (WEXITSTATUS(status) == 126 || WEXITSTATUS(status) == 127) {
-        toys.exitval = WEXITSTATUS(status);
-        return;
-      } else if (WEXITSTATUS(status) >= 1 && WEXITSTATUS(status) <= 125) {
-        toys.exitval = 123;
-      } else if (WEXITSTATUS(status) == 255) {
-        error_msg("%s: exited with status 255; aborting", out[0]);
-        toys.exitval = 124;
-        return;
-      }
-    } else toys.exitval = 127;
+    if (TT.P == 1 && wait_for_pid(pid) < 0) break;
 
     // Abritrary number of execs, can't just leak memory each time...
 skip:
@@ -202,5 +229,6 @@ skip:
     dlist = 0;
     free(out);
   }
+  while (wait_for_pid(-1) > 0);
   if (TT.tty) fclose(TT.tty);
 }
-- 
2.28.0.163.g6104cc2f0b6-goog



More information about the Toybox mailing list