nut/drivers/dstate.c
2022-06-29 12:37:36 +02:00

1524 lines
38 KiB
C

/* dstate.c - Network UPS Tools driver-side state management
Copyright (C)
2003 Russell Kroll <rkroll@exploits.org>
2008 Arjen de Korte <adkorte-guest@alioth.debian.org>
2012 - 2017 Arnaud Quette <arnaud.quette@free.fr>
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 "config.h" /* must be the first header */
#include <stdio.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "common.h"
#include "dstate.h"
#include "state.h"
#include "parseconf.h"
#include "attribute.h"
#include "nut_stdint.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)
__attribute__((noreturn));
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 <username>)\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;
check_unix_socket_filename(fn);
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, ...)
{
ssize_t ret;
char buf[ST_SOCK_BUF_LEN];
size_t buflen;
va_list ap;
conn_t *conn, *cnext;
va_start(ap, fmt);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY
#pragma GCC diagnostic ignored "-Wformat-security"
#endif
ret = vsnprintf(buf, sizeof(buf), fmt, ap);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
va_end(ap);
if (ret < 1) {
upsdebugx(2, "%s: nothing to write", __func__);
return;
}
if (ret <= INT_MAX)
upsdebugx(5, "%s: %.*s", __func__, (int)(ret-1), buf);
buflen = strlen(buf);
if (buflen >= SSIZE_MAX) {
/* Can't compare buflen to ret... though should not happen with ST_SOCK_BUF_LEN */
upslog_with_errno(LOG_NOTICE, "%s failed: buffered message too large", __func__);
return;
}
for (conn = connhead; conn; conn = cnext) {
cnext = conn->next;
ret = write(conn->fd, buf, buflen);
if ((ret < 1) || (ret != (ssize_t)buflen)) {
upsdebugx(0, "WARNING: %s: write %zd bytes to "
"socket %d failed (ret=%zd), disconnecting: %s",
__func__, buflen, conn->fd, ret, strerror(errno));
upsdebugx(6, "failed write: %s", buf);
sock_disconnect(conn);
/* TOTHINK: Maybe fallback elsewhere in other cases? */
if (ret < 0 && errno == EAGAIN && do_synchronous == -1) {
upsdebugx(0, "%s: synchronous mode was 'auto', "
"will try 'on' for next connections",
__func__);
do_synchronous = 1;
}
dstate_setinfo("driver.parameter.synchronous", "%s",
(do_synchronous==1)?"yes":((do_synchronous==0)?"no":"auto"));
} else {
upsdebugx(6, "%s: write %zd bytes to socket %d succeeded "
"(ret=%zd): %s",
__func__, buflen, conn->fd, ret, buf);
}
}
}
static int send_to_one(conn_t *conn, const char *fmt, ...)
{
ssize_t ret;
va_list ap;
char buf[ST_SOCK_BUF_LEN];
size_t buflen;
va_start(ap, fmt);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY
#pragma GCC diagnostic ignored "-Wformat-security"
#endif
ret = vsnprintf(buf, sizeof(buf), fmt, ap);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
va_end(ap);
upsdebugx(2, "%s: sending %.*s", __func__, (int)strcspn(buf, "\n"), buf);
if (ret < 1) {
upsdebugx(2, "%s: nothing to write", __func__);
return 1;
}
buflen = strlen(buf);
if (buflen >= SSIZE_MAX) {
/* Can't compare buflen to ret... though should not happen with ST_SOCK_BUF_LEN */
upslog_with_errno(LOG_NOTICE, "%s failed: buffered message too large", __func__);
return 0; /* failed */
}
if (ret <= INT_MAX)
upsdebugx(5, "%s: %.*s", __func__, (int)(ret-1), buf);
/*
upsdebugx(0, "%s: writing %zd bytes to socket %d: %s",
__func__, buflen, conn->fd, buf);
*/
ret = write(conn->fd, buf, buflen);
if (ret < 0) {
/* Hacky bugfix: throttle down for upsd to read that */
upsdebugx(1, "%s: had to throttle down to retry "
"writing %zd bytes to socket %d "
"(ret=%zd, errno=%d, strerror=%s): %s",
__func__, buflen, conn->fd,
ret, errno, strerror(errno),
buf);
usleep(200);
ret = write(conn->fd, buf, buflen);
if (ret == (ssize_t)buflen) {
upsdebugx(1, "%s: throttling down helped", __func__);
}
}
if ((ret < 1) || (ret != (ssize_t)buflen)) {
upsdebugx(0, "WARNING: %s: write %zd bytes to "
"socket %d failed (ret=%zd), disconnecting: %s",
__func__, buflen, conn->fd, ret, strerror(errno));
upsdebugx(6, "failed write: %s", buf);
sock_disconnect(conn);
/* TOTHINK: Maybe fallback elsewhere in other cases? */
if (ret < 0 && errno == EAGAIN && do_synchronous == -1) {
upsdebugx(0, "%s: synchronous mode was 'auto', "
"will try 'on' for next connections",
__func__);
do_synchronous = 1;
}
dstate_setinfo("driver.parameter.synchronous", "%s",
(do_synchronous==1)?"yes":((do_synchronous==0)?"no":"auto"));
return 0; /* failed */
} else {
upsdebugx(6, "%s: write %zd bytes to socket %d succeeded "
"(ret=%zd): %s",
__func__, buflen, conn->fd, ret, buf);
}
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?
* -1 = auto (try async, allow fallback to sync)
* 0 = async
* 1 = sync
*/
if (do_synchronous < 1) {
upsdebugx(0, "%s: enabling asynchronous mode (%s)",
__func__, (do_synchronous<0)?"auto":"fixed");
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;
}
}
else {
upsdebugx(0, "%s: keeping default synchronous mode", __func__);
}
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 %ld\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 (node->flags & ST_FLAG_NUMBER) {
snprintfcat(flist, sizeof(flist), " NUMBER");
}
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 void send_tracking(conn_t *conn, const char *id, int value)
{
send_to_one(conn, "TRACKING %s %i\n", id, value);
}
static int sock_arg(conn_t *conn, size_t numarg, char **arg)
{
upsdebugx(6, "Driver on %s is now handling %s with %zu args",
sockfn, numarg ? arg[0] : "<skipped: no command>", numarg);
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 <cmdname> [<cmdparam>] [TRACKING <id>] */
if (!strcasecmp(arg[0], "INSTCMD")) {
int ret;
char *cmdname = arg[1];
char *cmdparam = NULL;
char *cmdid = NULL;
/* Check if <cmdparam> and TRACKING were provided */
if (numarg == 3) {
cmdparam = arg[2];
} else if (numarg == 4 && !strcasecmp(arg[2], "TRACKING")) {
cmdid = arg[3];
} else if (numarg == 5 && !strcasecmp(arg[3], "TRACKING")) {
cmdparam = arg[2];
cmdid = arg[4];
} else if (numarg != 2) {
upslogx(LOG_NOTICE, "Malformed INSTCMD request");
return 0;
}
if (cmdid)
upsdebugx(3, "%s: TRACKING = %s", __func__, cmdid);
/* try the new handler first if present */
if (upsh.instcmd) {
ret = upsh.instcmd(cmdname, cmdparam);
/* send back execution result */
if (cmdid)
send_tracking(conn, cmdid, ret);
/* The command was handled, status is a separate consideration */
return 1;
}
upslogx(LOG_NOTICE, "Got INSTCMD, but driver lacks a handler");
return 1;
}
if (numarg < 3) {
return 0;
}
/* SET <var> <value> [TRACKING <id>] */
if (!strcasecmp(arg[0], "SET")) {
int ret;
char *setid = NULL;
/* Check if TRACKING was provided */
if (numarg == 5) {
if (!strcasecmp(arg[3], "TRACKING")) {
setid = arg[4];
}
else {
upslogx(LOG_NOTICE, "Got SET <var> with unsupported parameters (%s/%s)",
arg[3], arg[4]);
return 0;
}
upsdebugx(3, "%s: TRACKING = %s", __func__, setid);
}
/* try the new handler first if present */
if (upsh.setvar) {
ret = upsh.setvar(arg[1], arg[2]);
/* send back execution result */
if (setid)
send_tracking(conn, setid, ret);
/* The command was handled, status is a separate consideration */
return 1;
}
upslogx(LOG_NOTICE, "Got SET, but driver lacks a handler");
return 1;
}
/* unknown */
return 0;
}
static void sock_read(conn_t *conn)
{
ssize_t ret, i;
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 < INT_MAX; 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 */
char * dstate_init(const char *prog, const char *devname)
{
char sockname[SMALLBUF];
/* do this here for now */
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_STRICT_PROTOTYPES)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wstrict-prototypes"
#endif
signal(SIGPIPE, SIG_IGN);
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_STRICT_PROTOTYPES)
# pragma GCC diagnostic pop
#endif
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);
/* NOTE: Caller must free this string */
return xstrdup(sockname);
}
/* 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);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY
#pragma GCC diagnostic ignored "-Wformat-security"
#endif
vsnprintf(value, sizeof(value), fmt, ap);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
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);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY
#pragma GCC diagnostic ignored "-Wformat-security"
#endif
vsnprintf(value, sizeof(value), fmt, ap);
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
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);
/* Also add the "NUMBER" flag for ranges */
dstate_addflags(var, ST_FLAG_NUMBER);
}
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");
}
if (flags & ST_FLAG_NUMBER) {
snprintfcat(flist, sizeof(flist), " NUMBER");
}
/* update listeners */
send_to_all("SETFLAGS %s\n", flist);
}
void dstate_addflags(const char *var, const int addflags)
{
int flags = state_getflags(dtree_root, var);
if (flags == -1) {
upslogx(LOG_ERR, "%s: cannot get flags of '%s'", __func__, var);
return;
}
/* Already set */
if ((flags & addflags) == addflags)
return;
flags |= addflags;
dstate_setflags(var, flags);
}
void dstate_delflags(const char *var, const int delflags)
{
int flags = state_getflags(dtree_root, var);
if (flags == -1) {
upslogx(LOG_ERR, "%s: cannot get flags of '%s'", __func__, var);
return;
}
/* Already not set */
if (!(flags & delflags))
return;
flags &= ~delflags;
dstate_setflags(var, flags);
}
void dstate_setaux(const char *var, long 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 %ld\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)
{
/* reinit global counter */
alarm_active = 0;
device_alarm_init();
}
#if (!defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_INSIDEFUNC) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS_BESIDEFUNC)
# pragma GCC diagnostic ignored "-Wtype-limits"
#endif
#if (!defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_INSIDEFUNC) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE_BESIDEFUNC)
# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare"
#endif
void alarm_set(const char *buf)
{
int ret;
if (strlen(alarm_buf) > 0) {
ret = snprintfcat(alarm_buf, sizeof(alarm_buf), " %s", buf);
} else {
ret = snprintfcat(alarm_buf, sizeof(alarm_buf), "%s", buf);
}
if (ret < 0) {
/* Should we also try to print the potentially unusable buf?
* Generally - likely not. But if it is short enough...
* Note: LARGEBUF was the original limit mismatched vs alarm_buf
* size before PR #986.
*/
char alarm_tmp[LARGEBUF];
memset(alarm_tmp, 0, sizeof(alarm_tmp));
/* A bit of complexity to keep both (int)snprintf(...) and (size_t)sizeof(...) happy */
int ibuflen = snprintf(alarm_tmp, sizeof(alarm_tmp), "%s", buf);
size_t buflen;
if (ibuflen < 0) {
alarm_tmp[0] = 'N';
alarm_tmp[1] = '/';
alarm_tmp[2] = 'A';
alarm_tmp[3] = '\0';
buflen = strlen(alarm_tmp);
} else {
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) )
/* Note for gating macros above: unsuffixed HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP
* means support of contexts both inside and outside function body, so the push
* above and pop below (outside this finction) are not used.
*/
# pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS
/* Note that the individual warning pragmas for use inside function bodies
* are named without a _INSIDEFUNC suffix, for simplicity and legacy reasons
*/
# pragma GCC diagnostic ignored "-Wtype-limits"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE
# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare"
#endif
if ((unsigned long long int)ibuflen < SIZE_MAX) {
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) )
# pragma GCC diagnostic pop
#endif
buflen = (size_t)ibuflen;
} else {
buflen = SIZE_MAX;
}
}
upslogx(LOG_ERR, "%s: error setting alarm_buf to: %s%s",
__func__, alarm_tmp, ( (buflen < sizeof(alarm_tmp)) ? "" : "...<truncated>" ) );
} else if ((size_t)ret > sizeof(alarm_buf)) {
char alarm_tmp[LARGEBUF];
memset(alarm_tmp, 0, sizeof(alarm_tmp));
int ibuflen = snprintf(alarm_tmp, sizeof(alarm_tmp), "%s", buf);
size_t buflen;
if (ibuflen < 0) {
alarm_tmp[0] = 'N';
alarm_tmp[1] = '/';
alarm_tmp[2] = 'A';
alarm_tmp[3] = '\0';
buflen = strlen(alarm_tmp);
} else {
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) )
# pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS
# pragma GCC diagnostic ignored "-Wtype-limits"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE
# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare"
#endif
if ((unsigned long long int)ibuflen < SIZE_MAX) {
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) )
# pragma GCC diagnostic pop
#endif
buflen = (size_t)ibuflen;
} else {
buflen = SIZE_MAX;
}
}
upslogx(LOG_WARNING, "%s: result was truncated while setting or appending "
"alarm_buf (limited to %zu bytes), with message: %s%s",
__func__, sizeof(alarm_buf), alarm_tmp,
( (buflen < sizeof(alarm_tmp)) ? "" : "...<also truncated>" ));
}
}
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_BESIDEFUNC) && (!defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_INSIDEFUNC) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS_BESIDEFUNC) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE_BESIDEFUNC) )
# pragma GCC diagnostic pop
#endif
/* 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;
}
}
void device_alarm_init(void)
{
/* only clear the buffer, don't touch the alarms counter */
memset(alarm_buf, 0, sizeof(alarm_buf));
}
/* same as above, but writes to "device.X.ups.alarm" or "ups.alarm" */
void device_alarm_commit(const int device_number)
{
char info_name[20];
memset(info_name, 0, 20);
if (device_number != 0) /* would then go into "device.%i.alarm" */
snprintf(info_name, 20, "device.%i.ups.alarm", device_number);
else /* would then go into "device.alarm" */
snprintf(info_name, 20, "ups.alarm");
/* Daisychain subdevices note:
* increase the counter when alarms are present on a subdevice, but
* don't decrease the count. Otherwise, we may not get the ALARM flag
* in ups.status, while there are some alarms present on device.X */
if (strlen(alarm_buf) > 0) {
dstate_setinfo(info_name, "%s", alarm_buf);
alarm_active++;
} else {
dstate_delinfo(info_name);
}
}
/* For devices where we do not have phase-count info (no mapping provided
* in the tables), nor in the device data/protocol, we can still guesstimate
* and report a value. This routine may also replace an existing value, e.g.
* if we've found new data disproving old one (e.g. if the 3-phase UPS was
* disbalanced when the driver was started, so we thought it is 1-phase in
* practice, and then the additional lines came up loaded, hence the bools
* "may_reevaluate" and the readonly flag "may_change_dstate" (so the caller
* can query the current apparent situation, without changing any dstates).
* It is up to callers to decide if they already have data they want to keep.
* The "xput_prefix" is e.g. "input." or "input.bypass." or "output." with
* the trailing dot where applicable - we use this string verbatim below.
* The "inited_phaseinfo" and "num_phases" are addresses of caller's own
* variables to store the flag (if we have successfully inited) and the
* discovered amount of phases, or NULL if caller does not want to track it.
*
* NOTE: The code below can detect if the device is 1, 2 (split phase) or 3
* phases.
*
* Returns:
* -1 Runtime/input error (non fatal, but routine was skipped)
* 0 Nothing changed: could not determine a value
* 1 A phase value was just determined (and set, if not read-only mode)
* 2 Nothing changed: already inited (and may_reevaluate==false)
* 3 Nothing changed: detected a value but it is already published
* as a dstate; populated inited_phaseinfo and num_phases though
*/
int dstate_detect_phasecount(
const char *xput_prefix,
const int may_change_dstate,
int *inited_phaseinfo,
int *num_phases,
const int may_reevaluate
) {
/* If caller does not want either of these back - loopback the values below */
int local_inited_phaseinfo = 0, local_num_phases = -1;
/* Temporary local value storage */
int old_num_phases = -1, detected_phaseinfo = 0;
if (!inited_phaseinfo)
inited_phaseinfo = &local_inited_phaseinfo;
if (!num_phases)
num_phases = &local_num_phases;
old_num_phases = *num_phases;
upsdebugx(3, "Entering %s('%s', %i, %i, %i, %i)", __func__,
xput_prefix, may_change_dstate, *inited_phaseinfo, *num_phases, may_reevaluate);
if (!(*inited_phaseinfo) || may_reevaluate) {
const char *v1, *v2, *v3, *v0,
*v1n, *v2n, *v3n,
*v12, *v23, *v31,
*c1, *c2, *c3, *c0;
char buf[MAX_STRING_SIZE]; /* For concatenation of "xput_prefix" with items we want to query */
size_t xput_prefix_len;
size_t bufrw_max;
char *bufrw_ptr = NULL;
if (!xput_prefix) {
upsdebugx(0, "%s(): Bad xput_prefix was passed: it is NULL - function skipped", __func__);
return -1;
}
xput_prefix_len = strlen(xput_prefix);
if (xput_prefix_len < 1) {
upsdebugx(0, "%s(): Bad xput_prefix was passed: it is empty - function skipped", __func__);
return -1;
}
bufrw_max = sizeof(buf) - xput_prefix_len;
if (bufrw_max <= 15) {
/* We need to append max ~13 chars per below, so far */
upsdebugx(0, "%s(): Bad xput_prefix was passed: it is too long - function skipped", __func__);
return -1;
}
memset(buf, 0, sizeof(buf));
strncpy(buf, xput_prefix, sizeof(buf));
bufrw_ptr = buf + xput_prefix_len ;
/* We either have defined and non-zero (numeric) values below, or NULLs.
* Note that as "zero" we should expect any valid numeric representation
* of a zero value as some drivers may save strangely formatted values.
* For now, we limit the level of paranoia with missing dstate entries,
* empty entries, and actual single zero character as contents of the
* string. Other obscure cases (string of multiple zeroes, a floating
* point zero, surrounding whitespace etc. may be solved if the need
* does arise in the future. Arguably, drivers' translation/mapping
* tables should take care of this with converion routine and numeric
* data type flags. */
#define dstate_getinfo_nonzero(var, suffix) \
do { strncpy(bufrw_ptr, suffix, bufrw_max); \
if ( (var = dstate_getinfo(buf)) ) { \
if ( (var[0] == '0' && var[1] == '\0') || \
(var[0] == '\0') ) { \
var = NULL; \
} \
} \
} while(0)
dstate_getinfo_nonzero(v1, "L1.voltage");
dstate_getinfo_nonzero(v2, "L2.voltage");
dstate_getinfo_nonzero(v3, "L3.voltage");
dstate_getinfo_nonzero(v1n, "L1-N.voltage");
dstate_getinfo_nonzero(v2n, "L2-N.voltage");
dstate_getinfo_nonzero(v3n, "L3-N.voltage");
dstate_getinfo_nonzero(v1n, "L1-N.voltage");
dstate_getinfo_nonzero(v12, "L1-L2.voltage");
dstate_getinfo_nonzero(v23, "L2-L3.voltage");
dstate_getinfo_nonzero(v31, "L3-L1.voltage");
dstate_getinfo_nonzero(c1, "L1.current");
dstate_getinfo_nonzero(c2, "L2.current");
dstate_getinfo_nonzero(c3, "L3.current");
dstate_getinfo_nonzero(v0, "voltage");
dstate_getinfo_nonzero(c0, "current");
if ( (v1 && v2 && !v3) ||
(v1n && v2n && !v3n) ||
(c1 && c2 && !c3) ||
(v12 && !v23 && !v31) ) {
upsdebugx(5, "%s(): determined a 2-phase case", __func__);
*num_phases = 2;
*inited_phaseinfo = 1;
detected_phaseinfo = 1;
} else if ( (v1 && v2 && v3) ||
(v1n && v2n && v3n) ||
(c1 && (c2 || c3)) ||
(c2 && (c1 || c3)) ||
(c3 && (c1 || c2)) ||
v12 || v23 || v31 ) {
upsdebugx(5, "%s(): determined a 3-phase case", __func__);
*num_phases = 3;
*inited_phaseinfo = 1;
detected_phaseinfo = 1;
} else if ( /* We definitely have only one non-zero line */
!v12 && !v23 && !v31 && (
(c0 && !c1 && !c2 && !c3) ||
(v0 && !v1 && !v2 && !v3) ||
(c1 && !c2 && !c3) ||
(!c1 && c2 && !c3) ||
(!c1 && !c2 && c3) ||
(v1 && !v2 && !v3) ||
(!v1 && v2 && !v3) ||
(!v1 && !v2 && v3) ||
(v1n && !v2n && !v3n) ||
(!v1n && v2n && !v3n) ||
(!v1n && !v2n && v3n) ) ) {
*num_phases = 1;
*inited_phaseinfo = 1;
detected_phaseinfo = 1;
upsdebugx(5, "%s(): determined a 1-phase case", __func__);
} else {
upsdebugx(5, "%s(): could not determine the phase case", __func__);
}
if (detected_phaseinfo) {
const char *oldphases;
strncpy(bufrw_ptr, "phases", bufrw_max);
oldphases = dstate_getinfo(buf);
if (oldphases) {
if (atoi(oldphases) == *num_phases) {
/* Technically, a bit has changed: we have set the flag which may have been missing before */
upsdebugx(5, "%s(): Nothing changed, with a valid reason; dstate already published with the same value: %s=%s (detected %d)",
__func__, buf, oldphases, *num_phases);
return 3;
}
}
if ( (*num_phases != old_num_phases) || (!oldphases) ) {
if (may_change_dstate) {
dstate_setinfo(buf, "%d", *num_phases);
upsdebugx(3, "%s(): calculated non-XML value for NUT variable %s was set to %d",
__func__, buf, *num_phases);
} else {
upsdebugx(3, "%s(): calculated non-XML value for NUT variable %s=%d but did not set its dstate (read-only request)",
__func__, buf, *num_phases);
}
return 1;
}
}
upsdebugx(5, "%s(): Nothing changed: could not determine a value", __func__);
return 0;
}
upsdebugx(5, "%s(): Nothing changed, with a valid reason; already inited", __func__);
return 2;
}
/* Dump the data tree (in upsc-like format) to stdout */
/* Actual implementation */
static int dstate_tree_dump(const st_tree_t *node)
{
int ret;
if (!node) {
return 1; /* not an error */
}
if (node->left) {
ret = dstate_tree_dump(node->left);
if (!ret) {
return 0; /* write failed in the child */
}
}
printf("%s: %s\n", node->var, node->val);
if (node->right) {
return dstate_tree_dump(node->right);
}
return 1; /* everything's OK here ... */
}
/* Dump the data tree (in upsc-like format) to stdout */
/* Public interface */
void dstate_dump(void)
{
upsdebugx(3, "Entering %s", __func__);
const st_tree_t *node = (const st_tree_t *)dstate_getroot();
dstate_tree_dump(node);
}