Reputation: 160
I'm trying to write a wrapper around bash, redirecting standard in/out/err to and from a parent process. So far, I've got a wrapper around Window's cmd.exe. I can type in a command and have it run in the console, then read the output of that command and display it to the user. So I thought it would be an easy task to wrap it around bash in the same manner. However... If I set the process to open bash, I get no output whatsoever. Same goes if I open a cmd process and run "bash" or even, as in the demonstration below, run a single command with the "-c" option. No output. I've compiled the smallest test case I could, which is as follows:
#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("\r\n");
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);
}
Upvotes: 0
Views: 1012
Reputation: 160
It turns out I'm an idiot. Because I was only ever reading from stdout, I didn't notice that cmd was sending output to stderr in the form of a message reading, "'bash' is not recognized as an internal or external command, operable program or batch file." So I then displayed the results of the command dir "%windir%\System32" | findstr bash.exe
. Nothing. Empty output. That was weird, I thought.
Turns out if you're running a 64-bit copy of Windows, it redirects any requests for System32 to SysWOW64 if the request is coming from a 32-bit application. Bash is installed to System32. Recompiled my application to run in a 64-bit environment et voilà, "bash -c ls" output the contents of the folder my executable was running in. Neat.
Upvotes: 1