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 diff --git a/Makefile.am b/Makefile.am index 1622c034ed..8db3c2ba47 100644 --- a/Makefile.am +++ b/Makefile.am @@ -94,22 +94,46 @@ 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 + +$(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 +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 \ + docs/man/.all.nut_linkman-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 ; \ @@ -122,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 @@ -134,7 +168,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 +176,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 +268,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 ; \ ) @@ -343,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 @@ -361,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)" \ @@ -371,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 @@ -761,7 +813,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=$$? ; \ @@ -781,8 +833,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 ; \ @@ -1087,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/NEWS.adoc b/NEWS.adoc index e308d6fc79..d9de4084e0 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -46,6 +46,11 @@ https://github.com/networkupstools/nut/milestone/12 - common code: * Introduced `setproctag()` and `getproctag()` (see examples in `upsmon`) to help track the log messages from massively-forking NUT daemons. [#3084] + * Introduced `NUT_DEBUG_PROCNAME` environment variable support to optionally + log also the process name (or however the program chose to identify itself). + This may be useful when multiple NUT daemons log into the same file or + console, without syslog to prefix the name into each line (e.g. in tests, + single init script systems like Home Assistant, etc.) [PR 3368] * Extended with plural `checkprocnames()` and `compareprocnames()`, as well as `sendsignalpidaliases()` and `sendsignalfnaliases()`, for binaries that expect to have one of several names at the moment (e.g. @@ -86,6 +91,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] @@ -327,7 +336,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] @@ -337,7 +346,10 @@ https://github.com/networkupstools/nut/milestone/12 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, 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 @@ -502,6 +514,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/clients/Makefile.am b/clients/Makefile.am index 36a59ec8b4..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 - AM_CXXFLAGS += $(LIBSSL_CFLAGS) -endif WITH_SSL +if WITH_SSL_CXX + AM_CXXFLAGS += $(LIBSSL_CXXFLAGS) +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..821c296d12 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/) */ @@ -258,11 +260,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 +275,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 +289,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 +308,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 +329,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,22 +430,36 @@ 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 */ { + /* Initialize timeout from envvar NUT_DEFAULT_CONNECT_TIMEOUT if present */ + char *s = getenv("NUT_DEFAULT_CONNECT_TIMEOUT"); + _tv.tv_sec = -1; + if (s) { + long l = atol(s); + if (l > 0) + _tv.tv_sec = l; + } + _tv.tv_usec = 0; } @@ -761,17 +782,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 +806,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 +817,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 +845,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 +875,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 +888,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 +900,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 +912,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 +941,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 +951,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 +973,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 +1050,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 +1060,7 @@ void Socket::startTLS() disconnect(); throw nut::SSLException("SSL support not compiled in"); } -#endif +#endif /* WITH_SSL_CXX */ } bool Socket::isConnected()const @@ -1061,7 +1087,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 +1128,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 +1346,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..20c6d7a2d1 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -29,13 +29,15 @@ /* 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 */ +#endif /* WITH_SSL_CXX */ #include #include diff --git a/clients/upsc.c b/clients/upsc.c index eaca4d834d..91a746612d 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -380,6 +380,10 @@ static void clean_exit(void) free(upsname); free(hostname); free(ups); + + /* Not a sub-process (do not let common::proctag_cleanup() mis-report us as such) */ + upsdebugx(1, "%s: finished, exiting", __func__); + setproctag(NULL); } int main(int argc, char **argv) @@ -391,6 +395,7 @@ int main(int argc, char **argv) const char *net_connect_timeout = NULL; char *s = NULL; + setproctag(prog); /* 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 @@ -466,6 +471,7 @@ int main(int argc, char **argv) fatalx_error_json_simple(0, "invalid UPS definition.\nRequired format: upsname[@hostname[:port]]"); } } + setproctag(argv[0]); upsdebugx(1, "upsname='%s' hostname='%s' port='%" PRIu16 "'", NUT_STRARG(upsname), NUT_STRARG(hostname), port); diff --git a/clients/upsclient.c b/clients/upsclient.c index 6af58cb62e..b248cc6713 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -2007,6 +2007,8 @@ int upscli_splitaddr(const char *buf, char **hostname, uint16_t *port) int upscli_disconnect(UPSCONN_t *ups) { + char tmp[UPSCLI_NETBUF_LEN]; + if (!ups) { return -1; } @@ -2026,6 +2028,23 @@ int upscli_disconnect(UPSCONN_t *ups) net_write(ups, "LOGOUT\n", 7, 0); + /* Give it a bit of time to gracefully close connections, + * drain the buffer and avoid noise in logs of upsd like: + * write() failed for 127.0.0.1: Transport endpoint is not connected + */ + if (net_read(ups, tmp, sizeof(tmp), 5) > 0) { + if (!strcmp(tmp, "OK Goodbye")) { + /* There may be trailing garbage from the buffer after the newline, not sure why */ + upsdebugx(1, "%s: We logged out, and server said '%s' nicely, as expected", __func__, tmp); + } else if (!strncmp(tmp, "OK", 2)) { + upsdebugx(1, "%s: We logged out, and server said '%s' nicely, good enough", __func__, tmp); + } else { + upsdebugx(1, "%s: We logged out, and server said '%s', not OK but oh well", __func__, tmp); + } + } else { + upsdebugx(1, "%s: We logged out, and server did not reply in a short time frame", __func__); + } + #ifdef WITH_OPENSSL if (ups->ssl) { SSL_shutdown(ups->ssl); diff --git a/clients/upscmd.c b/clients/upscmd.c index 0206918595..05fc13d1d7 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -286,6 +286,10 @@ static void clean_exit(void) free(upsname); free(hostname); free(ups); + + /* Not a sub-process (do not let common::proctag_cleanup() mis-report us as such) */ + upsdebugx(1, "%s: finished, exiting", __func__); + setproctag(NULL); } int main(int argc, char **argv) @@ -298,6 +302,7 @@ int main(int argc, char **argv) const char *prog = xbasename(argv[0]); const char *net_connect_timeout = NULL; + setproctag(prog); /* 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 @@ -378,6 +383,7 @@ int main(int argc, char **argv) if (upscli_splitname(argv[0], &upsname, &hostname, &port) != 0) { fatalx(EXIT_FAILURE, "Error: invalid UPS definition. Required format: upsname[@hostname[:port]]"); } + setproctag(argv[0]); ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); diff --git a/clients/upslog.c b/clients/upslog.c index b8561203d9..f7f879d570 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -526,6 +526,7 @@ int main(int argc, char **argv) callback_upsconf_args = do_upsconf_args; #endif + setproctag(prog); print_banner_once(prog, 0); while ((i = getopt(argc, argv, "+hDs:l:i:d:Nf:u:Vp:FBm:W:")) != -1) { diff --git a/clients/upsrw.c b/clients/upsrw.c index c5ab5fa2ff..662e816036 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -89,6 +89,10 @@ static void clean_exit(void) free(upsname); free(hostname); free(ups); + + /* Not a sub-process (do not let common::proctag_cleanup() mis-report us as such) */ + upsdebugx(1, "%s: finished, exiting", __func__); + setproctag(NULL); } #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_BESIDEFUNC) && (!defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP_INSIDEFUNC) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS_BESIDEFUNC) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE_BESIDEFUNC) ) @@ -656,6 +660,7 @@ int main(int argc, char **argv) const char *net_connect_timeout = NULL; char *password = NULL, *username = NULL, *setvar = NULL, *s = NULL; + setproctag(prog); /* 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 @@ -732,6 +737,7 @@ int main(int argc, char **argv) if (upscli_splitname(argv[0], &upsname, &hostname, &port) != 0) { fatalx(EXIT_FAILURE, "Error: invalid UPS definition. Required format: upsname[@hostname[:port]]"); } + setproctag(argv[0]); ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); diff --git a/common/common.c b/common/common.c index aec564876c..71312a5e26 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 @@ -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 */ @@ -3961,6 +3964,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)) { @@ -4237,9 +4248,56 @@ static char *proctag = NULL, *proctag_for_upsdebug = NULL, static void proctag_cleanup(void) { if (proctag) { - upsdebugx(2, "a %s sub-process (%s) is exiting now", - NUT_STRARG(getmyprocbasename()), getproctag()); + char *pn = xstrdup(getmyprocbasename()); + char *tn = xstrdup(proctag); + +#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code" +#endif + if (strlen(EXEEXT) > 0) { + /* TOTHINK: Generalize provided-if-missing strcasestr()? */ + char *s; + if (pn) { + s = strstr(pn, EXEEXT); + if (s) *s='\0'; + } + + if (tn) { + s = strstr(tn, EXEEXT); + if (s) *s='\0'; + } + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic pop +#endif + + if (pn && tn && !strcmp(pn, tn)) { + /* Avoid reporting this line as misleading "sub-process" + * after a plain singular setproctag(progname) call in + * a NUT program: */ + upsdebugx(2, "a process (%s) is exiting now", pn); + } else { + /* Some ptr not set, or strings not equal */ + upsdebugx(2, "a %s sub-process (%s) is exiting now", + NUT_STRARG(pn), proctag); + } + + if (pn) + free(pn); + if (tn) + free(tn); } + setproctag(NULL); } @@ -4281,7 +4339,66 @@ void setproctag(const char *tag) proctag_for_upsdebug_buflen = strlen(tag) + 2; proctag_for_upsdebug = (char *)xcalloc(proctag_for_upsdebug_buflen, sizeof(char)); if (proctag_for_upsdebug) { - snprintf(proctag_for_upsdebug, proctag_for_upsdebug_buflen, ":%s", tag); + char *pn = xstrdup(getmyprocbasename()); + char *tn = xstrdup(tag); + int tagged = 0; + +#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code" +#endif + if (strlen(EXEEXT) > 0) { + /* TOTHINK: Generalize provided-if-missing strcasestr()? + * One implementation is currently tucked away in + * libusb0.c because net-snmp may provide another... + */ + char *s; + if (pn) { + s = strstr(pn, EXEEXT); + if (s) *s='\0'; + } + + if (tn) { + s = strstr(tn, EXEEXT); + if (s) *s='\0'; + } + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic pop +#endif + + if (pn && tn && getenv("NUT_DEBUG_PROCNAME") != NULL && strcmp(pn, tn)) { + /* Only add the process name if asked for and substantially + * different from tag value -- e.g. do not duplicate text + * when callers initialize with settagname(progname) */ + char *s = NULL; + proctag_for_upsdebug_buflen += strlen(pn) + 1; + s = (char *)xcalloc(proctag_for_upsdebug_buflen, sizeof(char)); + if (s) { + snprintf(s, proctag_for_upsdebug_buflen, ":%s:%s", pn, tag); + free(proctag_for_upsdebug); + proctag_for_upsdebug = s; + tagged = 1; + } + } + + if (!tagged) { + snprintf(proctag_for_upsdebug, proctag_for_upsdebug_buflen, ":%s", tag); + } + + if (pn) + free(pn); + if (tn) + free(tn); } } diff --git a/conf/nut.conf.sample b/conf/nut.conf.sample index fd79043ff7..01a6cc02da 100644 --- a/conf/nut.conf.sample +++ b/conf/nut.conf.sample @@ -106,6 +106,13 @@ MODE=none #NUT_DEBUG_PID=true #export NUT_DEBUG_PID +# Optionally log process name (or however the program chose to identify itself). +# This may be useful when multiple NUT daemons log into the same file or console, +# without syslog to prefix the name into each line (e.g. in tests, single init +# script systems like Home Assistant, etc.). +#NUT_DEBUG_PROCNAME=true +#export NUT_DEBUG_PROCNAME + # Normally NUT can (attempt to) use the syslog or Event Log (WIN32), but the # environment variable 'NUT_DEBUG_SYSLOG' allows to bypass it, and perhaps keep # the daemons logging to stderr (useful e.g. in NUT Integration Test suite to 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 diff --git a/configure.ac b/configure.ac index b29c5d5566..92ed45dc07 100644 --- a/configure.ac +++ b/configure.ac @@ -7063,24 +7063,30 @@ 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) -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) +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}" +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]) 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++]) @@ -7089,16 +7095,100 @@ 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_CFLAGS="${my_FIXX} ${LIBSSL_CFLAGS}" - ], - [AC_MSG_RESULT([LIBSSL flags just won't work]) - have_cxx11=no + [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 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 + 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 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" + ]) + ]) + ]) + + 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" = true], + [nut_with_ssl_cxx=yes], + [AC_MSG_RESULT([LIBSSL flags just won't work with C++]) + dnl We disable just SSL support in the C++ library + ]) + ] + ) CFLAGS="${my_CFLAGS}" CPPFLAGS="${my_CPPFLAGS}" @@ -7106,9 +7196,16 @@ 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 + unset my_FIXED + 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/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 diff --git a/docs/Makefile.am b/docs/Makefile.am index ee725f8c58..9a9c735abc 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. @@ -987,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; \ @@ -999,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`" ; \ 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/. diff --git a/docs/man/nut.conf.txt b/docs/man/nut.conf.txt index a183f761b3..94518dfc0a 100644 --- a/docs/man/nut.conf.txt +++ b/docs/man/nut.conf.txt @@ -139,6 +139,12 @@ Optionally add current process ID to tags with debug-level identifiers. This may be useful when many NUT daemons write to the same console or log file, such as in containers/plugins for Home Assistant, storage appliances... +*NUT_DEBUG_PROCNAME*:: +Optionally log process name (or however the program chose to identify itself). +This may be useful when multiple NUT daemons log into the same file or console, +without syslog to prefix the name into each line (e.g. in tests, single init +script systems like Home Assistant, etc.) + *NUT_DEBUG_SYSLOG*:: Optional, unset by default. Normally NUT can (attempt to) use the syslog or Event Log (WIN32), but the diff --git a/docs/nut.dict b/docs/nut.dict index e830687f99..1faf5c6983 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 3707 utf-8 AAC AAS ABI @@ -963,6 +963,7 @@ PPP PR PR'ed PRIuSIZE +PROCNAME PROGRA PROGS PROTVER @@ -2409,6 +2410,7 @@ isbmex ish iso isolator +isystem iter ivp ivtscd diff --git a/drivers/main.c b/drivers/main.c index 847afc435e..88590117ad 100644 --- a/drivers/main.c +++ b/drivers/main.c @@ -2004,6 +2004,10 @@ static void exit_cleanup(void) free((char*)(prognames[i])); } } + + /* Not a sub-process (do not let common::proctag_cleanup() mis-report us as such) */ + upsdebugx(1, "%s: finished, exiting", __func__); + setproctag(NULL); } #endif /* DRIVERS_MAIN_WITHOUT_MAIN */ @@ -2506,6 +2510,7 @@ int main(int argc, char **argv) fatalx(EXIT_FAILURE, "Error: specifying '-a id' or '-s id' is now mandatory. Try -h for help."); } + setproctag(upsname); /* we need to get the port from somewhere, unless we are just sending a signal and exiting */ if (!device_path && !cmd) { diff --git a/drivers/upsdrvctl.c b/drivers/upsdrvctl.c index 8b1ee9a9db..93b838a007 100644 --- a/drivers/upsdrvctl.c +++ b/drivers/upsdrvctl.c @@ -3,7 +3,7 @@ Copyright (C) 2001 Russell Kroll 2005 - 2017 Arnaud Quette - 2017 - 2025 Jim Klimov + 2017 - 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 @@ -54,6 +54,7 @@ typedef struct { pid_t pid; #else /* WIN32 */ int pid; /* for WIN32 used just as a flag that this UPS was started by this tool in this run */ + PROCESS_INFORMATION ProcessInformation; #endif /* WIN32 */ void *next; } ups_t; @@ -806,6 +807,73 @@ static void debugcmdline(int level, const char *msg, char *const argv[]) upsdebugx(level, "%s", cmdline); } +#ifndef WIN32 +static int forkexec_parent_analyze(pid_t waitret, int wstat, const ups_t *ups) +#else +static int forkexec_parent_analyze(DWORD res, const ups_t *ups) +#endif +{ + /* work around const for this one... */ +#ifdef WIN32 + int *pupid = (int *)&(ups->pid); +#endif + int *puexectimeout = (int *)&(ups->exceeded_timeout); + + *puexectimeout = 0; + +#ifndef WIN32 + if (waitret == -1) { + upslogx(LOG_WARNING, "Startup timer elapsed, continuing..."); + exec_timeout++; + *puexectimeout = 1; + return 0; + } + + if (WIFEXITED(wstat) == 0) { + upslogx(LOG_WARNING, "Driver exited abnormally"); + exec_error++; + return -1; + } + + /* the rest of checks only work when WIFEXITED is nonzero */ + + if (WEXITSTATUS(wstat) != 0) { + upslogx(LOG_WARNING, "Driver failed to start" + " (exit status=%d)", WEXITSTATUS(wstat)); + exec_error++; + return -2; + } + + if (WIFSIGNALED(wstat)) { + upslog_with_errno(LOG_WARNING, "Driver died after signal %d", + WTERMSIG(wstat)); + exec_error++; + return -3; + } + +#else /* WIN32 */ + + if (res == WAIT_OBJECT_0) { + /* all ok, no-op */ + } else + if (res == WAIT_TIMEOUT) { + upslogx(LOG_WARNING, "Startup timer elapsed, continuing..."); + *pupid = 0; /* For WIN32, just a flag (not "-1" has a meaning) */ + *puexectimeout = 1; + return 0; + } else { + DWORD exit_code = 0; + GetExitCodeProcess( ups->ProcessInformation.hProcess, &exit_code ); + upslogx(LOG_WARNING, "Driver failed to start (exit status=%d)", exit_code); + exec_error++; + return -2; + } +#endif /* WIN32 */ + + upsdebugx(3, "%s: startup seems successful", __func__); + return 1; +} + static void forkexec(char *const argv[], const ups_t *ups) { #ifndef WIN32 @@ -878,47 +946,38 @@ 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__); + } } + /* Block until alarm */ waitret = waitpid(pid, &wstat, 0); alarm(0); - if (waitret == -1) { - upslogx(LOG_WARNING, "Startup timer elapsed, continuing..."); - exec_timeout++; - *puexectimeout = 1; - return; - } - - if (WIFEXITED(wstat) == 0) { - upslogx(LOG_WARNING, "Driver exited abnormally"); - exec_error++; - return; - } - - /* the rest only work when WIFEXITED is nonzero */ - - if (WEXITSTATUS(wstat) != 0) { - upslogx(LOG_WARNING, "Driver failed to start" - " (exit status=%d)", WEXITSTATUS(wstat)); - exec_error++; - return; - } - - if (WIFSIGNALED(wstat)) { - upslog_with_errno(LOG_WARNING, "Driver died after signal %d", - WTERMSIG(wstat)); - exec_error++; - } + /* Bump timeout or error counts if appropriate */ + forkexec_parent_analyze(waitret, wstat, ups); return; - } + } /* end of pid != 0 (fork parent) part */ if (getproctag()) { char tag[SMALLBUF]; @@ -927,7 +986,7 @@ static void forkexec(char *const argv[], const ups_t *ups) } else { setproctag("child"); } - } + } /* forked, maybe */ /* child or foreground mode (no fork, e.g. single driver operation) * execute the specified binary and args into current process @@ -942,13 +1001,14 @@ static void forkexec(char *const argv[], const ups_t *ups) #else /* WIN32 */ BOOL ret; DWORD res; - DWORD exit_code = 0; char commandline[LARGEBUF]; STARTUPINFO StartupInfo; - PROCESS_INFORMATION ProcessInformation; - int i = 1; + /* work around const for this one... */ + PROCESS_INFORMATION *pProcessInformation = (PROCESS_INFORMATION*)&(ups->ProcessInformation); + int i = 1, waited = 0; memset(&StartupInfo, 0, sizeof(STARTUPINFO)); + memset(pProcessInformation, 0, sizeof(PROCESS_INFORMATION)); /* the command line is made of the driver name followed by args */ if (strstr(argv[0], ups->driver)) { @@ -986,33 +1046,54 @@ static void forkexec(char *const argv[], const ups_t *ups) NULL, NULL, &StartupInfo, - &ProcessInformation + pProcessInformation ); - if (ret == 0) { + if (!ret) { fatal_with_errno(EXIT_FAILURE, "execv"); } /* Wait a bit then look at driver process. - * 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. + * 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); - if (res != WAIT_TIMEOUT) { - GetExitCodeProcess( ProcessInformation.hProcess, &exit_code ); - upslogx(LOG_WARNING, "Driver failed to start (exit status=%d)", ret); - exec_error++; - return; - } else { - /* work around const for this one... */ - int *pupid = (int *)&(ups->pid); - int *puexectimeout = (int *)&(ups->exceeded_timeout); - *pupid = 0; /* For WIN32, just a flag (not "-1" has a meaning) */ - *puexectimeout = 1; + /* 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(pProcessInformation->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(pProcessInformation->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(pProcessInformation->hProcess, + 0); } + forkexec_parent_analyze(res, ups); + return; #endif /* WIN32 */ } @@ -1370,12 +1451,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 +1469,34 @@ 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); + + if (upscount > 1 && ups->exceeded_timeout) { + /* Final checks, similar to those in forkexec() */ +#ifndef WIN32 + int wstat; + pid_t waitret; + + waitret = waitpid(ups->pid, &wstat, WNOHANG); + if (forkexec_parent_analyze(waitret, wstat, ups) > 0) +#else + DWORD res = WaitForSingleObject(ups->ProcessInformation.hProcess, 0); + if (forkexec_parent_analyze(res, ups)) +#endif + { + upslogx(LOG_INFO, "%s: driver %s [%s] completed startup " + "while we were sleeping to retry", __func__, + ups->driver, ups->upsname); + drv_maxretry = 0; + exec_error = initial_exec_error; + exec_timeout = initial_exec_timeout; + } + } + + } } } } @@ -1942,7 +2050,7 @@ int main(int argc, char **argv) if (waitret == tmp->pid) { upsdebugx(1, "Driver [%s] PID %" PRIdMAX " initially exceeded " - "maxstartdelay %d sec but has finished by now", + "maxstartdelay %d sec but has finished starting by now", tmp->upsname, (intmax_t)tmp->pid, (tmp->maxstartdelay!=-1?tmp->maxstartdelay:maxstartdelay)); tmp->exceeded_timeout = 0; @@ -2104,6 +2212,8 @@ int main(int argc, char **argv) reset_signal_flag(); upsdebugx(1, "upsdrvctl: handling signal: finished"); } +#else /* WIN32 */ + /* TOTHINK: Is there something we can do on the platform? */ #endif /* !WIN32 */ sleep(1); 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 ]) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 39fffdbb0f..b86c2d3799 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -100,8 +100,12 @@ class PyNUTClient : __version = "1.9.0" __release = "2026-03-16" + try: + NUT_DEFAULT_CONNECT_TIMEOUT = float(os.getenv("NUT_DEFAULT_CONNECT_TIMEOUT", "5.0")) + except: + NUT_DEFAULT_CONNECT_TIMEOUT = 5.0 - def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=5, + def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, use_ssl=False, ssl_context=None, cert_verify=None, ca_file=None, ca_path=None, cert_file=None, key_file=None, key_pass=None, force_ssl=False ) : """ Class initialization method @@ -126,7 +130,7 @@ force_ssl : Boolean, if True, the connection must be secure (default to False) if self.__debug : print( "[DEBUG] Class initialization..." ) - print( "[DEBUG] -> Host = %s (port %s)" % ( host, port ) ) + print( "[DEBUG] -> Host = %s (port %s, timeout %s)" % ( host, port, timeout) ) print( "[DEBUG] -> Login = '%s' / '%s'" % ( login, password ) ) print( "[DEBUG] -> SSL = %s (force: %s, verify: %s)" % (use_ssl, force_ssl, cert_verify) ) @@ -134,7 +138,10 @@ force_ssl : Boolean, if True, the connection must be secure (default to False) self.__port = port self.__login = login self.__password = password - self.__timeout = timeout + # TOTHINK: we could call socket.getdefaulttimeout() but it + # might upset other scripts that use PyNUTClient, and/or + # their imposed timeouts might confuse NUT expectations: + self.__timeout = NUT_DEFAULT_CONNECT_TIMEOUT if (timeout is None or type(timeout) not in [float, int] or timeout < 0.0) else timeout if ssl_available: self.__use_ssl = use_ssl or force_ssl self.__force_ssl = force_ssl diff --git a/scripts/systemd/README.adoc b/scripts/systemd/README.adoc index e164c2bc2a..ca473d87ff 100644 --- a/scripts/systemd/README.adoc +++ b/scripts/systemd/README.adoc @@ -17,7 +17,8 @@ configuration files rather than editing the installed unit files directly ---- # cat /etc/systemd/system/nut-monitor.service.d/debug.conf [Service] -Environment="NUT_DEBUG_PID=yes" +Environment="NUT_DEBUG_PID=true" +#Environment="NUT_DEBUG_PROCNAME=true" ---- ...followed up by `systemctl daemon-reload` and a restart of the unit itself. diff --git a/server/upsd.c b/server/upsd.c index b843f1b5e1..eb8aa83c03 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -1547,6 +1547,7 @@ static void mainloop(void) size_t chunk = 0; #endif /* WIN32 */ + size_t nfds_tmp_type_all, nfds_tmp_chosen; /* Report socket counts per type (driver, client...) */ size_t nfds_wanted = 0, /* Connections we looked at (some may be invalid) */ nfds_considered = 0; /* Connections we wanted to poll (but might be over maxconn limit) */ nfds_t nfds = 0; @@ -1572,8 +1573,11 @@ static void mainloop(void) #ifndef WIN32 /* scan through driver sockets */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (ups = firstups; ups; ups = ups->next) { nfds_considered++; + nfds_tmp_type_all++; /* see if we need to (re)connect to the socket */ if (INVALID_FD(ups->sock_fd)) { @@ -1613,8 +1617,11 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for DRIVER [%s, FD %d]", - __func__, (uintmax_t)nfds, ups->name, ups->sock_fd); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for DRIVER (%" PRIuMAX "/%" PRIuMAX ") [%s, FD %d]", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all, + ups->name, ups->sock_fd); fds[nfds].fd = ups->sock_fd; fds[nfds].events = POLLIN; @@ -1625,9 +1632,12 @@ static void mainloop(void) } /* scan through client sockets */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (client = firstclient; client; client = cnext) { cnext = client->next; nfds_considered++; + nfds_tmp_type_all++; if (difftime(now, client->last_heard) > 60) { /* shed clients after 1 minute of inactivity */ @@ -1650,8 +1660,11 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for CLIENT [%s => %s, FD %d]", - __func__, (uintmax_t)nfds, client->addr, client->loginups, client->sock_fd); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for CLIENT (%" PRIuMAX "/%" PRIuMAX ") [%s => %s, FD %d]", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all, + client->addr, client->loginups, client->sock_fd); fds[nfds].fd = client->sock_fd; fds[nfds].events = POLLIN; @@ -1662,8 +1675,11 @@ static void mainloop(void) } /* scan through server sockets */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (server = firstaddr; server; server = server->next) { nfds_considered++; + nfds_tmp_type_all++; if (INVALID_FD_SOCK(server->sock_fd)) { upsdebugx(5, "%s: skip invalid SERVER listener [%s:%s, FD %d]: socket not bound", __func__, server->addr, server->port, server->sock_fd); @@ -1677,8 +1693,11 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for SERVER listener [%s:%s, FD %d]", - __func__, (uintmax_t)nfds, server->addr, server->port, server->sock_fd); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for SERVER listener (%" PRIuMAX "/%" PRIuMAX ") [%s:%s, FD %d]", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all, + server->addr, server->port, server->sock_fd); fds[nfds].fd = server->sock_fd; fds[nfds].events = POLLIN; @@ -1713,34 +1732,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; } } @@ -1867,10 +1890,15 @@ static void mainloop(void) continue; } } + #else /* WIN32 */ + /* scan through driver sockets */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (ups = firstups; ups; ups = ups->next) { nfds_considered++; + nfds_tmp_type_all++; /* see if we need to (re)connect to the socket */ if (INVALID_FD(ups->sock_fd)) { @@ -1903,7 +1931,7 @@ static void mainloop(void) } if (INVALID_FD(ups->read_overlapped.hEvent)) { - upsdebugx(5, "%s: skip DRIVER [%s, handle %p: event loop not bound", __func__, ups->name, ups->sock_fd); + upsdebugx(5, "%s: skip DRIVER [%s, handle %p]: event loop not bound", __func__, ups->name, ups->sock_fd); continue; } @@ -1914,8 +1942,11 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for DRIVER [%s, handle %p]", - __func__, (uintmax_t)nfds, ups->name, ups->sock_fd); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for DRIVER (%" PRIuMAX "/%" PRIuMAX ") [%s, handle %p]", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all, + ups->name, ups->sock_fd); fds[nfds] = ups->read_overlapped.hEvent; handler[nfds].type = DRIVER; @@ -1925,9 +1956,12 @@ static void mainloop(void) } /* scan through client sockets */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (client = firstclient; client; client = cnext) { cnext = client->next; nfds_considered++; + nfds_tmp_type_all++; if (difftime(now, client->last_heard) > 60) { /* shed clients after 1 minute of inactivity */ @@ -1954,8 +1988,11 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for CLIENT [%s => %s, FD %" PRIuMAX "]", - __func__, (uintmax_t)nfds, client->addr, client->loginups, (uintmax_t)client->sock_fd); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for CLIENT (%" PRIuMAX "/%" PRIuMAX ") [%s => %s, FD %" PRIuMAX "]", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all, + client->addr, client->loginups, (uintmax_t)client->sock_fd); fds[nfds] = client->Event; handler[nfds].type = CLIENT; @@ -1965,8 +2002,11 @@ static void mainloop(void) } /* scan through server sockets */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (server = firstaddr; server; server = server->next) { nfds_considered++; + nfds_tmp_type_all++; if (INVALID_FD_SOCK(server->sock_fd)) { upsdebugx(5, "%s: skip invalid SERVER listener [%s:%s, FD %" PRIuMAX "]: socket not bound", __func__, server->addr, server->port, (uintmax_t)server->sock_fd); @@ -1985,8 +2025,11 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for SERVER listener [%s:%s, FD %" PRIuMAX "]", - __func__, (uintmax_t)nfds, server->addr, server->port, (uintmax_t)server->sock_fd); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for SERVER listener (%" PRIuMAX "/%" PRIuMAX ") [%s:%s, FD %" PRIuMAX "]", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all, + server->addr, server->port, (uintmax_t)server->sock_fd); fds[nfds] = server->Event; handler[nfds].type = SERVER; @@ -1996,8 +2039,11 @@ static void mainloop(void) } /* Wait on the read IO on named pipe */ + nfds_tmp_type_all = 0; + nfds_tmp_chosen = 0; for (conn = pipe_connhead; conn; conn = conn->next) { nfds_considered++; + nfds_tmp_type_all++; /* FIXME: derive name from conn->handle to report it. * See GetFileInformationByHandleEx() in API @@ -2014,8 +2060,10 @@ static void mainloop(void) continue; } - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for NAMED PIPE", - __func__, (uintmax_t)nfds); + nfds_tmp_chosen++; + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for NAMED PIPE (%" PRIuMAX "/%" PRIuMAX ")", + __func__, (uintmax_t)nfds, + (uintmax_t)nfds_tmp_chosen, (uintmax_t)nfds_tmp_type_all); fds[nfds] = conn->overlapped.hEvent; handler[nfds].type = NAMED_PIPE; handler[nfds].data = (void *)conn; @@ -2032,7 +2080,7 @@ static void mainloop(void) /* ignore listeners that we are unable to handle */ upsdebugx(5, "%s: skip handler for new NAMED PIPE connections: too many handled already", __func__); } else { - upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for new NAMED PIPE connection", + upsdebugx(4, "%s: adding FD handler #%" PRIuMAX " for new NAMED PIPE connection (1/1)", __func__, (uintmax_t)nfds); fds[nfds] = pipe_connection_overlapped.hEvent; handler[nfds].type = NAMED_PIPE; @@ -2070,29 +2118,36 @@ static void mainloop(void) */ size_t last_chunk = nfds % sysmaxconn, chunks = nfds / sysmaxconn + (last_chunk ? 1 : 0); - DWORD poll_TO = 2000 / chunks, tmpret; - - if (poll_TO < 10) - poll_TO = 10; + DWORD poll_TO, poll_TO_chunk = 2000 / chunks, tmpret; - 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, with %" PRIu64 " hits so far", - __func__, chunk, chunks, ret); - tmpret = WaitForMultipleObjects( - (last_chunk && chunk == chunks - 1 ? last_chunk : sysmaxconn), - &fds[chunk * sysmaxconn], - FALSE, poll_TO); - if (tmpret != WAIT_TIMEOUT) { - 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; + } } } } @@ -2122,18 +2177,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 @@ -2303,6 +2359,7 @@ int main(int argc, char **argv) struct passwd *new_uid = NULL; progname = xbasename(argv[0]); + setproctag(progname); #if (defined ENABLE_SHARED_PRIVATE_LIBS) && ENABLE_SHARED_PRIVATE_LIBS callback_upsconf_args = do_upsconf_args; diff --git a/tests/Makefile.am b/tests/Makefile.am index 078321ff3c..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 - AM_CXXFLAGS += $(LIBSSL_CFLAGS) -endif WITH_SSL +if WITH_SSL_CXX + AM_CXXFLAGS += $(LIBSSL_CXXFLAGS) +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/NIT/nit.sh b/tests/NIT/nit.sh index 44f64ea9bb..ad59173ac9 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -98,6 +98,9 @@ export NUT_DEBUG_LEVEL NUT_DEBUG_PID="true" export NUT_DEBUG_PID +NUT_DEBUG_PROCNAME="true" +export NUT_DEBUG_PROCNAME + # Just keep upsdrvctl quiet if used in test builds or with the sandbox NUT_QUIET_INIT_NDE_WARNING="true" export NUT_QUIET_INIT_NDE_WARNING @@ -111,6 +114,10 @@ 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 +[ -n "${UPSLOG_SWARM_COUNT}" ] && [ "${UPSLOG_SWARM_COUNT}" -gt 0 ] || UPSLOG_SWARM_COUNT=0 + log_separator() { echo "" >&2 echo "================================" >&2 @@ -447,6 +454,16 @@ PID_UPSSCHED="" PID_DUMMYUPS="" PID_DUMMYUPS1="" PID_DUMMYUPS2="" +PIDS_DUMMYUPS_SWARM="" +PIDS_UPSLOG_SWARM="" + +if [ -z "${NUT_DEFAULT_CONNECT_TIMEOUT-}" ] && [ 30 -lt "`expr ${DUMMY_UPS_SWARM_COUNT} \* ${UPSLOG_SWARM_COUNT}`" ] ; then + # upsd may take longer to walk its connections, + # so upsc et al should be more patient: + NUT_DEFAULT_CONNECT_TIMEOUT="`expr ${DUMMY_UPS_SWARM_COUNT} \* ${UPSLOG_SWARM_COUNT} / 5`" + export NUT_DEFAULT_CONNECT_TIMEOUT + log_info "Applying NUT_DEFAULT_CONNECT_TIMEOUT='$NUT_DEFAULT_CONNECT_TIMEOUT' due to DUMMY_UPS_SWARM_COUNT=$DUMMY_UPS_SWARM_COUNT and UPSLOG_SWARM_COUNT=$UPSLOG_SWARM_COUNT" +fi 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 +796,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$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 $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 $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="" @@ -791,6 +808,8 @@ stop_daemons() { PID_DUMMYUPS="" PID_DUMMYUPS1="" PID_DUMMYUPS2="" + PIDS_DUMMYUPS_SWARM="" + PIDS_UPSLOG_SWARM="" unset PID_UPSSCHED_NOW } @@ -895,7 +914,7 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in ( # Sub-shelling here to keep soft failure cases handled once, # and changes of directory constrained without pushd/popd # (not in all shells) or remembering of `pwd` (clumsy-ish) - log_info "Setting up crypto material storage for SSL capability tests..." + log_info "Setting up crypto material storage for SSL capability tests under '${TESTCERT_PATH_BASE}'..." if shouldDebug ; then set -x @@ -908,8 +927,11 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in *cert) rm -rf "${TESTCERT_PATH_BASE}" || true ;; *) log_warn "TESTCERT_PATH_BASE seems wrong: '${TESTCERT_PATH_BASE}'" ;; esac - mkdir -p "${TESTCERT_PATH_ROOTCA}" || exit - ( cd "${TESTCERT_PATH_ROOTCA}" + + SKID="0x1234567890abcdef1234" + + mkdir -p "${TESTCERT_PATH_ROOTCA}" || die "Could not mkdir TESTCERT_PATH_ROOTCA" + ( cd "${TESTCERT_PATH_ROOTCA}" || exit log_info "SSL: Preparing test Root CA..." echo "${TESTCERT_ROOTCA_PASS}" > ".pwfile" case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in @@ -920,19 +942,70 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in [ -e /dev/random ] && \ dd if=/dev/random of=.random bs=16 count=1 } || date > .random + # Create the certificate database: - certutil -N -d . -f .pwfile + certutil -N -d . -f .pwfile \ + || die "Could not init NSS CA database in `pwd`" + # Generate a certificate for CA: # HACK NOTE: The first "yes" is for "Is this a CA certificate [y/N]?" question, # others default (empty) for possible other questions, e.g. # Enter the path length constraint, enter to skip [<0 for unlimited path]: > # Is this a critical extension [y/N]? : - (echo y; yes "") | certutil -S -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -s "CN=${TESTCERT_ROOTCA_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" -t "CT,," -x -2 -z .random || exit + # Some builds of certutil fail with SIGSEGV due to infinite input from `yes ""`, + # but generally we do not know how many questions are asked: + cscmd() { + certutil -S -x \ + -d . -f .pwfile \ + -n "${TESTCERT_ROOTCA_NAME}" \ + -s "CN=${TESTCERT_ROOTCA_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" \ + -t "CT,C,C" \ + -m 1 \ + --keyUsage critical,certSigning,crlSigning,digitalSignature,nonRepudiation \ + -z .random \ + -2 \ + -3 \ + --extSKID + } + if [ x"${NUT_CERTUTIL_INTERACTIVE-}" = xtrue ] ; then + cscmd + else { + ## Generating key. This may take a few moments... + #> Is this a CA certificate [y/N]? + echo y + #> Enter the path length constraint, enter to skip [<0 for unlimited path]: + echo '-1' + #> Is this a critical extension [y/N] + echo y + + #> Enter value for the authKeyID extension [y/N]? + echo y + #> Enter value for the key identifier fields,enter to omit: + echo "${SKID}" + ## Select one of the following general name type: + ## [...] Any other number to finish + #> Choice: > + echo '' + #> Enter value for the authCertSerial field, enter to omit: + echo '' + #> Is this a critical extension [y/N]? + echo '' + + ## Adding Subject Key ID extension. + #> Enter value for the key identifier fields,enter to omit: + echo "${SKID}" + #> Is this a critical extension [y/N]? + echo n + } | cscmd + fi || die "Could not generate NSS CA certificate ($?)" + # Extract the CA certificate to be able to use or import it later: - certutil -L -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -a -o rootca.pem || exit + certutil -L -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -a -o rootca.pem \ + || die "Could not extract the NSS CA certificate to PEM" # Use this later for signing, move on to server/client requests... - ls -l "${TESTCERT_PATH_ROOTCA}"/*.db "${TESTCERT_PATH_ROOTCA}"/*.txt || exit + ls -l "${TESTCERT_PATH_ROOTCA}"/*.db "${TESTCERT_PATH_ROOTCA}"/*.txt \ + || die "Could not list NSS CA DB files" ;; esac @@ -940,7 +1013,8 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in *OpenSSL*) # Generate an AES encrypted private key: - openssl genrsa -aes256 -out rootca.key -passout file:.pwfile 4096 || exit + openssl genrsa -aes256 -out rootca.key -passout file:.pwfile 4096 \ + || die "Could not generate an AES encrypted private key for OpenSSL CA" # Generate a certificate for CA using that key; # note that not all "openssl" versions have the # "-extfile" option for self-signed (CA) certs; @@ -967,7 +1041,7 @@ EOF # Older OpenSSL versions (e.g. 1.0.2 in CentOS 7) do not support this option: sed 's,^\(authorityKeyIdentifier=\),###\1,' -i rootca.req.conf \ && openssl req -x509 -new -nodes -key rootca.key -passin file:.pwfile -sha256 -days 1826 -out rootca.pem -config rootca.req.conf - } || exit + } || die "Could not self-sign OpenSSL CA req" ;; esac @@ -975,45 +1049,114 @@ EOF *OpenSSL*) # OpenSSL CA trust "database" should include hashes # of CA PEM certificates as symlinks to actual files: - CERTHASH="`openssl x509 -subject_hash -in rootca.pem | head -1`" + CERTHASH="`openssl x509 -subject_hash -in rootca.pem | head -1`" \ + && [ -n "${CERTHASH}" ] \ + || die "Could not determine OpenSSL certificate hash for Root CA files" + # NOTE: Symlinking may be prohibited or not implemented on some platforms (e.g. Windows) or file systems ln -fs rootca.pem "${CERTHASH}".0 || ln -f rootca.pem "${CERTHASH}".0 || cp -f rootca.pem "${CERTHASH}".0 ln -fs rootca.pem "${CERTHASH}" || ln -f rootca.pem "${CERTHASH}" || cp -f rootca.pem "${CERTHASH}" - ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem "${TESTCERT_PATH_ROOTCA}/${CERTHASH}"* + ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem "${TESTCERT_PATH_ROOTCA}/${CERTHASH}"* \ + || die "Could not list OpenSSL CA PEM file and hash links" ;; *) - ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem || exit + ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + || die "Could not list OpenSSL CA PEM file (exported from NSS)" ;; esac - ) || exit + ) || die "Could not prepare Root CA in '${TESTCERT_PATH_ROOTCA}'" mkdir -p "${TESTCERT_PATH_SERVER}" - ( cd "${TESTCERT_PATH_SERVER}" + ( cd "${TESTCERT_PATH_SERVER}" || exit log_info "SSL: Preparing test server certificate..." echo "${TESTCERT_SERVER_PASS}" > ".pwfile" case "${WITH_SSL_SERVER}" in NSS) # Create the certificate database: - certutil -N -d . -f .pwfile + certutil -N -d . -f .pwfile \ + || die "Could not init NSS Server database in `pwd`" + # Import the CA certificate, so users of this DB trust it: - certutil -A -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -t "TC,," -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem || exit + certutil -A -d . -f .pwfile \ + -n "${TESTCERT_ROOTCA_NAME}" \ + -t "TC,," \ + -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + || die "Could not import the CA certificate to NSS Server database" + # Create a server certificate request: # NOTE: IRL Each run should have a separate random seed; for tests we cut a few corners! - certutil -R -d . -f .pwfile -s "CN=${TESTCERT_SERVER_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" -a -o server.req -z "${TESTCERT_PATH_ROOTCA}"/.random || exit + certutil -R -d . -f .pwfile \ + -s "CN=${TESTCERT_SERVER_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" \ + -a -o server.req \ + -z "${TESTCERT_PATH_ROOTCA}"/.random \ + --extKeyUsage "serverAuth" \ + --nsCertType sslServer \ + --keyUsage critical,dataEncipherment,keyEncipherment,digitalSignature,nonRepudiation \ + --extSAN "dns:localhost,dns:localhost6,dns:127.0.0.1,dns:::1,ip:127.0.0.1,ip:::1" \ + || die "Could not create a NSS Server certificate request" # Sign a certificate request with the CA certificate: # HACK NOTE: "No" for "Is this a CA certificate" question, defaults for others - (echo n; yes "") | certutil -C -d "${TESTCERT_PATH_ROOTCA}" -f "${TESTCERT_PATH_ROOTCA}"/.pwfile -c "${TESTCERT_ROOTCA_NAME}" -a -i server.req -o server.crt -2 --extKeyUsage "serverAuth" --nsCertType sslServer || exit + # Some builds of certutil fail with SIGSEGV due to infinite input from `yes ""`, + # but generally we do not know how many questions are asked: + cscmd() { + certutil -C -d "${TESTCERT_PATH_ROOTCA}" \ + -f "${TESTCERT_PATH_ROOTCA}"/.pwfile \ + -c "${TESTCERT_ROOTCA_NAME}" \ + -a -i server.req -o server.crt \ + --extKeyUsage "serverAuth" \ + --nsCertType sslServer \ + -m 2 \ + -2 \ + -3 \ + --extSKID + } + if [ x"${NUT_CERTUTIL_INTERACTIVE-}" = xtrue ] ; then + cscmd + else { + ## Generating key. This may take a few moments... + #> Is this a CA certificate [y/N]? + echo n + #> Enter the path length constraint, enter to skip [<0 for unlimited path]: + echo '' + #> Is this a critical extension [y/N] + echo n + + #> Enter value for the authKeyID extension [y/N]? + echo y + #> Enter value for the key identifier fields,enter to omit: + echo "${SKID}" + ## Select one of the following general name type: + ## [...] Any other number to finish + #> Choice: > + echo '' + #> Enter value for the authCertSerial field, enter to omit: + echo '' + #> Is this a critical extension [y/N]? + echo '' + + ## Adding Subject Key ID extension. + #> Enter value for the key identifier fields,enter to omit: + echo "${SKID}" + #> Is this a critical extension [y/N]? + echo n + } | cscmd + fi || die "Could not sign a NSS Server certificate request with the NSS CA database ($?)" # Import the signed certificate into server database: - certutil -A -d . -f .pwfile -n "${TESTCERT_SERVER_NAME}" -a -i server.crt -t ",," || exit + certutil -A -d . -f .pwfile \ + -n "${TESTCERT_SERVER_NAME}" \ + -a -i server.crt -t ",," \ + || die "Could not import the signed NSS Server certificate into server database" - ls -l "${TESTCERT_PATH_SERVER}"/*.db "${TESTCERT_PATH_SERVER}"/*.txt || exit + ls -l "${TESTCERT_PATH_SERVER}"/*.db "${TESTCERT_PATH_SERVER}"/*.txt \ + || die "Could not list NSS Server DB files" ;; OpenSSL) # Create a server certificate request: MSYS_NO_PATHCONV=1 \ - openssl req -new -nodes -out server.req -newkey rsa:4096 -passout file:.pwfile -keyout server.key -subj "/CN=${TESTCERT_SERVER_NAME}/OU=Test/O=NIT/ST=StateOfChaos/C=US" || exit + openssl req -new -nodes -out server.req -newkey rsa:4096 -passout file:.pwfile -keyout server.key -subj "/CN=${TESTCERT_SERVER_NAME}/OU=Test/O=NIT/ST=StateOfChaos/C=US" \ + || die "Could not create a OpenSSL Server certificate request" cat > server.v3.ext << EOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE @@ -1031,25 +1174,35 @@ EOF # Sign a certificate request with the CA certificate: ( cd "${TESTCERT_PATH_ROOTCA}" openssl x509 -req -in "${TESTCERT_PATH_SERVER}/server.req" -passin file:.pwfile -CA rootca.pem -CAkey rootca.key -CAcreateserial -out "${TESTCERT_PATH_SERVER}/server.crt" -days 730 -sha256 -extfile "${TESTCERT_PATH_SERVER}/server.v3.ext" - ) || exit - cat server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem server.key > upsd.pem || exit + ) || die "Could not sign a OpenSSL Server certificate request with the OpenSSL CA certificate" - ls -l "${TESTCERT_PATH_SERVER}"/upsd.pem || exit + cat server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem server.key > upsd.pem \ + || die "Could not combine an upsd.pem" + + ls -l "${TESTCERT_PATH_SERVER}"/upsd.pem \ + || die "Could not list an upsd.pem" ;; esac - ) || exit + ) || die "Could not prepare Server certs in '${TESTCERT_PATH_SERVER}'" mkdir -p "${TESTCERT_PATH_CLIENT}" - ( cd "${TESTCERT_PATH_CLIENT}" + ( cd "${TESTCERT_PATH_CLIENT}" || exit case "${WITH_SSL_CLIENT}" in NSS) log_info "SSL: Preparing test client certificate..." # Also create 3-file database of client key+cert store echo "${TESTCERT_CLIENT_PASS}" > ".pwfile" + # Create the certificate database: - certutil -N -d . -f .pwfile + certutil -N -d . -f .pwfile \ + || die "Could not init NSS Client database in `pwd`" + # Import the CA certificate, so users of this DB trust it: - certutil -A -d . -f .pwfile -n "${TESTCERT_ROOTCA_NAME}" -t "TC,," -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem || exit + certutil -A -d . -f .pwfile \ + -n "${TESTCERT_ROOTCA_NAME}" \ + -t "TC,," \ + -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + || die "Could not import the CA certificate to NSS Client database" # Import server cert into client database so we can trust it (CERTHOST directive): # NOTE: Seems we must do this before requesting or signing the client cert, @@ -1058,32 +1211,92 @@ EOF # certutil: could not decode certificate: SEC_ERROR_REUSED_ISSUER_AND_SERIAL: # You are attempting to import a cert with the same issuer/serial # as an existing cert, but that is not the same cert. - certutil -A -d . -f .pwfile -n "${TESTCERT_SERVER_NAME}" -a -i "${TESTCERT_PATH_SERVER}/server.crt" -t ",," || exit + certutil -A -d . -f .pwfile \ + -n "${TESTCERT_SERVER_NAME}" \ + -a -i "${TESTCERT_PATH_SERVER}/server.crt" \ + -t ",," \ + || die "Could not import the Server certificate to NSS Client database" # Create a client certificate request: # NOTE: IRL Each run should have a separate random seed; for tests we cut a few corners! - certutil -R -d . -f .pwfile -s "CN=${TESTCERT_CLIENT_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" -a -o client.req -z "${TESTCERT_PATH_ROOTCA}"/.random || exit + certutil -R -d . -f .pwfile \ + -s "CN=${TESTCERT_CLIENT_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" \ + -a -o client.req \ + -z "${TESTCERT_PATH_ROOTCA}"/.random \ + || die "Could not create a NSS Client certificate request" # Sign a certificate request with the CA certificate: # HACK NOTE: "No" for "Is this a CA certificate" question, defaults for others - (echo n; yes "") | certutil -C -d "${TESTCERT_PATH_ROOTCA}" -f "${TESTCERT_PATH_ROOTCA}"/.pwfile -c "${TESTCERT_ROOTCA_NAME}" -a -i client.req -o client.crt -2 --extKeyUsage "clientAuth" --nsCertType sslClient || exit + # Some builds of certutil fail with SIGSEGV due to infinite input from `yes ""`, + # but generally we do not know how many questions are asked: + cscmd() { + certutil -C -d "${TESTCERT_PATH_ROOTCA}" \ + -f "${TESTCERT_PATH_ROOTCA}"/.pwfile \ + -c "${TESTCERT_ROOTCA_NAME}" \ + -a -i client.req -o client.crt \ + --extKeyUsage "clientAuth" \ + --nsCertType sslClient \ + -m 3 \ + -2 \ + -3 \ + --extSKID + } + if [ x"${NUT_CERTUTIL_INTERACTIVE-}" = xtrue ] ; then + cscmd + else { + ## Generating key. This may take a few moments... + #> Is this a CA certificate [y/N]? + echo n + #> Enter the path length constraint, enter to skip [<0 for unlimited path]: + echo '' + #> Is this a critical extension [y/N] + echo n + + #> Enter value for the authKeyID extension [y/N]? + echo y + #> Enter value for the key identifier fields,enter to omit: + echo "${SKID}" + ## Select one of the following general name type: + ## [...] Any other number to finish + #> Choice: > + echo '' + #> Enter value for the authCertSerial field, enter to omit: + echo '' + #> Is this a critical extension [y/N]? + echo '' + + ## Adding Subject Key ID extension. + #> Enter value for the key identifier fields,enter to omit: + echo "${SKID}" + #> Is this a critical extension [y/N]? + echo n + } | cscmd + fi || die "Could not sign a NSS Client certificate request with the NSS CA database ($?)" # Import the signed certificate into client database: - certutil -A -d . -f .pwfile -n "${TESTCERT_CLIENT_NAME}" -a -i client.crt -t ",," || exit + certutil -A -d . -f .pwfile \ + -n "${TESTCERT_CLIENT_NAME}" \ + -a -i client.crt -t ",," \ + || die "Could not import the signed NSS Client certificate into client database" - ls -l "${TESTCERT_PATH_CLIENT}"/*.db "${TESTCERT_PATH_CLIENT}"/*.txt || exit + ls -l "${TESTCERT_PATH_CLIENT}"/*.db "${TESTCERT_PATH_CLIENT}"/*.txt \ + || die "Could not list NSS Client DB files" ;; OpenSSL) # NOTE: No special keys for an OpenSSL client so far, # it only checks/trusts a server (public data in a PEM file) log_info "SSL: Exporting public data of server certificate for client use..." - cat "${TESTCERT_PATH_SERVER}"/server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem > upsd-public.pem || exit + cat "${TESTCERT_PATH_SERVER}"/server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem > upsd-public.pem \ + || die "Could not combine a upsd-public.pem" - ls -l "${TESTCERT_PATH_CLIENT}/upsd-public.pem" || exit + ls -l "${TESTCERT_PATH_CLIENT}/upsd-public.pem" \ + || die "Could not list a upsd-public.pem" ;; esac - ) || exit - ) || { + ) || die "Could not prepare Client certs in '${TESTCERT_PATH_CLIENT}'" + ) && { + log_info "SUCCESS: Prepared crypto credential stores for SSL tests; WITH_SSL_CLIENT='${WITH_SSL_CLIENT}' WITH_SSL_SERVER='${WITH_SSL_SERVER}'" + } || { if [ x"${WITH_SSL_TESTS}" = xrequired-conditional ]; then die "Aborting because SSL tests are required (due to WITH_SSL_TESTS='${WITH_SSL_TESTS}') and something failed with crypto material setup" fi @@ -1171,6 +1384,12 @@ 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 ] || [ "$UPSLOG_SWARM_COUNT" -gt 5 ] ; then + # Enable select-group looping (especially on Windows with sysmaxconn=64); + # note that each upslog monitors all devices (*) so has many connections: + echo "MAXCONN `expr \( 3 + $DUMMY_UPS_SWARM_COUNT \) \* \( 1 + $UPSLOG_SWARM_COUNT \) + 30`" >> "$NUT_CONFPATH/upsd.conf" || exit + fi } generatecfg_upsd_nodev() { @@ -1470,7 +1689,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 @@ -1556,8 +1777,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 +1818,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 +2099,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 +2126,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 +2145,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 @@ -1892,6 +2167,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 @@ -1902,6 +2183,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}"' ]' @@ -2226,6 +2519,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" @@ -2325,7 +2628,12 @@ setenv_ssl_python() { # NUT_KEYFILE = os.getenv('NUT_KEYFILE', None) case "${WITH_SSL_SERVER}" in - none) return 0;; + none) + NUT_SSL=false + NUT_FORCESSL=0 + NUT_CERTVERIFY=0 + export NUT_SSL NUT_FORCESSL NUT_CERTVERIFY + ;; OpenSSL|NSS) log_info "Adding client-side (Open)SSL config to python env to talk to our ${WITH_SSL_SERVER}-capable upsd" @@ -2352,7 +2660,12 @@ setenv_ssl_python() { # Same vars are also used for Python (PyNUTClient) tests setenv_ssl_cppnit() { case "${WITH_SSL_CLIENT}" in - none) return 0;; + none) + NUT_SSL=false + NUT_FORCESSL=0 + NUT_CERTVERIFY=0 + export NUT_SSL NUT_FORCESSL NUT_CERTVERIFY + ;; OpenSSL|NSS) log_info "Adding client-side SSL (${WITH_SSL_CLIENT}) config to C++ env to talk to our ${WITH_SSL_SERVER}-capable upsd" @@ -2652,7 +2965,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 *='`" 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:" diff --git a/tools/nut-scanner/Makefile.am b/tools/nut-scanner/Makefile.am index b25237b3b0..eac84193e4 100644 --- a/tools/nut-scanner/Makefile.am +++ b/tools/nut-scanner/Makefile.am @@ -151,7 +151,7 @@ libnutscan_la_LDFLAGS += -version-info 5:0:1 # copies of "nut_debug_level" making fun of our debug-logging attempts. # One solution to tackle if needed for those cases would be to make some # dynamic/shared libnutcommon (etc.) -libnutscan_la_LDFLAGS += -export-symbols-regex '^(nutscan_|nut_debug_level|s_upsdebug|fatalx|fatal_with_errno|xcalloc|xbasename|snprintfcat|snprintf_dynamic|max_threads|curr_threads|nut_report_config_flags|upsdebugx_report_search_paths|nut_prepare_search_paths|print_banner_once|suggest_doc_links)' +libnutscan_la_LDFLAGS += -export-symbols-regex '^(nutscan_|nut_debug_level|s_upsdebug|fatalx|fatal_with_errno|setproctag|xcalloc|xbasename|snprintfcat|snprintf_dynamic|max_threads|curr_threads|nut_report_config_flags|upsdebugx_report_search_paths|nut_prepare_search_paths|print_banner_once|suggest_doc_links)' libnutscan_la_CFLAGS = \ -I$(top_builddir)/clients -I$(top_srcdir)/clients \ -I$(top_builddir)/include -I$(top_srcdir)/include \ diff --git a/tools/nut-scanner/nut-scanner.c b/tools/nut-scanner/nut-scanner.c index 322afe239a..f07bf99018 100644 --- a/tools/nut-scanner/nut-scanner.c +++ b/tools/nut-scanner/nut-scanner.c @@ -1230,6 +1230,8 @@ int main(int argc, char *argv[]) } #endif /* HAVE_PTHREAD && ( HAVE_PTHREAD_TRYJOIN || HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED ) && HAVE_SYS_RESOURCE_H */ + setproctag(progname); + memset(&snmp_sec, 0, sizeof(snmp_sec)); memset(&ipmi_sec, 0, sizeof(ipmi_sec)); memset(&xml_sec, 0, sizeof(xml_sec)); @@ -1255,8 +1257,11 @@ int main(int argc, char *argv[]) nut_debug_level++; } + setproctag("init-ip-ranges"); nutscan_init_ip_ranges(&ip_ranges_list); + setproctag("init-libnutscan"); nutscan_init(); + setproctag(progname); /* Default, see -Q/-N/-P below */ display_func = nutscan_display_ups_conf_with_sanity_check; @@ -1721,8 +1726,13 @@ int main(int argc, char *argv[]) } /* TODO/discuss : Should the #else...#endif code below for lack of pthreads - * during build also serve as a fallback for pthread failure at runtime? + * during build also serve as a fallback for pthread failure at runtime? + * Also consider a setproctag() variant for threads, where it would be most + * useful in troubleshooting. Currently it relies on a process-wide variable, + * and threads are all in same process space... */ + setproctag("scanning"); + if (allow_usb && nutscan_avail_usb) { upsdebugx(quiet, "Scanning USB bus."); #ifdef HAVE_PTHREAD @@ -1731,6 +1741,7 @@ int main(int argc, char *argv[]) nutscan_avail_usb = 0; } #else + setproctag("usb"); upsdebugx(1, "USB SCAN: no pthread support, starting nutscan_scan_usb..."); dev[TYPE_USB] = run_usb(&cli_link_detail_level); #endif /* HAVE_PTHREAD */ @@ -1752,6 +1763,7 @@ int main(int argc, char *argv[]) nutscan_avail_snmp = 0; } #else + setproctag("snmp"); upsdebugx(1, "SNMP SCAN: no pthread support, starting nutscan_scan_snmp..."); /* dev[TYPE_SNMP] = nutscan_scan_snmp(start_ip, end_ip, timeout, &snmp_sec); */ run_snmp(&snmp_sec); @@ -1776,6 +1788,7 @@ int main(int argc, char *argv[]) nutscan_avail_xml_http = 0; } #else + setproctag("netxml"); upsdebugx(1, "XML/HTTP SCAN: no pthread support, starting nutscan_scan_xml_http_range()..."); /* dev[TYPE_XML] = nutscan_scan_xml_http_range(start_ip, end_ip, timeout, &xml_sec); */ run_xml(&xml_sec); @@ -1798,6 +1811,7 @@ int main(int argc, char *argv[]) nutscan_avail_nut = 0; } #else + setproctag("oldnut"); upsdebugx(1, "NUT bus (old) SCAN: no pthread support, starting nutscan_scan_nut..."); /*dev[TYPE_NUT] = nutscan_scan_nut(start_ip, end_ip, port, timeout);*/ run_nut_old(NULL); @@ -1816,6 +1830,7 @@ int main(int argc, char *argv[]) nutscan_avail_nut_simulation = 0; } #else + setproctag("nut_simulation"); upsdebugx(1, "NUT simulation devices SCAN: no pthread support, starting nutscan_scan_nut_simulation..."); /* dev[TYPE_NUT_SIMULATION] = nutscan_scan_nut_simulation(); */ run_nut_simulation(NULL); @@ -1833,6 +1848,7 @@ int main(int argc, char *argv[]) nutscan_avail_avahi = 0; } #else + setproctag("avahi"); upsdebugx(1, "NUT bus (avahi) SCAN: no pthread support, starting nutscan_scan_avahi..."); /* dev[TYPE_AVAHI] = nutscan_scan_avahi(timeout); */ run_avahi(NULL); @@ -1855,6 +1871,7 @@ int main(int argc, char *argv[]) nutscan_avail_ipmi = 0; } #else + setproctag("ipmi"); upsdebugx(1, "IPMI SCAN: no pthread support, starting nutscan_scan_ipmi..."); /* dev[TYPE_IPMI] = nutscan_scan_ipmi(start_ip, end_ip, &ipmi_sec); */ run_ipmi(&ipmi_sec); @@ -1872,6 +1889,7 @@ int main(int argc, char *argv[]) nutscan_avail_upower = 0; } #else + setproctag("upower"); upsdebugx(1, "UPOWER SCAN: no pthread support, starting nutscan_scan_upower..."); run_upower(NULL); #endif /* HAVE_PTHREAD */ @@ -1889,6 +1907,7 @@ int main(int argc, char *argv[]) /* upsdebugx(1, "pthread_create returned an error; disabling this scan mode"); */ /* nutscan_avail_eaton_serial(?) = 0; */ #else + setproctag("serial"); upsdebugx(1, "SERIAL SCAN: no pthread support, starting nutscan_scan_eaton_serial..."); /* dev[TYPE_EATON_SERIAL] = nutscan_scan_eaton_serial (serial_ports); */ run_eaton_serial(serial_ports); @@ -1936,6 +1955,8 @@ int main(int argc, char *argv[]) } #endif /* HAVE_PTHREAD */ + setproctag("post-processing"); + upsdebugx(1, "SCANS DONE: display results"); upsdebugx(1, "SCANS DONE: display results: USB"); @@ -1983,6 +2004,7 @@ int main(int argc, char *argv[]) upsdebugx(1, "SCANS DONE: free resources: SERIAL"); nutscan_free_device(dev[TYPE_EATON_SERIAL]); + setproctag("cleanup"); #ifdef HAVE_PTHREAD # ifdef HAVE_SEMAPHORE_UNNAMED sem_destroy(nutscan_semaphore()); @@ -1999,6 +2021,10 @@ int main(int argc, char *argv[]) nutscan_free_ip_ranges(&ip_ranges_list); nutscan_free(); + /* Not a sub-process (do not let common::proctag_cleanup() mis-report us as such) */ + setproctag(progname); + upsdebugx(1, "SCANS DONE: EXIT_SUCCESS"); + return EXIT_SUCCESS; }