Split out Scraper class from Agent; add a "CONERR" mode

If the WINPTY_FLAG_CONERR flag is specified when starting the agent, the
agent creates a separate, inactive console buffer to use for collecting
error output.  The buffer is passed to children using
STARTUPINFO.hStdError.  The agent scrapes from both the initial STDOUT
screen buffer and the new error buffer using two Scraper objects, two
Terminal objects, and two NamedPipe objects.

Clients connect to the CONERR pipe just as they would connect to the CONOUT
pipe.  There is a winpty_conerr_name function for querying the CONERR
pipe's name.

Console frozenness is a property of the entire console, rather than a
screen buffer, so it is consolidated into the Win32Console class.  During a
typical output poll, the console is frozen, then both buffers are scraped,
then the console is unfrozen.

Related: previously CONOUT$ was reopening at each poll timeout.  Now, the
buffer is only open for the duration is is needed.  (i.e. It is closed at
the end of the resizing/scraping operation.)  This new behavior might be
more correct in scenarios where programs change the active screen buffer.
If a program activates its own screen buffer, then exits, the screen buffer
is destroyed because no program references it.  When it is destroyed, a
different buffer is activated.  By opening CONOUT$, winpty can accidentally
prevent a screen buffer from being destroyed, at least temporarily.
This commit is contained in:
Ryan Prichard 2016-05-26 21:26:58 -05:00
parent f453c9ca1d
commit ccb11afd62
17 changed files with 875 additions and 584 deletions

View File

