summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:28:04 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:28:04 +0300
commit681e61b5ab23760db91aea6b30cd38ef3526e308 (patch)
tree5227011b83263515d7f292cbd872bacb01e58c7c /corebinutils
parent76166d4f612e1318ffe99493f9d8b578887b1278 (diff)
parent2dea7718929906912270d2d02cc19fbbf2848579 (diff)
downloadProject-Tick-681e61b5ab23760db91aea6b30cd38ef3526e308.tar.gz
Project-Tick-681e61b5ab23760db91aea6b30cd38ef3526e308.zip
Add 'corebinutils/pwait/' from commit '2dea7718929906912270d2d02cc19fbbf2848579'
git-subtree-dir: corebinutils/pwait git-subtree-mainline: 76166d4f612e1318ffe99493f9d8b578887b1278 git-subtree-split: 2dea7718929906912270d2d02cc19fbbf2848579
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/pwait/.gitignore3
-rw-r--r--corebinutils/pwait/GNUmakefile37
-rw-r--r--corebinutils/pwait/LICENSE23
-rw-r--r--corebinutils/pwait/LICENSES/BSD-2-Clause.txt9
-rw-r--r--corebinutils/pwait/README.md45
-rw-r--r--corebinutils/pwait/pwait.1156
-rw-r--r--corebinutils/pwait/pwait.c332
-rw-r--r--corebinutils/pwait/tests/test.sh149
8 files changed, 754 insertions, 0 deletions
diff --git a/corebinutils/pwait/.gitignore b/corebinutils/pwait/.gitignore
new file mode 100644
index 0000000000..3193332093
--- /dev/null
+++ b/corebinutils/pwait/.gitignore
@@ -0,0 +1,3 @@
+build/
+out/
+*.o
diff --git a/corebinutils/pwait/GNUmakefile b/corebinutils/pwait/GNUmakefile
new file mode 100644
index 0000000000..5c441b20d7
--- /dev/null
+++ b/corebinutils/pwait/GNUmakefile
@@ -0,0 +1,37 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS ?=
+CPPFLAGS += -D_GNU_SOURCE
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/pwait
+OBJS := $(OBJDIR)/pwait.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/pwait.o: $(CURDIR)/pwait.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/pwait.c" -o "$@"
+
+test: $(TARGET)
+ CC="$(CC)" PWAIT_BIN="$(TARGET)" \
+ sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/pwait/LICENSE b/corebinutils/pwait/LICENSE
new file mode 100644
index 0000000000..252a612dfb
--- /dev/null
+++ b/corebinutils/pwait/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2004-2009, Jilles Tjoelker
+Copyright (c) 2026 Project Tick. 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 REGENTS 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 REGENTS 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.
diff --git a/corebinutils/pwait/LICENSES/BSD-2-Clause.txt b/corebinutils/pwait/LICENSES/BSD-2-Clause.txt
new file mode 100644
index 0000000000..5f662b354c
--- /dev/null
+++ b/corebinutils/pwait/LICENSES/BSD-2-Clause.txt
@@ -0,0 +1,9 @@
+Copyright (c) <year> <owner>
+
+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 HOLDER 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.
diff --git a/corebinutils/pwait/README.md b/corebinutils/pwait/README.md
new file mode 100644
index 0000000000..d32860c8fb
--- /dev/null
+++ b/corebinutils/pwait/README.md
@@ -0,0 +1,45 @@
+# pwait
+
+Standalone Linux-native port of FreeBSD `pwait` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Port Strategy
+
+| BSD mechanism | Linux replacement |
+|---|---|
+| `EVFILT_PROC` / `NOTE_EXIT` (kqueue) | `pidfd_open(2)` + `poll(2)` with `waitid(2)` |
+| `setitimer(2)` + `SIGALRM` | `timerfd_create(2)` + `poll(2)` |
+| `RB_TREE` indexing | Dynamic struct array sorted with `qsort(3)` |
+
+### API Mapping
+
+- Instead of BSD's kqueue `EVFILT_PROC` which can notify on arbitrary process exit, we rely on Linux's `pidfd_open()` which creates a file descriptor representing a process. We then use `poll()` on the array of pidfds.
+- When `poll()` indicates the pidfd is readable (`POLLIN`), we use `waitid(P_PIDFD, fd, &info, WEXITED | WNOHANG)` to get the exit status, which only works if the target process is a child of `pwait`. For unrelated processes, `waitid()` will fail with `ECHILD` but we still know the process exited because the pidfd signaled `POLLIN`.
+- Timeouts in the original implementation used `setitimer` and `SIGALRM`. Here, we use `timerfd` and wait on it along with the `pidfd` array inside the same `poll()` loop.
+
+## Supported Semantics
+
+- Waiting for one or more process IDs to terminate.
+- Outputting the remaining running processes on exit with `-p`.
+- Exiting on the first target termination with `-o`.
+- Printing the exit code or termination signal of the targets with `-v`.
+- Timeout mechanism with `-t` supporting `s`, `m`, and `h` units.
+
+## Unsupported / Not Available on Linux
+
+All BSD semantics from `pwait.1` are cleanly supported in this Linux port. No known major incompatibilities, apart from minor differences in internal implementation limits (e.g. `kern.pid_max` fallback uses `INT_MAX`, preventing hard limitations).
+
+- `waitid` will only fetch the exit status of **children** of the `pwait` process on Linux. For unrelated processes, `pwait -v` will print `terminated.` instead of `exited with status X.` because the Linux kernel does not allow reading another user process's exact exit code cleanly without `PTRACE` or it being a child.
diff --git a/corebinutils/pwait/pwait.1 b/corebinutils/pwait/pwait.1
new file mode 100644
index 0000000000..9e0844b235
--- /dev/null
+++ b/corebinutils/pwait/pwait.1
@@ -0,0 +1,156 @@
+.\"
+.\" Copyright (c) 2004-2009, Jilles Tjoelker
+.\" Copyright (c) 2026 Project Tick.
+.\" 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/corebinutils/pwait/pwait.c b/corebinutils/pwait/pwait.c
new file mode 100644
index 0000000000..323a798e86
--- /dev/null
+++ b/corebinutils/pwait/pwait.c
@@ -0,0 +1,332 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2004-2009, Jilles Tjoelker
+ * Copyright (c) 2026 Project Tick.
+ * 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/syscall.h>
+#include <sys/timerfd.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <limits.h>
+#include <stdint.h>
+
+static int
+x_pidfd_open(pid_t pid)
+{
+#if defined(__NR_pidfd_open)
+ return syscall(__NR_pidfd_open, pid, 0);
+#elif defined(SYS_pidfd_open)
+ return syscall(SYS_pidfd_open, pid, 0);
+#else
+ errno = ENOSYS;
+ return -1;
+#endif
+}
+
+// We need to keep track of pids, whether they are done, and their open pidfd
+struct target_pid {
+ long pid;
+ int fd;
+ bool done;
+};
+
+static int
+pidcmp(const void *a, const void *b)
+{
+ const struct target_pid *pa = a;
+ const struct target_pid *pb = b;
+ return (pa->pid > pb->pid ? 1 : pa->pid < pb->pid ? -1 : 0);
+}
+
+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 target_pid *targets = NULL;
+ char *end, *s;
+ double timeout = 0.0;
+ long pid;
+ pid_t mypid;
+ int ntargets = 0, nallocated = 0;
+ int ndone = 0, nleft = 0, opt, ret;
+ bool oflag = false, pflag = false, tflag = false, verbose = false;
+
+ 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");
+ }
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ usage();
+ /* NOTREACHED */
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc == 0) {
+ usage();
+ }
+
+ mypid = getpid();
+ long pid_max = 4194304;
+ FILE *f = fopen("/proc/sys/kernel/pid_max", "r");
+ if (f != NULL) {
+ if (fscanf(f, "%ld", &pid_max) != 1) {
+ pid_max = 4194304;
+ }
+ fclose(f);
+ }
+
+ for (int 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;
+ }
+
+ /* Check for duplicates */
+ bool dup = false;
+ for (int i = 0; i < ntargets; i++) {
+ if (targets[i].pid == pid) {
+ dup = true;
+ break;
+ }
+ }
+ if (dup) {
+ continue;
+ }
+
+ if (ntargets >= nallocated) {
+ nallocated = nallocated == 0 ? 16 : nallocated * 2;
+ targets = realloc(targets, nallocated * sizeof(struct target_pid));
+ if (targets == NULL) {
+ err(EX_OSERR, "realloc");
+ }
+ }
+
+ targets[ntargets].pid = pid;
+ targets[ntargets].done = false;
+ int fd = x_pidfd_open((pid_t)pid);
+ if (fd == -1) {
+ /* Not found or invalid */
+ warn("%ld", pid);
+ targets[ntargets].fd = -1;
+ targets[ntargets].done = true;
+ ndone++;
+ } else {
+ targets[ntargets].fd = fd;
+ nleft++;
+ }
+ ntargets++;
+ }
+
+ /* Sort targets ascending by PID to match BSD behavior */
+ qsort(targets, ntargets, sizeof(struct target_pid), pidcmp);
+
+ int timer_fd = -1;
+ if ((ndone == 0 || !oflag) && nleft > 0 && tflag) {
+ timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
+ if (timer_fd == -1) {
+ err(EX_OSERR, "timerfd_create");
+ }
+ struct itimerspec its = {};
+ its.it_value.tv_sec = (time_t)timeout;
+ its.it_value.tv_nsec = (long)((timeout - its.it_value.tv_sec) * 1000000000UL);
+ if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) {
+ /* timerfd needs non-zero value to arm */
+ its.it_value.tv_nsec = 1;
+ }
+ if (timerfd_settime(timer_fd, 0, &its, NULL) == -1) {
+ err(EX_OSERR, "timerfd_settime");
+ }
+ }
+
+ struct pollfd *pfds = calloc(ntargets + 1, sizeof(struct pollfd));
+ if (pfds == NULL) {
+ err(EX_OSERR, "calloc");
+ }
+
+ ret = EX_OK;
+ while ((ndone == 0 || !oflag) && ret == EX_OK && nleft > 0) {
+ int npoll = 0;
+ if (timer_fd != -1) {
+ pfds[npoll].fd = timer_fd;
+ pfds[npoll].events = POLLIN;
+ pfds[npoll].revents = 0;
+ npoll++;
+ }
+
+ for (int i = 0; i < ntargets; i++) {
+ if (!targets[i].done) {
+ pfds[npoll].fd = targets[i].fd;
+ pfds[npoll].events = POLLIN | POLLHUP | POLLERR;
+ pfds[npoll].revents = 0;
+ npoll++;
+ }
+ }
+
+ int n = poll(pfds, npoll, -1);
+ if (n == -1) {
+ if (errno == EINTR) {
+ continue;
+ }
+ err(EX_OSERR, "poll");
+ }
+
+ if (timer_fd != -1 && (pfds[0].revents & POLLIN)) {
+ uint64_t expirations;
+ (void)read(timer_fd, &expirations, sizeof(expirations));
+ if (verbose) {
+ printf("timeout\n");
+ }
+ ret = 124;
+ }
+
+ int pfds_idx = timer_fd != -1 ? 1 : 0;
+ for (int i = 0; i < ntargets; i++) {
+ if (targets[i].done) {
+ continue;
+ }
+ if (pfds[pfds_idx].revents & (POLLIN | POLLHUP | POLLERR)) {
+ pid = targets[i].pid;
+ if (verbose) {
+ siginfo_t info = {};
+ int wret = waitid(P_PIDFD, targets[i].fd, &info, WEXITED | WNOHANG);
+ if (wret == 0 && info.si_pid != 0) {
+ if (info.si_code == CLD_EXITED) {
+ printf("%ld: exited with status %d.\n",
+ pid, info.si_status);
+ } else if (info.si_code == CLD_KILLED || info.si_code == CLD_DUMPED) {
+ printf("%ld: killed by signal %d.\n",
+ pid, info.si_status);
+ } else {
+ printf("%ld: terminated.\n", pid);
+ }
+ } else {
+ printf("%ld: terminated.\n", pid);
+ }
+ }
+ close(targets[i].fd);
+ targets[i].fd = -1;
+ targets[i].done = true;
+ ndone++;
+ nleft--;
+ }
+ pfds_idx++;
+ }
+ }
+
+ if (timer_fd != -1) {
+ close(timer_fd);
+ }
+ free(pfds);
+
+ if (pflag) {
+ for (int i = 0; i < ntargets; i++) {
+ if (!targets[i].done) {
+ printf("%ld\n", targets[i].pid);
+ }
+ }
+ }
+
+ free(targets);
+ exit(ret);
+}
diff --git a/corebinutils/pwait/tests/test.sh b/corebinutils/pwait/tests/test.sh
new file mode 100644
index 0000000000..745a50f4ef
--- /dev/null
+++ b/corebinutils/pwait/tests/test.sh
@@ -0,0 +1,149 @@
+#!/bin/sh
+
+# tests/test.sh for pwait Linux port
+
+set -eu
+
+# Fallbacks for paths
+CC="${CC:-cc}"
+PWAIT_BIN="${PWAIT_BIN:-$(pwd)/out/pwait}"
+
+fail() {
+ echo "FAIL: $*" >&2
+ exit 1
+}
+
+assert_exit_code() {
+ local expected=$1
+ shift
+ local cmd="$@"
+ set +e
+ eval "$cmd" > /tmp/pwait_test.out 2> /tmp/pwait_test.err
+ local st=$?
+ set -e
+ if [ "$st" != "$expected" ]; then
+ echo "Expected exit code $expected, got $st for: $cmd" >&2
+ cat /tmp/pwait_test.out >&2
+ cat /tmp/pwait_test.err >&2
+ fail "exit code assertion failed"
+ fi
+}
+
+assert_stdout() {
+ local expected_str="$1"
+ shift
+ local cmd="$@"
+ set +e
+ eval "$cmd" > /tmp/pwait_test.out 2> /tmp/pwait_test.err
+ local st=$?
+ set -e
+ local out=$(cat /tmp/pwait_test.out)
+ if ! echo "$out" | grep -qF "$expected_str"; then
+ echo "Expected stdout to contain '$expected_str' for: $cmd" >&2
+ echo "Got stdout:" >&2
+ cat /tmp/pwait_test.out >&2
+ fail "stdout assertion failed"
+ fi
+}
+
+assert_stderr() {
+ local expected_str="$1"
+ shift
+ local cmd="$@"
+ set +e
+ eval "$cmd" > /tmp/pwait_test.out 2> /tmp/pwait_test.err
+ local st=$?
+ set -e
+ local err=$(cat /tmp/pwait_test.err)
+ if ! echo "$err" | grep -qF "$expected_str"; then
+ echo "Expected stderr to contain '$expected_str' for: $cmd" >&2
+ echo "Got stderr:" >&2
+ cat /tmp/pwait_test.err >&2
+ fail "stderr assertion failed"
+ fi
+}
+
+echo "=== Basic tests ==="
+sleep 1 &
+p1=$!
+sleep 3 &
+p3=$!
+
+assert_exit_code 0 timeout 10 "$PWAIT_BIN" $p1 $p3
+
+echo "=== Timeout tests ==="
+sleep 10 &
+p10=$!
+
+assert_exit_code 124 timeout 15 "$PWAIT_BIN" -t 1 $p10
+assert_exit_code 0 timeout 15 "$PWAIT_BIN" -t 12 $p10
+
+kill $p10 2>/dev/null || true
+wait $p10 2>/dev/null || true
+
+echo "=== OR (-o) tests ==="
+sleep 1 &
+p1=$!
+sleep 10 &
+p10=$!
+assert_exit_code 0 timeout 4 "$PWAIT_BIN" -o $p1 $p10
+# $p10 might still be running
+kill $p10 2>/dev/null || true
+
+echo "=== Missing PID tests ==="
+# Give an invalid process ID, should err and consider it done
+assert_stderr "999999" timeout 2 "$PWAIT_BIN" 999999
+assert_exit_code 0 timeout 2 "$PWAIT_BIN" 999999
+
+echo "=== -p (print remaining) tests ==="
+sleep 1 &
+p1=$!
+sleep 10 &
+p10=$!
+# Wait for 3 sec, p1 finishes, p10 remains
+assert_stdout "$p10" timeout 5 "$PWAIT_BIN" -t 3 -p $p1 $p10
+kill $p10 2>/dev/null || true
+
+echo "=== -v (verbose exit) tests ==="
+sleep 1 &
+p1=$!
+# It's a child process because we started it from our shell script. However, pwait doesn't own it.
+# So pwait will print `terminated.` instead of exact exit code.
+assert_stdout "$p1: terminated" timeout 3 "$PWAIT_BIN" -v $p1
+
+echo "=== ENOSYS (Mock failure) test ==="
+# We can't easily mock ENOSYS for pidfd_open from shell.
+# We'll skip it in automated tests for now unless we LD_PRELOAD.
+
+echo "=== timeout=0 test ==="
+sleep 2 &
+p2=$!
+assert_exit_code 124 timeout 2 "$PWAIT_BIN" -t 0 $p2
+kill $p2 2>/dev/null || true
+
+echo "=== Duplicate PID test ==="
+sleep 1 &
+p1=$!
+# wait for duplicate PID, should just wait normally once
+assert_exit_code 0 timeout 4 "$PWAIT_BIN" $p1 $p1
+
+echo "=== Killed by signal test ==="
+sleep 10 &
+p10=$!
+# We don't trace child signals for unrelated procs,
+# but we can check if pwait returns successfully when the target dies from a signal.
+kill -9 $p10 2>/dev/null || true
+assert_exit_code 0 timeout 2 "$PWAIT_BIN" $p10
+
+echo "=== Mixed valid/invalid test ==="
+sleep 1 &
+p1=$!
+sleep 10 &
+# 999999 invalid
+assert_stderr "bad process id" timeout 2 "$PWAIT_BIN" abc xyz || true
+assert_stderr "999999" timeout 2 "$PWAIT_BIN" 999999 $p1
+# wait should still succeed as soon as valid process exits
+assert_exit_code 0 timeout 4 "$PWAIT_BIN" 999999 $p1
+
+echo "ALL TESTS PASSED."
+exit 0