From d4640890cf00346d403c9688a057013b7cf2f3f4 Mon Sep 17 00:00:00 2001 From: Ryan Prichard Date: Tue, 29 Sep 2015 02:32:49 -0500 Subject: [PATCH] Rewrite resize logic, introduce "direct mode", and tweak scroll scraping. * Unfreeze the console while changing the buffer size. Changing the buffer size hangs conhost.exe. See: - https://github.com/rprichard/winpty/issues/31 - https://wpdev.uservoice.com/forums/266908-command-prompt/suggestions/9941292-conhost-exe-hangs-in-win10-if-setconsolescreenbuff * Detect buffer size changes and switch to a "direct mode". Direct mode makes no attempt to track incremental console changes. Instead, the content of the current console window is printed. This mode is intended for full-screen apps that resize the console. * Reopen CONOUT$, which detects apps that change the active screen buffer. Fixes https://github.com/rprichard/winpty/issues/34. * In the scroll scraping (scrollingScrapeOutput), consider a line changed if the new content is truncated relative to the content previously output. Previously, we only compared against the line-buffer up to the current console width. e.g. If this: |C:\Program| turns into: |C:\Prog| |ram | we previously left |C:\Program| in the line-buffer for the first line and did not re-output the first line. We *should* reoutput the first line at this point so that, if the line scrolls upward, and the terminal is later expanded, we will have output an "Erase in Line" CSI command to clear the obscured "ram" text. We need to update the line-buffer for the sake of Windows 10 combined with terminals like xterm and putty. On such a terminal, if the terminal later widened, Windows 10 will restore the console to the first state. At that point, we need to reoutput the line, because xterm and putty do not save and restore truncated line content extending past the current terminal width. --- agent/Agent.cc | 297 ++++++++++++++++++++++++++++++------- agent/Agent.h | 16 +- agent/ConsoleInput.cc | 9 +- agent/ConsoleInput.h | 3 +- agent/SmallRect.h | 12 ++ agent/Terminal.cc | 5 +- agent/Win32Console.cc | 53 ++----- agent/Win32Console.h | 15 +- misc/BufferResizeTests.cc | 91 ++++++++++++ misc/ChangeScreenBuffer.cc | 54 +++++++ misc/DumpLines.py | 5 + misc/SetCursorPos.cc | 11 ++ misc/TestUtil.cc | 33 ++++- misc/Win10WrapTest2.cc | 2 +- 14 files changed, 486 insertions(+), 120 deletions(-) create mode 100755 misc/BufferResizeTests.cc create mode 100755 misc/ChangeScreenBuffer.cc create mode 100755 misc/DumpLines.py create mode 100755 misc/SetCursorPos.cc diff --git a/agent/Agent.cc b/agent/Agent.cc index ef7e138..29e4ae4 100644 --- a/agent/Agent.cc +++ b/agent/Agent.cc @@ -38,7 +38,6 @@ const int SC_CONSOLE_MARK = 0xFFF2; const int SC_CONSOLE_SELECT_ALL = 0xFFF5; -const int SYNC_MARKER_LEN = 16; static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType) { @@ -74,7 +73,9 @@ Agent::Agent(LPCWSTR controlPipeName, m_terminal(NULL), m_childProcess(NULL), m_childExitCode(-1), - m_syncCounter(0) + m_syncCounter(0), + m_directMode(false), + m_ptySize(initialCols, initialRows) { trace("Agent starting..."); @@ -82,16 +83,16 @@ Agent::Agent(LPCWSTR controlPipeName, m_console = new Win32Console; m_console->setSmallFont(); - m_console->reposition( - Coord(initialCols, BUFFER_LINE_COUNT), - SmallRect(0, 0, initialCols, initialRows)); + m_console->moveWindow(SmallRect(0, 0, 1, 1)); + m_console->resizeBuffer(Coord(initialCols, BUFFER_LINE_COUNT)); + m_console->moveWindow(SmallRect(0, 0, initialCols, initialRows)); m_console->setCursorPosition(Coord(0, 0)); m_console->setTitle(m_currentTitle); m_controlSocket = makeSocket(controlPipeName); m_dataSocket = makeSocket(dataPipeName); m_terminal = new Terminal(m_dataSocket); - m_consoleInput = new ConsoleInput(m_console, this); + m_consoleInput = new ConsoleInput(this); resetConsoleTracking(false); @@ -196,6 +197,11 @@ void Agent::handlePacket(ReadBuffer &packet) result = 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. result = handleSetSizePacket(packet); break; case AgentMsg::GetExitCode: @@ -314,8 +320,9 @@ void Agent::onPollTimeout() // Scrape for output *after* the above exit-check to ensure that we collect // the child process's final output. - if (!m_dataSocket->isClosed()) - scrapeOutput(); + if (!m_dataSocket->isClosed()) { + syncConsoleContentAndSize(false); + } if (m_closingDataSocket && !m_dataSocket->isClosed() && @@ -328,9 +335,8 @@ void Agent::onPollTimeout() // Detect window movement. If the window moves down (presumably as a // result of scrolling), then assume that all screen buffer lines down to // the bottom of the window are dirty. -void Agent::markEntireWindowDirty() +void Agent::markEntireWindowDirty(const SmallRect &windowRect) { - SmallRect windowRect = m_console->windowRect(); m_dirtyLineCount = std::max(m_dirtyLineCount, windowRect.top() + windowRect.height()); } @@ -366,40 +372,182 @@ void Agent::scanForDirtyLines() } } -void Agent::resizeWindow(int cols, int rows) +// Clear lines in the line buffer. The `firstRow` parameter is in +// screen-buffer coordinates. +void Agent::clearBufferLines( + const int firstRow, + const int count, + const WORD attributes) { - freezeConsole(); + ASSERT(!m_directMode); + CHAR_INFO blankLine[MAX_CONSOLE_WIDTH]; + memset(&blankLine, 0, sizeof(blankLine)); + for (int col = 0; col < MAX_CONSOLE_WIDTH; ++col) { + blankLine[col].Char.UnicodeChar = L' '; + blankLine[col].Attributes = attributes; + } + for (int row = firstRow; row < firstRow + count; ++row) { + int bufRow = (row + m_scrolledCount) % BUFFER_LINE_COUNT; + memcpy(&m_bufferData[bufRow], &blankLine, sizeof(blankLine)); + } +} - Coord bufferSize = m_console->bufferSize(); - SmallRect windowRect = m_console->windowRect(); - Coord newBufferSize(cols, bufferSize.Y); - SmallRect newWindowRect; +// This function is called with the console frozen, and the console is still +// frozen when it returns. +void Agent::resizeImpl(const ConsoleScreenBufferInfo &origInfo) +{ + const int cols = m_ptySize.X; + const int rows = m_ptySize.Y; - // This resize behavior appears to match what happens when I resize the - // console window by hand. - if (windowRect.top() + windowRect.height() == bufferSize.Y || - windowRect.top() + rows >= bufferSize.Y) { - // Lock the bottom of the new window to the bottom of the buffer if either - // - the window was already at the bottom of the buffer, OR - // - there isn't enough room. - newWindowRect = SmallRect(0, newBufferSize.Y - rows, cols, rows); - } else { - // Keep the top of the window where it is. - newWindowRect = SmallRect(0, windowRect.top(), cols, rows); + { + // + // To accommodate Windows 10, erase all lines up to the top of the + // visible window. It's hard to tell whether this is strictly + // necessary. It ensures that the sync marker won't move downward, + // and it ensures that we won't repeat lines that have already scrolled + // up into the scrollback. + // + // It *is* possible for these blank lines to reappear in the visible + // window (e.g. if the window is made taller), but because we blanked + // the lines in the line buffer, we still don't output them again. + // + const Coord origBufferSize = origInfo.bufferSize(); + const SmallRect origWindowRect = origInfo.windowRect(); + + if (!m_directMode) { + m_console->clearLines(0, origWindowRect.Top, origInfo); + clearBufferLines(0, origWindowRect.Top, origInfo.wAttributes); + if (m_syncRow != -1) { + createSyncMarker(m_syncRow); + } + } + + const Coord finalBufferSize( + cols, + // If there was previously no scrollback (e.g. a full-screen app + // in direct mode) and we're reducing the window height, then + // reduce the console buffer's height too. + (origWindowRect.height() == origBufferSize.Y) + ? rows + : std::max(rows, origBufferSize.Y)); + const bool cursorWasInWindow = + origInfo.cursorPosition().Y >= origWindowRect.Top && + origInfo.cursorPosition().Y <= origWindowRect.Bottom; + + // Step 1: move the window. + const int tmpWindowWidth = std::min(origBufferSize.X, finalBufferSize.X); + const int tmpWindowHeight = std::min(origBufferSize.Y, rows); + SmallRect tmpWindowRect( + 0, + std::min(origBufferSize.Y - tmpWindowHeight, + origWindowRect.Top), + tmpWindowWidth, + tmpWindowHeight); + if (cursorWasInWindow) { + tmpWindowRect = tmpWindowRect.ensureLineIncluded( + origInfo.cursorPosition().Y); + } + m_console->moveWindow(tmpWindowRect); + + // Step 2: resize the buffer. + unfreezeConsole(); + m_console->resizeBuffer(finalBufferSize); } - if (m_dirtyWindowTop != -1 && m_dirtyWindowTop < windowRect.top()) - markEntireWindowDirty(); - m_dirtyWindowTop = newWindowRect.top(); + // Step 3: expand the window to its full size. + { + freezeConsole(); + const ConsoleScreenBufferInfo info = m_console->bufferInfo(); + const bool cursorWasInWindow = + info.cursorPosition().Y >= info.windowRect().Top && + info.cursorPosition().Y <= info.windowRect().Bottom; + + SmallRect finalWindowRect( + 0, + std::min(info.bufferSize().Y - rows, + info.windowRect().Top), + cols, + rows); + + // + // Once a line in the screen buffer is "dirty", it should stay visible + // in the console window, so that we continue to update its content in + // the terminal. This code is particularly (only?) necessary on + // Windows 10, where making the buffer wider can rewrap lines and move + // the console window upward. + // + if (!m_directMode && m_dirtyLineCount > finalWindowRect.Bottom + 1) { + // In theory, we avoid ensureLineIncluded, because, a massive + // amount of output could have occurred while the console was + // unfrozen, so that the *top* of the window is now below the + // dirtiest tracked line. + finalWindowRect = SmallRect( + 0, m_dirtyLineCount - rows, + cols, rows); + } + + // Highest priority constraint: ensure that the cursor remains visible. + if (cursorWasInWindow) { + finalWindowRect = finalWindowRect.ensureLineIncluded( + info.cursorPosition().Y); + } + + m_console->moveWindow(finalWindowRect); + m_dirtyWindowTop = finalWindowRect.Top; + } +} + +void Agent::resizeWindow(const int cols, const int rows) +{ + if (cols < 1 || + cols > MAX_CONSOLE_WIDTH || + rows < 1 || + rows > BUFFER_LINE_COUNT - 1) { + trace("resizeWindow: invalid size: cols=%d,rows=%d", cols, rows); + return; + } + m_ptySize = Coord(cols, rows); + syncConsoleContentAndSize(true); +} + +void Agent::syncConsoleContentAndSize(bool forceResize) +{ + reopenConsole(); + freezeConsole(); + syncConsoleTitle(); + + const ConsoleScreenBufferInfo info = m_console->bufferInfo(); + + // If an app resizes the buffer height, then we enter "direct mode", where + // we stop trying to track incremental console changes. + const bool newDirectMode = (info.bufferSize().Y != BUFFER_LINE_COUNT); + if (newDirectMode != m_directMode) { + trace("Entering %s mode", newDirectMode ? "direct" : "scrolling"); + resetConsoleTracking(); + m_directMode = newDirectMode; + + // When we switch from direct->scrolling mode, make sure the console is + // the right size. + if (!m_directMode) { + forceResize = true; + } + } + + if (m_directMode) { + directScrapeOutput(info); + } else { + scrollingScrapeOutput(info); + } + + if (forceResize) { + resizeImpl(info); + } - m_console->reposition(newBufferSize, newWindowRect); unfreezeConsole(); } -void Agent::scrapeOutput() +void Agent::syncConsoleTitle() { - freezeConsole(); - std::wstring newTitle = m_console->title(); if (newTitle != m_currentTitle) { std::string command = std::string("\x1b]0;") + @@ -407,9 +555,47 @@ void Agent::scrapeOutput() m_dataSocket->write(command.c_str()); m_currentTitle = newTitle; } +} - const Coord cursor = m_console->cursorPosition(); +void Agent::directScrapeOutput(const ConsoleScreenBufferInfo &info) +{ + const Coord cursor = info.cursorPosition(); const SmallRect windowRect = m_console->windowRect(); + const int stopLine = std::min(windowRect.height(), m_ptySize.Y); + bool sawModifiedLine = false; + + for (int line = 0; line < stopLine; ++line) { + CHAR_INFO curLine[MAX_CONSOLE_WIDTH]; // TODO: bufoverflow + const int w = std::min(windowRect.width(), m_ptySize.X); + m_console->read(SmallRect(0, windowRect.top() + line, w, 1), curLine); + + // TODO: The memcpy can overflow the m_bufferData buffer. + CHAR_INFO (&bufLine)[MAX_CONSOLE_WIDTH] = + m_bufferData[line % BUFFER_LINE_COUNT]; + if (sawModifiedLine || + line > m_maxBufferedLine || + memcmp(curLine, bufLine, sizeof(CHAR_INFO) * w) != 0) { + //trace("sent line %d", line); + m_terminal->sendLine(line, curLine, windowRect.width()); + memset(bufLine, 0, sizeof(bufLine)); + memcpy(bufLine, curLine, sizeof(CHAR_INFO) * w); + for (int col = w; col < MAX_CONSOLE_WIDTH; ++col) { + bufLine[col].Attributes = curLine[w - 1].Attributes; + bufLine[col].Char.UnicodeChar = ' '; + } + m_maxBufferedLine = std::max(m_maxBufferedLine, line); + sawModifiedLine = true; + } + } + + m_terminal->finishOutput(std::pair(cursor.X, + cursor.Y - windowRect.top())); +} + +void Agent::scrollingScrapeOutput(const ConsoleScreenBufferInfo &info) +{ + const Coord cursor = info.cursorPosition(); + const SmallRect windowRect = info.windowRect(); if (m_syncRow != -1) { // If a synchronizing marker was placed into the history, look for it @@ -426,7 +612,7 @@ void Agent::scrapeOutput() m_scrolledCount += (m_syncRow - markerRow); m_syncRow = markerRow; // If the buffer has scrolled, then the entire window is dirty. - markEntireWindowDirty(); + markEntireWindowDirty(windowRect); } } @@ -438,7 +624,7 @@ void Agent::scrapeOutput() if (m_dirtyWindowTop != -1) { if (windowRect.top() > m_dirtyWindowTop) { // The window has moved down, presumably as a result of scrolling. - markEntireWindowDirty(); + markEntireWindowDirty(windowRect); } else if (windowRect.top() < m_dirtyWindowTop) { // The window has moved upward. This is generally not expected to // happen, but the CMD/PowerShell CLS command will move the window @@ -457,10 +643,10 @@ void Agent::scrapeOutput() // Note that it's possible for all the lines on the current window to // be non-dirty. - int firstLine = std::min(m_scrapedLineCount, - windowRect.top() + m_scrolledCount); - int stopLine = std::min(m_dirtyLineCount, - windowRect.top() + windowRect.height()) + + const int firstLine = std::min(m_scrapedLineCount, + windowRect.top() + m_scrolledCount); + const int stopLine = std::min(m_dirtyLineCount, + windowRect.top() + windowRect.height()) + m_scrolledCount; bool sawModifiedLine = false; @@ -468,22 +654,26 @@ void Agent::scrapeOutput() for (int line = firstLine; line < stopLine; ++line) { CHAR_INFO curLine[MAX_CONSOLE_WIDTH]; // TODO: bufoverflow const int w = windowRect.width(); + ASSERT(w >= 1 && w <= MAX_CONSOLE_WIDTH); m_console->read(SmallRect(0, line - m_scrolledCount, w, 1), curLine); + CHAR_INFO trailing; + memset(&trailing, 0, sizeof(trailing)); + trailing.Attributes = curLine[w - 1].Attributes; + trailing.Char.UnicodeChar = L' '; + for (int col = w; col < MAX_CONSOLE_WIDTH; ++col) { + curLine[col] = trailing; + } + // TODO: The memcpy can overflow the m_bufferData buffer. CHAR_INFO (&bufLine)[MAX_CONSOLE_WIDTH] = m_bufferData[line % BUFFER_LINE_COUNT]; if (sawModifiedLine || line > m_maxBufferedLine || - memcmp(curLine, bufLine, sizeof(CHAR_INFO) * w) != 0) { + memcmp(curLine, bufLine, sizeof(curLine)) != 0) { //trace("sent line %d", line); m_terminal->sendLine(line, curLine, windowRect.width()); - memset(bufLine, 0, sizeof(bufLine)); - memcpy(bufLine, curLine, sizeof(CHAR_INFO) * w); - for (int col = w; col < MAX_CONSOLE_WIDTH; ++col) { - bufLine[col].Attributes = curLine[w - 1].Attributes; - bufLine[col].Char.UnicodeChar = L' '; - } + memcpy(bufLine, curLine, sizeof(curLine)); m_maxBufferedLine = std::max(m_maxBufferedLine, line); sawModifiedLine = true; } @@ -501,8 +691,14 @@ void Agent::scrapeOutput() m_terminal->finishOutput(std::pair(cursor.X, cursor.Y + m_scrolledCount)); +} - unfreezeConsole(); +void Agent::reopenConsole() +{ + // Reopen CONOUT. The application may have changed the active screen + // buffer. (See https://github.com/rprichard/winpty/issues/34) + delete m_console; + m_console = new Win32Console(); } void Agent::freezeConsole() @@ -553,7 +749,8 @@ void Agent::createSyncMarker(int row) // Clear the lines around the marker to ensure that Windows 10's rewrapping // does not affect the marker. - m_console->clearLines(row - 1, SYNC_MARKER_LEN + 1); + m_console->clearLines(row - 1, SYNC_MARKER_LEN + 1, + m_console->bufferInfo()); // Write a new marker. m_syncCounter++; diff --git a/agent/Agent.h b/agent/Agent.h index 2540167..eddcd4b 100644 --- a/agent/Agent.h +++ b/agent/Agent.h @@ -25,15 +25,19 @@ #include #include "EventLoop.h" #include "DsrSender.h" +#include "Coord.h" +#include "SmallRect.h" class Win32Console; class ConsoleInput; class Terminal; class ReadBuffer; class NamedPipe; +struct ConsoleScreenBufferInfo; const int BUFFER_LINE_COUNT = 3000; // TODO: Use something like 9000. const int MAX_CONSOLE_WIDTH = 500; +const int SYNC_MARKER_LEN = 16; class Agent : public EventLoop, public DsrSender { @@ -61,10 +65,16 @@ protected: virtual void onPipeIo(NamedPipe *namedPipe); private: - void markEntireWindowDirty(); + void markEntireWindowDirty(const SmallRect &windowRect); void scanForDirtyLines(); + void clearBufferLines(int firstRow, int count, WORD attributes); + void resizeImpl(const ConsoleScreenBufferInfo &origInfo); void resizeWindow(int cols, int rows); - void scrapeOutput(); + void syncConsoleContentAndSize(bool forceResize); + void syncConsoleTitle(); + void directScrapeOutput(const ConsoleScreenBufferInfo &info); + void scrollingScrapeOutput(const ConsoleScreenBufferInfo &info); + void reopenConsole(); void freezeConsole(); void unfreezeConsole(); void syncMarkerText(CHAR_INFO *output); @@ -84,6 +94,8 @@ private: int m_syncRow; int m_syncCounter; + bool m_directMode; + Coord m_ptySize; int m_scrapedLineCount; int m_scrolledCount; int m_maxBufferedLine; diff --git a/agent/ConsoleInput.cc b/agent/ConsoleInput.cc index 7673249..cb4e861 100644 --- a/agent/ConsoleInput.cc +++ b/agent/ConsoleInput.cc @@ -61,8 +61,8 @@ ConsoleInput::KeyDescriptor ConsoleInput::keyDescriptorTable[] = { { ESC"OF", VK_END, 0, 0, }, // gnome-terminal }; -ConsoleInput::ConsoleInput(Win32Console *console, DsrSender *dsrSender) : - m_console(console), +ConsoleInput::ConsoleInput(DsrSender *dsrSender) : + m_console(new Win32Console), m_dsrSender(dsrSender), m_dsrSent(false), lastWriteTick(0) @@ -166,6 +166,11 @@ ConsoleInput::ConsoleInput(Win32Console *console, DsrSender *dsrSender) : } } +ConsoleInput::~ConsoleInput() +{ + delete m_console; +} + void ConsoleInput::writeInput(const std::string &input) { trace("writeInput: %d bytes", input.size()); diff --git a/agent/ConsoleInput.h b/agent/ConsoleInput.h index 4c3e845..00f0abf 100644 --- a/agent/ConsoleInput.h +++ b/agent/ConsoleInput.h @@ -31,7 +31,8 @@ class DsrSender; class ConsoleInput { public: - ConsoleInput(Win32Console *console, DsrSender *dsrSender); + ConsoleInput(DsrSender *dsrSender); + ~ConsoleInput(); void writeInput(const std::string &input); void flushIncompleteEscapeCode(); diff --git a/agent/SmallRect.h b/agent/SmallRect.h index c33f536..e641f3e 100644 --- a/agent/SmallRect.h +++ b/agent/SmallRect.h @@ -85,6 +85,18 @@ struct SmallRect : SMALL_RECT std::max(0, y2 - y1 + 1)); } + SmallRect ensureLineIncluded(SHORT line) const + { + const SHORT h = height(); + if (line < Top) { + return SmallRect(Left, line, width(), h); + } else if (line > Bottom) { + return SmallRect(Left, line - h + 1, width(), h); + } else { + return *this; + } + } + SHORT top() const { return Top; } SHORT left() const { return Left; } SHORT width() const { return Right - Left + 1; } diff --git a/agent/Terminal.cc b/agent/Terminal.cc index 1b04060..d1cdfd4 100644 --- a/agent/Terminal.cc +++ b/agent/Terminal.cc @@ -65,8 +65,11 @@ void Terminal::setConsoleMode(int mode) void Terminal::reset(bool sendClearFirst, int newLine) { - if (sendClearFirst && !m_consoleMode) + if (sendClearFirst && !m_consoleMode) { + // 1;1H ==> move cursor to top-left position + // 2J ==> clear the entire screen m_output->write(CSI"1;1H"CSI"2J"); + } m_remoteLine = newLine; m_cursorHidden = false; m_cursorPos = std::pair(0, newLine); diff --git a/agent/Win32Console.cc b/agent/Win32Console.cc index 53cce32..5c008a9 100644 --- a/agent/Win32Console.cc +++ b/agent/Win32Console.cc @@ -55,12 +55,15 @@ namespace { Win32Console::Win32Console() : m_titleWorkBuf(16) { m_conin = GetStdHandle(STD_INPUT_HANDLE); - m_conout = GetStdHandle(STD_OUTPUT_HANDLE); + m_conout = CreateFileW(L"CONOUT$", + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, 0, NULL); + ASSERT(m_conout != INVALID_HANDLE_VALUE); } Win32Console::~Win32Console() { - CloseHandle(m_conin); CloseHandle(m_conout); } @@ -314,14 +317,12 @@ void Win32Console::dumpConsoleFont(const char *prefix) } } -void Win32Console::clearLines(int row, int count) +void Win32Console::clearLines( + int row, + int count, + const ConsoleScreenBufferInfo &info) { // TODO: error handling - CONSOLE_SCREEN_BUFFER_INFO info; - memset(&info, 0, sizeof(info)); - if (!GetConsoleScreenBufferInfo(m_conout, &info)) { - trace("GetConsoleScreenBufferInfo failed"); - } const int width = SmallRect(info.srWindow).width(); DWORD actual = 0; if (!FillConsoleOutputCharacterW( @@ -372,42 +373,6 @@ void Win32Console::moveWindow(const SmallRect &rect) } } -void Win32Console::reposition(const Coord &newBufferSize, - const SmallRect &newWindowRect) -{ - // Windows has one API for resizing the screen buffer and a different one - // for resizing the window. It seems that either API can fail if the - // window does not fit on the screen buffer. - - const SmallRect origWindowRect(windowRect()); - const SmallRect origBufferRect(Coord(), bufferSize()); - - ASSERT(!newBufferSize.isEmpty()); - SmallRect bufferRect(Coord(), newBufferSize); - ASSERT(bufferRect.contains(newWindowRect)); - - SmallRect tempWindowRect = origWindowRect.intersected(bufferRect); - if (tempWindowRect.width() <= 0) { - tempWindowRect.setLeft(newBufferSize.X - 1); - tempWindowRect.setWidth(1); - } - if (tempWindowRect.height() <= 0) { - tempWindowRect.setTop(newBufferSize.Y - 1); - tempWindowRect.setHeight(1); - } - - // Alternatively, if we can immediately use the new window size, - // do that instead. - if (origBufferRect.contains(newWindowRect)) - tempWindowRect = newWindowRect; - - if (tempWindowRect != origWindowRect) - moveWindow(tempWindowRect); - resizeBuffer(newBufferSize); - if (newWindowRect != tempWindowRect) - moveWindow(newWindowRect); -} - Coord Win32Console::cursorPosition() { return bufferInfo().dwCursorPosition; diff --git a/agent/Win32Console.h b/agent/Win32Console.h index c7830e3..e250bd8 100644 --- a/agent/Win32Console.h +++ b/agent/Win32Console.h @@ -35,15 +35,9 @@ struct ConsoleScreenBufferInfo : CONSOLE_SCREEN_BUFFER_INFO memset(this, 0, sizeof(*this)); } - Coord bufferSize() const - { - return dwSize; - } - - SmallRect windowRect() const - { - return srWindow; - } + Coord bufferSize() const { return dwSize; } + SmallRect windowRect() const { return srWindow; } + Coord cursorPosition() const { return dwCursorPosition; } }; class Win32Console @@ -63,7 +57,7 @@ private: bool setSmallConsoleFontVista(); void dumpConsoleFont(const char *prefix); public: - void clearLines(int row, int count); + void clearLines(int row, int count, const ConsoleScreenBufferInfo &info); // Buffer and window sizes. ConsoleScreenBufferInfo bufferInfo(); @@ -71,7 +65,6 @@ public: SmallRect windowRect(); void resizeBuffer(const Coord &size); void moveWindow(const SmallRect &rect); - void reposition(const Coord &bufferSize, const SmallRect &windowRect); // Cursor. Coord cursorPosition(); diff --git a/misc/BufferResizeTests.cc b/misc/BufferResizeTests.cc new file mode 100755 index 0000000..c2ded1a --- /dev/null +++ b/misc/BufferResizeTests.cc @@ -0,0 +1,91 @@ +#include +#include + +#include "../shared/DebugClient.cc" +#include "TestUtil.cc" + +void dumpInfoToTrace() { + CONSOLE_SCREEN_BUFFER_INFO info; + assert(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info)); + trace("win=(%d,%d,%d,%d)", + (int)info.srWindow.Left, + (int)info.srWindow.Top, + (int)info.srWindow.Right, + (int)info.srWindow.Bottom); + trace("buf=(%d,%d)", + (int)info.dwSize.X, + (int)info.dwSize.Y); + trace("cur=(%d,%d)", + (int)info.dwCursorPosition.X, + (int)info.dwCursorPosition.Y); +} + +int main(int argc, char *argv[]) { + if (argc == 1) { + startChildProcess(L"CHILD"); + return 0; + } + + setWindowPos(0, 0, 1, 1); + + if (false) { + // Reducing the buffer height can move the window up. + setBufferSize(80, 25); + setWindowPos(0, 20, 80, 5); + Sleep(2000); + setBufferSize(80, 10); + } + + if (false) { + // Reducing the buffer height moves the window up and the buffer + // contents up too. + setBufferSize(80, 25); + setWindowPos(0, 20, 80, 5); + setCursorPos(0, 20); + printf("TEST1\nTEST2\nTEST3\nTEST4\n"); + fflush(stdout); + Sleep(2000); + setBufferSize(80, 10); + } + + if (false) { + // Reducing the buffer width can move the window left. + setBufferSize(80, 25); + setWindowPos(40, 0, 40, 25); + Sleep(2000); + setBufferSize(60, 25); + } + + if (false) { + // Sometimes the buffer contents are shifted up; sometimes they're + // shifted down. It seems to depend on the cursor position? + + // setBufferSize(80, 25); + // setWindowPos(0, 20, 80, 5); + // setCursorPos(0, 20); + // printf("TESTa\nTESTb\nTESTc\nTESTd\nTESTe"); + // fflush(stdout); + // setCursorPos(0, 0); + // printf("TEST1\nTEST2\nTEST3\nTEST4\nTEST5"); + // fflush(stdout); + // setCursorPos(0, 24); + // Sleep(5000); + // setBufferSize(80, 24); + + setBufferSize(80, 20); + setWindowPos(0, 10, 80, 10); + setCursorPos(0, 18); + + printf("TEST1\nTEST2"); + fflush(stdout); + setCursorPos(0, 18); + + Sleep(2000); + setBufferSize(80, 18); + } + + dumpInfoToTrace(); + Sleep(30000); + + return 0; +} diff --git a/misc/ChangeScreenBuffer.cc b/misc/ChangeScreenBuffer.cc new file mode 100755 index 0000000..7308e7a --- /dev/null +++ b/misc/ChangeScreenBuffer.cc @@ -0,0 +1,54 @@ +// A test program for CreateConsoleScreenBuffer / SetConsoleActiveScreenBuffer +// + +#include +#include +#include +#include +#include + +#include "../shared/DebugClient.cc" +#include "TestUtil.cc" + +int main() +{ + HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE childBuffer = CreateConsoleScreenBuffer( + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, CONSOLE_TEXTMODE_BUFFER, NULL); + + SetConsoleActiveScreenBuffer(childBuffer); + + while (true) { + char buf[1024]; + CONSOLE_SCREEN_BUFFER_INFO info; + + assert(GetConsoleScreenBufferInfo(origBuffer, &info)); + trace("child.size=(%d,%d)", (int)info.dwSize.X, (int)info.dwSize.Y); + trace("child.cursor=(%d,%d)", (int)info.dwCursorPosition.X, (int)info.dwCursorPosition.Y); + trace("child.window=(%d,%d,%d,%d)", + (int)info.srWindow.Left, (int)info.srWindow.Top, + (int)info.srWindow.Right, (int)info.srWindow.Bottom); + trace("child.maxSize=(%d,%d)", (int)info.dwMaximumWindowSize.X, (int)info.dwMaximumWindowSize.Y); + + int ch = getch(); + sprintf(buf, "%02x\n", ch); + DWORD actual = 0; + WriteFile(childBuffer, buf, strlen(buf), &actual, NULL); + if (ch == 0x1b/*ESC*/ || ch == 0x03/*CTRL-C*/) + break; + + if (ch == 'b') { + setBufferSize(origBuffer, 40, 25); + } else if (ch == 'w') { + setWindowPos(origBuffer, 1, 1, 38, 23); + } else if (ch == 'c') { + setCursorPos(origBuffer, 10, 10); + } + } + + SetConsoleActiveScreenBuffer(origBuffer); + + return 0; +} diff --git a/misc/DumpLines.py b/misc/DumpLines.py new file mode 100755 index 0000000..4004996 --- /dev/null +++ b/misc/DumpLines.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +import sys + +for i in range(1, int(sys.argv[1]) + 1): + print i, "X" * 78 diff --git a/misc/SetCursorPos.cc b/misc/SetCursorPos.cc new file mode 100755 index 0000000..56ed649 --- /dev/null +++ b/misc/SetCursorPos.cc @@ -0,0 +1,11 @@ +#include + +#include "TestUtil.cc" +#include "../shared/DebugClient.cc" + +int main(int argc, char *argv[]) { + int col = atoi(argv[1]); + int row = atoi(argv[2]); + setCursorPos(col, row); + return 0; +} diff --git a/misc/TestUtil.cc b/misc/TestUtil.cc index d33bac8..c8ff0ed 100755 --- a/misc/TestUtil.cc +++ b/misc/TestUtil.cc @@ -29,26 +29,35 @@ static void startChildProcess(const wchar_t *args) { &sui, &pi); } -static void setBufferSize(int x, int y) { +static void setBufferSize(HANDLE conout, int x, int y) { COORD size = { x, y }; - HANDLE conout = GetStdHandle(STD_OUTPUT_HANDLE); BOOL success = SetConsoleScreenBufferSize(conout, size); trace("setBufferSize: (%d,%d), result=%d", x, y, success); } -static void setWindowPos(int x, int y, int w, int h) { +static void setWindowPos(HANDLE conout, int x, int y, int w, int h) { SMALL_RECT r = { x, y, x + w - 1, y + h - 1 }; - HANDLE conout = GetStdHandle(STD_OUTPUT_HANDLE); BOOL success = SetConsoleWindowInfo(conout, /*bAbsolute=*/TRUE, &r); trace("setWindowPos: (%d,%d,%d,%d), result=%d", x, y, w, h, success); } -static void setCursorPos(int x, int y) { +static void setCursorPos(HANDLE conout, int x, int y) { COORD coord = { x, y }; - HANDLE conout = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(conout, coord); } +static void setBufferSize(int x, int y) { + setBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), x, y); +} + +static void setWindowPos(int x, int y, int w, int h) { + setWindowPos(GetStdHandle(STD_OUTPUT_HANDLE), x, y, w, h); +} + +static void setCursorPos(int x, int y) { + setCursorPos(GetStdHandle(STD_OUTPUT_HANDLE), x, y); +} + static void countDown(int sec) { for (int i = sec; i > 0; --i) { printf("%d.. ", i); @@ -58,7 +67,7 @@ static void countDown(int sec) { printf("\n"); } -static void fillBox(int x, int y, int w, int h, char ch, int attributes=7) { +static void writeBox(int x, int y, int w, int h, char ch, int attributes=7) { CHAR_INFO info = { 0 }; info.Char.AsciiChar = ch; info.Attributes = attributes; @@ -71,7 +80,15 @@ static void fillBox(int x, int y, int w, int h, char ch, int attributes=7) { } static void setChar(int x, int y, char ch, int attributes=7) { - fillBox(x, y, 1, 1, ch, attributes); + writeBox(x, y, 1, 1, ch, attributes); +} + +static void fillChar(int x, int y, int repeat, char ch) { + COORD coord = { x, y }; + DWORD actual = 0; + FillConsoleOutputCharacterA( + GetStdHandle(STD_OUTPUT_HANDLE), + ch, repeat, coord, &actual); } static void repeatChar(int count, char ch) { diff --git a/misc/Win10WrapTest2.cc b/misc/Win10WrapTest2.cc index ce5ea4b..7fd2b33 100755 --- a/misc/Win10WrapTest2.cc +++ b/misc/Win10WrapTest2.cc @@ -25,7 +25,7 @@ int main(int argc, char *argv[]) { repeatChar(WIDTH * 5, '.'); repeatChar(10, '\n'); setWindowPos(0, 20, WIDTH, 20); - fillBox(0, 5, 1, 10, '|'); + writeBox(0, 5, 1, 10, '|'); Sleep(120000); }