From aeb32d09cdeaa7995b887618833583ebf2839453 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 19 Oct 2026 16:59:12 +0200 Subject: [PATCH 01/13] appveyor.yml, .github/scripts/appveyor/02-install.bat: use standard CCACHE_MAXSIZE envvar if possible [#3754] Signed-off-by: Jim Klimov --- .github/scripts/appveyor/02-install.bat | 2 +- appveyor.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/appveyor/02-install.bat b/.github/scripts/appveyor/02-install.bat index 6914e4715a..124cd9c7b8 100644 --- a/.github/scripts/appveyor/02-install.bat +++ b/.github/scripts/appveyor/02-install.bat @@ -45,5 +45,5 @@ REM Preserve the current working directory: set CHERE_INVOKING=yes REM Start a 64 bit Mingw environment: set MSYSTEM=MINGW64 -C:\msys64\usr\bin\bash -lc 'PATH="/mingw64/bin:$PATH" ; export PATH ; pwd ; ccache -sv || echo "SKIP: Could not query ccache stats" ; ccache -o sloppiness=file_macro || true ; ccache -o compression=true || true ; ccache -o max_size=750M || true ' +C:\msys64\usr\bin\bash -lc 'PATH="/mingw64/bin:$PATH" ; export PATH ; pwd ; ccache -sv || echo "SKIP: Could not query ccache stats" ; ccache -o sloppiness=file_macro || true ; ccache -o compression=true || true ; if [ x"${CCACHE_MAXSIZE}" = x ] ; then ccache -o max_size=750M ; fi || true ' if errorlevel 1 exit /b %ERRORLEVEL% diff --git a/appveyor.yml b/appveyor.yml index 8ae93250fa..78f96c9944 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -39,6 +39,7 @@ environment: # (see ci_build.sh for details): CI_AUTOGEN_STABLE_VERSION: true CCACHE_DIR: /home/appveyor/.ccache + CCACHE_MAXSIZE: 750MiB DO_CLEAN_NUTCI_CACHE: no DO_USE_NUTCI_CACHE: yes MSYS: "winsymlinks:nativestrict" From bb3e3b82d0c768b91ee002ac381ac390f7fa72ee Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 19 Oct 2026 17:13:39 +0200 Subject: [PATCH 02/13] docs/man/Makefile.am: refactor with DOCBUILD_MAN Signed-off-by: Jim Klimov --- docs/man/Makefile.am | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index e643b88eeb..56e49bba22 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -2119,6 +2119,13 @@ DOCBUILD_END = { \ fi ; \ } +DOCBUILD_MAN = { \ + echo " DOC-MAN Generating $@"; \ + $(DOCBUILD_BEGIN) ; RES=0; \ + $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ + $(DOCBUILD_END) ; exit $$RES ; \ +} + ### Call the prep step consistently to create symlinks (out-of-tree) ### or just touch-files for peace of mind (in-tree builds). Then we ### use these path names (truncated "-prepped") now surely located @@ -2188,38 +2195,23 @@ else !KNOWN_UNABLE_MANS .txt-prepped.$(MAN_SECTION_CMD_USR): @A2X_OUTDIR="tmp/man$(MAN_SECTION_CMD_USR).$(@F).$$$$" ; \ - echo " DOC-MAN Generating $@"; \ - $(DOCBUILD_BEGIN) ; RES=0; \ - $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ - $(DOCBUILD_END) ; exit $$RES + $(DOCBUILD_MAN) .txt-prepped.$(MAN_SECTION_API): @A2X_OUTDIR="tmp/man$(MAN_SECTION_API).$(@F).$$$$" ; \ - echo " DOC-MAN Generating $@"; \ - $(DOCBUILD_BEGIN) ; RES=0; \ - $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ - $(DOCBUILD_END) ; exit $$RES + $(DOCBUILD_MAN) .txt-prepped.$(MAN_SECTION_CFG): @A2X_OUTDIR="tmp/man$(MAN_SECTION_CFG).$(@F).$$$$" ; \ - echo " DOC-MAN Generating $@"; \ - $(DOCBUILD_BEGIN) ; RES=0; \ - $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ - $(DOCBUILD_END) ; exit $$RES + $(DOCBUILD_MAN) .txt-prepped.$(MAN_SECTION_CMD_SYS): @A2X_OUTDIR="tmp/man$(MAN_SECTION_CMD_SYS).$(@F).$$$$" ; \ - echo " DOC-MAN Generating $@"; \ - $(DOCBUILD_BEGIN) ; RES=0; \ - $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ - $(DOCBUILD_END) ; exit $$RES + $(DOCBUILD_MAN) .txt-prepped.$(MAN_SECTION_MISC): @A2X_OUTDIR="tmp/man$(MAN_SECTION_MISC).$(@F).$$$$" ; \ - echo " DOC-MAN Generating $@"; \ - $(DOCBUILD_BEGIN) ; RES=0; \ - $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ - $(DOCBUILD_END) ; exit $$RES + $(DOCBUILD_MAN) endif !KNOWN_UNABLE_MANS From 79d53c5fdf4cc4e9c5e5d1712be96100015478c8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 19 Oct 2026 17:17:39 +0200 Subject: [PATCH 03/13] docs/man/Makefile.am: DOCBUILD_MAN: throttle parallel builds on FreeBSD :\ Signed-off-by: Jim Klimov --- docs/man/Makefile.am | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 56e49bba22..8019576ec8 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -2085,6 +2085,11 @@ CLEANFILES += *.xml *.html *.pdf # NOTE: MKDIR_P may be defined via expanded $(top_builddir)/install-sh # so should be run from $(abs_builddir) to be safe, as we jump around # the build workspace +# For some reason, parallel builds of man pages on FreeBSD 15 with tmpfs +# (to create a tarball during `make dist`, or inside `make distcheck` +# context) fail sufficiently often to be noticeable. May be a system +# problem with some files not becoming visible in time (some other +# tests had failed suspiciously similarly). Sleep just in case... DOCBUILD_BEGIN = { \ if test -n "$${A2X_OUTDIR}" && test "$${A2X_OUTDIR}" != '.' ; then \ rm -rf "./$${A2X_OUTDIR}" || true ; \ @@ -2101,6 +2106,12 @@ DOCBUILD_BEGIN = { \ ; then chmod u+w "${builddir}/docbook-xsl.css" ; fi ; \ chmod -R u+w "./$${A2X_OUTDIR}" || true ; \ A2X_VERBOSE="$(ASCIIDOC_VERBOSE)"; if [ "$(V)" = 1 ]; then A2X_VERBOSE="--verbose"; fi; \ + case "-$(MAKEFLAGS) $(AM_MAKEFLAGS)" in \ + *-j|*-j" "*|*-{j,l}{0,1,2,3,4,5,6,7,8,9}*|*-[jl][0123456789]*|*{-l,--jobs,--load-average,--max-load}" "{-,0,1,2,3,4,5,6,7,8,9}*|*--jobserver*|*--jobs" "[0123456789]*|*--load-average" "[0123456789]*|*--max-load" "[0123456789]*) \ + case "`uname -s | tr 'A-Z' 'a-z'`" in \ + *freebsd*) sleep 1 ;; \ + esac ;; \ + esac ; \ } # Note that documents with sub-pages (see LIBNUTCLIENT_*_DEPS above) From d814ac7917725a8e870fb441510da07d79ba3642 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 19 Oct 2026 17:49:37 +0200 Subject: [PATCH 04/13] tests/parseconf-test.c: fix variable declaration style [#3633] Signed-off-by: Jim Klimov --- tests/parseconf-test.c | 58 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/parseconf-test.c b/tests/parseconf-test.c index 85ee00cfea..2729e6bd6e 100644 --- a/tests/parseconf-test.c +++ b/tests/parseconf-test.c @@ -23,13 +23,13 @@ #endif typedef struct { - const char *name; - const char *input; - size_t numargs; - const char *args[8]; + const char *name; + const char *input; + size_t numargs; + const char *args[8]; } testcase_t; -static const testcase_t cases[] = { +static const testcase_t cases[] = { { "ordinary", "one two\n", 2, { "one", "two" } }, { "empty", " \t\n", 0, { NULL } }, { "comment", "# ignored\n", 0, { NULL } }, @@ -65,11 +65,11 @@ static const testcase_t cases[] = { { "character filter", "\"a\001\t\r\200\377b\177\"\n", 1, { "ab\177" } } }; -static int failures = 0; +static int failures = 0; static void check_args(PCONF_CTX_t *ctx, const testcase_t *test, const char *mode) { - size_t i; + size_t i; if (pconf_parse_error(ctx)) { fprintf(stderr, "%s/%s: unexpected parse error: %s\n", @@ -112,9 +112,9 @@ static void file_input(PCONF_CTX_t *ctx, const char *input) static void check_case(const testcase_t *test) { - PCONF_CTX_t ctx; - size_t i, len = strlen(test->input); - int ret; + PCONF_CTX_t ctx; + size_t i, len = strlen(test->input); + int ret; pconf_init(&ctx, NULL); check_result(pconf_line(&ctx, test->input), 1, test->name); @@ -141,20 +141,20 @@ static void check_case(const testcase_t *test) static void check_boundaries(void) { - PCONF_CTX_t ctx; - static const testcase_t incomplete = { + PCONF_CTX_t ctx; + static const testcase_t incomplete = { "incomplete quote", "key \"unfinished", 2, { "key", "unfinished" } }; - static const testcase_t limited = { + static const testcase_t limited = { "limits", "abcde fghij discarded\n", 2, { "abcd", "fghi" } }; - static const testcase_t trailing = { + static const testcase_t trailing = { "trailing backslash", "key value\\", 2, { "key", "value" } }; - static const testcase_t next = { + static const testcase_t next = { "next line", "next\n", 1, { "next" } }; - size_t i; + size_t i; /* These incomplete inputs historically return their collected words. */ pconf_init(&ctx, NULL); @@ -197,12 +197,12 @@ static void check_boundaries(void) static void check_encoder(void) { - static const char raw[] = "# \\\"' = end#"; - static const char encoded[] = "\\# \\\\\\\"' = end\\#"; - PCONF_CTX_t ctx; - char output[128], input[132]; - const testcase_t roundtrip = { "encoder roundtrip", NULL, 1, { raw } }; - const testcase_t short_result = { "short encoder roundtrip", NULL, 1, { "#" } }; + static const char raw[] = "# \\\"' = end#"; + static const char encoded[] = "\\# \\\\\\\"' = end\\#"; + PCONF_CTX_t ctx; + char output[128], input[132]; + const testcase_t roundtrip = { "encoder roundtrip", NULL, 1, { raw } }; + const testcase_t short_result = { "short encoder roundtrip", NULL, 1, { "#" } }; check_result(pconf_encode(raw, output, sizeof(output)) == output, 1, "encoder return"); check_result(strcmp(output, encoded), 0, "encoder bytes (including escaped hash)"); @@ -228,10 +228,10 @@ static void check_encoder(void) /* Parse input with stderr sent to a temporary file, return what was written */ static void capture_stderr(const char *input, int from_file, char *out, size_t outlen) { - PCONF_CTX_t ctx; - FILE *cap = tmpfile(); - int saved; - size_t n; + PCONF_CTX_t ctx; + FILE *cap = tmpfile(); + int saved; + size_t n; out[0] = '\0'; if (!cap) @@ -261,8 +261,8 @@ static void capture_stderr(const char *input, int from_file, char *out, size_t o static void check_attached_hash_warning(void) { - static const char line[] = "MONITOR ups@localhost 1 upsmon Example#123 primary\n"; - char msg[512]; + static const char line[] = "MONITOR ups@localhost 1 upsmon Example#123 primary\n"; + char msg[512]; /* A config file line cut by a glued hash names its position, and * none of the text before the hash */ @@ -283,7 +283,7 @@ static void check_attached_hash_warning(void) int main(void) { - size_t i; + size_t i; for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) check_case(&cases[i]); From c67967103d199072ee56243fc6f1009330ea59b5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 19 Oct 2026 16:54:47 +0200 Subject: [PATCH 05/13] tests/upsset-string-test.py: use a common default_timeout=30 (sec) for stressed CI builders [#3668] Signed-off-by: Jim Klimov --- tests/upsset-string-test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index 6dae1e7659..03ecef04a5 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -14,6 +14,7 @@ import threading from urllib.parse import urlencode +default_timeout = 30 def quote(value): return '"' + value.replace('\\', '\\\\').replace('"', '\\"').replace('#', '\\#') + '"' @@ -24,7 +25,7 @@ def request(binary, metadata, value='original', new_value=None): with tempfile.TemporaryDirectory(prefix='nut-upsset-') as directory, socket.socket() as server: server.bind(('127.0.0.1', 0)) server.listen(1) - server.settimeout(10) + server.settimeout(default_timeout) target = 'dummy@127.0.0.1:' + str(server.getsockname()[1]) config = Path(directory) (config / 'upsset.conf').write_text('I_HAVE_SECURED_MY_CGI_DIRECTORY\n') @@ -36,7 +37,7 @@ def serve(): try: connection, unused = server.accept() with connection: - connection.settimeout(10) + connection.settimeout(default_timeout) with connection.makefile('rb') as stream: for raw in stream: command = raw.decode('ascii').rstrip('\r\n') @@ -77,9 +78,9 @@ def serve(): REQUEST_METHOD='POST') try: result = subprocess.run([binary], input=body, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=environment, timeout=10) + stderr=subprocess.PIPE, env=environment, timeout=default_timeout) finally: - thread.join(timeout=30) + thread.join(timeout=default_timeout + 5) assert not thread.is_alive(), 'Server did not finish' assert not errors, errors assert result.returncode == 0, (metadata, result.returncode, result.stderr.decode()) From 0bcab02f4dfc50aba60d4f19f0410c5681cadbb2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 19 Oct 2026 21:57:54 +0200 Subject: [PATCH 06/13] tests/upsset-string-test.py: mock web-server better [#3668] Signed-off-by: Jim Klimov --- tests/upsset-string-test.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index 03ecef04a5..fea3b755fe 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -74,8 +74,16 @@ def serve(): fields['UPSVAR_test.string'] = new_value body = urlencode(fields).encode('ascii') + b'\n' environment = {key: val for key, val in os.environ.items() if not key.startswith('NUT_')} - environment.update(NUT_CONFPATH=directory, CONTENT_LENGTH=str(len(body)), - REQUEST_METHOD='POST') + # Ensure CGI environment is sufficiently defined for predictable behavior + # so upsset.cgi reliably parses POSTed form data and proceeds to contact + # the mocked NUT server. Missing CONTENT_TYPE may cause it to ignore the + # request body and skip any network activity, leading to a timeout here. + environment.update( + NUT_CONFPATH=directory, + CONTENT_LENGTH=str(len(body)), + REQUEST_METHOD='POST', + CONTENT_TYPE='application/x-www-form-urlencoded' + ) try: result = subprocess.run([binary], input=body, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment, timeout=default_timeout) From 3b09fbee3fee7bab66ed44834007d1021ea8e50f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 20 Oct 2026 00:47:49 +0200 Subject: [PATCH 07/13] tests/upsset-string-test.py: make sure upsset.cgi is always executed [#3668] Signed-off-by: Jim Klimov --- tests/upsset-string-test.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index fea3b755fe..27542f3f0b 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -26,7 +26,9 @@ def request(binary, metadata, value='original', new_value=None): server.bind(('127.0.0.1', 0)) server.listen(1) server.settimeout(default_timeout) - target = 'dummy@127.0.0.1:' + str(server.getsockname()[1]) + server_port_str = str(server.getsockname()[1]) + assert server_port_str is not None and server_port_str != "" and server_port_str != "None", "Server port is None or empty" + target = 'dummy@127.0.0.1:' + server_port_str config = Path(directory) (config / 'upsset.conf').write_text('I_HAVE_SECURED_MY_CGI_DIRECTORY\n') (config / 'hosts.conf').write_text('MONITOR ' + target + ' "Test UPS"\n') @@ -82,11 +84,30 @@ def serve(): NUT_CONFPATH=directory, CONTENT_LENGTH=str(len(body)), REQUEST_METHOD='POST', - CONTENT_TYPE='application/x-www-form-urlencoded' + CONTENT_TYPE='application/x-www-form-urlencoded', + # Provide a minimal but complete CGI context; some servers/frameworks + # always set these, and upsset.cgi/cgilib can rely on them in places. + GATEWAY_INTERFACE='CGI/1.1', + SERVER_PROTOCOL='HTTP/1.1', + SERVER_SOFTWARE='nut-test', + SERVER_NAME='localhost', + SERVER_PORT=server_port_str, + SCRIPT_NAME='/upsset.cgi', + REQUEST_URI='/upsset.cgi', + QUERY_STRING='', + REMOTE_ADDR='127.0.0.1', + HTTP_HOST='localhost' ) try: - result = subprocess.run([binary], input=body, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=environment, timeout=default_timeout) + try: + result = subprocess.run([binary], input=body, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=environment, timeout=default_timeout) + except (FileNotFoundError, PermissionError, OSError) as e: + # Some build trees provide a libtool wrapper script (shell script) + # which may not be directly executable in certain environments. + # Retry explicitly via /bin/bash to ensure it launches. + result = subprocess.run(['/usr/bin/env', 'bash', binary], input=body, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=environment, timeout=default_timeout) finally: thread.join(timeout=default_timeout + 5) assert not thread.is_alive(), 'Server did not finish' From 3e73629d501d265d39ddacc56c569dbf54325175 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 20 Oct 2026 01:35:13 +0200 Subject: [PATCH 08/13] tests/upsset-string-test.py: honour NUT_DEBUG_LEVEL [#3668] Signed-off-by: Jim Klimov --- tests/upsset-string-test.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index 27542f3f0b..eca3969099 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -15,6 +15,9 @@ from urllib.parse import urlencode default_timeout = 30 +nut_debug_level_env = int(os.getenv('NUT_DEBUG_LEVEL', 0)) +# Might hack here to debug PY script independently of upsset.cgi binary +nut_debug_level = nut_debug_level_env def quote(value): return '"' + value.replace('\\', '\\\\').replace('"', '\\"').replace('#', '\\#') + '"' @@ -36,13 +39,22 @@ def request(binary, metadata, value='original', new_value=None): (config / 'nutauth.conf').chmod(0o600) def serve(): + """ Mock upsd """ try: + if nut_debug_level > 0: + print("Server awaiting connection on port", server_port_str) connection, unused = server.accept() + if nut_debug_level > 0: + print("Server accepted connection") with connection: + if nut_debug_level > 0: + print("Server using connection") connection.settimeout(default_timeout) with connection.makefile('rb') as stream: for raw in stream: command = raw.decode('ascii').rstrip('\r\n') + if nut_debug_level > 0: + print("Server got command:", command) commands.append(command) if command == 'STARTTLS': reply = 'ERR FEATURE-NOT-SUPPORTED' @@ -61,13 +73,20 @@ def serve(): reply = 'OK' elif command == 'LOGOUT': connection.sendall(b'OK Goodbye\n') + if nut_debug_level > 0: + print("Server ended connection") break else: raise AssertionError('Unexpected request: ' + command) connection.sendall((reply + '\n').encode('ascii')) + if nut_debug_level > 0: + print("Server thread ended without errors") except Exception as error: errors.append(error) + if nut_debug_level > 0: + print("Server thread ended with errors") + # Start mock upsd as a thread, get upsset.cgi to talk to it thread = threading.Thread(target=serve, daemon=True) thread.start() fields = {'username': 'test', 'password': 'test', 'monups': target, @@ -75,7 +94,7 @@ def serve(): if new_value is not None: fields['UPSVAR_test.string'] = new_value body = urlencode(fields).encode('ascii') + b'\n' - environment = {key: val for key, val in os.environ.items() if not key.startswith('NUT_')} + environment = {key: val for key, val in os.environ.items() if (key == 'NUT_DEBUG_LEVEL' or not key.startswith('NUT_'))} # Ensure CGI environment is sufficiently defined for predictable behavior # so upsset.cgi reliably parses POSTed form data and proceeds to contact # the mocked NUT server. Missing CONTENT_TYPE may cause it to ignore the @@ -114,7 +133,11 @@ def serve(): assert not errors, errors assert result.returncode == 0, (metadata, result.returncode, result.stderr.decode()) output = result.stdout.decode('ascii') - assert output.rstrip().endswith(''), output + if nut_debug_level_env > 0: + assert '' in output + assert output.rstrip().endswith(''), output + else: + assert output.rstrip().endswith(''), output assert 'Error:' not in output, output if new_value is None: assert 'GET TYPE dummy test.string' in commands, commands From c78b61f520a9bce5aefac92863a19d8034ea3a32 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 20 Oct 2026 01:35:55 +0200 Subject: [PATCH 09/13] tests/upsset-string-test.py: pass NUT_AUTHCONF_PATH to avoid confusion with testing user config [#3668] Signed-off-by: Jim Klimov --- tests/upsset-string-test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index eca3969099..8343eccde0 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -101,6 +101,7 @@ def serve(): # request body and skip any network activity, leading to a timeout here. environment.update( NUT_CONFPATH=directory, + NUT_AUTHCONF_PATH=directory, CONTENT_LENGTH=str(len(body)), REQUEST_METHOD='POST', CONTENT_TYPE='application/x-www-form-urlencoded', From 41a57267ee541f38f8619f129f63971eaf33333b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 20 Oct 2026 01:36:41 +0200 Subject: [PATCH 10/13] tests/upsset-string-test.py: use subprocess.Popen() for finer debugging [#3668] Signed-off-by: Jim Klimov --- tests/upsset-string-test.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index 8343eccde0..4132bd0550 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -120,20 +120,44 @@ def serve(): ) try: try: - result = subprocess.run([binary], input=body, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=environment, timeout=default_timeout) + p = subprocess.Popen([binary], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=environment) except (FileNotFoundError, PermissionError, OSError) as e: # Some build trees provide a libtool wrapper script (shell script) # which may not be directly executable in certain environments. # Retry explicitly via /bin/bash to ensure it launches. - result = subprocess.run(['/usr/bin/env', 'bash', binary], input=body, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=environment, timeout=default_timeout) + p = subprocess.Popen(['/usr/bin/env', 'bash', binary], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=environment) + + if nut_debug_level > 0: + print('Client PID', p.pid) + try: + if nut_debug_level > 1: + print("Client INPUT:") + print(str(body)) + out, err = p.communicate(input=body, timeout=default_timeout) + # err = p.stderr.decode() + if nut_debug_level > 1: + print("Client OUT:") + print(out.decode()) + print("ERR:") + print(err.decode()) + finally: + rc = p.returncode + if nut_debug_level > 0: + print('Client finished', rc) finally: thread.join(timeout=default_timeout + 5) assert not thread.is_alive(), 'Server did not finish' assert not errors, errors - assert result.returncode == 0, (metadata, result.returncode, result.stderr.decode()) - output = result.stdout.decode('ascii') + assert p.returncode == 0, (metadata, p.returncode, err.decode()) + output = out.decode('ascii') if nut_debug_level_env > 0: assert '' in output assert output.rstrip().endswith(''), output From 2cca7428f4a75d122e97bc5b43f9f46873904682 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 20 Oct 2026 02:15:31 +0200 Subject: [PATCH 11/13] clients/upsset.c: savesettings(): init desc=NULL to avoid bogus free() [#3668] Signed-off-by: Jim Klimov --- clients/upsset.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/upsset.c b/clients/upsset.c index 81ad260088..606ddfb212 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1032,7 +1032,7 @@ static void savesettings(void) static void savesettings(void) { int changed = 0; - char *desc; + char *desc = NULL; uvtype_t *upsvar; if (!checkhost(monups, &desc)) From 0bc848ad65effa2f38a53fd0f90863ba150b90a5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 20 Oct 2026 16:31:46 +0000 Subject: [PATCH 12/13] docs/man/Makefile.am: recover (once) from broken man page builds on freebsd with parallel builds Signed-off-by: Jim Klimov --- docs/man/Makefile.am | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 8019576ec8..4d3976dd02 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -2124,9 +2124,14 @@ DOCBUILD_END = { \ for F in $(LINKMAN_INCLUDE_GENERATED) ; do \ rm -f "./$${A2X_OUTDIR}/$$F" || true ; \ done ; \ - mv -f "./$${A2X_OUTDIR}/$(@F)" ./ || exit ; \ - mv -f "./$${A2X_OUTDIR}/"*.* ./ 2>/dev/null || true ; \ - rm -rf "./$${A2X_OUTDIR}" ; \ + RESMV=0; \ + mv -f "./$${A2X_OUTDIR}/$(@F)" ./ || RESMV=$$? ; \ + if [ 0 = $$RESMV ] ; then \ + mv -f "./$${A2X_OUTDIR}/"*.* ./ 2>/dev/null || true ; \ + rm -rf "./$${A2X_OUTDIR}" ; \ + else \ + RES=$$RESMV ; \ + fi ; \ fi ; \ } @@ -2134,7 +2139,20 @@ DOCBUILD_MAN = { \ echo " DOC-MAN Generating $@"; \ $(DOCBUILD_BEGIN) ; RES=0; \ $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ - $(DOCBUILD_END) ; exit $$RES ; \ + $(DOCBUILD_END) ; \ + if [ 0 = "$${RES}" ] ; then exit 0 ; fi ; \ + case "-$(MAKEFLAGS) $(AM_MAKEFLAGS)" in \ + *-j|*-j" "*|*-{j,l}{0,1,2,3,4,5,6,7,8,9}*|*-[jl][0123456789]*|*{-l,--jobs,--load-average,--max-load}" "{-,0,1,2,3,4,5,6,7,8,9}*|*--jobserver*|*--jobs" "[0123456789]*|*--load-average" "[0123456789]*|*--max-load" "[0123456789]*) \ + case "`uname -s | tr 'A-Z' 'a-z'`" in \ + *freebsd*) sleep 1 ; \ + echo " DOC-MAN RETRY: Generating $@"; \ + $(DOCBUILD_BEGIN) ; RES=0; \ + $(A2X) $(A2X_MANPAGE_OPTS) '$<' || RES=$$? ; \ + $(DOCBUILD_END) ; \ + ;; \ + esac ;; \ + esac ; \ + exit $$RES ; \ } ### Call the prep step consistently to create symlinks (out-of-tree) From fc1442be02af39ff12e57160151edea6acc327f7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 21 Oct 2026 09:33:58 +0200 Subject: [PATCH 13/13] .github/scripts/appveyor/02-install.bat: fix cmd+bash syntax [#3786, #3790] Signed-off-by: nmbro Signed-off-by: Jim Klimov Co-authored-by: Nicolai Brogaard --- .github/scripts/appveyor/02-install.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/appveyor/02-install.bat b/.github/scripts/appveyor/02-install.bat index 124cd9c7b8..fb57758bb2 100644 --- a/.github/scripts/appveyor/02-install.bat +++ b/.github/scripts/appveyor/02-install.bat @@ -45,5 +45,5 @@ REM Preserve the current working directory: set CHERE_INVOKING=yes REM Start a 64 bit Mingw environment: set MSYSTEM=MINGW64 -C:\msys64\usr\bin\bash -lc 'PATH="/mingw64/bin:$PATH" ; export PATH ; pwd ; ccache -sv || echo "SKIP: Could not query ccache stats" ; ccache -o sloppiness=file_macro || true ; ccache -o compression=true || true ; if [ x"${CCACHE_MAXSIZE}" = x ] ; then ccache -o max_size=750M ; fi || true ' +C:\msys64\usr\bin\bash -lc 'PATH="/mingw64/bin:$PATH" ; export PATH ; pwd ; if ! ccache -sv ; then echo "SKIP: Could not query ccache stats" ; fi ; ccache -o compression=true ; echo "ccache settings: max_size=`ccache -k max_size` compression=`ccache -k compression`" ; true ' if errorlevel 1 exit /b %ERRORLEVEL%