/*
 * Copyright (c) 2012 Eric Radman <ericshane@eradman.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/param.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/wait.h>

#include <sys/event.h>

#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <paths.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#include "missing/compat.h"

#include "data.h"
#include "status.h"

/* events to watch for */

#define NOTE_ALL NOTE_DELETE | NOTE_WRITE | NOTE_RENAME | NOTE_TRUNCATE | NOTE_ATTRIB

/* shortcuts */

#define min(a, b) (((a) < (b)) ? (a) : (b))
#define MEMBER_SIZE(S, M) sizeof(((S *) 0)->M)

/* shared state */

extern int optind;
pid_t status_pid;
WatchFile **files;

/* globals */

WatchFile *leading_edge;
int child_pid;
int child_status;
int terminating;
int restart_signal;

int aggressive_opt;
int clear_opt;
int dirwatch_opt;
int noninteractive_opt;
int oneshot_opt;
int postpone_opt;
int restart_opt;
int shell_opt;
int status_filter_opt;

int termios_set;
struct termios canonical_tty;

static char *shell, *shell_base;
static char *argv0, *argv0_base;

/* function pointers */

int (*xstat)(const char *path, struct stat *sb);

/* forwards */

static void usage(bool);
static void terminate_utility();
static void set_restart_signal();
static void handle_exit(int sig);
static void proc_exit(int sig);
static void print_child_status(int status);
static int process_input(FILE *, WatchFile *[], int);
static int set_options(char *[]);
static int list_dir(char *);
static void run_utility(char *[]);
static void watch_file(int, WatchFile *);
static int compare_dir_contents(WatchFile *);
static void watch_loop(int, char *[]);

/*
 * The Event Notify Test Runner
 * run arbitrary commands when files change
 */
