/** * lsyncd.c Live (Mirror) Syncing Demon * * License: GPLv2 (see COPYING) or any later version * * Authors: Axel Kittenberger * Eugene Sanivsky */ #include "config.h" #define _GNU_SOURCE #include #include #include #include #ifdef HAVE_SYS_INOTIFY_H # include #else # include "inotify-nosys.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef XML_CONFIG #include #include #endif /** * Define for debug memchecking. */ //#define MEMCHECK /** * Number of inotifies to read max. at once from the kernel. */ #define INOTIFY_BUF_LEN (64 * (sizeof(struct inotify_event) + 16)) /** * Initial size of vectors */ #define VECT_INIT_SIZE 2 /** * Defaults values */ #define DEFAULT_BINARY "/usr/bin/rsync" #define DEFAULT_CONF_FILENAME "/etc/lsyncd.conf.xml" /** * Macros to compare times() values * (borrowed from linux/jiffies.h) * * time_after(a,b) returns true if the time a is after time b. */ #define time_after(a,b) ((long)(b) - (long)(a) < 0) #define time_before(a,b) time_after(b,a) #define time_after_eq(a,b) ((long)(a) - (long)(b) >= 0) #define time_before_eq(a,b) time_after_eq(b,a) /** * Importance of log messages */ enum log_code { DEBUG = 1, NORMAL = 2, ERROR = 3, }; /** * Possible exit codes for this application */ enum lsyncd_exit_code { LSYNCD_SUCCESS = 0, /* out-of memory */ LSYNCD_OUTOFMEMORY = 1, /* file was not found, or failed to write */ LSYNCD_FILENOTFOUND = 2, /* execution somehow failed */ LSYNCD_EXECFAIL = 3, /* command-line arguments given to lsyncd are bad */ LSYNCD_BADPARAMETERS = 4, /* Too many excludes files were specified */ LSYNCD_TOOMANYDIRECTORYEXCLUDES = 5, /* something wrong with the config file */ LSYNCD_BADCONFIGFILE = 6, /* cannot open inotify instance */ LSYNCD_NOINOTIFY = 7, /* something internal went really wrong */ LSYNCD_INTERNALFAIL = 255, }; /** * An option paramater for the action call can either be: */ enum call_option_kind { CO_EOL, // end of list, CO_TEXT, // specified by text, CO_EXCLUDE, // be the exclude file CO_SOURCE, // be the source of the operation CO_DEST, // be the destination of the operation }; /*--------------------------------------------------------------------------* * Structure definitions *--------------------------------------------------------------------------*/ /** * An option parameter for the call. */ struct call_option { /** * The kind of this option. */ enum call_option_kind kind; /** * The text if its text. */ char *text; }; /** * A configurated directory to sync (including subdirectories) * * In case of beeing called with simple argument without a config file * there will be exactly one sync_directory. */ struct dir_conf { /** * Source dir to watch (no default value) */ char * source; /** * NULL terminated list of targets to rsync to (no default value). */ char ** targets; /** * binary to call (defaults to global default setting) */ char * binary; /** * the options to call the binary (defaults to global default setting) */ struct call_option * callopts; /** * the exclude-file to pass to rsync (defaults to global default setting) * TODO, Currently ignored! */ char * exclude_file; }; /** * Structure to store the directory watches. */ struct watch { /** * The watch descriptor returned by kernel. */ int wd; /** * The name of the directory. * In case of the root dir to be watched, it is a full path * and parent == -1. Otherwise its just the name of the * directory and parent points to the parent directory thats * also watched. */ char * dirname; /** * Points to the parent. NULL if no parent. */ struct watch *parent; /** * On a delay to be handled. */ bool delayed; /** * Point in time when rsync should be called. */ clock_t alarm; /** * The applicable configuration for this directory */ struct dir_conf *dir_conf; }; /** * Global options relevant for logging. * Part of struct global_options. */ struct log { /** * Global Option: The loglevel is how eloquent lsyncd will be. */ int loglevel; /** * Global Option: if true, do not detach and log to stdout/stderr. */ int flag_nodaemon; /** * If not NULL, this file will be accessed directly to write log messages to. * If NULL, syslog will be used. * * Not as difference that the output of child processes (rsync) will be redirected * to the logfile if specified, if syslogging the child-output will run into /dev/null. * * If flag_nodaemon is present stdout/stderr will be used. */ char * logfile; }; /** * Global variables */ struct global_options { /** * Options relevant for logging. */ struct log log; /** * Global Option: if true no action will actually be called. */ int flag_dryrun; /** * Global Option: if true, ignore rsync errors on startup. * (during normal operations they have to be ignored eitherway, * since rsync may also fail due e.g. the directory already * beeing deleted when lsyncd wants to sync it.) */ int flag_stubborn; /** * Global Option: if true, lsyncd will not perform the startup sync. */ int flag_nostartup; /** * Global Option: pidfile, which holds the PID of the running daemon process. */ char *pidfile; #ifdef XML_CONFIG /** * Global Option: the filename to read config from. */ char *conf_filename; #endif /** * Global Option: this binary is used if no other specified in dir_conf. */ char *default_binary; /** * Global Option: default exclude file */ char *default_exclude_file; /** * Global Option: default options to call the binary with. * * TODO copy on init. */ struct call_option *default_callopts; /** * Seconds of delay between event and action */ clock_t delay; /** * The configuratiton for dirs to synchronize */ struct dir_conf *dir_confs; /** * The number of configurated dirs to sync. */ int dir_conf_n; }; /** * Standard default options to call the binary with. */ struct call_option standard_callopts[] = { { CO_TEXT, "-lt%r" }, { CO_TEXT, "--delete" }, { CO_EXCLUDE, NULL }, { CO_SOURCE, NULL }, { CO_DEST, NULL }, { CO_EOL, NULL }, }; /** * Structure to store strings for the diversve inotfy masked events. * Used for comfortable log messages only. */ struct inotify_mask_text { int mask; char const * text; }; /** * A constant that assigns every inotify mask a printable string. * Used for debugging. */ struct inotify_mask_text mask_texts[] = { { IN_ACCESS, "ACCESS" }, { IN_ATTRIB, "ATTRIB" }, { IN_CLOSE_WRITE, "CLOSE_WRITE" }, { IN_CLOSE_NOWRITE, "CLOSE_NOWRITE" }, { IN_CREATE, "CREATE" }, { IN_DELETE, "DELETE" }, { IN_DELETE_SELF, "DELETE_SELF" }, { IN_IGNORED, "IGNORED" }, { IN_MODIFY, "MODIFY" }, { IN_MOVE_SELF, "MOVE_SELF" }, { IN_MOVED_FROM, "MOVED_FROM" }, { IN_MOVED_TO, "MOVED_TO" }, { IN_OPEN, "OPEN" }, { 0, "" }, }; /** * Holds all directories being watched. */ struct watch_vector { /** * TODO */ struct watch **data; size_t size; size_t len; }; /** * Holds all entries on delay. */ struct delay_vector { /** * TODO */ struct watch **data; size_t size; size_t len; }; /** * Array of strings of directory names to include. * This is limited to MAX_EXCLUDES. * It's not worth to code a dynamic size handling... */ #define MAX_EXCLUDES 256 struct exclude_vector { char * data[MAX_EXCLUDES]; size_t len; }; /*--------------------------------------------------------------------------* * MEMCHECK *--------------------------------------------------------------------------*/ /** * This routines keep track which memory allocs * have not been freed. Debugging purposes. */ #ifdef MEMCHECK #include int memc = 0; void * mroot = NULL; struct mentry { const void *data; const char *desc; }; int mcompare(const void *pa, const void *pb) { const struct mentry *ma = (const struct mentry *) pa; const struct mentry *mb = (const struct mentry *) pb; if (ma->data < mb->data) { return -1; } if (ma->data > mb->data) { return 1; } return 0; } void maction(const void *nodep, const VISIT which, const int depth) { if (which == leaf || which == postorder) { struct mentry * r = *((struct mentry **) nodep); memc--; fprintf(stderr, "<*> unfreed data %p:%s\n", r->data, r->desc); } } #endif #ifndef MEMCHECK #define s_free(x) free(x) #endif /*--------------------------------------------------------------------------* * Small generic helper routines. * (signal catching, memory fetching, message output) *--------------------------------------------------------------------------*/ /** * Set to 0 in signal handler, when lsyncd should end ASAP. * This can be either a TERM or a HUP signal. * In case of HUP killed is 0 and start over. */ volatile sig_atomic_t keep_going = 1; /** * Received a TERM signal, TERMinate nicely. */ volatile sig_atomic_t termed = 0; /** * Called (out-of-order) when signals arrive */ void catch_alarm(int sig) { switch(sig) { case SIGTERM : termed = 1; /* fall through */ case SIGHUP : keep_going = 0; } } /** * Just like exit, but logs the exit. * Does not return! */ void terminate(const struct log *log, int status) { if (log && !log->flag_nodaemon) { if (log->logfile) { FILE * flog; flog = fopen(log->logfile, "a"); if (flog) { fprintf(flog, "exit!"); fclose(flog); } } else { syslog(LOG_ERR, "exit!"); } } exit(status); } /** * Prints a message to either the log stream, preceding a timestamp or * forwards a message to syslogd. * * Otherwise it behaves like printf(); * * It will also always produce error messages on stderr. * So when startup fails, the message will be logged * _and_ displayed on screen. If lsyncd daemonized already, * stderr will be run into the void of /dev/null. */ void printlogf(const struct log *log, int level, const char *fmt, ...) { va_list ap; char * ct; time_t mtime; FILE * flog1 = NULL, * flog2 = NULL; int sysp = 0; if (log && level < log->loglevel) { return; } if (log && !log->flag_nodaemon && log->logfile) { flog1 = fopen(log->logfile, "a"); if (flog1 == NULL) { fprintf(stderr, "cannot open logfile [%s]!\n", log->logfile); terminate(log, LSYNCD_FILENOTFOUND); } } time(&mtime); ct = ctime(&mtime); ct[strlen(ct) - 1] = 0; // cut trailing linefeed switch (level) { case DEBUG : sysp = LOG_DEBUG; if (!log || log->flag_nodaemon) { flog2 = stdout; } break; case NORMAL : sysp = LOG_NOTICE; if (!log || log->flag_nodaemon) { flog2 = stdout; } break; case ERROR : sysp = LOG_ERR; // write on stderr even when daemon. flog2 = stderr; break; } // write time on fileoutput if (flog1) { fprintf(flog1, "%s: ", ct); } if (level == ERROR) { if (flog1) { fprintf(flog1, "ERROR: "); } if (flog2) { fprintf(flog2, "ERROR: "); } } if (flog1) { va_start(ap, fmt); vfprintf(flog1, fmt, ap); va_end(ap); } else { va_start(ap, fmt); vsyslog(sysp, fmt, ap); va_end(ap); } if (flog2) { va_start(ap, fmt); vfprintf(flog2, fmt, ap); va_end(ap); } if (flog1) { fprintf(flog1, "\n"); } if (flog2) { fprintf(flog2, "\n"); } if (flog1) { fclose(flog1); } } /** * "secured" malloc, meaning the deamon shall kill itself * in case of out of memory. * * On linux systems, which is actually the only system this * deamon will run at, due to the use of inotify, this is * an "academic" cleaness only, linux will never return out * memory, but kill a process to ensure memory will be * available. */ void * s_malloc(const struct log *log, size_t size, const char *desc) { void *r = malloc(size); if (r == NULL) { printlogf(log, ERROR, "Out of memory!"); terminate(log, LSYNCD_OUTOFMEMORY); } #ifdef MEMCHECK { struct mentry * mentry; memc++; mentry = malloc(sizeof(struct mentry)); mentry->data = r; mentry->desc = desc; if (!mentry) { printlogf(log, ERROR, "Out of memory in memcheck!"); terminate(log, LSYNCD_OUTOFMEMORY); } tsearch(mentry, &mroot, mcompare); } #endif return r; } /** * "secured" calloc. */ void * s_calloc(const struct log *log, size_t nmemb, size_t size, const char *desc) { void *r = calloc(nmemb, size); if (r == NULL) { printlogf(log, ERROR, "Out of memory!"); terminate(log, LSYNCD_OUTOFMEMORY); } #ifdef MEMCHECK { struct mentry * mentry; memc++; mentry = malloc(sizeof(struct mentry)); mentry->data = r; mentry->desc = desc; if (!mentry) { printlogf(log, ERROR, "Out of memory in memcheck!"); terminate(log, LSYNCD_OUTOFMEMORY); } tsearch(mentry, &mroot, mcompare); } #endif return r; } /** * "secured" realloc. */ void * s_realloc(const struct log *log, void *ptr, size_t size) { void *r = realloc(ptr, size); if (r == NULL) { printlogf(log, ERROR, "Out of memory!"); terminate(log, LSYNCD_OUTOFMEMORY); } #ifdef MEMCHECK { struct mentry * mentry = malloc(sizeof(struct mentry)); struct mentry **ret; if (!mentry) { printlogf(log, ERROR, "Out of memory in memcheck!"); terminate(log, LSYNCD_OUTOFMEMORY); } if (ptr == NULL) { fprintf(stderr, "<*> Reallocating NULL!?\n"); return r; } // first delete the old entry mentry->data = ptr; ret = tfind(mentry, &mroot, mcompare); if (ret == NULL) { fprintf(stderr, "<*> Memcheck error, reallocating unknown pointer %p!\n", ptr); return r; } mentry->desc = (*ret)->desc; tdelete(mentry, &mroot, mcompare); // and reenter the reallocated entry mentry->data = r; tsearch(mentry, &mroot, mcompare); } #endif return r; } /** * "secured" strdup. */ char * s_strdup(const struct log *log, const char *src, const char *desc) { char *s = strdup(src); if (s == NULL) { printlogf(log, ERROR, "Out of memory!"); terminate(log, LSYNCD_OUTOFMEMORY); } #ifdef MEMCHECK { struct mentry * mentry; memc++; mentry = malloc(sizeof(struct mentry)); mentry->data = s; mentry->desc = desc; if (!mentry) { printlogf(log, ERROR, "Out of memory in memcheck!"); terminate(log, LSYNCD_OUTOFMEMORY); } tsearch(mentry, &mroot, mcompare); } #endif return s; } #ifdef MEMCHECK void s_free(void *p) { struct mentry mentry = {0,}; struct mentry **r; memc--; if (p == NULL) { fprintf(stderr, "<*> Memcheck freeing NULL!\n"); return; } mentry.data = p; r = tdelete(&mentry, &mroot, mcompare); if (r == NULL) { fprintf(stderr, "<*> Memcheck error, freeing unknown pointer %p!\n", p); } free(p); } #endif /** * Returns the canonicalized path of a directory with a final '/'. * Makes sure it is a directory. */ char * realdir(const struct log *log, const char *dir) { char* cs = s_malloc(log, PATH_MAX+1, "realdir/cs"); cs = realpath(dir, cs); if (cs == NULL) { return NULL; } if (strlen(cs) + 1 >= PATH_MAX) { // at systems maxpath already, we cannot add a '/' anyway. return NULL; } struct stat st; stat(cs, &st); if (!S_ISDIR(st.st_mode)) { s_free(cs); return NULL; } strcat(cs, "/"); return cs; } /*--------------------------------------------------------------------------* * Options. *--------------------------------------------------------------------------*/ /** * Cleans up the memory used by a CO_EOL terminated array of call options * * @param call_options the array to free. */ void free_options(struct call_option *options) { struct call_option *co = options; while (co->kind != CO_EOL) { if (co->text) { s_free(co->text); } co++; } s_free(options); } /** * (Re)sets global options to default values. * * TODO memfree's */ void reset_options(struct global_options *opts) { opts->log.loglevel = NORMAL; opts->log.flag_nodaemon = 0; if (opts->log.logfile) { s_free(opts->log.logfile); opts->log.logfile = NULL; } opts->flag_dryrun = 0; opts->flag_stubborn = 0; opts->flag_nostartup = 0; if (opts->pidfile) { s_free(opts->pidfile); opts->pidfile = NULL; } #ifdef XML_CONFIG if (opts->conf_filename && opts->conf_filename != DEFAULT_CONF_FILENAME) { s_free(opts->conf_filename); } opts->conf_filename = DEFAULT_CONF_FILENAME; #endif if (opts->default_binary && opts->default_binary != DEFAULT_BINARY) { s_free(opts->default_binary); } opts->default_binary = DEFAULT_BINARY; if (opts->default_exclude_file) { s_free(opts->default_exclude_file); opts->default_exclude_file = NULL; } if (opts->default_callopts != standard_callopts) { if (opts->default_callopts) { free_options(opts->default_callopts); } opts->default_callopts = standard_callopts; } opts->delay = 5; if (opts->dir_confs) { int i; for(i = 0; i < opts->dir_conf_n; i++) { struct dir_conf *dc = opts->dir_confs + i; if (dc->source) { s_free(dc->source); } { char **t = dc->targets; while (*t) { s_free(*t); t++; } s_free(dc->targets); } if (dc->binary) { s_free(dc->binary); } if (dc->callopts) { free_options(dc->callopts); dc->callopts = NULL; } if (dc->exclude_file) { s_free(dc->exclude_file); } } s_free(opts->dir_confs); opts->dir_confs = NULL; } opts->dir_conf_n = 0; }; /*--------------------------------------------------------------------------* * Per directory configuration handling. *--------------------------------------------------------------------------*/ /** * (re)allocates space for a new dir_config and sets all values to 0/Null. * * (Yes we know, its a bit unoptimal, since when 6 dir_confs are given * in the config file, lsyncd will reallocate dir_confs 6 times. Well we * can live with that.) * * @return the pointer to the newly allocated dir_conf */ struct dir_conf * new_dir_conf(struct global_options *opts) { const struct log* log = &opts->log; if (opts->dir_conf_n > 0) { // enhance allocated space by 1. opts->dir_conf_n++; opts->dir_confs = s_realloc(log, opts->dir_confs, opts->dir_conf_n * sizeof(struct dir_conf)); memset(opts->dir_confs + opts->dir_conf_n - 1, 0, sizeof(struct dir_conf)); // creates targets NULL terminator (no targets yet) opts->dir_confs[opts->dir_conf_n - 1].targets = s_calloc(log, 1, sizeof(char *), "dir_conf"); return opts->dir_confs + opts->dir_conf_n - 1; } else { // create the memory. opts->dir_conf_n = 1; opts->dir_confs = s_calloc(log, opts->dir_conf_n, sizeof(struct dir_conf), "dir_confs"); // creates targets NULL terminator (no targets yet) opts->dir_confs[0].targets = s_calloc(log, 1, sizeof(char *), "dir_conf-target"); return opts->dir_confs; } } /** * Adds a target to a dir_conf.target. * *target string will duped. * * @param dir_conf dir_conf to add the target to. * @param target target to add. */ void dir_conf_add_target(const struct log *log, struct dir_conf *dir_conf, char *target) { char **t; int target_n = 0; // count current targets for (t = dir_conf->targets; *t; ++t) { target_n++; } dir_conf->targets = s_realloc(log, dir_conf->targets, (target_n + 2) * sizeof(char *)); dir_conf->targets[target_n] = s_strdup(log, target, "dupped target"); dir_conf->targets[target_n + 1] = NULL; } /*--------------------------------------------------------------------------* * Tackle list handling. *--------------------------------------------------------------------------*/ /** * Adds a directory on the delays list * * @param log logging information * @param delays the delays FIFO * @param watch the index in watches to the directory * @param alarm times() when the directory should be acted */ bool append_delay(const struct log *log, struct delay_vector *delays, struct watch *watch, clock_t alarm) { printlogf(log, DEBUG, "append_delay(%s, %d)", watch->dirname, alarm); if (watch->delayed) { printlogf(log, DEBUG, "ignored since already delayed."); return false; } watch->delayed = true; watch->alarm = alarm; // extend the delays vector if needed if (delays->len + 1 >= delays->size) { delays->size *= 2; delays->data = s_realloc(log, delays->data, delays->size * sizeof(struct watch *)); } delays->data[delays->len++] = watch; return true; } /** * Removes the first entry on the delay list. * * @param delays the delay FIFO. */ void remove_first_delay(struct delay_vector *delays) { delays->data[0]->delayed = false; delays->data[0]->alarm = 0; memmove(delays->data, delays->data + 1, (--delays->len) * sizeof(struct watch *)); } /*--------------------------------------------------------------------------* * ToSync Stack handling. *--------------------------------------------------------------------------*/ /** * Parses an option text, replacing all '%' specifiers with * elaborated stuff. duh, currently there is only one, so this * fuction is a bit overkill but oh well :-) * * @param text string to parse. * @param recursive info for replacements. * * @return a newly allocated string. */ char * parse_option_text(const struct log *log, char *text, bool recursive) { char * str = s_strdup(log, text, "dupped option text"); char * chr; // search result for %. // replace all '%' specifiers with there special meanings for(chr = strchr(str, '%'); chr; chr = strchr(str, '%')) { char *p; // chr points now to the '%' thing to be replaced switch (chr[1]) { case 'r' : // replace %r with 'r' when recursive or 'd' when not. chr[0] = recursive ? 'r' : 'd'; for(p = chr + 1; *p != 0; p++) { p[0] = p[1]; } break; case 0: // wtf, '%' was at the end of the string! default : // unknown char printlogf(log, ERROR, "don't know how to handle '\%' specifier in \"%s\"!", *text); terminate(log, LSYNCD_BADPARAMETERS); } } return str; } /** * Creates one string with all arguments concated. * * @param argv the arguments * @param argc number of arguments */ char * get_arg_str(const struct log *log, char **argv, int argc) { int i; int len = 0; char * str; // calc length for (i = 0; i < argc; i++) { len += strlen(argv[i]); } // alloc str = s_malloc(log, len + 2 * argc + 1, "argument string"); str[0] = 0; for(i = 0; i < argc; i++) { if (i > 0) { strcat(str, ", "); } strcat(str, argv[i]); } return str; } /** * Calls the specified action (most likely rsync) to sync from src to dest. * Returns after the forked process has finished. * * @param dir_conf the config applicatable for this dir. * @param src source string. * @param dest destination string, * @param recursive if true -r will be handled on, -d (single directory) otherwise * * @return true if successful, false if not. */ bool action(const struct global_options *opts, struct dir_conf *dir_conf, const char *src, const char *dest, bool recursive) { pid_t pid; int status; const int MAX_ARGS = 100; char * argv[MAX_ARGS]; int argc = 0; int i; struct call_option* optp; const struct log* log = &opts->log; optp = dir_conf->callopts ? dir_conf->callopts : opts->default_callopts; // makes a copy of all call parameters // step 1 binary itself argv[argc++] = s_strdup(log, dir_conf->binary ? dir_conf->binary : opts->default_binary, "argv"); // now all other parameters for(; optp->kind != CO_EOL; optp++) { switch (optp->kind) { case CO_TEXT : argv[argc++] = parse_option_text(log, optp->text, recursive); continue; case CO_EXCLUDE : // --exclude-from and the exclude file // insert only when the exclude file is present otherwise skip it. if (dir_conf->exclude_file == NULL && opts->default_exclude_file == NULL) { continue; } argv[argc++] = s_strdup(log, "--exclude-from", "argv exclude-from"); argv[argc++] = s_strdup(log, dir_conf->exclude_file ? dir_conf->exclude_file : opts->default_exclude_file, "argv exclude-file"); continue; case CO_SOURCE : argv[argc++] = s_strdup(log, src, "argv source"); continue; case CO_DEST : argv[argc++] = s_strdup(log, dest, "argv dest"); continue; default: printlogf(log, ERROR, "Internal error: unknown kind of option."); terminate(log, LSYNCD_INTERNALFAIL); } if (argc >= MAX_ARGS) { // check for error condition printlogf(log, ERROR, "Error: too many (>%i) options passed", argc); return false; } } argv[argc++] = NULL; if (opts->flag_dryrun) { // just make a nice log message char * binary = dir_conf->binary ? dir_conf->binary : opts->default_binary; char * argall = get_arg_str(log, argv, argc); printlogf(log, NORMAL, "dry run: would call %s(%s)", binary, argall); s_free(argall); for (i = 0; i < argc; ++i) { if (argv[i]) { s_free(argv[i]); } } return true; } pid = fork(); if (pid == 0) { char * binary = dir_conf->binary ? dir_conf->binary : opts->default_binary; if (!log->flag_nodaemon && log->logfile) { if (!freopen(log->logfile, "a", stdout)) { printlogf(log, ERROR, "cannot redirect stdout to [%s].", log->logfile); } if (!freopen(log->logfile, "a", stderr)) { printlogf(log, ERROR, "cannot redirect stderr to [%s].", log->logfile); } } execv(binary, argv); // in a sane world execv does not return! printlogf(log, ERROR, "Failed executing [%s]", binary); terminate(log, LSYNCD_INTERNALFAIL); } // free the memory from the arguments. for (i = 0; i < argc; ++i) { if (argv[i]) { s_free(argv[i]); } } waitpid(pid, &status, 0); assert(WIFEXITED(status)); if (WEXITSTATUS(status) == LSYNCD_INTERNALFAIL){ printlogf(log, ERROR, "Fork exit code of %i, execv failure", WEXITSTATUS(status)); return false; } else if (WEXITSTATUS(status)) { printlogf(log, NORMAL, "Forked binary process returned non-zero return code: %i", WEXITSTATUS(status)); return false; } printlogf(log, DEBUG, "Rsync of [%s] -> [%s] finished", src, dest); return true; } /** * Adds a directory to watch. * * @param log logging information * @param watches the vector of watches * @param inotify_fd inotify file descriptor * @param pathname the absolute path of the directory to watch * @param dirname the name of the directory only * @param parent if not -1 the index to the parent directory that is already watched * @param dir_conf the applicateable configuration * * @return the watches of the new dir, NULL on error */ struct watch * add_watch(const struct log *log, struct watch_vector *watches, int inotify_fd, char const *pathname, char const *dirname, struct watch *parent, struct dir_conf *dir_conf) { int wd; // kernels inotify descriptor int wi; // index to insert this watch into the watch vector struct watch *w; // the new watch wd = inotify_add_watch(inotify_fd, pathname, IN_ATTRIB | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM | IN_MOVED_TO | IN_DONT_FOLLOW | IN_ONLYDIR); if (wd == -1) { printlogf(log, ERROR, "Cannot add watch %s (%d:%s)", pathname, errno, strerror(errno)); return NULL; } // look if an unused slot can be found. // // lsyncd currently does not free unused slots, but marks // them as unused with wd < 0. for (wi = 0; wi < watches->len; wi++) { if (watches->data[wi]->wd < 0) { break; } } // there is no unused entry if (wi == watches->len) { // extend the vector if necessary if (watches->len + 1 >= watches->size) { watches->size *= 2; watches->data = s_realloc(log, watches->data, watches->size * sizeof(struct watch *)); } // allocate memory for a new watch watches->data[watches->len++] = s_calloc(log, 1, sizeof(struct watch), "watch"); } w = watches->data[wi]; w->wd = wd; w->parent = parent; w->dirname = s_strdup(log, dirname, "dirname"); w->dir_conf = dir_conf; w->alarm = 0; // not needed, just to be save w->delayed = false; return w; } /** * Writes the path of a watched directory into pathname. * * @param pathname path to write to * @param pathsize size of the pathname buffer * @param watch watched dir to build path for * @param prefix replace root dir with this (as target) * * @return -1 if pathname buffer was too small * contents of pathname will be garbled then. * strlen(pathname) if successful */ int builddir(char *pathname, int pathsize, struct watch *watch, char const *prefix) { int len = 0; if (!watch) { // TODO Is this ever called? char const * p = prefix ? prefix : ""; len = strlen(p); if (pathsize <= len) { return -1; } strcpy(pathname, p); } else if (!watch->parent) { // this is a watch root. char const * p = prefix ? prefix : watch->dirname; len = strlen(p); if (pathsize <= len) { return -1; } strcpy(pathname, p); } else { // this is some sub dir len = builddir(pathname, pathsize, watch->parent, prefix); /* recurse */ len += strlen(watch->dirname); if (pathsize <= len) { return -1; } strcat(pathname, watch->dirname); } // add the trailing slash if it is missing if (*pathname && pathname[strlen(pathname)-1] != '/') { strcat(pathname, "/"); len++; } return len; } /** * Builds the abolute path name of a given directory watch * * @param pathname destination buffer to store the result to. * @param pathsize max size of this buffer * @param watch the watches of the directory. * @param dirname if not NULL it is added at the end of pathname * @param prefix if not NULL it is added at the beginning of pathname */ bool buildpath(const struct log *log, char *pathname, int pathsize, struct watch *watch, const char *dirname, const char *prefix) { int len = builddir(pathname, pathsize, watch, prefix); if (len < 0) { printlogf(log, ERROR, "path too long!"); return false; } if (dirname) { if (pathsize < len + strlen(dirname) + 1) { printlogf(log, ERROR, "path too long!"); return false; } strcat(pathname, dirname); } return true; } /** * Syncs a directory. * TODO: make better error handling (differ between * directory gone away, and thus cannot work, or network * failed) * * @param watch the watch of the directory. * * @returns true when all targets were successful. */ bool rsync_dir(const struct global_options *opts, struct watch *watch) { char pathname[PATH_MAX+1]; char destname[PATH_MAX+1]; bool status = true; char ** target; const struct log *log = &opts->log; if (!buildpath(log, pathname, sizeof(pathname), watch, NULL, NULL)) { return false; } for (target = watch->dir_conf->targets; *target; target++) { if (!buildpath(log, destname, sizeof(destname), watch, NULL, *target)) { status = false; continue; } printlogf(log, NORMAL, "rsyncing %s --> %s", pathname, destname); // call rsync to propagate changes in the directory if (!action(opts, watch->dir_conf, pathname, destname, false)) { printlogf(log, ERROR, "Rsync from %s to %s failed", pathname, destname); status = false; } } return status; } /** * Puts a directory on the delay list OR * directly calls rsync_dir if delay == 0; * * @param delays the delay FIFO * @param delay if true will put it on the delay, if false act immediately * @param watch the watches of the delayed directory. * @param alarm times() when the directory handling should be fired. */ void delay_or_act_dir(const struct global_options *opts, struct delay_vector *delays, bool delay, struct watch *watch, clock_t alarm) { const struct log *log = &opts->log; char pathname[PATH_MAX+1]; if (!delay) { rsync_dir(opts, watch); } else { if (!buildpath(log, pathname, sizeof(pathname), watch, NULL, NULL)) { return; } if (append_delay(log, delays, watch, alarm)) { printlogf(log, NORMAL, "Putted %s on a delay", pathname); } else { printlogf(log, NORMAL, "Not acted on %s already on delay", pathname); } } } /** * Adds a directory including all subdirectories to watch. * Puts the directory with all subdirectories on the delay FIFO. * * @param opts global options * @param watches the watch vector * @param delays the delay vector * @param excludes the excludes vector * @param inotify_fd inotify file descriptor. * @param dirname The name or absolute path of the directory to watch. * @param parent If not NULL, the watches to the parent directory already watched. * Must have absolute path if parent == NULL. * @param dir_conf applicateable configuration * * @returns the watches of the directory or NULL on fail. */ struct watch * add_dirwatch(const struct global_options *opts, struct watch_vector *watches, struct delay_vector *delays, struct exclude_vector *excludes, int inotify_fd, char const *dirname, struct watch *parent, struct dir_conf *dir_conf) { const struct log *log = &opts->log; DIR *d; struct watch *w; char pathname[PATH_MAX+1]; printlogf(log, DEBUG, "add_dirwatch(%s, p->dirname:%s, ...)", dirname, parent ? parent->dirname : "NULL"); if (!buildpath(log, pathname, sizeof(pathname), parent, dirname, NULL)) { return NULL; } { int i; for (i = 0; i < excludes->len; i++) { if (!strcmp(pathname, excludes->data[i])) { printlogf(log, NORMAL, "Excluded %s", pathname); return NULL; } printlogf(log, DEBUG, "comparing %s with %s not an exclude so far.", pathname, excludes->data[i]); } } // watch this directory w = add_watch(log, watches, inotify_fd, pathname, dirname, parent, dir_conf); if (!w) { return NULL; } // put this directory on list to be synced ASAP. delay_or_act_dir(opts, delays, true, w, times(NULL)); if (strlen(pathname) + strlen(dirname) + 2 > sizeof(pathname)) { printlogf(log, ERROR, "pathname too long %s//%s", pathname, dirname); return NULL; } d = opendir(pathname); if (d == NULL) { printlogf(log, ERROR, "cannot open dir %s.", dirname); return NULL; } while (keep_going) { // terminate early on KILL signal bool isdir; struct dirent *de = readdir(d); if (de == NULL) { // finished reading the directory break; } // detemine if an entry is a directory or file if (de->d_type == DT_DIR) { isdir = true; } else if (de->d_type == DT_UNKNOWN) { // in case of reiserfs, d_type will be UNKNOWN, how evil! :-( // use traditional means to determine if its a directory. char subdir[PATH_MAX+1]; struct stat st; isdir = buildpath(log, subdir, sizeof(subdir), w, de->d_name, NULL) && !stat(subdir, &st) && S_ISDIR(st.st_mode); } else { isdir = false; } // add watches if its a directory and not . or .. if (isdir && strcmp(de->d_name, "..") && strcmp(de->d_name, ".")) { // recurse into subdirectories struct watch *nw = add_dirwatch(opts, watches, delays, excludes, inotify_fd, de->d_name, w, dir_conf); printlogf(log, NORMAL, "found new directory: %s in %s -- %s", de->d_name, dirname, nw ? "will be synced" : "ignored it"); } } closedir(d); return w; } /** * Removes a watched dir, including recursevily all subdirs. * * @param opts global options * @param watches the watch vector * @param delays the delay FIFO * @param inotify_fd inotify file descriptor * @param name optionally. If not NULL, the directory name * to remove which is a child of parent * @param parent the parent directory of the * directory 'name' to remove, or to be removed * itself if name == NULL. */ bool remove_dirwatch(const struct global_options *opts, struct watch_vector *watches, struct delay_vector *delays, int inotify_fd, const char * name, struct watch *parent) { const struct log *log = &opts->log; struct watch *w; // the watch to remove if (name) { int i; // look for the child with the name // TODO optimize by using subdir lists for (i = 0; i < watches->len; i++) { w = watches->data[i]; if (w->wd >= 0 && w->parent == parent && !strcmp(name, w->dirname)) { break; } } if (i >= watches->len) { printlogf(log, ERROR, "Cannot find entry for %s:/:%s :-(", parent->dirname, name); return false; } } else { w = parent; } { // recurse into all subdirectories removing them. // TODO possible optimization by keeping a list of subdirs int i; for (i = 0; i < watches->len; i++) { struct watch * iw = watches->data[i]; if (iw->wd >= 0 && iw->parent == w) { // recurse into the subdirectory remove_dirwatch(opts, watches, delays, inotify_fd, NULL, iw); } } } inotify_rm_watch(inotify_fd, w->wd); // mark this entry invalid w->wd = -1; s_free(w->dirname); w->dirname = NULL; // remove a possible delay // (this dir is on the to do/delay list) if (delays->len > 0 && w->delayed) { int i; for(i = 0; i < delays->len; i++) { if (delays->data[i] == w) { // move the list up. memmove(delays->data + i, delays->data + i + 1, (delays->len - i - 1) * sizeof(int)); delays->len--; break; } } } return true; } /** * Find the matching watch descriptor * * @param watches the watch vector * @param wd the wd (watch descriptor) given by inotify * * @return the watch or NULL if not found */ struct watch * get_watch(const struct watch_vector *watches, int wd) { int i; for (i = 0; i < watches->len; i++) { if (watches->data[i]->wd == wd) { return watches->data[i]; } } return NULL; } /** * Handles an inotify event. * * @param opts global options * @param watches the watch vector * @param delays the delay FIFO. * @param inotify_fd inotify file descriptor * @param event the event to handle * @param alarm times() moment when it should fire */ bool handle_event(const struct global_options *opts, struct watch_vector *watches, struct delay_vector *delays, struct exclude_vector *excludes, int inotify_fd, struct inotify_event *event, clock_t alarm) { const struct log *log = &opts->log; char masktext[255] = {0,}; struct watch *watch; { // creates a string for logging that shows which flags // were raised in the event struct inotify_mask_text *p; int mask = event->mask; for (p = mask_texts; p->mask; p++) { if (mask & p->mask) { if (strlen(masktext) + strlen(p->text) + 3 >= sizeof(masktext)) { printlogf(log, ERROR, "bufferoverflow in handle_event"); return false; } if (*masktext) { strcat(masktext, ", "); } strcat(masktext, p->text); } } printlogf(log, DEBUG, "inotfy event: %s:%s", masktext, event->name); } if (IN_IGNORED & event->mask) { return true; } { // TODO, is this needed? or will it be excluded already? int i; for (i = 0; i < excludes->len; i++) { if (!strcmp(event->name, excludes->data[i])) { return true; } } } watch = get_watch(watches, event->wd); if (!watch) { // this can happen in case of data moving faster than lsyncd can monitor it. printlogf(log, NORMAL, "received an inotify event that doesnt match any watched directory."); return false; } // put the watch on the delay or act if delay == 0 if ((IN_ATTRIB | IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM) & event->mask ) { printlogf(log, NORMAL, "received event %s:%s for %d.", masktext, event->name, alarm); delay_or_act_dir(opts, delays, opts->delay > 0, watch, alarm); } else { printlogf(log, DEBUG, "... ignored this event."); } // in case of a new directory create new watches of the subdir if (((IN_CREATE | IN_MOVED_TO) & event->mask) && (IN_ISDIR & event->mask)) { add_dirwatch(opts, watches, delays, excludes, inotify_fd, event->name, watch, watch->dir_conf); } // in case of a removed directory remove watches from the subdir if (((IN_DELETE | IN_MOVED_FROM) & event->mask) && (IN_ISDIR & event->mask)) { remove_dirwatch(opts, watches, delays, inotify_fd, event->name, watch); } return true; } /** * The control loop waiting for inotify events. * * @param opts global options * @param watches the watch vector * @param delays the delay vector * @param excludes the exclude vector * @param inotify_fd inotify file descriptor */ bool master_loop(const struct global_options *opts, struct watch_vector *watches, struct delay_vector *delays, struct exclude_vector *excludes, int inotify_fd) { char buf[INOTIFY_BUF_LEN]; int len; long clocks_per_sec = sysconf(_SC_CLK_TCK); struct timeval tv; fd_set readfds; clock_t now; clock_t alarm; const struct log *log = &opts->log; FD_ZERO(&readfds); FD_SET(inotify_fd, &readfds); if (opts->delay > 0) { if (clocks_per_sec <= 0) { printlogf(log, ERROR, "Clocks per seoond invalid! %d", printlogf); terminate(log, LSYNCD_INTERNALFAIL); } } while (keep_going) { int do_read; if (delays->len > 0 && time_after_eq(times(NULL), delays->data[0]->alarm)) { // there is a delay that wants to be handled already // do not read from inotify_fd and jump directly to delay handling printlogf(log, DEBUG, "immediately handling delayed entries"); do_read = 0; } else if (opts->delay > 0 && delays->len > 0) { // use select() to determine what happens first // a new event or "alarm" of an event to actually // call its binary. The delay with the index 0 // should have the nearest alarm time. alarm = delays->data[0]->alarm; now = times(NULL); tv.tv_sec = (alarm - now) / clocks_per_sec; tv.tv_usec = (alarm - now) * 1000000 / clocks_per_sec % 1000000; if (tv.tv_sec > opts->delay) { // security boundary in case of times() wrap around. tv.tv_sec = opts->delay; tv.tv_usec = 0; } // if select returns a positive number there is data on inotify // on zero the timemout occured. do_read = select(inotify_fd + 1, &readfds, NULL, NULL, &tv); if (do_read) { printlogf(log, DEBUG, "theres data on inotify."); } else { printlogf(log, DEBUG, "select() timeouted, doiong delays."); } } else { // if nothing to wait for, enter a blocking read printlogf(log, DEBUG, "gone blocking"); do_read = 1; } if (do_read) { len = read (inotify_fd, buf, INOTIFY_BUF_LEN); } else { len = 0; } if (len < 0) { if (!keep_going) { printlogf(log, NORMAL, "read exited due to TERM signal."); } else { printlogf(log, ERROR, "failed to read from inotify (%d:%s)", errno, strerror(errno)); } return false; } now = times(NULL); alarm = now + opts->delay * clocks_per_sec; { // first handle all events read. int i = 0; while (i < len) { struct inotify_event *event = (struct inotify_event *) &buf[i]; handle_event(opts, watches, delays, excludes, inotify_fd, event, alarm); i += sizeof(struct inotify_event) + event->len; } } // Then take of directories from the top of the delay FIFO // until one item is found whose expiry time has not yet come // or the stack is empty. Using now time - times(NULL) - everytime // again as time may progresses while handling delayed entries. while (delays->len > 0 && time_after_eq(times(NULL), delays->data[0]->alarm)) { printlogf(log, DEBUG, "time %d for '%s' arrived. now=%d", delays->data[0]->alarm, delays->data[0]->dirname, times(NULL)); rsync_dir(opts, delays->data[0]); remove_first_delay(delays); } } return true; } /** * Utility function to check file exists. * Prints out error message and die. * * @param filename filename to check */ void check_file_exists(const struct log* log, const char* filename, const char *errmsg) { struct stat st; if (-1==stat(filename, &st)) { printlogf(log, ERROR, "%s [%s] does not exist.\n", filename); terminate(log, LSYNCD_FILENOTFOUND); } } /** * Utility function to check given path is absolute path. * * @param filename Filename to check * @param errmsg Filetype text to prepend to the error message. */ void check_absolute_path(const struct log* log, const char* filename, const char *filetype) { if (filename[0] != '/') { printlogf(log, ERROR, "%s [%s] has do be an absolute path.\n", filetype, filename); terminate(log, LSYNCD_FILENOTFOUND); } } /** * Prints the help text and exits 0. * * @param arg0 argv[0] to show what lsyncd was called with. */ void print_help(char *arg0) { printf("\n"); #ifdef XML_CONFIG printf("USAGE: %s [OPTION]... [SOURCE] [TARGET 1] [TARGET 2] ...\n", arg0); #else printf("USAGE: %s [OPTION]... SOURCE TARGET-1 TARGET-2 ...\n", arg0); #endif printf("\n"); printf("SOURCE: a directory to watch and rsync.\n"); printf("\n"); printf("TARGET: can be any name accepted by rsync. e.g. \"foohost::barmodule/\"\n"); printf("\n"); #ifdef XML_CONFIG printf("When called without SOURCE and TARGET, the\n"); printf("configuration will be read from the config file\"\n"); #endif printf("\n"); printf("OPTIONS:\n"); printf(" --binary FILE Call this binary to sync " "(DEFAULT: %s)\n", DEFAULT_BINARY); #ifdef XML_CONFIG printf(" --conf FILE Load configuration from this file\n"); printf(" (DEFAULT: %s if called without SOURCE/TARGET)\n", DEFAULT_CONF_FILENAME); #endif printf(" --debug Log debug messages\n"); printf(" --delay SECS Delay between event and action\n"); printf(" --dryrun Do not call any actions, run dry only\n"); printf(" --exclude-from FILE Exclude file handled to rsync (DEFAULT: None)\n"); printf(" --help Print this help text and exit.\n"); printf(" --logfile FILE Put log here (DEFAULT: uses syslog if not specified)\n"); printf(" --no-daemon Do not detach, log to stdout/stderr\n"); printf(" --no-startup Do not execute a startup sync (disadviced, know what you doing)\n"); printf(" --pidfile FILE Create a file containing pid of the daemon\n"); printf(" --scarce Only log errors\n"); printf(" --stubborn Ignore rsync errors on startup.\n"); printf(" --version Print version an exit.\n"); printf("\n"); printf("EXCLUDE FILE: \n"); printf(" The exclude file may have either filebased general masks like \"*.php\" without directory specifications,\n"); printf(" or exclude complete directories like \"Data/\". lsyncd will recognize directory excludes by the trailing '/'\n"); printf(" and will not add watches of directories of exactly such name including sub-directories of them.\n"); printf(" Please do not try to use more sophisticated exclude masks like \"Data/*.dat\" or \"Da*a/\", \"Data/Volatile/\" etc.\n"); printf(" This will not work like you would expect it to.\n"); printf("\n"); printf("LICENSE\n"); printf(" GPLv2 or any later version. See COPYING\n"); printf("\n"); #ifndef XML_CONFIG printf("(this lsyncd binary was not compiled to be able to read config files)\n"); #endif exit(0); } #ifdef XML_CONFIG /*--------------------------------------------------------------------------* * Config file parsing *--------------------------------------------------------------------------*/ /** * Parses * * @return the allocated and filled calloptions structure */ struct call_option * parse_callopts(struct global_options *opts, xmlNodePtr node) { xmlNodePtr cnode; xmlChar *xc; int opt_n = 0; struct call_option * asw; // count how many options are there for (cnode = node->children; cnode; cnode = cnode->next) { if (cnode->type != XML_ELEMENT_NODE) { continue; } if (xmlStrcmp(cnode->name, BAD_CAST "option") && xmlStrcmp(cnode->name, BAD_CAST "exclude-file") && xmlStrcmp(cnode->name, BAD_CAST "source") && xmlStrcmp(cnode->name, BAD_CAST "destination") ) { printlogf(NULL, ERROR, "error unknown call option type \"%s\"", cnode->name); terminate(NULL, LSYNCD_BADCONFIGFILE); } opt_n++; } opt_n++; asw = (struct call_option *) s_calloc(NULL, opt_n, sizeof(struct call_option), "call options"); // fill in the answer opt_n = 0; for (cnode = node->children; cnode; cnode = cnode->next) { if (cnode->type != XML_ELEMENT_NODE) { continue; } asw[opt_n].text = NULL; if (!xmlStrcmp(cnode->name, BAD_CAST "option")) { xc = xmlGetProp(cnode, BAD_CAST "text"); if (xc == NULL) { printlogf(NULL, ERROR, "error in config file: text attribute missing from