/*- * Copyright (c) 2009-2016 Janne Snabb. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * * Software web site: * https://dist.epipe.com/downtimed/ * */ /* Include config.h in case we use autoconf. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* * _GNU_SOURCE is required to enable in asprintf() and vasprintf() * in on GNU/Linux. * * _BSD_SOURCE is required to enable BSD style signal(3) facility * on GNU/Linux. */ #if defined(__linux__) || defined(__GLIBC__) || defined(__GNU__) /* GNU/Linux, GNU/kFreeBSD, and GNU/Hurd */ #define _GNU_SOURCE #define _BSD_SOURCE #endif /* * Pull in facilitynames array if has it. */ #ifdef HAVE_SYSLOG_FACILITYNAMES #define SYSLOG_NAMES #endif /* Standard includes that we need */ #include #include #ifdef HAVE_SYS_PARAM_H #include #endif #ifdef HAVE_SYS_SYSCTL_H #include #endif #include #include #include #include #include #include #ifdef HAVE_PATHS_H #include #endif #include #include #include #include #include #include #include #include #if defined(__SVR4) && defined(HAVE_UTMPX_H) #include #endif #include #include "downtimedb.h" /* Some global defines */ #define PROGNAME "downtimed" /* PACKAGE_VERSION is defined by autoconf, if used. */ #ifdef PACKAGE_VERSION #define PROGVERSION PACKAGE_VERSION #else #define PROGVERSION "0.0undef" #endif /* We expect the following preprocessor macros to be defined */ /* from */ #ifndef _PATH_VARRUN #define _PATH_VARRUN "/var/run/" #endif #ifndef _PATH_DEVNULL #define _PATH_DEVNULL "/dev/null" #endif /* from */ #ifndef DEFFILEMODE #define DEFFILEMODE 0666 #endif /* Function prototypes */ int main(int, char *[]); static time_t getboottime(void); static void updatedowntimedb(time_t, int, time_t); static void report(void); static void sighandler(int); static void touch(const char *, time_t); static void loginit(void); static void logdeinit(void); static void logwr(int, const char *, ...); static void version(void); static void usage(void); static void parseargs(int, char *[]); static int makepidfile(void); static void removepidfile(void); #ifndef HAVE_DAEMON static int daemon(int, int); #endif /* Command line arguments with their defaults */ static char * cf_log = "daemon"; /* syslog facility or filename with / */ static int cf_fork = 1; /* whether to call daemon() which fork()s */ static char * cf_pidfile = _PATH_VARRUN PROGNAME ".pid"; static char * cf_datadir = PATH_DOWNTIMEDBDIR; static long cf_sleep = 15; /* update time stamp every 15 seconds */ #ifdef HAVE_FUTIMES static int cf_fsync = 1; /* set to fsync() stamp files after touching */ #endif static int cf_downtimedb = 1; /* if true, update downtimedb */ static char * cf_downtimedbfile = PATH_DOWNTIMEDBFILE; static char * cf_timefmt = FMT_DATETIME; /* Logging destination, determined from cf_log */ static int cf_logfacility = 0; static int cf_logfd = -1; /* Global variables */ static char * ts_stamp = NULL; static char * ts_shutdown = NULL; static char * ts_boot = NULL; static time_t boottime = 0; static time_t starttime = 0; /* The following are set by the signal handler */ static volatile sig_atomic_t exiting = 0; static volatile sig_atomic_t reopenlog = 0; /* * downtimed: system downtime monitoring and reporting daemon. * * This daemon sits in the background, periodically updating a time stamp * on the disk. If the daemon is killed with a signal associated with a * normal system shutdown procedure, it will record the shutdown time on * the disk. When the daemon is restarted during the next boot process, * it will report how long the system was down and whether it was properly * shut down or crashed. */ int main(int argc, char *argv[]) { struct stat sb; time_t uptime; /* record daemon startup time for later use */ starttime = time((time_t *)NULL); /* parse command line arguments */ parseargs(argc, argv); /* protect our files from tampering by other users */ umask(S_IWGRP | S_IWOTH); /* initialize logging subsystem */ loginit(); /* find out system boot time */ boottime = getboottime(); /* check if datadir exists */ if (stat(cf_datadir, &sb) < 0 || !S_ISDIR(sb.st_mode)) { logwr(LOG_CRIT, "data directory %s does not exist", cf_datadir); errx(EX_CANTCREAT, "data directory %s does not exist", cf_datadir); } /* set time stamp file names */ if (asprintf(&ts_stamp, "%s/downtimed.stamp", cf_datadir) < 0 || asprintf(&ts_shutdown, "%s/downtimed.shutdown", cf_datadir) < 0 || asprintf(&ts_boot, "%s/downtimed.boot", cf_datadir) < 0) { logwr(LOG_CRIT, "asprintf failed, out of memory?"); errx(EX_OSERR, "asprintf failed, out of memory?"); } if (cf_fork) { /* run as daemon (fork and detach from controlling tty) */ if (daemon(0, 0) < 0) { logwr(LOG_CRIT, "starting daemon failed: %s", strerror(errno)); err(EX_OSERR, "starting daemon failed"); } } /* create pid file */ if (makepidfile() < 0) { /* the error has been logged already in makepidfile() */ exit(EX_UNAVAILABLE); } /* check & log startup status */ report(); /* set up the signal handlers */ signal(SIGHUP, sighandler); signal(SIGINT, sighandler); signal(SIGTERM, sighandler); /* touch system boot time */ touch(ts_boot, boottime); /* * main loop: run until we receive a signal or system dies, * touching the time stamp file regularly */ while (exiting == 0) { touch(ts_stamp, 0); sleep(cf_sleep); if (reopenlog) { reopenlog = 0; logdeinit(); loginit(); } } /* * Record normal shutdown. If using syslog for logging, this * might fail because syslogd may have exited already. */ uptime = time((time_t *)NULL) - boottime; logwr(LOG_NOTICE, "shutting down, uptime %s (%d seconds)", timestr_int(uptime), uptime); touch(ts_stamp, 0); touch(ts_shutdown, 0); /* We could write the downtime database shutdown record here * in case of graceful shutdown, but we have chosen to update * it consistently only at the program start. */ logdeinit(); removepidfile(); exit(EX_OK); } /* Find out system boot time */ static time_t getboottime() { #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) \ || defined(__DragonFly__) || defined(__APPLE__) \ || defined(__FreeBSD_kernel__) /* * BSDish systems have the boot time available through sysctl. */ int mib[2]; struct timeval bt; size_t btsize; mib[0] = CTL_KERN; mib[1] = KERN_BOOTTIME; btsize = sizeof(bt); if (sysctl(mib, 2, &bt, &btsize, (void *)NULL, 0) != -1 && bt.tv_sec != 0) return (bt.tv_sec); #elif defined(__linux__) /* * Linux systems have the boot time available through /proc filesystem. */ char str[1024]; FILE *fp; long unsigned bt = 0; if ((fp = fopen("/proc/stat", "r")) != NULL) { while (fgets(str, sizeof(str), fp) != NULL) if (strncmp(str, "btime ", 6) == 0) if (sscanf(str, "btime %lu", &bt) == 1 && bt != 0) { fclose(fp); return ((time_t)bt); } fclose(fp); } #elif defined(__SVR4) && defined(HAVE_UTMPX_H) /* * This should work on some SVR4 systems, such as Solaris * and possibly HP-UX. */ struct utmpx ut, *utp; memset(&ut, 0, sizeof(ut)); ut.ut_type = BOOT_TIME; if ((utp = getutxid(&ut)) != NULL) { time_t boottime; boottime = utp->ut_tv.tv_sec; endutxent(); return (boottime); } endutxent(); #endif /* * We fall through here if we do not have OS specific code * implemented or if the OS specific code fails. * * The logic could be improved to stat() some files which are * accessed or modified early at system startup (for example the * system log, dmesg.boot, kernel, utmp, etc.) XXX */ logwr(LOG_ERR, "can not determine system boot time on this OS"); return (starttime); /* give up */ } /* Update downtime database */ void updatedowntimedb(time_t up, int crashed, time_t down) { struct downtimedb dbent; int fd; if ((fd = open(cf_downtimedbfile, O_WRONLY | O_CREAT | O_APPEND, DEFFILEMODE)) < 0) { logwr(LOG_ERR, "can not open %s: %s", cf_downtimedbfile, strerror(errno)); return; } /* ensure that padding bytes are zero */ memset(&dbent, 0, sizeof(struct downtimedb)); dbent.what = crashed ? DOWNTIMEDB_WHAT_CRASH : DOWNTIMEDB_WHAT_SHUTDOWN; dbent.when = (uint64_t) down; if (downtimedb_write(fd, &dbent) < 0) logwr(LOG_ERR, "can not write to %s: %s", cf_downtimedbfile, strerror(errno)); /* not really needed, but ensure again that padding bytes are zero */ memset(&dbent, 0, sizeof(struct downtimedb)); dbent.what = DOWNTIMEDB_WHAT_UP; dbent.when = (uint64_t) up; if (downtimedb_write(fd, &dbent) < 0) logwr(LOG_ERR, "can not write to %s: %s", cf_downtimedbfile, strerror(errno)); close(fd); } /* Report the downtime and shutdown reason when starting up */ static void report() { struct stat sb_stamp, sb_shutdown, sb_oldboot; int have_stamp = 0, have_shutdown = 0, have_oldboot = 0; time_t olduptime, downtime; if (stat(ts_stamp, &sb_stamp) == 0) have_stamp = 1; if (stat(ts_shutdown, &sb_shutdown) == 0) have_shutdown = 1; if (stat(ts_boot, &sb_oldboot) == 0) have_oldboot = 1; if (!have_stamp && !have_shutdown && !have_oldboot) { logwr(LOG_NOTICE, "starting up first time, " "no knowledge of downtime"); return; } if (!have_stamp) { logwr(LOG_ERR, "no old run-time stamp (%s)", ts_stamp); return; } if (!have_oldboot) { logwr(LOG_ERR, "no old boot-time stamp (%s)", ts_boot); return; } if (have_stamp && have_shutdown && sb_shutdown.st_mtime < sb_stamp.st_mtime) have_shutdown = 0; olduptime = (have_shutdown ? sb_shutdown.st_mtime : sb_stamp.st_mtime) - sb_oldboot.st_mtime; downtime = boottime - (have_shutdown ? sb_shutdown.st_mtime : sb_stamp.st_mtime); if (downtime < 0) { /* * This happens if we quit and re-start the process (we * normally only exit when system goes down. */ logwr(LOG_NOTICE, "restarted, system was not down"); return; } logwr(LOG_NOTICE, "started %d seconds after boot", starttime - boottime); if (cf_downtimedb) updatedowntimedb(boottime, !have_shutdown, (have_shutdown ? sb_shutdown.st_mtime : sb_stamp.st_mtime)); if (have_shutdown) { logwr(LOG_NOTICE, "system shutdown at %s", timestr_abs(sb_shutdown.st_mtime, cf_timefmt, 0)); } else { logwr(LOG_NOTICE, "system crashed at %s", timestr_abs(sb_stamp.st_mtime, cf_timefmt, 0)); } logwr(LOG_NOTICE, "previous uptime was %s (%d seconds)", timestr_int(olduptime), olduptime); logwr(LOG_NOTICE, "downtime was %s (%d seconds)", timestr_int(downtime), downtime); } /* Handle signals */ static void sighandler(int signum) { if (signum == SIGINT || signum == SIGTERM) exiting = 1; if (signum == SIGHUP) reopenlog = 1; } /* Update time-stamp of file */ static void touch(const char *fn, time_t t) { struct stat sb; struct timeval tv[2]; int fd; if (t != 0) { tv[0].tv_sec = t; tv[0].tv_usec = 0; tv[1].tv_sec = t; tv[1].tv_usec = 0; } #ifdef HAVE_FUTIMES if (cf_fsync) { /* we need to open the file so that we can do fsync() to it */ if ((fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE)) < 0) { logwr(LOG_ERR, "%s: %s", fn, strerror(errno)); return; } if (futimes(fd, t == 0 ? (struct timeval *)NULL : tv) < 0) { logwr(LOG_ERR, "%s: %s", fn, strerror(errno)); } else fsync(fd); if (close(fd) < 0) logwr(LOG_ERR, "%s: %s", fn, strerror(errno)); } else { #endif /* HAVE_FUTIMES */ /* create the file in case it is missing */ if (stat(fn, &sb) < 0) { if ((fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE)) < 0) { logwr(LOG_ERR, "%s: %s", fn, strerror(errno)); return; } if (close(fd) < 0) logwr(LOG_ERR, "%s: %s", fn, strerror(errno)); } if (utimes(fn, t == 0 ? (struct timeval *)NULL : tv) < 0) logwr(LOG_ERR, "%s: %s", fn, strerror(errno)); #ifdef HAVE_FUTIMES } #endif /* HAVE_FUTIMES */ } /* Compatibility for systems without facilitynames in */ #ifndef HAVE_SYSLOG_FACILITYNAMES static const struct { const char *c_name; const int c_val; } facilitynames[] = { #ifdef LOG_KERN { "kern", LOG_KERN }, #endif #ifdef LOG_USER { "user", LOG_USER }, #endif #ifdef LOG_MAIL { "mail", LOG_MAIL }, #endif #ifdef LOG_DAEMON { "daemon", LOG_DAEMON }, #endif #ifdef LOG_AUTH { "auth", LOG_AUTH }, #endif #ifdef LOG_SYSLOG { "syslog", LOG_SYSLOG }, #endif #ifdef LOG_LPR { "lpr", LOG_LPR }, #endif #ifdef LOG_NEWS { "news", LOG_NEWS }, #endif #ifdef LOG_AUDIT { "audit", LOG_AUDIT }, #endif #ifdef LOG_CRON { "cron", LOG_CRON }, #endif #ifdef LOG_LOCAL0 { "local0", LOG_LOCAL0 }, #endif #ifdef LOG_LOCAL1 { "local1", LOG_LOCAL1 }, #endif #ifdef LOG_LOCAL2 { "local2", LOG_LOCAL2 }, #endif #ifdef LOG_LOCAL3 { "local3", LOG_LOCAL3 }, #endif #ifdef LOG_LOCAL4 { "local4", LOG_LOCAL4 }, #endif #ifdef LOG_LOCAL5 { "local5", LOG_LOCAL5 }, #endif #ifdef LOG_LOCAL6 { "local6", LOG_LOCAL6 }, #endif #ifdef LOG_LOCAL7 { "local7", LOG_LOCAL7 }, #endif { NULL, -1 } }; #endif /* !defined(HAVE_SYSLOG_FACILITYNAMES) */ /* Determine log destination & initialize */ static void loginit() { int i; if (strchr(cf_log, '/') == NULL) { /* Logging to syslog if there is no slash in the name. */ for(i = 0; facilitynames[i].c_name != NULL; i++) if (strcmp(facilitynames[i].c_name, cf_log) == 0) cf_logfacility = facilitynames[i].c_val; if (cf_logfacility == 0) errx(EX_USAGE, "-l argument is not syslog facility or file path"); openlog(PROGNAME, LOG_PID, cf_logfacility); } else { /* We are logging to a file. */ if ((cf_logfd = open(cf_log, O_WRONLY | O_APPEND | O_CREAT, DEFFILEMODE)) < 0) err(EX_CANTCREAT, "%s", cf_log); } } /* De-initialize & close logging */ static void logdeinit() { if (cf_logfd < 0) { closelog(); } else { close(cf_logfd); cf_logfd = -1; } } /* Log a message */ static void logwr(int pri, const char *fmt, ...) { char *str, *str2; va_list ap; va_start(ap, fmt); if (cf_logfd < 0) { vsyslog(pri, fmt, ap); } else { if (vasprintf(&str, fmt, ap) < 0) goto err; if (asprintf(&str2, "%s: %s\n", timestr_abs(time((time_t *) NULL), cf_timefmt, 0), str) < 0) { free(str); goto err; } /* * Write a log entry to log file. We do not do any error * checking because there is not much we can do if logging * fails (most likely due to disk full situation) as we * can not log the error. Even if logging fails, it still * makes sense to keep running and updating timestamps. */ if (write(cf_logfd, str2, strlen(str2)) < 0) /* do nothing but keep compiler happy */ ; free(str2); free(str); } err: va_end(ap); } /* Usage help & exit */ static void usage() { fputs("usage: " PROGNAME " [-DFvS] [-d datadir] [-f timefmt] " "[-l log] [-p pidfile] [-s sleep]\n", stderr); exit(EX_USAGE); } /* Output version information, default settings & exit */ static void version() { puts(PROGNAME " " PROGVERSION " - system downtime reporting daemon\n"); puts("Copyright (c) 2009-2016 Janne Snabb. " "All rights reserved."); puts("This software is licensed under the terms and conditions of the"); puts("Simplified BSD License. You should have received a copy of that"); puts("license along with this software.\n"); puts("Default settings:"); printf(" log = %s\n", cf_log); printf(" pidfile = %s\n", cf_pidfile); printf(" datadir = %s\n", cf_datadir); printf(" downtimedbfile = %s\n", cf_downtimedbfile); printf(" sleep = %ld\n", cf_sleep); #ifdef HAVE_FUTIMES printf(" fsync = %d\n", cf_fsync); #endif printf(" timefmt = %s\n", cf_timefmt); #ifdef PACKAGE_URL puts("\nSee the following web site for more information and updates:"); puts(" " PACKAGE_URL "\n"); #endif exit(EX_OK); } /* Handle command line arguments */ static void parseargs(int argc, char *argv[]) { int c; char *p; while ((c = getopt(argc, argv, "Dd:Ff:l:p:s:Svh?")) != -1) { switch (c) { case 'D': cf_downtimedb = 0; break; case 'd': cf_datadir = optarg; break; case 'F': cf_fork = 0; break; case 'f': cf_timefmt = optarg; break; case 'l': cf_log = optarg; break; case 'p': cf_pidfile = optarg; break; case 's': p = NULL; errno = 0; cf_sleep = strtol(optarg, &p, 10); if ((p != NULL && *p != '\0') || errno != 0) errx(EX_USAGE, "-s argument is not a number"); break; case 'S': #ifdef HAVE_FUTIMES cf_fsync = 0; #endif break; case 'v': version(); /* NOTREACHED */ break; case 'h': case '?': default: usage(); /* NOTREACHED */ break; } } if (argc != optind) usage(); } /* * Create a pid file, return -1 on error. This is loosely based on * FreeBSD flopen.c. */ static int makepidfile() { char str[100]; struct stat sb, sb2; int fd; if (strcmp(cf_pidfile, "none") == 0) { return (0); } retry: if ((fd = open(cf_pidfile, O_WRONLY | O_CREAT, DEFFILEMODE)) < 0) { logwr(LOG_ERR, "can not open pid file %s: %s", cf_pidfile, strerror(errno)); return (-1); } #ifdef HAVE_FLOCK if (flock(fd, LOCK_EX | LOCK_NB) < 0) { #else if (lockf(fd, F_TLOCK, 0) < 0) { #endif if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EACCES) logwr(LOG_ERR, "another process is running already"); else logwr(LOG_ERR, "can not lock pid file %s: %s", cf_pidfile, strerror(errno)); close(fd); return (-1); } if (stat(cf_pidfile, &sb) < 0) { /* disappeared from under our feet */ close(fd); goto retry; } if (fstat(fd, &sb2) < 0) { /* can not happen :) */ logwr(LOG_ERR, "can not fstat pid file %s: %s", cf_pidfile, strerror(errno)); close(fd); return (-1); } if (sb.st_dev != sb2.st_dev || sb.st_ino != sb2.st_ino) { /* changed under our feet */ close(fd); goto retry; } if (ftruncate(fd, 0) < 0) { /* should not happen */ logwr(LOG_ERR, "can not ftruncate pid file %s: %s", cf_pidfile, strerror(errno)); close(fd); return (-1); } snprintf(str, sizeof(str), "%ld\n", (long)getpid()); if (write(fd, str, strlen(str)) != strlen(str)) { logwr(LOG_ERR, "can not write pid file %s: %s", cf_pidfile, strerror(errno)); close(fd); return (-1); } /* * NOTE: we leave the file open (although we can not access the * file descriptor any more) so that we can retain the lock as * long as the process is alive. */ return (0); } /* Remove pid file */ static void removepidfile() { if (strcmp(cf_pidfile, "none") == 0) { return; } /* * We are not releasing the lock. We still have an open * file descriptor to the unlink()ed file, but that is sorted * out when we exit shortly after calling this function. */ unlink(cf_pidfile); } /* Compatibility daemon(3) function for OSes which lack it */ #ifndef HAVE_DAEMON static int daemon(int nochdir, int noclose) { int fd; switch (fork()) { case -1: return (-1); case 0: break; default: exit(0); } if (setsid() == -1) return (-1); if (!nochdir) chdir("/"); if (!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) close(fd); } return (0); } #endif /* !HAVE_DAEMON */ /* eof */