Skip to content

Commit

Permalink
Capture and report errors from the Microsoft Bash Launcher (bash.exe)
Browse files Browse the repository at this point in the history
In the elevated/non-elevated situation, this output appears now:

    $ ./out/wslbridge.exe
    wslbridge error: failed to start backend process
    note: bash.exe output: Cannot launch bash because another instance is running elevated.  Elevated and un-elevated instances are not permitted to run simultaneously.

This work would have been a lot easier if bash.exe didn't put up a
"Press any key to continue" prompt after it prints the error.

wslbridge works around the prompt by synthesizing a VK_RETURN keypress into
the console.  wslbridge uses a new console, unrelated to the console
Cygwin is using (whether visible or hidden).  I don't want to disconnect
from the existing console, mostly because in theory, wslbridge.exe could
be the only process attached to a visible console.  Instead, wslbridge
spawns a copy of itself with a --press-return argument.  This child
attaches to the new bash.exe console to insert the VK_RETURN.

wslbridge assumes that stdout is UTF-16, while stderr is UTF-8.  This works
for now, at least.

I noticed that the access X_OK check wasn't working anymore.  Apparently
Cygwin considers these paths executable:

 - C:\some\path\cygprog.exe
 - C:\some\path\elf64prog
 - /cygdrive/c/some/path/cygprog.exe

But not this path:

 - /cygdrive/c/some/path/elf64prog

Anyway, as of 15063, and this commit, if the wslbridge-backend file isn't
executable, the underlying bash.exe error is reported:

    $ ./out/wslbridge.exe
    wslbridge error: failed to start backend process
    note: backend error output: /bin/bash: /mnt/c/rprichard/proj/wslbridge/out/wslbridge-backend: Permission denied

Fixes #13
  • Loading branch information
rprichard committed May 31, 2017
1 parent 54fd2aa commit fadef3f
Showing 1 changed file with 259 additions and 20 deletions.
279 changes: 259 additions & 20 deletions frontend/wslbridge.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <arpa/inet.h>
#include <assert.h>
#include <ctype.h>
#include <fcntl.h>
#include <getopt.h>
#include <locale.h>
Expand All @@ -25,6 +26,7 @@
#include <atomic>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
Expand Down Expand Up @@ -128,9 +130,12 @@ static std::wstring mbsToWcs(const std::string &s) {
return ret;
}

