From b47cf64295b7d447a2de0b4f0d91beb46ed6afe7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 09:24:10 +0100 Subject: [PATCH 01/29] NEWS.adoc: Adjusted C++ header search path on Termux [#3353] Signed-off-by: Jim Klimov --- NEWS.adoc | 2 ++ docs/nut.dict | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.adoc b/NEWS.adoc index e308d6fc79..fffede2b59 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -502,6 +502,8 @@ several `FSD` notifications into one executed action. [PR #3097] * Added an option to (primarily) `--disable-threading` for systems with detected but broken `libpthread` support, or to test alternate code paths during development or in CI. [#3300] + * Adjusted C++ header search path on Termux when `-isystem` is involved, + as noted with NSS builds. [issues #1599, #1711, PR #3353] - Recipes, CI and helper script updates not classified above: * Fixed CI recipes for PyPI publication of PyNUT(Client) module to also diff --git a/docs/nut.dict b/docs/nut.dict index e830687f99..19e02ba4e0 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3705 utf-8 +personal_ws-1.1 en 3706 utf-8 AAC AAS ABI @@ -2409,6 +2409,7 @@ isbmex ish iso isolator +isystem iter ivp ivtscd From b3c171d1dcbd293c8edbe6c7b8f39e0ef4f89be7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 09:23:42 +0100 Subject: [PATCH 02/29] NEWS.adoc: fix typo Signed-off-by: Jim Klimov --- NEWS.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.adoc b/NEWS.adoc index fffede2b59..5eead11d4a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -327,7 +327,7 @@ https://github.com/networkupstools/nut/milestone/12 - `upsd` data server updates: * Sometimes "Data for UPS [X] is stale" and "UPS [X] data is no longer - stale" messages were logged in the same second, especially no busy + stale" messages were logged in the same second, especially on busy systems. Now we allow one more second on top of `MAXAGE` setting to declare the device dead, just in case fractional/whole second rounding comes into play and breaks things. [issue #661] From 704bf17dfdd2212a2a2b8cbb35398d9106b38c0d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 21:50:34 +0100 Subject: [PATCH 03/29] conf/ups.conf.sample: clarify run-time group nuances [#3356] Signed-off-by: Jim Klimov --- conf/ups.conf.sample | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conf/ups.conf.sample b/conf/ups.conf.sample index 4d04554001..bbf508751e 100644 --- a/conf/ups.conf.sample +++ b/conf/ups.conf.sample @@ -131,7 +131,9 @@ maxretry = 3 # user, group: OPTIONAL. Overrides the compiled-in (also global-section, # when used in driver section) default unprivileged user/group # name for NUT device driver. Impacts access rights used for -# the socket file access (group) and communication ports (user). +# the socket file access (group) and communication ports (user +# and its default group; you may want to add that user in the +# operating system to `dialout` group to access serial ports). # # synchronous: OPTIONAL. The driver work by default in asynchronous # mode (like *no*) with fallback to synchronous if sending From 0faed3a3caba2eec5f8a37303100511cf7e04e59 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 16:49:21 +0100 Subject: [PATCH 04/29] configure.ac, clients/Makefile.am, tests/Makefile.am: refactor with LIBSSL_CXXFLAGS [#1711, #1599] Signed-off-by: Jim Klimov --- clients/Makefile.am | 2 +- configure.ac | 6 ++++-- tests/Makefile.am | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/clients/Makefile.am b/clients/Makefile.am index 36a59ec8b4..94c6d6c255 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -17,7 +17,7 @@ CLEANFILES = # was never triggered in fact, not until pushed through command line like this: AM_CXXFLAGS = -DHAVE_NUTCOMMON=1 -I$(top_builddir)/include -I$(top_srcdir)/include if WITH_SSL - AM_CXXFLAGS += $(LIBSSL_CFLAGS) + AM_CXXFLAGS += $(LIBSSL_CXXFLAGS) endif WITH_SSL # Make sure out-of-dir dependencies exist (especially when dev-building parts): diff --git a/configure.ac b/configure.ac index b29c5d5566..5fdc1f0b29 100644 --- a/configure.ac +++ b/configure.ac @@ -7063,6 +7063,7 @@ AC_MSG_RESULT([C: ${nut_with_debuginfo_C}; C++: ${nut_with_debuginfo_CXX}]) dnl Only in the end, do you understand... AC_SUBST(CPPUNIT_NUT_CXXFLAGS) +LIBSSL_CXXFLAGS="${LIBSSL_CFLAGS}" AS_IF([test "${have_cxx11}" = yes && test x"${TERMUX__PREFIX-}" != x && test -d "${TERMUX__PREFIX-}"], [ dnl There is a bit of trouble with third-party dependencies whose pkgconf dnl files or our own code add `-isystem` entries (to avoid warnings about @@ -7093,7 +7094,7 @@ AS_IF([test "${have_cxx11}" = yes && test x"${TERMUX__PREFIX-}" != x && test -d [CXXFLAGS="${my_CXXFLAGS} ${my_FIXX} ${LIBSSL_CFLAGS}" AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[${CPLUSPLUS_DECL}]], [[${CPLUSPLUS_MAIN}]])], [AC_MSG_RESULT([yes, -isystem preference helps for LIBSSL fkags]) - LIBSSL_CFLAGS="${my_FIXX} ${LIBSSL_CFLAGS}" + LIBSSL_CXXFLAGS="${my_FIXX} ${LIBSSL_CFLAGS}" ], [AC_MSG_RESULT([LIBSSL flags just won't work]) have_cxx11=no @@ -7106,8 +7107,9 @@ AS_IF([test "${have_cxx11}" = yes && test x"${TERMUX__PREFIX-}" != x && test -d AC_LANG_POP([C++]) unset CPLUSPLUS_MAIN unset CPLUSPLUS_DECL - umset my_FIXX + unset my_FIXX ]) +AC_SUBST(LIBSSL_CXXFLAGS) AC_DEFINE_UNQUOTED([EXEEXT], ["${EXEEXT}"], [Platform-specific extension for binary program files (may be empty where not required)]) AC_SUBST(EXEEXT) diff --git a/tests/Makefile.am b/tests/Makefile.am index 078321ff3c..38bf0bca83 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -24,7 +24,7 @@ CLEANFILES = *.trs *.log AM_CFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include -I$(top_srcdir)/drivers AM_CXXFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include if WITH_SSL - AM_CXXFLAGS += $(LIBSSL_CFLAGS) + AM_CXXFLAGS += $(LIBSSL_CXXFLAGS) endif WITH_SSL # Compiler flags for cppunit tests From 65b977fdaa11dcea1a7e2dbb0dc223661a857be0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 19:41:10 +0100 Subject: [PATCH 05/29] configure.ac: generalize fix for LIBSSL_CXXFLAGS when -isystem is involved [#1599, #1711] Signed-off-by: Jim Klimov --- configure.ac | 130 +++++++++++++++++++++++++++++++------- m4/nut_compiler_family.m4 | 6 ++ 2 files changed, 114 insertions(+), 22 deletions(-) diff --git a/configure.ac b/configure.ac index 5fdc1f0b29..ef71107c3f 100644 --- a/configure.ac +++ b/configure.ac @@ -7063,25 +7063,29 @@ AC_MSG_RESULT([C: ${nut_with_debuginfo_C}; C++: ${nut_with_debuginfo_CXX}]) dnl Only in the end, do you understand... AC_SUBST(CPPUNIT_NUT_CXXFLAGS) +dnl We can use OpenSSL or NSS in C++ libnutclient builds, and their CFLAGS on +dnl various platforms can pull in options which can confuse C++ preprocessing, +dnl such as -isystem settings which preclude looking for standard headers (due +dnl to `#include_next` involved in C++ headers ignoring certain directories to +dnl avoid loops). + +dnl There is a bit of trouble with third-party dependencies whose pkgconf +dnl files or our own code add `-isystem` entries (to avoid warnings about +dnl sloppy code, among other things): this can circumvent (clang++) search +dnl for C++ headers. Simple `-I/path/name` is okay in this regard. +dnl Such a problem for NUT was only seen/reported on Termux and MSYS2 so far. + LIBSSL_CXXFLAGS="${LIBSSL_CFLAGS}" -AS_IF([test "${have_cxx11}" = yes && test x"${TERMUX__PREFIX-}" != x && test -d "${TERMUX__PREFIX-}"], [ - dnl There is a bit of trouble with third-party dependencies whose pkgconf - dnl files or our own code add `-isystem` entries (to avoid warnings about - dnl sloppy code, among other things): this can circumvent (clang++) search - dnl for C++ headers. Simple `-I/path/name` is okay in this regard. - dnl Such a problem for NUT was only seen/reported on Termux so far. - dnl https://github.com/termux/termux-packages/issues/23578 - dnl https://github.com/termux/termux-packages/pull/23579 - AC_MSG_CHECKING(if C++11 support in current compiler needs help in Termux) +AS_IF([test "${have_cxx11}" = yes && test -n "${LIBSSL_CXXFLAGS}"], [ + AC_MSG_CHECKING([if C++ support in current compiler needs flag tweaks to build with SSL libs]) my_CFLAGS="${CFLAGS}" my_CPPFLAGS="${CPPFLAGS}" my_CXXFLAGS="${CXXFLAGS}" - - dnl # set the proper header include order - first package includes, then prefix includes - dnl # -isystem${TERMUX_PREFIX}/include/c++/v1 is needed here for on-device building to work correctly - dnl NOTE: two underscores when building, at least on android directly - my_FIXX="-isystem${TERMUX__PREFIX}/include/c++/v1 -isystem${TERMUX__PREFIX}/include" + my_FIXX="" + my_INCLUDEDIR_C="" + my_INCLUDEDIR_CXX="" + my_FIXED=false AC_LANG_PUSH([C++]) @@ -7090,16 +7094,95 @@ AS_IF([test "${have_cxx11}" = yes && test x"${TERMUX__PREFIX-}" != x && test -d CXXFLAGS="${my_CXXFLAGS} ${LIBSSL_CFLAGS}" AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[${CPLUSPLUS_DECL}]], [[${CPLUSPLUS_MAIN}]])], - [AC_MSG_RESULT([LIBSSL flags work out of the box])], - [CXXFLAGS="${my_CXXFLAGS} ${my_FIXX} ${LIBSSL_CFLAGS}" - AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[${CPLUSPLUS_DECL}]], [[${CPLUSPLUS_MAIN}]])], - [AC_MSG_RESULT([yes, -isystem preference helps for LIBSSL fkags]) - LIBSSL_CXXFLAGS="${my_FIXX} ${LIBSSL_CFLAGS}" - ], - [AC_MSG_RESULT([LIBSSL flags just won't work]) + [AC_MSG_RESULT([LIBSSL flags work out of the box]) + my_FIXED=true], + [AC_MSG_RESULT([This system needs help. We have a few platform-dependent ideas ready...]) + AS_IF([test x"${TERMUX__PREFIX-}" != x && test -d "${TERMUX__PREFIX-}/include"], + [AC_MSG_CHECKING([whether tweaks for Termux would help]) + dnl https://github.com/termux/termux-packages/issues/23578 + dnl https://github.com/termux/termux-packages/pull/23579 : + dnl # set the proper header include order - first package includes, then prefix includes + dnl # -isystem${TERMUX_PREFIX}/include/c++/v1 is needed here for on-device building to work correctly + dnl NOTE: two underscores when building, at least on android directly + my_INCLUDEDIR_C="${TERMUX__PREFIX}/include" + ], [ + AS_IF([test x"${MSYS_HOME-}" != x && test -d "${MSYS_HOME-}/../include"], + [AC_MSG_CHECKING([whether tweaks for MSYS2 would help]) + my_INCLUDEDIR_C="${MSYS_HOME}/../include" + ]) + ]) + + AS_IF([test -n "$my_INCLUDEDIR_C"], [ + AS_IF([test "${CLANGCC}" = "yes" && test -d "${my_INCLUDEDIR_C}/c++/v1"], + [my_INCLUDEDIR_CXX="${my_INCLUDEDIR_C}/c++/v1"], + [AS_IF([test "${GCC}" = "yes" && test -d "${my_INCLUDEDIR_C}/c++/${CXX_VERSION_NUMBER}"], + [my_INCLUDEDIR_CXX="${my_INCLUDEDIR_C}/c++/${CXX_VERSION_NUMBER}"] + )] + ) + ]) + + AS_IF([test -n "$my_INCLUDEDIR_CXX"], [ + my_FIXX="-isystem${my_INCLUDEDIR_CXX} -isystem${my_INCLUDEDIR_C}" + CXXFLAGS="${my_CXXFLAGS} ${my_FIXX} ${LIBSSL_CFLAGS}" + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[${CPLUSPLUS_DECL}]], [[${CPLUSPLUS_MAIN}]])], + [AC_MSG_RESULT([yes, tweaking -isystem preference helps for LIBSSL flags]) + LIBSSL_CXXFLAGS="${my_FIXX} ${LIBSSL_CFLAGS}" + my_FIXED=true], + [AC_MSG_RESULT([no, tweaking -isystem preference did not help]) + AS_IF([test x"${nut_enable_configure_debug}" = xyes], [ + AC_MSG_NOTICE([(CONFIGURE-DEVEL-DEBUG) Tried LIBSSL_CXXFLAGS="${my_FIXX-} ${LIBSSL_CFLAGS}"]) + ]) + ]) + ],[AC_MSG_RESULT([no, could not determine directories to tweak -isystem preference])] + ) + + AS_IF([test "$my_FIXED" = false], [ + AS_IF([test x"${nut_enable_configure_debug}" = xyes], [ + AC_MSG_NOTICE([(CONFIGURE-DEVEL-DEBUG) Tried my_INCLUDEDIR_C="${my_INCLUDEDIR_C-}"]) + AC_MSG_NOTICE([(CONFIGURE-DEVEL-DEBUG) Tried my_INCLUDEDIR_CXX="${my_INCLUDEDIR_CXX-}"]) + AC_MSG_NOTICE([(CONFIGURE-DEVEL-DEBUG) CXX_VERSION_NUMBER="${CXX_VERSION_NUMBER}"]) + ]) + + AS_CASE([${LIBSSL_CXXFLAGS}], + [*-isystem*], + [AC_MSG_CHECKING([whether removing -isystem from LIBSSL_CXXFLAGS would help]) + my_FIXX="" + my_SKIP=false + for TOKEN in $LIBSSL_CXXFLAGS ; do + AS_CASE([${TOKEN}], + [-isystem], [my_SKIP=true], + [-isystem*], [my_SKIP=false], + [AS_IF([test "${my_SKIP}" = true], + [my_SKIP=false], + [my_FIXX="$my_FIXX $TOKEN"] + )] + ) + done + unset TOKEN + unset my_SKIP + + CXXFLAGS="${my_CXXFLAGS} ${my_FIXX}" + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[${CPLUSPLUS_DECL}]], [[${CPLUSPLUS_MAIN}]])], + [AC_MSG_RESULT([yes, removing -isystem helps for LIBSSL flags]) + LIBSSL_CXXFLAGS="${my_FIXX}" + my_FIXED=true], + [AC_MSG_RESULT([no, removing -isystem from LIBSSL_CXXFLAGS did not help]) + AS_IF([test x"${nut_enable_configure_debug}" = xyes], [ + AC_MSG_NOTICE([(CONFIGURE-DEVEL-DEBUG) Tried LIBSSL_CXXFLAGS="${my_FIXX-}"]) + ]) + ] + ) + ] + ) + ]) + + AS_IF([test "$my_FIXED" = false], + [AC_MSG_RESULT([LIBSSL flags just won't work with C++]) + dnl FIXME: Disable just C++ support in the library? (need more make/macro definitions) have_cxx11=no ]) - ]) + ] + ) CFLAGS="${my_CFLAGS}" CPPFLAGS="${my_CPPFLAGS}" @@ -7108,6 +7191,9 @@ AS_IF([test "${have_cxx11}" = yes && test x"${TERMUX__PREFIX-}" != x && test -d unset CPLUSPLUS_MAIN unset CPLUSPLUS_DECL unset my_FIXX + unset my_FIXED + unset my_INCLUDEDIR_C + unset my_INCLUDEDIR_CXX ]) AC_SUBST(LIBSSL_CXXFLAGS) diff --git a/m4/nut_compiler_family.m4 b/m4/nut_compiler_family.m4 index f1719c8ae3..692907b193 100644 --- a/m4/nut_compiler_family.m4 +++ b/m4/nut_compiler_family.m4 @@ -89,6 +89,12 @@ if test -z "${nut_compiler_family_seen}"; then AS_IF([test "x$CC_VERSION" = x], [CC_VERSION="`echo \"${CC_VERSION_FULL}\" | head -1`"]) AS_IF([test "x$CXX_VERSION" = x], [CXX_VERSION="`echo \"${CXX_VERSION_FULL}\" | head -1`"]) AS_IF([test "x$CPP_VERSION" = x], [CPP_VERSION="`echo \"${CPP_VERSION_FULL}\" | head -1`"]) + + dnl Starting with number like "6.0.0" or "7.5.0-il-0" is fair game, + dnl but a "gcc-4.4.4-il-4" (starting with "gcc") is not + CC_VERSION_NUMBER="`echo \"${CC_VERSION}\" | sed -e 's,^.* \(@<:@0-9@:>@@<:@0-9@:>@*\.@<:@0-9@:>@@<:@^ ),@:>@*\).*$,\1,' -e 's, .*$,,' | ${EGREP} '^@<:@0-9@:>@' | head -1`" + CXX_VERSION_NUMBER="`echo \"${CXX_VERSION}\" | sed -e 's,^.* \(@<:@0-9@:>@@<:@0-9@:>@*\.@<:@0-9@:>@@<:@^ ),@:>@*\).*$,\1,' -e 's, .*$,,' | ${EGREP} '^@<:@0-9@:>@' | head -1`" + CPP_VERSION_NUMBER="`echo \"${CPP_VERSION}\" | sed -e 's,^.* \(@<:@0-9@:>@@<:@0-9@:>@*\.@<:@0-9@:>@@<:@^ ),@:>@*\).*$,\1,' -e 's, .*$,,' | ${EGREP} '^@<:@0-9@:>@' | head -1`" fi ]) From 722fb0093297f7820b718236ede23520ef8fe67a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 20:42:40 +0100 Subject: [PATCH 06/29] configure.ac, clients/nutclient.{cpp,h}, tests/cpputest-client.cpp, clients/Makefile.am, tests/Makefile.am: introduce separate automake condition and macro WITH_SSL_CXX [#1599, #1711] Signed-off-by: Jim Klimov --- clients/Makefile.am | 8 ++-- clients/nutclient.cpp | 97 ++++++++++++++++++++++----------------- clients/nutclient.h | 18 ++++++-- configure.ac | 10 ++-- tests/Makefile.am | 12 ++--- tests/cpputest-client.cpp | 24 ++++++++++ 6 files changed, 112 insertions(+), 57 deletions(-) diff --git a/clients/Makefile.am b/clients/Makefile.am index 94c6d6c255..4af2e01732 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -16,9 +16,9 @@ CLEANFILES = # 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_builddir)/include -I$(top_srcdir)/include -if WITH_SSL +if WITH_SSL_CXX AM_CXXFLAGS += $(LIBSSL_CXXFLAGS) -endif WITH_SSL +endif WITH_SSL_CXX # Make sure out-of-dir dependencies exist (especially when dev-building parts): $(top_builddir)/include/nut_version.h \ @@ -309,9 +309,9 @@ if HAVE_WINDOWS # Many versions of MingW seem to fail to build non-static DLL without this libnutclient_la_LDFLAGS += -no-undefined endif HAVE_WINDOWS -if WITH_SSL +if WITH_SSL_CXX libnutclient_la_LIBADD += $(LIBSSL_LDFLAGS_RPATH) $(LIBSSL_LIBS) -endif WITH_SSL +endif WITH_SSL_CXX else !HAVE_CXX11 EXTRA_DIST += nutclient.h nutclient.cpp endif !HAVE_CXX11 diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 9df97b0b9d..b62d70b3a5 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -258,11 +258,13 @@ class Socket private: SOCKET _sock; -#ifdef WITH_OPENSSL +#ifdef WITH_SSL_CXX +# ifdef WITH_OPENSSL SSL* _ssl; static SSL_CTX* _ssl_ctx; -#elif defined(WITH_NSS) +# elif defined(WITH_NSS) PRFileDesc* _ssl; +# endif #endif bool _debugConnect; struct timeval _tv; @@ -271,7 +273,8 @@ class Socket uint16_t _port; int _ssl_configured; int _force_ssl; /* Always known, so even non-SSL builds can fail if security is required */ -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#ifdef WITH_SSL_CXX +# if defined(WITH_OPENSSL) || defined(WITH_NSS) int _certverify; /* OpenSSL specific */ std::string _ca_path; @@ -284,14 +287,14 @@ class Socket std::string _certstore_prefix; std::string _certident_name; std::string _certhost_name; -#endif +# endif -#if defined(WITH_OPENSSL) +# if defined(WITH_OPENSSL) /* Callbacks, syntax dictated by OpenSSL */ static int openssl_password_callback(char *buf, int size, int rwflag, void *userdata); /* pem_passwd_cb, 1.1.0+ */ -#endif +# endif -#if defined(WITH_NSS) +# if defined(WITH_NSS) /* Callbacks, syntax dictated by NSS */ static char *nss_password_callback(PK11SlotInfo *slot, PRBool retry, void *arg); static SECStatus AuthCertificate(CERTCertDBHandle *arg, PRFileDesc *fd, @@ -303,10 +306,12 @@ class Socket CERTDistNames *caNames, CERTCertificate **pRetCert, SECKEYPrivateKey **pRetKey); static void HandshakeCallback(PRFileDesc *fd, void *arg); -#endif +# endif +#endif /* WITH_SSL_CXX */ }; -#ifdef WITH_OPENSSL +#ifdef WITH_SSL_CXX +# ifdef WITH_OPENSSL SSL_CTX* Socket::_ssl_ctx = nullptr; /*static*/ int Socket::openssl_password_callback(char *buf, int size, int rwflag, void *userdata) /* pem_passwd_cb, 1.1.0+ */ @@ -322,9 +327,9 @@ SSL_CTX* Socket::_ssl_ctx = nullptr; buf[size - 1] = '\0'; return static_cast(strlen(buf)); } -#endif +# endif /* WITH_OPENSSL */ -#ifdef WITH_NSS +# ifdef WITH_NSS static void nss_error(const char* funcname) { char buffer[256]; @@ -423,20 +428,25 @@ static void nss_error(const char* funcname) std::cerr << "SSL handshake done successfully with server " << sock->_host << std::endl; } } -#endif /* WITH_NSS */ +# endif /* WITH_NSS */ +#endif /* WITH_SSL_CXX */ Socket::Socket(): _sock(INVALID_SOCKET), -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#ifdef WITH_SSL_CXX +# if defined(WITH_OPENSSL) || defined(WITH_NSS) _ssl(nullptr), +# endif #endif _debugConnect(false), _tv(), _port(NUT_PORT), _force_ssl(0) -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#ifdef WITH_SSL_CXX +# if defined(WITH_OPENSSL) || defined(WITH_NSS) ,_certverify(-1) -#endif +# endif +#endif /* WITH_SSL_CXX */ { _tv.tv_sec = -1; _tv.tv_usec = 0; @@ -761,17 +771,20 @@ void Socket::connect(const std::string& host, uint16_t port) void Socket::disconnect() { -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#ifdef WITH_SSL_CXX +# if defined(WITH_OPENSSL) || defined(WITH_NSS) if (_ssl) { -# ifdef WITH_OPENSSL +# ifdef WITH_OPENSSL SSL_shutdown(_ssl); SSL_free(_ssl); -# elif defined(WITH_NSS) +# elif defined(WITH_NSS) PR_Close(_ssl); -# endif +# endif _ssl = nullptr; } -#endif +# endif +#endif /* WITH_SSL_CXX */ + if(_sock != INVALID_SOCKET) { ::closesocket(_sock); @@ -782,7 +795,7 @@ void Socket::disconnect() bool Socket::isSSL()const { -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#if defined (WITH_SSL_CXX) && (defined(WITH_OPENSSL) || defined(WITH_NSS)) return _ssl != nullptr; #else return false; @@ -793,7 +806,7 @@ void Socket::setSSLConfig_OpenSSL(bool force_ssl, int certverify, const std::str { _force_ssl = force_ssl; -#if defined(WITH_OPENSSL) +#if defined(WITH_SSL_CXX) && defined(WITH_OPENSSL) _certverify = certverify; /* These need to be saved at least to handle callbacks * (to see if errors are fatal or ignorable) @@ -821,7 +834,7 @@ void Socket::setSSLConfig_NSS(bool force_ssl, int certverify, const std::string& { _force_ssl = force_ssl; -#if defined(WITH_NSS) +#if defined(WITH_SSL_CXX) && defined(WITH_NSS) _certverify = certverify; /* These need to be saved at least to handle NSS callbacks * (to see if errors are fatal or ignorable) @@ -851,7 +864,8 @@ void Socket::startTLS() throw nut::NotConnectedException(); } -#ifdef WITH_OPENSSL +#ifdef WITH_SSL_CXX +# ifdef WITH_OPENSSL if (!(_ssl_configured & UPSCLI_SSL_CAPS_OPENSSL)) { if (_debugConnect) std::cerr << "[D2] Socket::startTLS(): Not configured for OpenSSL" << @@ -863,7 +877,7 @@ void Socket::startTLS() } return; } -#elif defined(WITH_NSS) +# elif defined(WITH_NSS) if (!(_ssl_configured & UPSCLI_SSL_CAPS_NSS)) { if (_debugConnect) std::cerr << "[D2] Socket::startTLS(): Not configured for NSS" << @@ -875,9 +889,9 @@ void Socket::startTLS() } return; } -#endif /* WITH_OPENSSL || WITH_NSS */ +# endif /* WITH_OPENSSL || WITH_NSS */ -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +# if defined(WITH_OPENSSL) || defined(WITH_NSS) write("STARTTLS"); std::string res = read(); if (res.substr(0, 11) != "OK STARTTLS") { @@ -887,17 +901,17 @@ void Socket::startTLS() } return; } -#endif /* WITH_OPENSSL || WITH_NSS */ +# endif /* WITH_OPENSSL || WITH_NSS */ -#ifdef WITH_OPENSSL +# ifdef WITH_OPENSSL if (!_ssl_ctx) { -# if OPENSSL_VERSION_NUMBER < 0x10100000L +# if OPENSSL_VERSION_NUMBER < 0x10100000L SSL_load_error_strings(); SSL_library_init(); _ssl_ctx = SSL_CTX_new(SSLv23_client_method()); -# else +# else _ssl_ctx = SSL_CTX_new(TLS_client_method()); -# endif +# endif if (!_ssl_ctx) { throw nut::SSLException_OpenSSL("Cannot create SSL context"); } @@ -916,9 +930,9 @@ void Socket::startTLS() throw nut::SSLException_OpenSSL("Failed to load client certificate file"); } if (!_key_pass.empty()) { -# if OPENSSL_VERSION_NUMBER < 0x10100000L +# if OPENSSL_VERSION_NUMBER < 0x10100000L throw nut::SSLException_OpenSSL("Private key password support not implemented for OpenSSL < 1.1 yet"); -# else +# else /* OpenSSL 1.1.0+ * https://docs.openssl.org/3.5/man3/SSL_CTX_set_default_passwd_cb/#return-values */ @@ -926,7 +940,7 @@ void Socket::startTLS() SSL_CTX_set_default_passwd_cb(_ssl_ctx, openssl_password_callback); /* 2. Set the userdata to the password string */ SSL_CTX_set_default_passwd_cb_userdata(_ssl_ctx, const_cast(static_cast(_key_pass.c_str()))); -# endif +# endif } if (SSL_CTX_use_PrivateKey_file(_ssl_ctx, _key_file.empty() ? _cert_file.c_str() : _key_file.c_str(), SSL_FILETYPE_PEM) != 1) { throw nut::SSLException_OpenSSL("Failed to load client private key file"); @@ -948,7 +962,7 @@ void Socket::startTLS() throw nut::SSLException_OpenSSL(std::string("SSL connection failed: ") + errbuf); } -#elif defined(WITH_NSS) +# elif defined(WITH_NSS) /* NSS implementation following upsclient.c logic */ static bool nss_initialized = false; @@ -1025,6 +1039,7 @@ void Socket::startTLS() disconnect(); throw nut::SSLException_NSS("Handshake failed"); } +# endif /* WITH_NSS */ #else if (_debugConnect) std::cerr << "[D2] Socket::startTLS(): SSL support not compiled in" << @@ -1034,7 +1049,7 @@ void Socket::startTLS() disconnect(); throw nut::SSLException("SSL support not compiled in"); } -#endif +#endif /* WITH_SSL_CXX */ } bool Socket::isConnected()const @@ -1061,7 +1076,7 @@ size_t Socket::read(void* buf, size_t sz) } ssize_t res; -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#if defined(WITH_SSL_CXX) && (defined(WITH_OPENSSL) || defined(WITH_NSS)) if (_ssl) { # ifdef WITH_OPENSSL res = SSL_read(_ssl, buf, static_cast(sz)); @@ -1102,7 +1117,7 @@ size_t Socket::write(const void* buf, size_t sz) } ssize_t res; -#if defined(WITH_OPENSSL) || defined(WITH_NSS) +#if defined(WITH_SSL_CXX) && (defined(WITH_OPENSSL) || defined(WITH_NSS)) if (_ssl) { # ifdef WITH_OPENSSL res = SSL_write(_ssl, buf, static_cast(sz)); @@ -1320,14 +1335,14 @@ TcpClient::~TcpClient() { int ret = UPSCLI_SSL_CAPS_NONE; -#ifdef WITH_SSL +#ifdef WITH_SSL_CXX # ifdef WITH_OPENSSL ret |= UPSCLI_SSL_CAPS_OPENSSL; # endif # ifdef WITH_NSS ret |= UPSCLI_SSL_CAPS_NSS; # endif -#endif /* WITH_SSL */ +#endif /* WITH_SSL_CXX */ return ret; } diff --git a/clients/nutclient.h b/clients/nutclient.h index abb22c866b..5a3f6a8fc8 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -29,13 +29,25 @@ /* Begin of C++ nutclient library declaration */ #ifdef __cplusplus -#ifdef WITH_OPENSSL +#ifdef WITH_SSL_CXX +# ifdef WITH_OPENSSL # include # include -#elif defined(WITH_NSS) /* not WITH_OPENSSL */ +# elif defined(WITH_NSS) /* not WITH_OPENSSL */ # include # include -#endif /* WITH_OPENSSL | WITH_NSS */ +# endif /* WITH_OPENSSL | WITH_NSS */ +/* +// This should not be needed if macros in code are all in the right places: +#else +# ifdef WITH_OPENSSL +# undefine WITH_OPENSSL +# endif +# ifdef WITH_NSS +# undefine WITH_NSS +# endif +*/ +#endif /* WITH_SSL_CXX */ #include #include diff --git a/configure.ac b/configure.ac index ef71107c3f..dc17796da3 100644 --- a/configure.ac +++ b/configure.ac @@ -7076,6 +7076,7 @@ dnl for C++ headers. Simple `-I/path/name` is okay in this regard. dnl Such a problem for NUT was only seen/reported on Termux and MSYS2 so far. LIBSSL_CXXFLAGS="${LIBSSL_CFLAGS}" +nut_with_ssl_cxx=no AS_IF([test "${have_cxx11}" = yes && test -n "${LIBSSL_CXXFLAGS}"], [ AC_MSG_CHECKING([if C++ support in current compiler needs flag tweaks to build with SSL libs]) @@ -7176,10 +7177,10 @@ AS_IF([test "${have_cxx11}" = yes && test -n "${LIBSSL_CXXFLAGS}"], [ ) ]) - AS_IF([test "$my_FIXED" = false], + AS_IF([test "$my_FIXED" = true], + [nut_with_ssl_cxx=yes], [AC_MSG_RESULT([LIBSSL flags just won't work with C++]) - dnl FIXME: Disable just C++ support in the library? (need more make/macro definitions) - have_cxx11=no + dnl We disable just SSL support in the C++ library ]) ] ) @@ -7195,7 +7196,10 @@ AS_IF([test "${have_cxx11}" = yes && test -n "${LIBSSL_CXXFLAGS}"], [ unset my_INCLUDEDIR_C unset my_INCLUDEDIR_CXX ]) + AC_SUBST(LIBSSL_CXXFLAGS) +NUT_REPORT_FEATURE([enable SSL support in C++ client library], [${nut_with_ssl_cxx}], [${nut_ssl_lib}], + [WITH_SSL_CXX], [Define to enable SSL in libnutclient]) AC_DEFINE_UNQUOTED([EXEEXT], ["${EXEEXT}"], [Platform-specific extension for binary program files (may be empty where not required)]) AC_SUBST(EXEEXT) diff --git a/tests/Makefile.am b/tests/Makefile.am index 38bf0bca83..2bdab40017 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -23,9 +23,9 @@ CLEANFILES = *.trs *.log AM_CFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include -I$(top_srcdir)/drivers AM_CXXFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include -if WITH_SSL +if WITH_SSL_CXX AM_CXXFLAGS += $(LIBSSL_CXXFLAGS) -endif WITH_SSL +endif WITH_SSL_CXX # Compiler flags for cppunit tests CPPUNIT_NUT_CXXFLAGS = @CPPUNIT_NUT_CXXFLAGS@ @@ -217,20 +217,20 @@ if WITH_LIBNUTCONF cppunittest_SOURCES += $(CPPUNITTESTSRC_NUTCONF) cppunittest_LDADD += $(top_builddir)/common/libnutconf.la endif WITH_LIBNUTCONF -if WITH_SSL +if WITH_SSL_CXX # Not really used here, but transient dependency from libnutclient # At least on NetBSD consumers should know directly... cppunittest_LDADD += $(LIBSSL_LIBS) -endif WITH_SSL +endif WITH_SSL_CXX cppnit_CXXFLAGS = $(AM_CXXFLAGS) $(CPPUNIT_CFLAGS) $(CPPUNIT_CXXFLAGS) $(CPPUNIT_NUT_CXXFLAGS) $(CXXFLAGS) cppnit_LDFLAGS = $(CPPUNIT_LDFLAGS) $(CPPUNIT_LIBS) cppnit_LDADD = $(top_builddir)/clients/libnutclientstub.la cppnit_LDADD += $(top_builddir)/clients/libnutclient.la cppnit_SOURCES = $(CPPCLIENTTESTSRC) $(CPPUNITTESTERSRC) -if WITH_SSL +if WITH_SSL_CXX cppnit_LDADD += $(LIBSSL_LIBS) -endif WITH_SSL +endif WITH_SSL_CXX else !HAVE_CPPUNIT diff --git a/tests/cpputest-client.cpp b/tests/cpputest-client.cpp index c22859da8f..3fef89413d 100644 --- a/tests/cpputest-client.cpp +++ b/tests/cpputest-client.cpp @@ -181,7 +181,11 @@ void NutActiveClientTest::setUp() s = std::getenv("NUT_FORCESSL"); if (s && (std::string(s) == "1" || std::string(s) == "true" || std::string(s) == "yes")) { +#ifdef WITH_SSL_CXX env_NUT_FORCESSL = true; +#else + std::cerr << "[D] Not built with WITH_SSL_CXX, ignoring NUT_FORCESSL=true" << std::endl; +#endif } s = std::getenv("NUT_CERTVERIFY"); @@ -245,6 +249,9 @@ void NutActiveClientTest::setupClientSSL(nut::TcpClient &c) || !env_NUT_CERTFILE.empty() || !env_NUT_KEYFILE.empty() ) { +#ifndef WITH_SSL_CXX + try { +#endif c.setSSLConfig(SSLConfig_OpenSSL( env_NUT_FORCESSL, env_NUT_CERTVERIFY, @@ -254,6 +261,13 @@ void NutActiveClientTest::setupClientSSL(nut::TcpClient &c) env_NUT_KEYFILE, env_NUT_KEYPASS )); +#ifndef WITH_SSL_CXX + } + catch(nut::SSLException& ex) + { + std::cerr << "[D] Not built with WITH_SSL_CXX and reasonably failed to setSSLConfig(OpenSSL): " << ex.what() << std::endl; + } +#endif } if (env_NUT_CERTVERIFY != -1 @@ -263,6 +277,9 @@ void NutActiveClientTest::setupClientSSL(nut::TcpClient &c) || !env_NUT_CERTHOST_NAME.empty() || !env_NUT_CERTIDENT_NAME.empty() ) { +#ifndef WITH_SSL_CXX + try { +#endif c.setSSLConfig(SSLConfig_NSS( env_NUT_FORCESSL, env_NUT_CERTVERIFY, @@ -272,6 +289,13 @@ void NutActiveClientTest::setupClientSSL(nut::TcpClient &c) env_NUT_CERTHOST_NAME, env_NUT_CERTIDENT_NAME )); +#ifndef WITH_SSL_CXX + } + catch(nut::SSLException& ex) + { + std::cerr << "[D] Not built with WITH_SSL_CXX and reasonably failed to setSSLConfig(NSS): " << ex.what() << std::endl; + } +#endif } std::cerr << "[D] C++ NUT Client lib enabled SSL options:" From 9e341dffdf85bd450c854858e640961dc301f07b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 23:08:37 +0100 Subject: [PATCH 07/29] configure.ac: extend detection of LIBSSL_CXXFLAGS on MSYS2 [#1599] Signed-off-by: Jim Klimov --- configure.ac | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index dc17796da3..92ed45dc07 100644 --- a/configure.ac +++ b/configure.ac @@ -7099,7 +7099,7 @@ AS_IF([test "${have_cxx11}" = yes && test -n "${LIBSSL_CXXFLAGS}"], [ my_FIXED=true], [AC_MSG_RESULT([This system needs help. We have a few platform-dependent ideas ready...]) AS_IF([test x"${TERMUX__PREFIX-}" != x && test -d "${TERMUX__PREFIX-}/include"], - [AC_MSG_CHECKING([whether tweaks for Termux would help]) + [AC_MSG_CHECKING([whether tweaks for Termux via TERMUX__PREFIX would help]) dnl https://github.com/termux/termux-packages/issues/23578 dnl https://github.com/termux/termux-packages/pull/23579 : dnl # set the proper header include order - first package includes, then prefix includes @@ -7108,8 +7108,13 @@ AS_IF([test "${have_cxx11}" = yes && test -n "${LIBSSL_CXXFLAGS}"], [ my_INCLUDEDIR_C="${TERMUX__PREFIX}/include" ], [ AS_IF([test x"${MSYS_HOME-}" != x && test -d "${MSYS_HOME-}/../include"], - [AC_MSG_CHECKING([whether tweaks for MSYS2 would help]) - my_INCLUDEDIR_C="${MSYS_HOME}/../include" + [AC_MSG_CHECKING([whether tweaks for MSYS2 via MSYS_HOME would help]) + dnl my_INCLUDEDIR_C="${MSYS_HOME}/../include" + my_INCLUDEDIR_C="`printf '%s' \"${MSYS_HOME}/../include\" | tr '\\' '/'`"], + [AS_IF([test x"${MSYSTEM_PREFIX-}" != x && test -d "${MSYSTEM_PREFIX-}/include"], + [AC_MSG_CHECKING([whether tweaks for MSYS2 via MSYSTEM_PREFIX would help]) + my_INCLUDEDIR_C="${MSYSTEM_PREFIX-}/include" + ]) ]) ]) From 0a6d278c063192cebc69c6795f7fc0d3197bcd93 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 23:44:56 +0100 Subject: [PATCH 08/29] clients/nutclient.cpp: fix includes for non-NSS builds [#1599] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index b62d70b3a5..77e396ee15 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -47,14 +47,16 @@ #include #include -#ifdef WITH_NSS -# include -# include -# include -# include -# include -# include -#endif /* WITH_NSS */ +#ifdef WITH_SSL_CXX +# ifdef WITH_NSS +# include +# include +# include +# include +# include +# include +# endif /* WITH_NSS */ +#endif /* WITH_SSL_CXX */ /* Windows/Linux Socket compatibility layer: */ /* Thanks to Benjamin Roux (http://broux.developpez.com/articles/c/sockets/) */ From 4254b3add95d25e332df04016fbd309abd29c25b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 23:45:28 +0100 Subject: [PATCH 09/29] clients/nutclient.h: drop commented-away undefine of NSS/OPENSSL when building without SSL_CXX [#1599] Signed-off-by: Jim Klimov --- clients/nutclient.h | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/clients/nutclient.h b/clients/nutclient.h index 5a3f6a8fc8..20c6d7a2d1 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -37,16 +37,6 @@ # include # include # endif /* WITH_OPENSSL | WITH_NSS */ -/* -// This should not be needed if macros in code are all in the right places: -#else -# ifdef WITH_OPENSSL -# undefine WITH_OPENSSL -# endif -# ifdef WITH_NSS -# undefine WITH_NSS -# endif -*/ #endif /* WITH_SSL_CXX */ #include From c82538076993ff072ca30bb2c9d5a682d7168357 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 14:41:21 +0100 Subject: [PATCH 10/29] NEWS.adoc: highlight that PR #3233 also fixes fallout of #3008 Signed-off-by: Jim Klimov --- NEWS.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 5eead11d4a..6adbd1595a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -86,6 +86,10 @@ https://github.com/networkupstools/nut/milestone/12 not-requested (e.g. do not start `upsd` in `MODE=netclient`), even though the unit is nominally enabled; this should make packaging and providing service pre-sets more simple. [issue #837, PR #3233] ++ +In a way, this should also fix fallout of a change delivered in NUT v2.8.4 +release, where `upsmon` could have started with `MODE=none` in `nut.conf`, +but the `nutshutdown` script would bail out quickly and quietly. [PR #3008] * Fixed thread-safety of IP address printout in `libupsclient` method `upscli_tryconnect()` (practical bug seen in `nut-scanner` parallel scans). [issue #3234] From 73fc5025726dd957b3ca74c6c525907dff4c9c04 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 15:29:00 +0100 Subject: [PATCH 11/29] Makefile.am: reformat spellcheck rule shell scriptlet Signed-off-by: Jim Klimov --- Makefile.am | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 1622c034ed..1d0711aeac 100644 --- a/Makefile.am +++ b/Makefile.am @@ -781,8 +781,9 @@ spellcheck spellcheck-interactive: @dotMAKE@ fi ; \ export SUBDIR_MAKE_VERBOSE ; \ ( $(MAKE) $(AM_MAKEFLAGS) SPELLCHECK_TGT='$@' SUBDIR_MAKE_VERBOSE="$${SUBDIR_MAKE_VERBOSE}" -k -s $(SPELLCHECK_DIRS) && exit ; \ - echo "WARNING: FAILED fanned-out attempt in $@, retrying with NUT_MAKE_SKIP_FANOUT" >&2 ; \ - $(MAKE) $(AM_MAKEFLAGS) NUT_MAKE_SKIP_FANOUT=true SPELLCHECK_TGT='$@' -k -s $(SPELLCHECK_DIRS) ) || exit ; \ + echo "WARNING: FAILED fanned-out attempt in $@, retrying with NUT_MAKE_SKIP_FANOUT" >&2 ; \ + $(MAKE) $(AM_MAKEFLAGS) NUT_MAKE_SKIP_FANOUT=true SPELLCHECK_TGT='$@' -k -s $(SPELLCHECK_DIRS) \ + ) || exit ; \ if [ x'$@' = xspellcheck-interactive ] ; then \ echo "SUCCESS: $@: follow up with spellcheck-quick to revise and update timestamps"; \ $(MAKE) $(AM_MAKEFLAGS) spellcheck-quick ; \ From 7eb3358c2de4dccb1bf7aa8548b9ebe3b22ad88d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 15:30:47 +0100 Subject: [PATCH 12/29] docs/Makefile.am: move NEWS.adoc and UPGRADING.adoc to top of SPELLCHECK_SRC_DEFAULT and comment why they may not be the first checked anyway [#2871] Signed-off-by: Jim Klimov --- docs/Makefile.am | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/Makefile.am b/docs/Makefile.am index ee725f8c58..0b7e901299 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -811,14 +811,24 @@ qa-guide.pdf: docinfo-since-v2.8.3.xml qa-guide-docinfo.xml @$(GENERATE_PDF) # Used below for spellcheck and for .prep-src-docs -SPELLCHECK_SRC_DEFAULT = $(ALL_TXT_SRC) \ +# List most-frequently edited files first, to hit typos there sooner when +# developing. Some files from sub-directories are spell-checked here and +# not in their own makefiles, because they are included as chapters in +# some larger documents anchored here. +# Note that in builds with enabled parallel fanout mode (even if done +# sequentially, e.g. without NUT_MAKE_SKIP_FANOUT=true explicitly), +# files located in the current directory are processed first anyway, +# as one (possibly parallelized) sub-make call. +SPELLCHECK_SRC_DEFAULT = \ + ../NEWS.adoc ../UPGRADING.adoc \ asciidoc-vars.conf \ - ../ci_build.adoc ../README.adoc ../NEWS.adoc \ - ../INSTALL.nut.adoc ../UPGRADING.adoc \ + ../ci_build.adoc ../README.adoc \ + ../INSTALL.nut.adoc \ ../TODO.adoc ../scripts/ufw/README.adoc \ ../scripts/augeas/README.adoc ../lib/README.adoc \ ../tools/nut-scanner/README.adoc \ - ../AUTHORS ../COPYING ../LICENSE-GPL2 ../LICENSE-GPL3 ../LICENSE-DCO + ../AUTHORS ../COPYING ../LICENSE-GPL2 ../LICENSE-GPL3 ../LICENSE-DCO \ + $(ALL_TXT_SRC) if HAVE_ASPELL # Non-interactively spell check all documentation source files. From 8187a553e4b29db045764109ce6d0581973d55bf Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 15:32:06 +0100 Subject: [PATCH 13/29] docs/Makefile.am: spellcheck: debug SPELLCHECK_SRCDIR and SPELLCHECK_SRC values [#2871] Signed-off-by: Jim Klimov --- docs/Makefile.am | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Makefile.am b/docs/Makefile.am index 0b7e901299..feb2c867c4 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -997,6 +997,8 @@ spellcheck: @dotMAKE@ $(ASPELL) --help || true; \ (command -v dpkg) && ( dpkg -l | $(GREP) -i aspell ) || true ; \ echo "ASPELL automatic execution line is : ( sed 's,^\(.*\)$$, \1,' < docfile.txt | $(ASPELL) -a $(ASPELL_NUT_TEXMODE_ARGS) $(ASPELL_NUT_COMMON_ARGS) | $(EGREP) -b -v '$(ASPELL_OUT_NOTERRORS)' )" ; \ + echo "SPELLCHECK_SRCDIR: $(SPELLCHECK_SRCDIR)" ; \ + echo "SPELLCHECK_SRC: $(SPELLCHECK_SRC)" ; \ echo "ASPELL proceeding to spellchecking job..."; \ else true; fi +@FAILED="" ; LANG=C; LC_ALL=C; export LANG; export LC_ALL; \ From 2c87f42f07ecc1232d3ddae2397a78a68a185f34 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 15:40:25 +0100 Subject: [PATCH 14/29] docs/Makefile.am: spellcheck: prioritize certain SPELLCHECK_NOEXT_DOCS_FIRST (if present) even before parallel rules [#2871] Signed-off-by: Jim Klimov --- docs/Makefile.am | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/Makefile.am b/docs/Makefile.am index feb2c867c4..9a9c735abc 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -1011,11 +1011,18 @@ spellcheck: @dotMAKE@ || FAILED="$$FAILED $(SPELLCHECK_SRCDIR)/$$docsrc"; \ done ; \ else \ + SPELLCHECK_NOEXT_DOCS_FIRST="`for docsrc in $(SPELLCHECK_SRC); do case \"$${docsrc}\" in ../NEWS.adoc|../UPGRADING.adoc) printf '%s ' \"$${docsrc}\" ;; *) ;; esac ; done`" ; \ SPELLCHECK_AUTO_TGT="`for docsrc in $(SPELLCHECK_SRC); do case \"$${docsrc}\" in */*) ;; *.adoc|*.txt|*.in|*.conf|*.sample) printf '%s ' \"$${docsrc}-spellchecked-auto\" ;; esac ; done`" ; \ - SPELLCHECK_NOEXT_DOCS="`for docsrc in $(SPELLCHECK_SRC); do case \"$${docsrc}\" in */*) printf '%s ' \"$${docsrc}\" ;; *.adoc|*.txt|*.in|*.conf|*.sample) ;; *) printf '%s ' \"$${docsrc}\" ;; esac ; done`" ; \ + SPELLCHECK_NOEXT_DOCS="`for docsrc in $(SPELLCHECK_SRC); do case \"$${docsrc}\" in ../NEWS.adoc|../UPGRADING.adoc) ;; */*) printf '%s ' \"$${docsrc}\" ;; *.adoc|*.txt|*.in|*.conf|*.sample) ;; *) printf '%s ' \"$${docsrc}\" ;; esac ; done`" ; \ if test "$(SPELLCHECK_ENV_DEBUG)" != no ; then \ - echo "ASPELL MAKEFILE DEBUG: from `pwd`: SPELLCHECK_NOEXT_DOCS='$${SPELLCHECK_NOEXT_DOCS}' SPELLCHECK_AUTO_TGT='$${SPELLCHECK_AUTO_TGT}'" ; \ + echo "ASPELL MAKEFILE DEBUG: from `pwd`: SPELLCHECK_NOEXT_DOCS_FIRST='$${SPELLCHECK_NOEXT_DOCS_FIRST}' SPELLCHECK_AUTO_TGT='$${SPELLCHECK_AUTO_TGT}' SPELLCHECK_NOEXT_DOCS='$${SPELLCHECK_NOEXT_DOCS}'" ; \ else true ; fi ; \ + if [ x"$${SPELLCHECK_NOEXT_DOCS_FIRST}" != x ] ; then \ + for docsrc in $${SPELLCHECK_NOEXT_DOCS_FIRST} ; do \ + $(MAKE) $(AM_MAKEFLAGS) -k -s -f "$(abs_top_builddir)/docs/Makefile" SPELLCHECK_SRC="" SPELLCHECK_SRC_ONE="$${docsrc}" SPELLCHECK_BUILDDIR="$(SPELLCHECK_BUILDDIR)" SPELLCHECK_SRCDIR="$(SPELLCHECK_SRCDIR)" VPATH="$(SPELLCHECK_SRCDIR):$(SPELLCHECK_BUILDDIR):$(VPATH)" "$(SPELLCHECK_BUILDDIR)/$${docsrc}-spellchecked" \ + || FAILED="$$FAILED $(SPELLCHECK_SRCDIR)/$$docsrc"; \ + done ; \ + fi ; \ if [ x"$${SPELLCHECK_AUTO_TGT}" != x ] ; then \ $(MAKE) $(AM_MAKEFLAGS) -k -s -f "$(abs_top_builddir)/docs/Makefile" SPELLCHECK_SRC="" SPELLCHECK_BUILDDIR="$(SPELLCHECK_BUILDDIR)" SPELLCHECK_SRCDIR="$(SPELLCHECK_SRCDIR)" VPATH="$(SPELLCHECK_SRCDIR):$(SPELLCHECK_BUILDDIR):$(VPATH)" $${SPELLCHECK_AUTO_TGT} ; \ FAILED="`for docsrc in $(SPELLCHECK_SRC); do if [ -f \"$(SPELLCHECK_BUILDDIR)/$${docsrc}-spellchecked-auto.failed\" ] ; then printf '%s ' \"$(SPELLCHECK_SRCDIR)/$${docsrc}\" ; rm -f \"$(SPELLCHECK_BUILDDIR)/$${docsrc}-spellchecked-auto.failed\" ; fi ; done`" ; \ From ccdea767e998cf29696bb7772c0824c62141d4dc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 15:55:20 +0100 Subject: [PATCH 15/29] Makefile.am: revise tracing of SUBDIR-MAKE reports to make them consitent and easier for troubleshooting [#2871, #3039] Signed-off-by: Jim Klimov --- Makefile.am | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile.am b/Makefile.am index 1d0711aeac..8256ca1fa8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -134,7 +134,7 @@ all-fanout-maybe: @dotMAKE@ clients/.all.libupsclient_version-generated.timestamp +@if [ x"$(NUT_MAKE_SKIP_FANOUT)" = xtrue ] ; then \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE $@: skip optimization for parallel make - NUT_MAKE_SKIP_FANOUT is set" ; \ + echo " SUBDIR-MAKE SKIP $@: skip optimization for parallel make - NUT_MAKE_SKIP_FANOUT is set" ; \ fi ; \ $(MAKE) $(AM_MAKEFLAGS) generated-headers-with-a-touch || exit ; \ exit 0 ; \ @@ -142,15 +142,15 @@ all-fanout-maybe: @dotMAKE@ case "-$(MAKEFLAGS) $(AM_MAKEFLAGS)" in \ *-j|*-j" "*|*-{j,l}{0,1,2,3,4,5,6,7,8,9}*|*-[jl][0123456789]*|*{-l,--jobs,--load-average,--max-load}" "{-,0,1,2,3,4,5,6,7,8,9}*|*--jobserver*|*--jobs" "[0123456789]*|*--load-average" "[0123456789]*|*--max-load" "[0123456789]*) \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE $@: implement optimization for parallel make as 'make all-fanout-subdirs'" ; \ + echo " SUBDIR-MAKE LAUNCH $@: implement optimization for parallel make as 'make all-fanout-subdirs'" ; \ fi ; \ $(MAKE) $(AM_MAKEFLAGS) all-fanout-subdirs || exit ; \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE $@: optimization for parallel make as 'make all-fanout-subdirs' finished; now automake rules can follow up with default implementation of 'all-recursive' and/or 'all' (may try to regenerate some files again; should keep existing results though)" ; \ + echo " SUBDIR-MAKE FINISH $@: optimization for parallel make as 'make all-fanout-subdirs' finished; now automake rules can follow up with default implementation of 'all-recursive' and/or 'all' (may try to regenerate some files again; should keep existing results though)" ; \ fi ;; \ *) \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE $@: skip optimization for parallel make - we seem to run sequentially now, seen MAKEFLAGS='$(MAKEFLAGS)' AM_MAKEFLAGS='$(AM_MAKEFLAGS)'" ; \ + echo " SUBDIR-MAKE SKIP $@: skip optimization for parallel make - we seem to run sequentially now, seen MAKEFLAGS='$(MAKEFLAGS)' AM_MAKEFLAGS='$(AM_MAKEFLAGS)'" ; \ fi ; \ $(MAKE) $(AM_MAKEFLAGS) generated-headers-with-a-touch || exit ; \ ;; \ @@ -234,12 +234,12 @@ SUBDIR_TGT_RULE = ( \ [ x"$${TGT-}" != x ] || TGT="`echo '$@' | awk -F/ '{print $$1}'`" ; \ [ x"$${DIR-}" != x ] || DIR="`echo '$@' | sed 's,^[^/]*/,,'`" ; \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE STARTING $@: 'make $${SUBDIR_TGT_MAKEFLAGS-} $$TGT' in $$DIR ..." ; \ + echo " SUBDIR-MAKE STARTING $@: 'make $${SUBDIR_TGT_MAKEFLAGS-} $$TGT' in $$DIR ..." ; \ fi ; \ cd "$(abs_builddir)/$${DIR}" && \ $(MAKE) $(AM_MAKEFLAGS) $${SUBDIR_TGT_MAKEFLAGS-} "$${TGT}" || { RES=$$?; echo " SUBDIR-MAKE FAILURE: 'make $$TGT' in $$DIR" >&2 ; exit $$RES ; } ; \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE SUCCESS $@: 'make $${SUBDIR_TGT_MAKEFLAGS-} $$TGT' in $$DIR" ; \ + echo " SUBDIR-MAKE SUCCESS $@: 'make $${SUBDIR_TGT_MAKEFLAGS-} $$TGT' in $$DIR" ; \ fi ; \ ) @@ -761,7 +761,7 @@ spellcheck spellcheck-interactive: @dotMAKE@ if [ x"$(NUT_MAKE_SKIP_FANOUT)" = xtrue ] ; then \ RES=0 ; \ if [ x"$(SUBDIR_MAKE_VERBOSE)" != x0 ] ; then \ - echo " SUBDIR-MAKE $@: skip optimization for parallel make - NUT_MAKE_SKIP_FANOUT is set" ; \ + echo " SUBDIR-MAKE SKIP $@: skip optimization for parallel make - NUT_MAKE_SKIP_FANOUT is set" ; \ fi ; \ (cd $(builddir)/docs && $(MAKE) $(AM_MAKEFLAGS) -k -s $(abs_top_builddir)/docs/.prep-src-docs) || RES=$$? ; \ (cd $(builddir)/docs/man && $(MAKE) $(AM_MAKEFLAGS) -k -s $(abs_top_builddir)/docs/man/.prep-src-docs) || RES=$$? ; \ From 2bd16d37cc930eec97e0e657834e15aa97ec9b7e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 16:46:23 +0100 Subject: [PATCH 16/29] Makefile.am: fix "make check" done from scratch [#2871, #3039] * Goal for "generated-headers-with-a-touch" stepped on "all" done as part of "check". * Not using `+` prefix in that goal was not right too. * Made cleanup dependencies separate for all/check/install recursive rules. Signed-off-by: Jim Klimov --- Makefile.am | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/Makefile.am b/Makefile.am index 8256ca1fa8..8c83a6bf82 100644 --- a/Makefile.am +++ b/Makefile.am @@ -94,22 +94,43 @@ SUBDIRS_ALL_LIBS_LOCAL = \ #all all-recursive all-am-local all-local: all-fanout-maybe all-recursive: all-fanout-maybe -check-recursive install-recursive: generated-headers-with-a-touch -all check install: all-fanout-cleanup +# Make sure automake-defined check/install goals begin with our hack for +# touch-files for generated headers (so each depending sub-directory does +# not re-evaluate those rules in whole); note that these goals may end up +# also calling all-fanout-maybe (as they depend on "all"): +check-recursive install-recursive: generated-headers-with-a-touch +# NOTE: Beside dependency above, also called from some code paths in +# the all-fanout-maybe implementation: generated-headers-with-a-touch: @dotMAKE@ - $(MAKE) $(AM_MAKEFLAGS) NUT_VERSION_H_GENERATED=false nut_version.h - $(MAKE) $(AM_MAKEFLAGS) touch-include-all-nut_version-generated.timestamp - $(MAKE) $(AM_MAKEFLAGS) libupsclient-version.h - $(MAKE) $(AM_MAKEFLAGS) touch-clients-all-libupsclient_version-generated.timestamp + +$(MAKE) $(AM_MAKEFLAGS) NUT_VERSION_H_GENERATED=false nut_version.h + +$(MAKE) $(AM_MAKEFLAGS) touch-include-all-nut_version-generated.timestamp + +$(MAKE) $(AM_MAKEFLAGS) libupsclient-version.h + +$(MAKE) $(AM_MAKEFLAGS) touch-clients-all-libupsclient_version-generated.timestamp + +# After completing the automake-defined goals, clean up: +all: all-fanout-cleanup +check: check-fanout-cleanup +install: install-fanout-cleanup # Run as part of "all", but after the autotools-standard "all-recursive" # where we quiesce nut_version.h regeneration attempts for each subdir -all-fanout-cleanup: all-recursive +# which automake recipes iterate; similarly for "check" and "install": +cleanup-touchfiles-for-generated-headers: @rm -f include/.all.nut_version-generated.timestamp \ clients/.all.libupsclient_version-generated.timestamp +all-fanout-cleanup: all-recursive @dotMAKE@ + +@$(MAKE) $(AM_MAKEFLAGS) cleanup-touchfiles-for-generated-headers + +check-fanout-cleanup: check-recursive @dotMAKE@ + +@$(MAKE) $(AM_MAKEFLAGS) cleanup-touchfiles-for-generated-headers + +install-fanout-cleanup: install-recursive @dotMAKE@ + +@$(MAKE) $(AM_MAKEFLAGS) cleanup-touchfiles-for-generated-headers + +# Called from generated-headers-with-a-touch: touch-include-all-nut_version-generated.timestamp: @[ -s include/nut_version.h ] @touch -r include/nut_version.h -d '-10 seconds' include/.all.nut_version-generated.timestamp && exit ; \ From e17d8a790def5cef5a583a8036b94dd8aab62a81 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 17:02:21 +0100 Subject: [PATCH 17/29] docs/FAQ.txt: expand the section on driver inability to connect to a device [#3356] Signed-off-by: Jim Klimov --- docs/FAQ.txt | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/FAQ.txt b/docs/FAQ.txt index 9d6baa5e8d..34f131e037 100644 --- a/docs/FAQ.txt +++ b/docs/FAQ.txt @@ -74,6 +74,38 @@ linkman:ups.conf[5]. Just define it before any of your `[sections]`: port = /dev/ttyS0 ---- +Alternately, you can specify a `user=` in the driver section. + +NOTE: The driver program runs with the group account(s) associated with +that user account in the system, and sets its group ID to the primary +group of that account. For serial port access, you may want to add +your 'ups' or 'nut' role account to the 'dialout' group on most Linux +and Unix systems. The `group=` setting in `ups.conf` is for something +else: sharing access to socket files used for communications with +the `upsd` data server, which may run as another different user account. + +*Answer 3* + +Some other programs that you start may steal the port, so the NUT driver +loses a connection (or can never establish one). Revise your device +file system node permissions for corresponding USB or serial ports, +and group membership for NUT driver daemons and other programs in your +system. USB and serial ports might change ownership as the logged-in +console user sessions (physically attached virtual terminals) are +switched on some OS distributions. + +Investigate `udev` or similar framework to assign access permissions +to certain devices on your system, and if you find an offending program +that enumerates all ports thus "stealing" them from NUT and other +consumers, find a way to configure it with a specific port list that +it should use, or to run as a user account that has no access to the +ports you want dedicated to NUT. + +One user report dealt with WINE emulator used for modem programs, so +it was deliberately picking serial port devices and was running as a +member of the `dialout` group. That was an interesting way to shoot +oneself in the foot... + == upsc, upsstats, and the other clients say 'access denied'. The device communication port (serial, USB or network) permissions are fine, so what gives? In this case, "access denied" means the access to linkman:upsd[8], not the From a22f8a932648471d0a3c946bc070554c697df719 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 17:32:08 +0100 Subject: [PATCH 18/29] GitIgnore .all.*-generated.timestamp files [#2871] Signed-off-by: Jim Klimov --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index de8f6c43df..79412200cf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ Makefile Makefile.in tags +.all.*-generated.timestamp ## Parent directory only /aclocal.m4 From 15c2094a2c1b356816a41927d8be19a8a4059c7f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 18:15:10 +0100 Subject: [PATCH 19/29] Makefile.am, docs/man/Makefile.am: apply ticks similar to those for generated headers to generated linkman*.txt snippets, to avoid re-evaluating them verbosely in parallel/recursive builds [#2871] Signed-off-by: Jim Klimov --- Makefile.am | 53 ++++++++++++++++++++++++++++++++++++-------- docs/man/Makefile.am | 41 +++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/Makefile.am b/Makefile.am index 8c83a6bf82..8db3c2ba47 100644 --- a/Makefile.am +++ b/Makefile.am @@ -108,6 +108,8 @@ generated-headers-with-a-touch: @dotMAKE@ +$(MAKE) $(AM_MAKEFLAGS) touch-include-all-nut_version-generated.timestamp +$(MAKE) $(AM_MAKEFLAGS) libupsclient-version.h +$(MAKE) $(AM_MAKEFLAGS) touch-clients-all-libupsclient_version-generated.timestamp + +$(MAKE) $(AM_MAKEFLAGS) NUT_LINKMAN_GENERATED=false prep-linkman-generated + +$(MAKE) $(AM_MAKEFLAGS) touch-docs-man-all-linkman-generated-generated.timestamp # After completing the automake-defined goals, clean up: all: all-fanout-cleanup @@ -119,6 +121,7 @@ install: install-fanout-cleanup # which automake recipes iterate; similarly for "check" and "install": cleanup-touchfiles-for-generated-headers: @rm -f include/.all.nut_version-generated.timestamp \ + docs/man/.all.nut_linkman-generated.timestamp \ clients/.all.libupsclient_version-generated.timestamp all-fanout-cleanup: all-recursive @dotMAKE@ @@ -143,6 +146,16 @@ touch-clients-all-libupsclient_version-generated.timestamp: touch -d '1970-01-01' clients/.all.libupsclient_version-generated.timestamp && exit ; \ touch clients/.all.libupsclient_version-generated.timestamp +touch-docs-man-all-linkman-generated-generated.timestamp: + @[ -s docs/man/linkman-driver-names.txt ] && [ -s docs/man/linkman-drivertool-names.txt ] + @if test -n "`find docs/man/linkman-driver-names.txt -newer docs/man/linkman-drivertool-names.txt`" ; then \ + touch -r docs/man/linkman-drivertool-names.txt -d '-10 seconds' docs/man/.all.nut_linkman-generated.timestamp && exit ; \ + else \ + touch -r docs/man/linkman-driver-names.txt -d '-10 seconds' docs/man/.all.nut_linkman-generated.timestamp && exit ; \ + fi ; \ + touch -d '1970-01-01' docs/man/.all.nut_linkman-generated.timestamp && exit ; \ + touch docs/man/.all.nut_linkman-generated.timestamp + # Verbosity for fanout rule tracing; 0/1 (or "default" that may auto-set # to 0 or 1 in some rules below) SUBDIR_MAKE_VERBOSE = default @@ -364,16 +377,30 @@ all/include: all-libs-local/include @dotMAKE@ +@NUT_VERSION_H_GENERATED=true; export NUT_VERSION_H_GENERATED; \ $(SUBDIR_TGT_RULE) -prep-src-docs/docs/man: @dotMAKE@ - +@SUBDIR_TGT_MAKEFLAGS='MAINTAINER_DOCS_PREP_MAN_DELAY=3'; export SUBDIR_TGT_MAKEFLAGS; \ +# Quickly bail out if somehow recursed into this goal from a dependency +# (looking at all/docs with its several sub-make calls): +prep-linkman-generated/docs/man: @dotMAKE@ + +@if [ x"$(NUT_LINKMAN_GENERATED)" = xtrue ] || [ x"$${NUT_LINKMAN_GENERATED}" = xtrue ] ; then exit 0 ; fi ; \ + SUBDIR_TGT_MAKEFLAGS='NUT_LINKMAN_GENERATED=false'; export SUBDIR_TGT_MAKEFLAGS; \ + NUT_LINKMAN_GENERATED=false; export NUT_LINKMAN_GENERATED; \ + $(SUBDIR_TGT_RULE) || exit ; \ + $(MAKE) $(AM_MAKEFLAGS) touch-docs-man-all-linkman-generated-generated.timestamp + +prep-src-docs/docs/man: prep-linkman-generated/docs/man @dotMAKE@ + +@SUBDIR_TGT_MAKEFLAGS='MAINTAINER_DOCS_PREP_MAN_DELAY=3 NUT_LINKMAN_GENERATED=true'; export SUBDIR_TGT_MAKEFLAGS; \ + NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ $(SUBDIR_TGT_RULE) -prep-src-docs/docs: @dotMAKE@ - +@DOCS_NO_MAN=true; export DOCS_NO_MAN; \ +prep-src-docs/docs: prep-linkman-generated/docs/man @dotMAKE@ + +@SUBDIR_TGT_MAKEFLAGS='NUT_LINKMAN_GENERATED=true'; export SUBDIR_TGT_MAKEFLAGS; \ + DOCS_NO_MAN=true; export DOCS_NO_MAN; \ + NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ $(SUBDIR_TGT_RULE) all/docs/man: prep-src-docs/docs/man @dotMAKE@ - +@$(SUBDIR_TGT_RULE) + +@SUBDIR_TGT_MAKEFLAGS='NUT_LINKMAN_GENERATED=true'; export SUBDIR_TGT_MAKEFLAGS; \ + NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ + $(SUBDIR_TGT_RULE) # Note: we optionally sort of depend on ChangeLog.adoc so it is pre-made and # pre-processed for html/pdf renders (if any are requested), so they surely @@ -382,7 +409,8 @@ all/docs/man: prep-src-docs/docs/man @dotMAKE@ # types are enabled. MAINTAINER_ASCIIDOCS_CHANGELOG_DELAY = 0 all/docs: prep-src-docs/docs/man @dotMAKE@ - +@case "@DOC_BUILD_LIST@" in \ + +@NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ + case "@DOC_BUILD_LIST@" in \ *pdf*|*html-single*|*html-chunked*) \ echo " DOC-CHANGELOG-ASCIIDOC Pre-generate ChangeLog artifacts before the bulk of $@ ..." ; \ MAINTAINER_ASCIIDOCS_CHANGELOG_DELAY="$(MAINTAINER_ASCIIDOCS_CHANGELOG_DELAY)" \ @@ -392,11 +420,14 @@ all/docs: prep-src-docs/docs/man @dotMAKE@ echo " DOC-CHANGELOG-ASCIIDOC Pre-generate ChangeLog artifacts before the bulk of $@ : SUCCESS" ;; \ *) ;; \ esac - +@$(MAKE) $(AM_MAKEFLAGS) prep-src-docs/docs - +@DOCS_NO_MAN=true; export DOCS_NO_MAN; $(SUBDIR_TGT_RULE) + +@NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ + $(MAKE) $(AM_MAKEFLAGS) prep-src-docs/docs + +@NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ + DOCS_NO_MAN=true; export DOCS_NO_MAN; $(SUBDIR_TGT_RULE) all-recursive/docs: all/docs all/docs/man @dotMAKE@ - +@$(SUBDIR_TGT_RULE) + +@NUT_LINKMAN_GENERATED=true; export NUT_LINKMAN_GENERATED; \ + $(SUBDIR_TGT_RULE) # Dependencies below are dictated by who needs whose library from another dir # (generated by a sub-make there, so we pre-emptively ensure it exists to avoid @@ -1109,6 +1140,10 @@ $(abs_top_builddir)/ChangeLog: tools/gitlog2changelog.py dummy-stamp ChangeLog.adoc: ChangeLog @dotMAKE@ +cd $(abs_top_builddir)/docs && $(MAKE) $(AM_MAKEFLAGS) ../ChangeLog.adoc +prep-linkman-generated: @dotMAKE@ + @rm -f docs/man/.all.nut_linkman-generated.timestamp + +cd $(abs_top_builddir)/docs/man && $(MAKE) $(AM_MAKEFLAGS) prep-linkman-generated + nut_version.h include/nut_version.h: @dotMAKE@ @rm -f include/.all.nut_version-generated.timestamp +cd $(abs_top_builddir)/include && $(MAKE) $(AM_MAKEFLAGS) nut_version.h diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 2b31b160ca..278aaec68e 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -1704,15 +1704,43 @@ LINKMAN_INCLUDE_CONSUMERS = index.txt upsd.txt nutupsdrv.txt nut.txt # TOTHINK: Parse sources for `include::` hits dynamically to build (parts of) this regex? LINKMAN_EXCLUDE_NONDRIVER = '^(nutupsdrv|blazer-common|nut_usb_addvars|networked_hostnames)\.txt$$' -CLEANFILES = linkman-*.txt.tmp* +CLEANFILES = linkman-*.txt.tmp* .all.nut_*-generated.timestamp # Note we can have a pre-built file from the tarball, sort of useful when # e.g. no man page building tools are locally available (we might not be # able to render a new instance though). At least do not let '$@' confuse # us into overwriting its instance in srcdir (if differs from builddir). + +# Shared shell snippet to quickly bail out from (re-)building these files +# in a parallelized build/check sequence: +LINKMAN_CHECK_GENERATED = { \ + if [ -s '$@' ] ; then \ + if [ x"$(NUT_LINKMAN_GENERATED)" = xtrue ] || [ x"$${NUT_LINKMAN_GENERATED}" = xtrue ] ; then \ + if [ x"$(MAINTAINER_GENERATE_HEADER_DEBUG)" = xyes ] ; then \ + echo "=== SKIP (include) $@ (NUT_LINKMAN_GENERATED makevar=$(NUT_LINKMAN_GENERATED) shellvar=$${NUT_LINKMAN_GENERATED})" >&2; \ + fi ; \ + exit 0 ; \ + fi ; \ + if test -n "`find '$@' -newer '.all.nut_linkman-generated.timestamp' 2>/dev/null`" \ + && [ x"$(NUT_LINKMAN_GENERATED)" != xfalse ] \ + && [ x"$${NUT_LINKMAN_GENERATED}" != xfalse ] \ + ; then \ + if [ x"$(MAINTAINER_GENERATE_HEADER_DEBUG)" = xyes ] ; then \ + echo "=== SKIP (include) $@ (.all.nut_linkman-generated.timestamp was made in this larger run and is older than the generated file)" >&2; \ + fi ; \ + exit 0 ; \ + fi ; \ + fi; \ + if [ x"$(MAINTAINER_GENERATE_HEADER_DEBUG)" = xyes ] ; then \ + echo " GENERATE-LINKMAN $@ (NUT_LINKMAN_GENERATED makevar=$(NUT_LINKMAN_GENERATED) shellvar=$${NUT_LINKMAN_GENERATED})"; \ + else \ + echo " GENERATE-LINKMAN $@"; \ + fi ; \ +} + linkman-driver-names.txt: Makefile - @echo " GENERATE-LINKMAN $@" - @(LC_ALL=C; LANG=C; export LC_ALL LANG; \ + @$(LINKMAN_CHECK_GENERATED) ; \ + (LC_ALL=C; LANG=C; export LC_ALL LANG; \ for F in $(LINKMAN_PAGES_DRIVERS) ; do echo "$$F" ; done \ | $(EGREP) -v $(LINKMAN_EXCLUDE_NONDRIVER) \ | sort -n | uniq \ @@ -1733,8 +1761,8 @@ linkman-driver-names.txt: Makefile fi linkman-drivertool-names.txt: Makefile - @echo " GENERATE-LINKMAN $@" - @(LC_ALL=C; LANG=C; export LC_ALL LANG; \ + @$(LINKMAN_CHECK_GENERATED) ; \ + (LC_ALL=C; LANG=C; export LC_ALL LANG; \ for F in $(LINKMAN_PAGES_DRIVERTOOLS) ; do echo "$$F" ; done \ | sort -n | uniq \ | sed 's,^\(.*\)\.txt$$,- linkman:\1[$(MAN_SECTION_CMD_SYS)],' ; \ @@ -2140,6 +2168,9 @@ spellcheck spellcheck-interactive spellcheck-sortdict: @dotMAKE@ PREP_SRC = $(LINKMAN_INCLUDE_GENERATED) $(SRC_ALL_PAGES) asciidoc.conf ASCIIDOC_LINKMANEXT_SECTION_REWRITE = @ASCIIDOC_LINKMANEXT_SECTION_REWRITE@ +# Allow this to be called before top-level parallel or recursive build fanout: +prep-linkman-generated: $(LINKMAN_INCLUDE_GENERATED) Makefile + # NOTE: Some "make" implementations prefix a relative or absent path to # the filenames in PREP_SRC, others (e.g. Sun make) prepend the absolute # path to locate the sources, so we end up with bogus trees under docs/. From dd45bc06fed7d5eda910a1070bfb61af14db1292 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 19:52:31 +0100 Subject: [PATCH 20/29] tests/NIT/nit.sh, NEWS.adoc: add optional DUMMY_UPS_SWARM_COUNT support to start hundreds of drivers to see what happens [#3302] Signed-off-by: Jim Klimov --- NEWS.adoc | 5 +++- tests/NIT/nit.sh | 71 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 6adbd1595a..333585ac67 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -341,7 +341,10 @@ but the `nutshutdown` script would bail out quickly and quietly. [PR #3008] operating system allows, by only waiting for that amount of Unix sockets or Windows `HANDLE`'s at a time, and moving on to another chunk. The system-provided value can be further limited by `NUT_SYSMAXCONN_LIMIT` - environment variable (e.g. in tests). [#3302] + environment variable (e.g. in tests). For the other side of the coin, the + NIT script now supports an optional `DUMMY_UPS_SWARM_COUNT` environment + variable which can specify the amount of additional drivers to spawn for + the sake of stress-testing. [#3302] * Extended processing of `CERTREQUEST` setting to handle numeric or specific string values, to match both ways of reading ambiguous documentation. Added `configure --with-ssl-client-validation` toggle to expose the diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 44f64ea9bb..87ab75e546 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -111,6 +111,9 @@ if [ x"${NUT_FOREGROUND_WITH_PID-}" = xtrue ] ; then ARG_FG="-FF" ; fi TABCHAR="`printf '\t'`" +# Special case to launch a lot of drivers and stress-test the select() loops etc. +[ -n "${DUMMY_UPS_SWARM_COUNT}" ] && [ "${DUMMY_UPS_SWARM_COUNT}" -gt 0 ] || DUMMY_UPS_SWARM_COUNT=0 + log_separator() { echo "" >&2 echo "================================" >&2 @@ -447,6 +450,7 @@ PID_UPSSCHED="" PID_DUMMYUPS="" PID_DUMMYUPS1="" PID_DUMMYUPS2="" +PIDS_DUMMYUPS_SWARM="" WITH_SSL_CLIENT="`upsmon -Dh 2>&1 | grep 'Using NUT libupsclient library'`" || WITH_SSL_CLIENT="none" # NOTE: Currently OpenSSL/NSS builds and codepaths are exclusive of each other! @@ -779,10 +783,10 @@ stop_daemons() { PID_UPSSCHED_NOW="`head -1 \"$NUT_PIDPATH/upssched.pid\"`" fi - if [ -n "$PID_UPSD$PID_UPSMON$PID_DUMMYUPS$PID_DUMMYUPS1$PID_DUMMYUPS2$PID_UPSSCHED$PID_UPSSCHED_NOW" ] ; then + if [ -n "$PID_UPSD$PID_UPSMON$PID_DUMMYUPS$PID_DUMMYUPS1$PID_DUMMYUPS2$PIDS_DUMMYUPS_SWARM$PID_UPSSCHED$PID_UPSSCHED_NOW" ] ; then log_info "Stopping test daemons" - kill -15 $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PID_UPSSCHED $PID_UPSSCHED_NOW 2>/dev/null || return 0 - wait $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PID_UPSSCHED $PID_UPSSCHED_NOW || true + kill -15 $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PIDS_DUMMYUPS_SWARM $PID_UPSSCHED $PID_UPSSCHED_NOW 2>/dev/null || return 0 + wait $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PIDS_DUMMYUPS_SWARM $PID_UPSSCHED $PID_UPSSCHED_NOW || true fi PID_UPSD="" @@ -791,6 +795,7 @@ stop_daemons() { PID_DUMMYUPS="" PID_DUMMYUPS1="" PID_DUMMYUPS2="" + PIDS_DUMMYUPS_SWARM="" unset PID_UPSSCHED_NOW } @@ -1556,8 +1561,38 @@ EOF mv -f "$F.bak" "$F" ${EGREP} '^ups.status:' "$F" >/dev/null || { echo "ups.status: OL BOOST" >> "$F"; } done - fi + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 0 ] ; then + log_info "Adding a swarm of ${DUMMY_UPS_SWARM_COUNT} drivers" + for N in `seq 1 $DUMMY_UPS_SWARM_COUNT` ; do + case "`expr $N % 3`" in + 0) cat << EOF +[UPSwarm$N] + driver = dummy-ups + desc = "Example event sequence" + port = evolution500.seq +EOF + ;; + 1) cat << EOF +[UPSwarm$N] + driver = dummy-ups + desc = "Example ePDU data dump" + port = epdu-managed.dev + mode = dummy-once +EOF + ;; + 2) cat << EOF +[UPSwarm$N] + driver = dummy-ups + desc = "Example ePDU data dump (loop)" + port = epdu-managed.dev + mode = dummy-loop +EOF + ;; + esac + done >> "$NUT_CONFPATH/ups.conf" + fi + fi } ##################################################### @@ -1567,6 +1602,21 @@ isPidAlive() { [ -d "/proc/$1" ] || kill -0 "$1" 2>/dev/null } +arePidsAlive() { + _DEAD="" + for _PID in "$@" ; do + isPidAlive "$_PID" || _DEAD="${_DEAD} $_PID" + done + unset _PID + if [ -n "${_DEAD}" ]; then + log_error "[arePidsAlive] Some are dead:${_DEAD}" + unset _DEAD + return 1 + fi + unset _DEAD + return 0 +} + FAILED=0 FAILED_FUNCS="" PASSED=0 @@ -1833,7 +1883,7 @@ sandbox_start_upsd() { sandbox_start_drivers() { if isPidAlive "$PID_DUMMYUPS" \ - && { [ x"${TOP_SRCDIR}" != x ] && isPidAlive "$PID_DUMMYUPS1" && isPidAlive "$PID_DUMMYUPS2" \ + && { [ x"${TOP_SRCDIR}" != x ] && arePidsAlive "$PID_DUMMYUPS1" "$PID_DUMMYUPS2" $PIDS_DUMMYUPS_SWARM \ || [ x"${TOP_SRCDIR}" = x ] ; } \ ; then # All drivers expected for this environment are already running @@ -1860,6 +1910,15 @@ sandbox_start_drivers() { execcmd dummy-ups -a UPS2 ${ARG_USER} ${ARG_FG} & PID_DUMMYUPS2="$!" log_debug "Tried to start dummy-ups driver for 'UPS2' as PID $PID_DUMMYUPS2" + + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 0 ] ; then + log_info "Starting a swarm of ${DUMMY_UPS_SWARM_COUNT} drivers" + for N in `seq 1 $DUMMY_UPS_SWARM_COUNT` ; do + execcmd dummy-ups -a UPSwarm$N ${ARG_USER} ${ARG_FG} & + PIDS_DUMMYUPS_SWARM="$PIDS_DUMMYUPS_SWARM $!" + log_debug "Tried to start dummy-ups driver for 'UPSwarm$N' as PID $!" + done + fi fi NUT_DEBUG_LEVEL="${NUT_DEBUG_LEVEL_ORIG}" @@ -1870,7 +1929,7 @@ sandbox_start_drivers() { fi if isPidAlive "$PID_DUMMYUPS" \ - && { [ x"${TOP_SRCDIR}" != x ] && isPidAlive "$PID_DUMMYUPS1" && isPidAlive "$PID_DUMMYUPS2" \ + && { [ x"${TOP_SRCDIR}" != x ] && arePidsAlive "$PID_DUMMYUPS1" "$PID_DUMMYUPS2" $PIDS_DUMMYUPS_SWARM \ || [ x"${TOP_SRCDIR}" = x ] ; } \ ; then # All drivers expected for this environment are already running From e56bb0a7d82369f047a84dce65f9fdb71b5c2775 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 20:37:16 +0100 Subject: [PATCH 21/29] drivers/upsdrvctl.c: more logging around forkexec() and sleeps/waits involved [#3302] Signed-off-by: Jim Klimov --- drivers/upsdrvctl.c | 63 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/drivers/upsdrvctl.c b/drivers/upsdrvctl.c index 8b1ee9a9db..dfa95f2b75 100644 --- a/drivers/upsdrvctl.c +++ b/drivers/upsdrvctl.c @@ -878,11 +878,26 @@ static void forkexec(char *const argv[], const ups_t *ups) /* Use the local maxstartdelay, if available */ if (ups->maxstartdelay != -1) { - if (ups->maxstartdelay >= 0) + if (ups->maxstartdelay >= 0) { + upsdebugx(2, "%s[POSIX]: will wait for %u seconds " + "to check that driver survived this long " + "(per device configuration section)", + __func__, (unsigned int)ups->maxstartdelay); alarm((unsigned int)ups->maxstartdelay); + } } else { /* Otherwise, use the global (or default) value */ - if (maxstartdelay >= 0) + if (maxstartdelay >= 0) { + upsdebugx(2, "%s[POSIX]: will wait for %u seconds " + "to check that driver survived this long " + "(per global configuration section)", + __func__, (unsigned int)maxstartdelay); alarm((unsigned int)maxstartdelay); + } else { + upsdebugx(2, "%s[POSIX]: will NOT wait " + "to check that driver survived this long " + "(not required by global nor by device " + "configuration sections)", __func__); + } } waitret = waitpid(pid, &wstat, 0); @@ -946,7 +961,7 @@ static void forkexec(char *const argv[], const ups_t *ups) char commandline[LARGEBUF]; STARTUPINFO StartupInfo; PROCESS_INFORMATION ProcessInformation; - int i = 1; + int i = 1, waited = 0; memset(&StartupInfo, 0, sizeof(STARTUPINFO)); @@ -997,8 +1012,38 @@ static void forkexec(char *const argv[], const ups_t *ups) * Unlike under Linux, Windows spawn drivers directly. If the driver is alive, all is OK. * An optimization can probably be implemented to prevent waiting so much time when all is OK. */ - res = WaitForSingleObject(ProcessInformation.hProcess, - (ups->maxstartdelay!=-1?ups->maxstartdelay:maxstartdelay)*1000); + + /* Use the local maxstartdelay, if available */ + if (ups->maxstartdelay != -1) { + if (ups->maxstartdelay >= 0) { + upsdebugx(2, "%s[WIN32]: will wait for %u seconds " + "to check that driver survived this long " + "(per device configuration section)", + __func__, (unsigned int)ups->maxstartdelay); + res = WaitForSingleObject(ProcessInformation.hProcess, + ((unsigned int)ups->maxstartdelay) * 1000); + waited = 1; + } + } else { /* Otherwise, use the global (or default) value */ + if (maxstartdelay >= 0) { + upsdebugx(2, "%s[WIN32]: will wait for %u seconds " + "to check that driver survived this long " + "(per global configuration section)", + __func__, (unsigned int)maxstartdelay); + res = WaitForSingleObject(ProcessInformation.hProcess, + ((unsigned int)maxstartdelay) * 1000); + waited = 1; + } + } + + if (!waited) { + upsdebugx(2, "%s[WIN32]: will NOT wait " + "to check that driver survived this long " + "(not required by global nor by device " + "configuration sections)", __func__); + res = WaitForSingleObject(ProcessInformation.hProcess, + 0); + } if (res != WAIT_TIMEOUT) { GetExitCodeProcess( ProcessInformation.hProcess, &exit_code ); @@ -1370,12 +1415,13 @@ static void start_driver(const ups_t *ups) int cur_exec_error = exec_error; int cur_exec_timeout = exec_timeout; - upsdebugx(2, "%i remaining attempts", drv_maxretry); + upsdebugx(2, "%s: %i remaining attempts", __func__, drv_maxretry); debugcmdline(2, "exec: ", argv); drv_maxretry--; if (!testmode) { forkexec(argv, ups); + upsdebugx(3, "%s: forkexec() finished", __func__); } /* driver command succeeded */ @@ -1387,8 +1433,11 @@ static void start_driver(const ups_t *ups) else { /* otherwise, retry if still needed */ if (drv_maxretry > 0) - if (drv_retrydelay >= 0) + if (drv_retrydelay >= 0) { + upsdebugx(3, "%s: retrying after %u seconds", + __func__, (unsigned int)drv_retrydelay); sleep ((unsigned int)drv_retrydelay); + } } } } From 82dfe9c5d4af37b327163653021278022b257e0b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 20:56:00 +0100 Subject: [PATCH 22/29] tests/NIT/nit.sh: customize a short maxstartdelay=1 for upsdrvctl [#3302] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 87ab75e546..a310867c22 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -1475,7 +1475,9 @@ EOF generatecfg_ups_trivial() { # Populate the configs for the run - ( echo 'maxretry = 3' > "$NUT_CONFPATH/ups.conf" || exit + ( # Hints primarily for upsdrvctl: + echo 'maxretry = 3' > "$NUT_CONFPATH/ups.conf" || exit + echo 'maxstartdelay = 1' >> "$NUT_CONFPATH/ups.conf" || exit if [ x"${ABS_TOP_BUILDDIR}" != x ]; then # NOTE: Windows backslashes are pre-escaped in the configure-generated value case "${ABS_TOP_BUILDDIR}" in From a882c760d50991ef678ced0b4551a7def602988c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 21:32:02 +0100 Subject: [PATCH 23/29] server/upsd.c: fix looping over more than MAXSYSCONN FDs [#3302] Signed-off-by: Jim Klimov --- server/upsd.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/upsd.c b/server/upsd.c index b843f1b5e1..4b1633f434 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -2084,13 +2084,16 @@ static void mainloop(void) for (chunk = 0; chunk < chunks; chunk++) { upsdebugx(5, "%s: chunked filedescriptor polling #%" PRIuSIZE - " of %" PRIuSIZE " chunks, with %" PRIu64 " hits so far", - __func__, chunk, chunks, ret); + " of %" PRIuSIZE " chunks", + __func__, chunk, chunks, ret == WAIT_TIMEOUT ? 0 : ret); tmpret = WaitForMultipleObjects( (last_chunk && chunk == chunks - 1 ? last_chunk : sysmaxconn), &fds[chunk * sysmaxconn], FALSE, poll_TO); if (tmpret != WAIT_TIMEOUT) { + /* NOTE: Actual offset depends on this value + * being in ABANDONED or OBJECT range, further + * shifted for array lookup by N chunks */ ret = tmpret; break; } @@ -2122,18 +2125,19 @@ static void mainloop(void) #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE # pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" #endif + /* https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects */ if (ret >= WAIT_ABANDONED_0 && ret <= WAIT_ABANDONED_0 + (nfds < sysmaxconn ? nfds : sysmaxconn) - 1) { /* One abandoned mutex object that satisfied the wait? */ - ret = ret - WAIT_ABANDONED_0; - upsdebugx(5, "%s: got abandoned FD array item: %" PRIu64, __func__, nfds, ret); + ret = ret - WAIT_ABANDONED_0 + chunk * sysmaxconn; + upsdebugx(5, "%s: got abandoned FD array item: %" PRIu64 " of %" PRIu64, __func__, ret, nfds - 1); /* FIXME: Should this be handled somehow? Cleanup? Abort?.. */ } else - if (ret >= WAIT_OBJECT_0 && ret <= WAIT_OBJECT_0 + nfds - 1) { + if (ret >= WAIT_OBJECT_0 && ret <= WAIT_OBJECT_0 + (nfds < sysmaxconn ? nfds : sysmaxconn) - 1) { /* Which one handle was triggered this time? */ /* Note: WAIT_OBJECT_0 may be currently defined as 0, * but docs insist on checking and shifting the range */ ret = ret - WAIT_OBJECT_0 + chunk * sysmaxconn; - upsdebugx(5, "%s: got event on FD array item: %" PRIu64, __func__, nfds, ret); + upsdebugx(5, "%s: got event on FD array item: %" PRIu64 " of %" PRIu64, __func__, ret, nfds - 1); } #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) ) # pragma GCC diagnostic pop From 024b32856dbbd384cad875cd3093db2091744f08 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 21:32:45 +0100 Subject: [PATCH 24/29] tests/NIT/nit.sh: bump MAXCONN in upsd.conf with non-trivial DUMMY_UPS_SWARM_COUNT [#3302] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index a310867c22..d0bee0c3e4 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -1176,6 +1176,11 @@ EOF if [ -n "${NUT_DEBUG_MIN-}" ] ; then echo "DEBUG_MIN ${NUT_DEBUG_MIN}" >> "$NUT_CONFPATH/upsd.conf" || exit fi + + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 5 ] ; then + # Enable select-group looping (especially on Windows with sysmaxconn=64): + echo "MAXCONN `expr $DUMMY_UPS_SWARM_COUNT + 20`" >> "$NUT_CONFPATH/upsd.conf" || exit + fi } generatecfg_upsd_nodev() { From 7624ff4fac7f4df7644df5e176db3a151f26f611 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 21 Mar 2026 21:44:54 +0100 Subject: [PATCH 25/29] tests/NIT/nit.sh: adjust testcase expectations to possibility of DUMMY_UPS_SWARM_COUNT>0 [#3302] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index d0bee0c3e4..b09a96f373 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -1958,6 +1958,12 @@ testcase_sandbox_start_upsd_alone() { EXPECTED_UPSLIST="$EXPECTED_UPSLIST UPS1 UPS2" + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 0 ] ; then + for N in `seq 1 $DUMMY_UPS_SWARM_COUNT` ; do + EXPECTED_UPSLIST="$EXPECTED_UPSLIST +UPSwarm$N" + done + fi # For windows runners (strip CR if any): EXPECTED_UPSLIST="`echo \"$EXPECTED_UPSLIST\" | tr -d '\r'`" fi @@ -1968,6 +1974,18 @@ UPS2" EXPECTED_UPSLIST_JSON="${EXPECTED_UPSLIST_JSON},"' "UPS1", "UPS2"' + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 0 ] ; then + EXPECTED_UPSLIST_JSON="$EXPECTED_UPSLIST_JSON," + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 1 ] ; then + DUMMY_UPS_SWARM_COUNT_1="`expr $DUMMY_UPS_SWARM_COUNT - 1`" + for N in `seq 1 $DUMMY_UPS_SWARM_COUNT_1` ; do + EXPECTED_UPSLIST_JSON="$EXPECTED_UPSLIST_JSON + \"UPSwarm$N\"," + done + fi + EXPECTED_UPSLIST_JSON="$EXPECTED_UPSLIST_JSON + \"UPSwarm${DUMMY_UPS_SWARM_COUNT}\"" + fi fi EXPECTED_UPSLIST_JSON="${EXPECTED_UPSLIST_JSON}"' ]' @@ -2718,7 +2736,7 @@ testcase_sandbox_nutscanner_list() { if [ x"${TOP_SRCDIR}" = x ]; then PORTS_WANT=1 else - PORTS_WANT=3 + PORTS_WANT="`expr 3 + $DUMMY_UPS_SWARM_COUNT`" fi PORTS_SEEN="`echo \"$CMDOUT\" | ${EGREP} -c 'port *='`" From 8f8c3801b26f200e32e4073540e2764769064500 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 01:38:21 +0100 Subject: [PATCH 26/29] common/common.c: vupslog(): Ensure line buffering for sane logs on Windows console Signed-off-by: Jim Klimov --- common/common.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/common/common.c b/common/common.c index aec564876c..2a487409ee 100644 --- a/common/common.c +++ b/common/common.c @@ -1,7 +1,7 @@ /* common.c - common useful functions Copyright (C) 2000 Russell Kroll - Copyright (C) 2021-2025 Jim Klimov + 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 @@ -3961,6 +3961,14 @@ static void vupslog(int priority, const char *fmt, va_list va, int use_strerror) gettimeofday(&now, NULL); upslog_start = now; + +#ifdef WIN32 + /* Ensure line buffering for sane logs on Windows console + * especially when many threads/daemons write there. */ + setvbuf(stderr, NULL, _IOLBF, BUFSIZ); + /* Also stdout (some messages go there) for good measure: */ + setvbuf(stdout, NULL, _IOLBF, BUFSIZ); +#endif } if (xbit_test(upslog_flags, UPSLOG_STDERR) || xbit_test(upslog_flags, UPSLOG_STDOUT)) { From 4a857fc96db3a2cc2fae440f8b266c6731342dd6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 01:58:47 +0100 Subject: [PATCH 27/29] tests/NIT/nit.sh, NEWS.adoc: introduce `UPSLOG_SWARM_COUNT` for clients [#3302] Signed-off-by: Jim Klimov --- NEWS.adoc | 2 +- tests/NIT/nit.sh | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 333585ac67..2b470bbd24 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -344,7 +344,7 @@ but the `nutshutdown` script would bail out quickly and quietly. [PR #3008] environment variable (e.g. in tests). For the other side of the coin, the NIT script now supports an optional `DUMMY_UPS_SWARM_COUNT` environment variable which can specify the amount of additional drivers to spawn for - the sake of stress-testing. [#3302] + the sake of stress-testing, and `UPSLOG_SWARM_COUNT` for clients. [#3302] * Extended processing of `CERTREQUEST` setting to handle numeric or specific string values, to match both ways of reading ambiguous documentation. Added `configure --with-ssl-client-validation` toggle to expose the diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index b09a96f373..68d6e9a422 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -113,6 +113,7 @@ TABCHAR="`printf '\t'`" # Special case to launch a lot of drivers and stress-test the select() loops etc. [ -n "${DUMMY_UPS_SWARM_COUNT}" ] && [ "${DUMMY_UPS_SWARM_COUNT}" -gt 0 ] || DUMMY_UPS_SWARM_COUNT=0 +[ -n "${UPSLOG_SWARM_COUNT}" ] && [ "${UPSLOG_SWARM_COUNT}" -gt 0 ] || UPSLOG_SWARM_COUNT=0 log_separator() { echo "" >&2 @@ -451,6 +452,7 @@ PID_DUMMYUPS="" PID_DUMMYUPS1="" PID_DUMMYUPS2="" PIDS_DUMMYUPS_SWARM="" +PIDS_UPSLOG_SWARM="" WITH_SSL_CLIENT="`upsmon -Dh 2>&1 | grep 'Using NUT libupsclient library'`" || WITH_SSL_CLIENT="none" # NOTE: Currently OpenSSL/NSS builds and codepaths are exclusive of each other! @@ -783,10 +785,10 @@ stop_daemons() { PID_UPSSCHED_NOW="`head -1 \"$NUT_PIDPATH/upssched.pid\"`" fi - if [ -n "$PID_UPSD$PID_UPSMON$PID_DUMMYUPS$PID_DUMMYUPS1$PID_DUMMYUPS2$PIDS_DUMMYUPS_SWARM$PID_UPSSCHED$PID_UPSSCHED_NOW" ] ; then + if [ -n "$PID_UPSD$PID_UPSMON$PID_DUMMYUPS$PID_DUMMYUPS1$PID_DUMMYUPS2$PIDS_DUMMYUPS_SWARM$PIDS_UPSLOG_SWARM$PID_UPSSCHED$PID_UPSSCHED_NOW" ] ; then log_info "Stopping test daemons" - kill -15 $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PIDS_DUMMYUPS_SWARM $PID_UPSSCHED $PID_UPSSCHED_NOW 2>/dev/null || return 0 - wait $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PIDS_DUMMYUPS_SWARM $PID_UPSSCHED $PID_UPSSCHED_NOW || true + kill -15 $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PIDS_DUMMYUPS_SWARM $PIDS_UPSLOG_SWARM $PID_UPSSCHED $PID_UPSSCHED_NOW 2>/dev/null || return 0 + wait $PID_UPSD $PID_UPSMON $PID_DUMMYUPS $PID_DUMMYUPS1 $PID_DUMMYUPS2 $PIDS_DUMMYUPS_SWARM $PIDS_UPSLOG_SWARM $PID_UPSSCHED $PID_UPSSCHED_NOW || true fi PID_UPSD="" @@ -796,6 +798,7 @@ stop_daemons() { PID_DUMMYUPS1="" PID_DUMMYUPS2="" PIDS_DUMMYUPS_SWARM="" + PIDS_UPSLOG_SWARM="" unset PID_UPSSCHED_NOW } @@ -1177,9 +1180,9 @@ EOF echo "DEBUG_MIN ${NUT_DEBUG_MIN}" >> "$NUT_CONFPATH/upsd.conf" || exit fi - if [ "$DUMMY_UPS_SWARM_COUNT" -gt 5 ] ; then + if [ "$DUMMY_UPS_SWARM_COUNT" -gt 5 ] || [ "$UPSLOG_SWARM_COUNT" -gt 5 ] ; then # Enable select-group looping (especially on Windows with sysmaxconn=64): - echo "MAXCONN `expr $DUMMY_UPS_SWARM_COUNT + 20`" >> "$NUT_CONFPATH/upsd.conf" || exit + echo "MAXCONN `expr $DUMMY_UPS_SWARM_COUNT + $UPSLOG_SWARM_COUNT + 30`" >> "$NUT_CONFPATH/upsd.conf" || exit fi } @@ -2310,6 +2313,16 @@ testcase_sandbox_upsc_query_timer() { PID_UPSLOG="$!" NUT_DEBUG_LEVEL="${NUT_DEBUG_LEVEL_ORIG}" + # No timeout, no kill - keep them running if requested (trap exit): + if [ "$UPSLOG_SWARM_COUNT" -gt 0 ] ; then + log_info "Starting a swarm of ${UPSLOG_SWARM_COUNT} clients" + for N in `seq 1 $UPSLOG_SWARM_COUNT` ; do + execcmd upslog -F -i 1 -N -m "*@localhost:${NUT_PORT},${NUT_STATEPATH}/upslog-dummy-$N.log" & + PIDS_UPSLOG_SWARM="$PIDS_UPSLOG_SWARM $!" + log_debug "Tried to start upslog as PID $!" + done + fi + # TODO: Any need to convert to runcmd()? OUT1="`execcmd upsc dummy@localhost:$NUT_PORT ups.status`" || die "[testcase_sandbox_upsc_query_timer] upsd does not respond on port ${NUT_PORT} ($?): $OUT1" ; sleep 3 OUT2="`execcmd upsc dummy@localhost:$NUT_PORT ups.status`" || die "[testcase_sandbox_upsc_query_timer] upsd does not respond on port ${NUT_PORT} ($?): $OUT2" From 5208ac5031cfec0b4a61a91854e87dc08258ad03 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 22 Mar 2026 23:36:23 +0100 Subject: [PATCH 28/29] server/upsd.c: mainloop(): revise chunked select - first try with zero timeout (pick already waiting connections), only then try time-waited select [#3302] Signed-off-by: Jim Klimov --- server/upsd.c | 100 +++++++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/server/upsd.c b/server/upsd.c index 4b1633f434..14b4ffec47 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -1713,34 +1713,38 @@ static void mainloop(void) */ size_t last_chunk = nfds % sysmaxconn, chunk, chunks = nfds / sysmaxconn + (last_chunk ? 1 : 0); - int poll_TO = 2000 / chunks, tmpret; + int poll_TO, poll_TO_chunk = 2000 / chunks, tmpret; - if (poll_TO < 10) - poll_TO = 10; - - upsdebugx(4, "%s: chunked filedescriptor polling via %" PRIuSIZE - " chunks, last one sized %" PRIuSIZE - ", with timeout of %d msec per chunk", - __func__, chunks, last_chunk, poll_TO); + if (poll_TO_chunk < 10) + poll_TO_chunk = 10; ret = 0; - for (chunk = 0; chunk < chunks; chunk++) { - upsdebugx(5, - "%s: chunked filedescriptor polling #%" PRIuSIZE - " of %" PRIuSIZE " chunks, with %d hits so far", - __func__, chunk, chunks, ret); - tmpret = poll(&fds[chunk * sysmaxconn], - (last_chunk && chunk == chunks - 1 ? last_chunk : sysmaxconn), - poll_TO); - if (tmpret < 0) { - upsdebug_with_errno(2, - "%s: failed during chunked polling, handled %" PRIuSIZE - " of %" PRIuSIZE " chunks so far, with %d hits", + /* First run a quick check if anyone is already waiting + * (especially in non-first chunks), then a loop with waits */ + for (poll_TO = 0; poll_TO <= poll_TO_chunk && ret == 0; poll_TO += poll_TO_chunk) { + upsdebugx(4, "%s: chunked filedescriptor polling via %" PRIuSIZE + " chunks, last one sized %" PRIuSIZE + ", with timeout of %d msec per chunk", + __func__, chunks, last_chunk, poll_TO); + + for (chunk = 0; chunk < chunks; chunk++) { + upsdebugx(5, + "%s: chunked filedescriptor polling #%" PRIuSIZE + " of %" PRIuSIZE " chunks, with %d hits so far", __func__, chunk, chunks, ret); - ret = tmpret; - break; + tmpret = poll(&fds[chunk * sysmaxconn], + (last_chunk && chunk == chunks - 1 ? last_chunk : sysmaxconn), + poll_TO); + if (tmpret < 0) { + upsdebug_with_errno(2, + "%s: failed during chunked polling, handled %" PRIuSIZE + " of %" PRIuSIZE " chunks so far, with %d hits", + __func__, chunk, chunks, ret); + ret = tmpret; + break; + } + ret += tmpret; } - ret += tmpret; } } @@ -2070,32 +2074,36 @@ static void mainloop(void) */ size_t last_chunk = nfds % sysmaxconn, chunks = nfds / sysmaxconn + (last_chunk ? 1 : 0); - DWORD poll_TO = 2000 / chunks, tmpret; + DWORD poll_TO, poll_TO_chunk = 2000 / chunks, tmpret; - if (poll_TO < 10) - poll_TO = 10; - - upsdebugx(4, "%s: chunked filedescriptor polling via %" PRIuSIZE - " chunks, last one sized %" PRIuSIZE - ", with timeout of %" PRIi64 " msec per chunk", - __func__, chunks, last_chunk, poll_TO); + if (poll_TO_chunk < 10) + poll_TO_chunk = 10; ret = WAIT_TIMEOUT; - for (chunk = 0; chunk < chunks; chunk++) { - upsdebugx(5, - "%s: chunked filedescriptor polling #%" PRIuSIZE - " of %" PRIuSIZE " chunks", - __func__, chunk, chunks, ret == WAIT_TIMEOUT ? 0 : ret); - tmpret = WaitForMultipleObjects( - (last_chunk && chunk == chunks - 1 ? last_chunk : sysmaxconn), - &fds[chunk * sysmaxconn], - FALSE, poll_TO); - if (tmpret != WAIT_TIMEOUT) { - /* NOTE: Actual offset depends on this value - * being in ABANDONED or OBJECT range, further - * shifted for array lookup by N chunks */ - ret = tmpret; - break; + /* First run a quick check if anyone is already waiting + * (especially in non-first chunks), then a loop with waits */ + for (poll_TO = 0; poll_TO <= poll_TO_chunk && ret == WAIT_TIMEOUT; poll_TO += poll_TO_chunk) { + upsdebugx(4, "%s: chunked filedescriptor polling via %" PRIuSIZE + " chunks, last one sized %" PRIuSIZE + ", with timeout of %" PRIi64 " msec per chunk", + __func__, chunks, last_chunk, poll_TO); + + for (chunk = 0; chunk < chunks; chunk++) { + upsdebugx(5, + "%s: chunked filedescriptor polling #%" PRIuSIZE + " of %" PRIuSIZE " chunks", + __func__, chunk, chunks); + tmpret = WaitForMultipleObjects( + (last_chunk && chunk == chunks - 1 ? last_chunk : sysmaxconn), + &fds[chunk * sysmaxconn], + FALSE, poll_TO); + if (tmpret != WAIT_TIMEOUT) { + /* NOTE: Actual offset depends on this value + * being in ABANDONED or OBJECT range, further + * shifted for array lookup by N chunks */ + ret = tmpret; + break; + } } } } From eb16616792d02a4ccb7af0d9461366d2ffe85c18 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 23 Mar 2026 17:10:29 +0100 Subject: [PATCH 29/29] common/common.c: background(): when reporting "Startup successful", also report getmyprocbasename() [#1711] In particular, help discern replies from the drivers launched by upsdrvctl en masse. Signed-off-by: Jim Klimov --- common/common.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/common.c b/common/common.c index 2a487409ee..35da0c6f99 100644 --- a/common/common.c +++ b/common/common.c @@ -47,6 +47,9 @@ # include #endif +static const char * getmyprocname(void); +static const char * getmyprocbasename(void); + #if (defined WITH_LIBSYSTEMD_INHIBITOR) && (defined WITH_LIBSYSTEMD && WITH_LIBSYSTEMD) && (defined WITH_LIBSYSTEMD_INHIBITOR && WITH_LIBSYSTEMD_INHIBITOR) && !(defined(WITHOUT_LIBSYSTEMD) && (WITHOUT_LIBSYSTEMD)) # ifdef HAVE_SYSTEMD_SD_BUS_H # include @@ -760,7 +763,7 @@ void background(void) NUT_WIN32_INCOMPLETE_MAYBE_NOT_APPLICABLE(); #endif /* WIN32 */ - upslogx(LOG_INFO, "Startup successful"); + upslogx(LOG_INFO, "Startup successful: %s", getmyprocbasename()); } /* do this here to keep pwd/grp stuff out of the main files */