/* upssched.c - upsmon's scheduling helper for offset timers Copyright (C) 2000 Russell Kroll <rkroll@exploits.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* design notes for the curious: * * 1. we get called with a upsname and notifytype from upsmon * 2. the config file is searched for an AT condition that matches * 3. the conditions on any matching lines are parsed * * starting a timer: the timer is added to the daemon's timer queue * cancelling a timer: the timer is removed from that queue * execute a command: the command is passed straight to the cmdscript * * if the daemon is not already running and is required (to start a timer) * it will be started automatically * * when the time arrives, the command associated with a timer will be * executed by the daemon (via the cmdscript) * * timers can be cancelled at any time before they trigger * * the daemon will shut down automatically when no more timers are active * */ #include "common.h" #include <sys/types.h> #include <sys/wait.h> #include <sys/socket.h> #include <sys/un.h> #include <netinet/in.h> #include "upssched.h" #include "timehead.h" typedef struct ttype_s { char *name; time_t etime; struct ttype_s *next; } ttype_t; ttype_t *thead = NULL; static conn_t *connhead = NULL; char *cmdscript = NULL, *pipefn = NULL, *lockfn = NULL; int verbose = 0; /* use for debugging */ /* ups name and notify type (string) as received from upsmon */ const char *upsname, *notify_type; #define PARENT_STARTED -2 #define PARENT_UNNECESSARY -3 #define MAX_TRIES 30 #define EMPTY_WAIT 15 /* min passes with no timers to exit */ #define US_LISTEN_BACKLOG 16 #define US_SOCK_BUF_LEN 256 #define US_MAX_READ 128 /* --- server functions --- */ static void exec_cmd(const char *cmd) { int err; char buf[LARGEBUF]; snprintf(buf, sizeof(buf), "%s %s", cmdscript, cmd); err = system(buf); if (WIFEXITED(err)) { if (WEXITSTATUS(err)) { upslogx(LOG_INFO, "exec_cmd(%s) returned %d", buf, WEXITSTATUS(err)); } } else { if (WIFSIGNALED(err)) { upslogx(LOG_WARNING, "exec_cmd(%s) terminated with signal %d", buf, WTERMSIG(err)); } else { upslogx(LOG_ERR, "Execute command failure: %s", buf); } } return; } static void removetimer(ttype_t *tfind) { ttype_t *tmp, *last; last = NULL; tmp = thead; while (tmp) { if (tmp == tfind) { /* found it */ if (last == NULL) /* deleting first */ thead = tmp->next; else last->next = tmp->next; free(tmp->name); free(tmp); return; } last = tmp; tmp = tmp->next; } /* this one should never happen */ upslogx(LOG_ERR, "removetimer: failed to locate target at %p", (void *)tfind); } static void checktimers(void) { ttype_t *tmp, *tmpnext; time_t now; static int emptyctr = 0; /* if the queue is empty we might be ready to exit */ if (!thead) { emptyctr++; /* wait a little while in case someone wants us again */ if (emptyctr < EMPTY_WAIT) return; if (verbose) upslogx(LOG_INFO, "Timer queue empty, exiting"); #ifdef UPSSCHED_RACE_TEST upslogx(LOG_INFO, "triggering race: sleeping 15 sec before exit"); sleep(15); #endif unlink(pipefn); exit(EXIT_SUCCESS); } emptyctr = 0; /* flip through LL, look for activity */ tmp = thead; time(&now); while (tmp) { tmpnext = tmp->next; if (now >= tmp->etime) { if (verbose) upslogx(LOG_INFO, "Event: %s ", tmp->name); exec_cmd(tmp->name); /* delete from queue */ removetimer(tmp); } tmp = tmpnext; } } static void start_timer(const char *name, const char *ofsstr) { time_t now; int ofs; ttype_t *tmp, *last; /* get the time */ time(&now); /* add an event for <now> + <time> */ ofs = strtol(ofsstr, (char **) NULL, 10); if (ofs < 0) { upslogx(LOG_INFO, "bogus offset for timer, ignoring"); return; } if (verbose) upslogx(LOG_INFO, "New timer: %s (%d seconds)", name, ofs); /* now add to the queue */ tmp = last = thead; while (tmp) { last = tmp; tmp = tmp->next; } tmp = xmalloc(sizeof(ttype_t)); tmp->name = xstrdup(name); tmp->etime = now + ofs; tmp->next = NULL; if (last) last->next = tmp; else thead = tmp; } static void cancel_timer(const char *name, const char *cname) { ttype_t *tmp; for (tmp = thead; tmp != NULL; tmp = tmp->next) { if (!strcmp(tmp->name, name)) { /* match */ if (verbose) upslogx(LOG_INFO, "Cancelling timer: %s", name); removetimer(tmp); return; } } /* this is not necessarily an error */ if (cname && cname[0]) { if (verbose) upslogx(LOG_INFO, "Cancel %s, event: %s", name, cname); exec_cmd(cname); } } static void us_serialize(int op) { static int pipefd[2]; int ret; char ch; switch(op) { case SERIALIZE_INIT: ret = pipe(pipefd); if (ret != 0) fatal_with_errno(EXIT_FAILURE, "serialize: pipe"); break; case SERIALIZE_SET: close(pipefd[0]); close(pipefd[1]); break; case SERIALIZE_WAIT: close(pipefd[1]); ret = read(pipefd[0], &ch, 1); close(pipefd[0]); break; } } static int open_sock(void) { int ret, fd; struct sockaddr_un ssaddr; fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) fatal_with_errno(EXIT_FAILURE, "Can't create a unix domain socket"); ssaddr.sun_family = AF_UNIX; snprintf(ssaddr.sun_path, sizeof(ssaddr.sun_path), "%s", pipefn); unlink(pipefn); umask(0007); ret = bind(fd, (struct sockaddr *) &ssaddr, sizeof ssaddr); if (ret < 0) fatal_with_errno(EXIT_FAILURE, "bind %s failed", pipefn); ret = chmod(pipefn, 0660); if (ret < 0) fatal_with_errno(EXIT_FAILURE, "chmod(%s, 0660) failed", pipefn); ret = listen(fd, US_LISTEN_BACKLOG); if (ret < 0) fatal_with_errno(EXIT_FAILURE, "listen(%d, %d) failed", fd, US_LISTEN_BACKLOG); return fd; } static void conn_del(conn_t *target) { conn_t *tmp, *last = NULL; tmp = connhead; while (tmp) { if (tmp == target) { if (last) last->next = tmp->next; else connhead = tmp->next; pconf_finish(&tmp->ctx); free(tmp); return; } last = tmp; tmp = tmp->next; } upslogx(LOG_ERR, "Tried to delete a bogus state connection"); } static int send_to_one(conn_t *conn, const char *fmt, ...) { int ret; va_list ap; char buf[US_SOCK_BUF_LEN]; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); ret = write(conn->fd, buf, strlen(buf)); if ((ret < 1) || (ret != (int) strlen(buf))) { upsdebugx(2, "write to fd %d failed", conn->fd); close(conn->fd); conn_del(conn); return 0; /* failed */ } return 1; /* OK */ } static void conn_add(int sockfd) { int acc, ret; conn_t *tmp, *last; struct sockaddr_un saddr; #if defined(__hpux) && !defined(_XOPEN_SOURCE_EXTENDED) int salen; #else socklen_t salen; #endif salen = sizeof(saddr); acc = accept(sockfd, (struct sockaddr *) &saddr, &salen); if (acc < 0) { upslog_with_errno(LOG_ERR, "accept on unix fd failed"); return; } /* enable nonblocking I/O */ ret = fcntl(acc, F_GETFL, 0); if (ret < 0) { upslog_with_errno(LOG_ERR, "fcntl get on unix fd failed"); close(acc); return; } ret = fcntl(acc, F_SETFL, ret | O_NDELAY); if (ret < 0) { upslog_with_errno(LOG_ERR, "fcntl set O_NDELAY on unix fd failed"); close(acc); return; } tmp = last = connhead; while (tmp) { last = tmp; tmp = tmp->next; } tmp = xmalloc(sizeof(conn_t)); tmp->fd = acc; tmp->next = NULL; if (last) last->next = tmp; else connhead = tmp; upsdebugx(3, "new connection on fd %d", acc); pconf_init(&tmp->ctx, NULL); } static int sock_arg(conn_t *conn) { if (conn->ctx.numargs < 1) return 0; /* CANCEL <name> [<cmd>] */ if (!strcmp(conn->ctx.arglist[0], "CANCEL")) { if (conn->ctx.numargs < 3) cancel_timer(conn->ctx.arglist[1], NULL); else cancel_timer(conn->ctx.arglist[1], conn->ctx.arglist[2]); send_to_one(conn, "OK\n"); return 1; } if (conn->ctx.numargs < 3) return 0; /* START <name> <length> */ if (!strcmp(conn->ctx.arglist[0], "START")) { start_timer(conn->ctx.arglist[1], conn->ctx.arglist[2]); send_to_one(conn, "OK\n"); return 1; } /* unknown */ return 0; } static void log_unknown(int numarg, char **arg) { int i; upslogx(LOG_INFO, "Unknown command on socket: "); for (i = 0; i < numarg; i++) upslogx(LOG_INFO, "arg %d: %s", i, arg[i]); } static int sock_read(conn_t *conn) { int i, ret; char ch; for (i = 0; i < US_MAX_READ; i++) { ret = read(conn->fd, &ch, 1); if (ret < 1) { /* short read = no parsing, come back later */ if ((ret == -1) && (errno == EAGAIN)) return 0; /* some other problem */ return -1; /* error */ } ret = pconf_char(&conn->ctx, ch); if (ret == 0) /* nothing to parse yet */ continue; if (ret == -1) { upslogx(LOG_NOTICE, "Parse error on sock: %s", conn->ctx.errmsg); return 0; /* nothing parsed */ } /* try to use it, and complain about unknown commands */ if (!sock_arg(conn)) { log_unknown(conn->ctx.numargs, conn->ctx.arglist); send_to_one(conn, "ERR UNKNOWN\n"); } return 1; /* we did some work */ } return 0; /* fell out without parsing anything */ } static void start_daemon(int lockfd) { int maxfd, pid, pipefd, ret; struct timeval tv; fd_set rfds; conn_t *tmp, *tmpnext; us_serialize(SERIALIZE_INIT); if ((pid = fork()) < 0) fatal_with_errno(EXIT_FAILURE, "Unable to enter background"); if (pid != 0) { /* parent */ /* wait for child to set up the listener */ us_serialize(SERIALIZE_WAIT); return; } /* child */ close(0); close(1); close(2); /* make fds 0-2 point somewhere defined */ if (open("/dev/null", O_RDWR) != 0) fatal_with_errno(EXIT_FAILURE, "open /dev/null"); if (dup(0) == -1) fatal_with_errno(EXIT_FAILURE, "dup"); if (dup(0) == -1) fatal_with_errno(EXIT_FAILURE, "dup"); pipefd = open_sock(); if (verbose) upslogx(LOG_INFO, "Timer daemon started"); /* release the parent */ us_serialize(SERIALIZE_SET); /* drop the lock now that the background is running */ unlink(lockfn); close(lockfd); /* now watch for activity */ for (;;) { /* wait at most 1s so we can check our timers regularly */ tv.tv_sec = 1; tv.tv_usec = 0; FD_ZERO(&rfds); FD_SET(pipefd, &rfds); maxfd = pipefd; for (tmp = connhead; tmp != NULL; tmp = tmp->next) { FD_SET(tmp->fd, &rfds); if (tmp->fd > maxfd) maxfd = tmp->fd; } ret = select(maxfd + 1, &rfds, NULL, NULL, &tv); if (ret > 0) { if (FD_ISSET(pipefd, &rfds)) conn_add(pipefd); tmp = connhead; while (tmp) { tmpnext = tmp->next; if (FD_ISSET(tmp->fd, &rfds)) { if (sock_read(tmp) < 0) { close(tmp->fd); conn_del(tmp); } } tmp = tmpnext; } } checktimers(); } } /* --- 'client' functions --- */ static int try_connect(void) { int pipefd, ret; struct sockaddr_un saddr; memset(&saddr, '\0', sizeof(saddr)); saddr.sun_family = AF_UNIX; snprintf(saddr.sun_path, sizeof(saddr.sun_path), "%s", pipefn); pipefd = socket(AF_UNIX, SOCK_STREAM, 0); if (pipefd < 0) fatal_with_errno(EXIT_FAILURE, "socket"); ret = connect(pipefd, (const struct sockaddr *) &saddr, sizeof(saddr)); if (ret != -1) return pipefd; return -1; } static int get_lock(const char *fn) { return open(fn, O_RDONLY | O_CREAT | O_EXCL, 0); } /* try to connect to bg process, and start one if necessary */ static int check_parent(const char *cmd, const char *arg2) { int pipefd, lockfd, tries = 0; for (tries = 0; tries < MAX_TRIES; tries++) { pipefd = try_connect(); if (pipefd != -1) return pipefd; /* timer daemon isn't running */ /* it's not running, so there's nothing to cancel */ if (!strcmp(cmd, "CANCEL") && (arg2 == NULL)) return PARENT_UNNECESSARY; /* arg2 non-NULL means there is a cancel action available */ /* we need to start the daemon, so try to get the lock */ lockfd = get_lock(lockfn); if (lockfd != -1) { start_daemon(lockfd); return PARENT_STARTED; /* started successfully */ } /* we didn't get the lock - must be two upsscheds running */ /* blow this away in case we crashed before */ unlink(lockfn); /* give the other one a chance to start it, then try again */ usleep(250000); } upslog_with_errno(LOG_ERR, "Failed to connect to parent and failed to create parent"); exit(EXIT_FAILURE); } static void read_timeout(int sig) { /* ignore this */ return; } static void setup_sigalrm(void) { struct sigaction sa; sigset_t nut_upssched_sigmask; sigemptyset(&nut_upssched_sigmask); sa.sa_mask = nut_upssched_sigmask; sa.sa_flags = 0; sa.sa_handler = read_timeout; sigaction(SIGALRM, &sa, NULL); } static void sendcmd(const char *cmd, const char *arg1, const char *arg2) { int i, pipefd, ret; char buf[SMALLBUF], enc[SMALLBUF]; /* insanity */ if (!arg1) return; /* build the request */ snprintf(buf, sizeof(buf), "%s \"%s\"", cmd, pconf_encode(arg1, enc, sizeof(enc))); if (arg2) snprintfcat(buf, sizeof(buf), " \"%s\"", pconf_encode(arg2, enc, sizeof(enc))); snprintf(enc, sizeof(enc), "%s\n", buf); /* see if the parent needs to be started (and maybe start it) */ for (i = 0; i < MAX_TRIES; i++) { pipefd = check_parent(cmd, arg2); if (pipefd == PARENT_STARTED) { /* loop back and try to connect now */ usleep(250000); continue; } /* special case for CANCEL when no parent is running */ if (pipefd == PARENT_UNNECESSARY) return; /* we're connected now */ ret = write(pipefd, enc, strlen(enc)); /* if we can't send the whole thing, loop back and try again */ if ((ret < 1) || (ret != (int) strlen(enc))) { upslogx(LOG_ERR, "write failed, trying again"); close(pipefd); continue; } /* ugh - probably should use select here... */ setup_sigalrm(); alarm(2); ret = read(pipefd, buf, sizeof(buf)); alarm(0); signal(SIGALRM, SIG_IGN); close(pipefd); /* same idea: no OK = go try it all again */ if (ret < 2) { upslogx(LOG_ERR, "read confirmation failed, trying again"); continue; } if (!strncmp(buf, "OK", 2)) return; /* success */ upslogx(LOG_ERR, "read confirmation got [%s]", buf); /* try again ... */ } fatalx(EXIT_FAILURE, "Unable to connect to daemon and unable to start daemon"); } static void parse_at(const char *ntype, const char *un, const char *cmd, const char *ca1, const char *ca2) { /* complain both ways in case we don't have a tty */ if (!cmdscript) { printf("CMDSCRIPT must be set before any ATs in the config file!\n"); fatalx(EXIT_FAILURE, "CMDSCRIPT must be set before any ATs in the config file!"); } if (!pipefn) { printf("PIPEFN must be set before any ATs in the config file!\n"); fatalx(EXIT_FAILURE, "PIPEFN must be set before any ATs in the config file!"); } if (!lockfn) { printf("LOCKFN must be set before any ATs in the config file!\n"); fatalx(EXIT_FAILURE, "LOCKFN must be set before any ATs in the config file!"); } /* check upsname: does this apply to us? */ if (strcmp(upsname, un) != 0) if (strcmp(un, "*") != 0) return; /* not for us, and not the wildcard */ /* see if the current notify type matches the one from the .conf */ if (strcasecmp(notify_type, ntype) != 0) return; /* if command is valid, send it to the daemon (which may start it) */ if (!strcmp(cmd, "START-TIMER")) { sendcmd("START", ca1, ca2); return; } if (!strcmp(cmd, "CANCEL-TIMER")) { sendcmd("CANCEL", ca1, ca2); return; } if (!strcmp(cmd, "EXECUTE")) { if (ca1 == '\0') { upslogx(LOG_ERR, "Empty EXECUTE command argument"); return; } if (verbose) upslogx(LOG_INFO, "Executing command: %s", ca1); exec_cmd(ca1); return; } upslogx(LOG_ERR, "Invalid command: %s", cmd); } static int conf_arg(int numargs, char **arg) { if (numargs < 2) return 0; /* CMDSCRIPT <scriptname> */ if (!strcmp(arg[0], "CMDSCRIPT")) { cmdscript = xstrdup(arg[1]); return 1; } /* PIPEFN <pipename> */ if (!strcmp(arg[0], "PIPEFN")) { pipefn = xstrdup(arg[1]); return 1; } /* LOCKFN <filename> */ if (!strcmp(arg[0], "LOCKFN")) { lockfn = xstrdup(arg[1]); return 1; } if (numargs < 5) return 0; /* AT <notifytype> <upsname> <command> <cmdarg1> [<cmdarg2>] */ if (!strcmp(arg[0], "AT")) { /* don't use arg[5] unless we have it... */ if (numargs > 5) parse_at(arg[1], arg[2], arg[3], arg[4], arg[5]); else parse_at(arg[1], arg[2], arg[3], arg[4], NULL); return 1; } return 0; } /* called for fatal errors in parseconf like malloc failures */ static void upssched_err(const char *errmsg) { upslogx(LOG_ERR, "Fatal error in parseconf(upssched.conf): %s", errmsg); } static void checkconf(void) { char fn[SMALLBUF]; PCONF_CTX_t ctx; snprintf(fn, sizeof(fn), "%s/upssched.conf", confpath()); pconf_init(&ctx, upssched_err); if (!pconf_file_begin(&ctx, fn)) { pconf_finish(&ctx); fatalx(EXIT_FAILURE, "%s", ctx.errmsg); } while (pconf_file_next(&ctx)) { if (pconf_parse_error(&ctx)) { upslogx(LOG_ERR, "Parse error: %s:%d: %s", fn, ctx.linenum, ctx.errmsg); continue; } if (ctx.numargs < 1) continue; if (!conf_arg(ctx.numargs, ctx.arglist)) { unsigned int i; char errmsg[SMALLBUF]; snprintf(errmsg, sizeof(errmsg), "upssched.conf: invalid directive"); for (i = 0; i < ctx.numargs; i++) snprintfcat(errmsg, sizeof(errmsg), " %s", ctx.arglist[i]); upslogx(LOG_WARNING, "%s", errmsg); } } pconf_finish(&ctx); } int main(int argc, char **argv) { const char *prog = xbasename(argv[0]); verbose = 1; /* TODO: remove when done testing */ /* normally we don't have stderr, so get this going to syslog early */ open_syslog(prog); syslogbit_set(); upsname = getenv("UPSNAME"); notify_type = getenv("NOTIFYTYPE"); if ((!upsname) || (!notify_type)) { printf("Error: UPSNAME and NOTIFYTYPE must be set.\n"); printf("This program should only be run from upsmon.\n"); exit(EXIT_FAILURE); } /* see if this matches anything in the config file */ checkconf(); exit(EXIT_SUCCESS); }