/* emacs settings:  -*- c-basic-offset: 8 tab-width: 8 -*-
 *
 * pgrep/pkill -- utilities to filter the process table
 *
 * Copyright 2000 Kjetil Torgrim Homme <kjetilho@ifi.uio.no>
 *
 * May be distributed under the conditions of the
 * GNU General Public License; a copy is in COPYING
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <pwd.h>
#include <grp.h>
#include <regex.h>
#include <errno.h>

#include "proc/readproc.h"
#include "proc/signals.h"
#include "proc/devname.h"
#include "proc/sysinfo.h"
#include "proc/version.h"

static int i_am_pkill = 0;
char *progname = "pgrep";

union el {
	long num;
	char *str;
};

/* User supplied arguments */

static int opt_full = 0;
static int opt_long = 0;
static int opt_newest = 0;
static int opt_negate = 0;
static int opt_exact = 0;
static int opt_signal = SIGTERM;

static char *opt_delim = "\n";
static union el *opt_pgrp = NULL;
static union el *opt_gid = NULL;
static union el *opt_ppid = NULL;
static union el *opt_sid = NULL;
static union el *opt_term = NULL;
static union el *opt_euid = NULL;
static union el *opt_uid = NULL;
static char *opt_pattern = NULL;

/* Prototypes */

static union el *split_list(const char *, char,
			    int (*)(const char *, union el *));
static int conv_uid(const char *, union el *);
static int conv_gid(const char *, union el *);
static int conv_sid(const char *, union el *);
static int conv_pgrp(const char *, union el *);
static int conv_num(const char *, union el *);
static int conv_str(const char *, union el *);
static int match_numlist(long, const union el *);
static int match_strlist(const char *, const union el *);

static int
usage(int opt)
{
	if (!i_am_pkill)
		fprintf(stderr, "Usage: pgrep [-flnvx] [-d DELIM] ");
	else
		fprintf(stderr, "Usage: pkill [-SIGNAL] [-fnvx] ");
	fprintf(stderr, "[-P PPIDLIST] [-g PGRPLIST] [-s SIDLIST]\n"
		"\t[-u EUIDLIST] [-U UIDLIST] [-G GIDLIST] [-t TERMLIST] "
		"[PATTERN]\n");
	exit(opt == '?' ? 0 : 2);
	return -1;		/* this is never reached */
}

static void
parse_opts(int argc, char **argv)
{
	char opts[32] = "";
	int opt;
	int criteria_count = 0;

	if (strstr(argv[0], "pkill")) {
		i_am_pkill = 1;
		progname = "pkill";
		/* Look for a signal name or number as first argument */
		if (argc > 1 && argv[1][0] == '-') {
			int sig;
			sig = get_signal2(argv[1] + 1);
			if (sig == -1 && isdigit(argv[1][1]))
				sig = atoi(argv[1] + 1);
			if (sig != -1) {
				int i;
				for (i = 2; i < argc; i++)
					argv[i - 1] = argv[i];
				--argc;
				opt_signal = sig;
			}
		}
	} else {
		/* These options are for pgrep only */
		strcat(opts, "ld:");
	}

	strcat(opts, "fnvVxP:g:s:u:U:G:t:?");

	while ((opt = getopt(argc, argv, opts)) != -1) {
		switch (opt) {
		case 'f':
			opt_full = 1;
			break;
		case 'l':
			opt_long = 1;
			break;
		case 'n':
			opt_newest = 1;
			++criteria_count;
			break;
		case 'v':
			opt_negate = 1;
			break;
		case 'x':
			opt_exact = 1;
			break;
		case 'd':
			opt_delim = strdup(optarg);
			break;
		case 'P':
			opt_ppid = split_list(optarg, ',', conv_num);
			if (opt_ppid == NULL)
				usage(opt);
			++criteria_count;
			break;
		case 'g':
			opt_pgrp = split_list(optarg, ',', conv_pgrp);
			if (opt_pgrp == NULL)
				usage(opt);
			break;
		case 's':
			opt_sid = split_list(optarg, ',', conv_sid);
			if (opt_sid == NULL)
				usage(opt);
			++criteria_count;
			break;
		case 'u':
			opt_euid = split_list(optarg, ',', conv_uid);
			if (opt_euid == NULL)
				usage(opt);
			++criteria_count;
			break;
		case 'U':
			opt_uid = split_list(optarg, ',', conv_uid);
			if (opt_uid == NULL)
				usage(opt);
			++criteria_count;
			break;
		case 'G':
			opt_gid = split_list(optarg, ',', conv_gid);
			if (opt_gid == NULL)
				usage(opt);
			++criteria_count;
			break;
		case 't':
			opt_term = split_list(optarg, ',', conv_str);
			if (opt_term == NULL)
				usage(opt);
			++criteria_count;
			break;
		case 'V':
			display_version();
			exit(0);
			break;
		case '?':
			usage(opt);
			break;
		}
	}
	if (argc - optind == 1)
		opt_pattern = argv[optind];
	else if (argc - optind > 1)
		usage(0);
	else if (criteria_count == 0) {
		fprintf(stderr, "%s: No matching criteria specified\n",
			progname);
		usage(0);
	}
}

static union el *
split_list(const char *str, char sep, int (*convert) (const char *, union el *))
{
	char *copy = strdup(str);
	char *ptr = copy;
	char *sep_pos;
	int i = 1, size = 32;
	union el *list;

	list = malloc(size * sizeof (union el));
	if (list == NULL)
		exit(3);

	do {
		sep_pos = strchr(ptr, sep);
		if (sep_pos)
			*sep_pos = 0;
		if (convert(ptr, &list[i]))
			++i;
		else
			exit(2);
		if (i == size) {
			size *= 2;
			list = realloc(list, size * sizeof (union el));
			if (list == NULL)
				exit(3);
		}
		if (sep_pos)
			ptr = sep_pos + 1;
	} while (sep_pos);

	free(copy);
	if (i == 1) {
		free(list);
		list = NULL;
	} else {
		list[0].num = i - 1;
	}
	return (list);
}

/* strict_atol returns a Boolean: TRUE if the input string contains a
   plain number, FALSE if there are any non-digits. */

static int
strict_atol(const char *str, long *value)
{
	int res = 0;
	int sign = 1;

	if (*str == '+')
		++str;
	else if (*str == '-') {
		++str;
		sign = -1;
	}

	for (; *str; ++str) {
		if (!isdigit(*str))
			return (0);
		res *= 10;
		res += *str - '0';
	}
	*value = sign * res;
	return (1);
}

static int
conv_uid(const char *name, union el *e)
{
	struct passwd *pwd;

	if (strict_atol(name, &e->num))
		return (1);

	pwd = getpwnam(name);
	if (pwd == NULL) {
		fprintf(stderr, "%s: invalid user name: %s\n", progname, name);
		return (0);
	}
	e->num = pwd->pw_uid;
	return (1);
}

static int
conv_gid(const char *name, union el *e)
{
	struct group *grp;

	if (strict_atol(name, &e->num))
		return (1);

	grp = getgrnam(name);
	if (grp == NULL) {
		fprintf(stderr, "%s: invalid group name: %s\n", progname, name);
		return (0);
	}
	e->num = grp->gr_gid;
	return (1);
}

static int
conv_pgrp(const char *name, union el *e)
{
	if (!strict_atol(name, &e->num)) {
		fprintf(stderr, "%s: invalid process group: %s\n",
			progname, name);
		return (0);
	}
	if (e->num == 0)
		e->num = getpgrp();
	return (1);
}

static int
conv_sid(const char *name, union el *e)
{
	if (!strict_atol(name, &e->num)) {
		fprintf(stderr, "%s: invalid session id: %s\n", progname, name);
		return (0);
	}
	if (e->num == 0)
		e->num = getsid(0);
	return (1);
}

static int
conv_num(const char *name, union el *e)
{
	if (!strict_atol(name, &e->num)) {
		fprintf(stderr, "%s: not a number: %s\n", progname, name);
		return (0);
	}
	return (1);
}

static int
conv_str(const char *name, union el *e)
{
	e->str = strdup(name);
	return (1);
}

static int
match_numlist(long value, const union el *list)
{
	int found = 0;
	if (list == NULL)
		found = 0;
	else {
		int i;
		for (i = list[0].num; i > 0; i--) {
			if (list[i].num == value)
				found = 1;
		}
	}
	return (found);
}

static int
match_strlist(const char *value, const union el *list)
{
	int found = 0;
	if (list == NULL)
		found = 0;
	else {
		int i;
		for (i = list[0].num; i > 0; i--) {
			if (!strcmp(list[i].str, value))
				found = 1;
		}
	}
	return (found);
}

static void
output_numlist(const union el *list)
{
	int i;
	for (i = 1; i < list[0].num; i++)
		printf("%ld%s", list[i].num, opt_delim);

	if (list[0].num)
		printf("%ld\n", list[i].num);
}

static void
output_strlist(const union el *list)
{
	int i;
	for (i = 1; i < list[0].num; i++)
		printf("%s%s", list[i].str, opt_delim);

	if (list[0].num)
		printf("%s\n", list[i].str);
}

static PROCTAB *
do_openproc(void)
{
	PROCTAB *ptp;
	int flags = PROC_FILLANY;

	if (opt_pattern || opt_full)
		flags |= PROC_FILLCMD;
	if (opt_uid)
		flags |= PROC_FILLSTATUS;
	if (opt_euid && !opt_negate) {
		int num = opt_euid[0].num;
		int i = num;
		uid_t *uids = malloc(num * sizeof (uid_t));
		if (uids == NULL)
			exit(3);
		while (i-- > 0) {
			uids[i] = opt_euid[i + 1].num;
		}
		flags |= PROC_UID;
		ptp = openproc(flags, uids, num);
	} else {
		ptp = openproc(flags);
	}
	return (ptp);
}

static regex_t *
do_regcomp(void)
{
	regex_t *preg = NULL;

	if (opt_pattern) {
		char *re;
		char errbuf[256];
		int re_err;

		preg = malloc(sizeof (regex_t));
		if (preg == NULL)
			exit(3);
		if (opt_exact) {
			re = malloc(strlen(opt_pattern) + 5);
			if (re == NULL)
				exit(3);
			sprintf(re, "^(%s)$", opt_pattern);
		} else {
			re = opt_pattern;
		}

		re_err = regcomp(preg, re, REG_EXTENDED | REG_NOSUB);
		if (re_err) {
			regerror(re_err, preg, errbuf, sizeof (errbuf));
			fprintf(stderr, errbuf);
			exit(2);
		}
	}
	return preg;
}

#ifdef NOT_USED
static time_t
jiffies_to_time_t(long jiffies)
{
	static time_t time_of_boot = 0;
	if (time_of_boot == 0) {
		time_of_boot = time(NULL) - uptime(0, 0);
	}
	return (time_of_boot + jiffies / Hertz);
}
#endif

static union el *
select_procs(void)
{
	PROCTAB *ptp;
	proc_t task;
	unsigned long long newest_start_time = 0;
	pid_t newest_pid = 0;
	int matches = 0;
	int size = 32;
	regex_t *preg;
	pid_t myself = getpid();
	union el *list;
	char cmd[4096];

	list = malloc(size * sizeof (union el));
	if (list == NULL)
		exit(3);

	ptp = do_openproc();
	preg = do_regcomp();

	memset(&task, 0, sizeof (task));
	while (readproc(ptp, &task)) {
		int match = 1;

		if (task.pid == myself)
			continue;
		else if (opt_newest && task.start_time < newest_start_time)
			match = 0;
		else if (opt_ppid && !match_numlist(task.ppid, opt_ppid))
			match = 0;
		else if (opt_pgrp && !match_numlist(task.pgrp, opt_pgrp))
			match = 0;
		else if (opt_euid && !match_numlist(task.euid, opt_euid))
			match = 0;
		else if (opt_uid && !match_numlist(task.ruid, opt_uid))
			match = 0;
		else if (opt_gid && !match_numlist(task.rgid, opt_gid))
			match = 0;
		else if (opt_sid && !match_numlist(task.session, opt_sid))
			match = 0;
		else if (opt_term) {
			if (task.tty == -1) {
				match = 0;
			} else {
				char tty[256];
				dev_to_tty(tty, sizeof (tty) - 1,
					   task.tty, task.pid, ABBREV_DEV);
				match = match_strlist(tty, opt_term);
			}
		}
		if (opt_long || (match && opt_pattern)) {
			if (opt_full && task.cmdline) {
				int i = 0;
				int bytes = sizeof (cmd) - 1;

				/* make sure it is always NUL-terminated */
				cmd[bytes] = 0;
				/* make room for SPC in loop below */
				--bytes;

				strncpy(cmd, task.cmdline[i], bytes);
				bytes -= strlen(task.cmdline[i++]);
				while (task.cmdline[i] && bytes > 0) {
					strncat(cmd, " ", bytes);
					strncat(cmd, task.cmdline[i], bytes);
					bytes -= strlen(task.cmdline[i++]) + 1;
				}
			} else {
				strcpy(cmd, task.cmd);
			}
		}

		if (match && opt_pattern) {
			if (regexec(preg, cmd, 0, NULL, 0) != 0)
				match = 0;
		}

		if (match ^ opt_negate) {	/* Exclusive OR is neat */
			if (opt_newest) {
				if (newest_start_time == task.start_time &&
				    newest_pid > task.pid)
					continue;
				newest_start_time = task.start_time;
				newest_pid = task.pid;
				matches = 0;
			}
			if (opt_long) {
				char buff[4096];
				sprintf(buff, "%d %s", task.pid, cmd);
				list[++matches].str = strdup(buff);
			} else {
				list[++matches].num = task.pid;
			}
			if (matches == size) {
				size *= 2;
				list = realloc(list, size * sizeof (union el));
				if (list == NULL)
					exit(3);
			}
		}

		memset(&task, 0, sizeof (task));
	}
	closeproc(ptp);

	list[0].num = matches;
	return (list);
}

int
main(int argc, char **argv)
{
	union el *procs;

	parse_opts(argc, argv);

	procs = select_procs();
	if (i_am_pkill) {
		int i;
		for (i = 1; i <= procs[0].num; i++) {
			if (kill(procs[i].num, opt_signal) == -1)
				fprintf(stderr, "pkill: %ld - %s\n",
					procs[i].num, strerror(errno));
		}
	} else {
		if (opt_long)
			output_strlist(procs);
		else
			output_numlist(procs);
	}
	return ((procs[0].num) == 0 ? 1 : 0);
}
