├── .gitattributes ├── .gitignore ├── CHANGES.md ├── LICENSE.txt ├── README.md ├── VERSION.txt ├── backend ├── Makefile └── wslbridge-backend.cc ├── common ├── SocketIo.cc └── SocketIo.h ├── dist ├── appveyor.yml ├── cygwin-prebuilts │ ├── appveyor.yml │ ├── dllversion.py │ ├── make-cygwin-prebuilts.py │ └── make-msys2-prebuilts.py ├── make-backend-package.py ├── make-bin-package.bat ├── make-frontend-packages.py └── util.py └── frontend ├── Makefile └── wslbridge.cc /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | Makefile text 3 | .gitattributes text 4 | .gitignore text 5 | *.bat text 6 | *.cc text 7 | *.h text 8 | *.md text 9 | *.txt text 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /out 3 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Version 0.2.5 (upcoming version) 2 | 3 | * When available, use `wslpath` to find the WSL path to `wslbridge-backend` 4 | rather than assume it is available on a `/mnt/` mount. Fixes a problem 5 | with custom mounts in `/etc/wsl.conf`. 6 | [#22](https://github.com/rprichard/wslbridge/issues/22) 7 | 8 | * By default, start the user's configured shell rather than assume it is 9 | `/bin/bash`, and invoke it in login mode (i.e. prefix argv[0] with a dash). 10 | Add `-l` (and `--no-login`) options to allow overriding the default login 11 | shell. (`wslbridge -l zsh` and `wslbridge -t zsh -l` will both create a 12 | login shell with a pty, but the first command does so using a `-zsh` 13 | argv[0].) 14 | [#18](https://github.com/rprichard/wslbridge/issues/18) 15 | 16 | * Added a --backend option to specify a custom path to wslbridge-backend. 17 | [#23](https://github.com/rprichard/wslbridge/issues/23) 18 | 19 | # Version 0.2.4 (2017-08-14) 20 | 21 | Changes since 0.2.3 22 | 23 | * Added a --distro-guid option that allows selecting which WSL Linux 24 | distribution to use. 25 | 26 | # Version 0.2.3 (2017-06-30) 27 | 28 | Changes since 0.2.2 29 | 30 | * Binaries are linked with -static-libgcc and -static-libstdc++. 31 | 32 | * Build the binaries with an Ubuntu 14.04 WSL installation. The binaries 33 | for 0.2.2 were built with Ubuntu 16.04, and do not work on 14.04. 34 | 35 | # Version 0.2.2 (2017-06-23) 36 | 37 | Changes since 0.2.1 38 | 39 | * Fix a race condition affecting spawn failure. 40 | 41 | * Capture and report errors from the Microsoft Bash Launcher (bash.exe). 42 | In particular, don't hang if the user tries to simultaneously run elevated 43 | and non-elevated WSL instances. 44 | [#13](https://github.com/rprichard/wslbridge/issues/13) 45 | 46 | # Version 0.2.1 (2016-11-14) 47 | 48 | Changes since 0.2.0 49 | 50 | * The frontend now synchronizes the Cygwin environment with the Win32 51 | environment before creating the WSL backend process. 52 | https://github.com/mintty/wsltty/issues/14 53 | 54 | # Version 0.2.0 (2016-09-28) 55 | 56 | Changes since 0.1.0: 57 | 58 | * Added a `-C` option that changes the working directory before running the 59 | command. The argument is a WSL path. A `~` component at the start of the 60 | argument is replaced with the user's home directory. (i.e. `$HOME`). 61 | [#2](https://github.com/rprichard/wslbridge/issues/2) 62 | 63 | * The frontend now canonicalizes its path to the `wslbridge-backend` binary. 64 | [#4](https://github.com/rprichard/wslbridge/issues/4) 65 | 66 | # Version 0.1.0 (2016-08-17) 67 | 68 | Initial release 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan Prichard 4 | Copyright (c) 2017-2018 Google LLC 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including without limitation the 9 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wslbridge 2 | 3 | wslbridge is a Cygwin program that allows connecting to the WSL command-line 4 | environment over TCP sockets, as with ssh, but without the overhead of 5 | configuring an SSH server. 6 | 7 | ## Building wslbridge 8 | 9 | You'll need a Cygwin (32 or 64 bit) environment, as well as a WSL environment 10 | (or any other Linux environment). Make sure you have the g++ and make packages 11 | installed. 12 | 13 | In Cygwin: 14 | 15 | $ cd frontend 16 | make 17 | 18 | In WSL/Linux: 19 | 20 | $ cd backend 21 | make 22 | 23 | The Cygwin frontend program is written to `out/wslbridge.exe`. The ELF64 24 | backend program is written to `out/wslbridge-backend`. The files can be copied 25 | somewhere more convenient, but they need to be on a letter drive (e.g. not a 26 | `\\server\share\...` UNC path). They also need to be on an NTFS volume. The 27 | frontend looks for the backend in its own directory. 28 | 29 | ## Usage 30 | 31 | Usage is similar to that of `ssh`. Run `wslbridge` with no arguments to start 32 | a bash session in a WSL pty. Append a command-line to run that command in WSL 33 | without a pty (i.e. using 3 pipes for stdio). 34 | 35 | `wslbridge` runs its WSL command with either a pty or using pipes. Pass `-t` 36 | to enable pty mode or `-T` to enable pipe mode. Pass `-t -t` to force pty mode 37 | even if stdin is not a terminal. 38 | 39 | Pass `-eVAR=VAL` to set an environment variable within WSL. Pass just `-eVAR` 40 | to copy the value from the Cygwin environment. 41 | 42 | ## wsltty (mintty/wslbridge package) 43 | 44 | The mintty project provides [wsltty](https://github.com/mintty/wsltty), a 45 | standalone WSL terminal package containing mintty, wslbridge, and part of 46 | Cygwin (e.g. the runtime library). 47 | 48 | ## Copyright 49 | 50 | This project is distributed under the MIT license (see the `LICENSE` file in 51 | the project root). 52 | 53 | By submitting a pull request for this project, you agree to license your 54 | contribution under the MIT license to this project. 55 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 0.2.5-dev 2 | -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | STRIP ?= strip 2 | 3 | all : ../out/wslbridge-backend 4 | 5 | ../out/wslbridge-backend : wslbridge-backend.cc ../common/SocketIo.cc ../common/SocketIo.h ../VERSION.txt Makefile 6 | mkdir -p ../out 7 | $(CXX) -std=c++11 -fno-exceptions \ 8 | -static-libgcc -static-libstdc++ \ 9 | -D_GNU_SOURCE \ 10 | -DWSLBRIDGE_VERSION=$(shell cat ../VERSION.txt) \ 11 | -Wall -O2 $< ../common/SocketIo.cc -o $@ -lutil -pthread 12 | $(STRIP) $@ 13 | 14 | clean: 15 | rm -f ../out/wslbridge-backend 16 | -------------------------------------------------------------------------------- /backend/wslbridge-backend.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "../common/SocketIo.h" 32 | 33 | namespace { 34 | 35 | static int connectSocket(int port, const std::string &key) { 36 | const int s = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); 37 | 38 | setSocketNoDelay(s); 39 | 40 | sockaddr_in addr = {}; 41 | addr.sin_family = AF_INET; 42 | addr.sin_port = htons(port); 43 | addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 44 | const int connectRet = connect(s, reinterpret_cast(&addr), sizeof(addr)); 45 | assert(connectRet == 0); 46 | 47 | size_t i = 0; 48 | while (i < key.size()) { 49 | const size_t remaining = key.size() - i; 50 | const ssize_t actual = write(s, &key[i], remaining); 51 | assert(actual > 0 && static_cast(actual) <= remaining); 52 | i += actual; 53 | } 54 | 55 | return s; 56 | } 57 | 58 | static std::string resolveCwd(const std::string &cwd) { 59 | if (cwd == "~" || cwd.substr(0, 2) == "~/") { 60 | const auto home = getenv("HOME") ?: ""; 61 | return home + cwd.substr(1); 62 | } 63 | return cwd; 64 | } 65 | 66 | struct ChildParams { 67 | bool usePty = false; 68 | int cols = -1; 69 | int rows = -1; 70 | std::vector env; 71 | std::string prog; 72 | std::vector argv; 73 | std::string cwd; 74 | }; 75 | 76 | struct Child { 77 | SpawnError spawnError = {}; 78 | pid_t pid = -1; 79 | int masterFd = -1; 80 | int inputFd = -1; 81 | int outputFd = -1; 82 | int errorFd = -1; 83 | }; 84 | 85 | class UniqueFd { 86 | int fd_; 87 | public: 88 | int fd() const { return fd_; } 89 | UniqueFd() : fd_(-1) {} 90 | explicit UniqueFd(int fd) : fd_(fd) {} 91 | ~UniqueFd() { close(); } 92 | int release() { 93 | const int ret = fd_; 94 | fd_ = -1; 95 | return ret; 96 | } 97 | void close() { 98 | if (fd_ != -1) { 99 | ::close(fd_); 100 | fd_ = -1; 101 | } 102 | } 103 | UniqueFd(const UniqueFd &other) = delete; 104 | UniqueFd &operator=(const UniqueFd &other) = delete; 105 | UniqueFd(UniqueFd &&other) : fd_(other.release()) {} 106 | UniqueFd &operator=(UniqueFd &&other) { 107 | close(); 108 | fd_ = other.release(); 109 | return *this; 110 | } 111 | }; 112 | 113 | struct PipePair { 114 | UniqueFd read; 115 | UniqueFd write; 116 | }; 117 | 118 | struct ProcessPipes { 119 | UniqueFd inputPipe; 120 | UniqueFd outputPipe; 121 | UniqueFd errorPipe; 122 | }; 123 | 124 | static PipePair makePipePair(int oflags) { 125 | int vals[2]; 126 | if (pipe2(vals, oflags) != 0) { 127 | fatalPerror("error: pipe2 failed"); 128 | } 129 | return PipePair { 130 | UniqueFd(vals[0]), 131 | UniqueFd(vals[1]) 132 | }; 133 | } 134 | 135 | static pid_t forkPipes(ProcessPipes &out) { 136 | auto inputPipe = makePipePair(0); 137 | auto outputPipe = makePipePair(0); 138 | auto errorPipe = makePipePair(0); 139 | const pid_t child = fork(); 140 | if (child == static_cast(-1)) { 141 | // Do nothing. 142 | } else if (child == 0) { 143 | close(STDIN_FILENO); 144 | close(STDOUT_FILENO); 145 | close(STDERR_FILENO); 146 | dup2(inputPipe.read.fd(), STDIN_FILENO); 147 | dup2(outputPipe.write.fd(), STDOUT_FILENO); 148 | dup2(errorPipe.write.fd(), STDERR_FILENO); 149 | } else { 150 | inputPipe.read.close(); 151 | outputPipe.write.close(); 152 | errorPipe.write.close(); 153 | out.inputPipe = std::move(inputPipe.write); 154 | out.outputPipe = std::move(outputPipe.read); 155 | out.errorPipe = std::move(errorPipe.read); 156 | } 157 | return child; 158 | } 159 | 160 | static Child spawnChild(const ChildParams ¶ms) { 161 | assert(params.argv.size() >= 2); 162 | assert(params.argv.back() == nullptr); 163 | 164 | winsize ws = {}; 165 | ws.ws_col = params.cols; 166 | ws.ws_row = params.rows; 167 | 168 | PipePair spawnErrPipe = makePipePair(O_CLOEXEC); 169 | ProcessPipes processPipes; 170 | 171 | int masterFdRaw = -1; 172 | const pid_t pid = 173 | params.usePty 174 | ? forkpty(&masterFdRaw, nullptr, nullptr, &ws) 175 | : forkPipes(processPipes); 176 | if (pid == static_cast(-1)) { 177 | // forkpty failed 178 | const SpawnError err = { 179 | SpawnError::Type::ForkPtyFailed, 180 | bridgedError(errno) 181 | }; 182 | Child ret; 183 | ret.spawnError = err; 184 | return ret; 185 | 186 | } else if (pid == 0) { 187 | // forked process 188 | const auto childFailed = [&](SpawnError::Type type, int savedErrno) { 189 | const SpawnError err = { 190 | type, 191 | bridgedError(savedErrno) 192 | }; 193 | writeAllRestarting(spawnErrPipe.write.fd(), &err, sizeof(err)); 194 | _exit(1); 195 | }; 196 | spawnErrPipe.read.close(); 197 | for (const auto &setting : params.env) { 198 | putenv(setting); 199 | } 200 | if (!params.cwd.empty()) { 201 | if (chdir(resolveCwd(params.cwd).c_str()) != 0) { 202 | childFailed(SpawnError::Type::ChdirFailed, errno); 203 | } 204 | } 205 | execvp(params.prog.c_str(), params.argv.data()); 206 | childFailed(SpawnError::Type::ExecFailed, errno); 207 | } 208 | 209 | UniqueFd masterFd(masterFdRaw); 210 | 211 | spawnErrPipe.write.close(); 212 | 213 | SpawnError err = {}; 214 | if (readAllRestarting(spawnErrPipe.read.fd(), &err, sizeof(err))) { 215 | // The child exec call failed. 216 | int dummy = 0; 217 | waitpid(pid, &dummy, 0); 218 | Child ret; 219 | ret.spawnError = err; 220 | return ret; 221 | } 222 | 223 | Child ret; 224 | ret.spawnError = SpawnError { SpawnError::Type::Success, bridgedError(0) }; 225 | ret.pid = pid; 226 | ret.masterFd = masterFd.release(); 227 | if (params.usePty) { 228 | ret.inputFd = ret.masterFd; 229 | ret.outputFd = ret.masterFd; 230 | ret.errorFd = -1; 231 | } else { 232 | ret.inputFd = processPipes.inputPipe.release(); 233 | ret.outputFd = processPipes.outputPipe.release(); 234 | ret.errorFd = processPipes.errorPipe.release(); 235 | } 236 | return ret; 237 | } 238 | 239 | struct ChannelWindow { 240 | std::mutex mutex; 241 | std::condition_variable increaseCV; 242 | std::atomic increaseAmt = {0}; 243 | }; 244 | 245 | struct IoLoop { 246 | bool usePty = false; 247 | int controlSocketFd = -1; 248 | int childFd = -1; 249 | WindowParams windowParams = {}; 250 | ChannelWindow outputWindow; 251 | ChannelWindow errorWindow; 252 | struct { 253 | pthread_t thread; 254 | int pipeFd = -1; 255 | int socketFd = -1; 256 | } stdoutAutoClose; 257 | }; 258 | 259 | static void connectionBrokenAbort() { 260 | fatal("error: connection broken\n"); 261 | } 262 | 263 | // Atomically replace the FD with /dev/null. 264 | // http://www.drdobbs.com/parallel/file-descriptors-and-multithreaded-progr/212001285 265 | static void revokeFd(int fd) { 266 | const int nullFd = open("/dev/null", O_RDWR | O_CLOEXEC); 267 | if (nullFd < 0) { 268 | fatalPerror("revokeFd: could not open /dev/null"); 269 | } 270 | if (dup2(nullFd, fd) < 0) { 271 | fatalPerror("revokeFd: dup2 failed"); 272 | } 273 | close(nullFd); 274 | } 275 | 276 | static void writePacket(int controlSocketFd, const Packet &p) { 277 | assert(p.size >= sizeof(p)); 278 | if (!writeAllRestarting(controlSocketFd, 279 | reinterpret_cast(&p), p.size)) { 280 | connectionBrokenAbort(); 281 | } 282 | } 283 | 284 | static void socketToChildThread(IoLoop *ioloop, int socketFd, int outputFd) { 285 | std::array buf; 286 | while (true) { 287 | const ssize_t amt1 = readRestarting(socketFd, buf.data(), buf.size()); 288 | if (amt1 <= 0) { 289 | break; 290 | } 291 | if (!writeAllRestarting(outputFd, buf.data(), amt1)) { 292 | break; 293 | } 294 | } 295 | // If we're using pipes and the frontend hits EOF on stdin, then we must 296 | // close our write-end stdin child pipe to propagate EOF. ssh doesn't seem 297 | // to ever propagate EOF from the child up to the parent, so this code also 298 | // doesn't propagate EOF from the backend to the frontend (i.e. we don't 299 | // close a socket because child pipe I/O failed). 300 | if (!ioloop->usePty) { 301 | revokeFd(outputFd); 302 | } 303 | revokeFd(socketFd); 304 | } 305 | 306 | static void childToSocketThread(IoLoop *ioloop, bool isErrorPipe, int inputFd, int socketFd) { 307 | ChannelWindow &window = isErrorPipe ? ioloop->errorWindow : ioloop->outputWindow; 308 | const auto windowThreshold = ioloop->windowParams.threshold; 309 | const auto windowSize = ioloop->windowParams.size; 310 | std::array buf; 311 | int32_t locWindow = windowSize; 312 | const auto hasWindow = [&](bool readAtomic = true) -> bool { 313 | if (readAtomic) { 314 | const int32_t iw = window.increaseAmt.exchange(0); 315 | assert(iw <= windowSize - locWindow); 316 | locWindow += iw; 317 | } 318 | return locWindow >= windowThreshold; 319 | }; 320 | while (true) { 321 | assert(locWindow >= 0 && locWindow <= windowSize); 322 | if (!hasWindow(false) && !hasWindow()) { 323 | std::unique_lock lock(window.mutex); 324 | window.increaseCV.wait(lock, hasWindow); 325 | } 326 | const ssize_t amt1 = 327 | readRestarting(inputFd, buf.data(), 328 | std::min(buf.size(), locWindow)); 329 | if (amt1 <= 0) { 330 | break; 331 | } 332 | if (!writeAllRestarting(socketFd, buf.data(), amt1)) { 333 | break; 334 | } 335 | locWindow -= amt1; 336 | } 337 | // The pty has closed. Shutdown I/O on the data socket to signal 338 | // I/O completion to the frontend. 339 | revokeFd(socketFd); 340 | } 341 | 342 | static void discardPacket(void*, const Packet&) { 343 | // Do nothing. 344 | } 345 | 346 | static void handlePacket(IoLoop *ioloop, const Packet &p) { 347 | switch (p.type) { 348 | case Packet::Type::SetSize: { 349 | winsize ws = {}; 350 | ws.ws_col = p.u.termSize.cols; 351 | ws.ws_row = p.u.termSize.rows; 352 | if (ioloop->childFd != -1) { 353 | ioctl(ioloop->childFd, TIOCSWINSZ, &ws); 354 | } 355 | break; 356 | } 357 | case Packet::Type::IncreaseWindow: { 358 | ChannelWindow &window = 359 | p.u.window.isErrorPipe ? 360 | ioloop->errorWindow : ioloop->outputWindow; 361 | { 362 | // Read ioloop->window into cw once to ensure a stable value. 363 | const int32_t max = ioloop->windowParams.size; 364 | const int32_t cw = window.increaseAmt; 365 | const int32_t iw = p.u.window.amount; 366 | assert(cw >= 0 && cw <= max && 367 | iw >= 0 && iw <= max - cw); 368 | std::lock_guard lock(window.mutex); 369 | window.increaseAmt += iw; 370 | } 371 | window.increaseCV.notify_one(); 372 | break; 373 | } 374 | case Packet::Type::CloseStdoutPipe: { 375 | // Shut down child->socket stdout I/O. This code is insufficent 376 | // to kill the thread, because it could be blocked waiting for 377 | // bytes in its window. It *is* sufficient to close the read-end 378 | // of the child stdout pipe and kill any in-progress syscall. 379 | assert(!ioloop->usePty); 380 | revokeFd(ioloop->stdoutAutoClose.pipeFd); 381 | revokeFd(ioloop->stdoutAutoClose.socketFd); 382 | pthread_kill(ioloop->stdoutAutoClose.thread, SIGUSR1); 383 | break; 384 | } 385 | default: { 386 | fatal("internal error: unexpected packet %d\n", 387 | static_cast(p.type)); 388 | } 389 | } 390 | } 391 | 392 | static void mainLoop(bool usePty, int controlSocketFd, 393 | int inputSocketFd, int outputSocketFd, int errorSocketFd, 394 | const char *exe, Child child, WindowParams windowParams) { 395 | if (child.spawnError.type == SpawnError::Type::Success) { 396 | IoLoop ioloop; 397 | ioloop.usePty = usePty; 398 | ioloop.controlSocketFd = controlSocketFd; 399 | ioloop.childFd = child.masterFd; 400 | ioloop.windowParams = windowParams; 401 | 402 | std::thread s2c(socketToChildThread, &ioloop, inputSocketFd, child.inputFd); 403 | std::thread c2s(childToSocketThread, &ioloop, false, 404 | child.outputFd, outputSocketFd); 405 | std::unique_ptr ec2s; 406 | if (errorSocketFd != -1) { 407 | ec2s = std::unique_ptr( 408 | new std::thread(childToSocketThread, &ioloop, true, 409 | child.errorFd, errorSocketFd)); 410 | } 411 | 412 | // handlePacket needs stdoutThread so it can propagate stdout closing 413 | // from the frontend to the child's stdout pipe. 414 | ioloop.stdoutAutoClose.thread = c2s.native_handle(); 415 | ioloop.stdoutAutoClose.pipeFd = child.outputFd; 416 | ioloop.stdoutAutoClose.socketFd = outputSocketFd; 417 | 418 | std::thread rcs( 419 | readControlSocketThread, 420 | controlSocketFd, &ioloop); 421 | 422 | // Block until the child process finishes, then notify the frontend of 423 | // child exit. 424 | int exitStatus = 0; 425 | if (waitpid(child.pid, &exitStatus, 0) != child.pid) { 426 | fatalPerror("waitpid failed"); 427 | } 428 | if (WIFEXITED(exitStatus)) { 429 | exitStatus = WEXITSTATUS(exitStatus); 430 | } else { 431 | // XXX: I'm just making something up here. I've got 432 | // no idea whether this makes sense. 433 | exitStatus = 1; 434 | } 435 | Packet p = { sizeof(Packet), Packet::Type::ChildExitStatus }; 436 | p.u.exitStatus = exitStatus; 437 | writePacket(controlSocketFd, p); 438 | 439 | // If we're using pipes, then close the write-end of the child stdin 440 | // pipe and the read-end of the stderr pipe. This seems to be what ssh 441 | // does. 442 | if (!ioloop.usePty) { 443 | revokeFd(child.inputFd); 444 | pthread_kill(s2c.native_handle(), SIGUSR1); 445 | revokeFd(child.errorFd); 446 | pthread_kill(ec2s->native_handle(), SIGUSR1); 447 | } 448 | 449 | // Ensure that the parent thread outlives its child threads. The program 450 | // should exit before all the worker threads finish. Join rcs first, so 451 | // that ioloop.stdoutThread remains valid if handlePacket is called. 452 | // 453 | // The rcs thread doesn't end gracefully -- when the control socket 454 | // reaches EOF, the backend process terminates. 455 | rcs.join(); 456 | s2c.join(); 457 | c2s.join(); 458 | if (ec2s) { ec2s->join(); } 459 | } else { 460 | PacketSpawnFailed p = {}; 461 | p.size = sizeof(p); 462 | p.type = Packet::Type::SpawnFailed; 463 | p.u.spawnError = child.spawnError; 464 | snprintf(p.exe, sizeof(p.exe), "%s", exe); 465 | writePacket(controlSocketFd, p); 466 | 467 | // Keep the backend alive until the control socket closes. 468 | readControlSocketThread( 469 | controlSocketFd, nullptr); 470 | } 471 | } 472 | 473 | template 474 | void optionRequired(const char *opt, const T &val, const T &unset) { 475 | if (val == unset) { 476 | fatal("error: option '%s' is missing\n", opt); 477 | } 478 | } 479 | 480 | template 481 | void optionNotAllowed(const char *opt, const char *why, const T &val, const T &unset) { 482 | if (val != unset) { 483 | fatal("error: option '%s' is not allowed%s\n", opt, why); 484 | } 485 | } 486 | 487 | static void frontendVersionCheck(const char *frontendVersion) { 488 | if (strcmp(frontendVersion, STRINGIFY(WSLBRIDGE_VERSION)) != 0) { 489 | fatal("error: wslbridge frontend-backend version mismatch" 490 | " (frontend is version '%s', backend is version '%s')\n", 491 | frontendVersion, STRINGIFY(WSLBRIDGE_VERSION)); 492 | } 493 | } 494 | 495 | } // namespace 496 | 497 | int main(int argc, char *argv[]) { 498 | 499 | // If the backend crashes, it prints a message to its stderr, which is a 500 | // hidden console, and immediately exits. We can show the console, but we 501 | // need to keep the process around to see the error. To do this, have a 502 | // mode where the backend immediately forks itself, and the child does the 503 | // real work. The parent just sticks around for a while. 504 | if (argc >= 2 && !strcmp(argv[1], "--debug-fork")) { 505 | pid_t child = fork(); 506 | if (child != 0) { 507 | sleep(3600); 508 | return 0; 509 | } 510 | } 511 | 512 | int controlSocketPort = -1; 513 | int inputSocketPort = -1; 514 | int outputSocketPort = -1; 515 | int errorSocketPort = -1; 516 | std::string key; 517 | int windowSize = -1; 518 | int windowThreshold = -1; 519 | ChildParams childParams; 520 | int ptyMode = -1; 521 | bool loginMode = false; 522 | 523 | const struct option kOptionTable[] = { 524 | { "pty", false, &ptyMode, 1 }, 525 | { "pipes", false, &ptyMode, 0 }, 526 | // This debugging option is handled earlier. Include it in this table 527 | // just to discard it. 528 | { "debug-fork", false, nullptr, 0 }, 529 | { "version", false, nullptr, 'v' }, 530 | { "check-version", true, nullptr, 'V' }, 531 | { nullptr, false, nullptr, 0 }, 532 | }; 533 | 534 | int ch = 0; 535 | bool versionChecked = false; 536 | while ((ch = getopt_long(argc, argv, "+3:0:1:2:k:c:r:w:t:e:C:l", kOptionTable, nullptr)) != -1) { 537 | switch (ch) { 538 | case 0: 539 | // This is returned for the two long options. getopt_long 540 | // already writes to ptyMode, so there's nothing more to do. 541 | break; 542 | case '3': controlSocketPort = atoi(optarg); break; 543 | case '0': inputSocketPort = atoi(optarg); break; 544 | case '1': outputSocketPort = atoi(optarg); break; 545 | case '2': errorSocketPort = atoi(optarg); break; 546 | case 'k': key = optarg; break; 547 | case 'c': childParams.cols = atoi(optarg); break; 548 | case 'r': childParams.rows = atoi(optarg); break; 549 | case 'w': windowSize = atoi(optarg); break; 550 | case 't': windowThreshold = atoi(optarg); break; 551 | case 'e': childParams.env.push_back(strdup(optarg)); break; 552 | case 'C': childParams.cwd = optarg; break; 553 | case 'l': loginMode = true; break; 554 | case 'v': 555 | printf("wslbridge-backend " STRINGIFY(WSLBRIDGE_VERSION) "\n"); 556 | exit(0); 557 | case 'V': 558 | // Do the version check early before we choke on an unexpected 559 | // argument from a mismatched frontend. 560 | frontendVersionCheck(optarg); 561 | versionChecked = true; 562 | break; 563 | default: 564 | exit(1); 565 | } 566 | } 567 | if (!versionChecked) { 568 | frontendVersionCheck(""); // The frontend predates the version-checking. 569 | } 570 | for (int i = optind; i < argc; ++i) { 571 | childParams.argv.push_back(argv[i]); 572 | } 573 | if (childParams.argv.empty()) { 574 | const char *shell = "/bin/sh"; 575 | struct passwd *pw = getpwuid(getuid()); 576 | if (pw == nullptr) { 577 | fatalPerror("error: getpwuid failed"); 578 | } else if (pw->pw_shell == nullptr) { 579 | fatal("error: getpwuid(...)->pw_shell is NULL\n"); 580 | } else { 581 | shell = pw->pw_shell; 582 | } 583 | childParams.argv.push_back(strdup(shell)); 584 | } 585 | // XXX: Replace char* args/envstrings with std::string? 586 | childParams.prog = childParams.argv[0]; 587 | if (loginMode) { 588 | std::string argv0 = childParams.argv[0]; 589 | const auto pos = argv0.find_last_of('/'); 590 | if (pos != std::string::npos) { 591 | argv0 = argv0.substr(pos + 1); 592 | } 593 | argv0 = '-' + argv0; 594 | childParams.argv[0] = strdup(argv0.c_str()); 595 | } 596 | childParams.argv.push_back(nullptr); 597 | 598 | optionRequired("--pty/--pipes", ptyMode, -1); 599 | optionRequired("-3", controlSocketPort, -1); 600 | optionRequired("-0", inputSocketPort, -1); 601 | optionRequired("-1", outputSocketPort, -1); 602 | optionRequired("-k", key, std::string()); 603 | if (ptyMode) { 604 | optionRequired("-c", childParams.cols, -1); 605 | optionRequired("-r", childParams.rows, -1); 606 | optionNotAllowed("-2", " with --pty", errorSocketPort, -1); 607 | } else { 608 | optionNotAllowed("-c", " with --pipes", childParams.cols, -1); 609 | optionNotAllowed("-r", " with --pipes", childParams.rows, -1); 610 | optionRequired("-2", errorSocketPort, -1); 611 | } 612 | optionRequired("-w", windowSize, -1); 613 | optionRequired("-t", windowThreshold, -1); 614 | 615 | childParams.usePty = ptyMode; 616 | 617 | const WindowParams windowParams = { windowSize, windowThreshold }; 618 | assert(windowParams.size >= 1); 619 | assert(windowParams.threshold >= 1); 620 | assert(windowParams.threshold <= windowParams.size); 621 | 622 | const int controlSocket = connectSocket(controlSocketPort, key); 623 | const int inputSocket = connectSocket(inputSocketPort, key); 624 | const int outputSocket = connectSocket(outputSocketPort, key); 625 | const int errorSocket = ptyMode ? -1 : connectSocket(errorSocketPort, key); 626 | 627 | const auto child = spawnChild(childParams); 628 | 629 | // We must not register signal handlers until *after* spawning the child. 630 | // It will inherit at least any SIG_IGN settings. 631 | // 632 | // We want to handle EPIPE rather than receiving SIGPIPE. 633 | signal(SIGPIPE, SIG_IGN); 634 | // Register a do-nothing SIGUSR1 handler that can be used to interrupt 635 | // threads. On Linux (and WSL), simply closing a file descriptor (whether 636 | // via close or dup2), doesn't interrupt blocking I/O syscalls. We need to 637 | // fire a signal after closing the FDs. 638 | struct sigaction sa = {}; 639 | sa.sa_handler = [](int signo) {}; 640 | sigaction(SIGUSR1, &sa, nullptr); 641 | 642 | mainLoop(childParams.usePty, controlSocket, 643 | inputSocket, outputSocket, errorSocket, 644 | childParams.prog.c_str(), child, windowParams); 645 | 646 | return 0; 647 | } 648 | -------------------------------------------------------------------------------- /common/SocketIo.cc: -------------------------------------------------------------------------------- 1 | #include "SocketIo.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | void fatal(const char *fmt, ...) { 19 | va_list ap; 20 | va_start(ap, fmt); 21 | fatalv(fmt, ap); 22 | va_end(ap); 23 | } 24 | 25 | void fatalv(const char *fmt, va_list ap) { 26 | vfprintf(stderr, fmt, ap); 27 | fflush(stdout); 28 | fflush(stderr); 29 | // Avoid calling exit, which would call global destructors and destruct the 30 | // global WakeupFd object. 31 | _exit(1); 32 | } 33 | 34 | void fatalPerror(const char *msg) { 35 | perror(msg); 36 | fflush(stdout); 37 | fflush(stderr); 38 | // Avoid calling exit, which would call global destructors and destruct the 39 | // global WakeupFd object. 40 | _exit(1); 41 | } 42 | 43 | ssize_t writeRestarting(int fd, const void *buf, size_t count) { 44 | ssize_t ret = 0; 45 | do { 46 | ret = write(fd, buf, count); 47 | } while (ret < 0 && errno == EINTR); 48 | return ret; 49 | } 50 | 51 | bool writeAllRestarting(int fd, const void *buf, size_t count) { 52 | while (count > 0) { 53 | ssize_t amt = writeRestarting(fd, buf, count); 54 | if (amt <= 0) { 55 | return false; 56 | } 57 | assert(static_cast(amt) <= count); 58 | buf = reinterpret_cast(buf) + amt; 59 | count -= amt; 60 | } 61 | return true; 62 | } 63 | 64 | ssize_t readRestarting(int fd, void *buf, size_t count) { 65 | ssize_t ret = 0; 66 | do { 67 | ret = read(fd, buf, count); 68 | } while (ret < 0 && errno == EINTR); 69 | return ret; 70 | } 71 | 72 | bool readAllRestarting(int fd, void *buf, size_t count) { 73 | while (count > 0) { 74 | ssize_t amt = readRestarting(fd, buf, count); 75 | if (amt <= 0) { 76 | return false; 77 | } 78 | assert(static_cast(amt) <= count); 79 | buf = reinterpret_cast(buf) + amt; 80 | count -= amt; 81 | } 82 | return true; 83 | } 84 | 85 | void setSocketNoDelay(int s) { 86 | const int flag = 1; 87 | const int nodelayRet = setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); 88 | assert(nodelayRet == 0); 89 | } 90 | 91 | BridgedErrno bridgedErrno(int err) { 92 | switch (err) { 93 | case 0: return BridgedErrno::Success; 94 | case E2BIG: return BridgedErrno::bE2BIG; 95 | case EACCES: return BridgedErrno::bEACCES; 96 | case EAGAIN: return BridgedErrno::bEAGAIN; 97 | case EFAULT: return BridgedErrno::bEFAULT; 98 | case EINVAL: return BridgedErrno::bEINVAL; 99 | case EIO: return BridgedErrno::bEIO; 100 | case EISDIR: return BridgedErrno::bEISDIR; 101 | case ELIBBAD: return BridgedErrno::bELIBBAD; 102 | case ELOOP: return BridgedErrno::bELOOP; 103 | case EMFILE: return BridgedErrno::bEMFILE; 104 | case ENAMETOOLONG: return BridgedErrno::bENAMETOOLONG; 105 | case ENFILE: return BridgedErrno::bENFILE; 106 | case ENOENT: return BridgedErrno::bENOENT; 107 | case ENOEXEC: return BridgedErrno::bENOEXEC; 108 | case ENOMEM: return BridgedErrno::bENOMEM; 109 | case ENOTDIR: return BridgedErrno::bENOTDIR; 110 | case EPERM: return BridgedErrno::bEPERM; 111 | case ETXTBSY: return BridgedErrno::bETXTBSY; 112 | default: return BridgedErrno::Unknown; 113 | } 114 | } 115 | 116 | BridgedError bridgedError(int err) { 117 | return BridgedError { err, bridgedErrno(err) }; 118 | } 119 | 120 | std::string errorString(BridgedError err) { 121 | int bridgedErrno = 0; 122 | switch (err.bridged) { 123 | case BridgedErrno::Success: bridgedErrno = 0; break; 124 | case BridgedErrno::bE2BIG: bridgedErrno = E2BIG; break; 125 | case BridgedErrno::bEACCES: bridgedErrno = EACCES; break; 126 | case BridgedErrno::bEAGAIN: bridgedErrno = EAGAIN; break; 127 | case BridgedErrno::bEFAULT: bridgedErrno = EFAULT; break; 128 | case BridgedErrno::bEINVAL: bridgedErrno = EINVAL; break; 129 | case BridgedErrno::bEIO: bridgedErrno = EIO; break; 130 | case BridgedErrno::bEISDIR: bridgedErrno = EISDIR; break; 131 | case BridgedErrno::bELIBBAD: bridgedErrno = ELIBBAD; break; 132 | case BridgedErrno::bELOOP: bridgedErrno = ELOOP; break; 133 | case BridgedErrno::bEMFILE: bridgedErrno = EMFILE; break; 134 | case BridgedErrno::bENAMETOOLONG: bridgedErrno = ENAMETOOLONG; break; 135 | case BridgedErrno::bENFILE: bridgedErrno = ENFILE; break; 136 | case BridgedErrno::bENOENT: bridgedErrno = ENOENT; break; 137 | case BridgedErrno::bENOEXEC: bridgedErrno = ENOEXEC; break; 138 | case BridgedErrno::bENOMEM: bridgedErrno = ENOMEM; break; 139 | case BridgedErrno::bENOTDIR: bridgedErrno = ENOTDIR; break; 140 | case BridgedErrno::bEPERM: bridgedErrno = EPERM; break; 141 | case BridgedErrno::bETXTBSY: bridgedErrno = ETXTBSY; break; 142 | default: 143 | return "WSL error #" + std::to_string(err.actual); 144 | } 145 | char buf[512]; 146 | char *errStr = strerror_r(bridgedErrno, buf, sizeof(buf)); 147 | assert(errStr != nullptr); 148 | return errStr; 149 | } 150 | 151 | WakeupFd::WakeupFd() { 152 | if (pipe2(fds_, O_NONBLOCK | O_CLOEXEC) != 0) { 153 | fatalPerror("error: pipe2 failed"); 154 | } 155 | FD_ZERO(&fdset_); 156 | } 157 | 158 | void WakeupFd::wait() { 159 | do { 160 | FD_SET(readFd(), &fdset_); 161 | const int selectRet = select(readFd() + 1, &fdset_, nullptr, nullptr, nullptr); 162 | if (selectRet < 0 && errno == EINTR) { 163 | // Try again. 164 | continue; 165 | } else if (selectRet < 0) { 166 | fatalPerror("internal error: select on wakeup pipe failed"); 167 | } 168 | std::array dummy; 169 | const ssize_t readRet = readRestarting(readFd(), dummy.data(), dummy.size()); 170 | if (readRet < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { 171 | // I'm not sure whether this can happen. 172 | continue; 173 | } else if (readRet <= 0) { 174 | fatalPerror("internal error: wakeup pipe read failed"); 175 | } 176 | } while (false); 177 | } 178 | -------------------------------------------------------------------------------- /common/SocketIo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #define XSTRINGIFY(x) #x 16 | #define STRINGIFY(x) XSTRINGIFY(x) 17 | 18 | void fatal(const char *fmt, ...) 19 | __attribute__((noreturn)) 20 | __attribute__((format(printf, 1, 2))); 21 | void fatalv(const char *fmt, va_list ap) __attribute__((noreturn)); 22 | void fatalPerror(const char *msg) __attribute__((noreturn)); 23 | ssize_t writeRestarting(int fd, const void *buf, size_t count); 24 | bool writeAllRestarting(int fd, const void *buf, size_t count); 25 | ssize_t readRestarting(int fd, void *buf, size_t count); 26 | bool readAllRestarting(int fd, void *buf, size_t count); 27 | void setSocketNoDelay(int s); 28 | 29 | struct TermSize { 30 | uint16_t cols; 31 | uint16_t rows; 32 | 33 | bool operator==(const TermSize &o) const { 34 | return cols == o.cols && rows == o.rows; 35 | } 36 | bool operator!=(const TermSize &o) const { 37 | return !(*this == o); 38 | } 39 | }; 40 | 41 | struct WindowParams { 42 | int32_t size; // Maximum number of bytes in flight. 43 | int32_t threshold; // Minimum remaining window to initiate I/O. 44 | }; 45 | 46 | enum class BridgedErrno : int32_t { 47 | Success = 0, 48 | Unknown, 49 | bE2BIG, 50 | bEACCES, 51 | bEAGAIN, 52 | bEFAULT, 53 | bEINVAL, 54 | bEIO, 55 | bEISDIR, 56 | bELIBBAD, 57 | bELOOP, 58 | bEMFILE, 59 | bENAMETOOLONG, 60 | bENFILE, 61 | bENOENT, 62 | bENOEXEC, 63 | bENOMEM, 64 | bENOTDIR, 65 | bEPERM, 66 | bETXTBSY, 67 | }; 68 | 69 | struct BridgedError { 70 | int32_t actual; 71 | BridgedErrno bridged; 72 | }; 73 | 74 | struct SpawnError { 75 | enum class Type : int32_t { 76 | Success = 0, 77 | ForkPtyFailed, 78 | ExecFailed, 79 | ChdirFailed, 80 | } type; 81 | BridgedError error; 82 | }; 83 | 84 | BridgedErrno bridgedErrno(int err); 85 | BridgedError bridgedError(int err); 86 | std::string errorString(BridgedError err); 87 | 88 | struct Packet { 89 | uint32_t size; 90 | enum class Type : int32_t { 91 | SetSize, 92 | IncreaseWindow, 93 | SpawnFailed, 94 | ChildExitStatus, 95 | CloseStdoutPipe 96 | } type; 97 | union { 98 | TermSize termSize; 99 | struct { 100 | int32_t amount; 101 | bool isErrorPipe; 102 | } window; 103 | int32_t exitStatus; 104 | SpawnError spawnError; 105 | } u; 106 | }; 107 | 108 | struct PacketSpawnFailed : Packet { 109 | char exe[1024]; 110 | }; 111 | 112 | class WakeupFd { 113 | public: 114 | WakeupFd(); 115 | ~WakeupFd() { 116 | close(fds_[0]); 117 | close(fds_[1]); 118 | } 119 | 120 | void set() { 121 | char dummy = 0; 122 | writeRestarting(fds_[1], &dummy, 1); 123 | } 124 | 125 | void wait(); 126 | 127 | private: 128 | int readFd() const { return fds_[0]; } 129 | 130 | fd_set fdset_; 131 | int fds_[2]; 132 | }; 133 | 134 | template 135 | void readControlSocketThread(int controlSocketFd, T *userObj) { 136 | union { 137 | Packet base; 138 | PacketSpawnFailed spawnFailed; 139 | } packet = {}; 140 | while (true) { 141 | if (!readAllRestarting(controlSocketFd, &packet.base, 142 | sizeof(packet.base))) { 143 | readFailure(); 144 | } 145 | if (packet.base.size < sizeof(Packet) || 146 | packet.base.size > sizeof(packet)) { 147 | readFailure(); 148 | } 149 | if (!readAllRestarting(controlSocketFd, &packet.base + 1, 150 | packet.base.size - sizeof(packet.base))) { 151 | readFailure(); 152 | } 153 | packetHandlerFunc(userObj, packet.base); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /dist/appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Ubuntu 3 | - Visual Studio 2017 4 | 5 | environment: 6 | matrix: 7 | - { COMPONENT: frontend, CYGWIN_VARIANT: cygwin32 } 8 | - { COMPONENT: frontend, CYGWIN_VARIANT: cygwin64 } 9 | - { COMPONENT: frontend, CYGWIN_VARIANT: msys32 } 10 | - { COMPONENT: frontend, CYGWIN_VARIANT: msys64 } 11 | - { COMPONENT: backend } 12 | 13 | matrix: 14 | exclude: 15 | - { image: Ubuntu, COMPONENT: frontend } 16 | - { image: Visual Studio 2017, COMPONENT: backend } 17 | 18 | for: 19 | - matrix: { only: [ COMPONENT: backend ] } 20 | build_script: 21 | - python3 dist/make-backend-package.py 22 | - matrix: { only: [ COMPONENT: frontend ] } 23 | build_script: 24 | - C:\Python35-x64\python.exe dist\make-frontend-packages.py 25 | 26 | artifacts: 27 | - path: out\artifact\*.tar.gz 28 | -------------------------------------------------------------------------------- /dist/cygwin-prebuilts/appveyor.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - cygwin-prebuilts 4 | 5 | build_script: 6 | - C:\Python36-x64\python.exe dist\cygwin-prebuilts\make-cygwin-prebuilts.py 7 | - C:\Python36-x64\python.exe dist\cygwin-prebuilts\make-msys2-prebuilts.py 8 | 9 | artifacts: 10 | - path: out\artifact\*.7z 11 | -------------------------------------------------------------------------------- /dist/cygwin-prebuilts/dllversion.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def versionDict(dllpath): 8 | if not os.path.isfile(dllpath): 9 | sys.exit('error: {} is not an existing file'.format(dllpath)) 10 | txt = subprocess.check_output([ 11 | 'powershell', 12 | '[System.Diagnostics.FileVersionInfo]::GetVersionInfo("{}") | ConvertTo-Json'.format(dllpath.replace('\\', '/')), 13 | ]) 14 | return json.loads(txt) 15 | 16 | 17 | def fileVersion(dllpath): 18 | ret = versionDict(dllpath)['FileVersion'] 19 | assert ret not in (None, '') 20 | return ret 21 | -------------------------------------------------------------------------------- /dist/cygwin-prebuilts/make-cygwin-prebuilts.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import os 3 | import sys 4 | sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | import util 6 | 7 | import re 8 | import shutil 9 | import subprocess 10 | 11 | import dllversion 12 | 13 | from os.path import abspath 14 | from subprocess import check_call 15 | from util import glob_paths, rmpath, mkdirs, buildTimeStamp, projectDir, getGppVer 16 | 17 | 18 | sys.platform == 'win32' or sys.exit('error: script only runs on Windows (no Cygwin/MSYS)') 19 | shutil.which('7z') or sys.exit('error: 7z missing') 20 | shutil.which('curl') or sys.exit('error: curl missing') 21 | 22 | 23 | buildDir = os.path.join(projectDir, 'out\\build-cygwin') 24 | artifactDir = os.path.join(projectDir, 'out\\artifact') 25 | rmpath(buildDir) 26 | mkdirs(buildDir) 27 | mkdirs(artifactDir) 28 | 29 | os.chdir(buildDir) 30 | 31 | for setup, cygwin in (('setup-x86_64', 'cygwin64'), ('setup-x86', 'cygwin32')): 32 | 33 | check_call(['curl', '-fL', '-O', 'https://cygwin.com/{}.exe'.format(setup)]) 34 | 35 | check_call([ 36 | abspath('{}.exe'.format(setup)), 37 | '-l', abspath('{}-packages'.format(cygwin)), 38 | '-P', 'gcc-g++,make', 39 | '-s', 'http://mirrors.kernel.org/sourceware/cygwin', 40 | '-R', abspath(cygwin), 41 | '--no-admin', '--no-desktop', '--no-shortcuts', '--no-startmenu', '--quiet-mode', 42 | ]) 43 | 44 | check_call(['{}/bin/ash.exe'.format(cygwin), '/bin/rebaseall', '-v']) 45 | 46 | cygVer = dllversion.fileVersion('{}/bin/cygwin1.dll'.format(cygwin)) 47 | gppVer = getGppVer('{}/bin/g++.exe'.format(cygwin)) 48 | 49 | filename = '{}\\{}-{}-dll{}-gcc{}.7z'.format(artifactDir, cygwin, buildTimeStamp, cygVer, gppVer) 50 | rmpath(filename) 51 | 52 | open(cygwin + '/tmp/.keep', 'wb').close() 53 | 54 | check_call(['7z', 'a', '-mx=9', filename] + glob_paths([ 55 | cygwin + '/dev', 56 | cygwin + '/etc/setup', 57 | cygwin + '/tmp/.keep', 58 | cygwin + '/bin', 59 | cygwin + '/lib', 60 | cygwin + '/usr/include', 61 | cygwin + '/usr/*-pc-cygwin', 62 | ])) 63 | -------------------------------------------------------------------------------- /dist/cygwin-prebuilts/make-msys2-prebuilts.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import os 3 | import sys 4 | sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | import util 6 | 7 | import hashlib 8 | import re 9 | import shutil 10 | import subprocess 11 | import urllib 12 | 13 | import dllversion 14 | 15 | from os.path import abspath 16 | from subprocess import check_call 17 | from util import glob_paths, rmpath, mkdirs, buildTimeStamp, projectDir, getGppVer 18 | 19 | 20 | def sha256(path): 21 | with open(path, 'rb') as fp: 22 | return hashlib.sha256(fp.read()).hexdigest() 23 | 24 | def checkSha256(path, expected): 25 | actual = sha256(path) 26 | if actual != expected: 27 | sys.exit('error: sha256 hash mismatch on {}: expected {}, found {}'.format( 28 | path, expected, actual)) 29 | 30 | 31 | sys.platform == 'win32' or sys.exit('error: script only runs on Windows (no Cygwin/MSYS)') 32 | shutil.which('7z') or sys.exit('error: 7z missing') 33 | shutil.which('curl') or sys.exit('error: curl missing') 34 | 35 | buildDir = os.path.join(projectDir, 'out\\build-msys2') 36 | artifactDir = os.path.join(projectDir, 'out\\artifact') 37 | rmpath(buildDir) 38 | mkdirs(buildDir) 39 | mkdirs(artifactDir) 40 | 41 | os.chdir(buildDir) 42 | 43 | check_call(['curl', '-fL', '-O', 'http://repo.msys2.org/distrib/i686/msys2-base-i686-20161025.tar.xz']) 44 | check_call(['curl', '-fL', '-O', 'http://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20161025.tar.xz']) 45 | checkSha256('msys2-base-i686-20161025.tar.xz', '8bafd3d52f5a51528a8671c1cae5591b36086d6ea5b1e76e17e390965cf6768f') 46 | checkSha256('msys2-base-x86_64-20161025.tar.xz', 'bb1f1a0b35b3d96bf9c15092da8ce969a84a134f7b08811292fbc9d84d48c65d') 47 | 48 | for name, arch in (('msys64', 'x86_64'), ('msys32', 'i686')): 49 | 50 | baseArchive = 'msys2-base-{}-20161025'.format(arch) 51 | check_call(['7z', 'x', '{}.tar.xz'.format(baseArchive)]) 52 | check_call(['7z', 'x', '{}.tar'.format(baseArchive)]) 53 | 54 | bashPath = abspath(name + '\\usr\\bin\\bash.exe') 55 | 56 | check_call([bashPath, '--login', '-c', 'exit']) 57 | good = False 58 | for i in range(5): 59 | # Apparently in the base install, the 'msys-runtime' and 'catgets' 60 | # packages are incompatible, and passing --ask=20 confirms to MSYS2 61 | # that we should do the necessary thing (remove catgets, I guess?) 62 | # See https://github.com/Alexpux/MSYS2-packages/issues/1141. 63 | cmd = [bashPath, '--login', '-c', 'pacman --ask=20 --noconfirm -Syuu'] 64 | print('Running {} ...'.format(repr(cmd))) 65 | p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8') 66 | sys.stdout.write(p.stdout) 67 | if p.returncode != 0: 68 | sys.exit('error: MSYS2 system update failed') 69 | 70 | good = 'there is nothing to do' in [x.strip() for x in p.stdout.splitlines()] 71 | 72 | good or sys.exit('error: MSYS2 system update never finished') 73 | 74 | cmd = [bashPath, '--login', '-c', 'pacman --noconfirm -S msys/gcc msys/make msys/tar'] 75 | print('Running {} ...'.format(repr(cmd))) 76 | check_call(cmd) 77 | 78 | # The -p option passed by autorebase.bat doesn't look necessary. It relaxes 79 | # the sanity checking to allow more than just ash.exe/dash.exe processes. 80 | check_call(['{}/usr/bin/ash.exe'.format(name), '/usr/bin/rebaseall', '-v']) 81 | 82 | msysVer = dllversion.fileVersion('{}/usr/bin/msys-2.0.dll'.format(name)) 83 | gppVer = getGppVer('{}/usr/bin/g++.exe'.format(name)) 84 | 85 | filename = '{}\\{}-{}-dll{}-gcc{}.7z'.format(artifactDir, name, buildTimeStamp, msysVer, gppVer) 86 | rmpath(filename) 87 | 88 | open(name + '/tmp/.keep', 'wb').close() 89 | open(name + '/etc/.keep', 'wb').close() 90 | 91 | check_call(['7z', 'a', '-mx=9', filename] + glob_paths([ 92 | name + '/autorebase.bat', 93 | name + '/dev', 94 | name + '/etc/.keep', 95 | name + '/tmp/.keep', 96 | name + '/usr/bin', 97 | name + '/usr/lib', 98 | name + '/usr/include', 99 | name + '/usr/*-pc-msys', 100 | ])) 101 | -------------------------------------------------------------------------------- /dist/make-backend-package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import util 3 | 4 | import os 5 | import shutil 6 | import sys 7 | 8 | from os.path import abspath 9 | from subprocess import check_call 10 | from util import projectDir, rmpath, mkdirs 11 | 12 | sys.platform == 'linux' or sys.exit('error: script only runs on Linux') 13 | shutil.which('git') or sys.exit('error: git missing') 14 | shutil.which('make') or sys.exit('error: make missing') 15 | shutil.which('tar') or sys.exit('error: tar missing') 16 | 17 | buildDir = os.path.join(projectDir, 'out', 'build-backend') 18 | artifactDir = os.path.join(projectDir, 'out', 'artifact') 19 | 20 | rmpath(buildDir) 21 | mkdirs(buildDir) 22 | mkdirs(artifactDir) 23 | 24 | os.chdir(buildDir) 25 | 26 | prebuiltUrl = 'https://github.com/rprichard/x86_64-linux-glibc2.15-4.8.git' 27 | prebuiltPath = abspath('linux-prebuilt') 28 | artifactPath = os.path.join(artifactDir, 'backend.tar.gz') 29 | 30 | check_call(['git', 'clone', prebuiltUrl, prebuiltPath]) 31 | 32 | os.putenv('CXX', os.path.join(prebuiltPath, 'bin', 'x86_64-linux-g++')) 33 | os.putenv('STRIP', os.path.join(prebuiltPath, 'bin', 'x86_64-linux-strip')) 34 | check_call(['make', 'clean'], cwd=os.path.join(projectDir, 'backend')) 35 | check_call(['make'], cwd=os.path.join(projectDir, 'backend')) 36 | check_call(['tar', 'cfa', artifactPath, '-C', '..', 'wslbridge-backend']) 37 | -------------------------------------------------------------------------------- /dist/make-bin-package.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | call :main || goto :failed 5 | exit /b 0 6 | 7 | :failed 8 | echo Build failed 9 | exit /b 1 10 | 11 | 12 | ::::::::::::::::::::::::::::::::::::::: 13 | :: main routine 14 | 15 | :main 16 | setlocal 17 | 18 | set Unix2Dos=C:\cygwin64\bin\unix2dos.exe 19 | set ProjectRoot=%~dp0.. 20 | set ProjectRootM=%ProjectRoot:\=/% 21 | cd "%ProjectRoot%" 22 | rmdir /s/q out 2>NUL 23 | mkdir out\packages || exit /b 1 24 | set /p Version= out/WslGccVersion.txt" || exit /b 1 28 | %Unix2Dos% out/WslGccVersion.txt || exit /b 1 29 | 30 | call :makeFrontend "cygwin32" "C:\cygwin\bin" "cygwin1.dll" || exit /b 1 31 | call :makeFrontend "cygwin64" "C:\cygwin64\bin" "cygwin1.dll" || exit /b 1 32 | call :makeFrontend "msys32" "C:\msys32\usr\bin" "msys-2.0.dll" || exit /b 1 33 | call :makeFrontend "msys64" "C:\msys64\usr\bin" "msys-2.0.dll" || exit /b 1 34 | 35 | echo ============= TAR FILES ============= 36 | set PATH=C:\cygwin64\bin;%PATH% 37 | C:\cygwin64\bin\bash.exe -c "for p in out/packages/*; do printf '\n'$p'\n'; tar tfv $p; done" 38 | 39 | exit /b 0 40 | 41 | 42 | ::::::::::::::::::::::::::::::::::::::: 43 | :: makeFrontend subroutine 44 | 45 | :makeFrontend 46 | setlocal 47 | 48 | set PackageName=wslbridge-%Version%-%1 49 | set PackageDir=out\%PackageName% 50 | set PackageDirM=out/%PackageName% 51 | mkdir %PackageDir% || exit /b 1 52 | 53 | del out\wslbridge.exe 2>NUL 54 | %2\bash.exe -l -c "cd '%ProjectRootM%' && make -C frontend && strip out/wslbridge.exe" || exit /b 1 55 | %2\bash.exe -l -c "cd '%ProjectRootM%' && g++ --version > out/CygGccVersion.txt" || exit /b 1 56 | %Unix2Dos% out/CygGccVersion.txt || exit /b 1 57 | 58 | copy out\wslbridge.exe %PackageDir% || exit /b 1 59 | copy out\wslbridge-backend %PackageDir% || exit /b 1 60 | 61 | powershell "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"%2\" + \"\\\" + \"%3\") | Set-Content -Encoding ASCII out\CygDllVersion.txt" || exit /b 1 62 | 63 | :: Always use Cygwin tar to package the binary. MSYS2 tar won't mark the 64 | :: backend executable because its POSIX emulation uses filetype to determine 65 | :: executability. Cygwin, on the other hand, uses NTFS ACL entries. If the 66 | :: MSYS2 package were then extracted using Cygwin tar, the backend file would 67 | :: have non-executable ACL entries, and WSL would refuse to run it. 68 | :: 69 | :: Use bash.exe to copy files so the execute bit is left unset. 70 | set DistSavedPath=%PATH% 71 | set PATH=C:\cygwin64\bin;%PATH% 72 | C:\cygwin64\bin\bash.exe -c "cat README.md | unix2dos > %PackageDirM%/README.md" || exit /b 1 73 | C:\cygwin64\bin\bash.exe -c "cat LICENSE.txt | unix2dos > %PackageDirM%/LICENSE.txt" || exit /b 1 74 | C:\cygwin64\bin\bash.exe -c "cat out/CygDllVersion.txt out/CygGccVersion.txt out/WslGccVersion.txt | unix2dos.exe > %PackageDirM%/BuildInfo.txt" || exit /b 1 75 | C:\cygwin64\bin\tar.exe cfz out/packages/%PackageName%.tar.gz --numeric-owner --owner=0 --group=0 -C out %PackageName% || exit /b 1 76 | set PATH=%DistSavedPath% 77 | 78 | exit /b 0 79 | -------------------------------------------------------------------------------- /dist/make-frontend-packages.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import util 3 | 4 | import os 5 | import shutil 6 | import sys 7 | 8 | from os.path import abspath, relpath 9 | from subprocess import check_call 10 | from util import projectDir, rmpath, mkdirs 11 | 12 | sys.platform == 'win32' or sys.exit('error: script only runs on Windows (no Cygwin/MSYS)') 13 | shutil.which('7z') or sys.exit('error: 7z missing') 14 | shutil.which('curl') or sys.exit('error: curl missing') 15 | 16 | buildDir = os.path.join(projectDir, 'out', 'build-frontend') 17 | artifactDir = os.path.join(projectDir, 'out', 'artifact') 18 | 19 | rmpath(buildDir) 20 | mkdirs(buildDir) 21 | mkdirs(artifactDir) 22 | 23 | os.chdir(buildDir) 24 | 25 | baseUrl = 'https://github.com/rprichard/wslbridge/raw/wslbridge-cygwin-prebuilts-1.0.7/' 26 | 27 | for platform, binDir, url in [ 28 | ('cygwin32', 'bin', baseUrl + 'cygwin32-20180502-dll2.10.0-gcc6.4.0.7z'), 29 | ('cygwin64', 'bin', baseUrl + 'cygwin64-20180502-dll2.10.0-gcc6.4.0.7z'), 30 | ('msys32', 'usr/bin', baseUrl + 'msys32-20180502-dll2.10.0-gcc7.3.0.7z'), 31 | ('msys64', 'usr/bin', baseUrl + 'msys64-20180502-dll2.10.0-gcc7.3.0.7z'), 32 | ]: 33 | 34 | if os.getenv('CYGWIN_VARIANT') and os.getenv('CYGWIN_VARIANT') != platform: 35 | continue 36 | 37 | print('Building {} ...'.format(platform)) 38 | 39 | check_call(['curl', '-fL', url, '-o', '{}.7z'.format(platform)]) 40 | check_call(['7z', 'x', '{}.7z'.format(platform)]) 41 | 42 | platformBinDir = abspath(os.path.join(platform, binDir)) 43 | artifactPath = os.path.join(artifactDir, '{}-frontend.tar.gz'.format(platform)) 44 | 45 | origPATH = os.getenv('PATH') 46 | os.putenv('PATH', platformBinDir + os.pathsep + origPATH) 47 | # Rebase the binaries after installing/moving them onto the new machine. 48 | check_call([os.path.join(platformBinDir, 'ash.exe'), '/{}/rebaseall'.format(binDir), '-v']) 49 | check_call([os.path.join(platformBinDir, 'make.exe'), 'clean'], cwd=os.path.join(projectDir, 'frontend')) 50 | check_call([os.path.join(platformBinDir, 'make.exe')], cwd=os.path.join(projectDir, 'frontend')) 51 | check_call([os.path.join(platformBinDir, 'tar.exe'), 'cfa', 52 | relpath(artifactPath, os.getcwd()).replace('\\', '/'), 53 | '-C', '..', 'wslbridge.exe']) 54 | os.putenv('PATH', origPATH) 55 | -------------------------------------------------------------------------------- /dist/util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # Install Python from https://www.python.org. 4 | (sys.version_info[0:2] >= (3, 5)) or sys.exit('error: script requires Python 3.5 or above') 5 | 6 | import os 7 | import re 8 | import shutil 9 | import stat 10 | import subprocess 11 | 12 | from glob import glob 13 | from datetime import datetime 14 | 15 | 16 | def glob_paths(patterns): 17 | ret = [] 18 | for p in patterns: 19 | batch = glob(p.replace('/', '\\')) 20 | if len(batch) == 0: 21 | sys.exit('error: pattern matched no files: {}'.format(p)) 22 | ret.extend(batch) 23 | return ret 24 | 25 | 26 | def rmpath(path): 27 | if os.path.islink(path): 28 | os.remove(path) 29 | elif os.path.isfile(path): 30 | # MSYS2 makes files read-only, and we need this chmod command to delete 31 | # them. 32 | os.chmod(path, stat.S_IWRITE) 33 | os.remove(path) 34 | elif os.path.isdir(path): 35 | for child in os.listdir(path): 36 | # listdir excludes '.' and '..' 37 | rmpath(os.path.join(path, child)) 38 | os.rmdir(path) 39 | 40 | 41 | 42 | def mkdirs(path): 43 | if not os.path.isdir(path): 44 | os.makedirs(path) 45 | 46 | 47 | def getGppVer(path): 48 | txt = subprocess.check_output([path, '--version']).decode() 49 | txt = txt.splitlines()[0] 50 | m = re.match(r'g\+\+ \(GCC\) (\d+\.\d+\.\d+)$', txt) 51 | if not m: 52 | sys.exit('error: g++ version did not match pattern: {}'.format(repr(txt))) 53 | return m.group(1) 54 | 55 | 56 | buildTimeStamp = datetime.now().strftime('%Y%m%d') 57 | projectDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 58 | -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | STRIP ?= strip 2 | 3 | all : ../out/wslbridge.exe 4 | 5 | ../out/wslbridge.exe : wslbridge.cc ../common/SocketIo.cc ../common/SocketIo.h ../VERSION.txt Makefile 6 | mkdir -p ../out 7 | $(CXX) -std=c++11 -fno-exceptions \ 8 | -static -static-libgcc -static-libstdc++ \ 9 | -D_GNU_SOURCE -D_WIN32_WINNT=0x0600 -DUNICODE -D_UNICODE \ 10 | -DWSLBRIDGE_VERSION=$(shell cat ../VERSION.txt) \ 11 | -Wall -O2 $< ../common/SocketIo.cc -o $@ 12 | $(STRIP) $@ 13 | 14 | clean: 15 | rm -f ../out/wslbridge.exe 16 | -------------------------------------------------------------------------------- /frontend/wslbridge.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include "../common/SocketIo.h" 36 | 37 | #define BACKEND_PROGRAM "wslbridge-backend" 38 | 39 | // SystemFunction036 is also known as RtlGenRandom. It might be possible to 40 | // replace this with getentropy, if not now, then later. 41 | extern "C" BOOLEAN WINAPI SystemFunction036(PVOID, ULONG); 42 | 43 | namespace { 44 | 45 | const int32_t kOutputWindowSize = 8192; 46 | 47 | static WakeupFd *g_wakeupFd = nullptr; 48 | 49 | static TermSize terminalSize() { 50 | winsize ws = {}; 51 | if (isatty(STDIN_FILENO) && ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) { 52 | return TermSize { ws.ws_col, ws.ws_row }; 53 | } else { 54 | return TermSize { 80, 24 }; 55 | } 56 | } 57 | 58 | class Socket { 59 | public: 60 | Socket(); 61 | ~Socket() { close(); } 62 | int port() { return port_; } 63 | int accept(); 64 | void close(); 65 | 66 | private: 67 | int s_; 68 | int port_; 69 | }; 70 | 71 | Socket::Socket() { 72 | s_ = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); 73 | assert(s_ >= 0); 74 | 75 | setSocketNoDelay(s_); 76 | 77 | sockaddr_in addr = {}; 78 | addr.sin_family = AF_INET; 79 | addr.sin_port = htons(0); 80 | addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 81 | const int bindRet = bind(s_, reinterpret_cast(&addr), sizeof(addr)); 82 | assert(bindRet == 0); 83 | 84 | const int listenRet = listen(s_, 1); 85 | assert(listenRet == 0); 86 | 87 | socklen_t addrLen = sizeof(addr); 88 | const int getRet = getsockname(s_, reinterpret_cast(&addr), &addrLen); 89 | assert(getRet == 0); 90 | 91 | port_ = ntohs(addr.sin_port); 92 | } 93 | 94 | int Socket::accept() { 95 | const int cs = ::accept(s_, nullptr, nullptr); 96 | assert(cs >= 0); 97 | setSocketNoDelay(cs); 98 | return cs; 99 | } 100 | 101 | void Socket::close() { 102 | if (s_ != -1) { 103 | ::close(s_); 104 | s_ = -1; 105 | } 106 | } 107 | 108 | static std::string randomString() { 109 | char buf[32] = {}; 110 | if (!SystemFunction036(&buf, sizeof(buf))) { 111 | assert(false && "RtlGenRandom failed"); 112 | } 113 | std::string out; 114 | for (char ch : buf) { 115 | out.push_back("0123456789ABCDEF"[(ch >> 4) & 0xF]); 116 | out.push_back("0123456789ABCDEF"[(ch >> 0) & 0xF]); 117 | } 118 | return out; 119 | } 120 | 121 | static std::wstring mbsToWcs(const std::string &s) { 122 | const size_t len = mbstowcs(nullptr, s.c_str(), 0); 123 | if (len == static_cast(-1)) { 124 | fatal("error: mbsToWcs: invalid string\n"); 125 | } 126 | std::wstring ret; 127 | ret.resize(len); 128 | const size_t len2 = mbstowcs(&ret[0], s.c_str(), len); 129 | assert(len == len2); 130 | return ret; 131 | } 132 | 133 | static std::string wcsToMbs(const std::wstring &s, bool emptyOnError=false) { 134 | const size_t len = wcstombs(nullptr, s.c_str(), 0); 135 | if (len == static_cast(-1)) { 136 | if (emptyOnError) { 137 | return {}; 138 | } 139 | fatal("error: wcsToMbs: invalid string\n"); 140 | } 141 | std::string ret; 142 | ret.resize(len); 143 | const size_t len2 = wcstombs(&ret[0], s.c_str(), len); 144 | assert(len == len2); 145 | return ret; 146 | } 147 | 148 | // As long as clients only get one chance to provide a key, this function 149 | // should be unnecessary. 150 | static bool secureStrEqual(const std::string &x, const std::string &y) { 151 | if (x.size() != y.size()) { 152 | return false; 153 | } 154 | volatile char ch = 0; 155 | volatile const char *xp = &x[0]; 156 | volatile const char *yp = &y[0]; 157 | for (size_t i = 0; i < x.size(); ++i) { 158 | ch |= (xp[i] ^ yp[i]); 159 | } 160 | return ch == 0; 161 | } 162 | 163 | static int acceptClientAndAuthenticate(Socket &socket, const std::string &key) { 164 | const int cs = socket.accept(); 165 | std::string checkBuf; 166 | checkBuf.resize(key.size()); 167 | size_t i = 0; 168 | while (i < checkBuf.size()) { 169 | const size_t remaining = checkBuf.size() - i; 170 | const ssize_t actual = read(cs, &checkBuf[i], remaining); 171 | assert(actual > 0 && static_cast(actual) <= remaining); 172 | i += actual; 173 | } 174 | if (!secureStrEqual(checkBuf, key)) { 175 | fatal("error: key check failed\n"); 176 | } 177 | return cs; 178 | } 179 | 180 | class TerminalState { 181 | private: 182 | std::mutex mutex_; 183 | bool inRawMode_ = false; 184 | bool modeValid_[2] = {false, false}; 185 | termios mode_[2] = {}; 186 | 187 | public: 188 | void enterRawMode(); 189 | 190 | private: 191 | void leaveRawMode(const std::lock_guard &lock); 192 | 193 | public: 194 | void fatal(const char *fmt, ...) 195 | __attribute__((noreturn)) 196 | __attribute__((format(printf, 2, 3))); 197 | void fatalv(const char *fmt, va_list ap) __attribute__((noreturn)); 198 | void exitCleanly(int exitStatus); 199 | }; 200 | 201 | // Put the input terminal into non-canonical mode. 202 | void TerminalState::enterRawMode() { 203 | std::lock_guard lock(mutex_); 204 | 205 | assert(!inRawMode_); 206 | inRawMode_ = true; 207 | 208 | for (int i = 0; i < 2; ++i) { 209 | if (!isatty(i)) { 210 | modeValid_[i] = false; 211 | } else { 212 | if (tcgetattr(i, &mode_[i]) < 0) { 213 | fatalPerror("tcgetattr failed"); 214 | } 215 | modeValid_[i] = true; 216 | } 217 | } 218 | 219 | if (modeValid_[0]) { 220 | termios buf; 221 | if (tcgetattr(0, &buf) < 0) { 222 | fatalPerror("tcgetattr failed"); 223 | } 224 | buf.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 225 | buf.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 226 | buf.c_cflag &= ~(CSIZE | PARENB); 227 | buf.c_cflag |= CS8; 228 | buf.c_cc[VMIN] = 1; // blocking read 229 | buf.c_cc[VTIME] = 0; 230 | if (tcsetattr(0, TCSAFLUSH, &buf) < 0) { 231 | fatalPerror("tcsetattr failed"); 232 | } 233 | } 234 | 235 | if (modeValid_[1]) { 236 | termios buf; 237 | if (tcgetattr(1, &buf) < 0) { 238 | fatalPerror("tcgetattr failed"); 239 | } 240 | buf.c_cflag &= ~(CSIZE | PARENB); 241 | buf.c_cflag |= CS8; 242 | buf.c_oflag &= ~OPOST; 243 | if (tcsetattr(1, TCSAFLUSH, &buf) < 0) { 244 | fatalPerror("tcsetattr failed"); 245 | } 246 | } 247 | } 248 | 249 | void TerminalState::leaveRawMode(const std::lock_guard &lock) { 250 | if (!inRawMode_) { 251 | return; 252 | } 253 | for (int i = 0; i < 2; ++i) { 254 | if (modeValid_[i]) { 255 | if (tcsetattr(i, TCSAFLUSH, &mode_[i]) < 0) { 256 | fatalPerror("error restoring terminal mode"); 257 | } 258 | } 259 | } 260 | } 261 | 262 | // This function cannot be used from a signal handler. 263 | void TerminalState::fatal(const char *fmt, ...) { 264 | va_list ap; 265 | va_start(ap, fmt); 266 | this->fatalv(fmt, ap); 267 | va_end(ap); 268 | } 269 | 270 | void TerminalState::fatalv(const char *fmt, va_list ap) { 271 | std::lock_guard lock(mutex_); 272 | leaveRawMode(lock); 273 | ::fatalv(fmt, ap); 274 | } 275 | 276 | void TerminalState::exitCleanly(int exitStatus) { 277 | std::lock_guard lock(mutex_); 278 | leaveRawMode(lock); 279 | fflush(stdout); 280 | fflush(stderr); 281 | // Avoid calling exit, which would call global destructors and destruct the 282 | // WakeupFd object. 283 | _exit(exitStatus); 284 | } 285 | 286 | static TerminalState g_terminalState; 287 | 288 | struct IoLoop { 289 | std::string spawnCwd; 290 | bool usePty = false; 291 | std::mutex mutex; 292 | bool ioFinished = false; 293 | int controlSocketFd = -1; 294 | bool childReaped = false; 295 | int childExitStatus = -1; 296 | }; 297 | 298 | static void fatalConnectionBroken() { 299 | g_terminalState.fatal("\nwslbridge error: connection broken\n"); 300 | } 301 | 302 | static void writePacket(IoLoop &ioloop, const Packet &p) { 303 | assert(p.size >= sizeof(p)); 304 | std::lock_guard lock(ioloop.mutex); 305 | if (!writeAllRestarting(ioloop.controlSocketFd, 306 | reinterpret_cast(&p), p.size)) { 307 | fatalConnectionBroken(); 308 | } 309 | } 310 | 311 | static void parentToSocketThread(int socketFd) { 312 | std::array buf; 313 | while (true) { 314 | const ssize_t amt1 = readRestarting(STDIN_FILENO, buf.data(), buf.size()); 315 | if (amt1 <= 0) { 316 | // If we reach EOF reading from stdin, propagate EOF to the child. 317 | close(socketFd); 318 | break; 319 | } 320 | if (!writeAllRestarting(socketFd, buf.data(), amt1)) { 321 | // We don't propagate EOF backwards, but we do let data build up. 322 | break; 323 | } 324 | } 325 | } 326 | 327 | static void socketToParentThread(IoLoop *ioloop, bool isErrorPipe, int socketFd, int outFd) { 328 | uint32_t bytesWritten = 0; 329 | std::array buf; 330 | while (true) { 331 | const ssize_t amt1 = readRestarting(socketFd, buf.data(), buf.size()); 332 | if (amt1 == 0) { 333 | std::lock_guard lock(ioloop->mutex); 334 | ioloop->ioFinished = true; 335 | g_wakeupFd->set(); 336 | break; 337 | } 338 | if (amt1 < 0) { 339 | break; 340 | } 341 | if (!writeAllRestarting(outFd, buf.data(), amt1)) { 342 | if (!ioloop->usePty && !isErrorPipe) { 343 | // ssh seems to propagate an stdout EOF backwards to the remote 344 | // program, so do the same thing. It doesn't do this for 345 | // stderr, though, where the remote process is allowed to block 346 | // forever. 347 | Packet p = { sizeof(Packet), Packet::Type::CloseStdoutPipe }; 348 | writePacket(*ioloop, p); 349 | } 350 | shutdown(socketFd, SHUT_RDWR); 351 | break; 352 | } 353 | bytesWritten += amt1; 354 | if (bytesWritten >= kOutputWindowSize / 2) { 355 | Packet p = { sizeof(Packet), Packet::Type::IncreaseWindow }; 356 | p.u.window.amount = bytesWritten; 357 | p.u.window.isErrorPipe = isErrorPipe; 358 | writePacket(*ioloop, p); 359 | bytesWritten = 0; 360 | } 361 | } 362 | } 363 | 364 | static void handlePacket(IoLoop *ioloop, const Packet &p) { 365 | switch (p.type) { 366 | case Packet::Type::ChildExitStatus: { 367 | std::lock_guard lock(ioloop->mutex); 368 | ioloop->childReaped = true; 369 | ioloop->childExitStatus = p.u.exitStatus; 370 | g_wakeupFd->set(); 371 | break; 372 | } 373 | case Packet::Type::SpawnFailed: { 374 | const PacketSpawnFailed &psf = reinterpret_cast(p); 375 | std::string msg; 376 | switch (p.u.spawnError.type) { 377 | case SpawnError::Type::ForkPtyFailed: 378 | msg = "error: forkpty failed: "; 379 | break; 380 | case SpawnError::Type::ChdirFailed: 381 | msg = "error: could not chdir to '" + ioloop->spawnCwd + "': "; 382 | break; 383 | case SpawnError::Type::ExecFailed: 384 | msg = "error: could not exec '" + std::string(psf.exe) + "': "; 385 | break; 386 | default: 387 | assert(false && "Unhandled SpawnError type"); 388 | } 389 | msg += errorString(p.u.spawnError.error); 390 | g_terminalState.fatal("%s\n", msg.c_str()); 391 | break; 392 | } 393 | default: { 394 | g_terminalState.fatal("internal error: unexpected packet %d\n", 395 | static_cast(p.type)); 396 | } 397 | } 398 | } 399 | 400 | static void mainLoop(const std::string &spawnCwd, 401 | bool usePty, int controlSocketFd, 402 | int inputSocketFd, int outputSocketFd, int errorSocketFd, 403 | TermSize termSize) { 404 | IoLoop ioloop; 405 | ioloop.spawnCwd = spawnCwd; 406 | ioloop.usePty = usePty; 407 | ioloop.controlSocketFd = controlSocketFd; 408 | std::thread p2s(parentToSocketThread, inputSocketFd); 409 | std::thread s2p(socketToParentThread, &ioloop, false, outputSocketFd, STDOUT_FILENO); 410 | std::unique_ptr es2p; 411 | if (errorSocketFd != -1) { 412 | es2p = std::unique_ptr( 413 | new std::thread(socketToParentThread, &ioloop, true, errorSocketFd, STDERR_FILENO)); 414 | } 415 | std::thread rcs(readControlSocketThread, 416 | controlSocketFd, &ioloop); 417 | int32_t exitStatus = -1; 418 | 419 | while (true) { 420 | g_wakeupFd->wait(); 421 | const auto newSize = terminalSize(); 422 | if (newSize != termSize) { 423 | Packet p = { sizeof(Packet), Packet::Type::SetSize }; 424 | p.u.termSize = termSize = newSize; 425 | writePacket(ioloop, p); 426 | } 427 | std::lock_guard lock(ioloop.mutex); 428 | if (ioloop.childReaped && ioloop.ioFinished) { 429 | exitStatus = ioloop.childExitStatus; 430 | break; 431 | } 432 | } 433 | 434 | // Socket-to-pty I/O is finished already. 435 | s2p.join(); 436 | 437 | // We can't return, because the threads could still be running. Rather 438 | // than shut them down gracefully, which seems hard(?), just let the OS 439 | // clean everything up. 440 | g_terminalState.exitCleanly(exitStatus); 441 | } 442 | 443 | static bool pathExists(const std::wstring &path) { 444 | return GetFileAttributesW(path.c_str()) != 0xFFFFFFFF; 445 | } 446 | 447 | static std::wstring dirname(const std::wstring &path) { 448 | std::wstring::size_type pos = path.find_last_of(L"\\/"); 449 | if (pos == std::wstring::npos) { 450 | return L""; 451 | } else { 452 | return path.substr(0, pos); 453 | } 454 | } 455 | 456 | static HMODULE getCurrentModule() { 457 | HMODULE module; 458 | if (!GetModuleHandleExW( 459 | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | 460 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, 461 | reinterpret_cast(getCurrentModule), 462 | &module)) { 463 | fatal("error: GetModuleHandleEx failed\n"); 464 | } 465 | return module; 466 | } 467 | 468 | static std::wstring getModuleFileName(HMODULE module) { 469 | const int bufsize = 4096; 470 | wchar_t path[bufsize]; 471 | int size = GetModuleFileNameW(module, path, bufsize); 472 | assert(size != 0 && size != bufsize); 473 | return std::wstring(path); 474 | } 475 | 476 | static std::wstring findBackendProgram(const std::string &customBackendPath) { 477 | std::wstring ret; 478 | if (!customBackendPath.empty()) { 479 | char *winPath = static_cast( 480 | cygwin_create_path(CCP_POSIX_TO_WIN_A, customBackendPath.c_str())); 481 | if (winPath == nullptr) { 482 | fatalPerror(("error: bad path: '" + customBackendPath + "'").c_str()); 483 | } 484 | ret = mbsToWcs(winPath); 485 | free(winPath); 486 | } else { 487 | const auto progDir = dirname(getModuleFileName(getCurrentModule())); 488 | ret = progDir + (L"\\" BACKEND_PROGRAM); 489 | } 490 | if (!pathExists(ret)) { 491 | fatal("error: '%s' backend program is missing\n", 492 | wcsToMbs(ret).c_str()); 493 | } 494 | return ret; 495 | } 496 | 497 | static wchar_t lowerDrive(wchar_t ch) { 498 | if (ch >= L'a' && ch <= L'z') { 499 | return ch; 500 | } else if (ch >= L'A' && ch <= 'Z') { 501 | return ch - L'A' + L'a'; 502 | } else { 503 | return L'\0'; 504 | } 505 | } 506 | 507 | static std::pair 508 | normalizePath(const std::wstring &path) { 509 | const auto getFinalPathName = [&](HANDLE h) -> std::wstring { 510 | std::wstring ret; 511 | ret.resize(MAX_PATH + 1); 512 | while (true) { 513 | const auto sz = GetFinalPathNameByHandleW(h, &ret[0], ret.size(), 0); 514 | if (sz == 0) { 515 | fatal("error: GetFinalPathNameByHandle failed on '%s'\n", 516 | wcsToMbs(path).c_str()); 517 | } else if (sz < ret.size()) { 518 | ret.resize(sz); 519 | return ret; 520 | } else { 521 | assert(sz > ret.size()); 522 | ret.resize(sz); 523 | } 524 | } 525 | }; 526 | const auto h = CreateFileW( 527 | path.c_str(), 528 | GENERIC_READ, 529 | FILE_SHARE_READ | FILE_SHARE_WRITE, 530 | nullptr, 531 | OPEN_EXISTING, 0, nullptr); 532 | if (h == INVALID_HANDLE_VALUE) { 533 | fatal("error: could not open '%s'\n", wcsToMbs(path).c_str()); 534 | } 535 | auto npath = getFinalPathName(h); 536 | std::array fsname; 537 | fsname.back() = L'\0'; 538 | if (!GetVolumeInformationByHandleW( 539 | h, nullptr, 0, nullptr, nullptr, nullptr, 540 | &fsname[0], fsname.size())) { 541 | fsname[0] = L'\0'; 542 | } 543 | CloseHandle(h); 544 | // Example of GetFinalPathNameByHandle result: 545 | // \\?\C:\cygwin64\bin\wslbridge-backend 546 | // 0123456 547 | // \\?\UNC\server\share\file 548 | // 01234567 549 | if (npath.size() >= 7 && 550 | npath.substr(0, 4) == L"\\\\?\\" && 551 | lowerDrive(npath[4]) && 552 | npath.substr(5, 2) == L":\\") { 553 | // Strip off the atypical \\?\ prefix. 554 | npath = npath.substr(4); 555 | } else if (npath.substr(0, 8) == L"\\\\?\\UNC\\") { 556 | // Strip off the \\\\?\\UNC\\ prefix and replace it with \\. 557 | npath = L"\\\\" + npath.substr(8); 558 | } 559 | return std::make_pair(std::move(npath), fsname.data()); 560 | } 561 | 562 | static std::wstring convertPathToWsl(const std::wstring &path) { 563 | const auto isSlash = [](wchar_t ch) -> bool { 564 | return ch == L'/' || ch == L'\\'; 565 | }; 566 | if (path.size() >= 3) { 567 | const auto drive = lowerDrive(path[0]); 568 | if (drive && path[1] == L':' && isSlash(path[2])) { 569 | // Acceptable path. 570 | std::wstring ret = L"/mnt/"; 571 | ret.push_back(drive); 572 | ret.append(path.substr(2)); 573 | for (wchar_t &ch : ret) { 574 | if (ch == L'\\') { 575 | ch = L'/'; 576 | } 577 | } 578 | return ret; 579 | } 580 | } 581 | fatal( 582 | "error: the backend program '%s' must be located on a " 583 | "letter drive so WSL can access it with a /mnt/ path\n", 584 | wcsToMbs(path).c_str()); 585 | } 586 | 587 | static std::wstring findSystemProgram(const wchar_t *name) { 588 | std::array windir; 589 | windir[0] = L'\0'; 590 | if (GetWindowsDirectoryW(windir.data(), windir.size()) == 0) { 591 | fatal("error: GetWindowsDirectory call failed\n"); 592 | } 593 | const wchar_t *const kPart32 = L"\\System32\\"; 594 | const auto path = [&](const wchar_t *part) -> std::wstring { 595 | return std::wstring(windir.data()) + part + name; 596 | }; 597 | #if defined(__x86_64__) 598 | const auto ret = path(kPart32); 599 | if (pathExists(ret)) { 600 | return ret; 601 | } else { 602 | fatal("error: '%s' does not exist\n" 603 | "note: Ubuntu-on-Windows must be installed\n", 604 | wcsToMbs(ret).c_str()); 605 | } 606 | #elif defined(__i386__) 607 | const wchar_t *const kPartNat = L"\\Sysnative\\"; 608 | const auto pathNat = path(kPartNat); 609 | if (pathExists(pathNat)) { 610 | return std::move(pathNat); 611 | } 612 | const auto path32 = path(kPart32); 613 | if (pathExists(path32)) { 614 | return std::move(path32); 615 | } 616 | fatal("error: neither '%s' nor '%s' exist\n" 617 | "note: Ubuntu-on-Windows must be installed\n", 618 | wcsToMbs(pathNat).c_str(), wcsToMbs(path32).c_str()); 619 | #else 620 | #error "Could not determine architecture" 621 | #endif 622 | } 623 | 624 | static void usage(const char *prog) { 625 | printf("Usage: %s [options] [--] [command]...\n", prog); 626 | printf("Runs a program within a Windows Subsystem for Linux (WSL) pty\n"); 627 | printf("\n"); 628 | printf("Options:\n"); 629 | printf(" -C WSLDIR Changes the working directory to WSLDIR first.\n"); 630 | printf(" An initial '~' indicates the WSL home directory.\n"); 631 | printf(" -e VAR Copies VAR into the WSL environment.\n"); 632 | printf(" -e VAR=VAL Sets VAR to VAL in the WSL environment.\n"); 633 | printf(" -l Start a login shell.\n"); 634 | printf(" --no-login Do not start a login shell.\n"); 635 | printf(" -T Do not use a pty.\n"); 636 | printf(" -t Use a pty (as long as stdin is a tty).\n"); 637 | printf(" -t -t Force a pty (even if stdin is not a tty).\n"); 638 | printf(" --distro-guid GUID\n"); 639 | printf(" Uses the WSL distribution identified by GUID.\n"); 640 | printf(" See HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\n"); 641 | printf(" for a list of distribution GUIDs.\n"); 642 | printf(" --backend BACKEND\n"); 643 | printf(" Overrides the default path to wslbridge-backend. BACKEND is a\n"); 644 | printf(" Cygwin-style path (not a WSL path).\n"); 645 | exit(0); 646 | } 647 | 648 | class Environment { 649 | public: 650 | void set(const std::string &var) { 651 | const char *value = getenv(var.c_str()); 652 | if (value != nullptr) { 653 | set(var, value); 654 | } 655 | } 656 | 657 | void set(const std::string &var, const std::string &value) { 658 | pairs_.push_back(std::make_pair(mbsToWcs(var), mbsToWcs(value))); 659 | } 660 | 661 | bool hasVar(const std::wstring &var) { 662 | for (const auto &pair : pairs_) { 663 | if (pair.first == var) { 664 | return true; 665 | } 666 | } 667 | return false; 668 | } 669 | 670 | const std::vector> &pairs() { return pairs_; } 671 | 672 | private: 673 | std::vector> pairs_; 674 | }; 675 | 676 | static void appendBashArg(std::wstring &out, const std::wstring &arg) { 677 | if (!out.empty()) { 678 | out.push_back(L' '); 679 | } 680 | const auto isCharSafe = [](wchar_t ch) -> bool { 681 | switch (ch) { 682 | case L'%': 683 | case L'+': 684 | case L',': 685 | case L'-': 686 | case L'.': 687 | case L'/': 688 | case L':': 689 | case L'=': 690 | case L'@': 691 | case L'_': 692 | case L'{': 693 | case L'}': 694 | return true; 695 | default: 696 | return (ch >= L'0' && ch <= L'9') || 697 | (ch >= L'a' && ch <= L'z') || 698 | (ch >= L'A' && ch <= L'Z'); 699 | } 700 | }; 701 | if (arg.empty()) { 702 | out.append(L"''"); 703 | return; 704 | } 705 | if (std::all_of(arg.begin(), arg.end(), isCharSafe)) { 706 | out.append(arg); 707 | return; 708 | } 709 | bool inQuote = false; 710 | const auto enterQuote = [&](bool newInQuote) { 711 | if (inQuote != newInQuote) { 712 | out.push_back(L'\''); 713 | inQuote = newInQuote; 714 | } 715 | }; 716 | enterQuote(true); 717 | for (auto ch : arg) { 718 | if (ch == L'\'') { 719 | enterQuote(false); 720 | out.append(L"\\'"); 721 | enterQuote(true); 722 | } else if (isCharSafe(ch)) { 723 | out.push_back(ch); 724 | } else { 725 | out.push_back(ch); 726 | } 727 | } 728 | enterQuote(false); 729 | } 730 | 731 | static std::string errorMessageToString(DWORD err) { 732 | // Use FormatMessageW rather than FormatMessageA, because we want to use 733 | // wcstombs to convert to the Cygwin locale, which might not match the 734 | // codepage FormatMessageA would use. We need to convert using wcstombs, 735 | // rather than print using %ls, because %ls doesn't work in the original 736 | // MSYS. 737 | wchar_t *wideMsgPtr = NULL; 738 | const DWORD formatRet = FormatMessageW( 739 | FORMAT_MESSAGE_FROM_SYSTEM | 740 | FORMAT_MESSAGE_ALLOCATE_BUFFER | 741 | FORMAT_MESSAGE_IGNORE_INSERTS, 742 | NULL, 743 | err, 744 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 745 | reinterpret_cast(&wideMsgPtr), 746 | 0, 747 | NULL); 748 | if (formatRet == 0 || wideMsgPtr == NULL) { 749 | return std::string(); 750 | } 751 | std::string msg = wcsToMbs(wideMsgPtr); 752 | LocalFree(wideMsgPtr); 753 | const size_t pos = msg.find_last_not_of(" \r\n\t"); 754 | if (pos == std::string::npos) { 755 | msg.clear(); 756 | } else { 757 | msg.erase(pos + 1); 758 | } 759 | return msg; 760 | } 761 | 762 | static std::string formatErrorMessage(DWORD err) { 763 | char buf[64]; 764 | sprintf(buf, "error %#x", static_cast(err)); 765 | std::string ret = errorMessageToString(err); 766 | if (ret.empty()) { 767 | ret += buf; 768 | } else { 769 | ret += " ("; 770 | ret += buf; 771 | ret += ")"; 772 | } 773 | return ret; 774 | } 775 | 776 | struct PipeHandles { 777 | HANDLE rh; 778 | HANDLE wh; 779 | }; 780 | 781 | static PipeHandles createPipe() { 782 | SECURITY_ATTRIBUTES sa {}; 783 | sa.nLength = sizeof(sa); 784 | sa.bInheritHandle = TRUE; 785 | PipeHandles ret {}; 786 | const BOOL success = CreatePipe(&ret.rh, &ret.wh, &sa, 0); 787 | assert(success && "CreatePipe failed"); 788 | return ret; 789 | } 790 | 791 | class StartupInfoAttributeList { 792 | public: 793 | StartupInfoAttributeList(PPROC_THREAD_ATTRIBUTE_LIST &attrList, int count) { 794 | SIZE_T size {}; 795 | InitializeProcThreadAttributeList(nullptr, count, 0, &size); 796 | assert(size > 0 && "InitializeProcThreadAttributeList failed"); 797 | buffer_ = std::unique_ptr(new char[size]); 798 | const BOOL success = InitializeProcThreadAttributeList(get(), count, 0, &size); 799 | assert(success && "InitializeProcThreadAttributeList failed"); 800 | attrList = get(); 801 | } 802 | StartupInfoAttributeList(const StartupInfoAttributeList &) = delete; 803 | StartupInfoAttributeList &operator=(const StartupInfoAttributeList &) = delete; 804 | ~StartupInfoAttributeList() { 805 | DeleteProcThreadAttributeList(get()); 806 | } 807 | private: 808 | PPROC_THREAD_ATTRIBUTE_LIST get() { 809 | return reinterpret_cast(buffer_.get()); 810 | } 811 | std::unique_ptr buffer_; 812 | }; 813 | 814 | class StartupInfoInheritList { 815 | public: 816 | StartupInfoInheritList(PPROC_THREAD_ATTRIBUTE_LIST attrList, 817 | std::vector &&inheritList) : 818 | inheritList_(std::move(inheritList)) { 819 | const BOOL success = UpdateProcThreadAttribute( 820 | attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, 821 | inheritList_.data(), inheritList_.size() * sizeof(HANDLE), 822 | nullptr, nullptr); 823 | assert(success && "UpdateProcThreadAttribute failed"); 824 | } 825 | StartupInfoInheritList(const StartupInfoInheritList &) = delete; 826 | StartupInfoInheritList &operator=(const StartupInfoInheritList &) = delete; 827 | ~StartupInfoInheritList() {} 828 | private: 829 | std::vector inheritList_; 830 | }; 831 | 832 | // WSL bash will print an error if the user tries to run elevated and 833 | // non-elevated instances simultaneously, and maybe other situations. We'd 834 | // like to detect this situation and report the error back to the user. 835 | // 836 | // Two complications: 837 | // - WSL bash will print the error to stdout/stderr, but if the file is a 838 | // pipe, then WSL bash doesn't print it until it exits (presumably due to 839 | // block buffering). 840 | // - WSL bash puts up a prompt, "Press any key to continue", and it reads 841 | // that key from the attached console, not from stdin. 842 | // 843 | // This function spawns the frontend again and instructs it to attach to the 844 | // new WSL bash console and send it a return keypress. 845 | // 846 | // The HANDLE must be inheritable. 847 | static void spawnPressReturnProcess(HANDLE bashProcess) { 848 | const auto exePath = getModuleFileName(getCurrentModule()); 849 | std::wstring cmdline; 850 | cmdline.append(L"\""); 851 | cmdline.append(exePath); 852 | cmdline.append(L"\" --press-return "); 853 | cmdline.append(std::to_wstring(reinterpret_cast(bashProcess))); 854 | STARTUPINFOEXW sui {}; 855 | sui.StartupInfo.cb = sizeof(sui); 856 | StartupInfoAttributeList attrList { sui.lpAttributeList, 1 }; 857 | StartupInfoInheritList inheritList { sui.lpAttributeList, { bashProcess } }; 858 | PROCESS_INFORMATION pi {}; 859 | const BOOL success = CreateProcessW(exePath.c_str(), &cmdline[0], nullptr, nullptr, 860 | true, 0, nullptr, nullptr, &sui.StartupInfo, &pi); 861 | if (!success) { 862 | fprintf(stderr, "wslbridge warning: could not spawn: %s\n", wcsToMbs(cmdline).c_str()); 863 | } 864 | if (WaitForSingleObject(pi.hProcess, 10000) != WAIT_OBJECT_0) { 865 | fprintf(stderr, "wslbridge warning: process didn't exit after 10 seconds: %ls\n", 866 | cmdline.c_str()); 867 | } else { 868 | DWORD code {}; 869 | BOOL success = GetExitCodeProcess(pi.hProcess, &code); 870 | if (!success) { 871 | fprintf(stderr, "wslbridge warning: GetExitCodeProcess failed\n"); 872 | } else if (code != 0) { 873 | fprintf(stderr, "wslbridge warning: process failed: %ls\n", cmdline.c_str()); 874 | } 875 | } 876 | CloseHandle(pi.hProcess); 877 | CloseHandle(pi.hThread); 878 | } 879 | 880 | static int handlePressReturn(const char *pidStr) { 881 | // AttachConsole replaces STD_INPUT_HANDLE with a new console input 882 | // handle. See https://github.com/rprichard/win32-console-docs. The 883 | // bash.exe process has already started, but console creation and 884 | // process creation don't happen atomically, so poll for the console's 885 | // existence. 886 | auto str2handle = [](const char *str) { 887 | std::stringstream ss(str); 888 | uintptr_t n {}; 889 | ss >> n; 890 | return reinterpret_cast(n); 891 | }; 892 | const HANDLE bashProcess = str2handle(pidStr); 893 | const DWORD bashPid = GetProcessId(bashProcess); 894 | FreeConsole(); 895 | for (int i = 0; i < 400; ++i) { 896 | if (WaitForSingleObject(bashProcess, 0) == WAIT_OBJECT_0) { 897 | // bash.exe has exited, give up immediately. 898 | return 0; 899 | } else if (AttachConsole(bashPid)) { 900 | std::array ir {}; 901 | ir[0].EventType = KEY_EVENT; 902 | ir[0].Event.KeyEvent.bKeyDown = TRUE; 903 | ir[0].Event.KeyEvent.wRepeatCount = 1; 904 | ir[0].Event.KeyEvent.wVirtualKeyCode = VK_RETURN; 905 | ir[0].Event.KeyEvent.wVirtualScanCode = MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC); 906 | ir[0].Event.KeyEvent.uChar.UnicodeChar = '\r'; 907 | ir[1] = ir[0]; 908 | ir[1].Event.KeyEvent.bKeyDown = FALSE; 909 | DWORD actual {}; 910 | WriteConsoleInputW( 911 | GetStdHandle(STD_INPUT_HANDLE), 912 | ir.data(), ir.size(), &actual); 913 | return 0; 914 | } 915 | Sleep(25); 916 | } 917 | return 1; 918 | } 919 | 920 | static std::vector readAllFromHandle(HANDLE h) { 921 | std::vector ret; 922 | char buf[1024]; 923 | DWORD actual {}; 924 | while (ReadFile(h, buf, sizeof(buf), &actual, nullptr) && actual > 0) { 925 | ret.insert(ret.end(), buf, buf + actual); 926 | } 927 | return ret; 928 | } 929 | 930 | static std::tuple windowsVersion() { 931 | OSVERSIONINFO info {}; 932 | info.dwOSVersionInfoSize = sizeof(info); 933 | const BOOL success = GetVersionEx(&info); 934 | assert(success && "GetVersionEx failed"); 935 | if (info.dwMajorVersion == 6 && info.dwMinorVersion == 2) { 936 | // We want to distinguish between Windows 10.0.14393 and 10.0.15063, 937 | // but if the EXE doesn't have an appropriate manifest, then 938 | // GetVersionEx will report the lesser of 6.2 and the true version. 939 | fprintf(stderr, "wslbridge warning: GetVersionEx reports version 6.2 -- " 940 | "is wslbridge.exe properly manifested?\n"); 941 | } 942 | return std::make_tuple(info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber); 943 | } 944 | 945 | static std::string replaceAll(std::string str, const std::string &from, const std::string &to) { 946 | size_t pos {}; 947 | while ((pos = str.find(from, pos)) != std::string::npos) { 948 | str = str.replace(pos, from.size(), to); 949 | pos += to.size(); 950 | } 951 | return str; 952 | } 953 | 954 | static std::string stripTrailing(std::string str) { 955 | while (!str.empty() && isspace(str.back())) { 956 | str.pop_back(); 957 | } 958 | return str; 959 | } 960 | 961 | // Ensure that the GUID is lower-case and surrounded with braces. 962 | // If the string isn't a valid GUID, then return an empty string. 963 | static std::string canonicalGuid(std::string guid) { 964 | if (guid.size() == 38 && guid[0] == '{' && guid[37] == '}') { 965 | // OK 966 | } else if (guid.size() == 36) { 967 | guid = '{' + guid + '}'; 968 | } else { 969 | return {}; 970 | } 971 | assert(guid.size() == 38); 972 | for (size_t i = 1; i <= 36; ++i) { 973 | if (i == 9 || i == 14 || i == 19 || i == 24) { 974 | if (guid[i] != '-') { 975 | return {}; 976 | } 977 | } else { 978 | guid[i] = tolower(guid[i]); 979 | if (!isxdigit(guid[i])) { 980 | return {}; 981 | } 982 | } 983 | } 984 | return guid; 985 | } 986 | 987 | } // namespace 988 | 989 | int main(int argc, char *argv[]) { 990 | setlocale(LC_ALL, ""); 991 | cygwin_internal(CW_SYNC_WINENV); 992 | g_wakeupFd = new WakeupFd(); 993 | 994 | if (argc == 3 && !strcmp(argv[1], "--press-return")) { 995 | return handlePressReturn(argv[2]); 996 | } 997 | 998 | Environment env; 999 | std::string spawnCwd; 1000 | std::string distroGuid; 1001 | std::string customBackendPath; 1002 | enum class TtyRequest { Auto, Yes, No, Force } ttyRequest = TtyRequest::Auto; 1003 | enum class LoginMode { Auto, Yes, No } loginMode = LoginMode::Auto; 1004 | 1005 | int debugFork = 0; 1006 | int c = 0; 1007 | if (argv[0][0] == '-') { 1008 | loginMode = LoginMode::Yes; 1009 | } 1010 | const struct option kOptionTable[] = { 1011 | { "help", false, nullptr, 'h' }, 1012 | { "debug-fork", false, &debugFork, 1 }, 1013 | { "version", false, nullptr, 'v' }, 1014 | { "distro-guid", true, nullptr, 'd' }, 1015 | { "no-login", false, nullptr, 'L' }, 1016 | { "backend", true, nullptr, 'b' }, 1017 | { nullptr, false, nullptr, 0 }, 1018 | }; 1019 | while ((c = getopt_long(argc, argv, "+e:C:tTl", kOptionTable, nullptr)) != -1) { 1020 | switch (c) { 1021 | case 0: 1022 | // Ignore long option. 1023 | break; 1024 | case 'e': { 1025 | const char *eq = strchr(optarg, '='); 1026 | const auto varname = eq ? std::string(optarg, eq - optarg) : std::string(optarg); 1027 | if (varname.empty()) { 1028 | fatal("error: -e variable name cannot be empty: '%s'\n", optarg); 1029 | } 1030 | if (eq) { 1031 | env.set(varname, eq + 1); 1032 | } else { 1033 | env.set(varname); 1034 | } 1035 | break; 1036 | } 1037 | case 'C': 1038 | spawnCwd = optarg; 1039 | if (spawnCwd.empty()) { 1040 | fatal("error: the -C option requires a non-empty string argument\n"); 1041 | } 1042 | break; 1043 | case 'h': 1044 | usage(argv[0]); 1045 | break; 1046 | case 't': 1047 | if (ttyRequest == TtyRequest::Yes) { 1048 | ttyRequest = TtyRequest::Force; 1049 | } else { 1050 | ttyRequest = TtyRequest::Yes; 1051 | } 1052 | break; 1053 | case 'T': 1054 | ttyRequest = TtyRequest::No; 1055 | break; 1056 | case 'v': 1057 | printf("wslbridge " STRINGIFY(WSLBRIDGE_VERSION) "\n"); 1058 | exit(0); 1059 | case 'd': 1060 | distroGuid = canonicalGuid(optarg); 1061 | if (distroGuid.empty()) { 1062 | fatal("error: the --distro-guid argument '%s' is invalid\n", optarg); 1063 | } 1064 | break; 1065 | case 'l': 1066 | loginMode = LoginMode::Yes; 1067 | break; 1068 | case 'L': 1069 | loginMode = LoginMode::No; 1070 | break; 1071 | case 'b': 1072 | customBackendPath = optarg; 1073 | if (customBackendPath.empty()) { 1074 | fatal("error: the --backend option requires a non-empty string argument\n"); 1075 | } 1076 | break; 1077 | default: 1078 | fatal("Try '%s --help' for more information.\n", argv[0]); 1079 | } 1080 | } 1081 | 1082 | const bool hasCommand = optind < argc; 1083 | if (loginMode == LoginMode::Auto) { 1084 | loginMode = hasCommand ? LoginMode::No : LoginMode::Yes; 1085 | } 1086 | if (ttyRequest == TtyRequest::Auto) { 1087 | ttyRequest = loginMode == LoginMode::No ? TtyRequest::No : TtyRequest::Yes; 1088 | } 1089 | if (ttyRequest == TtyRequest::Yes && !isatty(STDIN_FILENO)) { 1090 | fprintf(stderr, "Pseudo-terminal will not be allocated because stdin is not a terminal.\n"); 1091 | ttyRequest = TtyRequest::No; 1092 | } 1093 | const bool usePty = ttyRequest != TtyRequest::No; 1094 | 1095 | if (!env.hasVar(L"TERM")) { 1096 | // This seems to be what OpenSSH is doing. 1097 | if (usePty) { 1098 | const char *termVal = getenv("TERM"); 1099 | env.set("TERM", termVal && *termVal ? termVal : "dumb"); 1100 | } else { 1101 | env.set("TERM", "dumb"); 1102 | } 1103 | } 1104 | 1105 | // We must register this handler *before* determining the initial terminal 1106 | // size. 1107 | struct sigaction sa = {}; 1108 | sa.sa_handler = [](int signo) { g_wakeupFd->set(); }; 1109 | sa.sa_flags = SA_RESTART; 1110 | ::sigaction(SIGWINCH, &sa, nullptr); 1111 | sa = {}; 1112 | // We want to handle EPIPE rather than receiving SIGPIPE. 1113 | signal(SIGPIPE, SIG_IGN); 1114 | 1115 | Socket controlSocket; 1116 | Socket inputSocket; 1117 | Socket outputSocket; 1118 | std::unique_ptr errorSocket; 1119 | if (!usePty) { 1120 | errorSocket = std::unique_ptr(new Socket); 1121 | } 1122 | 1123 | const auto bashPath = findSystemProgram(L"bash.exe"); 1124 | const auto backendPathInfo = normalizePath(findBackendProgram(customBackendPath)); 1125 | const auto backendPathWin = backendPathInfo.first; 1126 | const auto fsname = backendPathInfo.second; 1127 | const auto backendPathWsl = convertPathToWsl(backendPathWin); 1128 | const auto initialSize = terminalSize(); 1129 | const auto key = randomString(); 1130 | 1131 | // Prepare the backend command line. 1132 | std::wstring bashCmdLine; 1133 | bashCmdLine.append(L"\"$(if [ \"$(command -v wslpath)\" ]; then wslpath"); 1134 | appendBashArg(bashCmdLine, backendPathWin); 1135 | bashCmdLine.append(L" || echo false; else echo"); 1136 | appendBashArg(bashCmdLine, backendPathWsl); 1137 | bashCmdLine.append(L"; fi)\""); 1138 | 1139 | if (debugFork) { 1140 | appendBashArg(bashCmdLine, L"--debug-fork"); 1141 | } 1142 | 1143 | appendBashArg(bashCmdLine, L"--check-version=" STRINGIFY(WSLBRIDGE_VERSION)); 1144 | 1145 | std::array buffer; 1146 | int iRet = swprintf(buffer.data(), buffer.size(), 1147 | L" -3%d -0%d -1%d -k%s -w%d -t%d", 1148 | controlSocket.port(), 1149 | inputSocket.port(), 1150 | outputSocket.port(), 1151 | key.c_str(), 1152 | kOutputWindowSize, 1153 | kOutputWindowSize / 4); 1154 | assert(iRet > 0); 1155 | bashCmdLine.append(buffer.data()); 1156 | 1157 | if (usePty) { 1158 | iRet = swprintf(buffer.data(), buffer.size(), 1159 | L" --pty -c%d -r%d", 1160 | initialSize.cols, 1161 | initialSize.rows); 1162 | } else { 1163 | iRet = swprintf(buffer.data(), buffer.size(), 1164 | L" --pipes -2%d", 1165 | errorSocket->port()); 1166 | } 1167 | assert(iRet > 0); 1168 | bashCmdLine.append(buffer.data()); 1169 | 1170 | if (loginMode == LoginMode::Yes) { 1171 | appendBashArg(bashCmdLine, L"-l"); 1172 | } 1173 | for (const auto &envPair : env.pairs()) { 1174 | appendBashArg(bashCmdLine, L"-e" + envPair.first + L"=" + envPair.second); 1175 | } 1176 | if (!spawnCwd.empty()) { 1177 | appendBashArg(bashCmdLine, L"-C" + mbsToWcs(spawnCwd)); 1178 | } 1179 | appendBashArg(bashCmdLine, L"--"); 1180 | for (int i = optind; i < argc; ++i) { 1181 | appendBashArg(bashCmdLine, mbsToWcs(argv[i])); 1182 | } 1183 | 1184 | std::wstring cmdLine; 1185 | cmdLine.append(L"\""); 1186 | cmdLine.append(bashPath); 1187 | cmdLine.append(L"\""); 1188 | if (!distroGuid.empty()) { 1189 | cmdLine.append(L" "); 1190 | cmdLine.append(mbsToWcs(distroGuid)); 1191 | } 1192 | cmdLine.append(L" -c "); 1193 | appendBashArg(cmdLine, bashCmdLine); 1194 | 1195 | const auto outputPipe = createPipe(); 1196 | const auto errorPipe = createPipe(); 1197 | STARTUPINFOEXW sui {}; 1198 | sui.StartupInfo.cb = sizeof(sui); 1199 | StartupInfoAttributeList attrList { sui.lpAttributeList, 1 }; 1200 | StartupInfoInheritList inheritList { sui.lpAttributeList, 1201 | { outputPipe.wh, errorPipe.wh } 1202 | }; 1203 | 1204 | if (windowsVersion() >= std::make_tuple(10u, 0u, 15063u)) { 1205 | // WSL was first officially shipped in 14393, but in that version, 1206 | // bash.exe did not allow redirecting stdout/stderr to a pipe. 1207 | // Redirection is allowed starting with 15063, and we'd like to use it 1208 | // to help report errors. 1209 | sui.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; 1210 | sui.StartupInfo.hStdOutput = outputPipe.wh; 1211 | sui.StartupInfo.hStdError = errorPipe.wh; 1212 | } 1213 | 1214 | PROCESS_INFORMATION pi = {}; 1215 | BOOL success = CreateProcessW(bashPath.c_str(), &cmdLine[0], nullptr, nullptr, 1216 | true, 1217 | debugFork ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW, 1218 | nullptr, nullptr, &sui.StartupInfo, &pi); 1219 | if (!success) { 1220 | fatal("error starting bash.exe adapter: %s\n", 1221 | formatErrorMessage(GetLastError()).c_str()); 1222 | } 1223 | 1224 | CloseHandle(outputPipe.wh); 1225 | CloseHandle(errorPipe.wh); 1226 | success = SetHandleInformation(pi.hProcess, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); 1227 | assert(success && "SetHandleInformation failed"); 1228 | spawnPressReturnProcess(pi.hProcess); 1229 | 1230 | std::atomic backendStarted = { false }; 1231 | 1232 | // If the backend process exits before the frontend, then something has 1233 | // gone wrong. 1234 | const auto watchdog = std::thread([&]() { 1235 | WaitForSingleObject(pi.hProcess, INFINITE); 1236 | 1237 | // Because bash.exe has exited, we know that the write ends of the 1238 | // output pipes are closed. Finish reading anything bash.exe wrote. 1239 | // bash.exe writes at least one error via stdout in UTF-16; 1240 | // wslbridge-backend could write to stderr in UTF-8. 1241 | auto outVec = readAllFromHandle(outputPipe.rh); 1242 | auto errVec = readAllFromHandle(errorPipe.rh); 1243 | std::wstring outWide(outVec.size() / sizeof(wchar_t), L'\0'); 1244 | memcpy(&outWide[0], outVec.data(), outWide.size() * sizeof(wchar_t)); 1245 | std::string out { wcsToMbs(outWide, true) }; 1246 | std::string err { errVec.begin(), errVec.end() }; 1247 | out = stripTrailing(replaceAll(out, "Press any key to continue...", "")); 1248 | err = stripTrailing(err); 1249 | 1250 | std::string msg; 1251 | if (backendStarted) { 1252 | msg = "\nwslbridge error: backend process died\n"; 1253 | } else { 1254 | msg = "wslbridge error: failed to start backend process\n"; 1255 | if (fsname != L"NTFS") { 1256 | msg.append("note: backend program is at '"); 1257 | msg.append(wcsToMbs(backendPathWin)); 1258 | msg.append("'\n"); 1259 | msg.append("note: backend is on a volume of type '"); 1260 | msg.append(wcsToMbs(fsname)); 1261 | msg.append("', expected 'NTFS'\n" 1262 | "note: WSL only supports local NTFS volumes\n"); 1263 | } 1264 | } 1265 | if (!out.empty()) { 1266 | msg.append("note: bash.exe output: "); 1267 | msg.append(out); 1268 | msg.push_back('\n'); 1269 | } 1270 | if (!err.empty()) { 1271 | msg.append("note: backend error output: "); 1272 | msg.append(err); 1273 | msg.push_back('\n'); 1274 | } 1275 | g_terminalState.fatal("%s", msg.c_str()); 1276 | }); 1277 | 1278 | const int controlSocketC = acceptClientAndAuthenticate(controlSocket, key); 1279 | const int inputSocketC = acceptClientAndAuthenticate(inputSocket, key); 1280 | const int outputSocketC = acceptClientAndAuthenticate(outputSocket, key); 1281 | const int errorSocketC = !errorSocket ? -1 : acceptClientAndAuthenticate(*errorSocket, key); 1282 | controlSocket.close(); 1283 | inputSocket.close(); 1284 | outputSocket.close(); 1285 | if (errorSocket) { errorSocket->close(); } 1286 | 1287 | if (usePty) { 1288 | g_terminalState.enterRawMode(); 1289 | } 1290 | 1291 | backendStarted = true; 1292 | 1293 | mainLoop(spawnCwd, 1294 | usePty, controlSocketC, 1295 | inputSocketC, outputSocketC, errorSocketC, 1296 | initialSize); 1297 | return 0; 1298 | } 1299 | --------------------------------------------------------------------------------