@ -41,17 +41,13 @@
#include "../shared/StringUtil.h"
#include "../shared/WindowsVersion.h"
#include "../shared/WinptyAssert.h"
#include "../shared/winpty_snprintf.h"
#include "ConsoleFont.h"
#include "ConsoleInput.h"
#include "NamedPipe.h"
#include "Scraper.h"
#include "Terminal.h"
#include "Win32ConsoleBuffer.h"
const int SC_CONSOLE_MARK = 0xFFF2;
const int SC_CONSOLE_SELECT_ALL = 0xFFF5;
namespace {
static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType)
@ -63,20 +59,6 @@ static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType)
return FALSE;
}
template <typename T>
T constrained(T min, T val, T max) {
ASSERT(min <= max);
return std::min(std::max(min, val), max);
}
static void sendSysCommand(HWND hwnd, int command) {
SendMessage(hwnd, WM_SYSCOMMAND, command, 0);
}
static void sendEscape(HWND hwnd) {
SendMessage(hwnd, WM_CHAR, 27, 0x00010001);
}
// In versions of the Windows console before Windows 10, the SelectAll and
// Mark commands both run quickly, but Mark changes the cursor position read
// by GetConsoleScreenBufferInfo. Therefore, use SelectAll to be less
@ -88,20 +70,28 @@ static void sendEscape(HWND hwnd) {
// The Windows 10 legacy-mode console behaves the same way as previous console
// versions, so detect which syscommand to use by testing whether Mark changes
// the cursor position.
static bool detectWhetherMarkMovesCursor(
static void initConsoleFreezeMethod(
Win32Console &console, Win32ConsoleBuffer &buffer)
{
const ConsoleScreenBufferInfo info = buffer.bufferInfo();
// Make sure the buffer and window aren't 1x1. (Is that even possible?)
buffer.resizeBuffer(Coord(
std::max<int>(2, info.dwSize.X),
std::max<int>(2, info.dwSize.Y)));
buffer.moveWindow(SmallRect(0, 0, 2, 2));
const Coord initialPosition(1, 1);
buffer.setCursorPosition(initialPosition);
sendSysCommand(console.hwnd(), SC_CONSOLE_MARK);
bool ret = buffer.cursorPosition() != initialPosition;
sendEscape(console.hwnd());
return ret;
// Test whether MARK moves the cursor.
ASSERT(!console.frozen());
console.setFreezeUsesMark(true);
console.setFrozen(true);
const bool useMark = (buffer.cursorPosition() == initialPosition);
console.setFrozen(false);
trace("Using %s syscommand to freeze console",
useMark ? "MARK" : "SELECT_ALL");
console.setFreezeUsesMark(useMark);
}
static inline WriteBuffer newPacket() {
@ -135,44 +125,64 @@ Agent::Agent(LPCWSTR controlPipeName,
uint64_t agentFlags,
int initialCols,
int initialRows) :
m_ptySize(initialCols, initialRows)
m_useConerr(agentFlags & WINPTY_FLAG_CONERR),
m_plainMode(agentFlags & WINPTY_FLAG_PLAIN_OUTPUT)
{
trace("Agent::Agent entered");
m_bufferData.resize(BUFFER_LINE_COUNT);
const bool outputColor =
!m_plainMode || (agentFlags & WINPTY_FLAG_COLOR_ESCAPES);
const Coord initialSize(initialCols, initialRows);
m_consoleBuffer.reset(new Win32ConsoleBuffer);
setSmallFont(m_consoleBuffer->conout());
m_useMark = !detectWhetherMarkMovesCursor(m_console, *m_consoleBuffer);
trace("Using %s syscommand to freeze console",
m_useMark ? "MARK" : "SELECT_ALL");
m_consoleBuffer->moveWindow(SmallRect(0, 0, 1, 1));
m_consoleBuffer->resizeBuffer(Coord(initialCols, BUFFER_LINE_COUNT));
m_consoleBuffer->moveWindow(SmallRect(0, 0, initialCols, initialRows));
m_consoleBuffer->setCursorPosition(Coord(0, 0));
m_console.setTitle(m_currentTitle);
auto primaryBuffer = openPrimaryBuffer();
if (m_useConerr) {
m_errorBuffer = Win32ConsoleBuffer::createErrorBuffer();
}
// For the sake of the color translation heuristic, set the console color
// to LtGray-on-Black.
m_consoleBuffer->setTextAttribute(7);
m_consoleBuffer->clearAllLines(m_consoleBuffer->bufferInfo());
initConsoleFreezeMethod(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_terminal.reset(new Terminal(*m_conoutPipe));
m_consoleInput.reset(new ConsoleInput(conin, *this));
resetConsoleTracking(Terminal::OmitClear, m_consoleBuffer->windowRect());
// 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
@ -211,8 +221,9 @@ Agent::~Agent()
// bytes before it are complete keypresses.
void Agent::sendDsr()
{
// TODO: Disable this in console mode.
if (!m_plainMode) {
m_conoutPipe->write("\x1B[6n");
}
}
NamedPipe &Agent::connectToControlPipe(LPCWSTR pipeName)
@ -244,28 +255,10 @@ NamedPipe &Agent::createDataServerPipe(bool write, const wchar_t *kind)
return pipe;
}
void Agent::resetConsoleTracking(
Terminal::SendClearFlag sendClear, const SmallRect &windowRect)
{
for (std::vector<ConsoleLine>::iterator
it = m_bufferData.begin(), itEnd = m_bufferData.end();
it != itEnd;
++it) {
it->reset();
}
m_syncRow = -1;
m_scrapedLineCount = windowRect.top();
m_scrolledCount = 0;
m_maxBufferedLine = -1;
m_dirtyWindowTop = -1;
m_dirtyLineCount = 0;
m_terminal->reset(sendClear, m_scrapedLineCount);
}
void Agent::onPipeIo(NamedPipe &namedPipe)
{
if (&namedPipe == m_conoutPipe) {
pollConoutPipe();
if (&namedPipe == m_conoutPipe || &namedPipe == m_conerrPipe) {
autoClosePipesForShutdown();
} else if (&namedPipe == m_coninPipe) {
pollConinPipe();
} else if (&namedPipe == m_controlPipe) {
@ -339,7 +332,7 @@ void Agent::writePacket(WriteBuffer &packet)
void Agent::handleStartProcessPacket(ReadBuffer &packet)
{
ASSERT(m_childProcess == nullptr);
ASSERT(!m_closingConoutPipe);
ASSERT(!m_closingOutputPipes);
const uint64_t spawnFlags = packet.getInt64();
const bool wantProcessHandle = packet.getInt32();
@ -364,12 +357,19 @@ void Agent::handleStartProcessPacket(ReadBuffer &packet)
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=*/FALSE,
/*dwCreationFlags=*/CREATE_UNICODE_ENVIRONMENT |
/*CREATE_NEW_PROCESS_GROUP*/0,
/*bInheritHandles=*/inheritHandles,
/*dwCreationFlags=*/CREATE_UNICODE_ENVIRONMENT,
envArg, cwdArg, &sui, &pi);
const int lastError = success ? 0 : GetLastError();
@ -426,18 +426,6 @@ void Agent::pollConinPipe()
}
}
void Agent::pollConoutPipe()
{
// If the child process had exited, then close the data socket if we've
// finished sending all of the collected output.
if (m_closingConoutPipe &&
!m_conoutPipe->isClosed() &&
m_conoutPipe->bytesToSend() == 0) {
trace("Closing data pipe after data is sent");
m_conoutPipe->closePipe();
}
}
void Agent::onPollTimeout()
{
// Check the mouse input flag so we can output a trace message.
@ -447,7 +435,7 @@ void Agent::onPollTimeout()
// escape sequence (e.g. pressing ESC).
m_consoleInput->flushIncompleteEscapeCode();
const bool shouldScrapeContent = !m_closingConoutPipe;
const bool shouldScrapeContent = !m_closingOutputPipes;
// Check if the child process has exited.
if (m_autoShutdown &&
@ -459,174 +447,47 @@ void Agent::onPollTimeout()
// 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_closingConoutPipe = true;
m_closingOutputPipes = true;
}
// Scrape for output *after* the above exit-check to ensure that we collect
// the child process's final output.
if (shouldScrapeContent) {
syncConsoleContentAndSize(false);
syncConsoleTitle();
scrapeBuffers();
}
if (m_closingConoutPipe &&
!m_conoutPipe->isClosed() &&
autoClosePipesForShutdown();
}
void Agent::autoClosePipesForShutdown()
{
if (m_closingOutputPipes) {
if (!m_conoutPipe->isClosed() &&
m_conoutPipe->bytesToSend() == 0) {
trace("Closing data pipe after child exit");
trace("Closing CONOUT pipe (auto-shutdown)");
m_conoutPipe->closePipe();
}
}
// 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(const SmallRect &windowRect)
{
m_dirtyLineCount = std::max(m_dirtyLineCount,
windowRect.top() + windowRect.height());
}
// Scan the screen buffer and advance the dirty line count when we find
// non-empty lines.
void Agent::scanForDirtyLines(const SmallRect &windowRect)
{
const int w = m_readBuffer.rect().width();
ASSERT(m_dirtyLineCount >= 1);
const CHAR_INFO *const prevLine =
m_readBuffer.lineData(m_dirtyLineCount - 1);
WORD prevLineAttr = prevLine[w - 1].Attributes;
const int stopLine = windowRect.top() + windowRect.height();
for (int line = m_dirtyLineCount; line < stopLine; ++line) {
const CHAR_INFO *lineData = m_readBuffer.lineData(line);
for (int col = 0; col < w; ++col) {
const WORD colAttr = lineData[col].Attributes;
if (lineData[col].Char.UnicodeChar != L' ' ||
colAttr != prevLineAttr) {
m_dirtyLineCount = line + 1;
break;
if (m_conerrPipe != nullptr &&
!m_conerrPipe->isClosed() &&
m_conerrPipe->bytesToSend() == 0) {
trace("Closing CONERR pipe (auto-shutdown)");
m_conerrPipe->closePipe();
}
}
prevLineAttr = lineData[w - 1].Attributes;
}
}
// 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)
std::unique_ptr<Win32ConsoleBuffer> Agent::openPrimaryBuffer()
{
ASSERT(!m_directMode);
for (int row = firstRow; row < firstRow + count; ++row) {
const int64_t bufLine = row + m_scrolledCount;
m_maxBufferedLine = std::max(m_maxBufferedLine, bufLine);
m_bufferData[bufLine % BUFFER_LINE_COUNT].blank(attributes);
}
}
// 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;
{
//
// 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_consoleBuffer->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<int>(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<int>(origBufferSize.Y, rows);
SmallRect tmpWindowRect(
0,
std::min<int>(origBufferSize.Y - tmpWindowHeight,
origWindowRect.Top),
tmpWindowWidth,
tmpWindowHeight);
if (cursorWasInWindow) {
tmpWindowRect = tmpWindowRect.ensureLineIncluded(
origInfo.cursorPosition().Y);
}
m_consoleBuffer->moveWindow(tmpWindowRect);
// Step 2: resize the buffer.
unfreezeConsole();
m_consoleBuffer->resizeBuffer(finalBufferSize);
}
// Step 3: expand the window to its full size.
{
freezeConsole();
const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
const bool cursorWasInWindow =
info.cursorPosition().Y >= info.windowRect().Top &&
info.cursorPosition().Y <= info.windowRect().Bottom;
SmallRect finalWindowRect(
0,
std::min<int>(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_consoleBuffer->moveWindow(finalWindowRect);
m_dirtyWindowTop = finalWindowRect.Top;
// 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();
}
}
@ -639,45 +500,25 @@ void Agent::resizeWindow(const int cols, const int rows)
trace("resizeWindow: invalid size: cols=%d,rows=%d", cols, rows);
return;
}
m_ptySize = Coord(cols, rows);
syncConsoleContentAndSize(true);
Win32Console::FreezeGuard guard(m_console, true);
const Coord newSize(cols, rows);
ConsoleScreenBufferInfo info;
m_primaryScraper->resizeWindow(*openPrimaryBuffer(), newSize, info);
m_consoleInput->setMouseWindowRect(info.windowRect());
if (m_errorScraper) {
m_errorScraper->resizeWindow(*m_errorBuffer, newSize, info);
}
}
void Agent::syncConsoleContentAndSize(bool forceResize)
void Agent::scrapeBuffers()
{
reopenConsole();
freezeConsole();
syncConsoleTitle();
const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
Win32Console::FreezeGuard guard(m_console, true);
ConsoleScreenBufferInfo info;
m_primaryScraper->scrapeBuffer(*openPrimaryBuffer(), info);
m_consoleInput->setMouseWindowRect(info.windowRect());
// 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(Terminal::SendClear, info.windowRect());
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_errorScraper) {
m_errorScraper->scrapeBuffer(*m_errorBuffer, info);
}
}
if (m_directMode) {
directScrapeOutput(info);
} else {
scrollingScrapeOutput(info);
}
if (forceResize) {
resizeImpl(info);
}
unfreezeConsole();
}
void Agent::syncConsoleTitle()
@ -690,229 +531,3 @@ void Agent::syncConsoleTitle()
m_currentTitle = newTitle;
}
}
void Agent::directScrapeOutput(const ConsoleScreenBufferInfo &info)
{
const Coord cursor = info.cursorPosition();
const SmallRect windowRect = info.windowRect();
const SmallRect scrapeRect(
windowRect.left(), windowRect.top(),
std::min<SHORT>(std::min(windowRect.width(), m_ptySize.X),
MAX_CONSOLE_WIDTH),
std::min<SHORT>(std::min(windowRect.height(), m_ptySize.Y),
BUFFER_LINE_COUNT));
const int w = scrapeRect.width();
const int h = scrapeRect.height();
largeConsoleRead(m_readBuffer, *m_consoleBuffer, scrapeRect);
bool sawModifiedLine = false;
for (int line = 0; line < h; ++line) {
const CHAR_INFO *curLine =
m_readBuffer.lineData(scrapeRect.top() + line);
ConsoleLine &bufLine = m_bufferData[line];
if (sawModifiedLine) {
bufLine.setLine(curLine, w);
} else {
sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w);
}
if (sawModifiedLine) {
//trace("sent line %d", line);
m_terminal->sendLine(line, curLine, w);
}
}
m_terminal->finishOutput(
std::pair<int, int64_t>(
constrained(0, cursor.X - scrapeRect.Left, w - 1),
constrained(0, cursor.Y - scrapeRect.Top, h - 1)));
}
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
// and adjust the scroll count.
int markerRow = findSyncMarker();
if (markerRow == -1) {
// Something has happened. Reset the terminal.
trace("Sync marker has disappeared -- resetting the terminal"
" (m_syncCounter=%d)",
m_syncCounter);
resetConsoleTracking(Terminal::SendClear, windowRect);
} else if (markerRow != m_syncRow) {
ASSERT(markerRow < m_syncRow);
m_scrolledCount += (m_syncRow - markerRow);
m_syncRow = markerRow;
// If the buffer has scrolled, then the entire window is dirty.
markEntireWindowDirty(windowRect);
}
}
// Update the dirty line count:
// - If the window has moved, the entire window is dirty.
// - Everything up to the cursor is dirty.
// - All lines above the window are dirty.
// - Any non-blank lines are dirty.
if (m_dirtyWindowTop != -1) {
if (windowRect.top() > m_dirtyWindowTop) {
// The window has moved down, presumably as a result of scrolling.
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
// to the top as part of clearing everything else in the console.
trace("Window moved upward -- resetting the terminal"
" (m_syncCounter=%d)",
m_syncCounter);
resetConsoleTracking(Terminal::SendClear, windowRect);
}
}
m_dirtyWindowTop = windowRect.top();
m_dirtyLineCount = std::max(m_dirtyLineCount, cursor.Y + 1);
m_dirtyLineCount = std::max(m_dirtyLineCount, (int)windowRect.top());
// There will be at least one dirty line, because there is a cursor.
ASSERT(m_dirtyLineCount >= 1);
// The first line to scrape, in virtual line coordinates.
const int64_t firstVirtLine = std::min(m_scrapedLineCount,
windowRect.top() + m_scrolledCount);
// Read all the data we will need from the console. Start reading with the
// first line to scrape, but adjust the the read area upward to account for
// scanForDirtyLines' need to read the previous attribute. Read to the
// bottom of the window. (It's not clear to me whether the
// m_dirtyLineCount adjustment here is strictly necessary. It isn't
// necessary so long as the cursor is inside the current window.)
const int firstReadLine = std::min<int>(firstVirtLine - m_scrolledCount,
m_dirtyLineCount - 1);
const int stopReadLine = std::max(windowRect.top() + windowRect.height(),
m_dirtyLineCount);
ASSERT(firstReadLine >= 0 && stopReadLine > firstReadLine);
largeConsoleRead(m_readBuffer,
*m_consoleBuffer,
SmallRect(0, firstReadLine,
std::min<SHORT>(info.bufferSize().X,
MAX_CONSOLE_WIDTH),
stopReadLine - firstReadLine));
scanForDirtyLines(windowRect);
// Note that it's possible for all the lines on the current window to
// be non-dirty.
// The line to stop scraping at, in virtual line coordinates.
const int64_t stopVirtLine =
std::min(m_dirtyLineCount, windowRect.top() + windowRect.height()) +
m_scrolledCount;
bool sawModifiedLine = false;
const int w = m_readBuffer.rect().width();
for (int64_t line = firstVirtLine; line < stopVirtLine; ++line) {
const CHAR_INFO *curLine =
m_readBuffer.lineData(line - m_scrolledCount);
ConsoleLine &bufLine = m_bufferData[line % BUFFER_LINE_COUNT];
if (line > m_maxBufferedLine) {
m_maxBufferedLine = line;
sawModifiedLine = true;
}
if (sawModifiedLine) {
bufLine.setLine(curLine, w);
} else {
sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w);
}
if (sawModifiedLine) {
//trace("sent line %d", line);
m_terminal->sendLine(line, curLine, w);
}
}
m_scrapedLineCount = windowRect.top() + m_scrolledCount;
// Creating a new sync row requires clearing part of the console buffer, so
// avoid doing it if there's already a sync row that's good enough.
// TODO: replace hard-coded constants
const int newSyncRow = static_cast<int>(windowRect.top()) - 200;
if (newSyncRow >= 1 && newSyncRow >= m_syncRow + 200) {
createSyncMarker(newSyncRow);
}
m_terminal->finishOutput(
std::pair<int, int64_t>(cursor.X,
cursor.Y + m_scrolledCount));
}
void Agent::reopenConsole()
{
// Reopen CONOUT. The application may have changed the active screen
// buffer. (See https://github.com/rprichard/winpty/issues/34)
m_consoleBuffer.reset(new Win32ConsoleBuffer());
}
void Agent::freezeConsole()
{
sendSysCommand(m_console.hwnd(), m_useMark ? SC_CONSOLE_MARK
: SC_CONSOLE_SELECT_ALL);
}
void Agent::unfreezeConsole()
{
sendEscape(m_console.hwnd());
}
void Agent::syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN])
{
// XXX: The marker text generated here could easily collide with ordinary
// console output. Does it make sense to try to avoid the collision?
char str[SYNC_MARKER_LEN + 1];
winpty_snprintf(str, "S*Y*N*C*%08x", m_syncCounter);
for (int i = 0; i < SYNC_MARKER_LEN; ++i) {
output[i].Char.UnicodeChar = str[i];
output[i].Attributes = 7;
}
}
int Agent::findSyncMarker()
{
ASSERT(m_syncRow >= 0);
CHAR_INFO marker[SYNC_MARKER_LEN];
CHAR_INFO column[BUFFER_LINE_COUNT];
syncMarkerText(marker);
SmallRect rect(0, 0, 1, m_syncRow + SYNC_MARKER_LEN);
m_consoleBuffer->read(rect, column);
int i;
for (i = m_syncRow; i >= 0; --i) {
int j;
for (j = 0; j < SYNC_MARKER_LEN; ++j) {
if (column[i + j].Char.UnicodeChar != marker[j].Char.UnicodeChar)
break;
}
if (j == SYNC_MARKER_LEN)
return i;
}
return -1;
}
void Agent::createSyncMarker(int row)
{
ASSERT(row >= 1);
// Clear the lines around the marker to ensure that Windows 10's rewrapping
// does not affect the marker.
m_consoleBuffer->clearLines(row - 1, SYNC_MARKER_LEN + 1,
m_consoleBuffer->bufferInfo());
// Write a new marker.
m_syncCounter++;
CHAR_INFO marker[SYNC_MARKER_LEN];
syncMarkerText(marker);
m_syncRow = row;
SmallRect markerRect(0, m_syncRow, 1, SYNC_MARKER_LEN);
m_consoleBuffer->write(markerRect, marker);
}

