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:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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