diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:27:44 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:27:44 +0300 |
| commit | e99e87d16cdba1de0cfd84ce1a231ad3371e43db (patch) | |
| tree | b0fb3898bdcd7ece9ebd4769b6e3695f1fd022f4 | |
| parent | 4f84a681e5fc545999076703b5920d4c4cf29a78 (diff) | |
| parent | c07449c1ae05a076b9e554267513c794c86e3ba5 (diff) | |
| download | Project-Tick-e99e87d16cdba1de0cfd84ce1a231ad3371e43db.tar.gz Project-Tick-e99e87d16cdba1de0cfd84ce1a231ad3371e43db.zip | |
Add 'corebinutils/pkill/' from commit 'c07449c1ae05a076b9e554267513c794c86e3ba5'
git-subtree-dir: corebinutils/pkill
git-subtree-mainline: 4f84a681e5fc545999076703b5920d4c4cf29a78
git-subtree-split: c07449c1ae05a076b9e554267513c794c86e3ba5
| -rw-r--r-- | corebinutils/pkill/.gitignore | 3 | ||||
| -rw-r--r-- | corebinutils/pkill/GNUmakefile | 47 | ||||
| -rw-r--r-- | corebinutils/pkill/LICENSE | 26 | ||||
| -rw-r--r-- | corebinutils/pkill/LICENSES/BSD-2-Clause.txt | 9 | ||||
| -rw-r--r-- | corebinutils/pkill/README.md | 69 | ||||
| -rw-r--r-- | corebinutils/pkill/pkill.1 | 430 | ||||
| -rw-r--r-- | corebinutils/pkill/pkill.c | 1278 | ||||
| -rw-r--r-- | corebinutils/pkill/tests/spin_helper.c | 190 | ||||
| -rw-r--r-- | corebinutils/pkill/tests/test.sh | 730 |
9 files changed, 2782 insertions, 0 deletions
diff --git a/corebinutils/pkill/.gitignore b/corebinutils/pkill/.gitignore new file mode 100644 index 0000000000..3193332093 --- /dev/null +++ b/corebinutils/pkill/.gitignore @@ -0,0 +1,3 @@ +build/ +out/ +*.o diff --git a/corebinutils/pkill/GNUmakefile b/corebinutils/pkill/GNUmakefile new file mode 100644 index 0000000000..29cb49ca59 --- /dev/null +++ b/corebinutils/pkill/GNUmakefile @@ -0,0 +1,47 @@ +.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)/pkill +PGREP := $(OUTDIR)/pgrep +OBJS := $(OBJDIR)/pkill.o + +HELPER_SRC := $(CURDIR)/tests/spin_helper.c +HELPER_BIN := $(OUTDIR)/spin_helper + +.PHONY: all clean dirs status test + +all: $(TARGET) $(PGREP) $(HELPER_BIN) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(PGREP): $(TARGET) | dirs + ln -sf pkill "$@" + +$(OBJDIR)/pkill.o: $(CURDIR)/pkill.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/pkill.c" -o "$@" + +$(HELPER_BIN): $(HELPER_SRC) | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) "$<" -o "$@" $(LDLIBS) + +test: $(TARGET) $(PGREP) $(HELPER_BIN) + CC="$(CC)" PKILL_BIN="$(TARGET)" PGREP_BIN="$(PGREP)" \ + HELPER_BIN="$(HELPER_BIN)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/pkill/LICENSE b/corebinutils/pkill/LICENSE new file mode 100644 index 0000000000..067cf5500a --- /dev/null +++ b/corebinutils/pkill/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2002 The NetBSD Foundation, Inc. +Copyright (c) 2005 Pawel Jakub Dawidek <pjd@FreeBSD.org> + +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/pkill/LICENSES/BSD-2-Clause.txt b/corebinutils/pkill/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..5f662b354c --- /dev/null +++ b/corebinutils/pkill/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/pkill/README.md b/corebinutils/pkill/README.md new file mode 100644 index 0000000000..28eed178aa --- /dev/null +++ b/corebinutils/pkill/README.md @@ -0,0 +1,69 @@ +# pkill / pgrep + +Standalone Linux-native port of FreeBSD `pkill`/`pgrep` for Project Tick BSD/Linux Distribution. + +Both `pkill` and `pgrep` are built from the same binary; `pgrep` is installed as a symlink. + +## 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 | +|---|---| +| `kvm_openfiles(3)` / `kvm_getprocs(3)` | `/proc` directory enumeration | +| `struct kinfo_proc` | `/proc/[pid]/stat` + `/proc/[pid]/status` + `/proc/[pid]/cmdline` | +| `sys_signame[]` / `NSIG` | Static signal table (same as `bin/kill` port) | +| `SLIST_*` (BSD queue macros) | Dynamic arrays (`realloc`-grown) | +| `getprogname(3)` | `argv[0]` basename via `strrchr` | +| `err(3)` / `warn(3)` | Kept: `err.h` is POSIX-compatible on Linux | +| `flock(2)` for `-L` | `flock(2)` is also available on Linux | + +### /proc field mapping + +- `pid`, `ppid`, `pgrp`, `sid`, `tdev`: parsed from `/proc/[pid]/stat` fields +- `ruid`, `euid`, `rgid`, `egid`: parsed from `/proc/[pid]/status` (`Uid:`/`Gid:` lines) +- Process start time (`starttime`): field 22 of `/proc/[pid]/stat` (for `-n`/`-o`) +- Command name (`comm`): the `(comm)` field of `/proc/[pid]/stat`; kernel truncates to 15 characters +- Full command line: `/proc/[pid]/cmdline` (NUL-separated, joined with spaces for `-f`) +- Kernel thread detection: empty `/proc/[pid]/cmdline` in a non-zombie process + +## Supported Semantics + +- `pgrep`/`pkill` with pattern (ERE), `-f`, `-i`, `-x`, `-v` +- `-l` long output, `-d delim` delimiter (pgrep only), `-q` quiet (pgrep only) +- `-n` newest, `-o` oldest +- `-P ppid`, `-u euid`, `-U ruid`, `-G rgid`, `-g pgrp`, `-s sid` +- `-t tty`: resolves `/dev/ttyXX`, `/dev/pts/N`; `-` matches no-tty processes +- `-F pidfile`: read target PID from file +- `-L`: verify pidfile is locked (daemon is running) +- `-a` include ancestors, `-I` interactive confirmation (pkill only) +- `-S` include kernel threads (pgrep only) +- Signal specification: `-SIGTERM`, `-TERM`, `-15`, `-s`, `-USR1`, `RTMIN`, `RTMIN+N`, `RTMAX-N` + +## Unsupported / Not Available on Linux + +| Option | Reason | +|---|---| +| `-j jail` | FreeBSD jail IDs have no Linux equivalent; exits with explicit error | +| `-c class` | FreeBSD login class (`ki_loginclass`) has no Linux equivalent; exits with explicit error | +| `-M core` | Requires `kvm(3)` for core file analysis; exits with explicit error | +| `-N system` | Requires `kvm(3)` for name list extraction; exits with explicit error | +| `ki_comm` > 15 chars | Linux kernel truncates `comm` to 15 chars; use `-f` for full cmdline matching | + +## Known Limits + +- `comm` matching is limited to the **first 15 characters** of the executable name (Linux kernel invariant). Use `-f` to match against the full argument list. +- Kernel threads are excluded by default (no cmdline). Use `-S` with `pgrep` to include them. +- TTY matching uses device numbers from `/proc/[pid]/stat` field `tty_nr`. On some container environments this field may be 0 for all processes; `-t` will produce no matches in that case. diff --git a/corebinutils/pkill/pkill.1 b/corebinutils/pkill/pkill.1 new file mode 100644 index 0000000000..7b82588cf0 --- /dev/null +++ b/corebinutils/pkill/pkill.1 @@ -0,0 +1,430 @@ +.\" $NetBSD: pkill.1,v 1.8 2003/02/14 15:59:18 grant Exp $ +.\" +.\" Copyright (c) 2002 The NetBSD Foundation, Inc. +.\" All rights reserved. +.\" +.\" Copyright (c) 2026 +.\" Project Tick. All rights reserved. +.\" +.\" This code is derived from software contributed to The NetBSD Foundation +.\" by Andrew Doran. +.\" +.\" 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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 June 24, 2025 +.Dt PKILL 1 +.Os +.Sh NAME +.Nm pgrep , pkill +.Nd find or signal processes by name +.Sh SYNOPSIS +.Nm pgrep +.Op Fl LSafilnoqvx +.Op Fl F Ar pidfile +.Op Fl G Ar gid +.Op Fl M Ar core +.Op Fl N Ar system +.Op Fl P Ar ppid +.Op Fl U Ar uid +.Op Fl c Ar class +.Op Fl d Ar delim +.Op Fl g Ar pgrp +.Op Fl j Ar jail +.Op Fl s Ar sid +.Op Fl t Ar tty +.Op Fl u Ar euid +.Ar pattern ... +.Nm pkill +.Op Fl Ar signal +.Op Fl ILafilnovx +.Op Fl F Ar pidfile +.Op Fl G Ar gid +.Op Fl M Ar core +.Op Fl N Ar system +.Op Fl P Ar ppid +.Op Fl U Ar uid +.Op Fl c Ar class +.Op Fl g Ar pgrp +.Op Fl j Ar jail +.Op Fl s Ar sid +.Op Fl t Ar tty +.Op Fl u Ar euid +.Ar pattern ... +.Sh DESCRIPTION +The +.Nm pgrep +command searches the process table on the running system and prints the +process IDs of all processes that match the criteria given on the command +line, excluding itself and all direct ancestors unless the +.Fl a +option is specified. +.Pp +The +.Nm pkill +command searches the process table on the running system and signals all +processes that match the criteria given on the command line, excluding itself +and all direct ancestors unless the +.Fl a +option is specified. +.Pp +The following options are available: +.Bl -tag -width ".Fl F Ar pidfile" +.It Fl F Ar pidfile +Restrict matches to a process whose PID is stored in the +.Ar pidfile +file. +.It Fl G Ar gid +Restrict matches to processes with a real group ID in the comma-separated +list +.Ar gid . +.It Fl I +Request confirmation before attempting to signal each process. +.It Fl L +The +.Ar pidfile +file given for the +.Fl F +option must be locked with the +.Xr flock 2 +syscall or created with +.Xr pidfile 3 . +.It Fl M Ar core +Extract values associated with the name list from the specified core +instead of the currently running system. +.It Fl N Ar system +Extract the name list from the specified system instead of the default, +which is the kernel image the system has booted from. +.It Fl P Ar ppid +Restrict matches to processes with a parent process ID in the +comma-separated list +.Ar ppid . +.It Fl S +Search also in system processes (kernel threads). +.It Fl U Ar uid +Restrict matches to processes with a real user ID in the comma-separated +list +.Ar uid . +.It Fl d Ar delim +Specify a delimiter to be printed between each process ID. +The default is a newline. +This option can only be used with the +.Nm pgrep +command. +.It Fl a +Include process ancestors in the match list. +By default, the current +.Nm pgrep +or +.Nm pkill +process and all of its ancestors are excluded (unless +.Fl v +is used). +Note that the +.Fl a +option will not +.Dq unhide +the +.Nm pgrep +or +.Nm pkill +process itself, even with +.Fl v . +.It Fl c Ar class +Restrict matches to processes running with specified login class +.Ar class . +.It Fl f +Match against full argument lists. +The default is to match against process names. +.It Fl g Ar pgrp +Restrict matches to processes with a process group ID in the comma-separated +list +.Ar pgrp . +The value zero is taken to mean the process group ID of the running +.Nm pgrep +or +.Nm pkill +command. +.It Fl i +Ignore case distinctions in both the process table and the supplied pattern. +.It Fl j Ar jail +Restrict matches to processes inside the specified jails. +The argument +.Ar jail +may be +.Dq Li any +to match processes in any jail, +.Dq Li none +to match processes not in jail, +or a comma-separated list of jail IDs or names. +.It Fl l +Long output. +For +.Nm pgrep , +print the process name in addition to the process ID for each matching +process. +If used in conjunction with +.Fl f , +print the process ID and the full argument list for each matching process. +For +.Nm pkill , +display the kill command used for each process killed. +.It Fl n +Select only the newest (most recently started) of the matching processes. +.It Fl o +Select only the oldest (least recently started) of the matching processes. +.It Fl q +For +.Nm pgrep , +Do not write anything to standard output. +.It Fl s Ar sid +Restrict matches to processes with a session ID in the comma-separated +list +.Ar sid . +The value zero is taken to mean the session ID of the running +.Nm pgrep +or +.Nm pkill +command. +.It Fl t Ar tty +Restrict matches to processes associated with a terminal in the +comma-separated list +.Ar tty . +Terminal names may be of the form +.Pa tty Ns Ar xx +or the shortened form +.Ar xx . +A single dash +.Pq Ql - +matches processes not associated with a terminal. +.It Fl u Ar euid +Restrict matches to processes with an effective user ID in the +comma-separated list +.Ar euid . +.It Fl v +Reverse the sense of the matching; display processes that do not match the +given criteria. +.It Fl x +Require an exact match of the process name, or argument list if +.Fl f +is given. +The default is to match any substring. +.It Fl Ns Ar signal +A non-negative decimal number or symbolic signal name specifying the signal +to be sent instead of the default +.Dv TERM . +This option is valid only when given as the first argument to +.Nm pkill . +.El +.Pp +If any +.Ar pattern +operands are specified, they are used as extended regular expressions to match +the command name or full argument list of each process. +If the +.Fl f +option is not specified, then the +.Ar pattern +will attempt to match the command name. +However, presently +.Fx +will only keep track of the first 19 characters of the command +name for each process. +Attempts to match any characters after the first 19 of a command name +will quietly fail. +.Pp +Note that a running +.Nm pgrep +or +.Nm pkill +process will never consider itself nor system processes (kernel threads) as +a potential match. +.Sh IMPLEMENTATION NOTES +The Sun Solaris implementation utilised procfs to obtain process information. +This implementation utilises +.Xr kvm 3 +instead. +On a live system, +.Xr kvm 3 +uses +.Va kern.proc +MIB to obtain the list of processes, kernel memory through +.Pa /dev/kmem +is not accessed. +.Sh EXIT STATUS +The +.Nm pgrep +and +.Nm pkill +utilities +return one of the following values upon exit: +.Bl -tag -width indent +.It 0 +One or more processes were matched. +.It 1 +No processes were matched. +.It 2 +Invalid options were specified on the command line. +.It 3 +An internal error occurred. +.El +.Sh EXAMPLES +Show the pid of the process holding the +.Pa /tmp/.X0-lock +pid file: +.Bd -literal -offset indent +$ pgrep -F /tmp/.X0-lock +1211 +.Ed +.Pp +Show the pid and the name of the process including kernel threads in the +search: +.Bd -literal -offset indent +$ pgrep -lS vnlru +37 vnlru +.Ed +.Pp +Search for processes including kernel threads that match the extended regular +expression pattern: +.Bd -literal -offset indent +$ pgrep -S 'crypto.*[2-3]' +20 +19 +6 +5 +.Ed +.Pp +Show long output for firefox processes: +.Bd -literal -offset indent +$ pgrep -l firefox +1312 firefox +1309 firefox +1288 firefox +1280 firefox +1279 firefox +1278 firefox +1277 firefox +1264 firefox +.Ed +.Pp +Same as above but just showing the pid of the most recent process: +.Bd -literal -offset indent +$ pgrep -n firefox +1312 +.Ed +.Pp +Look for vim processes. +Match against the full argument list: +.Bd -literal -offset indent +$ pgrep -f vim +44968 +30790 +.Ed +.Pp +Same as above but matching against the +.Ql list +word and showing the full argument list: +.Bd -literal -offset indent +$ pgrep -f -l list +30790 vim list.txt +.Ed +.Pp +Send +.Va SIGSTOP +signal to processes that are an exact match: +.Bd -literal -offset indent +$ pkill -SIGSTOP -f -x "vim list.txt" +.Ed +.Pp +Without +.Fl f +names over 19 characters will silently fail: +.Bd -literal -offset indent +$ vim this_is_a_very_long_file_name & +[1] 36689 +$ + +[1]+ Stopped vim this_is_a_very_long_file_name +$ pgrep "vim this" +$ +.Ed +.Pp +Same as above using the +.Fl f +flag: +.Bd -literal -offset indent +$ pgrep -f "vim this" +36689 +.Ed +.Pp +Find the +.Xr top 1 +command running in any jail: +.Bd -literal -offset indent +$ pgrep -j any top +34498 +.Ed +.Pp +Show all processes running in jail ID 58: +.Bd -literal -offset indent +$ pgrep -l -j58 '.*' +28397 pkg-static +28396 pkg-static +28255 sh +28254 make +.Ed +.Sh COMPATIBILITY +Historically the option +.Dq Fl j Li 0 +means any jail, although in other utilities such as +.Xr ps 1 +jail ID +.Li 0 +has the opposite meaning, not in jail. +Therefore +.Dq Fl j Li 0 +is deprecated, and its use is discouraged in favor of +.Dq Fl j Li any . +.Sh SEE ALSO +.Xr kill 1 , +.Xr killall 1 , +.Xr ps 1 , +.Xr flock 2 , +.Xr kill 2 , +.Xr sigaction 2 , +.Xr kvm 3 , +.Xr pidfile 3 , +.Xr re_format 7 +.\" Xr signal 7 +.Sh HISTORY +The +.Nm pkill +and +.Nm pgrep +utilities +first appeared in +.Nx 1.6 . +They are modelled after utilities of the same name that appeared in Sun +Solaris 7. +They made their first appearance in +.Fx 5.3 . +.Sh AUTHORS +.An Andrew Doran Aq Mt ad@NetBSD.org diff --git a/corebinutils/pkill/pkill.c b/corebinutils/pkill/pkill.c new file mode 100644 index 0000000000..91710b70f1 --- /dev/null +++ b/corebinutils/pkill/pkill.c @@ -0,0 +1,1278 @@ +/* $NetBSD: pkill.c,v 1.16 2005/10/10 22:13:20 kleink Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2002 The NetBSD Foundation, Inc. + * Copyright (c) 2005 Pawel Jakub Dawidek <pjd@FreeBSD.org> + * Copyright (c) 2026 Project Tick. All rights reserved. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Andrew Doran. + * + * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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. + * + * Linux-native port: replaces kvm(3) process enumeration with /proc + * filesystem parsing. FreeBSD jail (-j), login class (-c), and core file + * analysis (-M/-N) options are not available on Linux and produce explicit + * errors. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <ctype.h> +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <limits.h> +#include <locale.h> +#include <pwd.h> +#include <regex.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/file.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +/* ------------------------------------------------------------------ */ +/* Exit status codes (match manpage) */ +/* ------------------------------------------------------------------ */ +#define STATUS_MATCH 0 +#define STATUS_NOMATCH 1 +#define STATUS_BADUSAGE 2 +#define STATUS_ERROR 3 + +/* ------------------------------------------------------------------ */ +/* Signal name table */ +/* ------------------------------------------------------------------ */ +struct signal_entry { + const char *name; + int number; +}; + +#define SIG_ENTRY(n) { #n, SIG##n } + +static const struct signal_entry signals[] = { +#ifdef SIGHUP + SIG_ENTRY(HUP), +#endif +#ifdef SIGINT + SIG_ENTRY(INT), +#endif +#ifdef SIGQUIT + SIG_ENTRY(QUIT), +#endif +#ifdef SIGILL + SIG_ENTRY(ILL), +#endif +#ifdef SIGTRAP + SIG_ENTRY(TRAP), +#endif +#ifdef SIGABRT + SIG_ENTRY(ABRT), +#endif +#ifdef SIGBUS + SIG_ENTRY(BUS), +#endif +#ifdef SIGFPE + SIG_ENTRY(FPE), +#endif +#ifdef SIGKILL + SIG_ENTRY(KILL), +#endif +#ifdef SIGUSR1 + SIG_ENTRY(USR1), +#endif +#ifdef SIGSEGV + SIG_ENTRY(SEGV), +#endif +#ifdef SIGUSR2 + SIG_ENTRY(USR2), +#endif +#ifdef SIGPIPE + SIG_ENTRY(PIPE), +#endif +#ifdef SIGALRM + SIG_ENTRY(ALRM), +#endif +#ifdef SIGTERM + SIG_ENTRY(TERM), +#endif +#ifdef SIGSTKFLT + SIG_ENTRY(STKFLT), +#endif +#ifdef SIGCHLD + SIG_ENTRY(CHLD), +#endif +#ifdef SIGCONT + SIG_ENTRY(CONT), +#endif +#ifdef SIGSTOP + SIG_ENTRY(STOP), +#endif +#ifdef SIGTSTP + SIG_ENTRY(TSTP), +#endif +#ifdef SIGTTIN + SIG_ENTRY(TTIN), +#endif +#ifdef SIGTTOU + SIG_ENTRY(TTOU), +#endif +#ifdef SIGURG + SIG_ENTRY(URG), +#endif +#ifdef SIGXCPU + SIG_ENTRY(XCPU), +#endif +#ifdef SIGXFSZ + SIG_ENTRY(XFSZ), +#endif +#ifdef SIGVTALRM + SIG_ENTRY(VTALRM), +#endif +#ifdef SIGPROF + SIG_ENTRY(PROF), +#endif +#ifdef SIGWINCH + SIG_ENTRY(WINCH), +#endif +#ifdef SIGIO + SIG_ENTRY(IO), +#endif +#ifdef SIGPWR + SIG_ENTRY(PWR), +#endif +#ifdef SIGSYS + SIG_ENTRY(SYS), +#endif +}; +#define NSIGNALS (sizeof(signals) / sizeof(signals[0])) + +/* ------------------------------------------------------------------ */ +/* Process information (read from /proc) */ +/* ------------------------------------------------------------------ */ +struct procinfo { + pid_t pid; + pid_t ppid; + pid_t pgrp; + pid_t sid; + uid_t ruid; + uid_t euid; + gid_t rgid; + gid_t egid; + dev_t tdev; /* controlling tty device number */ + unsigned long long starttime; /* jiffies since boot */ + char state; /* R, S, D, Z, T … */ + char comm[256]; /* short command name */ + char *cmdline; /* full command line (heap) */ + int kthread; /* 1 = kernel thread */ +}; + +struct proclist { + struct procinfo *procs; + size_t count; + size_t capacity; +}; + +/* ------------------------------------------------------------------ */ +/* Dynamic filter list */ +/* ------------------------------------------------------------------ */ +struct filterlist { + long *values; + size_t count; + size_t capacity; +}; + +/* ------------------------------------------------------------------ */ +/* Globals (reduced from BSD original) */ +/* ------------------------------------------------------------------ */ +static const char *progname; /* argv[0] basename */ +static int pgrep_mode; /* 1 if invoked as pgrep */ +static int signum = SIGTERM; +static int newest; +static int oldest; +static int interactive; +static int inverse; +static int longfmt; +static int matchargs; +static int fullmatch; +static int kthreads; +static int cflags = REG_EXTENDED; +static int quiet; +static int ancestors; +static int debug_opt; +static const char *delim = "\n"; +static pid_t mypid; + +static struct filterlist euidlist; +static struct filterlist ruidlist; +static struct filterlist rgidlist; +static struct filterlist pgrplist; +static struct filterlist ppidlist; +static struct filterlist tdevlist; +static struct filterlist sidlist; + +/* ------------------------------------------------------------------ */ +/* Forward declarations */ +/* ------------------------------------------------------------------ */ +static void usage(void) __attribute__((__noreturn__)); +static void addfilter(struct filterlist *, long); +static bool infilter(const struct filterlist *, long); + +static bool parse_signal_name(const char *, int *); +static int read_proc_stat(pid_t, struct procinfo *); +static int read_proc_status(pid_t, struct procinfo *); +static char *read_proc_cmdline(pid_t); +static int scan_processes(struct proclist *); +static void free_proclist(struct proclist *); +static int takepid(const char *, int); + +static void parse_uid_filter(struct filterlist *, char *); +static void parse_gid_filter(struct filterlist *, char *); +static void parse_generic_filter(struct filterlist *, char *); +static void parse_pgrp_filter(struct filterlist *, char *); +static void parse_sid_filter(struct filterlist *, char *); +static void parse_tty_filter(struct filterlist *, char *); + +/* ------------------------------------------------------------------ */ +/* usage */ +/* ------------------------------------------------------------------ */ +static void +usage(void) +{ + const char *ustr; + + if (pgrep_mode) + ustr = "[-LSafilnoqvx] [-d delim]"; + else + ustr = "[-signal] [-ILafilnovx]"; + + fprintf(stderr, + "usage: %s %s [-F pidfile] [-G gid]\n" + " [-P ppid] [-U uid] [-g pgrp]\n" + " [-s sid] [-t tty] [-u euid] pattern ...\n", + progname, ustr); + + exit(STATUS_BADUSAGE); +} + +/* ------------------------------------------------------------------ */ +/* Filter list helpers */ +/* ------------------------------------------------------------------ */ +static void +addfilter(struct filterlist *fl, long value) +{ + if (fl->count >= fl->capacity) { + size_t newcap = fl->capacity ? fl->capacity * 2 : 8; + long *nv = realloc(fl->values, newcap * sizeof(long)); + + if (nv == NULL) + err(STATUS_ERROR, "realloc"); + fl->values = nv; + fl->capacity = newcap; + } + fl->values[fl->count++] = value; +} + +static bool +infilter(const struct filterlist *fl, long value) +{ + for (size_t i = 0; i < fl->count; i++) { + if (fl->values[i] == value) + return true; + } + return false; +} + +/* ------------------------------------------------------------------ */ +/* Signal helpers */ +/* ------------------------------------------------------------------ */ +static bool +parse_signal_name(const char *token, int *out) +{ + const char *p = token; + char *end; + long val; + + /* Try numeric first. */ + errno = 0; + val = strtol(token, &end, 10); + if (*end == '\0' && errno == 0 && val >= 0 && val < 256) { + *out = (int)val; + return true; + } + + /* Strip optional "SIG" prefix, case-insensitive. */ + if (strncasecmp(p, "SIG", 3) == 0) + p += 3; + + for (size_t i = 0; i < NSIGNALS; i++) { + if (strcasecmp(p, signals[i].name) == 0) { + *out = signals[i].number; + return true; + } + } + +#ifdef SIGRTMIN +#ifdef SIGRTMAX + if (strcasecmp(p, "RTMIN") == 0) { + *out = SIGRTMIN; + return true; + } + if (strcasecmp(p, "RTMAX") == 0) { + *out = SIGRTMAX; + return true; + } + if (strncasecmp(p, "RTMIN+", 6) == 0) { + errno = 0; + val = strtol(p + 6, &end, 10); + if (*end == '\0' && errno == 0 && val >= 0 && + SIGRTMIN + (int)val <= SIGRTMAX) { + *out = SIGRTMIN + (int)val; + return true; + } + } + if (strncasecmp(p, "RTMAX-", 6) == 0) { + errno = 0; + val = strtol(p + 6, &end, 10); + if (*end == '\0' && errno == 0 && val >= 0 && + SIGRTMAX - (int)val >= SIGRTMIN) { + *out = SIGRTMAX - (int)val; + return true; + } + } +#endif +#endif + + return false; +} + +/* ------------------------------------------------------------------ */ +/* /proc reading */ +/* ------------------------------------------------------------------ */ + +/* + * Parse /proc/<pid>/stat. + * Format: pid (comm) state ppid pgrp session tty_nr ... starttime ... + * The comm field is parenthesised and may contain spaces/parens, + * so we locate the *last* ')' to delimit it reliably. + */ +static int +read_proc_stat(pid_t pid, struct procinfo *pi) +{ + char path[64], buf[4096]; + int fd; + ssize_t n; + char *comm_start, *comm_end, *p; + + snprintf(path, sizeof(path), "/proc/%d/stat", (int)pid); + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return -1; + buf[n] = '\0'; + + comm_start = strchr(buf, '('); + if (comm_start == NULL) + return -1; + comm_start++; + + comm_end = strrchr(buf, ')'); + if (comm_end == NULL || comm_end <= comm_start) + return -1; + + /* Copy comm. */ + { + size_t clen = (size_t)(comm_end - comm_start); + + if (clen >= sizeof(pi->comm)) + clen = sizeof(pi->comm) - 1; + memcpy(pi->comm, comm_start, clen); + pi->comm[clen] = '\0'; + } + + /* Tokenise fields after ") " */ + p = comm_end + 2; + + /* + * We need tokens 0..19 (field 3..22 in stat), where: + * 0 = state 3 = session 4 = tty_nr + * 1 = ppid 6 = flags 19 = starttime + */ + char *tokens[20]; + int ntok = 0; + + while (ntok < 20) { + while (*p == ' ') + p++; + if (*p == '\0') + break; + tokens[ntok++] = p; + while (*p != ' ' && *p != '\0') + p++; + if (*p == ' ') + *p++ = '\0'; + } + if (ntok < 20) + return -1; + + pi->state = tokens[0][0]; + pi->ppid = (pid_t)atoi(tokens[1]); + pi->pgrp = (pid_t)atoi(tokens[2]); + pi->sid = (pid_t)atoi(tokens[3]); + pi->tdev = (dev_t)strtoul(tokens[4], NULL, 10); + pi->starttime = strtoull(tokens[19], NULL, 10); + + return 0; +} + +/* + * Parse /proc/<pid>/status for Uid: and Gid: lines. + */ +static int +read_proc_status(pid_t pid, struct procinfo *pi) +{ + char path[64], line[512]; + FILE *fp; + int got = 0; + unsigned int r, e; + + snprintf(path, sizeof(path), "/proc/%d/status", (int)pid); + fp = fopen(path, "r"); + if (fp == NULL) + return -1; + + while (fgets(line, (int)sizeof(line), fp) != NULL && got < 2) { + if (strncmp(line, "Uid:", 4) == 0) { + if (sscanf(line + 4, " %u %u", &r, &e) == 2) { + pi->ruid = (uid_t)r; + pi->euid = (uid_t)e; + got++; + } + } else if (strncmp(line, "Gid:", 4) == 0) { + if (sscanf(line + 4, " %u %u", &r, &e) == 2) { + pi->rgid = (gid_t)r; + pi->egid = (gid_t)e; + got++; + } + } + } + fclose(fp); + return (got == 2) ? 0 : -1; +} + +/* + * Read /proc/<pid>/cmdline. NUL-separated arguments are joined with + * spaces. Returns heap-allocated string, or NULL for kernel threads + * and zombies. + */ +static char * +read_proc_cmdline(pid_t pid) +{ + char path[64]; + int fd; + ssize_t n; + size_t total = 0, bufsz = 4096; + char *buf; + + snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)pid); + fd = open(path, O_RDONLY); + if (fd < 0) + return NULL; + + buf = malloc(bufsz + 1); + if (buf == NULL) { + close(fd); + return NULL; + } + + while ((n = read(fd, buf + total, bufsz - total)) > 0) { + total += (size_t)n; + if (total >= bufsz) { + bufsz *= 2; + char *nb = realloc(buf, bufsz + 1); + + if (nb == NULL) { + free(buf); + close(fd); + return NULL; + } + buf = nb; + } + } + close(fd); + + if (total == 0) { + free(buf); + return NULL; + } + + /* Strip trailing NULs, then convert internal NULs to spaces. */ + while (total > 0 && buf[total - 1] == '\0') + total--; + + for (size_t i = 0; i < total; i++) { + if (buf[i] == '\0') + buf[i] = ' '; + } + buf[total] = '\0'; + return buf; +} + +/* + * Enumerate all processes from /proc. + */ +static int +scan_processes(struct proclist *pl) +{ + DIR *d; + struct dirent *ent; + char *endp; + pid_t pid; + + d = opendir("/proc"); + if (d == NULL) + err(STATUS_ERROR, "Cannot open /proc"); + + while ((ent = readdir(d)) != NULL) { + pid = (pid_t)strtol(ent->d_name, &endp, 10); + if (*endp != '\0' || pid <= 0) + continue; + + if (pl->count >= pl->capacity) { + size_t nc = pl->capacity ? pl->capacity * 2 : 256; + struct procinfo *np; + + np = realloc(pl->procs, nc * sizeof(*np)); + if (np == NULL) + err(STATUS_ERROR, "realloc"); + pl->procs = np; + pl->capacity = nc; + } + + struct procinfo *pi = &pl->procs[pl->count]; + + memset(pi, 0, sizeof(*pi)); + pi->pid = pid; + + if (read_proc_stat(pid, pi) < 0) + continue; /* process vanished */ + if (read_proc_status(pid, pi) < 0) + continue; + + pi->cmdline = read_proc_cmdline(pid); + pi->kthread = (pi->cmdline == NULL && pi->state != 'Z'); + + pl->count++; + } + closedir(d); + return 0; +} + +static void +free_proclist(struct proclist *pl) +{ + for (size_t i = 0; i < pl->count; i++) + free(pl->procs[i].cmdline); + free(pl->procs); + pl->procs = NULL; + pl->count = pl->capacity = 0; +} + +/* ------------------------------------------------------------------ */ +/* Filter parsers */ +/* ------------------------------------------------------------------ */ +static void +parse_uid_filter(struct filterlist *fl, char *arg) +{ + char *sp; + + while ((sp = strsep(&arg, ",")) != NULL) { + if (*sp == '\0') + errx(STATUS_BADUSAGE, "empty value in user list"); + char *ep; + long val = strtol(sp, &ep, 10); + + if (*ep == '\0') { + addfilter(fl, val); + } else { + struct passwd *pw = getpwnam(sp); + + if (pw == NULL) + errx(STATUS_BADUSAGE, + "Unknown user '%s'", sp); + addfilter(fl, (long)pw->pw_uid); + } + } +} + +static void +parse_gid_filter(struct filterlist *fl, char *arg) +{ + char *sp; + + while ((sp = strsep(&arg, ",")) != NULL) { + if (*sp == '\0') + errx(STATUS_BADUSAGE, "empty value in group list"); + char *ep; + long val = strtol(sp, &ep, 10); + + if (*ep == '\0') { + addfilter(fl, val); + } else { + struct group *gr = getgrnam(sp); + + if (gr == NULL) + errx(STATUS_BADUSAGE, + "Unknown group '%s'", sp); + addfilter(fl, (long)gr->gr_gid); + } + } +} + +static void +parse_generic_filter(struct filterlist *fl, char *arg) +{ + char *sp; + + while ((sp = strsep(&arg, ",")) != NULL) { + if (*sp == '\0') + errx(STATUS_BADUSAGE, "empty value in list"); + char *ep; + long val = strtol(sp, &ep, 10); + + if (*ep != '\0') + errx(STATUS_BADUSAGE, + "Invalid numeric value '%s'", sp); + addfilter(fl, val); + } +} + +static void +parse_pgrp_filter(struct filterlist *fl, char *arg) +{ + char *sp; + + while ((sp = strsep(&arg, ",")) != NULL) { + if (*sp == '\0') + errx(STATUS_BADUSAGE, + "empty value in process group list"); + char *ep; + long val = strtol(sp, &ep, 10); + + if (*ep != '\0') + errx(STATUS_BADUSAGE, + "Invalid process group '%s'", sp); + if (val == 0) + val = (long)getpgrp(); + addfilter(fl, val); + } +} + +static void +parse_sid_filter(struct filterlist *fl, char *arg) +{ + char *sp; + + while ((sp = strsep(&arg, ",")) != NULL) { + if (*sp == '\0') + errx(STATUS_BADUSAGE, + "empty value in session list"); + char *ep; + long val = strtol(sp, &ep, 10); + + if (*ep != '\0') + errx(STATUS_BADUSAGE, + "Invalid session ID '%s'", sp); + if (val == 0) + val = (long)getsid(0); + addfilter(fl, val); + } +} + +static void +parse_tty_filter(struct filterlist *fl, char *arg) +{ + char *sp; + + while ((sp = strsep(&arg, ",")) != NULL) { + if (*sp == '\0') + errx(STATUS_BADUSAGE, + "empty value in terminal list"); + if (strcmp(sp, "-") == 0) { + addfilter(fl, -1); /* no controlling terminal */ + continue; + } + + struct stat st; + char path[PATH_MAX]; + char *ep; + long num = strtol(sp, &ep, 10); + + /* Numeric → try /dev/pts/<N> */ + if (*ep == '\0' && num >= 0) { + snprintf(path, sizeof(path), "/dev/pts/%s", sp); + if (stat(path, &st) == 0 && S_ISCHR(st.st_mode)) { + addfilter(fl, (long)st.st_rdev); + continue; + } + errx(STATUS_BADUSAGE, + "No such tty: '/dev/pts/%s'", sp); + } + + /* Try /dev/<name> */ + snprintf(path, sizeof(path), "/dev/%s", sp); + if (stat(path, &st) == 0 && S_ISCHR(st.st_mode)) { + addfilter(fl, (long)st.st_rdev); + continue; + } + + /* Try /dev/tty<name> */ + snprintf(path, sizeof(path), "/dev/tty%s", sp); + if (stat(path, &st) == 0 && S_ISCHR(st.st_mode)) { + addfilter(fl, (long)st.st_rdev); + continue; + } + + errx(STATUS_BADUSAGE, "No such tty: '%s'", sp); + } +} + +/* ------------------------------------------------------------------ */ +/* PID-file reader */ +/* ------------------------------------------------------------------ */ +static int +takepid(const char *pidfile, int pidfilelock) +{ + char line[BUFSIZ], *endp; + FILE *fh; + long rval; + + fh = fopen(pidfile, "r"); + if (fh == NULL) + err(STATUS_ERROR, "Cannot open pidfile '%s'", pidfile); + + if (pidfilelock) { + if (flock(fileno(fh), LOCK_EX | LOCK_NB) == 0) { + (void)fclose(fh); + errx(STATUS_ERROR, + "File '%s' can be locked; daemon not running", + pidfile); + } else if (errno != EWOULDBLOCK) { + (void)fclose(fh); + err(STATUS_ERROR, + "Error while locking file '%s'", pidfile); + } + } + + if (fgets(line, (int)sizeof(line), fh) == NULL) { + if (feof(fh)) { + (void)fclose(fh); + errx(STATUS_ERROR, + "Pidfile '%s' is empty", pidfile); + } + (void)fclose(fh); + err(STATUS_ERROR, + "Cannot read from pidfile '%s'", pidfile); + } + (void)fclose(fh); + + errno = 0; + rval = strtol(line, &endp, 10); + if ((*endp != '\0' && !isspace((unsigned char)*endp)) || errno != 0) + errx(STATUS_ERROR, + "Invalid pid in file '%s'", pidfile); + if (rval <= 0 || rval > (long)INT_MAX) + errx(STATUS_ERROR, + "Invalid pid in file '%s'", pidfile); + + return (int)rval; +} + +/* ------------------------------------------------------------------ */ +/* Process display */ +/* ------------------------------------------------------------------ */ +static void +show_process(const struct procinfo *pi) +{ + if (quiet) + return; + + if ((longfmt || !pgrep_mode) && matchargs && pi->cmdline != NULL) + printf("%d %s", (int)pi->pid, pi->cmdline); + else if (longfmt || !pgrep_mode) + printf("%d %s", (int)pi->pid, pi->comm); + else + printf("%d", (int)pi->pid); +} + +/* ------------------------------------------------------------------ */ +/* Actions */ +/* ------------------------------------------------------------------ */ +static int +killact(const struct procinfo *pi) +{ + if (interactive) { + int ch, first; + + printf("kill "); + show_process(pi); + printf("? "); + fflush(stdout); + first = ch = getchar(); + while (ch != '\n' && ch != EOF) + ch = getchar(); + if (first != 'y' && first != 'Y') + return 1; + } + if (kill(pi->pid, signum) == -1) { + if (errno != ESRCH) + warn("signalling pid %d", (int)pi->pid); + return 0; + } + return 1; +} + +static int +grepact(const struct procinfo *pi) +{ + static bool first = true; + + if (!quiet && !first) + printf("%s", delim); + show_process(pi); + first = false; + return 1; +} + +/* ------------------------------------------------------------------ */ +/* main */ +/* ------------------------------------------------------------------ */ +int +main(int argc, char **argv) +{ + char *pidfile = NULL; + int ch, criteria, pidfilelock, pidfromfile; + int (*action)(const struct procinfo *); + struct proclist pl; + char *selected; + char *p; + + setlocale(LC_ALL, ""); + + /* Determine program name and mode from argv[0]. */ + progname = strrchr(argv[0], '/'); + progname = progname ? progname + 1 : argv[0]; + if (strcmp(progname, "pgrep") == 0) { + action = grepact; + pgrep_mode = 1; + } else { + action = killact; + + /* pkill: try to parse leading signal argument. */ + if (argc > 1 && argv[1][0] == '-') { + p = argv[1] + 1; + if (*p != '\0' && *p != '-') { + int sig; + + if (parse_signal_name(p, &sig)) { + signum = sig; + argv++; + argc--; + } + } + } + } + + criteria = 0; + pidfilelock = 0; + memset(&pl, 0, sizeof(pl)); + + while ((ch = getopt(argc, argv, + "DF:G:ILM:N:P:SU:ac:d:fg:ij:lnoqs:t:u:vx")) != -1) + switch (ch) { + case 'D': + debug_opt++; + break; + case 'F': + pidfile = optarg; + criteria = 1; + break; + case 'G': + parse_gid_filter(&rgidlist, optarg); + criteria = 1; + break; + case 'I': + if (pgrep_mode) + usage(); + interactive = 1; + break; + case 'L': + pidfilelock = 1; + break; + case 'M': + errx(STATUS_BADUSAGE, + "Core file analysis (-M) requires kvm(3) " + "and is not supported on Linux"); + break; + case 'N': + errx(STATUS_BADUSAGE, + "Kernel name list (-N) requires kvm(3) " + "and is not supported on Linux"); + break; + case 'P': + parse_generic_filter(&ppidlist, optarg); + criteria = 1; + break; + case 'S': + if (!pgrep_mode) + usage(); + kthreads = 1; + break; + case 'U': + parse_uid_filter(&ruidlist, optarg); + criteria = 1; + break; + case 'a': + ancestors++; + break; + case 'c': + errx(STATUS_BADUSAGE, + "FreeBSD login class filtering (-c) " + "is not supported on Linux"); + break; + case 'd': + if (!pgrep_mode) + usage(); + delim = optarg; + break; + case 'f': + matchargs = 1; + break; + case 'g': + parse_pgrp_filter(&pgrplist, optarg); + criteria = 1; + break; + case 'i': + cflags |= REG_ICASE; + break; + case 'j': + errx(STATUS_BADUSAGE, + "FreeBSD jail filtering (-j) " + "is not supported on Linux"); + break; + case 'l': + longfmt = 1; + break; + case 'n': + newest = 1; + criteria = 1; + break; + case 'o': + oldest = 1; + criteria = 1; + break; + case 'q': + if (!pgrep_mode) + usage(); + quiet = 1; + break; + case 's': + parse_sid_filter(&sidlist, optarg); + criteria = 1; + break; + case 't': + parse_tty_filter(&tdevlist, optarg); + criteria = 1; + break; + case 'u': + parse_uid_filter(&euidlist, optarg); + criteria = 1; + break; + case 'v': + inverse = 1; + break; + case 'x': + fullmatch = 1; + break; + default: + usage(); + /* NOTREACHED */ + } + + argc -= optind; + argv += optind; + if (argc != 0) + criteria = 1; + if (!criteria) + usage(); + if (newest && oldest) + errx(STATUS_ERROR, + "Options -n and -o are mutually exclusive"); + + if (pidfile != NULL) + pidfromfile = takepid(pidfile, pidfilelock); + else { + if (pidfilelock) + errx(STATUS_ERROR, + "Option -L doesn't make sense without -F"); + pidfromfile = -1; + } + + mypid = getpid(); + + /* ---- Read the process table from /proc. ---- */ + scan_processes(&pl); + + /* Allocate selection bitmap. */ + selected = calloc(pl.count, 1); + if (selected == NULL) + err(STATUS_ERROR, "calloc"); + + /* ---- Apply regex patterns. ---- */ + { + char errbuf[256]; + regex_t reg; + regmatch_t rm; + + for (int a = 0; a < argc; a++) { + int rv = regcomp(®, argv[a], cflags); + + if (rv != 0) { + regerror(rv, ®, errbuf, sizeof(errbuf)); + errx(STATUS_BADUSAGE, + "Cannot compile regular expression " + "'%s' (%s)", argv[a], errbuf); + } + + for (size_t i = 0; i < pl.count; i++) { + struct procinfo *pi = &pl.procs[i]; + + /* Always skip self. */ + if (pi->pid == mypid) + continue; + /* Skip kernel threads unless -S. */ + if (!kthreads && pi->kthread) + continue; + + const char *mstr; + + if (matchargs && pi->cmdline != NULL) + mstr = pi->cmdline; + else + mstr = pi->comm; + + rv = regexec(®, mstr, 1, &rm, 0); + if (rv == 0) { + if (fullmatch) { + if (rm.rm_so == 0 && + rm.rm_eo == + (regoff_t)strlen(mstr)) + selected[i] = 1; + } else { + selected[i] = 1; + } + } else if (rv != REG_NOMATCH) { + regerror(rv, ®, errbuf, + sizeof(errbuf)); + errx(STATUS_ERROR, + "Regular expression evaluation " + "error (%s)", errbuf); + } + + if (debug_opt > 1) { + fprintf(stderr, "* %s %5d %3u %s\n", + selected[i] ? "Matched" + : "NoMatch", + (int)pi->pid, + (unsigned)pi->euid, mstr); + } + } + regfree(®); + } + } + + /* ---- Apply attribute filters. ---- */ + for (size_t i = 0; i < pl.count; i++) { + struct procinfo *pi = &pl.procs[i]; + + if (pi->pid == mypid) + continue; + if (!kthreads && pi->kthread) + continue; + + if (pidfromfile >= 0 && pi->pid != pidfromfile) { + selected[i] = 0; + continue; + } + + if (ruidlist.count > 0 && + !infilter(&ruidlist, (long)pi->ruid)) { + selected[i] = 0; + continue; + } + if (rgidlist.count > 0 && + !infilter(&rgidlist, (long)pi->rgid)) { + selected[i] = 0; + continue; + } + if (euidlist.count > 0 && + !infilter(&euidlist, (long)pi->euid)) { + selected[i] = 0; + continue; + } + if (ppidlist.count > 0 && + !infilter(&ppidlist, (long)pi->ppid)) { + selected[i] = 0; + continue; + } + if (pgrplist.count > 0 && + !infilter(&pgrplist, (long)pi->pgrp)) { + selected[i] = 0; + continue; + } + if (tdevlist.count > 0) { + bool match = false; + + for (size_t j = 0; j < tdevlist.count; j++) { + if (tdevlist.values[j] == -1 && + pi->tdev == 0) { + match = true; + break; + } + if ((long)pi->tdev == tdevlist.values[j]) { + match = true; + break; + } + } + if (!match) { + selected[i] = 0; + continue; + } + } + if (sidlist.count > 0 && + !infilter(&sidlist, (long)pi->sid)) { + selected[i] = 0; + continue; + } + + /* If no regex patterns given, select by filter only. */ + if (argc == 0) + selected[i] = 1; + } + + /* ---- Exclude ancestors (unless -a). ---- */ + if (!ancestors) { + pid_t pid = mypid; + + while (pid > 1) { + for (size_t i = 0; i < pl.count; i++) { + if (pl.procs[i].pid == pid) { + selected[i] = 0; + pid = pl.procs[i].ppid; + goto next_ancestor; + } + } + /* Process not found in list. */ + if (pid == mypid) + pid = getppid(); + else + break; +next_ancestor:; + } + } + + /* ---- Handle -n (newest) / -o (oldest). ---- */ + if (newest || oldest) { + unsigned long long best = 0; + int bestidx = -1; + + for (size_t i = 0; i < pl.count; i++) { + if (!selected[i]) + continue; + if (bestidx == -1) { + /* first match */ + } else if (pl.procs[i].starttime > best) { + if (oldest) + continue; + } else { + if (newest) + continue; + } + best = pl.procs[i].starttime; + bestidx = (int)i; + } + + memset(selected, 0, pl.count); + if (bestidx != -1) + selected[bestidx] = 1; + } + + /* ---- Execute action. ---- */ + int did_action = 0, rv = 0; + + for (size_t i = 0; i < pl.count; i++) { + struct procinfo *pi = &pl.procs[i]; + + if (pi->pid == mypid) + continue; + if (!kthreads && pi->kthread) + continue; + + if (selected[i]) { + if (longfmt && !pgrep_mode) { + did_action = 1; + printf("kill -%d %d\n", signum, + (int)pi->pid); + } + if (inverse) + continue; + } else if (!inverse) { + continue; + } + rv |= (*action)(pi); + } + if (rv && pgrep_mode && !quiet) + putchar('\n'); + if (!did_action && !pgrep_mode && longfmt) + fprintf(stderr, + "No matching processes belonging to you were found\n"); + + free(selected); + free_proclist(&pl); + + return rv ? STATUS_MATCH : STATUS_NOMATCH; +} diff --git a/corebinutils/pkill/tests/spin_helper.c b/corebinutils/pkill/tests/spin_helper.c new file mode 100644 index 0000000000..bf3ec550ba --- /dev/null +++ b/corebinutils/pkill/tests/spin_helper.c @@ -0,0 +1,190 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2023 Klara, Inc. + * 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 AUTHOR 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 AUTHOR 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. + * + * Linux-native test helper for pkill/pgrep tests. + * Replaces BSD __DECONST and <sys/cdefs.h>. + * + * Usage: + * spin_helper --spin flagfile sentinel + * Creates flagfile, then spins until killed. + * + * spin_helper --short flagfile sentinel + * Re-execs with short arguments, then spins. + * + * spin_helper --long flagfile sentinel + * Re-execs with maximally long arguments, then spins. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static volatile sig_atomic_t got_signal; + +static void +sighandler(int signo) +{ + got_signal = signo; +} + +static void +exec_shortargs(char *argv[]) +{ + char *nargv[] = { argv[0], (char *)"--spin", argv[2], argv[3], NULL }; + char *nenvp[] = { NULL }; + + execve(argv[0], nargv, nenvp); + perror("execve"); + _exit(1); +} + +static void +exec_largeargs(char *argv[]) +{ + size_t bufsz; + char *s = NULL; + char *nargv[] = { + argv[0], (char *)"--spin", argv[2], NULL, argv[3], NULL + }; + char *nenvp[] = { NULL }; + + /* + * Compute maximum argument size. Account for each argument + NUL + * terminator, plus an extra NUL. + */ + long arg_max = sysconf(_SC_ARG_MAX); + + if (arg_max <= 0) + arg_max = 131072; + + bufsz = (size_t)arg_max - + ((strlen(argv[0]) + 1) + sizeof("--spin") + + (strlen(argv[2]) + 1) + (strlen(argv[3]) + 1) + 1); + + /* + * Keep trying with smaller sizes until execve stops returning + * E2BIG. + */ + do { + char *ns = realloc(s, bufsz + 1); + + if (ns == NULL) + abort(); + s = ns; + memset(s, 'x', bufsz); + s[bufsz] = '\0'; + nargv[3] = s; + + execve(argv[0], nargv, nenvp); + bufsz--; + } while (errno == E2BIG && bufsz > 0); + + perror("execve"); + _exit(1); +} + +int +main(int argc, char *argv[]) +{ + if (argc > 1 && strcmp(argv[1], "--spin") == 0) { + int fd; + struct sigaction sa; + + if (argc < 4) { + fprintf(stderr, + "usage: %s --spin flagfile sentinel\n", + argv[0]); + return 1; + } + + /* Install signal handler for SIGUSR1 and SIGTERM. */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sighandler; + sigemptyset(&sa.sa_mask); + sigaction(SIGUSR1, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + + /* Create flag file to indicate readiness. */ + fd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + perror(argv[2]); + return 1; + } + /* Write our PID to the flag file. */ + dprintf(fd, "%d\n", (int)getpid()); + close(fd); + + /* Spin until signal received. */ + while (got_signal == 0) + pause(); + + /* + * Write received signal name to the sentinel file + * so the test can verify which signal was delivered. + */ + { + const char *sname = "UNKNOWN"; + + if (got_signal == SIGUSR1) + sname = "USR1"; + else if (got_signal == SIGTERM) + sname = "TERM"; + + FILE *fp = fopen(argv[argc - 1], "w"); + + if (fp != NULL) { + fprintf(fp, "%s\n", sname); + fclose(fp); + } + } + + return 0; + } + + if (argc != 4) { + fprintf(stderr, + "usage: %s [--short | --long] flagfile sentinel\n", + argv[0]); + return 1; + } + + if (strcmp(argv[1], "--short") == 0) + exec_shortargs(argv); + else + exec_largeargs(argv); + + return 1; +} diff --git a/corebinutils/pkill/tests/test.sh b/corebinutils/pkill/tests/test.sh new file mode 100644 index 0000000000..94a271a45d --- /dev/null +++ b/corebinutils/pkill/tests/test.sh @@ -0,0 +1,730 @@ +#!/bin/sh +# Comprehensive test suite for pkill/pgrep Linux port. +# Follows the same pattern as bin/kill/tests/test.sh. +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +PKILL_BIN=${PKILL_BIN:-"$ROOT/out/pkill"} +PGREP_BIN=${PGREP_BIN:-"$ROOT/out/pgrep"} +HELPER_BIN=${HELPER_BIN:-"$ROOT/out/spin_helper"} +CC=${CC:-cc} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/pkill-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" + +# Unique name: must be <= 15 chars to stay within Linux /proc/PID/stat comm limit. +# Format: pkt + truncated_PID. "pkt" (3) + "_" (1) + up to 11 PID digits. +# In practice PID is at most 7 digits on Linux, so max = 11 chars. +UNIQUE="pkt_$$" +# Guard against pathological shells where $$ is very long. +if [ ${#UNIQUE} -gt 15 ]; then + _suffix=$(echo "$$" | tail -c8) + UNIQUE="pkt_$_suffix" +fi +HELPER_COPY="$WORKDIR/$UNIQUE" + +PIDS_TO_KILL="" + +export LC_ALL=C + +cleanup() { + for p in $PIDS_TO_KILL; do + kill -KILL "$p" 2>/dev/null || true + wait "$p" 2>/dev/null || true + done + rm -rf "$WORKDIR" +} +trap cleanup EXIT INT TERM + +PASS_COUNT=0 +FAIL_COUNT=0 + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + exit 1 +} + +pass() { + PASS_COUNT=$((PASS_COUNT + 1)) + printf ' ok: %s\n' "$1" +} + +assert_eq() { + name=$1; expected=$2; actual=$3 + if [ "$expected" != "$actual" ]; then + printf 'FAIL: %s\n' "$name" >&2 + printf '--- expected ---\n%s\n--- actual ---\n%s\n' \ + "$expected" "$actual" >&2 + exit 1 + fi +} + +assert_contains() { + name=$1; text=$2; pattern=$3 + case $text in + *"$pattern"*) ;; + *) fail "$name: expected to contain '$pattern', got: $text" ;; + esac +} + +assert_not_contains() { + name=$1; text=$2; pattern=$3 + case $text in + *"$pattern"*) fail "$name: should not contain '$pattern'" ;; + *) ;; + esac +} + +assert_empty() { + name=$1; text=$2 + if [ -n "$text" ]; then + printf 'FAIL: %s\n--- expected empty ---\n--- actual ---\n%s\n' \ + "$name" "$text" >&2 + exit 1 + fi +} + +assert_status() { + name=$1; expected=$2; actual=$3 + if [ "$expected" -ne "$actual" ]; then + printf 'FAIL: %s\nexpected status: %s\nactual status: %s\n' \ + "$name" "$expected" "$actual" >&2 + exit 1 + fi +} + +run_capture() { + if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +# Start a helper process with a unique command name. +# Uses --spin directly so that comm matches the helper copy's basename +# immediately without any execve race. +# Sets HELPER_PID, HELPER_SENTINEL. +start_helper() { + tag=$1 + flag="$WORKDIR/${tag}.flag" + sentinel="$WORKDIR/${tag}.sentinel" + rm -f "$flag" "$sentinel" + + # --spin: creates flag file then waits for signal. + "$HELPER_COPY" --spin "$flag" "$sentinel" & + HELPER_PID=$! + PIDS_TO_KILL="$PIDS_TO_KILL $HELPER_PID" + + # Wait for flag file to appear (readiness signal). + tries=0 + while [ ! -f "$flag" ]; do + tries=$((tries + 1)) + if [ $tries -gt 100 ]; then + fail "helper '$tag' (pid=$HELPER_PID) did not become ready" + fi + sleep 0.05 + done + HELPER_SENTINEL="$sentinel" +} + +# Start two helpers for oldest/newest tests. +start_two_helpers() { + start_helper "older" + OLDER_PID=$HELPER_PID + OLDER_SENTINEL=$HELPER_SENTINEL + sleep 0.2 # ensure different starttime + start_helper "newer" + NEWER_PID=$HELPER_PID + NEWER_SENTINEL=$HELPER_SENTINEL +} + +stop_helper() { + pid=$1 + kill -KILL "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + PIDS_TO_KILL=$(echo "$PIDS_TO_KILL" | sed "s/ *$pid//g") +} + +wait_helper_exit() { + pid=$1 + tries=0 + while kill -0 "$pid" 2>/dev/null; do + tries=$((tries + 1)) + if [ $tries -gt 50 ]; then + fail "helper pid=$pid did not exit" + fi + sleep 0.1 + done + wait "$pid" 2>/dev/null || true + PIDS_TO_KILL=$(echo "$PIDS_TO_KILL" | sed "s/ *$pid//g") +} + +# ============================================================ +# Prerequisite checks +# ============================================================ +[ -x "$PKILL_BIN" ] || fail "missing pkill binary: $PKILL_BIN" +[ -x "$PGREP_BIN" ] || fail "missing pgrep binary: $PGREP_BIN" +[ -x "$HELPER_BIN" ] || fail "missing helper binary: $HELPER_BIN" +[ -d /proc ] || fail "/proc not available" + +# Create a helper copy with unique name. +cp "$HELPER_BIN" "$HELPER_COPY" +chmod +x "$HELPER_COPY" + +printf '=== pkill/pgrep test suite ===\n' +printf 'helper name: %s\n' "$UNIQUE" + +# ============================================================ +# 1. Usage / error tests +# ============================================================ + +# pgrep with no arguments → status 2 +run_capture "$PGREP_BIN" +assert_status "pgrep no args: status" 2 "$LAST_STATUS" +assert_empty "pgrep no args: stdout" "$LAST_STDOUT" +assert_contains "pgrep no args: stderr" "$LAST_STDERR" "usage:" +pass "pgrep no args → usage error" + +# pkill with no arguments → status 2 +run_capture "$PKILL_BIN" +assert_status "pkill no args: status" 2 "$LAST_STATUS" +assert_empty "pkill no args: stdout" "$LAST_STDOUT" +assert_contains "pkill no args: stderr" "$LAST_STDERR" "usage:" +pass "pkill no args → usage error" + +# Bad regex → status 2 +run_capture "$PGREP_BIN" '[invalid' +assert_status "bad regex: status" 2 "$LAST_STATUS" +assert_empty "bad regex: stdout" "$LAST_STDOUT" +assert_contains "bad regex: stderr" "$LAST_STDERR" "Cannot compile regular expression" +pass "bad regex → error" + +# -n and -o together → status 3 +run_capture "$PGREP_BIN" -n -o 'anything' +assert_status "-n -o together: status" 3 "$LAST_STATUS" +assert_contains "-n -o together: stderr" "$LAST_STDERR" "mutually exclusive" +pass "-n -o together → error" + +# BSD-only options produce explicit errors on Linux +run_capture "$PGREP_BIN" -j any 'test' +assert_status "-j: status" 2 "$LAST_STATUS" +assert_contains "-j: stderr" "$LAST_STDERR" "not supported on Linux" +pass "-j → explicit unsupported error" + +run_capture "$PGREP_BIN" -c user 'test' +assert_status "-c: status" 2 "$LAST_STATUS" +assert_contains "-c: stderr" "$LAST_STDERR" "not supported on Linux" +pass "-c → explicit unsupported error" + +run_capture "$PGREP_BIN" -M /dev/null 'test' +assert_status "-M: status" 2 "$LAST_STATUS" +assert_contains "-M: stderr" "$LAST_STDERR" "not supported on Linux" +pass "-M → explicit unsupported error" + +run_capture "$PGREP_BIN" -N /dev/null 'test' +assert_status "-N: status" 2 "$LAST_STATUS" +assert_contains "-N: stderr" "$LAST_STDERR" "not supported on Linux" +pass "-N → explicit unsupported error" + +# -L without -F → error +run_capture "$PGREP_BIN" -L 'test' +assert_status "-L without -F: status" 3 "$LAST_STATUS" +assert_contains "-L without -F: stderr" "$LAST_STDERR" "doesn't make sense" +pass "-L without -F → error" + +# pgrep-only options in pkill mode → usage +run_capture "$PKILL_BIN" -d, 'test' +assert_status "pkill -d: status" 2 "$LAST_STATUS" +pass "pkill -d → usage error" + +run_capture "$PKILL_BIN" -q 'test' +assert_status "pkill -q: status" 2 "$LAST_STATUS" +pass "pkill -q → usage error" + +# pkill-only option in pgrep mode → usage +run_capture "$PGREP_BIN" -I 'test' +assert_status "pgrep -I: status" 2 "$LAST_STATUS" +pass "pgrep -I → usage error" + +# ============================================================ +# 2. pgrep basic matching +# ============================================================ +start_helper basic + +run_capture "$PGREP_BIN" "$UNIQUE" +assert_status "pgrep basic: status" 0 "$LAST_STATUS" +assert_contains "pgrep basic: stdout" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep basic match" + +# Ensure pgrep does not match itself. +assert_not_contains "pgrep self-exclusion" "$LAST_STDOUT" "$$" +pass "pgrep excludes self" + +stop_helper "$HELPER_PID" + +# No match → status 1 +run_capture "$PGREP_BIN" "nonexistent_process_name_$$$RANDOM" +assert_status "pgrep no match: status" 1 "$LAST_STATUS" +assert_empty "pgrep no match: stdout" "$LAST_STDOUT" +pass "pgrep no match → status 1" + +# ============================================================ +# 3. pgrep -x (exact match) +# ============================================================ +start_helper exact + +run_capture "$PGREP_BIN" -x "$UNIQUE" +assert_status "pgrep -x exact: status" 0 "$LAST_STATUS" +assert_contains "pgrep -x exact: stdout" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -x exact match" + +# Partial name should NOT match with -x. +partial=$(echo "$UNIQUE" | cut -c1-6) +run_capture "$PGREP_BIN" -x "$partial" +# It should not match the helper (could match other things though) +assert_not_contains "pgrep -x partial" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -x rejects partial match" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 4. pgrep -f (match full args) +# ============================================================ +start_helper fullarg + +run_capture "$PGREP_BIN" -f -- "--spin.*$WORKDIR" +assert_status "pgrep -f: status" 0 "$LAST_STATUS" +assert_contains "pgrep -f: stdout" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -f matches full cmdline" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 5. pgrep -i (case insensitive) +# ============================================================ +start_helper casematch + +UPPER=$(echo "$UNIQUE" | tr '[:lower:]' '[:upper:]') +run_capture "$PGREP_BIN" -i "$UPPER" +assert_status "pgrep -i: status" 0 "$LAST_STATUS" +assert_contains "pgrep -i: stdout" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -i case insensitive" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 6. pgrep -l (long output) +# ============================================================ +start_helper longout + +run_capture "$PGREP_BIN" -l "$UNIQUE" +assert_status "pgrep -l: status" 0 "$LAST_STATUS" +assert_contains "pgrep -l: stdout" "$LAST_STDOUT" "$HELPER_PID" +# Long format should include process name. +assert_contains "pgrep -l shows name" "$LAST_STDOUT" "$UNIQUE" +pass "pgrep -l long output" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 7. pgrep -l -f (long + full args) +# ============================================================ +start_helper longfull + +run_capture "$PGREP_BIN" -l -f "$UNIQUE" +assert_status "pgrep -lf: status" 0 "$LAST_STATUS" +# Should show PID and full cmdline +assert_contains "pgrep -lf: shows PID" "$LAST_STDOUT" "$HELPER_PID" +assert_contains "pgrep -lf: shows --spin" "$LAST_STDOUT" "--spin" +pass "pgrep -l -f long + full args" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 8. pgrep -v (inverse) +# ============================================================ +start_helper inverse + +# -v should NOT include the helper in results when it matches. +# We search for patterns that DO match, and check helper PID is absent. +run_capture "$PGREP_BIN" -v "$UNIQUE" +assert_status "pgrep -v: status" 0 "$LAST_STATUS" +assert_not_contains "pgrep -v no helper" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -v inverse match" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 9. pgrep -q (quiet) +# ============================================================ +start_helper quiettest + +run_capture "$PGREP_BIN" -q "$UNIQUE" +assert_status "pgrep -q: status" 0 "$LAST_STATUS" +assert_empty "pgrep -q: stdout" "$LAST_STDOUT" +pass "pgrep -q quiet mode" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 10. pgrep -d (delimiter) +# ============================================================ +start_helper delim1 +PID1=$HELPER_PID +start_helper delim2 +PID2=$HELPER_PID + +run_capture "$PGREP_BIN" -d, "$UNIQUE" +assert_status "pgrep -d: status" 0 "$LAST_STATUS" +# Output should contain comma-separated PIDs +assert_contains "pgrep -d: comma" "$LAST_STDOUT" "," +pass "pgrep -d custom delimiter" + +stop_helper "$PID1" +stop_helper "$PID2" + +# ============================================================ +# 11. pgrep -n (newest) / -o (oldest) +# ============================================================ +start_two_helpers + +run_capture "$PGREP_BIN" -n "$UNIQUE" +assert_status "pgrep -n: status" 0 "$LAST_STATUS" +assert_contains "pgrep -n: newest" "$LAST_STDOUT" "$NEWER_PID" +assert_not_contains "pgrep -n: not older" "$LAST_STDOUT" "$OLDER_PID" +pass "pgrep -n selects newest" + +run_capture "$PGREP_BIN" -o "$UNIQUE" +assert_status "pgrep -o: status" 0 "$LAST_STATUS" +assert_contains "pgrep -o: oldest" "$LAST_STDOUT" "$OLDER_PID" +assert_not_contains "pgrep -o: not newer" "$LAST_STDOUT" "$NEWER_PID" +pass "pgrep -o selects oldest" + +stop_helper "$OLDER_PID" +stop_helper "$NEWER_PID" + +# ============================================================ +# 12. pgrep -P ppid (parent PID filter) +# ============================================================ +start_helper ppidtest + +PPID_OF_HELPER=$(awk '{print $4}' "/proc/$HELPER_PID/stat" 2>/dev/null || echo "") +if [ -n "$PPID_OF_HELPER" ]; then + run_capture "$PGREP_BIN" -P "$PPID_OF_HELPER" "$UNIQUE" + assert_status "pgrep -P: status" 0 "$LAST_STATUS" + assert_contains "pgrep -P: match" "$LAST_STDOUT" "$HELPER_PID" + pass "pgrep -P parent PID filter" + + # Wrong parent → no match + run_capture "$PGREP_BIN" -P 1 "$UNIQUE" + if [ "$PPID_OF_HELPER" != "1" ]; then + assert_status "pgrep -P wrong parent: status" 1 "$LAST_STATUS" + pass "pgrep -P wrong parent → no match" + fi +else + printf ' SKIP: pgrep -P (cannot read ppid)\n' +fi + +stop_helper "$HELPER_PID" + +# ============================================================ +# 13. pgrep -u euid (effective user ID filter) +# ============================================================ +start_helper euidtest + +MY_EUID=$(id -u) +run_capture "$PGREP_BIN" -u "$MY_EUID" "$UNIQUE" +assert_status "pgrep -u euid: status" 0 "$LAST_STATUS" +assert_contains "pgrep -u euid: match" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -u effective UID filter" + +# Wrong euid → no match +run_capture "$PGREP_BIN" -u 99999 "$UNIQUE" +assert_status "pgrep -u wrong euid: status" 1 "$LAST_STATUS" +pass "pgrep -u wrong UID → no match" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 14. pgrep -U ruid (real user ID filter) +# ============================================================ +start_helper ruidtest + +MY_RUID=$(id -ru) +run_capture "$PGREP_BIN" -U "$MY_RUID" "$UNIQUE" +assert_status "pgrep -U ruid: status" 0 "$LAST_STATUS" +assert_contains "pgrep -U ruid: match" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -U real UID filter" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 15. pgrep -G rgid (real group ID filter) +# ============================================================ +start_helper rgidtest + +MY_RGID=$(id -rg) +run_capture "$PGREP_BIN" -G "$MY_RGID" "$UNIQUE" +assert_status "pgrep -G rgid: status" 0 "$LAST_STATUS" +assert_contains "pgrep -G rgid: match" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -G real GID filter" + +# Wrong GID +run_capture "$PGREP_BIN" -G 99999 "$UNIQUE" +assert_status "pgrep -G wrong gid: status" 1 "$LAST_STATUS" +pass "pgrep -G wrong GID → no match" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 16. pgrep -g pgrp (process group filter) +# ============================================================ +start_helper pgrptest + +# The helper's pgrp is its own PID (if it calls setpgid) or inherited. +# Read from proc. +HELPER_PGRP=$(awk '{print $5}' "/proc/$HELPER_PID/stat" 2>/dev/null || echo "") +if [ -n "$HELPER_PGRP" ] && [ "$HELPER_PGRP" != "0" ]; then + run_capture "$PGREP_BIN" -g "$HELPER_PGRP" "$UNIQUE" + assert_status "pgrep -g: status" 0 "$LAST_STATUS" + assert_contains "pgrep -g: match" "$LAST_STDOUT" "$HELPER_PID" + pass "pgrep -g process group filter" +else + printf ' SKIP: pgrep -g (cannot read pgrp)\n' +fi + +stop_helper "$HELPER_PID" + +# ============================================================ +# 17. pgrep -s sid (session ID filter) +# ============================================================ +start_helper sidtest + +HELPER_SID=$(awk '{print $6}' "/proc/$HELPER_PID/stat" 2>/dev/null || echo "") +if [ -n "$HELPER_SID" ] && [ "$HELPER_SID" != "0" ]; then + run_capture "$PGREP_BIN" -s "$HELPER_SID" "$UNIQUE" + assert_status "pgrep -s: status" 0 "$LAST_STATUS" + assert_contains "pgrep -s: match" "$LAST_STDOUT" "$HELPER_PID" + pass "pgrep -s session ID filter" +else + printf ' SKIP: pgrep -s (cannot read sid)\n' +fi + +stop_helper "$HELPER_PID" + +# ============================================================ +# 18. pgrep -F pidfile +# ============================================================ +start_helper pidfile + +PIDFILE="$WORKDIR/test.pid" +echo "$HELPER_PID" > "$PIDFILE" + +run_capture "$PGREP_BIN" -F "$PIDFILE" +assert_status "pgrep -F: status" 0 "$LAST_STATUS" +assert_contains "pgrep -F: match" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -F pidfile" + +# Non-existent pidfile → error +run_capture "$PGREP_BIN" -F "$WORKDIR/nonexistent.pid" +assert_status "pgrep -F nonexist: status" 3 "$LAST_STATUS" +assert_contains "pgrep -F nonexist: stderr" "$LAST_STDERR" "Cannot open" +pass "pgrep -F nonexistent → error" + +# Empty pidfile → error +> "$WORKDIR/empty.pid" +run_capture "$PGREP_BIN" -F "$WORKDIR/empty.pid" +assert_status "pgrep -F empty: status" 3 "$LAST_STATUS" +assert_contains "pgrep -F empty: stderr" "$LAST_STDERR" "empty" +pass "pgrep -F empty pidfile → error" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 19. pkill basic (send SIGTERM) +# ============================================================ +start_helper pkill_basic + +run_capture "$PKILL_BIN" "$UNIQUE" +assert_status "pkill basic: status" 0 "$LAST_STATUS" + +# Wait for helper to receive signal and exit. +wait_helper_exit "$HELPER_PID" + +# Check sentinel file for signal name. +if [ -f "$HELPER_SENTINEL" ]; then + signame=$(cat "$HELPER_SENTINEL") + assert_eq "pkill basic: signal" "TERM" "$signame" +fi +pass "pkill basic SIGTERM" + +# ============================================================ +# 20. pkill -SIGUSR1 (custom signal) +# ============================================================ +start_helper pkill_sig + +run_capture "$PKILL_BIN" -USR1 "$UNIQUE" +assert_status "pkill -USR1: status" 0 "$LAST_STATUS" + +wait_helper_exit "$HELPER_PID" + +if [ -f "$HELPER_SENTINEL" ]; then + signame=$(cat "$HELPER_SENTINEL") + assert_eq "pkill -USR1: signal" "USR1" "$signame" +fi +pass "pkill -USR1 custom signal" + +# ============================================================ +# 21. pkill -<number> (numeric signal) +# ============================================================ +start_helper pkill_num + +# SIGUSR1 is typically 10 +run_capture "$PKILL_BIN" -10 "$UNIQUE" +assert_status "pkill -10: status" 0 "$LAST_STATUS" + +wait_helper_exit "$HELPER_PID" + +if [ -f "$HELPER_SENTINEL" ]; then + signame=$(cat "$HELPER_SENTINEL") + assert_eq "pkill -10: signal" "USR1" "$signame" +fi +pass "pkill -<number> numeric signal" + +# ============================================================ +# 22. pkill -f (match full args) +# ============================================================ +start_helper pkill_full + +run_capture "$PKILL_BIN" -f -- "--spin.*$WORKDIR" +assert_status "pkill -f: status" 0 "$LAST_STATUS" + +wait_helper_exit "$HELPER_PID" +pass "pkill -f matches full cmdline" + +# ============================================================ +# 23. pkill with -l (list mode) +# ============================================================ +start_helper pkill_list + +run_capture "$PKILL_BIN" -l "$UNIQUE" +assert_status "pkill -l: status" 0 "$LAST_STATUS" +assert_contains "pkill -l: stdout" "$LAST_STDOUT" "kill" +assert_contains "pkill -l: stdout" "$LAST_STDOUT" "$HELPER_PID" +pass "pkill -l shows kill command" + +wait_helper_exit "$HELPER_PID" + +# ============================================================ +# 24. pkill no match → status 1 +# ============================================================ +run_capture "$PKILL_BIN" "nonexistent_process_$$$RANDOM" +assert_status "pkill no match: status" 1 "$LAST_STATUS" +pass "pkill no match → status 1" + +# ============================================================ +# 25. pgrep -S (include kernel threads) +# ============================================================ +# This should not error out. We just check it runs. +run_capture "$PGREP_BIN" -S 'kthreadd' +# kthreadd might or might not be visible depending on environment. +# Just verify it doesn't crash. +if [ "$LAST_STATUS" -ne 0 ] && [ "$LAST_STATUS" -ne 1 ]; then + fail "pgrep -S unexpected status: $LAST_STATUS" +fi +pass "pgrep -S kernel threads (no crash)" + +# ============================================================ +# 26. pgrep -a (include ancestors) +# ============================================================ +start_helper ancestors + +# Without -a, our shell (ancestor) might be excluded from results. +# With -a, ancestors are included. Hard to test precisely, but +# verify -a doesn't cause errors. +run_capture "$PGREP_BIN" -a "$UNIQUE" +assert_status "pgrep -a: status" 0 "$LAST_STATUS" +pass "pgrep -a include ancestors (no crash)" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 27. Multiple patterns +# ============================================================ +start_helper multi1 +PID_M1=$HELPER_PID + +run_capture "$PGREP_BIN" "$UNIQUE" +assert_status "multi pattern: status" 0 "$LAST_STATUS" +assert_contains "multi pattern: match" "$LAST_STDOUT" "$PID_M1" +pass "multiple pattern matching" + +stop_helper "$PID_M1" + +# ============================================================ +# 28. User name filter (if available) +# ============================================================ +start_helper usertest + +MY_USER=$(id -un) +run_capture "$PGREP_BIN" -u "$MY_USER" "$UNIQUE" +assert_status "pgrep -u username: status" 0 "$LAST_STATUS" +assert_contains "pgrep -u username: match" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -u username filter" + +# Unknown user → error +run_capture "$PGREP_BIN" -u "nonexistent_user_xyz_$$" "$UNIQUE" +assert_status "pgrep -u unknown user: status" 2 "$LAST_STATUS" +assert_contains "pgrep -u unknown: stderr" "$LAST_STDERR" "Unknown user" +pass "pgrep -u unknown user → error" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 29. Group name filter +# ============================================================ +start_helper grouptest + +MY_GROUP=$(id -gn) +run_capture "$PGREP_BIN" -G "$MY_GROUP" "$UNIQUE" +assert_status "pgrep -G groupname: status" 0 "$LAST_STATUS" +assert_contains "pgrep -G groupname: match" "$LAST_STDOUT" "$HELPER_PID" +pass "pgrep -G groupname filter" + +# Unknown group → error +run_capture "$PGREP_BIN" -G "nonexistent_group_xyz_$$" "$UNIQUE" +assert_status "pgrep -G unknown group: status" 2 "$LAST_STATUS" +assert_contains "pgrep -G unknown: stderr" "$LAST_STDERR" "Unknown group" +pass "pgrep -G unknown group → error" + +stop_helper "$HELPER_PID" + +# ============================================================ +# 30. pgrep -F -L (pidfile with lock check) +# ============================================================ +start_helper locktest + +LOCKPID_FILE="$WORKDIR/locked.pid" +echo "$HELPER_PID" > "$LOCKPID_FILE" + +# File is not locked, so -L should report error. +run_capture "$PGREP_BIN" -F "$LOCKPID_FILE" -L +assert_status "pgrep -F -L unlocked: status" 3 "$LAST_STATUS" +assert_contains "pgrep -F -L: stderr" "$LAST_STDERR" "can be locked" +pass "pgrep -F -L detects unlocked pidfile" + +stop_helper "$HELPER_PID" + +# ============================================================ +# Summary +# ============================================================ +printf '\n=== %d tests passed ===\n' "$PASS_COUNT" +if [ "$FAIL_COUNT" -ne 0 ]; then + printf '=== %d tests FAILED ===\n' "$FAIL_COUNT" + exit 1 +fi |
