运行 通过 CreateProcess 在 C++ 中执行命令并处理其输入和输出
Run command in c++ via CreateProcess and handle its input and output
我正在尝试创建一个程序,您可以在其中执行命令。这些命令的输出应该显示在 GUI 中。为此,我使用 QT(因为我想熟悉 WinAPI,所以我不使用 QProcess)。在当前程序中,已经可以使用句柄重定向命令的输出。现在我的问题是,如果命令需要用户输入,怎么可能中断 ReadFile
。
例如,我想 运行 来自 C++ 的命令 yarn run
。
此 returns 作为输出,表示此命令不存在,并询问我想执行哪个命令。目前命令在那里中止(与 CTRL+C 相比)和 returns 错误没有指定命令 。然而,在这一点上,用户输入应该是可能的。
计划的预期结果:
我得到的输出是:
如图 1 所示,yarn 要求用户输入。在图 2 中,根本没有问题。例如,如果在问题输入出现时按 CTRL+C,则可能会出现此行为。
那么如何在 gui 中进行用户输入(目前将变量的值重定向到输入中就足够了)并将其重定向回进程。该进程应该等到它获得输入。
Command.h
#ifndef COMMAND_H
#define COMMAND_H
#include <string>
#include <cstdlib>
#include <cstdio>
#include <io.h>
#include <fcntl.h>
#include <stdexcept>
#include <windows.h>
#include <iostream>
#define BUFSIZE 256
class Project;
class Command
{
private:
int exitStatus;
const Project * project;
std::string cmd;
HANDLE g_hChildStd_IN_Rd = nullptr;
HANDLE g_hChildStd_IN_Wr = nullptr;
HANDLE g_hChildStd_OUT_Rd = nullptr;
HANDLE g_hChildStd_OUT_Wr = nullptr;
HANDLE g_hInputFile = nullptr;
void setupWindowsPipes();
void createWindowsError(const std::string &errorText);
void readFromPipe();
public:
Command() = delete;
explicit Command(std::string cmd, const Project *project);
void exec();
};
#endif // COMMAND_H
Command.cpp(gui调用的入口点是exec()
)
#include "command.h"
#include "project.h"
Command::Command(std::string cmd, const Project *project) : exitStatus(0), project(project), cmd(std::move(cmd)) {}
void Command::createWindowsError(const std::string &errorText) {
DWORD code = GetLastError();
LPSTR lpMsgBuf;
if(code == 0) return;
auto size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR) &lpMsgBuf,
0, NULL );
std::string msg(lpMsgBuf, size);
LocalFree(lpMsgBuf);
throw std::runtime_error(errorText + "()" + std::to_string(code) + ": " + msg);
}
void Command::setupWindowsPipes(){
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = true;
saAttr.lpSecurityDescriptor = nullptr;
if(!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0))
createWindowsError("StdOutRd CreatePipe");
if(!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0))
createWindowsError("StdOut SetHandleInformation");
if(!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0))
createWindowsError("StdInRd CreatePipe");
if(!SetHandleInformation(g_hChildStd_IN_Rd, HANDLE_FLAG_INHERIT, 0))
createWindowsError("StdIn SetHandleInformation");
}
void Command::readFromPipe() {
DWORD dwRead;
char chBuf[BUFSIZE];
bool bSuccess = false;
for (;;)
{
dwRead = 0;
for(int i = 0;i<BUFSIZE;++i) {
chBuf[i] = '[=12=]';
}
bSuccess = ReadFile( g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL);
if( ! bSuccess || dwRead <= 0 ) break;
std::cout << chBuf;
}
std::cout << std::endl;
}
void Command::exec() {
std::cout << "CMD to run: " << this->cmd << std::endl;
this->setupWindowsPipes();
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.hStdError = g_hChildStd_OUT_Wr;
si.hStdOutput = g_hChildStd_OUT_Wr;
si.hStdInput = g_hChildStd_IN_Rd;
si.dwFlags |= STARTF_USESTDHANDLES;
ZeroMemory(&pi, sizeof(pi));
char* dir = nullptr;
if(this->project != nullptr) {
auto n = this->project->getLocalUrl().size() + 1;
auto nString = this->project->getLocalUrl().replace("/", "\");
dir = new char[n];
std::strncpy(dir, nString.toStdString().c_str(), n);
}
std::string cmdString = "cmd /c ";
cmdString.append(this->cmd);
char cmdCopy[cmdString.size() + 1];
cmdString.copy(cmdCopy, cmdString.size());
cmdCopy[cmdString.size() + 1] = '[=12=]';
bool rc = CreateProcessA( nullptr,
cmdCopy,
nullptr,
nullptr,
true,
CREATE_NO_WINDOW,
nullptr,
dir,
&si,
&pi);
delete []dir;
if(!rc)
createWindowsError("Failed to create process");
std::cout << "PID: " << pi.dwProcessId << std::endl;
CloseHandle(g_hChildStd_OUT_Wr);
CloseHandle(g_hChildStd_IN_Rd);
readFromPipe();
std::cout << "fin reading pipe" << std::endl;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
听起来你遇到了 XY 问题,幸运的是你描述了 X,所以我们可以解决它。
问题不是您未能调用 WriteFile
将响应存储到重定向的输入管道中。如果程序试图读取输入,它会等待。
问题是程序根本不请求输入。它检测到无法进行交互式输入,因为它检测到一个管道并假定该管道不是交互式的。所以它根本不执行提示或尝试从标准输入读取。 您无法回答程序未提出的问题!
(要确认这是您生成的 yarn
程序的行为,您可以使用管道从 cmd.exe 启动它以提供输入。cmd.exe 有很好的-测试了重定向输入和输出句柄的缓冲逻辑,您可以确定代码中任何可疑的死锁都不会影响 cmd.exe)
在类 Unix 系统上,这是通过重定向到伪 tty (ptty) 特殊文件而不是管道特殊文件来解决的,这会导致 isatty()
函数为 return true。
在 Windows 上,这过去实际上是不可能的,因为在内核级别实现的控制台 API 永久关联到控制台子系统 csrss.exe,后者仅与官方控制台主机进程(控制台所有者 windows)。
但是现在,Windows API 支持伪控制台。您可以在 Microsoft Dev Blog
上找到完整的介绍
从 Windows 10 版本 1809(2018 年 10 月更新)开始,CreatePseudoConsole
支持您需要的重要功能(以防 link 中断)。
当您使用 CreatePseudoConsole
提升管道然后将此控制台提供给 CreateProcess
(而不是将管道附加到您的子流程标准 I/O 流)时,子流程将检测到交互式控制台,可以使用控制台 API 功能,例如 AttachConsole
,可以打开特殊文件名 CONIN$
等。数据来自您(和来自您)而不是 linked 到控制台 window.
还有a complete sample on GitHub.
同一篇博客 post 还讨论了在 Windows 10 中添加 CreatePseudoConsole
之前“终端”和“远程 shell” 类型软件使用的解决方法,即使用分离的控制台设置子进程,隐藏关联的控制台 window,并抓取控制台屏幕缓冲区。
我正在尝试创建一个程序,您可以在其中执行命令。这些命令的输出应该显示在 GUI 中。为此,我使用 QT(因为我想熟悉 WinAPI,所以我不使用 QProcess)。在当前程序中,已经可以使用句柄重定向命令的输出。现在我的问题是,如果命令需要用户输入,怎么可能中断 ReadFile
。
例如,我想 运行 来自 C++ 的命令 yarn run
。
此 returns 作为输出,表示此命令不存在,并询问我想执行哪个命令。目前命令在那里中止(与 CTRL+C 相比)和 returns 错误没有指定命令 。然而,在这一点上,用户输入应该是可能的。
计划的预期结果:
我得到的输出是:
如图 1 所示,yarn 要求用户输入。在图 2 中,根本没有问题。例如,如果在问题输入出现时按 CTRL+C,则可能会出现此行为。
那么如何在 gui 中进行用户输入(目前将变量的值重定向到输入中就足够了)并将其重定向回进程。该进程应该等到它获得输入。
Command.h
#ifndef COMMAND_H
#define COMMAND_H
#include <string>
#include <cstdlib>
#include <cstdio>
#include <io.h>
#include <fcntl.h>
#include <stdexcept>
#include <windows.h>
#include <iostream>
#define BUFSIZE 256
class Project;
class Command
{
private:
int exitStatus;
const Project * project;
std::string cmd;
HANDLE g_hChildStd_IN_Rd = nullptr;
HANDLE g_hChildStd_IN_Wr = nullptr;
HANDLE g_hChildStd_OUT_Rd = nullptr;
HANDLE g_hChildStd_OUT_Wr = nullptr;
HANDLE g_hInputFile = nullptr;
void setupWindowsPipes();
void createWindowsError(const std::string &errorText);
void readFromPipe();
public:
Command() = delete;
explicit Command(std::string cmd, const Project *project);
void exec();
};
#endif // COMMAND_H
Command.cpp(gui调用的入口点是exec()
)
#include "command.h"
#include "project.h"
Command::Command(std::string cmd, const Project *project) : exitStatus(0), project(project), cmd(std::move(cmd)) {}
void Command::createWindowsError(const std::string &errorText) {
DWORD code = GetLastError();
LPSTR lpMsgBuf;
if(code == 0) return;
auto size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR) &lpMsgBuf,
0, NULL );
std::string msg(lpMsgBuf, size);
LocalFree(lpMsgBuf);
throw std::runtime_error(errorText + "()" + std::to_string(code) + ": " + msg);
}
void Command::setupWindowsPipes(){
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = true;
saAttr.lpSecurityDescriptor = nullptr;
if(!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0))
createWindowsError("StdOutRd CreatePipe");
if(!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0))
createWindowsError("StdOut SetHandleInformation");
if(!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0))
createWindowsError("StdInRd CreatePipe");
if(!SetHandleInformation(g_hChildStd_IN_Rd, HANDLE_FLAG_INHERIT, 0))
createWindowsError("StdIn SetHandleInformation");
}
void Command::readFromPipe() {
DWORD dwRead;
char chBuf[BUFSIZE];
bool bSuccess = false;
for (;;)
{
dwRead = 0;
for(int i = 0;i<BUFSIZE;++i) {
chBuf[i] = '[=12=]';
}
bSuccess = ReadFile( g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL);
if( ! bSuccess || dwRead <= 0 ) break;
std::cout << chBuf;
}
std::cout << std::endl;
}
void Command::exec() {
std::cout << "CMD to run: " << this->cmd << std::endl;
this->setupWindowsPipes();
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.hStdError = g_hChildStd_OUT_Wr;
si.hStdOutput = g_hChildStd_OUT_Wr;
si.hStdInput = g_hChildStd_IN_Rd;
si.dwFlags |= STARTF_USESTDHANDLES;
ZeroMemory(&pi, sizeof(pi));
char* dir = nullptr;
if(this->project != nullptr) {
auto n = this->project->getLocalUrl().size() + 1;
auto nString = this->project->getLocalUrl().replace("/", "\");
dir = new char[n];
std::strncpy(dir, nString.toStdString().c_str(), n);
}
std::string cmdString = "cmd /c ";
cmdString.append(this->cmd);
char cmdCopy[cmdString.size() + 1];
cmdString.copy(cmdCopy, cmdString.size());
cmdCopy[cmdString.size() + 1] = '[=12=]';
bool rc = CreateProcessA( nullptr,
cmdCopy,
nullptr,
nullptr,
true,
CREATE_NO_WINDOW,
nullptr,
dir,
&si,
&pi);
delete []dir;
if(!rc)
createWindowsError("Failed to create process");
std::cout << "PID: " << pi.dwProcessId << std::endl;
CloseHandle(g_hChildStd_OUT_Wr);
CloseHandle(g_hChildStd_IN_Rd);
readFromPipe();
std::cout << "fin reading pipe" << std::endl;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
听起来你遇到了 XY 问题,幸运的是你描述了 X,所以我们可以解决它。
问题不是您未能调用 WriteFile
将响应存储到重定向的输入管道中。如果程序试图读取输入,它会等待。
问题是程序根本不请求输入。它检测到无法进行交互式输入,因为它检测到一个管道并假定该管道不是交互式的。所以它根本不执行提示或尝试从标准输入读取。 您无法回答程序未提出的问题!
(要确认这是您生成的 yarn
程序的行为,您可以使用管道从 cmd.exe 启动它以提供输入。cmd.exe 有很好的-测试了重定向输入和输出句柄的缓冲逻辑,您可以确定代码中任何可疑的死锁都不会影响 cmd.exe)
在类 Unix 系统上,这是通过重定向到伪 tty (ptty) 特殊文件而不是管道特殊文件来解决的,这会导致 isatty()
函数为 return true。
在 Windows 上,这过去实际上是不可能的,因为在内核级别实现的控制台 API 永久关联到控制台子系统 csrss.exe,后者仅与官方控制台主机进程(控制台所有者 windows)。
但是现在,Windows API 支持伪控制台。您可以在 Microsoft Dev Blog
上找到完整的介绍从 Windows 10 版本 1809(2018 年 10 月更新)开始,CreatePseudoConsole
支持您需要的重要功能(以防 link 中断)。
当您使用 CreatePseudoConsole
提升管道然后将此控制台提供给 CreateProcess
(而不是将管道附加到您的子流程标准 I/O 流)时,子流程将检测到交互式控制台,可以使用控制台 API 功能,例如 AttachConsole
,可以打开特殊文件名 CONIN$
等。数据来自您(和来自您)而不是 linked 到控制台 window.
还有a complete sample on GitHub.
同一篇博客 post 还讨论了在 Windows 10 中添加 CreatePseudoConsole
之前“终端”和“远程 shell” 类型软件使用的解决方法,即使用分离的控制台设置子进程,隐藏关联的控制台 window,并抓取控制台屏幕缓冲区。