Skip to content

Commit fadef3f

Browse files
committed
Capture and report errors from the Microsoft Bash Launcher (bash.exe)
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
1 parent 54fd2aa commit fadef3f

File tree

1 file changed

+259
-20
lines changed

1 file changed

+259
-20
lines changed

frontend/wslbridge.cc

Lines changed: 259 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <arpa/inet.h>
44
#include <assert.h>
5+
#include <ctype.h>
56
#include <fcntl.h>
67
#include <getopt.h>
78
#include <locale.h>
@@ -25,6 +26,7 @@
2526
#include <atomic>
2627
#include <memory>
2728
#include <mutex>
29+
#include <sstream>
2830
#include <string>
2931
#include <thread>
3032
#include <utility>
@@ -128,9 +130,12 @@ static std::wstring mbsToWcs(const std::string &s) {
128130
return ret;
129131
}
130132

131-
static std::string wcsToMbs(const std::wstring &s) {
133+
static std::string wcsToMbs(const std::wstring &s, bool emptyOnError=false) {
132134
const size_t len = wcstombs(nullptr, s.c_str(), 0);
133135
if (len == static_cast<size_t>(-1)) {
136+
if (emptyOnError) {
137+
return {};
138+
}
134139
fatal("error: wcsToMbs: invalid string\n");
135140
}
136141
std::string ret;
@@ -748,13 +753,202 @@ static std::string formatErrorMessage(DWORD err) {
748753
return ret;
749754
}
750755

756+
struct PipeHandles {
757+
HANDLE rh;
758+
HANDLE wh;
759+
};
760+
761+
static PipeHandles createPipe() {
762+
SECURITY_ATTRIBUTES sa {};
763+
sa.nLength = sizeof(sa);
764+
sa.bInheritHandle = TRUE;
765+
PipeHandles ret {};
766+
const BOOL success = CreatePipe(&ret.rh, &ret.wh, &sa, 0);
767+
assert(success && "CreatePipe failed");
768+
return ret;
769+
}
770+
771+
class StartupInfoAttributeList {
772+
public:
773+
StartupInfoAttributeList(PPROC_THREAD_ATTRIBUTE_LIST &attrList, int count) {
774+
SIZE_T size {};
775+
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
776+
assert(size > 0 && "InitializeProcThreadAttributeList failed");
777+
buffer_ = std::unique_ptr<char[]>(new char[size]);
778+
const BOOL success = InitializeProcThreadAttributeList(get(), count, 0, &size);
779+
assert(success && "InitializeProcThreadAttributeList failed");
780+
attrList = get();
781+
}
782+
StartupInfoAttributeList(const StartupInfoAttributeList &) = delete;
783+
StartupInfoAttributeList &operator=(const StartupInfoAttributeList &) = delete;
784+
~StartupInfoAttributeList() {
785+
DeleteProcThreadAttributeList(get());
786+
}
787+
private:
788+
PPROC_THREAD_ATTRIBUTE_LIST get() {
789+
return reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(buffer_.get());
790+
}
791+
std::unique_ptr<char[]> buffer_;
792+
};
793+
794+
class StartupInfoInheritList {
795+
public:
796+
StartupInfoInheritList(PPROC_THREAD_ATTRIBUTE_LIST attrList,
797+
std::vector<HANDLE> &&inheritList) :
798+
inheritList_(std::move(inheritList)) {
799+
const BOOL success = UpdateProcThreadAttribute(
800+
attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
801+
inheritList_.data(), inheritList_.size() * sizeof(HANDLE),
802+
nullptr, nullptr);
803+
assert(success && "UpdateProcThreadAttribute failed");
804+
}
805+
StartupInfoInheritList(const StartupInfoInheritList &) = delete;
806+
StartupInfoInheritList &operator=(const StartupInfoInheritList &) = delete;
807+
~StartupInfoInheritList() {}
808+
private:
809+
std::vector<HANDLE> inheritList_;
810+
};
811+
812+
// WSL bash will print an error if the user tries to run elevated and
813+
// non-elevated instances simultaneously, and maybe other situations. We'd
814+
// like to detect this situation and report the error back to the user.
815+
//
816+
// Two complications:
817+
// - WSL bash will print the error to stdout/stderr, but if the file is a
818+
// pipe, then WSL bash doesn't print it until it exits (presumably due to
819+
// block buffering).
820+
// - WSL bash puts up a prompt, "Press any key to continue", and it reads
821+
// that key from the attached console, not from stdin.
822+
//
823+
// This function spawns the frontend again and instructs it to attach to the
824+
// new WSL bash console and send it a return keypress.
825+
//
826+
// The HANDLE must be inheritable.
827+
static void spawnPressReturnProcess(HANDLE bashProcess) {
828+
const auto exePath = getModuleFileName(getCurrentModule());
829+
std::wstring cmdline;
830+
cmdline.append(L"\"");
831+
cmdline.append(exePath);
832+
cmdline.append(L"\" --press-return ");
833+
cmdline.append(std::to_wstring(reinterpret_cast<uintptr_t>(bashProcess)));
834+
STARTUPINFOEXW sui {};
835+
sui.StartupInfo.cb = sizeof(sui);
836+
StartupInfoAttributeList attrList { sui.lpAttributeList, 1 };
837+
StartupInfoInheritList inheritList { sui.lpAttributeList, { bashProcess } };
838+
PROCESS_INFORMATION pi {};
839+
const BOOL success = CreateProcessW(exePath.c_str(), &cmdline[0], nullptr, nullptr,
840+
true, 0, nullptr, nullptr, &sui.StartupInfo, &pi);
841+
if (!success) {
842+
fprintf(stderr, "wslbridge warning: could not spawn: %s\n", wcsToMbs(cmdline).c_str());
843+
}
844+
if (WaitForSingleObject(pi.hProcess, 10000) != WAIT_OBJECT_0) {
845+
fprintf(stderr, "wslbridge warning: process didn't exit after 10 seconds: %ls\n",
846+
cmdline.c_str());
847+
} else {
848+
DWORD code {};
849+
BOOL success = GetExitCodeProcess(pi.hProcess, &code);
850+
if (!success) {
851+
fprintf(stderr, "wslbridge warning: GetExitCodeProcess failed\n");
852+
} else if (code != 0) {
853+
fprintf(stderr, "wslbridge warning: process failed: %ls\n", cmdline.c_str());
854+
}
855+
}
856+
CloseHandle(pi.hProcess);
857+
CloseHandle(pi.hThread);
858+
}
859+
860+
static int handlePressReturn(const char *pidStr) {
861+
// AttachConsole replaces STD_INPUT_HANDLE with a new console input
862+
// handle. See https://github.com/rprichard/win32-console-docs. The
863+
// bash.exe process has already started, but console creation and
864+
// process creation don't happen atomically, so poll for the console's
865+
// existence.
866+
auto str2handle = [](const char *str) {
867+
std::stringstream ss(str);
868+
uintptr_t n {};
869+
ss >> n;
870+
return reinterpret_cast<HANDLE>(n);
871+
};
872+
const HANDLE bashProcess = str2handle(pidStr);
873+
const DWORD bashPid = GetProcessId(bashProcess);
874+
FreeConsole();
875+
for (int i = 0; i < 400; ++i) {
876+
if (WaitForSingleObject(bashProcess, 0) == WAIT_OBJECT_0) {
877+
// bash.exe has exited, give up immediately.
878+
return 0;
879+
} else if (AttachConsole(bashPid)) {
880+
std::array<INPUT_RECORD, 2> ir {};
881+
ir[0].EventType = KEY_EVENT;
882+
ir[0].Event.KeyEvent.bKeyDown = TRUE;
883+
ir[0].Event.KeyEvent.wRepeatCount = 1;
884+
ir[0].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
885+
ir[0].Event.KeyEvent.wVirtualScanCode = MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC);
886+
ir[0].Event.KeyEvent.uChar.UnicodeChar = '\r';
887+
ir[1] = ir[0];
888+
ir[1].Event.KeyEvent.bKeyDown = FALSE;
889+
DWORD actual {};
890+
WriteConsoleInputW(
891+
GetStdHandle(STD_INPUT_HANDLE),
892+
ir.data(), ir.size(), &actual);
893+
return 0;
894+
}
895+
Sleep(25);
896+
}
897+
return 1;
898+
}
899+
900+
static std::vector<char> readAllFromHandle(HANDLE h) {
901+
std::vector<char> ret;
902+
char buf[1024];
903+
DWORD actual {};
904+
while (ReadFile(h, buf, sizeof(buf), &actual, nullptr) && actual > 0) {
905+
ret.insert(ret.end(), buf, buf + actual);
906+
}
907+
return ret;
908+
}
909+
910+
static std::tuple<DWORD, DWORD, DWORD> windowsVersion() {
911+
OSVERSIONINFO info {};
912+
info.dwOSVersionInfoSize = sizeof(info);
913+
const BOOL success = GetVersionEx(&info);
914+
assert(success && "GetVersionEx failed");
915+
if (info.dwMajorVersion == 6 && info.dwMinorVersion == 2) {
916+
// We want to distinguish between Windows 10.0.14393 and 10.0.15063,
917+
// but if the EXE doesn't have an appropriate manifest, then
918+
// GetVersionEx will report the lesser of 6.2 and the true version.
919+
fprintf(stderr, "wslbridge warning: GetVersionEx reports version 6.2 -- "
920+
"is wslbridge.exe properly manifested?\n");
921+
}
922+
return std::make_tuple(info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber);
923+
}
924+
925+
static std::string replaceAll(std::string str, const std::string &from, const std::string &to) {
926+
size_t pos {};
927+
while ((pos = str.find(from, pos)) != std::string::npos) {
928+
str = str.replace(pos, from.size(), to);
929+
pos += to.size();
930+
}
931+
return str;
932+
}
933+
934+
static std::string stripTrailing(std::string str) {
935+
while (!str.empty() && isspace(str.back())) {
936+
str.pop_back();
937+
}
938+
return str;
939+
}
940+
751941
} // namespace
752942

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

