nut-debian/clients/upssched.c
2012-08-12 23:39:31 +02:00

925 lines
18 KiB
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 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);
}