View File

@ -26,29 +26,17 @@
#include <memory>
#include <string>
#include <vector>
#include "ConsoleLine.h"
#include "Coord.h"
#include "DsrSender.h"
#include "EventLoop.h"
#include "LargeConsoleRead.h"
#include "SmallRect.h"
#include "Terminal.h"
#include "Win32Console.h"
class ConsoleInput;
class ConsoleScreenBufferInfo;
class NamedPipe;
class ReadBuffer;
class Scraper;
class WriteBuffer;
// We must be able to issue a single ReadConsoleOutputW call of
// MAX_CONSOLE_WIDTH characters, and a single read of approximately several
// hundred fewer characters than BUFFER_LINE_COUNT.
const int BUFFER_LINE_COUNT = 3000;
const int MAX_CONSOLE_WIDTH = 2500;
const int SYNC_MARKER_LEN = 16;
class Win32ConsoleBuffer;
class Agent : public EventLoop, public DsrSender
{
@ -63,8 +51,6 @@ public:
private:
NamedPipe &connectToControlPipe(LPCWSTR pipeName);
NamedPipe &createDataServerPipe(bool write, const wchar_t *kind);
void resetConsoleTracking(
Terminal::SendClearFlag sendClear, const SmallRect &windowRect);
private:
void pollControlPipe();
@ -73,54 +59,33 @@ private:
void handleStartProcessPacket(ReadBuffer &packet);
void handleSetSizePacket(ReadBuffer &packet);
void pollConinPipe();
void pollConoutPipe();
protected:
virtual void onPollTimeout();
virtual void onPipeIo(NamedPipe &namedPipe);
private:
void markEntireWindowDirty(const SmallRect &windowRect);
void scanForDirtyLines(const SmallRect &windowRect);
void clearBufferLines(int firstRow, int count, WORD attributes);
void resizeImpl(const ConsoleScreenBufferInfo &origInfo);
void autoClosePipesForShutdown();
std::unique_ptr<Win32ConsoleBuffer> openPrimaryBuffer();
void resizeWindow(int cols, int rows);
void syncConsoleContentAndSize(bool forceResize);
void scrapeBuffers();
void syncConsoleTitle();
void directScrapeOutput(const ConsoleScreenBufferInfo &info);
void scrollingScrapeOutput(const ConsoleScreenBufferInfo &info);
void reopenConsole();
void freezeConsole();
void unfreezeConsole();
void syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN]);
int findSyncMarker();
void createSyncMarker(int row);
private:
bool m_useMark = false;
const bool m_useConerr;
const bool m_plainMode;
Win32Console m_console;
std::unique_ptr<Win32ConsoleBuffer> m_consoleBuffer;
std::unique_ptr<Scraper> m_primaryScraper;
std::unique_ptr<Scraper> m_errorScraper;
std::unique_ptr<Win32ConsoleBuffer> m_errorBuffer;
NamedPipe *m_controlPipe = nullptr;
NamedPipe *m_coninPipe = nullptr;
NamedPipe *m_conoutPipe = nullptr;
bool m_closingConoutPipe = false;
std::unique_ptr<Terminal> m_terminal;
NamedPipe *m_conerrPipe = nullptr;
bool m_autoShutdown = false;
bool m_closingOutputPipes = false;
std::unique_ptr<ConsoleInput> m_consoleInput;
HANDLE m_childProcess = nullptr;
bool m_autoShutdown = false;
int m_syncRow = 0;
int m_syncCounter = 0;
bool m_directMode = false;
Coord m_ptySize;
int64_t m_scrapedLineCount = 0;
int64_t m_scrolledCount = 0;
int64_t m_maxBufferedLine = 0;
LargeConsoleReadBuffer m_readBuffer;
std::vector<ConsoleLine> m_bufferData;
int m_dirtyWindowTop = 0;
int m_dirtyLineCount = 0;
// If the title is initialized to the empty string, then cmd.exe will
// sometimes print this error:

