adhocify/adhocify.c

714 lines
15 KiB
C

/*
* Copyright (c) 2014-2017 Albert S. <adhocify@quitesimple.org>
*
* 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.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <libgen.h>
#include <inttypes.h>
#include <time.h>
#include <stdbool.h>
#include <stdarg.h>
#include <errno.h>
#include <fnmatch.h>
#include <getopt.h>
#include <ftw.h>
#include <linux/limits.h>
#define BUF_SIZE (sizeof(struct inotify_event) + NAME_MAX + 1) * 1024
#define STREQ(s1,s2) ( strcmp(s1,s2) == 0 )
#define SCRIPT_PLACE_SPECIFIER "{}" //TODO: think of a better name...
struct watchlistentry
{
int ifd;
char *path;
bool isdir;
struct watchlistentry *next;
};
struct watchlistentry *watchlist_head = NULL;
struct watchlistentry **watchlist = &watchlist_head;
struct ignorelist
{
char *ignore;
struct ignorelist *next;
};
struct ignorelist *ignorelist_head = NULL;
struct ignorelist **ignorelist_current = &ignorelist_head;
/* Write-once globals. Set from process_arguments*/
bool silent = false;
bool noenv = false;
bool fromstdin = false;
bool forkbombcheck = true;
bool daemonize = false;
bool exit_with_child = false;
int awaited_child_exit_code = -1;
bool negate_child_exit_code = false;
uint32_t mask = 0;
char *prog = NULL;
char *path_logfile = NULL;
char **script_arguments = NULL; //options to be passed to script we are calling
size_t n_script_arguments = 0;
void *xmalloc(size_t size)
{
void *m = malloc(size);
if(m == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}
return m;
}
char *xstrdup(const char *s)
{
char *tmp = strdup(s);
if(tmp == NULL)
{
perror("strdup");
exit(EXIT_FAILURE);
}
return tmp;
}
char *xrealpath(const char *path, char *resolved_path)
{
char *tmp = realpath(path, resolved_path);
if(tmp == NULL)
{
char *errorstr = strerror(errno);
fprintf(stderr, "realpath on %s failed: %s\n", path, errorstr);
exit(EXIT_FAILURE);
}
return tmp;
}
char *ndirname(const char *path)
{
if(path == NULL)
{
return xstrdup(".");
}
char *c = strdupa(path);
return xstrdup(dirname(c));
}
char *find_ifd_path(int ifd)
{
for(struct watchlistentry *lkp = watchlist_head; lkp != NULL; lkp = lkp->next)
{
if(lkp->ifd == ifd)
{
return lkp->path;
}
}
return NULL;
}
bool is_ignored(const char *filename)
{
for(struct ignorelist *l = ignorelist_head; l != NULL; l = l->next)
{
if(fnmatch(l->ignore, filename, 0) == 0)
{
return true;
}
}
return false;
}
bool path_is_directory(const char *path)
{
struct stat sb;
int r = stat(path, &sb);
if(r == -1)
{
perror("stat");
return false;
}
return S_ISDIR(sb.st_mode);
}
static inline bool file_exists(const char *path)
{
return access(path, F_OK) == 0;
}
void add_to_ignore_list(const char *str)
{
*ignorelist_current = xmalloc(sizeof(struct ignorelist));
(*ignorelist_current)->ignore = xstrdup(str);
ignorelist_current = &(*ignorelist_current)->next;
}
void logwrite(const char *format, ...)
{
if(silent)
{
return;
}
va_list args;
va_start(args, format);
vfprintf(stdout, format, args);
fflush(stdout);
va_end(args);
}
void logerror(const char *format, ...)
{
va_list args;
va_start(args, format);
char *prefix = "Error: ";
char *tmp = alloca(strlen(format) + strlen(prefix) + 1);
strcpy(tmp, prefix);
strcat(tmp, format);
vfprintf(stderr, tmp, args);
fflush(stderr);
va_end(args);
}
void watchqueue_add_path(const char *pathname)
{
*watchlist = xmalloc(sizeof(struct watchlistentry));
struct watchlistentry *e = *watchlist;
char *path = xrealpath(pathname, NULL);
e->ifd = 0;
e->path = path;
e->isdir = path_is_directory(pathname);
e->next = NULL;
watchlist= &e->next;
}
void create_watches(int fd, uint32_t mask)
{
for(struct watchlistentry *lkp = watchlist_head; lkp != NULL; lkp = lkp->next)
{
int ret = inotify_add_watch(fd, lkp->path, mask);
if(ret == -1)
{
perror("inotify_add_watch");
exit(EXIT_FAILURE);
}
lkp->ifd = ret;
}
}
bool redirect_stdout(const char *outfile)
{
int fd = open(outfile, O_CREAT | O_WRONLY | O_APPEND, S_IRUSR | S_IWUSR);
if(fd == -1)
{
perror("open");
return false;
}
if(dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
{
perror("dup2");
return false;
}
return true;
}
bool run_prog(const char *eventfile, uint32_t eventmask)
{
pid_t pid = fork();
if(pid == 0)
{
if(path_logfile)
{
if(! redirect_stdout(path_logfile))
{
return false;
}
}
if(! noenv)
{
char envvar[30];
snprintf(envvar, sizeof(envvar), "ADHOCIFYEVENT=%"PRIu32, eventmask);
putenv(envvar);
}
//This can be thrown away, if in create_script_args we create pointer to the pointer pointing to the SCRIPT_PLACE_SPECIFIER string.
//Then all what we have to do here is to set the pointer to eventfile. However, we can have multiple of those, probably not worth it as we shouldn't have tons of arguments
for(unsigned int i = 0; i < n_script_arguments; i++)
{
char *argument = script_arguments[i];
if(argument && STREQ(argument, SCRIPT_PLACE_SPECIFIER))
script_arguments[i] = eventfile;
}
execv(prog, script_arguments);
perror("execv");
return false;
}
if(pid == -1)
{
perror("fork");
return false;
}
return true;
}
uint32_t nameToMask(const char *name)
{
if(STREQ(name, "IN_CLOSE_WRITE"))
return IN_CLOSE_WRITE;
else if(STREQ(name, "IN_OPEN"))
return IN_OPEN;
else if(STREQ(name, "IN_MODIFY"))
return IN_MODIFY;
else if(STREQ(name, "IN_DELETE"))
return IN_DELETE;
else if(STREQ(name, "IN_ATTRIB"))
return IN_ATTRIB;
else if(STREQ(name, "IN_CLOSE_NOWRITE"))
return IN_CLOSE_NOWRITE;
else if(STREQ(name, "IN_MOVED_FROM"))
return IN_MOVED_FROM;
else if(STREQ(name, "IN_MOVED_TO"))
return IN_MOVED_TO;
else if(STREQ(name, "IN_CREATE"))
return IN_CREATE;
else if(STREQ(name, "IN_DELETE_SELF"))
return IN_DELETE_SELF;
else if(STREQ(name, "IN_MOVE_SELF"))
return IN_MOVE_SELF;
else if(STREQ(name, "IN_ALL_EVENTS"))
return IN_ALL_EVENTS;
else if(STREQ(name, "IN_CLOSE"))
return IN_CLOSE;
else if(STREQ(name, "IN_MOVE"))
return IN_MOVE;
else
return 0;
}
void check_forkbomb(const char *path_logfile, const char *path_prog)
{
char *dir_log = ndirname(path_logfile);
char *dir_prog = ndirname(path_prog);
struct watchlistentry *lkp = watchlist_head;
while(lkp)
{
if(lkp->isdir)
{
char *dir_lkpPath = lkp->path;
if( STREQ(dir_lkpPath, dir_log) || STREQ(dir_lkpPath, dir_prog) )
{
logerror("Don't place your logfiles or command in a directory you are watching for events. Pass -b to bypass this check.\n");
exit(EXIT_FAILURE);
}
}
lkp = lkp->next;
}
free(dir_log);
free(dir_prog);
}
void queue_watches_from_stdin()
{
char *line = NULL;
size_t n = 0;
ssize_t r;
while((r = getline(&line, &n, stdin)) != -1)
{
if(line[r-1] == '\n')
line[r-1] = 0;
watchqueue_add_path(line);
}
}
char *get_eventfile_abspath(struct inotify_event *event)
{
char *wdpath = find_ifd_path(event->wd);
if(wdpath == NULL)
{
return NULL;
}
char *result = NULL;
if((event->len) > 0 )
{
if(asprintf(&result, "%s/%s", wdpath, event->name) == -1)
{
return NULL;
}
}
else
{
result = strdup(wdpath);
}
return result;
}
void handle_event(struct inotify_event *event)
{
if(event->mask & mask)
{
char *eventfile_abspath = get_eventfile_abspath(event);
if(eventfile_abspath == NULL)
{
logerror("Could not get absolute path for event. Watch descriptor %i\n", event->wd);
exit(EXIT_FAILURE);
}
if(is_ignored(eventfile_abspath))
{
free(eventfile_abspath);
return;
}
logwrite("Starting execution of child %s\n", prog);
bool r = run_prog(eventfile_abspath, event->mask);
if(!r)
{
logerror("Execution of child %s failed\n", prog);
exit(EXIT_FAILURE);
}
fflush(stdout);
fflush(stderr);
free (eventfile_abspath);
}
}
static inline char *get_cwd()
{
return getcwd(NULL,0);
}
void print_usage()
{
printf("adhocify [OPTIONS] command [arguments for command] - Monitor for inotify events and launch commands\n\n");
printf("--daemon, -d run as a daemon\n");
printf("--path, -w adds the specified path to the watchlist\n");
printf("--logfile, -o path to write output of adhocify and stdout/stderr of launched commands to\n");
printf("--mask, -m inotify event to watch for (see inotify(7)). Can be specified multiple times to watch for several events\n");
printf("--no-env, -a if specified, the inotify event which occured won't be passed to the command as an environment variable\n");
printf("--silent, -q surpress any output created by adhocify itself\n");
printf("--stdin, -s Read the paths which must be added to the watchlist from stdin. Each path must be in a seperate line\n");
printf("--no-forkbomb-check, -b Disable fork bomb detection\n");
printf("--ignore, -i Shell wildcard pattern (see glob(7)) to ignore events on files for which the pattern matches\n");
printf("--exit-with-child, -e Exit when the commands exits. You can also specify a return code (e. g. -e=1 to exit only on errors)\n"); printf("\nIf your command should know the file the event occured on, use the {} placeholder when you specify the arguments (like xargs)\n");
}
static struct option long_options[] =
{
{ "daemon", no_argument, 0, 'd' },
{ "logfile", required_argument, 0, 'o' },
{ "mask", required_argument, 0, 'm' },
{ "path", required_argument, 0, 'w' },
{ "no-env", no_argument, 0, 'a' },
{ "stdin", no_argument, 0, 's' },
{ "no-forkbomb-check", no_argument, 0, 'b' },
{ "ignore", required_argument, 0, 'i' },
{ "silent", no_argument, 0, 'q' },
{ "help", no_argument, 0, 'h' },
{ "exit-with-child", optional_argument, 0, 'e' }
};
//fills global n_script_arguments and script_arguments var
void fill_script_arguments(size_t n_args, char *args[])
{
n_script_arguments = n_args + 2; //2 = argv0 and terminating NULL
char **arguments = xmalloc( n_script_arguments * sizeof(char *) );
const char *argv0 = memrchr(prog, '/', strlen(prog));
argv0 = ( argv0 == NULL ) ? prog : argv0+1;
arguments[0] = argv0;
const int begin_offset = 1;
for(unsigned int i = 0; i < n_args; i++)
{
char *argument = args[i];
arguments[i+begin_offset] = strdup(argument);
}
arguments[n_args+begin_offset] = NULL;
script_arguments = arguments;
}
void parse_options(int argc, char **argv)
{
char *watchpath = NULL;
int option;
int option_index;
uint32_t optmask = 0;
while((option = getopt_long(argc, argv, "absdo:w:m:l:i:e::", long_options, &option_index)) != -1)
{
switch(option)
{
case 'd':
daemonize = true;
break;
case 'o':
path_logfile = optarg;
break;
case 'm':
optmask = nameToMask(optarg);
if(optmask == 0) {
logerror("Not supported inotify event: %s\n", optmask);
exit(EXIT_FAILURE);
}
mask |= optmask;
break;
case 'w':
watchpath = optarg;
watchqueue_add_path(watchpath);
break;
case 'a':
noenv=true;
break;
case 's':
fromstdin=true;
break;
case 'b':
forkbombcheck=false;
break;
case 'i':
add_to_ignore_list(optarg);
break;
case 'q':
silent=true;
break;
case 'h':
print_usage();
exit(EXIT_SUCCESS);
case 'e':
exit_with_child = true;
if(optarg)
{
if(*optarg == '!')
{
negate_child_exit_code = true;
++optarg;
}
if(*optarg == '\0')
{
logerror("Please specify the exit code\n");
exit(EXIT_FAILURE);
}
awaited_child_exit_code = atoi(optarg);
}
break;
}
}
if(optind == argc)
{
print_usage();
logerror("missing command path\n");
exit(EXIT_FAILURE);
}
prog = argv[optind++];
if(optind <= argc)
{
fill_script_arguments(argc - optind, &argv[optind]);
}
}
void process_options()
{
if(fromstdin)
{
queue_watches_from_stdin();
}
if(watchlist_head == NULL)
{
watchqueue_add_path(get_cwd());
}
if(mask == 0)
{
mask |= IN_CLOSE_WRITE;
}
if(! file_exists(prog))
{
fprintf(stderr, "File %s does not exist\n", prog);
exit(EXIT_FAILURE);
}
if(path_logfile)
{
path_logfile = xrealpath(path_logfile, NULL);
}
if(forkbombcheck)
{
char *path_prog = xrealpath(prog, NULL);
check_forkbomb(path_logfile, path_prog);
}
if(daemonize)
{
if(daemon(0,0) == -1)
{
perror("daemon");
exit(EXIT_FAILURE);
}
}
}
void start_monitoring(int ifd)
{
while(1)
{
int len;
int offset =0;
char buf[BUF_SIZE];
len = read(ifd, buf, BUF_SIZE);
if(len == -1)
{
if(errno == EINTR)
continue;
perror("read");
exit(EXIT_FAILURE);
}
while(offset < len)
{
struct inotify_event *event = (struct inotify_event *)&buf[offset];
handle_event(event);
offset+=sizeof(struct inotify_event) + event->len;
}
}
}
void child_handler(int signum, siginfo_t *info, void *context)
{
if(signum != SIGCHLD)
{
return;
}
int status;
pid_t p = waitpid(-1, &status, WNOHANG);
if(p == -1)
{
logerror("waitpid failed when handling child exit\n");
exit(EXIT_FAILURE);
}
if(exit_with_child)
{
int adhocify_exit_code = 0;
if(WIFEXITED(status))
{
adhocify_exit_code = WEXITSTATUS(status);
if(awaited_child_exit_code > -1)
{
bool must_exit = adhocify_exit_code == awaited_child_exit_code;
if(negate_child_exit_code)
{
must_exit = !must_exit;
}
if(must_exit)
{
logwrite("child exited with specified exit code, exiting too\n");
exit(adhocify_exit_code);
}
return; //not the exit code we wanted, keep running
}
}
if(WIFSIGNALED(status))
{
adhocify_exit_code = 128 + WTERMSIG(status); //copy bash's behaviour
}
//TODO: coredump?
exit(adhocify_exit_code);
}
}
void set_signals()
{
struct sigaction action;
action.sa_flags = SA_NOCLDSTOP | SA_SIGINFO;
action.sa_sigaction = &child_handler;
if(sigaction(SIGCHLD, &action, NULL) == -1)
{
logerror("Error when setting up the signal handler\n");
exit(EXIT_FAILURE);
}
}
int main(int argc, char **argv)
{
if(argc < 2)
{
print_usage();
exit(EXIT_FAILURE);
}
//signal(SIGCHLD, SIG_IGN);
set_signals();
parse_options(argc, argv);
process_options();
int ifd = inotify_init();
if(ifd == -1)
{
perror("inotify_init");
exit(EXIT_FAILURE);
}
create_watches(ifd, mask);
start_monitoring(ifd);
}