static std::string wcsToMbs(const std::wstring &s) {
static std::string wcsToMbs(const std::wstring &s, bool emptyOnError=false) {
const size_t len = wcstombs(nullptr, s.c_str(), 0);
if (len == static_cast<size_t>(-1)) {
if (emptyOnError) {
return {};
}
fatal("error: wcsToMbs: invalid string\n");
}
std::string ret;
Expand Down Expand Up @@ -748,13 +753,202 @@ static std::string formatErrorMessage(DWORD err) {
return ret;
}

struct PipeHandles {
HANDLE rh;
HANDLE wh;
};

static PipeHandles createPipe() {
SECURITY_ATTRIBUTES sa {};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
PipeHandles ret {};
const BOOL success = CreatePipe(&ret.rh, &ret.wh, &sa, 0);
assert(success && "CreatePipe failed");
return ret;
}

class StartupInfoAttributeList {
public:
StartupInfoAttributeList(PPROC_THREAD_ATTRIBUTE_LIST &attrList, int count) {
SIZE_T size {};
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
assert(size > 0 && "InitializeProcThreadAttributeList failed");
buffer_ = std::unique_ptr<char[]>(new char[size]);
const BOOL success = InitializeProcThreadAttributeList(get(), count, 0, &size);
assert(success && "InitializeProcThreadAttributeList failed");
attrList = get();
}
StartupInfoAttributeList(const StartupInfoAttributeList &) = delete;
StartupInfoAttributeList &operator=(const StartupInfoAttributeList &) = delete;
~StartupInfoAttributeList() {
DeleteProcThreadAttributeList(get());
}
private:
PPROC_THREAD_ATTRIBUTE_LIST get() {
return reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(buffer_.get());
}
std::unique_ptr<char[]> buffer_;
};

class StartupInfoInheritList {
public:
StartupInfoInheritList(PPROC_THREAD_ATTRIBUTE_LIST attrList,
std::vector<HANDLE> &&inheritList) :
inheritList_(std::move(inheritList)) {
const BOOL success = UpdateProcThreadAttribute(
attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
inheritList_.data(), inheritList_.size() * sizeof(HANDLE),
nullptr, nullptr);
assert(success && "UpdateProcThreadAttribute failed");
}
StartupInfoInheritList(const StartupInfoInheritList &) = delete;
StartupInfoInheritList &operator=(const StartupInfoInheritList &) = delete;
~StartupInfoInheritList() {}
private:
std::vector<HANDLE> inheritList_;
};

// WSL bash will print an error if the user tries to run elevated and
// non-elevated instances simultaneously, and maybe other situations. We'd
// like to detect this situation and report the error back to the user.
//
// Two complications:
// - WSL bash will print the error to stdout/stderr, but if the file is a
// pipe, then WSL bash doesn't print it until it exits (presumably due to
// block buffering).
// - WSL bash puts up a prompt, "Press any key to continue", and it reads
// that key from the attached console, not from stdin.
//
// This function spawns the frontend again and instructs it to attach to the
// new WSL bash console and send it a return keypress.
//
// The HANDLE must be inheritable.
static void spawnPressReturnProcess(HANDLE bashProcess) {
const auto exePath = getModuleFileName(getCurrentModule());
std::wstring cmdline;
cmdline.append(L"\"");
cmdline.append(exePath);
cmdline.append(L"\" --press-return ");
cmdline.append(std::to_wstring(reinterpret_cast<uintptr_t>(bashProcess)));
STARTUPINFOEXW sui {};
sui.StartupInfo.cb = sizeof(sui);
StartupInfoAttributeList attrList { sui.lpAttributeList, 1 };
StartupInfoInheritList inheritList { sui.lpAttributeList, { bashProcess } };
PROCESS_INFORMATION pi {};
const BOOL success = CreateProcessW(exePath.c_str(), &cmdline[0], nullptr, nullptr,
true, 0, nullptr, nullptr, &sui.StartupInfo, &pi);
if (!success) {
fprintf(stderr, "wslbridge warning: could not spawn: %s\n", wcsToMbs(cmdline).c_str());
}
if (WaitForSingleObject(pi.hProcess, 10000) != WAIT_OBJECT_0) {
fprintf(stderr, "wslbridge warning: process didn't exit after 10 seconds: %ls\n",
cmdline.c_str());
} else {
DWORD code {};
BOOL success = GetExitCodeProcess(pi.hProcess, &code);
if (!success) {
fprintf(stderr, "wslbridge warning: GetExitCodeProcess failed\n");
} else if (code != 0) {
fprintf(stderr, "wslbridge warning: process failed: %ls\n", cmdline.c_str());
}
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}

static int handlePressReturn(const char *pidStr) {
// AttachConsole replaces STD_INPUT_HANDLE with a new console input
// handle. See https://github.com/rprichard/win32-console-docs. The
// bash.exe process has already started, but console creation and
// process creation don't happen atomically, so poll for the console's
// existence.
auto str2handle = [](const char *str) {
std::stringstream ss(str);
uintptr_t n {};
ss >> n;
return reinterpret_cast<HANDLE>(n);
};
const HANDLE bashProcess = str2handle(pidStr);
const DWORD bashPid = GetProcessId(bashProcess);
FreeConsole();
for (int i = 0; i < 400; ++i) {
if (WaitForSingleObject(bashProcess, 0) == WAIT_OBJECT_0) {
// bash.exe has exited, give up immediately.
return 0;
} else if (AttachConsole(bashPid)) {
std::array<INPUT_RECORD, 2> ir {};
ir[0].EventType = KEY_EVENT;
ir[0].Event.KeyEvent.bKeyDown = TRUE;
ir[0].Event.KeyEvent.wRepeatCount = 1;
ir[0].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
ir[0].Event.KeyEvent.wVirtualScanCode = MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC);
ir[0].Event.KeyEvent.uChar.UnicodeChar = '\r';
ir[1] = ir[0];
ir[1].Event.KeyEvent.bKeyDown = FALSE;
DWORD actual {};
WriteConsoleInputW(
GetStdHandle(STD_INPUT_HANDLE),
ir.data(), ir.size(), &actual);
return 0;
}
Sleep(25);
}
return 1;
}

static std::vector<char> readAllFromHandle(HANDLE h) {
std::vector<char> ret;
char buf[1024];
DWORD actual {};
while (ReadFile(h, buf, sizeof(buf), &actual, nullptr) && actual > 0) {
ret.insert(ret.end(), buf, buf + actual);
}
return ret;
}

static std::tuple<DWORD, DWORD, DWORD> windowsVersion() {
OSVERSIONINFO info {};
info.dwOSVersionInfoSize = sizeof(info);
const BOOL success = GetVersionEx(&info);
assert(success && "GetVersionEx failed");
if (info.dwMajorVersion == 6 && info.dwMinorVersion == 2) {
// We want to distinguish between Windows 10.0.14393 and 10.0.15063,
// but if the EXE doesn't have an appropriate manifest, then
// GetVersionEx will report the lesser of 6.2 and the true version.
fprintf(stderr, "wslbridge warning: GetVersionEx reports version 6.2 -- "
"is wslbridge.exe properly manifested?\n");
}
return std::make_tuple(info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber);
}

static std::string replaceAll(std::string str, const std::string &from, const std::string &to) {
size_t pos {};
while ((pos = str.find(from, pos)) != std::string::npos) {
str = str.replace(pos, from.size(), to);
pos += to.size();
}
return str;
}

static std::string stripTrailing(std::string str) {
while (!str.empty() && isspace(str.back())) {
str.pop_back();
}
return str;
}

} // namespace

int main(int argc, char *argv[]) {
setlocale(LC_ALL, "");
cygwin_internal(CW_SYNC_WINENV);
g_wakeupFd = new WakeupFd();

if (argc == 3 && !strcmp(argv[1], "--press-return")) {
return handlePressReturn(argv[2]);
}

Environment env;
std::string spawnCwd;
enum class TtyRequest { Auto, Yes, No, Force } ttyRequest = TtyRequest::Auto;
Expand Down Expand Up @@ -916,40 +1110,85 @@ int main(int argc, char *argv[]) {
cmdLine.append(L"\" -c ");
appendBashArg(cmdLine, bashCmdLine);

STARTUPINFOW sui = {};
sui.cb = sizeof(sui);
const auto outputPipe = createPipe();
const auto errorPipe = createPipe();
STARTUPINFOEXW sui {};
sui.StartupInfo.cb = sizeof(sui);
StartupInfoAttributeList attrList { sui.lpAttributeList, 1 };
StartupInfoInheritList inheritList { sui.lpAttributeList,
{ outputPipe.wh, errorPipe.wh }
};

if (windowsVersion() >= std::make_tuple(10u, 0u, 15063u)) {
// WSL was first officially shipped in 14393, but in that version,
// bash.exe did not allow redirecting stdout/stderr to a pipe.
// Redirection is allowed starting with 15063, and we'd like to use it
// to help report errors.
sui.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
sui.StartupInfo.hStdOutput = outputPipe.wh;
sui.StartupInfo.hStdError = errorPipe.wh;
}

PROCESS_INFORMATION pi = {};
BOOL success = CreateProcessW(bashPath.c_str(), &cmdLine[0], nullptr, nullptr,
false,
true,
debugFork ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW,
nullptr, nullptr, &sui, &pi);
nullptr, nullptr, &sui.StartupInfo, &pi);
if (!success) {
fatal("error starting bash.exe adapter: %s\n",
formatErrorMessage(GetLastError()).c_str());
}

CloseHandle(outputPipe.wh);
CloseHandle(errorPipe.wh);
success = SetHandleInformation(pi.hProcess, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
assert(success && "SetHandleInformation failed");
spawnPressReturnProcess(pi.hProcess);

std::atomic<bool> backendStarted = { false };

// If the backend process exits before the frontend, then something has
// gone wrong.
const auto watchdog = std::thread([&]() {
WaitForSingleObject(pi.hProcess, INFINITE);

// Because bash.exe has exited, we know that the write ends of the
// output pipes are closed. Finish reading anything bash.exe wrote.
// bash.exe writes at least one error via stdout in UTF-16;
// wslbridge-backend could write to stderr in UTF-8.
auto outVec = readAllFromHandle(outputPipe.rh);
auto errVec = readAllFromHandle(errorPipe.rh);
std::wstring outWide(outVec.size() / sizeof(wchar_t), L'\0');
memcpy(&outWide[0], outVec.data(), outWide.size() * sizeof(wchar_t));
std::string out { wcsToMbs(outWide, true) };
std::string err { errVec.begin(), errVec.end() };
out = stripTrailing(replaceAll(out, "Press any key to continue...", ""));
err = stripTrailing(err);

std::string msg;
if (backendStarted) {
g_terminalState.fatal("\nwslbridge error: backend process died\n");
}
std::string msg = "wslbridge error: failed to start backend process\n";
msg.append("note: backend program is at '");
msg.append(wcsToMbs(backendPathWin));
msg.append("'\n");
if (access(wcsToMbs(backendPathWin).c_str(), X_OK) == -1 && errno == EACCES) {
msg.append("note: the backend file is not executable "
"(use 'chmod +x' on it?)\n");
}
if (fsname != L"NTFS") {
msg.append("note: backend is on a volume of type '");
msg.append(wcsToMbs(fsname));
msg.append("', expected 'NTFS'\n"
"note: WSL only supports local NTFS volumes\n");
msg = "\nwslbridge error: backend process died\n";
} else {
msg = "wslbridge error: failed to start backend process\n";
if (fsname != L"NTFS") {
msg.append("note: backend program is at '");
msg.append(wcsToMbs(backendPathWin));
msg.append("'\n");
msg.append("note: backend is on a volume of type '");
msg.append(wcsToMbs(fsname));
msg.append("', expected 'NTFS'\n"
"note: WSL only supports local NTFS volumes\n");
}
}
if (!out.empty()) {
msg.append("note: bash.exe output: ");
msg.append(out);
msg.push_back('\n');
}
if (!err.empty()) {
msg.append("note: backend error output: ");
msg.append(err);
msg.push_back('\n');
}
g_terminalState.fatal("%s", msg.c_str());
});
Expand Down

0 comments on commit fadef3f

Please sign in to comment.