948+
if (argc == 3 && !strcmp(argv[1], "--press-return")) {
949+
return handlePressReturn(argv[2]);
950+
}
951+
758952
Environment env;
759953
std::string spawnCwd;
760954
enum class TtyRequest { Auto, Yes, No, Force } ttyRequest = TtyRequest::Auto;
@@ -916,40 +1110,85 @@ int main(int argc, char *argv[]) {
9161110
cmdLine.append(L"\" -c ");
9171111
appendBashArg(cmdLine, bashCmdLine);
9181112

919-
STARTUPINFOW sui = {};
920-
sui.cb = sizeof(sui);
1113+
const auto outputPipe = createPipe();
1114+
const auto errorPipe = createPipe();
1115+
STARTUPINFOEXW sui {};
1116+
sui.StartupInfo.cb = sizeof(sui);
1117+
StartupInfoAttributeList attrList { sui.lpAttributeList, 1 };
1118+
StartupInfoInheritList inheritList { sui.lpAttributeList,
1119+
{ outputPipe.wh, errorPipe.wh }
1120+
};
1121+
1122+
if (windowsVersion() >= std::make_tuple(10u, 0u, 15063u)) {
1123+
// WSL was first officially shipped in 14393, but in that version,
1124+
// bash.exe did not allow redirecting stdout/stderr to a pipe.
1125+
// Redirection is allowed starting with 15063, and we'd like to use it
1126+
// to help report errors.
1127+
sui.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
1128+
sui.StartupInfo.hStdOutput = outputPipe.wh;
1129+
sui.StartupInfo.hStdError = errorPipe.wh;
1130+
}
1131+
9211132
PROCESS_INFORMATION pi = {};
9221133
BOOL success = CreateProcessW(bashPath.c_str(), &cmdLine[0], nullptr, nullptr,
923-
false,
1134+
true,
9241135
debugFork ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW,
925-
nullptr, nullptr, &sui, &pi);
1136+
nullptr, nullptr, &sui.StartupInfo, &pi);
9261137
if (!success) {
9271138
fatal("error starting bash.exe adapter: %s\n",
9281139
formatErrorMessage(GetLastError()).c_str());
9291140
}
9301141

