pax_global_header00006660000000000000000000000064147512074700014521gustar00rootroot0000000000000052 comment=bf7ab3ad39f29576c947f2b976c93b3da041438d entr-5.7/000077500000000000000000000000001475120747000123445ustar00rootroot00000000000000entr-5.7/.clang-format000066400000000000000000000006171475120747000147230ustar00rootroot00000000000000--- UseTab: ForIndentation Language: Cpp TabWidth: 4 ColumnLimit: 100 IndentWidth: 4 BasedOnStyle: LLVM AlignOperands: false IncludeBlocks: Preserve SpaceBeforeParens: ControlStatements Cpp11BracedListStyle: false SpaceAfterCStyleCast: true AlignAfterOpenBracket: DontAlign BreakBeforeBinaryOperators: NonAssignment SpacesBeforeTrailingComments: 2 AlwaysBreakAfterDefinitionReturnType: TopLevel ... entr-5.7/.gitignore000066400000000000000000000000331475120747000143300ustar00rootroot00000000000000*.o Makefile compat.c entr entr-5.7/LICENSE000066400000000000000000000065051475120747000133570ustar00rootroot000000000000001) 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-5.7/Makefile.bsd000066400000000000000000000015461475120747000145610ustar00rootroot00000000000000CLANG_FORMAT ?= clang-format PREFIX ?= /usr/local MANPREFIX ?= ${PREFIX}/man RELEASE = 5.7 COMPONENTS = compat.o status.o entr.o all: entr compat.c: missing/* cat /dev/null ${EXTRA_SRC} > compat.c .c.o: ${CC} ${CFLAGS} ${CPPFLAGS} -DRELEASE=\"${RELEASE}\" -c $< entr: ${COMPONENTS} ${CC} ${CFLAGS} ${CPPFLAGS} -o $@ ${COMPONENTS} ${LDFLAGS} test: entr ls entr.1 | EV_TRACE=1 ./entr -zn wc -l entr.1 check: entr ./system_test.sh clean: rm -f *.o compat.c entr distclean: clean rm -f Makefile install: entr @mkdir -p ${DESTDIR}${PREFIX}/bin @mkdir -p ${DESTDIR}${MANPREFIX}/man1 install entr ${DESTDIR}${PREFIX}/bin install -m 644 entr.1 ${DESTDIR}${MANPREFIX}/man1 uninstall: rm ${DESTDIR}${PREFIX}/bin/entr rm ${DESTDIR}${MANPREFIX}/man1/entr.1 format: ${CLANG_FORMAT} -i *.c *.h .PHONY: all test check clean format distclean install uninstall entr-5.7/Makefile.freebsd000066400000000000000000000000671475120747000154200ustar00rootroot00000000000000MANPREFIX ?= ${PREFIX}/share/man include Makefile.bsd entr-5.7/Makefile.linux000066400000000000000000000002161475120747000151410ustar00rootroot00000000000000CPPFLAGS += -D_GNU_SOURCE -D_LINUX_PORT -Imissing MANPREFIX ?= ${PREFIX}/share/man EXTRA_SRC = missing/kqueue_inotify.c include Makefile.bsd entr-5.7/Makefile.linux-compat000066400000000000000000000002401475120747000164170ustar00rootroot00000000000000CPPFLAGS += -D_GNU_SOURCE -D_LINUX_PORT -Imissing MANPREFIX ?= ${PREFIX}/share/man EXTRA_SRC = missing/strlcpy.c missing/kqueue_inotify.c include Makefile.bsd entr-5.7/Makefile.macos000066400000000000000000000001351475120747000151040ustar00rootroot00000000000000CPPFLAGS += -D_MACOS_PORT MANPREFIX ?= ${PREFIX}/share/man EXTRA_SRC = include Makefile.bsd entr-5.7/NEWS000066400000000000000000000307751475120747000130570ustar00rootroot00000000000000= Release History == 5.7: February 6, 2025 - Update MANPREFIX for FreeBSD - Format source using clang-format - Reorganize system tests to run without a TTY - Remove unused X option character == 5.6: July 1, 2024 - Process exit status messages using background awk scripts - Use min(OPEN_MAX, rl.rlim_max) to set rlim_cur on MacOS - Detect and use native strlcpy(3) - Always build with GNU_SOURCE set on Linux == 5.5: November 17, 2023 - Report correct error if open(3) fails - Use kern.maxfilesperproc to determine file open limit on MacOS == 5.4: August 1, 2023 - 'make test' runs a quick smoketest, 'make check' runs regressions - Set IN_CLOEXEC only for inotify_init, kqueue uses similar setting by default - Unconditionally try to set soft file limit to 2^16 on MacOS - Use non-reentrant calls sparingly in signal handlers - configure: use TARGET_OS to override the output of uname(1) == 5.3: January 20, 2023 - Symlink changes detected on Linux by setting 'ENTR_INOTIFY_SYMLINK' - Use /dev/null rather then closed pipe for stdin in -r mode - Utilize {O,FD}_CLOEXEC flag for unintentional leaks of descriptors to executed utilities - Remove C unit tests - Only respond to attribute/inode changes on Linux == 5.2: May 05, 2022 - Update copy of strlcpy(3) for Linux - Detect file deletion from directories on Linux - Print the signal that terminated a child when using '-s' - Return 128+signal that terminated a child when using '-z' - Ensure terminal settings are reset when '-z' is set == 5.1: December 28, 2021 - Detect files moved to or from directories on Linux - Allow detection of directory entries beginning with '.' by specifying '-d' twice - Only reset terminal settings in exit handler if settings were changed == 5.0: July 2, 2021 - Add missing call to getrlimit(3) on MacOS - Eliminate memory management warnings on Linux - EV_TRACE prints file mode and file name - Ignore asynchronous attribute changes on MacOS 11 == 4.9: May 3, 2021 - EV_TRACE also prints file/notify descriptor limit - Don't raise rlim_cur on MacOS - Set 2^16 watches if inotify limits cannot be read - Raise an error and suggest '-n' if terminal attributes cannot be read == 4.8: February 26, 2021 - Set a maximum of 2^19 watches to guard against absurd file open limits on MacOS - Use control sequences to clear the display and specify '-c' twice to erase the scrollback buffer == 4.7: January 29, 2021 - Use system file descriptor limits when max_user_watches is not accessible - Return the exit status of the child process when the '-z' option is used - Handle SIGHUP so child process are terminated when a terminal is closed - More accurately return shell exit code using '-s' option == 4.6: July 1, 2020 - Always call waitpid(2) to avoid dead processes - Duplicate STDIN file descriptor before closing; for the '-r' option == 4.5: April 20, 2020 - New '-z' "one-shot" option self-terminates after the utility exits - Termination by 'q' or 'SIGINT' results in an exit status of 0 == 4.4: January 02, 2020 - 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 - 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 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 - Renamed regress.sh to system_test.sh, and drop dependency on ksh == 2.7: February 12, 2014 - Changed function pointer prefixes from '_' to 'x' to avoid runtime symbol collisions in libc. Required to build on DragonFlyBSD - Add this NEWS file to provide detailed release history for port maintainers - Substitute '/_' 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') - 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 losing 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 - 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-5.7/README.md000066400000000000000000000030431475120747000136230ustar00rootroot00000000000000Event Notify Test Runner ======================== A utility for running arbitrary commands when files change. Uses kqueue(2) or inotify(7) to avoid polling. `entr` was written to facilitate rapid feedback on the command line. Source Installation - BSD, Mac OS, and Linux -------------------------------------------- ./configure make test make install To see available build options run `./configure -h` Docker and WSL -------------- Incomplete inotify support on _Windows Subsystem for Linux_ and _Docker for Mac_ may cause `entr` to respond incorrectly. Setting the environment variable `ENTR_INOTIFY_WORKAROUND` enables `entr` to operate in these environments. Linux Features -------------- Symlinks can be monitored for changes by setting the environment variable `ENTR_INOTIFY_SYMLINK`. Man Page Examples ----------------- Rebuild a project if source files change, limiting output to the first 20 lines: $ find src/ | entr -s 'make | head -n 20' Launch and auto-reload a node.js server: $ ls *.js | entr -r node app.js Clear the screen and run a query after the SQL script is updated: $ echo my.sql | entr -cp psql -f /_ Rebuild project if a source file is modified or added to the src/ directory: $ while sleep 0.1; do ls src/*.rb | entr -d make; done Auto-reload a web server, or terminate if the server exits $ ls * | entr -rz ./httpd News ---- Notification of new releases are provided by an [Atom feed](https://github.com/eradman/entr/releases.atom), and release history is covered in the [NEWS](NEWS) file. entr-5.7/configure000077500000000000000000000014611475120747000142550ustar00rootroot00000000000000#!/bin/sh test_libc_features() { CFLAGS="-D_GNU_SOURCE" : ${CC:=cc} 2>/dev/null $CC -xc $CFLAGS -o /dev/null - <<-EOF #include int main(void) { char dst[4]; strlcpy(dst, "1234", sizeof dst); return 0; } EOF } copy_mk() { cmd="cp Makefile.$1 Makefile" echo "+ $cmd"; $cmd } usage() { cat <<-HELP Usage: configure [-h] Example: build a static binary and install to your home directory ./configure CFLAGS="-static" make PREFIX=\$HOME/local make install HELP exit 1 } while [ $# -gt 0 ]; do case $1 in -h) usage ;; *) echo "configure: unused argument: $1" ;; esac shift done case "${TARGET_OS:-`uname`}" in Darwin) copy_mk macos ;; FreeBSD) copy_mk freebsd ;; Linux) test_libc_features && copy_mk linux || copy_mk linux-compat ;; *) copy_mk bsd ;; esac entr-5.7/data.h000066400000000000000000000017711475120747000134340ustar00rootroot00000000000000/* * 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; /* defined in entr.c */ extern WatchFile **files; entr-5.7/entr.1000066400000000000000000000126251475120747000134040ustar00rootroot00000000000000.\" .\" 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 February 6, 2025 .Dt ENTR 1 .Os .Sh NAME .Nm entr .Nd run arbitrary commands when files change .Sh SYNOPSIS .Nm .Op Fl acdnprsxz .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 Clear the screen before invoking the .Ar utility specified on the command line. Specify twice to erase the scrollback buffer. .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. If specified twice, all new entries to a directory are recognized, otherwise 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. .It Fl x Format custom exit status messages using a persistent .Xr awk 1 process. The path to the status script is defined by the environment variable .Ev ENTR_STATUS_SCRIPT . If the status script does not exist, .Nm will create an example. Shell commands and file redirection is not permitted by default, but may be enabled by specifying .Fl x twice. .It Fl z Exit after the .Ar utility completes. When combined with .Fl r the .Ar utility will be restarted again only in response to commands or file system events. .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 .Bl -tag -width "ENTR_ENVIRON" .It Ev ENTR_STATUS_SCRIPT The path to the exit status script enabled by the .Fl x option. By default .Pa $HOME/.entr/status.awk is evaluated. .It Ev PAGER Set to .Pa /bin/cat by default to prevent interactive utilities from waiting for keyboard input if output does not fit on the screen. .It Ev SHELL Specify the shell to use with the .Fl s flag. The default is .Pa /bin/sh . .It Ev EV_TRACE Print file system event messages. .El .Sh EXIT STATUS If the .Fl z flag is set and the .Ar utility is successfully executed, the status of the child process is returned. If the child process was terminated by a signal, the exit status is the signal number plus 128. .Pp .Nm normally returns one of the following values: .Pp .Bl -tag -width Ds -offset indent -compact .It 0 Normal termination after receiving .Dv SIGINT .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 | head -n 20' .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 sleep 0.1; do ls src/*.rb | entr -d make; done .Pp Auto-reload a web server, or terminate if the server exits .Pp .Dl $ ls * | entr -rz ./httpd entr-5.7/entr.c000066400000000000000000000446521475120747000134730ustar00rootroot00000000000000/* * 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" #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 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; /* forwards */ static void usage(); static void terminate_utility(); 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[]) { struct rlimit rl; 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(); 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"); /* 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"); #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) if (getrlimit(RLIMIT_NOFILE, &rl) == -1) err(1, "getrlimit"); open_max = min(OPEN_MAX, rl.rlim_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 */ 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 */ setenv("SHELL", "/bin/sh", 0); shell = getenv("SHELL"); shell_base = strdup(shell); if (shell_base == NULL) err(1, "cannot duplicate string"); 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 ((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, 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() { fprintf(stderr, "release: %s\n", RELEASE); fprintf(stderr, "usage: entr [-acdnprsxz] utility [argument [/_] ...] < filenames\n"); exit(1); } void terminate_utility() { int status; terminating = 1; if (child_pid > 0) { killpg(child_pid, SIGTERM); waitpid(child_pid, &status, 0); child_pid = 0; } terminating = 0; } /* 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; 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; if (stat(buf, &sb) == -1) { warnx("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 > 0) { 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; i < n_files; i++) if (strcmp(files[i]->fn, 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 = 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; /* 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(); } } if (argv[optind] == 0) usage(); 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; 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 *)); realpath(leading_edge->fn, arg_buf); new_argv[0] = 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 substitution 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; i < argc; i++) { new_argv[i] = p; if ((m < 1) && (strcmp(argv[i], "/_")) == 0) { p += strlen(realpath(leading_edge->fn, p)); m++; } else p += strlcpy(p, argv[i], ARG_MAX - (p - arg_buf)); 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 (;;) { #ifdef O_EVTONLY file->fd = open(file->fn, O_RDONLY | O_CLOEXEC | O_EVTONLY); #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"); /* escape for test runner */ if ((nev == -2) && (collate_only == 0)) return; 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 && stat(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; } entr-5.7/missing/000077500000000000000000000000001475120747000140155ustar00rootroot00000000000000entr-5.7/missing/compat.h000066400000000000000000000005301475120747000154470ustar00rootroot00000000000000/* compat.h */ #if !defined(NOTE_TRUNCATE) #define NOTE_TRUNCATE 0 #endif #if defined(_LINUX_PORT) #define INOTIFY_MAX_USER_WATCHES 2 size_t strlcpy(char *dst, const char *src, size_t dsize); int fs_sysctl(const int name); #endif #if !defined(ARG_MAX) #define ARG_MAX (256 * 1024) #endif #ifndef __OpenBSD__ #define pledge(s, p) (0) #endif entr-5.7/missing/kqueue_inotify.c000066400000000000000000000140761475120747000172310ustar00rootroot00000000000000/* * Copyright (c) 2013 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 "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) { /* failed to read max_user_watches; sometimes inaccessible on Android */ value = 0; } else value = atoi(line); if (file) 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_MOVE|IN_ATTRIB|IN_CREATE|IN_DELETE /* * inotify and kqueue ids both have the type `int` */ int kqueue(void) { static int inotify_queue; if (inotify_queue == 0) inotify_queue = inotify_init1(IN_CLOEXEC); if (getenv("ENTR_INOTIFY_WORKAROUND")) warnx("broken inotify workaround enabled"); else if (getenv("ENTR_INOTIFY_SYMLINK")) warnx("monitoring symlinks"); 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 nfds; int ignored = 0; struct pollfd pfd[2]; pfd[0].fd = kq; pfd[0].events = POLLIN; pfd[1].fd = STDIN_FILENO; pfd[1].events = POLLIN; if (nchanges > 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 if (getenv("ENTR_INOTIFY_SYMLINK")) wd = inotify_add_watch(kq, file->fn, IN_ALL|IN_DONT_FOLLOW); 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_DELETE) fflags |= NOTE_WRITE; if (iev->mask & IN_MOVE_SELF) fflags |= NOTE_RENAME; if (iev->mask & IN_MOVED_TO) fflags |= NOTE_RENAME; if (iev->mask & IN_MOVED_FROM) 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)); return n; } entr-5.7/missing/strlcpy.c000066400000000000000000000030711475120747000156620ustar00rootroot00000000000000/* $OpenBSD: strlcpy.c,v 1.16 2019/01/25 00:19:25 millert Exp $ */ /* * Copyright (c) 1998, 2015 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 string src to buffer dst of size dsize. At most dsize-1 * chars will be copied. Always NUL terminates (unless dsize == 0). * Returns strlen(src); if retval >= dsize, truncation occurred. */ size_t strlcpy(char *dst, const char *src, size_t dsize) { const char *osrc = src; size_t nleft = dsize; /* Copy as many bytes as will fit. */ if (nleft != 0) { while (--nleft != 0) { if ((*dst++ = *src++) == '\0') break; } } /* Not enough room in dst, add NUL and traverse rest of src. */ if (nleft == 0) { if (dsize != 0) *dst = '\0'; /* NUL-terminate dst */ while (*src++) ; } return(src - osrc - 1); /* count does not include NUL */ } entr-5.7/missing/sys/000077500000000000000000000000001475120747000146335ustar00rootroot00000000000000entr-5.7/missing/sys/event.h000066400000000000000000000060251475120747000161300ustar00rootroot00000000000000/* * 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-5.7/status.c000066400000000000000000000060331475120747000140350ustar00rootroot00000000000000/* * Copyright (c) 2024 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 "missing/compat.h" #include "status.h" /* globals */ int status_stdin_pipe[2]; void start_log_filter(int safe) { char *argv[8]; char *awk_script; struct passwd *pw; awk_script = getenv("ENTR_STATUS_SCRIPT"); if ((!awk_script) || (strlen(awk_script) == 0)) { pw = getpwuid(getuid()); asprintf(&awk_script, "%s/.entr/status.awk", pw->pw_dir); } create_dir(xdirname(awk_script)); install_file(awk_script, "# http://eradman.com/entrproject/status-filters.html\n" "/^signal/ { print $3, \"terminated by signal\", $2; }\n" "/^exit/ { print $3, \"returned exit code\", $2; }\n"); argv[0] = "/usr/bin/awk"; argv[1] = "-F"; argv[2] = "|"; argv[3] = "-f"; argv[4] = awk_script; #if defined(_LINUX_PORT) argv[5] = "-S"; #else argv[5] = "-safe"; #endif argv[6] = NULL; if (safe == 2) argv[5] = NULL; pipe(status_stdin_pipe); status_pid = fork(); if (status_pid == -1) err(1, "fork"); if (status_pid == 0) { close(status_stdin_pipe[1]); dup2(status_stdin_pipe[0], STDIN_FILENO); execvp("awk", argv); err(1, "could not exec %s", argv[0]); } close(status_stdin_pipe[0]); } void write_log_filter(char *input, size_t len) { if (write(status_stdin_pipe[1], input, len) == -1) err(1, "write to child"); } void end_log_filter() { close(status_stdin_pipe[1]); kill(status_pid, SIGKILL); } /* * xdirname - mimic dirname(3) on OpenBSD which does not modify input * create_dir - ensure a directory exists * install_file - create file is it does not exist */ char * xdirname(const char *path) { static char dname[PATH_MAX]; strlcpy(dname, path, sizeof(dname)); return dirname(dname); } void create_dir(const char *dir) { struct stat dst_sb; if (stat(dir, &dst_sb) == -1) mkdir(dir, 0750); } void install_file(const char *dst, const char *content) { int fd; struct stat dst_sb; if (stat(dst, &dst_sb) == -1) { printf("entr: created '%s'\n", dst); fd = open(dst, O_WRONLY | O_CREAT, 0640); if (fd == -1) err(1, "open"); if (write(fd, content, strlen(content)) == -1) err(1, "write"); close(fd); } } entr-5.7/status.h000066400000000000000000000020321475120747000140350ustar00rootroot00000000000000/* * Copyright (c) 2024 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. */ void start_log_filter(int safe); void write_log_filter(char *input, size_t len); void end_log_filter(); char *xdirname(const char *path); void create_dir(const char *dir); void install_file(const char *dst, const char *content); extern pid_t status_pid; entr-5.7/system_test.sh000077500000000000000000000400701475120747000152670ustar00rootroot00000000000000#!/bin/sh -u trap '' ERR 2> /dev/null || exec bash $0 "$@" # test runner function try { let tests+=1; this="$1"; } function assert { [[ "$1" == "$2" ]] && { printf "."; return; } printf "\nFAIL: $this\n'$1' != '$2'\n" echo "*************************************" echo "System test directory contents:" : | head -n 50 $(find $tmp -type f ! -perm -111) echo "*************************************" exit 1 } function skip { printf "s"; } function zz { sleep 0.25; } function setup { rm -f $tmp/* touch $tmp/file1 $tmp/file2 unset ENTR_STATUS_SCRIPT } tmp=$(cd $(mktemp -d ${TMPDIR:-/tmp}/entr-system-test-XXXXXX); pwd -P) tsession=$(basename $tmp) let tests=0 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 trap 'printf "\nTerminated by SIGINT at line $LINENO\n"; exit 1' INT # required utilities utils="file pgrep git vim tmux" for util in $utils; do p=$(command -pv $util) || { echo "ERROR: could not locate the '$util' utility" >&2 echo "System tests depend on the following: $utils" >&2 exit 1 } done if [ $(uname) == 'Linux' ]; then date | awk -S '{}' 2> /dev/null || { echo "ERROR: gawk required on Linux" exit 1 } fi # local binary, non-interactive by default alias entr='./entr -n' alias entr_tty='./entr' # alias expansion is not enabled by default in non-interactive Bourne Again Shell (bash) sessions command -v shopt > /dev/null && shopt -s expand_aliases # 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 # status message tests try "install default status script" setup export ENTR_STATUS_SCRIPT="$tmp/status.awk" ls $tmp/* | entr -zx true >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz wait $bgpid assert "$(cat $tmp/exec.err)" "" assert "$(cat $tmp/exec.out)" "$(printf "entr: created '$tmp/status.awk'\ntrue returned exit code 0\n")" try "status script not compatible with restart option" setup ls $tmp/* | entr -zrx true >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz wait $bgpid; assert "$?" "1" try "block unsafe status script" setup export ENTR_STATUS_SCRIPT="$tmp/status.awk" cat > $ENTR_STATUS_SCRIPT <<-EOF { system("date") } EOF ls $tmp/* | entr -zx true >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz sed -i -e "s,.*awk: .*,awk: system is unsafe ... status.awk," $tmp/exec.err assert "$(head -n1 $tmp/exec.err)" "awk: system is unsafe ... status.awk" assert "$(head -n1 $tmp/exec.out)" "" try "allow unsafe status script" setup export ENTR_STATUS_SCRIPT="$tmp/status.awk" cat > $ENTR_STATUS_SCRIPT <<-EOF { system("date") } EOF ls $tmp/* | entr -zxx true >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz assert "$(head -n1 $tmp/exec.err)" "" try "use custom status script" setup export ENTR_STATUS_SCRIPT="$tmp/status.awk" cat > $ENTR_STATUS_SCRIPT <<-'EOF' { print "=", $1, $2, "=" } EOF ls $tmp/* | entr -zx false >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz assert "$(cat $tmp/exec.err)" "" assert "$(cat $tmp/exec.out)" "$(printf '= exit 1 =')" try "use custom status script with shell option and kill" setup export ENTR_STATUS_SCRIPT="$tmp/status.awk" cat > $ENTR_STATUS_SCRIPT <<-'EOF' { print "=", $1, $2, "=" } EOF ls $tmp/* | entr -zx -s 'kill -9 $$' >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz assert "$(cat $tmp/exec.err)" "" assert "$(cat $tmp/exec.out)" "$(printf '= signal 9 =')" try "abort if status script terminates" setup export ENTR_STATUS_SCRIPT="$tmp/status.awk" cat > $ENTR_STATUS_SCRIPT <<-'EOF' { exit; } EOF ls $tmp/* | entr -x true >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz assert "$(cat $tmp/exec.err)" "entr: status process terminated" assert "$(cat $tmp/exec.out)" "" # terminal tests unset TMUX try "spacebar triggers utility" setup env SHELL=/bin/sh 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 using one-shot option" setup ls $tmp/file2 | entr -zp cat $tmp/file2 >$tmp/exec.out 2>$tmp/exec.err & bgpid=$! ; zz echo 456 >> $tmp/file2 ; zz wait $bgpid; assert "$?" "0" assert "$(cat $tmp/exec.err)" "" assert "$(head -n1 $tmp/exec.out)" "$(printf '456\n')" try "exec a command using one-shot option and return signal number" setup ls $tmp/file2 | entr -z sh -c 'kill -9 $$' >$tmp/exec.out 2>$tmp/exec.err assert "$?" "137" assert "$(cat $tmp/exec.err)" "" assert "$(cat $tmp/exec.out)" "" try "exec a command using one-shot and shell options and return signal" setup ls $tmp/file2 | entr -z -s 'kill -9 $$' >$tmp/exec.out 2>$tmp/exec.err assert "$?" "137" assert "$(tail -c23 $tmp/exec.out)" "" try "fail to exec a command using one-shot option" setup ls $tmp/file* | entr -z /usr/bin/false_X 2>$tmp/exec.err assert "$?" "1" try "exec a command using one-shot option exit code from child" setup ls $tmp/file* | entr -z sh -c 'exit 4' & bgpid=$! ; zz wait $bgpid; assert "$?" "4" try "restart a server when a file is modified using one-shot option" setup if [ $(uname) == 'Linux' ]; then skip "GNU nc spins while retrying SELECT(2); busybox does not support domain sockets" else ls $tmp/file2 | entr -rz nc -l -U $tmp/nc.s >> $tmp/exec.out & bgpid=$! ; zz echo "123" | nc -NU $tmp/nc.s 2> /dev/null || { echo "123" | nc -U $tmp/nc.s } ; zz echo 456 >> $tmp/file2 ; zz wait $bgpid; assert "$?" "0" assert "$(cat $tmp/exec.out)" "123" fi try "exec a command in non-intertive mode" setup ls $tmp/file* | entr tty >$tmp/exec.out & bgpid=$! ; zz kill -INT $bgpid wait $bgpid; assert "$?" "0" 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 "$?" "0" assert "$(cat $tmp/exec.out)" "1" try "exec a command as a background task, and verify that read from stdin doesn't complain" setup ls $tmp/file* | entr -r sh -c 'read X' 2>$tmp/exec.err & bgpid=$! ; zz kill -INT $bgpid wait $bgpid; assert "$?" "0" assert "$(cat $tmp/exec.err)" "" 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 "$?" "0" 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 "$?" "0" 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 hidden subdirectory is added" setup ls -d $tmp | entr -ddp sh -c 'echo ping' >$tmp/exec.out 2>$tmp/exec.err \ || true & bgpid=$! ; zz mkdir $tmp/.newdir wait $bgpid; assert "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" if [ $(uname | grep -E '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" if [ ! -d /usr/include ]; then skip "Operating system does not include files in a standard location" else setup cp /usr/include/*.h $tmp/ cd $tmp git init -q git config --local user.email entr.test@example.com git add *.h git commit -m "initial checkin" -q for f in `ls *.h | head`; 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 "$?" "0" rm -rf $tmp/.git assert "$(cat $tmp/exec.out)" "changed" fi 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "ensure that all shell subprocesses are terminated when terminal is closed" 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 -HUP $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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" 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 "$?" "0" assert "$(cat $tmp/exec.out)" "vroom" try "exec an interactive utility when a file changes" setup if ! test -t 0 ; then skip "A TTY is not available" else ls $tmp/file* | entr_tty -p sh -c 'tty | cut -c1-9' 2> /dev/null > $tmp/exec.out & bgpid=$! ; zz echo 456 >> $tmp/file2 ; zz kill -INT $bgpid wait $bgpid; assert "$?" "0" 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 "$?" "0" 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 "$?" "0" 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"