/* -*- Mode: C -*- */ /* fifowatchd: Daemon to watch a fifo and execute a script on input. */ /*- * Copyright (c) 2013 Taylor R. Campbell * 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 COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN * NO EVENT SHALL THE COPYRIGHT HOLDERS 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. */ /* Define various feature macro crap to get everything... */ #define _GNU_SOURCE #define _NETBSD_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void usage(void); static void parse_arguments(int, char **); static void open_fifo(const char *); static void setup_cleanup(void); static void maybe_daemonize(void); static void detach(void); static void wait_for_fifo(void); static void run_program(void); static void wait_for_delay(void); static void cleanup(void); static void signal_handler(int); static void signal_ignorer(int); static void write_pidfile(pid_t); static void cleanup_pidfile(void); static void fail(const char *, ...); static void failx(const char *, ...); static void warning(const char *, ...); static void warningx(const char *, ...); static void vlog(int, const char *, va_list); static void vlogc(int, int, const char *, va_list); static void vlog_errno(int, const char *, va_list); /* Non-static to match BSD declaration. */ void setprogname(const char *); const char * getprogname(void); static unsigned int delay = 0; static int fflag = 0; static const char *pidfile = NULL; static int fiford = -1; static int fifowr = -1; static char **program = NULL; static bool detached = false; int main(int argc, char **argv) { /* Sanitize the world. */ setprogname(argv[0]); if (signal(SIGPIPE, &signal_ignorer) == SIG_ERR) fail("ignore SIGPIPE"); /* Get ready. */ parse_arguments(argc, argv); setup_cleanup(); maybe_daemonize(); /* Go! */ for (;;) { wait_for_fifo(); warningx("poked"); run_program(); wait_for_delay(); } } static void usage(void) { failx("Usage: %s [-f] [-d delay] [-n name] [-p pidfile]" " fifo cmd args...", getprogname()); } static void parse_arguments(int argc, char **argv) { extern char *optarg; extern int optind; int ch; while ((ch = getopt(argc, argv, "+d:fn:p:")) != -1) { switch (ch) { case 'd': { unsigned long d; char *end; /* Parse the argument, carefully. */ errno = 0; d = strtoul(optarg, &end, 0); if ((optarg[0] == '\0') || (end[0] != '\0')) failx("invalid delay"); if (((errno == ERANGE) && (d == ULONG_MAX)) || (UINT_MAX < d)) failx("delay out of range"); delay = d; break; } case 'f': fflag = 1; break; case 'n': (void)strncpy(argv[0], optarg, strlen(argv[0])); argv[0] = optarg; setprogname(optarg); break; case 'p': pidfile = optarg; break; case '?': case '+': /* Whisky tango foxtrot, GNU? */ default: usage(); } } /* Advance past option arguments to the fifo. */ argc -= optind; argv += optind; /* Make sure there's a fifo and command left, at least. */ if (argc < 2) usage(); /* Open the fifo. */ open_fifo(argv[0]); /* Advance past fifo to command and arguments. */ argc -= 1; argv += 1; /* Set the program. */ program = argv; } static void open_fifo(const char *pathname) { struct stat st; int flags; /* Open the fifo for read without blocking. */ fiford = open(pathname, (O_RDONLY | O_NONBLOCK), 0); if (fiford == -1) fail("open fifo %s", pathname); /* Make sure it's a fifo. */ if (fstat(fiford, &st) == -1) fail("stat fifo %s", pathname); if (!S_ISFIFO(st.st_mode)) failx("open fifo: %s: not a fifo", pathname); /* Close the fifo on exec. */ flags = fcntl(fiford, F_GETFD, 0); if (flags == -1) fail("fcntl(F_GETFD)"); if (fcntl(fiford, F_SETFD, (flags | FD_CLOEXEC)) == -1) fail("fcntl(F_SETFD)"); /* * Open the fifo for write without blocking. We don't ever * write to it, but we need it open to make poll block rather * than return POLLHUP immediately in a loop. Must happen * after we open for read or else this will fail with ENXIO. */ fifowr = open(pathname, (O_WRONLY | O_NONBLOCK), 0); if (fifowr == -1) fail("open fifo for write %s", pathname); /* Make sure it's a fifo. */ if (fstat(fifowr, &st) == -1) fail("stat fifo %s", pathname); if (!S_ISFIFO(st.st_mode)) failx("open fifo: %s: not a fifo", pathname); /* Close the fifo on exec. */ flags = fcntl(fifowr, F_GETFD, 0); if (flags == -1) fail("fcntl(F_GETFD)"); if (fcntl(fifowr, F_SETFD, (flags | FD_CLOEXEC)) == -1) fail("fcntl(F_SETFD)"); } static void setup_cleanup(void) { static const struct sigaction zero_sa; struct sigaction sa = zero_sa; /* Create a sigaction. */ sa.sa_handler = &signal_handler; (void)sigfillset(&sa.sa_mask); sa.sa_flags = 0; /* Install it. */ if (sigaction(SIGHUP, &sa, NULL) == -1) fail("sigaction(SIGHUP)"); if (sigaction(SIGINT, &sa, NULL) == -1) fail("sigaction(SIGINT)"); if (sigaction(SIGTERM, &sa, NULL) == -1) fail("sigaction(SIGTERM)"); /* Install atexit too, in case we somehow exit not on a signal. */ if (atexit(&cleanup) == -1) fail("atexit"); } static void maybe_daemonize(void) { pid_t pid; /* * Write the pidfile in the parent in either case -- if we do * fork, we want the pidfile written before the parent returns. */ if (fflag) { write_pidfile(getpid()); } else { pid = fork(); switch (pid) { case -1: fail("fork"); case 0: /* child */ detach(); break; default: /* parent */ write_pidfile(pid); _exit(0); } } } static void detach(void) { int null_fd; openlog(getprogname(), LOG_PID, LOG_DAEMON); detached = true; warningx("starting"); if (setsid() == -1) fail("setsid"); null_fd = open("/dev/null", O_RDWR, 0); if (null_fd == -1) fail("open(/dev/null)"); if (dup2(null_fd, STDIN_FILENO) == -1) fail("dup2(/dev/null, stdin)"); if (dup2(null_fd, STDOUT_FILENO) == -1) fail("dup2(/dev/null, stdout)"); if (dup2(null_fd, STDERR_FILENO) == -1) fail("dup2(/dev/null, stderr)"); if (STDERR_FILENO < null_fd) { if (close(null_fd) == -1) warning("close(/dev/null)"); } } static void wait_for_fifo(void) { static const struct pollfd zero_pfd; struct pollfd pfd; int nfds; char buf; ssize_t nread; retry: pfd = zero_pfd; pfd.fd = fiford; pfd.events = POLLIN; nfds = poll(&pfd, 1, -1); if (nfds == -1) fail("poll"); if (nfds == 0) failx("poll timed out with no timeout!"); if (pfd.revents & POLLHUP) failx("fifo hung up"); if (pfd.revents & POLLERR) failx("fifo error"); if (!(pfd.revents & POLLIN)) failx("poll returned without data ready"); nread = read(fiford, &buf, 1); if (nread == -1) { if (errno == EAGAIN) goto retry; fail("read from fifo"); } if (nread < 0) failx("negative read from fifo: %zd", nread); if (nread < 1) /* Fifo can correctly yield 0 bytes, so don't warn. */ goto retry; if (1 < nread) /* paranoia */ failx("long read from fifo: %zd", nread); } static void run_program(void) { pid_t pid, waited; int status; pid = vfork(); switch (pid) { case -1: fail("vfork"); case 0: /* child */ if (execve(program[0], program, NULL) == -1) fail("execve"); fail("execve returned"); default: /* parent */ waited = waitpid(pid, &status, 0); if (waited == -1) fail("waitpid"); if (waited != pid) failx("waitpid returned wrong pid" ": %"PRIdMAX" != %"PRIdMAX, waited, pid); if (WIFEXITED(status) && (WEXITSTATUS(status) != 0)) warningx("child %"PRIdMAX" exited with nonzero status" ": %d", (intmax_t)pid, (int)WEXITSTATUS(status)); if (WIFSIGNALED(status)) warningx("child %"PRIdMAX" terminated on signal: %d", (intmax_t)pid, (int)WTERMSIG(status)); break; } } static void wait_for_delay(void) { unsigned int d; /* If sleep is interrupted, it returns the remaining time. */ for (d = delay; 0 < d; d = sleep(d)) continue; } static void cleanup(void) { cleanup_pidfile(); } static void signal_handler(int signo) { (void)warningx("exiting on signal %d", signo); /* * exit is unsafe in a signal handler, so clean up explicitly * and use _exit instead. */ cleanup(); _exit(0x80 | signo); } static void signal_ignorer(int signo) { (void)signo; /* ignore */ } static void write_pidfile(pid_t pid) { FILE *fp; if (pidfile == NULL) { warningx("no pidfile specified"); return; } fp = fopen(pidfile, "wx"); if (fp == NULL) fail("unable to create pidfile"); if (fprintf(fp, "%"PRIdMAX"\n", (intmax_t)pid) < 0) fail("unable to write pidfile"); if (fclose(fp) == EOF) fail("unable to close pidfile"); } static void cleanup_pidfile(void) { if (pidfile == NULL) return; if (unlink(pidfile) == -1) warning("unable to unlink pidfile %s", pidfile); } static void fail(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog_errno(LOG_ERR, fmt, ap); va_end(ap); exit(1); } static void failx(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog(LOG_ERR, fmt, ap); va_end(ap); exit(1); } static void warning(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog_errno(LOG_WARNING, fmt, ap); va_end(ap); } static void warningx(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog(LOG_WARNING, fmt, ap); va_end(ap); } static void vlog(int pri, const char *fmt, va_list ap) { if (detached) { vsyslog(pri, fmt, ap); } else { /* XXX Use pri? */ (void)fprintf(stderr, "%s: ", getprogname()); (void)vfprintf(stderr, fmt, ap); (void)fprintf(stderr, "\n"); } } static void vlogc(int pri, int error, const char *fmt, va_list ap) { if (detached) { char *msg; if (vasprintf(&msg, fmt, ap) == -1) { syslog(LOG_ERR, "out of memory!"); vsyslog(pri, fmt, ap); } else { assert(msg != NULL); syslog(pri, "%s: %s", msg, strerror(error)); free(msg); } } else { /* XXX Use pri? */ (void)fprintf(stderr, "%s: ", getprogname()); (void)vfprintf(stderr, fmt, ap); (void)fprintf(stderr, ": %s\n", strerror(error)); } } static void vlog_errno(int pri, const char *fmt, va_list ap) { vlogc(pri, errno, fmt, ap); } static const char *progname; void setprogname(const char *p) { progname = strrchr(p, '/'); if (progname != NULL) progname += 1; /* skip slash */ else progname = p; } const char * getprogname(void) { assert(progname != NULL); return progname; }