diff --git a/frontend/wslbridge.cc b/frontend/wslbridge.cc index 2d62e7e..978ac04 100644 --- a/frontend/wslbridge.cc +++ b/frontend/wslbridge.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -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(-1)) { + if (emptyOnError) { + return {}; + } fatal("error: wcsToMbs: invalid string\n"); } std::string ret; @@ -748,6 +753,191 @@ 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(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(buffer_.get()); + } + std::unique_ptr buffer_; +}; + +class StartupInfoInheritList { +public: + StartupInfoInheritList(PPROC_THREAD_ATTRIBUTE_LIST attrList, + std::vector &&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 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(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(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 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 readAllFromHandle(HANDLE h) { + std::vector 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 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[]) { @@ -755,6 +945,10 @@ int main(int argc, char *argv[]) { 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; @@ -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 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()); });