From 2d7f275410352f6db900bbfe1e4b922688cb6316 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:26:05 +0000 Subject: [PATCH 01/17] nut-scanner: balance worker admission and cleanup Skip inactive protocol semaphores, unwind partial admission, and return owned permits when worker allocation or creation fails. Allocate tracking entries before starting workers and keep NUT hostname and authentication cleanup with the worker until it finishes. Add offline regression tests using real threads and semaphores, preserve the counter and non-threaded paths, and record the change in NEWS. Related to #1166. Global semaphore initialisation policy remains separate. Prepared with assistance from OpenAI Codex (gpt-6-astra). Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- Makefile.am | 4 +- NEWS.adoc | 4 + tests/Makefile.am | 41 ++- tests/nutscan-thread-test.c | 541 +++++++++++++++++++++++++++++ tools/nut-scanner/Makefile.am | 2 +- tools/nut-scanner/nutscan-thread.h | 92 +++++ tools/nut-scanner/scan_ipmi.c | 91 ++--- tools/nut-scanner/scan_nut.c | 151 ++++---- tools/nut-scanner/scan_snmp.c | 110 +++--- tools/nut-scanner/scan_xml_http.c | 91 ++--- 10 files changed, 836 insertions(+), 291 deletions(-) create mode 100644 tests/nutscan-thread-test.c create mode 100644 tools/nut-scanner/nutscan-thread.h diff --git a/Makefile.am b/Makefile.am index 23f7d5b345..8d683c3f8b 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 d2b9679ced..1d2b341cd9 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -47,6 +47,10 @@ https://github.com/networkupstools/nut/milestone/13 extracting the response. Returned keys and descriptions remain byte sequences. [issue #3621] * `nut-scanner` tool updates: + - 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] * USB drivers: fixed an out-of-bounds read of the candidate HID report diff --git a/tests/Makefile.am b/tests/Makefile.am index 9a2a26b307..76d8f01ba1 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -52,7 +52,8 @@ $(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/libnutscan.la: dummy @dotMAKE@ +@cd $(@D) && $(MAKE) $(AM_MAKEFLAGS) $(@F) $(top_builddir)/clients/upslog$(EXEEXT): dummy @dotMAKE@ @@ -262,6 +263,44 @@ endif !WITH_OPENSSL endif WITH_SSL endif WITH_SNMP +# Compile each scanner's actual admission loop with offline worker probes. +if WITH_NUT_SCANNER +if WITH_THREADING +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) + +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_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_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_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_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-thread-test.c b/tests/nutscan-thread-test.c new file mode 100644 index 0000000000..14d251f4ba --- /dev/null +++ b/tests/nutscan-thread-test.c @@ -0,0 +1,541 @@ +/* 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" +#include "nut-scan.h" +#include "upsclient.h" +#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) +static void *test_malloc(size_t size); +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); +# 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 + +/* Compile the actual scanner loop. Only probes and injected resource + * failures are replaced; threads, semaphore accounting and cleanup run. + */ +# define malloc test_malloc +# define realloc test_realloc +# define free test_free +# define pthread_create test_create +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED +# define sem_wait test_wait +# define sem_trywait test_trywait +# 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 + +# 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" +# else +# include "scan_xml_http.c" +# endif + +# undef malloc +# undef realloc +# undef free +# undef pthread_create +# 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; + +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(); +} + +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 (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 == 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++; + 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); + } + 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 +static sem_t *global_sem, *protocol_sem; +static unsigned int protocol_capacity; +static int fail_init, interrupt_wait, fail_wait, fail_try_global, fail_try_protocol; +# if 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); +} + +# ifdef HAVE_SEMAPHORE_UNNAMED +static int test_init(sem_t *sem, int shared, unsigned int value) +{ + if (fail_init) { + errno = ENOSPC; + return -1; + } + protocol_sem = sem; + protocol_capacity = value; + return sem_init(sem, shared, value); +} + +static int test_destroy(sem_t *sem) +{ + assert(sem == protocol_sem); + check_capacity(sem, protocol_capacity); + protocol_sem = NULL; + return sem_destroy(sem); +} +# 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); + 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) +{ + assert(sem == protocol_sem); + check_capacity(sem, protocol_capacity); + protocol_sem = NULL; + return sem_close(sem); +} + +static int test_unlink(const char *name) +{ + NUT_UNUSED_VARIABLE(name); + return 0; +} +# endif +# 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) +{ + 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); +} + +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; + 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 + 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 +# 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 !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..b9992e2e2d 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 diff --git a/tools/nut-scanner/nutscan-thread.h b/tools/nut-scanner/nutscan-thread.h new file mode 100644 index 0000000000..1d14b09b8e --- /dev/null +++ b/tools/nut-scanner/nutscan-thread.h @@ -0,0 +1,92 @@ +/* Internal ownership helpers for parallel scanner workers. + * Copyright (C) 2026 NUT contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef NUTSCAN_THREAD_H +#define NUTSCAN_THREAD_H + +#include "nut-scan.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. + */ +static int nutscan_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 +static void nutscan_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. + */ +static int nutscan_semaphore_acquire(sem_t *global, sem_t *protocol, + size_t limit, int wait) +{ + int ret, saved_errno; + + 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) { + saved_errno = errno; + if (limit > 0) { + sem_post(protocol); + } + errno = saved_errno; + return errno == EAGAIN ? 0 : -1; + } + return 1; +} +# endif /* HAVE_SEMAPHORE_UNNAMED || HAVE_SEMAPHORE_NAMED */ +#endif /* HAVE_PTHREAD */ +#endif /* NUTSCAN_THREAD_H */ diff --git a/tools/nut-scanner/scan_ipmi.c b/tools/nut-scanner/scan_ipmi.c index f554c9c657..17a584f647 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) @@ -867,33 +867,14 @@ 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; - upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE - " curr_threads=%" PRIuSIZE - " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%u", - __func__, max_threads_scantype, - curr_threads, thread_count, - stwST, stwS, pass - ); + { + int admitted = nutscan_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; } # else # ifdef HAVE_PTHREAD_TRYJOIN @@ -976,6 +957,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) + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif break; } @@ -983,32 +967,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 = nutscan_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 + nutscan_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); @@ -1037,9 +1009,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; @@ -1048,9 +1017,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); + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1065,6 +1032,8 @@ 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); + #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); @@ -1080,9 +1049,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); + nutscan_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 2ebef69445..37e2488885 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,19 @@ 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) +{ + /* 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 +544,12 @@ 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); + } + if (nut_arg) { + free_nut_arg(nut_arg); + } return NULL; } @@ -613,12 +627,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 +652,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 +724,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); @@ -750,14 +771,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 @@ -807,33 +820,14 @@ 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; - upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE - " curr_threads=%" PRIuSIZE - " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%u", - __func__, max_threads_scantype, - curr_threads, thread_count, - stwST, stwS, pass - ); + { + int admitted = nutscan_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; } # else # ifdef HAVE_PTHREAD_TRYJOIN @@ -927,8 +921,13 @@ 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) + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif free(ip_dest); break; } @@ -957,53 +956,24 @@ 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 = nutscan_thread_create(&thread_array, &thread_count, + list_nut_devices_thready, (void *)nut_arg); + if (ret != 0) { + free_nut_arg(nut_arg); +# if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED + nutscan_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 */ @@ -1022,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; @@ -1033,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); + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1050,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__); @@ -1065,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); + nutscan_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 c890322f64..5ed419cce8 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 @@ -1141,13 +1142,23 @@ 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 pthread_mutex_init(&dev_mutex, NULL); # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) @@ -1194,14 +1205,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 @@ -1238,33 +1241,14 @@ 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); - upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE - " curr_threads=%" PRIuSIZE - " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%d", - __func__, max_threads_scantype, - curr_threads, thread_count, - stwST, stwS, pass - ); + { + int admitted = nutscan_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; } # else # ifdef HAVE_PTHREAD_TRYJOIN @@ -1347,6 +1331,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) + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif break; } @@ -1354,32 +1341,20 @@ 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 = nutscan_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 + nutscan_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); @@ -1408,9 +1383,6 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( * 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; @@ -1419,9 +1391,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( 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); + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1436,6 +1406,8 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } /* 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__); @@ -1451,9 +1423,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } 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); + nutscan_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_xml_http.c b/tools/nut-scanner/scan_xml_http.c index 92d197d434..2101c6934f 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) @@ -607,33 +607,14 @@ 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; - upsdebugx(4, "%s: max_threads_scantype=%" PRIuSIZE - " curr_threads=%" PRIuSIZE - " thread_count=%" PRIuSIZE - " stwST=%d stwS=%d pass=%u", - __func__, max_threads_scantype, - curr_threads, thread_count, - stwST, stwS, pass - ); + { + int admitted = nutscan_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; } # else # ifdef HAVE_PTHREAD_TRYJOIN @@ -716,6 +697,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) + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); +#endif break; } @@ -726,32 +710,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 = nutscan_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 + nutscan_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); @@ -780,9 +752,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; @@ -791,9 +760,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); + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -808,6 +775,8 @@ 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); + #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); @@ -823,9 +792,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); + nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); From e92e3fdc6b2525b019556c4e30055b2d39edd066 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:47:42 +0000 Subject: [PATCH 02/17] tests: fix scanner build dependencies Pass the configured SSL include flags to the scanner tests for NSS builds. Order scanner recursion after common and client dependencies, and generate the SNMP header before compiling the test source. Prepared with assistance from OpenAI Codex (gpt-6-astra). Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tests/Makefile.am | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Makefile.am b/tests/Makefile.am index 76d8f01ba1..3c9480398c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -268,6 +268,12 @@ if WITH_NUT_SCANNER if WITH_THREADING 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) +endif WITH_SSL + +# Finish shared dependencies before the scanner's independent sub-make. +$(top_builddir)/tools/nut-scanner/libnutscan.la: $(NUT_LIBCOMMON) $(top_builddir)/common/libcommonversion.la $(top_builddir)/clients/libupsclient.la TESTS += nutscan-nut-test nutscan_nut_test_SOURCES = nutscan-thread-test.c @@ -276,6 +282,10 @@ 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_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) if WITH_SNMP +# The scanner library generates this header before the test can include it. +$(top_builddir)/tools/nut-scanner/nutscan-snmp.h: $(top_builddir)/tools/nut-scanner/libnutscan.la +nutscan-thread-test.c: $(top_builddir)/tools/nut-scanner/nutscan-snmp.h + 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 From d2691e0116ab84ebb84c2904ef4f96597e539080 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Tue, 15 Sep 2026 07:55:12 +0000 Subject: [PATCH 03/17] tests: check scanner semaphore fixture lifetime Track the protocol semaphore only after successful initialisation, and check destruction or close before clearing fixture state. Assert clean state at scenario entry and clear the global semaphore after teardown. Assisted by OpenAI Codex (gpt-6-astra). Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tests/nutscan-thread-test.c | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/nutscan-thread-test.c b/tests/nutscan-thread-test.c index 14d251f4ba..666449298d 100644 --- a/tests/nutscan-thread-test.c +++ b/tests/nutscan-thread-test.c @@ -189,6 +189,7 @@ static int test_create(pthread_t *thread, const pthread_attr_t *attr, } # 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; static unsigned int protocol_capacity; static int fail_init, interrupt_wait, fail_wait, fail_try_global, fail_try_protocol; @@ -240,21 +241,31 @@ static int test_trywait(sem_t *sem) # 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; } - protocol_sem = sem; - protocol_capacity = value; - return sem_init(sem, shared, value); + ret = sem_init(sem, shared, value); + if (ret == 0) { + protocol_sem = sem; + protocol_capacity = value; + } + return ret; } static int test_destroy(sem_t *sem) { - assert(sem == protocol_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 sem_destroy(sem); + return ret; } # else static sem_t *test_open(const char *name, int flags, ...) @@ -264,6 +275,7 @@ static sem_t *test_open(const char *name, int flags, ...) NUT_UNUSED_VARIABLE(name); NUT_UNUSED_VARIABLE(flags); + assert(protocol_sem == NULL); if (fail_init) { errno = ENOSPC; return SEM_FAILED; @@ -282,10 +294,14 @@ static sem_t *test_open(const char *name, int flags, ...) static int test_close(sem_t *sem) { - assert(sem == protocol_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 sem_close(sem); + return ret; } static int test_unlink(const char *name) @@ -444,7 +460,7 @@ static void scenario(unsigned int limit, int failure) max_threads = 2; curr_threads = 0; # if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED - protocol_sem = NULL; + assert(protocol_sem == NULL); fail_init = failure == 6; interrupt_wait = failure == 7 ? 1 : 0; fail_wait = failure == 8 ? 1 : (failure == 9 ? 2 : 0); @@ -476,6 +492,7 @@ static void scenario(unsigned int limit, int failure) 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) { From dfb53848a1ce5c307aa9bbec1ca289e3a6256e4a Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:28:28 +0000 Subject: [PATCH 04/17] nut-scanner: compile worker helpers once Move the shared worker helpers into nutscan-thread.c and keep declarations in the header. Preserve internal symbol visibility and the existing failure-injection tests. Assisted by OpenAI Codex (gpt-6-astra). Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tests/Makefile.am | 8 +-- tests/nutscan-thread-test.c | 3 + tools/nut-scanner/Makefile.am | 2 +- tools/nut-scanner/nutscan-thread.c | 89 ++++++++++++++++++++++++++++++ tools/nut-scanner/nutscan-thread.h | 82 ++------------------------- tools/nut-scanner/scan_ipmi.c | 12 ++-- tools/nut-scanner/scan_nut.c | 12 ++-- tools/nut-scanner/scan_snmp.c | 12 ++-- tools/nut-scanner/scan_xml_http.c | 12 ++-- 9 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 tools/nut-scanner/nutscan-thread.c diff --git a/tests/Makefile.am b/tests/Makefile.am index 3c9480398c..87380fa9e9 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -279,7 +279,7 @@ 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_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +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 # The scanner library generates this header before the test can include it. @@ -290,7 +290,7 @@ 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_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +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 @@ -298,7 +298,7 @@ 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_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +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 @@ -306,7 +306,7 @@ 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_builddir)/tools/nut-scanner/libnutscan.la $(NUT_LIBCOMMON) +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 diff --git a/tests/nutscan-thread-test.c b/tests/nutscan-thread-test.c index 666449298d..c238d239c8 100644 --- a/tests/nutscan-thread-test.c +++ b/tests/nutscan-thread-test.c @@ -52,6 +52,9 @@ static int test_unlink(const char *name); # 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 diff --git a/tools/nut-scanner/Makefile.am b/tools/nut-scanner/Makefile.am index b9992e2e2d..3c48f879b7 100644 --- a/tools/nut-scanner/Makefile.am +++ b/tools/nut-scanner/Makefile.am @@ -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..3064b1f802 --- /dev/null +++ b/tools/nut-scanner/nutscan-thread.c @@ -0,0 +1,89 @@ +/* Internal ownership helpers for parallel scanner workers. + * Copyright (C) 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, saved_errno; + + 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) { + saved_errno = errno; + if (limit > 0) { + sem_post(protocol); + } + errno = saved_errno; + return errno == EAGAIN ? 0 : -1; + } + 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 index 1d14b09b8e..1007a34fca 100644 --- a/tools/nut-scanner/nutscan-thread.h +++ b/tools/nut-scanner/nutscan-thread.h @@ -8,85 +8,15 @@ #include "nut-scan.h" +/* Keep these internal helpers outside the exported nutscan_ namespace. */ #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. - */ -static int nutscan_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; -} +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 -static void nutscan_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. - */ -static int nutscan_semaphore_acquire(sem_t *global, sem_t *protocol, - size_t limit, int wait) -{ - int ret, saved_errno; - - 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) { - saved_errno = errno; - if (limit > 0) { - sem_post(protocol); - } - errno = saved_errno; - return errno == EAGAIN ? 0 : -1; - } - return 1; -} +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_ipmi.c b/tools/nut-scanner/scan_ipmi.c index 17a584f647..7895b56842 100644 --- a/tools/nut-scanner/scan_ipmi.c +++ b/tools/nut-scanner/scan_ipmi.c @@ -868,7 +868,7 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) { - int admitted = nutscan_semaphore_acquire(semaphore, + 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__); @@ -958,7 +958,7 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut if (tmp_sec == NULL) { upsdebugx(0, "%s: Memory allocation error", __func__); #if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); #endif break; } @@ -968,14 +968,14 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut #ifdef HAVE_PTHREAD { - int ret = nutscan_thread_create(&thread_array, &thread_count, + 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 - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # endif if (ret < 0) { break; @@ -1017,7 +1017,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); } - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1049,7 +1049,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) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_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 37e2488885..5edc1daab7 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -821,7 +821,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) { - int admitted = nutscan_semaphore_acquire(semaphore, + 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__); @@ -926,7 +926,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * ) { upsdebugx(0, "%s: Memory allocation error", __func__); #if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); #endif free(ip_dest); break; @@ -957,12 +957,12 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * #ifdef HAVE_PTHREAD { - int ret = nutscan_thread_create(&thread_array, &thread_count, + 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); # if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # endif if (ret < 0) { break; @@ -1000,7 +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); } - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1032,7 +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) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_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 5ed419cce8..47aecd8362 100644 --- a/tools/nut-scanner/scan_snmp.c +++ b/tools/nut-scanner/scan_snmp.c @@ -1242,7 +1242,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) { - int admitted = nutscan_semaphore_acquire(semaphore, + 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__); @@ -1332,7 +1332,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( if (tmp_sec == NULL) { upsdebugx(0, "%s: Memory allocation error", __func__); #if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); #endif break; } @@ -1342,14 +1342,14 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( #ifdef HAVE_PTHREAD { - int ret = nutscan_thread_create(&thread_array, &thread_count, + 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 - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # endif if (ret < 0) { break; @@ -1391,7 +1391,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( upsdebugx(0, "WARNING: %s: Midway clean-up: pthread_join() returned code %i", __func__, ret); } - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -1423,7 +1423,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } thread_array[i].active = FALSE; # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_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_xml_http.c b/tools/nut-scanner/scan_xml_http.c index 2101c6934f..0d9ca05c0f 100644 --- a/tools/nut-scanner/scan_xml_http.c +++ b/tools/nut-scanner/scan_xml_http.c @@ -608,7 +608,7 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) { - int admitted = nutscan_semaphore_acquire(semaphore, + 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__); @@ -698,7 +698,7 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, if (tmp_sec == NULL) { upsdebugx(0, "%s: Memory allocation error", __func__); #if defined HAVE_PTHREAD && (defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); #endif break; } @@ -711,14 +711,14 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, #ifdef HAVE_PTHREAD { - int ret = nutscan_thread_create(&thread_array, &thread_count, + 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 - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # endif if (ret < 0) { break; @@ -760,7 +760,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); } - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } thread_count = 0; free(thread_array); @@ -792,7 +792,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) - nutscan_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); + nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); From ebd506bf29dd42a741422d4c76020d98088669e2 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Sun, 20 Sep 2026 16:07:22 +1200 Subject: [PATCH 05/17] nut-scanner: retry SNMP session opens after descriptor exhaustion Retain SNMP hosts whose session open fails with EMFILE and retry them serially after the original workers finish. Report persistent exhaustion without requeueing, preserving admission accounting and target ownership. Add focused recovery tests and a NEWS entry. This partially addresses issue #2576 and follows the admission helpers from PR #3649. Assisted by OpenAI Codex. Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- NEWS.adoc | 5 + tests/Makefile.am | 18 +- tests/nutscan-snmp-retry-test.c | 306 ++++++++++++++++++++++++++++++++ tools/nut-scanner/scan_snmp.c | 102 +++++++++-- 4 files changed, 415 insertions(+), 16 deletions(-) create mode 100644 tests/nutscan-snmp-retry-test.c diff --git a/NEWS.adoc b/NEWS.adoc index 1d2b341cd9..af3d4cfc7a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -47,6 +47,11 @@ 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] - 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 diff --git a/tests/Makefile.am b/tests/Makefile.am index 87380fa9e9..0a419e1ac6 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -265,7 +265,6 @@ endif WITH_SNMP # Compile each scanner's actual admission loop with offline worker probes. if WITH_NUT_SCANNER -if WITH_THREADING 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 @@ -275,6 +274,19 @@ endif WITH_SSL # Finish shared dependencies before the scanner's independent sub-make. $(top_builddir)/tools/nut-scanner/libnutscan.la: $(NUT_LIBCOMMON) $(top_builddir)/common/libcommonversion.la $(top_builddir)/clients/libupsclient.la +if WITH_SNMP +# The scanner library generates this header before the tests can include it. +$(top_builddir)/tools/nut-scanner/nutscan-snmp.h: $(top_builddir)/tools/nut-scanner/libnutscan.la +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-nut-test nutscan_nut_test_SOURCES = nutscan-thread-test.c nutscan_nut_test_CFLAGS = $(NUTSCAN_TEST_CFLAGS) -DNUTSCAN_TEST_PROTOCOL=3 @@ -282,10 +294,6 @@ 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 -# The scanner library generates this header before the test can include it. -$(top_builddir)/tools/nut-scanner/nutscan-snmp.h: $(top_builddir)/tools/nut-scanner/libnutscan.la -nutscan-thread-test.c: $(top_builddir)/tools/nut-scanner/nutscan-snmp.h - 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 diff --git a/tests/nutscan-snmp-retry-test.c b/tests/nutscan-snmp-retry-test.c new file mode 100644 index 0000000000..05497c8232 --- /dev/null +++ b/tests/nutscan-snmp-retry-test.c @@ -0,0 +1,306 @@ +/* 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" +#include "nut-scan.h" +#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; + + 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 + } + return EXIT_SUCCESS; +} diff --git a/tools/nut-scanner/scan_snmp.c b/tools/nut-scanner/scan_snmp.c index 47aecd8362..a22fa2a09e 100644 --- a/tools/nut-scanner/scan_snmp.c +++ b/tools/nut-scanner/scan_snmp.c @@ -960,8 +960,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) { @@ -988,8 +989,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); @@ -1093,6 +1104,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 */ @@ -1133,6 +1153,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) @@ -1159,6 +1181,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } #ifdef HAVE_PTHREAD + nutscan_init_ip_ranges(&retry_ranges); pthread_mutex_init(&dev_mutex, NULL); # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) @@ -1277,17 +1300,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); @@ -1357,14 +1384,13 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } } #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 */ @@ -1378,6 +1404,7 @@ 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 */ @@ -1386,10 +1413,12 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( 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); } nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); } @@ -1413,13 +1442,16 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( 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) @@ -1442,6 +1474,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) From 76a607b85b67c7b613a4f97ad7e15e6598f36f68 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Sun, 20 Sep 2026 05:09:11 +0000 Subject: [PATCH 06/17] tests: keep scanner assertions enabled with NDEBUG Keep test setup, scanner execution and checks active when built with -DNDEBUG. Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tests/nutscan-snmp-retry-test.c | 2 ++ tests/nutscan-thread-test.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/nutscan-snmp-retry-test.c b/tests/nutscan-snmp-retry-test.c index 05497c8232..5b36624f45 100644 --- a/tests/nutscan-snmp-retry-test.c +++ b/tests/nutscan-snmp-retry-test.c @@ -4,6 +4,8 @@ */ #include "common.h" #include "nut-scan.h" +/* Test setup, execution and checks must run even with -DNDEBUG. */ +#undef NDEBUG #include static void *test_malloc(size_t size); diff --git a/tests/nutscan-thread-test.c b/tests/nutscan-thread-test.c index c238d239c8..ad84d7ab9c 100644 --- a/tests/nutscan-thread-test.c +++ b/tests/nutscan-thread-test.c @@ -5,6 +5,8 @@ #include "common.h" #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 From d008380d3ed3b6904bf13d56b8dce587466d2ab3 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:57:04 +0000 Subject: [PATCH 07/17] tests: initialise Winsock for SNMP retry checks Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tests/nutscan-snmp-retry-test.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/nutscan-snmp-retry-test.c b/tests/nutscan-snmp-retry-test.c index 5b36624f45..93f1d89708 100644 --- a/tests/nutscan-snmp-retry-test.c +++ b/tests/nutscan-snmp-retry-test.c @@ -280,6 +280,11 @@ 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; @@ -304,5 +309,8 @@ int main(void) # endif #endif } +#ifdef WIN32 + assert(WSACleanup() == 0); +#endif return EXIT_SUCCESS; } From c84b01a7dbbe33edd261bcd5816c73e09e0af78b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Sep 2026 15:10:23 +0200 Subject: [PATCH 08/17] tests/Makefile.am: group nut-scanner related dependencies on top, like other out-of-dir stuff [#3673] Signed-off-by: Jim Klimov --- tests/Makefile.am | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index 0a419e1ac6..7955c1b44f 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -53,6 +53,7 @@ $(top_builddir)/common/libparseconf.la \ $(top_builddir)/clients/libupsclient.la \ $(top_builddir)/clients/libnutclient.la \ $(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) @@ -95,6 +96,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 @@ -271,12 +279,7 @@ if WITH_SSL NUTSCAN_TEST_CFLAGS += $(LIBSSL_CFLAGS) endif WITH_SSL -# Finish shared dependencies before the scanner's independent sub-make. -$(top_builddir)/tools/nut-scanner/libnutscan.la: $(NUT_LIBCOMMON) $(top_builddir)/common/libcommonversion.la $(top_builddir)/clients/libupsclient.la - if WITH_SNMP -# The scanner library generates this header before the tests can include it. -$(top_builddir)/tools/nut-scanner/nutscan-snmp.h: $(top_builddir)/tools/nut-scanner/libnutscan.la nutscan-thread-test.c nutscan-snmp-retry-test.c: $(top_builddir)/tools/nut-scanner/nutscan-snmp.h TESTS += nutscan-snmp-retry-test From c78dfd07deaf6cbb99d14287055710a69367f33f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Sep 2026 19:39:55 +0200 Subject: [PATCH 09/17] tools/nut-scanner/nutscan-thread.c: style fixes [#3673] Signed-off-by: Jim Klimov --- tools/nut-scanner/nutscan-thread.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/nut-scanner/nutscan-thread.c b/tools/nut-scanner/nutscan-thread.c index 3064b1f802..602671ccdd 100644 --- a/tools/nut-scanner/nutscan-thread.c +++ b/tools/nut-scanner/nutscan-thread.c @@ -15,8 +15,8 @@ int nut_scanner_thread_create(nutscan_thread_t **array, size_t *count, void *(*worker)(void *), void *arg) { - nutscan_thread_t *grown; - int ret; + nutscan_thread_t *grown; + int ret; if (*count >= SIZE_MAX / sizeof(**array)) { upsdebugx(1, "%s: Thread array is too large", __func__); @@ -28,12 +28,15 @@ int nut_scanner_thread_create(nutscan_thread_t **array, size_t *count, 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)++; @@ -61,7 +64,7 @@ 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) { - int ret, saved_errno; + int ret; if (limit > 0) { do { @@ -76,13 +79,15 @@ int nut_scanner_semaphore_acquire(sem_t *global, sem_t *protocol, ret = wait ? sem_wait(global) : sem_trywait(global); } while (ret != 0 && errno == EINTR); if (ret != 0) { - saved_errno = errno; + 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 */ From 5baa9c3bea0bef58170b3c7ac9c144c131d26828 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Sep 2026 19:42:39 +0200 Subject: [PATCH 10/17] tools/nut-scanner/nutscan-thread.{c,h}: update (C) for transplanted code [#3673] Signed-off-by: Jim Klimov --- tools/nut-scanner/nutscan-thread.c | 5 ++++- tools/nut-scanner/nutscan-thread.h | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/nut-scanner/nutscan-thread.c b/tools/nut-scanner/nutscan-thread.c index 602671ccdd..c324ab13ed 100644 --- a/tools/nut-scanner/nutscan-thread.c +++ b/tools/nut-scanner/nutscan-thread.c @@ -1,5 +1,8 @@ /* Internal ownership helpers for parallel scanner workers. - * Copyright (C) 2026 NUT contributors + * Copyright (C) + * 2016 - 2021 EATON - Various threads-related improvements + * 2020 - 2026 Jim Klimov + * 2026 NUT contributors * SPDX-License-Identifier: GPL-2.0-or-later */ diff --git a/tools/nut-scanner/nutscan-thread.h b/tools/nut-scanner/nutscan-thread.h index 1007a34fca..9428e758f1 100644 --- a/tools/nut-scanner/nutscan-thread.h +++ b/tools/nut-scanner/nutscan-thread.h @@ -1,5 +1,8 @@ /* Internal ownership helpers for parallel scanner workers. - * Copyright (C) 2026 NUT contributors + * Copyright (C) + * 2016 - 2021 EATON - Various threads-related improvements + * 2020 - 2026 Jim Klimov + * 2026 NUT contributors * SPDX-License-Identifier: GPL-2.0-or-later */ From a01ccb47349391a40cc5c133658d98b41bcb3810 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Sep 2026 19:50:33 +0200 Subject: [PATCH 11/17] tools/nut-scanner/scan_*.c: restore debug-log after successful thread acquire [#3673] Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_ipmi.c | 10 ++++++++++ tools/nut-scanner/scan_nut.c | 10 ++++++++++ tools/nut-scanner/scan_snmp.c | 10 ++++++++++ tools/nut-scanner/scan_xml_http.c | 10 ++++++++++ 4 files changed, 40 insertions(+) diff --git a/tools/nut-scanner/scan_ipmi.c b/tools/nut-scanner/scan_ipmi.c index 7895b56842..2f3448355d 100644 --- a/tools/nut-scanner/scan_ipmi.c +++ b/tools/nut-scanner/scan_ipmi.c @@ -870,11 +870,21 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut { 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 + " pass=%u", + __func__, max_threads_scantype, + curr_threads, thread_count, + pass + ); } # else # ifdef HAVE_PTHREAD_TRYJOIN diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 5edc1daab7..6e13d4f57d 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -823,11 +823,21 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * { 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 + " pass=%u", + __func__, max_threads_scantype, + curr_threads, thread_count, + pass + ); } # else # ifdef HAVE_PTHREAD_TRYJOIN diff --git a/tools/nut-scanner/scan_snmp.c b/tools/nut-scanner/scan_snmp.c index a22fa2a09e..7caf73b3ec 100644 --- a/tools/nut-scanner/scan_snmp.c +++ b/tools/nut-scanner/scan_snmp.c @@ -1267,11 +1267,21 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( { 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 + " pass=%u", + __func__, max_threads_scantype, + curr_threads, thread_count, + pass + ); } # else # ifdef HAVE_PTHREAD_TRYJOIN diff --git a/tools/nut-scanner/scan_xml_http.c b/tools/nut-scanner/scan_xml_http.c index 0d9ca05c0f..dd28acaec7 100644 --- a/tools/nut-scanner/scan_xml_http.c +++ b/tools/nut-scanner/scan_xml_http.c @@ -610,11 +610,21 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, { 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 + " pass=%u", + __func__, max_threads_scantype, + curr_threads, thread_count, + pass + ); } # else # ifdef HAVE_PTHREAD_TRYJOIN From 20e217304ed6ced6cd16437d7f92f72b0af70084 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Sep 2026 19:52:31 +0200 Subject: [PATCH 12/17] tools/nut-scanner/scan_nut.c: move nut_arg==NULL check into free_nut_arg() for simplicity [#3673] Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_nut.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 6e13d4f57d..bfe861fc3d 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -396,6 +396,9 @@ int nutscan_load_upsclient_library(const char *libname_path) /* 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); @@ -547,9 +550,7 @@ static void * list_nut_devices_thready(void * arg) if (hostname) { free(hostname); } - if (nut_arg) { - free_nut_arg(nut_arg); - } + free_nut_arg(nut_arg); return NULL; } From 827e2cba8c81c5fca8f39b81e167cc5b32a739be Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Sep 2026 19:58:02 +0200 Subject: [PATCH 13/17] tools/nut-scanner/scan_*.c: when logging "pass" (bool_t), err on the safe side to cast it as (unsigned int) [#3673] ...This may get revisited after we consistently use one boolean type in the code base. Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_ipmi.c | 2 +- tools/nut-scanner/scan_nut.c | 2 +- tools/nut-scanner/scan_snmp.c | 2 +- tools/nut-scanner/scan_xml_http.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/nut-scanner/scan_ipmi.c b/tools/nut-scanner/scan_ipmi.c index 2f3448355d..3adcc18f3c 100644 --- a/tools/nut-scanner/scan_ipmi.c +++ b/tools/nut-scanner/scan_ipmi.c @@ -883,7 +883,7 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - pass + (unsigned int)pass ); } # else diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index bfe861fc3d..6eb5ea02d4 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -837,7 +837,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - pass + (unsigned int)pass ); } # else diff --git a/tools/nut-scanner/scan_snmp.c b/tools/nut-scanner/scan_snmp.c index 7caf73b3ec..c2462902f4 100644 --- a/tools/nut-scanner/scan_snmp.c +++ b/tools/nut-scanner/scan_snmp.c @@ -1280,7 +1280,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - pass + (unsigned int)pass ); } # else diff --git a/tools/nut-scanner/scan_xml_http.c b/tools/nut-scanner/scan_xml_http.c index dd28acaec7..12727df280 100644 --- a/tools/nut-scanner/scan_xml_http.c +++ b/tools/nut-scanner/scan_xml_http.c @@ -623,7 +623,7 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, " pass=%u", __func__, max_threads_scantype, curr_threads, thread_count, - pass + (unsigned int)pass ); } # else From f9269a1081a0605e3e16f3582d98bceb59108e42 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:06:30 +0000 Subject: [PATCH 14/17] nut-scanner: clear freed scanner locals Clear local pointers after releasing scanner-owned storage. Assisted by OpenAI Codex. Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tools/nut-scanner/scan_ipmi.c | 1 + tools/nut-scanner/scan_nut.c | 3 +++ tools/nut-scanner/scan_snmp.c | 1 + tools/nut-scanner/scan_xml_http.c | 1 + 4 files changed, 6 insertions(+) diff --git a/tools/nut-scanner/scan_ipmi.c b/tools/nut-scanner/scan_ipmi.c index 3adcc18f3c..f8ace4fa30 100644 --- a/tools/nut-scanner/scan_ipmi.c +++ b/tools/nut-scanner/scan_ipmi.c @@ -1043,6 +1043,7 @@ nutscan_device_t * nutscan_scan_ip_range_ipmi(nutscan_ip_range_list_t * irl, nut } /* while */ free(ip_str); + ip_str = NULL; #ifdef HAVE_PTHREAD if (thread_array != NULL) { diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 6eb5ea02d4..d8d5b61645 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -551,6 +551,7 @@ static void * list_nut_devices_thready(void * arg) free(hostname); } free_nut_arg(nut_arg); + nut_arg = NULL; return NULL; } @@ -940,6 +941,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * nut_scanner_semaphore_release(semaphore, semaphore_scantype, max_threads_scantype); #endif free(ip_dest); + ip_dest = NULL; break; } @@ -972,6 +974,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * 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 diff --git a/tools/nut-scanner/scan_snmp.c b/tools/nut-scanner/scan_snmp.c index c2462902f4..e268e161e6 100644 --- a/tools/nut-scanner/scan_snmp.c +++ b/tools/nut-scanner/scan_snmp.c @@ -1446,6 +1446,7 @@ nutscan_device_t * nutscan_scan_ip_range_snmp( } /* while */ free(ip_str); + ip_str = NULL; #ifdef HAVE_PTHREAD if (thread_array != NULL) { diff --git a/tools/nut-scanner/scan_xml_http.c b/tools/nut-scanner/scan_xml_http.c index 12727df280..c999fd6f8a 100644 --- a/tools/nut-scanner/scan_xml_http.c +++ b/tools/nut-scanner/scan_xml_http.c @@ -786,6 +786,7 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, } /* while */ free(ip_str); + ip_str = NULL; #ifdef HAVE_PTHREAD if (thread_array != NULL) { From 62722933f4cac3018d6918e83f2ccdbcc3de1931 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:06:58 +0000 Subject: [PATCH 15/17] nut-scanner: balance serial worker admission and cleanup Reuse the shared admission and thread helpers for serial scans, release permits when worker creation fails, and join workers before freeing their borrowed port names or destroying the device mutex. Extend the existing offline admission test for serial scans and record the change in NEWS. Assisted by OpenAI Codex. Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- NEWS.adoc | 3 + tests/Makefile.am | 6 + tests/nutscan-thread-test.c | 167 ++++++++++++++++++++++---- tools/nut-scanner/scan_eaton_serial.c | 63 ++++------ 4 files changed, 174 insertions(+), 65 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index af3d4cfc7a..1de1402c17 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -52,6 +52,9 @@ https://github.com/networkupstools/nut/milestone/13 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 diff --git a/tests/Makefile.am b/tests/Makefile.am index 7955c1b44f..e4650c1887 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -290,6 +290,12 @@ nutscan_snmp_retry_test_DEPENDENCIES = $(top_srcdir)/tools/nut-scanner/scan_snmp 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 = $(NUTSCAN_TEST_LDADD) $(top_builddir)/drivers/libserial-nutscan.la $(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 diff --git a/tests/nutscan-thread-test.c b/tests/nutscan-thread-test.c index ad84d7ab9c..59ac625fe0 100644 --- a/tests/nutscan-thread-test.c +++ b/tests/nutscan-thread-test.c @@ -16,7 +16,17 @@ #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, @@ -24,33 +34,46 @@ static int test_create(pthread_t *thread, const pthread_attr_t *attr, # if defined HAVE_SEMAPHORE_UNNAMED || defined HAVE_SEMAPHORE_NAMED static int test_wait(sem_t *sem); static int test_trywait(sem_t *sem); -# ifdef HAVE_SEMAPHORE_UNNAMED +# 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 +# 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. */ -# define malloc test_malloc +# 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 -# 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 +# 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 @@ -63,6 +86,8 @@ static int test_unlink(const char *name); # 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 @@ -71,6 +96,9 @@ static int test_unlink(const char *name); # 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 @@ -91,7 +119,50 @@ 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; @@ -111,6 +182,7 @@ static void *test_malloc(size_t size) } abort(); } +# endif static void *test_realloc(void *ptr, size_t size) { @@ -124,6 +196,12 @@ 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) { @@ -145,7 +223,16 @@ static void *offline_worker(void *opaque) { struct test_worker *work = (struct test_worker *)opaque; -# if NUTSCAN_TEST_PROTOCOL == 3 +# 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 @@ -161,6 +248,9 @@ static void *offline_worker(void *opaque) 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; @@ -186,6 +276,11 @@ static int test_create(pthread_t *thread, const pthread_attr_t *attr, 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); } @@ -196,9 +291,11 @@ static int test_create(pthread_t *thread, const pthread_attr_t *attr, # 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 defined HAVE_SEMAPHORE_NAMED && !defined HAVE_SEMAPHORE_UNNAMED +# if NUTSCAN_TEST_PROTOCOL != 5 && defined HAVE_SEMAPHORE_NAMED && !defined HAVE_SEMAPHORE_UNNAMED static char protocol_name[128]; # endif @@ -243,7 +340,8 @@ static int test_trywait(sem_t *sem) return sem_trywait(sem); } -# ifdef HAVE_SEMAPHORE_UNNAMED +# if NUTSCAN_TEST_PROTOCOL != 5 +# ifdef HAVE_SEMAPHORE_UNNAMED static int test_init(sem_t *sem, int shared, unsigned int value) { int ret; @@ -272,7 +370,7 @@ static int test_destroy(sem_t *sem) protocol_sem = NULL; return ret; } -# else +# else static sem_t *test_open(const char *name, int flags, ...) { va_list ap; @@ -314,7 +412,8 @@ static int test_unlink(const char *name) NUT_UNUSED_VARIABLE(name); return 0; } -# endif +# endif +# endif /* NUTSCAN_TEST_PROTOCOL != 5 */ # endif /* semaphore support */ # if NUTSCAN_TEST_PROTOCOL == 1 @@ -394,31 +493,35 @@ static void offline_free_cert(const char *host, const char *cert) 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 +# if NUTSCAN_TEST_PROTOCOL == 1 nutscan_snmp_t settings; -# elif NUTSCAN_TEST_PROTOCOL == 2 +# elif NUTSCAN_TEST_PROTOCOL == 2 nutscan_ipmi_t settings; -# elif NUTSCAN_TEST_PROTOCOL == 3 +# elif NUTSCAN_TEST_PROTOCOL == 3 nutscan_nut_authconf_t settings; -# else +# else nutscan_xml_t settings; -# endif +# 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 +# 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 +# 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 +# elif NUTSCAN_TEST_PROTOCOL == 3 max_threads_oldnut = limit; nutscan_avail_nut = 1; nut_upscli_splitaddr = offline_splitaddr; @@ -431,12 +534,13 @@ static void scan(unsigned int limit) nut_upscli_free_host_cert = offline_free_cert; settings.authconf_file = "none"; assert(nutscan_scan_ip_range_nut_authconf(&ranges, &settings) == NULL); -# else +# else max_threads_netxml = limit; nutscan_avail_xml_http = 1; assert(nutscan_scan_ip_range_xml_http(&ranges, 1, &settings) == NULL); -# endif +# endif nutscan_free_ip_ranges(&ranges); +# endif } static void scenario(unsigned int limit, int failure) @@ -449,6 +553,11 @@ static void scenario(unsigned int limit, int failure) 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) { @@ -539,6 +648,14 @@ int main(int argc, char **argv) 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; 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); From 958e8072097e4b85ea94d3f9960a1bbf377dbad4 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Mon, 28 Sep 2026 04:10:31 +0000 Subject: [PATCH 16/17] tests: fix serial scanner test library link order Link the serial archive before the common libraries that supply its logging and Windows serial helpers. This fixes the unresolved symbols when linking nutscan-serial-test with shared private libraries on MinGW. Assisted by OpenAI Codex (GPT-6). Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- tests/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index db76fc2f06..148b59f1c2 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -297,7 +297,7 @@ 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 = $(NUTSCAN_TEST_LDADD) $(top_builddir)/drivers/libserial-nutscan.la $(SERLIBS) +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 From 0857234a789e9aaf86123850ff601e73ef4afbb9 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:14:14 +0000 Subject: [PATCH 17/17] tests: fix static scanner and retry linkage Carry the common scanner-test Net-SNMP executable flags and SSL dependency so static archive references resolve for every fixture, including retries. Retain the original library passthrough flags and cache both forms through the existing dependency macro. Keep both fixture mutexes separate from the library and order common-library sub-makes to prevent races over shared replacement-function objects. Validated static scanner and retry tests on Linux and native MinGW Windows, clean two-job builds, and distcheck-light with install/uninstall. Prepared with OpenAI Codex (GPT-6) assistance, following the earlier Claude Opus 5.5 review. Signed-off-by: user01010111 <12504630+user01010111@users.noreply.github.com> --- configure.ac | 1 + m4/nut_check_libnetsnmp.m4 | 14 +++++++++++++- tests/Makefile.am | 8 ++++++++ tests/nutscan-snmp-retry-test.c | 2 ++ tests/nutscan-thread-test.c | 2 ++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 02f822b00a..a2309c0721 100644 --- a/configure.ac +++ b/configure.ac @@ -6502,6 +6502,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 2a2cedf828..9059ec39f0 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,11 +181,13 @@ AC_DEFUN([NUT_CHECK_LIBNETSNMP], nut_cv_LIBNETSNMP_CFLAGS="" nut_cv_LIBNETSNMP_LIBS="" + nut_cv_LIBNETSNMP_LIBS_EXEC="" nut_cv_SOFILE_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], @@ -376,6 +385,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], []) @@ -383,6 +393,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 @@ -429,6 +440,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 148b59f1c2..673a0ea819 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -64,6 +64,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 @@ -281,7 +284,12 @@ NUTSCAN_TEST_CFLAGS = $(AM_CFLAGS) -I$(top_srcdir)/tools/nut-scanner -I$(top_bui 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 diff --git a/tests/nutscan-snmp-retry-test.c b/tests/nutscan-snmp-retry-test.c index 93f1d89708..1297bcafde 100644 --- a/tests/nutscan-snmp-retry-test.c +++ b/tests/nutscan-snmp-retry-test.c @@ -3,6 +3,8 @@ * 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 diff --git a/tests/nutscan-thread-test.c b/tests/nutscan-thread-test.c index 59ac625fe0..d1d6c4fedf 100644 --- a/tests/nutscan-thread-test.c +++ b/tests/nutscan-thread-test.c @@ -3,6 +3,8 @@ * 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. */