int
main(int argc, char *argv[]) {
	int kq;
	struct sigaction act;
	int ttyfd;
	short argv_index;
	int n_files;
	int i;
	struct kevent evSet;
	int open_max;

	/* call usage() if no command is supplied */
	if (argc < 2)
		usage(false);
	argv_index = set_options(argv);

	sigemptyset(&act.sa_mask);

	/* normally a user will exit this utility by do_execting Ctrl-C */
	act.sa_flags = SA_RESETHAND;
	act.sa_handler = handle_exit;
	if (sigaction(SIGINT, &act, NULL) != 0)
		err(1, "Failed to set SIGINT handler");
	if (sigaction(SIGTERM, &act, NULL) != 0)
		err(1, "Failed to set SIGTERM handler");
	if (sigaction(SIGHUP, &act, NULL) != 0)
		err(1, "Failed to set SIGHUP handler");

	set_restart_signal();

	/* notification used to combine the one-shot and restart options */
	act.sa_flags = 0;
	act.sa_handler = proc_exit;
	if (sigaction(SIGCHLD, &act, NULL) != 0)
		err(1, "Failed to set SIGCHLD handler");

	/* monitor symlinks if possible */
	xstat = stat;
#if defined(O_PATH) || defined(O_SYMLINK)
	if (getenv("ENTR_FOLLOW_SYMLINK") == NULL)
		xstat = lstat;
#endif

#if defined(_LINUX_PORT)
	/* attempt to read inotify limits */
	open_max = (unsigned) fs_sysctl(INOTIFY_MAX_USER_WATCHES);
	if (open_max == 0)
		open_max = 65536;
#elif defined(_MACOS_PORT)
	struct rlimit rl;
	int mib[2] = { CTL_KERN, KERN_MAXFILESPERPROC };
	size_t namelen = sizeof(open_max);

	if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
		err(1, "getrlimit");
	if (sysctl(mib, 2, &open_max, &namelen, NULL, 0) == -1)
		open_max = OPEN_MAX;
	rl.rlim_cur = open_max;
	if (setrlimit(RLIMIT_NOFILE, &rl) != 0)
		err(1, "setrlimit cannot set rlim_cur to %u", open_max);
#else /* BSD */
	struct rlimit rl;

	if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
		err(1, "getrlimit");
	open_max = (unsigned) rl.rlim_max;
	rl.rlim_cur = (rlim_t) open_max;
	if (setrlimit(RLIMIT_NOFILE, &rl) != 0)
		err(1, "setrlimit cannot set rlim_cur to %u", open_max);
#endif

	if (getenv("EV_TRACE"))
		fprintf(stderr, "open_max: %d\n", open_max);

	/* prevent interactive utilities from paging output */
	setenv("PAGER", "/bin/cat", 0);

	/* ensure a shell is available to use */
	if ((shell = getenv("SHELL")) == NULL)
		shell = "/bin/sh";
	shell_base = strdup(shell);
	shell_base = basename(shell_base);

	/* initialize status filter */
	if (shell_opt)
		argv0 = shell;
	else
		argv0 = (argv + argv_index)[0];
	argv0_base = basename(argv0);
	if (status_filter_opt)
		start_log_filter(status_filter_opt);

	/* drop privileges */
	if (pledge("stdio rpath tty proc exec", NULL) == -1)
		err(1, "pledge");

	/* sequential scan may depend on a 0 at the end */
	files = calloc(open_max + 1, sizeof(WatchFile *));
	if (files == NULL)
		err(1, "calloc");

	if ((kq = kqueue()) == -1)
		err(1, "cannot create kqueue");

	/* expect file list from a pipe */
	if (isatty(fileno(stdin)))
		usage(false);

	/* read input and populate watch list, skipping non-regular files */
	n_files = process_input(stdin, files, open_max);
	if (n_files == 0)
		errx(1, "No regular files to watch");
	if (n_files == -1)
		errx(1,
		    "Too many files listed; the hard limit for your login"
		    " class is %u. Please consult"
		    " http://eradman.com/entrproject/limits.html",
		    open_max);
	for (i = 0; i < n_files; i++)
		watch_file(kq, files[i]);

	if (!noninteractive_opt) {
		/* Attempt to open a tty so that editors don't complain */
		ttyfd = open(_PATH_TTY, O_RDONLY);
		if (ttyfd > STDIN_FILENO) {
			if (dup2(ttyfd, STDIN_FILENO) != 0)
				warnx("can't dup2 to stdin");
			close(ttyfd);
		}

		/* remember terminal settings */
		if (tcgetattr(STDIN_FILENO, &canonical_tty) == -1)
			errx(1, "unable to get terminal attributes, use '-n' to run non-interactively");

		/* Use keyboard input as a trigger */
		EV_SET(&evSet, STDIN_FILENO, EVFILT_READ, EV_ADD, NOTE_LOWAT, 1, NULL);
		if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1)
			warnx("failed to register stdin");
	}

	watch_loop(kq, argv + argv_index);
	return 1;
}

/* Utility functions */

void
usage(bool summary) {
	fprintf(stderr, "release: %s\n", RELEASE);
	fprintf(stderr, "usage: entr [-acdnprsxz] utility [argument [/_] ...] < filenames\n");
	if (!summary) {
		fprintf(stderr, "hint: use -h to display option summary\n");
		goto end;
	}

	printf("summary:\n"
	       "    -a  Do not consolidate events\n"
	       "    -c  Clear screen before execution\n"
	       "    -d  Track files added or removed from directories\n"
	       "    -n  Non-interactive mode\n"
	       "    -p  Wait for first event\n"
	       "    -r  Run as a background process, use signal to restart\n"
	       "    -s  Evaluate using a shell\n"
	       "    -x  Format exit status\n"
	       "    -z  Exit after the utility completes\n");
	printf("docs:\n"
	       "    man entr\n");

end:
	exit(1);
}

void
terminate_utility() {
	int status;

	terminating = 1;

	if (child_pid > 0) {
		killpg(child_pid, restart_signal);
		waitpid(child_pid, &status, 0);
		child_pid = 0;
	}

	terminating = 0;
}

void
set_restart_signal() {
	const char *sig;
	const int signum[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 };
	const char *signame[] = { "HUP", "INT", "QUIT", "TERM", "USR1", "USR2" };
	int i;

	if ((sig = getenv("ENTR_RESTART_SIGNAL")) == NULL) {
		restart_signal = SIGTERM;
		return;
	}

	if (strncmp(sig, "SIG", 3) == 0)
		sig += 3;

	for (i = 0; signum[i] < 6; i++) {
		if (strcmp(sig, signame[i]) == 0)
			restart_signal = signum[i];
	}

	if (restart_signal == 0)
		errx(1, "unrecognized signal: %s <> (HUP, INT, QUIT, TERM, USR1, USR2)", sig);
}

/* Callbacks */

void
handle_exit(int sig) {
	if ((!noninteractive_opt) && (termios_set))
		tcsetattr(STDIN_FILENO, TCSADRAIN, &canonical_tty);

	terminate_utility();

	if (status_filter_opt)
		end_log_filter();

	if ((sig == SIGINT || sig == SIGHUP))
		_exit(0);
	else
		raise(sig);
}

void
proc_exit(int sig) {
	int status;
	int saved_errno = errno;

	if (status_filter_opt && (terminating == 0)) {
		if (waitpid(status_pid, &status, WNOHANG) > 0) {
			if (WIFSIGNALED(status)) {
				terminating = 1;
				warnx("status process killed by signal");
				kill(getpid(), SIGINT);
			}
			if (WIFEXITED(status)) {
				terminating = 1;
				warnx("status process terminated");
				kill(getpid(), SIGINT);
			}
		}
	}

	if (waitpid(child_pid, &status, 0) != -1) {
		child_status = status;

		if ((!noninteractive_opt) && (termios_set))
			tcsetattr(STDIN_FILENO, TCSADRAIN, &canonical_tty);

		if ((oneshot_opt == 1) && (terminating == 0)) {
			if (restart_opt == 0)
				print_child_status(child_status);

			if (WIFSIGNALED(child_status))
				_exit(128 + WTERMSIG(child_status));
			else
				_exit(WEXITSTATUS(child_status));
		}
	}
	/* restore errno so that the resuming code is unimpacted. */
	errno = saved_errno;
}

void
print_child_status(int status) {
	int len;
	char buf[2048];

	if (status_filter_opt) {
		if (WIFSIGNALED(status))
			len = snprintf(buf, sizeof(buf), "signal|%d|%s\n", WTERMSIG(status), argv0_base);
		else
			len = snprintf(buf, sizeof(buf), "exit|%d|%s\n", WEXITSTATUS(status), argv0_base);
		write_log_filter(buf, len);
	}
}

/*
 * Read lines from a file stream (normally STDIN).  Returns the number of
 * regular files to be watched or -1 if max_files is exceeded.
 */
int
process_input(FILE *file, WatchFile *files[], int max_files) {
	char buf[PATH_MAX];
	char *p, *path, *parent_path;
	int n_files = 0;
	struct stat sb;
	int i, matches;

	while (fgets(buf, sizeof(buf), file) != NULL) {
		if ((p = strchr(buf, '\n')) != NULL)
			*p = '\0';
		if (buf[0] == '\0')
			continue;
		path = &buf[0];

		if (xstat(path, &sb) == -1) {
			warnx("unable to stat '%s'", path);
			continue;
		}

		if ((S_ISREG(sb.st_mode) | S_ISLNK(sb.st_mode)) != 0) {
			files[n_files] = malloc(sizeof(WatchFile));
			if (files[n_files] == NULL)
				err(1, "malloc");
			strlcpy(files[n_files]->fn, path, MEMBER_SIZE(WatchFile, fn));
			files[n_files]->is_dir = 0;
			files[n_files]->is_symlink = (S_ISLNK(sb.st_mode) != 0) ? 1 : 0;
			files[n_files]->file_count = 0;
			files[n_files]->mode = sb.st_mode;
			files[n_files]->ino = sb.st_ino;
			n_files++;

			/* also watch the directory if it's not already in the list */
			if (dirwatch_opt > 0) {
				if ((parent_path = dirname(path)) == 0)
					err(1, "dirname '%s' failed", path);
				for (matches = 0, i = 0; i < n_files; i++) {
					if ((files[i]->is_dir == 1) && (strcmp(files[i]->fn, parent_path) == 0))
						matches++;
				}
				if (matches == 0) {
					if (stat(parent_path, &sb) == -1)
						warnx("unable to stat '%s'", parent_path);
					path = parent_path;
				}
			}
		}
		if (S_ISDIR(sb.st_mode) != 0) {
			files[n_files] = malloc(sizeof(WatchFile));
			if (files[n_files] == NULL)
				err(1, "malloc");
			strlcpy(files[n_files]->fn, path, MEMBER_SIZE(WatchFile, fn));
			files[n_files]->is_dir = 1;
			files[n_files]->is_symlink = 0;
			files[n_files]->file_count = list_dir(path);
			files[n_files]->mode = sb.st_mode;
			files[n_files]->ino = sb.st_ino;
			n_files++;
		}
		if (n_files + 1 > max_files)
			return -1;
	}
	return n_files;
}

int
list_dir(char *dir) {
	struct dirent *dp;
	DIR *dfd = opendir(dir);
	int count = 0;

	if (dfd == NULL)
		errx(1, "unable to open directory: '%s'", dir);
	while ((dp = readdir(dfd)) != NULL)
		if ((dirwatch_opt == 2) || (dp->d_name[0] != '.'))
			count++;
	closedir(dfd);
	return count;
}

/*
 * Evaluate command line arguments and return an offset to the command to
 * execute.
 */
int
set_options(char *argv[]) {
	int ch;
	int argc;

	if (argv[1] && strcmp(argv[1], "-h") == 0)
		usage(true);

	/* read arguments until we reach a command */
	for (argc = 1; argv[argc] != 0 && argv[argc][0] == '-'; argc++)
		;
	while ((ch = getopt(argc, argv, "acdnprsxz")) != -1) {
		switch (ch) {
		case 'a':
			aggressive_opt = 1;
			break;
		case 'c':
			clear_opt = clear_opt ? 2 : 1;
			break;
		case 'd':
			dirwatch_opt = dirwatch_opt ? 2 : 1;
			break;
		case 'n':
			noninteractive_opt = 1;
			break;
		case 'p':
			postpone_opt = 1;
			break;
		case 'r':
			restart_opt = 1;
			break;
		case 's':
			shell_opt = 1;
			break;
		case 'x':
			status_filter_opt = status_filter_opt ? 2 : 1;
			break;
		case 'z':
			oneshot_opt = 1;
			break;
		default:
			usage(false);
		}
	}
	if (argv[optind] == 0)
		usage(false);

	if (status_filter_opt && restart_opt)
		errx(1, "-r and -x may not be combined");

	if ((shell_opt == 1) && (argv[optind + 1] != 0))
		errx(1, "-s requires commands to be formatted as a single argument");
	return optind;
}

/*
 * Execute the program supplied on the command line. If restart was set
 * then send the child process SIGTERM and restart it.
 */
void
run_utility(char *argv[]) {
	int pid;
	int i, m;
	int ret, status;
	struct timespec delay = { 0, 1000000 };
	char **new_argv;
	char *p, *arg_buf;
	int argc;
	size_t remaining;

	if (restart_opt == 1)
		terminate_utility();

	arg_buf = malloc(ARG_MAX);

	if (shell_opt == 1) {
		/* run argv[1] with a shell using the leading edge as $0 */
		argc = 4;
		new_argv = calloc(argc + 1, sizeof(char *));
		if (new_argv == NULL)
			err(1, "calloc");
		new_argv[0] = shell;
		new_argv[1] = "-c";
		new_argv[2] = argv[0];
		new_argv[3] = leading_edge->fn;
	} else {
		/* clone argv on each invocation to make the implementation of more
		 * complex substitution rules possible and easy
		 */
		for (argc = 0; argv[argc]; argc++)
			;
		new_argv = calloc(argc + 1, sizeof(char *));
		if (new_argv == NULL)
			err(1, "calloc");
		new_argv[0] = "/bin/false";
		for (m = 0, i = 0, p = arg_buf; i < argc; i++) {
			remaining = ARG_MAX - (p - arg_buf);
			new_argv[i] = p;
			if ((m < 1) && (strcmp(argv[i], "/_")) == 0) {
				p += strlcpy(p, leading_edge->fn, remaining);
				m++;
			} else
				p += strlcpy(p, argv[i], remaining);
			p++;
		}
	}

	pid = fork();
	if (pid == -1)
		err(1, "can't fork");

	if (pid == 0) {
		/* 2J - erase the entire display
		 * 3J - clear scrollback buffer
		 * H  - set cursor position to the default
		 */
		if (clear_opt == 1)
			printf("\033[2J\033[H");
		if (clear_opt == 2)
			printf("\033[2J\033[3J\033[H");
		fflush(stdout);

		/* Set process group so subprocess can be signaled */
		if (restart_opt == 1) {
			setpgid(0, getpid());
			close(STDIN_FILENO);
			open(_PATH_DEVNULL, O_RDONLY);
		}
		/* wait up to 1 seconds for each file to become available */
		for (i = 0; i < 10; i++) {
			ret = execvp(new_argv[0], new_argv);
			if (errno == ETXTBSY)
				nanosleep(&delay, NULL);
			else
				break;
		}
		if (ret != 0)
			err(1, "exec %s", new_argv[0]);
	}
	child_pid = pid;

	if (restart_opt == 0 && oneshot_opt == 0) {
		if (waitpid(child_pid, &status, 0) != -1)
			child_status = status;

		print_child_status(child_status);
	}

	free(arg_buf);
	free(new_argv);
}

