// 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 #include #include #include #include #include #include #include #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(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(reinterpret_cast(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 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 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 packetData; packetData.resize(packetSize); const auto amt2 = m_controlPipe->read(packetData.data(), packetSize); ASSERT(amt2 == packetSize); try { ReadBuffer buffer(std::move(packetData)); buffer.getRawValue(); // 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(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(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(StartProcessResult::ProcessCreated)); reply.putInt64(replyProcess); reply.putInt64(replyThread); } else { reply.putInt32(static_cast(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(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 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; } }