/** * 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 INOTIFY_BUF_LEN (512 * (sizeof(struct inotify_event) + 16)) /** * 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) 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 of the deamon. */ struct dir_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 == NULL. Otherwise its just the name of the * directory and parent points to the parent directory thats * also watched. */ char * dirname; /** * Points to the index of the parent. * -1 if no parent */ int parent; /** * On a delay to be handled. */ bool tackled; /** * Point in time when rsync should be called. */ clock_t alarm; /** * The applicable configuration for this directory */ struct dir_conf * dir_conf; }; /** * Structure to store strings for the diversve inotfy masked events. * Actually used for comfortable debugging only. */ struct inotify_mask_text { int mask; char const * text; }; /*--------------------------------------------------------------------------* * Global variables *--------------------------------------------------------------------------*/ /** * Global Option: The loglevel is how eloquent lsyncd will be. */ int loglevel = NORMAL; /** * Global Option: if true no action will actually be called. */ int flag_dryrun = 0; /** * Global Option: if true, do not detach and log to stdout/stderr. */ int flag_nodaemon = 0; /** * 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 = 0; /** * Global Option: if true, lsyncd will not perform the startup sync. */ int flag_nostartup = 0; /** * Global Option: pidfile, which holds the PID of the running daemon process. */ char * pidfile = NULL; #ifdef XML_CONFIG /** * Global Option: the filename to read config from. */ char * conf_filename = "/etc/lsyncd.conf.xml"; #endif /** * Global Option: this binary is used if no other specified in dir_conf. */ char * default_binary = "/usr/bin/rsync"; /** * Global Option: default exclude file */ char * default_exclude_file = NULL; /** * 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 }, }; /** * Global Option: default options to call the binary with. */ struct call_option * default_callopts = standard_callopts; /** * The configuratiton for dirs to synchronize */ struct dir_conf * dir_confs = NULL; /** * The number of configurated dirs to sync. */ int dir_conf_n = 0; /** * A stack of offset pointers to dir_watches to directories to sync. */ int *tosync = NULL; /** * Number of ints allocaetd for tosync stack */ int tosync_size = 0; /** * The pointer of the current tosync position. */ int tosync_pos = 0; /** * List of directories on a delay. */ int *tackles; /** * Size allocated of the tackle list. */ int tackle_size = 0; /** * Number of tackles. */ int tackle_num = 0; /** * 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 an allocated array of all directories watched. */ struct dir_watch *dir_watches; /** * The allocated size of dir_watches; */ int dir_watch_size = 0; /** * The largest dir_watch number used; */ int dir_watch_num = 0; /** * 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 = NULL; /** * The inotify instance. */ int inotf; /** * Seconds of delay between event and action */ clock_t delay = 5; /** * 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 char * exclude_dirs[MAX_EXCLUDES] = {NULL, }; int exclude_dir_n = 0; /*--------------------------------------------------------------------------* * Small generic helper routines. * (signal catching, memory fetching, message output) *--------------------------------------------------------------------------*/ /** * Set to 0 in signal handler, when lsyncd should TERMinate nicely. */ volatile sig_atomic_t keep_going = 1; /** * Called (out-of-order) when signals arrive */ void catch_alarm(int sig) { keep_going = 0; } /** * Just like exit, but logs the exit. * * Does not return! */ void terminate(int status) { if (!flag_nodaemon) { if (logfile) { FILE * flog; flog = fopen(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(int level, const char *fmt, ...) { va_list ap; char * ct; time_t mtime; FILE * flog1 = NULL, * flog2 = NULL; int sysp = 0; if (level < loglevel) { return; } if (!flag_nodaemon && logfile) { flog1 = fopen(logfile, "a"); if (flog1 == NULL) { fprintf(stderr, "cannot open logfile [%s]!\n", logfile); terminate(LSYNCD_FILENOTFOUND); } } time(&mtime); ct = ctime(&mtime); ct[strlen(ct) - 1] = 0; // cut trailing linefeed switch (level) { case DEBUG : sysp = LOG_DEBUG; if (flag_nodaemon) { flog2 = stdout; } break; case NORMAL : sysp = LOG_NOTICE; if (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(size_t size) { void *r = malloc(size); if (r == NULL) { printlogf(ERROR, "Out of memory!"); terminate(LSYNCD_OUTOFMEMORY); } return r; } /** * "secured" calloc. */ void *s_calloc(size_t nmemb, size_t size) { void *r = calloc(nmemb, size); if (r == NULL) { printlogf(ERROR, "Out of memory!"); terminate(LSYNCD_OUTOFMEMORY); } return r; } /** * "secured" realloc. */ void *s_realloc(void *ptr, size_t size) { void *r = realloc(ptr, size); if (r == NULL) { printlogf(ERROR, "Out of memory!"); terminate(LSYNCD_OUTOFMEMORY); } return r; } /** * "secured" strdup. */ char *s_strdup(const char* src) { char *s = strdup(src); if (s == NULL) { printlogf(ERROR, "Out of memory!"); terminate(LSYNCD_OUTOFMEMORY); } return s; } /** * Returns the canonicalized path of a directory with a final '/', * Makes sure it is a directory. */ char *realdir(const char * dir) { char* cs = s_malloc(PATH_MAX+1); 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)) { free(cs); return NULL; } strcat(cs, "/"); return cs; } /*--------------------------------------------------------------------------* * dir_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() { if (dir_conf_n > 0) { dir_conf_n++; dir_confs = s_realloc(dir_confs, dir_conf_n * sizeof(struct dir_conf)); memset(&dir_confs[dir_conf_n - 1], 0, sizeof(struct dir_conf)); // creates targets NULL terminator (no targets yet) dir_confs[dir_conf_n - 1].targets = s_calloc(1, sizeof(char *)); return &dir_confs[dir_conf_n - 1]; } dir_conf_n++; dir_confs = s_calloc(dir_conf_n, sizeof(struct dir_conf)); // creates targets NULL terminator (no targets yet) dir_confs[0].targets = s_calloc(1, sizeof(char *)); return dir_confs; } /** * Adds a target to a dir_conf. target string will duped. * * @param dir_conf dir_conf to add the target to. * @param target target to add. */ void dir_conf_add_target(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(dir_conf->targets, (target_n + 2) * sizeof(char *)); dir_conf->targets[target_n] = s_strdup(target); dir_conf->targets[target_n + 1] = NULL; } /*--------------------------------------------------------------------------* * Tackle list handling. *--------------------------------------------------------------------------*/ /** * Adds a directory on the tackle len (on a delay) * * @param watch the index in dir_watches to the directory. * @param alarm times() when the directory should be acted. */ bool append_tackle(int watch, clock_t alarm) { printlogf(DEBUG, "add tackle(%d)", watch); if (dir_watches[watch].tackled) { printlogf(DEBUG, "ignored since already tackled.", watch); return false; } dir_watches[watch].tackled = true; dir_watches[watch].alarm = alarm; if (tackle_num + 1 >= tackle_size) { tackle_size *= 2; tackles = s_realloc(tackles, tackle_size*sizeof(int)); } tackles[tackle_num++] = watch; return true; } /** * Removes the first directory on the tackle list. */ void remove_first_tackle() { int tw = tackles[0]; memmove(tackles, tackles + 1, (--tackle_num) * sizeof(int)); dir_watches[tw].tackled = false; } /** * Removes a tosync entry in the stack at the position p. */ //bool remove_tosync_pos(int p) { // int i; // assert(p < tosync_pos); // // //TODO improve performance by using memcpy. // for(i = p; i < tosync_pos; i++) { // tosync[i] = tosync[i + 1]; // } // tosync_pos--; // return true; //} /*--------------------------------------------------------------------------* * ToSync Stack handling. *--------------------------------------------------------------------------*/ /** * Adds a directory to sync. * * @param watch the index in dir_watches to the directory. */ bool append_tosync_watch(int watch) { int i; printlogf(DEBUG, "append_tosync_watch(%d)", watch); // look if its already in the tosync list. for(i = 0; i < tosync_pos; i++) { if (tosync[i] == watch) { return true; } } if (tosync_pos + 1 >= tosync_size) { tosync_size *= 2; tosync = s_realloc(tosync, tosync_size*sizeof(int)); } tosync[tosync_pos++] = watch; return true; } /** * 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(char *text, bool recursive) { char * str = s_strdup(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(ERROR, "don't know how to handle '\%' specifier in \"%s\"!", *text); terminate(LSYNCD_BADPARAMETERS); } } return str; } /** * Creates one string with all argument concated * @param argv the arguments * @param argc number of arguments */ char* get_arg_str(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(len + 2 * argc + 1); 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 the is 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(struct dir_conf * dir_conf, char const * 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; optp = dir_conf->callopts ? dir_conf->callopts : default_callopts; argv[argc++] = s_strdup(dir_conf->binary ? dir_conf->binary : default_binary); for(; optp->kind != CO_EOL; optp++) { switch (optp->kind) { case CO_TEXT : argv[argc++] = parse_option_text(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 && default_exclude_file == NULL) { continue; } argv[argc++] = s_strdup("--exclude-from"); argv[argc++] = s_strdup(dir_conf->exclude_file ? dir_conf->exclude_file : default_exclude_file); continue; case CO_SOURCE : argv[argc++] = s_strdup(src); continue; case CO_DEST : argv[argc++] = s_strdup(dest); continue; default: assert(false); } if (argc >= MAX_ARGS) { /* check for error condition */ printlogf(ERROR, "Internal error: too many (>%i) options passed", argc); return false; } } argv[argc++] = NULL; /* debug dump of command-line options */ //for (i=0; ibinary ? dir_conf->binary : default_binary; char * argall = get_arg_str(argv, argc); printlogf(NORMAL, "dry run: would call %s(%s)", binary, argall); free(argall); for (i = 0; i < argc; ++i) { if (argv[i]) { free(argv[i]); } } return true; } pid = fork(); if (pid == 0) { char * binary = dir_conf->binary ? dir_conf->binary : default_binary; if (!flag_nodaemon && logfile) { if (!freopen(logfile, "a", stdout)) { printlogf(ERROR, "cannot redirect stdout to [%s].", logfile); } if (!freopen(logfile, "a", stderr)) { printlogf(ERROR, "cannot redirect stderr to [%s].", logfile); } } execv(binary, argv); // in a sane world does not return! printlogf(ERROR, "Failed executing [%s]", binary); terminate(LSYNCD_INTERNALFAIL); } for (i = 0; i < argc; ++i) { if (argv[i]) { free(argv[i]); } } waitpid(pid, &status, 0); assert(WIFEXITED(status)); if (WEXITSTATUS(status) == LSYNCD_INTERNALFAIL){ printlogf(ERROR, "Fork exit code of %i, execv failure", WEXITSTATUS(status)); return false; } else if (WEXITSTATUS(status)) { printlogf(NORMAL, "Forked binary process returned non-zero return code: %i", WEXITSTATUS(status)); return false; } printlogf(DEBUG, "Rsync of [%s] -> [%s] finished", src, dest); return true; } /** * Adds a directory to watch. * * @param pathname the absolute path of the directory to watch. * @param dirname the name of the directory only (yes this is a bit redudant, but oh well) * @param parent if not -1 the index to the parent directory that is already watched * @param dir_conf the applicateable configuration * * @return index to dir_watches of the new dir, -1 on error. */ int add_watch(char const * pathname, char const * dirname, int parent, struct dir_conf * dir_conf) { int wd; int newdw; wd = inotify_add_watch(inotf, 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(ERROR, "Cannot add watch %s (%d:%s)", pathname, errno, strerror(errno)); return -1; } // look if an unused slot can be found. for (newdw = 0; newdw < dir_watch_num; newdw++) { if (dir_watches[newdw].wd < 0) { break; } } if (newdw == dir_watch_num) { if (dir_watch_num + 1 >= dir_watch_size) { dir_watch_size *= 2; dir_watches = s_realloc(dir_watches, dir_watch_size * sizeof(struct dir_watch)); } dir_watch_num++; } dir_watches[newdw].wd = wd; dir_watches[newdw].parent = parent; dir_watches[newdw].dirname = s_strdup(dirname); dir_watches[newdw].dir_conf = dir_conf; dir_watches[newdw].alarm = 0; // not needed, just to be clear dir_watches[newdw].tackled = false; return newdw; } /** * Writes the path of a watched directory into pathname. * * @param pathname path to write to * @param pathsize size of the pathname buffer * @param watch index of 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, int watch, char const * prefix) { int len = 0; if (watch == -1) { // When is this called this way??? char const * p = prefix ? prefix : ""; len = strlen(p); if (pathsize <= len) { return -1; } strcpy(pathname, p); } else if (dir_watches[watch].parent == -1) { // this is a watch root. char const * p = prefix ? prefix : dir_watches[watch].dirname; len = strlen(p); if (pathsize <= len) { return -1; } strcpy(pathname, p); } else { // this is some sub dir len = builddir(pathname, pathsize, dir_watches[watch].parent, prefix); /* recurse */ len += strlen(dir_watches[watch].dirname); if (pathsize <= len) { return -1; } strcat(pathname, dir_watches[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 beeing * watched from the dir_watches information. * * @param pathname destination buffer to store the result to. * @param pathsize max size of this buffer * @param watch the index in dir_watches to 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(char *pathname, int pathsize, int watch, const char *dirname, const char *prefix) { int len = builddir(pathname, pathsize, watch, prefix); if (len < 0) { printlogf(ERROR, "path too long!"); return false; } if (dirname) { if (pathsize < len + strlen(dirname) + 1) { printlogf(ERROR, "path too long!"); return false; } strcat(pathname, dirname); } printlogf(DEBUG, " BUILDPATH(%d, %s, %s) -> %s", watch, dirname, prefix, pathname); 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 index in dir_watches to the directory. * * @returns true when all targets were successful. */ bool rsync_dir(int watch) { char pathname[PATH_MAX+1]; char destname[PATH_MAX+1]; bool status = true; char ** target; if (!buildpath(pathname, sizeof(pathname), watch, NULL, NULL)) { return false; } for (target = dir_watches[watch].dir_conf->targets; *target; target++) { if (!buildpath(destname, sizeof(destname), watch, NULL, *target)) { status = false; continue; } printlogf(NORMAL, "rsyncing %s --> %s", pathname, destname); // call rsync to propagate changes in the directory if (!action(dir_watches[watch].dir_conf, pathname, destname, false)) { printlogf(ERROR, "Rsync from %s to %s failed", pathname, destname); status = false; } } return status; } /** * Puts a directory on the TO-DO list. Waiting for its delay * to be actually executed. * * Directly calls rsync_dir if delay == 0; * * @param watch the index in dir_watches to the directory. * @param alarm times() when the directory handling should be fired. */ void tackle_dir(int watch, clock_t alarm) { char pathname[PATH_MAX+1]; if (delay == 0) { rsync_dir(watch); return; } if (!buildpath(pathname, sizeof(pathname), watch, NULL, NULL)) { return; } if (append_tackle(watch, alarm)) { printlogf(NORMAL, "Putted %s on a delay", pathname); } else { printlogf(NORMAL, "Not acted on %s already on delay", pathname); } } /** * Adds a dir to watch. * * @param dirname The name or absolute path of the directory to watch. * @param parent If not -1, the index in dir_watches to the parent directory already watched. * Must have absolute path if parent == -1. * * @return the index in dir_watches off the directory or -1 on fail. * */ int add_dirwatch(char const * dirname, int parent, struct dir_conf * dir_conf) { DIR *d; struct dirent *de; int dw, i; char pathname[PATH_MAX+1]; printlogf(DEBUG, "add_dirwatch(%s, p->dirname:%s, ...)", dirname, parent >= 0 ? dir_watches[parent].dirname : "NULL"); if (!buildpath(pathname, sizeof(pathname), parent, dirname, NULL)) { return -1; } for (i = 0; i < exclude_dir_n; i++) { if (!strcmp(pathname, exclude_dirs[i])) { printlogf(NORMAL, "Excluding %s", pathname); return -1; } printlogf(DEBUG, "comparing %s with %s not an exclude so far.", pathname, exclude_dirs[i]); } dw = add_watch(pathname, dirname, parent, dir_conf); if (dw == -1) { return -1; } if (strlen(pathname) + strlen(dirname) + 2 > sizeof(pathname)) { printlogf(ERROR, "pathname too long %s//%s", pathname, dirname); return -1; } d = opendir(pathname); if (d == NULL) { printlogf(ERROR, "cannot open dir %s.", dirname); return -1; } while (keep_going) { // terminate early on KILL signal struct stat st; char subdir[PATH_MAX+1]; bool isdir; 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. isdir = buildpath(subdir, sizeof(subdir), dw, 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, ".")) { int ndw = add_dirwatch(de->d_name, dw, dir_conf); printlogf(NORMAL, "found new directory: %s in %s -- %s", de->d_name, dirname, ndw >= 0 ? "added on tosync stack" : "ignored it"); append_tosync_watch(ndw); } } closedir(d); return dw; } /** * Removes a watched dir, including recursevily all subdirs. * * @param name Optionally. If not NULL, the directory name * to remove which is a child of parent. * @param parent The index to the parent directory of the * directory 'name' to remove, or to be removed * itself if name == NULL. */ bool remove_dirwatch(const char * name, int parent) { int i; int dw; if (name) { // look for the child with the name for (i = 0; i < dir_watch_num; i++) { if (dir_watches[i].wd >= 0 && dir_watches[i].parent == parent && !strcmp(name, dir_watches[i].dirname) ) { dw = i; break; } } if (i >= dir_watch_num) { printlogf(ERROR, "Cannot find entry for %s:/:%s :-(", dir_watches[parent].dirname, name); return false; } } else { dw = parent; } for (i = 0; i < dir_watch_num; i++) { if (dir_watches[i].wd >= 0 && dir_watches[i].parent == dw) { remove_dirwatch(NULL, i); } } inotify_rm_watch(inotf, dir_watches[dw].wd); dir_watches[dw].wd = -1; free(dir_watches[dw].dirname); dir_watches[dw].dirname = NULL; // remove a possible tackle // (this dir is on the delay list) if (delay > 0 && dir_watches[dw].tackled) { for(i = 0; i < tackle_num; i++) { if (tackles[i] == dw) { // move the list up. memmove(tackles + i, tackles + i + 1, (tackle_num - i - 1) * sizeof(int)); tackle_num--; break; } } } return true; } /** * Find the matching dw entry from wd (watch descriptor), and return * the offset in the table. * * @param wd The wd (watch descriptor) given by inotify * @return offset, or -1 if not found */ int get_dirwatch_offset(int wd) { int i; for (i = 0; i < dir_watch_num; i++) { if (dir_watches[i].wd == wd) { break; } } if (i >= dir_watch_num) { return -1; } else { return i; } } /** * Processes through the tosync stack, rysncing all its directories. * * TODO: make special logic to determine who is a subdirectory of whom, and maybe optimizie calls. */ bool process_tosync_stack(clock_t alarm) { printlogf(DEBUG, "Processing through tosync stack."); while(tosync_pos > 0) { tackle_dir(tosync[--tosync_pos], alarm); } printlogf(DEBUG, "being done with tosync stack"); return true; } /** * Handles an inotify event. * * @param event The event to handle * @param alarm times() moment t */ bool handle_event(struct inotify_event *event, clock_t alarm) { char masktext[255] = {0,}; int mask = event->mask; int i, watch; int subwatch = -1; struct inotify_mask_text *p; // creates a string for logging that shows which flags // were raised in the event for (p = mask_texts; p->mask; p++) { if (mask & p->mask) { if (strlen(masktext) + strlen(p->text) + 3 >= sizeof(masktext)) { printlogf(ERROR, "bufferoverflow in handle_event"); return false; } if (*masktext) { strcat(masktext, ", "); } strcat(masktext, p->text); } } printlogf(DEBUG, "inotfy event: %s:%s", masktext, event->name); if (IN_IGNORED & event->mask) { return true; } // TODO, is this needed? for (i = 0; i < exclude_dir_n; i++) { if (!strcmp(event->name, exclude_dirs[i])) { return true; } } watch = get_dirwatch_offset(event->wd); if (watch == -1) { // this should not happen! printlogf(ERROR, "received an inotify event that doesnt match any watched directory :-(%d,%d)", event->mask, event->wd); return false; } // in case of a new directory create new watches if (((IN_CREATE | IN_MOVED_TO) & event->mask) && (IN_ISDIR & event->mask)) { subwatch = add_dirwatch(event->name, watch, dir_watches[watch].dir_conf); } // in case of a removed directory remove watches if (((IN_DELETE | IN_MOVED_FROM) & event->mask) && (IN_ISDIR & event->mask)) { remove_dirwatch(event->name, watch); } // call the binary if something changed if ((IN_ATTRIB | IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM) & event->mask ) { printlogf(NORMAL, "event %s:%s triggered.", masktext, event->name); tackle_dir(watch, alarm); if (subwatch >= 0) { // sync through the new created directory as well. tackle_dir(subwatch, alarm); } } else { printlogf(DEBUG, "... ignored this event."); } process_tosync_stack(alarm); return true; } /** * The control loop waiting for inotify events. */ bool master_loop() { char buf[INOTIFY_BUF_LEN]; int len, i = 0; long clocks_per_sec = sysconf(_SC_CLK_TCK); struct timeval tv; fd_set readfds; clock_t now; clock_t alarm; int ready; FD_ZERO(&readfds); FD_SET(inotf, &readfds); if (delay > 0) { if (clocks_per_sec <= 0) { printlogf(ERROR, "Clocks per seoond invalid! %d", printlogf); terminate(LSYNCD_INTERNALFAIL); } tackle_size = 2; tackles = s_calloc(tackle_size, sizeof(int)); tackle_num = 0; } while (keep_going) { if (delay > 0 && tackle_num > 0) { // use select() to determine what happens first // a new event or "alarm" of an event to actually // call its binary. alarm = dir_watches[tackles[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 > delay) { // security boundary in case of times() wrap around. tv.tv_sec = delay; tv.tv_usec = 0; } ready = select(inotf + 1, &readfds, NULL, NULL, &tv); } else { // if nothing to wait for, enter a blocking read // (sorry this variable is named a bit confusing // in this case) ready = 1; } if (ready) { len = read (inotf, buf, INOTIFY_BUF_LEN); } else { len = 0; } if (len < 0) { printlogf(ERROR, "failed to read from inotify (%d:%s)", errno, strerror(errno)); return false; } now = times(NULL); alarm = now + delay * clocks_per_sec; // first handle all events that might have happened i = 0; while (i < len) { struct inotify_event *event = (struct inotify_event *) &buf[i]; handle_event(event, alarm); i += sizeof(struct inotify_event) + event->len; } while (tackle_num > 0 && time_after(times(NULL), dir_watches[tackles[0]].alarm)) { printlogf(DEBUG, "time for %d arrived.", tackles[0]); rsync_dir(tackles[0]); remove_first_tackle(); } } return true; } /** * Utility function to check file exists. Print out error message and die. * * @param filename filename to check */ void check_file_exists(const char* filename) { struct stat st; if (-1==stat(filename, &st)) { printlogf(ERROR, "File [%s] does not exist\n", filename); terminate(LSYNCD_FILENOTFOUND); } } /** * Utility function to check given path is absolute path. * * @param filename filename to check */ void check_absolute_path(const char* filename) { if (filename[0] != '/') { printlogf(ERROR, "Filename [%s] is not an absolute path\n", filename); terminate(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", 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(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(ERROR, "error unknown call option type \"%s\"", cnode->name); terminate(LSYNCD_BADCONFIGFILE); } opt_n++; } opt_n++; asw = (struct call_option *) s_calloc(opt_n, sizeof(struct call_option)); // 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(ERROR, "error in config file: text attribute missing from