diff --git a/Makefile.am b/Makefile.am index 111cafb6e9..dba5dc23df 100644 --- a/Makefile.am +++ b/Makefile.am @@ -571,9 +571,9 @@ all/tests/NIT: all/clients all/server all-drivers all-recursive/tools all-recurs ### LIB-Requires-ext: #COMMENTED-AWAY# common/libcommon.la ### Requires-ext: common/libcommon.la common/libnutconf.la ### Requires-ext: clients/libnutclient.la clients/libnutclientstub.la -### Requires-ext: drivers/libdummy_mockdrv.la +### Requires-ext: drivers/libdummy_mockdrv.la tools/nut-scanner/libnutscan.la ### Requires-int: libdriverstubusb.la -all/tests: all-libs-local/tests all-libs-local/drivers all-libs-local/common all-libs-local/clients @dotMAKE@ +all/tests: all-libs-local/tests all-libs-local/drivers all-libs-local/common all-libs-local/clients all-libs-local/tools/nut-scanner @dotMAKE@ +@NUT_VERSION_H_GENERATED=true; export NUT_VERSION_H_GENERATED; \ $(SUBDIR_TGT_RULE) diff --git a/NEWS.adoc b/NEWS.adoc index 51c221fc52..ff5ce604c7 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -55,6 +55,18 @@ https://github.com/networkupstools/nut/milestone/13 extracting the response. Returned keys and descriptions remain byte sequences. [issue #3621] * `nut-scanner` tool updates: + - Retry SNMP hosts whose session could not open because the process + ran out of file descriptors. After the original SNMP scans finish, + retry those hosts one at a time and report any remaining descriptor + exhaustion. Other scan types and their limits are unchanged. + [issue #2576] + - Keep serial scan permits balanced when admission or thread creation + fails, and retain port names until all started workers finish if + allocating thread tracking fails. + - Balance concurrency permits and worker ownership during SNMP, IPMI, + NUT and XML/HTTP scans, including disabled protocol limits and failed + thread creation. Keep NUT authentication data alive until the scan + worker has finished. [issue #1166] - Mutexes used in parallelized scans were not properly released on failure code paths (impacts likely all 2.8.x until now). [PR #3551] - Third-party libraries which `nut-scanner` loads dynamically (libusb, diff --git a/configure.ac b/configure.ac index a4f3a6c632..14989fdd42 100644 --- a/configure.ac +++ b/configure.ac @@ -6444,6 +6444,7 @@ AC_SUBST(LIBGIO_CFLAGS) AC_SUBST(LIBGIO_LIBS) AC_SUBST(LIBNETSNMP_CFLAGS) AC_SUBST(LIBNETSNMP_LIBS) +AC_SUBST(LIBNETSNMP_LIBS_EXEC) AC_SUBST(LIBREGEX_LIBS) AC_SUBST(LIBUSB_CFLAGS) AC_SUBST(LIBUSB_LIBS) diff --git a/m4/nut_check_libnetsnmp.m4 b/m4/nut_check_libnetsnmp.m4 index 089de14960..e9cf61f812 100644 --- a/m4/nut_check_libnetsnmp.m4 +++ b/m4/nut_check_libnetsnmp.m4 @@ -1,6 +1,6 @@ dnl Check for LIBNETSNMP compiler flags. On success, set dnl nut_have_libnetsnmp="yes" and set LIBNETSNMP_CFLAGS and -dnl LIBNETSNMP_LIBS. On failure, set nut_have_libnetsnmp="no". +dnl LIBNETSNMP_LIBS and LIBNETSNMP_LIBS_EXEC. On failure, set nut_have_libnetsnmp="no". dnl This macro can be run multiple times, but will do the dnl checking only once. @@ -18,6 +18,9 @@ AC_DEFUN([NUT_CHECK_LIBNETSNMP], NUT_ARG_WITH_LIBOPTS_LIBS([snmp], [auto], [Net-SNMP]) nut_noncv_checked_libnetsnmp_now=no + dnl Refresh older caches which did not retain executable linker flags. + AS_IF([test "${nut_cv_LIBNETSNMP_LIBS_EXEC+set}" != set], + [AS_UNSET([nut_cv_checked_libnetsnmp])]) AC_CACHE_VAL([nut_cv_checked_libnetsnmp], [ nut_noncv_checked_libnetsnmp_now=yes @@ -129,6 +132,8 @@ AC_DEFUN([NUT_CHECK_LIBNETSNMP], ) AC_MSG_RESULT([${depLIBS} (source: ${depLIBS_SOURCE})]) + depLIBS_EXEC="${depLIBS}" + dnl Check if the Net-SNMP library is usable CFLAGS="${CFLAGS_ORIG} ${depCFLAGS}" LIBS="${LIBS_ORIG} ${depLIBS}" @@ -150,6 +155,8 @@ AC_DEFUN([NUT_CHECK_LIBNETSNMP], [*mingw*], [ AC_MSG_NOTICE([mingw builds of net-snmp might provide only a static library - retrying for that]) depLIBS="`$PKG_CONFIG --silence-errors --libs --static netsnmp 2>/dev/null`" + dnl Executables need ordinary flags after their static libraries. + depLIBS_EXEC="${depLIBS}" dnl # Some workarouds here, to avoid libtool bailing out like this: dnl # *** Warning: This system cannot link to static lib archive /usr/x86_64-w64-mingw32/lib//libnetsnmp.la. dnl # *** I have the capability to make that library automatically link in when @@ -174,12 +181,14 @@ AC_DEFUN([NUT_CHECK_LIBNETSNMP], nut_cv_LIBNETSNMP_CFLAGS="" nut_cv_LIBNETSNMP_LIBS="" + nut_cv_LIBNETSNMP_LIBS_EXEC="" nut_cv_SOFILE_LIBNETSNMP="" nut_cv_SONAME_LIBNETSNMP="" nut_cv_SOPATH_LIBNETSNMP="" AS_IF([test "${nut_cv_have_libnetsnmp}" = "yes"], [ nut_cv_LIBNETSNMP_CFLAGS="${depCFLAGS}" nut_cv_LIBNETSNMP_LIBS="${depLIBS}" + nut_cv_LIBNETSNMP_LIBS_EXEC="${depLIBS_EXEC}" AC_CACHE_CHECK([for defined usmAESPrivProtocol], [nut_cv_NUT_HAVE_LIBNETSNMP_usmAESPrivProtocol], @@ -378,6 +387,7 @@ int num = NETSNMP_DRAFT_BLUMENTHAL_AES_04 + 1; /* if defined, NETSNMP_DRAFT_BLUM AC_CACHE_VAL([nut_cv_prefer_NET_SNMP_CONFIG], []) AC_CACHE_VAL([nut_cv_LIBNETSNMP_CFLAGS], []) AC_CACHE_VAL([nut_cv_LIBNETSNMP_LIBS], []) + AC_CACHE_VAL([nut_cv_LIBNETSNMP_LIBS_EXEC], []) AC_CACHE_VAL([nut_cv_LIBNETSNMP_CFLAGS_SOURCE], []) AC_CACHE_VAL([nut_cv_LIBNETSNMP_LIBS_SOURCE], []) AC_CACHE_VAL([nut_cv_SOFILE_LIBNETSNMP], []) @@ -385,6 +395,7 @@ int num = NETSNMP_DRAFT_BLUMENTHAL_AES_04 + 1; /* if defined, NETSNMP_DRAFT_BLUM unset depCFLAGS unset depLIBS + unset depLIBS_EXEC unset depCFLAGS_SOURCE unset depLIBS_SOURCE @@ -431,6 +442,7 @@ int num = NETSNMP_DRAFT_BLUMENTHAL_AES_04 + 1; /* if defined, NETSNMP_DRAFT_BLUM LIBNETSNMP_CFLAGS="${nut_cv_LIBNETSNMP_CFLAGS}" LIBNETSNMP_LIBS="${nut_cv_LIBNETSNMP_LIBS}" + LIBNETSNMP_LIBS_EXEC="${nut_cv_LIBNETSNMP_LIBS_EXEC}" dnl For nut-scanner style autoloading: SOPATH_LIBNETSNMP="${nut_cv_SOPATH_LIBNETSNMP}" diff --git a/tests/Makefile.am b/tests/Makefile.am index 3577f83028..495a8a6a4d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -56,7 +56,9 @@ $(top_builddir)/common/libcommon.la \ $(top_builddir)/common/libparseconf.la \ $(top_builddir)/clients/libupsclient.la \ $(top_builddir)/clients/libnutclient.la \ -$(top_builddir)/clients/libnutclientstub.la: dummy @dotMAKE@ +$(top_builddir)/clients/libnutclientstub.la \ +$(top_builddir)/tools/nut-scanner/nutscan-snmp.h \ +$(top_builddir)/tools/nut-scanner/libnutscan.la: dummy @dotMAKE@ +@cd $(@D) && $(MAKE) $(AM_MAKEFLAGS) $(@F) $(top_builddir)/clients/upslog$(EXEEXT): dummy @dotMAKE@ @@ -66,6 +68,9 @@ $(top_builddir)/clients/upslog$(EXEEXT): dummy @dotMAKE@ # started from scratch right in this dir get dependencies in proper order # (sub-makes are independent as far as trying to write into same files): $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonclient.la: $(top_builddir)/common/libparseconf.la +# These independent sub-makes share the replacement-function objects. +# Finish one library before the other can rebuild those objects. +$(top_builddir)/common/libcommon.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/common/libcommonversion.la: $(top_builddir)/include/nut_version.h $(top_builddir)/clients/libnutclientstub.la: $(top_builddir)/clients/libnutclient.la @@ -104,6 +109,13 @@ NUT_LIBCOMMON = $(top_builddir)/common/libcommon.la endif !ENABLE_SHARED_PRIVATE_LIBS +# Finish shared dependencies before the scanner's independent sub-make +# (normally invoked only if building WITH_NUT_SCANNER). +$(top_builddir)/tools/nut-scanner/libnutscan.la: $(NUT_LIBCOMMON) $(top_builddir)/common/libcommonversion.la $(top_builddir)/clients/libupsclient.la +# The scanner library generates this header before the tests can include it +# (if building WITH_NUT_SCANNER and WITH_SNMP). +$(top_builddir)/tools/nut-scanner/nutscan-snmp.h: $(top_builddir)/tools/nut-scanner/libnutscan.la + TESTS += test_cgilib test_cgilib_SOURCES = test_cgilib.c nodist_test_cgilib_SOURCES = cgilib.c @@ -312,6 +324,68 @@ endif !WITH_OPENSSL endif WITH_SSL endif WITH_SNMP +# Compile each scanner's actual admission loop with offline worker probes. +if WITH_NUT_SCANNER +NUTSCAN_TEST_CFLAGS = $(AM_CFLAGS) -I$(top_srcdir)/tools/nut-scanner -I$(top_builddir)/tools/nut-scanner -I$(top_srcdir)/clients $(LIBIPMI_CFLAGS) +NUTSCAN_TEST_LDADD = $(NUT_LIBCOMMON) $(top_builddir)/tools/nut-scanner/libnutscan.la $(LIBLTDL_LIBS) $(SEMLIBS) $(NETLIBS) +if WITH_SSL +NUTSCAN_TEST_CFLAGS += $(LIBSSL_CFLAGS) +NUTSCAN_TEST_LDADD += $(LIBSSL_LIBS) +endif WITH_SSL +# Any fixture may pull the SNMP scanner from static libnutscan. +if WITH_SNMP_STATIC +NUTSCAN_TEST_LDADD += $(LIBNETSNMP_LIBS_EXEC) +endif WITH_SNMP_STATIC + +if WITH_SNMP +nutscan-thread-test.c nutscan-snmp-retry-test.c: $(top_builddir)/tools/nut-scanner/nutscan-snmp.h + +TESTS += nutscan-snmp-retry-test +nutscan_snmp_retry_test_SOURCES = nutscan-snmp-retry-test.c +nutscan_snmp_retry_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) $(LIBNETSNMP_CFLAGS) +nutscan_snmp_retry_test_LDADD = $(NUTSCAN_TEST_LDADD) +nutscan_snmp_retry_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_snmp.c $(top_builddir)/tools/nut-scanner/nutscan-snmp.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.c $(top_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +endif WITH_SNMP + +if WITH_THREADING +TESTS += nutscan-serial-test +nutscan_serial_test_SOURCES = nutscan-thread-test.c +nutscan_serial_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) -DNUTSCAN_TEST_PROTOCOL=5 +nutscan_serial_test_LDADD = $(top_builddir)/drivers/libserial-nutscan.la $(NUTSCAN_TEST_LDADD) $(SERLIBS) +nutscan_serial_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_eaton_serial.c $(top_srcdir)/tools/nut-scanner/nutscan-thread.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.c $(top_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) + +TESTS += nutscan-nut-test +nutscan_nut_test_SOURCES = nutscan-thread-test.c +nutscan_nut_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) -DNUTSCAN_TEST_PROTOCOL=3 +nutscan_nut_test_LDADD = $(NUTSCAN_TEST_LDADD) +nutscan_nut_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_nut.c $(top_srcdir)/tools/nut-scanner/nutscan-thread.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.c $(top_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) + +if WITH_SNMP +TESTS += nutscan-snmp-test +nutscan_snmp_test_SOURCES = nutscan-thread-test.c +nutscan_snmp_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) $(LIBNETSNMP_CFLAGS) -DNUTSCAN_TEST_PROTOCOL=1 +nutscan_snmp_test_LDADD = $(NUTSCAN_TEST_LDADD) +nutscan_snmp_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_snmp.c $(top_builddir)/tools/nut-scanner/nutscan-snmp.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.c $(top_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +endif WITH_SNMP + +if WITH_IPMI +TESTS += nutscan-ipmi-test +nutscan_ipmi_test_SOURCES = nutscan-thread-test.c +nutscan_ipmi_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) -DNUTSCAN_TEST_PROTOCOL=2 +nutscan_ipmi_test_LDADD = $(NUTSCAN_TEST_LDADD) +nutscan_ipmi_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_ipmi.c $(top_srcdir)/tools/nut-scanner/nutscan-thread.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.c $(top_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +endif WITH_IPMI + +if WITH_NEON +TESTS += nutscan-xml-test +nutscan_xml_test_SOURCES = nutscan-thread-test.c +nutscan_xml_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) $(LIBNEON_CFLAGS) -DNUTSCAN_TEST_PROTOCOL=4 +nutscan_xml_test_LDADD = $(NUTSCAN_TEST_LDADD) +nutscan_xml_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_xml_http.c $(top_srcdir)/tools/nut-scanner/nutscan-thread.h $(top_srcdir)/tools/nut-scanner/nutscan-thread.c $(top_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +endif WITH_NEON +endif WITH_THREADING +endif WITH_NUT_SCANNER + ### Optional tests which can not be built everywhere # List of src files for CppUnit tests CPPUNITTESTSRC = example.cpp nutclienttest.cpp diff --git a/tests/nutscan-snmp-retry-test.c b/tests/nutscan-snmp-retry-test.c new file mode 100644 index 0000000000..1297bcafde --- /dev/null +++ b/tests/nutscan-snmp-retry-test.c @@ -0,0 +1,318 @@ +/* Offline checks of SNMP session-open recovery through the actual scanner. + * Copyright (C) 2026 NUT contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#include "common.h" +/* Keep the included scanner's mutex separate from libnutscan. */ +#define threadcount_mutex test_threadcount_mutex +#include "nut-scan.h" +/* Test setup, execution and checks must run even with -DNDEBUG. */ +#undef NDEBUG +#include + +static void *test_malloc(size_t size); +static void test_free(void *ptr); +#ifdef HAVE_PTHREAD +static void *test_realloc(void *ptr, size_t size); +static int test_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*worker)(void *), void *arg); +# define realloc test_realloc +# define pthread_create test_create +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +static int test_wait(sem_t *sem); +# define sem_wait test_wait +# endif +#endif +#define malloc test_malloc +#define free test_free +#include "nutscan-thread.c" +#include "scan_snmp.c" +#undef malloc +#undef free +#undef realloc +#undef pthread_create +#undef sem_wait + +#ifdef HAVE_PTHREAD +static pthread_mutex_t test_mutex = PTHREAD_MUTEX_INITIALIZER; +# ifdef HAVE_PTHREAD_TRYJOIN +pthread_mutex_t threadcount_mutex = PTHREAD_MUTEX_INITIALIZER; +# endif +# define LOCK() pthread_mutex_lock(&test_mutex) +# define UNLOCK() pthread_mutex_unlock(&test_mutex) +#else +# define LOCK() ((void)0) +# define UNLOCK() ((void)0) +#endif + +static int mode, attempts[5], closed[5], malloc_calls, fail_malloc; +static void *allocations[16]; +static int test_snmp_error; +#ifdef HAVE_PTHREAD +static int realloc_calls, create_calls; +#endif + +static void *test_malloc(size_t size) +{ + void *ptr; + size_t i; + + LOCK(); + malloc_calls++; + if (malloc_calls == fail_malloc) { + UNLOCK(); + return NULL; + } + ptr = malloc(size); + assert(ptr != NULL); + for (i = 0; i < sizeof(allocations) / sizeof(allocations[0]); i++) { + if (allocations[i] == NULL) { + allocations[i] = ptr; + UNLOCK(); + return ptr; + } + } + abort(); +} + +static void test_free(void *ptr) +{ + size_t i; + + LOCK(); + for (i = 0; i < sizeof(allocations) / sizeof(allocations[0]); i++) { + if (allocations[i] == ptr) { + allocations[i] = NULL; + break; + } + } + free(ptr); + UNLOCK(); +} + +#ifdef HAVE_PTHREAD +static void *test_realloc(void *ptr, size_t size) +{ + if (++realloc_calls == 3 && fail_malloc == -2) { + return NULL; + } + return realloc(ptr, size); +} + +static int test_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*worker)(void *), void *arg) +{ + if (++create_calls == 3 && fail_malloc == -3) { + return EAGAIN; + } + return pthread_create(thread, attr, worker, arg); +} + +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +static int test_wait(sem_t *sem) +{ + if (malloc_calls > 5 && fail_malloc == -4) { + errno = EINVAL; + return -1; + } + return sem_wait(sem); +} +# endif +#endif + +static int host_index(const char *host) +{ + int index = host[strlen(host) - 1] - '1'; + assert(index >= 0 && index < 5); + return index; +} + +static void session_init(netsnmp_session *session) +{ + memset(session, 0, sizeof(*session)); + /* An unrelated earlier call must not cause a false retry. */ + errno = EMFILE; +} + +static void *session_open(netsnmp_session *session) +{ + int index = host_index(session->peername); + int attempt; + + LOCK(); + attempt = ++attempts[index]; + UNLOCK(); + if (mode == 3) { + session->s_errno = EACCES; + errno = EMFILE; + return NULL; + } + if (mode == 4) { + /* Leave errno untouched after the caller cleared stale EMFILE. */ + return NULL; + } + if (mode == 2 || (mode != 0 && attempt == 1)) { + session->s_errno = mode == 6 ? 0 : EMFILE; + errno = mode == 5 ? EACCES : EMFILE; + return NULL; + } + return session->peername; +} + +static int session_close(void *handle) +{ + LOCK(); + closed[host_index((const char *)handle)]++; + UNLOCK(); + return 1; +} + +static void *parse_oid(const char *input, oid *output, size_t *length) +{ + NUT_UNUSED_VARIABLE(input); + NUT_UNUSED_VARIABLE(output); + NUT_UNUSED_VARIABLE(length); + /* Stop after a successful session open. Protocol discovery is separate. */ + return NULL; +} + +static char *toggle_options(char *options) +{ + NUT_UNUSED_VARIABLE(options); + return NULL; +} + +static const char *error_string(int error) +{ + NUT_UNUSED_VARIABLE(error); + return "offline probe complete"; +} + +static void scenario(int error_mode, unsigned int limit, int allocation_failure) +{ + nutscan_ip_range_list_t ranges; + nutscan_snmp_t settings; + size_t i; +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + sem_t *global; +# if !defined HAVE_SEMAPHORE_UNNAMED + char name[128]; +# endif +#endif + mode = error_mode; + memset(attempts, 0, sizeof(attempts)); + memset(closed, 0, sizeof(closed)); + malloc_calls = 0; +#ifdef HAVE_PTHREAD + realloc_calls = create_calls = 0; +#endif + fail_malloc = allocation_failure; + memset(&settings, 0, sizeof(settings)); + settings.community = "public"; + nutscan_init_ip_ranges(&ranges); + nutscan_add_ip_range(&ranges, strdup("192.0.2.1"), strdup("192.0.2.5")); +#if defined HAVE_PTHREAD && (defined HAVE_PTHREAD_TRYJOIN || defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + max_threads = 2; + max_threads_netsnmp = limit; + curr_threads = 0; +#else + NUT_UNUSED_VARIABLE(limit); +#endif +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) +# if defined HAVE_SEMAPHORE_UNNAMED + global = nutscan_semaphore(); + assert(sem_init(global, 0, 2) == 0); +# else + snprintf(name, sizeof(name), "/nut-snmp-retry-%ld", (long)getpid()); + global = sem_open(name, O_CREAT | O_EXCL, 0600, 2); + assert(global != SEM_FAILED); + assert(sem_unlink(name) == 0); + nutscan_semaphore_set(global); +# endif +#endif + assert(nutscan_scan_ip_range_snmp(&ranges, 1, &settings) == NULL); + nutscan_free_ip_ranges(&ranges); + for (i = 0; i < sizeof(allocations) / sizeof(allocations[0]); i++) { + assert(allocations[i] == NULL); + } +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + assert(sem_trywait(global) == 0); + assert(sem_trywait(global) == 0); + assert(sem_trywait(global) == -1 && errno == EAGAIN); +# if defined HAVE_SEMAPHORE_UNNAMED + assert(sem_destroy(global) == 0); +# else + assert(sem_close(global) == 0); + nutscan_semaphore_set(NULL); +# endif +#endif +#if defined HAVE_PTHREAD && (defined HAVE_PTHREAD_TRYJOIN || defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + assert(curr_threads == 0); +#endif + for (i = 0; i < 5; i++) { + int expected_attempts = 1; + int expected_closed = mode == 0 ? 1 : 0; +#ifdef HAVE_PTHREAD + if (mode == 1 || mode == 2 || mode == 5 || mode == 6) { + expected_attempts = 2; + expected_closed = mode == 2 ? 0 : 1; + } +#endif + if (allocation_failure == 2 && i > 0) { + expected_attempts = expected_closed = 0; + } +#ifdef HAVE_PTHREAD + if ((allocation_failure == -2 && i >= 2) + || (allocation_failure == -3 && i == 2) + ) { + expected_attempts = expected_closed = 0; + } + if (allocation_failure == 6 || allocation_failure == -4) { + expected_attempts = 1; + expected_closed = 0; + } +#endif + assert(attempts[i] == expected_attempts); + assert(closed[i] == expected_closed); + } + printf("mode=%d limit=%u allocation_failure=%d: PASS\n", mode, limit, allocation_failure); +} + +int main(void) +{ + int error_mode; + unsigned int limit; +#ifdef WIN32 + WSADATA wsa_data; + + assert(WSAStartup(MAKEWORD(2, 2), &wsa_data) == 0); +#endif + + nutscan_avail_snmp = 1; + nut_initialized_snmp = 1; + nut_snmp_sess_init = session_init; + nut_snmp_sess_open = session_open; + nut_snmp_sess_close = session_close; + nut_snmp_parse_oid = parse_oid; + nut_snmp_out_toggle_options = toggle_options; + nut_snmp_api_errstring = error_string; + nut_snmp_errno = &test_snmp_error; + for (limit = 0; limit <= 3; limit++) { + for (error_mode = 0; error_mode <= 6; error_mode++) { + scenario(error_mode, limit, 0); + } + scenario(1, limit, 2); +#ifdef HAVE_PTHREAD + scenario(1, limit, 6); + scenario(1, limit, -2); + scenario(1, limit, -3); +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + scenario(1, limit, -4); +# endif +#endif + } +#ifdef WIN32 + assert(WSACleanup() == 0); +#endif + return EXIT_SUCCESS; +} diff --git a/tests/nutscan-thread-test.c b/tests/nutscan-thread-test.c new file mode 100644 index 0000000000..d1d6c4fedf --- /dev/null +++ b/tests/nutscan-thread-test.c @@ -0,0 +1,682 @@ +/* Offline regression checks for scanner admission and worker ownership. + * Copyright (C) 2026 NUT contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#include "common.h" +/* Keep the included scanner's mutex separate from libnutscan. */ +#define threadcount_mutex test_threadcount_mutex +#include "nut-scan.h" +#include "upsclient.h" +/* Test setup, execution and checks must run even with -DNDEBUG. */ +#undef NDEBUG +#include + +#if defined WIN32 && NUTSCAN_TEST_PROTOCOL == 4 +/* Keep the allocation wrappers out of Windows inline socket helpers. */ +# include +# include +#endif + +#if defined HAVE_PTHREAD && (defined HAVE_PTHREAD_TRYJOIN || defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) +# if NUTSCAN_TEST_PROTOCOL == 5 +# include "nutscan-serial.h" +static char **test_serial_ports(const char *range); +static int test_join(pthread_t thread, void **result); +# if defined HAVE_PTHREAD_TRYJOIN && !defined HAVE_SEMAPHORE_UNNAMED && !defined HAVE_SEMAPHORE_NAMED +static int test_tryjoin(pthread_t thread, void **result); +# endif +# endif +# if NUTSCAN_TEST_PROTOCOL != 5 +static void *test_malloc(size_t size); +# endif +static void *test_realloc(void *ptr, size_t size); +static void test_free(void *ptr); +static int test_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*worker)(void *), void *arg); +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +static int test_wait(sem_t *sem); +static int test_trywait(sem_t *sem); +# if NUTSCAN_TEST_PROTOCOL != 5 +# ifdef HAVE_SEMAPHORE_UNNAMED +static int test_init(sem_t *sem, int shared, unsigned int value); +static int test_destroy(sem_t *sem); +# else +static sem_t *test_open(const char *name, int flags, ...); +static int test_close(sem_t *sem); +static int test_unlink(const char *name); +# endif +# endif +# endif + +/* Compile the actual scanner loop. Only probes and injected resource + * failures are replaced; threads, semaphore accounting and cleanup run. + */ +# if NUTSCAN_TEST_PROTOCOL != 5 +# define malloc test_malloc +# endif +# define realloc test_realloc +# define free test_free +# define pthread_create test_create +# if NUTSCAN_TEST_PROTOCOL == 5 +# define nutscan_get_serial_ports_list test_serial_ports +# define pthread_join test_join +# if defined HAVE_PTHREAD_TRYJOIN && !defined HAVE_SEMAPHORE_UNNAMED && !defined HAVE_SEMAPHORE_NAMED +# define pthread_tryjoin_np test_tryjoin +# endif +# endif +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +# define sem_wait test_wait +# define sem_trywait test_trywait +# if NUTSCAN_TEST_PROTOCOL != 5 +# ifdef HAVE_SEMAPHORE_UNNAMED +# define sem_init test_init +# define sem_destroy test_destroy +# else +# define sem_open test_open +# define sem_close test_close +# define sem_unlink test_unlink +# endif +# endif +# endif + +/* Intercept the shared helpers as well as the scanner loop. */ +# include "nutscan-thread.c" + +# if NUTSCAN_TEST_PROTOCOL == 1 +# include "scan_snmp.c" +# elif NUTSCAN_TEST_PROTOCOL == 2 +# include "scan_ipmi.c" +# elif NUTSCAN_TEST_PROTOCOL == 3 +# include "scan_nut.c" +# elif NUTSCAN_TEST_PROTOCOL == 5 +# include "scan_eaton_serial.c" +# else +# include "scan_xml_http.c" +# endif + +# undef malloc +# undef realloc +# undef free +# undef pthread_create +# undef nutscan_get_serial_ports_list +# undef pthread_join +# undef pthread_tryjoin_np +# undef sem_wait +# undef sem_trywait +# undef sem_init +# undef sem_destroy +# undef sem_open +# undef sem_close +# undef sem_unlink + +/* The library keeps this counter mutex private. The included scanner + * owns the test instance and never runs a library scanner. */ +# ifdef HAVE_PTHREAD_TRYJOIN +pthread_mutex_t threadcount_mutex; +# endif + +static int fail_malloc, fail_realloc, fail_create, create_calls, completed; +static int allocations, auth_allocations; +static void *arguments[32]; +static pthread_mutex_t test_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t test_done = PTHREAD_COND_INITIALIZER; +static int completed_before_return; +# if NUTSCAN_TEST_PROTOCOL == 5 +static int live_workers, release_workers; + +static char **test_serial_ports(const char *range) +{ + char **ports = (char **)calloc(6, sizeof(*ports)); + size_t i; + + NUT_UNUSED_VARIABLE(range); + assert(ports != NULL); + for (i = 0; i < 5; i++) { + ports[i] = strdup("test-port"); + assert(ports[i] != NULL); + } + return ports; +} + +/* Keep serial workers using borrowed port names alive until a join attempt. + * This exposes cleanup which frees port storage before tracking workers. + */ +static void release_serial_workers(void) +{ + pthread_mutex_lock(&test_mutex); + release_workers = 1; + pthread_cond_broadcast(&test_done); + pthread_mutex_unlock(&test_mutex); +} + +static int test_join(pthread_t thread, void **result) +{ + release_serial_workers(); + return pthread_join(thread, result); +} + +# if defined HAVE_PTHREAD_TRYJOIN && !defined HAVE_SEMAPHORE_UNNAMED && !defined HAVE_SEMAPHORE_NAMED +static int test_tryjoin(pthread_t thread, void **result) +{ + release_serial_workers(); + return pthread_tryjoin_np(thread, result); +} +# endif +# endif + +# if NUTSCAN_TEST_PROTOCOL != 5 +static void *test_malloc(size_t size) +{ + void *ptr; + size_t i; + + if (fail_malloc && --fail_malloc == 0) { + return NULL; + } + ptr = malloc(size); + assert(ptr != NULL); + for (i = 0; i < sizeof(arguments) / sizeof(arguments[0]); i++) { + if (arguments[i] == NULL) { + arguments[i] = ptr; + allocations++; + return ptr; + } + } + abort(); +} +# endif + +static void *test_realloc(void *ptr, size_t size) +{ + if (fail_realloc && --fail_realloc == 0) { + return NULL; + } + return realloc(ptr, size); +} + +static void test_free(void *ptr) +{ + size_t i; + +# if NUTSCAN_TEST_PROTOCOL == 5 + pthread_mutex_lock(&test_mutex); + assert(live_workers == 0); + pthread_mutex_unlock(&test_mutex); +# endif + + if (ptr != NULL) { + for (i = 0; i < sizeof(arguments) / sizeof(arguments[0]); i++) { + if (arguments[i] == ptr) { + arguments[i] = NULL; + allocations--; + break; + } + } + } + free(ptr); +} + +struct test_worker { + void *(*worker)(void *); + void *arg; +}; + +static void *offline_worker(void *opaque) +{ + struct test_worker *work = (struct test_worker *)opaque; + +# if NUTSCAN_TEST_PROTOCOL == 5 + pthread_mutex_lock(&test_mutex); + while (!release_workers) { + pthread_cond_wait(&test_done, &test_mutex); + } + pthread_mutex_unlock(&test_mutex); + assert(strcmp((char *)work->arg, "test-port") == 0); + assert(pthread_mutex_lock(&dev_mutex) == 0); + assert(pthread_mutex_unlock(&dev_mutex) == 0); +# elif NUTSCAN_TEST_PROTOCOL == 3 + work->worker(work->arg); +# else +# if NUTSCAN_TEST_PROTOCOL == 1 + nutscan_snmp_t *arg = (nutscan_snmp_t *)work->arg; +# elif NUTSCAN_TEST_PROTOCOL == 2 + nutscan_ipmi_t *arg = (nutscan_ipmi_t *)work->arg; +# else + nutscan_xml_t *arg = (nutscan_xml_t *)work->arg; +# endif + test_free(arg->peername); + test_free(arg); +# endif + free(work); + pthread_mutex_lock(&test_mutex); + completed++; +# if NUTSCAN_TEST_PROTOCOL == 5 + live_workers--; +# endif + pthread_cond_signal(&test_done); + pthread_mutex_unlock(&test_mutex); + return NULL; +} + +static int test_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*worker)(void *), void *arg) +{ + struct test_worker *work; + int ret, previous; + + create_calls++; + if (fail_create && --fail_create == 0) { + return EAGAIN; + } + work = (struct test_worker *)malloc(sizeof(*work)); + assert(work != NULL); + work->worker = worker; + work->arg = arg; + pthread_mutex_lock(&test_mutex); + previous = completed; + ret = pthread_create(thread, attr, offline_worker, work); + if (ret != 0) { + free(work); + } +# if NUTSCAN_TEST_PROTOCOL == 5 + else { + live_workers++; + } +# endif + while (ret == 0 && completed_before_return && completed == previous) { + pthread_cond_wait(&test_done, &test_mutex); + } + pthread_mutex_unlock(&test_mutex); + return ret; +} + +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +/* Borrowed only until the scanner destroys or closes its semaphore. */ +static sem_t *global_sem, *protocol_sem; +# if NUTSCAN_TEST_PROTOCOL != 5 +static unsigned int protocol_capacity; +# endif +static int fail_init, interrupt_wait, fail_wait, fail_try_global, fail_try_protocol; +# if NUTSCAN_TEST_PROTOCOL != 5 && defined HAVE_SEMAPHORE_NAMED && !defined HAVE_SEMAPHORE_UNNAMED +static char protocol_name[128]; +# endif + +/* sem_getvalue() is unavailable on some supported systems. */ +static void check_capacity(sem_t *sem, unsigned int expected) +{ + unsigned int actual = 0, i; + + while (sem_trywait(sem) == 0) { + actual++; + } + assert(errno == EAGAIN); + assert(actual == expected); + for (i = 0; i < actual; i++) { + assert(sem_post(sem) == 0); + } +} + +static int test_wait(sem_t *sem) +{ + assert(sem == global_sem || (protocol_sem != NULL && sem == protocol_sem)); + if (interrupt_wait && --interrupt_wait == 0) { + errno = EINTR; + return -1; + } + if (fail_wait && --fail_wait == 0) { + errno = EINVAL; + return -1; + } + return sem_wait(sem); +} + +static int test_trywait(sem_t *sem) +{ + assert(sem == global_sem || (protocol_sem != NULL && sem == protocol_sem)); + if ((sem == global_sem && fail_try_global && --fail_try_global == 0) + || (sem == protocol_sem && fail_try_protocol && --fail_try_protocol == 0) + ) { + errno = EAGAIN; + return -1; + } + return sem_trywait(sem); +} + +# if NUTSCAN_TEST_PROTOCOL != 5 +# ifdef HAVE_SEMAPHORE_UNNAMED +static int test_init(sem_t *sem, int shared, unsigned int value) +{ + int ret; + + assert(sem != NULL && sem != global_sem && protocol_sem == NULL); + if (fail_init) { + errno = ENOSPC; + return -1; + } + ret = sem_init(sem, shared, value); + if (ret == 0) { + protocol_sem = sem; + protocol_capacity = value; + } + return ret; +} + +static int test_destroy(sem_t *sem) +{ + int ret; + + assert(protocol_sem != NULL && sem == protocol_sem); + check_capacity(sem, protocol_capacity); + ret = sem_destroy(sem); + assert(ret == 0); + protocol_sem = NULL; + return ret; +} +# else +static sem_t *test_open(const char *name, int flags, ...) +{ + va_list ap; + unsigned int value; + + NUT_UNUSED_VARIABLE(name); + NUT_UNUSED_VARIABLE(flags); + assert(protocol_sem == NULL); + if (fail_init) { + errno = ENOSPC; + return SEM_FAILED; + } + va_start(ap, flags); + (void)va_arg(ap, int); + value = va_arg(ap, unsigned int); + va_end(ap); + snprintf(protocol_name, sizeof(protocol_name), "/nut-test-proto-%ld", (long)getpid()); + protocol_sem = sem_open(protocol_name, O_CREAT | O_EXCL, 0600, value); + assert(protocol_sem != SEM_FAILED); + assert(sem_unlink(protocol_name) == 0); + protocol_capacity = value; + return protocol_sem; +} + +static int test_close(sem_t *sem) +{ + int ret; + + assert(protocol_sem != NULL && sem == protocol_sem); + check_capacity(sem, protocol_capacity); + ret = sem_close(sem); + assert(ret == 0); + protocol_sem = NULL; + return ret; +} + +static int test_unlink(const char *name) +{ + NUT_UNUSED_VARIABLE(name); + return 0; +} +# endif +# endif /* NUTSCAN_TEST_PROTOCOL != 5 */ +# endif /* semaphore support */ + +# if NUTSCAN_TEST_PROTOCOL == 1 +static char *offline_snmp_options(char *options) +{ + NUT_UNUSED_VARIABLE(options); + return NULL; +} +# endif + +# if NUTSCAN_TEST_PROTOCOL == 3 +static upscli_authconf_t *offline_auth_get(const char *user, const char *host, + const char *port, int add_to_list) +{ + upscli_authconf_t *auth = (upscli_authconf_t *)calloc(1, sizeof(*auth)); + + NUT_UNUSED_VARIABLE(user); + NUT_UNUSED_VARIABLE(host); + NUT_UNUSED_VARIABLE(port); + NUT_UNUSED_VARIABLE(add_to_list); + assert(auth != NULL); + auth_allocations++; + return auth; +} + +static upscli_authconf_t *offline_auth_find(const char *user, const char *host, const char *port) +{ + NUT_UNUSED_VARIABLE(user); + NUT_UNUSED_VARIABLE(host); + NUT_UNUSED_VARIABLE(port); + return NULL; +} + +static int offline_auth_init(upscli_authconf_t *auth) +{ + assert(auth != NULL); + return 0; +} + +static void offline_auth_free(upscli_authconf_t *auth) +{ + assert(auth != NULL && auth_allocations > 0); + auth_allocations--; + free(auth); +} + +static int offline_auth_read(const char *filename, int fatal_errors, int debug_level) +{ + NUT_UNUSED_VARIABLE(filename); + NUT_UNUSED_VARIABLE(fatal_errors); + NUT_UNUSED_VARIABLE(debug_level); + return 0; +} + +static int offline_authenticate(UPSCONN_t *ups, upscli_authconf_t *auth) +{ + NUT_UNUSED_VARIABLE(ups); + assert(auth != NULL && auth_allocations > 0); + return 0; +} + +static int offline_splitaddr(const char *target, char **host, uint16_t *port) +{ + assert(target != NULL && strlen(target) > 0); + assert(auth_allocations > 0); + NUT_UNUSED_VARIABLE(host); + NUT_UNUSED_VARIABLE(port); + return -1; +} + +static void offline_free_cert(const char *host, const char *cert) +{ + assert(host != NULL && strlen(host) > 0); + NUT_UNUSED_VARIABLE(cert); +} +# endif + +static void scan(unsigned int limit) +{ +# if NUTSCAN_TEST_PROTOCOL == 5 + NUT_UNUSED_VARIABLE(limit); + assert(nutscan_scan_eaton_serial("test-ports") == NULL); +# else + nutscan_ip_range_list_t ranges; +# if NUTSCAN_TEST_PROTOCOL == 1 + nutscan_snmp_t settings; +# elif NUTSCAN_TEST_PROTOCOL == 2 + nutscan_ipmi_t settings; +# elif NUTSCAN_TEST_PROTOCOL == 3 + nutscan_nut_authconf_t settings; +# else + nutscan_xml_t settings; +# endif + + memset(&settings, 0, sizeof(settings)); + nutscan_init_ip_ranges(&ranges); + nutscan_add_ip_range(&ranges, strdup("192.0.2.1"), strdup("192.0.2.5")); +# if NUTSCAN_TEST_PROTOCOL == 1 + max_threads_netsnmp = limit; + nutscan_avail_snmp = 1; + nut_initialized_snmp = 1; + nut_snmp_out_toggle_options = offline_snmp_options; + assert(nutscan_scan_ip_range_snmp(&ranges, 1, &settings) == NULL); +# elif NUTSCAN_TEST_PROTOCOL == 2 + max_threads_ipmi = limit; + nutscan_avail_ipmi = 1; + assert(nutscan_scan_ip_range_ipmi(&ranges, &settings) == NULL); +# elif NUTSCAN_TEST_PROTOCOL == 3 + max_threads_oldnut = limit; + nutscan_avail_nut = 1; + nut_upscli_splitaddr = offline_splitaddr; + nut_upscli_get_authconf_item = offline_auth_get; + nut_upscli_find_authconf_item = offline_auth_find; + nut_upscli_init_authconf = offline_auth_init; + nut_upscli_free_authconf_item = offline_auth_free; + nut_upscli_read_authconf_file = offline_auth_read; + nut_upscli_authenticate_authconf = offline_authenticate; + nut_upscli_free_host_cert = offline_free_cert; + settings.authconf_file = "none"; + assert(nutscan_scan_ip_range_nut_authconf(&ranges, &settings) == NULL); +# else + max_threads_netxml = limit; + nutscan_avail_xml_http = 1; + assert(nutscan_scan_ip_range_xml_http(&ranges, 1, &settings) == NULL); +# endif + nutscan_free_ip_ranges(&ranges); +# endif +} + +static void scenario(unsigned int limit, int failure) +{ +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +# if defined HAVE_SEMAPHORE_NAMED && !defined HAVE_SEMAPHORE_UNNAMED + char name[128]; +# endif +# endif + fail_malloc = fail_realloc = fail_create = 0; + create_calls = completed = 0; + completed_before_return = 1; +# if NUTSCAN_TEST_PROTOCOL == 5 + assert(live_workers == 0); + completed_before_return = 0; + release_workers = 0; +# endif + assert(allocations == 0); + assert(auth_allocations == 0); + if (failure == 1) { + fail_malloc = 1; + } else if (failure == 2) { + fail_realloc = 1; + } else if (failure == 3) { + fail_realloc = 2; + } else if (failure == 4) { + fail_create = 1; + } else if (failure == 5) { + fail_create = 2; + } + max_threads = 2; + curr_threads = 0; +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + assert(protocol_sem == NULL); + fail_init = failure == 6; + interrupt_wait = failure == 7 ? 1 : 0; + fail_wait = failure == 8 ? 1 : (failure == 9 ? 2 : 0); + fail_try_global = failure == 10 ? 1 : 0; + fail_try_protocol = failure == 11 ? 1 : 0; +# ifdef HAVE_SEMAPHORE_UNNAMED + global_sem = nutscan_semaphore(); + assert(sem_init(global_sem, 0, 2) == 0); +# else + snprintf(name, sizeof(name), "/nut-test-global-%ld", (long)getpid()); + global_sem = sem_open(name, O_CREAT | O_EXCL, 0600, 2); + assert(global_sem != SEM_FAILED); + assert(sem_unlink(name) == 0); + nutscan_semaphore_set(global_sem); +# endif +# endif +# ifdef HAVE_PTHREAD_TRYJOIN + assert(pthread_mutex_init(&threadcount_mutex, NULL) == 0); +# endif + scan(limit); + assert(allocations == 0); + assert(auth_allocations == 0); +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + check_capacity(global_sem, 2); + assert(protocol_sem == NULL); +# ifdef HAVE_SEMAPHORE_UNNAMED + assert(sem_destroy(global_sem) == 0); +# else + assert(sem_close(global_sem) == 0); + nutscan_semaphore_set(NULL); +# endif + global_sem = NULL; +# endif + assert(curr_threads == 0); + if (failure == 1 || failure == 2 || failure == 8 || failure == 9) { + assert(completed == 0); + } else if (failure == 3) { + assert(completed == 1); + } else if (failure == 4 || failure == 5) { + assert(completed == 4 && create_calls == 5); + } else { + assert(completed == 5 && create_calls == 5); + } +# ifdef HAVE_PTHREAD_TRYJOIN + assert(pthread_mutex_destroy(&threadcount_mutex) == 0); +# endif + printf("protocol=%d limit=%u failure=%d: PASS\n", NUTSCAN_TEST_PROTOCOL, limit, failure); +} +#endif /* HAVE_PTHREAD */ + +int main(int argc, char **argv) +{ +#if defined HAVE_PTHREAD && (defined HAVE_PTHREAD_TRYJOIN || defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + unsigned int limits[] = {0, 1, 3}; + size_t i; + int failure, repeat; +# ifdef WIN32 + WSADATA wsa_data; + + assert(WSAStartup(MAKEWORD(2, 2), &wsa_data) == 0); +# endif + + if (argc == 3) { + scenario((unsigned int)atoi(argv[1]), atoi(argv[2])); +# ifdef WIN32 + assert(WSACleanup() == 0); +# endif + return EXIT_SUCCESS; + } + + for (repeat = 0; repeat < 2; repeat++) { + for (i = 0; i < sizeof(limits) / sizeof(limits[0]); i++) { + for (failure = 0; failure < 12; failure++) { +# if NUTSCAN_TEST_PROTOCOL == 5 + /* Serial has no allocated worker argument or protocol limit. */ + if (i > 0 || failure == 1 || failure == 6 + || failure == 9 || failure == 11 + ) { + continue; + } +# endif +# if !defined HAVE_SEMAPHORE_UNNAMED && !defined HAVE_SEMAPHORE_NAMED + if (failure >= 6) { + continue; + } +# endif + if (limits[i] == 0 && (failure == 9 || failure == 11)) { + continue; + } + scenario(limits[i], failure); + } + } + } +# ifdef WIN32 + assert(WSACleanup() == 0); +# endif + return EXIT_SUCCESS; +#else + NUT_UNUSED_VARIABLE(argc); + NUT_UNUSED_VARIABLE(argv); + return 77; +#endif +} diff --git a/tools/nut-scanner/Makefile.am b/tools/nut-scanner/Makefile.am index c52dbbd3e9..3c48f879b7 100644 --- a/tools/nut-scanner/Makefile.am +++ b/tools/nut-scanner/Makefile.am @@ -92,7 +92,7 @@ endif !ENABLE_SHARED_PRIVATE_LIBS bin_PROGRAMS = lib_LTLIBRARIES = include_HEADERS = -dist_noinst_HEADERS = +dist_noinst_HEADERS = nutscan-thread.h # Note: we only build nut-scanner, and its library, if libltdl was found (required ATM!) if WITH_NUT_SCANNER @@ -101,7 +101,7 @@ if WITH_NUT_SCANNER endif WITH_NUT_SCANNER libnutscan_la_SOURCES = scan_nut.c scan_nut_simulation.c scan_ipmi.c \ nutscan-device.c nutscan-ip.c nutscan-display.c \ - nutscan-init.c scan_usb.c scan_snmp.c scan_xml_http.c \ + nutscan-init.c nutscan-thread.c scan_usb.c scan_snmp.c scan_xml_http.c \ scan_avahi.c scan_eaton_serial.c nutscan-serial.c \ scan_upower.c libnutscan_la_LIBADD = $(NETLIBS) diff --git a/tools/nut-scanner/nutscan-thread.c b/tools/nut-scanner/nutscan-thread.c new file mode 100644 index 0000000000..c324ab13ed --- /dev/null +++ b/tools/nut-scanner/nutscan-thread.c @@ -0,0 +1,97 @@ +/* Internal ownership helpers for parallel scanner workers. + * Copyright (C) + * 2016 - 2021 EATON - Various threads-related improvements + * 2020 - 2026 Jim Klimov + * 2026 NUT contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "common.h" +#include "nutscan-thread.h" + +#ifdef HAVE_PTHREAD + +/* Allocate the tracking entry before transferring ownership of arg. + * Return -1 for allocation failure, a pthread error, or 0 on success. + * On any failure the caller still owns arg and any admission permits. + */ +int nut_scanner_thread_create(nutscan_thread_t **array, size_t *count, + void *(*worker)(void *), void *arg) +{ + nutscan_thread_t *grown; + int ret; + + if (*count >= SIZE_MAX / sizeof(**array)) { + upsdebugx(1, "%s: Thread array is too large", __func__); + return -1; + } + + grown = (nutscan_thread_t *)realloc(*array, (*count + 1) * sizeof(**array)); + if (grown == NULL) { + upsdebugx(1, "%s: Failed to realloc thread array", __func__); + return -1; + } + + *array = grown; + + ret = pthread_create(&grown[*count].thread, NULL, worker, arg); + if (ret != 0) { + upsdebugx(1, "%s: pthread_create() returned code %i", __func__, ret); + return ret; + } + + grown[*count].active = 1; + (*count)++; + +# if defined HAVE_PTHREAD_TRYJOIN && !defined HAVE_SEMAPHORE_UNNAMED && !defined HAVE_SEMAPHORE_NAMED + pthread_mutex_lock(&threadcount_mutex); + curr_threads++; + pthread_mutex_unlock(&threadcount_mutex); +# endif + return 0; +} + +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +void nut_scanner_semaphore_release(sem_t *global, sem_t *protocol, size_t limit) +{ + sem_post(global); + if (limit > 0) { + sem_post(protocol); + } +} + +/* Preserve blocking admission for an empty batch and try/reap for a + * non-empty batch. A disabled protocol limit never accesses protocol. + * Return 1 with both permits owned, 0 if busy, or -1 for other errors. + */ +int nut_scanner_semaphore_acquire(sem_t *global, sem_t *protocol, + size_t limit, int wait) +{ + int ret; + + if (limit > 0) { + do { + ret = wait ? sem_wait(protocol) : sem_trywait(protocol); + } while (ret != 0 && errno == EINTR); + if (ret != 0) { + return errno == EAGAIN ? 0 : -1; + } + } + + do { + ret = wait ? sem_wait(global) : sem_trywait(global); + } while (ret != 0 && errno == EINTR); + if (ret != 0) { + int saved_errno = errno; + if (limit > 0) { + sem_post(protocol); + } + errno = saved_errno; + return errno == EAGAIN ? 0 : -1; + } + + /* Success on both fronts */ + return 1; +} +# endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ +#endif /* HAVE_PTHREAD */ diff --git a/tools/nut-scanner/nutscan-thread.h b/tools/nut-scanner/nutscan-thread.h new file mode 100644 index 0000000000..9428e758f1 --- /dev/null +++ b/tools/nut-scanner/nutscan-thread.h @@ -0,0 +1,25 @@ +/* Internal ownership helpers for parallel scanner workers. + * Copyright (C) + * 2016 - 2021 EATON - Various threads-related improvements + * 2020 - 2026 Jim Klimov + * 2026 NUT contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef NUTSCAN_THREAD_H +#define NUTSCAN_THREAD_H + +#include "nut-scan.h" + +/* Keep these internal helpers outside the exported nutscan_ namespace. */ +#ifdef HAVE_PTHREAD +int nut_scanner_thread_create(nutscan_thread_t **array, size_t *count, + void *(*worker)(void *), void *arg); + +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +void nut_scanner_semaphore_release(sem_t *global, sem_t *protocol, size_t limit); +int nut_scanner_semaphore_acquire(sem_t *global, sem_t *protocol, + size_t limit, int wait); +# endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ +#endif /* HAVE_PTHREAD */ +#endif /* NUTSCAN_THREAD_H */ diff --git a/tools/nut-scanner/scan_eaton_serial.c b/tools/nut-scanner/scan_eaton_serial.c index 1f07ac738c..ba5ed9b83f 100644 --- a/tools/nut-scanner/scan_eaton_serial.c +++ b/tools/nut-scanner/scan_eaton_serial.c @@ -26,6 +26,7 @@ #include "common.h" #include "nut-scan.h" +#include "nutscan-thread.h" #include "nut_stdint.h" #include @@ -422,7 +423,6 @@ nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) * ports to care much, usually... right? */ # endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ - pthread_t thread; nutscan_thread_t * thread_array = NULL; size_t thread_count = 0; @@ -464,16 +464,15 @@ nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) */ # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - /* Just wait for someone to free a semaphored slot, - * if none are available, and then/otherwise grab one - */ - if (thread_array == NULL) { - /* Starting point, or after a wait to complete - * all earlier runners */ - sem_wait(semaphore); - pass = TRUE; - } else { - pass = (sem_trywait(semaphore) == 0) ? TRUE : FALSE; + { + int admitted = nut_scanner_semaphore_acquire(semaphore, + NULL, 0, thread_array == NULL); + + if (admitted < 0) { + upsdebug_with_errno(0, "%s: Semaphore admission failed", __func__); + break; + } + pass = admitted > 0 ? TRUE : FALSE; } # else # ifdef HAVE_PTHREAD_TRYJOIN @@ -550,38 +549,23 @@ nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) current_port_name = serial_ports_list[current_port_nb]; #ifdef HAVE_PTHREAD - if (pthread_create(&thread, NULL, nutscan_scan_eaton_serial_device_thready, (void*)current_port_name) == 0) { - nutscan_thread_t *new_thread_array; -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_lock(&threadcount_mutex); - curr_threads++; -# endif /* HAVE_PTHREAD_TRYJOIN */ - - thread_count++; - new_thread_array = (nutscan_thread_t*)realloc(thread_array, - thread_count * sizeof(nutscan_thread_t)); - if (new_thread_array == NULL) { - upsdebugx(1, "%s: Failed to realloc thread array", __func__); -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ - break; - } - else { - thread_array = new_thread_array; + { + int ret = nut_scanner_thread_create(&thread_array, &thread_count, + nutscan_scan_eaton_serial_device_thready, (void *)current_port_name); + if (ret != 0) { +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nut_scanner_semaphore_release(semaphore, NULL, 0); +# endif + if (ret < 0) { + break; + } } - thread_array[thread_count - 1].thread = thread; - thread_array[thread_count - 1].active = TRUE; - -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ } #else /* if not HAVE_PTHREAD */ nutscan_scan_eaton_serial_device_thready(current_port_name); #endif /* if HAVE_PTHREAD */ - /* Prepare the next iteration */ + /* Workers borrow the port names until all scans have finished. */ current_port_nb++; } else { /* if not pass -- all slots busy */ #ifdef HAVE_PTHREAD @@ -599,7 +583,6 @@ nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) * but handle it just in case */ upsdebugx(0, "WARNING: %s: Midway clean-up: did not expect thread %" PRIuSIZE " to be not active", __func__, i); - sem_post(semaphore); continue; } thread_array[i].active = FALSE; @@ -608,7 +591,7 @@ nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) upsdebugx(0, "WARNING: %s: Midway clean-up: pthread_join() returned code %i", __func__, ret); } - sem_post(semaphore); + nut_scanner_semaphore_release(semaphore, NULL, 0); } thread_count = 0; free(thread_array); @@ -638,7 +621,7 @@ nutscan_device_t * nutscan_scan_eaton_serial(const char* ports_range) } thread_array[i].active = FALSE; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - sem_post(semaphore); + nut_scanner_semaphore_release(semaphore, NULL, 0); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); diff --git a/tools/nut-scanner/scan_ipmi.c b/tools/nut-scanner/scan_ipmi.c index b4a6bd0569..588b59f5af 100644 --- a/tools/nut-scanner/scan_ipmi.c +++ b/tools/nut-scanner/scan_ipmi.c @@ -32,6 +32,7 @@ int nutscan_unload_ipmi_library(void); #if (defined WITH_IPMI) && WITH_IPMI +#include "nutscan-thread.h" #include #include @@ -789,7 +790,6 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut sem_t * semaphore_scantype = NULL; # endif # endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ - pthread_t thread; nutscan_thread_t * thread_array = NULL; size_t thread_count = 0, i; # if (defined HAVE_PTHREAD_TRYJOIN) || (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) @@ -853,32 +853,23 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut */ # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - /* Just wait for someone to free a semaphored slot, - * if none are available, and then/otherwise grab one - */ - if (thread_array == NULL) { - /* Starting point, or after a wait to complete - * all earlier runners */ - if (max_threads_scantype > 0) - sem_wait(semaphore_scantype); - sem_wait(semaphore); - pass = TRUE; - } else { - /* If successful (the lock was acquired), - * sem_wait() and sem_trywait() will return 0. - * Otherwise, -1 is returned and errno is set, - * and the state of the semaphore is unchanged. - */ - int stwST = sem_trywait(semaphore_scantype); - int stwS = sem_trywait(semaphore); - pass = ((max_threads_scantype == 0 || stwST == 0) && stwS == 0) ? TRUE : FALSE; + { + int admitted = nut_scanner_semaphore_acquire(semaphore, + semaphore_scantype, max_threads_scantype, thread_array == NULL); + + if (admitted < 0) { + upsdebug_with_errno(0, "%s: Semaphore admission failed", __func__); + break; + } + pass = admitted > 0 ? TRUE : FALSE; + upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE " curr_threads=%" PRIuSIZE " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%u", + " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - stwST, stwS, pass + (unsigned int)pass ); } # else @@ -962,6 +953,9 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut tmp_sec = (nutscan_ipmi_t*)malloc(sizeof(nutscan_ipmi_t)); if (tmp_sec == NULL) { upsdebugx(0, "%s: Memory allocation error", __func__); +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif break; } @@ -969,32 +963,20 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut tmp_sec->peername = ip_str; #ifdef HAVE_PTHREAD - if (pthread_create(&thread, NULL, nutscan_scan_ipmi_device_thready, (void*)tmp_sec) == 0) { - nutscan_thread_t *new_thread_array; -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_lock(&threadcount_mutex); - curr_threads++; -# endif /* HAVE_PTHREAD_TRYJOIN */ - - thread_count++; - new_thread_array = (nutscan_thread_t*)realloc(thread_array, - thread_count * sizeof(nutscan_thread_t)); - if (new_thread_array == NULL) { - upsdebugx(1, "%s: Failed to realloc thread array", __func__); -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ - break; - } - else { - thread_array = new_thread_array; + { + int ret = nut_scanner_thread_create(&thread_array, &thread_count, + nutscan_scan_ipmi_device_thready, (void *)tmp_sec); + if (ret != 0) { + free(tmp_sec); + free(ip_str); + ip_str = NULL; +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +# endif + if (ret < 0) { + break; + } } - thread_array[thread_count - 1].thread = thread; - thread_array[thread_count - 1].active = TRUE; - -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ } #else /* if not HAVE_PTHREAD */ nutscan_scan_ipmi_device_thready(tmp_sec); @@ -1023,9 +1005,6 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut * but handle it just in case */ upsdebugx(0, "WARNING: %s: Midway clean-up: did not expect thread %" PRIuSIZE " to be not active", __func__, i); - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); continue; } thread_array[i].active = FALSE; @@ -1034,9 +1013,7 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut upsdebugx(0, "WARNING: %s: Midway clean-up: pthread_join() returned code %i", __func__, ret); } - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1051,6 +1028,9 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut } /* if: could we "pass" or not? */ } /* while */ + free(ip_str); + ip_str = NULL; + #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); @@ -1066,9 +1046,7 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut } thread_array[i].active = FALSE; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 355e69c6ea..281d24ce27 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -30,6 +30,7 @@ #include "upsclient.h" #include "nut-scan.h" #include "nut_stdint.h" +#include "nutscan-thread.h" /* externally visible to nutscan-init */ int nutscan_unload_upsclient_library(void); @@ -393,6 +394,22 @@ int nutscan_load_upsclient_library(const char *libname_path) /* end of dynamic link library stuff */ /* FIXME: SSL support */ +static void free_nut_arg(struct scan_nut_arg *arg) +{ + if (!arg) + return; + + /* Work with host_cert list is mutex'ed in the upsclient library. */ + if (nut_upscli_free_host_cert) { + (*nut_upscli_free_host_cert)(arg->hostname, NULL); + } + if (arg->ac_current && nut_upscli_free_authconf_item) { + (*nut_upscli_free_authconf_item)(arg->ac_current); + } + free(arg->hostname); + free(arg); +} + /* Performs a (parallel-able) NUT protocol scan of one remote host:port. * Returns NULL, updates global dev_ret when a scan is successful. * FREES the caller's copy of "nut_arg" and "hostname" in it, if applicable. @@ -530,12 +547,11 @@ static void * list_nut_devices_thready(void * arg) free(ups); } - if (target_hostname) - free(target_hostname); - if (hostname) + if (hostname) { free(hostname); - if (nut_arg) - free(nut_arg); + } + free_nut_arg(nut_arg); + nut_arg = NULL; return NULL; } @@ -613,12 +629,12 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * sem_t * semaphore_scantype = NULL; # endif # endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ - pthread_t thread; nutscan_thread_t * thread_array = NULL; size_t thread_count = 0, i; # if (defined HAVE_PTHREAD_TRYJOIN) || (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) size_t max_threads_scantype = max_threads_oldnut; # endif +#endif /* HAVE_PTHREAD */ const char *nutauth = sec ? sec->authconf_file : NULL; upscli_authconf_t *ac_default = NULL; @@ -638,6 +654,14 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * * If no spec from caller, apply the default we have. */ useconds_t usec_timeout = sec ? sec->usec_timeout : (useconds_t)UPSCLI_DEFAULT_CONNECT_TIMEOUT_SEC * (1000*1000); + if (!nutscan_avail_nut) { + return NULL; + } + + if (irl == NULL || irl->ip_ranges == NULL) { + return NULL; + } + if (nut_upscli_init_default_connect_timeout && nut_upscli_get_default_connect_timeout ) { @@ -702,7 +726,6 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) semaphore = nutscan_semaphore(); # endif -#endif pthread_mutex_init(&dev_mutex, NULL); @@ -736,14 +759,6 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * #endif /* HAVE_PTHREAD */ - if (!nutscan_avail_nut) { - return NULL; - } - - if (irl == NULL || irl->ip_ranges == NULL) { - return NULL; - } - if (!irl->ip_ranges->start_ip) { upsdebugx(1, "%s: no starting IP address specified", __func__); } else if (irl->ip_ranges_count == 1 @@ -793,32 +808,23 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * */ # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - /* Just wait for someone to free a semaphored slot, - * if none are available, and then/otherwise grab one - */ - if (thread_array == NULL) { - /* Starting point, or after a wait to complete - * all earlier runners */ - if (max_threads_scantype > 0) - sem_wait(semaphore_scantype); - sem_wait(semaphore); - pass = TRUE; - } else { - /* If successful (the lock was acquired), - * sem_wait() and sem_trywait() will return 0. - * Otherwise, -1 is returned and errno is set, - * and the state of the semaphore is unchanged. - */ - int stwST = sem_trywait(semaphore_scantype); - int stwS = sem_trywait(semaphore); - pass = (((max_threads_scantype == 0) || (stwST == 0)) && (stwS == 0)) ? TRUE : FALSE; + { + int admitted = nut_scanner_semaphore_acquire(semaphore, + semaphore_scantype, max_threads_scantype, thread_array == NULL); + + if (admitted < 0) { + upsdebug_with_errno(0, "%s: Semaphore admission failed", __func__); + break; + } + pass = admitted > 0 ? TRUE : FALSE; + upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE " curr_threads=%" PRIuSIZE " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%u", + " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - stwST, stwS, pass + (unsigned int)pass ); } # else @@ -913,9 +919,15 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * ip_dest = strdup(ip_str); } - if ((nut_arg = (struct scan_nut_arg*)malloc(sizeof(struct scan_nut_arg))) == NULL) { + if (ip_dest == NULL + || (nut_arg = (struct scan_nut_arg*)malloc(sizeof(struct scan_nut_arg))) == NULL + ) { upsdebugx(0, "%s: Memory allocation error", __func__); +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif free(ip_dest); + ip_dest = NULL; break; } @@ -943,53 +955,25 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * } #ifdef HAVE_PTHREAD - if (pthread_create(&thread, NULL, list_nut_devices_thready, (void*)nut_arg) == 0) { - nutscan_thread_t *new_thread_array; -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_lock(&threadcount_mutex); - curr_threads++; -# endif /* HAVE_PTHREAD_TRYJOIN */ - - thread_count++; - new_thread_array = (nutscan_thread_t*)realloc(thread_array, - thread_count * sizeof(nutscan_thread_t)); - if (new_thread_array == NULL) { - upsdebugx(1, "%s: Failed to realloc thread array", __func__); -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ - break; - } - else { - thread_array = new_thread_array; + { + int ret = nut_scanner_thread_create(&thread_array, &thread_count, + list_nut_devices_thready, (void *)nut_arg); + if (ret != 0) { + free_nut_arg(nut_arg); + nut_arg = NULL; +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +# endif + if (ret < 0) { + break; + } } - thread_array[thread_count - 1].thread = thread; - thread_array[thread_count - 1].active = TRUE; - -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ } #else /* if not HAVE_PTHREAD */ list_nut_devices_thready(nut_arg); #endif /* if HAVE_PTHREAD */ - /* NOTE: Work with host_cert list is - * mutex'ed in the upsclient library */ - if (nut_upscli_free_host_cert) { - (*nut_upscli_free_host_cert)(ip_dest, NULL); - } - - /* Prepare the next iteration; note that - * nutscan_scan_ipmi_device_thready() - * takes care of freeing "tmp_sec" and its - * copy (note strdup!) of "ip_str" as - * hostname, possibly suffixed with a port. - */ - if (nut_arg->ac_current && have_nutauth_methods) { - (*nut_upscli_free_authconf_item)(nut_arg->ac_current); - nut_arg->ac_current = NULL; - } + /* The worker owns nut_arg and its copied hostname. */ free(ip_str); ip_str = nutscan_ip_ranges_iter_inc(&ip); } else { /* if not pass -- all slots busy */ @@ -1008,9 +992,6 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * * but handle it just in case */ upsdebugx(0, "WARNING: %s: Midway clean-up: did not expect thread %" PRIuSIZE " to be not active", __func__, i); - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); continue; } thread_array[i].active = FALSE; @@ -1019,9 +1000,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * upsdebugx(0, "WARNING: %s: Midway clean-up: pthread_join() returned code %i", __func__, ret); } - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1036,6 +1015,8 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * } /* if: could we "pass" or not? */ } /* while */ + free(ip_str); + #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); @@ -1051,9 +1032,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * } thread_array[i].active = FALSE; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); diff --git a/tools/nut-scanner/scan_snmp.c b/tools/nut-scanner/scan_snmp.c index 84568c8c68..3d97061eec 100644 --- a/tools/nut-scanner/scan_snmp.c +++ b/tools/nut-scanner/scan_snmp.c @@ -34,6 +34,7 @@ int nutscan_unload_snmp_library(void); #if (defined WITH_SNMP) && WITH_SNMP +#include "nutscan-thread.h" #ifndef WIN32 # include @@ -939,8 +940,9 @@ static void * wrap_nut_snmp_sess_open(struct snmp_session *session) } /* Performs a (parallel-able) SNMP protocol scan of one remote host. - * Returns NULL, updates global dev_ret when a scan is successful. - * FREES the caller's copy of "sec" and "peername" in it, if applicable. + * Returns the caller-owned peername on session-open EMFILE, or NULL otherwise. + * Updates global dev_ret when a scan is successful. Frees sec in either case, + * and peername unless returned for recovery after other scans have finished. */ static void * try_SysOID_thready(void * arg) { @@ -967,8 +969,18 @@ static void * try_SysOID_thready(void * arg) snmp_sess.timeout = (long)g_usec_timeout; /* Open the session */ + errno = 0; handle = wrap_nut_snmp_sess_open(&snmp_sess); /* establish the session */ if (handle == NULL) { + /* Prefer the session's saved system error. Use errno only when + * the session did not save a system error. */ + if (snmp_sess.s_errno == EMFILE + || (snmp_sess.s_errno == 0 && errno == EMFILE) + ) { + char *peername = sec->peername; + free(sec); + return peername; + } upsdebugx(2, "Failed to open SNMP session for %s", sec->peername); @@ -1072,6 +1084,15 @@ static void * try_SysOID_thready(void * arg) return NULL; } +static void report_snmp_open_failure(char *peername) +{ + if (peername != NULL) { + upsdebugx(0, "WARNING: SNMP scan of %s could not open a session: " + "too many open files, even with one SNMP scan at a time", peername); + free(peername); + } +} + static void init_snmp_once(void) { /* Initialize the SNMP library */ @@ -1112,6 +1133,8 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( char * ip_str = NULL; #ifdef HAVE_PTHREAD + nutscan_ip_range_list_t retry_ranges; + nutscan_ip_range_t *retry; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) sem_t * semaphore = nutscan_semaphore(); # if (defined HAVE_SEMAPHORE_UNNAMED) @@ -1121,13 +1144,24 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( sem_t * semaphore_scantype = NULL; # endif # endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ - pthread_t thread; nutscan_thread_t * thread_array = NULL; size_t thread_count = 0, i; # if (defined HAVE_PTHREAD_TRYJOIN) || (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) size_t max_threads_scantype = max_threads_netsnmp; # endif +#endif /* HAVE_PTHREAD */ + + if (!nutscan_avail_snmp) { + return NULL; + } + + if (irl == NULL || irl->ip_ranges == NULL) { + return NULL; + } + +#ifdef HAVE_PTHREAD + nutscan_init_ip_ranges(&retry_ranges); pthread_mutex_init(&dev_mutex, NULL); # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) @@ -1160,14 +1194,6 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( #endif /* HAVE_PTHREAD */ - if (!nutscan_avail_snmp) { - return NULL; - } - - if (irl == NULL || irl->ip_ranges == NULL) { - return NULL; - } - if (!irl->ip_ranges->start_ip) { upsdebugx(1, "%s: no starting IP address specified", __func__); } else if (irl->ip_ranges_count == 1 @@ -1204,32 +1230,23 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( */ # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - /* Just wait for someone to free a semaphored slot, - * if none are available, and then/otherwise grab one - */ - if (thread_array == NULL) { - /* Starting point, or after a wait to complete - * all earlier runners */ - if (max_threads_scantype > 0) - sem_wait(semaphore_scantype); - sem_wait(semaphore); - pass = TRUE; - } else { - /* If successful (the lock was acquired), - * sem_wait() and sem_trywait() will return 0. - * Otherwise, -1 is returned and errno is set, - * and the state of the semaphore is unchanged. - */ - int stwST = sem_trywait(semaphore_scantype); - int stwS = sem_trywait(semaphore); - pass = ((max_threads_scantype == 0 || stwST == 0) && stwS == 0); + { + int admitted = nut_scanner_semaphore_acquire(semaphore, + semaphore_scantype, max_threads_scantype, thread_array == NULL); + + if (admitted < 0) { + upsdebug_with_errno(0, "%s: Semaphore admission failed", __func__); + break; + } + pass = admitted > 0 ? TRUE : FALSE; + upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE " curr_threads=%" PRIuSIZE " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%d", + " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - stwST, stwS, pass + (unsigned int)pass ); } # else @@ -1259,17 +1276,21 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( ) { for (i = 0; i < thread_count ; i++) { int ret; + void *failed_peer = NULL; if (!thread_array[i].active) continue; pthread_mutex_lock(&threadcount_mutex); upsdebugx(3, "%s: Trying to join thread #%" PRIuSIZE "...", __func__, i); - ret = pthread_tryjoin_np(thread_array[i].thread, NULL); + ret = pthread_tryjoin_np(thread_array[i].thread, &failed_peer); switch (ret) { case ESRCH: /* No thread with the ID thread could be found - already "joined"? */ upsdebugx(5, "%s: Was thread #%" PRIuSIZE " joined earlier?", __func__, i); break; case 0: /* thread exited */ + if (failed_peer != NULL) { + nutscan_add_ip_range(&retry_ranges, (char *)failed_peer, NULL); + } if (curr_threads > 0) { curr_threads --; upsdebugx(4, "%s: Joined a finished thread #%" PRIuSIZE, __func__, i); @@ -1313,6 +1334,9 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( tmp_sec = (nutscan_snmp_t*)malloc(sizeof(nutscan_snmp_t)); if (tmp_sec == NULL) { upsdebugx(0, "%s: Memory allocation error", __func__); +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif break; } @@ -1320,42 +1344,29 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( tmp_sec->peername = ip_str; #ifdef HAVE_PTHREAD - if (pthread_create(&thread, NULL, try_SysOID_thready, (void*)tmp_sec) == 0) { - nutscan_thread_t *new_thread_array; -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_lock(&threadcount_mutex); - curr_threads++; -# endif /* HAVE_PTHREAD_TRYJOIN */ - - thread_count++; - new_thread_array = (nutscan_thread_t*)realloc(thread_array, - thread_count * sizeof(nutscan_thread_t)); - if (new_thread_array == NULL) { - upsdebugx(1, "%s: Failed to realloc thread array", __func__); -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ - break; - } - else { - thread_array = new_thread_array; + { + int ret = nut_scanner_thread_create(&thread_array, &thread_count, + try_SysOID_thready, (void *)tmp_sec); + if (ret != 0) { + free(tmp_sec); + free(ip_str); + ip_str = NULL; +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +# endif + if (ret < 0) { + break; + } } - thread_array[thread_count - 1].thread = thread; - thread_array[thread_count - 1].active = TRUE; - -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ } #else /* if not HAVE_PTHREAD */ - try_SysOID_thready(tmp_sec); + /* Already serial: no other SNMP worker can release descriptors. */ + report_snmp_open_failure((char *)try_SysOID_thready(tmp_sec)); #endif /* if HAVE_PTHREAD */ - /* Prepare the next iteration; note that - * try_SysOID_thready() - * takes care of freeing "tmp_sec" and its - * reference (NOT strdup!) to "ip_str" as - * peername. + /* Prepare the next iteration. The worker owns tmp_sec and + * its peername (ip_str, NOT strdup), returning the latter + * only when the host needs recovery after other scans finish. */ ip_str = nutscan_ip_ranges_iter_inc(&ip); } else { /* if not pass -- all slots busy */ @@ -1369,25 +1380,23 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( __func__, thread_count); for (i = 0; i < thread_count ; i++) { int ret; + void *failed_peer = NULL; if (!thread_array[i].active) { /* Probably should not get here, * but handle it just in case */ upsdebugx(0, "WARNING: %s: Midway clean-up: did not expect thread %" PRIuSIZE " to be not active", __func__, i); - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); continue; } thread_array[i].active = FALSE; - ret = pthread_join(thread_array[i].thread, NULL); + ret = pthread_join(thread_array[i].thread, &failed_peer); if (ret != 0) { upsdebugx(0, "WARNING: %s: Midway clean-up: pthread_join() returned code %i", __func__, ret); + } else if (failed_peer != NULL) { + nutscan_add_ip_range(&retry_ranges, (char *)failed_peer, NULL); } - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1402,24 +1411,28 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } /* if: could we "pass" or not? */ } /* while */ + free(ip_str); + ip_str = NULL; + #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); for (i = 0; i < thread_count; i++) { int ret; + void *failed_peer = NULL; if (!thread_array[i].active) continue; - ret = pthread_join(thread_array[i].thread, NULL); + ret = pthread_join(thread_array[i].thread, &failed_peer); if (ret != 0) { upsdebugx(0, "WARNING: %s: Clean-up: pthread_join() returned code %i", __func__, ret); + } else if (failed_peer != NULL) { + nutscan_add_ip_range(&retry_ranges, (char *)failed_peer, NULL); } thread_array[i].active = FALSE; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); @@ -1438,6 +1451,54 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( free(thread_array); upsdebugx(2, "%s: all threads freed", __func__); } + + /* All original SNMP sessions have closed. Retry only session-open + * EMFILE failures, serially, without changing other scanners' limits. + * A second EMFILE has no further SNMP work to wait for: report it and + * move on rather than sleeping or indefinitely requeueing the host. + */ + for (retry = retry_ranges.ip_ranges; retry != NULL; retry = retry->next) { + tmp_sec = (nutscan_snmp_t *)malloc(sizeof(*tmp_sec)); + if (tmp_sec == NULL) { + upsdebugx(0, "%s: Memory allocation error during SNMP recovery", __func__); + break; + } +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + if (nut_scanner_semaphore_acquire(semaphore, semaphore_scantype, + max_threads_scantype, 1) <= 0) { + upsdebug_with_errno(0, "%s: SNMP recovery admission failed", __func__); + free(tmp_sec); + break; + } +# elif defined HAVE_PTHREAD_TRYJOIN + /* Other protocols still share the counter. Reserve one slot for + * the synchronous probe, just as for a new worker. */ + for (;;) { + pthread_mutex_lock(&threadcount_mutex); + if (curr_threads < max_threads) { + curr_threads++; + pthread_mutex_unlock(&threadcount_mutex); + break; + } + pthread_mutex_unlock(&threadcount_mutex); + usleep(10000); + } +# endif + memcpy(tmp_sec, sec, sizeof(*tmp_sec)); + tmp_sec->peername = retry->start_ip; + retry->start_ip = retry->end_ip = NULL; + upsdebugx(1, "%s: Retrying SNMP scan of %s after closing earlier sessions", + __func__, tmp_sec->peername); + report_snmp_open_failure((char *)try_SysOID_thready(tmp_sec)); +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +# elif defined HAVE_PTHREAD_TRYJOIN + pthread_mutex_lock(&threadcount_mutex); + curr_threads--; + pthread_mutex_unlock(&threadcount_mutex); +# endif + } + nutscan_free_ip_ranges(&retry_ranges); pthread_mutex_destroy(&dev_mutex); # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) diff --git a/tools/nut-scanner/scan_xml_http.c b/tools/nut-scanner/scan_xml_http.c index aec59c4f6c..be023b344a 100644 --- a/tools/nut-scanner/scan_xml_http.c +++ b/tools/nut-scanner/scan_xml_http.c @@ -34,6 +34,7 @@ int nutscan_unload_neon_library(void); #if (defined WITH_NEON) && WITH_NEON +#include "nutscan-thread.h" #ifndef WIN32 # include @@ -529,7 +530,6 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, sem_t * semaphore_scantype = NULL; # endif # endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ - pthread_t thread; nutscan_thread_t * thread_array = NULL; size_t thread_count = 0, i; # if (defined HAVE_PTHREAD_TRYJOIN) || (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) @@ -593,32 +593,23 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, */ # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - /* Just wait for someone to free a semaphored slot, - * if none are available, and then/otherwise grab one - */ - if (thread_array == NULL) { - /* Starting point, or after a wait to complete - * all earlier runners */ - if (max_threads_scantype > 0) - sem_wait(semaphore_scantype); - sem_wait(semaphore); - pass = TRUE; - } else { - /* If successful (the lock was acquired), - * sem_wait() and sem_trywait() will return 0. - * Otherwise, -1 is returned and errno is set, - * and the state of the semaphore is unchanged. - */ - int stwST = sem_trywait(semaphore_scantype); - int stwS = sem_trywait(semaphore); - pass = ((max_threads_scantype == 0 || stwST == 0) && stwS == 0) ? TRUE : FALSE; + { + int admitted = nut_scanner_semaphore_acquire(semaphore, + semaphore_scantype, max_threads_scantype, thread_array == NULL); + + if (admitted < 0) { + upsdebug_with_errno(0, "%s: Semaphore admission failed", __func__); + break; + } + pass = admitted > 0 ? TRUE : FALSE; + upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE " curr_threads=%" PRIuSIZE " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%u", + " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - stwST, stwS, pass + (unsigned int)pass ); } # else @@ -702,6 +693,9 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, tmp_sec = (nutscan_xml_t*)malloc(sizeof(nutscan_xml_t)); if (tmp_sec == NULL) { upsdebugx(0, "%s: Memory allocation error", __func__); +#if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif break; } @@ -712,32 +706,20 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, } #ifdef HAVE_PTHREAD - if (pthread_create(&thread, NULL, nutscan_scan_xml_http_thready, (void*)tmp_sec) == 0) { - nutscan_thread_t *new_thread_array; -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_lock(&threadcount_mutex); - curr_threads++; -# endif /* HAVE_PTHREAD_TRYJOIN */ - - thread_count++; - new_thread_array = (nutscan_thread_t*)realloc(thread_array, - thread_count * sizeof(nutscan_thread_t)); - if (new_thread_array == NULL) { - upsdebugx(1, "%s: Failed to realloc thread array", __func__); -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ - break; - } - else { - thread_array = new_thread_array; + { + int ret = nut_scanner_thread_create(&thread_array, &thread_count, + nutscan_scan_xml_http_thready, (void *)tmp_sec); + if (ret != 0) { + free(tmp_sec); + free(ip_str); + ip_str = NULL; +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +# endif + if (ret < 0) { + break; + } } - thread_array[thread_count - 1].thread = thread; - thread_array[thread_count - 1].active = TRUE; - -# ifdef HAVE_PTHREAD_TRYJOIN - pthread_mutex_unlock(&threadcount_mutex); -# endif /* HAVE_PTHREAD_TRYJOIN */ } #else /* if not HAVE_PTHREAD */ nutscan_scan_xml_http_thready(tmp_sec); @@ -766,9 +748,6 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, * but handle it just in case */ upsdebugx(0, "WARNING: %s: Midway clean-up: did not expect thread %" PRIuSIZE " to be not active", __func__, i); - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); continue; } thread_array[i].active = FALSE; @@ -777,9 +756,7 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, upsdebugx(0, "WARNING: %s: Midway clean-up: pthread_join() returned code %i", __func__, ret); } - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -794,6 +771,9 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, } /* if: could we "pass" or not? */ } /* while */ + free(ip_str); + ip_str = NULL; + #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); @@ -809,9 +789,7 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, } thread_array[i].active = FALSE; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - sem_post(semaphore); - if (max_threads_scantype > 0) - sem_post(semaphore_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex);