1 // Copyright (c) 2011-2015 Ryan Prichard
3 // Permission is hereby granted, free of charge, to any person obtaining a copy
4 // of this software and associated documentation files (the "Software"), to
5 // deal in the Software without restriction, including without limitation the
6 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 // sell copies of the Software, and to permit persons to whom the Software is
8 // furnished to do so, subject to the following conditions:
10 // The above copyright notice and this permission notice shall be included in
11 // all copies or substantial portions of the Software.
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
34 #include "../include/winpty_constants.h"
36 #include "../shared/AgentMsg.h"
37 #include "../shared/Buffer.h"
38 #include "../shared/DebugClient.h"
39 #include "../shared/GenRandom.h"
40 #include "../shared/StringBuilder.h"
41 #include "../shared/StringUtil.h"
42 #include "../shared/WindowsVersion.h"
43 #include "../shared/WinptyAssert.h"
45 #include "ConsoleFont.h"
46 #include "ConsoleInput.h"
47 #include "NamedPipe.h"
50 #include "Win32ConsoleBuffer.h"
54 static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType)
56 if (dwCtrlType == CTRL_C_EVENT) {
57 // Do nothing and claim to have handled the event.
63 // We can detect the new Windows 10 console by observing the effect of the
64 // Mark command. In older consoles, Mark temporarily moves the cursor to the
65 // top-left of the console window. In the new console, the cursor isn't
68 // We might like to use Mark to freeze the console, but we can't, because when
69 // the Mark command ends, the console moves the cursor back to its starting
70 // point, even if the console application has moved it in the meantime.
71 static void detectNewWindows10Console(
72 Win32Console &console, Win32ConsoleBuffer &buffer)
74 if (!isAtLeastWindows8()) {
78 ConsoleScreenBufferInfo info = buffer.bufferInfo();
80 // Make sure the window isn't 1x1. AFAIK, this should never happen
81 // accidentally. It is difficult to make it happen deliberately.
82 if (info.srWindow.Left == info.srWindow.Right &&
83 info.srWindow.Top == info.srWindow.Bottom) {
84 trace("detectNewWindows10Console: Initial console window was 1x1 -- "
85 "expanding for test");
86 setSmallFont(buffer.conout(), 400, false);
87 buffer.moveWindow(SmallRect(0, 0, 1, 1));
88 buffer.resizeBuffer(Coord(400, 1));
89 buffer.moveWindow(SmallRect(0, 0, 2, 1));
90 // This use of GetLargestConsoleWindowSize ought to be unnecessary
91 // given the behavior I've seen from moveWindow(0, 0, 1, 1), but
92 // I'd like to be especially sure, considering that this code will
94 const auto largest = GetLargestConsoleWindowSize(buffer.conout());
96 SmallRect(0, 0, std::min(largest.X, buffer.bufferSize().X), 1));
97 info = buffer.bufferInfo();
98 ASSERT(info.srWindow.Right > info.srWindow.Left &&
99 "Could not expand console window from 1x1");
102 // Test whether MARK moves the cursor.
103 const Coord initialPosition(info.srWindow.Right, info.srWindow.Bottom);
104 buffer.setCursorPosition(initialPosition);
105 ASSERT(!console.frozen());
106 console.setFreezeUsesMark(true);
107 console.setFrozen(true);
108 const bool isNewW10 = (buffer.cursorPosition() == initialPosition);
109 console.setFrozen(false);
110 buffer.setCursorPosition(Coord(0, 0));
112 trace("Attempting to detect new Windows 10 console using MARK: %s",
113 isNewW10 ? "detected" : "not detected");
114 console.setFreezeUsesMark(false);
115 console.setNewW10(isNewW10);
118 static inline WriteBuffer newPacket() {
120 packet.putRawValue<uint64_t>(0); // Reserve space for size.
124 static HANDLE duplicateHandle(HANDLE h) {
125 HANDLE ret = nullptr;
126 if (!DuplicateHandle(
127 GetCurrentProcess(), h,
128 GetCurrentProcess(), &ret,
129 0, FALSE, DUPLICATE_SAME_ACCESS)) {
130 ASSERT(false && "DuplicateHandle failed!");
135 // It's safe to truncate a handle from 64-bits to 32-bits, or to sign-extend it
136 // back to 64-bits. See the MSDN article, "Interprocess Communication Between
137 // 32-bit and 64-bit Applications".
138 // https://msdn.microsoft.com/en-us/library/windows/desktop/aa384203.aspx
139 static int64_t int64FromHandle(HANDLE h) {
140 return static_cast<int64_t>(reinterpret_cast<intptr_t>(h));
143 } // anonymous namespace
145 Agent::Agent(LPCWSTR controlPipeName,
150 m_useConerr((agentFlags & WINPTY_FLAG_CONERR) != 0),
151 m_plainMode((agentFlags & WINPTY_FLAG_PLAIN_OUTPUT) != 0),
152 m_mouseMode(mouseMode)
154 trace("Agent::Agent entered");
156 ASSERT(initialCols >= 1 && initialRows >= 1);
157 initialCols = std::min(initialCols, MAX_CONSOLE_WIDTH);
158 initialRows = std::min(initialRows, MAX_CONSOLE_HEIGHT);
160 const bool outputColor =
161 !m_plainMode || (agentFlags & WINPTY_FLAG_COLOR_ESCAPES);
162 const Coord initialSize(initialCols, initialRows);
164 auto primaryBuffer = openPrimaryBuffer();
166 m_errorBuffer = Win32ConsoleBuffer::createErrorBuffer();
169 detectNewWindows10Console(m_console, *primaryBuffer);
171 m_controlPipe = &connectToControlPipe(controlPipeName);
172 m_coninPipe = &createDataServerPipe(false, L"conin");
173 m_conoutPipe = &createDataServerPipe(true, L"conout");
175 m_conerrPipe = &createDataServerPipe(true, L"conerr");
178 // Send an initial response packet to winpty.dll containing pipe names.
180 auto setupPacket = newPacket();
181 setupPacket.putWString(m_coninPipe->name());
182 setupPacket.putWString(m_conoutPipe->name());
184 setupPacket.putWString(m_conerrPipe->name());
186 writePacket(setupPacket);
189 std::unique_ptr<Terminal> primaryTerminal;
190 primaryTerminal.reset(new Terminal(*m_conoutPipe,
193 m_primaryScraper.reset(new Scraper(m_console,
195 std::move(primaryTerminal),
198 std::unique_ptr<Terminal> errorTerminal;
199 errorTerminal.reset(new Terminal(*m_conerrPipe,
202 m_errorScraper.reset(new Scraper(m_console,
204 std::move(errorTerminal),
208 m_console.setTitle(m_currentTitle);
210 const HANDLE conin = GetStdHandle(STD_INPUT_HANDLE);
211 m_consoleInput.reset(
212 new ConsoleInput(conin, m_mouseMode, *this, m_console));
214 // Setup Ctrl-C handling. First restore default handling of Ctrl-C. This
215 // attribute is inherited by child processes. Then register a custom
216 // Ctrl-C handler that does nothing. The handler will be called when the
217 // agent calls GenerateConsoleCtrlEvent.
218 SetConsoleCtrlHandler(NULL, FALSE);
219 SetConsoleCtrlHandler(consoleCtrlHandler, TRUE);
226 trace("Agent::~Agent entered");
228 if (m_childProcess != NULL) {
229 CloseHandle(m_childProcess);
233 // Write a "Device Status Report" command to the terminal. The terminal will
234 // reply with a row+col escape sequence. Presumably, the DSR reply will not
235 // split a keypress escape sequence, so it should be safe to assume that the
236 // bytes before it are complete keypresses.
237 void Agent::sendDsr()
239 if (!m_plainMode && !m_conoutPipe->isClosed()) {
240 m_conoutPipe->write("\x1B[6n");
244 NamedPipe &Agent::connectToControlPipe(LPCWSTR pipeName)
246 NamedPipe &pipe = createNamedPipe();
247 pipe.connectToServer(pipeName, NamedPipe::OpenMode::Duplex);
248 pipe.setReadBufferSize(64 * 1024);
252 // Returns a new server named pipe. It has not yet been connected.
253 NamedPipe &Agent::createDataServerPipe(bool write, const wchar_t *kind)
257 << L"\\\\.\\pipe\\winpty-"
259 << GenRandom().uniqueName()).str_moved();
260 NamedPipe &pipe = createNamedPipe();
263 write ? NamedPipe::OpenMode::Writing
264 : NamedPipe::OpenMode::Reading,
268 pipe.setReadBufferSize(64 * 1024);
273 void Agent::onPipeIo(NamedPipe &namedPipe)
275 if (&namedPipe == m_conoutPipe || &namedPipe == m_conerrPipe) {
276 autoClosePipesForShutdown();
277 } else if (&namedPipe == m_coninPipe) {
279 } else if (&namedPipe == m_controlPipe) {
284 void Agent::pollControlPipe()
286 if (m_controlPipe->isClosed()) {
287 trace("Agent exiting (control pipe is closed)");
293 uint64_t packetSize = 0;
295 m_controlPipe->peek(&packetSize, sizeof(packetSize));
296 if (amt1 < sizeof(packetSize)) {
299 ASSERT(packetSize >= sizeof(packetSize) && packetSize <= SIZE_MAX);
300 if (m_controlPipe->bytesAvailable() < packetSize) {
301 if (m_controlPipe->readBufferSize() < packetSize) {
302 m_controlPipe->setReadBufferSize(packetSize);
306 std::vector<char> packetData;
307 packetData.resize(packetSize);
308 const auto amt2 = m_controlPipe->read(packetData.data(), packetSize);
309 ASSERT(amt2 == packetSize);
311 ReadBuffer buffer(std::move(packetData));
312 buffer.getRawValue<uint64_t>(); // Discard the size.
313 handlePacket(buffer);
314 } catch (const ReadBuffer::DecodeError&) {
315 ASSERT(false && "Decode error");
320 void Agent::handlePacket(ReadBuffer &packet)
322 const int type = packet.getInt32();
324 case AgentMsg::StartProcess:
325 handleStartProcessPacket(packet);
327 case AgentMsg::SetSize:
328 // TODO: I think it might make sense to collapse consecutive SetSize
329 // messages. i.e. The terminal process can probably generate SetSize
330 // messages faster than they can be processed, and some GUIs might
331 // generate a flood of them, so if we can read multiple SetSize packets
332 // at once, we can ignore the early ones.
333 handleSetSizePacket(packet);
335 case AgentMsg::GetConsoleProcessList:
336 handleGetConsoleProcessListPacket(packet);
339 trace("Unrecognized message, id:%d", type);
343 void Agent::writePacket(WriteBuffer &packet)
345 const auto &bytes = packet.buf();
346 packet.replaceRawValue<uint64_t>(0, bytes.size());
347 m_controlPipe->write(bytes.data(), bytes.size());
350 void Agent::handleStartProcessPacket(ReadBuffer &packet)
352 ASSERT(m_childProcess == nullptr);
353 ASSERT(!m_closingOutputPipes);
355 const uint64_t spawnFlags = packet.getInt64();
356 const bool wantProcessHandle = packet.getInt32() != 0;
357 const bool wantThreadHandle = packet.getInt32() != 0;
358 const auto program = packet.getWString();
359 const auto cmdline = packet.getWString();
360 const auto cwd = packet.getWString();
361 const auto env = packet.getWString();
362 const auto desktop = packet.getWString();
365 auto cmdlineV = vectorWithNulFromString(cmdline);
366 auto desktopV = vectorWithNulFromString(desktop);
367 auto envV = vectorFromString(env);
369 LPCWSTR programArg = program.empty() ? nullptr : program.c_str();
370 LPWSTR cmdlineArg = cmdline.empty() ? nullptr : cmdlineV.data();
371 LPCWSTR cwdArg = cwd.empty() ? nullptr : cwd.c_str();
372 LPWSTR envArg = env.empty() ? nullptr : envV.data();
374 STARTUPINFOW sui = {};
375 PROCESS_INFORMATION pi = {};
376 sui.cb = sizeof(sui);
377 sui.lpDesktop = desktop.empty() ? nullptr : desktopV.data();
378 BOOL inheritHandles = FALSE;
380 inheritHandles = TRUE;
381 sui.dwFlags |= STARTF_USESTDHANDLES;
382 sui.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
383 sui.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
384 sui.hStdError = m_errorBuffer->conout();
388 CreateProcessW(programArg, cmdlineArg, nullptr, nullptr,
389 /*bInheritHandles=*/inheritHandles,
390 /*dwCreationFlags=*/CREATE_UNICODE_ENVIRONMENT,
391 envArg, cwdArg, &sui, &pi);
392 const int lastError = success ? 0 : GetLastError();
394 trace("CreateProcess: %s %u",
395 (success ? "success" : "fail"),
396 static_cast<unsigned int>(pi.dwProcessId));
398 auto reply = newPacket();
400 int64_t replyProcess = 0;
401 int64_t replyThread = 0;
402 if (wantProcessHandle) {
403 replyProcess = int64FromHandle(duplicateHandle(pi.hProcess));
405 if (wantThreadHandle) {
406 replyThread = int64FromHandle(duplicateHandle(pi.hThread));
408 CloseHandle(pi.hThread);
409 m_childProcess = pi.hProcess;
410 m_autoShutdown = (spawnFlags & WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN) != 0;
411 m_exitAfterShutdown = (spawnFlags & WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN) != 0;
412 reply.putInt32(static_cast<int32_t>(StartProcessResult::ProcessCreated));
413 reply.putInt64(replyProcess);
414 reply.putInt64(replyThread);
416 reply.putInt32(static_cast<int32_t>(StartProcessResult::CreateProcessFailed));
417 reply.putInt32(lastError);
422 void Agent::handleSetSizePacket(ReadBuffer &packet)
424 const int cols = packet.getInt32();
425 const int rows = packet.getInt32();
427 resizeWindow(cols, rows);
428 auto reply = newPacket();
432 void Agent::handleGetConsoleProcessListPacket(ReadBuffer &packet)
436 auto processList = std::vector<DWORD>(64);
437 auto processCount = GetConsoleProcessList(&processList[0], processList.size());
438 if (processList.size() < processCount) {
439 processList.resize(processCount);
440 processCount = GetConsoleProcessList(&processList[0], processList.size());
443 if (processCount == 0) {
444 trace("GetConsoleProcessList failed");
447 auto reply = newPacket();
448 reply.putInt32(processCount);
449 for (DWORD i = 0; i < processCount; i++) {
450 reply.putInt32(processList[i]);
455 void Agent::pollConinPipe()
457 const std::string newData = m_coninPipe->readAllToString();
458 if (hasDebugFlag("input_separated_bytes")) {
459 // This debug flag is intended to help with testing incomplete escape
460 // sequences and multibyte UTF-8 encodings. (I wonder if the normal
461 // code path ought to advance a state machine one byte at a time.)
462 for (size_t i = 0; i < newData.size(); ++i) {
463 m_consoleInput->writeInput(newData.substr(i, 1));
466 m_consoleInput->writeInput(newData);
470 void Agent::onPollTimeout()
472 m_consoleInput->updateInputFlags();
473 const bool enableMouseMode = m_consoleInput->shouldActivateTerminalMouse();
475 // Give the ConsoleInput object a chance to flush input from an incomplete
476 // escape sequence (e.g. pressing ESC).
477 m_consoleInput->flushIncompleteEscapeCode();
479 const bool shouldScrapeContent = !m_closingOutputPipes;
481 // Check if the child process has exited.
482 if (m_autoShutdown &&
483 m_childProcess != nullptr &&
484 WaitForSingleObject(m_childProcess, 0) == WAIT_OBJECT_0) {
485 CloseHandle(m_childProcess);
486 m_childProcess = nullptr;
488 // Close the data socket to signal to the client that the child
489 // process has exited. If there's any data left to send, send it
490 // before closing the socket.
491 m_closingOutputPipes = true;
494 // Scrape for output *after* the above exit-check to ensure that we collect
495 // the child process's final output.
496 if (shouldScrapeContent) {
501 // We must ensure that we disable mouse mode before closing the CONOUT
502 // pipe, so update the mouse mode here.
503 m_primaryScraper->terminal().enableMouseMode(
504 enableMouseMode && !m_closingOutputPipes);
506 autoClosePipesForShutdown();
509 void Agent::autoClosePipesForShutdown()
511 if (m_closingOutputPipes) {
512 // We don't want to close a pipe before it's connected! If we do, the
513 // libwinpty client may try to connect to a non-existent pipe. This
514 // case is important for short-lived programs.
515 if (m_conoutPipe->isConnected() &&
516 m_conoutPipe->bytesToSend() == 0) {
517 trace("Closing CONOUT pipe (auto-shutdown)");
518 m_conoutPipe->closePipe();
520 if (m_conerrPipe != nullptr &&
521 m_conerrPipe->isConnected() &&
522 m_conerrPipe->bytesToSend() == 0) {
523 trace("Closing CONERR pipe (auto-shutdown)");
524 m_conerrPipe->closePipe();
526 if (m_exitAfterShutdown &&
527 m_conoutPipe->isClosed() &&
528 (m_conerrPipe == nullptr || m_conerrPipe->isClosed())) {
529 trace("Agent exiting (exit-after-shutdown)");
535 std::unique_ptr<Win32ConsoleBuffer> Agent::openPrimaryBuffer()
537 // If we're using a separate buffer for stderr, and a program were to
538 // activate the stderr buffer, then we could accidentally scrape the same
539 // buffer twice. That probably shouldn't happen in ordinary use, but it
540 // can be avoided anyway by using the original console screen buffer in
543 return Win32ConsoleBuffer::openConout();
545 return Win32ConsoleBuffer::openStdout();
549 void Agent::resizeWindow(int cols, int rows)
551 ASSERT(cols >= 1 && rows >= 1);
552 cols = std::min(cols, MAX_CONSOLE_WIDTH);
553 rows = std::min(rows, MAX_CONSOLE_HEIGHT);
555 Win32Console::FreezeGuard guard(m_console, m_console.frozen());
556 const Coord newSize(cols, rows);
557 ConsoleScreenBufferInfo info;
558 auto primaryBuffer = openPrimaryBuffer();
559 m_primaryScraper->resizeWindow(*primaryBuffer, newSize, info);
560 m_consoleInput->setMouseWindowRect(info.windowRect());
561 if (m_errorScraper) {
562 m_errorScraper->resizeWindow(*m_errorBuffer, newSize, info);
565 // Synthesize a WINDOW_BUFFER_SIZE_EVENT event. Normally, Windows
566 // generates this event only when the buffer size changes, not when the
567 // window size changes. This behavior is undesirable in two ways:
568 // - When winpty expands the window horizontally, it must expand the
569 // buffer first, then the window. At least some programs (e.g. the WSL
570 // bash.exe wrapper) use the window width rather than the buffer width,
571 // so there is a short timespan during which they can read the wrong
573 // - If the window's vertical size is changed, no event is generated,
574 // even though a typical well-behaved console program cares about the
575 // *window* height, not the *buffer* height.
576 // This synthesization works around a design flaw in the console. It's probably
577 // harmless. See https://github.com/rprichard/winpty/issues/110.
578 INPUT_RECORD sizeEvent {};
579 sizeEvent.EventType = WINDOW_BUFFER_SIZE_EVENT;
580 sizeEvent.Event.WindowBufferSizeEvent.dwSize = primaryBuffer->bufferSize();
582 WriteConsoleInputW(GetStdHandle(STD_INPUT_HANDLE), &sizeEvent, 1, &actual);
585 void Agent::scrapeBuffers()
587 Win32Console::FreezeGuard guard(m_console, m_console.frozen());
588 ConsoleScreenBufferInfo info;
589 m_primaryScraper->scrapeBuffer(*openPrimaryBuffer(), info);
590 m_consoleInput->setMouseWindowRect(info.windowRect());
591 if (m_errorScraper) {
592 m_errorScraper->scrapeBuffer(*m_errorBuffer, info);
596 void Agent::syncConsoleTitle()
598 std::wstring newTitle = m_console.title();
599 if (newTitle != m_currentTitle) {
600 std::string command = std::string("\x1b]0;") +
601 utf8FromWide(newTitle) + "\x07";
602 m_conoutPipe->write(command.c_str());
603 m_currentTitle = newTitle;