捕获cmd.exe输出时ReadFile出现阻塞的技术问题排查
我正在编写一个C++例程,用于同步生成进程并以字符串形式返回输出。需求如下:
- 替代
popen,在Windows上静默执行(无控制台窗口弹出) - 保持与
popen/system的调用兼容性:无需在dir c:/mydir这类命令前添加cmd /c即可直接通过cmd.exe执行
当前代码在传入命令"dir \"c:/Program Files\""时出现问题:ReadFile读取前128字节后阻塞。但如果将CreateProcessW的lpApplicationName设为NULL,并在命令前手动添加cmd /c,则能正常运行无阻塞。
当前代码如下:
static bool _GetCmdExeFullPath(std::basic_string<WCHAR>& result) { WCHAR sysDir[MAX_PATH]; // Get the system directory if (GetSystemDirectoryW(sysDir, MAX_PATH) == 0) { // Handle error return false; } result = sysDir; result += L"\\cmd.exe"; return true; } bool __process_execute(const std::string& cmd, std::string& processOutput) { std::basic_string<WCHAR> cmdExe; if (! _GetCmdExeFullPath(cmdExe)) return false; SECURITY_ATTRIBUTES saAttr; HANDLE hRead, hWrite; saAttr.nLength = sizeof(saAttr); saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; if (!CreatePipe(&hRead, &hWrite, &saAttr, 0)) { //std::cerr << "Error: Unable to create pipe" << std::endl; return false; } STARTUPINFOW si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.hStdError = hWrite; si.hStdOutput = hWrite; si.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; ZeroMemory(&pi, sizeof(pi)); const std::basic_string<WCHAR> wCmd = __utf8_to_WCHAR(cmd.c_str()); // We have to cast away const here because the API doesn't specify const. But it also does not modify the string. //if (!CreateProcessW(NULL, const_cast<LPWSTR>(wCmd.c_str()), NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) if (!CreateProcessW(cmdExe.c_str(), const_cast<LPWSTR>(wCmd.c_str()), NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { DWORD error = GetLastError(); //std::cerr << "Error: Unable to create process" << std::endl; CloseHandle(hRead); CloseHandle(hWrite); return false; } CloseHandle(hWrite); DWORD bytesRead; const int bufferSize = 4096; char buffer[bufferSize]; std::string output = ""; while (true) { if (!ReadFile(hRead, buffer, bufferSize - 1, &bytesRead, NULL) || bytesRead == 0) { break; } buffer[bytesRead] = '\0'; output += buffer; } CloseHandle(hRead); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); std::basic_string<WCHAR> wOutput = __char_to_WCHAR(output.c_str()); processOutput = __WCHAR_to_utf8(wOutput.c_str()); return true; }
当明确指定lpApplicationName为cmd.exe时,lpCommandLine参数会直接作为cmd.exe的启动参数。此时如果仅传入dir "c:/Program Files",cmd.exe会进入交互模式(而非执行命令后退出),它会等待用户输入更多指令,导致进程一直存活,管道的读端hRead会因为没有更多数据且写端虽关闭但进程未退出而阻塞在ReadFile调用上。
而当lpApplicationName设为NULL时,系统会自动解析命令行字符串,将开头的cmd识别为程序,后续的/c ...识别为参数,此时cmd.exe会执行指定命令后立即退出,管道能正常读取所有数据。
需要手动构造cmd.exe的正确参数:给用户传入的命令添加/c前缀,并包裹引号(处理命令中含空格的情况),让cmd.exe执行完命令后自动退出。
1. 基础修改
替换代码中构造wCmd的部分:
// 替换原来的wCmd构造代码 std::wstring rawCmd = __utf8_to_WCHAR(cmd.c_str()); // 构造cmd.exe的参数:/c "用户命令" std::wstring wCmd = L"/c \""; wCmd += rawCmd; wCmd += L"\"";
CreateProcessW调用保持不变:
if (!CreateProcessW(cmdExe.c_str(), const_cast<LPWSTR>(wCmd.c_str()), NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi))
2. 处理命令中的引号
如果用户传入的命令本身包含双引号,需要对内部引号转义(将"替换为\"),否则会导致命令解析错误。添加辅助函数:
std::wstring escapeQuotes(const std::wstring& input) { std::wstring result; for (wchar_t c : input) { if (c == L'"') { result += L'\\"'; } else { result += c; } } return result; }
修改构造wCmd的代码:
std::wstring rawCmd = __utf8_to_WCHAR(cmd.c_str()); std::wstring escapedCmd = escapeQuotes(rawCmd); std::wstring wCmd = L"/c \""; wCmd += escapedCmd; wCmd += L"\"";
修改后,传入"dir \"c:/Program Files\""会被转换成/c "dir \"c:/Program Files\"",cmd.exe会正确执行该命令并退出,ReadFile能读取完整输出数据,不会出现阻塞。
内容的提问来源于stack exchange,提问作者rpatters1