View File

@ -23,7 +23,7 @@
#include <stdlib.h>
#include "../shared/WindowsVersion.h"
#include "Agent.h"
#include "Scraper.h"
#include "Win32ConsoleBuffer.h"
LargeConsoleReadBuffer::LargeConsoleReadBuffer() :

507
src/agent/Scraper.cc Executable file
View File

@ -0,0 +1,507 @@
// Copyright (c) 2011-2016 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 "Scraper.h"
#include <windows.h>
#include <stdint.h>
#include <algorithm>
#include <utility>
#include "../shared/WinptyAssert.h"
#include "../shared/winpty_snprintf.h"
#include "ConsoleFont.h"
#include "Win32Console.h"
#include "Win32ConsoleBuffer.h"
namespace {
template <typename T>
T constrained(T min, T val, T max) {
ASSERT(min <= max);
return std::min(std::max(min, val), max);
}
} // anonymous namespace
Scraper::Scraper(
Win32Console &console,
Win32ConsoleBuffer &buffer,
std::unique_ptr<Terminal> terminal,
Coord initialSize) :
m_console(console),
m_terminal(std::move(terminal)),
m_ptySize(initialSize)
{
resetConsoleTracking(Terminal::OmitClear, buffer.windowRect());
m_bufferData.resize(BUFFER_LINE_COUNT);
setSmallFont(buffer.conout());
buffer.moveWindow(SmallRect(0, 0, 1, 1));
buffer.resizeBuffer(Coord(initialSize.X, BUFFER_LINE_COUNT));
buffer.moveWindow(SmallRect(0, 0, initialSize.X, initialSize.Y));
buffer.setCursorPosition(Coord(0, 0));
// For the sake of the color translation heuristic, set the console color
// to LtGray-on-Black.
buffer.setTextAttribute(7);
buffer.clearAllLines(m_consoleBuffer->bufferInfo());
}
Scraper::~Scraper()
{
}
void Scraper::resizeWindow(Win32ConsoleBuffer &buffer,
Coord newSize,
ConsoleScreenBufferInfo &finalInfoOut)
{
m_consoleBuffer = &buffer;
m_ptySize = newSize;
syncConsoleContentAndSize(true, finalInfoOut);
m_consoleBuffer = nullptr;
}
void Scraper::scrapeBuffer(Win32ConsoleBuffer &buffer,
ConsoleScreenBufferInfo &finalInfoOut)
{
m_consoleBuffer = &buffer;
syncConsoleContentAndSize(false, finalInfoOut);
m_consoleBuffer = nullptr;
}
void Scraper::resetConsoleTracking(
Terminal::SendClearFlag sendClear, const SmallRect &windowRect)
{
for (ConsoleLine &line : m_bufferData) {
line.reset();
}
m_syncRow = -1;
m_scrapedLineCount = windowRect.top();
m_scrolledCount = 0;
m_maxBufferedLine = -1;
m_dirtyWindowTop = -1;
m_dirtyLineCount = 0;
m_terminal->reset(sendClear, m_scrapedLineCount);
}
// 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 Scraper::markEntireWindowDirty(const SmallRect &windowRect)
{
m_dirtyLineCount = std::max(m_dirtyLineCount,
windowRect.top() + windowRect.height());
}
// Scan the screen buffer and advance the dirty line count when we find
// non-empty lines.
void Scraper::scanForDirtyLines(const SmallRect &windowRect)
{
const int w = m_readBuffer.rect().width();
ASSERT(m_dirtyLineCount >= 1);
const CHAR_INFO *const prevLine =
m_readBuffer.lineData(m_dirtyLineCount - 1);
WORD prevLineAttr = prevLine[w - 1].Attributes;
const int stopLine = windowRect.top() + windowRect.height();
for (int line = m_dirtyLineCount; line < stopLine; ++line) {
const CHAR_INFO *lineData = m_readBuffer.lineData(line);
for (int col = 0; col < w; ++col) {
const WORD colAttr = lineData[col].Attributes;
if (lineData[col].Char.UnicodeChar != L' ' ||
colAttr != prevLineAttr) {
m_dirtyLineCount = line + 1;
break;
}
}
prevLineAttr = lineData[w - 1].Attributes;
}
}
// Clear lines in the line buffer. The `firstRow` parameter is in
// screen-buffer coordinates.
void Scraper::clearBufferLines(
const int firstRow,
const int count,
const WORD attributes)
{
ASSERT(!m_directMode);
for (int row = firstRow; row < firstRow + count; ++row) {
const int64_t bufLine = row + m_scrolledCount;
m_maxBufferedLine = std::max(m_maxBufferedLine, bufLine);
m_bufferData[bufLine % BUFFER_LINE_COUNT].blank(attributes);
}
}
void Scraper::resizeImpl(const ConsoleScreenBufferInfo &origInfo)
{
ASSERT(m_console.frozen());
const int cols = m_ptySize.X;
const int rows = m_ptySize.Y;
{
//
// 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_consoleBuffer->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<int>(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<int>(origBufferSize.Y, rows);
SmallRect tmpWindowRect(
0,
std::min<int>(origBufferSize.Y - tmpWindowHeight,
origWindowRect.Top),
tmpWindowWidth,
tmpWindowHeight);
if (cursorWasInWindow) {
tmpWindowRect = tmpWindowRect.ensureLineIncluded(
origInfo.cursorPosition().Y);
}
m_consoleBuffer->moveWindow(tmpWindowRect);
// Step 2: resize the buffer.
m_console.setFrozen(false);
m_consoleBuffer->resizeBuffer(finalBufferSize);
}
// Step 3: expand the window to its full size.
{
m_console.setFrozen(true);
const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
const bool cursorWasInWindow =
info.cursorPosition().Y >= info.windowRect().Top &&
info.cursorPosition().Y <= info.windowRect().Bottom;
SmallRect finalWindowRect(
0,
std::min<int>(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_consoleBuffer->moveWindow(finalWindowRect);
m_dirtyWindowTop = finalWindowRect.Top;
}
ASSERT(m_console.frozen());
}
void Scraper::syncConsoleContentAndSize(
bool forceResize,
ConsoleScreenBufferInfo &finalInfoOut)
{
Win32Console::FreezeGuard guard(m_console, true);
const ConsoleScreenBufferInfo info = m_consoleBuffer->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(Terminal::SendClear, info.windowRect());
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);
finalInfoOut = m_consoleBuffer->bufferInfo();
} else {
finalInfoOut = info;
}
}
void Scraper::directScrapeOutput(const ConsoleScreenBufferInfo &info)
{
const Coord cursor = info.cursorPosition();
const SmallRect windowRect = info.windowRect();
const SmallRect scrapeRect(
windowRect.left(), windowRect.top(),
std::min<SHORT>(std::min(windowRect.width(), m_ptySize.X),
MAX_CONSOLE_WIDTH),
std::min<SHORT>(std::min(windowRect.height(), m_ptySize.Y),
BUFFER_LINE_COUNT));
const int w = scrapeRect.width();
const int h = scrapeRect.height();
largeConsoleRead(m_readBuffer, *m_consoleBuffer, scrapeRect);
bool sawModifiedLine = false;
for (int line = 0; line < h; ++line) {
const CHAR_INFO *curLine =
m_readBuffer.lineData(scrapeRect.top() + line);
ConsoleLine &bufLine = m_bufferData[line];
if (sawModifiedLine) {
bufLine.setLine(curLine, w);
} else {
sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w);
}
if (sawModifiedLine) {
//trace("sent line %d", line);
m_terminal->sendLine(line, curLine, w);
}
}
m_terminal->finishOutput(
std::pair<int, int64_t>(
constrained(0, cursor.X - scrapeRect.Left, w - 1),
constrained(0, cursor.Y - scrapeRect.Top, h - 1)));
}
void Scraper::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
// and adjust the scroll count.
int markerRow = findSyncMarker();
if (markerRow == -1) {
// Something has happened. Reset the terminal.
trace("Sync marker has disappeared -- resetting the terminal"
" (m_syncCounter=%d)",
m_syncCounter);
resetConsoleTracking(Terminal::SendClear, windowRect);
} else if (markerRow != m_syncRow) {
ASSERT(markerRow < m_syncRow);
m_scrolledCount += (m_syncRow - markerRow);
m_syncRow = markerRow;
// If the buffer has scrolled, then the entire window is dirty.
markEntireWindowDirty(windowRect);
}
}
// Update the dirty line count:
// - If the window has moved, the entire window is dirty.
// - Everything up to the cursor is dirty.
// - All lines above the window are dirty.
// - Any non-blank lines are dirty.
if (m_dirtyWindowTop != -1) {
if (windowRect.top() > m_dirtyWindowTop) {
// The window has moved down, presumably as a result of scrolling.
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
// to the top as part of clearing everything else in the console.
trace("Window moved upward -- resetting the terminal"
" (m_syncCounter=%d)",
m_syncCounter);
resetConsoleTracking(Terminal::SendClear, windowRect);
}
}
m_dirtyWindowTop = windowRect.top();
m_dirtyLineCount = std::max(m_dirtyLineCount, cursor.Y + 1);
m_dirtyLineCount = std::max(m_dirtyLineCount, (int)windowRect.top());
// There will be at least one dirty line, because there is a cursor.
ASSERT(m_dirtyLineCount >= 1);
// The first line to scrape, in virtual line coordinates.
const int64_t firstVirtLine = std::min(m_scrapedLineCount,
windowRect.top() + m_scrolledCount);
// Read all the data we will need from the console. Start reading with the
// first line to scrape, but adjust the the read area upward to account for
// scanForDirtyLines' need to read the previous attribute. Read to the
// bottom of the window. (It's not clear to me whether the
// m_dirtyLineCount adjustment here is strictly necessary. It isn't
// necessary so long as the cursor is inside the current window.)
const int firstReadLine = std::min<int>(firstVirtLine - m_scrolledCount,
m_dirtyLineCount - 1);
const int stopReadLine = std::max(windowRect.top() + windowRect.height(),
m_dirtyLineCount);
ASSERT(firstReadLine >= 0 && stopReadLine > firstReadLine);
largeConsoleRead(m_readBuffer,
*m_consoleBuffer,
SmallRect(0, firstReadLine,
std::min<SHORT>(info.bufferSize().X,
MAX_CONSOLE_WIDTH),
stopReadLine - firstReadLine));
scanForDirtyLines(windowRect);
// Note that it's possible for all the lines on the current window to
// be non-dirty.
// The line to stop scraping at, in virtual line coordinates.
const int64_t stopVirtLine =
std::min(m_dirtyLineCount, windowRect.top() + windowRect.height()) +
m_scrolledCount;
bool sawModifiedLine = false;
const int w = m_readBuffer.rect().width();
for (int64_t line = firstVirtLine; line < stopVirtLine; ++line) {
const CHAR_INFO *curLine =
m_readBuffer.lineData(line - m_scrolledCount);
ConsoleLine &bufLine = m_bufferData[line % BUFFER_LINE_COUNT];
if (line > m_maxBufferedLine) {
m_maxBufferedLine = line;
sawModifiedLine = true;
}
if (sawModifiedLine) {
bufLine.setLine(curLine, w);
} else {
sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w);
}
if (sawModifiedLine) {
//trace("sent line %d", line);
m_terminal->sendLine(line, curLine, w);
}
}
m_scrapedLineCount = windowRect.top() + m_scrolledCount;
// Creating a new sync row requires clearing part of the console buffer, so
// avoid doing it if there's already a sync row that's good enough.
// TODO: replace hard-coded constants
const int newSyncRow = static_cast<int>(windowRect.top()) - 200;
if (newSyncRow >= 1 && newSyncRow >= m_syncRow + 200) {
createSyncMarker(newSyncRow);
}
m_terminal->finishOutput(
std::pair<int, int64_t>(cursor.X,
cursor.Y + m_scrolledCount));
}
void Scraper::syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN])
{
// XXX: The marker text generated here could easily collide with ordinary
// console output. Does it make sense to try to avoid the collision?
char str[SYNC_MARKER_LEN + 1];
winpty_snprintf(str, "S*Y*N*C*%08x", m_syncCounter);
for (int i = 0; i < SYNC_MARKER_LEN; ++i) {
output[i].Char.UnicodeChar = str[i];
output[i].Attributes = 7;
}
}
int Scraper::findSyncMarker()
{
ASSERT(m_syncRow >= 0);
CHAR_INFO marker[SYNC_MARKER_LEN];
CHAR_INFO column[BUFFER_LINE_COUNT];
syncMarkerText(marker);
SmallRect rect(0, 0, 1, m_syncRow + SYNC_MARKER_LEN);
m_consoleBuffer->read(rect, column);
int i;
for (i = m_syncRow; i >= 0; --i) {
int j;
for (j = 0; j < SYNC_MARKER_LEN; ++j) {
if (column[i + j].Char.UnicodeChar != marker[j].Char.UnicodeChar)
break;
}
if (j == SYNC_MARKER_LEN)
return i;
}
return -1;
}
void Scraper::createSyncMarker(int row)
{
ASSERT(row >= 1);
// Clear the lines around the marker to ensure that Windows 10's rewrapping
// does not affect the marker.
m_consoleBuffer->clearLines(row - 1, SYNC_MARKER_LEN + 1,
m_consoleBuffer->bufferInfo());
// Write a new marker.
m_syncCounter++;
CHAR_INFO marker[SYNC_MARKER_LEN];
syncMarkerText(marker);
m_syncRow = row;
SmallRect markerRect(0, m_syncRow, 1, SYNC_MARKER_LEN);
m_consoleBuffer->write(markerRect, marker);
}

