diff --git a/NEWS.adoc b/NEWS.adoc index 0313d740df..3f28440190 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -51,8 +51,17 @@ https://github.com/networkupstools/nut/milestone/11 * Addition of "NUT Simulated devices" support to `nut-scanner` in v2.8.2 broke detection of (in-)ability to find and query "Old NUT" servers via `libupsclient.so` (internal flag got always enabled). [#2246] - - - drivers, upsd, upsmon: reduce "scary noise" about failure to `fopen()` + * A fix for `upsmon` v2.8.1 setting of `OFFDURATION` [PR #2108, issue #2104, + revisiting PR #2055, issue #2044] was overly zealous and impacted also + the `OB` state in cases where communications to the data server were + severed and `DEADTIME` setting was not honored. [PR #2462, issue #2454] + * Using `drivername -c reload` (e.g. facilitated by `nut-driver-enumerator` + script and service when editing `ups.conf`) led to disconnected Unix + sockets and a tight polling loop that hogged CPU. While the underlying + bug is ancient, it took recent development to hit it as a practical + regression. [issue #1904, issue #2484] + + - drivers, `upsd`, `upsmon`: reduce "scary noise" about failure to `fopen()` the PID file (which most of the time means that no previous instance of the daemon was running to potentially conflict with), especially useless since in recent NUT releases the verdicts from `sendsignal*()` methods @@ -87,6 +96,10 @@ https://github.com/networkupstools/nut/milestone/11 string values reported by devices via USB are welcome), so these devices would now report `battery.voltage` and `battery.voltage.nominal`. [#2380] + - bicker_ser: added new driver for Bicker 12/24Vdc UPS via RS-232 serial + communication protocol, which supports any UPS shipped with the PSZ-1053 + extension module. [PR #2448] + - USB drivers could log `(nut_)libusb_get_string: Success` due to either reading an empty string or getting a success code `0` from libusb. This difference should now be better logged, and not into syslog. [#2399] @@ -116,10 +129,33 @@ https://github.com/networkupstools/nut/milestone/11 exiting (and/or start-up has aborted due to configuration or run-time issues). Warning about "world readable" files clarified. [#2417] - - common code: root-owned daemons now use not the hard-coded `PIDPATH` value - set by the `configure` script during build, but can override it with a - `NUT_PIDPATH` environment variable in certain use-cases (such as tests). - [#2407] + - common code: + * introduced a `NUT_DEBUG_SYSLOG` environment variable to tweak activation + of syslog message emission (and related detachment of `stderr` when + backgrounding), primarily useful for NIT and perhaps systemd. Most + methods relied on logging bits being set, so this change aims to be + minimally invasive to impact setting of those bits (or not) in the + first place. [#2394] + * `root`-owned daemons now use not the hard-coded `PIDPATH` value set + by the `configure` script during build, but can override it with a + `NUT_PIDPATH` environment variable in certain use-cases (such as + tests). [#2407] + * introduced a check for daemons working with PID files to double-check + that if they can resolve the program name of a running process with + this identifier, that such name matches the current program (avoid + failures to start NUT daemons if PID files are on persistent storage, + and some unrelated program got that PID after a reboot). This might + introduce regressions for heavily customized NUT builds (e.g. those + embedded in NAS or similar devices) where binary file names differ + significantly from a `progname` string defined in the respective NUT + source file, so a boolean `NUT_IGNORE_CHECKPROCNAME` environment + variable support was added to optionally disable this verification. + Also the NUT daemons should request to double-check against their + run-time process name (if it can be detected). [issue #2463] + + - various recipe, documentation and source files were revised to address + respective warnings issued by the new generations of analysis tools. + [#823, #2437, link:https://github.com/networkupstools/nut-website/issues/52[nut-website issue #52]] - revised `nut.exe` (the NUT for Windows wrapper for all-in-one service) to be more helpful with command-line use (report that it failed to start @@ -133,6 +169,24 @@ https://github.com/networkupstools/nut/milestone/11 + NOTE: The `html-chunked` documents are currently still not installed. + - added support to `./configure --with-doc=man=dist-auto` to use distributed + manual page files if present; only fall back to build them if we can. [#2473] + + - added a `make distcheck-light-man` recipe to require verification that the + manual page files can be built using the prepared "tarball" archive. [#2473] + + - added a `common/Makefile.am` build product for a new internal library + `libcommonstr.la` which allows a smaller selection of helper methods + for tools like `nut-scanner` which do not need the full `libcommon.la` + nor `libcommonclient.la`. [#2478, #2491] + + - added a `drivers/Makefile.am` build product for a new internal library + `libserial-nutscan.la` to simplify `tools/nut-scanner/Makefile.am` recipes. + [#2490] + + - build of `snmp-ups` and `netxml-ups` drivers now explicitly brings linker + dependency on chosen SSL libraries. [#2479] + - brought keyword dictionaries of `nutconf` and `augeas` NUT configuration file parsers up to date; restored automated checks for `augeas` lenses. [issue #657, issue #2294] @@ -142,10 +196,6 @@ https://github.com/networkupstools/nut/milestone/11 of more complex configurations (e.g. some line patterns that involve too many double-quote characters) which are valid for NUT proper. [#657] - - various recipe, documentation and source files were revised to address - respective warnings issued by the new generations of analysis tools. - [#823, #2437, link:https://github.com/networkupstools/nut-website/issues/52[nut-website issue #52]] - Release notes for NUT 2.8.2 - what's new since 2.8.1 ---------------------------------------------------- diff --git a/README.adoc b/README.adoc index 2164db7f4a..c507647fd5 100644 --- a/README.adoc +++ b/README.adoc @@ -217,6 +217,8 @@ which the larger potential sponsors consider when choosing how to help FOSS projects. Keeping the lights shining in such a large non-regression build matrix is a big undertaking! +image:https://api.star-history.com/svg?repos=networkupstools/nut&type=Date[link="https://star-history.com/#networkupstools/nut&Date" alt="Star History Chart"] + See <> for an overview of the shared effort. ===== diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 19a95b78e0..0c5a7ac125 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -33,6 +33,22 @@ Changes from 2.8.2 to 2.8.3 pages which are delivered automatically. Packaging recipes can likely be simplified now. [#2445] +- Internal API change for `sendsignalpid()` and `sendsignalfn()` methods, + which can impact NUT forks which build using `libcommon.la` and similar + libraries. Added new last argument with `const char *progname` (may be + `NULL`) to check that we are signalling an expected program name when we + work with a PID. With the same effort, NUT programs which deal with PID + files to send signals (`upsd`, `upsmon`, drivers and `upsdrvctl`) would + now default to a safety precaution -- checking that the running process + with that PID has the expected program name (on platforms where we can + determine one). This might introduce regressions for heavily customized + NUT builds (e.g. embedded in NAS or similar devices) whose binary file + names differ significantly from a `progname` defined in the respective + NUT source file, so a boolean `NUT_IGNORE_CHECKPROCNAME` environment + variable support was added to optionally disable this verification. + Also the NUT daemons should request to double-check against their + run-time process name (if it can be detected). [issue #2463] + Changes from 2.8.1 to 2.8.2 --------------------------- diff --git a/clients/nutclient.h b/clients/nutclient.h index 8047e26ced..19dbdcfde4 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -18,7 +18,7 @@ */ #ifndef NUTCLIENT_HPP_SEEN -#define NUTCLIENT_HPP_SEEN +#define NUTCLIENT_HPP_SEEN 1 /* Begin of C++ nutclient library declaration */ #ifdef __cplusplus diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 6363262681..8584a9ca45 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -18,7 +18,7 @@ */ #ifndef NUTCLIENTMEM_HPP_SEEN -#define NUTCLIENTMEM_HPP_SEEN +#define NUTCLIENTMEM_HPP_SEEN 1 /* Begin of C++ nutclient library declaration */ #ifdef __cplusplus @@ -117,4 +117,4 @@ NUTCLIENT_MEM_t nutclient_mem_create_client(); #endif /* __cplusplus */ /* End of C nutclient library declaration */ -#endif /* NUTCLIENTMOCK_HPP_SEEN */ +#endif /* NUTCLIENTMEM_HPP_SEEN */ diff --git a/clients/upsclient.h b/clients/upsclient.h index c79be96cfd..197ef503ef 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -18,7 +18,7 @@ */ #ifndef UPSCLIENT_H_SEEN -#define UPSCLIENT_H_SEEN +#define UPSCLIENT_H_SEEN 1 #ifdef WITH_OPENSSL #include @@ -53,6 +53,10 @@ # endif #endif +#if defined HAVE_SYS_TYPES_H + #include +#endif + #ifdef __cplusplus /* *INDENT-OFF* */ extern "C" { diff --git a/clients/upsmon.c b/clients/upsmon.c index 1e64c564a7..72314267dc 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -1113,11 +1113,17 @@ static int is_ups_critical(utype_t *ups) } if (ups->linestate == 0) { - upslogx(LOG_WARNING, + /* Just a message for post-mortem troubleshooting: + * no flag flips, no return values issued just here + * (note the message is likely to appear on every + * cycle when the communications are down, to help + * track when this was the case; no log throttling). + */ + upsdebugx(1, "UPS [%s] was last known to be not fully online " - "and currently is not communicating, assuming dead", + "and currently is not communicating, just so you " + "know (waiting for DEADTIME to elapse)", ups->sys); - return 1; } } @@ -3016,15 +3022,15 @@ int main(int argc, char *argv[]) * is running by sending signal '0' (i.e. 'kill 0' equivalent) */ if (oldpid < 0) { - cmdret = sendsignal(prog, cmd); + cmdret = sendsignal(prog, cmd, 1); } else { - cmdret = sendsignalpid(oldpid, cmd); + cmdret = sendsignalpid(oldpid, cmd, prog, 1); } #else /* WIN32 */ if (cmd) { /* Command the running daemon, it should be there */ - cmdret = sendsignal(UPSMON_PIPE_NAME, cmd); + cmdret = sendsignal(UPSMON_PIPE_NAME, cmd, 1); } else { /* Starting new daemon, check for competition */ mutex = CreateMutex(NULL, TRUE, UPSMON_PIPE_NAME); diff --git a/common/Makefile.am b/common/Makefile.am index dbc12567bd..bb910a0266 100644 --- a/common/Makefile.am +++ b/common/Makefile.am @@ -54,7 +54,7 @@ libcommonclient_la_SOURCES = state.c str.c noinst_LTLIBRARIES += libcommonstr.la libcommonstr_la_SOURCES = str.c libcommonstr_la_CFLAGS = $(AM_CFLAGS) -DWITHOUT_LIBSYSTEMD=1 -libcommonstr_la_LIBADD = @LTLIBOBJS@ +libcommonstr_la_LIBADD = @LTLIBOBJS@ @BSDKVMPROCLIBS@ if BUILDING_IN_TREE libcommon_la_SOURCES += common.c @@ -111,8 +111,8 @@ endif HAVE_WINDOWS # ensure inclusion of local implementation of missing systems functions # using LTLIBOBJS. Refer to configure.in/.ac -> AC_REPLACE_FUNCS -libcommon_la_LIBADD = libparseconf.la @LTLIBOBJS@ @NETLIBS@ -libcommonclient_la_LIBADD = libparseconf.la @LTLIBOBJS@ @NETLIBS@ +libcommon_la_LIBADD = libparseconf.la @LTLIBOBJS@ @NETLIBS@ @BSDKVMPROCLIBS@ +libcommonclient_la_LIBADD = libparseconf.la @LTLIBOBJS@ @NETLIBS@ @BSDKVMPROCLIBS@ libcommon_la_CFLAGS = $(AM_CFLAGS) libcommonclient_la_CFLAGS = $(AM_CFLAGS) diff --git a/common/common.c b/common/common.c index b503e9fe66..22fec1f871 100644 --- a/common/common.c +++ b/common/common.c @@ -1,7 +1,7 @@ /* common.c - common useful functions Copyright (C) 2000 Russell Kroll - Copyright (C) 2021-2022 Jim Klimov + Copyright (C) 2021-2024 Jim Klimov 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 @@ -23,13 +23,19 @@ #include #ifndef WIN32 -#include -#include -#include -#include -#include +# include +# include +# include +# include +# include #else -#include +# include +# include +# include +#endif + +#ifdef HAVE_UNISTD_H +# include /* readlink */ #endif #include @@ -84,6 +90,17 @@ const char *UPS_VERSION = NUT_VERSION_MACRO; #include #include #include + +#if defined(HAVE_LIB_BSD_KVM_PROC) && HAVE_LIB_BSD_KVM_PROC +# include +# include +# include +#endif + +#if defined(HAVE_LIB_ILLUMOS_PROC) && HAVE_LIB_ILLUMOS_PROC +# include +#endif + pid_t get_max_pid_t(void) { #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE @@ -151,11 +168,38 @@ static int xbit_test(int val, int flag) return ((val & flag) == flag); } +int syslog_is_disabled(void) +{ + static int value = -1; + + if (value < 0) { + char *s = getenv("NUT_DEBUG_SYSLOG"); + /* Not set or not disabled by the setting: default is enabled (inversed per method name) */ + value = 0; + if (s) { + if (!strcmp(s, "stderr")) { + value = 1; + } else if (!strcmp(s, "none") || !strcmp(s, "false")) { + value = 2; + } else if (!strcmp(s, "syslog") || !strcmp(s, "true") || !strcmp(s, "default")) { + /* Just reserve a value to quietly do the default */ + value = 0; + } else { + upsdebugx(0, "%s: unknown NUT_DEBUG_SYSLOG='%s' value, ignored (assuming enabled)", + __func__, s); + } + } + } + + return value; +} + /* enable writing upslog_with_errno() and upslogx() type messages to the syslog */ void syslogbit_set(void) { - xbit_set(&upslog_flags, UPSLOG_SYSLOG); + if (!syslog_is_disabled()) + xbit_set(&upslog_flags, UPSLOG_SYSLOG); } /* get the syslog ready for us */ @@ -164,12 +208,15 @@ void open_syslog(const char *progname) #ifndef WIN32 int opt; + if (syslog_is_disabled()) + return; + opt = LOG_PID; /* we need this to grab /dev/log before chroot */ -#ifdef LOG_NDELAY +# ifdef LOG_NDELAY opt |= LOG_NDELAY; -#endif +# endif /* LOG_NDELAY */ openlog(progname, opt, LOG_FACILITY); @@ -202,31 +249,43 @@ void open_syslog(const char *progname) break; default: fatalx(EXIT_FAILURE, "Invalid log level threshold"); -#else +# else case 0: break; default: upslogx(LOG_INFO, "Changing log level threshold not possible"); break; -#endif +# endif /* HAVE_SETLOGMASK && HAVE_DECL_LOG_UPTO */ } #else EventLogName = progname; -#endif +#endif /* WIND32 */ } /* close ttys and become a daemon */ void background(void) { + /* Normally we enable SYSLOG and disable STDERR, + * unless NUT_DEBUG_SYSLOG envvar interferes as + * interpreted in syslog_is_disabled() method: */ + int syslog_disabled = syslog_is_disabled(), + stderr_disabled = (syslog_disabled == 0 || syslog_disabled == 2); + #ifndef WIN32 int pid; if ((pid = fork()) < 0) fatal_with_errno(EXIT_FAILURE, "Unable to enter background"); +#endif - xbit_set(&upslog_flags, UPSLOG_SYSLOG); - xbit_clear(&upslog_flags, UPSLOG_STDERR); + if (!syslog_disabled) + /* not disabled: NUT_DEBUG_SYSLOG is unset or invalid */ + xbit_set(&upslog_flags, UPSLOG_SYSLOG); + if (stderr_disabled) + /* NUT_DEBUG_SYSLOG="none" or unset/invalid */ + xbit_clear(&upslog_flags, UPSLOG_STDERR); +#ifndef WIN32 if (pid != 0) { /* parent */ /* these are typically fds 0-2: */ @@ -239,7 +298,7 @@ void background(void) /* child */ /* make fds 0-2 (typically) point somewhere defined */ -#ifdef HAVE_DUP2 +# ifdef HAVE_DUP2 /* system can close (if needed) and (re-)open a specific FD number */ if (1) { /* scoping */ TYPE_FD devnull = open("/dev/null", O_RDWR); @@ -250,13 +309,15 @@ void background(void) fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDIN"); if (dup2(devnull, STDOUT_FILENO) != STDOUT_FILENO) fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDOUT"); - if (dup2(devnull, STDERR_FILENO) != STDERR_FILENO) - fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDERR"); + if (stderr_disabled) { + if (dup2(devnull, STDERR_FILENO) != STDERR_FILENO) + fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDERR"); + } close(devnull); } -#else -# ifdef HAVE_DUP +# else +# ifdef HAVE_DUP /* opportunistically duplicate to the "lowest-available" FD number */ close(STDIN_FILENO); if (open("/dev/null", O_RDWR) != STDIN_FILENO) @@ -266,10 +327,12 @@ void background(void) if (dup(STDIN_FILENO) != STDOUT_FILENO) fatal_with_errno(EXIT_FAILURE, "dup /dev/null as STDOUT"); - close(STDERR_FILENO); - if (dup(STDIN_FILENO) != STDERR_FILENO) - fatal_with_errno(EXIT_FAILURE, "dup /dev/null as STDERR"); -# else + if (stderr_disabled) { + close(STDERR_FILENO); + if (dup(STDIN_FILENO) != STDERR_FILENO) + fatal_with_errno(EXIT_FAILURE, "dup /dev/null as STDERR"); + } +# else close(STDIN_FILENO); if (open("/dev/null", O_RDWR) != STDIN_FILENO) fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDIN"); @@ -278,20 +341,18 @@ void background(void) if (open("/dev/null", O_RDWR) != STDOUT_FILENO) fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDOUT"); - close(STDERR_FILENO); - if (open("/dev/null", O_RDWR) != STDERR_FILENO) - fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDERR"); + if (stderr_disabled) { + close(STDERR_FILENO); + if (open("/dev/null", O_RDWR) != STDERR_FILENO) + fatal_with_errno(EXIT_FAILURE, "re-open /dev/null as STDERR"); + } +# endif # endif -#endif -#ifdef HAVE_SETSID +# ifdef HAVE_SETSID setsid(); /* make a new session to dodge signals */ -#endif - -#else /* WIN32 */ - xbit_set(&upslog_flags, UPSLOG_SYSLOG); - xbit_clear(&upslog_flags, UPSLOG_STDERR); -#endif +# endif +#endif /* not WIN32 */ upslogx(LOG_INFO, "Startup successful"); } @@ -394,6 +455,15 @@ void become_user(struct passwd *pw) #endif } +#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 push +#endif +#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 /* drop down into a directory and throw away pointers to the old path */ void chroot_start(const char *path) { @@ -416,6 +486,616 @@ void chroot_start(const char *path) #endif } +char * getprocname(pid_t pid) +{ + /* Try to identify process (program) name for the given PID, + * return NULL if we can not for any reason (does not run, + * no rights, do not know how to get it on current OS, etc.) + * If the returned value is not NULL, caller should free() it. + * Some implementation pieces borrowed from + * https://man7.org/linux/man-pages/man2/readlink.2.html and + * https://github.com/openbsd/src/blob/master/bin/ps/ps.c + * NOTE: Very much platform-dependent! + */ + char *procname = NULL; + size_t procnamelen = 0; +#ifdef UNIX_PATH_MAX + char pathname[UNIX_PATH_MAX]; +#else + char pathname[PATH_MAX]; +#endif + struct stat st; + +#ifdef WIN32 + /* Try Windows API calls, then fall through to /proc emulation in MinGW/MSYS2 + * https://stackoverflow.com/questions/1591342/c-how-to-determine-if-a-windows-process-is-running + * http://cppip.blogspot.com/2013/01/check-if-process-is-running.html + */ + upsdebugx(5, "%s: begin to query WIN32 process info", __func__); + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)pid); + if (process) { + DWORD ret = GetModuleFileNameExA( + process, /* hProcess */ + NULL, /* hModule */ + (LPSTR)pathname, + (DWORD)(sizeof(pathname)) + ); + CloseHandle(process); + pathname[sizeof(pathname) - 1] = '\0'; + + if (ret) { + /* length of the string copied to the buffer */ + procnamelen = strlen(pathname); + + upsdebugx(3, "%s: try to parse the name from WIN32 process info", + __func__); + if (ret != procnamelen) { + upsdebugx(3, "%s: length mismatch getting WIN32 process info: %" + PRIuMAX " vs. " PRIuSIZE, + __func__, (uintmax_t)ret, procnamelen); + } + + if ((procname = (char*)calloc(procnamelen + 1, sizeof(char)))) { + if (snprintf(procname, procnamelen + 1, "%s", pathname) < 1) { + upsdebug_with_errno(3, "%s: failed to snprintf procname: WIN32-like", __func__); + } else { + goto finish; + } + } else { + upsdebug_with_errno(3, "%s: failed to allocate the procname " + "string to store token from WIN32 size %" PRIuSIZE, + __func__, procnamelen); + } + + /* Fall through to try /proc etc. if available */ + } else { + LPVOID WinBuf; + DWORD WinErr = GetLastError(); + FormatMessage( + FORMAT_MESSAGE_MAX_WIDTH_MASK | + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + WinErr, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR) &WinBuf, + 0, NULL ); + + upsdebugx(3, "%s: failed to get WIN32 process info: %s", + __func__, (char *)WinBuf); + LocalFree(WinBuf); + } + } +#endif + + if (stat("/proc", &st) == 0 && ((st.st_mode & S_IFMT) == S_IFDIR)) { + upsdebugx(3, "%s: /proc is an accessible directory, investigating", __func__); + +#if (defined HAVE_READLINK) && HAVE_READLINK + /* Linux-like */ + if (snprintf(pathname, sizeof(pathname), "/proc/%" PRIuMAX "/exe", (uintmax_t)pid) < 10) { + upsdebug_with_errno(3, "%s: failed to snprintf pathname: Linux-like", __func__); + goto finish; + } + + if (lstat(pathname, &st) == 0) { + goto process_stat_symlink; + } + + /* FreeBSD-like */ + if (snprintf(pathname, sizeof(pathname), "/proc/%" PRIuMAX "/file", (uintmax_t)pid) < 10) { + upsdebug_with_errno(3, "%s: failed to snprintf pathname: FreeBSD-like", __func__); + goto finish; + } + + if (lstat(pathname, &st) == 0) { + goto process_stat_symlink; + } + + goto process_parse_file; + +process_stat_symlink: + upsdebugx(3, "%s: located symlink for PID %" PRIuMAX " at: %s", + __func__, (uintmax_t)pid, pathname); + /* Some magic symlinks under (for example) /proc and /sys + * report 'st_size' as zero. In that case, take PATH_MAX + * or equivalent as a "good enough" estimate. */ + if (st.st_size) { + /* Add one for ending '\0' */ + procnamelen = st.st_size + 1; + } else { + procnamelen = sizeof(pathname); + } + + /* Not xcalloc() here, not too fatal if we fail */ + procname = (char*)calloc(procnamelen, sizeof(char)); + if (procname) { + int nbytes = readlink(pathname, procname, procnamelen); + if (nbytes < 0) { + upsdebug_with_errno(1, "%s: failed to readlink() from %s", + __func__, pathname); + free(procname); + procname = NULL; + goto process_parse_file; + } +#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 int)nbytes > SIZE_MAX || procnamelen <= (size_t)nbytes) { +#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 + upsdebugx(1, "%s: failed to readlink() from %s: may have been truncated", + __func__, pathname); + free(procname); + procname = NULL; + goto process_parse_file; + } + + /* Got a useful reply */ + procname[nbytes] = '\0'; + goto finish; + } else { + upsdebug_with_errno(3, "%s: failed to allocate the procname string " + "to readlink() size %" PRIuSIZE, __func__, procnamelen); + goto finish; + } +#else + upsdebugx(3, "%s: this platform does not have readlink(), skipping this method", __func__); + goto process_parse_file; +#endif /* HAVE_READLINK */ + +process_parse_file: + upsdebugx(5, "%s: try to parse some files under /proc", __func__); + + /* Check /proc/NNN/cmdline (may start with a '-' to ignore, for + * a title string like "-bash" where programs edit their argv[0] + * (Linux-like OSes at least). Inspired by + * https://gist.github.com/evanslai/30c6d588a80222f665f10b4577dadd61 + */ + if (snprintf(pathname, sizeof(pathname), "/proc/%" PRIuMAX "/cmdline", (uintmax_t)pid) < 10) { + upsdebug_with_errno(3, "%s: failed to snprintf pathname: Linux-like", __func__); + goto finish; + } + + if (stat(pathname, &st) == 0) { + FILE* fp = fopen(pathname, "r"); + if (fp) { + char buf[sizeof(pathname)]; + if (fgets(buf, sizeof(buf), fp) != NULL) { + /* check the first token in the file, the program name */ + char* first = strtok(buf, " "); + + fclose(fp); + if (first) { + if (*first == '-') + first++; + + /* Not xcalloc() here, not too fatal if we fail */ + if ((procnamelen = strlen(first))) { + upsdebugx(3, "%s: try to parse some files under /proc: processing %s", + __func__, pathname); + if ((procname = (char*)calloc(procnamelen + 1, sizeof(char)))) { + if (snprintf(procname, procnamelen + 1, "%s", first) < 1) { + upsdebug_with_errno(3, "%s: failed to snprintf procname: Linux-like", __func__); + } + } else { + upsdebug_with_errno(3, "%s: failed to allocate the procname " + "string to store token from 'cmdline' size %" PRIuSIZE, + __func__, procnamelen); + } + + goto finish; + } + } + } else { + fclose(fp); + } + } + } + + /* Check /proc/NNN/stat (second token, in parentheses, may be truncated) + * see e.g. https://stackoverflow.com/a/12675103/4715872 */ + if (snprintf(pathname, sizeof(pathname), "/proc/%" PRIuMAX "/stat", (uintmax_t)pid) < 10) { + upsdebug_with_errno(3, "%s: failed to snprintf pathname: Linux-like", __func__); + goto finish; + } + + if (stat(pathname, &st) == 0) { + FILE* fp = fopen(pathname, "r"); + if (fp) { + long spid; + char sstate; + char buf[sizeof(pathname)]; + + memset (buf, 0, sizeof(buf)); + if ( (fscanf(fp, "%ld (%[^)]) %c", &spid, buf, &sstate)) == 3 ) { + /* Some names can be pretty titles like "init(Ubuntu)" + * or "Relay(223)". Or truncated like "docker-desktop-". + * Tokenize by "(" " " and extract the first token to + * address the former "problem", not too much we can + * do about the latter except for keeping NUT program + * names concise. + */ + char* first = strtok(buf, "( "); + + fclose(fp); + if (first) { + /* Not xcalloc() here, not too fatal if we fail */ + if ((procnamelen = strlen(first))) { + upsdebugx(3, "%s: try to parse some files under /proc: processing %s " + "(WARNING: may be truncated)", + __func__, pathname); + if ((procname = (char*)calloc(procnamelen + 1, sizeof(char)))) { + if (snprintf(procname, procnamelen + 1, "%s", first) < 1) { + upsdebug_with_errno(3, "%s: failed to snprintf procname: Linux-like", __func__); + } + } else { + upsdebug_with_errno(3, "%s: failed to allocate the procname " + "string to store token from 'stat' size %" PRIuSIZE, + __func__, procnamelen); + } + + goto finish; + } + } + } else { + fclose(fp); + } + } + } + +#if defined(HAVE_LIB_ILLUMOS_PROC) && HAVE_LIB_ILLUMOS_PROC + /* Solaris/illumos: parse binary structure at /proc/NNN/psinfo */ + if (snprintf(pathname, sizeof(pathname), "/proc/%" PRIuMAX "/psinfo", (uintmax_t)pid) < 10) { + upsdebug_with_errno(3, "%s: failed to snprintf pathname: Solaris/illumos-like", __func__); + goto finish; + } + + if (stat(pathname, &st) == 0) { + FILE* fp = fopen(pathname, "r"); + if (!fp) { + upsdebug_with_errno(3, "%s: try to parse '%s':" + "fopen() returned NULL", __func__, pathname); + } else { + psinfo_t info; /* process information from /proc */ + size_t r; + + memset (&info, 0, sizeof(info)); + r = fread((char *)&info, sizeof (info), 1, fp); + if (r != 1) { + upsdebug_with_errno(3, "%s: try to parse '%s': " + "unexpected read size: got %" PRIuSIZE + " record(s) from file of size %" PRIuMAX + " vs. 1 piece of %" PRIuSIZE " struct size", + __func__, pathname, r, + (uintmax_t)st.st_size, sizeof (info)); + fclose(fp); + } else { + fclose(fp); + + /* Not xcalloc() here, not too fatal if we fail */ + if ((procnamelen = strlen(info.pr_fname))) { + upsdebugx(3, "%s: try to parse some files under /proc: processing %s", + __func__, pathname); + if ((procname = (char*)calloc(procnamelen + 1, sizeof(char)))) { + if (snprintf(procname, procnamelen + 1, "%s", info.pr_fname) < 1) { + upsdebug_with_errno(3, "%s: failed to snprintf pathname: Solaris/illumos-like", __func__); + } + } else { + upsdebug_with_errno(3, "%s: failed to allocate the procname " + "string to store token from 'psinfo' size %" PRIuSIZE, + __func__, procnamelen); + } + + goto finish; + } + } + } + } +#endif + } else { + upsdebug_with_errno(3, "%s: /proc is not a directory or not accessible", __func__); + } + +#if defined(HAVE_LIB_BSD_KVM_PROC) && HAVE_LIB_BSD_KVM_PROC + /* OpenBSD, maybe other BSD: no /proc; use API call, see ps.c link above and + * https://kaashif.co.uk/2015/06/18/how-to-get-a-list-of-processes-on-openbsd-in-c/ + */ + if (!procname) { + char errbuf[_POSIX2_LINE_MAX]; + kvm_t *kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); + + upsdebugx(3, "%s: try to parse BSD KVM process info snapsnot", __func__); + if (!kd) { + upsdebugx(3, "%s: try to parse BSD KVM process info snapsnot: " + "kvm_openfiles() returned NULL", __func__); + } else { + int nentries = 0; + struct kinfo_proc *kp = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(*kp), &nentries); + + if (!kp) { + upsdebugx(3, "%s: try to parse BSD KVM process info snapsnot: " + "kvm_getprocs() returned NULL", __func__); + } else { + int i; + if (nentries != 1) + upsdebugx(3, "%s: expected to get 1 reply from BSD kvm_getprocs but got %d", + __func__, nentries); + for (i = 0; i < nentries; i++) { + upsdebugx(5, "%s: processing reply #%d from BSD" + " kvm_getprocs: pid=%" PRIuMAX " name='%s'", + __func__, i, (uintmax_t)kp[i].p_pid, kp[i].p_comm); + if ((uintmax_t)(kp[i].p_pid) == (uintmax_t)pid) { + /* Not xcalloc() here, not too fatal if we fail */ + if ((procnamelen = strlen(kp[i].p_comm))) { + if ((procname = (char*)calloc(procnamelen + 1, sizeof(char)))) { + if (snprintf(procname, procnamelen + 1, "%s", kp[i].p_comm) < 1) { + upsdebug_with_errno(3, "%s: failed to snprintf procname: BSD-like", __func__); + } + } else { + upsdebug_with_errno(3, "%s: failed to allocate the procname " + "string to store token from BSD KVM process info " + "snapsnot size %" PRIuSIZE, + __func__, procnamelen); + } + + goto finish; + } + } + } + } + } + } +#endif /* HAVE_LIB_BSD_KVM_PROC */ + + goto finish; + +finish: + if (procname) { + procnamelen = strlen(procname); + if (procnamelen == 0) { + free(procname); + procname = NULL; + } else { + upsdebugx(1, "%s: determined process name for PID %" PRIuMAX ": %s", + __func__, (uintmax_t)pid, procname); + } + } + + if (!procname) { + upsdebugx(1, "%s: failed to determine process name for PID %" PRIuMAX, + __func__, (uintmax_t)pid); + } + + return procname; +} +#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 + +size_t parseprogbasename(char *buf, size_t buflen, const char *progname, size_t *pprogbasenamelen, size_t *pprogbasenamedot) +{ + size_t i, + progbasenamelen = 0, + progbasenamedot = 0; + + if (pprogbasenamelen) + *pprogbasenamelen = 0; + + if (pprogbasenamedot) + *pprogbasenamedot = 0; + + if (!buf || !progname || !buflen || progname[0] == '\0') + return 0; + + for (i = 0; i < buflen && progname[i] != '\0'; i++) { + if (progname[i] == '/' +#ifdef WIN32 + || progname[i] == '\\' +#endif + ) { + progbasenamelen = 0; + progbasenamedot = 0; + continue; + } + + if (progname[i] == '.') + progbasenamedot = progbasenamelen; + + buf[progbasenamelen++] = progname[i]; + } + buf[progbasenamelen] = '\0'; + buf[buflen - 1] = '\0'; + + if (pprogbasenamelen) + *pprogbasenamelen = progbasenamelen; + + if (pprogbasenamedot) + *pprogbasenamedot = progbasenamedot; + + return progbasenamelen; +} + +int checkprocname(pid_t pid, const char *progname) +{ + /* If we can determine the binary path name of the specified "pid", + * check if it matches the assumed name of the current program. + * Returns: + * -3 Skipped because NUT_IGNORE_CHECKPROCNAME is set + * -2 Could not parse a program name (ok to proceed, + * risky - but matches legacy behavior) + * -1 Could not identify a program name (ok to proceed, + * risky - but matches legacy behavior) + * 0 Process name identified, does not seem to match + * 1+ Process name identified, and seems to match with + * varying precision + * Generally speaking, if (checkprocname(...)) then ok to proceed + */ + char *procname = NULL, *s; + int ret = -127; + size_t procbasenamelen = 0, progbasenamelen = 0; + /* Track where the last dot is in the basename; 0 means none */ + size_t procbasenamedot = 0, progbasenamedot = 0; +#ifdef UNIX_PATH_MAX + char procbasename[UNIX_PATH_MAX], progbasename[UNIX_PATH_MAX]; +#else + char procbasename[PATH_MAX], progbasename[PATH_MAX]; +#endif + + if ((s = getenv("NUT_IGNORE_CHECKPROCNAME"))) { + /* FIXME: Make server/conf.c::parse_boolean() reusable */ + if ( (!strcasecmp(s, "true")) || (!strcasecmp(s, "on")) || (!strcasecmp(s, "yes")) || (!strcasecmp(s, "1"))) { + upsdebugx(1, "%s: skipping because caller set NUT_IGNORE_CHECKPROCNAME", __func__); + ret = -3; + goto finish; + } + } + + procname = getprocname(pid); + if (!procname || !progname) { + ret = -1; + goto finish; + } + + /* First quickly try for an exact hit (possible dir names included) */ + if (!strcmp(procname, progname)) { + ret = 1; + goto finish; + } + + /* Parse the basenames apart */ + if (!parseprogbasename(progbasename, sizeof(progbasename), progname, &progbasenamelen, &progbasenamedot) + || !parseprogbasename(procbasename, sizeof(procbasename), procname, &procbasenamelen, &procbasenamedot) + ) { + ret = -2; + goto finish; + } + + /* First quickly try for an exact hit of base names */ + if (progbasenamelen == procbasenamelen && progbasenamedot == procbasenamedot && !strcmp(procname, progname)) { + ret = 2; + goto finish; + } + + /* Check for executable program filename extensions and/or case-insensitive + * matching on some platforms */ +#ifdef WIN32 + if (!strcasecmp(procname, progname)) { + ret = 3; + goto finish; + } + + if (!strcasecmp(procbasename, progbasename)) { + ret = 4; + goto finish; + } + + if (progbasenamedot == procbasenamedot || !progbasenamedot || !procbasenamedot) { + /* Same base name before ext, maybe different casing or absence of ext in one of them */ + size_t dot = progbasenamedot ? progbasenamedot : procbasenamedot; + + if (!strncasecmp(progbasename, procbasename, dot - 1) && + ( (progbasenamedot && !strcasecmp(progbasename + progbasenamedot, ".exe")) + || (procbasenamedot && !strcasecmp(procbasename + procbasenamedot, ".exe")) ) + ) { + ret = 5; + goto finish; + } + } +#endif + + /* Nothing above has matched */ + ret = 0; + +finish: + switch (ret) { + case 5: + upsdebugx(1, + "%s: case-insensitive base name hit with " + "an executable program extension involved for " + "PID %" PRIuMAX " of '%s'=>'%s' and checked " + "'%s'=>'%s'", + __func__, (uintmax_t)pid, + procname, procbasename, + progname, progbasename); + break; + + case 4: + upsdebugx(1, + "%s: case-insensitive base name hit for PID %" + PRIuMAX " of '%s'=>'%s' and checked '%s'=>'%s'", + __func__, (uintmax_t)pid, + procname, procbasename, + progname, progbasename); + break; + + case 3: + upsdebugx(1, + "%s: case-insensitive full name hit for PID %" + PRIuMAX " of '%s' and checked '%s'", + __func__, (uintmax_t)pid, procname, progname); + break; + + case 2: + upsdebugx(1, + "%s: case-sensitive base name hit for PID %" + PRIuMAX " of '%s'=>'%s' and checked '%s'=>'%s'", + __func__, (uintmax_t)pid, + procname, procbasename, + progname, progbasename); + break; + + case 1: + upsdebugx(1, + "%s: exact case-sensitive full name hit for PID %" + PRIuMAX " of '%s' and checked '%s'", + __func__, (uintmax_t)pid, procname, progname); + break; + + case 0: + upsdebugx(1, + "%s: did not find any match of program names " + "for PID %" PRIuMAX " of '%s'=>'%s' and checked " + "'%s'=>'%s'", + __func__, (uintmax_t)pid, + procname, procbasename, + progname, progbasename); + break; + + case -1: + /* failed to getprocname(), logged above in it */ + break; + + case -2: + upsdebugx(1, + "%s: failed to parse base names of the programs", + __func__); + break; + + case -3: + /* skipped due to envvar, logged above */ + break; + + default: + upsdebugx(1, + "%s: unexpected result looking for process name " + "of PID %" PRIuMAX ": %d", + __func__, (uintmax_t)pid, ret); + ret = -127; + break; + } + + return ret; +} + #ifdef WIN32 /* In WIN32 all non binaries files (namely configuration and PID files) are retrieved relative to the path of the binary itself. @@ -475,11 +1155,14 @@ void writepid(const char *name) /* send sig to pid, returns -1 for error, or * zero for a successfully sent signal */ -int sendsignalpid(pid_t pid, int sig) +int sendsignalpid(pid_t pid, int sig, const char *progname, int check_current_progname) { #ifndef WIN32 - int ret; + int ret, cpn1 = -10, cpn2 = -10; + char *current_progname = NULL; + /* TOTHINK: What about containers where a NUT daemon *is* the only process + * and is the PID=1 of the container (recycle if dead)? */ if (pid < 2 || pid > get_max_pid_t()) { if (nut_debug_level > 0 || nut_sendsignal_debug_level > 0) upslogx(LOG_NOTICE, @@ -488,7 +1171,117 @@ int sendsignalpid(pid_t pid, int sig) return -1; } - /* see if this is going to work first - does the process exist? */ + ret = 0; + if (progname) { + /* Check against some expected (often built-in) name */ + if (!(cpn1 = checkprocname(pid, progname))) { + /* Did not match expected (often built-in) name */ + ret = -1; + } else { + if (cpn1 > 0) { + /* Matched expected name, ok to proceed */ + ret = 1; + } + /* ...else could not determine name of PID; think later */ + } + } + /* if (cpn1 == -3) => NUT_IGNORE_CHECKPROCNAME=true */ + /* if (cpn1 == -1) => could not determine name of PID... retry just in case? */ + if (ret <= 0 && check_current_progname && cpn1 != -3) { + /* NOTE: This could be optimized a bit by pre-finding the procname + * of "pid" and re-using it, but this is not a hot enough code path + * to bother much. + */ + current_progname = getprocname(getpid()); + if (current_progname && (cpn2 = checkprocname(pid, current_progname))) { + if (cpn2 > 0) { + /* Matched current process as asked, ok to proceed */ + ret = 2; + } + /* ...else could not determine name of PID; think later */ + } else { + if (current_progname) { + /* Did not match current process name */ + ret = -2; + } /* else just did not determine current process + * name, so did not actually check either + * // ret = -3; + */ + } + } + + /* if ret == 0, ok to proceed - not asked for any sanity checks; + * if ret > 0 we had some definitive match above + */ + if (ret < 0) { + upsdebugx(1, + "%s: ran at least one check, and all such checks " + "found a process name for PID %" PRIuMAX " and " + "failed to match: expected progname='%s' (res=%d), " + "current progname='%s' (res=%d)", + __func__, (uintmax_t)pid, + NUT_STRARG(progname), cpn1, + NUT_STRARG(current_progname), cpn2); + + if (nut_debug_level > 0 || nut_sendsignal_debug_level > 1) { + switch (ret) { + case -1: + upslogx(LOG_ERR, "Tried to signal PID %" PRIuMAX + " which exists but is not of" + " expected program '%s'; not asked" + " to cross-check current PID's name", + (uintmax_t)pid, progname); + break; + + /* Maybe we tried both data sources, maybe just current_progname */ + case -2: + /*case -3:*/ + if (progname && current_progname) { + /* Tried both, downgraded verdict further */ + upslogx(LOG_ERR, "Tried to signal PID %" PRIuMAX + " which exists but is not of expected" + " program '%s' nor current '%s'", + (uintmax_t)pid, progname, current_progname); + } else if (current_progname) { + /* Not asked for progname==NULL */ + upslogx(LOG_ERR, "Tried to signal PID %" PRIuMAX + " which exists but is not of" + " current program '%s'", + (uintmax_t)pid, current_progname); + } else if (progname) { + upslogx(LOG_ERR, "Tried to signal PID %" PRIuMAX + " which exists but is not of" + " expected program '%s'; could not" + " cross-check current PID's name", + (uintmax_t)pid, progname); + } else { + /* Both NULL; one not asked, another not detected; + * should not actually get here (wannabe `ret==-3`) + */ + upslogx(LOG_ERR, "Tried to signal PID %" PRIuMAX + " but could not cross-check current PID's" + " name: did not expect to get here", + (uintmax_t)pid); + } + break; + } + } + + if (current_progname) { + free(current_progname); + current_progname = NULL; + } + + /* Logged or not, sanity-check was requested and failed */ + return -1; + } + if (current_progname) { + free(current_progname); + current_progname = NULL; + } + + /* see if this is going to work first - does the process exist, + * and do we have permissions to signal it? */ ret = kill(pid, 0); if (ret < 0) { @@ -512,6 +1305,9 @@ int sendsignalpid(pid_t pid, int sig) #else NUT_UNUSED_VARIABLE(pid); NUT_UNUSED_VARIABLE(sig); + NUT_UNUSED_VARIABLE(progname); + NUT_UNUSED_VARIABLE(check_current_progname); + /* Windows builds use named pipes, not signals per se */ upslogx(LOG_ERR, "%s: not implemented for Win32 and " "should not have been called directly!", @@ -552,7 +1348,7 @@ pid_t parsepid(const char *buf) * zero for a successfully sent signal */ #ifndef WIN32 -int sendsignalfn(const char *pidfn, int sig) +int sendsignalfn(const char *pidfn, int sig, const char *progname, int check_current_progname) { char buf[SMALLBUF]; FILE *pidf; @@ -588,7 +1384,7 @@ int sendsignalfn(const char *pidfn, int sig) if (pid >= 0) { /* this method actively reports errors, if any */ - ret = sendsignalpid(pid, sig); + ret = sendsignalpid(pid, sig, progname, check_current_progname); } fclose(pidf); @@ -597,9 +1393,11 @@ int sendsignalfn(const char *pidfn, int sig) #else /* => WIN32 */ -int sendsignalfn(const char *pidfn, const char * sig) +int sendsignalfn(const char *pidfn, const char * sig, const char *progname_ignored, int check_current_progname_ignored) { BOOL ret; + NUT_UNUSED_VARIABLE(progname_ignored); + NUT_UNUSED_VARIABLE(check_current_progname_ignored); ret = send_to_named_pipe(pidfn, sig); @@ -662,18 +1460,21 @@ int snprintfcat(char *dst, size_t size, const char *fmt, ...) /* lazy way to send a signal if the program uses the PIDPATH */ #ifndef WIN32 -int sendsignal(const char *progname, int sig) +int sendsignal(const char *progname, int sig, int check_current_progname) { char fn[SMALLBUF]; snprintf(fn, sizeof(fn), "%s/%s.pid", rootpidpath(), progname); - return sendsignalfn(fn, sig); + return sendsignalfn(fn, sig, progname, check_current_progname); } #else -int sendsignal(const char *progname, const char * sig) +int sendsignal(const char *progname, const char * sig, int check_current_progname) { - return sendsignalfn(progname, sig); + /* progname is used as the pipe name for WIN32 + * check_current_progname is de-facto ignored + */ + return sendsignalfn(progname, sig, NULL, check_current_progname); } #endif @@ -881,9 +1682,15 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) va_end(va); if ((ret < 0) || (ret >= (int) sizeof(msgbuf))) { - syslog(LOG_WARNING, - "%s (%s:%d): vsnprintf needed more than %" PRIuSIZE " bytes: %d", - __func__, __FILE__, __LINE__, sizeof(msgbuf), ret); + if (syslog_is_disabled()) { + fprintf(stderr, + "%s (%s:%d): vsnprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(msgbuf), ret); + } else { + syslog(LOG_WARNING, + "%s (%s:%d): vsnprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(msgbuf), ret); + } } else { msglen = strlen(msgbuf); } @@ -919,9 +1726,15 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) usec_t monots = timespec_load(&monoclock_ts); ret = snprintf(monoclock_str + 1, sizeof(monoclock_str) - 1, "MONOTONIC_USEC=%" PRI_USEC, monots); if ((ret < 0) || (ret >= (int) sizeof(monoclock_str) - 1)) { - syslog(LOG_WARNING, - "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", - __func__, __FILE__, __LINE__, sizeof(monoclock_str), ret); + if (syslog_is_disabled()) { + fprintf(stderr, + "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(monoclock_str), ret); + } else { + syslog(LOG_WARNING, + "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(monoclock_str), ret); + } msglen = 0; } else { monoclock_str[0] = '\n'; @@ -938,9 +1751,15 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) if (msglen) { ret = snprintf(buf, sizeof(buf), "STATUS=%s", msgbuf); if ((ret < 0) || (ret >= (int) sizeof(buf))) { - syslog(LOG_WARNING, - "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", - __func__, __FILE__, __LINE__, sizeof(buf), ret); + if (syslog_is_disabled()) { + fprintf(stderr, + "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(buf), ret); + } else { + syslog(LOG_WARNING, + "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(buf), ret); + } msglen = 0; } else { msglen = (size_t)ret; @@ -1133,9 +1952,15 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) if ((ret < 0) || (ret >= (int) sizeof(buf))) { /* Refusal to send the watchdog ping is not an error to report */ if ( !(ret == -126 && (state == NOTIFY_STATE_WATCHDOG)) ) { - syslog(LOG_WARNING, - "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", - __func__, __FILE__, __LINE__, sizeof(buf), ret); + if (syslog_is_disabled()) { + fprintf(stderr, + "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(buf), ret); + } else { + syslog(LOG_WARNING, + "%s (%s:%d): snprintf needed more than %" PRIuSIZE " bytes: %d", + __func__, __FILE__, __LINE__, sizeof(buf), ret); + } } ret = -1; } else { @@ -1370,10 +2195,17 @@ static void vupslog(int priority, const char *fmt, va_list va, int use_strerror) /* Arbitrary limit, gotta stop somewhere */ if (bufsize > LARGEBUF * 64) { vupslog_too_long: - syslog(LOG_WARNING, "vupslog: vsnprintf needed " - "more than %" PRIuSIZE " bytes; logged " - "output can be truncated", - bufsize); + if (syslog_is_disabled()) { + fprintf(stderr, "vupslog: vsnprintf needed " + "more than %" PRIuSIZE " bytes; logged " + "output can be truncated", + bufsize); + } else { + syslog(LOG_WARNING, "vupslog: vsnprintf needed " + "more than %" PRIuSIZE " bytes; logged " + "output can be truncated", + bufsize); + } break; } } while(1); @@ -1677,8 +2509,13 @@ void s_upsdebug_with_errno(int level, const char *fmt, ...) ret = snprintf(fmt2, sizeof(fmt2), "[D%d] %s", level, fmt); } if ((ret < 0) || (ret >= (int) sizeof(fmt2))) { - syslog(LOG_WARNING, "upsdebug_with_errno: snprintf needed more than %d bytes", - LARGEBUF); + if (syslog_is_disabled()) { + fprintf(stderr, "upsdebug_with_errno: snprintf needed more than %d bytes", + LARGEBUF); + } else { + syslog(LOG_WARNING, "upsdebug_with_errno: snprintf needed more than %d bytes", + LARGEBUF); + } } else { fmt = (const char *)fmt2; } @@ -1727,8 +2564,13 @@ void s_upsdebugx(int level, const char *fmt, ...) } if ((ret < 0) || (ret >= (int) sizeof(fmt2))) { - syslog(LOG_WARNING, "upsdebugx: snprintf needed more than %d bytes", - LARGEBUF); + if (syslog_is_disabled()) { + fprintf(stderr, "upsdebugx: snprintf needed more than %d bytes", + LARGEBUF); + } else { + syslog(LOG_WARNING, "upsdebugx: snprintf needed more than %d bytes", + LARGEBUF); + } } else { fmt = (const char *)fmt2; } @@ -1855,10 +2697,25 @@ void s_upsdebug_ascii(int level, const char *msg, const void *buf, size_t len) static void vfatal(const char *fmt, va_list va, int use_strerror) { + /* Normally we enable SYSLOG and disable STDERR, + * unless NUT_DEBUG_SYSLOG envvar interferes as + * interpreted in syslog_is_disabled() method: */ + int syslog_disabled = syslog_is_disabled(), + stderr_disabled = (syslog_disabled == 0 || syslog_disabled == 2); + if (xbit_test(upslog_flags, UPSLOG_STDERR_ON_FATAL)) xbit_set(&upslog_flags, UPSLOG_STDERR); - if (xbit_test(upslog_flags, UPSLOG_SYSLOG_ON_FATAL)) - xbit_set(&upslog_flags, UPSLOG_SYSLOG); + if (xbit_test(upslog_flags, UPSLOG_SYSLOG_ON_FATAL)) { + if (syslog_disabled) { + /* FIXME: Corner case... env asked for stderr + * instead of syslog - should we care about + * UPSLOG_STDERR_ON_FATAL being not set? */ + if (!stderr_disabled) + xbit_set(&upslog_flags, UPSLOG_STDERR); + } else { + xbit_set(&upslog_flags, UPSLOG_SYSLOG); + } + } #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic push diff --git a/conf/nut.conf.sample b/conf/nut.conf.sample index 1c6ecbf11d..ab69529893 100644 --- a/conf/nut.conf.sample +++ b/conf/nut.conf.sample @@ -91,3 +91,22 @@ export ALLOW_NO_DEVICE # verbosity passed to NUT daemons. As an environment variable, its priority sits # between that of 'DEBUG_MIN' setting of a driver and the command-line options. #NUT_DEBUG_LEVEL=0 + +# Normally NUT can (attempt to) use the syslog or Event Log (WIN32), but the +# environment variable 'NUT_DEBUG_SYSLOG' allows to bypass it, and perhaps keep +# the daemons logging to stderr (useful e.g. in NUT Integration Test suite to +# not pollute the OS logs, or in systemd where stderr and syslog both go into +# the same journal). Recognized values: +# `stderr` Disabled and background() keeps stderr attached +# `none` Disabled and background() detaches stderr as usual +# `default`/unset/other Not disabled +#NUT_DEBUG_SYSLOG=stderr + +# Normally NUT can (attempt to) verify that the program file name matches the +# name associated with a running process, when using PID files to send signals. +# The `NUT_IGNORE_CHECKPROCNAME` boolean toggle allows to quickly skip such +# verification, in case it causes problems (e.g. NUT programs were renamed and +# do not match built-in expectations). This environment variable can also be +# optionally set in init-scripts or service methods for `upsd`, `upsmon` and +# NUT drivers/`upsdrvctl`. +#NUT_IGNORE_CHECKPROCNAME=true diff --git a/configure.ac b/configure.ac index 9b212314fa..7edb3a9ba7 100644 --- a/configure.ac +++ b/configure.ac @@ -1047,6 +1047,12 @@ AC_CHECK_HEADER([sys/select.h], [AC_DEFINE([HAVE_SYS_SELECT_H], [1], [Define to 1 if you have .])]) +AC_CHECK_HEADER([unistd.h], + [AC_DEFINE([HAVE_UNISTD_H], [1], + [Define to 1 if you have .])]) + +AC_CHECK_FUNCS(readlink) + AC_CACHE_CHECK([for suseconds_t], [ac_cv_type_suseconds_t], [AC_COMPILE_IFELSE( @@ -1105,6 +1111,68 @@ AS_IF([test x"${ac_cv_func_usleep}" = xyes], [AC_MSG_WARN([Required C library routine usleep not found; try adding -D_POSIX_C_SOURCE=200112L])] ) +dnl OpenBSD (at least) methods to query process info, per +dnl https://github.com/openbsd/src/blob/master/bin/ps/ps.c +dnl https://kaashif.co.uk/2015/06/18/how-to-get-a-list-of-processes-on-openbsd-in-c/ +BSDKVMPROCLIBS="" +myLIBS="$LIBS" +LIBS="$LIBS -lkvm" +AC_CACHE_CHECK([for BSD KVM process info libs], + [ac_cv_lib_bsd_kvm_proc], + [AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[ +#include +#include +#include +#include +#include + ]], + [[ + char errbuf[_POSIX2_LINE_MAX]; + kvm_t *kernel = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); + int nentries = 0; + struct kinfo_proc *kinfo = kvm_getprocs(kernel, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &nentries); + int i; + for (i = 0; i < nentries; ++i) { + printf("%s\n", kinfo[i].p_comm); + } +/* autoconf adds ";return 0;" */ +/* we hope the code above fails if type is not defined or range is not sufficient */ +]])], + [ac_cv_lib_bsd_kvm_proc=yes + BSDKVMPROCLIBS="-lkvm" + ], [ac_cv_lib_bsd_kvm_proc=no] + )]) +LIBS="$myLIBS" + +AS_IF([test x"${ac_cv_lib_bsd_kvm_proc}" = xyes], + [AC_DEFINE([HAVE_LIB_BSD_KVM_PROC], 1, [defined if we have libs, includes and methods for BSD KVM process info])] + ) + +dnl https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/procfs.h#L318 +dnl https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/ps/ps.c +AC_CACHE_CHECK([for Solaris/illumos process info libs], + [ac_cv_lib_illumos_proc], + [AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[ +#include +#include + ]], + [[ + psinfo_t info; + printf("%s", info.pr_fname) +/* autoconf adds ";return 0;" */ +/* we hope the code above fails if type is not defined or range is not sufficient */ +]])], + [ac_cv_lib_illumos_proc=yes + ], [ac_cv_lib_illumos_proc=no] + )]) +LIBS="$myLIBS" + +AS_IF([test x"${ac_cv_lib_illumos_proc}" = xyes], + [AC_DEFINE([HAVE_LIB_ILLUMOS_PROC], 1, [defined if we have libs, includes and methods for Solaris/illumos process info])] + ) + AC_LANG_POP([C]) dnl These routines' arg types differ in strict C standard mode @@ -2812,26 +2880,26 @@ dnl not fail if we have no tools to generate it (so add to SKIP list). dnl Avoid rebuilding existing build products due to their timestamp dependencies: touch -r "${abs_srcdir}"/docs/man/Makefile.am "${abs_srcdir}"/docs/man/*.{1,2,3,4,5,6,7,8,9}* "${abs_srcdir}"/docs/man/*.{txt,xml,html,pdf} || true else - if test "${can_build_doc_man}" = yes ; then - AC_MSG_RESULT(yes) - DOC_BUILD_LIST="${DOC_BUILD_LIST} ${nut_doc_build_target_base}" - else - AC_MSG_RESULT(no) - if test "${nut_doc_build_target_flag}" = "yes" ; then - DOC_CANNOTBUILD_LIST="${DOC_CANNOTBUILD_LIST} ${nut_doc_build_target_base}" - AC_MSG_WARN([Unable to build ${nut_doc_build_target_base} documentation which you requested]) + if test "${can_build_doc_man}" = yes ; then + AC_MSG_RESULT(yes) + DOC_BUILD_LIST="${DOC_BUILD_LIST} ${nut_doc_build_target_base}" else - DOC_SKIPBUILD_LIST="${DOC_SKIPBUILD_LIST} ${nut_doc_build_target_base}" - if test "${nut_doc_build_target_flag}" = "auto" || test "${nut_doc_build_target_flag}" = "dist-auto" ; then - if test "${have_disted_doc_man}" = yes ; then - AC_MSG_WARN([Unable to build ${nut_doc_build_target_base} documentation, but can install pre-built distributed copies]) - DOC_INSTALL_DISTED_MANS="yes" - else - AC_MSG_WARN([Unable to build ${nut_doc_build_target_base} documentation, and unable to install pre-built distributed copies because they are absent]) - fi - fi # Other variants include "no", "skip"... + AC_MSG_RESULT(no) + if test "${nut_doc_build_target_flag}" = "yes" ; then + DOC_CANNOTBUILD_LIST="${DOC_CANNOTBUILD_LIST} ${nut_doc_build_target_base}" + AC_MSG_WARN([Unable to build ${nut_doc_build_target_base} documentation which you requested]) + else + DOC_SKIPBUILD_LIST="${DOC_SKIPBUILD_LIST} ${nut_doc_build_target_base}" + if test "${nut_doc_build_target_flag}" = "auto" || test "${nut_doc_build_target_flag}" = "dist-auto" ; then + if test "${have_disted_doc_man}" = yes ; then + AC_MSG_WARN([Unable to build ${nut_doc_build_target_base} documentation, but can install pre-built distributed copies]) + DOC_INSTALL_DISTED_MANS="yes" + else + AC_MSG_WARN([Unable to build ${nut_doc_build_target_base} documentation, and unable to install pre-built distributed copies because they are absent]) + fi + fi # Other variants include "no", "skip"... + fi fi - fi fi ;; @@ -2878,9 +2946,9 @@ NUT_REPORT_FEATURE([build specific documentation format(s)], [${nut_with_doc}], # for "make check" in "docs/" here... DOC_CHECK_LIST="" if test "${nut_with_doc}" = yes ; then - for V in $DOC_BUILD_LIST ; do - DOC_CHECK_LIST="$DOC_CHECK_LIST check-$V" - done + for V in $DOC_BUILD_LIST ; do + DOC_CHECK_LIST="$DOC_CHECK_LIST check-$V" + done fi WITH_MANS=no @@ -3899,9 +3967,9 @@ fi AM_CONDITIONAL(WITH_AUGLENS, test -n "${auglensdir}") if test -n "${auglensdir}"; then - auglenstestsdir="${auglensdir}/tests" + auglenstestsdir="${auglensdir}/tests" else - auglenstestsdir='' + auglenstestsdir='' fi AC_PATH_PROGS([AUGPARSE], [augparse], [none]) @@ -3913,6 +3981,7 @@ else AC_MSG_RESULT(no) fi +dnl ---------------------------------------------------------------------- AC_MSG_CHECKING(whether to install hotplug rules) AC_ARG_WITH(hotplug-dir, @@ -4944,6 +5013,7 @@ AC_SUBST(DRIVER_BUILD_LIST) AC_SUBST(DRIVER_MAN_LIST) AC_SUBST(DRIVER_MAN_LIST_PAGES) AC_SUBST(DRIVER_INSTALL_TARGET) +AC_SUBST(BSDKVMPROCLIBS) AC_SUBST(NETLIBS) AC_SUBST(SERLIBS) AC_SUBST(SEMLIBS) diff --git a/data/driver.list.in b/data/driver.list.in index 7543a1fc02..9e0cdf9f42 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -196,6 +196,11 @@ "Best Power" "ups" "1" "Micro-Ferrups" "" "bestuferrups" "Best Power" "ups" "1" "Fortress/Ferrups" "f-command support" "bestfcom" +"Bicker" "ups" "3" "UPSIC-1205" "" "bicker_ser" +"Bicker" "ups" "3" "UPSIC-2403" "" "bicker_ser" +"Bicker" "ups" "3" "DC2412-UPS" "" "bicker_ser" +"Bicker" "ups" "3" "DC2412-UPS-LD" "" "bicker_ser" + "Borri" "ups" "2" "B400-010-B/B400-020-B/B400-030-B/B400-010-C/B400-020-C/B400-030-C" "USB" "blazer_usb" "Borri" "ups" "2" "B400-R010-B/B400-R020-B/B400-R030-B/B400-R010-C/B400-R020-C/B400-R030-C" "USB" "blazer_usb" "Borri" "ups" "2" "B500-060-B/B500-100-B/B500-060-C/B500-100-C" "USB" "blazer_usb" @@ -223,7 +228,7 @@ "Crown" "ups" "2" "CMU-SP1200IEC" "USB" "nutdrv_qx port=auto vendorid=0001 productid=0000 protocol=hunnox langid_fix=0x0409 novendor noscanlangid" # https://github.com/networkupstools/nut/pull/638 caveats at https://github.com/networkupstools/nut/issues/1014 -"Cyber Energy" "ups" "3" "Models with USB ID 0483:A430" "USB" "usbhid-ups" # https://alioth-lists.debian.net/pipermail/nut-upsdev/2024-February/007966.html +"Cyber Energy" "ups" "3" "Models with USB" "USB" "usbhid-ups" # https://alioth-lists.debian.net/pipermail/nut-upsdev/2024-February/007966.html https://alioth-lists.debian.net/pipermail/nut-upsdev/2024-June/008002.html "Cyber Power Systems" "ups" "1" "550SL" "" "genericups upstype=7" "Cyber Power Systems" "ups" "1" "725SL" "" "genericups upstype=7" diff --git a/docs/configure.txt b/docs/configure.txt index 3cc3faac24..b008a9f1d7 100644 --- a/docs/configure.txt +++ b/docs/configure.txt @@ -265,10 +265,17 @@ Other values understood for this option are listed below: * A `--with-doc=no` quietly skips generation of all types of documentation, including manpages. -* `--with-doc=skip` is used to configure some of the `make distcheck*` +* A `--with-doc=skip` is used to configure some of the `make distcheck*` scenarios to re-use man page files built and distributed by the main build and not waste time on re-generation of those. +* A `--with-doc=dist-auto` allows to use pre-distributed MAN pages if present + (should be in "tarball" release archives; should not be among Git-tracked + sources; may be left over from earlier builds in same workspace), or build + those if we can (the `auto` part). Practically this is implemented in detail + only for `--with-doc=man=dist-auto`, as we do not dist HTML and PDF products; + it is a placeholder for those to simplify the generic configuration calls. + Multiple documentation format values can be specified, separated with comma. Each such value can be suffixed with `=yes` to require building of this one documentation format (abort configuration if tools are missing), `=auto` to diff --git a/docs/documentation.txt b/docs/documentation.txt index ddc2d1f425..27abb1b76b 100644 --- a/docs/documentation.txt +++ b/docs/documentation.txt @@ -23,7 +23,7 @@ ifndef::website[] - link:https://www.networkupstools.org/ddl/index.html#_supported_devices[Devices Dumps Library (DDL)]: Provides information on how devices are supported; see also link:https://www.networkupstools.org/stable-hcl.html[the HCL] - link:../solaris-usb.html[Notes on NUT monitoring of USB devices in Solaris and related operating systems] endif::website[] -- link:https://github.com/networkupstools/ConfigExamples/releases/latest[NUT Configuration Examples] book maintained by Roger Price +- link:https://github.com/networkupstools/ConfigExamples/releases/latest/download/ConfigExamples.pdf[NUT Configuration Examples] book maintained by Roger Price - link:https://github.com/networkupstools/nut/wiki[NUT GitHub Wiki] Developer Documentation @@ -96,7 +96,11 @@ These are general information about UPS, PDU, ATS, PSU and SCD: These are writeups by users of the software. -- link:http://rogerprice.org/NUT.html[NUT Setup with openSUSE] '(Roger Price)' +- link:http://rogerprice.org/NUT[NUT Configuration Examples and helper scripts] + '(Roger Price)' (sources replicated in NUT GitHub organization as + link:https://github.com/networkupstools/ConfigExamples[ConfigExamples], + link:https://github.com/networkupstools/TLS-UPSmon[TLS-UPSmon], + and link:https://github.com/networkupstools/TLS-Shims[TLS-Shims]) - link:http://www.dimat.unina2.it/LCS/MonitoraggioUpsNutUbuntu10-eng.htm[Deploying NUT on an Ubuntu 10.04 cluster] '(Stefano Angelone)' - link:http://blog.shadypixel.com/monitoring-a-ups-with-nut-on-debian-or-ubuntu-linux[Monitoring a UPS with nut on Debian or Ubuntu Linux] '(Avery Fay)' - link:http://linux.developpez.com/cours/upsusb/[Installation et gestion d'un UPS USB en réseau sous linux] '(Olivier Van Hoof, french)' diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 403d6b9ad6..e39e2428c5 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -430,6 +430,7 @@ SRC_SERIAL_PAGES = \ bestuferrups.txt \ bestups.txt \ bestfcom.txt \ + bicker_ser.txt \ blazer-common.txt \ blazer_ser.txt \ clone.txt \ @@ -477,6 +478,7 @@ MAN_SERIAL_PAGES = \ bestuferrups.8 \ bestups.8 \ bestfcom.8 \ + bicker_ser.8 \ blazer_ser.8 \ clone.8 \ dummy-ups.8 \ @@ -526,6 +528,7 @@ HTML_SERIAL_MANS = \ bestuferrups.html \ bestups.html \ bestfcom.html \ + bicker_ser.html \ blazer_ser.html \ clone.html \ dummy-ups.html \ diff --git a/docs/man/bicker_ser.txt b/docs/man/bicker_ser.txt new file mode 100644 index 0000000000..bebecceef7 --- /dev/null +++ b/docs/man/bicker_ser.txt @@ -0,0 +1,123 @@ +BICKER_SER(8) +============= + +NAME +---- + +bicker_ser - Driver for Bicker DC UPS via serial port connections + +SYNOPSIS +-------- + +*bicker_ser* -h + +*bicker_ser* -a 'UPS_NAME' ['OPTIONS'] + +NOTE: This man page only documents the hardware-specific features of the +*bicker_ser* driver. For information about the core driver, see +linkman:nutupsdrv[8]. + +SUPPORTED HARDWARE +------------------ + +*bicker_ser* supports all Bicker UPSes shipped with the PSZ-1053 extension +module such as UPSIC-1205, UPSIC-2403, DC2412-UPS and DC2412-UPS-LD. + +CABLING +------- + +The needed cable is a standard pin-to-pin serial cable with pins 2, 3 and 5 +(on DB9 connector) connected. + +EXTRA ARGUMENTS +--------------- + +This driver supports no extra arguments from linkman:ups.conf[5]. + +VARIABLES +--------- + +Depending on the type of your UPS unit, some of the following variables may +be changed with linkman:upsrw[8]. If the driver can't read a variable from the +UPS, it will not be made available. Whenever not explicitly stated, any variable +can be disabled, in which case the action it performs will not be executed. To +disable a variable, set it to an empty value. + +*ups.delay.shutdown* (in seconds, default disabled):: +If activated and the UPS is in battery mode and the set time has expired, the +output will be disabled, and the UPS and energy storage will be disconnected. + +*ups.delay.start* (in seconds, default disabled):: +If activated and a restart condition switches the UPS output off and on again, +the set time is the delay between switching on and off. The time should cause a +defined off time so that capacities in the application can be discharged. + +*battery.charge.restart* (in percent, default disabled):: +If activated and the UPS is off or restarts, the UPS output will not be released +until the energy storage device has the set charge state. The energy storage +device is charged in the meantime. + +*battery.charge.low* (in percent, default `20`):: +If activated and the UPS is in battery mode and the battery level drops below +the set value, a shutdown command via relay event is signaled. + +*experimental.output.current.low* (in mA, default `200`):: +If activated and the UPS is in battery mode and the current drops below the set +value, the output of the UPS will shut down and disconnect the energy storage to +prevent self-discharge. + +*experimental.ups.delay.shutdown.signal* (in seconds, default disabled):: +If activated and the UPS is in battery mode and the set time has elapsed, a +shutdown command via relay event is signaled. + +*experimental.ups.delay.shutdown.signal.masked* (in seconds, default disabled):: +If activated and the UPS is in battery mode and the signal at the IN-1 input is +high and the set time has expired, a shutdown command via relay event is +signaled. + +*experimental.battery.charge.low.empty* (in percent, default `20`):: +This parameter stores the threshold value for the "Battery Empty" signal. +Currently this setting is only valid for relay signaling. Cannot be disabled. + +*experimental.ups.relay.mode* (default `0x01`):: +This parameter controls the behavior of the relay in case of different events. +Cannot be disabled. ++ +Available relay modes: +[horizontal] +`0x01`::: On power fail (normally closed) +`0x02`::: On power fail (normally opened) +`0x03`::: Shutdown impulse (1 second) +`0x04`::: Battery low signal (normally closed) +`0x05`::: Battery defect signal (normally closed) + +INSTANT COMMANDS +---------------- + +*shutdown.return*:: +Turn off the load and return when power is back. + +KNOWN ISSUES AND BUGS +--------------------- + +*ups.delay.shutdown is not honored*:: +Although that delay is properly set when sending the shutdown command, it seems +some UPS ignore it and use a fixed 2 seconds delay instead. + +AUTHOR +------ + +Nicola Fontana + +SEE ALSO +-------- + +The core driver: +~~~~~~~~~~~~~~~~ + +linkman:nutupsdrv[8] + +Internet resources: +~~~~~~~~~~~~~~~~~~~ + +The NUT (Network UPS Tools) home page: https://www.networkupstools.org/ diff --git a/docs/man/nut.conf.txt b/docs/man/nut.conf.txt index ac3b60654e..44a90f64b9 100644 --- a/docs/man/nut.conf.txt +++ b/docs/man/nut.conf.txt @@ -117,6 +117,35 @@ Optional, defaults to `0`. This setting controls the default debugging message verbosity passed to NUT daemons. As an environment variable, its priority sits between that of 'DEBUG_MIN' setting of a driver and the command-line options. +*NUT_DEBUG_SYSLOG*:: +Optional, unset by default. +Normally NUT can (attempt to) use the syslog or Event Log (WIN32), but the +environment variable 'NUT_DEBUG_SYSLOG' allows to bypass it, and perhaps keep +the daemons logging to stderr (useful e.g. in NUT Integration Test suite to +not pollute the OS logs, or in systemd where stderr and syslog both go into +the same journal). Recognized values: ++ +[options="header"] +|=========================================================================== +| Value | Description +| `stderr` | Disabled and background() keeps stderr attached +| `none` | Disabled and background() detaches stderr as usual +| `default` | Not disabled +| unset/other | Not disabled +|=========================================================================== + +*NUT_IGNORE_CHECKPROCNAME*:: +Optional, defaults to `false`. Normally NUT can (attempt to) verify that +the program file name matches the name associated with a running process, +when using PID files to send signals. ++ +The `NUT_IGNORE_CHECKPROCNAME` boolean toggle allows to quickly skip such +verification, in case it causes problems (e.g. NUT programs were renamed +and do not match built-in expectations). ++ +This environment variable can also be optionally set in init-scripts or +service methods for `upsd`, `upsmon` and NUT drivers/`upsdrvctl`. + EXAMPLE ------- diff --git a/docs/man/nut.exe.txt b/docs/man/nut.exe.txt index 6109629a85..c176a89d33 100644 --- a/docs/man/nut.exe.txt +++ b/docs/man/nut.exe.txt @@ -26,6 +26,46 @@ UPS shutdown command in case of FSD handling, or for mere 'netclient' systems it would run just the 'upsmon' client to monitor remote UPS device(s) and initiate the OS shut down on the local Windows system as applicable. +Beside launching or stopping a set of the NUT programs in certain cases, +this helper program also allows to register (or un-register) itself as a +Windows service. To actually manage the service from command line you can +execute the Windows `net` command, e.g.: + +---- +net stop "Network UPS Tools" +net start "Network UPS Tools" +---- + +You can also execute `nut start` to automatically register the service +(if not yet registered) and start it, and `nut stop` to stop the service +(if registered and running). + +Note that for a Windows machine to act as a NUT data server for further +clients, you may have to add Windows Firewall rules to allow incoming +connections (by default to port `3493/tcp`), e.g. using PowerShell to +elevate (alternately right-click a "Command Prompt" shortcut and select +"Run as administrator"), and execute `netsh` to actually configure the +needed "Advanced Firewall" rule: + +---- +REM Elevate to administrator status then run netsh to add firewall rule. +REM Recommended to adapt "LocalIP" to expected listeners of this server, +REM and "RemoteIP" to your single or comma-separated subnet(s) in CIDR +REM notation, specific client IP address(es), or ranges of address(es) +REM (dash-separated, as IP1-IP2). + +REM The following goes as one long command line: + +powershell.exe -Command "Start-Process netsh.exe -ArgumentList + \"advfirewall firewall add rule name=NUT-upsd-data-server + dir=in action=allow localip=ANY remoteip=ANY + program=%ProgramFiles%\NUT\sbin\upsd.exe + localport=3493 protocol=tcp\" -Verb RunAs" +---- + +Keep in mind that by default NUT `upsd` only listens on `localhost`, so +you would need to add some `LISTEN` directives in `upsd.conf` as well. + OPTIONS ------- @@ -56,6 +96,13 @@ Uninstall the Windows service. *-N*:: Run once in non-service mode (for troubleshooting). +*start*:: +Install as a Windows service called "Network UPS Tools" (if not yet done), +and try to start this service. + +*stop*:: +Try to stop a Windows service called "Network UPS Tools". + DIAGNOSTICS ----------- diff --git a/docs/nut.dict b/docs/nut.dict index 7f9a7c6bc2..4098de4d23 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3187 utf-8 +personal_ws-1.1 en 3210 utf-8 AAC AAS ABI @@ -60,6 +60,7 @@ Antonino Apodaca AppData AppVeyor +ArgumentList Arjen Arkadiusz Armin @@ -153,6 +154,7 @@ CERTIDENT CERTREQUEST CERTVERIF CEST +CHECKPROCNAME CHOST CHRG CL @@ -375,6 +377,7 @@ Fideltronik Filipozzi Fiskars FlossMetrics +Fontana Forza Fosshost Frama @@ -613,6 +616,7 @@ LineB Lintian ListClients Lite's +LocalIP LogMax LogMin LowBatt @@ -863,6 +867,7 @@ PSSENTR PSUs PSW PSX +PSZ PThreads PULS PV @@ -910,6 +915,7 @@ PowerPS PowerPal PowerPanel PowerShare +PowerShell PowerShield PowerSure PowerTech @@ -927,6 +933,7 @@ PresentStatus Priv ProductID Progra +ProgramFiles Proxmox Prynych Pulizzi @@ -1000,6 +1007,7 @@ Redhat Regados Reinholdtsen Remi +RemoteIP Remoting Rene René @@ -1017,6 +1025,7 @@ Rodríguez Rouben Rozman Rucelf +RunAs RunUPSCommand RxD Ryabov @@ -1253,6 +1262,7 @@ UPS's UPSCONN UPSDESC UPSHOST +UPSIC UPSIMAGEPATH UPSLC UPSNOTIFY @@ -1411,6 +1421,7 @@ adm admin's adminbox adoc +advfirewall advorder ae aec @@ -2151,6 +2162,8 @@ libaugeas libavahi libc libcommon +libcommonclient +libcommonstr libcppunit libcrypto libcurl @@ -2187,6 +2200,7 @@ libpowerman libre libregex libs +libserial libsnmp libssl libsystemd @@ -2227,6 +2241,8 @@ lnetsnmp loadPercentage localcalculation localhost +localip +localport localtime lockf logfacility @@ -2375,6 +2391,7 @@ nds netcat netclient netserver +netsh netsnmp netvision networkupstools @@ -2557,6 +2574,7 @@ powernet poweroff powerpal powerpanel +powershell powerup powervalue powerware @@ -2585,6 +2603,7 @@ probu proc productid prog +progname prtconf ps psu @@ -2638,6 +2657,7 @@ reindex relatime releasekeyring relicensing +remoteip renderer renderers repindex @@ -2719,6 +2739,8 @@ sendback sendline sendmail sendsignal +sendsignalfn +sendsignalpid sequentialized ser seria @@ -2852,6 +2874,7 @@ sublicense sublicenses submodule submodules +subnet subtree sudo suid diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 532d9cf890..d26b351861 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -57,7 +57,7 @@ SERIAL_DRIVERLIST = al175 bcmxcp belkin belkinunv bestfcom \ gamatronic genericups isbmex liebert liebert-esp2 masterguard metasys \ mge-utalk microdowell microsol-apc mge-shut oneac optiups powercom rhino \ safenet nutdrv_siemens-sitop solis tripplite tripplitesu upscode2 victronups powerpanel \ - blazer_ser ivtscd apcsmart apcsmart-old riello_ser sms_ser + blazer_ser ivtscd apcsmart apcsmart-old riello_ser sms_ser bicker_ser SNMP_DRIVERLIST = snmp-ups if WITH_DMFMIB SNMP_DRIVERLIST += snmp-ups-dmf @@ -189,6 +189,8 @@ riello_ser_SOURCES = riello.c riello_ser.c riello_ser_LDADD = $(LDADD) -lm sms_ser_SOURCES = sms_ser.c sms_ser_LDADD = $(LDADD) -lm +bicker_ser_SOURCES = bicker_ser.c +bicker_ser_LDADD = $(LDADD) -lm # non-serial drivers: these use custom LDADD and/or CFLAGS @@ -301,6 +303,11 @@ if WITH_SSL snmp_ups_dmf_LDADD += $(LIBSSL_LIBS) endif +if WITH_SSL + snmp_ups_CFLAGS += $(LIBSSL_CFLAGS) + snmp_ups_LDADD += $(LIBSSL_LIBS) +endif + # NEON XML/HTTP netxml_ups_SOURCES = netxml-ups.c mge-xml.c netxml_ups_LDADD = $(LDADD_DRIVERS) $(LIBNEON_LIBS) diff --git a/drivers/bicker_ser.c b/drivers/bicker_ser.c new file mode 100644 index 0000000000..a5c2c4db49 --- /dev/null +++ b/drivers/bicker_ser.c @@ -0,0 +1,925 @@ +/* + * bicker_ser.c: support for Bicker SuperCapacitors DC UPSes + * + * Copyright (C) 2024 - Nicola Fontana + * + * 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 + */ + +/* The protocol is reported in many Bicker's manuals but (according to + * Bicker itself) the best source is the UPS Gen Software's user manual: + * + * https://www.bicker.de/media/pdf/ff/cc/fe/en_user_manual_ups-gen2-configuration-software.pdf + * + * Basically, this is a binary protocol without checksums: + * + * 1 byte 1 byte 1 byte 1 byte 0..252 bytes 1 byte + * +-------+-------+-------+-------+--- - - - ---+-------+ + * | SOH | Size | Index | CMD | Data | EOT | + * +-------+-------+-------+-------+--- - - - ---+-------+ + * | HEADER | + * + * where: + * - `SOH` is the start signal (0x01) + * - `Size` is the length (in bytes) of the header and the data field + * - `Index` is the command index: see AVAILABLE COMMANDS + * - `CMD` is the command code to execute: see AVAILABLE COMMANDS + * - `Data` is the (optional) argument of the command + * - `EOT` is the end signal (0x04) + * + * The same format is used for incoming and outcoming packets. The data + * returned in the `Data` field is always in little-endian order. + * + * AVAILABLE COMMANDS + * ------------------ + * + * - Index = 0x01 (GENERIC) + * - CMD = 0x40 (status flags) + * - CMD = 0x41 (input voltage) + * - CMD = 0x42 (input current) + * - CMD = 0x43 (output voltage) + * - CMD = 0x44 (output current) + * - CMD = 0x45 (battery voltage) + * - CMD = 0x46 (battery current) + * - CMD = 0x47 (battery state of charge) + * - CMD = 0x48 (battery state of health) + * - CMD = 0x49 (battery cycles) + * - CMD = 0x4A (battery temperature) + * - CMD = 0x60 (manufacturer) + * - CMD = 0x61 (serial number) + * - CMD = 0x62 (device name) + * - CMD = 0x63 (firmware version) + * - CMD = 0x64 (battery pack) + * - CMD = 0x65 (firmware core version) + * - CMD = 0x66 (CPU temperature) + * - CMD = 0x67 (hardware revision) + * - CMD = 0x21 (UPS output) + * - CMD = 0x2F (shutdown flag) + * - CMD = 0x7A (reset parameter settings) + * + * - Index = 0x07 (PARAMETER) + * - CMD = 0x00 (get/set dummy entry: do not use!) + * - CMD = 0x01 (get/set load sensor) + * - CMD = 0x02 (get/set maximum backup time) + * - CMD = 0x03 (get/set os shutdown by timer) + * - CMD = 0x04 (get/set restart delay timer) + * - CMD = 0x05 (get/set minimum capacity to start) + * - CMD = 0x06 (get/set maximum backup time by in-1) + * - CMD = 0x07 (get/set os shutdown by soc) + * - CMD = 0x08 (get/set battery soc low threshold) + * - CMD = 0x09 (get/set relay event configuration) + * - CMD = 0x0A (get/set RS232 port configuration: place holder!) + * + * - Index = 0x03 (COMMANDS GOT FROM UPSIC MANUAL) + * - CMD = 0x1B (GetChargeStatusRegister) + * - CMD = 0x1C (GetMonitorStatusRegister) + * - CMD = 0x1E (GetCapacity) + * - CMD = 0x1F (GetEsr) + * - CMD = 0x20 (GetVCap1Voltage) + * - CMD = 0x21 (GetVCap2Voltage) + * - CMD = 0x22 (GetVCap3Voltage) + * - CMD = 0x23 (GetVCap4Voltage) + * - CMD = 0x25 (GetInputVoltage) + * - CMD = 0x26 (GetCapStackVoltage) + * - CMD = 0x27 (GetOutputVoltage) + * - CMD = 0x28 (GetInputCurrent) + * - CMD = 0x29 (GetChargeCurrent) + * - CMD = 0x31 (StartCapEsrMeasurement) + * - CMD = 0x32 (SetTimeToShutdown) + */ + +#include "config.h" +#include "main.h" +#include "attribute.h" +#include "nut_stdint.h" + +#include "serial.h" + +#define DRIVER_NAME "Bicker serial protocol" +#define DRIVER_VERSION "0.02" + +#define BICKER_SOH 0x01 +#define BICKER_EOT 0x04 +#define BICKER_TIMEOUT 1 +#define BICKER_DELAY 20 +#define BICKER_RETRIES 3 +#define BICKER_MAXID 0x0A /* Max parameter ID */ +#define BICKER_MAXVAL 0xFFFF /* Max parameter value */ + +/* Protocol lengths */ +#define BICKER_HEADER 3 +#define BICKER_MAXDATA (255 - BICKER_HEADER) +#define BICKER_PACKET(datalen) (1 + BICKER_HEADER + (datalen) + 1) + +#define TOUINT(ch) ((unsigned)(uint8_t)(ch)) +#define LOWBYTE(w) ((uint8_t)((uint16_t)(w) & 0x00FF)) +#define HIGHBYTE(w) ((uint8_t)(((uint16_t)(w) & 0xFF00) >> 8)) +#define WORDLH(l,h) ((uint16_t)((l) + ((h) << 8))) + +upsdrv_info_t upsdrv_info = { + DRIVER_NAME, + DRIVER_VERSION, + "Nicola Fontana ", + DRV_EXPERIMENTAL, + { NULL } +}; + +typedef struct { + uint8_t id; + uint16_t min; + uint16_t max; + uint16_t std; + uint8_t enabled; + uint16_t value; +} BickerParameter; + +typedef struct { + uint8_t bicker_id; + const char *nut_name; + const char *description; +} BickerMapping; + +static const BickerMapping bicker_mappings[] = { + /* Official variables present in docs/nut-names.txt */ + { 0x02, "ups.delay.shutdown", + "Interval to wait after shutdown with delay command (seconds)" }, + { 0x04, "ups.delay.start", + "Interval to wait before restarting the load (seconds)" }, + { 0x05, "battery.charge.restart", + "Minimum battery level for UPS restart after power-off" }, + { 0x07, "battery.charge.low", + "Remaining battery level when UPS switches to LB (percent)" }, + + /* Unofficial variables under the "experimental" namespace */ + { 0x01, "experimental.output.current.low", + "Current threshold under which the power will be cut (mA)" }, + { 0x03, "experimental.ups.delay.shutdown.signal", + "Interval to wait before sending the shutdown signal (seconds)" }, + { 0x06, "experimental.ups.delay.shutdown.signal.masked", + "Interval to wait with IN1 high before sending the shutdown signal (seconds)" }, + { 0x08, "experimental.battery.charge.low.empty", + "Battery level threshold for the empty signal (percent)" }, + { 0x09, "experimental.ups.relay.mode", + "Behavior of the relay" }, +}; + +/** + * Parameter id validation. + * @param id Id of the parameter + * @param context Description of the calling code for the log message + * @return 1 on valid id, 0 on errors. + * + * The id is valid if within the 0x01..BICKER_MAXID range (inclusive). + */ +static int bicker_valid_id(uint8_t id, const char *context) +{ + if (id < 1 || id > BICKER_MAXID) { + upslogx(LOG_ERR, "%s: parameter id 0x%02X is out of range (0x01..0x%02X)", + context, (unsigned)id, (unsigned)BICKER_MAXID); + return 0; + } + return 1; +} + +/** + * Send a packet. + * @param idx Command index + * @param cmd Command + * @param data Source data or NULL for no data field + * @param datalen Size of the source data field or 0 + * @return `datalen` on success or -1 on errors. + */ +static ssize_t bicker_send(uint8_t idx, uint8_t cmd, const void *data, size_t datalen) +{ + uint8_t buf[BICKER_PACKET(BICKER_MAXDATA)]; + size_t buflen; + ssize_t ret; + + if (data != NULL) { + if (datalen > BICKER_MAXDATA) { + upslogx(LOG_ERR, + "Data size exceeded: %" PRIuSIZE " > %d", + datalen, BICKER_MAXDATA); + return -1; + } + memcpy(&buf[1 + BICKER_HEADER], data, datalen); + } else { + datalen = 0; + } + + ser_flush_io(upsfd); + + buflen = BICKER_PACKET(datalen); + buf[0] = BICKER_SOH; + buf[1] = BICKER_HEADER + datalen; + buf[2] = idx; + buf[3] = cmd; + buf[buflen - 1] = BICKER_EOT; + + ret = ser_send_buf(upsfd, buf, buflen); + if (ret < 0) { + upslog_with_errno(LOG_WARNING, "ser_send_buf failed"); + return -1; + } else if ((size_t) ret != buflen) { + upslogx(LOG_WARNING, "ser_send_buf returned %" + PRIiSIZE " instead of %" PRIuSIZE, + ret, buflen); + return -1; + } + + upsdebug_hex(3, "bicker_send", buf, buflen); + return datalen; +} + +/** + * Receive a packet with a data field of unknown size. + * @param idx Command index + * @param cmd Command + * @param data Destination buffer or NULL to discard the data field + * @return The size of the data field on success or -1 on errors. + * + * The data field is stored directly in the destination buffer. `data`, + * if not NULL, must have at least BICKER_MAXDATA bytes. + */ +static ssize_t bicker_receive(uint8_t idx, uint8_t cmd, void *data) +{ + ssize_t ret; + size_t buflen, datalen; + uint8_t buf[BICKER_PACKET(BICKER_MAXDATA)]; + + /* Read first two bytes (SOH + size) */ + ret = ser_get_buf_len(upsfd, buf, 2, BICKER_TIMEOUT, 0); + if (ret < 0) { + upslog_with_errno(LOG_WARNING, "Initial ser_get_buf_len failed"); + return -1; + } else if (ret < 2) { + upslogx(LOG_WARNING, "Timeout waiting for response packet"); + return -1; + } else if (buf[0] != BICKER_SOH) { + upslogx(LOG_WARNING, "Received 0x%02X instead of SOH (0x%02X)", + (unsigned)buf[0], (unsigned)BICKER_SOH); + return -1; + } + + /* buf[1] (the size field) is BICKER_HEADER + data length, so */ + datalen = buf[1] - BICKER_HEADER; + + /* Read the rest of the packet */ + buflen = BICKER_PACKET(datalen); + ret = ser_get_buf_len(upsfd, buf + 2, buflen - 2, BICKER_TIMEOUT, 0); + if (ret < 0) { + upslog_with_errno(LOG_WARNING, "ser_get_buf_len failed"); + return -1; + } + + upsdebug_hex(3, "bicker_receive", buf, ret + 2); + + if ((size_t)ret < buflen - 2) { + upslogx(LOG_WARNING, "Timeout waiting for the end of the packet"); + return -1; + } else if (buf[buflen - 1] != BICKER_EOT) { + upslogx(LOG_WARNING, "Received 0x%02X instead of EOT (0x%02X)", + (unsigned)buf[buflen - 1], (unsigned)BICKER_EOT); + return -1; + } else if (idx != 0xEE && buf[2] == 0xEE) { + /* I found experimentally that, when the syntax is + * formally correct but a feature is not supported, + * the device returns 0x01 0x03 0xEE 0x07 0x04. */ + upsdebugx(2, "Command is not supported"); + return -1; + } else if (buf[2] != idx) { + upslogx(LOG_WARNING, "Indexes do not match: sent 0x%02X, received 0x%02X", + (unsigned)idx, (unsigned)buf[2]); + return -1; + } else if (buf[3] != cmd) { + upslogx(LOG_WARNING, "Commands do not match: sent 0x%02X, received 0x%02X", + (unsigned)cmd, (unsigned)buf[3]); + return -1; + } + + if (data != NULL) { + memcpy(data, &buf[1 + BICKER_HEADER], datalen); + } + + return datalen; +} + +/** + * Receive a packet with a data field of known size. + * @param idx Command index + * @param cmd Command + * @param dst Destination buffer or NULL to discard the data field + * @param datalen The expected size of the data field + * @return `datalen` on success or -1 on errors. + * + * `dst`, if not NULL, must have at least `datalen` bytes. If the data + * is not exactly `datalen` bytes, an error is thrown. + */ +static ssize_t bicker_receive_known(uint8_t idx, uint8_t cmd, void *dst, size_t datalen) +{ + ssize_t ret; + uint8_t data[BICKER_MAXDATA]; + + ret = bicker_receive(idx, cmd, data); + if (ret < 0) { + return ret; + } + + if (datalen != (size_t)ret) { + upslogx(LOG_ERR, "Data size does not match: expected %" + PRIuSIZE " but got %" PRIiSIZE " bytes", + datalen, ret); + return -1; + } + + if (dst != NULL) { + memcpy(dst, data, datalen); + } + + return datalen; +} + +/** + * Receive the response of a set/get parameter command. + * @param id Id of the parameter + * @param dst Where to store the response or NULL to discard + * @return The size of the data field on success or -1 on errors. + */ +static ssize_t bicker_receive_parameter(uint8_t id, BickerParameter *dst) +{ + ssize_t ret; + uint8_t data[10]; + BickerParameter parameter; + + if (!bicker_valid_id(id, "bicker_receive_parameter")) { + return -1; + } + + ret = bicker_receive_known(0x07, id, data, sizeof(data)); + if (ret < 0) { + return ret; + } + + /* The returned `data` is in the format: + * [AA] [bbBB] [ccCC] [ddDD] [EE] [ffFF] + * where: + * [AA] = parameter id (Byte) + * [BBbb] = minimum value (UInt16) + * [CCcc] = maximum value (UInt16) + * [DDdd] = standard value (UInt16) + * [EE] = enabled (Bool) + * [FFff] = value (UInt16) + */ + parameter.id = data[0]; + parameter.min = WORDLH(data[1], data[2]); + parameter.max = WORDLH(data[3], data[4]); + parameter.std = WORDLH(data[5], data[6]); + parameter.enabled = data[7]; + parameter.value = WORDLH(data[8], data[9]); + + upsdebugx(3, "Parameter %u = %u (%s, min = %u, max = %u, std = %u)", + (unsigned)parameter.id, (unsigned)parameter.value, + parameter.enabled ? "enabled" : "disabled", + (unsigned)parameter.min, (unsigned)parameter.max, + (unsigned)parameter.std); + + if (dst != NULL) { + memcpy(dst, ¶meter, sizeof(parameter)); + } + + return ret; +} + +/** + * Execute a command that returns an uint8_t value. + * @param idx Command index + * @param cmd Command + * @param dst Destination for the value + * @return The size of the data field on success or -1 on errors. + */ +static ssize_t bicker_read_uint8(uint8_t idx, uint8_t cmd, uint8_t *dst) +{ + ssize_t ret; + + ret = bicker_send(idx, cmd, NULL, 0); + if (ret < 0) { + return ret; + } + + return bicker_receive_known(idx, cmd, dst, 1); +} + +/** + * Execute a command that returns an uint16_t value. + * @param idx Command index + * @param cmd Command + * @param dst Destination for the value or NULL to discard + * @return The size of the data field on success or -1 on errors. + */ +static ssize_t bicker_read_uint16(uint8_t idx, uint8_t cmd, uint16_t *dst) +{ + ssize_t ret; + uint8_t data[2]; + + ret = bicker_send(idx, cmd, NULL, 0); + if (ret < 0) { + return ret; + } + + ret = bicker_receive_known(idx, cmd, data, 2); + if (ret < 0) { + return ret; + } + + if (dst != NULL) { + *dst = WORDLH(data[0], data[1]); + } + + return ret; +} + +/** + * Execute a command that returns an int16_t value. + * @param idx Command index + * @param cmd Command + * @param dst Destination for the value or NULL to discard + * @return The size of the data field on success or -1 on errors. + */ +static ssize_t bicker_read_int16(uint8_t idx, uint8_t cmd, int16_t *dst) +{ + return bicker_read_uint16(idx, cmd, (uint16_t *) dst); +} + +/** + * Execute a command that returns a string. + * @param idx Command index + * @param cmd Command + * @param dst Destination for the string or NULL to discard + * @return The size of the data field on success or -1 on errors. + * + * `dst`, if not NULL, must have at least BICKER_MAXDATA+1 bytes, the + * additional byte needed to accomodate the ending '\0'. + */ +static ssize_t bicker_read_string(uint8_t idx, uint8_t cmd, char *dst) +{ + ssize_t ret; + + ret = bicker_send(idx, cmd, NULL, 0); + if (ret < 0) { + return ret; + } + + ret = bicker_receive(idx, cmd, dst); + if (ret < 0) { + return ret; + } + + dst[ret] = '\0'; + return ret; +} + +/** + * Create a read-write Bicker parameter. + * @param parameter Source information + * @param mapping How that parameter is mapped to NUT + */ +static void bicker_new(const BickerParameter *parameter, const BickerMapping *mapping) +{ + const char *varname; + + varname = mapping->nut_name; + if (parameter->enabled) { + dstate_setinfo(varname, "%u", (unsigned)parameter->value); + } else { + /* dstate_setinfo(varname, "") triggers a GCC warning */ + dstate_setinfo(varname, "%s", ""); + } + + /* Using ST_FLAG_STRING so an empty string can be used + * to identify a disabled parameter */ + dstate_setflags(varname, ST_FLAG_RW | ST_FLAG_STRING); + + /* Just tested it: setting a range does not hinder setting + * an empty string with `dstate_setinfo(varname, "")` */ + if (parameter->min == BICKER_MAXVAL) { + /* The device here is likely corrupt: + * apply a standard range to try using it anyway */ + upslogx(LOG_WARNING, "Parameter %s is corrupt", varname); + dstate_addrange(varname, 0, BICKER_MAXVAL); + } else { + dstate_addrange(varname, parameter->min, parameter->max); + } + + /* Maximum value for an uint16_t is 65535, i.e. 5 digits */ + dstate_setaux(varname, 5); +} + +/** + * Get a Bicker parameter. + * @param id Id of the parameter + * @param dst Where to store the response or NULL to discard + * @return The size of the data field on success or -1 on errors. + */ +static ssize_t bicker_get(uint8_t id, BickerParameter *dst) +{ + ssize_t ret; + + if (!bicker_valid_id(id, "bicker_get")) { + return -1; + } + + ret = bicker_send(0x07, id, NULL, 0); + if (ret < 0) { + return ret; + } + + return bicker_receive_parameter(id, dst); +} + +/** + * Set a Bicker parameter. + * @param id Id of the parameter + * @param enabled 0 to disable, 1 to enable + * @param value What to set in the value field + * @param dst Where to store the response or NULL to discard + * @return The size of the data field on success or -1 on errors. + */ +static ssize_t bicker_set(uint8_t id, uint8_t enabled, uint16_t value, BickerParameter *dst) +{ + ssize_t ret; + uint8_t data[3]; + + if (!bicker_valid_id(id, "bicker_set")) { + return -1; + } else if (enabled > 1) { + upslogx(LOG_ERR, "bicker_set(0x%02X, %d, %u): enabled must be 0 or 1", + (unsigned)id, enabled, (unsigned)value); + return -1; + } + + /* Format of `data` is "[EE] [ffFF]" + * where: + * [EE] = enabled (Bool) + * [FFff] = value (UInt16) + */ + data[0] = enabled; + data[1] = LOWBYTE(value); + data[2] = HIGHBYTE(value); + ret = bicker_send(0x07, id, data, 3); + if (ret < 0) { + return ret; + } + + return bicker_receive_parameter(id, dst); +} + +/** + * Write to a Bicker parameter. + * @param id Id of the parameter + * @param val A string with the value to write + * @param dst Where to store the response or NULL to discard + * @return The size of the data field on success or -1 on errors. + * + * This function is similar to bicker_set() but accepts string values. + * If `val` is NULL or empty, the underlying Bicker parameter is + * disabled and reset to its standard value. + */ +static int bicker_write(uint8_t id, const char *val, BickerParameter *dst) +{ + ssize_t ret; + BickerParameter parameter; + uint8_t enabled; + uint16_t value; + + if (val == NULL || val[0] == '\0') { + ret = bicker_get(id, ¶meter); + if (ret < 0) { + return ret; + } + enabled = 0; + value = parameter.std; + } else { + enabled = 1; + value = atoi(val); + } + + ret = bicker_set(id, enabled, value, ¶meter); + if (ret < 0) { + return ret; + } + + if (dst != NULL) { + memcpy(dst, ¶meter, sizeof(parameter)); + } + + return ret; +} + +/* For some reason the `seconds` delay (at least on my UPSIC-2403D) + * is not honored: the shutdown is always delayed by 2 seconds. This + * fixed delay seems to be independent from the state of the UPS (on + * line or on battery) and from the DIP switches setting. + * + * As response I get the same command with `0xE0` in the data field. + */ +static ssize_t bicker_delayed_shutdown(uint8_t seconds) +{ + ssize_t ret; + uint8_t response; + + ret = bicker_send(0x03, 0x32, &seconds, 1); + if (ret < 0) { + return ret; + } + + ret = bicker_receive_known(0x03, 0x32, &response, 1); + if (ret >= 0) { + upslogx(LOG_INFO, "Shutting down in %d seconds: response = 0x%02X", + seconds, (unsigned)response); + } + + return ret; +} + +static ssize_t bicker_shutdown(void) +{ + const char *str; + int delay; + + str = dstate_getinfo("ups.delay.shutdown"); + delay = str != NULL ? atoi(str) : 0; + if (delay > 255) { + upslogx(LOG_WARNING, "Shutdown delay too big: %d > 255", + delay); + delay = 255; + } + + return bicker_delayed_shutdown(delay); +} + +static int bicker_instcmd(const char *cmdname, const char *extra) +{ + NUT_UNUSED_VARIABLE(extra); + + if (!strcasecmp(cmdname, "shutdown.return")) { + bicker_shutdown(); + } + + upslogx(LOG_NOTICE, "instcmd: unknown command [%s]", cmdname); + return STAT_INSTCMD_UNKNOWN; +} + +static int bicker_setvar(const char *varname, const char *val) +{ + const BickerMapping *mapping; + unsigned i; + BickerParameter parameter; + + /* This should not be needed because when `bicker_write()` is + * successful the `parameter` struct is populated but gcc seems + * not to be smart enough to realize that and errors out with + * "error: ‘parameter...’ may be used uninitialized in this function" + */ + parameter.id = 0; + parameter.min = 0; + parameter.max = BICKER_MAXVAL; + parameter.std = 0; + parameter.enabled = 0; + parameter.value = 0; + + /* Handle mapped parameters */ + for (i = 0; i < SIZEOF_ARRAY(bicker_mappings); ++i) { + mapping = &bicker_mappings[i]; + if (!strcasecmp(varname, mapping->nut_name)) { + if (bicker_write(mapping->bicker_id, val, ¶meter) < 0) { + return STAT_SET_FAILED; + } + + if (parameter.enabled) { + dstate_setinfo(varname, "%u", + (unsigned)parameter.value); + } else { + /* Disabled parameters are removed from NUT */ + dstate_delinfo(varname); + } + return STAT_SET_HANDLED; + } + } + + upslogx(LOG_NOTICE, "setvar: unknown variable [%s]", varname); + return STAT_SET_UNKNOWN; +} + +void upsdrv_initinfo(void) +{ + char string[BICKER_MAXDATA + 1]; + + dstate_setinfo("device.type", "ups"); + + if (bicker_read_string(0x01, 0x60, string) >= 0) { + dstate_setinfo("device.mfr", "%s", string); + } + + if (bicker_read_string(0x01, 0x61, string) >= 0) { + dstate_setinfo("device.serial", "%s", string); + } + + if (bicker_read_string(0x01, 0x62, string) >= 0) { + dstate_setinfo("device.model", "%s", string); + } + + dstate_addcmd("shutdown.return"); + + upsh.instcmd = bicker_instcmd; + upsh.setvar = bicker_setvar; +} + +void upsdrv_updateinfo(void) +{ + uint8_t u8; + uint16_t u16; + int16_t i16; + ssize_t ret; + + ret = bicker_read_uint16(0x01, 0x41, &u16); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("input.voltage", "%.1f", (double) u16 / 1000); + + ret = bicker_read_uint16(0x01, 0x42, &u16); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("input.current", "%.3f", (double) u16 / 1000); + + ret = bicker_read_uint16(0x01, 0x43, &u16); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("output.voltage", "%.3f", (double) u16 / 1000); + + ret = bicker_read_uint16(0x01, 0x44, &u16); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("output.current", "%.3f", (double) u16 / 1000); + + /* This is a supercap UPS so, in this context, + * the "battery" is the supercap stack */ + ret = bicker_read_uint16(0x01, 0x45, &u16); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("battery.voltage", "%.3f", (double) u16 / 1000); + + ret = bicker_read_int16(0x01, 0x46, &i16); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("battery.current", "%.3f", (double) i16 / 1000); + + /* Not implemented for all energy packs: failure acceptable */ + if (bicker_read_uint16(0x01, 0x4A, &u16) >= 0) { + dstate_setinfo("battery.temperature", "%.1f", (double) u16 - 273.16); + } + + /* Not implemented for all energy packs: failure acceptable */ + if (bicker_read_uint8(0x01, 0x48, &u8) >= 0) { + dstate_setinfo("battery.status", "%d%%", u8); + } + + ret = bicker_read_uint8(0x01, 0x47, &u8); + if (ret < 0) { + dstate_datastale(); + return; + } + dstate_setinfo("battery.charge", "%d", u8); + + status_init(); + + /* In `u8` we already have the battery charge */ + if (u8 < atoi(dstate_getinfo("battery.charge.low"))) { + status_set("LB"); + } + + /* StatusFlags() returns an 8 bit register: + * 0. Charging + * 1. Discharging + * 2. Power present + * 3. Battery present + * 4. Shutdown received + * 5. Overcurrent + * 6. --- + * 7. --- + */ + ret = bicker_read_uint8(0x01, 0x40, &u8); + if (ret < 0) { + dstate_datastale(); + return; + } + + if ((u8 & 0x01) > 0) { + status_set("CHRG"); + } + if ((u8 & 0x02) > 0) { + status_set("DISCHRG"); + } + dstate_setinfo("battery.charger.status", + (u8 & 0x01) > 0 ? "charging" : + (u8 & 0x02) > 0 ? "discharging" : + "resting"); + + status_set((u8 & 0x04) > 0 ? "OL" : "OB"); + if ((u8 & 0x20) > 0) { + status_set("OVER"); + } + + status_commit(); + + dstate_dataok(); +} + +void upsdrv_shutdown(void) +{ + int retry; + + for (retry = 1; retry <= BICKER_RETRIES; retry++) { + if (bicker_shutdown() > 0) { + set_exit_flag(-2); /* EXIT_SUCCESS */ + return; + } + } + + upslogx(LOG_ERR, "Shutdown failed!"); + set_exit_flag(-1); +} + +void upsdrv_help(void) +{ +} + +void upsdrv_makevartable(void) +{ +} + +void upsdrv_initups(void) +{ + char string[BICKER_MAXDATA + 1]; + BickerParameter parameter; + const BickerMapping *mapping; + unsigned i; + + upsfd = ser_open(device_path); + ser_set_speed(upsfd, device_path, B38400); + ser_set_dtr(upsfd, 1); + + if (bicker_read_string(0x01, 0x63, string) >= 0) { + dstate_setinfo("ups.firmware", "%s", string); + } + + if (bicker_read_string(0x01, 0x64, string) >= 0) { + dstate_setinfo("battery.type", "%s", string); + } + + /* Not implemented on all UPSes */ + if (bicker_read_string(0x01, 0x65, string) >= 0) { + dstate_setinfo("ups.firmware.aux", "%s", string); + } + + /* Initialize mapped parameters */ + for (i = 0; i < SIZEOF_ARRAY(bicker_mappings); ++i) { + mapping = &bicker_mappings[i]; + if (bicker_get(mapping->bicker_id, ¶meter) >= 0) { + bicker_new(¶meter, mapping); + } + } + + /* Ensure "battery.charge.low" variable is defined */ + if (dstate_getinfo("battery.charge.low") == NULL) { + dstate_setinfo("battery.charge.low", "%d", 20); + } +} + +void upsdrv_cleanup(void) +{ + ser_close(upsfd, device_path); +} diff --git a/drivers/dstate.c b/drivers/dstate.c index e91ef7ac4d..f721763499 100644 --- a/drivers/dstate.c +++ b/drivers/dstate.c @@ -3,7 +3,8 @@ Copyright (C) 2003 Russell Kroll 2008 Arjen de Korte - 2012 - 2017 Arnaud Quette + 2012-2017 Arnaud Quette + 2020-2024 Jim Klimov 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 @@ -24,15 +25,15 @@ #include #ifndef WIN32 -#include -#include -#include -#include -#include -#include +# include +# include +# include +# include +# include +# include #else -#include -#include "wincompat.h" +# include +# include "wincompat.h" #endif #include "common.h" @@ -569,6 +570,7 @@ static void sock_connect(TYPE_FD sock) #endif conn->nobroadcast = 0; + conn->readzero = 0; pconf_init(&conn->ctx, NULL); if (connhead) { @@ -890,6 +892,39 @@ static void sock_read(conn_t *conn) return; } } + + if (ret == 0) { + int flags = fcntl(conn->fd, F_GETFL), is_closed = 0; + upsdebugx(2, "%s: read() returned 0; flags=%04X O_NDELAY=%04X", __func__, flags, O_NDELAY); + if (flags & O_NDELAY || O_NDELAY == 0) { + /* O_NDELAY with zero bytes means nothing to read but + * since read() follows a successful select() with + * ready file descriptor, ret shouldn't be 0. + * This may also mean that the counterpart has exited + * and the file descriptor should be reaped. + * e.g. a `driver -c reload -a testups` fires its + * message over Unix socket and disconnects. + */ + is_closed = 1; + } else { + /* assume we will soon have data waiting in the buffer */ + conn->readzero++; + upsdebugx(1, "%s: got zero-sized reads %d times in a row", __func__, conn->readzero); + if (conn->readzero > DSTATE_CONN_READZERO_THROTTLE_MAX) { + is_closed = 2; + } else { + usleep(DSTATE_CONN_READZERO_THROTTLE_USEC); + } + } + + if (is_closed) { + upsdebugx(1, "%s: it seems the other side has closed the connection", __func__); + sock_disconnect(conn); + return; + } + } else { + conn->readzero = 0; + } #else char *buf = conn->buf; DWORD bytesRead; diff --git a/drivers/dstate.h b/drivers/dstate.h index 2fa754defa..fa2510d877 100644 --- a/drivers/dstate.h +++ b/drivers/dstate.h @@ -3,6 +3,7 @@ Copyright (C) 2003 Russell Kroll 2012-2017 Arnaud Quette + 2020-2024 Jim Klimov 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 @@ -52,8 +53,15 @@ typedef struct conn_s { struct conn_s *prev; struct conn_s *next; int nobroadcast; /* connections can request to ignore send_to_all() updates */ + int readzero; /* how many times in a row we had zero bytes read; see DSTATE_CONN_READZERO_THROTTLE_USEC and DSTATE_CONN_READZERO_THROTTLE_MAX */ } conn_t; +/* sleep after read()ing zero bytes */ +#define DSTATE_CONN_READZERO_THROTTLE_USEC 500 + +/* close socket after read()ing zero bytes this many times in a row */ +#define DSTATE_CONN_READZERO_THROTTLE_MAX 5 + #include "main.h" /* for set_exit_flag(); uses conn_t itself */ extern struct ups_handler upsh; diff --git a/drivers/generic_gpio_common.h b/drivers/generic_gpio_common.h index 3b17a86c9a..5589fc7ec7 100644 --- a/drivers/generic_gpio_common.h +++ b/drivers/generic_gpio_common.h @@ -21,7 +21,7 @@ */ #ifndef GENERIC_GPIO_COMMON_H_SEEN -#define GENERIC_GPIO_COMMON_H_SEEN +#define GENERIC_GPIO_COMMON_H_SEEN 1 #include #include diff --git a/drivers/generic_gpio_libgpiod.h b/drivers/generic_gpio_libgpiod.h index 66f13b4511..77deacefb8 100644 --- a/drivers/generic_gpio_libgpiod.h +++ b/drivers/generic_gpio_libgpiod.h @@ -21,7 +21,7 @@ */ #ifndef GENERIC_GPIO_LIBGPIOD_H_SEEN -#define GENERIC_GPIO_LIBGPIOD_H_SEEN +#define GENERIC_GPIO_LIBGPIOD_H_SEEN 1 #include diff --git a/drivers/hidparser.h b/drivers/hidparser.h index 9f604565e5..6d82927bd5 100644 --- a/drivers/hidparser.h +++ b/drivers/hidparser.h @@ -23,7 +23,7 @@ * -------------------------------------------------------------------------- */ #ifndef NUT_HID_PARSER_H_SEEN -#define NUT_HID_PARSER_H_SEEN +#define NUT_HID_PARSER_H_SEEN 1 #ifdef __cplusplus diff --git a/drivers/libhid.h b/drivers/libhid.h index 52feeb2601..1711b1b539 100644 --- a/drivers/libhid.h +++ b/drivers/libhid.h @@ -27,7 +27,7 @@ * -------------------------------------------------------------------------- */ #ifndef NUT_LIBHID_H_SEEN -#define NUT_LIBHID_H_SEEN +#define NUT_LIBHID_H_SEEN 1 /* "config.h" is generated by autotools and lacks a header guard, so * we use an unambiguously named macro we know we must have, as one. diff --git a/drivers/main.c b/drivers/main.c index 89950d1ed7..788a1aa163 100644 --- a/drivers/main.c +++ b/drivers/main.c @@ -2223,9 +2223,9 @@ int main(int argc, char **argv) int cmdret = -1; /* Send a signal to older copy of the driver, if any */ if (oldpid < 0) { - cmdret = sendsignalfn(pidfnbuf, cmd); + cmdret = sendsignalfn(pidfnbuf, cmd, progname, 1); } else { - cmdret = sendsignalpid(oldpid, cmd); + cmdret = sendsignalpid(oldpid, cmd, progname, 1); } switch (cmdret) { @@ -2321,7 +2321,7 @@ int main(int argc, char **argv) upslogx(LOG_WARNING, "Duplicate driver instance detected (PID file %s exists)! Terminating other driver!", pidfnbuf); - if ((sigret = sendsignalfn(pidfnbuf, SIGTERM) != 0)) { + if ((sigret = sendsignalfn(pidfnbuf, SIGTERM, progname, 1) != 0)) { upsdebugx(1, "Can't send signal to PID, assume invalid PID file %s; " "sendsignalfn() returned %d (errno=%d): %s", pidfnbuf, sigret, errno, strerror(errno)); @@ -2339,9 +2339,9 @@ int main(int argc, char **argv) struct stat st; if (stat(pidfnbuf, &st) == 0) { upslogx(LOG_WARNING, "Duplicate driver instance is still alive (PID file %s exists) after several termination attempts! Killing other driver!", pidfnbuf); - if (sendsignalfn(pidfnbuf, SIGKILL) == 0) { + if (sendsignalfn(pidfnbuf, SIGKILL, progname, 1) == 0) { sleep(5); - if (sendsignalfn(pidfnbuf, 0) == 0) { + if (sendsignalfn(pidfnbuf, 0, progname, 1) == 0) { upslogx(LOG_WARNING, "Duplicate driver instance is still alive (could signal the process)"); /* TODO: Should we writepid() below in this case? * Or if driver init fails, restore the old content @@ -2385,7 +2385,7 @@ int main(int argc, char **argv) upslogx(LOG_WARNING, "Duplicate driver instance detected! Terminating other driver!"); for(i=0;i<10;i++) { DWORD res; - sendsignal(name, COMMAND_STOP); + sendsignal(name, COMMAND_STOP, 1); if(mutex != NULL ) { res = WaitForSingleObject(mutex,1000); if(res==WAIT_OBJECT_0) { diff --git a/drivers/main.h b/drivers/main.h index 05e03d7926..5276c98ece 100644 --- a/drivers/main.h +++ b/drivers/main.h @@ -1,5 +1,5 @@ #ifndef NUT_MAIN_H_SEEN -#define NUT_MAIN_H_SEEN +#define NUT_MAIN_H_SEEN 1 #include "common.h" #include "upsconf.h" diff --git a/drivers/netxml-ups.c b/drivers/netxml-ups.c index 742a72d227..9b1953d71b 100644 --- a/drivers/netxml-ups.c +++ b/drivers/netxml-ups.c @@ -237,7 +237,6 @@ uint32_t ups_status = 0; static int timeout = 5; int shutdown_duration = 120; static int shutdown_timer = 0; -static int do_convert_deci = 0; /* Legacy MGE-XML conversion from 2000's, not needed in modern firmwares */ static time_t lastheard = 0; static subdriver_t *subdriver = &mge_xml_subdriver; static ne_session *session = NULL; @@ -592,27 +591,6 @@ void upsdrv_initups(void) } } - val = getval("do_convert_deci"); - upsdebugx(5, "incoming do_convert_deci = '%s'", val?val:""); - if (val) { - do_convert_deci = -1; - if ( strcasecmp(val, "on") == 0 || strcasecmp(val, "true") == 0 || strcasecmp(val, "yes") == 0 ) { - do_convert_deci = 1; - } else if ( strcasecmp(val, "off") == 0 || strcasecmp(val, "false") == 0 || strcasecmp(val, "no") == 0 ) { - do_convert_deci = 0; - } else { - do_convert_deci = atoi(val); - } - - if (do_convert_deci < 0) { - fatalx(EXIT_FAILURE, "do_convert_deci must be a boolean (no|yes / false|true / off|on) or numeric (0|1) value"); - } - if (do_convert_deci > 1) - do_convert_deci = 1; - - upsdebugx(5, "resulting do_convert_deci = '%d'", do_convert_deci); - } - if (nut_debug_level > 5) { ne_debug_init(stderr, NE_DBG_HTTP | NE_DBG_HTTPBODY); } diff --git a/drivers/snmp-ups.c b/drivers/snmp-ups.c index 91a9504729..53c4309509 100644 --- a/drivers/snmp-ups.c +++ b/drivers/snmp-ups.c @@ -5,8 +5,8 @@ * * Copyright (C) * 2002 - 2014 Arnaud Quette - * 2015 - 2021 Eaton (author: Arnaud Quette ) - * 2016 - 2021 Eaton (author: Jim Klimov ) + * 2015 - 2022 Eaton (author: Arnaud Quette ) + * 2016 - 2022 Eaton (author: Jim Klimov ) * 2016 Eaton (author: Carlos Dominguez ) * 2002 - 2006 Dmitry Frolov * J.W. Hoogervorst diff --git a/drivers/upsdrvctl.c b/drivers/upsdrvctl.c index 84ca633791..5eb62f839c 100644 --- a/drivers/upsdrvctl.c +++ b/drivers/upsdrvctl.c @@ -304,9 +304,9 @@ static void signal_driver_cmd(const ups_t *ups, nut_sendsignal_debug_level = NUT_SENDSIGNAL_DEBUG_LEVEL_KILL_SIG0PING - 1; #ifndef WIN32 if (ups->pid == -1) { - ret = sendsignalfn(pidfn, cmd); + ret = sendsignalfn(pidfn, cmd, ups->driver, 0); } else { - ret = sendsignalpid(ups->pid, cmd); + ret = sendsignalpid(ups->pid, cmd, ups->driver, 0); /* reap zombie if this child died */ if (waitpid(ups->pid, NULL, WNOHANG) == ups->pid) { upslog_with_errno(LOG_WARNING, @@ -315,7 +315,7 @@ static void signal_driver_cmd(const ups_t *ups, } } #else - ret = sendsignal(pidfn, cmd); + ret = sendsignal(pidfn, cmd, 0); #endif /* Restore the signal errors verbosity */ nut_sendsignal_debug_level = NUT_SENDSIGNAL_DEBUG_LEVEL_DEFAULT; @@ -381,25 +381,25 @@ static void stop_driver(const ups_t *ups) #ifndef WIN32 if (ups->pid == -1) { - ret = sendsignalfn(pidfn, SIGTERM); + ret = sendsignalfn(pidfn, SIGTERM, ups->driver, 0); } else { - ret = sendsignalpid(ups->pid, SIGTERM); + ret = sendsignalpid(ups->pid, SIGTERM, ups->driver, 0); /* reap zombie if this child died */ if (waitpid(ups->pid, NULL, WNOHANG) == ups->pid) { goto clean_return; } } #else - ret = sendsignal(pidfn, COMMAND_STOP); + ret = sendsignal(pidfn, COMMAND_STOP, 0); #endif if (ret < 0) { #ifndef WIN32 upsdebugx(2, "SIGTERM to %s failed, retrying with SIGKILL", pidfn); if (ups->pid == -1) { - ret = sendsignalfn(pidfn, SIGKILL); + ret = sendsignalfn(pidfn, SIGKILL, ups->driver, 0); } else { - ret = sendsignalpid(ups->pid, SIGKILL); + ret = sendsignalpid(ups->pid, SIGKILL, ups->driver, 0); /* reap zombie if this child died */ if (waitpid(ups->pid, NULL, WNOHANG) == ups->pid) { goto clean_return; @@ -407,7 +407,7 @@ static void stop_driver(const ups_t *ups) } #else upsdebugx(2, "Stopping %s failed, retrying again", pidfn); - ret = sendsignal(pidfn, COMMAND_STOP); + ret = sendsignal(pidfn, COMMAND_STOP, 0); #endif if (ret < 0) { upslog_with_errno(LOG_ERR, "Stopping %s failed", pidfn); @@ -419,16 +419,16 @@ static void stop_driver(const ups_t *ups) for (i = 0; i < 5 ; i++) { #ifndef WIN32 if (ups->pid == -1) { - ret = sendsignalfn(pidfn, 0); + ret = sendsignalfn(pidfn, 0, ups->driver, 0); } else { /* reap zombie if this child died */ if (waitpid(ups->pid, NULL, WNOHANG) == ups->pid) { goto clean_return; } - ret = sendsignalpid(ups->pid, 0); + ret = sendsignalpid(ups->pid, 0, ups->driver, 0); } #else - ret = sendsignalfn(pidfn, 0); + ret = sendsignalfn(pidfn, 0, ups->driver, 0); #endif if (ret != 0) { upsdebugx(2, "Sending signal to %s failed, driver is finally down or wrongly owned", pidfn); @@ -440,32 +440,32 @@ static void stop_driver(const ups_t *ups) #ifndef WIN32 upslog_with_errno(LOG_ERR, "Stopping %s failed, retrying harder", pidfn); if (ups->pid == -1) { - ret = sendsignalfn(pidfn, SIGKILL); + ret = sendsignalfn(pidfn, SIGKILL, ups->driver, 0); } else { /* reap zombie if this child died */ if (waitpid(ups->pid, NULL, WNOHANG) == ups->pid) { goto clean_return; } - ret = sendsignalpid(ups->pid, SIGKILL); + ret = sendsignalpid(ups->pid, SIGKILL, ups->driver, 0); } #else upslog_with_errno(LOG_ERR, "Stopping %s failed, retrying again", pidfn); - ret = sendsignal(pidfn, COMMAND_STOP); + ret = sendsignal(pidfn, COMMAND_STOP, 0); #endif if (ret == 0) { for (i = 0; i < 5 ; i++) { #ifndef WIN32 if (ups->pid == -1) { - ret = sendsignalfn(pidfn, 0); + ret = sendsignalfn(pidfn, 0, ups->driver, 0); } else { /* reap zombie if this child died */ if (waitpid(ups->pid, NULL, WNOHANG) == ups->pid) { goto clean_return; } - ret = sendsignalpid(ups->pid, 0); + ret = sendsignalpid(ups->pid, 0, ups->driver, 0); } #else - ret = sendsignalfn(pidfn, 0); + ret = sendsignalfn(pidfn, 0, ups->driver, 0); #endif if (ret != 0) { upsdebugx(2, "Sending signal to %s failed, driver is finally down or wrongly owned", pidfn); diff --git a/drivers/upsdrvquery.h b/drivers/upsdrvquery.h index 003337dfb9..e3854940b2 100644 --- a/drivers/upsdrvquery.h +++ b/drivers/upsdrvquery.h @@ -20,7 +20,7 @@ */ #ifndef NUT_UPSDRVQUERY_H_SEEN -#define NUT_UPSDRVQUERY_H_SEEN +#define NUT_UPSDRVQUERY_H_SEEN 1 #include "common.h" /* TYPE_FD etc. */ #include "timehead.h" diff --git a/include/common.h b/include/common.h index 32e0ee2041..9313245d63 100644 --- a/include/common.h +++ b/include/common.h @@ -1,6 +1,7 @@ /* common.h - prototypes for the common useful functions Copyright (C) 2000 Russell Kroll + Copyright (C) 2021-2024 Jim Klimov 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 @@ -199,6 +200,18 @@ extern const char *UPS_VERSION; /** @brief Default timeout (in seconds) for retrieving the result of a `TRACKING`-enabled operation (e.g. `INSTCMD`, `SET VAR`). */ #define DEFAULT_TRACKING_TIMEOUT 10 +/* Normally we can (attempt to) use the syslog or Event Log (WIN32), + * but environment variable NUT_DEBUG_SYSLOG allows to bypass it, and + * perhaps keep daemons logging to stderr (e.g. in NUT Integration Test + * suite to not pollute the OS logs, or in systemd where stderr and + * syslog both go into the same journal). Returns: + * 0 Not disabled (NUT_DEBUG_SYSLOG not set to a value below; unset or "default" + * values are handled quietly, but others emit a warning) + * 1 Disabled and background() keeps stderr attached (NUT_DEBUG_SYSLOG="stderr") + * 2 Disabled and background() detaches stderr as usual (NUT_DEBUG_SYSLOG="none") + */ +int syslog_is_disabled(void); + /* get the syslog ready for us */ void open_syslog(const char *progname); @@ -214,6 +227,38 @@ void become_user(struct passwd *pw); /* drop down into a directory and throw away pointers to the old path */ void chroot_start(const char *path); +/* Try to identify process (program) name for the given PID, + * return NULL if we can not for any reason (does not run, + * no rights, do not know how to get it on current OS, etc.) + * If the returned value is not NULL, caller should free() it. + * Some implementation pieces borrowed from + * https://man7.org/linux/man-pages/man2/readlink.2.html and + * https://github.com/openbsd/src/blob/master/bin/ps/ps.c + * NOTE: Very much platform-dependent! */ +char * getprocname(pid_t pid); + +/* Determine the base name of specified progname (may be full path) + * and the location of the last "." dot character in it for extension + * (caller's len and dot populated only if pointers are not NULL). + */ +size_t parseprogbasename(char *buf, size_t buflen, const char *progname, size_t *pprogbasenamelen, size_t *pprogbasenamedot); + +/* If we can determine the binary path name of the specified "pid", + * check if it matches the assumed name of the current program. + * Returns: + * -3 Skipped because NUT_IGNORE_CHECKPROCNAME is set + * -2 Could not parse a program name (ok to proceed, + * risky - but matches legacy behavior) + * -1 Could not identify a program name (ok to proceed, + * risky - but matches legacy behavior) + * 0 Process name identified, does not seem to match + * 1+ Process name identified, and seems to match with + * varying precision + * Generally speaking, if (checkprocname(...)) then ok to proceed + */ +int checkprocname(pid_t pid, const char *progname); + + /* write a pid file - is a full pathname *or* just the program name */ void writepid(const char *name); @@ -221,11 +266,11 @@ void writepid(const char *name); * a few sanity checks; returns -1 on error */ pid_t parsepid(const char *buf); -/* send a signal to another running process */ +/* send a signal to another running NUT process */ #ifndef WIN32 -int sendsignal(const char *progname, int sig); +int sendsignal(const char *progname, int sig, int check_current_progname); #else -int sendsignal(const char *progname, const char * sig); +int sendsignal(const char *progname, const char * sig, int check_current_progname); #endif int snprintfcat(char *dst, size_t size, const char *fmt, ...) @@ -236,7 +281,7 @@ pid_t get_max_pid_t(void); /* send sig to pid after some sanity checks, returns * -1 for error, or zero for a successfully sent signal */ -int sendsignalpid(pid_t pid, int sig); +int sendsignalpid(pid_t pid, int sig, const char *progname, int check_current_progname); /* open , get the pid, then send it * returns zero for successfully sent signal, @@ -246,10 +291,17 @@ int sendsignalpid(pid_t pid, int sig); * -1 Error sending signal */ #ifndef WIN32 -/* open , get the pid, then send it */ -int sendsignalfn(const char *pidfn, int sig); +/* open , get the pid, then send it + * if executable process with that pid has suitable progname + * (specified or that of the current process, depending on args: + * most daemons request to check_current_progname for their other + * process instancees, but upsdrvctl which manages differently + * named driver programs does not request it) + */ +int sendsignalfn(const char *pidfn, int sig, const char *progname, int check_current_progname); #else -int sendsignalfn(const char *pidfn, const char * sig); +/* No progname here - communications via named pipe */ +int sendsignalfn(const char *pidfn, const char * sig, const char *progname_ignored, int check_current_progname_ignored); #endif const char *xbasename(const char *file); diff --git a/scripts/Windows/wininit.c b/scripts/Windows/wininit.c index f4c29b3bf0..abb6643090 100644 --- a/scripts/Windows/wininit.c +++ b/scripts/Windows/wininit.c @@ -1,4 +1,5 @@ /* wininit.c - MS Windows service which replace the init script + (compiled as "nut.exe") Copyright (C) 2010 Frederic Bohe @@ -192,7 +193,7 @@ static void run_upsd(void) static void stop_upsd(void) { - if (sendsignal(UPSD_PIPE_NAME, COMMAND_STOP)) { + if (sendsignal(UPSD_PIPE_NAME, COMMAND_STOP, 0)) { print_event(LOG_ERR, "Error stopping upsd (%d)", GetLastError()); } } @@ -214,7 +215,7 @@ static void run_upsmon(void) static void stop_upsmon(void) { - if (sendsignal(UPSMON_PIPE_NAME, COMMAND_STOP)) { + if (sendsignal(UPSMON_PIPE_NAME, COMMAND_STOP, 0)) { print_event(LOG_ERR, "Error stopping upsmon (%d)", GetLastError()); } } @@ -423,6 +424,45 @@ static int SvcInstall(const char * SvcName, const char * args) return EXIT_SUCCESS; } +/* Returns a positive value if the service name exists + * -2 if we can not open the service manager + * -1 if we can not open the service itself + * +1 SvcName exists + */ +static int SvcExists(const char * SvcName) +{ + SC_HANDLE SCManager; + SC_HANDLE Service; + + SCManager = OpenSCManager( + NULL, /* local computer */ + NULL, /* ServicesActive database */ + SC_MANAGER_ALL_ACCESS); /* full access rights */ + + if (NULL == SCManager) { + upsdebugx(1, "OpenSCManager failed (%d)\n", (int)GetLastError()); + return -2; + } + + Service = OpenService( + SCManager, /* SCM database */ + SvcName, /* name of service */ + DELETE); /* need delete access */ + + if (Service == NULL) { + upsdebugx(1, "OpenService failed (%d) for \"%s\"\n", + (int)GetLastError(), SvcName); + CloseServiceHandle(SCManager); + return -1; + } + + CloseServiceHandle(Service); + CloseServiceHandle(SCManager); + + upsdebugx(1, "Service \"%s\" seems to exist", SvcName); + return 1; +} + static int SvcUninstall(const char * SvcName) { SC_HANDLE SCManager; @@ -666,9 +706,14 @@ static void help(const char *arg_progname) printf("NUT for Windows all-in-one wrapper for driver(s), data server and monitoring client\n"); printf("including shutdown and power-off handling (where supported). All together they rely\n"); - printf("on nut.conf and other files in %s\n\n", confpath()); + printf("on nut.conf and other files in %s\n", confpath()); - printf("Usage: %s [OPTION]\n\n", arg_progname); + printf("\nUsage: %s {start | stop}\n\n", arg_progname); + printf(" start Install as a service (%s) if not yet done, then `net start` it\n", SVCNAME); + printf(" stop If the service (%s) is installed, command it to `net stop`\n", SVCNAME); + printf("Note you may have to run this in an elevated privilege command shell, or use `runas`\n"); + + printf("\nUsage: %s [OPTION]\n\n", arg_progname); printf("Options (note minus not slash as the control character), one of:\n"); printf(" -I Install as a service (%s)\n", SVCNAME); @@ -684,9 +729,35 @@ int main(int argc, char **argv) int i, default_opterr = opterr; const char *progname = xbasename(argc > 0 ? argv[0] : "nut.exe"); - if (argc > 1 && !strcmp(argv[1], "/?")) { - help(progname); - return EXIT_SUCCESS; + if (argc > 1) { + if (!strcmp(argv[1], "/?")) { + help(progname); + return EXIT_SUCCESS; + } + + if (!strcmp(argv[1], "stop")) { + int ret; + if (SvcExists(SVCNAME) < 0) + fprintf(stderr, "WARNING: Can not access service \"%s\"", SVCNAME); + + ret = system("net stop \"" SVCNAME "\""); + if (ret == 0) + return EXIT_SUCCESS; + fatalx(EXIT_FAILURE, "FAILED stopping %s: %i", SVCNAME, ret); + } + + if (!strcmp(argv[1], "start")) { + int ret; + if (SvcExists(SVCNAME) < 0) { + fprintf(stderr, "WARNING: Can not access service \"%s\", registering first", SVCNAME); + SvcInstall(SVCNAME, NULL); + } + + ret = system("net start \"" SVCNAME "\""); + if (ret == 0) + return EXIT_SUCCESS; + fatalx(EXIT_FAILURE, "FAILED starting %s: %i", SVCNAME, ret); + } } /* TODO: Do not warn about unknown args - pass them to SvcMain() diff --git a/server/upsd.c b/server/upsd.c index 9c0bcfcb04..833b269395 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -2049,14 +2049,14 @@ int main(int argc, char **argv) */ if (oldpid < 0) { - cmdret = sendsignalfn(pidfn, cmd); + cmdret = sendsignalfn(pidfn, cmd, progname, 1); } else { - cmdret = sendsignalpid(oldpid, cmd); + cmdret = sendsignalpid(oldpid, cmd, progname, 1); } #else /* if WIN32 */ if (cmd) { /* Command the running daemon, it should be there */ - cmdret = sendsignal(UPSD_PIPE_NAME, cmd); + cmdret = sendsignal(UPSD_PIPE_NAME, cmd, 1); } else { /* Starting new daemon, check for competition */ mutex = CreateMutex(NULL, TRUE, UPSD_PIPE_NAME); diff --git a/server/upsd.h b/server/upsd.h index dfe27140ae..a6b96adce8 100644 --- a/server/upsd.h +++ b/server/upsd.h @@ -25,7 +25,7 @@ */ #ifndef UPSD_H_SEEN -#define UPSD_H_SEEN +#define UPSD_H_SEEN 1 #include "attribute.h" diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 2bb7742c30..7eb8e81354 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -17,6 +17,9 @@ # NUT_DEBUG_MIN=3 to set (minimum) debug level for drivers, upsd... # NUT_PORT=12345 custom port for upsd to listen and clients to query # +# Common sandbox run for testing goes from NUT root build directory like: +# DEBUG_SLEEP=600 NUT_PORT=12345 NIT_CASE=testcase_sandbox_start_drivers_after_upsd make check-NIT & +# # Design note: written with dumbed-down POSIX shell syntax, to # properly work in whatever different OSes have (bash, dash, # ksh, busybox sh...) @@ -37,6 +40,10 @@ export NUT_QUIET_INIT_SSL NUT_QUIET_INIT_UPSNOTIFY="true" export NUT_QUIET_INIT_UPSNOTIFY +# Avoid noise in syslog and OS console +NUT_DEBUG_SYSLOG="stderr" +export NUT_DEBUG_SYSLOG + NUT_DEBUG_PID="true" export NUT_DEBUG_PID @@ -1569,9 +1576,19 @@ if [ -n "${DEBUG_SLEEP-}" ] ; then log_info "Sleeping now as asked (for ${DEBUG_SLEEP} seconds starting `date -u`), so you can play with the driver and server running" log_info "Populated environment variables for this run into a file so you can source them: . '$NUT_CONFPATH/NIT.env'" printf "PID_NIT_SCRIPT='%s'\nexport PID_NIT_SCRIPT\n" "$$" >> "$NUT_CONFPATH/NIT.env" + set | grep -E '^PID_[^ =]*='"'?[0-9][0-9]*'?$" | while IFS='=' read K V ; do + V="`echo "$V" | tr -d "'"`" + # Dummy comment to reset syntax highlighting due to ' quote above + if [ -n "$V" ] ; then + printf "%s='%s'\nexport %s\n" "$K" "$V" "$K" + fi + done >> "$NUT_CONFPATH/NIT.env" log_separator cat "$NUT_CONFPATH/NIT.env" log_separator + log_info "See above about important variables from the test sandbox and a way to 'source' them into your shell" + log_info "You may want to press Ctrl+Z now and command 'bg' to the shell, if you did not start '$0 &' backgrounded already" + log_info "To kill the script early, return it to foreground with 'fg' and press Ctrl+C, or 'kill -2 \$PID_NIT_SCRIPT' (kill -2 $$)" sleep "${DEBUG_SLEEP}" log_info "Sleep finished" diff --git a/tools/nut-scanner/Makefile.am b/tools/nut-scanner/Makefile.am index 03d7197805..8d84c9699c 100644 --- a/tools/nut-scanner/Makefile.am +++ b/tools/nut-scanner/Makefile.am @@ -35,7 +35,6 @@ $(top_builddir)/common/libnutdmfsnmp.la \ $(top_builddir)/common/libnutwincompat.la \ $(top_builddir)/drivers/libserial-nutscan.la \ $(top_builddir)/common/libcommonstr.la \ -$(top_builddir)/common/libcommonclient.la \ $(top_builddir)/common/libcommon.la: dummy +@cd $(@D) && $(MAKE) $(AM_MAKEFLAGS) $(@F) @@ -45,22 +44,6 @@ $(top_builddir)/common/libcommon.la: dummy # be built before anything else nut-scanner.c: $(top_builddir)/include/nut_version.h -LINKED_SOURCE_FILES = - -# Separate the .deps of other dirs from this one -# NOTE: Not using "$<" due to a legacy Sun/illumos dmake bug with resolver -# of dynamic vars, see e.g. https://man.omnios.org/man1/make#BUGS -LINKED_SOURCE_FILES += serial.c -serial.c: $(top_srcdir)/drivers/serial.c - test -s "$@" || ln -s -f "$(top_srcdir)/drivers/serial.c" "$@" - -LINKED_SOURCE_FILES += bcmxcp_ser.c -bcmxcp_ser.c: $(top_srcdir)/drivers/bcmxcp_ser.c - test -s "$@" || ln -s -f "$(top_srcdir)/drivers/bcmxcp_ser.c" "$@" - -CLEANFILES += $(LINKED_SOURCE_FILES) -BUILT_SOURCES += $(LINKED_SOURCE_FILES) - # We optionally append values to this below bin_PROGRAMS = lib_LTLIBRARIES = @@ -116,13 +99,13 @@ libnutscan_la_SOURCES = scan_nut.c scan_nut_simulation.c scan_ipmi.c \ nutscan-device.c nutscan-ip.c nutscan-display.c \ nutscan-init.c scan_usb.c scan_snmp.c scan_xml_http.c \ scan_avahi.c scan_eaton_serial.c nutscan-serial.c -#nodist_libnutscan_la_SOURCES = $(LINKED_SOURCE_FILES) libnutscan_la_LIBADD = $(NETLIBS) libnutscan_la_LIBADD += $(top_builddir)/drivers/libserial-nutscan.la + if WITH_LIBLTDL libnutscan_la_LIBADD += $(LIBLTDL_LIBS) endif WITH_LIBLTDL -#libnutscan_la_LIBADD += $(top_builddir)/common/libcommonclient.la + if HAVE_SEMAPHORE_LIBS # Are additional libraries needed for semaphore support? libnutscan_la_LIBADD += $(SEMLIBS) @@ -144,7 +127,7 @@ endif HAVE_WINDOWS # object .so names would differ) # # libnutscan version information -libnutscan_la_LDFLAGS += -version-info 2:5:0 +libnutscan_la_LDFLAGS += -version-info 2:5:1 # libnutscan exported symbols regex # WARNING: Since the library includes parts of libcommon (as much as needed @@ -181,7 +164,6 @@ endif WITH_SNMP endif WITH_NEON libnutscan_la_LIBADD += $(top_builddir)/common/libcommonstr.la -#libnutscan_la_LIBADD += $(top_builddir)/common/libcommon.la $(top_builddir)/common/libparseconf.la nut_scanner_SOURCES = nut-scanner.c nut_scanner_CFLAGS = -I$(top_srcdir)/clients -I$(top_srcdir)/include