如何使用CreateProcess()和CreatePipe()从cmd.exe读取输出
我一直在尝试创建一个cmd.exe使用命令行指定执行的子进程/K dir.目的是使用管道将命令的输出读回到父进程.
我已经开始CreateProcess()工作,但涉及管道的步骤给我带来了麻烦.使用管道,新的控制台窗口不显示(就像之前一样),并且父进程卡在调用中ReadFile().
有没有人知道我做错了什么?
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
#define BUFFSZ 4096
HANDLE g_hChildStd_IN_Rd = NULL;
HANDLE g_hChildStd_IN_Wr = NULL;
HANDLE g_hChildStd_OUT_Rd = NULL;
HANDLE g_hChildStd_OUT_Wr = NULL;
int wmain(int argc, wchar_t* argv[])
{
int result;
wchar_t aCmd[BUFFSZ] = TEXT("/K dir"); // CMD /?
STARTUPINFO si;
PROCESS_INFORMATION pi;
SECURITY_ATTRIBUTES sa;
printf("Starting...\n");
ZeroMemory(&si, sizeof(STARTUPINFO));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));
// Create one-way pipe for child process STDOUT
if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &sa, 0)) {
printf("CreatePipe() error: %ld\n", GetLastError());
}
// Ensure read handle to pipe for STDOUT is not inherited
if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) {
printf("SetHandleInformation() error: %ld\n", GetLastError());
}
// Create one-way pipe for child process STDIN
if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &sa, 0)) {
printf("CreatePipe() error: %ld\n", GetLastError());
}
// Ensure write handle to pipe for STDIN is not inherited
if (!SetHandleInformation(g_hChildStd_IN_Rd, HANDLE_FLAG_INHERIT, 0)) {
printf("SetHandleInformation() error: %ld\n", GetLastError());
}
si.cb = sizeof(STARTUPINFO);
si.hStdError = g_hChildStd_OUT_Wr;
si.hStdOutput = g_hChildStd_OUT_Wr;
si.hStdInput = g_hChildStd_IN_Rd;
si.dwFlags |= STARTF_USESTDHANDLES;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
// Pipe handles are inherited
sa.bInheritHandle = true;
// Creates a child process
result = CreateProcess(
TEXT("C:\\Windows\\System32\\cmd.exe"), // Module
aCmd, // Command-line
NULL, // Process security attributes
NULL, // Primary thread security attributes
true, // Handles are inherited
CREATE_NEW_CONSOLE, // Creation flags
NULL, // Environment (use parent)
NULL, // Current directory (use parent)
&si, // STARTUPINFO pointer
&pi // PROCESS_INFORMATION pointer
);
if (result) {
printf("Child process has been created...\n");
}
else {
printf("Child process could not be created\n");
}
bool bStatus;
CHAR aBuf[BUFFSZ + 1];
DWORD dwRead;
DWORD dwWrite;
// GetStdHandle(STD_OUTPUT_HANDLE)
while (true) {
bStatus = ReadFile(g_hChildStd_OUT_Rd, aBuf, sizeof(aBuf), &dwRead, NULL);
if (!bStatus || dwRead == 0) {
break;
}
aBuf[dwRead] = '\0';
printf("%s\n", aBuf);
}
// Wait until child process exits
WaitForSingleObject(pi.hProcess, INFINITE);
// Close process and thread handles
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
printf("Stopping...\n");
return 0;
}
Run Code Online (Sandbox Code Playgroud)
解决问题的一个微妙方法是确保关闭不需要的管道末端:
Us Child
+------------------+ +---------------+
| | | |
| g_hChildStd_IN_Wr----->g_hChildStd_IN_Rd |
| | | |
| g_hChildStd_OUT_Rd<------g_hChildStd_OUT_Wr |
| | | |
+------------------+ +---------------+
Run Code Online (Sandbox Code Playgroud)
您的父进程只需要每个管道的一端:
一旦启动了子进程:确保关闭不再需要的管道末端.
result = CreateProcess(...);
//CreateProcess demands that we close these two populated handles when we're done with them. We're done with them.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
/*
We've given the console app the writable end of the pipe during CreateProcess; we don't need it anymore.
We do keep the handle for the *readable* end of the pipe; as we still need to read from it.
The other reason to close the writable-end handle now is so that there's only one out-standing reference to the writeable end: held by the console app.
When the app closes, it will close the pipe, and ReadFile will return code 109 (The pipe has been ended).
That's how we'll know the console app is done. (no need to wait on process handles with buggy infinite waits)
*/
CloseHandle(g_hChildStd_OUT_Wr);
g_hChildStd_OUT_Wr = 0;
CloseHandle(g_hChildStd_IN_Rd);
g_hChildStd_OUT_Wr = 0;
Run Code Online (Sandbox Code Playgroud)
大多数解决方案的常见问题是人们试图等待进程句柄.这有很多问题; 主要的一个是,如果你等待孩子终止,孩子将永远无法终止.
如果孩子试图通过管道向您发送输出,并且您正在INFINITE等待,那么您不会清空管道的末端.管道最终变满了.当孩子尝试写入已满的管道时,WriteFile呼叫会等待管道有一些空间.因此,子进程永远不会终止; 你已经陷入僵局.
只需从管道中读取即可获得正确的解决方案.子进程终止后,它将成为管道CloseHandle 的末尾.下次您尝试从管道读取时,您将被告知管道已关闭(ERROR_BROKEN_PIPE).这就是你知道这个过程是如何完成的,你没有更多东西要读; 所有没有危险的MsgWaitForSingleObject,容易错误地使用,并导致你想要避免的bug.
String outputText = "";
//Read will return when the buffer is full, or if the pipe on the other end has been broken
while (ReadFile(stdOutRead, aBuf, Length(aBuf), &bytesRead, null)
outputText = outputText + Copy(aBuf, 1, bytesRead);
//ReadFile will either tell us that the pipe has closed, or give us an error
DWORD le = GetLastError;
//And finally cleanup
CloseHandle(g_hChildStd_IN_Wr);
CloseHandle(g_hChildStd_OUT_Rd);
if (le != ERROR_BROKEN_PIPE) //"The pipe has been ended."
RaiseLastOSError(le);
Run Code Online (Sandbox Code Playgroud)
伊恩博伊德的回答有一个宝石:一旦你启动了你的子进程:一定要关闭你不再需要的管道末端。
我已经制作了另一个版本的CreatePipe+CreateProcess解决方案,我希望它更清楚:
int main()
{
BOOL ok = TRUE;
HANDLE hStdInPipeRead = NULL;
HANDLE hStdInPipeWrite = NULL;
HANDLE hStdOutPipeRead = NULL;
HANDLE hStdOutPipeWrite = NULL;
// Create two pipes.
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
ok = CreatePipe(&hStdInPipeRead, &hStdInPipeWrite, &sa, 0);
if (ok == FALSE) return -1;
ok = CreatePipe(&hStdOutPipeRead, &hStdOutPipeWrite, &sa, 0);
if (ok == FALSE) return -1;
// Create the process.
STARTUPINFO si = { };
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdError = hStdOutPipeWrite;
si.hStdOutput = hStdOutPipeWrite;
si.hStdInput = hStdInPipeRead;
PROCESS_INFORMATION pi = { };
LPCWSTR lpApplicationName = L"C:\\Windows\\System32\\cmd.exe";
LPWSTR lpCommandLine = (LPWSTR)L"C:\\Windows\\System32\\cmd.exe /c dir";
LPSECURITY_ATTRIBUTES lpProcessAttributes = NULL;
LPSECURITY_ATTRIBUTES lpThreadAttribute = NULL;
BOOL bInheritHandles = TRUE;
DWORD dwCreationFlags = 0;
LPVOID lpEnvironment = NULL;
LPCWSTR lpCurrentDirectory = NULL;
ok = CreateProcess(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttribute,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
&si,
&pi);
if (ok == FALSE) return -1;
// Close pipes we do not need.
CloseHandle(hStdOutPipeWrite);
CloseHandle(hStdInPipeRead);
// The main loop for reading output from the DIR command.
char buf[1024 + 1] = { };
DWORD dwRead = 0;
DWORD dwAvail = 0;
ok = ReadFile(hStdOutPipeRead, buf, 1024, &dwRead, NULL);
while (ok == TRUE)
{
buf[dwRead] = '\0';
OutputDebugStringA(buf);
puts(buf);
ok = ReadFile(hStdOutPipeRead, buf, 1024, &dwRead, NULL);
}
// Clean up and exit.
CloseHandle(hStdOutPipeRead);
CloseHandle(hStdInPipeWrite);
DWORD dwExitCode = 0;
GetExitCodeProcess(pi.hProcess, &dwExitCode);
return dwExitCode;
}
Run Code Online (Sandbox Code Playgroud)
一些注意事项:
DIR命令不需要用户输入(但是,我把它留在了代码中,因为它是运行其他命令的好模板)hStdInPipeRead& 相关的所有内容hStdInPipeWrite都可以省略si.hStdInput可以省略L"C:\\Windows\\System32\\cmd.exe"用读取COMSPEC环境变量替换硬编码。cmd.exe /k DIR,cmd.exe /c DIR因为当DIR命令完成时,我们真的不想cmd.exe留下来。我认为你做的一切都是对的。但是 cmd.exe 在启动和 ReadFile 块后不打印任何内容或打印很少量的数据。如果你移动你的周期
while (true) {
bStatus = ReadFile(g_hChildStd_OUT_Rd, aBuf, sizeof(aBuf), &dwRead, NULL);
if (!bStatus || dwRead == 0) {
break;
}
aBuf[dwRead] = '\0';
printf("%s\n", aBuf);
}
Run Code Online (Sandbox Code Playgroud)
进入后台线程并运行其他循环,该循环将读取您的输入并将其发送到 cmd.exe,我认为您可以看到任何效果。您可以减小读取缓冲区(例如 16 字节)。