/* dstate.c - Network UPS Tools driver-side state management Copyright (C) 2003 Russell Kroll 2008 Arjen de Korte 2012 Arnaud Quette 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 */ #include #include #include #include #include #include #include #include "common.h" #include "dstate.h" #include "state.h" #include "parseconf.h" static int sockfd = -1, stale = 1, alarm_active = 0, ignorelb = 0; static char *sockfn = NULL; static char status_buf[ST_MAX_VALUE_LEN], alarm_buf[ST_MAX_VALUE_LEN]; static st_tree_t *dtree_root = NULL; static conn_t *connhead = NULL; static cmdlist_t *cmdhead = NULL; struct ups_handler upsh; /* this may be a frequent stumbling point for new users, so be verbose here */ static void sock_fail(const char *fn) { int sockerr; struct passwd *user; /* save this so it doesn't get overwritten */ sockerr = errno; /* dispense with the usual upslog stuff since we have stderr here */ printf("\nFatal error: unable to create listener socket\n\n"); printf("bind %s failed: %s\n", fn, strerror(sockerr)); user = getpwuid(getuid()); if (!user) { fatal_with_errno(EXIT_FAILURE, "getpwuid"); } /* deal with some common problems */ switch (errno) { case EACCES: printf("\nCurrent user: %s (UID %d)\n\n", user->pw_name, (int)user->pw_uid); printf("Things to try:\n\n"); printf(" - set different owners or permissions on %s\n\n", dflt_statepath()); printf(" - run this as some other user " "(try -u )\n"); break; case ENOENT: printf("\nThings to try:\n\n"); printf(" - mkdir %s\n", dflt_statepath()); break; case ENOTDIR: printf("\nThings to try:\n\n"); printf(" - rm %s\n\n", dflt_statepath()); printf(" - mkdir %s\n", dflt_statepath()); break; } /* * there - that wasn't so bad. every helpful line of code here * prevents one more "help me" mail to the list a year from now */ printf("\n"); fatalx(EXIT_FAILURE, "Exiting."); } static int sock_open(const char *fn) { 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"); } /* keep this around for the unlink() when exiting */ sockfn = xstrdup(fn); ssaddr.sun_family = AF_UNIX; snprintf(ssaddr.sun_path, sizeof(ssaddr.sun_path), "%s", sockfn); unlink(sockfn); /* group gets access so upsd can be a different user but same group */ umask(0007); ret = bind(fd, (struct sockaddr *) &ssaddr, sizeof ssaddr); if (ret < 0) { sock_fail(sockfn); } ret = chmod(sockfn, 0660); if (ret < 0) { fatal_with_errno(EXIT_FAILURE, "chmod(%s, 0660) failed", sockfn); } ret = listen(fd, DS_LISTEN_BACKLOG); if (ret < 0) { fatal_with_errno(EXIT_FAILURE, "listen(%d, %d) failed", fd, DS_LISTEN_BACKLOG); } return fd; } static void sock_disconnect(conn_t *conn) { close(conn->fd); pconf_finish(&conn->ctx); if (conn->prev) { conn->prev->next = conn->next; } else { connhead = conn->next; } if (conn->next) { conn->next->prev = conn->prev; } else { /* conntail = conn->prev; */ } free(conn); } static void send_to_all(const char *fmt, ...) { int ret; char buf[ST_SOCK_BUF_LEN]; va_list ap; conn_t *conn, *cnext; va_start(ap, fmt); ret = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (ret < 1) { upsdebugx(2, "%s: nothing to write", __func__); return; } upsdebugx(5, "%s: %.*s", __func__, ret-1, buf); for (conn = connhead; conn; conn = cnext) { cnext = conn->next; ret = write(conn->fd, buf, strlen(buf)); if (ret != (int)strlen(buf)) { upsdebugx(2, "write %d bytes to socket %d failed", (int)strlen(buf), conn->fd); sock_disconnect(conn); } } } static int send_to_one(conn_t *conn, const char *fmt, ...) { int ret; va_list ap; char buf[ST_SOCK_BUF_LEN]; va_start(ap, fmt); ret = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (ret < 1) { upsdebugx(2, "%s: nothing to write", __func__); return 1; } upsdebugx(5, "%s: %.*s", __func__, ret-1, buf); ret = write(conn->fd, buf, strlen(buf)); if (ret != (int)strlen(buf)) { upsdebugx(2, "write %d bytes to socket %d failed", (int)strlen(buf), conn->fd); sock_disconnect(conn); return 0; /* failed */ } return 1; /* OK */ } static void sock_connect(int sock) { int fd, ret; conn_t *conn; struct sockaddr_un sa; #if defined(__hpux) && !defined(_XOPEN_SOURCE_EXTENDED) int salen; #else socklen_t salen; #endif salen = sizeof(sa); fd = accept(sock, (struct sockaddr *) &sa, &salen); if (fd < 0) { upslog_with_errno(LOG_ERR, "accept on unix fd failed"); return; } /* enable nonblocking I/O */ if (!do_synchronous) { ret = fcntl(fd, F_GETFL, 0); if (ret < 0) { upslog_with_errno(LOG_ERR, "fcntl get on unix fd failed"); close(fd); return; } ret = fcntl(fd, F_SETFL, ret | O_NDELAY); if (ret < 0) { upslog_with_errno(LOG_ERR, "fcntl set O_NDELAY on unix fd failed"); close(fd); return; } } conn = xcalloc(1, sizeof(*conn)); conn->fd = fd; pconf_init(&conn->ctx, NULL); if (connhead) { conn->next = connhead; connhead->prev = conn; } connhead = conn; upsdebugx(3, "new connection on fd %d", fd); } static int st_tree_dump_conn(st_tree_t *node, conn_t *conn) { int ret; enum_t *etmp; range_t *rtmp; if (!node) { return 1; /* not an error */ } if (node->left) { ret = st_tree_dump_conn(node->left, conn); if (!ret) { return 0; /* write failed in the child */ } } if (!send_to_one(conn, "SETINFO %s \"%s\"\n", node->var, node->val)) { return 0; /* write failed, bail out */ } /* send any enums */ for (etmp = node->enum_list; etmp; etmp = etmp->next) { if (!send_to_one(conn, "ADDENUM %s \"%s\"\n", node->var, etmp->val)) { return 0; } } /* send any ranges */ for (rtmp = node->range_list; rtmp; rtmp = rtmp->next) { if (!send_to_one(conn, "ADDRANGE %s %i %i\n", node->var, rtmp->min, rtmp->max)) { return 0; } } /* provide any auxiliary data */ if (node->aux) { if (!send_to_one(conn, "SETAUX %s %d\n", node->var, node->aux)) { return 0; } } /* finally report any flags */ if (node->flags) { char flist[SMALLBUF]; /* build the list */ snprintf(flist, sizeof(flist), "%s", node->var); if (node->flags & ST_FLAG_RW) { snprintfcat(flist, sizeof(flist), " RW"); } if (node->flags & ST_FLAG_STRING) { snprintfcat(flist, sizeof(flist), " STRING"); } if (!send_to_one(conn, "SETFLAGS %s\n", flist)) { return 0; } } if (node->right) { return st_tree_dump_conn(node->right, conn); } return 1; /* everything's OK here ... */ } static int cmd_dump_conn(conn_t *conn) { cmdlist_t *cmd; for (cmd = cmdhead; cmd; cmd = cmd->next) { if (!send_to_one(conn, "ADDCMD %s\n", cmd->name)) { return 0; } } return 1; } static int sock_arg(conn_t *conn, int numarg, char **arg) { if (numarg < 1) { return 0; } if (!strcasecmp(arg[0], "DUMPALL")) { /* first thing: the staleness flag */ if ((stale == 1) && !send_to_one(conn, "DATASTALE\n")) { return 1; } if (!st_tree_dump_conn(dtree_root, conn)) { return 1; } if (!cmd_dump_conn(conn)) { return 1; } if ((stale == 0) && !send_to_one(conn, "DATAOK\n")) { return 1; } send_to_one(conn, "DUMPDONE\n"); return 1; } if (!strcasecmp(arg[0], "PING")) { send_to_one(conn, "PONG\n"); return 1; } if (numarg < 2) { return 0; } /* INSTCMD []*/ if (!strcasecmp(arg[0], "INSTCMD")) { /* try the new handler first if present */ if (upsh.instcmd) { if (numarg > 2) { upsh.instcmd(arg[1], arg[2]); return 1; } upsh.instcmd(arg[1], NULL); return 1; } upslogx(LOG_NOTICE, "Got INSTCMD, but driver lacks a handler"); return 1; } if (numarg < 3) { return 0; } /* SET */ if (!strcasecmp(arg[0], "SET")) { /* try the new handler first if present */ if (upsh.setvar) { upsh.setvar(arg[1], arg[2]); return 1; } upslogx(LOG_NOTICE, "Got SET, but driver lacks a handler"); return 1; } /* unknown */ return 0; } static void sock_read(conn_t *conn) { int i, ret; char buf[SMALLBUF]; ret = read(conn->fd, buf, sizeof(buf)); if (ret < 0) { switch(errno) { case EINTR: case EAGAIN: return; default: sock_disconnect(conn); return; } } for (i = 0; i < ret; i++) { switch(pconf_char(&conn->ctx, buf[i])) { case 0: /* nothing to parse yet */ continue; case 1: /* try to use it, and complain about unknown commands */ if (!sock_arg(conn, conn->ctx.numargs, conn->ctx.arglist)) { size_t arg; upslogx(LOG_INFO, "Unknown command on socket: "); for (arg = 0; arg < conn->ctx.numargs; arg++) { upslogx(LOG_INFO, "arg %d: %s", (int)arg, conn->ctx.arglist[arg]); } } continue; default: /* nothing parsed */ upslogx(LOG_NOTICE, "Parse error on sock: %s", conn->ctx.errmsg); return; } } } static void sock_close(void) { conn_t *conn, *cnext; if (sockfd != -1) { close(sockfd); sockfd = -1; if (sockfn) { unlink(sockfn); free(sockfn); sockfn = NULL; } } for (conn = connhead; conn; conn = cnext) { cnext = conn->next; sock_disconnect(conn); } connhead = NULL; /* conntail = NULL; */ } /* interface */ void dstate_init(const char *prog, const char *devname) { char sockname[SMALLBUF]; /* do this here for now */ signal(SIGPIPE, SIG_IGN); if (devname) { snprintf(sockname, sizeof(sockname), "%s/%s-%s", dflt_statepath(), prog, devname); } else { snprintf(sockname, sizeof(sockname), "%s/%s", dflt_statepath(), prog); } sockfd = sock_open(sockname); upsdebugx(2, "dstate_init: sock %s open on fd %d", sockname, sockfd); } /* returns 1 if timeout expired or data is available on UPS fd, 0 otherwise */ int dstate_poll_fds(struct timeval timeout, int extrafd) { int ret, maxfd, overrun = 0; fd_set rfds; struct timeval now; conn_t *conn, *cnext; FD_ZERO(&rfds); FD_SET(sockfd, &rfds); maxfd = sockfd; if (extrafd != -1) { FD_SET(extrafd, &rfds); if (extrafd > maxfd) { maxfd = extrafd; } } for (conn = connhead; conn; conn = conn->next) { FD_SET(conn->fd, &rfds); if (conn->fd > maxfd) { maxfd = conn->fd; } } gettimeofday(&now, NULL); /* number of microseconds should always be positive */ if (timeout.tv_usec < now.tv_usec) { timeout.tv_sec -= 1; timeout.tv_usec += 1000000; } if (timeout.tv_sec < now.tv_sec) { timeout.tv_sec = 0; timeout.tv_usec = 0; overrun = 1; /* no time left */ } else { timeout.tv_sec -= now.tv_sec; timeout.tv_usec -= now.tv_usec; } ret = select(maxfd + 1, &rfds, NULL, NULL, &timeout); if (ret == 0) { return 1; /* timer expired */ } if (ret < 0) { switch (errno) { case EINTR: case EAGAIN: /* ignore interruptions from signals */ break; default: upslog_with_errno(LOG_ERR, "select unix sockets failed"); } return overrun; } if (FD_ISSET(sockfd, &rfds)) { sock_connect(sockfd); } for (conn = connhead; conn; conn = cnext) { cnext = conn->next; if (FD_ISSET(conn->fd, &rfds)) { sock_read(conn); } } /* tell the caller if that fd woke up */ if ((extrafd != -1) && (FD_ISSET(extrafd, &rfds))) { return 1; } return overrun; } int dstate_setinfo(const char *var, const char *fmt, ...) { int ret; char value[ST_MAX_VALUE_LEN]; va_list ap; va_start(ap, fmt); vsnprintf(value, sizeof(value), fmt, ap); va_end(ap); ret = state_setinfo(&dtree_root, var, value); if (ret == 1) { send_to_all("SETINFO %s \"%s\"\n", var, value); } return ret; } int dstate_addenum(const char *var, const char *fmt, ...) { int ret; char value[ST_MAX_VALUE_LEN]; va_list ap; va_start(ap, fmt); vsnprintf(value, sizeof(value), fmt, ap); va_end(ap); ret = state_addenum(dtree_root, var, value); if (ret == 1) { send_to_all("ADDENUM %s \"%s\"\n", var, value); } return ret; } int dstate_addrange(const char *var, const int min, const int max) { int ret; ret = state_addrange(dtree_root, var, min, max); if (ret == 1) { send_to_all("ADDRANGE %s %i %i\n", var, min, max); } return ret; } void dstate_setflags(const char *var, int flags) { st_tree_t *sttmp; char flist[SMALLBUF]; /* find the dtree node for var */ sttmp = state_tree_find(dtree_root, var); if (!sttmp) { upslogx(LOG_ERR, "%s: base variable (%s) does not exist", __func__, var); return; } if (sttmp->flags & ST_FLAG_IMMUTABLE) { upslogx(LOG_WARNING, "%s: base variable (%s) is immutable", __func__, var); return; } if (sttmp->flags == flags) { return; /* no change */ } sttmp->flags = flags; /* build the list */ snprintf(flist, sizeof(flist), "%s", var); if (flags & ST_FLAG_RW) { snprintfcat(flist, sizeof(flist), " RW"); } if (flags & ST_FLAG_STRING) { snprintfcat(flist, sizeof(flist), " STRING"); } /* update listeners */ send_to_all("SETFLAGS %s\n", flist); } void dstate_setaux(const char *var, int aux) { st_tree_t *sttmp; /* find the dtree node for var */ sttmp = state_tree_find(dtree_root, var); if (!sttmp) { upslogx(LOG_ERR, "dstate_setaux: base variable (%s) does not exist", var); return; } if (sttmp->aux == aux) { return; /* no change */ } sttmp->aux = aux; /* update listeners */ send_to_all("SETAUX %s %d\n", var, aux); } const char *dstate_getinfo(const char *var) { return state_getinfo(dtree_root, var); } void dstate_addcmd(const char *cmdname) { int ret; ret = state_addcmd(&cmdhead, cmdname); /* update listeners */ if (ret == 1) { send_to_all("ADDCMD %s\n", cmdname); } } int dstate_delinfo(const char *var) { int ret; ret = state_delinfo(&dtree_root, var); /* update listeners */ if (ret == 1) { send_to_all("DELINFO %s\n", var); } return ret; } int dstate_delenum(const char *var, const char *val) { int ret; ret = state_delenum(dtree_root, var, val); /* update listeners */ if (ret == 1) { send_to_all("DELENUM %s \"%s\"\n", var, val); } return ret; } int dstate_delrange(const char *var, const int min, const int max) { int ret; ret = state_delrange(dtree_root, var, min, max); /* update listeners */ if (ret == 1) { send_to_all("DELRANGE %s \"%i %i\"\n", var, min, max); } return ret; } int dstate_delcmd(const char *cmd) { int ret; ret = state_delcmd(&cmdhead, cmd); /* update listeners */ if (ret == 1) { send_to_all("DELCMD %s\n", cmd); } return ret; } void dstate_free(void) { state_infofree(dtree_root); dtree_root = NULL; state_cmdfree(cmdhead); cmdhead = NULL; sock_close(); } const st_tree_t *dstate_getroot(void) { return dtree_root; } const cmdlist_t *dstate_getcmdlist(void) { return cmdhead; } void dstate_dataok(void) { if (stale == 1) { stale = 0; send_to_all("DATAOK\n"); } } void dstate_datastale(void) { if (stale == 0) { stale = 1; send_to_all("DATASTALE\n"); } } int dstate_is_stale(void) { return stale; } /* ups.status management functions - reducing duplication in the drivers */ /* clean out the temp space for a new pass */ void status_init(void) { if (dstate_getinfo("driver.flag.ignorelb")) { ignorelb = 1; } memset(status_buf, 0, sizeof(status_buf)); } /* add a status element */ void status_set(const char *buf) { if (ignorelb && !strcasecmp(buf, "LB")) { upsdebugx(2, "%s: ignoring LB flag from device", __func__); return; } /* separate with a space if multiple elements are present */ if (strlen(status_buf) > 0) { snprintfcat(status_buf, sizeof(status_buf), " %s", buf); } else { snprintfcat(status_buf, sizeof(status_buf), "%s", buf); } } /* write the status_buf into the externally visible dstate storage */ void status_commit(void) { while (ignorelb) { const char *val, *low; val = dstate_getinfo("battery.charge"); low = dstate_getinfo("battery.charge.low"); if (val && low && (strtol(val, NULL, 10) < strtol(low, NULL, 10))) { snprintfcat(status_buf, sizeof(status_buf), " LB"); upsdebugx(2, "%s: appending LB flag [charge '%s' below '%s']", __func__, val, low); break; } val = dstate_getinfo("battery.runtime"); low = dstate_getinfo("battery.runtime.low"); if (val && low && (strtol(val, NULL, 10) < strtol(low, NULL, 10))) { snprintfcat(status_buf, sizeof(status_buf), " LB"); upsdebugx(2, "%s: appending LB flag [runtime '%s' below '%s']", __func__, val, low); break; } /* LB condition not detected */ break; } if (alarm_active) { dstate_setinfo("ups.status", "ALARM %s", status_buf); } else { dstate_setinfo("ups.status", "%s", status_buf); } } /* similar handlers for ups.alarm */ void alarm_init(void) { memset(alarm_buf, 0, sizeof(alarm_buf)); } void alarm_set(const char *buf) { if (strlen(alarm_buf) > 0) { snprintfcat(alarm_buf, sizeof(alarm_buf), " %s", buf); } else { snprintfcat(alarm_buf, sizeof(alarm_buf), "%s", buf); } } /* write the status_buf into the info array */ void alarm_commit(void) { if (strlen(alarm_buf) > 0) { dstate_setinfo("ups.alarm", "%s", alarm_buf); alarm_active = 1; } else { dstate_delinfo("ups.alarm"); alarm_active = 0; } }