diff --git a/.github/scripts/appveyor/02-install.bat b/.github/scripts/appveyor/02-install.bat index 6914e4715a..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 ; ccache -o max_size=750M || 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% 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" 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)) diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index e643b88eeb..4d3976dd02 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) @@ -2113,12 +2124,37 @@ 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 ; \ } +DOCBUILD_MAN = { \ + echo " DOC-MAN Generating $@"; \ + $(DOCBUILD_BEGIN) ; RES=0; \ + $(A2X) $(A2X_MANPAGE_OPTS) '$<' || 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) ### or just touch-files for peace of mind (in-tree builds). Then we ### use these path names (truncated "-prepped") now surely located @@ -2188,38 +2224,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 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]); diff --git a/tests/upsset-string-test.py b/tests/upsset-string-test.py index 6dae1e7659..4132bd0550 100644 --- a/tests/upsset-string-test.py +++ b/tests/upsset-string-test.py @@ -14,6 +14,10 @@ import threading 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('#', '\\#') + '"' @@ -24,8 +28,10 @@ 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) - target = 'dummy@127.0.0.1:' + str(server.getsockname()[1]) + server.settimeout(default_timeout) + 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') @@ -33,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: - connection.settimeout(10) + 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' @@ -58,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, @@ -72,19 +94,75 @@ 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.update(NUT_CONFPATH=directory, CONTENT_LENGTH=str(len(body)), - REQUEST_METHOD='POST') + 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 + # 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', + # 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=10) + try: + 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. + 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=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()) - output = result.stdout.decode('ascii') - assert output.rstrip().endswith(''), output + 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 + 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