diff --git a/.obs/workflows.yml b/.obs/workflows.yml index 280b70ad01..013722d0f3 100644 --- a/.obs/workflows.yml +++ b/.obs/workflows.yml @@ -15,8 +15,7 @@ rebuild_master: project: home:networkupstools:nut-stable package: "%{SCM_REPOSITORY_NAME}" filters: - event: - - push + event: push branches: only: - master @@ -28,5 +27,4 @@ release_tag: source_package: "%{SCM_REPOSITORY_NAME}" target_project: home:networkupstools:nut-releases filters: - event: - - tag_push + event: tag_push diff --git a/Jenkinsfile-dynamatrix b/Jenkinsfile-dynamatrix index ed56d50bcf..d8ab890abb 100644 --- a/Jenkinsfile-dynamatrix +++ b/Jenkinsfile-dynamatrix @@ -1240,17 +1240,19 @@ set | sort -n """ ], dynamatrixAxesCommonEnv: [], dynamatrixAxesCommonEnvCartesian: [ - ['LANG=C','LC_ALL=C','TZ=UTC', 'BUILD_TYPE=default-all-errors'], + [ ['LANG=C','LC_ALL=C','TZ=UTC', 'BUILD_TYPE=default-all-errors'] ], [ ['BUILD_WARNFATAL=yes','BUILD_WARNOPT=hard'], ['BUILD_WARNFATAL=no','BUILD_WARNOPT=minimal'] ] ], allowedFailure: [ dynacfgPipeline.axisCombos_STRICT_C, + // good by now? is in DMF... // dynacfgPipeline.axisCombos_COMPILER_GCC_TOO_OLD, [~/BUILD_WARNOPT=hard/] ], runAllowedFailure: true, - mergeMode: [ 'excludeCombos': 'merge', 'dynamatrixAxesCommonEnv': 'replace' ], // NOTE: We might want to replace other fields, but excludeCombos must be merged to filter compiler versions vs language standards as centrally defined! + mergeMode: [ 'excludeCombos': 'merge', 'dynamatrixAxesCommonEnv': 'replace' ], // NOTE: We might want to replace other fields, but excludeCombos must be merged to filter compiler versions vs. language standards as centrally defined! excludeCombos: dynacfgPipeline.excludeCombos_DEFAULT_CPPUNIT + [ dynacfgPipeline.axisCombos_GNU_C, + // good by now? is in DMF... // dynacfgPipeline.axisCombos_WINDOWS_CROSS, dynacfgPipeline.axisCombos_WINDOWS ] ], body) diff --git a/Makefile.am b/Makefile.am index c02d135aa4..fef1bdb21e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -222,6 +222,7 @@ all-libs-local/include: ### (consume only one of these at a time!) ### Delivers: libcommonversion.la (only version methods) ### Delivers: libparseconf.la libnutconf.la libnutwincompat.la +### May deliver: libnutprivate-$(SEMVER)-common-all.la and libnutprivate-$(SEMVER)-common-client.la for dynamic shared-object linking, libcommonversion-private.la (dependency for the above) ### Requires-ext: include/nut_version.h ### Requires-int: libparseconf.la libcommonclient.la all-libs-local/common: all-libs-local/include @@ -234,18 +235,21 @@ all-libs-local/common: all-libs-local/include ### Requires-ext: common/libcommon.la common/libcommonclient.la ### Requires-ext: common/libcommonversion.la ### Requires-ext: common/libparseconf.la +### For dynamic builds, alternately LIB-Requires-ext and Requires-ext: libnutprivate-$(SEMVER)-common*.la ### Requires-int: libupsclient.la all-libs-local/clients: all-libs-local/common +@NUT_VERSION_H_GENERATED=true; export NUT_VERSION_H_GENERATED; \ $(SUBDIR_TGT_RULE) -### Delivers: libdummy.la libdummy_serial.la libdummy_upsdrvquery.la +### Delivers: libdummy_main.la libdummy_serial.la libdummy_upsdrvquery.la ### Delivers: libdummy_mockdrv.la libserial-nutscan.la ### LIB-Requires-ext: common/libcommon.la common/libparseconf.la ### Requires-ext: common/libcommon.la common/libparseconf.la ### Requires-ext: clients/libupsclient.la (dummy-ups only) -### Requires-int: libdummy.la libdummy_upsdrvquery.la +### Requires-int: libdummy_main.la libdummy_upsdrvquery.la ### Requires-int: libdummy_serial.la +### For dynamic builds, alternately LIB-Requires-ext and Requires-ext: libnutprivate-$(SEMVER)-common*.la +### and may deliver: libnutprivate-$(SEMVER)-drivers-common.la and libnutprivate-$(SEMVER)-drivers-serial.la all-libs-local/drivers: all-libs-local/common +@NUT_VERSION_H_GENERATED=true; export NUT_VERSION_H_GENERATED; \ $(SUBDIR_TGT_RULE) @@ -270,6 +274,7 @@ all-libs-local/tools: ### LIB-Requires-ext: drivers/libserial-nutscan.la ### LIB-Requires-ext: common/libnutwincompat.la common/libcommonstr.la ### LIB-Requires-ext: common/libcommonversion.la +### For dynamic builds, alternately LIB-Requires-ext and Requires-ext: libnutprivate-$(SEMVER)-common*.la ### HDR-Requires-ext: clients/libupsclient-version.h ### HDR-Requires-ext: nut-scanner/nutscan-snmp.h nut-scanner/nutscan-usb.h ### (generated by nut-scanner-deps/tools aliased as all-libs-local/tools) @@ -358,7 +363,7 @@ all/clients: all/common all-libs-local/clients ### Requires-ext: common/libcommon.la common/libparseconf.la ### Requires-ext: common/libcommonversion.la ### Requires-ext: clients/libupsclient.la (dummy-ups only) -### Requires-int: libdummy.la libdummy_upsdrvquery.la +### Requires-int: libdummy_main.la libdummy_upsdrvquery.la ### Requires-int: libdummy_serial.la # TODO in the future: propagate the knowledge of whether we are building @@ -528,6 +533,7 @@ DISTCHECK_CONFIGURE_FLAGS = ${DISTCHECK_FLAGS} \ --with-systemdsystempresetdir='$${prefix}/usr/lib/systemd/system-preset' \ --with-systemdshutdowndir='$${prefix}/lib/systemd/system-shutdown' \ --with-systemdtmpfilesdir='$${prefix}/usr/lib/tmpfiles.d' \ + --with-systemdsysusersdir='$${prefix}/usr/lib/sysusers.d' \ --with-augeas-lenses-dir='$${prefix}/usr/share/augeas/lenses' \ --with-hotplug-dir='$${prefix}/etc/hotplug' \ --with-udev-dir='$${prefix}/etc/udev' \ @@ -1211,7 +1217,7 @@ distcheck-completeness: dist && cd "$$am__cwd" \ && echo " $@ diff $(distdir)-orig-$$$$ $(distdir)-derived-$$$$" \ && diff $(distdir)-orig-$$$$ $(distdir)-derived-$$$$ ; \ - } || RES=$?? ; + } || RES=$$? ; \ rm -rf $(distdir)-orig-$$$$ $(distdir)-derived-$$$$ $(distdir) ; \ exit $$RES @echo "SUCCESS: $(distdir) archives are self-reproducing" @@ -1368,6 +1374,12 @@ else !WITH_SYSTEMD_TMPFILES WITH_SYSTEMD_TMPFILES = false endif !WITH_SYSTEMD_TMPFILES +if WITH_SYSTEMD_SYSUSERS +WITH_SYSTEMD_SYSUSERS = true +else !WITH_SYSTEMD_SYSUSERS +WITH_SYSTEMD_SYSUSERS = false +endif !WITH_SYSTEMD_SYSUSERS + if WITH_SYSTEMD_PRESET WITH_SYSTEMD_PRESET = true else !WITH_SYSTEMD_PRESET @@ -1519,6 +1531,10 @@ install-as-root: echo "$@: Apply systemd-tmpfiles presets" >&2 ; \ $(SYSTEMD_TMPFILES_PROGRAM) --create || exit ; \ fi ; \ + if $(WITH_SYSTEMD_SYSUSERS) ; then \ + echo "$@: Apply systemd-sysusers presets" >&2 ; \ + $(SYSTEMD_SYSUSERS_PROGRAM) || exit ; \ + fi ; \ echo "$@: Learn systemd definition changes" >&2 ; \ $(SYSTEMD_SYSTEMCTL_PROGRAM) daemon-reload || exit ; \ APPLIED_SYSTEMD_PRESET=false ; \ diff --git a/NEWS.adoc b/NEWS.adoc index 2f2df4cfaa..c30766204a 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()`, @@ -64,6 +58,7 @@ https://github.com/networkupstools/nut/milestone/12 other too. [issue #3136] * Improved high-verbosity debug tracing of NUT `libhid` and `usbhid-ups` to help make sense of data processing issues (HID paths, strings). [#3201] + * Further removal of unbounded `strcpy()` and `sprintf()` usage. [#3253] * For state tree methods, introduced `difftime_st_tree_timespec()` to abstract platform-dependent definitions of `st_tree_timespec_t`. [PR #3156] * Introduced global variables for last changed timestamp and value of @@ -80,12 +75,32 @@ https://github.com/networkupstools/nut/milestone/12 systemd watchdog situation once, will not spam more about it" even if those "logged" messages were at an invisible verbosity level. [issue #3157, PR #3151] - * 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] + * Systemd units for some daemons were revised to honour the `MODE` setting + from `nut.conf`, if available, to gracefully not-start when explicitly + 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] * 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`, @@ -142,6 +157,7 @@ https://github.com/networkupstools/nut/milestone/12 Tested on A1000 and A2000 units. [#3181] * Hides QX_FLAG_NONUT variables from syslog unless the debug level is raised. [issue #3190, PR #3198] + * Added support for battery.charge in hunnox subdriver. [#3254] - `powerp-bin` and `powerp-txt` driver updates: * Their `upsdrv_initinfo()` methods did not explicitly reference the @@ -176,6 +192,9 @@ https://github.com/networkupstools/nut/milestone/12 - `usbhid-ups` driver updates: * Declared support for APC with USB ID `051d:0005` (details may evolve as the devices become better known). [issue #3047, PR #3081] + * Extended iDowell subdriver with data points for GoldenMate UPS using + LiFePO4 batteries (tested with GoldenMate LIPO 1500VA/1000W). + [issue #3015, PR #3265] * Improved support for Eaton 5S1500LCD and 5S1600LCD (US version). [#2380] * The driver used to report `CHRG` status if the device was not "fully charged", however the latter status was only queried and reported by @@ -193,6 +212,23 @@ https://github.com/networkupstools/nut/milestone/12 ignore any useful reports, and that we successfully use reasonably many of the existing mappings. Suggest how user can help improve the driver if too few data points were seen. [#3082, #3095] + * `cps-hid` updated with Replace Battery information. [PR #3268] + * `openups-hid` had `nobattery` definition inverted, causing alarms -- now + fixed to use the same mapping helper as other subdrivers. [issue #3246] + * `powercom-hid` subdriver improved with a `powercom_hack_voltage_lkp` to + query `battery.voltage`. [issue #2766] + * Improved handling of transient `LIBUSB_ERROR_IO` failures during polling. + Some devices (CyberPower, etc.) have firmware bugs causing random I/O + errors on certain HID reports. The driver now skips failing reports and + continues polling rather than triggering expensive reconnection attempts. + True disconnections are still detected via other error codes or when all + polls fail. [issue #3116] + + - `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 @@ -200,6 +236,8 @@ https://github.com/networkupstools/nut/milestone/12 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 @@ -207,7 +245,7 @@ https://github.com/networkupstools/nut/milestone/12 - `upslog` tool updates: * Updated `help()` and failure messages to suggest `-m '*,-'` for logging - of all known local devices to stdout. [#3083] + of all known local devices to stdout. [#3083, #2756] - `upsmon` client updates: * The `SHUTDOWNEXIT` option was not handled properly, and the requested @@ -236,10 +274,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 @@ -268,7 +310,12 @@ 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: + * Added an option to `--enable-shared-private-libs` to deliver NUT common + libraries as shared objects used by different NUT binaries, rather than + linking just the used bits into each binary. This has some potential + both for mayhem (so disabled by default) and for significant reduction + of installation footprint. [issue #2800] * 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 @@ -282,6 +329,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 @@ -296,6 +355,12 @@ 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] + * Added a `--with-systemdsysusersdir=PATH` option to have systemd pre-create + the user and group accounts for NUT, before packaging or manually executed + commands do (this is particularly important for immutable-image operating + environments). [PR #3262] * Streamlined handling of `--with-*` and `--enable-*` options using NUT 'm4' macros; fixed reporting help for options whose defaults are evaluated. [issue #3049, PR #3140] @@ -308,25 +373,80 @@ 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 `drivers/Makefile.am` to not always build `libdummy_serial.la`, + but only if it is needed by serial-only or modbus drivers. Introduced + a `libdummy_usb.la` to only build USB layer sources once and share the + results between several USB-capable drivers. [#2800] + * 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] @@ -334,74 +454,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 @@ -409,23 +470,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/README.adoc b/README.adoc index c19a588222..93b80a10b5 100644 --- a/README.adoc +++ b/README.adoc @@ -894,8 +894,12 @@ endif::env-github[] See more at link:https://stories.jenkins.io/user-story/jenkins-is-the-way-for-networkupstools/[Jenkins is the way to build multi-platform NUT] article. -| image:images/ci/fosshost_org_Host_Light_38px.png[alt="Fosshost logo",width="112",height="38"] -| Fosshost provided virtual machines where the multi-platform NUT CI farm with +| image:images/ci/DO_Powered_by_Badge_blue_140pxW.png[alt="DigitalOcean logo",width="140",height="29",link="https://www.digitalocean.com/?refcode=d2fbf2b9e082&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"] +| The link:https://www.digitalocean.com/?refcode=d2fbf2b9e082&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge[DigitalOcean] + droplets allow us to host NUT CI farm Jenkins controller and the build agents + for multiple operating systems. + + They are essentially virtual machines where the multi-platform NUT CI farm with a link:https://github.com/networkupstools/jenkins-dynamatrix/[jenkins-dynamatrix] link:https://github.com/networkupstools/nut/blob/master/Jenkinsfile-dynamatrix[setup] runs to arrange builds in numerous operating environments and a lot of toolkit @@ -908,6 +912,14 @@ endif::env-github[] of operating systems, compilers, script interpreters, tools and third-party dependencies. +| image:images/ci/obs-logo.png[alt="openSUSE Build Service logo",width="140",height="62",link="https://build.opensuse.org/"] +| The several variants of + link:https://build.opensuse.org/project/show/home:networkupstools[OBS NUT + packaging project] hosted on the link:https://build.opensuse.org/[openSUSE + Build Service (OBS)] allow us to propose reference packaging recipes for a + number of Linux distributions, as well as to test NUT PR and stable branch + iterations on those across several CPU architectures not available elsewhere. + | image:images/ci/CircleCI_vertical_black_logo.png[alt="CircleCI logo",width="130",height="107",link="https://circleci.com/"] | The link:https://app.circleci.com/pipelines/github/networkupstools/nut/[CircleCI @@ -918,13 +930,16 @@ endif::env-github[] NUT pipeline] allows us to test NUT CI builds on Windows (and publish preview tarballs with binaries). -| image:images/ci/DO_Powered_by_Badge_blue_140pxW.png[alt="DigitalOcean logo",width="140",height="29",link="https://www.digitalocean.com/?refcode=d2fbf2b9e082&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"] -| The link:https://www.digitalocean.com/?refcode=d2fbf2b9e082&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge[DigitalOcean] - droplets allow us to host NUT CI farm Jenkins controller and the build agents - for multiple operating systems. +| image:images/ci/fosshost_org_Host_Light_38px.png[alt="Fosshost logo",width="112",height="38"] +| Fosshost used to provide virtual machines where the multi-platform NUT CI farm + ran, following up from a take on multi-platform builds with Travis CI while it + was free for FOSS projects. In turn, DigitalOcean helped us move on from there + after the Fosshost project went defunct. | image:images/ci/gandi-ar21.png[alt="Gandi.Net logo",width="120",height="60",link="https://www.gandi.net/"] -| link:https://www.gandi.net/[Gandi.Net] took up the costs of NUT DNS hosting. +| link:https://www.gandi.net/[Gandi.Net] ultimately took up the costs of + NUT DNS hosting, which they have provided commercially for years before + that. | image:images/ci/OC_logo_merged_140x26.png[alt="Open Collective logo",width="140",height="26",link="https://opencollective.com/"] | https://opencollective.com/networkupstools allows us to arrange monetary diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 9ca1e7b572..fdfcdabd60 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -26,6 +26,15 @@ Changes from 2.8.4 to 2.8.5 - PLANNED: Keep track of any further API clean-up? +- Added a `configure` script option to `--enable-shared-private-libs` which + allows to deliver NUT common libraries as shared objects used by different + NUT binaries, rather than linking just the used bits into each binary. + This has some potential both for mayhem (so disabled by default) and + for significant reduction of installation footprint. If enabled with + a NUT packaging build, note there would be several more NUT shared + library files to deliver with the packages (formally versioned and + named by NUT release semantic version triplet). [issue #2800] + - 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 @@ -50,6 +59,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 fe1f0c0966..e98d80d959 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -106,7 +106,16 @@ 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" - 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' + 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 WITH_LIBNUTPRIVATE=true ./ci_build.sh --with-docs=no' after_build: @@ -138,7 +147,9 @@ after_test: set MSYSTEM=MINGW64 REM Oh the joys of shell scripting with strings passed through CMD: REM Note: currently Python installation path with MSYS is buggy [#1584] - C:\msys64\usr\bin\bash -lc 'date -u; set -e ; if ! rm -rf ".inst" ; then echo "WARNING: Failed to clean away .inst" ; fi ; PATH="/mingw64/lib/ccache/bin:/mingw64/bin:$PATH" make -s -j 8 install-win-bundle DESTDIR="`pwd`/.inst/NUT-for-Windows-x86_64-SNAPSHOT-%APPVEYOR_BUILD_VERSION%" ; rm -rf ./.inst/NUT-for-Windows-x86_64-SNAPSHOT || true ; ln -fs "NUT-for-Windows-x86_64-SNAPSHOT-%APPVEYOR_BUILD_VERSION%" ./.inst/NUT-for-Windows-x86_64-SNAPSHOT ; ( cd .inst/NUT-for-Windows-x86_64-SNAPSHOT ; find . -ls ; ) ; date -u' + C:\msys64\usr\bin\bash -lc 'date -u; set -e ; if ! rm -rf ".inst" ; then echo "WARNING: Failed to clean away .inst" ; fi ; PATH="/mingw64/lib/ccache/bin:/mingw64/bin:$PATH" make -s -j 8 install-win-bundle DESTDIR="`pwd`/.inst/NUT-for-Windows-x86_64-SNAPSHOT-%APPVEYOR_BUILD_VERSION%"' + C:\msys64\usr\bin\bash -lc 'date -u; set -e ; rm -rf ./.inst/NUT-for-Windows-x86_64-SNAPSHOT || true ; ln -fs "NUT-for-Windows-x86_64-SNAPSHOT-%APPVEYOR_BUILD_VERSION%" ./.inst/NUT-for-Windows-x86_64-SNAPSHOT ; ( cd .inst/NUT-for-Windows-x86_64-SNAPSHOT ; find . -ls ; )' + C:\msys64\usr\bin\bash -lc 'date -u' cd .inst 7z a ../NUT-for-Windows-x86_64-SNAPSHOT-%APPVEYOR_BUILD_VERSION%.7z NUT* - cmd: | diff --git a/autogen.sh b/autogen.sh index 9a3fcd8aa6..89b2739135 100755 --- a/autogen.sh +++ b/autogen.sh @@ -157,6 +157,12 @@ if [ ! -f scripts/systemd/nut-common-tmpfiles.conf.in ]; then ) > scripts/systemd/nut-common-tmpfiles.conf.in fi +if [ ! -f scripts/systemd/nut-common-sysusers.conf.in ]; then + ( echo '# autoconf requires this file exists before generating configure script;' + echo '# it will be overwritten by running configure during an actual build' + ) > scripts/systemd/nut-common-sysusers.conf.in +fi + # now we can safely call autoreconf if ( command -v dos2unix ) 2>/dev/null >/dev/null ; then if ( dos2unix < configure.ac | cmp - configure.ac ) 2>/dev/null >/dev/null ; then diff --git a/ci_build.sh b/ci_build.sh index 2bde8b09ae..1d77f666fe 100755 --- a/ci_build.sh +++ b/ci_build.sh @@ -109,6 +109,8 @@ if [ "$BUILD_TYPE" = fightwarn ]; then # Similarly for testing builds with and without "unmapped" values # (normally hidden by #ifdef blocks) in certain evolving drivers #[ -n "$NUT_UNMAPPED_VARIANTS" ] || NUT_UNMAPPED_VARIANTS=auto + + #[ -n "$NUT_LIBNUTPRIVATE_VARIANTS" ] || NUT_LIBNUTPRIVATE_VARIANTS=auto fi # configure default is "no"; an "auto" value is "yes unless CFLAGS say something" @@ -1610,6 +1612,13 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al CONFIG_OPTS+=("--without-unmapped-data-points") ;; *) ;; # Keep built-in default esac + case x"${WITH_LIBNUTPRIVATE-}" in + [Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]) + CONFIG_OPTS+=("--enable-shared-pivate-libs") ;; + [Ff][Aa][Ll][Ss][Ee]|[Nn][Oo]) + CONFIG_OPTS+=("--disable-shared-pivate-libs") ;; + *) ;; # Keep built-in default + esac ;; esac @@ -2148,15 +2157,35 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al done fi + if [ -z "$NUT_LIBNUTPRIVATE_VARIANTS" ] || [ "$NUT_LIBNUTPRIVATE_VARIANTS" = auto ] || [ "$NUT_LIBNUTPRIVATE_VARIANTS" = default ] ; then + # Handle similarly to NUT_UNMAPPED_VARIANTS + NUT_LIBNUTPRIVATE_VARIANTS=() + case x"${WITH_LIBNUTPRIVATE-}" in + [Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]) + NUT_LIBNUTPRIVATE_VARIANTS+=("yes") ;; + [Ff][Aa][Ll][Ss][Ee]|[Nn][Oo]) + NUT_LIBNUTPRIVATE_VARIANTS+=("no") ;; + *) + NUT_LIBNUTPRIVATE_VARIANTS+=("yes" "no") ;; + esac + else + TMP="$NUT_LIBNUTPRIVATE_VARIANTS" + NUT_LIBNUTPRIVATE_VARIANTS=() + for VAL in $TMP ; do + NUT_LIBNUTPRIVATE_VARIANTS+=("$VAL") + done + fi + # TODO: Similar loops for other variations like TESTING, # MGE SHUT vs. other serial protocols... BUILDSTODO_SSL="${#NUT_SSL_VARIANTS[@]}" BUILDSTODO_USB="${#NUT_USB_VARIANTS[@]}" BUILDSTODO_UNMAPPED="${#NUT_UNMAPPED_VARIANTS[@]}" + BUILDSTODO_LIBNUTPRIVATE="${#NUT_LIBNUTPRIVATE_VARIANTS[@]}" - echo "=== Found ${BUILDSTODO_SSL} SSL (${NUT_SSL_VARIANTS[*]}) and ${BUILDSTODO_USB} USB (${NUT_USB_VARIANTS[*]}) and ${BUILDSTODO_UNMAPPED} UNMAPPED (${NUT_UNMAPPED_VARIANTS[*]}) variations..." - if [ x"${BUILDSTODO_SSL}${BUILDSTODO_USB}${BUILDSTODO_UNMAPPED}" = x"000" ] ; then + echo "=== Found ${BUILDSTODO_SSL} SSL (${NUT_SSL_VARIANTS[*]}) and ${BUILDSTODO_USB} USB (${NUT_USB_VARIANTS[*]}) and ${BUILDSTODO_UNMAPPED} UNMAPPED (${NUT_UNMAPPED_VARIANTS[*]}) and ${BUILDSTODO_LIBNUTPRIVATE} LIBNUTPRIVATE (${NUT_LIBNUTPRIVATE_VARIANTS[*]}) variations..." + if [ x"${BUILDSTODO_SSL}${BUILDSTODO_USB}${BUILDSTODO_UNMAPPED}${BUILDSTODO_LIBNUTPRIVATE}" = x"0000" ] ; then echo "=== ERROR: BUILD_TYPE='${BUILD_TYPE}' got no builds to run!" >&2 exit 1 fi @@ -2179,10 +2208,14 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al if [ "$BUILDSTODO_UNMAPPED" = 1 ]; then BUILDSTODO_ALWAYS="NUT_UNMAPPED_VARIANT=${NUT_UNMAPPED_VARIANTS[*]};${BUILDSTODO_ALWAYS}" fi + if [ "$BUILDSTODO_LIBNUTPRIVATE" = 1 ]; then + BUILDSTODO_ALWAYS="NUT_LIBNUTPRIVATE_VARIANT=${NUT_LIBNUTPRIVATE_VARIANTS[*]};${BUILDSTODO_ALWAYS}" + fi if [ "$BUILDSTODO_SSL" -le 1 ] \ && [ "$BUILDSTODO_USB" -le 1 ] \ && [ "$BUILDSTODO_UNMAPPED" -le 1 ] \ + && [ "$BUILDSTODO_LIBNUTPRIVATE" -le 1 ] \ ; then echo "=== NOTE: Considering at most one variant in each category, will do them all at once" BUILDSTODO_LIST+=("${BUILDSTODO_ALWAYS}") @@ -2216,6 +2249,12 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al BUILDSTODO_LIST+=("NUT_UNMAPPED_VARIANT=${VAL};${BUILDSTODO_ALWAYS}") done fi + if [ "$BUILDSTODO_LIBNUTPRIVATE" -gt 1 ]; then + for VAL in "${NUT_LIBNUTPRIVATE_VARIANTS[@]}" ; do + if [ "$VAL" = no ] ; then continue ; fi # Default setting for other builds + BUILDSTODO_LIST+=("NUT_LIBNUTPRIVATE_VARIANT=${VAL};${BUILDSTODO_ALWAYS}") + done + fi else # Try to mix into the smallest amount of builds (randomize # the mix so we can cover many scenarios as different CI @@ -2232,6 +2271,9 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al [ "$BUILDSTODO_MAX" -ge "$BUILDSTODO_UNMAPPED" ] \ || { BUILDSTODO_MAX="$BUILDSTODO_UNMAPPED"; BUILDSTODO_MAX_TYPE="BUILDSTODO_UNMAPPED" ; } + [ "$BUILDSTODO_MAX" -ge "$BUILDSTODO_LIBNUTPRIVATE" ] \ + || { BUILDSTODO_MAX="$BUILDSTODO_LIBNUTPRIVATE"; BUILDSTODO_MAX_TYPE="BUILDSTODO_LIBNUTPRIVATE" ; } + # FIXME: Can this be eval'ed? # First populate the longer set of variants: case "${BUILDSTODO_MAX_TYPE}" in @@ -2250,6 +2292,11 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al BUILDSTODO_LIST+=("NUT_UNMAPPED_VARIANT=${VAL};${BUILDSTODO_ALWAYS}") done ;; + BUILDSTODO_LIBNUTPRIVATE) + for VAL in "${NUT_LIBNUTPRIVATE_VARIANTS[@]}" ; do + BUILDSTODO_LIST+=("NUT_LIBNUTPRIVATE_VARIANT=${VAL};${BUILDSTODO_ALWAYS}") + done + ;; esac case "${BUILDSTODO_MAX_TYPE}" in @@ -2267,6 +2314,13 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al BUILDSTODO_LIST[$i]="NUT_UNMAPPED_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) done + + i=$(($RANDOM % $BUILDSTODO_MAX)) + [ "$BUILDSTODO_LIBNUTPRIVATE" -le 1 ] || \ + for VAL in "${NUT_LIBNUTPRIVATE_VARIANTS[@]}" ; do + BUILDSTODO_LIST[$i]="NUT_LIBNUTPRIVATE_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" + i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) + done ;; BUILDSTODO_USB) i=$(($RANDOM % $BUILDSTODO_MAX)) @@ -2282,6 +2336,13 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al BUILDSTODO_LIST[$i]="NUT_UNMAPPED_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) done + + i=$(($RANDOM % $BUILDSTODO_MAX)) + [ "$BUILDSTODO_LIBNUTPRIVATE" -le 1 ] || \ + for VAL in "${NUT_LIBNUTPRIVATE_VARIANTS[@]}" ; do + BUILDSTODO_LIST[$i]="NUT_LIBNUTPRIVATE_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" + i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) + done ;; BUILDSTODO_UNMAPPED) i=$(($RANDOM % $BUILDSTODO_MAX)) @@ -2297,6 +2358,35 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al BUILDSTODO_LIST[$i]="NUT_USB_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) done + + i=$(($RANDOM % $BUILDSTODO_MAX)) + [ "$BUILDSTODO_LIBNUTPRIVATE" -le 1 ] || \ + for VAL in "${NUT_LIBNUTPRIVATE_VARIANTS[@]}" ; do + BUILDSTODO_LIST[$i]="NUT_LIBNUTPRIVATE_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" + i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) + done + ;; + BUILDSTODO_LIBNUTPRIVATE) + i=$(($RANDOM % $BUILDSTODO_MAX)) + [ "$BUILDSTODO_SSL" -le 1 ] || \ + for VAL in "${NUT_SSL_VARIANTS[@]}" ; do + BUILDSTODO_LIST[$i]="NUT_SSL_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" + i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) + done + + i=$(($RANDOM % $BUILDSTODO_MAX)) + [ "$BUILDSTODO_USB" -le 1 ] || \ + for VAL in "${NUT_USB_VARIANTS[@]}" ; do + BUILDSTODO_LIST[$i]="NUT_USB_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" + i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) + done + + i=$(($RANDOM % $BUILDSTODO_MAX)) + [ "$BUILDSTODO_UNMAPPED" -le 1 ] || \ + for VAL in "${NUT_UNMAPPED_VARIANTS[@]}" ; do + BUILDSTODO_LIST[$i]="NUT_UNMAPPED_VARIANT=${VAL};${BUILDSTODO_LIST[$i]}" + i=$(( $(($i + 1)) % $BUILDSTODO_MAX)) + done ;; esac fi @@ -2317,6 +2407,7 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al NUT_SSL_VARIANT="" NUT_USB_VARIANT="" NUT_UNMAPPED_VARIANT="" + NUT_LIBNUTPRIVATE_VARIANT="" eval $TESTCOMBO echo "=== Starting 'TESTCOMBO=${TESTCOMBO}', ${BUILDSTODO} build variants remaining..." @@ -2406,6 +2497,26 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-al ;; esac + case "${NUT_LIBNUTPRIVATE_VARIANT}" in + "") ;; + yes|no) # Try this variant + echo "=== Building with 'NUT_LIBNUTPRIVATE_VARIANT=${NUT_LIBNUTPRIVATE_VARIANT}' ..." + if [ "${NUT_SSL_VARIANTS[*]}" != "auto" ] && [ x"${NUT_SSL_VARIANT}" = x ] ; then + CONFIG_OPTS+=("--without-all") + CONFIG_OPTS+=("--without-ssl") + fi + CONFIG_OPTS+=("--with-serial=auto") + if [ "${NUT_USB_VARIANTS[*]}" != "no" ] && [ x"${NUT_USB_VARIANT}" = x ] ; then + CONFIG_OPTS+=("--with-usb=auto") + fi + CONFIG_OPTS+=("--enable-shared-private-libs=${NUT_LIBNUTPRIVATE_VARIANT}") + ;; + *) # Potentially something new? Unknown values can fail in the configure script. + echo "=== Building with 'NUT_LIBNUTPRIVATE_VARIANT=${NUT_LIBNUTPRIVATE_VARIANT}' (WARNING: may be not supported)..." + CONFIG_OPTS+=("--enable-shared-private-libs=${NUT_LIBNUTPRIVATE_VARIANT}") + ;; + esac + # Snippet from autogen.sh: restore files required by autoconf # for non-"foreign" projects that a deep clean in other loops # could have destroyed: @@ -2739,6 +2850,10 @@ bindings) CONFIG_OPTS+=("--with-unmapped-data-points=yes-if-not-DMF") fi; fi + if [ x"${WITH_LIBNUTPRIVATE-}" = xtrue ] ; then + CONFIG_OPTS+=("--enable-shared-private-libs") + fi + if [ -n "${BUILD_DEBUGINFO-}" ]; then CONFIG_OPTS+=("--with-debuginfo=${BUILD_DEBUGINFO}") else @@ -2877,6 +2992,13 @@ cross-windows-mingw*) ;; esac + if [ x"${WITH_LIBNUTPRIVATE-}" = x ] ; then + # For Windows we want compact builds + # (they reach into gigabytes anyway): + WITH_LIBNUTPRIVATE=true + fi # else we have some value from caller + export WITH_LIBNUTPRIVATE + SOURCEMODE="out-of-tree" \ MAKEFLAGS="$PARMAKE_FLAGS" \ KEEP_NUT_REPORT_FEATURE="true" \ diff --git a/clients/Makefile.am b/clients/Makefile.am index 36fc2be1bf..f9e4450fb8 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -15,9 +15,10 @@ CLEANFILES = # nutclient.cpp for some legacy reason (maybe initial detached development?) # optionally includes "common.h" with the NUT build setup - and this option # was never triggered in fact, not until pushed through command line like this: -AM_CXXFLAGS = -DHAVE_NUTCOMMON=1 -I$(top_srcdir)/include +AM_CXXFLAGS = -DHAVE_NUTCOMMON=1 -I$(top_builddir)/include -I$(top_srcdir)/include # Make sure out-of-dir dependencies exist (especially when dev-building parts): +$(top_builddir)/include/nut_version.h \ $(top_builddir)/common/libcommon.la \ $(top_builddir)/common/libcommonclient.la \ $(top_builddir)/common/libcommonversion.la \ @@ -30,19 +31,46 @@ $(top_builddir)/common/libparseconf.la: dummy # (sub-makes are independent as far as trying to write into same files): $(top_builddir)/common/libcommon.la: $(top_builddir)/common/libparseconf.la $(top_builddir)/common/libcommonclient.la: $(top_builddir)/common/libparseconf.la +$(top_builddir)/common/libcommonversion.la: $(top_builddir)/include/nut_version.h -LDADD_FULL = \ - $(top_builddir)/common/libcommon.la \ +LDADD_FULL = +LDADD_CLIENT = + +if ENABLE_SHARED_PRIVATE_LIBS +$(top_builddir)/common/libcommonversion-private.la \ +$(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la \ +$(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la: dummy + +@cd $(@D) && $(MAKE) $(AM_MAKEFLAGS) $(@F) + +$(top_builddir)/common/libcommonversion-private.la: $(top_builddir)/include/nut_version.h +$(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la: $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonversion-private.la +$(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/common/libcommonversion-private.la + +LDADD_FULL += \ + $(top_builddir)/common/libcommonversion.la \ + $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la + +LDADD_CLIENT += \ $(top_builddir)/common/libcommonversion.la \ + $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la +else !ENABLE_SHARED_PRIVATE_LIBS +LDADD_FULL += \ + $(top_builddir)/common/libcommon.la \ + $(top_builddir)/common/libcommonversion.la + +LDADD_CLIENT += \ + $(top_builddir)/common/libcommonclient.la \ + $(top_builddir)/common/libcommonversion.la +endif !ENABLE_SHARED_PRIVATE_LIBS + +LDADD_FULL += \ libupsclient.la \ $(NETLIBS) if WITH_SSL LDADD_FULL += $(LIBSSL_LIBS) $(LIBSSL_LDFLAGS_RPATH) endif WITH_SSL -LDADD_CLIENT = \ - $(top_builddir)/common/libcommonclient.la \ - $(top_builddir)/common/libcommonversion.la \ +LDADD_CLIENT += \ libupsclient.la \ $(NETLIBS) if WITH_SSL @@ -56,7 +84,7 @@ LDADD = $(LDADD_CLIENT) # Avoid per-target CFLAGS, because this will prevent re-use of object # files. In any case, CFLAGS are only -I options, so there is no harm, # but only add them if we really use the target. -AM_CFLAGS = -I$(top_srcdir)/include +AM_CFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include if WITH_SSL AM_CFLAGS += $(LIBSSL_CFLAGS) endif WITH_SSL @@ -93,31 +121,48 @@ endif WITH_CGI upsc_SOURCES = upsc.c upsclient.h upsc_LDADD = $(LDADD_CLIENT) $(top_builddir)/common/libcommonstrjson.la + upscmd_SOURCES = upscmd.c upsclient.h + upsrw_SOURCES = upsrw.c upsclient.h + upslog_SOURCES = upslog.c upsclient.h upslog.h upslog_LDADD = $(LDADD_FULL) + upsmon_SOURCES = upsmon.c upsmon.h upsclient.h upsmon_LDADD = $(LDADD_FULL) + if HAVE_WINDOWS_SOCKETS message_SOURCES = message.c endif HAVE_WINDOWS_SOCKETS upssched_SOURCES = upssched.c upssched.h -upssched_LDADD = \ +upssched_LDADD = +if ENABLE_SHARED_PRIVATE_LIBS +upssched_LDADD += \ + $(top_builddir)/common/libcommonversion.la \ + $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la +else !ENABLE_SHARED_PRIVATE_LIBS +upssched_LDADD += \ $(top_builddir)/common/libcommonclient.la \ $(top_builddir)/common/libcommonversion.la \ - $(top_builddir)/common/libparseconf.la \ + $(top_builddir)/common/libparseconf.la +endif !ENABLE_SHARED_PRIVATE_LIBS + +upssched_LDADD += \ $(NETLIBS) upsimage_cgi_SOURCES = upsimage.c upsclient.h upsimagearg.h cgilib.c cgilib.h upsimage_cgi_LDADD = $(LDADD) $(LIBGD_LDFLAGS) upsset_cgi_SOURCES = upsset.c upsclient.h cgilib.c cgilib.h + upsstats_cgi_SOURCES = upsstats.c upsclient.h status.h upsstats.h \ upsimagearg.h cgilib.c cgilib.h upsstats_cgi_LDADD = $(LDADD_CLIENT) $(top_builddir)/common/libcommonstrjson.la +################################## Plain C client library (libupsclient) : + # not LDADD... why? libupsclient_la_SOURCES = upsclient.c upsclient.h # NOTE: The library does not require libcommonversion.la @@ -185,6 +230,8 @@ libupsclient-version.h: libupsclient.la rm -f "$@.tmp.$$$$" ; \ exit $$RES +################################## C++ client library (libnutclient) : + if HAVE_CXX11 # libnutclient version information and build libnutclient_la_SOURCES = nutclient.h nutclient.cpp 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/upsc.c b/clients/upsc.c index 764811fa87..d8801f7496 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -397,6 +397,10 @@ int main(int argc, char **argv) } upsdebugx(1, "Starting NUT client: %s", prog); +#if (defined NUT_PLATFORM_AIX) && (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS + callback_upsconf_args = do_upsconf_args; +#endif + while ((i = getopt(argc, argv, "+hlLcVW:j")) != -1) { switch (i) @@ -492,6 +496,6 @@ int main(int argc, char **argv) /* Formal do_upsconf_args implementation to satisfy linker on AIX */ #if (defined NUT_PLATFORM_AIX) void do_upsconf_args(char *upsname, char *var, char *val) { - fatalx(EXIT_FAILURE, "INTERNAL ERROR: formal do_upsconf_args called"); + fatalx(EXIT_FAILURE, "INTERNAL ERROR: formal do_upsconf_args called"); } #endif /* end of #if (defined NUT_PLATFORM_AIX) */ diff --git a/clients/upscmd.c b/clients/upscmd.c index 2c5fe6ec03..47efe29161 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -308,6 +308,10 @@ int main(int argc, char **argv) } upsdebugx(1, "Starting NUT client: %s", prog); +#if (defined NUT_PLATFORM_AIX) && (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS + callback_upsconf_args = do_upsconf_args; +#endif + while ((i = getopt(argc, argv, "+lhu:p:t:wVW:")) != -1) { switch (i) 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/upslog.c b/clients/upslog.c index 66e99b8b0c..16b5791cd4 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -218,6 +218,8 @@ static void help(const char *prog) printf(" and it would not imply foregrounding\n"); printf(" - Unlike one '-s ups -l file' spec, you can specify many tuples\n"); printf(" - Example: -m '*,-' to view updates of all known local devices\n"); + printf(" - Example: -m '*@1.2.3.4,-' to view updates of all known remote\n"); + printf(" devices served by NUT data server with IP address 1.2.3.4\n"); printf(" -u - Switch to if started as root\n"); printf("\nCommon arguments:\n"); printf(" -V - display the version of this software\n"); @@ -518,6 +520,10 @@ int main(int argc, char **argv) logformat = DEFAULT_LOGFORMAT; user = RUN_AS_USER; +#if (defined NUT_PLATFORM_AIX) && (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS + callback_upsconf_args = do_upsconf_args; +#endif + print_banner_once(prog, 0); while ((i = getopt(argc, argv, "+hDs:l:i:d:Nf:u:Vp:FBm:W:")) != -1) { @@ -1025,6 +1031,6 @@ int main(int argc, char **argv) /* Formal do_upsconf_args implementation to satisfy linker on AIX */ #if (defined NUT_PLATFORM_AIX) void do_upsconf_args(char *upsname, char *var, char *val) { - fatalx(EXIT_FAILURE, "INTERNAL ERROR: formal do_upsconf_args called"); + fatalx(EXIT_FAILURE, "INTERNAL ERROR: formal do_upsconf_args called"); } #endif /* end of #if (defined NUT_PLATFORM_AIX) */ diff --git a/clients/upsmon.c b/clients/upsmon.c index 983c6e57e2..c349b193b5 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -233,19 +233,21 @@ static void wall(const char *text) pclose(wf); #else /* WIN32 */ # define MESSAGE_CMD "message.exe" - char * command; + char *command; /* first +1 is for the space between message and text second +1 is for trailing 0 +2 is for "" */ - command = malloc (strlen(MESSAGE_CMD) + 1 + 2 + strlen(text) + 1); - if( command == NULL ) { + size_t commandsz = strlen(MESSAGE_CMD) + 1 + 2 + strlen(text) + 1; + + command = malloc (commandsz); + if (command == NULL) { upslog_with_errno(LOG_NOTICE, "Not enough memory for wall"); return; } - sprintf(command,"%s \"%s\"",MESSAGE_CMD,text); - if ( system(command) != 0 ) { + snprintf(command, commandsz, "%s \"%s\"", MESSAGE_CMD, text); + if (system(command) != 0) { upslog_with_errno(LOG_NOTICE, "Can't invoke wall"); } free(command); diff --git a/clients/upsrw.c b/clients/upsrw.c index dc76c9336b..79c6ffa87e 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -666,6 +666,10 @@ int main(int argc, char **argv) } upsdebugx(1, "Starting NUT client: %s", prog); +#if (defined NUT_PLATFORM_AIX) && (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS + callback_upsconf_args = do_upsconf_args; +#endif + while ((i = getopt(argc, argv, "+hls:p:t:u:wVW:")) != -1) { switch (i) { 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..4ec650c451 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; - strcpy(var, s); + upsdebug_call_starting_for_str1(s); + + strncpy(var, s, sizeof(var) - 1); 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; - strcpy(var, s); + upsdebug_call_starting_for_str1(s); + + strncpy(var, s, sizeof(var) - 1); 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/Makefile.am b/common/Makefile.am index 464a9e0888..3064f4fc09 100644 --- a/common/Makefile.am +++ b/common/Makefile.am @@ -15,9 +15,42 @@ AM_LDFLAGS = -no-undefined EXTRA_DIST = CLEANFILES = +# Track unique NUT implementation-detail libraries that are commonly +# used in different programs, and may get bundled into a shared lib. +# Note that for rebuild-time optimization with static libraries, we +# have historically used a number of source-file selections which may +# overlap (e.g. libcommon includes libcommonclient and some more). noinst_LTLIBRARIES = libparseconf.la libcommon.la libcommonclient.la lib_LTLIBRARIES = +if ENABLE_SHARED_PRIVATE_LIBS +### Hopefully we can bundle this code together and +### install this somewhat away from the public eye?.. +### We still do build static (noinst) libs to include +### them into shared library files that should remain +### compatible for existing consumers (in/out of tree). + lib_LTLIBRARIES += libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_all_la_SOURCES = + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_all_la_LIBADD = libcommon.la libcommonversion-private.la + + lib_LTLIBRARIES += libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_client_la_SOURCES = + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_client_la_LIBADD = libcommonclient.la libcommonversion-private.la + +### Avoid unexpected linking across NUT generations +### far apart (e.g. custom rebuilds of new programs +### tucked into an existing deployment); see libtool +### docs about current:revision:age interface numbering. + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_all_la_LDFLAGS = -version-info 1:0:0 + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_client_la_LDFLAGS = -version-info 1:0:0 + +if HAVE_WINDOWS + # Many versions of MingW seem to fail to build non-static DLL without this + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_all_la_LDFLAGS += -no-undefined + libnutprivate_@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@_common_client_la_LDFLAGS += -no-undefined +endif HAVE_WINDOWS +endif ENABLE_SHARED_PRIVATE_LIBS + # We define the recipe below in any case, but only activate it by default # if the build configuration tells us to: if WITH_DEV_LIBNUTCONF @@ -93,6 +126,14 @@ libcommonversion_la_SOURCES = common-nut_version.c #libcommonversion_la_CFLAGS = $(AM_CFLAGS) -DWITHOUT_LIBSYSTEMD=1 #libcommonversion_la_LIBADD = @LTLIBOBJS@ @BSDKVMPROCLIBS@ +if ENABLE_SHARED_PRIVATE_LIBS +# A special build of the above for linking into shared private libs, +# so they also know about the NUT version they were built from + noinst_LTLIBRARIES += libcommonversion-private.la + libcommonversion_private_la_SOURCES = common-nut_version.c + libcommonversion_private_la_CFLAGS = $(AM_CFLAGS) -DBUILD_FOR_SHARED_PRIVATE_LIBS=1 +endif + # Eventually we expect more sources as the big common.c source gets split # into smaller better-focused files. COMMON_SRC = \ diff --git a/common/common-nut_version.c b/common/common-nut_version.c index defb8aedbf..461b7515df 100644 --- a/common/common-nut_version.c +++ b/common/common-nut_version.c @@ -2,7 +2,15 @@ (extracted from common.c to minimize the compilation unit impacted by git metadata changes during development) - Copyright (C) 2021-2025 Jim Klimov + WARNING: Be conservative about ABI/API changes here, the method + signatures are specified in drivers/main.h, so that shared driver + core code may be loaded as a dynamic library and use these data + and methods built into a driver binary via callbacks - they must + fit! While it is not feasible to satisfy all possible scenarios + of third-party fork builds like this, we don't want to make life + hard needlessly either. + + Copyright (C) 2021-2026 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 @@ -19,6 +27,26 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include "config.h" + +#if (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS +# if (defined BUILD_FOR_SHARED_PRIVATE_LIBS) && BUILD_FOR_SHARED_PRIVATE_LIBS +/* Special build that can be included into a libnutprivate* shared object, + * so it carries strings to identify the build separate from a program + * binary, while avoiding a naming conflict between the two at run-time. + */ +# define UPS_VERSION LIBNUTPRIVATE_UPS_VERSION +# define banner_is_disabled LIBNUTPRIVATE_banner_is_disabled +# define describe_NUT_VERSION_once LIBNUTPRIVATE_describe_NUT_VERSION_once +# define print_banner_once LIBNUTPRIVATE_print_banner_once +# define nut_report_config_flags LIBNUTPRIVATE_nut_report_config_flags +# define suggest_doc_links LIBNUTPRIVATE_suggest_doc_links +# define suggest_NDE_conflict LIBNUTPRIVATE_suggest_NDE_conflict +# else /* !BUILD_FOR_SHARED_PRIVATE_LIBS => for leaf binary */ + extern const char *LIBNUTPRIVATE_UPS_VERSION; +# endif /* !BUILD_FOR_SHARED_PRIVATE_LIBS */ +#endif + #include "common.h" #include #include @@ -91,6 +119,7 @@ const char *describe_NUT_VERSION_once(void) { static char buf[LARGEBUF]; static const char *printed = NULL; + int ret; if (printed) return printed; @@ -111,7 +140,7 @@ const char *describe_NUT_VERSION_once(void) * NUT_VERSION_IS_RELEASE make one of codepaths unreachable in * a particular build. So we pragmatically handwave this away. */ - if (1 < snprintf(buf, sizeof(buf), + ret = snprintf(buf, sizeof(buf), "%s %s%s%s", NUT_VERSION_MACRO, NUT_VERSION_IS_RELEASE ? "release" : @@ -120,12 +149,41 @@ const char *describe_NUT_VERSION_once(void) : "(development iteration after "), NUT_VERSION_IS_RELEASE ? "" : NUT_VERSION_SEMVER_MACRO, NUT_VERSION_IS_RELEASE ? "" : ")" - )) { + ); + + /* Depending on LIBC variant, truncation can be seen as either a + * negative ret, or longer than the buffer (would have written X); + * with our formatting string we expect at least 8 charsof text, + * plus the actual version string. + */ + if (ret > 8 && (size_t)ret < sizeof(buf)) { +#if (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS && ( !(defined BUILD_FOR_SHARED_PRIVATE_LIBS) || !BUILD_FOR_SHARED_PRIVATE_LIBS) + /* "Leaf" program using a dynamically linked libnutprivate core */ + size_t len1 = (size_t)ret; /* Number of printed chars without '\0' */ + int samever = !strcmp(UPS_VERSION, LIBNUTPRIVATE_UPS_VERSION); + + ret = snprintf(buf + len1, sizeof(buf) - len1, + " using a dynamic libnutprivate*%s%s", + samever ? "" : " version ", + samever ? "" : LIBNUTPRIVATE_UPS_VERSION + ); + + if (ret < 10 || (size_t)ret > (sizeof(buf) - len1)) { + /* Too long? forget it... */ + upsdebugx(1, "%s: could not report about a dynamic libnutprivate", __func__); + buf[len1 + 1] = '\0'; + } +#endif + printed = buf; - } else { + } + + if (!printed) { upslogx(LOG_WARNING, "%s: failed to report detailed NUT version", __func__); + /* Just pass on the built-in const string */ printed = UPS_VERSION; } + #ifdef __clang__ #pragma clang diagnostic pop #endif 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/common/dmfsnmp.c b/common/dmfsnmp.c index f75e494c56..1fe2a6c489 100644 --- a/common/dmfsnmp.c +++ b/common/dmfsnmp.c @@ -8,7 +8,7 @@ * Copyright (C) 2016 Michal Vyskocil * Copyright (C) 2016 - 2021 Jim Klimov * Copyright (C) 2019 Arnaud Quette - * Copyright (C) 2024 Jim Klimov + * Copyright (C) 2024-2026 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 @@ -1518,14 +1518,18 @@ mibdmf_xml_cdata_cb(void *userdata, int state, const char *cdata, size_t len) { if(!function_text) { - function_text = (char*) calloc(len + 2, sizeof(char)); - sprintf(function_text, "%.*s\n", (int) len, cdata); + size_t fun_bufsz = (len + 2) * sizeof(char); + + function_text = (char*) calloc(fun_bufsz, 1); + snprintf(function_text, fun_bufsz, "%.*s\n", (int) len, cdata); } else { - char *aux_str; - function_text = (char*) realloc(function_text, (strlen(function_text) + len + 2) * sizeof(char)); - aux_str = (char*) calloc(len + 2, sizeof(char)); - sprintf(aux_str, "%.*s\n", (int) len, cdata); - strcat(function_text, aux_str); + size_t aux_bufsz = len + 2, fun_bufsz = (strlen(function_text) + len + 2) * sizeof(char); + char *aux_str; + + function_text = (char*) realloc(function_text, fun_bufsz); + aux_str = (char*) calloc(aux_bufsz, sizeof(char)); + snprintf(aux_str, aux_bufsz, "%.*s\n", (int) len, cdata); + strncat(function_text, aux_str, fun_bufsz); free(aux_str); } } diff --git a/common/setenv.c b/common/setenv.c index f9e9c77a86..2a275a59e4 100644 --- a/common/setenv.c +++ b/common/setenv.c @@ -1,4 +1,6 @@ -/* setenv.c Ben Collver */ +/* fallback setenv.c Ben Collver + * tightened by Jim Klimov + */ #include "config.h" /* must be first */ #ifndef HAVE_SETENV @@ -10,6 +12,7 @@ int nut_setenv(const char *name, const char *value, int overwrite) { char *val; char *buffer; + size_t buflen = 0; int rv; if (overwrite == 0) { @@ -19,10 +22,15 @@ int nut_setenv(const char *name, const char *value, int overwrite) } } - buffer = xmalloc(strlen(value) + strlen(name) + 2); - strcpy(buffer, name); - strcat(buffer, "="); - strcat(buffer, value); + buflen = strlen(value) + strlen(name) + 2; + buffer = xmalloc(buflen); + /* TOTHINK: is this stack more portable than one command? + * snprintf(buffer, buflen, "%s=%s", name, value); + * (also can easily check that we got (buflen-1) as return value) + */ + strncpy(buffer, name, buflen); + strncat(buffer, "=", buflen); + strncat(buffer, value, buflen); rv = putenv(buffer); /* man putenv, do not free(buffer) */ return (rv); } diff --git a/common/upsconf.c b/common/upsconf.c index 5e3ddda250..b2e8fe273d 100644 --- a/common/upsconf.c +++ b/common/upsconf.c @@ -1,6 +1,7 @@ /* upsconf.c - code for handling ups.conf ini-style parsing Copyright (C) 2001 Russell Kroll + 2026 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 @@ -30,12 +31,24 @@ static char *ups_section; + void (*callback_upsconf_args)(char *upsname, char *var, char *val) = NULL; + /* handle arguments separated by parseconf */ static void conf_args(size_t numargs, char **arg) { if (numargs < 1) return; + if (callback_upsconf_args == NULL) { +#if (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS + upsdebugx(1, "%s: coding error: when building NUT with ENABLE_SHARED_PRIVATE_LIBS mode, 'callback_upsconf_args' must be initialized early in ultimate program code", __func__); + fatalx(EXIT_FAILURE, "FATAL: Dynamic consumer of a NUT private library was not initialized correctly"); +#else + /* We should see the original method in the binary in statically linked scope */ + callback_upsconf_args = do_upsconf_args; +#endif + } + /* look for section headers - [upsname] */ if ((arg[0][0] == '[') && (arg[0][strlen(arg[0])-1] == ']')) { @@ -48,7 +61,7 @@ static void conf_args(size_t numargs, char **arg) /* handle 'foo' (flag) */ if (numargs == 1) { - do_upsconf_args(ups_section, arg[0], NULL); + callback_upsconf_args(ups_section, arg[0], NULL); return; } @@ -57,7 +70,7 @@ static void conf_args(size_t numargs, char **arg) /* handle 'foo = bar', 'foo=bar', 'foo =bar' or 'foo= bar' forms */ if (!strcmp(arg[1], "=")) { - do_upsconf_args(ups_section, arg[0], arg[2]); + callback_upsconf_args(ups_section, arg[0], arg[2]); return; } } @@ -68,7 +81,7 @@ static void upsconf_err(const char *errmsg) upslogx(LOG_ERR, "Fatal error in parseconf(ups.conf): %s", errmsg); } -/* open the ups.conf, parse it, and call back do_upsconf_args() +/* open the ups.conf, parse it, and call back callback_upsconf_args() * returns -1 (or aborts the program) in case of errors; * returns 1 if processing finished successfully * See also reload_flag support in main.c for live-reload feature 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/nut.conf.sample b/conf/nut.conf.sample index 48b733267f..fd79043ff7 100644 --- a/conf/nut.conf.sample +++ b/conf/nut.conf.sample @@ -56,8 +56,8 @@ MODE=none # default flag that can be set in `upsd.conf`. If you want a data server always # running, even if it initially has nothing to serve (may be live-reloaded # later, when devices become configured), this option is for you. -ALLOW_NO_DEVICE=true -export ALLOW_NO_DEVICE +#ALLOW_NO_DEVICE=true +#export ALLOW_NO_DEVICE # Uncomment this to allow starting the `upsd` data server even if not all # `LISTEN` directives can be honoured at the moment. This environment variable 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