installed pty
[VSoRC/.git] / node_modules / node-pty / deps / winpty / src / agent / Scraper.cc
1 // Copyright (c) 2011-2016 Ryan Prichard
2 //
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:
9 //
10 // The above copyright notice and this permission notice shall be included in
11 // all copies or substantial portions of the Software.
12 //
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
19 // IN THE SOFTWARE.
20
21 #include "Scraper.h"
22
23 #include <windows.h>
24
25 #include <stdint.h>
26
27 #include <algorithm>
28 #include <utility>
29
30 #include "../shared/WinptyAssert.h"
31 #include "../shared/winpty_snprintf.h"
32
33 #include "ConsoleFont.h"
34 #include "Win32Console.h"
35 #include "Win32ConsoleBuffer.h"
36
37 namespace {
38
39 template <typename T>
40 T constrained(T min, T val, T max) {
41     ASSERT(min <= max);
42     return std::min(std::max(min, val), max);
43 }
44
45 } // anonymous namespace
46
47 Scraper::Scraper(
48         Win32Console &console,
49         Win32ConsoleBuffer &buffer,
50         std::unique_ptr<Terminal> terminal,
51         Coord initialSize) :
52     m_console(console),
53     m_terminal(std::move(terminal)),
54     m_ptySize(initialSize)
55 {
56     m_consoleBuffer = &buffer;
57
58     resetConsoleTracking(Terminal::OmitClear, buffer.windowRect().top());
59
60     m_bufferData.resize(BUFFER_LINE_COUNT);
61
62     // Setup the initial screen buffer and window size.
63     //
64     // Use SetConsoleWindowInfo to shrink the console window as much as
65     // possible -- to a 1x1 cell at the top-left.  This call always succeeds.
66     // Prior to the new Windows 10 console, it also actually resizes the GUI
67     // window to 1x1 cell.  Nevertheless, even though the GUI window can
68     // therefore be narrower than its minimum, calling
69     // SetConsoleScreenBufferSize with a 1x1 size still fails.
70     //
71     // While the small font intends to support large buffers, a user could
72     // still hit a limit imposed by their monitor width, so cap the new window
73     // size to GetLargestConsoleWindowSize().
74     setSmallFont(buffer.conout(), initialSize.X, m_console.isNewW10());
75     buffer.moveWindow(SmallRect(0, 0, 1, 1));
76     buffer.resizeBufferRange(Coord(initialSize.X, BUFFER_LINE_COUNT));
77     const auto largest = GetLargestConsoleWindowSize(buffer.conout());
78     buffer.moveWindow(SmallRect(
79         0, 0,
80         std::min(initialSize.X, largest.X),
81         std::min(initialSize.Y, largest.Y)));
82     buffer.setCursorPosition(Coord(0, 0));
83
84     // For the sake of the color translation heuristic, set the console color
85     // to LtGray-on-Black.
86     buffer.setTextAttribute(Win32ConsoleBuffer::kDefaultAttributes);
87     buffer.clearAllLines(m_consoleBuffer->bufferInfo());
88
89     m_consoleBuffer = nullptr;
90 }
91
92 Scraper::~Scraper()
93 {
94 }
95
96 // Whether or not the agent is frozen on entry, it will be frozen on exit.
97 void Scraper::resizeWindow(Win32ConsoleBuffer &buffer,
98                            Coord newSize,
99                            ConsoleScreenBufferInfo &finalInfoOut)
100 {
101     m_consoleBuffer = &buffer;
102     m_ptySize = newSize;
103     syncConsoleContentAndSize(true, finalInfoOut);
104     m_consoleBuffer = nullptr;
105 }
106
107 // This function may freeze the agent, but it will not unfreeze it.
108 void Scraper::scrapeBuffer(Win32ConsoleBuffer &buffer,
109                            ConsoleScreenBufferInfo &finalInfoOut)
110 {
111     m_consoleBuffer = &buffer;
112     syncConsoleContentAndSize(false, finalInfoOut);
113     m_consoleBuffer = nullptr;
114 }
115
116 void Scraper::resetConsoleTracking(
117     Terminal::SendClearFlag sendClear, int64_t scrapedLineCount)
118 {
119     for (ConsoleLine &line : m_bufferData) {
120         line.reset();
121     }
122     m_syncRow = -1;
123     m_scrapedLineCount = scrapedLineCount;
124     m_scrolledCount = 0;
125     m_maxBufferedLine = -1;
126     m_dirtyWindowTop = -1;
127     m_dirtyLineCount = 0;
128     m_terminal->reset(sendClear, m_scrapedLineCount);
129 }
130
131 // Detect window movement.  If the window moves down (presumably as a
132 // result of scrolling), then assume that all screen buffer lines down to
133 // the bottom of the window are dirty.
134 void Scraper::markEntireWindowDirty(const SmallRect &windowRect)
135 {
136     m_dirtyLineCount = std::max(m_dirtyLineCount,
137                                 windowRect.top() + windowRect.height());
138 }
139
140 // Scan the screen buffer and advance the dirty line count when we find
141 // non-empty lines.
142 void Scraper::scanForDirtyLines(const SmallRect &windowRect)
143 {
144     const int w = m_readBuffer.rect().width();
145     ASSERT(m_dirtyLineCount >= 1);
146     const CHAR_INFO *const prevLine =
147         m_readBuffer.lineData(m_dirtyLineCount - 1);
148     WORD prevLineAttr = prevLine[w - 1].Attributes;
149     const int stopLine = windowRect.top() + windowRect.height();
150
151     for (int line = m_dirtyLineCount; line < stopLine; ++line) {
152         const CHAR_INFO *lineData = m_readBuffer.lineData(line);
153         for (int col = 0; col < w; ++col) {
154             const WORD colAttr = lineData[col].Attributes;
155             if (lineData[col].Char.UnicodeChar != L' ' ||
156                     colAttr != prevLineAttr) {
157                 m_dirtyLineCount = line + 1;
158                 break;
159             }
160         }
161         prevLineAttr = lineData[w - 1].Attributes;
162     }
163 }
164
165 // Clear lines in the line buffer.  The `firstRow` parameter is in
166 // screen-buffer coordinates.
167 void Scraper::clearBufferLines(
168         const int firstRow,
169         const int count)
170 {
171     ASSERT(!m_directMode);
172     for (int row = firstRow; row < firstRow + count; ++row) {
173         const int64_t bufLine = row + m_scrolledCount;
174         m_maxBufferedLine = std::max(m_maxBufferedLine, bufLine);
175         m_bufferData[bufLine % BUFFER_LINE_COUNT].blank(
176             Win32ConsoleBuffer::kDefaultAttributes);
177     }
178 }
179
180 static bool cursorInWindow(const ConsoleScreenBufferInfo &info)
181 {
182     return info.dwCursorPosition.Y >= info.srWindow.Top &&
183            info.dwCursorPosition.Y <= info.srWindow.Bottom;
184 }
185
186 void Scraper::resizeImpl(const ConsoleScreenBufferInfo &origInfo)
187 {
188     ASSERT(m_console.frozen());
189     const int cols = m_ptySize.X;
190     const int rows = m_ptySize.Y;
191     Coord finalBufferSize;
192
193     {
194         //
195         // To accommodate Windows 10, erase all lines up to the top of the
196         // visible window.  It's hard to tell whether this is strictly
197         // necessary.  It ensures that the sync marker won't move downward,
198         // and it ensures that we won't repeat lines that have already scrolled
199         // up into the scrollback.
200         //
201         // It *is* possible for these blank lines to reappear in the visible
202         // window (e.g. if the window is made taller), but because we blanked
203         // the lines in the line buffer, we still don't output them again.
204         //
205         const Coord origBufferSize = origInfo.bufferSize();
206         const SmallRect origWindowRect = origInfo.windowRect();
207
208         if (m_directMode) {
209             for (ConsoleLine &line : m_bufferData) {
210                 line.reset();
211             }
212         } else {
213             m_consoleBuffer->clearLines(0, origWindowRect.Top, origInfo);
214             clearBufferLines(0, origWindowRect.Top);
215             if (m_syncRow != -1) {
216                 createSyncMarker(std::min(
217                     m_syncRow,
218                     BUFFER_LINE_COUNT - rows
219                                       - SYNC_MARKER_LEN
220                                       - SYNC_MARKER_MARGIN));
221             }
222         }
223
224         finalBufferSize = Coord(
225             cols,
226             // If there was previously no scrollback (e.g. a full-screen app
227             // in direct mode) and we're reducing the window height, then
228             // reduce the console buffer's height too.
229             (origWindowRect.height() == origBufferSize.Y)
230                 ? rows
231                 : std::max<int>(rows, origBufferSize.Y));
232
233         // Reset the console font size.  We need to do this before shrinking
234         // the window, because we might need to make the font bigger to permit
235         // a smaller window width.  Making the font smaller could expand the
236         // screen buffer, which would hang the conhost process in the
237         // Windows 10 (10240 build) if the console selection is in progress, so
238         // unfreeze it first.
239         m_console.setFrozen(false);
240         setSmallFont(m_consoleBuffer->conout(), cols, m_console.isNewW10());
241     }
242
243     // We try to make the font small enough so that the entire screen buffer
244     // fits on the monitor, but it can't be guaranteed.
245     const auto largest =
246         GetLargestConsoleWindowSize(m_consoleBuffer->conout());
247     const short visibleCols = std::min<short>(cols, largest.X);
248     const short visibleRows = std::min<short>(rows, largest.Y);
249
250     {
251         // Make the window small enough.  We want the console frozen during
252         // this step so we don't accidentally move the window above the cursor.
253         m_console.setFrozen(true);
254         const auto info = m_consoleBuffer->bufferInfo();
255         const auto &bufferSize = info.dwSize;
256         const int tmpWindowWidth = std::min(bufferSize.X, visibleCols);
257         const int tmpWindowHeight = std::min(bufferSize.Y, visibleRows);
258         SmallRect tmpWindowRect(
259             0,
260             std::min<int>(bufferSize.Y - tmpWindowHeight,
261                           info.windowRect().Top),
262             tmpWindowWidth,
263             tmpWindowHeight);
264         if (cursorInWindow(info)) {
265             tmpWindowRect = tmpWindowRect.ensureLineIncluded(
266                 info.cursorPosition().Y);
267         }
268         m_consoleBuffer->moveWindow(tmpWindowRect);
269     }
270
271     {
272         // Resize the buffer to the final desired size.
273         m_console.setFrozen(false);
274         m_consoleBuffer->resizeBufferRange(finalBufferSize);
275     }
276
277     {
278         // Expand the window to its full size.
279         m_console.setFrozen(true);
280         const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
281
282         SmallRect finalWindowRect(
283             0,
284             std::min<int>(info.bufferSize().Y - visibleRows,
285                           info.windowRect().Top),
286             visibleCols,
287             visibleRows);
288
289         //
290         // Once a line in the screen buffer is "dirty", it should stay visible
291         // in the console window, so that we continue to update its content in
292         // the terminal.  This code is particularly (only?) necessary on
293         // Windows 10, where making the buffer wider can rewrap lines and move
294         // the console window upward.
295         //
296         if (!m_directMode && m_dirtyLineCount > finalWindowRect.Bottom + 1) {
297             // In theory, we avoid ensureLineIncluded, because, a massive
298             // amount of output could have occurred while the console was
299             // unfrozen, so that the *top* of the window is now below the
300             // dirtiest tracked line.
301             finalWindowRect = SmallRect(
302                 0, m_dirtyLineCount - visibleRows,
303                 visibleCols, visibleRows);
304         }
305
306         // Highest priority constraint: ensure that the cursor remains visible.
307         if (cursorInWindow(info)) {
308             finalWindowRect = finalWindowRect.ensureLineIncluded(
309                 info.cursorPosition().Y);
310         }
311
312         m_consoleBuffer->moveWindow(finalWindowRect);
313         m_dirtyWindowTop = finalWindowRect.Top;
314     }
315
316     ASSERT(m_console.frozen());
317 }
318
319 void Scraper::syncConsoleContentAndSize(
320     bool forceResize,
321     ConsoleScreenBufferInfo &finalInfoOut)
322 {
323     // We'll try to avoid freezing the console by reading large chunks (or
324     // all!) of the screen buffer without otherwise attempting to synchronize
325     // with the console application.  We can only do this on Windows 10 and up
326     // because:
327     //  - Prior to Windows 8, the size of a ReadConsoleOutputW call was limited
328     //    by the ~32KB RPC buffer.
329     //  - Prior to Windows 10, an out-of-range read region crashes the caller.
330     //    (See misc/WindowsBugCrashReader.cc.)
331     //
332     if (!m_console.isNewW10() || forceResize) {
333         m_console.setFrozen(true);
334     }
335
336     const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
337     bool cursorVisible = true;
338     CONSOLE_CURSOR_INFO cursorInfo = {};
339     if (!GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo)) {
340         trace("GetConsoleCursorInfo failed");
341     } else {
342         cursorVisible = cursorInfo.bVisible != 0;
343     }
344
345     // If an app resizes the buffer height, then we enter "direct mode", where
346     // we stop trying to track incremental console changes.
347     const bool newDirectMode = (info.bufferSize().Y != BUFFER_LINE_COUNT);
348     if (newDirectMode != m_directMode) {
349         trace("Entering %s mode", newDirectMode ? "direct" : "scrolling");
350         resetConsoleTracking(Terminal::SendClear,
351                              newDirectMode ? 0 : info.windowRect().top());
352         m_directMode = newDirectMode;
353
354         // When we switch from direct->scrolling mode, make sure the console is
355         // the right size.
356         if (!m_directMode) {
357             m_console.setFrozen(true);
358             forceResize = true;
359         }
360     }
361
362     if (m_directMode) {
363         // In direct-mode, resizing the console redraws the terminal, so do it
364         // before scraping.
365         if (forceResize) {
366             resizeImpl(info);
367         }
368         directScrapeOutput(info, cursorVisible);
369     } else {
370         if (!m_console.frozen()) {
371             if (!scrollingScrapeOutput(info, cursorVisible, true)) {
372                 m_console.setFrozen(true);
373             }
374         }
375         if (m_console.frozen()) {
376             scrollingScrapeOutput(info, cursorVisible, false);
377         }
378         // In scrolling mode, we want to scrape before resizing, because we'll
379         // erase everything in the console buffer up to the top of the console
380         // window.
381         if (forceResize) {
382             resizeImpl(info);
383         }
384     }
385
386     finalInfoOut = forceResize ? m_consoleBuffer->bufferInfo() : info;
387 }
388
389 // Try to match Windows' behavior w.r.t. to the LVB attribute flags.  In some
390 // situations, Windows ignores the LVB flags on a character cell because of
391 // backwards compatibility -- apparently some programs set the flags without
392 // intending to enable reverse-video or underscores.
393 //
394 // [rprichard 2017-01-15] I haven't actually noticed any old programs that need
395 // this treatment -- the motivation for this function comes from the MSDN
396 // documentation for SetConsoleMode and ENABLE_LVB_GRID_WORLDWIDE.
397 WORD Scraper::attributesMask()
398 {
399     const auto WINPTY_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4u;
400     const auto WINPTY_ENABLE_LVB_GRID_WORLDWIDE          = 0x10u;
401     const auto WINPTY_COMMON_LVB_REVERSE_VIDEO           = 0x4000u;
402     const auto WINPTY_COMMON_LVB_UNDERSCORE              = 0x8000u;
403
404     const auto cp = GetConsoleOutputCP();
405     const auto isCjk = (cp == 932 || cp == 936 || cp == 949 || cp == 950);
406
407     const DWORD outputMode = [this]{
408         ASSERT(this->m_consoleBuffer != nullptr);
409         DWORD mode = 0;
410         if (!GetConsoleMode(this->m_consoleBuffer->conout(), &mode)) {
411             mode = 0;
412         }
413         return mode;
414     }();
415     const bool hasEnableLvbGridWorldwide =
416         (outputMode & WINPTY_ENABLE_LVB_GRID_WORLDWIDE) != 0;
417     const bool hasEnableVtProcessing =
418         (outputMode & WINPTY_ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
419
420     // The new Windows 10 console (as of 14393) seems to respect
421     // COMMON_LVB_REVERSE_VIDEO even in CP437 w/o the other enabling modes, so
422     // try to match that behavior.
423     const auto isReverseSupported =
424         isCjk || hasEnableLvbGridWorldwide || hasEnableVtProcessing || m_console.isNewW10();
425     const auto isUnderscoreSupported =
426         isCjk || hasEnableLvbGridWorldwide || hasEnableVtProcessing;
427
428     WORD mask = ~0;
429     if (!isReverseSupported)    { mask &= ~WINPTY_COMMON_LVB_REVERSE_VIDEO; }
430     if (!isUnderscoreSupported) { mask &= ~WINPTY_COMMON_LVB_UNDERSCORE; }
431     return mask;
432 }
433
434 void Scraper::directScrapeOutput(const ConsoleScreenBufferInfo &info,
435                                  bool consoleCursorVisible)
436 {
437     const SmallRect windowRect = info.windowRect();
438
439     const SmallRect scrapeRect(
440         windowRect.left(), windowRect.top(),
441         std::min<SHORT>(std::min(windowRect.width(), m_ptySize.X),
442                         MAX_CONSOLE_WIDTH),
443         std::min<SHORT>(std::min(windowRect.height(), m_ptySize.Y),
444                         BUFFER_LINE_COUNT));
445     const int w = scrapeRect.width();
446     const int h = scrapeRect.height();
447
448     const Coord cursor = info.cursorPosition();
449     const bool showTerminalCursor =
450         consoleCursorVisible && scrapeRect.contains(cursor);
451     const int cursorColumn = !showTerminalCursor ? -1 : cursor.X - scrapeRect.Left;
452     const int cursorLine = !showTerminalCursor ? -1 : cursor.Y - scrapeRect.Top;
453
454     if (!showTerminalCursor) {
455         m_terminal->hideTerminalCursor();
456     }
457
458     largeConsoleRead(m_readBuffer, *m_consoleBuffer, scrapeRect, attributesMask());
459
460     for (int line = 0; line < h; ++line) {
461         const CHAR_INFO *const curLine =
462             m_readBuffer.lineData(scrapeRect.top() + line);
463         ConsoleLine &bufLine = m_bufferData[line];
464         if (bufLine.detectChangeAndSetLine(curLine, w)) {
465             const int lineCursorColumn =
466                 line == cursorLine ? cursorColumn : -1;
467             m_terminal->sendLine(line, curLine, w, lineCursorColumn);
468         }
469     }
470
471     if (showTerminalCursor) {
472         m_terminal->showTerminalCursor(cursorColumn, cursorLine);
473     }
474 }
475
476 bool Scraper::scrollingScrapeOutput(const ConsoleScreenBufferInfo &info,
477                                     bool consoleCursorVisible,
478                                     bool tentative)
479 {
480     const Coord cursor = info.cursorPosition();
481     const SmallRect windowRect = info.windowRect();
482
483     if (m_syncRow != -1) {
484         // If a synchronizing marker was placed into the history, look for it
485         // and adjust the scroll count.
486         const int markerRow = findSyncMarker();
487         if (markerRow == -1) {
488             if (tentative) {
489                 // I *think* it's possible to keep going, but it's simple to
490                 // bail out.
491                 return false;
492             }
493             // Something has happened.  Reset the terminal.
494             trace("Sync marker has disappeared -- resetting the terminal"
495                   " (m_syncCounter=%u)",
496                   m_syncCounter);
497             resetConsoleTracking(Terminal::SendClear, windowRect.top());
498         } else if (markerRow != m_syncRow) {
499             ASSERT(markerRow < m_syncRow);
500             m_scrolledCount += (m_syncRow - markerRow);
501             m_syncRow = markerRow;
502             // If the buffer has scrolled, then the entire window is dirty.
503             markEntireWindowDirty(windowRect);
504         }
505     }
506
507     // Creating a new sync row requires clearing part of the console buffer, so
508     // avoid doing it if there's already a sync row that's good enough.
509     const int newSyncRow =
510         static_cast<int>(windowRect.top()) - SYNC_MARKER_LEN - SYNC_MARKER_MARGIN;
511     const bool shouldCreateSyncRow =
512         newSyncRow >= m_syncRow + SYNC_MARKER_LEN + SYNC_MARKER_MARGIN;
513     if (tentative && shouldCreateSyncRow) {
514         // It's difficult even in principle to put down a new marker if the
515         // console can scroll an arbitrarily amount while we're writing.
516         return false;
517     }
518
519     // Update the dirty line count:
520     //  - If the window has moved, the entire window is dirty.
521     //  - Everything up to the cursor is dirty.
522     //  - All lines above the window are dirty.
523     //  - Any non-blank lines are dirty.
524     if (m_dirtyWindowTop != -1) {
525         if (windowRect.top() > m_dirtyWindowTop) {
526             // The window has moved down, presumably as a result of scrolling.
527             markEntireWindowDirty(windowRect);
528         } else if (windowRect.top() < m_dirtyWindowTop) {
529             if (tentative) {
530                 // I *think* it's possible to keep going, but it's simple to
531                 // bail out.
532                 return false;
533             }
534             // The window has moved upward.  This is generally not expected to
535             // happen, but the CMD/PowerShell CLS command will move the window
536             // to the top as part of clearing everything else in the console.
537             trace("Window moved upward -- resetting the terminal"
538                   " (m_syncCounter=%u)",
539                   m_syncCounter);
540             resetConsoleTracking(Terminal::SendClear, windowRect.top());
541         }
542     }
543     m_dirtyWindowTop = windowRect.top();
544     m_dirtyLineCount = std::max(m_dirtyLineCount, cursor.Y + 1);
545     m_dirtyLineCount = std::max(m_dirtyLineCount, (int)windowRect.top());
546
547     // There will be at least one dirty line, because there is a cursor.
548     ASSERT(m_dirtyLineCount >= 1);
549
550     // The first line to scrape, in virtual line coordinates.
551     const int64_t firstVirtLine = std::min(m_scrapedLineCount,
552                                            windowRect.top() + m_scrolledCount);
553
554     // Read all the data we will need from the console.  Start reading with the
555     // first line to scrape, but adjust the the read area upward to account for
556     // scanForDirtyLines' need to read the previous attribute.  Read to the
557     // bottom of the window.  (It's not clear to me whether the
558     // m_dirtyLineCount adjustment here is strictly necessary.  It isn't
559     // necessary so long as the cursor is inside the current window.)
560     const int firstReadLine = std::min<int>(firstVirtLine - m_scrolledCount,
561                                             m_dirtyLineCount - 1);
562     const int stopReadLine = std::max(windowRect.top() + windowRect.height(),
563                                       m_dirtyLineCount);
564     ASSERT(firstReadLine >= 0 && stopReadLine > firstReadLine);
565     largeConsoleRead(m_readBuffer,
566                      *m_consoleBuffer,
567                      SmallRect(0, firstReadLine,
568                                std::min<SHORT>(info.bufferSize().X,
569                                                MAX_CONSOLE_WIDTH),
570                                stopReadLine - firstReadLine),
571                      attributesMask());
572
573     // If we're scraping the buffer without freezing it, we have to query the
574     // buffer position data separately from the buffer content, so the two
575     // could easily be out-of-sync.  If they *are* out-of-sync, abort the
576     // scrape operation and restart it frozen.  (We may have updated the
577     // dirty-line high-water-mark, but that should be OK.)
578     if (tentative) {
579         const auto infoCheck = m_consoleBuffer->bufferInfo();
580         if (info.bufferSize() != infoCheck.bufferSize() ||
581                 info.windowRect() != infoCheck.windowRect() ||
582                 info.cursorPosition() != infoCheck.cursorPosition()) {
583             return false;
584         }
585         if (m_syncRow != -1 && m_syncRow != findSyncMarker()) {
586             return false;
587         }
588     }
589
590     if (shouldCreateSyncRow) {
591         ASSERT(!tentative);
592         createSyncMarker(newSyncRow);
593     }
594
595     // At this point, we're finished interacting (reading or writing) the
596     // console, and we just need to convert our collected data into terminal
597     // output.
598
599     scanForDirtyLines(windowRect);
600
601     // Note that it's possible for all the lines on the current window to
602     // be non-dirty.
603
604     // The line to stop scraping at, in virtual line coordinates.
605     const int64_t stopVirtLine =
606         std::min(m_dirtyLineCount, windowRect.top() + windowRect.height()) +
607             m_scrolledCount;
608
609     const bool showTerminalCursor =
610         consoleCursorVisible && windowRect.contains(cursor);
611     const int64_t cursorLine = !showTerminalCursor ? -1 : cursor.Y + m_scrolledCount;
612     const int cursorColumn = !showTerminalCursor ? -1 : cursor.X;
613
614     if (!showTerminalCursor) {
615         m_terminal->hideTerminalCursor();
616     }
617
618     bool sawModifiedLine = false;
619
620     const int w = m_readBuffer.rect().width();
621     for (int64_t line = firstVirtLine; line < stopVirtLine; ++line) {
622         const CHAR_INFO *curLine =
623             m_readBuffer.lineData(line - m_scrolledCount);
624         ConsoleLine &bufLine = m_bufferData[line % BUFFER_LINE_COUNT];
625         if (line > m_maxBufferedLine) {
626             m_maxBufferedLine = line;
627             sawModifiedLine = true;
628         }
629         if (sawModifiedLine) {
630             bufLine.setLine(curLine, w);
631         } else {
632             sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w);
633         }
634         if (sawModifiedLine) {
635             const int lineCursorColumn =
636                 line == cursorLine ? cursorColumn : -1;
637             m_terminal->sendLine(line, curLine, w, lineCursorColumn);
638         }
639     }
640
641     m_scrapedLineCount = windowRect.top() + m_scrolledCount;
642
643     if (showTerminalCursor) {
644         m_terminal->showTerminalCursor(cursorColumn, cursorLine);
645     }
646
647     return true;
648 }
649
650 void Scraper::syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN])
651 {
652     // XXX: The marker text generated here could easily collide with ordinary
653     // console output.  Does it make sense to try to avoid the collision?
654     char str[SYNC_MARKER_LEN + 1];
655     winpty_snprintf(str, "S*Y*N*C*%08x", m_syncCounter);
656     for (int i = 0; i < SYNC_MARKER_LEN; ++i) {
657         output[i].Char.UnicodeChar = str[i];
658         output[i].Attributes = 7;
659     }
660 }
661
662 int Scraper::findSyncMarker()
663 {
664     ASSERT(m_syncRow >= 0);
665     CHAR_INFO marker[SYNC_MARKER_LEN];
666     CHAR_INFO column[BUFFER_LINE_COUNT];
667     syncMarkerText(marker);
668     SmallRect rect(0, 0, 1, m_syncRow + SYNC_MARKER_LEN);
669     m_consoleBuffer->read(rect, column);
670     int i;
671     for (i = m_syncRow; i >= 0; --i) {
672         int j;
673         for (j = 0; j < SYNC_MARKER_LEN; ++j) {
674             if (column[i + j].Char.UnicodeChar != marker[j].Char.UnicodeChar)
675                 break;
676         }
677         if (j == SYNC_MARKER_LEN)
678             return i;
679     }
680     return -1;
681 }
682
683 void Scraper::createSyncMarker(int row)
684 {
685     ASSERT(row >= 1);
686
687     // Clear the lines around the marker to ensure that Windows 10's rewrapping
688     // does not affect the marker.
689     m_consoleBuffer->clearLines(row - 1, SYNC_MARKER_LEN + 1,
690                                 m_consoleBuffer->bufferInfo());
691
692     // Write a new marker.
693     m_syncCounter++;
694     CHAR_INFO marker[SYNC_MARKER_LEN];
695     syncMarkerText(marker);
696     m_syncRow = row;
697     SmallRect markerRect(0, m_syncRow, 1, SYNC_MARKER_LEN);
698     m_consoleBuffer->write(markerRect, marker);
699 }