环境:win10 64位,vs2015 mfc程序
目的:记录CreateProcess()接收启动程序的输出。注:cmd程序,而不是带ui的程序
起始原由:通过调用git工具来完成自己的一些需求
参考:
https://bbs.csdn.net/topics/190138594,会读取控制台输出,但是如何向控制台输入数据?交互?
https://blog.csdn.net/waitig1992/article/details/23766833,MFC执行CMD命令并获得其返回信息源代码
之前几乎没有用过CreateProcess()函数,偶尔需要启动其它程序,一般用system(),也没有啥特殊要求,本次有点不一样,需要接收git的输出,以便进行分析其结果。先上个代码,由于代码是多次调试的结果,所以看上去可能会有点乱,但不影响分析过程。
void LoadGitDirs()
{
CString strText;
PROCESS_INFORMATION pi;
SECURITY_ATTRIBUTES sa;
memset(&pi, 0, sizeof(pi));
memset(&sa, 0, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE readPipe = NULL;
HANDLE wrtePipe = NULL;
if (!CreatePipe(&readPipe, &wrtePipe, &sa, 1024*20))
{
AfxMessageBox(TEXT("创建管道失败,无法继续!"), MB_ICONSTOP);
return;
}
STARTUPINFO si;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdOutput = wrtePipe;
si.hStdError = wrtePipe;
CString strCmdExe = TEXT("c:\\windows\\system32\\cmd.exe");
DWORD dwErr = 0;
CString strPars = m_strGitTool;
//strPars = TEXT("cmd dir");
strPars = TEXT(" /c ") + strPars;
BOOL isOk = CreateProcess(strCmdExe, (LPTSTR)strPars.GetString(), &sa, &sa, TRUE, 0, NULL, NULL, &si, &pi);
strPars.ReleaseBuffer();
CloseHandle(wrtePipe);
wrtePipe = NULL; // 这里不使用输入
if (!isOk)
{
dwErr = GetLastError();
CloseHandle(readPipe);
readPipe = NULL;
AfxMessageBox(TEXT("启动git命令失败,无法继续!"), MB_ICONSTOP);
return;
}
// 开始等待结束
DWORD dwWaitResult;
DWORD dwReadLen = 0;
CString strGitRes;
char szBuf[1024 * 24] = { 0 };
while (true)
{
dwWaitResult = WaitForSingleObject(pi.hProcess, 200);
if (dwWaitResult == WAIT_OBJECT_0 || dwWaitResult == WAIT_ABANDONED)
break;
if (ReadFile(readPipe, szBuf, 1024 * 20, &dwReadLen, NULL) && dwReadLen > 0)
{
strGitRes += szBuf;
}
memset(szBuf, 0, sizeof(szBuf));
dwReadLen = 0;
}
while(ReadFile(readPipe, szBuf, 1024 * 20, &dwReadLen, NULL) && dwReadLen > 0)
{
strGitRes += szBuf;
memset(szBuf, 0, sizeof(szBuf));
dwReadLen = 0;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(readPipe);
readPipe = NULL;
if (strGitRes != TEXT(""))
AfxMessageBox(strGitRes, MB_ICONINFORMATION);
}
本段代码是调试通过的,m_strGitTool是git.exe的绝对路径,原工程用的是unicode编码,所以实际上在ReadFile()之后,szBuf是转换为unicode之后加到strGitRes上的,本文进行了简化,去掉了转换过程,所以如果试验此代码,需要在多字符编码下试验,如果是在unicode下,先对szBuf转换后再加到strGitRes上,否则最后的提示可能就是一堆乱码。
本代码只是调试通过,能正确接收git工具的输出,而在实际使用中是否还有其它问题,本文暂不考虑,因为我只是在调通之后来写这文章,还没有实际使用过,不知道中间是否会有其它一些问题。
以上程序的运行结果,就是弹出一个框,内容就是git的输出,如下所示:
参考的文章,以及搜索出来的其它文章,我觉得本身只是简单记录,并没有考虑的全面,但上面的代码,考虑了启动程序长时间运行时的情况(当然本文说的并不全面),每过200毫秒接收一次,直到程序退出后再继续接收直到接收不到内容为止。
本文主要记录调试过程中遇到的两个问题,另外再加上之前遇到的一些问题说明。
先说sa,即 SECURITY_ATTRIBUTES 结构。这个安全结构,早期的时候我都没用过,有它的地方,全都是设置成了NULL,当然那时候主要是线程上会用到。话说有一天,又在一个程序中创建了线程,最后发现有的电脑上正常运行,而有的电脑上运行失败,通过跟踪发现是创建线程失败,再反复查找问题,最终把安全参数传入的NULL换成了一个sa把解决问题了,当然也就是像上面代码中那样,定义变量-->清空所有成员-->设置长度成员,没有进行其它的设置,这是我记得很清楚的一件事,至于更深层的原因,当时就没有追究了,直到现在也没有再去追究过,只是知道通过这个方法解决了那个问题,所以后来,直到现在,只要是有这个参数的,我一律传入没有经过特殊设置的sa参数而不再传入NULL。
试验一:去掉 /c 参数
对strPars的修改如下:
CString strPars = m_strGitTool;
//strPars = TEXT(" /c");
strPars = TEXT(" ") + strPars;
这个修改,是把上面的代码这一行分成了两行,同时注释掉参数/c。运行结果:
while (true)
{
dwWaitResult = WaitForSingleObject(pi.hProcess, 200);
if (dwWaitResult == WAIT_OBJECT_0 || dwWaitResult == WAIT_ABANDONED)
break;
if (ReadFile(readPipe, szBuf, 1024 * 20, &dwReadLen, NULL) && dwReadLen > 0)
{
gStr2U(szBuf, strText);
strGitRes += strText;
}
memset(szBuf, 0, sizeof(szBuf));
dwReadLen = 0;
}
这个while循环运行了一次,而且ReadFile成功,strGitRes的内容为:“Microsoft Windows [版本 10.0.18363.1139]\r\n(c) 2019 Microsoft Corporation。保留所有权利。\r\n\r\nE:\\myproject\\projectname>”,当循环第二次时,程序卡在了 ReadFile() 这一行上,只好终止调试。
试验二:
把关闭写的管道放在第二个while之后,像下面这样子:
CloseHandle(wrtePipe);
wrtePipe = NULL; // 这里不使用输入
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(readPipe);
readPipe = NULL;
注意成功的代码中,这个关闭是放在CreateProcess()后面的。现在这样修改之后,第一个while循环会直接break,第二个while的ReadFile()会卡住,程序无法继续往下走。
第三点,注意对CreateProcess()参数si的输出赋值,
si.hStdOutput = wrtePipe;
si.hStdError = wrtePipe;
输出应该是赋给写管道的句柄,刚开始我把它赋成了读管道的句柄,一直收不到任何信息。这个读写,是对启动程序来说的,对于宿主程序,则是相反的理解。
第四点,注意对CreateProcess()参数si的dwFlags赋值,这点非常重要,成功时的赋值如下(如第一段代码中):
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
我没有仔细研究其它的项,仅说明我遇到的情况。
STARTF_USESTDHANDLES:比较好理解,就是要用本结构中的输入输出,当它们的值不为NULL时有效。
STARTF_USESHOWWINDOW:这个标志非常重要,如果不设置,则后面的ReadFile()全部失败,即接收不到任何的程序输出。如果设置了此标志,则其后的 wShowWindow就非常重要了,wShowWindow可取值同CreateWindow(),这里只讨论两个值:SW_HIDE和SW_SHOW,如果设置了SW_SHOW,则cmd的黑色窗口将会显示出来(当然对于命令立马就执行完的情况时,这个黑色窗口就是闪一下而已,也就是说,它是在启动程序运行的过程中出现,启动程序结束时窗口也自然结束),此时,宿主程序将接收不到任何启动程序的输出,所以此项必需设置为SW_HIDE。
对这两个成员的设置总结就是,dwFlags必需设置 STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW(至于是否可以加其它标志,并没有研究过),wShowWindow必需设置为SW_HIDE,否则宿主程序无法接收到启动程序的打印信息。
附
本程序的思路:在子程序启动后,在while循环中最长不超过200毫秒不断地检测子程序是否已经退出,如果已经退出,则结束此while循环,否则就读取一次打印,如果读取成功,就保存收到的打印信息。当第一个while循环退出后,就表示子程序已经结束了运行,此时再尝试继续接收剩余的打印,直到没有打印信息可接收时结束整个任务。