Logo Search packages:      
Sourcecode: nut version File versions

upssched.c

/* 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 {
      char  *name;
      time_t      etime;
      void  *next;
}     ttype_t;

      ttype_t     *thead = NULL;
      static      struct      conn_t      *connhead = NULL;
      char  *cmdscript = NULL, *pipefn = NULL, *lockfn = NULL;
      int   verbose = 0;            /* use for debugging */

      struct  sigaction sa;
      sigset_t nut_upssched_sigmask;

      /* 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(struct conn_t *target)
{
      struct      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(struct 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;
      struct      conn_t      *tmp, *last;
      struct      sockaddr_un saddr;
      socklen_t   salen;

      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(struct 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(struct 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(struct 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;
      struct      conn_t      *tmp, *tmpnext;
      socklen_t   fromlen;

      fromlen = sizeof(struct sockaddr);

      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(&sa, '\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)
{
      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)));

      snprintfcat(buf, sizeof(buf), "\n");

      /* 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, buf, strlen(buf));

            /* if we can't send the whole thing, loop back and try again */
            if ((ret < 1) || (ret != (int) strlen(buf))) {
                  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)
{
      verbose = 1;            /* TODO: remove when done testing */

      /* normally we don't have stderr, so get this going to syslog early */
      openlog("upssched", LOG_PID, LOG_DAEMON);
      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);
}

Generated by  Doxygen 1.6.0   Back to index