From: Andre Noll Date: Tue, 11 Mar 2008 20:00:54 +0000 (+0100) Subject: Initial git checkin. X-Git-Tag: v0.0.2~12 X-Git-Url: http://git.tuebingen.mpg.de/?p=dss.git;a=commitdiff_plain;h=c418d2188c9c2c542270023d6fc3bc6cf34f8d29;ds=sidebyside Initial git checkin. Dyadic intervals are so harmonic. Do you feel it? --- c418d2188c9c2c542270023d6fc3bc6cf34f8d29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c9e16ac --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +dss_objects := dss.o cmdline.o string.o fd.o exec.o signal.o daemon.o +all: dss +man: dss.1 + +DEBUG_CPPFLAGS += -Wno-sign-compare -g -Wunused -Wundef -W +DEBUG_CPPFLAGS += -Wredundant-decls +CPPFLAGS += -Os +CPPFLAGS += -Wall +CPPFLAGS += -Wuninitialized +CPPFLAGS += -Wchar-subscripts +CPPFLAGS += -Wformat-security +CPPFLAGS += -Werror-implicit-function-declaration +CPPFLAGS += -Wmissing-format-attribute +CPPFLAGS += -Wunused-macros +CPPFLAGS += -Wbad-function-cast + +Makefile.deps: $(wildcard *.c *.h) + gcc -MM -MG *.c > $@ + +include Makefile.deps + +dss: $(dss_objects) + $(CC) $(CPPFLAGS) $(DEBUG_CPPFLAGS) -o $@ $(dss_objects) + +%.o: %.c + $(CC) -c $(CPPFLAGS) $(DEBUG_CPPFLAGS) $< + +%.ppm: %.sk + sk2ppm $< > $@ +%.png: %.ppm + convert $< $@ + +cmdline.c cmdline.h: dss.ggo + gengetopt --unamed-opts=command --conf-parser < $< + +dss.1: dss + help2man -N ./$< > $@ + +%.1.html: %.1 + man2html $< > $@ + +clean: + rm -f *.o dss dss.1 dss.1.html Makefile.deps *.ppm *.png *~ cmdline.c cmdline.h + + diff --git a/README b/README new file mode 100644 index 0000000..63b3b9c --- /dev/null +++ b/README @@ -0,0 +1,19 @@ +dss creates hardlink-based snapshots of a given directory on a remote +or local host using rsync's link-dest feature. + +dss is admin friendly: It is easy to configure and needs little +attention once the dss daemon is running because dss keeps track of +the available disk space and removes snapshots if disk space becomes +sparse or snapshots become older than the specified time. + +It's also user-friendly because users can browse the snapshot +directories and see the contents of the file system at the time the +snapshot was taken. For example, users can simply copy data from the +snapshot directory back to the live system in case they deleted files +by accident. + +Snapshot pruning takes place in a dyadic fashion: Many recent snapshots +are available, but the number of snapshots per time interval decreases +exponentially. For example, one can configure dss so that it keeps +16 snapshots not older than one week, 8 snapshots between one and +two weeks old, 4 snapshots between two and three weeks old and so on. diff --git a/daemon.c b/daemon.c new file mode 100644 index 0000000..ba890fb --- /dev/null +++ b/daemon.c @@ -0,0 +1,115 @@ +/* + * Copyright (C) 1997-2008 Andre Noll + * + * Licensed under the GPL v2. For licencing details see COPYING. + */ + +/** \file daemon.c Some helpers for programs that detach from the console. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gcc-compat.h" +#include "error.h" +#include "log.h" +#include "string.h" + +/** + * Do the usual stuff to become a daemon. + * + * Fork, become session leader, dup fd 0, 1, 2 to /dev/null. + * + * \sa fork(2), setsid(2), dup(2). + */ +void daemon_init(void) +{ + pid_t pid; + int null; + + DSS_INFO_LOG("daemonizing\n"); + pid = fork(); + if (pid < 0) + goto err; + if (pid) + exit(EXIT_SUCCESS); /* parent exits */ + /* become session leader */ + if (setsid() < 0) + goto err; + if (chdir("/") < 0) + goto err; + umask(0); + null = open("/dev/null", O_RDONLY); + if (null < 0) + goto err; + if (dup2(null, STDIN_FILENO) < 0) + goto err; + if (dup2(null, STDOUT_FILENO) < 0) + goto err; + if (dup2(null, STDERR_FILENO) < 0) + goto err; + close(null); + return; +err: + DSS_EMERG_LOG("fatal: %s\n", strerror(errno)); + exit(EXIT_FAILURE); +} + +/** + * fopen() the given file in append mode. + * + * \param logfile_name The name of the file to open. + * + * \return Either calls exit() or returns a valid file handle. + */ +FILE *open_log(const char *logfile_name) +{ + FILE *logfile; + + assert(logfile_name); + logfile = fopen(logfile_name, "a"); + if (!logfile) { + DSS_EMERG_LOG("can not open %s: %s\n", logfile_name, + strerror(errno)); + exit(EXIT_FAILURE); + } + setlinebuf(logfile); + return logfile; +} + +/** + * Close the log file of the daemon. + * + * \param logfile The log file handle. + * + * It's OK to call this with logfile == \p NULL. + */ +void close_log(FILE* logfile) +{ + if (!logfile) + return; + DSS_INFO_LOG("closing logfile\n"); + fclose(logfile); +} + +/** + * Log the startup message. + */ +void log_welcome(int loglevel) +{ + DSS_INFO_LOG("***** welcome to dss ******\n"); + DSS_DEBUG_LOG("using loglevel %d\n", loglevel); +} + +int com_daemon(int argc, char * const * argv) +{ + return 1; +} diff --git a/dss.c b/dss.c new file mode 100644 index 0000000..0913301 --- /dev/null +++ b/dss.c @@ -0,0 +1,720 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "gcc-compat.h" +#include "cmdline.h" +#include "log.h" +#include "string.h" +#include "error.h" +#include "fd.h" +#include "exec.h" + + +struct gengetopt_args_info conf; +char *dss_error_txt = NULL; + +DEFINE_DSS_ERRLIST; + +/** Defines one dss command. */ +struct server_command { + /** The name of the command. */ + const char *name; + /** Pointer to the function that handles the command. */ + int (*handler)(int, char * const * const); +}; + +/* a litte cpp magic helps to DRY */ +#define SERVER_COMMANDS \ + SERVER_COMMAND(ls) \ + SERVER_COMMAND(create) \ + SERVER_COMMAND(prune) \ + SERVER_COMMAND(daemon) +#define SERVER_COMMAND(x) int com_ ##x(int, char * const * const); +SERVER_COMMANDS +#undef SERVER_COMMAND +#define SERVER_COMMAND(x) {.name = #x, .handler = com_ ## x}, +static struct server_command command_list[] = { + SERVER_COMMANDS + {.name = NULL, .handler = NULL} +}; +#undef SERVER_COMMAND + +/* + * complete, not being deleted: 1204565370-1204565371.Sun_Mar_02_2008_14_33-Sun_Mar_02_2008_14_43 + * complete, being deleted: 1204565370-1204565371.being_deleted + * incomplete, not being deleted: 1204565370-incomplete + * incomplete, being deleted: 1204565370-incomplete.being_deleted + */ +enum snapshot_status_flags { + SS_COMPLETE = 1, + SS_BEING_DELETED = 2, +}; + +struct snapshot { + char *name; + int64_t creation_time; + int64_t completion_time; + enum snapshot_status_flags flags; + unsigned interval; +}; + +int is_snapshot(const char *dirname, int64_t now, struct snapshot *s) +{ + int i, ret; + char *dash, *dot, *tmp; + int64_t num; + + assert(dirname); + dash = strchr(dirname, '-'); + if (!dash || !dash[1] || dash == dirname) + return 0; + for (i = 0; dirname[i] != '-'; i++) + if (!isdigit(dirname[i])) + return 0; + tmp = dss_strdup(dirname); + tmp[i] = '\0'; + ret = dss_atoi64(tmp, &num); + free(tmp); + if (ret < 0) { + free(dss_error_txt); + return 0; + } + assert(num >= 0); + if (num > now) + return 0; + s->creation_time = num; + //DSS_DEBUG_LOG("%s start time: %lli\n", dirname, (long long)s->creation_time); + s->interval = (long long) ((now - s->creation_time) + / conf.unit_interval_arg / 24 / 3600); + if (!strcmp(dash + 1, "incomplete")) { + s->completion_time = -1; + s->flags = 0; /* neither complete, nor being deleted */ + goto success; + } + if (!strcmp(dash + 1, "incomplete.being_deleted")) { + s->completion_time = -1; + s->flags = SS_BEING_DELETED; /* mot cpmplete, being deleted */ + goto success; + } + tmp = dash + 1; + dot = strchr(tmp, '.'); + if (!dot || !dot[1] || dot == tmp) + return 0; + for (i = 0; tmp[i] != '.'; i++) + if (!isdigit(tmp[i])) + return 0; + tmp = dss_strdup(dash + 1); + tmp[i] = '\0'; + ret = dss_atoi64(tmp, &num); + free(tmp); + if (ret < 0) { + free(dss_error_txt); + return 0; + } + if (num > now) + return 0; + s->completion_time = num; + s->flags = SS_COMPLETE; + if (strcmp(dot + 1, "being_deleted")) + s->flags |= SS_BEING_DELETED; +success: + s->name = dss_strdup(dirname); + return 1; +} + +int64_t get_current_time(void) +{ + time_t now; + time(&now); + DSS_DEBUG_LOG("now: %lli\n", (long long) now); + return (int64_t)now; +} + +char *incomplete_name(int64_t start) +{ + return make_message("%lli-incomplete", (long long)start); +} + +char *being_deleted_name(struct snapshot *s) +{ + if (s->flags & SS_COMPLETE) + return make_message("%lli-%lli.being_deleted", + (long long)s->creation_time, + (long long)s->completion_time); + return make_message("%lli-incomplete.being_deleted", + (long long)s->creation_time); +} + +int complete_name(int64_t start, int64_t end, char **result) +{ + struct tm start_tm, end_tm; + time_t *start_seconds = (time_t *) (uint64_t *)&start; /* STFU, gcc */ + time_t *end_seconds = (time_t *) (uint64_t *)&end; /* STFU, gcc */ + char start_str[200], end_str[200]; + + if (!localtime_r(start_seconds, &start_tm)) { + make_err_msg("%lli", (long long)start); + return -E_LOCALTIME; + } + if (!localtime_r(end_seconds, &end_tm)) { + make_err_msg("%lli", (long long)end); + return -E_LOCALTIME; + } + if (!strftime(start_str, sizeof(start_str), "%a_%b_%d_%Y_%H_%M_%S", &start_tm)) { + make_err_msg("%lli", (long long)start); + return -E_STRFTIME; + } + if (!strftime(end_str, sizeof(end_str), "%a_%b_%d_%Y_%H_%M_%S", &end_tm)) { + make_err_msg("%lli", (long long)end); + return -E_STRFTIME; + } + *result = make_message("%lli-%lli.%s-%s", (long long) start, (long long) end, + start_str, end_str); + return 1; +} + +struct snapshot_list { + int64_t now; + unsigned num_snapshots; + unsigned array_size; + struct snapshot **snapshots; + /** + * Array of size num_intervals + 1 + * + * It contains the number of snapshots in each interval. interval_count[num_intervals] + * is the number of snapshots which belong to any interval greater than num_intervals. + */ + unsigned *interval_count; +}; + +#define FOR_EACH_SNAPSHOT(s, i, sl) \ + for ((i) = 0; (i) < (sl)->num_snapshots && ((s) = (sl)->snapshots[(i)]); (i)++) + + + +#define NUM_COMPARE(x, y) ((int)((x) < (y)) - (int)((x) > (y))) + +static int compare_snapshots(const void *a, const void *b) +{ + struct snapshot *s1 = *(struct snapshot **)a; + struct snapshot *s2 = *(struct snapshot **)b; + return NUM_COMPARE(s2->creation_time, s1->creation_time); +} + +/** Compute the minimum of \a a and \a b. */ +#define DSS_MIN(a,b) ((a) < (b) ? (a) : (b)) + +int add_snapshot(const char *dirname, void *private) +{ + struct snapshot_list *sl = private; + struct snapshot s; + int ret = is_snapshot(dirname, sl->now, &s); + + if (!ret) + return 1; + if (sl->num_snapshots >= sl->array_size) { + sl->array_size = 2 * sl->array_size + 1; + sl->snapshots = dss_realloc(sl->snapshots, + sl->array_size * sizeof(struct snapshot *)); + } + sl->snapshots[sl->num_snapshots] = dss_malloc(sizeof(struct snapshot)); + *(sl->snapshots[sl->num_snapshots]) = s; + sl->interval_count[DSS_MIN(s.interval, conf.num_intervals_arg)]++; + sl->num_snapshots++; + return 1; +} + +void get_snapshot_list(struct snapshot_list *sl) +{ + sl->now = get_current_time(); + sl->num_snapshots = 0; + sl->array_size = 0; + sl->snapshots = NULL; + sl->interval_count = dss_calloc((conf.num_intervals_arg + 1) * sizeof(unsigned)); + for_each_subdir(add_snapshot, sl); + qsort(sl->snapshots, sl->num_snapshots, sizeof(struct snapshot *), + compare_snapshots); +} + +void free_snapshot_list(struct snapshot_list *sl) +{ + int i; + struct snapshot *s; + + FOR_EACH_SNAPSHOT(s, i, sl) { + free(s->name); + free(s); + } + free(sl->interval_count); + free(sl->snapshots); +} + +/** + * Print a log message about the exit status of a child. + */ +void log_termination_msg(pid_t pid, int status) +{ + if (WIFEXITED(status)) + DSS_INFO_LOG("child %i exited. Exit status: %i\n", (int)pid, + WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + DSS_NOTICE_LOG("child %i was killed by signal %i\n", (int)pid, + WTERMSIG(status)); + else + DSS_WARNING_LOG("child %i terminated abormally\n", (int)pid); +} + +int wait_for_process(pid_t pid, int *status) +{ + int ret; + + DSS_DEBUG_LOG("Waiting for process %d to terminate\n", (int)pid); + for (;;) { + ret = waitpid(pid, status, 0); + if (ret >= 0 || errno != EINTR) + break; + } + if (ret < 0) { + ret = -ERRNO_TO_DSS_ERROR(errno); + make_err_msg("failed to wait for process %d", (int)pid); + } else + log_termination_msg(pid, *status); + return ret; +} + +int remove_snapshot(struct snapshot *s, pid_t *pid) +{ + int fds[3] = {0, 0, 0}; + char *new_name = being_deleted_name(s); + int ret = dss_rename(s->name, new_name); + char *argv[] = {"rm", "-rf", new_name, NULL}; + + if (ret < 0) + goto out; + DSS_NOTICE_LOG("removing %s (interval = %i)\n", s->name, s->interval); + ret = dss_exec(pid, argv[0], argv, fds); +out: + free(new_name); + return ret; +} + +int remove_redundant_snapshot(struct snapshot_list *sl, + int dry_run, pid_t *pid) +{ + int ret, i, interval; + struct snapshot *s; + unsigned missing = 0; + + DSS_INFO_LOG("looking for intervals containing too many snapshots\n"); + for (interval = conf.num_intervals_arg - 1; interval >= 0; interval--) { + unsigned keep = 1<<(conf.num_intervals_arg - interval - 1); + unsigned num = sl->interval_count[interval]; + struct snapshot *victim = NULL, *prev = NULL; + int64_t score = LONG_MAX; + + if (keep >= num) + missing += keep - num; + DSS_DEBUG_LOG("interval %i: keep: %u, have: %u, missing: %u\n", + interval, keep, num, missing); + if (keep + missing >= num) + continue; + /* redundant snapshot in this interval, pick snapshot with lowest score */ + FOR_EACH_SNAPSHOT(s, i, sl) { + int64_t this_score; + + DSS_DEBUG_LOG("checking %s\n", s->name); + if (s->interval > interval) { + prev = s; + continue; + } + if (s->interval < interval) + break; + if (!victim) { + victim = s; + prev = s; + continue; + } + assert(prev); + /* check if s is a better victim */ + this_score = s->creation_time - prev->creation_time; + assert(this_score >= 0); + DSS_DEBUG_LOG("%s: score %lli\n", s->name, (long long)score); + if (this_score < score) { + score = this_score; + victim = s; + } + prev = s; + } + assert(victim); + if (dry_run) { + printf("%s would be removed (interval = %i)\n", + victim->name, victim->interval); + continue; + } + ret = remove_snapshot(victim, pid); + return ret < 0? ret : 1; + } + return 0; +} + +int remove_old_snapshot(struct snapshot_list *sl, int dry_run, pid_t *pid) +{ + int i, ret; + struct snapshot *s; + + DSS_INFO_LOG("looking for snapshots belonging to intervals greater than %d\n", + conf.num_intervals_arg); + FOR_EACH_SNAPSHOT(s, i, sl) { + if (s->interval <= conf.num_intervals_arg) + continue; + if (dry_run) { + printf("%s would be removed (interval = %i)\n", + s->name, s->interval); + continue; + } + ret = remove_snapshot(s, pid); + if (ret < 0) + return ret; + return 1; + } + return 0; +} + +int wait_for_rm_process(pid_t pid) +{ + int status, es, ret = wait_for_process(pid, &status); + if (ret < 0) + return ret; + if (!WIFEXITED(status)) { + ret = E_INVOLUNTARY_EXIT; + make_err_msg("rm process %d died involuntary", (int)pid); + return ret; + } + es = WEXITSTATUS(status); + if (es) { + ret = -E_BAD_EXIT_CODE; + make_err_msg("rm process %d returned %d", (int)pid, es); + return ret; + } + return 1; +} + +int com_prune(int argc, char * const * argv) +{ + int ret, dry_run = 0; + struct snapshot_list sl; + pid_t pid; + + if (argc > 2) { + make_err_msg("too many arguments"); + return -E_SYNTAX; + } + if (argc == 2) { + if (strcmp(argv[1], "-d")) { + make_err_msg("%s", argv[1]); + return -E_SYNTAX; + } + dry_run = 1; + } + for (;;) { + get_snapshot_list(&sl); + ret = remove_old_snapshot(&sl, dry_run, &pid); + free_snapshot_list(&sl); + if (ret < 0) + return ret; + if (!ret) + break; + ret = wait_for_rm_process(pid); + if (ret < 0) + goto out; + } + for (;;) { + get_snapshot_list(&sl); + ret = remove_redundant_snapshot(&sl, dry_run, &pid); + free_snapshot_list(&sl); + if (ret < 0) + return ret; + if (!ret) + break; + ret = wait_for_rm_process(pid); + if (ret < 0) + goto out; + } + return 1; +out: + return ret; +} + +struct newest_snapshot_data { + char * newest_name; + int64_t newest_creation_time; + int64_t now; +}; + +int get_newest_complete(const char *dirname, void *private) +{ + struct newest_snapshot_data *nsd = private; + struct snapshot s; + int ret = is_snapshot(dirname, nsd->now, &s); + + if (ret <= 0) + return 1; + if (s.creation_time < nsd->newest_creation_time) + return 1; + nsd->newest_creation_time = s.creation_time; + free(nsd->newest_name); + nsd->newest_name = s.name; + return 1; +} + +__malloc char *name_of_newest_complete_snapshot(void) +{ + struct newest_snapshot_data nsd = { + .now = get_current_time(), + .newest_creation_time = -1 + }; + for_each_subdir(get_newest_complete, &nsd); + return nsd.newest_name; +} + +void create_rsync_argv(char ***argv, int64_t *num) +{ + char *logname, *newest = name_of_newest_complete_snapshot(); + int i = 0, j; + + *argv = dss_malloc((15 + conf.rsync_option_given) * sizeof(char *)); + (*argv)[i++] = dss_strdup("rsync"); + (*argv)[i++] = dss_strdup("-aq"); + (*argv)[i++] = dss_strdup("--delete"); + for (j = 0; j < conf.rsync_option_given; j++) + (*argv)[i++] = dss_strdup(conf.rsync_option_arg[j]); + if (newest) { + DSS_INFO_LOG("using %s as reference snapshot\n", newest); + (*argv)[i++] = make_message("--link-dest=../%s", newest); + free(newest); + } else + DSS_INFO_LOG("no previous snapshot found"); + if (conf.exclude_patterns_given) { + (*argv)[i++] = dss_strdup("--exclude-from"); + (*argv)[i++] = dss_strdup(conf.exclude_patterns_arg); + + } + logname = dss_logname(); + if (conf.remote_user_given && !strcmp(conf.remote_user_arg, logname)) + (*argv)[i++] = dss_strdup(conf.source_dir_arg); + else + (*argv)[i++] = make_message("%s@%s:%s/", conf.remote_user_given? + conf.remote_user_arg : logname, + conf.remote_host_arg, conf.source_dir_arg); + free(logname); + *num = get_current_time(); + (*argv)[i++] = incomplete_name(*num); + (*argv)[i++] = NULL; + for (j = 0; j < i; j++) + DSS_DEBUG_LOG("argv[%d] = %s\n", j, (*argv)[j]); +} + +void free_rsync_argv(char **argv) +{ + int i; + for (i = 0; argv[i]; i++) + free(argv[i]); + free(argv); +} + +int create_snapshot(char **argv, pid_t *pid) +{ + int fds[3] = {0, 0, 0}; + + return dss_exec(pid, argv[0], argv, fds); +} + +int rename_incomplete_snapshot(int64_t start) +{ + char *old_name, *new_name; + int ret; + + ret = complete_name(start, get_current_time(), &new_name); + if (ret < 0) + return ret; + old_name = incomplete_name(start); + ret = dss_rename(old_name, new_name); + if (ret >= 0) + DSS_NOTICE_LOG("%s -> %s\n", old_name, new_name); + free(old_name); + free(new_name); + return ret; +} + +int com_create(int argc, __a_unused char * const * argv) +{ + int ret, status, es; + char **rsync_argv; + int64_t snapshot_num; + pid_t pid; + + if (argc != 1) { + ret = -E_SYNTAX; + make_err_msg("create: no args expected, %d given", argc - 1); + return ret; + } + create_rsync_argv(&rsync_argv, &snapshot_num); + DSS_NOTICE_LOG("creating snapshot %lli\n", (long long)snapshot_num); + ret = create_snapshot(rsync_argv, &pid); + if (ret < 0) + goto out; + ret = wait_for_process(pid, &status); + if (ret < 0) + goto out; + if (!WIFEXITED(status)) { + ret = E_INVOLUNTARY_EXIT; + make_err_msg("rsync process %d died involuntary", (int)pid); + goto out; + } + es = WEXITSTATUS(status); + if (es != 0 && es != 23 && es != 24) { + ret = -E_BAD_EXIT_CODE; + make_err_msg("rsync process %d returned %d", (int)pid, es); + goto out; + } + ret = rename_incomplete_snapshot(snapshot_num); +out: + free_rsync_argv(rsync_argv); + return ret; +} + +int com_ls(int argc, __a_unused char * const * argv) +{ + int i, ret; + struct snapshot_list sl; + struct snapshot *s; + if (argc != 1) { + ret = -E_SYNTAX; + make_err_msg("ls: no args expected, %d given", argc - 1); + return ret; + } + get_snapshot_list(&sl); + FOR_EACH_SNAPSHOT(s, i, &sl) + printf("%u\t%s\n", s->interval, s->name); + free_snapshot_list(&sl); + return 1; +} + +/* TODO: Unlink pid file */ +__noreturn void clean_exit(int status) +{ + //kill(0, SIGTERM); + free(dss_error_txt); + exit(status); +} + +__printf_2_3 void dss_log(int ll, const char* fmt,...) +{ + va_list argp; + if (ll < conf.loglevel_arg) + return; + va_start(argp, fmt); + vfprintf(stderr, fmt, argp); + va_end(argp); +} + +int read_config_file(void) +{ + int ret; + char *config_file; + struct stat statbuf; + + if (conf.config_file_given) + config_file = dss_strdup(conf.config_file_arg); + else { + char *home = get_homedir(); + config_file = make_message("%s/.dssrc", home); + free(home); + } + ret = stat(config_file, &statbuf); + if (ret && conf.config_file_given) { + ret = -ERRNO_TO_DSS_ERROR(errno); + make_err_msg("failed to stat config file %s", config_file); + goto out; + } + if (!ret) { + struct cmdline_parser_params params = { + .override = 0, + .initialize = 0, + .check_required = 0, + .check_ambiguity = 0 + }; + cmdline_parser_config_file(config_file, &conf, ¶ms); + } + if (!conf.source_dir_given || !conf.dest_dir_given) { + ret = -E_SYNTAX; + make_err_msg("you need to specify both source_dir and dest_dir"); + goto out; + } + ret = 1; +out: + free(config_file); + return ret; +} + +int check_config(void) +{ + if (conf.unit_interval_arg <= 0) { + make_err_msg("bad unit interval: %i", conf.unit_interval_arg); + return -E_INVALID_NUMBER; + } + DSS_DEBUG_LOG("unit interval: %i day(s)\n", conf.unit_interval_arg); + if (conf.num_intervals_arg <= 0) { + make_err_msg("bad number of intervals %i", conf.num_intervals_arg); + return -E_INVALID_NUMBER; + } + DSS_DEBUG_LOG("number of intervals: %i\n", conf.num_intervals_arg); + return 1; +} + +int main(int argc, char **argv) +{ + int i, ret; + + cmdline_parser(argc, argv, &conf); /* aborts on errors */ + if (!conf.inputs_num) { + ret = -E_SYNTAX; + make_err_msg("no command given"); + goto out; + } + ret = read_config_file(); + if (ret < 0) + goto out; + ret = check_config(); + if (ret < 0) + goto out; + ret = dss_chdir(conf.dest_dir_arg); + if (ret < 0) + goto out; + for (i = 0; command_list[i].name; i++) { + if (strcmp(command_list[i].name, conf.inputs[0])) + continue; + ret = command_list[i].handler(conf.inputs_num, conf.inputs); + goto out; + } + ret = -E_INVALID_COMMAND; + make_err_msg("%s", conf.inputs[0]); +out: + if (ret < 0) + log_err_msg(EMERG, -ret); + clean_exit(ret >= 0? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/dss.ggo b/dss.ggo new file mode 100644 index 0000000..a126dec --- /dev/null +++ b/dss.ggo @@ -0,0 +1,222 @@ + +text " +dss snapshot aging is implemented in terms of intervals. There are +two command line options related to intervals: the duration of a +'unit' interval and the number of those intervals. + +dss removes any snapshots older than the given number of intervals +times the duration of an unit interval and tries to keep the following +amount of snapshots per interval: + + interval number number of snapshots + =============================================== + 0 2 ^ (num_intervals - 1) + 1 2 ^ (num_intervals - 2) + 2 2 ^ (num_intervals - 3) + ... + num_intervals - 2 2 + num_intervals - 1 1 + num_intervals 0 + +In other words, the oldest snapshot will at most be unit_interval * +num_intervala old (= 5 days * 4 = 20 days if default values are used). +Moreover, there are at most 2^num_intervals - 1 snapshots in total +(i.e. 31 by default). Observe that you have to create at least +num_intervals snapshots each interval for this to work out. " + + + +package "dss" +version "0.0.1" + +option "config_file" c +#~~~~~~~~~~~~~~~~~~~~~ +"(default='~/.dssrc')" + + string typestr="filename" + optional + +section "Logging" +#~~~~~~~~~~~~~~~~ + +option "loglevel" l +#~~~~~~~~~~~~~~~~~~ + +"set loglevel (0-6)" + + int typestr="level" + default="4" + optional + +option "logfile" L +#~~~~~~~~~~~~~~~~~~ + +"logfile for the dss daemon process" + + string typestr="filename" + optional + +section "rsync-related options" +#============================== + +option "remote_user" U +#~~~~~~~~~~~~~~~~~~~~~ + +"remote user name (default: current user)" + + string typestr="username" + optional + +option "remote_host" H +#~~~~~~~~~~~~~~~~~~~~~ + +"remote host" + + string typestr="hostname" + default="localhost" + optional + +option "source_dir" S +#~~~~~~~~~~~~~~~~~~~~ + +"directory to backup on the remote host" + + string typestr="dirname" + optional + +option "dest_dir" D +#~~~~~~~~~~~~~~~~~~ + +"snapshots dir on the local host" + + string typestr="dirname" + optional + +option "rsync_option" O +#~~~~~~~~~~~~~~~~~~~~~~ + +"further rsync options that are passed +verbatim to the rsync command." + + string typestr="option" + optional + multiple + + +option "exclude_patterns" e +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +"rsync exclude patterns" + + string typestr="path" + optional + + +section "Intervals" +#~~~~~~~~~~~~~~~~~~ + + +option "unit_interval" u +#~~~~~~~~~~~~~~~~~~~~~~~ +"the duration of a unit interval" + + int typestr="days" + default="4" + optional + +option "num_intervals" n +#~~~~~~~~~~~~~~~~~~~~~~~ +"the number of unit intervals" + + int typestr="num" + default="5" + optional + +section "Hooks" +#============== + +option "pre_create_hook" r +#~~~~~~~~~~~~~~~~~~~~~~~~~~ +"Executed before snapshot creation" + + string typestr="command" + default="/bin/true" + optional + +text " + Execute this command before trying to create a new snapshot + If this command returns with a non-zero exit status, do not + perform the backup. One possible application of this is to + return non-zero during office hours in order to not slow down + the file systems by taking snapshots. +" + + +option "post_create_hook" o +#~~~~~~~~~~~~~~~~~~~~~~~~~~ +"Executed after snapshot creation" + + string typestr="command" + default="/bin/true" + optional + +text " + Execute this after a snapshot has successfully been created + The return value on the command is ignored. For instance one + could count the number of files per user and/or the disk + usage patterns in order to store them in a database for + further treatment. +" +option "creation_sleep" s +#~~~~~~~~~~~~~~~~~~~~~~~~ +"sleep interval" + + int typestr="minutes" + default="60" + optional + +text " + The sleep interval for snapshot creation in minutes. + The daemon will, in an endlees loop, create a snapshot and + then sleep that many minutes. +" + + +option "min_free" m +#~~~~~~~~~~~~~~~~~~ + +"minimal amount of free space" + + int typestr="gigabytes" + default="50" + optional + +text " + If less that this many gigabytes of space is available, + dss will start to remove snapshots (starting from the oldest + snapshot) until the free disk space exeecds this value. +" + + +text " +subcommands: + +ls: + + Print list of existing snapshots. + + Usage: ls + +free: + + Remove old snapshots in order to free space. + + Usage: free [size] + + Without size parameter, print the amount of free space on the file system + in human-readable format. + + Otherwise, remove snapshots (starting from the oldest one) until the number of + free space exceeds the given number of gigabytes. + Use with caution! +" diff --git a/dss.sk b/dss.sk new file mode 100644 index 0000000..97d2531 --- /dev/null +++ b/dss.sk @@ -0,0 +1,94 @@ +##Sketch 1 2 +document() +layout((362.835,119.055),0) +layer('Layer 1',1,1,0,0,(0,0,0)) +G() +fp((1,1,0)) +le() +lw(1) +r(365.344,0,0,-120.099,0.422729,119.405) +G() +fp((0.251,0.251,1)) +le() +lw(1) +r(87.8038,0,0,-106.979,183.131,112.845) +fp((0,0,1)) +le() +lw(1) +r(87.8038,0,0,-106.979,271.005,112.845) +fp((0.753,0.753,1)) +le() +lw(1) +r(87.8038,0,0,-106.979,7.38173,112.845) +fp((0.502,0.502,1)) +le() +lw(1) +r(87.8038,0,0,-106.979,95.2567,112.845) +G() +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,279.884,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,289.891,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,299.897,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,309.903,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,319.91,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,329.916,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,339.922,38.1606) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,349.928,38.1606) +G_() +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,51.2837,35.1326) +G() +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,201.095,39.1696) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,218.387,39.1696) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,235.678,39.1696) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,252.969,39.1696) +G_() +G() +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,124.56,37.1516) +fp((1,0,0)) +lw(1) +e(5.04619,0,0,-5.04619,153.757,37.1516) +G_() +fp((0.058,0.227,0.465)) +lp((1,1,1)) +lw(2.83465) +la2(([(-4.0, 3.0), (2.0, 0.0), (-4.0, -3.0), (-4.0, 3.0)], 1)) +b() +bs(35.6407,57.3367,0) +bs(335.841,57.3367,0) +fp((1,1,1)) +le() +lw(1) +Fn('Times-Bold') +Fs(36) +txt('DSS',(154.731,69.4476)) +G_() +G_() +guidelayer('Guide Lines',1,0,0,1,(0,0,1)) +grid((0,0,20,20),0,(0,0,1),'Grid') diff --git a/error.h b/error.h new file mode 100644 index 0000000..b9cbfec --- /dev/null +++ b/error.h @@ -0,0 +1,82 @@ +extern char *dss_errlist[]; +extern char *dss_error_txt; + +__printf_2_3 void dss_log(int ll, const char* fmt,...); + +/** + * This bit indicates whether a number is considered a system error number + * If yes, the system errno is just the result of clearing this bit from + * the given number. + */ +#define SYSTEM_ERROR_BIT 30 + +/** Check whether the system error bit is set. */ +#define IS_SYSTEM_ERROR(num) (!!((num) & (1 << SYSTEM_ERROR_BIT))) + +/** Set the system error bit for the given number. */ +#define ERRNO_TO_DSS_ERROR(num) ((num) | (1 << SYSTEM_ERROR_BIT)) + +/** Check whether a given number is a system error number. + * + * \param num The value to be checked. + * \param _errno The system error number. + * + * \return True if \a num is dss' representation of the system + * error identified by \a _errno. + */ +static inline int is_errno(int num, int _errno) +{ + assert(num > 0 && _errno > 0); + return ERRNO_TO_DSS_ERROR(_errno) == num; +} + +/** + * dss' version of strerror(3). + * + * \param num The error number. + * + * \return The error text of \a num. + */ +static inline char *dss_strerror(int num) +{ + assert(num > 0); + if (IS_SYSTEM_ERROR(num)) + return strerror((num) & ((1 << SYSTEM_ERROR_BIT) - 1)); + else + return dss_errlist[num]; +} + +static inline void log_err_msg(int loglevel, int num) +{ + dss_log(loglevel, "%s (%s)\n", dss_error_txt, dss_strerror(num)); +} + +#define DSS_ERRORS \ + DSS_ERROR(SUCCESS, "success") \ + DSS_ERROR(SYNTAX, "syntax error") \ + DSS_ERROR(INVALID_COMMAND, "invalid command") \ + DSS_ERROR(ATOI_OVERFLOW, "value too large") \ + DSS_ERROR(STRTOLL, "unknown strtoll error") \ + DSS_ERROR(ATOI_NO_DIGITS, "no digits found in string") \ + DSS_ERROR(ATOI_JUNK_AT_END, "further characters after number") \ + DSS_ERROR(INVALID_NUMBER, "invalid number") \ + DSS_ERROR(STRFTIME, "strftime() failed") \ + DSS_ERROR(LOCALTIME, "localtime() failed") \ + DSS_ERROR(NULL_OPEN, "can not open /dev/null") \ + DSS_ERROR(DUP_PIPE, "exec error: can not create pipe") \ + DSS_ERROR(INVOLUNTARY_EXIT, "unexpected termination cause") \ + DSS_ERROR(BAD_EXIT_CODE, "unexpected exit code") \ + DSS_ERROR(SIGNAL_SIG_ERR, "signal() returned SIG_ERR") + +/** + * This is temporarily defined to expand to its first argument (prefixed by + * 'E_') and gets later redefined to expand to the error text only + */ +#define DSS_ERROR(err, msg) E_ ## err, + +enum dss_error_codes { + DSS_ERRORS +}; +#undef DSS_ERROR +#define DSS_ERROR(err, msg) msg, +#define DEFINE_DSS_ERRLIST char *dss_errlist[] = {DSS_ERRORS} diff --git a/exec.c b/exec.c new file mode 100644 index 0000000..13a0e4b --- /dev/null +++ b/exec.c @@ -0,0 +1,111 @@ +/** \file exec.c Helper functions for spawning new processes. */ + +#include +#include +#include +#include +#include +#include +#include + + +#include "gcc-compat.h" +#include "error.h" +#include "string.h" + + +/** + * Spawn a new process and redirect fd 0, 1, and 2. + * + * \param pid Will hold the pid of the created process upon return. + * \param file Path of the executable to execute. + * \param args The argument array for the command. + * \param fds a Pointer to a value-result array. + * + * \return Standard. + * + * \sa null(4), pipe(2), dup2(2), fork(2), exec(3). + */ +int dss_exec(pid_t *pid, const char *file, char *const *const args, int *fds) +{ + int ret, in[2] = {-1, -1}, out[2] = {-1, -1}, err[2] = {-1, -1}, + null = -1; /* ;) */ + + ret = -E_DUP_PIPE; + if (fds[0] > 0 && pipe(in) < 0) + goto err_out; + if (fds[1] > 0 && pipe(out) < 0) + goto err_out; + if (fds[2] > 0 && pipe(err) < 0) + goto err_out; + if (!fds[0] || !fds[1] || !fds[2]) { + ret = -E_NULL_OPEN; + null = open("/dev/null", O_RDONLY); + if (null < 0) + goto err_out; + } + if ((*pid = fork()) < 0) + exit(EXIT_FAILURE); + if (!(*pid)) { /* child */ + if (fds[0] >= 0) { + if (fds[0]) { + close(in[1]); + if (in[0] != STDIN_FILENO) + dup2(in[0], STDIN_FILENO); + } else + dup2(null, STDIN_FILENO); + } + if (fds[1] >= 0) { + if (fds[1]) { + close(out[0]); + if (out[1] != STDOUT_FILENO) + dup2(out[1], STDOUT_FILENO); + } else + dup2(null, STDOUT_FILENO); + } + if (fds[2] >= 0) { + if (fds[2]) { + close(err[0]); + if (err[1] != STDERR_FILENO) + dup2(err[1], STDERR_FILENO); + } else + dup2(null, STDERR_FILENO); + } + if (null >= 0) + close(null); + execvp(file, args); + _exit(EXIT_FAILURE); + } + if (fds[0] > 0) { + close(in[0]); + *fds = in[1]; + } + if (fds[1] > 0) { + close(out[1]); + *(fds + 1) = out[0]; + } + if (fds[2] > 0) { + close(err[1]); + *(fds + 2) = err[0]; + } + if (null >= 0) + close(null); + return 1; +err_out: + make_err_msg("failed to exec %s", file); + if (err[0] >= 0) + close(err[0]); + if (err[1] >= 0) + close(err[1]); + if (out[0] >= 0) + close(out[0]); + if (out[1] >= 0) + close(out[1]); + if (in[0] >= 0) + close(in[0]); + if (in[1] >= 0) + close(in[1]); + if (null >= 0) + close(null); + return ret; +} diff --git a/exec.h b/exec.h new file mode 100644 index 0000000..d428adc --- /dev/null +++ b/exec.h @@ -0,0 +1,2 @@ +int dss_exec(pid_t *pid, const char *file, char *const *const args, int *fds); + diff --git a/fd.c b/fd.c new file mode 100644 index 0000000..db0433f --- /dev/null +++ b/fd.c @@ -0,0 +1,104 @@ +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "gcc-compat.h" +#include "error.h" +#include "string.h" + +/** + * Call a function for each subdirectory of the current working directory. + * + * \param dirname The directory to traverse. + * \param func The function to call for each subdirecrtory. + * \param private_data Pointer to an arbitrary data structure. + * + * For each top-level directory under \a dirname, the supplied function \a func is + * called. The full path of the subdirectory and the \a private_data pointer + * are passed to \a func. + * + * \return This function returns immediately if \a func returned a negative + * value. In this case \a func must set error_txt and this negative value is + * returned to the caller. Otherwise the function returns when all + * subdirectories have been passed to \a func. + */ + +int for_each_subdir(int (*func)(const char *, void *), void *private_data) +{ + struct dirent *entry; + int ret; + DIR *dir = opendir("."); + + if (!dir) { + ret = -ERRNO_TO_DSS_ERROR(errno); + make_err_msg("opendir(\".\") failed"); + return ret; + } + while ((entry = readdir(dir))) { + mode_t m; + struct stat s; + + if (!strcmp(entry->d_name, ".")) + continue; + if (!strcmp(entry->d_name, "..")) + continue; + ret = lstat(entry->d_name, &s) == -1; + if (ret == -1) { + ret = -ERRNO_TO_DSS_ERROR(errno); + make_err_msg("lstat(\"%s\") failed", entry->d_name); + goto out; + } + m = s.st_mode; + if (!S_ISDIR(m)) + continue; + ret = func(entry->d_name, private_data); + if (ret < 0) + goto out; + } + ret = 1; +out: + closedir(dir); + return ret; +} +/** + * Wrapper for chdir(2). + * + * \param path The specified directory. + * + * \return Standard. + */ +int dss_chdir(const char *path) +{ + int ret = chdir(path); + + if (ret >= 0) + return 1; + ret = -ERRNO_TO_DSS_ERROR(errno); + make_err_msg("chdir to %s failed", path); + return ret; +} + +/** + * Set a file descriptor to non-blocking mode. + * + * \param fd The file descriptor. + * + * \return Standard. + */ +__must_check int mark_fd_nonblocking(int fd) +{ + int flags = fcntl(fd, F_GETFL); + if (flags < 0) + return -ERRNO_TO_DSS_ERROR(errno); + flags = fcntl(fd, F_SETFL, ((long)flags) | O_NONBLOCK); + if (flags < 0) + return -ERRNO_TO_DSS_ERROR(errno); + return 1; +} + diff --git a/fd.h b/fd.h new file mode 100644 index 0000000..fa14ad2 --- /dev/null +++ b/fd.h @@ -0,0 +1,23 @@ +int dss_chdir(const char *path); +int for_each_subdir(int (*func)(const char *, void *), void *private_data); +__must_check int mark_fd_nonblocking(int fd); +/** + * A wrapper for rename(2). + * + * \param old_path The source path. + * \param new_path The destination path. + * + * \return Standard. + * + * \sa rename(2). + */ +_static_inline_ int dss_rename(const char *old_path, const char *new_path) +{ + int ret; + + if (rename(old_path, new_path) >= 0) + return 1; + ret = -ERRNO_TO_DSS_ERROR(errno); + make_err_msg("rename %s -> %s failed", old_path, new_path); + return ret; +} diff --git a/gcc-compat.h b/gcc-compat.h new file mode 100644 index 0000000..61c3c88 --- /dev/null +++ b/gcc-compat.h @@ -0,0 +1,25 @@ +# define inline inline __attribute__ ((always_inline)) +# define __noreturn __attribute__ ((noreturn)) +# define __malloc __attribute__ ((malloc)) +# define __a_unused __attribute__ ((unused)) +# define likely(x) __builtin_expect (!!(x), 1) +# define unlikely(x) __builtin_expect (!!(x), 0) +/* + * p is the number of the "format string" parameter, and q is + * the number of the first variadic parameter + */ +# define __printf(p,q) __attribute__ ((format (printf, p, q))) +/* + * as direct use of __printf(p,q) confuses doxygen, here are two extra macros + * for those values p,q that are actually used by paraslash. + */ +#define __printf_1_2 __printf(1,2) +#define __printf_2_3 __printf(2,3) + +# if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 3) +# define __must_check __attribute__ ((warn_unused_result)) +# else +# define __must_check /* no warn_unused_result */ +# endif + +#define _static_inline_ static inline diff --git a/log.h b/log.h new file mode 100644 index 0000000..e140515 --- /dev/null +++ b/log.h @@ -0,0 +1,61 @@ + +/** debug loglevel, gets really noisy */ +#define DEBUG 1 +/** still noisy, but won't fill your disk */ +#define INFO 2 +/** normal, but significant event */ +#define NOTICE 3 +/** unexpected event that can be handled */ +#define WARNING 4 +/** unhandled error condition */ +#define ERROR 5 +/** system might be unreliable */ +#define CRIT 6 +/** last message before exit */ +#define EMERG 7 + +/** Log messages with lower priority than that will not be compiled in. */ +#define COMPILE_TIME_LOGLEVEL 0 + +/** \cond */ +#if DEBUG > COMPILE_TIME_LOGLEVEL +#define DSS_DEBUG_LOG(f,...) dss_log(DEBUG, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_DEBUG_LOG(...) do {;} while (0) +#endif + +#if INFO > COMPILE_TIME_LOGLEVEL +#define DSS_INFO_LOG(f,...) dss_log(INFO, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_INFO_LOG(...) do {;} while (0) +#endif + +#if NOTICE > COMPILE_TIME_LOGLEVEL +#define DSS_NOTICE_LOG(f,...) dss_log(NOTICE, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_NOTICE_LOG(...) do {;} while (0) +#endif + +#if WARNING > COMPILE_TIME_LOGLEVEL +#define DSS_WARNING_LOG(f,...) dss_log(WARNING, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_WARNING_LOG(...) do {;} while (0) +#endif + +#if ERROR > COMPILE_TIME_LOGLEVEL +#define DSS_ERROR_LOG(f,...) dss_log(ERROR, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_ERROR_LOG(...) do {;} while (0) +#endif + +#if CRIT > COMPILE_TIME_LOGLEVEL +#define DSS_CRIT_LOG(f,...) dss_log(CRIT, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_CRIT_LOG(...) do {;} while (0) +#endif + +#if EMERG > COMPILE_TIME_LOGLEVEL +#define DSS_EMERG_LOG(f,...) dss_log(EMERG, "%s: " f, __FUNCTION__, ## __VA_ARGS__) +#else +#define DSS_EMERG_LOG(...) +#endif diff --git a/signal.c b/signal.c new file mode 100644 index 0000000..160730f --- /dev/null +++ b/signal.c @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2004-2008 Andre Noll + * + * Licensed under the GPL v2. For licencing details see COPYING. + */ +/** \file signal.c Signal handling functions. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "gcc-compat.h" +#include "error.h" +#include "log.h" +#include "string.h" +#include "fd.h" + +static int signal_pipe[2]; + +/** + * Initialize the signal subsystem. + * + * This function creates a pipe, the signal pipe, to deliver pending signals to + * the application (Bernstein's trick). It should be called during the + * application's startup part, followed by subsequent calls to + * install_sighandler() for each signal that should be caught. + * + * signal_init() installs a generic signal handler which is used for all + * signals simultaneously. When a signal arrives, this generic signal handler + * writes the corresponding signal number to the signal pipe so that the + * application can test for pending signals simply by checking the signal pipe + * for reading, e.g. by using the select(2) system call. + * + * \return This function either succeeds or calls exit(2) to terminate + * the current process. On success, the file descriptor of the signal pipe is + * returned. + */ +int signal_init(void) +{ + int ret; + if (pipe(signal_pipe) < 0) { + ret = -ERRNO_TO_DSS_ERROR(errno); + goto err_out; + } + ret = mark_fd_nonblocking(signal_pipe[0]); + if (ret < 0) + goto err_out; + ret = mark_fd_nonblocking(signal_pipe[1]); + if (ret < 0) + goto err_out; + return signal_pipe[0]; +err_out: + DSS_EMERG_LOG("%s\n", dss_strerror(-ret)); + exit(EXIT_FAILURE); +} + +/* + * just write one integer to signal pipe + */ +static void generic_signal_handler(int s) +{ + write(signal_pipe[1], &s, sizeof(int)); + //fprintf(stderr, "got sig %i, write returned %d\n", s, ret); +} + +/** + * Reap one child. + * + * \param pid In case a child died, its pid is returned here. + * + * Call waitpid() and print a log message containing the pid and the cause of + * the child's death. + * + * \return A (negative) error code on errors, zero, if no child died, one + * otherwise. If and only if the function returns one, the content of \a pid is + * meaningful. + * + * \sa waitpid(2) + */ +int reap_child(pid_t *pid) +{ + int status; + *pid = waitpid(-1, &status, WNOHANG); + + if (!*pid) + return 0; + if (*pid < 0) + return -ERRNO_TO_DSS_ERROR(errno); + if (WIFEXITED(status)) + DSS_DEBUG_LOG("child %i exited. Exit status: %i\n", (int)*pid, + WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + DSS_DEBUG_LOG("child %i was killed by signal %i\n", (int)*pid, + WTERMSIG(status)); + else + DSS_WARNING_LOG("child %i terminated abormally\n", (int)*pid); + return 1; +} + +/** + * wrapper around signal(2) + * \param sig the number of the signal to catch + * + * This installs the generic signal handler for the given signal. + * \return This function returns 1 on success and \p -E_SIGNAL_SIG_ERR on errors. + * \sa signal(2) + */ +int install_sighandler(int sig) +{ + DSS_DEBUG_LOG("catching signal %d\n", sig); + return signal(sig, &generic_signal_handler) == SIG_ERR? -E_SIGNAL_SIG_ERR : 1; +} + +/** + * return number of next pending signal + * + * This should be called if the fd for the signal pipe is ready for reading. + * + * \return On success, the number of the received signal is returned. \p + * -E_SIGNAL_READ is returned if a read error occurred while reading the signal + * pipe. If the read was interrupted by another signal the function returns 0. + */ +int next_signal(void) +{ + int s; + ssize_t r; + + r = read(signal_pipe[0], &s, sizeof(s)); + if (r == sizeof(s)) { + DSS_DEBUG_LOG("next signal: %d\n", s); + return s; + } + return r < 0 && (errno != EAGAIN)? 0 : -ERRNO_TO_DSS_ERROR(errno); +} + +/** + * Close the signal pipe. + */ +void signal_shutdown(void) +{ + close(signal_pipe[1]); +} diff --git a/signal.h b/signal.h new file mode 100644 index 0000000..93fa8a8 --- /dev/null +++ b/signal.h @@ -0,0 +1,13 @@ +/* + * Copyright (C) 2007-2008 Andre Noll + * + * Licensed under the GPL v2. For licencing details see COPYING. + */ + +/** \file signal.h exported symbols from signal.c */ + +int signal_init(void); +int install_sighandler(int); +int reap_child(pid_t *pid); +int next_signal(void); +void signal_shutdown(void); diff --git a/string.c b/string.c new file mode 100644 index 0000000..cf6d0b1 --- /dev/null +++ b/string.c @@ -0,0 +1,246 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "cmdline.h" +#include "gcc-compat.h" +#include "log.h" +#include "error.h" + +__noreturn void clean_exit(int status); + +/** + * Write a message to a dynamically allocated string. + * + * \param fmt Usual format string. + * \param p Result pointer. + * + * \sa printf(3). */ +#define VSPRINTF(fmt, p) \ +{ \ + int n; \ + size_t size = 100; \ + p = dss_malloc(size); \ + while (1) { \ + va_list ap; \ + /* Try to print in the allocated space. */ \ + va_start(ap, fmt); \ + n = vsnprintf(p, size, fmt, ap); \ + va_end(ap); \ + /* If that worked, return the string. */ \ + if (n > -1 && n < size) \ + break; \ + /* Else try again with more space. */ \ + if (n > -1) /* glibc 2.1 */ \ + size = n + 1; /* precisely what is needed */ \ + else /* glibc 2.0 */ \ + size *= 2; /* twice the old size */ \ + p = dss_realloc(p, size); \ + } \ +} + +/** + * dss' version of realloc(). + * + * \param p Pointer to the memory block, may be \p NULL. + * \param size The desired new size. + * + * A wrapper for realloc(3). It calls \p exit(\p EXIT_FAILURE) on errors, + * i.e. there is no need to check the return value in the caller. + * + * \return A pointer to the newly allocated memory, which is suitably aligned + * for any kind of variable and may be different from \a p. + * + * \sa realloc(3). + */ +__must_check __malloc void *dss_realloc(void *p, size_t size) +{ + /* + * No need to check for NULL pointers: If p is NULL, the call + * to realloc is equivalent to malloc(size) + */ + assert(size); + if (!(p = realloc(p, size))) { + DSS_EMERG_LOG("realloc failed (size = %zu), aborting\n", + size); + clean_exit(EXIT_FAILURE); + } + return p; +} + +/** + * dss' version of malloc(). + * + * \param size The desired new size. + * + * A wrapper for malloc(3) which exits on errors. + * + * \return A pointer to the allocated memory, which is suitably aligned for any + * kind of variable. + * + * \sa malloc(3). + */ +__must_check __malloc void *dss_malloc(size_t size) +{ + assert(size); + void *p = malloc(size); + + if (!p) { + DSS_EMERG_LOG("malloc failed (size = %zu), aborting\n", + size); + clean_exit(EXIT_FAILURE); + } + return p; +} + +/** + * dss' version of calloc(). + * + * \param size The desired new size. + * + * A wrapper for calloc(3) which exits on errors. + * + * \return A pointer to the allocated and zeroed-out memory, which is suitably + * aligned for any kind of variable. + * + * \sa calloc(3) + */ +__must_check __malloc void *dss_calloc(size_t size) +{ + void *ret = dss_malloc(size); + + memset(ret, 0, size); + return ret; +} + +/** + * dss' version of strdup(). + * + * \param s The string to be duplicated. + * + * A wrapper for strdup(3). It calls \p exit(EXIT_FAILURE) on errors, i.e. + * there is no need to check the return value in the caller. + * + * \return A pointer to the duplicated string. If \p s was the NULL pointer, + * an pointer to an empty string is returned. + * + * \sa strdup(3) + */ + +__must_check __malloc char *dss_strdup(const char *s) +{ + char *ret; + + if ((ret = strdup(s? s: ""))) + return ret; + DSS_EMERG_LOG("strdup failed, aborting\n"); + clean_exit(EXIT_FAILURE); +} + +/** + * Allocate a sufficiently large string and print into it. + * + * \param fmt A usual format string. + * + * Produce output according to \p fmt. No artificial bound on the length of the + * resulting string is imposed. + * + * \return This function either returns a pointer to a string that must be + * freed by the caller or aborts without returning. + * + * \sa printf(3). + */ +__must_check __printf_1_2 __malloc char *make_message(const char *fmt, ...) +{ + char *msg; + + VSPRINTF(fmt, msg); + return msg; +} + +__printf_1_2 void make_err_msg(const char* fmt,...) +{ + free(dss_error_txt); + VSPRINTF(fmt, dss_error_txt); +} + +/** + * Get the home directory of the current user. + * + * \return A dynammically allocated string that must be freed by the caller. If + * the home directory could not be found, this function returns "/tmp". + */ +__must_check __malloc char *get_homedir(void) +{ + struct passwd *pw = getpwuid(getuid()); + return dss_strdup(pw? pw->pw_dir : "/tmp"); +} + +/** \cond LLONG_MAX and LLONG_LIN might not be defined. */ +#ifndef LLONG_MAX +#define LLONG_MAX (1 << (sizeof(long) - 1)) +#endif +#ifndef LLONG_MIN +#define LLONG_MIN (-LLONG_MAX - 1LL) +#endif +/** \endcond */ + +/** + * Convert a string to a 64-bit signed integer value. + * + * \param str The string to be converted. + * \param value Result pointer. + * + * \return Standard. + * + * \sa strtol(3), atoi(3). + */ +int dss_atoi64(const char *str, int64_t *value) +{ + char *endptr; + long long tmp; + + errno = 0; /* To distinguish success/failure after call */ + tmp = strtoll(str, &endptr, 10); + if (errno == ERANGE && (tmp == LLONG_MAX || tmp == LLONG_MIN)) { + make_err_msg("%s", str); + return -E_ATOI_OVERFLOW; + } + if (errno != 0 && tmp == 0) { /* other error */ + make_err_msg("%s", str); + return -E_STRTOLL; + } + if (endptr == str) { + make_err_msg("%s", str); + return -E_ATOI_NO_DIGITS; + } + if (*endptr != '\0') { /* Further characters after number */ + make_err_msg("%s", str); + return -E_ATOI_JUNK_AT_END; + } + *value = tmp; + return 1; +} + +/** + * Get the logname of the current user. + * + * \return A dynammically allocated string that must be freed by the caller. On + * errors, the string "unknown user" is returned, i.e. this function never + * returns \p NULL. + * + * \sa getpwuid(3). + */ +__must_check __malloc char *dss_logname(void) +{ + struct passwd *pw = getpwuid(getuid()); + return dss_strdup(pw? pw->pw_name : "unknown_user"); +} + diff --git a/string.h b/string.h new file mode 100644 index 0000000..41afe8f --- /dev/null +++ b/string.h @@ -0,0 +1,9 @@ +__must_check __malloc void *dss_realloc(void *p, size_t size); +__must_check __malloc void *dss_malloc(size_t size); +__must_check __malloc void *dss_calloc(size_t size); +__must_check __printf_1_2 __malloc char *make_message(const char *fmt, ...); +__must_check __malloc char *dss_strdup(const char *s); +__printf_1_2 void make_err_msg(const char* fmt,...); +__must_check __malloc char *get_homedir(void); +int dss_atoi64(const char *str, int64_t *value); +__must_check __malloc char *dss_logname(void);