diff options
Diffstat (limited to 'bin/pwait')
| -rw-r--r-- | bin/pwait/Makefile | 9 | ||||
| -rw-r--r-- | bin/pwait/Makefile.depend | 15 | ||||
| -rw-r--r-- | bin/pwait/pwait.1 | 155 | ||||
| -rw-r--r-- | bin/pwait/pwait.c | 260 | ||||
| -rw-r--r-- | bin/pwait/tests/Makefile | 3 | ||||
| -rw-r--r-- | bin/pwait/tests/Makefile.depend | 10 | ||||
| -rw-r--r-- | bin/pwait/tests/pwait_test.sh | 359 |
7 files changed, 811 insertions, 0 deletions
diff --git a/bin/pwait/Makefile b/bin/pwait/Makefile new file mode 100644 index 000000000000..99e6534034dc --- /dev/null +++ b/bin/pwait/Makefile @@ -0,0 +1,9 @@ +.include <src.opts.mk> + +PACKAGE=runtime +PROG= pwait + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/bin/pwait/Makefile.depend b/bin/pwait/Makefile.depend new file mode 100644 index 000000000000..6ef78fac5cbf --- /dev/null +++ b/bin/pwait/Makefile.depend @@ -0,0 +1,15 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + include \ + include/xlocale \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcompiler_rt \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/bin/pwait/pwait.1 b/bin/pwait/pwait.1 new file mode 100644 index 000000000000..d92b829b1d6a --- /dev/null +++ b/bin/pwait/pwait.1 @@ -0,0 +1,155 @@ +.\" +.\" Copyright (c) 2004-2009, Jilles Tjoelker +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with +.\" or without modification, are permitted provided that the +.\" following conditions are met: +.\" +.\" 1. Redistributions of source code must retain the above +.\" copyright notice, this list of conditions and the +.\" following disclaimer. +.\" 2. Redistributions in binary form must reproduce the +.\" above copyright notice, this list of conditions and +.\" the following disclaimer in the documentation and/or +.\" other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +.\" CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +.\" WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +.\" WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +.\" PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +.\" COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY +.\" DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +.\" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +.\" PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +.\" USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +.\" CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +.\" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +.\" NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +.\" USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +.\" OF SUCH DAMAGE. +.\" +.Dd October 22, 2025 +.Dt PWAIT 1 +.Os +.Sh NAME +.Nm pwait +.Nd wait for processes to terminate +.Sh SYNOPSIS +.Nm +.Op Fl t Ar duration +.Op Fl opv +.Ar pid +\&... +.Sh DESCRIPTION +The +.Nm +utility will wait until each of the given processes has terminated. +.Pp +The following option is available: +.Bl -tag -width indent +.It Fl o +Exit when any of the given processes has terminated. +.It Fl p +On exit, print a list of processes that have not terminated. +.It Fl t Ar duration +If any process is still running after +.Ar duration , +.Nm +will exit. +The +.Ar duration +value can be integer or decimal numbers. +Values without unit symbols are interpreted as seconds. +.Pp +Supported unit symbols are: +.Bl -tag -width indent -compact +.It s +seconds +.It m +minutes +.It h +hours +.El +.It Fl v +Print the exit status when each process terminates or +.Ql timeout +if the timer goes off earlier. +.El +.Sh EXIT STATUS +The +.Nm +utility exits 0 on success, and >0 if an error occurs. +.Pp +If the +.Fl t +flag is specified and a timeout occurs, the exit status will be 124. +.Pp +Invalid pids elicit a warning message but are otherwise ignored. +.Sh EXAMPLES +Start two +.Xr sleep 1 +processes in the background. +The first one will sleep for 30 seconds and the second one for one hour. +Wait for any of them to finish but no more than 5 seconds. +Since a timeout occurs the exit status is 124: +.Bd -literal -offset indent +$ sleep 30 & sleep 3600 & +[1] 1646 +[2] 1647 +$ pwait -o -t5 1646 1647 +$ echo $? +124 +.Ed +.Pp +Same as above but try to obtain the exit status of the processes. +In this case +.Ql timeout +is shown and the exit status is 124: +.Bd -literal -offset indent +$ sleep 30 & sleep 3600 & +[1] 1652 +[2] 1653 +$ pwait -v -t 5 1652 1653 +timeout +$ echo $? +124 +.Ed +.Pp +Start two +.Xr sleep 1 +processes in the background sleeping for 30 and 40 seconds respectively. +Wait 60 seconds for any of them to finish and get their exit codes: +.Bd -literal -offset indent +$ sleep 30 & sleep 40 & +[1] 1674 +[2] 1675 +$ pwait -v -t 60 1674 1675 +1674: exited with status 0. +1675: exited with status 0. +[1]- Done sleep 30 +[2]+ Done sleep 40 +$ echo $? +0 +.Ed +.Sh SEE ALSO +.Xr kill 1 , +.Xr pkill 1 , +.Xr ps 1 , +.Xr wait 1 , +.Xr kqueue 2 +.Sh NOTES +.Nm +is not a substitute for the +.Xr wait 1 +builtin +as it will not clean up any zombies or state in the parent process. +.Pp +To avoid deadlock, +.Nm +will ignore its own pid, if it is provided as a process id to wait for. +.Sh HISTORY +A +.Nm +command first appeared in SunOS 5.8. diff --git a/bin/pwait/pwait.c b/bin/pwait/pwait.c new file mode 100644 index 000000000000..59bf0eb93ced --- /dev/null +++ b/bin/pwait/pwait.c @@ -0,0 +1,260 @@ +/*- + * Copyright (c) 2004-2009, Jilles Tjoelker + * All rights reserved. + * + * Redistribution and use in source and binary forms, with + * or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * 2. Redistributions in binary form must reproduce the + * above copyright notice, this list of conditions and + * the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ + +#include <sys/types.h> +#include <sys/event.h> +#include <sys/sysctl.h> +#include <sys/time.h> +#include <sys/tree.h> +#include <sys/wait.h> + +#include <err.h> +#include <errno.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +struct pid { + RB_ENTRY(pid) entry; + pid_t pid; +}; + +static int +pidcmp(const struct pid *a, const struct pid *b) +{ + return (a->pid > b->pid ? 1 : a->pid < b->pid ? -1 : 0); +} + +RB_HEAD(pidtree, pid); +static struct pidtree pids = RB_INITIALIZER(&pids); +RB_GENERATE_STATIC(pidtree, pid, entry, pidcmp); + +static void +usage(void) +{ + fprintf(stderr, "usage: pwait [-t timeout] [-opv] pid ...\n"); + exit(EX_USAGE); +} + +/* + * pwait - wait for processes to terminate + */ +int +main(int argc, char *argv[]) +{ + struct itimerval itv; + struct kevent *e; + struct pid k, *p; + char *end, *s; + double timeout; + size_t sz; + long pid; + pid_t mypid; + int i, kq, n, ndone, nleft, opt, pid_max, ret, status; + bool oflag, pflag, tflag, verbose; + + oflag = false; + pflag = false; + tflag = false; + verbose = false; + memset(&itv, 0, sizeof(itv)); + + while ((opt = getopt(argc, argv, "opt:v")) != -1) { + switch (opt) { + case 'o': + oflag = true; + break; + case 'p': + pflag = true; + break; + case 't': + tflag = true; + errno = 0; + timeout = strtod(optarg, &end); + if (end == optarg || errno == ERANGE || timeout < 0) { + errx(EX_DATAERR, "timeout value"); + } + switch (*end) { + case '\0': + break; + case 's': + end++; + break; + case 'h': + timeout *= 60; + /* FALLTHROUGH */ + case 'm': + timeout *= 60; + end++; + break; + default: + errx(EX_DATAERR, "timeout unit"); + } + if (*end != '\0') { + errx(EX_DATAERR, "timeout unit"); + } + if (timeout > 100000000L) { + errx(EX_DATAERR, "timeout value"); + } + itv.it_value.tv_sec = (time_t)timeout; + timeout -= (time_t)timeout; + itv.it_value.tv_usec = + (suseconds_t)(timeout * 1000000UL); + break; + case 'v': + verbose = true; + break; + default: + usage(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) { + usage(); + } + + if ((kq = kqueue()) < 0) + err(EX_OSERR, "kqueue"); + + sz = sizeof(pid_max); + if (sysctlbyname("kern.pid_max", &pid_max, &sz, NULL, 0) != 0) { + pid_max = 99999; + } + if ((e = malloc((argc + tflag) * sizeof(*e))) == NULL) { + err(EX_OSERR, "malloc"); + } + ndone = nleft = 0; + mypid = getpid(); + for (n = 0; n < argc; n++) { + s = argv[n]; + /* Undocumented Solaris compat */ + if (strncmp(s, "/proc/", 6) == 0) { + s += 6; + } + errno = 0; + pid = strtol(s, &end, 10); + if (pid < 0 || pid > pid_max || *end != '\0' || errno != 0) { + warnx("%s: bad process id", s); + continue; + } + if (pid == mypid) { + warnx("%s: skipping my own pid", s); + continue; + } + if ((p = malloc(sizeof(*p))) == NULL) { + err(EX_OSERR, NULL); + } + p->pid = pid; + if (RB_INSERT(pidtree, &pids, p) != NULL) { + /* Duplicate. */ + free(p); + continue; + } + EV_SET(e + nleft, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); + if (kevent(kq, e + nleft, 1, NULL, 0, NULL) == -1) { + if (errno != ESRCH) + err(EX_OSERR, "kevent()"); + warn("%ld", pid); + RB_REMOVE(pidtree, &pids, p); + free(p); + ndone++; + } else { + nleft++; + } + } + + if ((ndone == 0 || !oflag) && nleft > 0 && tflag) { + /* + * Explicitly detect SIGALRM so that an exit status of 124 + * can be returned rather than 142. + */ + EV_SET(e + nleft, SIGALRM, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); + if (kevent(kq, e + nleft, 1, NULL, 0, NULL) == -1) { + err(EX_OSERR, "kevent"); + } + /* Ignore SIGALRM to not interrupt kevent(2). */ + signal(SIGALRM, SIG_IGN); + if (setitimer(ITIMER_REAL, &itv, NULL) == -1) { + err(EX_OSERR, "setitimer"); + } + } + ret = EX_OK; + while ((ndone == 0 || !oflag) && ret == EX_OK && nleft > 0) { + n = kevent(kq, NULL, 0, e, nleft + tflag, NULL); + if (n == -1) { + err(EX_OSERR, "kevent"); + } + for (i = 0; i < n; i++) { + if (e[i].filter == EVFILT_SIGNAL) { + if (verbose) { + printf("timeout\n"); + } + ret = 124; + } + pid = e[i].ident; + if (verbose) { + status = e[i].data; + if (WIFEXITED(status)) { + printf("%ld: exited with status %d.\n", + pid, WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + printf("%ld: killed by signal %d.\n", + pid, WTERMSIG(status)); + } else { + printf("%ld: terminated.\n", pid); + } + } + k.pid = pid; + if ((p = RB_FIND(pidtree, &pids, &k)) != NULL) { + RB_REMOVE(pidtree, &pids, p); + free(p); + ndone++; + } + --nleft; + } + } + if (pflag) { + RB_FOREACH(p, pidtree, &pids) { + printf("%d\n", p->pid); + } + } + exit(ret); +} diff --git a/bin/pwait/tests/Makefile b/bin/pwait/tests/Makefile new file mode 100644 index 000000000000..4e355ccfe787 --- /dev/null +++ b/bin/pwait/tests/Makefile @@ -0,0 +1,3 @@ +ATF_TESTS_SH= pwait_test + +.include <bsd.test.mk> diff --git a/bin/pwait/tests/Makefile.depend b/bin/pwait/tests/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/bin/pwait/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/bin/pwait/tests/pwait_test.sh b/bin/pwait/tests/pwait_test.sh new file mode 100644 index 000000000000..d31ca21cff93 --- /dev/null +++ b/bin/pwait/tests/pwait_test.sh @@ -0,0 +1,359 @@ + +atf_test_case basic +basic_head() +{ + atf_set "descr" "Basic tests on pwait(1) utility" +} + +basic_body() +{ + sleep 1 & + p1=$! + + sleep 5 & + p5=$! + + sleep 10 & + p10=$! + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout --preserve-status 15 pwait $p1 $p5 $p10 + + atf_check \ + -o empty \ + -e inline:"kill: $p1: No such process\n" \ + -s exit:1 \ + kill -0 $p1 + + atf_check \ + -o empty \ + -e inline:"kill: $p5: No such process\n" \ + -s exit:1 \ + kill -0 $p5 + + atf_check \ + -o empty \ + -e inline:"kill: $p10: No such process\n" \ + -s exit:1 \ + kill -0 $p10 + +} + +basic_cleanup() +{ + kill $p1 $p5 $p10 >/dev/null 2>&1 + wait $p1 $p5 $p10 >/dev/null 2>&1 +} + +atf_test_case time_unit +time_unit_head() +{ + atf_set "descr" "Test parsing the timeout unit and value" +} + +time_unit_body() +{ + init=1 + + atf_check \ + -o empty \ + -e inline:"pwait: timeout unit\n" \ + -s exit:65 \ + timeout --preserve-status 2 pwait -t 1d $init + + atf_check \ + -o empty \ + -e inline:"pwait: timeout unit\n" \ + -s exit:65 \ + timeout --preserve-status 2 pwait -t 1d $init + + atf_check \ + -o empty \ + -e inline:"pwait: timeout value\n" \ + -s exit:65 \ + timeout --preserve-status 2 pwait -t -1 $init + + atf_check \ + -o empty \ + -e inline:"pwait: timeout value\n" \ + -s exit:65 \ + timeout --preserve-status 2 pwait -t 100000001 $init + + # These long duration cases are expected to timeout from the + # timeout utility rather than pwait -t. + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status 2 pwait -t 100000000 $init + + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status 2 pwait -t 1h $init + + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status 2 pwait -t 1.5h $init + + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status 2 pwait -t 1m $init + + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status 2 pwait -t 1.5m $init + + atf_check \ + -o empty \ + -e empty \ + -s signal:15 \ + timeout --preserve-status 2 pwait -t 0 $init + + # The rest are fast enough that pwait -t is expected to trigger + # the timeout. + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 2 pwait -t 1s $init + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 2 pwait -t 1.5s $init + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 2 pwait -t 1 $init + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 2 pwait -t 1.5 $init + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 2 pwait -t 0.5 $init +} + +atf_test_case timeout_trigger_timeout +timeout_trigger_timeout_head() +{ + atf_set "descr" "Test that exceeding the timeout is detected" +} + +timeout_trigger_timeout_body() +{ + sleep 10 & + p10=$! + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 6.5 pwait -t 5 $p10 +} + +timeout_trigger_timeout_cleanup() +{ + kill $p10 >/dev/null 2>&1 + wait $p10 >/dev/null 2>&1 +} + +atf_test_case timeout_no_timeout +timeout_no_timeout_head() +{ + atf_set "descr" "Test that not exceeding the timeout continues to wait" +} + +timeout_no_timeout_body() +{ + sleep 10 & + p10=$! + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout --preserve-status 11.5 pwait -t 12 $p10 +} + +timeout_no_timeout_cleanup() +{ + kill $p10 >/dev/null 2>&1 + wait $p10 >/dev/null 2>&1 +} + +atf_test_case timeout_many +timeout_many_head() +{ + atf_set "descr" "Test timeout on many processes" +} + +timeout_many_body() +{ + sleep 1 & + p1=$! + + sleep 5 & + p5=$! + + sleep 10 & + p10=$! + + atf_check \ + -o empty \ + -e empty \ + -s exit:124 \ + timeout --preserve-status 7.5 pwait -t 6 $p1 $p5 $p10 +} + +timeout_many_cleanup() +{ + kill $p1 $p5 $p10 >/dev/null 2>&1 + wait $p1 $p5 $p10 >/dev/null 2>&1 +} + +atf_test_case or_flag +or_flag_head() +{ + atf_set "descr" "Test OR flag" +} + +or_flag_body() +{ + sleep 2 & + p2=$! + + sleep 4 & + p4=$! + + sleep 6 & + p6=$! + + atf_check \ + -o inline:"$p2: exited with status 0.\n" \ + -e empty \ + -s exit:0 \ + timeout --preserve-status 15 pwait -o -v $p2 $p4 $p6 + + atf_check \ + -o empty \ + -e inline:"pwait: $p2: No such process\n" \ + -s exit:0 \ + timeout --preserve-status 15 pwait -o $p2 $p4 $p6 + + atf_check \ + -o empty \ + -e empty \ + -s exit:0 \ + timeout --preserve-status 15 pwait -o $p4 $p6 + + atf_check \ + -o empty \ + -e inline:"pwait: $p4: No such process\n" \ + -s exit:0 \ + timeout --preserve-status 15 pwait -o $p4 $p6 + + atf_check \ + -o inline:"$p6: exited with status 0.\n" \ + -e empty \ + -s exit:0 \ + timeout --preserve-status 15 pwait -o -v $p6 + + atf_check \ + -o empty \ + -e inline:"pwait: $p6: No such process\n" \ + -s exit:0 \ + timeout --preserve-status 15 pwait -o $p6 + + atf_check \ + -o empty \ + -e inline:"kill: $p2: No such process\n" \ + -s exit:1 \ + kill -0 $p2 + + atf_check \ + -o empty \ + -e inline:"kill: $p4: No such process\n" \ + -s exit:1 \ + kill -0 $p4 + + atf_check \ + -o empty \ + -e inline:"kill: $p6: No such process\n" \ + -s exit:1 \ + kill -0 $p6 + +} + +or_flag_cleanup() +{ + kill $p2 $p4 $p6 >/dev/null 2>&1 + wait $p2 $p4 $p6 >/dev/null 2>&1 +} + +atf_test_case print +print_head() +{ + atf_set "descr" "Test the -p flag" +} + +print_body() +{ + sleep 1 & + p1=$! + + sleep 5 & + p5=$! + + sleep 10 & + p10=$! + + atf_check \ + -o inline:"$p5\n$p10\n" \ + -s exit:124 \ + pwait -t 2 -p $p10 $p5 $p1 $p5 $p10 + + atf_check \ + -e inline:"kill: $p1: No such process\n" \ + -s exit:1 \ + kill -0 $p1 + + atf_check kill -0 $p5 + atf_check kill -0 $p10 +} + +print_cleanup() +{ + kill $p1 $p5 $p10 >/dev/null 2>&1 + wait $p1 $p5 $p10 >/dev/null 2>&1 +} + +atf_init_test_cases() +{ + atf_add_test_case basic + atf_add_test_case time_unit + atf_add_test_case timeout_trigger_timeout + atf_add_test_case timeout_no_timeout + atf_add_test_case timeout_many + atf_add_test_case or_flag + atf_add_test_case print +} |
