diff --git a/bin/pwait/pwait.1 b/bin/pwait/pwait.1 index 9310b74d5683..c61f6854358b 100644 --- a/bin/pwait/pwait.1 +++ b/bin/pwait/pwait.1 @@ -1,153 +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 January 21, 2021 +.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 ov +.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 $? 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 $? 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 index b2fea04c1b72..6e9e634b4526 100644 --- a/bin/pwait/pwait.c +++ b/bin/pwait/pwait.c @@ -1,229 +1,261 @@ /*- * 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 #include #include +#include #include +#include #include #include #include #include #include #include #include #include #include #include +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] [-ov] pid ...\n"); + 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, nleft, opt, status; - bool oflag, tflag, verbose; + 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, "ot:v")) != -1) { + while ((opt = getopt(argc, argv, "opt:v")) != -1) { switch (opt) { case 'o': - oflag = 1; + 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(); } - kq = kqueue(); - if (kq == -1) { + if ((kq = kqueue()) < 0) err(EX_OSERR, "kqueue"); - } - e = malloc((argc + tflag) * sizeof(struct kevent)); - if (e == NULL) { + 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"); } - nleft = 0; + 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 || *end != '\0' || errno != 0) { + 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; } - for (i = 0; i < nleft; i++) { - if (e[i].ident == (uintptr_t)pid) { - break; - } + if ((p = malloc(sizeof(*p))) == NULL) { + err(EX_OSERR, NULL); } - if (i < nleft) { + 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); - if (oflag) { - exit(EX_OK); - } + RB_REMOVE(pidtree, &pids, p); + free(p); + ndone++; } else { nleft++; } } - if (nleft > 0 && tflag) { + 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"); } } - while (nleft > 0) { + 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"); } - exit(124); + ret = 124; } + pid = e[i].ident; if (verbose) { status = e[i].data; if (WIFEXITED(status)) { printf("%ld: exited with status %d.\n", - (long)e[i].ident, - WEXITSTATUS(status)); + pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("%ld: killed by signal %d.\n", - (long)e[i].ident, - WTERMSIG(status)); + pid, WTERMSIG(status)); } else { - printf("%ld: terminated.\n", - (long)e[i].ident); + printf("%ld: terminated.\n", pid); } } - if (oflag) { - exit(EX_OK); + k.pid = pid; + if ((p = RB_FIND(pidtree, &pids, &k)) != NULL) { + RB_REMOVE(pidtree, &pids, p); + free(p); + ndone++; } --nleft; } } - - exit(EX_OK); + if (pflag) { + RB_FOREACH(p, pidtree, &pids) { + printf("%d\n", p->pid); + } + } + exit(ret); } diff --git a/bin/pwait/tests/pwait_test.sh b/bin/pwait/tests/pwait_test.sh index 66bdd6981704..d31ca21cff93 100644 --- a/bin/pwait/tests/pwait_test.sh +++ b/bin/pwait/tests/pwait_test.sh @@ -1,321 +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 }