繁体   English   中英

Win32 GUI C(++) 应用程序将 stdout 和 stderr 重定向到磁盘上的同一个文件

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

我正在创建一个不能有关联控制台的 Windows 服务。 因此,我想将 stdout 和 stderr 重定向到(相同的)文件。 这是我迄今为止发现的:

  1. 在 C++ 中重定向coutcerr可以通过更改缓冲区来完成,但这不会像puts或 Windows I/O 句柄那样影响 CI/O。
  2. 因此,我们可以使用freopen将 stdout 或 stderr 作为文件重新打开,但我们不能两次指定同一个文件。
  3. 为了仍然对两者使用相同的文件,我们可以使用dup2将 stderr 重定向到 stdout,就像这里一样。

到目前为止一切顺利,当我们使用/SUBSYSTEM:CONSOLE (项目属性 → 链接器 → 系统)运行此代码时,一切正常:

#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; }

这很好地将以下文本写入log.log

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

当然我们想要表情符号,所以我们需要某种 unicode。在这种情况下,我们使用宽字符,这意味着我们需要使用setmode否则一切都会搞砸。您可能还需要将 cpp 文件保存为MSVC 可以理解,例如带有签名的 UTF-8。)

但是现在回到最初的问题:将其作为没有控制台的服务来执行,或者,等效但更易于调试的 GUI 应用程序 ( /SUBSYSTEM:WINDOWS )。 问题是在这种情况下dup2失败,因为fileno(stderr)不是有效的文件描述符,因为应用程序最初没有关联的流。 如此所述,在这种情况下, fileno(stderr) == -2

请注意,当我们第一次使用freopen打开 stderr 作为另一个文件时,一切正常,但我们创建了一个虚拟的空文件。

所以现在我的问题是:将 stdout 和 stderr 重定向到最初没有流的应用程序中的同一文件的最佳方法是什么?

回顾一下:问题是stdoutstderr没有与输出流关联时, fileno返回 -2 ,所以我们不能将它传递给dup2

(我不想更改用于实际打印的代码,因为这可能意味着外部函数产生的某些输出不会被重定向。)

这是一个程序示例,该程序创建一个用于写入的文件,然后使用CreateProcess并将该进程的stdoutstderr设置为所创建文件的HANDLE 这个例子只是从一个虚拟参数开始,让它向stdoutstderr写入很多东西,这些东西将被写入output.txt

// RedirectStd.cpp

#include <iostream>
#include <string_view>
#include <vector>

#include <Windows.h>

struct SecAttrs_t : public SECURITY_ATTRIBUTES {
    SecAttrs_t() : SECURITY_ATTRIBUTES{ 0 } {
        nLength = sizeof(SECURITY_ATTRIBUTES);
        bInheritHandle = TRUE;
    }
    operator SECURITY_ATTRIBUTES* () { return this;  }
};

struct StartupInfo_t : public STARTUPINFO {
    StartupInfo_t(HANDLE output) : STARTUPINFO{ 0 } {
        cb = sizeof(STARTUPINFO);
        dwFlags = STARTF_USESTDHANDLES;
        hStdOutput = output;
        hStdError = output;
    }
    operator STARTUPINFO* () { return this; }
};

int cppmain(const std::string_view program, std::vector<std::string_view> args) {
    if (args.size() == 0) {
        // no arguments, create a file and start a new process
        SecAttrs_t sa;

        HANDLE hFile = CreateFile(L"output.txt",
            GENERIC_WRITE,
            FILE_SHARE_READ,
            sa, // lpSecurityAttributes
            CREATE_ALWAYS, // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes
            NULL // dwFlagsAndAttributesparameter
        );
        if (hFile == INVALID_HANDLE_VALUE) return 1;

        StartupInfo_t su(hFile); // set output handles to hFile
        PROCESS_INFORMATION pi;

        std::wstring commandline = L"RedirectStd.exe dummy";
        BOOL bCreated = CreateProcess(
            NULL,
            commandline.data(),
            NULL, // lpProcessAttributes
            NULL, // lpThreadAttributes
            TRUE, // bInheritHandles
            0, // dwCreationFlags
            NULL, // lpEnvironment
            NULL, // lpCurrentDirectory
            su, // lpStartupInfo
            &pi
        );
        if (bCreated == 0) return 2;
        CloseHandle(pi.hThread); // no need for this

        WaitForSingleObject(pi.hProcess, INFINITE); // wait for the process to finish         
        CloseHandle(pi.hProcess);
        CloseHandle(hFile);
    }
    else {
        // called with an argument, output stuff to stdout and stderr 
        for (int i = 0; i < 1024; ++i) {
            std::cout << "stdout\n";
            std::cerr << "stderr\n";
        }
    }

    return 0;
}

int main(int argc, char* argv[]) {
    return cppmain(argv[0], { argv + 1, argv + argc });
}

我找到了一个有效的解决方案,并且不会创建临时文件log2.log 我们可以打开NUL (Windows 的/dev/null )而不是这个文件,因此代码变为:

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();

这确保_fileno(stderr)不再是-2 ,所以我们可以使用dup2

可能有一个更优雅的解决方案(不确定),但这有效并且不会创建一个虚拟的空文件(也不是一个名为NUL的文件)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM