/* dircond - directory content watcher daemon
Copyright (C) 2012, 2013 Sergey Poznyakoff
Dircond is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3 of the License, or (at your
option) any later version.
Dircond is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with dircond. If not, see . */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "dircond.h"
#ifndef SYSCONFDIR
# define SYSCONFDIR "/etc/"
#endif
/* Configuration settings */
const char *program_name; /* This program name */
const char *conffile = SYSCONFDIR "/dircond.conf";
int foreground; /* Remain in the foreground */
char *tag; /* Syslog tag */
int facility = -1; /* Use this syslog facility for logging.
-1 means log to stderr */
int debug_level; /* Debug verbosity level */
char *pidfile = NULL; /* Store PID to this file */
char *user = NULL; /* User to run as */
/* Diagnostic functions */
const char *
severity(int prio)
{
switch (prio) {
case LOG_EMERG:
return "EMERG";
case LOG_ALERT:
return "ALERT";
case LOG_CRIT:
return "CRIT";
case LOG_ERR:
return "ERROR";
case LOG_WARNING:
return "WARNING";
case LOG_NOTICE:
return "NOTICE";
case LOG_INFO:
return "INFO";
case LOG_DEBUG:
return "DEBUG";
}
return NULL;
}
void
vdiag(int prio, const char *fmt, va_list ap)
{
const char *s;
if (facility <= 0) {
fprintf(stderr, "%s: ", program_name);
s = severity(prio);
if (s)
fprintf(stderr, "[%s] ", s);
vfprintf(stderr, fmt, ap);
fputc('\n', stderr);
} else {
vsyslog(prio, fmt, ap);
}
}
void
diag(int prio, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vdiag(prio, fmt, ap);
va_end(ap);
}
void
debugprt(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vdiag(LOG_DEBUG, fmt, ap);
va_end(ap);
}
/* Memory allocation with error checking */
void *
emalloc(size_t size)
{
void *p = malloc(size);
if (!p) {
diag(LOG_CRIT, "not enough memory");
exit(2);
}
return p;
}
void *
ecalloc(size_t nmemb, size_t size)
{
void *p = calloc(nmemb, size);
if (!p) {
diag(LOG_CRIT, "not enough memory");
exit(2);
}
return p;
}
void *
erealloc(void *ptr, size_t size)
{
void *p = realloc(ptr, size);
if (!p) {
diag(LOG_CRIT, "not enough memory");
exit(2);
}
return p;
}
char *
estrdup(const char *str)
{
size_t len = strlen(str);
char *p = emalloc(len + 1);
memcpy(p, str, len);
p[len] = 0;
return p;
}
/* Create a full file name from directory and file name */
char *
mkfilename(const char *dir, const char *file)
{
char *tmp;
size_t dirlen = strlen(dir);
size_t fillen = strlen(file);
size_t len;
while (dirlen > 0 && dir[dirlen-1] == '/')
dirlen--;
len = dirlen + (dir[0] ? 1 : 0) + fillen;
tmp = malloc(len + 1);
if (tmp) {
memcpy(tmp, dir, dirlen);
if (dir[0])
tmp[dirlen++] = '/';
memcpy(tmp + dirlen, file, fillen);
tmp[len] = 0;
}
return tmp;
}
/* Process list */
/* Redirector codes */
#define REDIR_OUT 0
#define REDIR_ERR 1
/* A running process is described by this structure */
struct process {
struct process *next, *prev;
unsigned timeout; /* Timeout in seconds */
pid_t pid; /* PID */
time_t start; /* Time when the process started */
pid_t redir[2]; /* PIDs of redirector processes (0 if no
redirector) */
};
/* List of running processes */
struct process *proc_list;
/* List of available process slots */
struct process *proc_avail;
/* Declare functions for handling process lists */
struct process *
proc_unlink(struct process **root, struct process *p)
{
if (p->prev)
p->prev->next = p->next;
else
*root = p->next;
if (p->next)
p->next->prev = p->prev;
p->next = p->prev = NULL;
return p;
}
struct process *
proc_pop(struct process **pp)
{
if (*pp)
return proc_unlink(pp, *pp);
return NULL;
}
void
proc_push(struct process **pp, struct process *p)
{
p->prev = NULL;
p->next = *pp;
if (*pp)
(*pp)->prev = p;
*pp = p;
}
/* Command line processing and auxiliary functions */
static void
set_program_name(const char *arg)
{
char *p = strrchr(arg, '/');
if (p)
program_name = p + 1;
else
program_name = arg;
}
void
signal_setup(void (*sf) (int))
{
signal(SIGTERM, sf);
signal(SIGQUIT, sf);
signal(SIGINT, sf);
signal(SIGHUP, sf);
signal(SIGALRM, sf);
signal(SIGUSR1, sf);
signal(SIGUSR2, sf);
}
static void
close_fds(fd_set *fdset)
{
int i;
for (i = sysconf(_SC_OPEN_MAX) - 1; i >= 0; i--) {
if (fdset && FD_ISSET(i, fdset))
continue;
close(i);
}
}
void
storepid(const char *pidfile)
{
FILE *fp = fopen(pidfile, "w");
if (!fp) {
diag(LOG_ERR, "cannot open pidfile %s for writing: %s",
pidfile, strerror(errno));
} else {
fprintf(fp, "%lu\n", (unsigned long) getpid());
fclose(fp);
}
}
static int
membergid(gid_t gid, size_t gc, gid_t *gv)
{
int i;
for (i = 0; i < gc; i++)
if (gv[i] == gid)
return 1;
return 0;
}
static void
get_user_groups(uid_t uid, size_t *pgidc, gid_t **pgidv)
{
size_t gidc = 0, n = 0;
gid_t *gidv = NULL;
struct passwd *pw;
struct group *gr;
pw = getpwuid(uid);
if (!pw) {
diag(LOG_ERR, 0, "no used with UID %lu",
(unsigned long)uid);
exit(2);
}
n = 32;
gidv = ecalloc(n, sizeof(gidv[0]));
gidv[0] = pw->pw_gid;
gidc = 1;
setgrent();
while (gr = getgrent()) {
char **p;
for (p = gr->gr_mem; *p; p++)
if (strcmp(*p, pw->pw_name) == 0) {
if (n == gidc) {
n += 32;
gidv = erealloc(gidv,
n * sizeof(gidv[0]));
}
if (!membergid(gr->gr_gid, gidc, gidv))
gidv[gidc++] = gr->gr_gid;
}
}
endgrent();
*pgidc = gidc;
*pgidv = gidv;
}
void
setuser(const char *user)
{
struct passwd *pw;
size_t gidc;
gid_t *gidv;
pw = getpwnam(user);
if (!pw) {
diag(LOG_CRIT, "getpwnam(%s): %s", user, strerror(errno));
exit(2);
}
if (pw->pw_uid == 0)
return;
get_user_groups(pw->pw_uid, &gidc, &gidv);
if (setgroups(gidc, gidv) < 0) {
diag(LOG_CRIT, "setgroups: %s", strerror(errno));
exit(2);
}
free(gidv);
if (setgid(pw->pw_gid)) {
diag(LOG_CRIT, "setgid(%lu): %s", (unsigned long) pw->pw_gid,
strerror(errno));
exit(2);
}
if (setuid(pw->pw_uid)) {
diag(LOG_CRIT, "setuid(%lu): %s", (unsigned long) pw->pw_uid,
strerror(errno));
exit(2);
}
}
/* Process list handling (high-level) */
struct process *
register_process(pid_t pid, time_t t, unsigned timeout)
{
struct process *p;
if (proc_avail)
p = proc_pop(&proc_avail);
else
p = emalloc(sizeof(*p));
memset(p, 0, sizeof(*p));
p->timeout = timeout;
p->pid = pid;
p->start = t;
proc_push(&proc_list, p);
return p;
}
void
deregister_process(pid_t pid, time_t t)
{
struct process *p;
for (p = proc_list; p; p = p->next)
if (p->pid == pid) {
if (p->prev)
p->prev->next = p->next;
else
proc_list = p;
if (p->next)
p->next->prev = p->prev;
free(p);
break;
}
}
struct process *
process_lookup(pid_t pid)
{
struct process *p;
for (p = proc_list; p; p = p->next)
if (p->pid == pid)
return p;
return NULL;
}
static void
print_status(pid_t pid, int status, sigset_t *mask)
{
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == 0)
debug(1, ("process %lu exited successfully",
(unsigned long) pid));
else
diag(LOG_ERR, "process %lu failed with status %d",
(unsigned long) pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
int prio;
if (sigismember(mask, WTERMSIG(status)))
prio = LOG_DEBUG;
else
prio = LOG_ERR;
diag(prio, "process %lu terminated on signal %d",
(unsigned long) pid, WTERMSIG(status));
} else if (WIFSTOPPED(status))
diag(LOG_ERR, "process %lu stopped on signal %d",
(unsigned long) pid, WSTOPSIG(status));
#ifdef WCOREDUMP
else if (WCOREDUMP(status))
diag(LOG_ERR,
"process %lu dumped core", (unsigned long) pid);
#endif
else
diag(LOG_ERR,
"process %lu terminated with unrecognized status",
(unsigned long) pid);
}
void
process_cleanup(int expect_term)
{
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
sigset_t set;
struct process *p = process_lookup(pid);
sigemptyset(&set);
if (expect_term)
sigaddset(&set, SIGTERM);
if (!p) {
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGKILL);
}
print_status(pid, status, &set);
if (p) {
if (p->redir[REDIR_OUT])
kill(p->redir[REDIR_OUT], SIGKILL);
if (p->redir[REDIR_ERR])
kill(p->redir[REDIR_ERR], SIGKILL);
p->pid = 0;
proc_unlink(&proc_list, p);
proc_push(&proc_avail, p);
}
}
}
void
process_timeouts()
{
struct process *p;
time_t now = time(NULL);
time_t alarm_time = 0, x;
debug(2, ("begin scanning process list"));
for (p = proc_list; p; p = p->next) {
x = now - p->start;
if (x >= p->timeout) {
diag(LOG_ERR, "process %lu timed out",
(unsigned long) p->pid);
kill(p->pid, SIGKILL);
} else if (alarm_time == 0 ||
p->timeout - x < alarm_time)
alarm_time = p->timeout - x;
}
if (alarm_time) {
debug(2, ("scheduling alarm in %lu seconds",
(unsigned long) alarm_time));
alarm(alarm_time);
}
debug(2, ("end scanning process list"));
}
/* Operations with handlers and redirections */
static void
redir_exit(int sig)
{
_exit(0);
}
int
open_redirector(const char *tag, int prio, pid_t *return_pid)
{
int p[2];
FILE *fp;
char buf[512];
pid_t pid;
fd_set fdset;
if (pipe(p)) {
diag(LOG_ERR,
"cannot start redirector for %s, pipe failed: %s",
tag, strerror(errno));
return -1;
}
switch (pid = fork()) {
case 0:
/* Redirector process */
FD_ZERO(&fdset);
FD_SET(p[0], &fdset);
if (facility <= 0)
FD_SET(2, &fdset);
close_fds(&fdset);
alarm(0);
signal_setup(redir_exit);
fp = fdopen(p[0], "r");
if (fp == NULL)
_exit(1);
if (facility > 0)
openlog(tag, LOG_PID, facility);
while (fgets(buf, sizeof(buf), fp) > 0) {
int len = strlen(buf);
if (len && buf[len-1] == '\n')
buf[len-1] = 0;
diag(prio, "%s", buf);
}
_exit(0);
case -1:
diag(LOG_CRIT,
"cannot run redirector `%s': fork failed: %s",
tag, strerror(errno));
return -1;
default:
debug(1, ("redirector for %s started, pid=%lu",
tag, (unsigned long) pid));
close (p[0]);
*return_pid = pid;
return p[1];
}
}
static int
switchpriv(struct handler *hp)
{
if (hp->uid == 0 || hp->uid == getuid())
return 0;
if (setgroups(hp->gidc, hp->gidv) < 0) {
diag(LOG_CRIT, "setgroups: %s",
strerror(errno));
return 1;
}
if (setregid(hp->gidv[0], hp->gidv[0]) < 0) {
diag(LOG_CRIT, "setregid(%lu,%lu): %s",
(unsigned long) hp->gidv[0],
(unsigned long) hp->gidv[0],
strerror(errno));
return 1;
}
if (setreuid(hp->uid, hp->uid) < 0) {
diag(LOG_CRIT, "setreuid(%lu,%lu): %s",
(unsigned long) hp->uid,
(unsigned long) hp->uid,
strerror(errno));
return 1;
}
return 0;
}
static int
run_handler(struct dirwatcher *dp, struct handler *hp, int event,
const char *file)
{
pid_t pid;
char buf[1024];
int redir_fd[2] = { -1, -1 };
pid_t redir_pid[2];
struct process *p;
if (!hp->prog)
return 0;
if (access(hp->prog, X_OK)) {
diag(LOG_ERR, "watchpoint %s: cannot execute %s: %s",
dp->dirname, hp->prog, strerror(errno));
return 1;
}
debug(1, ("starting %s, dir=%s, file=%s", hp->prog, dp->dirname, file));
if (hp->flags & HF_STDERR)
redir_fd[REDIR_ERR] = open_redirector(hp->prog, LOG_ERR,
&redir_pid[REDIR_ERR]);
if (hp->flags & HF_STDOUT)
redir_fd[REDIR_OUT] = open_redirector(hp->prog, LOG_INFO,
&redir_pid[REDIR_OUT]);
pid = fork();
if (pid == -1) {
close(redir_fd[REDIR_OUT]);
close(redir_fd[REDIR_ERR]);
diag(LOG_ERR, "fork: %s", strerror(errno));
return -1;
}
if (pid == 0) {
/* child */
char *argv[2];
fd_set fdset;
if (switchpriv(hp))
_exit(127);
if (chdir(dp->dirname)) {
diag(LOG_CRIT, "cannot change to %s: %s",
dp->dirname, strerror(errno));
_exit(127);
}
FD_ZERO(&fdset);
if (redir_fd[REDIR_OUT] != -1) {
if (redir_fd[REDIR_OUT] != 1 &&
dup2(redir_fd[REDIR_OUT], 1) == -1) {
diag(LOG_ERR, "dup2: %s", strerror(errno));
_exit(127);
}
FD_SET(1, &fdset);
}
if (redir_fd[REDIR_ERR] != -1) {
if (redir_fd[REDIR_ERR] != 2 &&
dup2(redir_fd[REDIR_ERR], 2) == -1) {
diag(LOG_ERR, "dup2: %s", strerror(errno));
_exit(127);
}
FD_SET(2, &fdset);
}
close_fds(&fdset);
alarm(0);
signal_setup(SIG_DFL);
signal(SIGCHLD, SIG_DFL);
argv[0] = (char*) hp->prog;
argv[1] = NULL;
snprintf(buf, sizeof buf, "%d", event);
setenv("DIRCOND_EVENT_CODE", buf, 1);
setenv("DIRCOND_EVENT", ev_code_to_name(event), 1);
if (file)
setenv("DIRCOND_FILE", file, 1);
execv(argv[0], argv);
_exit(127);
}
/* master */
debug(1, ("%s running; dir=%s, file=%s, pid=%lu",
hp->prog, dp->dirname, file, (unsigned long)pid));
p = register_process(pid, time(NULL), hp->timeout);
memcpy(p->redir, redir_pid, sizeof(p->redir));
close(redir_fd[REDIR_OUT]);
close(redir_fd[REDIR_ERR]);
if (hp->flags & HF_NOWAIT) {
return 0;
}
debug(1, ("waiting for %s (%lu) to terminate",
hp->prog, (unsigned long)pid));
while (time(NULL) - p->start < 2 * p->timeout) {
sleep(1);
process_cleanup(1);
if (p->pid == 0)
break;
}
return 0;
}
/* Output a help summary. Return a code suitable for exit(2). */
int
help()
{
printf("Usage: %s [OPTIONS] [CONFIG]\n", program_name);
printf("OPTIONS are:\n\n");
printf(" -d increase debug verbosity\n");
printf(" -F FACILITY log under this syslog facility (default: daemon);\n");
printf(" use -F 0 to log to stderr instead\n");
printf(" -f run in the foreground\n");
printf(" -L TAG log with this syslog tag\n");
printf(" -P FILE write PID to FILE\n");
printf(" -t check configuration file for errors and exit\n");
printf(" -u USER run as this USER\n\n");
printf(" -h output this help summary\n");
printf(" -V print program version and exit\n\n");
printf("Report bugs to .\n");
return 0;
}
static char license[] = "\
License GPLv3+: GNU GPL version 3 or later \n\
This is free software: you are free to change and redistribute it.\n\
There is NO WARRANTY, to the extent permitted by law.\n";
int
version()
{
printf("dircond %s\n", VERSION);
printf("Copyright (C) 2012, 2013 Sergey Poznyakoff\n");
printf("%s\n", license);
return 0;
}
int
get_facility(const char *arg)
{
int f;
if (read_facility(arg, &f)) {
switch (errno) {
case EINVAL:
diag(LOG_CRIT,
"unknown syslog facility: %s", arg);
break;
case ERANGE:
diag(LOG_CRIT, "syslog facility out of range");
break;
default:
abort();
}
exit(1);
}
return f;
}
static void
process_event(struct inotify_event *ep)
{
struct dirwatcher *dp;
struct handler *h;
dp = dirwatcher_lookup_wd(ep->wd);
if (ep->mask & IN_IGNORED)
return;
else if (ep->mask & IN_Q_OVERFLOW) {
diag(LOG_NOTICE,
"event queue overflow");
return;
} else if (ep->mask & IN_UNMOUNT) {
/* FIXME: not sure if there's
anything to do. Perhaps we should
deregister the watched dirs that
were located under the mountpoint
*/
return;
} else if (!dp) {
if (ep->name)
diag(LOG_NOTICE, "unrecognized event %x"
"for %s", ep->mask, ep->name);
else
diag(LOG_NOTICE,
"unrecognized event %x", ep->mask);
return;
} else if (ep->mask & IN_CREATE) {
debug(1, ("%s/%s created", dp->dirname, ep->name));
check_new_watcher(dp->dirname, ep->name);
} else if (ep->mask & (IN_DELETE|IN_MOVED_FROM)) {
debug(1, ("%s/%s deleted", dp->dirname, ep->name));
remove_watcher(dp->dirname, ep->name);
}
ev_log(ep, dp);
for (h = dp->handler_list; h; h = h->next) {
if (h->ev_mask & ep->mask)
run_handler(dp, h, ep->mask, ep->name);
}
}
int signo = 0;
void
sigmain(int sig)
{
signo = sig;
signal(sig, sigmain);
}
char buffer[4096];
int ifd;
int
main(int argc, char **argv)
{
int c;
int opt_debug_level = 0;
int opt_foreground = 0;
char *opt_tag = NULL;
char *opt_pidfile = NULL;
char *opt_user = NULL;
int lint_only = 0;
set_program_name(argv[0]);
tag = (char*) program_name;
ifd = inotify_init();
if (ifd == -1) {
diag(LOG_CRIT, "inotify_init: %s", strerror(errno));
exit(1);
}
while ((c = getopt(argc, argv, "dF:fhLP:tu:V")) != EOF) {
switch (c) {
case 'd':
opt_debug_level++;
break;
case 'F':
opt_facility = get_facility(optarg);
break;
case 'f':
opt_foreground++;
break;
case 'h':
exit(help());
break;
case 'L':
opt_tag = optarg;
break;
case 'P':
opt_pidfile = optarg;
break;
case 't':
lint_only = 1;
break;
case 'u':
opt_user = optarg;
if (!getpwnam(opt_user)) {
diag(LOG_CRIT, "no such user: %s", opt_user);
exit(1);
}
break;
case 'V':
exit(version());
default:
exit(1);
}
}
argc -= optind;
argv += optind;
switch (argc) {
default:
diag(LOG_CRIT, "too many arguments");
exit(1);
case 1:
conffile = argv[0];
break;
case 0:
break;
}
config_parse(conffile);
if (lint_only)
return 0;
if (opt_debug_level)
debug_level += opt_debug_level;
if (opt_foreground)
foreground = opt_foreground;
if (opt_tag)
tag = opt_tag;
if (opt_pidfile)
pidfile = opt_pidfile;
if (opt_facility != -1)
facility = opt_facility;
if (opt_user)
user = opt_user;
setup_watchers();
/* Become a daemon */
if (!foreground) {
if (daemon(0, 0)) {
diag(LOG_CRIT, "daemon: %s", strerror(errno));
exit(1);
}
if (facility <= 0)
facility = LOG_DAEMON;
}
if (facility > 0)
openlog(tag, LOG_PID, facility);
diag(LOG_INFO, "started");
/* Write pidfile */
if (pidfile)
storepid(pidfile);
/* Relinquish superuser privileges */
if (user && getuid() == 0)
setuser(user);
signal_setup(sigmain);
signal(SIGCHLD, sigmain);
/* Main loop */
while (1) {
struct inotify_event *ep;
size_t size;
ssize_t rdbytes;
process_timeouts();
process_cleanup(0);
rdbytes = read(ifd, buffer, sizeof(buffer));
if (rdbytes == -1) {
if (errno == EINTR) {
if (signo == SIGCHLD || signo == SIGALRM)
continue;
diag(LOG_NOTICE, "got signal %d", signo);
break;
}
diag(LOG_NOTICE, "read failed: %s", strerror(errno));
break;
}
ep = (struct inotify_event *) buffer;
while (rdbytes) {
if (ep->wd >= 0)
process_event(ep);
size = sizeof(*ep) + ep->len;
ep = (struct inotify_event *) ((char*) ep + size);
rdbytes -= size;
}
}
diag(LOG_INFO, "stopped");
return 0;
}