Skip to content

[Bug] WriteConsoleW output cannot be captured via pipe or file redirection on Windows #1115

Description

@Puiching-Memory

Git commit

bda7fab

Operating System & Version

windows 11 25H2 (Build 26220)

GGML backends

CUDA

Command-line arguments used

powershell .\sd-cli.exe --diffusion-model nonexistent_model.gguf --prompt test --output test.png > output.log 2>&1
Or using Out-File:
powershell .\sd-cli.exe --diffusion-model nonexistent_model.gguf --prompt test --output test.png *> output.log

Steps to reproduce

  1. Run sd-cli.exe with output redirected to a file using PowerShell redirection (>, *>, or Out-File)
  2. Check the captured output file
  3. Compare with direct console output (without redirection)

What you expected to happen

When redirecting output to a file, all log messages should be captured, including both log level tags ([INFO], [WARN], [ERROR]) and the actual log content (error messages, status information, etc.), just like when running directly in the console.

What actually happened

When redirecting output to a file, only log level tags are captured, while the actual log content is lost.

Captured output when redirected (only tags, no content):

[INFO ] [INFO ] [INFO ] [INFO ] [INFO ] [WARN ] [WARN ] [INFO ]
[ERROR]

Output when run directly in console (complete with content):

[INFO ] ggml_extend.hpp:77   - ggml_cuda_init: GGML_CUDA_FORCE_MMQ:    no
[INFO ] ggml_extend.hpp:77   - ggml_cuda_init: GGML_CUDA_FORCE_CUBLAS: no
[INFO ] ggml_extend.hpp:77   - ggml_cuda_init: found 1 CUDA devices:
[INFO ] ggml_extend.hpp:77   -   Device 0: NVIDIA GeForce RTX 4060 Ti, compute capability 8.9, VMM: yes
[INFO ] stable-diffusion.cpp:233  - loading diffusion model from 'nonexistent_model.gguf'
[WARN ] model.cpp:379  - unknown format nonexistent_model.gguf
[WARN ] stable-diffusion.cpp:235  - loading diffusion model from 'nonexistent_model.gguf' failed
[ERROR] stable-diffusion.cpp:304  - get sd version from file failed: ''
[INFO ] main.cpp:568  - new_sd_ctx_t failed

Comparison:

  • Console output: Complete log messages with actual content
  • Redirected output: Only log level tags, actual content is missing

Logs / error messages / stack trace

This is a regression introduced in #1101 (commit ebe9d26, Dec 16, 2025).

