c++cwindowsstdoutdup2

Win32 GUI C(++) app redirect both stdout and stderr to the same file on disk


I'm creating a Windows service, which cannot have an associated console. Therefore I want to redirect stdout and stderr to a (the same) file. Here is what I discovered so far:

  1. Redirecting cout and cerr in C++ can be done by changing the buffers, but this does not affect C I/O like puts or Windows I/O handles.
  2. Hence we can use freopen to reopen stdout or stderr as a file like here, but we cannot specify the same file twice.
  3. To still use the same file for both we can redirect stderr to stdout using dup2 like here.

So far so good, and when we run this code with /SUBSYSTEM:CONSOLE (project properties → Linker → System) everything works fine:

#include <Windows.h>
#include <io.h>
#include <fcntl.h>
#include <cstdio>
#include <iostream>

void doit()
{
    FILE *stream;
    if (_wfreopen_s(&stream, L"log.log", L"w", stdout)) __debugbreak();
    // Also works as service when uncommenting this line: if (_wfreopen_s(&stream, L"log2.log", L"w", stderr)) __debugbreak();
    if (_dup2(_fileno(stdout), _fileno(stderr)))
    {
        const auto err /*EBADF if service; hover over in debugger*/ = errno;
        __debugbreak();
    }

    // Seemingly can be left out for console applications
    if (!SetStdHandle(STD_OUTPUT_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(stdout))))) __debugbreak();
    if (!SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(stderr))))) __debugbreak();

    if (_setmode(_fileno(stdout), _O_WTEXT) == -1) __debugbreak();
    if (_setmode(_fileno(stderr), _O_WTEXT) == -1) __debugbreak();

    std::wcout << L"1☺a" << std::endl;
    std::wcerr << L"1☺b" << std::endl;

    _putws(L"2☺a");
    fflush(stdout);
    fputws(L"2☺b\n", stderr);
    fflush(stderr);

    const std::wstring a3(L"3☺a\n"), b3(L"3☺b\n");
    if (!WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), a3.c_str(), a3.size() * sizeof(wchar_t), nullptr, nullptr))
        __debugbreak();
    if (!WriteFile(GetStdHandle(STD_ERROR_HANDLE), b3.c_str(), b3.size() * sizeof(wchar_t), nullptr, nullptr))
        __debugbreak();
}

int        main() { doit(); }
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { return doit(), 0; }

This nicely writes the following text to log.log:

1☺a
1☺b
2☺a
2☺b
3☺a
3☺b

(Of course we want emoji, so we need some sort of unicode. In this case we use wide characters, which means we need to use setmode or else everything will mess up. You may also need to save the cpp file in an encoding that MSVC understands, e.g. UTF-8 with signature.)

But now back to the original problem: doing this as a service without console, or, equivalent but easier to debug, a GUI app (/SUBSYSTEM:WINDOWS). The problem is that in this case dup2 fails because fileno(stderr) is not a valid file descriptor, because the app initially has no associated streams. As mentioned here, fileno(stderr) == -2 in this case.

Note that when we first open stderr as another file using freopen, everything works fine, but we created a dummy empty file.

So now my question is: what is the best way to redirect both stdout and stderr to the same file in an application which initially has no streams?

Just to recap: the problem is that when stdout or stderr is not associated with an output stream, fileno returns -2, so we cannot pass it to dup2.

(I do not want to change the code used for the actual printing, because that might mean that some output produced by external functions will not be redirected.)


Solution

  • I found a solution that works and does not create a temporary file log2.log. Instead of this file, we can open NUL (Windows's /dev/null), so the code becomes:

    FILE *stream;
    if (_wfreopen_s(&stream, L"log.log", L"w", stdout)) __debugbreak();
    if (freopen_s(&stream, "NUL", "w", stderr)) __debugbreak();
    if (_dup2(_fileno(stdout), _fileno(stderr))) __debugbreak();
    
    if (!SetStdHandle(STD_OUTPUT_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(stdout))))) __debugbreak();
    if (!SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(stderr))))) __debugbreak();
    
    if (_setmode(_fileno(stdout), _O_WTEXT) == -1) __debugbreak();
    if (_setmode(_fileno(stderr), _O_WTEXT) == -1) __debugbreak();
    

    This makes sure that _fileno(stderr) is not -2 anymore so we can use dup2.

    There might be a more elegant solution (not sure), but this works and does not create a dummy empty file (also not one named NUL).