无法在 Bash 中为窗口传输输出

Can't Pipe Output in Bash for Windows

本文关键字:窗口 传输 输出 Bash      更新时间:2023-10-16

我正在尝试编写一个关于 bash 的包装器,将标准输入/输出/错误重定向到父进程和从父进程重定向。到目前为止,我已经对 Windows 的 cmd .exe有一个包装器。我可以键入一个命令并让它在控制台中运行,然后读取该命令的输出并将其显示给用户。所以我认为以同样的方式将它包裹在 bash 上是一件容易的事。然而。。。如果我将进程设置为打开 bash,则不会得到任何输出。如果我打开一个cmd进程并运行"bash",甚至像下面的演示中那样,使用"-c"选项运行单个命令也是如此。无输出。我已经编译了我能编译的最小的测试用例,如下所示:

#include <algorithm>
#include <cassert>
#include <functional>
#include <string>
#include <tchar.h>
#include <Windows.h>
using wstring = std::wstring;
using astring = std::string;
#ifdef UNICODE
using tstring = wstring;
using tchar = wchar_t;
#else
using tstring = astring;
using tchar = char;
#endif
const tstring PROMPT = L"ATOTALLYRANDOMSTRING";
/**
 * Represents an instance of a terminal process with piped in, out, and err
 * handles.
 */
