diff --git a/NEWS.adoc b/NEWS.adoc index 94d2db1340..ea0e64c5ef 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -40,12 +40,6 @@ https://github.com/networkupstools/nut/milestone/12 to patterns defined in link:docs/nut-names.txt[] - common code: - * Generally improved shell script portability (including `Makefile.am` - embedded recipes) when mixing back-quotes and double-quotes. Namely, - the classic Solaris `ksh` implementation (also used in current versions - of Solaris and illumos based distributions) was strict about following - (an older revision of) the POSIX standard here, and some use-cases - misbehaved with our older script spelling. [issue #3196] * Introduced `setproctag()` and `getproctag()` (see examples in `upsmon`) to help track the log messages from massively-forking NUT daemons. [#3084] * Extended with plural `checkprocnames()` and `compareprocnames()`, @@ -85,12 +79,27 @@ https://github.com/networkupstools/nut/milestone/12 not-requested (e.g. do not start `upsd` in `MODE=netclient`), even though the unit is nominally enabled; this should make packaging and providing service pre-sets more simple. [issue #837, PR #3233] - * Revised detection of (relative) paths to program and configuration files - near the currently running program in NUT for Windows builds. [issues #3207, - #3063, #3065, #3219] * Fixed thread-safety of IP address printout in `libupsclient` method - `upscli_tryconnect()` (practical bug seen in `nut-scanner` parallel scans). - [issue #3234] + `upscli_tryconnect()` (practical bug seen in `nut-scanner` parallel scans). + [issue #3234] + + - NUT for Windows specific updates: + * Revised detection of (relative) paths to program and configuration files + near the currently running program in NUT for Windows builds, including + limited support for UNC paths (ignoring a localhost `\\?\` prefix). + [issues #3207, #3063, #3065, #3219] + * Augmented WIN32 `nut.exe` launcher with `status` and `restart` actions, + and general ability to debug-log the activities during service management + operations. (Re-)registration of the "Network UPS Tools" service should + now populate a nice description of it. The `-U` (uninstall) action should + now also try to stop the service first. [PR #3235] + * Revised WIN32 `WSAStartup()` and registration of `atexit(WSACleanup)` to + only be done once per program (and cleanups to be always registered); this + impacts the C `libupsclient` and C++ `libnutclient` libraries (and so most + NUT clients and tools which use the former, and `nutconf` which uses the + latter), the `apcupsd` driver, `upsd` data server, and `nut-scanner` tool. + [PR #3237] + * Documented setup of Microsoft IIS to host the NUT CGI programs. [#3207] - `asem`, `bestfortress`, `bestuferrups`, `bicker_ser`, `everups`, `metasys`, `masterguard`, `mge-utalk`, `oneac`, `phoenixcontact_modbus`, `pijuice`, @@ -201,12 +210,20 @@ https://github.com/networkupstools/nut/milestone/12 * `openups-hid` had `nobattery` definition inverted, causing alarms -- now fixed to use the same mapping helper as other subdrivers. [issue #3246] + - `nut-scanner` tool updates: + * Fixed `nut-scanner` search for "simulation devices" to not use only the + built-in NUT configuration path on all platforms, but to also consider + `NUT_CONFPATH` and other fallback locations, like other code does. + [PR #3249] + - `upsd` data server updates: * Sometimes "Data for UPS [X] is stale" and "UPS [X] data is no longer stale" messages were logged in the same second, especially no busy systems. Now we allow one more second on top of `MAXAGE` setting to declare the device dead, just in case fractional/whole second rounding comes into play and breaks things. [issue #661] + * Fixed `LISTEN *` handling for `upsd.exe` in NUT for Windows builds. + [PR #3237] - `upsdrvctl` tool updates: * Make use of `setproctag()` and `getproctag()` to report parent/child @@ -243,10 +260,14 @@ several `FSD` notifications into one executed action. [PR #3097] - `upsset` should now recognize `RANGE NUMBER` and `NUMBER` types. [#3164] - `upsstats` tool updates: - * Now has JSON output mode via `?json` (or `&json`) CGI query - parameter. [issue #2524, PR #3171] + * Now has JSON output mode via `?json` (or `&json`) CGI query parameter. + New `@TREELINK_JSON@` keyword added and HTML templates updated to expose + these JSON documents when browsing. [issue #2524, PRs #3171, #3249] * Handle `device.model` in addition to `ups.model` in upsstats HTML templates. [#3180] + * Introduced a `@NUT_UPSSTATS_TEMPLATE@` command which the HTML template + files now MUST start with (safety check that we are reading a template). + [issue #3252, PR #3249] - `upssched` tool updates: * Previously in PR #2896 (NUT releases v2.8.3 and v2.8.4) the `UPSNAME` and @@ -275,7 +296,7 @@ several `FSD` notifications into one executed action. [PR #3097] * Revised info/error/warning/debug message emission so they go to `stderr` and have a consistent look. Revised some typos along the way. [issue #3194] - - `configure` script options: + - `configure` script updates: * For ages, most recipes for building NUT had customized the `sysconfdir` to be `/etc/nut`, which is not exactly the *system* configuration directory. This is finally deprecated, with new `--with-confdir` configuration option @@ -289,6 +310,18 @@ several `FSD` notifications into one executed action. [PR #3097] most cases. A `--with-confdir-examples` option was also introduced, to help distributions that place `*.conf.sample` files into docs or other locations. [#3131] + * The `configure` script would now probe (if it can) the operating systems + for more user and group account names, such as `upsmon`, `nutmon`, `ups`, + `nut` (last hit wins, separately for user and groups accounts) settling + on one of those if detected instead of `nobody` (and optionally `nogroup`). + It would also warn if `nobody` or `nogroup` end up being used for a build. + [#3173] + * Default `PIDPATH` is now more strictly `/var/run`, unless building on a + system conforming to FHS-3.0 standard where that location is absent or + is a symlink, while `/run` exists and is a true directory. [#3099] + * Made default NUT CGI URI (used in HTML templates, which may be edited + later on a NUT deployment to customize for specific web server setup) + configurable `--with-cgi-uri`. [#3207] * Introduced `--with-python{,2,3}-modules-dir` to specify PyNUT(Client) module installation location (for module-named dir to be created under it), if not bundling with NUT-Monitor UI app. By default the respective @@ -303,6 +336,8 @@ several `FSD` notifications into one executed action. [PR #3097] Default settings were changed from plain `auto` (allowing for multiple `python*` interpreters and `site-package` locations to be used), to prefer a `python3` if available, else `python2`, else `python`. [#1792] + * The `configure` script should now try harder to report specifically + the "purelib" location as `PYTHON*_SITE_PACKAGES`. [#1209] * Streamlined handling of `--with-*` and `--enable-*` options using NUT 'm4' macros; fixed reporting help for options whose defaults are evaluated. [issue #3049, PR #3140] @@ -315,25 +350,74 @@ several `FSD` notifications into one executed action. [PR #3097] updated many help messages. The `configure` script logs should now clarify where CFLAGS/LIBS/LDFLAGS values come from ('pkg-config', 'default', user provided 'confargs' etc.) [#3140] - * Added configure script options for 'libregex' tuning, just so it is on par - with our other optional dependencies. [#3140] + * Added `configure` script options for 'libregex' tuning, just so that one + is on par with our other optional dependencies. [#3140] * Changed `--enable-extapi-enphase` to not default to `auto` and then get installed on every system capable of using it; this is a niche capability for users of specific devices via a cloud portal. [follow-up to PR #2813 for issue #2807] - - Fixed CI recipes for PyPI publication of PyNUT(Client) module to also - include the source distribution (was posted for NUT v2.8.1 and v2.8.2 - tagged releases, but absent for v2.8.3 and v2.8.4). [#3056] - - - The `configure` script would now probe (if it can) the operating systems for - more user and group account names, such as `upsmon`, `nutmon`, `ups`, `nut` - (last hit wins, separately for user and groups accounts) settling on one of - those if detected instead of `nobody` (and optionally `nogroup`). It would - also warn if `nobody` or `nogroup` end up being used for a build. [#3173] + - Recipes, CI and helper script updates not classified above: + * Fixed CI recipes for PyPI publication of PyNUT(Client) module to also + include the source distribution (was posted for NUT v2.8.1 and v2.8.2 + tagged releases, but absent for v2.8.3 and v2.8.4). [#3056] + * Updated `make spellcheck` to help avoid asciidoc admonition blocks with + visually invalid sentences (after rendering as a box in HTML or PDF). [#3077] + * Added a `make dist-docs` goal to generate, collect and tarball all document + types (`man`, `html-single`, `html-chunked`, `pdf`) that we may have enabled + for the current build configuration and available tooling. [#1400] + * Further developer workflow speedup with `make spellcheck-interactive-quick` + which should now not re-check source texts that were okay with the previous + dictionary contents, in case some new terms have to be added. [PRs #3186, + #2871] + * Introduced `make distcheck-completeness` goal to verify that our distribution + archives can exactly reproduce themselves. [#2829] + * Added a GitHub Actions CI job to generate, upload and recycle `make dist` + and `make dist-docs` tarballs so they are easier to obtain for people and + other CI systems (which might not want to follow a Git repository). Links + to these artifacts can be seen in workflow page (or its logs), and added + as a line in GitHub Checks list associated with a commit. Note that the + GitHub Action storage keeps artifacts for at most 90 days. [#1400, #2829] + * Revised CI and deliverable scripts, and Makefile recipes, to not use + the verbatim `grep -E` (loudly preferred by newer systems, but may be + absent on older ones) after all, nor use `egrep` (loudly disliked by + newer systems). Instead, use what `configure` script detected for the + generated files (or ones made from templates), and use a similar + detection in standalone scripts. Also revised the use of `grep -q`, + `id -u`, `diff -u`, etc. which are not ubiquitous, and of `test -e` + which is not only absent in some older shells, but can cause them to + abort processing the script immediately. Also the `if ! condition` + syntax is not supported everywhere (or the `!` operator generally). + [#3099, #1660] + * Generally improved shell script portability (including `Makefile.am` + embedded recipes) when mixing back-quotes and double-quotes. Namely, + the classic Solaris `ksh` implementation (also used in current versions + of Solaris and illumos based distributions) was strict about following + (an older revision of) the POSIX standard here, and some use-cases + misbehaved with our older script spelling. [issue #3196] + * Revised `tools/gitlog2version.sh` helper script (logic added into + a new `tools/semver-compare.sh`) with a mode to expand NUT SEMVER + components into wide zero-padded numbers, so it is easier to + alphanumerically compare different releases regardless of version + component lengths in digits (and differences in their amounts). You can + request an inverse operation with `NUT_VERSION_STRIP_LEADING_ZEROES=true`. + Added tests to cover different shell interpreter platforms (piggy-back + on the `tests/nut-driver-enumerator-test.sh` script), and made sure + that outputs of legacy-mode processing (with `NUT_VERSION_DEFAULT` + string provided by caller or saved in a tarball) are consistent with + git-mode. The new `tools/semver-compare.sh` helper can be used directly + to expand and strip version strings, sort and compare multiple versions + as one simple operation. [issue #3055, PRs #3213, #3217] + * Dropped the `compile` script from Git sources. It originates from automake + and is added to work area (if missing) during `autogen.sh` rituals anyway. + It is still distributed as part of `make dist` tarball. [#1209] - - Updated `make spellcheck` to help avoid asciidoc admonition blocks with - visually invalid sentences (after rendering as a box in HTML or PDF). [#3077] + - Upstreamed reference packaging recipes (DEB, RPM) from the 42ITy project + which can be used with OBS (Open Build Service by SUSE), both to support + end-user testing of NUT development, and to have a yet another NUT CI farm + player with a distinct opinion on code quality. Many of the changes listed + above happened thanks to this effort, and due to concerns raised by OBS + build agents with various Linux distributions. [#1209, #1316, #3144] - Updated `docs/*.txt`: add asciidoc comments with links to nut-website rendered contents of most interesting pages. [#3095] @@ -341,74 +425,15 @@ several `FSD` notifications into one executed action. [PR #3097] - HTML renditions of NUT man pages and books (or chunked chapters thereof) now include the AnchorJS script to provide permalinks to sections. [#3185] - - Added a `make dist-docs` goal to generate, collect and tarball all document - types (man, html-single, html-chunked, pdf) that we may have enabled for the - current build configuration and available tooling. [#1400] - - - Further developer workflow speedup with `make spellcheck-interactive-quick` - which should now not re-check source texts that were okay with the previous - dictionary contents, in case some new terms have to be added. [PRs #3186, #2871] - - - Added a GitHub Actions CI job to generate, upload and recycle `make dist` - and `make dist-docs` tarballs so they are easier to obtain for people and - other CI systems (which might not want to follow a Git repository). Links - to these artifacts can be seen in workflow page (or its logs), and added - as a line in GitHub Checks list associated with a commit. Note that the - GitHub Action storage keeps artifacts for at most 90 days. [#1400, #2829] - - Source directory `data/html` was renamed to `data/htmlcgi` in order to better reflect its contents and purpose, compared to documentation-oriented - `html*` directories (and recipe variable names). This may impact some - packaging recipes which do not rely on `make install` alone. [#3049] - - - Dropped the `compile` script from Git sources. It originates from automake - and is added to work area (if missing) during `autogen.sh` rituals anyway. - It is still distributed as part of `make dist` tarball. [#1209] - - - Default `PIDPATH` is now more strictly `/var/run`, unless building on a - system conforming to FHS-3.0 standard where that location is absent or - is a symlink, while `/run` exists and is a true directory. [#3099] - - - Augmented `nut.exe` launcher with `status` and `restart` actions, and - general ability to debug-log the activities during service management - operations. (Re-)registration of the "Network UPS Tools" service should - now populate a nice description of it. The `-U` (uninstall) action should - now also try to stop the service first. [PR #3235] - - - Fixed `LISTEN *` handling for `upsd.exe` in NUT for Windows builds. [PR #3237] - - - Revised WIN32 `WSAStartup()` and registration of `atexit(WSACleanup)` to - only be done once per program (and cleanups to be always registered); this - impacts the C `libupsclient` and C++ `libnutclient` libraries (and so most - NUT clients and tools which use the former, and `nutconf` which uses the - latter), the `apcupsd` driver, `upsd` data server, and `nut-scanner` tool. - [PR #3237] - - - Revised CI and deliverable scripts, and Makefile recipes, to not use - the verbatim `grep -E` (loudly preferred by newer systems, but may be - absent on older ones) after all, nor use `egrep` (loudly disliked by - newer systems). Instead, use what `configure` script detected for the - generated files (or ones made from templates), and use a similar - detection in standalone scripts. Also revised the use of `grep -q`, - `id -u`, `diff -u`, etc. which are not ubiquitous, and of `test -e` - which is not only absent in some older shells, but can cause them to - abort processing the script immediately. Also the `if ! condition` - syntax is not supported everywhere (or the `!` operator generally). - [#3099, #1660] - - - Revised `tools/gitlog2version.sh` helper script (logic added into - a new `tools/semver-compare.sh`) with a mode to expand NUT SEMVER - components into wide zero-padded numbers, so it is easier to - alphanumerically compare different releases regardless of version - component lengths in digits (and differences in their amounts). You can - request an inverse operation with `NUT_VERSION_STRIP_LEADING_ZEROES=true`. - Added tests to cover different shell interpreter platforms (piggy-back - on the `tests/nut-driver-enumerator-test.sh` script), and made sure - that outputs of legacy-mode processing (with `NUT_VERSION_DEFAULT` - string provided by caller or saved in a tarball) are consistent with - git-mode. The new `tools/semver-compare.sh` helper can be used directly - to expand and strip version strings, sort and compare multiple versions - as one simple operation. [issue #3055, PRs #3213, #3217] + `html*` directories (and recipe variable names). The static HTML pages now + also suggest a NUT icon (installed from `docs/images`). This may impact some + packaging recipes which do not rely on `make install` alone. [#3049, #3249] + + - Fixed man page naming for `nutdrv_siemens-sitop(.8)` (dash vs. underscore) + to match the driver program name. Follow-up from slightly botched renaming + in original contribution. [PR #545] - The NUT Integration Testing suite (NIT) script, if started as `root`, can now consult its run-time situation vs. `BUILTIN_RUN_AS_USER` and @@ -416,23 +441,6 @@ several `FSD` notifications into one executed action. [PR #3097] do not exist (e.g. running in a packaging build root), a different value like "nobody" or "nogroup" would be defaulted for tests. [#1209] - - Fixed man page naming for `nutdrv_siemens-sitop(.8)` (dash vs. underscore) - to match the driver program name. Follow-up from slightly botched renaming - in original contribution. [PR #545] - - - The `configure` script should now try harder to report specifically - the "purelib" location as `PYTHON*_SITE_PACKAGES`. [#1209] - - - Introduce `make distcheck-completeness` goal to verify that our distribution - archives can exactly reproduce themselves. [#2829] - - - Upstreamed reference packaging recipes (DEB, RPM) from the 42ITy project - which can be used with OBS (Open Build Service by SUSE), both to support - end-user testing of NUT development, and to have a yet another NUT CI farm - player with a distinct opinion on code quality. Many of the changes listed - above happened thanks to this effort, and due to concerns raised by OBS - build agents with various Linux distributions. [#1209, #1316, #3144] - - Revised use of NUT macros for USB Vendor IDs listed in different drivers, so the names would be better exposed in generated `udev.rules` and similar files. [PR #3139] diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 9ca1e7b572..fb5cfabfdb 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -50,6 +50,12 @@ Changes from 2.8.4 to 2.8.5 installed (and possibly customized) with the `*.html.sample` files delivered by the new build. [PR #3180] +- Introduced a `@NUT_UPSSTATS_TEMPLATE@` command which the NUT CGI template + files now MUST start with (safety check that we are reading a template). + While the delivered `upsstats*.html.sample` files would include the change, + ultimate `upsstats*.html` templates deployed for end-users MUST be updated. + [issue #3252, PR #3249] + - Dropped the `compile` script from Git sources. It originates from automake and is added to work area (if missing) during `autogen.sh` rituals anyway (as `make` says, `'automake --add-missing' can install 'compile'` when you diff --git a/appveyor.yml b/appveyor.yml index 2c3672b8ee..15aef8f564 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -106,6 +106,15 @@ build_script: set MSYSTEM=MINGW64 REM Note: currently we save job time and do not install asciidoc/a2x REM # --with-docs="man=auto html-single=auto html-chunked=no pdf=no" + REM The resulting configuration defaults to --prefix=/mingw{32,64} + REM as appropriate for the platform, which ends up as a subdirectory + REM under DESTDIR when we `make install` later, and in our 7z archive. + REM The builds are relocatable however, so end-users can extract to + REM e.g. "C:\Program Files\NUT" and forfeit "mingwXX" part, probably, + REM but this can complicate automatic relative directory resolution + REM to find "nearby" program or configuration files (see common.c + REM for current implementation). Hard-coded fallback strings may + REM end up getting used in those cases. C:\msys64\usr\bin\bash -lc 'date -u; PATH="/mingw64/bin:$PATH" CI_SKIP_CHECK=true CANBUILD_WITH_LIBMODBUS_USB=yes ./ci_build.sh --with-docs=no' diff --git a/clients/cgilib.c b/clients/cgilib.c index 6591a11686..687eed8c5b 100644 --- a/clients/cgilib.c +++ b/clients/cgilib.c @@ -116,42 +116,120 @@ void extractcgiargs(void) void extractpostargs(void) { - char buf[SMALLBUF], *ptr, *cleanval; - int ch; + char buf[SMALLBUF], *ptr, *cleanval, *server_software = NULL; + int ch, content_length = -1, bytes_seen = 0; + size_t buflen; + + /* First, see if there's anything waiting... + * the server may not close STDIN properly + * or somehow delay opening/populating it. */ +#ifndef WIN32 + int selret; + fd_set fds; + struct timeval tv; + + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + tv.tv_sec = 0; + tv.tv_usec = 250000; /* wait for up to 250ms for a POST query to come */ + + selret = select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); + if (selret <= 0) { +#else + HANDLE hSTDIN = GetStdHandle(STD_INPUT_HANDLE); + DWORD selret = WaitForSingleObject(hSTDIN, 250); + if (selret != WAIT_OBJECT_0) { /* or == WAIT_TIMEOUT ? */ +#endif /* WIN32 */ + upsdebug_with_errno(1, "%s: no stdin is waiting (%" PRIiMAX ")
", __func__, (intmax_t)selret); + return; + } - ch = fgetc(stdin); buf[0] = '\0'; - while (ch != EOF) { - if (ch == '&') { + /* Does the web server tell us how much it sent + * (and might keep the channel open... indefinitely)? */ + ptr = getenv("CONTENT_LENGTH"); + if (ptr) { + content_length = atoi(ptr); + } + + ptr = getenv("SERVER_SOFTWARE"); + if (ptr) { + server_software = ptr; + } else { + server_software = ""; + } + + if (content_length > 0 && strstr(server_software, "IIS")) { + /* Our POSTs end with a newline, and that one never arrives + * (reads hang), possibly buffered output from IIS? + * Our own setmode() in e.g. upsset.c does not help. + * So upsset.c ends each FORM with do_hidden_sentinel() + * to sacrifice a few bytes we would not use. + */ + upsdebugx(3, "%s: truncating expected content length on IIS
", __func__); + content_length--; + } + upsdebugx(3, "%s: starting to read %d POSTed bytes on server '%s'
", __func__, content_length, server_software); + + ch = fgetc(stdin); + upsdebugx(6, "%s: got char: '%c' (%d, 0x%02X)
", __func__, ch, ch, (unsigned int)ch); + + if (ch == EOF) { + bytes_seen++; + upsdebugx(3, "%s: got immediate EOF in stdin
", __func__); + } else while(1) { + bytes_seen++; + if (ch == '&' || ch == EOF || (content_length >= 0 && bytes_seen >= content_length)) { + buflen = strlen(buf); + upsdebugx(1, "%s: collected a chunk of %" PRIuSIZE " bytes on stdin: %s
", + __func__, buflen, buf); ptr = strchr(buf, '='); - if (!ptr) + if (!ptr) { + upsdebugx(3, "%s: parsearg('%s', '')
", __func__, buf); parsearg(buf, ""); - else { + } else { *ptr++ = '\0'; cleanval = unescape(ptr); + upsdebugx(3, "%s: parsearg('%s', '%s')
", __func__, buf, cleanval); parsearg(buf, cleanval); free(cleanval); } buf[0] = '\0'; + + if (ch == EOF || (content_length >= 0 && bytes_seen >= content_length)) + break; /* end the loop */ } else snprintfcat(buf, sizeof(buf), "%c", ch); +#ifndef WIN32 + /* Must re-init every time when looping (array is changed by select method) */ + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + tv.tv_sec = 0; + tv.tv_usec = 250000; /* wait for up to 250ms for a POST response */ + + selret = select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); + if (selret <= 0) { +#else + selret = WaitForSingleObject(hSTDIN, 250); + if (selret != WAIT_OBJECT_0) { /* or == WAIT_TIMEOUT ? */ +#endif + /* We do not always get EOF, so assume the input stream stopped */ + upsdebug_with_errno(1, "%s: timed out waiting for an stdin byte (%" PRIiMAX ")
", __func__, (intmax_t)selret); + break; + } + + fflush(stderr); ch = fgetc(stdin); - } + upsdebugx(6, "%s: got char: '%c' (%d, 0x%02X)
", __func__, ch, ch, (unsigned int)ch); + if (ch == EOF) + upsdebugx(3, "%s: got proper stdin EOF
", __func__); + upsdebugx(6, "%s: processed %d bytes with %d expected incoming content length on server '%s'
", __func__, bytes_seen, content_length, server_software); + } /* end of infinite loop */ - if (strlen(buf) != 0) { - ptr = strchr(buf, '='); - if (!ptr) - parsearg(buf, ""); - else { - *ptr++ = '\0'; - cleanval = unescape(ptr); - parsearg(buf, cleanval); - free(cleanval); - } - } + upsdebugx(3, "%s: processed %d bytes with %d incoming content length
", __func__, bytes_seen, content_length); } /* called for fatal errors in parseconf like malloc failures */ diff --git a/clients/upsimage.c b/clients/upsimage.c index a74b8f4d0c..909841704f 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -614,12 +614,38 @@ static int get_var(const char *var, char *buf, size_t buflen) int main(int argc, char **argv) { - char str[SMALLBUF]; + char str[SMALLBUF], *s; int i, min, nom, max; double var = 0; + +#ifdef WIN32 + /* Required ritual before calling any socket functions */ + static WSADATA WSAdata; + static int WSA_Started = 0; + if (!WSA_Started) { + WSAStartup(2, &WSAdata); + atexit((void(*)(void))WSACleanup); + WSA_Started = 1; + } + + /* Avoid binary output conversions, e.g. + * mangling what looks like CRLF on WIN32 */ + setmode(STDOUT_FILENO, O_BINARY); +#endif + NUT_UNUSED_VARIABLE(argc); NUT_UNUSED_VARIABLE(argv); + /* NOTE: Caller must `export NUT_DEBUG_LEVEL` to see debugs for upsc + * and NUT methods called from it. This line aims to just initialize + * the subsystem, and set initial timestamp. Debugging the client is + * primarily of use to developers, so is not exposed via `-D` args. + */ + s = getenv("NUT_DEBUG_LEVEL"); + if (s && str_to_int(s, &i, 10) && i > 0) { + nut_debug_level = i; + } + extractcgiargs(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); diff --git a/clients/upsset.c b/clients/upsset.c index ebfb2a5e91..03e10b504f 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -170,6 +170,14 @@ static void do_hidden(const char *next) next); } +static void do_hidden_sentinel(void) +{ + /* MS IIS tends to not close CGI STDIN and not serve the last byte(s) + * but just hangs at fgets(), so we truncate the inputs in cgilib, + * and add a dummy entry here that we can afford to lose in the end */ + printf("\n"); +} + /* generate SELECT chooser from hosts.conf entries */ static void upslist_arg(size_t numargs, char **arg) { @@ -202,7 +210,7 @@ static void do_pickups(const char *currfunc) snprintf(hostfn, sizeof(hostfn), "%s/hosts.conf", confpath()); - printf("
\n"); + printf("\n"); printf("Select UPS and function:\n
\n"); @@ -258,6 +266,8 @@ static void do_pickups(const char *currfunc) do_hidden(NULL); printf("\n"); + + do_hidden_sentinel(); printf("
\n"); } @@ -315,7 +325,7 @@ static void loginscreen(void) static void loginscreen(void) { do_header("Login"); - printf("
\n"); + printf("\n"); start_table(); printf("\n"); @@ -332,6 +342,7 @@ static void loginscreen(void) printf("\n"); printf("\n"); printf("\n"); + do_hidden_sentinel(); printf("\n"); printf("
\n"); printf("\n"); @@ -445,7 +456,7 @@ static void showcmds(void) "This UPS doesn't support any instant commands."); do_header("Instant commands"); - printf("
\n"); + printf("\n"); start_table(); /* include the description from checkhost() if present */ @@ -483,6 +494,7 @@ static void showcmds(void) printf("\n", monups); printf("\n"); printf("\n"); + do_hidden_sentinel(); printf("\n"); printf("\n"); printf("
\n"); @@ -873,7 +885,7 @@ static void showsettings(void) } do_header("Current settings"); - printf("
\n"); + printf("\n"); start_table(); /* include the description from checkhost() if present */ @@ -905,6 +917,7 @@ static void showsettings(void) printf("\n", monups); printf("\n"); printf("\n"); + do_hidden_sentinel(); printf("\n"); printf("\n"); printf("
\n"); @@ -1043,6 +1056,7 @@ static void check_conf(void) PCONF_CTX_t ctx; snprintf(fn, sizeof(fn), "%s/upsset.conf", confpath()); + upsdebugx(1, "%s: considering configuration file %s", __func__, fn); pconf_init(&ctx, upsset_conf_err); @@ -1092,30 +1106,50 @@ static void check_conf(void) int main(int argc, char **argv) { + char *s; + int i; + +#ifdef WIN32 + /* Required ritual before calling any socket functions */ + static WSADATA WSAdata; + static int WSA_Started = 0; + if (!WSA_Started) { + WSAStartup(2, &WSAdata); + atexit((void(*)(void))WSACleanup); + WSA_Started = 1; + } + + /* Avoid binary output conversions, e.g. + * mangling what looks like CRLF on WIN32 */ + setmode(STDOUT_FILENO, O_BINARY); + /* Also do not break what we receive from HTTP POST queries */ + setmode(STDIN_FILENO, O_BINARY); +#endif + NUT_UNUSED_VARIABLE(argc); NUT_UNUSED_VARIABLE(argv); username = password = function = monups = NULL; printf("Content-type: text/html\n\n"); + /* NOTE: Caller must `export NUT_DEBUG_LEVEL` to see debugs for upsc + * and NUT methods called from it. This line aims to just initialize + * the subsystem, and set initial timestamp. Debugging the client is + * primarily of use to developers, so is not exposed via `-D` args. + */ + s = getenv("NUT_DEBUG_LEVEL"); + if (s && str_to_int(s, &i, 10) && i > 0) { + nut_debug_level = i; + } + /* see if the magic string is present in the config file */ check_conf(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); - /* see if there's anything waiting .. the server my not close STDIN properly */ - if (1) { - fd_set fds; - struct timeval tv; + extractpostargs(); - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); - tv.tv_sec = 0; - tv.tv_usec = 250000; /* wait for up to 250ms for a POST response */ - - if ((select(STDIN_FILENO+1, &fds, 0, 0, &tv)) > 0) - extractpostargs(); - } + /* Nothing POSTed (or parsed correctly)? */ if ((!username) || (!password) || (!function)) loginscreen(); diff --git a/clients/upsstats.c b/clients/upsstats.c index 06a341a0f8..e61af6f68b 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -39,12 +39,30 @@ static char *monhost = NULL; static int use_celsius = 1, refreshdelay = -1, treemode = 0; static int output_json = 0; +/* call tracing for debug */ +static int call_depth = 0; +#define upsdebug_call_starting0() upsdebugx(2, "[depth=%02d+] starting %s...", call_depth++, __func__) +#define upsdebug_call_starting1(msgfmt) upsdebugx(2, "[depth=%02d+] starting %s " msgfmt "...", call_depth++, __func__) +#define upsdebug_call_starting2(msgfmt, arg1) upsdebugx(2, "[depth=%02d+] starting %s " msgfmt "...", call_depth++, __func__, arg1) +#define upsdebug_call_starting3(msgfmt, arg1, arg2) upsdebugx(2, "[depth=%02d+] starting %s " msgfmt "...", call_depth++, __func__, arg1, arg2) +#define upsdebug_call_starting4(msgfmt, arg1, arg2, arg3) upsdebugx(2, "[depth=%02d+] starting %s " msgfmt "...", call_depth++, __func__, arg1, arg2, arg3) + +#define upsdebug_call_starting_for_str1(arg1) upsdebug_call_starting2("for '%s'", NUT_STRARG(arg1)) +#define upsdebug_call_starting_for_str2(arg1, arg2) upsdebug_call_starting3("for '%s' '%s'", NUT_STRARG(arg1), NUT_STRARG(arg2)) +#define upsdebug_call_starting_for_str3(arg1, arg2, arg3) upsdebug_call_starting4("for '%s' '%s' '%s'", NUT_STRARG(arg1), NUT_STRARG(arg2), NUT_STRARG(arg3)) + +#define upsdebug_call_finished0() upsdebugx(2, "[depth=%02d-] finished %s", --call_depth, __func__) +#define upsdebug_call_finished1(msgfmt) upsdebugx(2, "[depth=%02d-] finished %s " msgfmt, --call_depth, __func__) +#define upsdebug_call_finished2(msgfmt, arg1) upsdebugx(2, "[depth=%02d-] finished %s " msgfmt, --call_depth, __func__, arg1) +#define upsdebug_call_finished3(msgfmt, arg1, arg2) upsdebugx(2, "[depth=%02d-] finished %s " msgfmt, --call_depth, __func__, arg1, arg2) +#define upsdebug_call_finished4(msgfmt, arg1, arg2, arg3) upsdebugx(2, "[depth=%02d-] finished %s " msgfmt, --call_depth, __func__, arg1, arg2, arg3) + /* from cgilib's checkhost() */ static char *monhostdesc = NULL; static uint16_t port; static char *upsname, *hostname; -static char *upsimgpath="upsimage.cgi", *upsstatpath="upsstats.cgi"; +static char *upsimgpath="upsimage.cgi" EXEEXT, *upsstatpath="upsstats.cgi" EXEEXT; static UPSCONN_t ups; static FILE *tf; @@ -56,14 +74,17 @@ static int skip_clause = 0, skip_block = 0; void parsearg(char *var, char *value) { + upsdebug_call_starting_for_str2(var, value); + /* avoid bogus junk from evil people */ - if ((strlen(var) > MAX_CGI_STRLEN) || (strlen(value) > MAX_CGI_STRLEN)) + if ((strlen(var) > MAX_CGI_STRLEN) || (strlen(value) > MAX_CGI_STRLEN)) { + upsdebug_call_finished1(": strings too long for CGI"); return; + } if (!strcmp(var, "host")) { free(monhost); monhost = xstrdup(value); - return; } if (!strcmp(var, "refresh")) @@ -77,23 +98,32 @@ void parsearg(char *var, char *value) if (!strcmp(var, "json")) { output_json = 1; } + + upsdebug_call_finished0(); } static void report_error(void) { + upsdebug_call_starting0(); + if (upscli_upserror(&ups) == UPSCLI_ERR_VARNOTSUPP) printf("Not supported\n"); else printf("[error: %s]\n", upscli_strerror(&ups)); + + upsdebug_call_finished0(); } /* make sure we're actually connected to upsd */ static int check_ups_fd(int do_report) { + upsdebug_call_starting0(); + if (upscli_fd(&ups) == -1) { if (do_report) report_error(); + upsdebug_call_finished1(": upscli_fd() failed"); return 0; } @@ -103,10 +133,12 @@ static int check_ups_fd(int do_report) if (do_report) printf("No UPS specified for monitoring\n"); + upsdebug_call_finished1(": currups is null"); return 0; } /* must be OK */ + upsdebug_call_finished0(); return 1; } @@ -117,14 +149,19 @@ static int get_var(const char *var, char *buf, size_t buflen, int verbose) const char *query[4]; char **answer; + upsdebug_call_starting_for_str2(upsname, var); + /* pass verbose to check_ups_fd */ - if (!check_ups_fd(verbose)) + if (!check_ups_fd(verbose)) { + upsdebug_call_finished0(); return 0; + } if (!upsname) { if (verbose) printf("[No UPS name specified]\n"); + upsdebug_call_finished1(": no UPS name"); return 0; } @@ -139,6 +176,8 @@ static int get_var(const char *var, char *buf, size_t buflen, int verbose) if (ret < 0) { if (verbose) report_error(); + + upsdebug_call_finished1(": upscli_get() failed"); return 0; } @@ -146,10 +185,12 @@ static int get_var(const char *var, char *buf, size_t buflen, int verbose) if (verbose) printf("[Invalid response]\n"); + upsdebug_call_finished1(": invalid response"); return 0; } snprintf(buf, buflen, "%s", answer[3]); + upsdebug_call_finished0(); return 1; } @@ -157,10 +198,15 @@ static void parse_var(const char *var) { char answer[SMALLBUF]; - if (!get_var(var, answer, sizeof(answer), 1)) + upsdebug_call_starting_for_str1(var); + + if (!get_var(var, answer, sizeof(answer), 1)) { + upsdebug_call_finished1(": get_var() failed"); return; + } printf("%s", answer); + upsdebug_call_finished0(); } static void do_status(void) @@ -168,12 +214,14 @@ static void do_status(void) int i; char status[SMALLBUF], *ptr, *last = NULL; + upsdebug_call_starting0(); + if (!get_var("ups.status", status, sizeof(status), 1)) { + upsdebug_call_finished1(": get_var() failed"); return; } for (ptr = strtok_r(status, " \n", &last); ptr != NULL; ptr = strtok_r(NULL, " \n", &last)) { - /* expand from table in status.h */ for (i = 0; stattab[i].name != NULL; i++) { @@ -182,6 +230,8 @@ static void do_status(void) } } } + + upsdebug_call_finished0(); } static void do_runtime(void) @@ -189,8 +239,12 @@ static void do_runtime(void) int total, hours, minutes, seconds; char runtime[SMALLBUF]; - if (!get_var("battery.runtime", runtime, sizeof(runtime), 1)) + upsdebug_call_starting0(); + + if (!get_var("battery.runtime", runtime, sizeof(runtime), 1)) { + upsdebug_call_finished1(": get_var() failed"); return; + } total = (int) strtol(runtime, (char **) NULL, 10); @@ -199,7 +253,7 @@ static void do_runtime(void) seconds = total % 60; printf("%02d:%02d:%02d", hours, minutes, seconds); - + upsdebug_call_finished0(); } static int do_date(const char *buf) @@ -208,12 +262,16 @@ static int do_date(const char *buf) time_t tod; struct tm tmbuf; + upsdebug_call_starting0(); + time(&tod); if (strftime(datebuf, sizeof(datebuf), buf, localtime_r(&tod, &tmbuf))) { printf("%s", datebuf); + upsdebug_call_finished0(); return 1; } + upsdebug_call_finished1(": failed"); return 0; } @@ -221,8 +279,12 @@ static int get_img_val(const char *var, const char *desc, const char *imgargs) { char answer[SMALLBUF]; - if (!get_var(var, answer, sizeof(answer), 1)) + upsdebug_call_starting_for_str3(var, desc, imgargs); + + if (!get_var(var, answer, sizeof(answer), 1)) { + upsdebug_call_finished1(": get_var() failed"); return 1; + } printf("sys, var); @@ -232,6 +294,7 @@ static int get_img_val(const char *var, const char *desc, const char *imgargs) printf("\" ALT=\"%s: %s\">", desc, answer); + upsdebug_call_finished0(); return 1; } @@ -289,6 +352,9 @@ static void split_imgarg(char *in, char *out, size_t outlen) static int do_img(char *buf) { char *type, *ptr, imgargs[SMALLBUF]; + int ret = 0; + + upsdebug_call_starting2("for type '%s'", buf); memset(imgargs, '\0', sizeof(imgargs)); @@ -309,15 +375,21 @@ static int do_img(char *buf) || !strcmp(type, "input.L3-N.voltage") || !strcmp(type, "input.L1-L2.voltage") || !strcmp(type, "input.L2-L3.voltage") - || !strcmp(type, "input.L3-L1.voltage")) { - return get_img_val(type, "Input voltage", imgargs); + || !strcmp(type, "input.L3-L1.voltage") + ) { + ret = get_img_val(type, "Input voltage", imgargs); + goto finish; } - if (!strcmp(type, "battery.voltage")) - return get_img_val(type, "Battery voltage", imgargs); + if (!strcmp(type, "battery.voltage")) { + ret = get_img_val(type, "Battery voltage", imgargs); + goto finish; + } - if (!strcmp(type, "battery.charge")) - return get_img_val(type, "Battery charge", imgargs); + if (!strcmp(type, "battery.charge")) { + ret = get_img_val(type, "Battery charge", imgargs); + goto finish; + } if (!strcmp(type, "output.voltage") || !strcmp(type, "output.L1-N.voltage") @@ -325,8 +397,10 @@ static int do_img(char *buf) || !strcmp(type, "output.L3-N.voltage") || !strcmp(type, "output.L1-L2.voltage") || !strcmp(type, "output.L2-L3.voltage") - || !strcmp(type, "output.L3-L1.voltage")) { - return get_img_val(type, "Output voltage", imgargs); + || !strcmp(type, "output.L3-L1.voltage") + ) { + ret = get_img_val(type, "Output voltage", imgargs); + goto finish; } if (!strcmp(type, "ups.load") @@ -335,26 +409,40 @@ static int do_img(char *buf) || !strcmp(type, "output.L3.power.percent") || !strcmp(type, "output.L1.realpower.percent") || !strcmp(type, "output.L2.realpower.percent") - || !strcmp(type, "output.L3.realpower.percent")) { - return get_img_val(type, "UPS load", imgargs); + || !strcmp(type, "output.L3.realpower.percent") + ) { + ret = get_img_val(type, "UPS load", imgargs); + goto finish; } - if (!strcmp(type, "input.frequency")) - return get_img_val(type, "Input frequency", imgargs); + if (!strcmp(type, "input.frequency")) { + ret = get_img_val(type, "Input frequency", imgargs); + goto finish; + } - if (!strcmp(type, "output.frequency")) - return get_img_val(type, "Output frequency", imgargs); + if (!strcmp(type, "output.frequency")) { + ret = get_img_val(type, "Output frequency", imgargs); + goto finish; + } - if (!strcmp(type, "ups.temperature")) - return get_img_val(type, "UPS temperature", imgargs); + if (!strcmp(type, "ups.temperature")) { + ret = get_img_val(type, "UPS temperature", imgargs); + goto finish; + } - if (!strcmp(type, "ambient.temperature")) - return get_img_val(type, "Ambient temperature", imgargs); + if (!strcmp(type, "ambient.temperature")) { + ret = get_img_val(type, "Ambient temperature", imgargs); + goto finish; + } - if (!strcmp(type, "ambient.humidity")) - return get_img_val(type, "Ambient humidity", imgargs); + if (!strcmp(type, "ambient.humidity")) { + ret = get_img_val(type, "Ambient humidity", imgargs); + goto finish; + } - return 0; +finish: + upsdebug_call_finished0(); + return ret; } static void ups_connect(void) @@ -363,23 +451,28 @@ static void ups_connect(void) char *newups, *newhost; uint16_t newport = 0; + upsdebug_call_starting0(); + /* try to minimize reconnects */ if (lastups) { /* don't reconnect if these are both the same UPS */ if (currups && !strcmp(lastups->sys, currups->sys)) { lastups = currups; + upsdebug_call_finished1(": skip: lastups same as currups"); return; } /* see if it's just on the same host */ newups = newhost = NULL; - if (currups && upscli_splitname(currups->sys, &newups, &newhost, - &newport) != 0) { + if (currups + && upscli_splitname(currups->sys, &newups, &newhost, &newport) != 0 + ) { printf("Unusable UPS definition [%s]\n", currups->sys); fprintf(stderr, "Unusable UPS definition [%s]\n", currups->sys); + upsdebug_call_finished1(": Unusable UPS definition"); exit(EXIT_FAILURE); } @@ -389,12 +482,16 @@ static void ups_connect(void) free(newhost); lastups = currups; + + upsdebug_call_finished2(": pick next device on already connected data server [%s]", NUT_STRARG(currups->sys)); return; } /* not the same upsd, so disconnect */ free(newups); free(newhost); + + upsdebugx(2, "%s: not same data server as used by lastups: will connect to another", __func__); } upscli_disconnect(&ups); @@ -407,6 +504,7 @@ static void ups_connect(void) if (currups && upscli_splitname(currups->sys, &upsname, &hostname, &port) != 0) { printf("Unusable UPS definition [%s]\n", currups->sys); fprintf(stderr, "Unusable UPS definition [%s]\n", currups->sys); + upsdebug_call_finished1(": Unusable UPS definition"); exit(EXIT_FAILURE); } @@ -414,11 +512,15 @@ static void ups_connect(void) fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", currups->sys, upscli_strerror(&ups)); lastups = currups; + upsdebug_call_finished2(": pick first device on newly connected data server [%s]", NUT_STRARG(currups->sys)); } static void do_hostlink(void) { + upsdebug_call_starting0(); + if (!currups) { + upsdebug_call_finished1(": no-op: no currups!"); return; } @@ -429,15 +531,39 @@ static void do_hostlink(void) } printf("\">%s", currups->desc); + upsdebug_call_finished0(); } -static void do_treelink(void) +static void do_treelink_json(const char *text) { + upsdebug_call_starting0(); + if (!currups) { + upsdebug_call_finished1(": no-op: no currups!"); return; } - printf("All data", upsstatpath, currups->sys); + printf("%s", + upsstatpath, currups->sys, + ((text && *text) ? text : "JSON")); + + upsdebug_call_finished0(); +} + +static void do_treelink(const char *text) +{ + upsdebug_call_starting0(); + + if (!currups) { + upsdebug_call_finished1(": no-op: no currups!"); + return; + } + + printf("%s", + upsstatpath, currups->sys, + ((text && *text) ? text : "All data")); + + upsdebug_call_finished0(); } /* see if the UPS supports this variable - skip to the next ENDIF if not */ @@ -446,25 +572,33 @@ static void do_ifsupp(const char *var, const char *val) { char dummy[SMALLBUF]; + upsdebug_call_starting3("for '%s' ( =? '%s')", NUT_STRARG(var), NUT_STRARG(val)); + /* if not connected, act like it's not supported and skip the rest */ if (!check_ups_fd(0)) { skip_clause = 1; + upsdebug_call_finished1(": check_ups_fd() failed"); return; } if (!get_var(var, dummy, sizeof(dummy), 0)) { skip_clause = 1; + upsdebug_call_finished1(": get_var() failed"); return; } - if(!val) { + if (!val) { + upsdebug_call_finished1(": ok, not checking val"); return; } - if(strcmp(dummy, val)) { + if (strcmp(dummy, val)) { skip_clause = 1; + upsdebug_call_finished1(": get_var() returned unexpected val"); return; } + + upsdebug_call_finished0(); } static int breakargs(char *s, char **aargs) @@ -497,15 +631,19 @@ static void do_ifeq(const char *s) char *aa[MAX_PARSE_ARGS]; int nargs; + upsdebug_call_starting_for_str1(s); + strcpy(var, s); nargs = breakargs(var, aa); if(nargs != 2) { printf("upsstats: IFEQ: Argument error!\n"); + upsdebug_call_finished1(": arg error"); return; } do_ifsupp(aa[0], aa[1]); + upsdebug_call_finished0(); } /* IFBETWEEN var1 var2 var3. Skip if var3 not between var1 @@ -519,60 +657,79 @@ static void do_ifbetween(const char *s) long v1, v2, v3; char *isvalid=NULL; + upsdebug_call_starting_for_str1(s); + strcpy(var, s); nargs = breakargs(var, aa); - if(nargs != 3) { + if (nargs != 3) { printf("upsstats: IFBETWEEN: Argument error!\n"); + upsdebug_call_finished1(": Argument error"); return; } if (!check_ups_fd(0)) { + upsdebug_call_finished1(": check_ups_fd() failed"); return; } if (!get_var(aa[0], tmp, sizeof(tmp), 0)) { + upsdebug_call_finished0(); return; } v1 = strtol(tmp, &isvalid, 10); - if(tmp == isvalid) { + if (tmp == isvalid) { + upsdebug_call_finished0(); return; } if (!get_var(aa[1], tmp, sizeof(tmp), 0)) { + upsdebug_call_finished0(); return; } v2 = strtol(tmp, &isvalid, 10); - if(tmp == isvalid) { + if (tmp == isvalid) { + upsdebug_call_finished0(); return; } if (!get_var(aa[2], tmp, sizeof(tmp), 0)) { + upsdebug_call_finished0(); return; } v3 = strtol(tmp, &isvalid, 10); - if(tmp == isvalid) { + if (tmp == isvalid) { + upsdebug_call_finished0(); return; } - if(v1 > v3 || v2 < v3) { + if (v1 > v3 || v2 < v3) { skip_clause = 1; + upsdebug_call_finished0(); return; } + + upsdebug_call_finished0(); } static void do_upsstatpath(const char *s) { + upsdebug_call_starting_for_str1(s); if(strlen(s)) { upsstatpath = strdup(s); } + + upsdebug_call_finished0(); } static void do_upsimgpath(const char *s) { + upsdebug_call_starting_for_str1(s); if(strlen(s)) { upsimgpath = strdup(s); } + + upsdebug_call_finished0(); } static void do_temp(const char *var) @@ -580,26 +737,36 @@ static void do_temp(const char *var) char tempc[SMALLBUF]; double tempf; - if (!get_var(var, tempc, sizeof(tempc), 1)) + upsdebug_call_starting_for_str1(var); + + if (!get_var(var, tempc, sizeof(tempc), 1)) { + upsdebug_call_finished1(": get_var() failed"); return; + } if (use_celsius) { printf("%s", tempc); + upsdebug_call_finished0(); return; } tempf = (strtod(tempc, (char **) NULL) * 1.8) + 32; printf("%.1f", tempf); + upsdebug_call_finished0(); } static void do_degrees(void) { + upsdebug_call_starting0(); + printf("°"); if (use_celsius) printf("C"); else printf("F"); + + upsdebug_call_finished0(); } /* plug in the right color string (like #FF0000) for the UPS status */ @@ -608,16 +775,19 @@ static void do_statuscolor(void) int severity, i; char stat[SMALLBUF], *ptr, *last = NULL; - if (!check_ups_fd(0)) { + upsdebug_call_starting0(); + if (!check_ups_fd(0)) { /* can't print the warning here - give a red error condition */ printf("#FF0000"); + upsdebug_call_finished1(": check_ups_fd() failed"); return; } if (!get_var("ups.status", stat, sizeof(stat), 0)) { /* status not available - give yellow as a warning */ printf("#FFFF00"); + upsdebug_call_finished1(": get_var() failed"); return; } @@ -639,19 +809,25 @@ static void do_statuscolor(void) default: printf("#FF0000"); break; /* red : error */ } + + upsdebug_call_finished0(); } static int do_command(char *cmd) { + upsdebug_call_starting_for_str1(cmd); + /* ending an if block? */ if (!strcmp(cmd, "ENDIF")) { skip_clause = 0; skip_block = 0; + upsdebug_call_finished1(": ENDIF"); return 1; } /* Skipping a block means skip until ENDIF, so... */ if (skip_block) { + upsdebug_call_finished1(": skip until ENDIF"); return 1; } @@ -662,64 +838,77 @@ static int do_command(char *cmd) } else { skip_block = 1; } + upsdebug_call_finished1(": ELSE (state toggle)"); return 1; } /* don't do any commands if skipping a section */ if (skip_clause == 1) { + upsdebug_call_finished1(": SKIP in effect"); return 1; } if (!strncmp(cmd, "VAR ", 4)) { parse_var(&cmd[4]); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "HOST")) { printf("%s", currups->sys); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "HOSTDESC")) { printf("%s", currups->desc); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "RUNTIME")) { do_runtime(); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "STATUS")) { do_status(); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "STATUSCOLOR")) { do_statuscolor(); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "TEMPF")) { use_celsius = 0; + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "TEMPC")) { use_celsius = 1; + upsdebug_call_finished0(); return 1; } if (!strncmp(cmd, "DATE ", 5)) { + upsdebug_call_finished0(); return do_date(&cmd[5]); } if (!strncmp(cmd, "IMG ", 4)) { + upsdebug_call_finished0(); return do_img(&cmd[4]); } if (!strcmp(cmd, "VERSION")) { printf("%s", UPS_VERSION); + upsdebug_call_finished0(); return 1; } @@ -727,6 +916,7 @@ static int do_command(char *cmd) if (refreshdelay > 0) { printf("", refreshdelay); } + upsdebug_call_finished0(); return 1; } @@ -734,82 +924,125 @@ static int do_command(char *cmd) forofs = ftell(tf); currups = ulhead; + upsdebugx(2, "%s: FOREACHUPS: begin with UPS [%s] [%s]", __func__, NUT_STRARG(currups->sys), NUT_STRARG(currups->desc)); + upsdebugx(2, "%s: current skip_clause=%d skip_block=%d", __func__, skip_clause, skip_block); ups_connect(); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "ENDFOR")) { - /* if not in a for, ignore this */ if (forofs == 0) { + upsdebug_call_finished1(": not in FOR"); return 1; } + upsdebugx(2, "%s: ENDFOR: done with UPS [%s] [%s]", __func__, NUT_STRARG(currups->sys), NUT_STRARG(currups->desc)); + upsdebugx(2, "%s: current skip_clause=%d skip_block=%d", __func__, skip_clause, skip_block); currups = currups->next; if (currups) { + upsdebugx(2, "%s: ENDFOR: proceed with next UPS [%s]", __func__, NUT_STRARG(currups->desc)); fseek(tf, forofs, SEEK_SET); ups_connect(); } + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "HOSTLINK")) { do_hostlink(); + upsdebug_call_finished0(); + return 1; + } + + if (!strncmp(cmd, "TREELINK_JSON ", 14)) { + do_treelink_json(&cmd[14]); + upsdebug_call_finished0(); + return 1; + } + + if (!strcmp(cmd, "TREELINK_JSON")) { + do_treelink_json(NULL); + upsdebug_call_finished0(); + return 1; + } + + if (!strncmp(cmd, "TREELINK ", 9)) { + do_treelink(&cmd[9]); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "TREELINK")) { - do_treelink(); + do_treelink(NULL); + upsdebug_call_finished0(); return 1; } if (!strncmp(cmd, "IFSUPP ", 7)) { do_ifsupp(&cmd[7], NULL); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "UPSTEMP")) { do_temp("ups.temperature"); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "BATTTEMP")) { do_temp("battery.temperature"); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "AMBTEMP")) { do_temp("ambient.temperature"); + upsdebug_call_finished0(); return 1; } if (!strcmp(cmd, "DEGREES")) { do_degrees(); + upsdebug_call_finished0(); return 1; } if (!strncmp(cmd, "IFEQ ", 5)) { do_ifeq(&cmd[5]); + upsdebug_call_finished0(); return 1; } if (!strncmp(cmd, "IFBETWEEN ", 10)) { do_ifbetween(&cmd[10]); + upsdebug_call_finished0(); return 1; } if (!strncmp(cmd, "UPSSTATSPATH ", 13)) { do_upsstatpath(&cmd[13]); + upsdebug_call_finished0(); return 1; } if (!strncmp(cmd, "UPSIMAGEPATH ", 13)) { do_upsimgpath(&cmd[13]); + upsdebug_call_finished0(); + return 1; + } + + if (!strncmp(cmd, "NUT_UPSSTATS_TEMPLATE ", 22) || !strcmp(cmd, "NUT_UPSSTATS_TEMPLATE")) { + upsdebugx(2, "%s: saw magic token, ignoring", __func__); + upsdebug_call_finished0(); return 1; } + upsdebug_call_finished2(": unknown cmd: '%s'", cmd); return 0; } @@ -819,8 +1052,9 @@ static void parse_line(const char *buf) size_t i, len; char do_cmd = 0; - for (i = 0; buf[i]; i += len) { + upsdebug_call_starting_for_str1(buf); + for (i = 0; buf[i]; i += len) { len = strcspn(&buf[i], "@"); if (len == 0) { @@ -849,21 +1083,47 @@ static void parse_line(const char *buf) /* pass it trough */ printf("%.*s", (int)len, &buf[i]); } + + upsdebug_call_finished0(); } static void display_template(const char *tfn) { char fn[NUT_PATH_MAX + 1], buf[LARGEBUF]; + upsdebug_call_starting_for_str1(tfn); + snprintf(fn, sizeof(fn), "%s/%s", confpath(), tfn); - tf = fopen(fn, "r"); + tf = fopen(fn, "rb"); if (!tf) { fprintf(stderr, "upsstats: Can't open %s: %s\n", fn, strerror(errno)); printf("Error: can't open template file (%s)\n", tfn); + upsdebug_call_finished1(": no template"); + exit(EXIT_FAILURE); + } + + if (!fgets(buf, sizeof(buf), tf)) { + fprintf(stderr, "upsstats: template file %s seems to be empty (fgets failed): %s\n", fn, strerror(errno)); + + printf("Error: template file %s seems to be empty\n", tfn); + + upsdebug_call_finished1(": empty template"); + exit(EXIT_FAILURE); + } + + /* Test first line for a bit of expected magic */ + if (!strncmp(buf, "@NUT_UPSSTATS_TEMPLATE", 22)) { + parse_line(buf); + } else { + fprintf(stderr, "upsstats: template file %s does not start with NUT_UPSSTATS_TEMPLATE command\n", fn); + + printf("Error: template file %s does not start with NUT_UPSSTATS_TEMPLATE command\n", tfn); + + upsdebug_call_finished1(": not a valid template"); exit(EXIT_FAILURE); } @@ -872,6 +1132,7 @@ static void display_template(const char *tfn) } fclose(tf); + upsdebug_call_finished0(); } static void display_tree(int verbose) @@ -880,9 +1141,12 @@ static void display_tree(int verbose) const char *query[4]; char **answer; + upsdebug_call_starting0(); + if (!upsname) { if (verbose) printf("[No UPS name specified]\n"); + upsdebug_call_finished1(": No UPS name specified"); return; } @@ -893,6 +1157,7 @@ static void display_tree(int verbose) if (upscli_list_start(&ups, numq, query) < 0) { if (verbose) report_error(); + upsdebug_call_finished1(": upscli_list_start() failed"); return; } @@ -922,6 +1187,7 @@ static void display_tree(int verbose) if (verbose) printf("[Invalid response]\n"); + upsdebug_call_finished1(": invalid response"); return; } @@ -939,6 +1205,7 @@ static void display_tree(int verbose) /* FIXME (AQ): add a save button (?), and a checkbt for showing var.desc */ printf("\n"); + upsdebug_call_finished0(); } static void add_ups(char *sys, char *desc) @@ -975,7 +1242,8 @@ static void load_hosts_conf(void) char fn[NUT_PATH_MAX + 1]; PCONF_CTX_t ctx; - snprintf(fn, sizeof(fn), "%s/hosts.conf", CONFPATH); + snprintf(fn, sizeof(fn), "%s/hosts.conf", confpath()); + upsdebugx(1, "%s: considering configuration file %s", __func__, fn); pconf_init(&ctx, upsstats_hosts_err); @@ -1040,9 +1308,12 @@ static void load_hosts_conf(void) static void display_single(void) { + upsdebug_call_starting0(); + if (!checkhost(monhost, &monhostdesc)) { printf("Access to that host [%s] is not authorized.\n", monhost); + upsdebug_call_finished1(": not auth"); exit(EXIT_FAILURE); } @@ -1058,6 +1329,7 @@ static void display_single(void) display_template("upsstats-single.html"); upscli_disconnect(&ups); + upsdebug_call_finished0(); } /* ------------------------------------------------------------- */ @@ -1081,6 +1353,8 @@ static void display_json(void) int is_first_var = 1; char *ptr, *last = NULL; + upsdebug_call_starting0(); + /* If monhost is set, we're in single-host mode. * If not, we're in multi-host mode. * We need to load hosts.conf ONLY in multi-host mode. @@ -1088,6 +1362,7 @@ static void display_json(void) if (monhost) { if (!checkhost(monhost, &monhostdesc)) { printf("{\"error\": \"Access to host %s is not authorized.\"}", monhost); + upsdebug_call_finished1(": not auth"); return; } add_ups(monhost, monhostdesc); @@ -1100,6 +1375,7 @@ static void display_json(void) if (!currups) { /* load_hosts_conf() would have exited, but check anyway */ printf("{\"error\": \"No hosts to monitor.\"}"); + upsdebug_call_finished1(": No hosts to monitor"); return; } @@ -1197,6 +1473,8 @@ static void display_json(void) if (!monhost) { printf("\n]}\n"); } + + upsdebug_call_finished0(); } @@ -1207,9 +1485,37 @@ static void display_json(void) int main(int argc, char **argv) { + char *s; + int i; + +#ifdef WIN32 + /* Required ritual before calling any socket functions */ + static WSADATA WSAdata; + static int WSA_Started = 0; + if (!WSA_Started) { + WSAStartup(2, &WSAdata); + atexit((void(*)(void))WSACleanup); + WSA_Started = 1; + } + + /* Avoid binary output conversions, e.g. + * mangling what looks like CRLF on WIN32 */ + setmode(STDOUT_FILENO, O_BINARY); +#endif + NUT_UNUSED_VARIABLE(argc); NUT_UNUSED_VARIABLE(argv); + /* NOTE: Caller must `export NUT_DEBUG_LEVEL` to see debugs for upsc + * and NUT methods called from it. This line aims to just initialize + * the subsystem, and set initial timestamp. Debugging the client is + * primarily of use to developers, so is not exposed via `-D` args. + */ + s = getenv("NUT_DEBUG_LEVEL"); + if (s && str_to_int(s, &i, 10) && i > 0) { + nut_debug_level = i; + } + extractcgiargs(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); diff --git a/common/common.c b/common/common.c index 10eb7ac73e..5e6697a052 100644 --- a/common/common.c +++ b/common/common.c @@ -953,6 +953,8 @@ char * getprocname(pid_t pid) pathname[sizeof(pathname) - 1] = '\0'; if (ret) { + size_t pathname_offset = 0; + /* length of the string copied to the buffer */ procnamelen = strlen(pathname); @@ -964,8 +966,32 @@ char * getprocname(pid_t pid) __func__, (uintmax_t)ret, procnamelen); } + /* At least when running under IIS, CGI programs were + * seen to use UNC filesystem paths like `\\?\c:\...`, + * which POSIX methods are not comfortable with. + * TOTHINK: Do we want to check with fopen()/fstat() + * first? At least the current "program module" named + * file should exist, right? + */ + if (pathname[0] == '\\' && pathname[1] == '\\') { + if (pathname[2] == '?' && pathname[3] == '\\' + && ( (pathname[4] >= 'a' && pathname[4] <= 'z') + || (pathname[4] >= 'A' && pathname[4] <= 'Z') ) + && pathname[5] == ':' + ) { + /* Chop off `\\?\ part */ + pathname_offset = 4; + procnamelen -= 4; + upsdebugx(3, "%s: GetModuleFileNameExA() returned '%s' which seems like localhost UNC path, chopping off the prefix to use just '%s'", + __func__, pathname, pathname + pathname_offset); + } else { + upsdebugx(1, "%s: GetModuleFileNameExA() returned '%s' which seems like UNC path, these are not currently supported and later calls may fail due to this", + __func__, pathname); + } + } + if ((procname = (char*)calloc(procnamelen + 1, sizeof(char)))) { - if (snprintf(procname, procnamelen + 1, "%s", pathname) < 1) { + if (snprintf(procname, procnamelen + 1, "%s", pathname + pathname_offset) < 1) { upsdebug_with_errno(3, "%s: failed to snprintf procname: WIN32-like", __func__); } else { goto finish; @@ -1745,8 +1771,13 @@ static void win_PREFIX_cleanup(void) { char * getfullpath(char * relative_path) { + /* NOTE: we keep the discovered bytes in buf[], and manipulate this + * string's ending below, but may also ignore some starting bytes + * using the (buf + buf_offset) formula. Finally we xstrdup() the + * useful contents returned to caller. + */ char buf[NUT_PATH_MAX + 1], *last_slash = NULL; - static size_t len_PREFIX = 0; + static size_t len_PREFIX = 0, buf_offset = 0; if (!len_PREFIX) { len_PREFIX = strlen(PREFIX); @@ -1763,11 +1794,34 @@ char * getfullpath(char * relative_path) upsdebugx(5, "%s: passed relative_path:\t'%s'", __func__, NUT_STRARG(relative_path)); upsdebugx(5, "%s: GetModuleFileName (buf):\t'%s'", __func__, buf); - /* remove trailing executable name and its preceeding slash */ - last_slash = strrchr(buf, '\\'); + /* At least when running under IIS, CGI programs were + * seen to use UNC filesystem paths like `\\?\c:\...`, + * which POSIX methods are not comfortable with. + * TOTHINK: Do we want to check with fopen()/fstat() + * first? At least the current "program module" named + * file should exist, right? + */ + if (buf[0] == '\\' && buf[1] == '\\') { + if (buf[2] == '?' && buf[3] == '\\' + && ( (buf[4] >= 'a' && buf[4] <= 'z') + || (buf[4] >= 'A' && buf[4] <= 'Z') ) + && buf[5] == ':' + ) { + /* Chop off `\\?\ part */ + buf_offset = 4; + upsdebugx(3, "%s: GetModuleFileName() returned '%s' which seems like localhost UNC path, chopping off the prefix to use just '%s'", + __func__, buf, buf + buf_offset); + } else { + upsdebugx(1, "%s: GetModuleFileName() returned '%s' which seems like UNC path, these are not currently supported and later calls may fail due to this", + __func__, buf); + } + } + + /* remove trailing executable name and its preceding slash */ + last_slash = strrchr(buf + buf_offset, '\\'); *last_slash = '\0'; - upsdebugx(6, "%s: buf truncated to:\t'%s'", __func__, buf); + upsdebugx(6, "%s: buf truncated to:\t'%s'", __func__, buf + buf_offset); /* no extra magic for hard-coded PATH_ETC and others, see config.h */ if (relative_path) { @@ -1782,7 +1836,7 @@ char * getfullpath(char * relative_path) ) { strncat(buf, "\\", sizeof(buf) - 1); buf_strlen++; - upsdebugx(6, "%s: buf increased to:\t'%s'", __func__, buf); + upsdebugx(6, "%s: buf increased to:\t'%s'", __func__, buf + buf_offset); } /* Is this an absolute-looking Unix-ish path string @@ -1793,7 +1847,7 @@ char * getfullpath(char * relative_path) && !strncmp(relative_path, PREFIX, len_PREFIX) && *(relative_path + len_PREFIX) == '/' ) { - char *prefix_in_buf = strstr(buf, PREFIX); + char *prefix_in_buf = strstr(buf + buf_offset, PREFIX); if (win_PREFIX == NULL) { win_PREFIX = xstrdup(PREFIX); @@ -1808,7 +1862,7 @@ char * getfullpath(char * relative_path) if (!prefix_in_buf && len_PREFIX > 1) { /* Retry: Was this path Windows-ized, * and is PREFIX non-trivial? */ - prefix_in_buf = strstr(buf, win_PREFIX); + prefix_in_buf = strstr(buf + buf_offset, win_PREFIX); } upsdebugx(6, "%s: prefix_in_buf:\t'%s'", __func__, NUT_STRARG(prefix_in_buf)); @@ -1840,7 +1894,7 @@ char * getfullpath(char * relative_path) while (depth > 0) { depth--; strncat(buf, "..\\", sizeof(buf) - 1); - upsdebugx(6, "%s: remaining depth=%d, buf increased to:\t'%s'", __func__, depth, buf); + upsdebugx(6, "%s: remaining depth=%d, buf increased to:\t'%s'", __func__, depth, buf + buf_offset); } } else { upsdebugx(6, "%s: did not find a non-trivial PREFIX followed by some slash in buf", __func__); @@ -1848,7 +1902,7 @@ char * getfullpath(char * relative_path) * single layer of directories ending with "bin". */ if (len_PREFIX == 1 && buf_strlen > 2) { - for (last_slash = buf + buf_strlen - 2; last_slash != buf; last_slash --) { + for (last_slash = buf + buf_offset + buf_strlen - 2; last_slash != buf + buf_offset; last_slash --) { if (*last_slash == '/' || *last_slash == '\\') break; } @@ -1856,7 +1910,7 @@ char * getfullpath(char * relative_path) if (last_slash && strstr(last_slash, "bin\\")) { upsdebugx(6, "%s: buf ends with 'bin\\', assume a single layer under a trivial PREFIX", __func__); strncat(buf, "..\\", sizeof(buf) - 1); - upsdebugx(6, "%s: remaining depth=0, buf increased to:\t'%s'", __func__, buf); + upsdebugx(6, "%s: remaining depth=0, buf increased to:\t'%s'", __func__, buf + buf_offset); } } } @@ -1871,14 +1925,14 @@ char * getfullpath(char * relative_path) } } - upsdebugx(6, "%s: concat '%s' and '%s'", __func__, buf, NUT_STRARG(relative_path)); + upsdebugx(6, "%s: concat '%s' and '%s'", __func__, buf + buf_offset, NUT_STRARG(relative_path)); strncat(buf, relative_path, sizeof(buf) - 1); } /* else: relative_path == NULL */ - upsdebugx(5, "%s: resulting buf:\t'%s'", __func__, buf); + upsdebugx(5, "%s: resulting buf:\t'%s'", __func__, buf + buf_offset); /* Caller should free this eventually */ - return(xstrdup(buf)); + return(xstrdup(buf + buf_offset)); } char * getfullpath2(char * cfg_path, char * fallback_path) diff --git a/conf/.gitignore b/conf/.gitignore index 10ed8bd5ce..b65c1e01a2 100644 --- a/conf/.gitignore +++ b/conf/.gitignore @@ -1,2 +1,4 @@ /upsmon.conf.sample /upssched.conf.sample +/upsstats-single.html.sample +/upsstats.html.sample diff --git a/conf/Makefile.am b/conf/Makefile.am index f0f200304c..235059cc66 100644 --- a/conf/Makefile.am +++ b/conf/Makefile.am @@ -1,25 +1,37 @@ # Network UPS Tools: conf +# (annotated sample configuration files) +# FIXME: Make use of this for SECFILES installation (hook?) INSTALL_0600 = $(INSTALL) -m 0600 -SECFILES = upsd.conf.sample upsd.users.sample -PUBFILES = nut.conf.sample ups.conf.sample -CGIPUB = hosts.conf.sample upsset.conf.sample upsstats.html.sample \ - upsstats-single.html.sample +# Note: ups.conf is a secured file, because for networked devices +# it can contain SNMP, NetXML, IPMI or similar credentials +SECFILES_STATIC = upsd.conf.sample upsd.users.sample ups.conf.sample +SECFILES_GENERATED = upsmon.conf.sample +PUBFILES_STATIC = nut.conf.sample +PUBFILES_GENERATED = upssched.conf.sample +CGIPUB_STATIC = hosts.conf.sample upsset.conf.sample +CGIPUB_GENERATED = upsstats.html.sample upsstats-single.html.sample + +SECFILES = $(SECFILES_STATIC) $(SECFILES_GENERATED) if WITH_CGI - CGI_INSTALL = $(CGIPUB) + CGI_INSTALL_STATIC = $(CGIPUB_STATIC) + CGI_INSTALL_GENERATED = $(CGIPUB_GENERATED) else CGI_INSTALL = + CGI_INSTALL_GENERATED = endif conf_examplesdir = @CONFPATH_EXAMPLES@ -dist_conf_examples_DATA = $(SECFILES) $(PUBFILES) $(CGI_INSTALL) -nodist_conf_examples_DATA = upssched.conf.sample upsmon.conf.sample +dist_conf_examples_DATA = $(SECFILES_STATIC) $(PUBFILES_STATIC) $(CGI_INSTALL_STATIC) +nodist_conf_examples_DATA = $(SECFILES_GENERATED) $(PUBFILES_GENERATED) \ + $(CGI_INSTALL_GENERATED) SPELLCHECK_SRC = $(dist_sysconf_DATA) \ - upssched.conf.sample.in upsmon.conf.sample.in + upssched.conf.sample.in upsmon.conf.sample.in \ + upsstats.html.sample.in upsstats-single.html.sample.in # NOTE: Due to portability, we do not use a GNU percent-wildcard extension. # We also have to export some variables that may be tainted by relative diff --git a/conf/upsset.conf.sample b/conf/upsset.conf.sample index 9825f787a0..2b03e29ef9 100644 --- a/conf/upsset.conf.sample +++ b/conf/upsset.conf.sample @@ -17,6 +17,13 @@ # enabling upsset in this configuration file. If you can't do this in # your web server, then you should *not* run this program. # +# Note that NUT for Windows builds may use IIS as their web server. +# The programs and configurations on the platform may have to include +# the `.exe` extension, and if you use a copy of the program and DLL +# files in its `cgi-bin` directory, you may have to somehow provide +# a `NUT_CONFPATH` environment variable to point to NUT configuration +# files located elsewhere. +# # For Apache, the .htaccess file can be used in the directory with the # programs. You'll need something like this for older versions: # diff --git a/conf/upsstats-single.html.sample b/conf/upsstats-single.html.sample.in similarity index 94% rename from conf/upsstats-single.html.sample rename to conf/upsstats-single.html.sample.in index 3a94cecce1..51b7da749d 100644 --- a/conf/upsstats-single.html.sample +++ b/conf/upsstats-single.html.sample.in @@ -1,3 +1,4 @@ +@NUT_UPSSTATS_TEMPLATE single@ @TEMPC@ -@UPSSTATSPATH upsstats.cgi@ -@UPSIMAGEPATH upsimage.cgi@ + + + +@UPSSTATSPATH upsstats.cgi@EXEEXT@@ +@UPSIMAGEPATH upsimage.cgi@EXEEXT@@ @@ -47,7 +51,7 @@ table{background:#000;} -Network UPS Tools upsstats @VERSION@ - @HOSTDESC@ - +Network UPS Tools upsstats @ESCAPED_TEMPLATE_VERSION@ - @HOSTDESC@ - @IFSUPP device.model@ @VAR device.model@ @ELSE@ @@ -314,7 +318,8 @@ on @HOST@ -
@TREELINK_JSON Device data as JSON@ | @TREELINK Device data as HTML table@ +Valid CSS @TEMPC@ -@UPSSTATSPATH upsstats.cgi@ -@UPSIMAGEPATH upsimage.cgi@ + + + +@UPSSTATSPATH upsstats.cgi@EXEEXT@@ +@UPSIMAGEPATH upsimage.cgi@EXEEXT@@ @@ -25,7 +29,7 @@ @REFRESH@ Network UPS Tools upsstats -@VERSION@ +@ESCAPED_TEMPLATE_VERSION@ : UPS Status