本文描述如何重定向命令行子进程的标准输入流、标准输出流、标准错误流。本文介绍的方式只对使用了标准输入、标准输出、标准错误流的命令行程序有用。
创建子进程时,我们一般使用CreateProcess
,而CreateProcess
提供的LPSTARTUPINFO
参数允许我们改变进程的标准输入输出句柄。
CreateProcess
函数定义如下:
BOOL WINAPI CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
参数LPSTARTUPINFO
定义如下:
typedef struct _STARTUPINFO {
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
我们需要将dwFlags
设置为STARTF_USESTDHANDLES
来表示hStdInput
,hStdOutput
,hStdError
这3个成员是有效的,并且这3个值就将分别用来作为子命令行进程(必须是命令行进程)的标准输入流句柄、标准输出流句柄、标准错误流句柄。
hStdInput
,hStdOutput
,hStdError
都是HANDLE
类型,可以是任何类型的HANDLE,如文件、管道等,只需要满足2个条件:
1. 能够支持通过ReadFile
, WriteFile
函数进行读写。
2. 是可以被继承的(inheritable),句柄默认是不能被继承的,可以通过设置SECURITY_ATTRIBUTES安全属性来设置为可以被继承。
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
我们也可以将hStdInput
,hStdOutput
,hStdError
中的某些进行重新定向,某些维持原样。针对想维持原样的,通过调用GetStdHandle
函数获取到指定类型的标准句柄设置上去即可,如:
hStdInput = GetStdHandle(STD_INPUT_HANDLE);
函数ExcuteConcosle
用于启动一个命令行程序,并且通过std_input
传入用户输入的内容到标准输入流,std_output
返回程序标准输出流内容,std_error
返回程序标准错误流内容。
#include
#include
bool ExcuteConcosle(const std::string &cmd, const std::string &std_input, std::string &std_output, std::string &std_error) {
HANDLE std_input_read = INVALID_HANDLE_VALUE;
HANDLE std_input_write = INVALID_HANDLE_VALUE;
HANDLE std_output_read = INVALID_HANDLE_VALUE;
HANDLE std_output_write = INVALID_HANDLE_VALUE;
HANDLE std_err_read = INVALID_HANDLE_VALUE;
HANDLE std_err_write = INVALID_HANDLE_VALUE;
bool ret = false;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
do
{
if (CreatePipe(&std_input_read, &std_input_write, &sa, 0) == 0) {
break;
}
if (CreatePipe(&std_output_read, &std_output_write, &sa, 0) == 0) {
break;
}
if (CreatePipe(&std_err_read, &std_err_write, &sa, 0) == 0) {
break;
}
// 向标准输入管道写入用户输入内容
//
// TODO:目前只支持用户输入一次数据
//
DWORD dwWritten = 0;
if (!WriteFile(std_input_write, std_input.c_str(), std_input.length(), &dwWritten, NULL) || dwWritten != std_input.length()) {
break;
}
PROCESS_INFORMATION pi;
STARTUPINFOA si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE; // 隐藏界面
si.hStdInput = std_input_read;
si.hStdOutput = std_output_write;
si.hStdError = std_err_write;
size_t cmd_len = cmd.length();
char *cmd_buf = new char[cmd_len + 1];
memcpy(cmd_buf, cmd.c_str(), cmd_len);
cmd_buf[cmd_len] = 0;
if (!CreateProcessA(NULL, cmd_buf, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi)) {
delete[]cmd_buf;
break;
}
delete[] cmd_buf;
// 等待子进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
// 开始读取管道中的内容
// 在读取管道内容前先关闭的管道的写入句柄,这样在就可以通过ReadFile返回值结合ERROR_BROKEN_PIPE来判断读取是否结束
//
// TODO: 子进程可以能输出的内容较多,将管道的缓冲区填满,导致WriteFile一直阻塞,从而导致子进程无法退出,前面的WaitForSingleObject一直阻塞住
// 解决方案:将管道内容的读取放到独立的线程中,在CreateProcess之前启动线程开始读取。
//
char lpBuffer[256];
DWORD nBytesRead = 0;
// 关闭标准输出管道的写入句柄,如果不关闭ReadFile会一直阻塞等待读取
if (std_output_write != INVALID_HANDLE_VALUE) {
CloseHandle(std_output_write);
std_output_write = INVALID_HANDLE_VALUE;
}
while (true) {
ZeroMemory(lpBuffer, 256);
if (!ReadFile(std_output_read, lpBuffer, sizeof(lpBuffer) - 1,
&nBytesRead, NULL) || !nBytesRead) {
if (GetLastError() == ERROR_BROKEN_PIPE)
break; // pipe done - normal exit
}
std_output.append(lpBuffer, nBytesRead);
}
// 关闭标准错误管道的写入句柄,如果不关闭ReadFile会一直阻塞等待读取
if (std_err_write != INVALID_HANDLE_VALUE) {
CloseHandle(std_err_write);
std_err_write = INVALID_HANDLE_VALUE;
}
while (true) {
ZeroMemory(lpBuffer, 256);
if (!ReadFile(std_err_read, lpBuffer, sizeof(lpBuffer) - 1,
&nBytesRead, NULL) || !nBytesRead) {
if (GetLastError() == ERROR_BROKEN_PIPE)
break; // pipe done - normal exit
}
std_error.append(lpBuffer, nBytesRead);
}
ret = true;
} while (false);
if (std_input_read != INVALID_HANDLE_VALUE)
CloseHandle(std_input_read);
if (std_input_write != INVALID_HANDLE_VALUE)
CloseHandle(std_input_write);
if (std_output_read != INVALID_HANDLE_VALUE)
CloseHandle(std_output_read);
if (std_output_write != INVALID_HANDLE_VALUE)
CloseHandle(std_output_write);
if (std_err_read != INVALID_HANDLE_VALUE)
CloseHandle(std_err_read);
if (std_err_write != INVALID_HANDLE_VALUE)
CloseHandle(std_err_write);
return ret;
}
用于测试的命令行程序如下(假设编译生成test.exe),该程序使用到了标准输入、输出、错误:
#include
#include
int main()
{
std::string name;
std::cin >> name;
std::cout << "input name is " << name << std::endl;
if (name != "jeff") {
std::cerr << "name input error\n";
}
return 0;
}
在其他进程中调用ExcuteConcosle
函数启动测试进程test.exe,传入输入内容,获取输出内容和错误内容:
int main()
{
std::string std_input = "jim\n"; // 传入用户输入内容jim\n,“\n”模拟用户输入结束
std::string std_output;
std::string std_error;
bool ret = ExcuteConcosle("D:\\test.exe", std_input, std_output, std_error);
printf("标准输出:%s\n", std_output.c_str());
printf("标准错误:%s\n", std_error.c_str());
return 0;
}
参考:https://support.microsoft.com/zh-cn/help/190351/how-to-spawn-console-processes-with-redirected-standard-handles