From da80f03f9e5e4f3327d095054adcde2b30fbe1d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 15:41:00 +0000 Subject: [PATCH] Write vdprintf's output without a stdio stream on Windows The Windows vdprintf opened a stream over the descriptor with _fdopen on every call and never closed it, since closing the stream closes the descriptor. The C runtime holds a fixed number of streams per process, about 500, and a CFL header is a few dozen of these calls, so a process that writes headers for a few dozen files -- a program running BART's commands in-process -- ran out: the next header failed with "Unable to open file." inside create_cfl's critical section. The output is now formatted into memory and written with _write, and the count returned is the bytes written, as write_stream_header needs to size a stream's header. test_io writes one header and compares its length with the file, and writes two hundred into the null device. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FavM1ptGCKq51oher8tVd9 --- Makefile | 2 +- src/win/vdprintf.h | 54 +++++++++++++++++++++++++++++------ utests/test_io.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 utests/test_io.c diff --git a/Makefile b/Makefile index 2f5a07af6..b16d3b376 100644 --- a/Makefile +++ b/Makefile @@ -907,7 +907,7 @@ UTARGETS += test_batchsvd MODULES_test_batchsvd = -llowrank # lib misc -UTARGETS += test_pattern test_types test_misc test_memcfl test_tree test_streams +UTARGETS += test_pattern test_types test_misc test_memcfl test_tree test_streams test_io # lib moba UTARGETS += test_moba diff --git a/src/win/vdprintf.h b/src/win/vdprintf.h index 6a52e6594..f2ddccff4 100644 --- a/src/win/vdprintf.h +++ b/src/win/vdprintf.h @@ -9,21 +9,57 @@ #ifndef _VDPRINTF_WINDOWS #define _VDPRINTF_WINDOWS +#include #include -#include "misc/misc.h" +#include +#include int vdprintf(int, const char*, va_list); +// Formats into memory and writes the bytes to the descriptor, as POSIX +// vdprintf does. A stdio stream opened over the descriptor for each call +// could not be closed without closing the descriptor, and the C runtime +// holds a fixed number of streams per process. int vdprintf(int fd, const char *format, va_list ap) { - FILE* stream = _fdopen(fd, "a"); - if (stream == NULL) - error("Unable to open file.\n"); - int err = vfprintf(stream, format, ap); - if (err == -1) - error("Unable to write to file.\n"); - fflush(stream); - return err; + char small[256]; + char* buf = small; + + va_list aq; + va_copy(aq, ap); + int len = vsnprintf(small, sizeof small, format, aq); + va_end(aq); + + if (len < 0) + return -1; + + if ((size_t)len >= sizeof small) { + + if (NULL == (buf = malloc((size_t)len + 1))) + return -1; + + vsnprintf(buf, (size_t)len + 1, format, ap); + } + + int done = 0; + + while (done < len) { + + int n = _write(fd, buf + done, (unsigned int)(len - done)); + + if (n <= 0) { + + done = -1; + break; + } + + done += n; + } + + if (small != buf) + free(buf); + + return done; } #endif /* _VDPRINTF_WINDOWS */ diff --git a/utests/test_io.c b/utests/test_io.c new file mode 100644 index 000000000..13291de11 --- /dev/null +++ b/utests/test_io.c @@ -0,0 +1,71 @@ +/* Copyright 2026. Pulserver contributors. + * All rights reserved. Use of this source code is governed by + * a BSD-style license which can be found in the LICENSE file. + * + * Headers written through the descriptor-level printf, many of them in one + * process, as a program that runs BART's commands in-process writes them. + */ + +#include +#include +#include +#include + +#include "misc/misc.h" +#include "misc/io.h" + +#ifdef _WIN32 +#include "win/open_patch.h" +#endif + +#include "utest.h" + + +static bool test_cfl_header_length_is_its_bytes(void) +{ + const char* name = "test_io_header.hdr"; + bart_dim_t dims[16] = { 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5 }; + + int fd = open(name, O_RDWR|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR); + + UT_RETURN_ON_FAILURE(-1 != fd); + + int written = write_cfl_header(fd, NULL, 16, dims); + + off_t size = lseek(fd, 0, SEEK_END); + + close(fd); + unlink(name); + + UT_RETURN_ASSERT((0 < written) && (size == written)); +} + +UT_REGISTER_TEST(test_cfl_header_length_is_its_bytes); + + +static bool test_many_cfl_headers_in_one_process(void) +{ +#ifdef _WIN32 + const char* null_file = "nul"; +#else + const char* null_file = "/dev/null"; +#endif + bart_dim_t dims[16] = { 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5 }; + + int fd = open(null_file, O_WRONLY); + + UT_RETURN_ON_FAILURE(-1 != fd); + + // Each header is a few dozen formatted writes, so this is more writes + // than a C runtime holds streams. + bool ok = true; + + for (int i = 0; ok && (i < 200); i++) + ok = (0 < write_cfl_header(fd, NULL, 16, dims)); + + close(fd); + + UT_RETURN_ASSERT(ok); +} + +UT_REGISTER_TEST(test_many_cfl_headers_in_one_process);