1142+
CloseHandle(outputPipe.wh);
1143+
CloseHandle(errorPipe.wh);
1144+
success = SetHandleInformation(pi.hProcess, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
1145+
assert(success && "SetHandleInformation failed");
1146+
spawnPressReturnProcess(pi.hProcess);
1147+
9311148
std::atomic<bool> backendStarted = { false };
9321149

9331150
// If the backend process exits before the frontend, then something has
9341151
// gone wrong.
9351152
const auto watchdog = std::thread([&]() {
9361153
WaitForSingleObject(pi.hProcess, INFINITE);
1154+
1155+
// Because bash.exe has exited, we know that the write ends of the
1156+
// output pipes are closed. Finish reading anything bash.exe wrote.
1157+
// bash.exe writes at least one error via stdout in UTF-16;
1158+
// wslbridge-backend could write to stderr in UTF-8.
1159+
auto outVec = readAllFromHandle(outputPipe.rh);
1160+
auto errVec = readAllFromHandle(errorPipe.rh);
1161+
std::wstring outWide(outVec.size() / sizeof(wchar_t), L'\0');
1162+
memcpy(&outWide[0], outVec.data(), outWide.size() * sizeof(wchar_t));
1163+
std::string out { wcsToMbs(outWide, true) };
1164+
std::string err { errVec.begin(), errVec.end() };
1165+
out = stripTrailing(replaceAll(out, "Press any key to continue...", ""));
1166+
err = stripTrailing(err);
1167+
1168+
std::string msg;
9371169
if (backendStarted) {
938-
g_terminalState.fatal("\nwslbridge error: backend process died\n");
939-
}
940-
std::string msg = "wslbridge error: failed to start backend process\n";
941-
msg.append("note: backend program is at '");
942-
msg.append(wcsToMbs(backendPathWin));
943-
msg.append("'\n");
944-
if (access(wcsToMbs(backendPathWin).c_str(), X_OK) == -1 && errno == EACCES) {
945-
msg.append("note: the backend file is not executable "
946-
"(use 'chmod +x' on it?)\n");
947-
}
948-
if (fsname != L"NTFS") {
949-
msg.append("note: backend is on a volume of type '");
950-
msg.append(wcsToMbs(fsname));
951-
msg.append("', expected 'NTFS'\n"
952-
"note: WSL only supports local NTFS volumes\n");
1170+
msg = "\nwslbridge error: backend process died\n";
1171+
} else {
1172+
msg = "wslbridge error: failed to start backend process\n";
1173+
if (fsname != L"NTFS") {
1174+
msg.append("note: backend program is at '");
1175+
msg.append(wcsToMbs(backendPathWin));
1176+
msg.append("'\n");
1177+
msg.append("note: backend is on a volume of type '");
1178+
msg.append(wcsToMbs(fsname));
1179+
msg.append("', expected 'NTFS'\n"
1180+
"note: WSL only supports local NTFS volumes\n");
1181+
}
1182+
}
1183+
if (!out.empty()) {
1184+
msg.append("note: bash.exe output: ");
1185+
msg.append(out);
1186+
msg.push_back('\n');
1187+
}
1188+
if (!err.empty()) {
1189+
msg.append("note: backend error output: ");
1190+
msg.append(err);
1191+
msg.push_back('\n');
9531192
}
9541193
g_terminalState.fatal("%s", msg.c_str());
9551194
});

0 commit comments

Comments
 (0)