【发布时间】:2023-01-15 11:08:09
【问题描述】:
我正在制作一个测试程序,它打开一个控制台应用程序并读取它的标准输入写入它的标准输出,但是管道有问题。我正在使用命名管道,因为我可能必须运行这个线程化的甚至打开多个可执行文件才能同时进行通信。这些将需要保持运行并不断接受输入并发出输出,就像在控制台计算器中一样,它会询问您是否要进行另一次计算或在每次计算后退出。
使用错误检查我发现管道已成功创建,我将它们应用于 startupInfo 结构并成功打开可执行文件。此处请注意,如果我在调用 createProcess 之后在 visual studio 中设置断点,则子进程会出现在我的任务管理器中,检查 STILL_ACTIVE 为真并且在管道处达到峰值显示一个空管道。如果没有设置断点,那么我就看不到它,并且检查 STILL_ACTIVE 是错误的。
为了简化问题,我回到了基础,一个简单的 c++ 中的 hello world 可执行文件。计算器将是下一个测试。这会将 hello world 打印到控制台,并通过 cin:get() 等待按下回车键。我用测试仪运行它并尝试从子进程中读取“Hello World”。我什么也得不到。
最终项目将是开源的,我不希望用户必须下载任何其他库来编译项目,而 Boost::Process 实际上需要 2 个安装,因为过程还不是标准的。
我知道我很接近,这是我的简单测试器,作为一个文件,其中提取了进程类以内联在 main 中。注意:我在我的编译器中启用了 c++20。
// Tester.cpp
#include <string>
#include <string_view>
#include <vector>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <io.h>
#include <fcntl.h>
#include <windows.h>
int main()
{
std::string data = "";
int id = 1;
std::string executable = "HelloWorld.exe";
if (_access((executable).c_str(), 0) != -1)
{
std::cerr << "Error: Executable file not found: " << executable << std::endl;
exit(0);
}
SECURITY_ATTRIBUTES saAttr{};
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
//Pipe names
std::wstring pipeErr = L"\\\\.\\pipe\\err_" + std::to_wstring(id);
std::wstring pipeOut = L"\\\\.\\pipe\\out_" + std::to_wstring(id);
std::wstring pipeIn = L"\\\\.\\pipe\\in_" + std::to_wstring(id);
// The Child error pipe for reading
CreateNamedPipeW(pipeErr.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 1024, 1024, 0, NULL);
HANDLE err_pipe = CreateFileW(pipeErr.c_str(), GENERIC_READ | GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
// The Child out pipe for reading
CreateNamedPipeW(pipeOut.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 1024, 1024, 0, NULL);
HANDLE out_pipe = CreateFileW(pipeOut.c_str(), GENERIC_READ | GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
// The Child in pipe for writing
CreateNamedPipeW(pipeIn.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 1024, 1024, 0, NULL);
HANDLE in_pipe = CreateFileW(pipeIn.c_str(), GENERIC_READ | GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
if (in_pipe == INVALID_HANDLE_VALUE || out_pipe == INVALID_HANDLE_VALUE || err_pipe == INVALID_HANDLE_VALUE)
{
std::cout << "Error Creating Handles, Code: " << GetLastError() << std::endl;
return 0;
}
// Make sure the handles' inheritance is set correctly
if (!SetHandleInformation(in_pipe, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) ||
!SetHandleInformation(out_pipe, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) ||
!SetHandleInformation(err_pipe, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT))
{
std::cerr << "Error: Failed to set handle information for the child process" << std::endl;
return 0;
}
// Set up the startup info struct
STARTUPINFOA startupInfo;
memset(&startupInfo, 0, sizeof(startupInfo));
startupInfo.cb = sizeof(STARTUPINFOA);
startupInfo.hStdInput = in_pipe;
startupInfo.hStdOutput = out_pipe;
startupInfo.hStdError = err_pipe;
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
// Set up the process info struct
PROCESS_INFORMATION processInfo;
memset(&processInfo, 0, sizeof(processInfo));
// Create the child process
if (CreateProcessA(NULL, executable.data(), NULL, NULL, TRUE, 0, NULL, NULL, &startupInfo, &processInfo) == 0)
{
std::cerr << "Error: Failed to create the child process" << std::endl;
return 0;
}
// Set the pipes to non-blocking mode
DWORD mode = PIPE_NOWAIT;
SetNamedPipeHandleState(out_pipe, &mode, NULL, NULL);
SetNamedPipeHandleState(err_pipe, &mode, NULL, NULL);
SetNamedPipeHandleState(in_pipe, &mode, NULL, NULL);
Sleep(500); //wait for child to start, may not be neccesary
// Get the exit code of the child process
DWORD exitCode;
GetExitCodeProcess(processInfo.hProcess, &exitCode);
if (exitCode == STILL_ACTIVE) {
// Set up the read buffer
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
DWORD bytesRead = 0;
DWORD bytesAvail = 0;
// Check if there is data available to read from the pipe
if (!PeekNamedPipe(out_pipe, buffer, sizeof(buffer), &bytesRead, &bytesAvail, NULL)) {
std::cerr << "PeekNamedPipe failed (" << GetLastError() << ").\n";
return 0;
}
if (bytesAvail == 0)
{
std::cerr << "Pipe is empty" << std::endl;
}
if (!ReadFile(out_pipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL))
{
std::cerr << "Failed to read from pipe. Error code: " << GetLastError() << std::endl;
return 0;
}
data = buffer;
}
if (data == "") {
std::cout << "Something went wrong. Code: " << GetLastError() << std::endl;
}
else {
std::cout << data << std::endl;
}
std::cout << "Press any key." << std::endl;
std::cin.get();
return 0;
}
并且,作为参考,这里是 helloworld.exe:
// HelloWorld.cpp
#include <iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
std::cin.get();
}
【问题讨论】:
-
您正在丢失
CreateNamedPipe返回的管道句柄。您只能访问管道的一端。写入out_pipe的内容应该从CreateNamedPipeW(pipeOut.c_str(), ...)调用返回的句柄中读取 - 但您不保留该句柄。 -
您可能不需要
PIPE_TYPE_MESSAGE,除非子程序专门设计用于编写消息。一方面,文档说“命名管道的客户端以字节模式开始,即使服务器端处于消息模式。为避免接收数据出现问题,也将客户端设置为消息模式”;你不那样做。但即使你这样做了——你有一个通过标准库编写的子程序。涉及缓冲 - 很难预测 CRT 何时决定写入底层句柄。您不会有有意义的消息界限。 -
我不明白你为什么要命名管道而不是匿名管道的解释(参见例如this sample)。 “运行此线程”或“同时打开多个可执行文件进行通信”都不会排除匿名管道的使用。
-
我正在用 C++ 重制 github.com/dreignier/cg-brutaltester。由于使用了过时的库,Java 版本无法编译。我想做一些更有前途的事情,因此没有第 3 方库,而是从头开始。结果将上传回github。我已经编写并测试了其他类,并决定通过完成剩余的少量通信代码来测试我的 Process 类,但没有得到任何乐趣。我在我的第一个问题中发布了所有内容,但 mods 不允许发布,因为它太“模糊”,所以我只用基础知识重新发布了问题。
-
Brutal Tester 使用两种通信类型。 “旧”模式打开裁判可执行文件和 2 或 4 个玩家可执行文件(最多 6 个子进程!)。然后,它“促进”裁判和球员之间的交流来回传递信息。 “新”模式仅打开 Referee 将播放器可执行文件作为命令行参数发送,Referee 处理其余部分并将分数发送回 Brutal Tester。裁判文件已经存在,一些播放器文件可供下载,但基本上您使用来自codingame.com 的代码作为播放器文件。
标签: c++ c++20 named-pipes