From c418d2188c9c2c542270023d6fc3bc6cf34f8d29 Mon Sep 17 00:00:00 2001 From: Andre Noll Date: Tue, 11 Mar 2008 21:00:54 +0100 Subject: [PATCH 1/1] Initial git checkin. Dyadic intervals are so harmonic. Do you feel it? --- Makefile | 45 ++++ README | 19 ++ daemon.c | 115 ++++++++ dss.c | 720 +++++++++++++++++++++++++++++++++++++++++++++++++++ dss.ggo | 222 ++++++++++++++++ dss.sk | 94 +++++++ error.h | 82 ++++++ exec.c | 111 ++++++++ exec.h | 2 + fd.c | 104 ++++++++ fd.h | 23 ++ gcc-compat.h | 25 ++ log.h | 61 +++++ signal.c | 151 +++++++++++ signal.h | 13 + string.c | 246 ++++++++++++++++++ string.h | 9 + 17 files changed, 2042 insertions(+) create mode 100644 Makefile create mode 100644 README create mode 100644 daemon.c create mode 100644 dss.c create mode 100644 dss.ggo create mode 100644 dss.sk create mode 100644 error.h create mode 100644 exec.c create mode 100644 exec.h create mode 100644 fd.c create mode 100644 fd.h create mode 100644 gcc-compat.h create mode 100644 log.h create mode 100644 signal.c create mode 100644 signal.h create mode 100644 string.c create mode 100644 string.h 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); -- 2.39.2