/* * Copyright (C) 2011 - EATON * Copyright (C) 2016-2021 - EATON - Various threads-related improvements * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /*! \file scan_nut.c \brief detect remote NUT services \author Frederic Bohe \author Jim Klimov */ #include "common.h" #include "upsclient.h" #include "nut-scan.h" #include /* dynamic link library stuff */ static lt_dlhandle dl_handle = NULL; static const char *dl_error = NULL; static int (*nut_upscli_splitaddr)(const char *buf, char **hostname, int *port); static int (*nut_upscli_tryconnect)(UPSCONN_t *ups, const char *host, int port, int flags, struct timeval * timeout); static int (*nut_upscli_list_start)(UPSCONN_t *ups, size_t numq, const char **query); static int (*nut_upscli_list_next)(UPSCONN_t *ups, size_t numq, const char **query, size_t *numa, char ***answer); static int (*nut_upscli_disconnect)(UPSCONN_t *ups); static nutscan_device_t * dev_ret = NULL; #ifdef HAVE_PTHREAD static pthread_mutex_t dev_mutex; #endif /* use explicit booleans */ #ifndef FALSE typedef enum ebool { FALSE = 0, TRUE } bool_t; #else typedef int bool_t; #endif struct scan_nut_arg { char * hostname; useconds_t timeout; }; /* return 0 on error; visible externally */ int nutscan_load_upsclient_library(const char *libname_path); int nutscan_load_upsclient_library(const char *libname_path) { if (dl_handle != NULL) { /* if previous init failed */ if (dl_handle == (void *)1) { return 0; } /* init has already been done */ return 1; } if (libname_path == NULL) { fprintf(stderr, "NUT client library not found. NUT search disabled.\n"); return 0; } if (lt_dlinit() != 0) { fprintf(stderr, "Error initializing lt_init\n"); return 0; } dl_handle = lt_dlopen(libname_path); if (!dl_handle) { dl_error = lt_dlerror(); goto err; } lt_dlerror(); /* Clear any existing error */ *(void **) (&nut_upscli_splitaddr) = lt_dlsym(dl_handle, "upscli_splitaddr"); if ((dl_error = lt_dlerror()) != NULL) { goto err; } *(void **) (&nut_upscli_tryconnect) = lt_dlsym(dl_handle, "upscli_tryconnect"); if ((dl_error = lt_dlerror()) != NULL) { goto err; } *(void **) (&nut_upscli_list_start) = lt_dlsym(dl_handle, "upscli_list_start"); if ((dl_error = lt_dlerror()) != NULL) { goto err; } *(void **) (&nut_upscli_list_next) = lt_dlsym(dl_handle, "upscli_list_next"); if ((dl_error = lt_dlerror()) != NULL) { goto err; } *(void **) (&nut_upscli_disconnect) = lt_dlsym(dl_handle, "upscli_disconnect"); if ((dl_error = lt_dlerror()) != NULL) { goto err; } return 1; err: fprintf(stderr, "Cannot load NUT library (%s) : %s. NUT search disabled.\n", libname_path, dl_error); dl_handle = (void *)1; lt_dlexit(); return 0; } /* FIXME: SSL support */ static void * list_nut_devices(void * arg) { struct scan_nut_arg * nut_arg = (struct scan_nut_arg*)arg; char *target_hostname = nut_arg->hostname; struct timeval tv; int port; size_t numq, numa; const char *query[4]; char **answer; char *hostname = NULL; UPSCONN_t *ups = malloc(sizeof(*ups)); nutscan_device_t * dev = NULL; size_t buf_size; tv.tv_sec = nut_arg->timeout / (1000*1000); tv.tv_usec = nut_arg->timeout % (1000*1000); query[0] = "UPS"; numq = 1; if ((*nut_upscli_splitaddr)(target_hostname, &hostname, &port) != 0) { free(target_hostname); free(nut_arg); free(ups); return NULL; } if ((*nut_upscli_tryconnect)(ups, hostname, port, UPSCLI_CONN_TRYSSL, &tv) < 0) { free(target_hostname); free(nut_arg); free(ups); return NULL; } if ((*nut_upscli_list_start)(ups, numq, query) < 0) { (*nut_upscli_disconnect)(ups); free(target_hostname); free(nut_arg); free(ups); return NULL; } while ((*nut_upscli_list_next)(ups, numq, query, &numa, &answer) == 1) { /* UPS */ if (numa < 3) { (*nut_upscli_disconnect)(ups); free(target_hostname); free(nut_arg); free(ups); return NULL; } /* FIXME: check for duplication by getting driver.port and device.serial * for comparison with other busses results */ /* FIXME: * - also print answer[2] if != "Unavailable"? * - for upsmon.conf or ups.conf (using dummy-ups)? */ dev = nutscan_new_device(); dev->type = TYPE_NUT; dev->driver = strdup("nutclient"); /* +1+1 is for '@' character and terminating 0 */ buf_size = strlen(answer[1]) + strlen(hostname) + 1 + 1; dev->port = malloc(buf_size); if (dev->port) { snprintf(dev->port, buf_size, "%s@%s", answer[1], hostname); #ifdef HAVE_PTHREAD pthread_mutex_lock(&dev_mutex); #endif dev_ret = nutscan_add_device_to_device(dev_ret, dev); #ifdef HAVE_PTHREAD pthread_mutex_unlock(&dev_mutex); #endif } } (*nut_upscli_disconnect)(ups); free(target_hostname); free(nut_arg); free(ups); return NULL; } nutscan_device_t * nutscan_scan_nut(const char* startIP, const char* stopIP, const char* port, useconds_t usec_timeout) { bool_t pass = TRUE; /* Track that we may spawn a scanning thread */ nutscan_ip_iter_t ip; char * ip_str = NULL; char * ip_dest = NULL; char buf[SMALLBUF]; struct sigaction oldact; int change_action_handler = 0; struct scan_nut_arg *nut_arg; #ifdef HAVE_PTHREAD # ifdef HAVE_SEMAPHORE sem_t * semaphore = nutscan_semaphore(); sem_t semaphore_scantype_inst; sem_t * semaphore_scantype = &semaphore_scantype_inst; # endif /* HAVE_SEMAPHORE */ pthread_t thread; nutscan_thread_t * thread_array = NULL; size_t thread_count = 0, i; # if (defined HAVE_PTHREAD_TRYJOIN) || (defined HAVE_SEMAPHORE) size_t max_threads_scantype = max_threads_oldnut; # endif pthread_mutex_init(&dev_mutex, NULL); # ifdef HAVE_SEMAPHORE if (max_threads_scantype > 0) { #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE #pragma GCC diagnostic push #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE #pragma GCC diagnostic ignored "-Wunreachable-code" #endif #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunreachable-code" #endif /* Different platforms, different sizes, none fits all... */ if (SIZE_MAX > UINT_MAX && max_threads_scantype > UINT_MAX) { #ifdef __clang__ #pragma clang diagnostic pop #endif #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE #pragma GCC diagnostic pop #endif upsdebugx(1, "WARNING: %s: Limiting max_threads_scantype to range acceptable for sem_init()", __func__); max_threads_scantype = UINT_MAX - 1; } sem_init(semaphore_scantype, 0, (unsigned int)max_threads_scantype); } # endif /* HAVE_SEMAPHORE */ #endif /* HAVE_PTHREAD */ if (!nutscan_avail_nut) { return NULL; } /* Ignore SIGPIPE if the caller hasn't set a handler for it yet */ if (sigaction(SIGPIPE, NULL, &oldact) == 0) { #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_STRICT_PROTOTYPES) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wstrict-prototypes" #endif if (oldact.sa_handler == SIG_DFL) { change_action_handler = 1; signal(SIGPIPE, SIG_IGN); } #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_STRICT_PROTOTYPES) # pragma GCC diagnostic pop #endif } ip_str = nutscan_ip_iter_init(&ip, startIP, stopIP); while (ip_str != NULL) { #ifdef HAVE_PTHREAD /* NOTE: With many enough targets to scan, this can crash * by spawning too many children; add a limit and loop to * "reap" some already done with their work. And probably * account them in thread_array[] as something to not wait * for below in pthread_join()... */ # ifdef HAVE_SEMAPHORE /* 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 { pass = ((max_threads_scantype == 0 || sem_trywait(semaphore_scantype) == 0) && sem_trywait(semaphore) == 0); } # else # ifdef HAVE_PTHREAD_TRYJOIN /* A somewhat naive and brute-force solution for * systems without a semaphore.h. This may suffer * some off-by-one errors, using a few more threads * than intended (if we race a bit at the wrong time, * probably up to one per enabled scanner routine). */ /* TOTHINK: Should there be a threadcount_mutex when * we just read the value in if() and while() below? * At worst we would overflow the limit a bit due to * other protocol scanners... */ if (curr_threads >= max_threads || (curr_threads >= max_threads_scantype && max_threads_scantype > 0) ) { upsdebugx(2, "%s: already running %zu scanning threads " "(launched overall: %zu), " "waiting until some would finish", __func__, curr_threads, thread_count); while (curr_threads >= max_threads || (curr_threads >= max_threads_scantype && max_threads_scantype > 0) ) { for (i = 0; i < thread_count ; i++) { int ret; if (!thread_array[i].active) continue; pthread_mutex_lock(&threadcount_mutex); upsdebugx(3, "%s: Trying to join thread #%i...", __func__, i); ret = pthread_tryjoin_np(thread_array[i].thread, NULL); switch (ret) { case ESRCH: /* No thread with the ID thread could be found - already "joined"? */ upsdebugx(5, "%s: Was thread #%zu joined earlier?", __func__, i); break; case 0: /* thread exited */ if (curr_threads > 0) { curr_threads --; upsdebugx(4, "%s: Joined a finished thread #%zu", __func__, i); } else { /* threadcount_mutex fault? */ upsdebugx(0, "WARNING: %s: Accounting of thread count " "says we are already at 0", __func__); } thread_array[i].active = FALSE; break; case EBUSY: /* actively running */ upsdebugx(6, "%s: thread #%zu still busy (%i)", __func__, i, ret); break; case EDEADLK: /* Errors with thread interactions... bail out? */ case EINVAL: /* Errors with thread interactions... bail out? */ default: /* new pthreads abilities? */ upsdebugx(5, "%s: thread #%zu reported code %i", __func__, i, ret); break; } pthread_mutex_unlock(&threadcount_mutex); } if (curr_threads >= max_threads || (curr_threads >= max_threads_scantype && max_threads_scantype > 0) ) { usleep (10000); /* microSec's, so 0.01s here */ } } upsdebugx(2, "%s: proceeding with scan", __func__); } /* NOTE: No change to default "pass" in this ifdef: * if we got to this line, we have a slot to use */ # endif /* HAVE_PTHREAD_TRYJOIN */ # endif /* HAVE_SEMAPHORE */ #endif /* HAVE_PTHREAD */ if (pass) { if (port) { if (ip.type == IPv4) { snprintf(buf, sizeof(buf), "%s:%s", ip_str, port); } else { snprintf(buf, sizeof(buf), "[%s]:%s", ip_str, port); } ip_dest = strdup(buf); } else { ip_dest = strdup(ip_str); } if ((nut_arg = malloc(sizeof(struct scan_nut_arg))) == NULL) { free(ip_dest); break; } nut_arg->timeout = usec_timeout; nut_arg->hostname = ip_dest; #ifdef HAVE_PTHREAD if (pthread_create(&thread, NULL, list_nut_devices, (void*)nut_arg) == 0) { # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); curr_threads++; # endif /* HAVE_PTHREAD_TRYJOIN */ thread_count++; nutscan_thread_t *new_thread_array = realloc(thread_array, thread_count * sizeof(nutscan_thread_t)); if (new_thread_array == NULL) { upsdebugx(1, "%s: Failed to realloc thread array", __func__); break; } else { thread_array = new_thread_array; } 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 /* not HAVE_PTHREAD */ list_nut_devices(nut_arg); #endif /* if HAVE_PTHREAD */ free(ip_str); ip_str = nutscan_ip_iter_inc(&ip); } else { /* if not pass -- all slots busy */ #ifdef HAVE_PTHREAD # ifdef HAVE_SEMAPHORE /* Wait for all current scans to complete */ if (thread_array != NULL) { upsdebugx (2, "%s: Running too many scanning threads, " "waiting until older ones would finish", __func__); for (i = 0; i < thread_count ; i++) { int ret; if (!thread_array[i].active) { /* Probably should not get here, * but handle it just in case */ upsdebugx(0, "WARNING: %s: Midway clean-up: did not expect thread %zu to be not active", __func__, i); sem_post(semaphore); if (max_threads_scantype > 0) sem_post(semaphore_scantype); continue; } thread_array[i].active = FALSE; ret = pthread_join(thread_array[i].thread, NULL); if (ret != 0) { 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); } thread_count = 0; free(thread_array); thread_array = NULL; } # else # ifdef HAVE_PTHREAD_TRYJOIN /* TODO: Move the wait-loop for TRYJOIN here? */ # endif /* HAVE_PTHREAD_TRYJOIN */ # endif /* HAVE_SEMAPHORE */ #endif /* HAVE_PTHREAD */ } /* if: could we "pass" or not? */ } /* while */ #ifdef HAVE_PTHREAD if (thread_array != NULL) { upsdebugx(2, "%s: all planned scans launched, waiting for threads to complete", __func__); for (i = 0; i < thread_count; i++) { int ret; if (!thread_array[i].active) continue; ret = pthread_join(thread_array[i].thread, NULL); if (ret != 0) { upsdebugx(0, "WARNING: %s: Clean-up: pthread_join() returned code %i", __func__, ret); } thread_array[i].active = FALSE; # ifdef HAVE_SEMAPHORE sem_post(semaphore); if (max_threads_scantype > 0) sem_post(semaphore_scantype); # else # ifdef HAVE_PTHREAD_TRYJOIN pthread_mutex_lock(&threadcount_mutex); if (curr_threads > 0) { curr_threads --; upsdebugx(5, "%s: Clean-up: Joined a finished thread #%zu", __func__, i); } else { upsdebugx(0, "WARNING: %s: Clean-up: Accounting of thread count " "says we are already at 0", __func__); } pthread_mutex_unlock(&threadcount_mutex); # endif /* HAVE_PTHREAD_TRYJOIN */ # endif /* HAVE_SEMAPHORE */ } free(thread_array); upsdebugx(2, "%s: all threads freed", __func__); } pthread_mutex_destroy(&dev_mutex); # ifdef HAVE_SEMAPHORE if (max_threads_scantype > 0) sem_destroy(semaphore_scantype); # endif /* HAVE_SEMAPHORE */ #endif /* HAVE_PTHREAD */ if (change_action_handler) { #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_STRICT_PROTOTYPES) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wstrict-prototypes" #endif signal(SIGPIPE, SIG_DFL); #if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_STRICT_PROTOTYPES) # pragma GCC diagnostic pop #endif } return nutscan_rewind_device(dev_ret); }