96
src/agent/Scraper.h Executable file
View File

@ -0,0 +1,96 @@
// Copyright (c) 2011-2016 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.
#ifndef AGENT_SCRAPER_H
#define AGENT_SCRAPER_H
#include <windows.h>
#include <stdint.h>
#include <memory>
#include <vector>
#include "ConsoleLine.h"
#include "Coord.h"
#include "LargeConsoleRead.h"
#include "SmallRect.h"
#include "Terminal.h"
class ConsoleScreenBufferInfo;
class Win32Console;
class Win32ConsoleBuffer;
// We must be able to issue a single ReadConsoleOutputW call of
// MAX_CONSOLE_WIDTH characters, and a single read of approximately several
// hundred fewer characters than BUFFER_LINE_COUNT.
const int BUFFER_LINE_COUNT = 3000;
const int MAX_CONSOLE_WIDTH = 2500;
const int SYNC_MARKER_LEN = 16;
class Scraper {
public:
Scraper(
Win32Console &console,
Win32ConsoleBuffer &buffer,
std::unique_ptr<Terminal> terminal,
Coord initialSize);
~Scraper();
void resizeWindow(Win32ConsoleBuffer &buffer,
Coord newSize,
ConsoleScreenBufferInfo &finalInfoOut);
void scrapeBuffer(Win32ConsoleBuffer &buffer,
ConsoleScreenBufferInfo &finalInfoOut);
private:
void resetConsoleTracking(
Terminal::SendClearFlag sendClear, const SmallRect &windowRect);
void markEntireWindowDirty(const SmallRect &windowRect);
void scanForDirtyLines(const SmallRect &windowRect);
void clearBufferLines(int firstRow, int count, WORD attributes);
void resizeImpl(const ConsoleScreenBufferInfo &origInfo);
void syncConsoleContentAndSize(bool forceResize,
ConsoleScreenBufferInfo &finalInfoOut);
void directScrapeOutput(const ConsoleScreenBufferInfo &info);
void scrollingScrapeOutput(const ConsoleScreenBufferInfo &info);
void syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN]);
int findSyncMarker();
void createSyncMarker(int row);
private:
Win32Console &m_console;
Win32ConsoleBuffer *m_consoleBuffer;
std::unique_ptr<Terminal> m_terminal;
int m_syncRow = -1;
int m_syncCounter = 0;
bool m_directMode = false;
Coord m_ptySize;
int64_t m_scrapedLineCount = 0;
int64_t m_scrolledCount = 0;
int64_t m_maxBufferedLine = -1;
LargeConsoleReadBuffer m_readBuffer;
std::vector<ConsoleLine> m_bufferData;
int m_dirtyWindowTop = -1;
int m_dirtyLineCount = 0;
};
#endif // AGENT_SCRAPER_H

