简体   繁体   中英

Run command in c++ via CreateProcess and handle its input and output

I am trying to create a program in which you can execute commands. The output of these commands should be displayed in a GUI. For this I use QT (because I want to get familiar with WinAPI I don't use QProcess). In the current program it is already possible to redirect the output of a command with a handle. Now my question, how is it possible to interrupt the ReadFile if the command expects a user input.

As an example, I want to run the command yarn run from C++.

This returns as output that this command does not exist and asks which command I want to execute instead. At the moment the command aborts there (comparable with CTRL+C) and returns error No command specified . At this point, however, a user input should be possible.

Expected outcome of the program:

计划的预期结果

The output I get instead:

我得到的输出

As you can see in picture 1 yarn asks the user for input. In image 2 there is no question at all. This behaviour is for example possible if you press CTRL+C if the question input shows up.

So how is it possible to make a user input in the gui (for now it would be enough to redirect the value of a variable into the input) and redirect it back to the process. The process should wait until it gets the input.

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 (the entry point which is called by the gui is 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] = '\0';
        }

        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] = '\0';
    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);

}

It sounds like you have an XY problem, luckily you described X so we can address it.

The issue is not your failure to call WriteFile to store the response into the redirected input pipe. If the program were trying to read input, it would wait.

The issue is that the program is not requesting input at all. It has detected that interactive input is not possible, because it detects a pipe and assumes that a pipe is not interactive . So it doesn't perform the prompt or try to read from standard input at all. You can't provide an answer to a question that the program didn't ask!

(To confirm this is the behavior of the yarn program you are spawning, you can launch it from cmd.exe using a pipe to provide the input. cmd.exe has well-tested buffering logic for redirected input and output handles and you can be sure that any suspected deadlock in your code doesn't affect cmd.exe)

On Unix-like systems, this is solved by redirecting to a pseudo-tty (ptty) special file instead of a pipe special file, which causes the isatty() function to return true.

On Windows, this used to be effectively impossible, as the console API, implemented at kernel level, was permanently associated to the console subsystem csrss.exe which only exchanged data with the official Console Host process (owner of console windows).

Now however, Windows API supports pseudo-consoles. You can find a complete introduction on the Microsoft Dev Blog

The important function you need (in case that link breaks) is CreatePseudoConsole supported starting with Windows 10 version 1809 (October 2018 update).

When you use CreatePseudoConsole to promote the pipes and then supply this console to CreateProcess (instead of attaching pipes to your subprocess standard I/O streams), the subprocess will detect an interactive console, can use console API functions such as AttachConsole , can open the special filenames CONIN$ etc. And the data comes to you (and from you) instead of being linked to a console window.

There's also a complete sample on GitHub .


That same blog post also discusses the workaround used by "Terminal" and "remote shell" type software prior to the addition of CreatePseudoConsole in Windows 10, namely setting up the subprocess with a detached console, hiding the associated console window, and screen-scraping the console screen buffer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM