运行 通过 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,并抓取控制台屏幕缓冲区。