Win32 GUI C(++) 应用程序将 stdout 和 stderr 重定向到磁盘上的同一个文件
Win32 GUI C(++) app redirect both stdout and stderr to the same file on disk
我正在创建一个 Windows 服务,它不能有关联的控制台。因此我想将 stdout 和 stderr 重定向到一个(相同的)文件。以下是我目前的发现:
- 在 C++ 中重定向
cout
和 cerr
可以通过 changing the buffers 完成,但这不会像 puts
或 [=70] 那样影响 C I/O =] I/O句柄。
- 因此我们可以使用
freopen
to reopen stdout or stderr as a file like here,但是我们不能指定同一个文件两次。
- 为了仍然对两者使用相同的文件,我们可以使用
dup2
like here. 将 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
或否则一切都会搞砸。您可能还需要以 MSVC 理解的编码保存 cpp 文件,例如 UTF-8 with 签名。)
但现在回到最初的问题:将此作为没有控制台的服务来执行,或者,等效但更易于调试的 GUI 应用程序 (/SUBSYSTEM:WINDOWS
)。
问题是在这种情况下 dup2
失败 因为 fileno(stderr)
不是有效的文件描述符,因为应用程序最初没有关联的流。如前所述 ,在本例中为 fileno(stderr) == -2
。
请注意,当我们第一次使用 freopen
打开 stderr 作为另一个文件时,一切正常,但我们创建了一个虚拟的空文件。
所以现在我的问题是:在最初没有流的应用程序中将 stdout 和 stderr 重定向到同一个文件的最佳方法是什么?
回顾一下:问题是 when stdout
or stderr
is not associated with an output stream, fileno
returns -2,所以我们不能将它传递给 dup2
。
(我不想更改用于实际打印的代码,因为这可能意味着外部函数产生的某些输出不会被重定向。)
这是一个程序示例,它创建一个用于写入的文件,然后使用 CreateProcess
并将 stdout
和 stderr
设置为 HANDLE
的进程创建的文件。这个例子只是从一个伪参数开始,让它向 stdout
和 stderr
写入很多东西,这些东西将被写入 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
的文件)。
我正在创建一个 Windows 服务,它不能有关联的控制台。因此我想将 stdout 和 stderr 重定向到一个(相同的)文件。以下是我目前的发现:
- 在 C++ 中重定向
cout
和cerr
可以通过 changing the buffers 完成,但这不会像puts
或 [=70] 那样影响 C I/O =] I/O句柄。 - 因此我们可以使用
freopen
to reopen stdout or stderr as a file like here,但是我们不能指定同一个文件两次。 - 为了仍然对两者使用相同的文件,我们可以使用
dup2
like here. 将 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
或否则一切都会搞砸。您可能还需要以 MSVC 理解的编码保存 cpp 文件,例如 UTF-8 with 签名。)
但现在回到最初的问题:将此作为没有控制台的服务来执行,或者,等效但更易于调试的 GUI 应用程序 (/SUBSYSTEM:WINDOWS
)。
问题是在这种情况下 dup2
失败 因为 fileno(stderr)
不是有效的文件描述符,因为应用程序最初没有关联的流。如前所述 fileno(stderr) == -2
。
请注意,当我们第一次使用 freopen
打开 stderr 作为另一个文件时,一切正常,但我们创建了一个虚拟的空文件。
所以现在我的问题是:在最初没有流的应用程序中将 stdout 和 stderr 重定向到同一个文件的最佳方法是什么?
回顾一下:问题是 when stdout
or stderr
is not associated with an output stream, fileno
returns -2,所以我们不能将它传递给 dup2
。
(我不想更改用于实际打印的代码,因为这可能意味着外部函数产生的某些输出不会被重定向。)
这是一个程序示例,它创建一个用于写入的文件,然后使用 CreateProcess
并将 stdout
和 stderr
设置为 HANDLE
的进程创建的文件。这个例子只是从一个伪参数开始,让它向 stdout
和 stderr
写入很多东西,这些东西将被写入 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
的文件)。