View File

@ -286,17 +286,9 @@ static inline void scanUnicodeScalarValue(
} // anonymous namespace
void Terminal::setConsoleMode(int mode)
{
if (mode == 1)
m_consoleMode = true;
else
m_consoleMode = false;
}
void Terminal::reset(SendClearFlag sendClearFirst, int64_t newLine)
{
if (sendClearFirst == SendClear && !m_consoleMode) {
if (sendClearFirst == SendClear && !m_plainMode) {
// 0m ==> reset SGR parameters
// 1;1H ==> move cursor to top-left position
// 2J ==> clear the entire screen
@ -321,7 +313,7 @@ void Terminal::sendLine(int64_t line, const CHAR_INFO *lineData, int width)
for (int i = 0; i < width; i += cellCount) {
int color = lineData[i].Attributes & COLOR_ATTRIBUTE_MASK;
if (color != m_remoteColor) {
if (!m_consoleMode) {
if (!m_plainMode) {
outputSetColor(m_termLine, color);
}
trimmedLineLength = m_termLine.size();
@ -340,7 +332,7 @@ void Terminal::sendLine(int64_t line, const CHAR_INFO *lineData, int width)
// issuing a CSI 0K at that point also erases the last cell in
// the line. Work around this behavior by issuing the erase
// one character early in that case.
if (!m_consoleMode) {
if (!m_plainMode) {
m_termLine.append(CSI"0K"); // Erase from cursor to EOL
}
alreadyErasedLine = true;
@ -359,7 +351,7 @@ void Terminal::sendLine(int64_t line, const CHAR_INFO *lineData, int width)
m_output.write(m_termLine.data(), trimmedLineLength);
if (!alreadyErasedLine && !m_consoleMode) {
if (!alreadyErasedLine && !m_plainMode) {
m_output.write(CSI"0K"); // Erase from cursor to EOL
}
}
@ -372,7 +364,7 @@ void Terminal::finishOutput(const std::pair<int, int64_t> &newCursorPos)
moveTerminalToLine(newCursorPos.second);
char buffer[32];
winpty_snprintf(buffer, CSI"%dG" CSI"?25h", newCursorPos.first + 1);
if (!m_consoleMode)
if (!m_plainMode)
m_output.write(buffer);
m_cursorHidden = false;
}
@ -383,7 +375,7 @@ void Terminal::hideTerminalCursor()
{
if (m_cursorHidden)
return;
if (!m_consoleMode)
if (!m_plainMode)
m_output.write(CSI"?25l");
m_cursorHidden = true;
}
@ -400,12 +392,12 @@ void Terminal::moveTerminalToLine(int64_t line)
char buffer[32];
winpty_snprintf(buffer, "\r" CSI"%dA",
static_cast<int>(m_remoteLine - line));
if (!m_consoleMode)
if (!m_plainMode)
m_output.write(buffer);
m_remoteLine = line;
} else if (line > m_remoteLine) {
while (line > m_remoteLine) {
if (!m_consoleMode)
if (!m_plainMode)
m_output.write("\r\n");
m_remoteLine++;
}

View File

@ -34,12 +34,15 @@ class NamedPipe;
class Terminal
{
public:
explicit Terminal(NamedPipe &output) : m_output(output) {}
explicit Terminal(NamedPipe &output, bool plainMode, bool outputColor)
: m_output(output), m_plainMode(plainMode), m_outputColor(outputColor)
{
}
enum SendClearFlag { OmitClear, SendClear };
void reset(SendClearFlag sendClearFirst, int64_t newLine);
void sendLine(int64_t line, const CHAR_INFO *lineData, int width);
void finishOutput(const std::pair<int, int64_t> &newCursorPos);
void setConsoleMode(int mode);
private:
void hideTerminalCursor();
@ -51,8 +54,9 @@ private:
bool m_cursorHidden = false;
std::pair<int, int64_t> m_cursorPos;
int m_remoteColor = -1;
bool m_consoleMode = false;
std::string m_termLine;
bool m_plainMode = false;
bool m_outputColor = true; // TODO: Respect the m_outputColor flag.
};
#endif // TERMINAL_H

View File

@ -87,3 +87,21 @@ void Win32Console::setTitle(const std::wstring &title)
trace("SetConsoleTitleW failed");
}
}
void Win32Console::setFrozen(bool frozen) {
const int SC_CONSOLE_MARK = 0xFFF2;
const int SC_CONSOLE_SELECT_ALL = 0xFFF5;
if (frozen == m_frozen) {
// Do nothing.
} else if (frozen) {
// Enter selection mode by activating either Mark or SelectAll.
const int command = m_freezeUsesMark ? SC_CONSOLE_MARK
: SC_CONSOLE_SELECT_ALL;
SendMessage(m_hwnd, WM_SYSCOMMAND, command, 0);
m_frozen = true;
} else {
// Send Escape to cancel the selection.
SendMessage(m_hwnd, WM_CHAR, 27, 0x00010001);
m_frozen = false;
}
}

View File

@ -29,14 +29,35 @@
class Win32Console
{
public:
class FreezeGuard {
public:
FreezeGuard(Win32Console &console, bool frozen) :
m_console(console), m_previous(console.frozen()) {
m_console.setFrozen(frozen);
}
~FreezeGuard() {
m_console.setFrozen(m_previous);
}
FreezeGuard(const FreezeGuard &other) = delete;
FreezeGuard &operator=(const FreezeGuard &other) = delete;
private:
Win32Console &m_console;
bool m_previous;
};
Win32Console();
HWND hwnd() { return m_hwnd; }
std::wstring title();
void setTitle(const std::wstring &title);
void setFreezeUsesMark(bool useMark) { m_freezeUsesMark = useMark; }
void setFrozen(bool frozen=true);
bool frozen() { return m_frozen; }
private:
HWND m_hwnd;
HWND m_hwnd = nullptr;
bool m_frozen = false;
bool m_freezeUsesMark = false;
std::vector<wchar_t> m_titleWorkBuf;
};

View File

@ -25,16 +25,34 @@
#include "../shared/DebugClient.h"
#include "../shared/WinptyAssert.h"
Win32ConsoleBuffer::Win32ConsoleBuffer() {
m_conout = CreateFileW(L"CONOUT$",
std::unique_ptr<Win32ConsoleBuffer> Win32ConsoleBuffer::openStdout() {
return std::unique_ptr<Win32ConsoleBuffer>(
new Win32ConsoleBuffer(GetStdHandle(STD_OUTPUT_HANDLE), false));
}
std::unique_ptr<Win32ConsoleBuffer> Win32ConsoleBuffer::openConout() {
const HANDLE 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);
ASSERT(conout != INVALID_HANDLE_VALUE);
return std::unique_ptr<Win32ConsoleBuffer>(
new Win32ConsoleBuffer(conout, true));
}
Win32ConsoleBuffer::~Win32ConsoleBuffer() {
CloseHandle(m_conout);
std::unique_ptr<Win32ConsoleBuffer> Win32ConsoleBuffer::createErrorBuffer() {
SECURITY_ATTRIBUTES sa = {};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
const HANDLE conout =
CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
&sa,
CONSOLE_TEXTMODE_BUFFER,
nullptr);
ASSERT(conout != INVALID_HANDLE_VALUE);
return std::unique_ptr<Win32ConsoleBuffer>(
new Win32ConsoleBuffer(conout, true));
}
HANDLE Win32ConsoleBuffer::conout() {

View File

@ -25,6 +25,8 @@
#include <string.h>
#include <memory>
#include "Coord.h"
#include "SmallRect.h"
@ -41,9 +43,25 @@ public:
};
class Win32ConsoleBuffer {
private:
Win32ConsoleBuffer(HANDLE conout, bool owned) :
m_conout(conout), m_owned(owned)
{
}
public:
Win32ConsoleBuffer();
~Win32ConsoleBuffer();
~Win32ConsoleBuffer() {
if (m_owned) {
CloseHandle(m_conout);
}
}
static std::unique_ptr<Win32ConsoleBuffer> openStdout();
static std::unique_ptr<Win32ConsoleBuffer> openConout();
static std::unique_ptr<Win32ConsoleBuffer> createErrorBuffer();
Win32ConsoleBuffer(const Win32ConsoleBuffer &other) = delete;
Win32ConsoleBuffer &operator=(const Win32ConsoleBuffer &other) = delete;
HANDLE conout();
void clearLines(int row, int count, const ConsoleScreenBufferInfo &info);
@ -67,7 +85,8 @@ public:
void setTextAttribute(WORD attributes);
private:
HANDLE m_conout;
HANDLE m_conout = nullptr;
bool m_owned = false;
};
#endif // AGENT_WIN32_CONSOLE_BUFFER_H

View File

@ -33,6 +33,7 @@ AGENT_OBJECTS = \
build/agent/agent/InputMap.o \
build/agent/agent/LargeConsoleRead.o \
build/agent/agent/NamedPipe.o \
build/agent/agent/Scraper.o \
build/agent/agent/Terminal.o \
build/agent/agent/Win32Console.o \
build/agent/agent/Win32ConsoleBuffer.o \

View File

@ -139,6 +139,7 @@ WINPTY_API HANDLE winpty_agent_process(winpty_t *wp);
* instantly. */
WINPTY_API LPCWSTR winpty_conin_name(winpty_t *wp);
WINPTY_API LPCWSTR winpty_conout_name(winpty_t *wp);
WINPTY_API LPCWSTR winpty_conerr_name(winpty_t *wp);

View File

@ -50,7 +50,26 @@
/*****************************************************************************
* Configuration of a new agent. */
#define WINPTY_FLAG_MASK 0ull
/* Create a new screen buffer (connected to the "conerr" terminal pipe) and
* pass it to child processes as the STDERR handle. This flag also prevents
* the agent from reopening CONOUT$ when it polls -- regardless of whether the
* active screen buffer changes, winpty continues to monitor the original
* primary screen buffer. */
#define WINPTY_FLAG_CONERR 0x1ull
/* Don't output escape sequences. */
#define WINPTY_FLAG_PLAIN_OUTPUT 0x2ull
/* Do output color escape sequences. These escapes are output by default, but
* are suppressed with WINPTY_FLAG_PLAIN_OUTPUT. Use this flag to reenable
* them. */
#define WINPTY_FLAG_COLOR_ESCAPES 0x4ull
#define WINPTY_FLAG_MASK (0ull \
| WINPTY_FLAG_CONERR \
| WINPTY_FLAG_PLAIN_OUTPUT \
| WINPTY_FLAG_COLOR_ESCAPES \
)

View File

@ -56,6 +56,7 @@ struct winpty_s {
OwnedHandle ioEvent;
std::wstring coninPipeName;
std::wstring conoutPipeName;
std::wstring conerrPipeName;
};
struct winpty_spawn_config_s {

View File

@ -639,6 +639,9 @@ winpty_open(const winpty_config_t *cfg,
auto packet = readPacket(*wp.get());
wp->coninPipeName = packet.getWString();
wp->conoutPipeName = packet.getWString();
if (cfg->flags & WINPTY_FLAG_CONERR) {
wp->conerrPipeName = packet.getWString();
}
packet.assertEof();
return wp.release();
@ -673,6 +676,15 @@ WINPTY_API LPCWSTR winpty_conout_name(winpty_t *wp) {
return cstrFromWStringOrNull(wp->conoutPipeName);
}
WINPTY_API LPCWSTR winpty_conerr_name(winpty_t *wp) {
ASSERT(wp != nullptr);
if (wp->conerrPipeName.empty()) {
return nullptr;
} else {
return cstrFromWStringOrNull(wp->conerrPipeName);
}
}
/*****************************************************************************

View File

@ -72,6 +72,8 @@
'agent/LargeConsoleRead.cc',
'agent/NamedPipe.h',
'agent/NamedPipe.cc',
'agent/Scraper.h',
'agent/Scraper.cc',
'agent/SimplePool.h',
'agent/SmallRect.h',
'agent/Terminal.h',