/*
 * Wait for file to become accessible and register a kevent to watch it
 */
void
watch_file(int kq, WatchFile *file) {
	struct kevent evSet;
	int i = 0;
	struct timespec delay = { 0, 100 * 1000000 };

	/* wait up to 1 second for file to become available */
	for (;;) {
#if defined(O_EVTONLY)
		file->fd = open(file->fn, O_RDONLY | O_CLOEXEC | O_EVTONLY | O_SYMLINK);
#elif defined(O_PATH)
		file->fd = open(file->fn, O_RDONLY | O_CLOEXEC | O_PATH | O_NOFOLLOW);
#else
		file->fd = open(file->fn, O_RDONLY | O_CLOEXEC);
#endif
		if (file->fd == -1) {
			if (i < 10)
				nanosleep(&delay, NULL);
			else {
				warn("cannot open '%s'", file->fn);
				terminate_utility();
				exit(1);
			}
		} else
			break;
		i++;
	}

	EV_SET(&evSet, file->fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_ALL, 0, file);
	if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1) {
		if (errno == ENOSPC)
			errx(1,
			    "Unable to allocate memory for kernel queue."
			    " Please consult"
			    " http://eradman.com/entrproject/limits.html");
		else
			err(1, "failed to register VNODE event");
	}
}

/*
 * Wait for directory contents to stabilize
 */
int
compare_dir_contents(WatchFile *file) {
	int i;
	struct timespec delay = { 0, 100 * 1000000 };

	/* wait up to 0.5 seconds for file to become available */
	for (i = 0; i < 5; i++) {
		if (list_dir(file->fn) == file->file_count)
			return 0;
		nanosleep(&delay, NULL);
	}
	return 1;
}

/*
 * Wait for events to and execute a command. Four major concerns are in play:
 *   leading_edge: Global reference to the first file to have changed
 *   reopen_only : Unlink or rename events which require us to spin while
 *                 waiting for the file to reappear. These must always be
 *                 processed
 *   collate_only: Changes that indicate that more events are likely to occur.
 *                 Watch for more events using a short timeout
 *   do_exec     : Delay execution until all events have been processed. Allow
 *                 the user to edit files while the utility is running without
 *                 any visible side-effects
 *   dir_modified: The number of files changed for a directory under watch
 */
