+++ /dev/null
-// 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;
- }
-}