class Terminal
{
public:
    using OutputCallback = std::function<void(tstring)>;
    /**
     * Terminal constructor.
     */
    Terminal();
    /**
     * Terminal destructor.
     */
    ~Terminal();
    /**
     * Executes the specified command.  If a callback is specified, the output
     * be passed as the first argument.
     *
     * @param command
     * @param callback
     * @param buffer   If specified, the callback parameter will be called as
     * output is available until the command is complete.
     */
    void exec(astring command, OutputCallback callback = nullptr, bool buffer = true);
    /**
     * Reads from the terminal, calling the specified callback as soon as any
     * output is available.
     *
     * @param callback
     */
    void read(OutputCallback callback);
    /**
     * Reads from the terminal, calling the specified callback upon reaching a
     * newline.
     *
     * @param callback
     * @param buffer   If specified, causes the callback to be called as output
     * is available until a newline is reached.
     */
    void readLine(OutputCallback callback, bool buffer = true);
    /**
     * Reads from the terminal, calling the specified callback upon reaching
     * the specified terminator.
     *
     * @param terminator
     * @param callback
     * @param buffer     If specified, causes the callback to be called as
     * output is available until the specified terminator is reached.
     */
    void readUntil(const tstring& terminator, OutputCallback callback, bool buffer = true);
    /**
     * Read from the terminal, calling the specified callback upon reaching the
     * prompt.
     *
     * @param callback
     * @param buffer   If specified, causes the callback to be called as output
     * is available until the specified prompt is reached.
     */
    void readUntilPrompt(OutputCallback callback, bool buffer = true);
    /**
     * Writes the specified text to the terminal.
     *
     * @param data
     */
    void write(const astring& data);
private:
    struct
    {
        struct
        {
            HANDLE in;
            HANDLE out;
            HANDLE err;
        } read, write;
    } pipes;
    tstring bufferedData;
    PROCESS_INFORMATION process;
    void initPipes();
    void initProcess();
    void terminatePipes();
    void terminateProcess();
};
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    Terminal terminal;
    terminal.readUntilPrompt([] (tstring startupInfo) {
        MessageBox(nullptr, startupInfo.c_str(), L"Startup Information", MB_OK);
    });
    // This works
    terminal.exec("dir", [] (tstring result) {
        MessageBox(nullptr, result.c_str(), L"dir", MB_OK);
    });
    // This doesn't (no output)
    terminal.exec("bash -c "ls"", [] (tstring result) {
        MessageBox(nullptr, result.c_str(), L"bash -c "ls"", MB_OK);
    });
    return 0;
}
Terminal::Terminal()
{
    this->initPipes();
    this->initProcess();
}
Terminal::~Terminal()
{
    this->terminateProcess();
    this->terminatePipes();
}
void Terminal::exec(astring command, OutputCallback callback, bool buffer)
{
    command.append("rn");
    this->write(command);
    this->readUntilPrompt([&callback, &command] (tstring data) {
        if (!callback) {
            return;
        }
        // Remove the prompt from the string
        data.erase(data.begin(), data.begin() + command.length());
        callback(data);
    }, buffer);
}
void Terminal::initPipes()
{
    SECURITY_ATTRIBUTES attr;
    attr.nLength = sizeof(attr);
    attr.bInheritHandle = TRUE;
    attr.lpSecurityDescriptor = nullptr;
    if(!CreatePipe(&this->pipes.read.in, &this->pipes.write.in, &attr, 0))
    {
        throw std::exception("Failed to create stdin pipe.");
    }
    if(!SetHandleInformation(this->pipes.write.in, HANDLE_FLAG_INHERIT, 0))
    {
        throw std::exception("Failed to unset stdin pipe inheritance flag.");
    }
    if(!CreatePipe(&this->pipes.read.out, &this->pipes.write.out, &attr, 0))
    {
        throw std::exception("Failed to create stdout pipe.");
    }
    if(!SetHandleInformation(this->pipes.read.out, HANDLE_FLAG_INHERIT, 0))
    {
        throw std::exception("Failed to unset stdout pipe inheritance flag.");
    }
    if(!CreatePipe(&this->pipes.read.err, &this->pipes.write.err, &attr, 0))
    {
        throw std::exception("Failed to create stderr pipe.");
    }
    if(!SetHandleInformation(this->pipes.read.err, HANDLE_FLAG_INHERIT, 0))
    {
        throw std::exception("Failed to unset stderr pipe inheritance flag.");
    }
}
void Terminal::initProcess()
{
    tstring     command;
    STARTUPINFO startup;
#ifdef UNICODE
    command.append(L"cmd /U /K "prompt ");
    command.append(PROMPT);
    command.append(L""");
#else
    command.append("cmd /A /K "prompt ");
    command.append(PROMPT);
    command.append(""");
#endif
    ZeroMemory(&this->process, sizeof(this->process));
    ZeroMemory(&startup, sizeof(startup));
    startup.cb = sizeof(startup);
    startup.dwFlags |= STARTF_USESTDHANDLES;
    startup.hStdInput = this->pipes.read.in;
    startup.hStdOutput = this->pipes.write.out;
    startup.hStdError = this->pipes.write.err;
    startup.dwFlags |= STARTF_USESHOWWINDOW;
    startup.wShowWindow = SW_HIDE;
    auto created = CreateProcess(
        nullptr,
        _tcsdup(command.c_str()),
        nullptr,
        nullptr,
        TRUE,
        0,
        nullptr,
        nullptr,
        &startup,
        &this->process
    );
    if (!created) {
        throw std::exception("Failed to create process.");
    }
}
void Terminal::read(OutputCallback callback)
{
    this->readUntil(L"", callback);
}
void Terminal::readLine(OutputCallback callback, bool buffer)
{
    this->readUntil(L"n", callback, buffer);
}
void Terminal::readUntil(const tstring& terminator, OutputCallback callback, bool buffer)
{
    auto terminatorIter = terminator.cbegin();
    auto terminatorEnd = terminator.cend();
    auto bufferIter = this->bufferedData.begin();
    auto bufferEnd = this->bufferedData.end();
    do {
        DWORD  bytesAvailable = 0;
        if (!PeekNamedPipe(this->pipes.read.out, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
            throw std::exception("Failed to peek command input pipe.");
        }
        if (bytesAvailable)
        {
            DWORD  bytesRead;
            tchar* data;
            data = new tchar[bytesAvailable / sizeof(tchar)];
            if (!ReadFile(this->pipes.read.out, data, bytesAvailable, &bytesRead, nullptr)) {
                throw std::exception("ReadFile failed.");
            }
            assert(bytesRead == bytesAvailable);
            auto iterDistance = bufferIter - this->bufferedData.begin();
            this->bufferedData.append(data, bytesRead / sizeof(tchar));
            bufferIter = this->bufferedData.begin() + iterDistance;
            bufferEnd = this->bufferedData.end();
        }
        if (terminator.empty()) {
            if (!this->bufferedData.empty())
            {
                bufferIter = bufferEnd;
                terminatorIter = terminatorEnd;
            }
        } else {
            while(bufferIter != bufferEnd && terminatorIter != terminatorEnd) {
                if (*bufferIter == *terminatorIter) {
                    ++terminatorIter;
                } else {
                    terminatorIter = terminator.begin();
                }
                ++bufferIter;
            }
        }
        if (!buffer || terminatorIter == terminatorEnd) {
            callback(tstring(this->bufferedData.begin(), bufferIter - terminator.length()));
            this->bufferedData.erase(this->bufferedData.begin(), bufferIter);
        }
    } while (terminatorIter != terminatorEnd);
}
void Terminal::readUntilPrompt(OutputCallback callback, bool buffer)
{
    this->readUntil(PROMPT, callback, buffer);
}
void Terminal::terminatePipes()
{
    if (this->pipes.read.err) {
        CloseHandle(this->pipes.read.err);
    }
    if (this->pipes.write.err) {
        CloseHandle(this->pipes.write.err);
    }
    if (this->pipes.read.out) {
        CloseHandle(this->pipes.read.out);
    }
    if (this->pipes.write.out) {
        CloseHandle(this->pipes.write.out);
    }
    if (this->pipes.read.in) {
        CloseHandle(this->pipes.read.in);
    }
    if (this->pipes.write.in) {
        CloseHandle(this->pipes.write.in);
    }
}
void Terminal::terminateProcess()
{
    if (this->process.hProcess) {
        CloseHandle(this->process.hProcess);
    }
}
void Terminal::write(const astring& data)
{
    DWORD byteCount;
    DWORD bytesWritten;
    byteCount = data.length();
    if (!WriteFile(this->pipes.write.in, data.c_str(), byteCount, &bytesWritten, nullptr)) {
        throw std::exception("WriteFile failed.");
    }
    assert(bytesWritten == byteCount);
}
原来

我是个白痴。 因为我只从 stdout 读取,所以我没有注意到 cmd 以消息的形式向 stderr 发送输出,"'bash' 不被识别为内部或外部命令、可操作程序或批处理文件。 所以我随后显示了命令的结果 dir "%windir%System32" | findstr bash.exe . 无。 空输出。 这很奇怪,我想。

事实证明,如果您运行的是 Windows 的 64 位副本,如果请求来自 32 位应用程序,它会将对 System32 的任何请求重定向到 SysWOW64。 Bash 已安装到 System32 中。 重新编译我的应用程序以在 64 位环境中运行,瞧,"bash -c ls"输出我的可执行文件运行的文件夹的内容。 整洁。