void
watch_loop(int kq, char *argv[]) {
	struct kevent evSet;
	struct kevent evList[32];
	int nev;
	WatchFile *file;
	int i;
	struct timespec evTimeout = { 0, 1000000 };
	int reopen_only = !aggressive_opt;
	int collate_only = 0;
	int do_exec = 0;
	int dir_modified = 0;
	int leading_edge_set = 0;
	struct stat sb;
	char c;
	struct termios character_tty;

	leading_edge = files[0]; /* default */
	if (postpone_opt == 0)
		run_utility(argv);

	if (!noninteractive_opt) {
		/* disabling/restore line buffering and local echo */
		character_tty = canonical_tty;
		character_tty.c_lflag &= ~(ICANON | ECHO);
	}

main:
	if (!noninteractive_opt) {
		tcsetattr(STDIN_FILENO, TCSADRAIN, &character_tty);
		termios_set = 1;
	}

	if ((reopen_only == 1) || (collate_only == 1)) {
		nev = kevent(kq, NULL, 0, evList, 32, &evTimeout);
	} else {
		nev = kevent(kq, NULL, 0, evList, 32, NULL);
		dir_modified = 0;
	}

	if ((nev == -1) && (errno != EINTR))
		warn("kevent failed");

	for (i = 0; i < nev; i++) {
		if (!noninteractive_opt && evList[i].filter == EVFILT_READ) {
			if (read(STDIN_FILENO, &c, 1) < 1) {
				EV_SET(&evSet, STDIN_FILENO, EVFILT_READ, EV_DELETE, NOTE_LOWAT, 0, NULL);
				if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1)
					err(1, "failed to remove READ event");
			} else {
				if (c == ' ')
					do_exec = 1;
				if (c == 'q')
					kill(getpid(), SIGINT);
			}
		}
		if (evList[i].filter != EVFILT_VNODE)
			continue;

		file = (WatchFile *) evList[i].udata;
		if (file->is_dir == 1)
			dir_modified += compare_dir_contents(file);
	}
	if (!noninteractive_opt)
		tcsetattr(STDIN_FILENO, TCSADRAIN, &canonical_tty);

	collate_only = 0;
	for (i = 0; i < nev; i++) {
		if (evList[i].filter != EVFILT_VNODE)
			continue;
		file = (WatchFile *) evList[i].udata;
		if (evList[i].fflags & NOTE_DELETE || evList[i].fflags & NOTE_RENAME) {
			EV_SET(&evSet, file->fd, EVFILT_VNODE, EV_DELETE, NOTE_ALL, 0, file);
			if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1)
				err(1, "failed to remove VNODE event");
#if !defined(_LINUX_PORT)
			/* free file descriptor no longer monitored by kqueue */
			if ((file->fd != -1) && (close(file->fd) == -1))
				err(1, "unable to close file");
#endif
			watch_file(kq, file);
			collate_only = 1;
		}
	}
	if (reopen_only == 1) {
		reopen_only = 0;
		goto main;
	}

	for (i = 0; i < nev && reopen_only == 0; i++) {

		if (evList[i].filter != EVFILT_VNODE)
			continue;
		file = (WatchFile *) evList[i].udata;
		if ((file->is_dir == 1) && (dir_modified == 0))
			continue;

		if (evList[i].fflags & NOTE_DELETE || evList[i].fflags & NOTE_WRITE
		    || evList[i].fflags & NOTE_RENAME || evList[i].fflags & NOTE_TRUNCATE) {
			if ((dir_modified > 0) && (restart_opt == 1))
				continue;
			do_exec = 1;
		}

		if (evList[i].fflags & NOTE_ATTRIB && S_ISREG(file->mode) != 0
		    && xstat(file->fn, &sb) == 0) {
			if (file->mode != sb.st_mode) {
				do_exec = 1;
				file->mode = sb.st_mode;
			}
			if (file->ino != sb.st_ino) {
#if defined(_LINUX_PORT)
				do_exec = 1;
#endif
				file->ino = sb.st_ino;
			}
		} else if (evList[i].fflags & NOTE_ATTRIB)
			continue;

		if ((leading_edge_set == 0) && (file->is_dir == 0) && (do_exec == 1)) {
			leading_edge = file;
			leading_edge_set = 1;
		}

		if (getenv("EV_TRACE")) {
			fprintf(stderr, "%d/%d: fflags: 0x%x %s %o %s\n", i, nev, evList[i].fflags,
			    file->is_dir ? "d" : "r", file->mode, file->fn);
		}
	}

	if (collate_only == 1)
		goto main;
	if (do_exec == 1) {
		do_exec = 0;
		run_utility(argv);
		if (!aggressive_opt)
			reopen_only = 1;
		leading_edge_set = 0;
	}
	if (dir_modified > 0) {
		terminate_utility();
		errx(2, "directory altered");
	}

	goto main;
}