Problem Analysis:

  1. Background:

    • PR feat: supports correct UTF-8 printing on windows #1101 ("feat: supports correct UTF-8 printing on windows") added the print_utf8() function to fix UTF-8 character encoding issues on Windows
    • Before this PR: Used fprintf(). Output could be captured via redirection, but UTF-8 characters displayed incorrectly.
    • After this PR: Uses print_utf8() which calls WriteConsoleW(). UTF-8 characters display correctly, but output cannot be captured via redirection (regression).
  2. Root Cause:

    • The call chain: stable-diffusion.cpp (LOG_INFO/LOG_ERROR) calls util.cpp (log_printf), which calls sd_log_cb (CLI callback), which calls log_print, which calls print_utf8.
      • util.cpp:278 (log_printf function):
        void log_printf(sd_log_level_t level, const char* file, int line, const char* format, ...) {
        va_list args;
        va_start(args, format);
        static char log_buffer[LOG_BUFFER_SIZE + 1];
        int written = snprintf(log_buffer, LOG_BUFFER_SIZE, "%s:%-4d - ", sd_basename(file).c_str(), line);
        if (written >= 0 && written < LOG_BUFFER_SIZE) {
        vsnprintf(log_buffer + written, LOG_BUFFER_SIZE - written, format, args);
        }
        size_t len = strlen(log_buffer);
        if (log_buffer[len - 1] != '\n') {
        strncat(log_buffer, "\n", LOG_BUFFER_SIZE - len);
        }
        if (sd_log_cb) {
        sd_log_cb(level, log_buffer, sd_log_cb_data);
        }
        va_end(args);
        }
      • examples/cli/main.cpp:274 (sd_log_cb callback):
        void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) {
        SDCliParams* cli_params = (SDCliParams*)data;
        log_print(level, log, cli_params->verbose, cli_params->color);
        }
    • The print_utf8() function in examples/common/common.hpp unconditionally uses WriteConsoleW:
      static void print_utf8(FILE* stream, const char* utf8) {
      if (!utf8)
      return;
      #ifdef _WIN32
      HANDLE h = (stream == stderr)
      ? GetStdHandle(STD_ERROR_HANDLE)
      : GetStdHandle(STD_OUTPUT_HANDLE);
      int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
      if (wlen <= 0)
      return;
      wchar_t* wbuf = (wchar_t*)malloc(wlen * sizeof(wchar_t));
      MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wbuf, wlen);
      DWORD written;
      WriteConsoleW(h, wbuf, wlen - 1, &written, NULL);
      free(wbuf);
      #else
      fputs(utf8, stream);
      #endif
      }
    • The log_print() function splits output into two parts:
      static void log_print(enum sd_log_level_t level, const char* log, bool verbose, bool color) {
      int tag_color;
      const char* level_str;
      FILE* out_stream = (level == SD_LOG_ERROR) ? stderr : stdout;
      if (!log || (!verbose && level <= SD_LOG_DEBUG)) {
      return;
      }
      switch (level) {
      case SD_LOG_DEBUG:
      tag_color = 37;
      level_str = "DEBUG";
      break;
      case SD_LOG_INFO:
      tag_color = 34;
      level_str = "INFO";
      break;
      case SD_LOG_WARN:
      tag_color = 35;
      level_str = "WARN";
      break;
      case SD_LOG_ERROR:
      tag_color = 31;
      level_str = "ERROR";
      break;
      default: /* Potential future-proofing */
      tag_color = 33;
      level_str = "?????";
      break;
      }
      if (color) {
      fprintf(out_stream, "\033[%d;1m[%-5s]\033[0m ", tag_color, level_str);
      } else {
      fprintf(out_stream, "[%-5s] ", level_str);
      }
      print_utf8(out_stream, log);
      fflush(out_stream);
      }
      • Part 1: Log tags ([INFO], [ERROR], etc.) are written via fprintf(). Can be captured because it uses stdout/stderr streams.
      • Part 2: Log content (actual messages) are written via print_utf8() which calls WriteConsoleW(). Cannot be captured because it bypasses stdout/stderr and writes directly to console.
    • Why tags work but content doesn't:
      • Log tags ([INFO], [ERROR], etc.) are written via fprintf(out_stream, "[%-5s] ", level_str):
        fprintf(out_stream, "[%-5s] ", level_str);
        • fprintf() writes to the FILE* stream (stdout/stderr), which can be redirected. This is a standard C library function that respects stream redirection.
      • Log content (actual messages) are written via print_utf8(out_stream, log):
        print_utf8(out_stream, log);
        • Which calls WriteConsoleW():
          WriteConsoleW(h, wbuf, wlen - 1, &written, NULL);
        • WriteConsoleW() writes directly to the console buffer, bypassing stdout/stderr streams.
        • When stdout/stderr are redirected, WriteConsoleW still writes to the console, not to the redirected destination.
      • Result: Only the log level tags (written via fprintf) are captured, but the actual content (written via WriteConsoleW) is lost.

Additional context / environment details

Impact

  • Regression: Output that was previously capturable (via fprintf) is now lost
  • Affects: All applications that need to capture sd-cli.exe output:
    • Electron applications
    • CI/CD scripts
    • Log collection tools
    • Automated testing frameworks

References

Microsoft Documentation:

Related Discussions:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions