installed pty
[VSoRC/.git] / node_modules / node-pty / deps / winpty / src / agent / Agent.cc
diff --git a/node_modules/node-pty/deps/winpty/src/agent/Agent.cc b/node_modules/node-pty/deps/winpty/src/agent/Agent.cc
new file mode 100644 (file)
index 0000000..a77b644
--- /dev/null
@@ -0,0 +1,605 @@
+// Copyright (c) 2011-2015 Ryan Prichard
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+#include "Agent.h"
+
+#include <windows.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "../include/winpty_constants.h"
+
+#include "../shared/AgentMsg.h"
+#include "../shared/Buffer.h"
+#include "../shared/DebugClient.h"
+#include "../shared/GenRandom.h"
+#include "../shared/StringBuilder.h"
+#include "../shared/StringUtil.h"
+#include "../shared/WindowsVersion.h"
+#include "../shared/WinptyAssert.h"
+
+#include "ConsoleFont.h"
+#include "ConsoleInput.h"
+#include "NamedPipe.h"
+#include "Scraper.h"
+#include "Terminal.h"
+#include "Win32ConsoleBuffer.h"
+
+namespace {
+
+static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType)
+{
+    if (dwCtrlType == CTRL_C_EVENT) {
+        // Do nothing and claim to have handled the event.
+        return TRUE;
+    }
+    return FALSE;
+}
+
+// We can detect the new Windows 10 console by observing the effect of the
+// Mark command.  In older consoles, Mark temporarily moves the cursor to the
+// top-left of the console window.  In the new console, the cursor isn't
+// initially moved.
+//
+// We might like to use Mark to freeze the console, but we can't, because when
+// the Mark command ends, the console moves the cursor back to its starting
+// point, even if the console application has moved it in the meantime.
+static void detectNewWindows10Console(
+        Win32Console &console, Win32ConsoleBuffer &buffer)
+{
+    if (!isAtLeastWindows8()) {
+        return;
+    }
+
+    ConsoleScreenBufferInfo info = buffer.bufferInfo();
+
+    // Make sure the window isn't 1x1.  AFAIK, this should never happen
+    // accidentally.  It is difficult to make it happen deliberately.
+    if (info.srWindow.Left == info.srWindow.Right &&
+            info.srWindow.Top == info.srWindow.Bottom) {
+        trace("detectNewWindows10Console: Initial console window was 1x1 -- "
+              "expanding for test");
+        setSmallFont(buffer.conout(), 400, false);
+        buffer.moveWindow(SmallRect(0, 0, 1, 1));
+        buffer.resizeBuffer(Coord(400, 1));
+        buffer.moveWindow(SmallRect(0, 0, 2, 1));
+        // This use of GetLargestConsoleWindowSize ought to be unnecessary
+        // given the behavior I've seen from moveWindow(0, 0, 1, 1), but
+        // I'd like to be especially sure, considering that this code will
+        // rarely be tested.
+        const auto largest = GetLargestConsoleWindowSize(buffer.conout());
+        buffer.moveWindow(
+            SmallRect(0, 0, std::min(largest.X, buffer.bufferSize().X), 1));
+        info = buffer.bufferInfo();
+        ASSERT(info.srWindow.Right > info.srWindow.Left &&
+            "Could not expand console window from 1x1");
+    }
+
+    // Test whether MARK moves the cursor.
+    const Coord initialPosition(info.srWindow.Right, info.srWindow.Bottom);
+    buffer.setCursorPosition(initialPosition);
+    ASSERT(!console.frozen());
+    console.setFreezeUsesMark(true);
+    console.setFrozen(true);
+    const bool isNewW10 = (buffer.cursorPosition() == initialPosition);
+    console.setFrozen(false);
+    buffer.setCursorPosition(Coord(0, 0));
+
+    trace("Attempting to detect new Windows 10 console using MARK: %s",
+        isNewW10 ? "detected" : "not detected");
+    console.setFreezeUsesMark(false);
+    console.setNewW10(isNewW10);
+}
+
+static inline WriteBuffer newPacket() {
+    WriteBuffer packet;
+    packet.putRawValue<uint64_t>(0); // Reserve space for size.
+    return packet;
+}
+
+static HANDLE duplicateHandle(HANDLE h) {
+    HANDLE ret = nullptr;
+    if (!DuplicateHandle(
+            GetCurrentProcess(), h,
+            GetCurrentProcess(), &ret,
+            0, FALSE, DUPLICATE_SAME_ACCESS)) {
+        ASSERT(false && "DuplicateHandle failed!");
+    }
+    return ret;
+}
+
+// It's safe to truncate a handle from 64-bits to 32-bits, or to sign-extend it
+// back to 64-bits.  See the MSDN article, "Interprocess Communication Between
+// 32-bit and 64-bit Applications".
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa384203.aspx
+static int64_t int64FromHandle(HANDLE h) {
+    return static_cast<int64_t>(reinterpret_cast<intptr_t>(h));
+}
+
+} // anonymous namespace
+
+Agent::Agent(LPCWSTR controlPipeName,
+             uint64_t agentFlags,
+             int mouseMode,
+             int initialCols,
+             int initialRows) :
+    m_useConerr((agentFlags & WINPTY_FLAG_CONERR) != 0),
+    m_plainMode((agentFlags & WINPTY_FLAG_PLAIN_OUTPUT) != 0),
+    m_mouseMode(mouseMode)
+{
+    trace("Agent::Agent entered");
+
+    ASSERT(initialCols >= 1 && initialRows >= 1);
+    initialCols = std::min(initialCols, MAX_CONSOLE_WIDTH);
+    initialRows = std::min(initialRows, MAX_CONSOLE_HEIGHT);
+
+    const bool outputColor =
+        !m_plainMode || (agentFlags & WINPTY_FLAG_COLOR_ESCAPES);
+    const Coord initialSize(initialCols, initialRows);
+
+    auto primaryBuffer = openPrimaryBuffer();
+    if (m_useConerr) {
+        m_errorBuffer = Win32ConsoleBuffer::createErrorBuffer();
+    }
+
+    detectNewWindows10Console(m_console, *primaryBuffer);
+
+    m_controlPipe = &connectToControlPipe(controlPipeName);
+    m_coninPipe = &createDataServerPipe(false, L"conin");
+    m_conoutPipe = &createDataServerPipe(true, L"conout");
+    if (m_useConerr) {
+        m_conerrPipe = &createDataServerPipe(true, L"conerr");
+    }
+
+    // Send an initial response packet to winpty.dll containing pipe names.
+    {
+        auto setupPacket = newPacket();
+        setupPacket.putWString(m_coninPipe->name());
+        setupPacket.putWString(m_conoutPipe->name());
+        if (m_useConerr) {
+            setupPacket.putWString(m_conerrPipe->name());
+        }
+        writePacket(setupPacket);
+    }
+
+    std::unique_ptr<Terminal> primaryTerminal;
+    primaryTerminal.reset(new Terminal(*m_conoutPipe,
+                                       m_plainMode,
+                                       outputColor));
+    m_primaryScraper.reset(new Scraper(m_console,
+                                       *primaryBuffer,
+                                       std::move(primaryTerminal),
+                                       initialSize));
+    if (m_useConerr) {
+        std::unique_ptr<Terminal> errorTerminal;
+        errorTerminal.reset(new Terminal(*m_conerrPipe,
+                                         m_plainMode,
+                                         outputColor));
+        m_errorScraper.reset(new Scraper(m_console,
+                                         *m_errorBuffer,
+                                         std::move(errorTerminal),
+                                         initialSize));
+    }
+
+    m_console.setTitle(m_currentTitle);
+
+    const HANDLE conin = GetStdHandle(STD_INPUT_HANDLE);
+    m_consoleInput.reset(
+        new ConsoleInput(conin, m_mouseMode, *this, m_console));
+
+    // Setup Ctrl-C handling.  First restore default handling of Ctrl-C.  This
+    // attribute is inherited by child processes.  Then register a custom
+    // Ctrl-C handler that does nothing.  The handler will be called when the
+    // agent calls GenerateConsoleCtrlEvent.
+    SetConsoleCtrlHandler(NULL, FALSE);
+    SetConsoleCtrlHandler(consoleCtrlHandler, TRUE);
+
+    setPollInterval(25);
+}
+
+Agent::~Agent()
+{
+    trace("Agent::~Agent entered");
+    agentShutdown();
+    if (m_childProcess != NULL) {
+        CloseHandle(m_childProcess);
+    }
+}
+
+// Write a "Device Status Report" command to the terminal.  The terminal will
+// reply with a row+col escape sequence.  Presumably, the DSR reply will not
+// split a keypress escape sequence, so it should be safe to assume that the
+// bytes before it are complete keypresses.
+void Agent::sendDsr()
+{
+    if (!m_plainMode && !m_conoutPipe->isClosed()) {
+        m_conoutPipe->write("\x1B[6n");
+    }
+}
+
+NamedPipe &Agent::connectToControlPipe(LPCWSTR pipeName)
+{
+    NamedPipe &pipe = createNamedPipe();
+    pipe.connectToServer(pipeName, NamedPipe::OpenMode::Duplex);
+    pipe.setReadBufferSize(64 * 1024);
+    return pipe;
+}
+
+// Returns a new server named pipe.  It has not yet been connected.
+NamedPipe &Agent::createDataServerPipe(bool write, const wchar_t *kind)
+{
+    const auto name =
+        (WStringBuilder(128)
+            << L"\\\\.\\pipe\\winpty-"
+            << kind << L'-'
+            << GenRandom().uniqueName()).str_moved();
+    NamedPipe &pipe = createNamedPipe();
+    pipe.openServerPipe(
+        name.c_str(),
+        write ? NamedPipe::OpenMode::Writing
+              : NamedPipe::OpenMode::Reading,
+        write ? 8192 : 0,
+        write ? 0 : 256);
+    if (!write) {
+        pipe.setReadBufferSize(64 * 1024);
+    }
+    return pipe;
+}
+
+void Agent::onPipeIo(NamedPipe &namedPipe)
+{
+    if (&namedPipe == m_conoutPipe || &namedPipe == m_conerrPipe) {
+        autoClosePipesForShutdown();
+    } else if (&namedPipe == m_coninPipe) {
+        pollConinPipe();
+    } else if (&namedPipe == m_controlPipe) {
+        pollControlPipe();
+    }
+}
+
+void Agent::pollControlPipe()
+{
+    if (m_controlPipe->isClosed()) {
+        trace("Agent exiting (control pipe is closed)");
+        shutdown();
+        return;
+    }
+
+    while (true) {
+        uint64_t packetSize = 0;
+        const auto amt1 =
+            m_controlPipe->peek(&packetSize, sizeof(packetSize));
+        if (amt1 < sizeof(packetSize)) {
+            break;
+        }
+        ASSERT(packetSize >= sizeof(packetSize) && packetSize <= SIZE_MAX);
+        if (m_controlPipe->bytesAvailable() < packetSize) {
+            if (m_controlPipe->readBufferSize() < packetSize) {
+                m_controlPipe->setReadBufferSize(packetSize);
+            }
+            break;
+        }
+        std::vector<char> packetData;
+        packetData.resize(packetSize);
+        const auto amt2 = m_controlPipe->read(packetData.data(), packetSize);
+        ASSERT(amt2 == packetSize);
+        try {
+            ReadBuffer buffer(std::move(packetData));
+            buffer.getRawValue<uint64_t>(); // Discard the size.
+            handlePacket(buffer);
+        } catch (const ReadBuffer::DecodeError&) {
+            ASSERT(false && "Decode error");
+        }
+    }
+}
+
+void Agent::handlePacket(ReadBuffer &packet)
+{
+    const int type = packet.getInt32();
+    switch (type) {
+    case AgentMsg::StartProcess:
+        handleStartProcessPacket(packet);
+        break;
+    case AgentMsg::SetSize:
+        // TODO: I think it might make sense to collapse consecutive SetSize
+        // messages.  i.e. The terminal process can probably generate SetSize
+        // messages faster than they can be processed, and some GUIs might
+        // generate a flood of them, so if we can read multiple SetSize packets
+        // at once, we can ignore the early ones.
+        handleSetSizePacket(packet);
+        break;
+    case AgentMsg::GetConsoleProcessList:
+        handleGetConsoleProcessListPacket(packet);
+        break;
+    default:
+        trace("Unrecognized message, id:%d", type);
+    }
+}
+
+void Agent::writePacket(WriteBuffer &packet)
+{
+    const auto &bytes = packet.buf();
+    packet.replaceRawValue<uint64_t>(0, bytes.size());
+    m_controlPipe->write(bytes.data(), bytes.size());
+}
+
+void Agent::handleStartProcessPacket(ReadBuffer &packet)
+{
+    ASSERT(m_childProcess == nullptr);
+    ASSERT(!m_closingOutputPipes);
+
+    const uint64_t spawnFlags = packet.getInt64();
+    const bool wantProcessHandle = packet.getInt32() != 0;
+    const bool wantThreadHandle = packet.getInt32() != 0;
+    const auto program = packet.getWString();
+    const auto cmdline = packet.getWString();
+    const auto cwd = packet.getWString();
+    const auto env = packet.getWString();
+    const auto desktop = packet.getWString();
+    packet.assertEof();
+
+    auto cmdlineV = vectorWithNulFromString(cmdline);
+    auto desktopV = vectorWithNulFromString(desktop);
+    auto envV = vectorFromString(env);
+
+    LPCWSTR programArg = program.empty() ? nullptr : program.c_str();
+    LPWSTR cmdlineArg = cmdline.empty() ? nullptr : cmdlineV.data();
+    LPCWSTR cwdArg = cwd.empty() ? nullptr : cwd.c_str();
+    LPWSTR envArg = env.empty() ? nullptr : envV.data();
+
+    STARTUPINFOW sui = {};
+    PROCESS_INFORMATION pi = {};
+    sui.cb = sizeof(sui);
+    sui.lpDesktop = desktop.empty() ? nullptr : desktopV.data();
+    BOOL inheritHandles = FALSE;
+    if (m_useConerr) {
+        inheritHandles = TRUE;
+        sui.dwFlags |= STARTF_USESTDHANDLES;
+        sui.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+        sui.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+        sui.hStdError = m_errorBuffer->conout();
+    }
+
+    const BOOL success =
+        CreateProcessW(programArg, cmdlineArg, nullptr, nullptr,
+                       /*bInheritHandles=*/inheritHandles,
+                       /*dwCreationFlags=*/CREATE_UNICODE_ENVIRONMENT,
+                       envArg, cwdArg, &sui, &pi);
+    const int lastError = success ? 0 : GetLastError();
+
+    trace("CreateProcess: %s %u",
+          (success ? "success" : "fail"),
+          static_cast<unsigned int>(pi.dwProcessId));
+
+    auto reply = newPacket();
+    if (success) {
+        int64_t replyProcess = 0;
+        int64_t replyThread = 0;
+        if (wantProcessHandle) {
+            replyProcess = int64FromHandle(duplicateHandle(pi.hProcess));
+        }
+        if (wantThreadHandle) {
+            replyThread = int64FromHandle(duplicateHandle(pi.hThread));
+        }
+        CloseHandle(pi.hThread);
+        m_childProcess = pi.hProcess;
+        m_autoShutdown = (spawnFlags & WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN) != 0;
+        m_exitAfterShutdown = (spawnFlags & WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN) != 0;
+        reply.putInt32(static_cast<int32_t>(StartProcessResult::ProcessCreated));
+        reply.putInt64(replyProcess);
+        reply.putInt64(replyThread);
+    } else {
+        reply.putInt32(static_cast<int32_t>(StartProcessResult::CreateProcessFailed));
+        reply.putInt32(lastError);
+    }
+    writePacket(reply);
+}
+
+void Agent::handleSetSizePacket(ReadBuffer &packet)
+{
+    const int cols = packet.getInt32();
+    const int rows = packet.getInt32();
+    packet.assertEof();
+    resizeWindow(cols, rows);
+    auto reply = newPacket();
+    writePacket(reply);
+}
+
+void Agent::handleGetConsoleProcessListPacket(ReadBuffer &packet)
+{
+    packet.assertEof();
+
+    auto processList = std::vector<DWORD>(64);
+    auto processCount = GetConsoleProcessList(&processList[0], processList.size());
+    if (processList.size() < processCount) {
+        processList.resize(processCount);
+        processCount = GetConsoleProcessList(&processList[0], processList.size());
+    }
+
+    if (processCount == 0) {
+        trace("GetConsoleProcessList failed");
+    }
+
+    auto reply = newPacket();
+    reply.putInt32(processCount);
+    for (DWORD i = 0; i < processCount; i++) {
+        reply.putInt32(processList[i]);
+    }
+    writePacket(reply);
+}
+
+void Agent::pollConinPipe()
+{
+    const std::string newData = m_coninPipe->readAllToString();
+    if (hasDebugFlag("input_separated_bytes")) {
+        // This debug flag is intended to help with testing incomplete escape
+        // sequences and multibyte UTF-8 encodings.  (I wonder if the normal
+        // code path ought to advance a state machine one byte at a time.)
+        for (size_t i = 0; i < newData.size(); ++i) {
+            m_consoleInput->writeInput(newData.substr(i, 1));
+        }
+    } else {
+        m_consoleInput->writeInput(newData);
+    }
+}
+
+void Agent::onPollTimeout()
+{
+    m_consoleInput->updateInputFlags();
+    const bool enableMouseMode = m_consoleInput->shouldActivateTerminalMouse();
+
+    // Give the ConsoleInput object a chance to flush input from an incomplete
+    // escape sequence (e.g. pressing ESC).
+    m_consoleInput->flushIncompleteEscapeCode();
+
+    const bool shouldScrapeContent = !m_closingOutputPipes;
+
+    // Check if the child process has exited.
+    if (m_autoShutdown &&
+            m_childProcess != nullptr &&
+            WaitForSingleObject(m_childProcess, 0) == WAIT_OBJECT_0) {
+        CloseHandle(m_childProcess);
+        m_childProcess = nullptr;
+
+        // Close the data socket to signal to the client that the child
+        // process has exited.  If there's any data left to send, send it
+        // before closing the socket.
+        m_closingOutputPipes = true;
+    }
+
+    // Scrape for output *after* the above exit-check to ensure that we collect
+    // the child process's final output.
+    if (shouldScrapeContent) {
+        syncConsoleTitle();
+        scrapeBuffers();
+    }
+
+    // We must ensure that we disable mouse mode before closing the CONOUT
+    // pipe, so update the mouse mode here.
+    m_primaryScraper->terminal().enableMouseMode(
+        enableMouseMode && !m_closingOutputPipes);
+
+    autoClosePipesForShutdown();
+}
+
+void Agent::autoClosePipesForShutdown()
+{
+    if (m_closingOutputPipes) {
+        // We don't want to close a pipe before it's connected!  If we do, the
+        // libwinpty client may try to connect to a non-existent pipe.  This
+        // case is important for short-lived programs.
+        if (m_conoutPipe->isConnected() &&
+                m_conoutPipe->bytesToSend() == 0) {
+            trace("Closing CONOUT pipe (auto-shutdown)");
+            m_conoutPipe->closePipe();
+        }
+        if (m_conerrPipe != nullptr &&
+                m_conerrPipe->isConnected() &&
+                m_conerrPipe->bytesToSend() == 0) {
+            trace("Closing CONERR pipe (auto-shutdown)");
+            m_conerrPipe->closePipe();
+        }
+        if (m_exitAfterShutdown &&
+                m_conoutPipe->isClosed() &&
+                (m_conerrPipe == nullptr || m_conerrPipe->isClosed())) {
+            trace("Agent exiting (exit-after-shutdown)");
+            shutdown();
+        }
+    }
+}
+
+std::unique_ptr<Win32ConsoleBuffer> Agent::openPrimaryBuffer()
+{
+    // If we're using a separate buffer for stderr, and a program were to
+    // activate the stderr buffer, then we could accidentally scrape the same
+    // buffer twice.  That probably shouldn't happen in ordinary use, but it
+    // can be avoided anyway by using the original console screen buffer in
+    // that mode.
+    if (!m_useConerr) {
+        return Win32ConsoleBuffer::openConout();
+    } else {
+        return Win32ConsoleBuffer::openStdout();
+    }
+}
+
+void Agent::resizeWindow(int cols, int rows)
+{
+    ASSERT(cols >= 1 && rows >= 1);
+    cols = std::min(cols, MAX_CONSOLE_WIDTH);
+    rows = std::min(rows, MAX_CONSOLE_HEIGHT);
+
+    Win32Console::FreezeGuard guard(m_console, m_console.frozen());
+    const Coord newSize(cols, rows);
+    ConsoleScreenBufferInfo info;
+    auto primaryBuffer = openPrimaryBuffer();
+    m_primaryScraper->resizeWindow(*primaryBuffer, newSize, info);
+    m_consoleInput->setMouseWindowRect(info.windowRect());
+    if (m_errorScraper) {
+        m_errorScraper->resizeWindow(*m_errorBuffer, newSize, info);
+    }
+
+    // Synthesize a WINDOW_BUFFER_SIZE_EVENT event.  Normally, Windows
+    // generates this event only when the buffer size changes, not when the
+    // window size changes.  This behavior is undesirable in two ways:
+    //  - When winpty expands the window horizontally, it must expand the
+    //    buffer first, then the window.  At least some programs (e.g. the WSL
+    //    bash.exe wrapper) use the window width rather than the buffer width,
+    //    so there is a short timespan during which they can read the wrong
+    //    value.
+    //  - If the window's vertical size is changed, no event is generated,
+    //    even though a typical well-behaved console program cares about the
+    //    *window* height, not the *buffer* height.
+    // This synthesization works around a design flaw in the console.  It's probably
+    // harmless.  See https://github.com/rprichard/winpty/issues/110.
+    INPUT_RECORD sizeEvent {};
+    sizeEvent.EventType = WINDOW_BUFFER_SIZE_EVENT;
+    sizeEvent.Event.WindowBufferSizeEvent.dwSize = primaryBuffer->bufferSize();
+    DWORD actual {};
+    WriteConsoleInputW(GetStdHandle(STD_INPUT_HANDLE), &sizeEvent, 1, &actual);
+}
+
+void Agent::scrapeBuffers()
+{
+    Win32Console::FreezeGuard guard(m_console, m_console.frozen());
+    ConsoleScreenBufferInfo info;
+    m_primaryScraper->scrapeBuffer(*openPrimaryBuffer(), info);
+    m_consoleInput->setMouseWindowRect(info.windowRect());
+    if (m_errorScraper) {
+        m_errorScraper->scrapeBuffer(*m_errorBuffer, info);
+    }
+}
+
+void Agent::syncConsoleTitle()
+{
+    std::wstring newTitle = m_console.title();
+    if (newTitle != m_currentTitle) {
+        std::string command = std::string("\x1b]0;") +
+                utf8FromWide(newTitle) + "\x07";
+        m_conoutPipe->write(command.c_str());
+        m_currentTitle = newTitle;
+    }
+}