entr-4.4/0000755000175000017500000000000013603450174011634 5ustar ydeliaydeliaentr-4.4/NEWS0000644000175000017500000002331613603450174012340 0ustar ydeliaydelia= Release History == 4.4: January 02, 2019 - Use a single inotify queue on Linux, limited by /proc/sys/fs/inotify/max_user_watches - Set the environment variable `ENTR_INOTIFY_WORKAROUND` to enable a compatibility mode for platforms with deformed inotify support == 4.3: September 25, 2019 - Move hosting from bitbucket.org to github.com - Rename tag names from entr-X.Y to X.Y - Replace hg with git as a dependency for system-test.sh == 4.2: March 28, 2019 - Use pledge(2) on OpenBSD - New '-a' option enables response to events that occur while the utility is running - Error messages direct users to http://eradman.com/entrproject/limits.html - Correctly report error when a file cannot be reopened == 4.1: June 13, 2018 - New '-n' non-interactive option disables keyboard input - EV_TRACE environment variable enables file system event tracing - Track changes to the inode number as a workaround for missing delete events on the Linux kernel == 4.0: February 02, 2018 - Warn instead of error if kqueue fails to register on STDIN - Close STDIN before running the utility when the restart option is used - Restore terminal settings if terminated by a signal == 3.9: September 19, 2017 - Fix use of poll(2) to avoid possible busy-loop on Linux - Disable keyboard input if reading STDIN fails == 3.8: August 11, 2017 - Run the utility if the spacebar is pressed - 'q' for quit == 3.7: February 27, 2017 - Terminate subprocess in restart mode if a file under watch disappears - Allow NOTE_ATTRIB to set '/_' only if file mode changes - New '-s' option executes commands using $SHELL -c - Print usage and exit if input is from a terminal instead of a pipe == 3.6: July 01, 2016 - Do not print warning if _TTY_PATH cannot be opened (for chroot, docker, ...) - Non-existent input files produce a warning instead of an error - Trigger run when permissions change on regular files == 3.5: April 01, 2016 - Removed contrib/* - Detect new subdirectories on Linux - Direct users to http://entrproject.org/limits.html if inotify hits a kernel limit == 3.4: December 12, 2015 - Terminate the child process before exiting when the reload option is combined with the directory watch option - Direct users to http://entrproject.org/limits.html if the maximum number of open files is exceeded == 3.3: October 23, 2015 - Substitute '/_' only with regular files when using the directory watch option - Bugfix when tracking the first file to change - Display release number along with usage == 3.2: April 9, 2015 - Execute the utility immediately after registering watch events. Old behavior is restored using new postpone option ('-p') - Relocated 'reload-browser' to http://entrproject.org/scripts/reload-browser - Drop FIFO support to further simply code and documentation. FIFO mode was conceptually good, but EXEC mode is intelligent hand easily handles the vast majority of use cases. - Remove kFreeBSD as a target platform == 3.1: March 4, 2015 - Return error code indicating exit reason after handling signals - Restart mode sets process group to ensure that wrapper scripts do not prevent signals from reaching child processes - Removed DEBUG printf statements == 3.0: December 19, 2014 - Man page highlights the flexibility of executing a commands using a shell - 'reload-browser', a cross-platform script for reloading the current tab in one or more browsers - system_test.sh auto-upgrades to bash if the default shell does not support the ERR trap - Workaround implemented for Linux using fstat(2) to detect if a binary was removed if inotify issues IN_ATTRIB but not IN_DELETE_SELF - Use calloc(3) to take advantage of overflow detection == 2.9: June 10, 2014 - RPM package by @funollet - Moved alternate Makefiles to contrib/ - Trigger actions when a file is truncated on BSD - Improved format of DEBUG messages - Added missing extern for missing/fmemopen.c to avoid warning on MacOS 10.7+ - No longer register NOTE_EXTEND; this event is not always merged with NOTE_WRITE on MacOS - New directory-watch option ('-d'). In this mode a shell while-loop can be used to track the addition of new files. == 2.8: March 15, 2014 - Iteratively check for new file system events using a 1ms timeout after a file has been renamed or removed in order to prevent the utility from being executed before successive changes from a version control system have completed. - Regression tests are now system tests that depend on and use Vim and Mercurial - Timing on regression tests has been tightened to only support real hardware; they are not expected to pass in emulated environments. - More correctly identify the first file to trigger a file change notification - Regression tests no longer require ksh - Renamed regress.sh to system_test.sh == 2.7: February 12, 2014 - Changed function pointer prefixes from '_' to 'x' to avoid runtime symbol collisions in libc. Required to build on DragonFlyBSD - Added this NEWS file to provide detailed release history for port maintainers - Subsitute '/_' argument with the first file that changed - Man page formatted with more semantically correct markup - Multiple events on the same file are merged on Linux to prevent duplicate writes to a named pipe == 2.6: January 23, 2014 - Added -c option to clear the screen before calling execvp(3) - Changed command line option '{}' to '/_' - Renamed Makefile.linux-lbsd to Makefile.debian - Include CPPFLAGS since some build systems (namely Debian) use it to specify hardening options - Raise ARG_MAX from 4K to 2MB on Linux, since _SC_ARG_MAX returns an incorrect value on Linux - Handle RENAME events that are not followed by DELETE. This enables us to cope with the backup option in Vim. == 2.5: December 30, 2013 - Restore full 50ms period for consolidating events on Linux. Required for the Geany editor - Remove use of setproctitle(3), no plans to implement this on Linux and MacOS. This also eliminates use of the 3-clause license - New feature: the first occurrence of '{}' on the command line will be replaced with the name of the first file under watch - Set PAGER to an absolute path (/bin/cat) - Prevent interactive utilities from paging output == 2.4: December 18, 2013 - Reintroduce ${DESTDIR} for STAGEDIR installation on FreeBSD and pkgsrc - License file describes the copyright holders for the compatibility libraries == 2.3: December 12, 2013 - Wait for processes to terminate in restart mode - Set process title for blocking events - Correct multiple bugs in the inotify->kqueue translation layer - Print out detailed instructions if the user provides configure arguments - Remove ${DESTDIR} from Makefile which appears to interfere with OpenBSD ports - The handler for SIGSEGV prints file name and line number if a unit test fails - Reduce the number of attempts to re-open files to 10 (total of 1 second) - Define architecture in Makefile instead of testing for __linux or __APPLE__ == 2.2: August 07, 2013 - Process every delete or rename event to ensure files remain tracked - Always process DELETE or RENAME events to prevent a files from escaping watch - inotify(7) events are consolidated for a duration of 50ms as a workaround for an apparent bug in glib/gio where a file is written and then removed - Execute mode can run a file under watch by retrying if text file is busy - Exit non-zero if list of regular files exceeds the hard limit set by the current login class - Ignore directories and special files; exit with error code 1 if no regular files are provided as input. == 2.1: July 01, 2013 - Zero-dependency build on Linux using built-in compatibility layer - Add poll with optional timeout in order to prevent change events from accumulating while running a command - Invalidate file->fd after inotify event is closed. Do not respond to IN_MOVE_SELF since this event only occurs on Linux NFS mounts - Optional build on GNU/Linux using external compatibility libraries libkqueue and libbsd - Fix bug in finding the correct filename to write to a FIFO - Correct build on MacOS == 2.0: June 17, 2013 - Support for old architectures without C99 support - More aggressively remove duplicate events - install/uninstall uses DESTDIR to be compatible with pkgsrc == 1.9: April 13, 2013 - New auto-reload option ('-r'). Thanks for Kevin Cox for the idea - Add a simple configure script, BSD is the default - Do not install directories...they should already exist - Man page installation path can be set with MANPATH. MacOS expects it to be under /usr/local/share == 1.8: May 12, 2013 - Ensure that loosing a file under watch is results in an error == 1.7: November 20, 2012 - Allocate an memory based on _SC_OPEN_MAX, not rl.rlim_max - Stat deleted file until success before running a command - Handle SIGTERM and remove named pipe on exit == 1.6: August 10, 2012 - Separate makefile for Linux to make building easier - No need for pthreads on BSD - Respond to RENAME and LINK events to improve behavior on NFS == 1.5: July 29, 2012 - Support interactive applications by opening a TTY - Successfully stat deleted files before running a command == 1.4: May 22, 2013 - Added regression tests - Delete events instead of relying on implicit removal by close(2) for better compatibility with libkqueue on Linux == 1.3: May 17, 2012 - Added FIFO support. Thanks to James K. Lowden for the idea - Use min(OPEN_MAX, rl.rlim_max) to be more compatible of Mac OS. - More correct use of fgets(3) when reading STDIN == 1.2: April 26, 2012 - Added support for Linux via libkqueue == 1.1: April 17, 2012 - Added support for Mac OS == 1.0: April 12, 2012 - Initial release at https://bitbucket.org/eradman/entr - Builds on all major BSD platforms entr-4.4/configure0000755000175000017500000000071013603450174013541 0ustar ydeliaydelia#!/bin/sh copy_mk() { cmd="cp Makefile.$1 Makefile" echo "$cmd"; $cmd } case `uname` in Darwin) copy_mk macos ;; Linux) copy_mk linux ;; *) copy_mk bsd ;; esac [ $# = 0 ] && exit 0 cat < * * 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 #include #include #include #include #include #include #include #include #include #include #include #include "compat.h" #include "../data.h" /* globals */ extern WatchFile **files; int read_stdin; /* forwards */ static WatchFile * file_by_descriptor(int fd); /* utility functions */ static WatchFile * file_by_descriptor(int wd) { int i; for (i=0; files[i] != NULL; i++) { if (files[i]->fd == wd) return files[i]; } return NULL; /* lookup failed */ } int fs_sysctl(const int name) { FILE *file; char line[8]; int value = 0; switch(name) { case INOTIFY_MAX_USER_WATCHES: file = fopen("/proc/sys/fs/inotify/max_user_watches", "r"); if (file == NULL || fgets(line, sizeof(line), file) == NULL) err(1, "max_user_watches"); value = atoi(line); fclose(file); break; } return value; } /* interface */ #define EVENT_SIZE (sizeof (struct inotify_event)) #define EVENT_BUF_LEN (32 * (EVENT_SIZE + 16)) #define IN_ALL IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF|IN_ATTRIB|IN_CREATE /* * inotify and kqueue ids both have the type `int` */ int kqueue(void) { static int inotify_queue; if (inotify_queue == 0) inotify_queue = inotify_init(); if (getenv("ENTR_INOTIFY_WORKAROUND")) warnx("broken inotify workaround enabled"); return inotify_queue; } /* * Emulate kqueue(2). Only monitors STDIN for EVFILT_READ and only the * EVFILT_VNODE flags used in entr.c are considered. Returns the number of * eventlist structs filled by this call */ int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout) { int n; int wd; WatchFile *file; char buf[EVENT_BUF_LEN]; ssize_t len; int pos; struct inotify_event *iev; u_int fflags; const struct kevent *kev; int ignored; struct pollfd *pfd; int nfds; pfd = calloc(2, sizeof(struct pollfd)); pfd[0].fd = kq; pfd[0].events = POLLIN; pfd[1].fd = STDIN_FILENO; pfd[1].events = POLLIN; if (nchanges > 0) { ignored = 0; for (n=0; nudata; if (kev->filter == EVFILT_READ) { if (kev->flags & EV_ADD) read_stdin = 1; if (kev->flags & EV_DELETE) read_stdin = 0; } if (kev->filter != EVFILT_VNODE) continue; if (kev->flags & EV_DELETE) { inotify_rm_watch(kq /* ifd */, kev->ident); file->fd = -1; /* invalidate */ } else if (kev->flags & EV_ADD) { if (getenv("ENTR_INOTIFY_WORKAROUND")) wd = inotify_add_watch(kq, file->fn, IN_ALL|IN_MODIFY); else wd = inotify_add_watch(kq, file->fn, IN_ALL); if (wd < 0) return -1; close(file->fd); file->fd = wd; /* replace with watch descriptor */ } else ignored++; } return nchanges - ignored; } if (read_stdin == 1) nfds = 2; /* inotify and stdin */ else nfds = 1; /* inotify */ if (timeout == NULL) poll(pfd, nfds, -1); else poll(pfd, nfds, timeout->tv_nsec/1000000); n = 0; do { if (pfd[0].revents & (POLLERR|POLLNVAL)) errx(1, "bad fd %d", pfd[0].fd); if (pfd[0].revents & POLLIN) { pos = 0; len = read(kq /* ifd */, &buf, EVENT_BUF_LEN); if (len < 0) { /* SA_RESTART doesn't work for inotify fds */ if (errno == EINTR) continue; else errx(1, "read of fd %d failed", pfd[0].fd); } while ((pos < len) && (n < nevents)) { iev = (struct inotify_event *) &buf[pos]; pos += EVENT_SIZE + iev->len; /* convert iev->mask; to comparable kqueue flags */ fflags = 0; if (iev->mask & IN_DELETE_SELF) fflags |= NOTE_DELETE; if (iev->mask & IN_CLOSE_WRITE) fflags |= NOTE_WRITE; if (iev->mask & IN_CREATE) fflags |= NOTE_WRITE; if (iev->mask & IN_MOVE_SELF) fflags |= NOTE_RENAME; if (iev->mask & IN_ATTRIB) fflags |= NOTE_ATTRIB; if (getenv("ENTR_INOTIFY_WORKAROUND")) if (iev->mask & IN_MODIFY) fflags |= NOTE_WRITE; if (fflags == 0) continue; /* merge events if we're not acting on a new file descriptor */ if ((n > 0) && (eventlist[n-1].ident == iev->wd)) fflags |= eventlist[--n].fflags; eventlist[n].ident = iev->wd; eventlist[n].filter = EVFILT_VNODE; eventlist[n].flags = 0; eventlist[n].fflags = fflags; eventlist[n].data = 0; eventlist[n].udata = file_by_descriptor(iev->wd); if (eventlist[n].udata) n++; } } if (read_stdin == 1) { if (pfd[1].revents & (POLLERR|POLLNVAL)) errx(1, "bad fd %d", pfd[1].fd); else if (pfd[1].revents & (POLLHUP|POLLIN)) { fflags = 0; eventlist[n].ident = pfd[1].fd; eventlist[n].filter = EVFILT_READ; eventlist[n].flags = 0; eventlist[n].fflags = fflags; eventlist[n].data = 0; eventlist[n].udata = NULL; n++; break; } } } while ((poll(pfd, nfds, 50) > 0)); free(pfd); return n; } entr-4.4/missing/compat.h0000644000175000017500000000101513603450174014736 0ustar ydeliaydelia/* compat.h */ #if !defined(NOTE_TRUNCATE) #define NOTE_TRUNCATE 0 #endif #if defined(_LINUX_PORT) && defined(__GLIBC__) #include size_t strlcpy(char *to, const char *from, int l); #endif #if defined(_LINUX_PORT) #define INOTIFY_MAX_USER_WATCHES 2 int fs_sysctl(const int name); #endif #if defined(_MACOS_PORT) #include FILE *fmemopen(void *buf, size_t size, const char *mode); #endif #if !defined(ARG_MAX) #define ARG_MAX (256 * 1024) #endif #ifndef __OpenBSD__ #define pledge(s, p) (0) #endif entr-4.4/missing/sys/0000755000175000017500000000000013603450174014123 5ustar ydeliaydeliaentr-4.4/missing/sys/event.h0000644000175000017500000000603113603450174015415 0ustar ydeliaydelia/*- * Copyright (c) 1999,2000,2001 Jonathan Lemon * 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. */ #include #include #define EVFILT_READ (-1) #define EVFILT_VNODE (-4) /* attached to vnodes */ /* actions */ #define EV_ADD 0x0001 /* add event to kq (implies enable) */ #define EV_DELETE 0x0002 /* delete event from kq */ #define EV_ENABLE 0x0004 /* enable event */ #define EV_DISABLE 0x0008 /* disable event (not reported) */ /* flags */ #define EV_ONESHOT 0x0010 /* only report one occurrence */ #define EV_CLEAR 0x0020 /* clear event state after reporting */ /* * data/hint flags for EVFILT_{READ|WRITE}, shared with userspace */ #define NOTE_LOWAT 0x0001 /* low water mark */ /* * data/hint flags for EVFILT_VNODE, shared with userspace */ #define NOTE_DELETE 0x0001 /* vnode was removed */ #define NOTE_WRITE 0x0002 /* data contents changed */ #define NOTE_EXTEND 0x0004 /* size increased */ #define NOTE_ATTRIB 0x0008 /* attributes changed */ #define NOTE_LINK 0x0010 /* link count changed */ #define NOTE_RENAME 0x0020 /* vnode was renamed */ #define NOTE_REVOKE 0x0040 /* vnode access was revoked */ #define NOTE_TRUNCATE 0x0080 /* vnode was truncated */ #define EV_SET(kevp, a, b, c, d, e, f) do { \ (kevp)->ident = (a); \ (kevp)->filter = (b); \ (kevp)->flags = (c); \ (kevp)->fflags = (d); \ (kevp)->data = (e); \ (kevp)->udata = (f); \ } while(0) struct kevent { u_int ident; /* identifier for this event */ short filter; /* filter for event */ u_short flags; u_int fflags; int data; void *udata; /* opaque user data identifier */ }; int kqueue(void); int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout); entr-4.4/missing/strlcpy.c0000644000175000017500000000303113603450174015146 0ustar ydeliaydelia/* $OpenBSD: strlcpy.c,v 1.11 2006/05/05 15:27:38 millert Exp $ */ /* * Copyright (c) 1998 Todd C. Miller * * 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 #include /* * Copy src to string dst of size siz. At most siz-1 characters * will be copied. Always NUL terminates (unless siz == 0). * Returns strlen(src); if retval >= siz, truncation occurred. */ size_t strlcpy(char *dst, const char *src, size_t siz) { char *d = dst; const char *s = src; size_t n = siz; /* Copy as many bytes as will fit */ if (n != 0) { while (--n != 0) { if ((*d++ = *s++) == '\0') break; } } /* Not enough room in dst, add NUL and traverse rest of src */ if (n == 0) { if (siz != 0) *d = '\0'; /* NUL-terminate dst */ while (*s++) ; } return(s - src - 1); /* count does not include NUL */ } entr-4.4/missing/fmemopen.c0000644000175000017500000000715713603450174015271 0ustar ydeliaydelia/* $OpenBSD: fmemopen.c,v 1.2 2013/03/27 15:06:25 mpi Exp $ */ /* * Copyright (c) 2011 Martin Pieuchot * Copyright (c) 2009 Ted Unangst * * 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 #include #include #include #include /* externs from from "local.h" */ FILE *__sfp(void); int __sflags(const char *, int *); struct state { char *string; /* actual stream */ size_t pos; /* current position */ size_t size; /* allocated size */ size_t len; /* length of the data */ int update; /* open for update */ }; static int fmemopen_read(void *v, char *b, int l) { struct state *st = v; int i; for (i = 0; i < l && i + st->pos < st->len; i++) b[i] = st->string[st->pos + i]; st->pos += i; return (i); } static int fmemopen_write(void *v, const char *b, int l) { struct state *st = v; int i; for (i = 0; i < l && i + st->pos < st->size; i++) st->string[st->pos + i] = b[i]; st->pos += i; if (st->pos >= st->len) { st->len = st->pos; if (st->len < st->size) st->string[st->len] = '\0'; else if (!st->update) st->string[st->size - 1] = '\0'; } return (i); } static fpos_t fmemopen_seek(void *v, fpos_t off, int whence) { struct state *st = v; ssize_t base = 0; switch (whence) { case SEEK_SET: break; case SEEK_CUR: base = st->pos; break; case SEEK_END: base = st->len; break; } if (off > st->size - base || off < -base) { errno = EOVERFLOW; return (-1); } st->pos = base + off; return (st->pos); } static int fmemopen_close(void *v) { free(v); return (0); } static int fmemopen_close_free(void *v) { struct state *st = v; free(st->string); free(st); return (0); } FILE * fmemopen(void *buf, size_t size, const char *mode) { struct state *st; FILE *fp; int flags, oflags; if (size == 0) { errno = EINVAL; return (NULL); } if ((flags = __sflags(mode, &oflags)) == 0) { errno = EINVAL; return (NULL); } if (buf == NULL && ((oflags & O_RDWR) == 0)) { errno = EINVAL; return (NULL); } if ((st = malloc(sizeof(*st))) == NULL) return (NULL); if ((fp = __sfp()) == NULL) { free(st); return (NULL); } st->pos = 0; st->len = (oflags & O_WRONLY) ? 0 : size; st->size = size; st->update = oflags & O_RDWR; if (buf == NULL) { if ((st->string = malloc(size)) == NULL) { free(st); fp->_flags = 0; return (NULL); } *st->string = '\0'; } else { st->string = (char *)buf; if (oflags & O_TRUNC) *st->string = '\0'; if (oflags & O_APPEND) { char *p; if ((p = memchr(st->string, '\0', size)) != NULL) st->pos = st->len = (p - st->string); else st->pos = st->len = size; } } fp->_flags = (short)flags; fp->_file = -1; fp->_cookie = (void *)st; fp->_read = (flags & __SWR) ? NULL : fmemopen_read; fp->_write = (flags & __SRD) ? NULL : fmemopen_write; fp->_seek = fmemopen_seek; fp->_close = (buf == NULL) ? fmemopen_close_free : fmemopen_close; return (fp); } entr-4.4/Makefile.linux0000644000175000017500000000024013603450174014426 0ustar ydeliaydeliaCPPFLAGS += -D_GNU_SOURCE -D_LINUX_PORT -Imissing MANPREFIX ?= ${PREFIX}/share/man EXTRA_SRC = missing/strlcpy.c missing/kqueue_inotify.c include Makefile.bsd entr-4.4/entr_spec.c0000644000175000017500000005653613603450174014001 0ustar ydeliaydelia/* * Copyright (c) 2012 Eric Radman * * 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 "entr.c" /* globals */ extern WatchFile **files; /* test context */ struct { struct { int count; char *file; char **argv; } exec; struct { struct kevent Set[32]; struct kevent List[32]; int nset; int nlist; } event; struct { int pid; int sig; int count; } signal; struct { int pid; int count; } wait; struct { int fd; const char *path; } open; struct { int count; } warn; struct { int count; } exit; } ctx; /* test runner */ int tests_run, failures; const char* func; int line; static void reset_state(); static void fail(); #define _() func=__func__; line=__LINE__; #define ok(test) do { _(); if (!(test)) { fail(); return 1; } } while(0) #define run(test) do { reset_state(); tests_run++; test(); } while(0) void fail() { failures++; fprintf(stderr, "test failure in %s() line %d\n", func, line); } void reset_state() { int i; int max_files = 32; /* getopt(3) keeps an external reference */ optind = 1; /* initialize global data */ aggressive_opt = 1; clear_opt = 0; dirwatch_opt = 0; noninteractive_opt = 0; postpone_opt = 0; restart_opt = 0; shell_opt = 0; leading_edge = 0; files = calloc(max_files, sizeof(WatchFile *)); for (i=0; ist_mode = S_IFDIR | S_IRUSR; else sb->st_mode = S_IFREG | S_IRUSR; return 0; } pid_t fake_waitpid(pid_t wpid, int *status, int options) { ctx.wait.pid = wpid; ctx.wait.count++; return wpid; } char * fake_realpath(const char *pathname, char *resolved) { snprintf(resolved, PATH_MAX, "/home/user/%s", pathname); return resolved; } int fake_list_dir(char *path) { return 2; } pid_t fake_fork() { return child_pid; /* pretend to be the child */ } void fake_free(void *ptr) { } int fake_tcsetattr(int fd, int action, const struct termios *tp) { return 0; } /* mock objects */ /* * kevent(2) is used to change and retrieve events. * This version always returns at most 2 events */ int fake_kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout) { int decrement; decrement = MIN(ctx.event.nlist, 2); /* record each event that the application sets */ if (nchanges > 0) { memcpy(&ctx.event.Set[ctx.event.nset], changelist, sizeof(struct kevent) * nchanges); ctx.event.nset += nchanges; return nchanges; } /* return list of events that each test sets up */ if ((nevents > 0) && (ctx.event.nlist > 0)) { memcpy(eventlist, &ctx.event.List, sizeof(struct kevent) * ctx.event.nlist); ctx.event.nlist -= decrement; return decrement; } /* no more events, use bogus return code to cause the main loop to exit */ return -2; } /* spies */ int fake_killpg(pid_t pid, int sig) { ctx.signal.pid = pid; ctx.signal.sig = sig; ctx.signal.count++; return 0; } int fake_execvp(const char *file, char *const argv[]) { ctx.exec.count++; ctx.exec.file = (char *)file; ctx.exec.argv = (char **)argv; return 0; } int fake_open(const char *path, int flags, ...) { ctx.open.path = path; ctx.open.fd++; return ctx.open.fd; } void fake_warnx(const char *msg, ...) { ctx.warn.count++; } void fake_errx(int eval, const char *msg, ...) { ctx.exit.count++; } /* tests */ /* * Read a list of user supplied files where input exceeds available watch * descriptors */ int process_input_01() { char input[] = "file1\nfile2\nfile3"; FILE *fake; int n_files; fake = fmemopen(input, strlen(input), "r"); n_files = process_input(fake, files, 3); ok(n_files == -1); ok(strcmp(files[0]->fn, "file1") == 0); ok(strcmp(files[1]->fn, "file2") == 0); ok(strcmp(files[2]->fn, "file3") == 0); return 0; } /* * Read a list of user supplied files and populate files array */ int process_input_02() { int n_files; FILE *fake; char input[] = "dir1\nfile1\nfile2\nfile3"; fake = fmemopen(input, strlen(input), "r"); n_files = process_input(fake, files, 16); ok(n_files == 3); ok(strcmp(files[0]->fn, "file1") == 0); ok(strcmp(files[1]->fn, "file2") == 0); ok(strcmp(files[2]->fn, "file3") == 0); return 0; } /* * Read a list of user supplied files and directories */ int process_input_03() { int n_files; FILE *fake; char input[] = "dir1\nfile1\nfile2\nfile3"; dirwatch_opt = 1; fake = fmemopen(input, strlen(input), "r"); n_files = process_input(fake, files, 32); ok(n_files == 5); ok(strcmp(files[0]->fn, "dir1") == 0); ok(strcmp(files[1]->fn, "file1") == 0); ok(strcmp(files[2]->fn, ".") == 0); ok(strcmp(files[3]->fn, "file2") == 0); ok(strcmp(files[4]->fn, "file3") == 0); ok(files[0]->is_dir == 1); /* dir1 */ ok(files[1]->is_dir == 0); /* file1 */ ok(files[2]->is_dir == 1); /* . */ ok(files[3]->is_dir == 0); /* file2 */ ok(files[4]->is_dir == 0); /* file3 */ return 0; } /* * Read a list of user supplied files where one of the files cannot be opened */ int process_input_04() { char input[] = "file1\nnosuch1\nfile2"; FILE *fake; int n_files; fake = fmemopen(input, strlen(input), "r"); n_files = process_input(fake, files, 4); ok(ctx.warn.count == 1); ok(n_files == 2); ok(strcmp(files[0]->fn, "file1") == 0); ok(strcmp(files[1]->fn, "file2") == 0); return 0; } /* * Remove a file */ int watch_fd_exec_01() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, "arg1", sizeof(files[0]->fn)); watch_file(kq, files[0]); /* event 1/1: 4 (-4) 0x21 0x1 0 0x84d5e800 */ ctx.event.nlist = 1; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_DELETE, 0, files[0]); watch_loop(kq, argv); ok(ctx.event.nset == 3); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]); ok(ctx.event.Set[1].ident); ok(ctx.event.Set[1].filter == EVFILT_VNODE); ok(ctx.event.Set[1].flags == EV_DELETE); /* remove */ ok(ctx.event.Set[1].fflags == (NOTE_ALL)); ok(ctx.event.Set[1].udata == files[0]); ok(ctx.event.Set[2].ident); ok(ctx.event.Set[2].filter == EVFILT_VNODE); ok(ctx.event.Set[2].flags == (EV_CLEAR|EV_ADD)); /* reopen */ ok(ctx.event.Set[2].fflags == (NOTE_ALL)); ok(ctx.event.Set[2].udata == files[0]); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "prog") == 0); ok(strcmp(ctx.exec.argv[0], "prog") == 0); ok(strcmp(ctx.exec.argv[1], "arg1") == 0); ok(strcmp(ctx.exec.argv[2], "arg2") == 0); ok(ctx.exit.count == 0); return 0; } /* * Change a file attribute */ int watch_fd_exec_02() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, "main.py", sizeof(files[0]->fn)); watch_file(kq, files[0]); ctx.event.nlist = 1; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_ATTRIB, 0, files[0]); watch_loop(kq, argv); ok(ctx.event.nset == 1); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]); ok(ctx.exec.count == 0); ok(ctx.exit.count == 0); return 0; } /* * Write to three files at once */ int watch_fd_exec_03() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; aggressive_opt = 0; postpone_opt = 1; strlcpy(files[0]->fn, "main.py", sizeof(files[0]->fn)); watch_file(kq, files[0]); strlcpy(files[1]->fn, "util.py", sizeof(files[1]->fn)); watch_file(kq, files[1]); strlcpy(files[2]->fn, "app.py", sizeof(files[2]->fn)); watch_file(kq, files[2]); ctx.event.nlist = 3; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[0]); EV_SET(&ctx.event.List[1], files[1]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[1]); EV_SET(&ctx.event.List[2], files[1]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[2]); watch_loop(kq, argv); ok(strcmp(leading_edge->fn, "main.py") == 0); ok(ctx.event.nset == 3); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].data == 0); ok(ctx.event.Set[0].udata == files[0]->fn); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "prog") == 0); ok(strcmp(ctx.exec.argv[0], "prog") == 0); ok(strcmp(ctx.exec.argv[1], "arg1") == 0); ok(strcmp(ctx.exec.argv[2], "arg2") == 0); ok(ctx.exit.count == 0); return 0; } /* * Write to a file and then remove it */ int watch_fd_exec_04() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, "arg1", sizeof(files[0]->fn)); watch_file(kq, files[0]); ctx.event.nlist = 2; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[0]); EV_SET(&ctx.event.List[1], files[0]->fd, EVFILT_VNODE, 0, NOTE_DELETE, 0, files[0]); watch_loop(kq, argv); ok(ctx.event.nset == 3); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]->fn); ok(ctx.event.Set[1].ident); ok(ctx.event.Set[1].filter == EVFILT_VNODE); ok(ctx.event.Set[1].flags == EV_DELETE); /* remove */ ok(ctx.event.Set[1].fflags == (NOTE_ALL)); ok(ctx.event.Set[1].udata == files[0]->fn); ok(ctx.event.Set[2].ident); ok(ctx.event.Set[2].filter == EVFILT_VNODE); ok(ctx.event.Set[2].flags == (EV_CLEAR|EV_ADD)); /* reopen */ ok(ctx.event.Set[2].fflags == (NOTE_ALL)); ok(ctx.event.Set[2].udata == files[0]->fn); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "prog") == 0); ok(strcmp(ctx.exec.argv[0], "prog") == 0); ok(strcmp(ctx.exec.argv[1], "arg1") == 0); ok(strcmp(ctx.exec.argv[2], "arg2") == 0); ok(ctx.exit.count == 0); return 0; } /* * Rename a file without removing it (e.g. Vim's backup option) */ int watch_fd_exec_05() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, "arg1", sizeof(files[0]->fn)); watch_file(kq, files[0]); ctx.event.nlist = 1; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_RENAME, 0, files[0]); watch_loop(kq, argv); ok(ctx.event.nset == 3); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]->fn); ok(ctx.event.Set[1].ident); ok(ctx.event.Set[1].filter == EVFILT_VNODE); ok(ctx.event.Set[1].flags == EV_DELETE); /* remove */ ok(ctx.event.Set[1].fflags == (NOTE_ALL)); ok(ctx.event.Set[1].udata == files[0]->fn); ok(ctx.event.Set[2].ident); ok(ctx.event.Set[2].filter == EVFILT_VNODE); ok(ctx.event.Set[2].flags == (EV_CLEAR|EV_ADD)); /* reopen */ ok(ctx.event.Set[2].fflags == (NOTE_ALL)); ok(ctx.event.Set[2].udata == files[0]->fn); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "prog") == 0); ok(strcmp(ctx.exec.argv[0], "prog") == 0); ok(strcmp(ctx.exec.argv[1], "arg1") == 0); ok(strcmp(ctx.exec.argv[2], "arg2") == 0); ok(ctx.exit.count == 0); return 0; } /* * Add a file to a directory and wait for child to exit */ int watch_fd_exec_06() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, ".", sizeof(files[0]->fn)); files[0]->is_dir = 1; files[0]->file_count = 1; strlcpy(files[1]->fn, "run.sh", sizeof(files[0]->fn)); watch_file(kq, files[0]); watch_file(kq, files[1]); child_pid = 222; dirwatch_opt = 1; restart_opt = 1; ctx.event.nlist = 1; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[0]); watch_loop(kq, argv); ok(ctx.event.nset == 2); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]->fn); ok(ctx.event.Set[1].ident); ok(ctx.event.Set[1].filter == EVFILT_VNODE); ok(ctx.event.Set[1].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[1].fflags == (NOTE_ALL)); ok(ctx.event.Set[1].udata == files[1]->fn); ok(ctx.exec.count == 0); ok(ctx.wait.count == 1); ok(ctx.wait.pid == 222); ok(ctx.exit.count == 1); return 0; } /* * Add a file to a directory and write to a file */ int watch_fd_exec_07() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, ".", sizeof(files[0]->fn)); files[0]->is_dir = 1; strlcpy(files[1]->fn, "run.sh", sizeof(files[0]->fn)); watch_file(kq, files[0]); watch_file(kq, files[1]); dirwatch_opt = 1; ctx.event.nlist = 2; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[0]); EV_SET(&ctx.event.List[1], files[1]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[1]); watch_loop(kq, argv); ok(ctx.event.nset == 2); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]->fn); ok(ctx.event.Set[1].ident); ok(ctx.event.Set[1].filter == EVFILT_VNODE); ok(ctx.event.Set[1].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[1].fflags == (NOTE_ALL)); ok(ctx.event.Set[1].udata == files[1]->fn); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "prog") == 0); ok(strcmp(ctx.exec.argv[0], "prog") == 0); ok(strcmp(ctx.exec.argv[1], "arg1") == 0); ok(strcmp(ctx.exec.argv[2], "arg2") == 0); ok(ctx.exit.count == 1); return 0; } /* * Write to a file in directory watch mode */ int watch_fd_exec_08() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; dirwatch_opt = 1; strlcpy(files[0]->fn, "src", sizeof(files[0]->fn)); files[0]->is_dir = 1; watch_file(kq, files[0]); strlcpy(files[1]->fn, "main.py", sizeof(files[1]->fn)); watch_file(kq, files[1]); ctx.event.nlist = 2; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[0]); EV_SET(&ctx.event.List[1], files[1]->fd, EVFILT_VNODE, 0, NOTE_WRITE, 0, files[1]); watch_loop(kq, argv); ok(ctx.event.nset == 2); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(ctx.exit.count == 1); ok(strcmp(leading_edge->fn, "main.py") == 0); return 0; } /* * Make a file executable */ int watch_fd_exec_09() { int kq = kqueue(); static char *argv[] = { "prog", "arg1", "arg2", NULL }; postpone_opt = 1; strlcpy(files[0]->fn, "main.py", sizeof(files[0]->fn)); watch_file(kq, files[0]); files[0]->mode = S_IFREG | S_IRUSR | S_IXUSR; ctx.event.nlist = 1; EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_ATTRIB, 0, files[0]); watch_loop(kq, argv); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "prog") == 0); ok(strcmp(ctx.exec.argv[0], "prog") == 0); ok(strcmp(ctx.exec.argv[1], "arg1") == 0); ok(strcmp(ctx.exec.argv[2], "arg2") == 0); ok(ctx.exit.count == 0); return 0; } /* * In submit mode only the first argment is used */ int watch_fd_shell_01() { int kq = kqueue(); char *argv[] = { "ruby main.rb", NULL }; shell_opt = 1; strlcpy(files[0]->fn, "main.rb", sizeof(files[0]->fn)); watch_file(kq, files[0]); ctx.event.nlist = 0; watch_loop(kq, argv); ok(strcmp(leading_edge->fn, "main.rb") == 0); ok(ctx.event.nset == 1); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "/bin/Xsh") == 0); /* FIXME */ ok(strcmp(ctx.exec.argv[0], "/bin/Xsh") == 0); ok(strcmp(ctx.exec.argv[1], "-c") == 0); ok(strcmp(ctx.exec.argv[2], "ruby main.rb") == 0); return 0; } /* * Parse command line arguments up to but not including the utility to execute */ int set_options_01() { int argv_offset; char *argv[] = { "entr", "ruby", "test1.rb", NULL }; argv_offset = set_options(argv); ok(argv_offset == 1); ok(restart_opt == 0); ok(clear_opt == 0); ok(dirwatch_opt == 0); return 0; } /* * Parse command line arguments for restart mode */ int set_options_02() { int argv_offset; char *argv[] = { "entr", "-r", "ruby", "test2.rb", NULL }; argv_offset = set_options(argv); ok(argv_offset == 2); ok(restart_opt == 1); ok(clear_opt == 0); ok(dirwatch_opt == 0); ok(postpone_opt == 0); return 0; } /* * Parse command line arguments with the clear option */ int set_options_03() { int argv_offset; char *argv[] = { "entr", "-c", "ruby", "test3.rb", NULL }; argv_offset = set_options(argv); ok(argv_offset == 2); ok(restart_opt == 0); ok(clear_opt == 1); ok(dirwatch_opt == 0); ok(postpone_opt == 0); return 0; } /* * Parse command line arguments with the directory watch option */ int set_options_04() { int argv_offset; char *argv[] = { "entr", "-d", "ruby", "test4.rb", NULL }; argv_offset = set_options(argv); ok(argv_offset == 2); ok(restart_opt == 0); ok(clear_opt == 0); ok(dirwatch_opt == 1); ok(postpone_opt == 0); return 0; } /* * Ensure that command line arguments are not confused with utility arguments */ int set_options_05() { int argv_offset; char *argv[] = { "entr", "ls", "-r", "-c", NULL }; argv_offset = set_options(argv); ok(argv_offset == 1); ok(restart_opt == 0); ok(clear_opt == 0); ok(shell_opt == 0); return 0; } /* * Run arguments in a shell */ int set_options_06() { int argv_offset; char *argv[] = { "entr", "-s", "make test", NULL }; argv_offset = set_options(argv); ok(argv_offset == 2); ok(restart_opt == 0); ok(clear_opt == 0); ok(shell_opt == 1); return 0; } /* * All command must be formatted as a single argument when run in a shell */ int set_options_07() { int argv_offset; char *argv[] = { "entr", "-s", "make", "test", NULL }; argv_offset = set_options(argv); ok(argv_offset == 2); ok(shell_opt == 1); ok(ctx.exit.count == 1); return 0; } /* * In restart mode the first action should be to start the server */ int watch_fd_restart_01() { int kq = kqueue(); char *argv[] = { "ruby", "main.rb", NULL }; restart_opt = 1; strlcpy(files[0]->fn, "main.rb", sizeof(files[0]->fn)); watch_file(kq, files[0]); ctx.event.nlist = 0; watch_loop(kq, argv); ok(strcmp(leading_edge->fn, "main.rb") == 0); ok(ctx.event.nset == 1); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "ruby") == 0); ok(strcmp(ctx.exec.argv[0], "ruby") == 0); ok(strcmp(ctx.exec.argv[1], "main.rb") == 0); return 0; } /* * Extending a file while in restart mode should result in start-kill-restart */ int watch_fd_restart_02() { int kq = kqueue(); char *argv[] = { "ruby", "main.rb", NULL }; restart_opt = 1; strlcpy(files[0]->fn, "main.rb", sizeof(files[0]->fn)); watch_file(kq, files[0]); child_pid = 222; ctx.event.nlist = 0; watch_loop(kq, argv); ok(ctx.event.nset == 1); ok(ctx.event.Set[0].ident); ok(ctx.event.Set[0].filter == EVFILT_VNODE); ok(ctx.event.Set[0].flags == (EV_CLEAR|EV_ADD)); /* open */ ok(ctx.event.Set[0].fflags == (NOTE_ALL)); ok(ctx.event.Set[0].udata == files[0]); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "ruby") == 0); ok(strcmp(ctx.exec.argv[0], "ruby") == 0); ok(strcmp(ctx.exec.argv[1], "main.rb") == 0); EV_SET(&ctx.event.List[0], files[0]->fd, EVFILT_VNODE, 0, NOTE_EXTEND, 0, files[0]); ctx.event.nlist = 0; watch_loop(kq, argv); ok(ctx.signal.count == 1); ok(ctx.signal.pid == 222); ok(ctx.signal.sig == 15); ok(ctx.exec.count == 2); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "ruby") == 0); ok(strcmp(ctx.exec.argv[0], "ruby") == 0); ok(strcmp(ctx.exec.argv[1], "main.rb") == 0); return 0; } /* * Substitue '/_' with the first file that leading_edge */ int run_utility_01() { static char *argv[] = { "psql", "-f", "/_", NULL }; char input[] = "one.sql\ntwo.sql"; FILE *fake; fake = fmemopen(input, strlen(input), "r"); process_input(fake, files, 3); leading_edge = files[1]; run_utility(argv); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "psql") == 0); ok(strcmp(ctx.exec.argv[0], "psql") == 0); ok(strcmp(ctx.exec.argv[1], "-f") == 0); ok(strcmp(ctx.exec.argv[2], "/home/user/two.sql") == 0); return 0; } /* * Substitue only the first occurance of '/_' */ int run_utility_02() { static char *argv[] = { "/_", "/_", NULL }; char input[] = "one.sh\ntwo.sh"; FILE *fake; fake = fmemopen(input, strlen(input), "r"); process_input(fake, files, 3); leading_edge = files[0]; run_utility(argv); ok(ctx.exec.count == 1); ok(ctx.exec.file != 0); ok(strcmp(ctx.exec.file, "/home/user/one.sh") == 0); ok(strcmp(ctx.exec.argv[0], "/home/user/one.sh") == 0); ok(strcmp(ctx.exec.argv[1], "/_") == 0); return 0; } /* * main */ int test_main(int argc, char *argv[]) { signal(SIGSEGV, sighandler); /* set up pointers to test doubles */ xstat = fake_stat; xkevent = fake_kevent; xkillpg = fake_killpg; xwaitpid = fake_waitpid; xexecvp = fake_execvp; xfork = fake_fork; xopen = fake_open; xrealpath = fake_realpath; xfree = fake_free; xerrx = fake_errx; xwarnx = fake_warnx; xlist_dir = fake_list_dir; xtcsetattr = fake_tcsetattr; /* all tests */ run(process_input_01); run(process_input_02); run(process_input_03); run(process_input_04); run(watch_fd_exec_01); run(watch_fd_exec_02); run(watch_fd_exec_03); run(watch_fd_exec_04); run(watch_fd_exec_05); run(watch_fd_exec_06); run(watch_fd_exec_07); run(watch_fd_exec_08); run(watch_fd_exec_09); run(watch_fd_shell_01); run(set_options_01); run(set_options_02); run(set_options_03); run(set_options_04); run(set_options_05); run(set_options_06); run(set_options_07); run(watch_fd_restart_01); run(watch_fd_restart_02); run(run_utility_01); run(run_utility_02); /* TODO: find out how we broke stdout */ fprintf(stderr, "%d of %d tests PASSED\n", tests_run-failures, tests_run); return failures; } int (*test_runner_main)(int argc, char **argv) = test_main; entr-4.4/entr.c0000644000175000017500000004025613603450174012757 0ustar ydeliaydelia/* * Copyright (c) 2012 Eric Radman * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "missing/compat.h" #include "data.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) /* function pointers */ int (*test_runner_main)(int, char**); int (*xstat)(const char *, struct stat *); int (*xkillpg)(pid_t, int); int (*xexecvp)(const char *, char *const []); pid_t (*xwaitpid)(pid_t, int *, int); pid_t (*xfork)(); int (*xkevent)(int, const struct kevent *, int, struct kevent *, int , const struct timespec *); int (*xopen)(const char *path, int flags, ...); char * (*xrealpath)(const char *, char *); void (*xfree)(void *); void (*xwarnx)(const char *, ...); void (*xerrx)(int, const char *, ...); int (*xlist_dir)(char *); int (*xtcsetattr)(int fd, int action, const struct termios *tp); /* globals */ extern int optind; extern WatchFile **files; WatchFile *leading_edge; int child_pid; int aggressive_opt; int clear_opt; int dirwatch_opt; int noninteractive_opt; int postpone_opt; int restart_opt; int shell_opt; struct termios canonical_tty; /* forwards */ static void usage(); static void terminate_utility(); static void handle_exit(int sig); 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[]) { struct rlimit rl; int kq; struct sigaction act; int ttyfd; short argv_index; int n_files; int i; struct kevent evSet; if ((*test_runner_main)) return(test_runner_main(argc, argv)); /* set up pointers to real functions */ xstat = stat; xkevent = kevent; xkillpg = killpg; xexecvp = execvp; xwaitpid = waitpid; xfork = fork; xopen = open; xrealpath = realpath; xfree = free; xwarnx = warnx; xerrx = errx; xlist_dir = list_dir; xtcsetattr = tcsetattr; if (pledge("stdio rpath tty proc exec", NULL) == -1) err(1, "pledge"); /* call usage() if no command is supplied */ if (argc < 2) usage(); argv_index = set_options(argv); /* normally a user will exit this utility by do_execting Ctrl-C */ act.sa_flags = 0; act.sa_flags = SA_RESETHAND; act.sa_handler = handle_exit; if (sigemptyset(&act.sa_mask) & (sigaction(SIGINT, &act, NULL) != 0)) err(1, "Failed to set SIGINT handler"); if (sigemptyset(&act.sa_mask) & (sigaction(SIGTERM, &act, NULL) != 0)) err(1, "Failed to set SIGTERM handler"); getrlimit(RLIMIT_NOFILE, &rl); #if defined(_LINUX_PORT) rl.rlim_cur = (rlim_t)fs_sysctl(INOTIFY_MAX_USER_WATCHES); #else /* raise soft limit */ rl.rlim_cur = min((rlim_t)sysconf(_SC_OPEN_MAX), rl.rlim_max); if (setrlimit(RLIMIT_NOFILE, &rl) != 0) err(1, "setrlimit cannot set rlim_cur to %d", (int)rl.rlim_cur); #endif /* prevent interactive utilities from paging output */ setenv("PAGER", "/bin/cat", 0); /* ensure a shell is available to use */ setenv("SHELL", "/bin/sh", 0); /* sequential scan may depend on a 0 at the end */ files = calloc(rl.rlim_cur+1, sizeof(WatchFile *)); if ((kq = kqueue()) == -1) err(1, "cannot create kqueue"); /* expect file list from a pipe */ if (isatty(fileno(stdin))) usage(); /* read input and populate watch list, skipping non-regular files */ n_files = process_input(stdin, files, rl.rlim_cur); 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 %d. Please consult" " http://eradman.com/entrproject/limits.html", (int)rl.rlim_cur); for (i=0; i STDIN_FILENO) { if (dup2(ttyfd, STDIN_FILENO) != 0) xwarnx("can't dup2 to stdin"); close(ttyfd); } /* remember terminal settings */ tcgetattr(STDIN_FILENO, &canonical_tty); /* Use keyboard input as a trigger */ EV_SET(&evSet, STDIN_FILENO, EVFILT_READ, EV_ADD, NOTE_LOWAT, 1, NULL); if (xkevent(kq, &evSet, 1, NULL, 0, NULL) == -1) xwarnx("failed to register stdin"); } watch_loop(kq, argv+argv_index); return 1; } /* Utility functions */ void usage() { fprintf(stderr, "release: %s\n", RELEASE); fprintf(stderr, "usage: entr [-acdnprs] utility [argument [/_] ...] < filenames\n"); exit(1); } void terminate_utility() { int status; if (child_pid > 0) { xkillpg(child_pid, SIGTERM); xwaitpid(child_pid, &status, 0); child_pid = 0; } } /* Callbacks */ void handle_exit(int sig) { if (!noninteractive_opt) xtcsetattr(0, TCSADRAIN, &canonical_tty); terminate_utility(); raise(sig); } /* * 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; int n_files = 0; struct stat sb; int i, matches; while (fgets(buf, sizeof(buf), file) != NULL) { buf[PATH_MAX-1] = '\0'; if ((p = strchr(buf, '\n')) != NULL) *p = '\0'; if (buf[0] == '\0') continue; if (xstat(buf, &sb) == -1) { xwarnx("unable to stat '%s'", buf); continue; } if (S_ISREG(sb.st_mode) != 0) { files[n_files] = malloc(sizeof(WatchFile)); strlcpy(files[n_files]->fn, buf, MEMBER_SIZE(WatchFile, fn)); files[n_files]->is_dir = 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 == 1) { if (S_ISDIR(sb.st_mode) != 0) path = &buf[0]; else if ((path = dirname(buf)) == 0) err(1, "dirname '%s' failed", buf); for (matches=0, i=0; ifn, path) == 0) matches++; if (matches == 0) { files[n_files] = malloc(sizeof(WatchFile)); strlcpy(files[n_files]->fn, path, MEMBER_SIZE(WatchFile, fn)); files[n_files]->is_dir = 1; files[n_files]->file_count = xlist_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 (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; /* read arguments until we reach a command */ for (argc=1; argv[argc] != 0 && argv[argc][0] == '-'; argc++); while ((ch = getopt(argc, argv, "acdnprs")) != -1) { switch (ch) { case 'a': aggressive_opt = 1; break; case 'c': clear_opt = 1; break; case 'd': dirwatch_opt = 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; default: usage(); } } if (argv[optind] == 0) usage(); if ((shell_opt == 1) && (argv[optind+1] != 0)) xerrx(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; if (restart_opt == 1) terminate_utility(); if (shell_opt == 1) { /* run argv[1] with a shell using the leading edge as $0 */ argc = 4; arg_buf = malloc(ARG_MAX); new_argv = calloc(argc+1, sizeof(char *)); xrealpath(leading_edge->fn, arg_buf); new_argv[0] = getenv("SHELL"); new_argv[1] = "-c"; new_argv[2] = argv[0]; new_argv[3] = arg_buf; } else { /* clone argv on each invocation to make the implementation of more * complex subsitution rules possible and easy */ for (argc=0; argv[argc]; argc++); arg_buf = malloc(ARG_MAX); new_argv = calloc(argc+1, sizeof(char *)); for (m=0, i=0, p=arg_buf; ifn, p)); m++; } else p += strlcpy(p, argv[i], ARG_MAX - (p - arg_buf)); p++; } } pid = xfork(); if (pid == -1) err(1, "can't fork"); if (pid == 0) { if (clear_opt == 1) system("/usr/bin/clear"); /* Set process group so subprocess can be signaled */ if (restart_opt == 1) { setpgid(0, getpid()); close(STDIN_FILENO); } /* wait up to 1 seconds for each file to become available */ for (i=0; i < 10; i++) { ret = xexecvp(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) { xwaitpid(pid, &status, 0); if (shell_opt == 1) fprintf(stdout, "%s returned exit code %d\n", basename(getenv("SHELL")), WEXITSTATUS(status)); } xfree(arg_buf); xfree(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; struct timespec delay = { 0, 100 * 1000000 }; /* wait up to 1 second for file to become available */ for (i=0; i < 10; i++) { #ifdef O_EVTONLY file->fd = xopen(file->fn, O_RDONLY|O_EVTONLY); #else file->fd = xopen(file->fn, O_RDONLY); #endif if (file->fd == -1) nanosleep(&delay, NULL); else break; } if (file->fd == -1) { warn("cannot open '%s'", file->fn); terminate_utility(); exit(1); } EV_SET(&evSet, file->fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_ALL, 0, file); if (xkevent(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 (xlist_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; char *trace_message; 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) xtcsetattr(0, TCSADRAIN, &character_tty); if ((reopen_only == 1) || (collate_only == 1)) { nev = xkevent(kq, NULL, 0, evList, 32, &evTimeout); } else { nev = xkevent(kq, NULL, 0, evList, 32, NULL); dir_modified = 0; } if (nev == -1) err(1, "kevent failed"); /* escape for test runner */ if ((nev == -2) && (collate_only == 0)) return; for (i=0; iis_dir == 1) dir_modified += compare_dir_contents(file); } if (!noninteractive_opt) xtcsetattr(0, TCSADRAIN, &canonical_tty); collate_only = 0; for (i=0; ifd, EVFILT_VNODE, EV_DELETE, NOTE_ALL, 0, file); if (xkevent(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; iis_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; trace_message = "mode changed"; } /* Possible on Linux when a running binary is unlinked */ if (file->ino != sb.st_ino) { do_exec = 1; file->ino = sb.st_ino; trace_message = "inode changed"; } } else if (evList[i].fflags & NOTE_ATTRIB) continue; if ((file->is_dir == 0) && (leading_edge_set == 0)) { leading_edge = file; leading_edge_set = 1; } if (getenv("EV_TRACE")) { fprintf(stderr, "EVFILT_VNODE: %d/%d: " "fflags: 0x%x %s\n", i, nev, evList[i].fflags, trace_message); } } 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(); xerrx(2, "directory altered"); } goto main; } entr-4.4/data.h0000644000175000017500000000177413603450174012727 0ustar ydeliaydelia/* * Copyright (c) 2012 Eric Radman * * 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 #include /* data */ typedef struct { char fn[PATH_MAX]; int fd; int is_dir; int file_count; mode_t mode; ino_t ino; } WatchFile; /* declare as extern in source */ WatchFile **files; entr-4.4/system_test.sh0000755000175000017500000002710413603450174014562 0ustar ydeliaydelia#!/bin/sh # # Copyright (c) 2012 Eric Radman # # 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. # test runner trap 'printf "$0: exit code $? on line $LINENO\nFAIL: $this\n"; exit 1' ERR \ 2> /dev/null || exec bash $0 "$@" typeset -i tests=0 function try { let tests+=1; this="$1"; } function assert { [[ "$1" == "$2" ]] && { printf "."; return; } printf "\nFAIL: $this\n'$1' != '$2'\n"; exit 1 } function skip { printf "s"; } function zz { sleep 0.25; } function setup { rm -f $tmp/*; touch $tmp/file{1,2}; zz; } tmp=$(cd $(mktemp -d ${TMPDIR:-/tmp}/entr-system-test-XXXXXX); pwd -P) tsession=$(basename $tmp) clear_tty='test -t 0 && stty echo icanon' clear_tmux='tmux kill-session -t $tsession 2>/dev/null || true' clear_tmp='rm -rf $tmp' trap "$clear_tty; $clear_tmux; $clear_tmp" EXIT # required utilities utils="git vim tmux" for util in $utils; do p=$(which $util 2> /dev/null) || { echo "ERROR: could not locate the '$util' utility" >&2 echo "System tests depend on the following: $utils" >&2 exit 1 } done # fast tests try "no arguments" ./entr 2> /dev/null || code=$? assert $code 1 try "no input" ./entr echo "vroom" 2> /dev/null || code=$? assert $code 1 try "reload and clear options with no utility to run" ./entr -r -c 2> /dev/null || code=$? assert $code 1 try "empty input" echo "" | ./entr echo 2> /dev/null || code=$? assert $code 1 try "no regular files provided as input" mkdir $tmp/dir1 ls $tmp | ./entr echo 2> /dev/null || code=$? rmdir $tmp/dir1 assert $code 1 # terminal tests unset TMUX try "spacebar triggers utility" setup tmux new-session -s $tsession -d echo "waiting" > $tmp/file1 echo "finished" > $tmp/file2 tmux send-keys -t $tsession:0 \ "ls $tmp/file2 | ./entr -p cp $tmp/file2 $tmp/file1" C-m ; zz assert "$(cat $tmp/file1)" "waiting" tmux send-keys -t $tsession:0 "xyz" C-m ; zz assert "$(cat $tmp/file1)" "waiting" tmux send-keys -t $tsession:0 " " ; zz assert "$(cat $tmp/file1)" "finished" tmux send-keys -t $tsession:0 "q" ; zz tmux kill-session -t $tsession # file system tests try "exec a command in non-intertive mode" setup ls $tmp/file* | ./entr -n tty >$tmp/exec.out & bgpid=$! ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "not a tty" try "exec a command as a background task and ensure stdin is closed" setup ls $tmp/file* | ./entr -r sh -c 'test -t 0; echo $?; kill $$' >$tmp/exec.out & bgpid=$! ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "1" try "exec single shell utility and exit when a file is added to an implicit watch path" setup ls $tmp/file* | ./entr -dp sh -c 'echo ping' >$tmp/exec.out 2>$tmp/exec.err \ || true & bgpid=$! ; zz touch $tmp/newfile wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "ping" assert "$(cat $tmp/exec.err)" "entr: directory altered" try "exec single shell utility and exit when a subdirectory is added" setup ls -d $tmp | ./entr -dp sh -c 'echo ping' >$tmp/exec.out 2>$tmp/exec.err \ || true & bgpid=$! ; zz mkdir $tmp/newdir wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "ping" assert "$(cat $tmp/exec.err)" "entr: directory altered" rmdir $tmp/newdir try "exec single shell utility and exit when a file is added to a specific path" setup ls -d $tmp | ./entr -dp sh -c 'echo ping' >$tmp/exec.out 2>$tmp/exec.err \ || true & bgpid=$! ; zz touch $tmp/newfile wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "ping" assert "$(cat $tmp/exec.err)" "entr: directory altered" try "do nothing when a file not monitored is changed in directory watch mode" setup ls $tmp/file2 | ./entr -dp echo "changed" >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz echo "123" > $tmp/file1 kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "" assert "$(cat $tmp/exec.err)" "" try "exec utility when a file is written by Vim in directory watch mode" setup ls $tmp/file* | ./entr -dp echo "changed" >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz vim -e -s -u NONE -N \ -c ":r!date" \ -c ":wq" $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "changed" assert "$(cat $tmp/exec.err)" "" try "exec utility when a file is opened for write and then closed" setup echo "---" > $tmp/file1 ls $tmp/file* | ./entr -p echo "changed" > $tmp/exec.out & bgpid=$! ; zz : > $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" if [ $(uname | egrep 'Darwin|FreeBSD|DragonFly') ]; then skip "NOTE_TRUNCATE not supported" else assert "$(cat $tmp/exec.out)" "changed" fi try "exec single utility when an entire stash of files is reverted" setup cp /usr/include/*.h $tmp/ cd $tmp git init -q git add *.h git commit -m "initial checkin" -q for f in `ls *.h`; do chmod 644 $f echo "" >> $f done cd - > /dev/null ; zz ls $tmp/*.h | ./entr -p echo "changed" > $tmp/exec.out & bgpid=$! ; zz cd $tmp git checkout *.h -q cd - > /dev/null ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "changed" try "exec utility when a file is written by Vim" setup ls $tmp/file* | ./entr -p echo "changed" > $tmp/exec.out & bgpid=$! ; zz vim -e -s -u NONE -N \ -c ":r!date" \ -c ":wq" $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "changed" try "exec shell utility when a file is written by Vim with 'backup'" setup ls $tmp/file* | ./entr -p echo "changed" > $tmp/exec.out & bgpid=$! ; zz vim -e -s -u NONE -N \ -c ":set backup" \ -c ":r!date" \ -c ":wq" $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "changed" try "exec shell utility when a file is written by Vim with 'nowritebackup'" setup ls $tmp/file* | ./entr -p echo "changed" > $tmp/exec.out & bgpid=$! ; zz vim -e -s -u NONE -N \ -c ":set nowritebackup" \ -c ":r!date" \ -c ":wq" $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "changed" try "restart a server when a file is modified" setup echo "started." > $tmp/file1 ls $tmp/file2 | ./entr -r tail -f $tmp/file1 2> /dev/null > $tmp/exec.out & bgpid=$! ; zz assert "$(cat $tmp/exec.out)" "started." echo 456 >> $tmp/file2 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "$(printf 'started.\nstarted.')" try "ensure that all shell subprocesses are terminated in restart mode" setup cat <<-SCRIPT > $tmp/go.sh #!/bin/sh trap 'echo "caught signal"; exit' TERM echo "running"; sleep 10 SCRIPT chmod +x $tmp/go.sh ls $tmp/file2 | ./entr -r sh -c "$tmp/go.sh" 2> /dev/null > $tmp/exec.out & bgpid=$! ; zz kill -INT $bgpid ; zz assert "$(cat $tmp/exec.out)" "$(printf 'running\ncaught signal')" try "exit with no action when restart and dirwatch flags are combined" setup echo "started." > $tmp/file1 ls $tmp/file* | ./entr -rd tail -f $tmp/file1 2> /dev/null > $tmp/exec.out & bgpid=$! ; zz assert "$(cat $tmp/exec.out)" "started." touch $tmp/newfile kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "$(printf 'started.')" try "exec single shell utility when two files change simultaneously" setup ln $tmp/file1 $tmp/file3 ls $tmp/file* | ./entr -p sh -c 'echo ping' > $tmp/exec.out & bgpid=$! ; zz echo 456 >> $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "ping" try "exec single shell utility on startup and when a file is changed" setup ls $tmp/file* | ./entr sh -c 'printf ping' > $tmp/exec.out & bgpid=$! ; zz echo 456 >> $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "pingping" try "exec a command if a file is made executable" setup ls $tmp/file* | ./entr -p echo /_ > $tmp/exec.out & bgpid=$! ; zz chmod +x $tmp/file2 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "$tmp/file2" try "ensure watches operate on a running executable" setup cp /bin/sleep $tmp/ ls $tmp/sleep | ./entr -rs "echo 'vroom'; $tmp/sleep 30" \ > $tmp/exec.out 2> /dev/null & bgpid=$! ; zz cp -f /bin/sleep $tmp/ ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" rm -f $tmp/sleep assert "$(cat $tmp/exec.out)" "$(printf 'vroom\nvroom\n')" try "exec a command using the first file to change" setup ls $tmp/file* | ./entr -p cat /_ > $tmp/exec.out & bgpid=$! ; zz echo 456 > $tmp/file1 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "456" try "exec single shell utility using utility substitution" setup ls $tmp/file1 $tmp/file2 | ./entr -p file /_ > $tmp/exec.out & bgpid=$! ; zz echo 456 >> $tmp/file2; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "$tmp/file2: ASCII text" try "watch and exec a program that is overwritten" setup touch $tmp/script; chmod 755 $tmp/script echo $tmp/script | ./entr -p $tmp/script $tmp/file1 > $tmp/exec.out & bgpid=$! ; zz cat > $tmp/script <<-EOF #!/bin/sh echo vroom EOF zz ; kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "vroom" try "exec an interactive utility when a file changes" setup ls $tmp/file* | ./entr -p sh -c 'tty | cut -c1-8' 2> /dev/null > $tmp/exec.out & bgpid=$! ; zz echo 456 >> $tmp/file2 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" if ! test -t 0 ; then skip "A TTY is not available" else assert "$(cat $tmp/exec.out | tr '/pts' '/tty')" "/dev/tty" fi try "exec a command using shell option" setup ls $tmp/file* | ./entr -ps 'file $0; exit 2' >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz echo 456 >> $tmp/file2 ; zz kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.err)" "" assert "$(head -n1 $tmp/exec.out)" "$(printf ${tmp}'/file2: ASCII text')" try "exec a command as a background task" setup (ls $tmp/file* | ./entr -ps 'echo terminating; kill $$' >$tmp/exec.out 2>$tmp/exec.err &) zz echo 456 >> $tmp/file2 ; zz assert "$(cat $tmp/exec.err)" "" assert "$(head -n1 $tmp/exec.out)" "terminating" # extra slow tests that rely on timeouts try "respond to events that occur while the utility is running" setup ls $tmp/file* | ./entr -a sh -c 'echo "vroom"; sleep 0.5' > $tmp/exec.out & bgpid=$! ; zz echo "123" > $tmp/file1 sleep 1 kill -INT $bgpid wait $bgpid || assert "$?" "130" assert "$(cat $tmp/exec.out)" "$(printf 'vroom\nvroom\n')" try "ensure that all subprocesses are terminated in restart mode when a file is removed" setup cat <<-SCRIPT > $tmp/go.sh #!/bin/sh trap 'echo "caught signal"; exit' TERM echo "running"; sleep 10 SCRIPT chmod +x $tmp/go.sh ls $tmp/file2 | ./entr -r sh -c "$tmp/go.sh" 2> /dev/null > $tmp/exec.out & bgpid=$! ; zz rm $tmp/file2; sleep 2 pgrep -P $bgpid > /dev/null || assert "$?" "1" assert "$(cat $tmp/exec.out)" "$(printf 'running\ncaught signal')" this="exit 0" echo; echo "$tests tests PASSED" entr-4.4/LICENSE0000644000175000017500000000650613603450174012650 0ustar ydeliaydelia1) Project Source Source code for `entr` is licensed under an ISC-style license, to the following copyright holders: Eric Radman * 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. 2) Compatibility Libraries (MacOS and Linux only) Some code under the /missing subdirectory is licensed under a 2-term BSD license, to the following copyright holders: Jonathan Lemon * 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. Some code under the /missing subdirectory is licensed under an ISC-style license, to the following copyright holders: Todd C. Miller Martin Pieuchot Ted Unangst * 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 TODD C. MILLER DISCLAIMS ALL * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL TODD C. MILLER 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. entr-4.4/.gitignore0000644000175000017500000000003013603450174013615 0ustar ydeliaydeliaMakefile entr entr_spec entr-4.4/entr.10000644000175000017500000001064013603450174012667 0ustar ydeliaydelia.\" .\" Copyright (c) 2012 Eric Radman .\" .\" 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. .\" .Dd November 27, 2019 .Dt ENTR 1 .Os .Sh NAME .Nm entr .Nd run arbitrary commands when files change .Sh SYNOPSIS .Nm .Op Fl acdprs .Ar utility .Op Ar argument /_ ... .Sh DESCRIPTION A list of files provided on standard input, and the .Ar utility is executed using the supplied arguments if any of them change. .Nm waits for the child process to finish before responding to subsequent file system events. A TTY is also opened before entering the watch loop in order to support interactive utilities. .Pp The arguments are as follows: .Bl -tag -width Ds .It Fl a Respond to all events which occur while the .Ar utility is running. Without this option, .Nm consolidates events in order to avoid looping. This option has no effect in conjunction with the .Fl r flag. .It Fl c Execute .Pa /usr/bin/clear before invoking the .Ar utility specified on the command line. .It Fl d Track the directories of regular files provided as input and exit if a new file is added. This option also enables directories to be specified explicitly. Files with names beginning with .Ql \&. are ignored. .It Fl n Run in non-interactive mode. In this mode .Nm entr does not attempt to read from the TTY or change its properties. .It Fl p Postpone the first execution of the .Ar utility until a file is modified. .It Fl r Reload a persistent child process. As with the standard mode of operation, a .Ar utility which terminates is not executed again until a file system or keyboard event is processed. .Dv SIGTERM is used to terminate the .Ar utility before it is restarted. A process group is created to prevent shell scripts from masking signals. .Nm waits for the .Ar utility to exit to ensure that resources such as sockets have been closed. Control of the TTY is not transferred to the child process. .It Fl s Evaluate the first argument using the interpreter specified by the .Ev SHELL environment variable. If standard output is a TTY, the name of the shell and exit code is printed after each invocation. .El .Pp The first argument named .Ar /_ is replaced with the absolute path of the first file to trigger an event. The first file under watch is used as the default. If the .Fl s option is used, the name of the first file to trigger an event can be read from .Va $0 . .Sh COMMANDS .Nm listens for keyboard input and responds to the following commands: .Bl -tag -width .It Aq Cm space Execute the utility immediately. If the .Fl Cm r option is set this will terminate and restart the child process as if a file change event had occurred. .It Cm q Quit; equivalent pressing .Aq Cm control-C . .El .Sh ENVIRONMENT If .Ev PAGER is undefined, .Nm will assign .Pa /bin/cat to prevent interactive utilities from waiting for keyboard input if output does not fit on the screen. .Pp If .Ev SHELL is undefined, .Nm entr will use .Pa /bin/sh . .Sh EXIT STATUS The .Nm utility exits with one of the following values: .Pp .Bl -tag -width Ds -offset indent -compact .It 0 .Dv SIGINT or .Dv SIGTERM was received .It 1 No regular files were provided as input or an error occurred .It 2 A file was added to a directory and the directory watch option was specified .El .Sh EXAMPLES Rebuild a project if source files change, limiting output to the first 20 lines: .Pp .Dl $ find src/ | entr -s 'make | sed 20q' .Pp Launch and auto-reload a node.js server: .Pp .Dl $ ls *.js | entr -r node app.js .Pp Clear the screen and run a query after the SQL script is updated: .Pp .Dl $ echo my.sql | entr -cp psql -f /_ .Pp Rebuild project if a source file is modified or added to the src/ directory: .Pp .Dl $ while true; do ls src/*.rb | entr -d make; done .Pp Self-terminate after a file is updated .Pp .Dl $ ls * | entr -p 'kill $PPID'