terminal/src/host/ConsoleArguments.cpp
Mike Griese 93b31f6e3f
Add support for "reflow"ing the Terminal buffer (#4741)
This PR adds support for "Resize with Reflow" to the Terminal. In
conhost, `ResizeWithReflow` is the function that's responsible for
reflowing wrapped lines of text as the buffer gets resized. Now that
#4415 has merged, we can also implement this in the Terminal. Now, when
the Terminal is resized, it will reflow the lines of it's buffer in the
same way that conhost does. This means, the terminal will no longer chop
off the ends of lines as the buffer is too small to represent them. 

As a happy side effect of this PR, it also fixed #3490. This was a bug
that plagued me during the investigation into this functionality. The
original #3490 PR, #4354, tried to fix this bug with some heavy conpty
changes. Turns out, that only made things worse, and far more
complicated. When I really got to thinking about it, I realized "conhost
can handle this right, why can't the Terminal?". Turns out, by adding
resize with reflow, I was also able to fix this at the same time.
Conhost does a little bit of math after reflowing to attempt to keep the
viewport in the same relative place after a reflow. By re-using that
logic in the Terminal, I was able to fix #3490.

I also included that big ole test from #3490, because everyone likes
adding 60 test cases in a PR.

## References
* #4200 - this scenario
* #405/#4415 - conpty emits wrapped lines, which was needed for this PR
* #4403 - delayed EOL wrapping via conpty, which was also needed for
  this
* #4354 - we don't speak of this PR anymore

## PR Checklist
* [x] Closes #1465
* [x] Closes #3490
* [x] Closes #4771
* [x] Tests added/passed

## EDIT: Changes to this PR on 5 March 2020

I learned more since my original version of this PR. I wrote that in
January, and despite my notes that say it was totally working, it
_really_ wasn't.

Part of the hard problem, as mentioned in #3490, is that the Terminal
might request a resize to (W, H-1), and while conpty is preparing that
frame, or before the terminal has received that frame, the Terminal
resizes to (W, H-2). Now, there aren't enough lines in the terminal
buffer to catch all the lines that conpty is about to emit. When that
happens, lines get duplicated in the buffer. From a UX perspective, this
certainly looks a lot worse than a couple lost lines. It looks like
utter chaos.

So I've introduced a new mode to conpty to try and counteract this
behavior. This behavior I'm calling "quirky resize". The **TL;DR** of
quirky resize mode is that conpty won't emit the entire buffer on a
resize, and will trust that the terminal is prepared to reflow it's
buffer on it's own.

This will enable the quirky resize behavior for applications that are
prepared for it. The "quirky resize" is "don't `InvalidateAll` when the
terminal resizes". This is added as a quirk as to not regress other
terminal applications that aren't prepared for this behavior
(gnome-terminal, conhost in particular). For those kinds of terminals,
when the buffer is resized, it's just going to lose lines. That's what
currently happens for them.  

When the quirk is enabled, conpty won't repaint the entire buffer. This
gets around the "duplicated lines" issue that requesting multiple
resizes in a row can cause. However, for these terminals that are
unprepared, the conpty cursor might end up in the wrong position after a
quirky resize.

The case in point is maximizing the terminal. For maximizing
(height->50) from a buffer that's 30 lines tall, with the cursor on
y=30, this is what happens: 

  * With the quirk disabled, conpty reprints the entire buffer. This is
    60 lines that get printed. This ends up blowing away about 20 lines
    of scrollback history, as the terminal app would have tried to keep
    the text pinned to the bottom of the window. The term. app moved the
    viewport up 20 lines, and then the 50 lines of conpty output (30
    lines of text, and 20 blank lines at the bottom) overwrote the lines
    from the scrollback. This is bad, but not immediately obvious, and
    is **what currently happens**. 


  * With the quirk enabled, conpty doesn't emit any lines, but the
    actual content of the window is still only in the top 30 lines.
    However, the terminal app has still moved 20 lines down from the
    scrollback back into the viewport. So the terminal's cursor is at
    y=50 now, but conpty's is at 30. This means that the terminal and
    conpty are out of sync, and there's not a good way of re-syncing
    these. It's very possible (trivial in `powershell`) that the new
    output will jump up to y=30 override the existing output in the
    terminal buffer. 

The Windows Terminal is already prepared for this quirky behavior, so it
doesn't keep the output at the bottom of the window. It shifts it's
viewport down to match what conpty things the buffer looks like.

What happens when we have passthrough mode and WT is like "I would like
quirky resize"? I guess things will just work fine, cause there won't be
a buffer behind the passthrough app that the terminal cares about. Sure,
in the passthrough case the Terminal could _not_ quirky resize, but the
quirky resize won't be wrong.
2020-03-12 17:43:37 -07:00

666 lines
21 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "ConsoleArguments.hpp"
#include "../types/inc/utils.hpp"
#include <shellapi.h>
using namespace Microsoft::Console::Utils;
const std::wstring_view ConsoleArguments::VT_MODE_ARG = L"--vtmode";
const std::wstring_view ConsoleArguments::HEADLESS_ARG = L"--headless";
const std::wstring_view ConsoleArguments::SERVER_HANDLE_ARG = L"--server";
const std::wstring_view ConsoleArguments::SIGNAL_HANDLE_ARG = L"--signal";
const std::wstring_view ConsoleArguments::HANDLE_PREFIX = L"0x";
const std::wstring_view ConsoleArguments::CLIENT_COMMANDLINE_ARG = L"--";
const std::wstring_view ConsoleArguments::FORCE_V1_ARG = L"-ForceV1";
const std::wstring_view ConsoleArguments::FILEPATH_LEADER_PREFIX = L"\\??\\";
const std::wstring_view ConsoleArguments::WIDTH_ARG = L"--width";
const std::wstring_view ConsoleArguments::HEIGHT_ARG = L"--height";
const std::wstring_view ConsoleArguments::INHERIT_CURSOR_ARG = L"--inheritcursor";
const std::wstring_view ConsoleArguments::RESIZE_QUIRK = L"--resizeQuirk";
const std::wstring_view ConsoleArguments::FEATURE_ARG = L"--feature";
const std::wstring_view ConsoleArguments::FEATURE_PTY_ARG = L"pty";
std::wstring EscapeArgument(std::wstring_view ac)
{
if (ac.empty())
{
return L"\"\"";
}
bool hasSpace = false;
auto n = ac.size();
for (auto c : ac)
{
switch (c)
{
case L'"':
case L'\\':
n++;
break;
case ' ':
case '\t':
hasSpace = true;
break;
default:
break;
}
}
if (hasSpace)
{
n += 2;
}
if (n == ac.size())
{
return std::wstring{ ac };
}
std::wstring buf;
if (hasSpace)
{
buf.push_back(L'"');
}
size_t slashes = 0;
for (auto c : ac)
{
switch (c)
{
case L'\\':
slashes++;
buf.push_back(L'\\');
break;
case L'"':
{
for (; slashes > 0; slashes--)
{
buf.push_back(L'\\');
}
buf.push_back(L'\\');
buf.push_back(c);
}
break;
default:
slashes = 0;
buf.push_back(c);
break;
}
}
if (hasSpace)
{
for (; slashes > 0; slashes--)
{
buf.push_back(L'\\');
}
buf.push_back(L'"');
}
return buf;
}
ConsoleArguments::ConsoleArguments(const std::wstring& commandline,
const HANDLE hStdIn,
const HANDLE hStdOut) :
_commandline(commandline),
_vtInHandle(hStdIn),
_vtOutHandle(hStdOut),
_receivedEarlySizeChange{ false },
_originalWidth{ -1 },
_originalHeight{ -1 }
{
_clientCommandline = L"";
_vtMode = L"";
_headless = false;
_createServerHandle = true;
_serverHandle = 0;
_signalHandle = 0;
_forceV1 = false;
_width = 0;
_height = 0;
_inheritCursor = false;
}
ConsoleArguments::ConsoleArguments() :
ConsoleArguments(L"", nullptr, nullptr)
{
}
ConsoleArguments& ConsoleArguments::operator=(const ConsoleArguments& other)
{
if (this != &other)
{
_commandline = other._commandline;
_clientCommandline = other._clientCommandline;
_vtInHandle = other._vtInHandle;
_vtOutHandle = other._vtOutHandle;
_vtMode = other._vtMode;
_headless = other._headless;
_createServerHandle = other._createServerHandle;
_serverHandle = other._serverHandle;
_signalHandle = other._signalHandle;
_forceV1 = other._forceV1;
_width = other._width;
_height = other._height;
_inheritCursor = other._inheritCursor;
_receivedEarlySizeChange = other._receivedEarlySizeChange;
}
return *this;
}
// Routine Description:
// - Consumes the argument at the given index off of the vector.
// Arguments:
// - args - The vector full of args
// - index - The item to consume/remove from the vector.
// Return Value:
// - <none>
void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector<std::wstring>& args, _In_ size_t& index)
{
args.erase(args.begin() + index);
}
// Routine Description:
// Given the commandline of tokens `args`, tries to find the argument at
// index+1, and places its value into pSetting.
// If there aren't enough args, then returns E_INVALIDARG.
// If we found a value, then we take the elements at both index and index+1 out
// of args. We'll also decrement index, so that a caller who is using index
// as a loop index will autoincrement it to have it point at the correct
// next index.
//
// EX: for args=[--foo, bar, --baz]
// index=0 would place "bar" in pSetting,
// args is now [--baz], index is now -1, caller increments to 0
// index=2 would return E_INVALIDARG,
// args is still [--foo, bar, --baz], index is still 2, caller increments to 3.
// Arguments:
// args: A collection of wstrings representing command-line arguments
// index: the index of the argument of which to get the value for. The value
// should be at (index+1). index will be decremented by one on success.
// pSetting: receives the string at index+1
// Return Value:
// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating
// failure.
[[nodiscard]] HRESULT ConsoleArguments::s_GetArgumentValue(_Inout_ std::vector<std::wstring>& args,
_Inout_ size_t& index,
_Out_opt_ std::wstring* const pSetting)
{
bool hasNext = (index + 1) < args.size();
if (hasNext)
{
s_ConsumeArg(args, index);
if (pSetting != nullptr)
{
*pSetting = args[index];
}
s_ConsumeArg(args, index);
}
return (hasNext) ? S_OK : E_INVALIDARG;
}
// Routine Description:
// Similar to s_GetArgumentValue.
// Attempts to get the next arg as a "feature" arg - this can be used for
// feature detection.
// If the next arg is not recognized, then we don't support that feature.
// Currently, the only supported feature arg is `pty`, to identify pty support.
// Arguments:
// args: A collection of wstrings representing command-line arguments
// index: the index of the argument of which to get the value for. The value
// should be at (index+1). index will be decremented by one on success.
// pSetting: receives the string at index+1
// Return Value:
// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating
// failure.
[[nodiscard]] HRESULT ConsoleArguments::s_HandleFeatureValue(_Inout_ std::vector<std::wstring>& args, _Inout_ size_t& index)
{
HRESULT hr = E_INVALIDARG;
bool hasNext = (index + 1) < args.size();
if (hasNext)
{
s_ConsumeArg(args, index);
std::wstring value = args[index];
if (value == FEATURE_PTY_ARG)
{
hr = S_OK;
}
s_ConsumeArg(args, index);
}
return (hasNext) ? hr : E_INVALIDARG;
}
// Method Description:
// Routine Description:
// Given the commandline of tokens `args`, tries to find the argument at
// index+1, and places its value into pSetting. See above for examples.
// This implementation attempts to parse a short from the argument.
// Arguments:
// args: A collection of wstrings representing command-line arguments
// index: the index of the argument of which to get the value for. The value
// should be at (index+1). index will be decremented by one on success.
// pSetting: receives the short at index+1
// Return Value:
// S_OK if we parsed the short successfully, otherwise E_INVALIDARG indicating
// failure. This could be the case for non-numeric arguments, or for >SHORT_MAX args.
[[nodiscard]] HRESULT ConsoleArguments::s_GetArgumentValue(_Inout_ std::vector<std::wstring>& args,
_Inout_ size_t& index,
_Out_opt_ short* const pSetting)
{
bool succeeded = (index + 1) < args.size();
if (succeeded)
{
s_ConsumeArg(args, index);
if (pSetting != nullptr)
{
try
{
size_t pos = 0;
int value = std::stoi(args[index], &pos);
// If the entire string was a number, pos will be equal to the
// length of the string. Otherwise, a string like 8foo will
// be parsed as "8"
if (value > SHORT_MAX || pos != args[index].length())
{
succeeded = false;
}
else
{
*pSetting = static_cast<short>(value);
succeeded = true;
}
}
catch (...)
{
succeeded = false;
}
}
s_ConsumeArg(args, index);
}
return (succeeded) ? S_OK : E_INVALIDARG;
}
// Routine Description:
// - Parsing helper that will turn a string into a handle value if possible.
// Arguments:
// - handleAsText - The string representation of the handle that was passed in on the command line
// - handleAsVal - The location to store the value if we can appropriately convert it.
// Return Value:
// - S_OK if we could successfully parse the given text and store it in the handle value location.
// - E_INVALIDARG if we couldn't parse the text as a valid hex-encoded handle number OR
// if the handle value was already filled.
[[nodiscard]] HRESULT ConsoleArguments::s_ParseHandleArg(const std::wstring& handleAsText, _Inout_ DWORD& handleAsVal)
{
HRESULT hr = S_OK;
// The handle should have a valid prefix.
if (handleAsText.substr(0, HANDLE_PREFIX.length()) != HANDLE_PREFIX)
{
hr = E_INVALIDARG;
}
else if (0 == handleAsVal)
{
handleAsVal = wcstoul(handleAsText.c_str(), nullptr /*endptr*/, 16 /*base*/);
// If the handle didn't parse into a reasonable handle ID, invalid.
if (handleAsVal == 0)
{
hr = E_INVALIDARG;
}
}
else
{
// If we're trying to set the handle a second time, invalid.
hr = E_INVALIDARG;
}
return hr;
}
// Routine Description:
// Given the commandline of tokens `args`, creates a wstring containing all of
// the remaining args after index joined with spaces. If skipFirst==true,
// then we omit the argument at index from this finished string. skipFirst
// should only be true if the first arg is
// ConsoleArguments::CLIENT_COMMANDLINE_ARG. Removes all the args starting
// at index from the collection.
// The finished commandline is placed in _clientCommandline
// Arguments:
// args: A collection of wstrings representing command-line arguments
// index: the index of the argument of which to start the commandline from.
// skipFirst: if true, omit the arg at index (which should be "--")
// Return Value:
// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating
// failure.
[[nodiscard]] HRESULT ConsoleArguments::_GetClientCommandline(_Inout_ std::vector<std::wstring>& args, const size_t index, const bool skipFirst)
{
auto start = args.begin() + index;
// Erase the first token.
// Used to get rid of the explicit commandline token "--"
if (skipFirst)
{
// Make sure that the arg we're deleting is "--"
FAIL_FAST_IF(!(CLIENT_COMMANDLINE_ARG == start->c_str()));
args.erase(start);
}
_clientCommandline = L"";
size_t j = 0;
for (j = index; j < args.size(); j++)
{
_clientCommandline += EscapeArgument(args[j]); // escape commandline
if (j + 1 < args.size())
{
_clientCommandline += L" ";
}
}
args.erase(args.begin() + index, args.begin() + j);
return S_OK;
}
// Routine Description:
// Attempts to parse the commandline that this ConsoleArguments was initialized
// with. Fills all of our members with values that were specified on the
// commandline.
// Arguments:
// <none>
// Return Value:
// S_OK if we parsed our _commandline successfully, otherwise E_INVALIDARG
// indicating failure.
[[nodiscard]] HRESULT ConsoleArguments::ParseCommandline()
{
// If the commandline was empty, quick return.
if (_commandline.length() == 0)
{
return S_OK;
}
std::vector<std::wstring> args;
HRESULT hr = S_OK;
// Make a mutable copy of the commandline for tokenizing
std::wstring copy = _commandline;
// Tokenize the commandline
int argc = 0;
wil::unique_hlocal_ptr<PWSTR[]> argv;
argv.reset(CommandLineToArgvW(copy.c_str(), &argc));
RETURN_LAST_ERROR_IF(argv == nullptr);
for (int i = 1; i < argc; ++i)
{
args.push_back(argv[i]);
}
// Parse args out of the commandline.
// As we handle a token, remove it from the args.
// At the end of parsing, there should be nothing left.
for (size_t i = 0; i < args.size();)
{
hr = E_INVALIDARG;
std::wstring arg = args[i];
if (arg.substr(0, HANDLE_PREFIX.length()) == HANDLE_PREFIX ||
arg == SERVER_HANDLE_ARG)
{
// server handle token accepted two ways:
// --server 0x4 (new method)
// 0x4 (legacy method)
// If we see >1 of these, it's invalid.
std::wstring serverHandleVal = arg;
if (arg == SERVER_HANDLE_ARG)
{
hr = s_GetArgumentValue(args, i, &serverHandleVal);
}
else
{
s_ConsumeArg(args, i);
hr = S_OK;
}
if (SUCCEEDED(hr))
{
hr = s_ParseHandleArg(serverHandleVal, _serverHandle);
if (SUCCEEDED(hr))
{
_createServerHandle = false;
}
}
}
else if (arg == SIGNAL_HANDLE_ARG)
{
std::wstring signalHandleVal;
hr = s_GetArgumentValue(args, i, &signalHandleVal);
if (SUCCEEDED(hr))
{
hr = s_ParseHandleArg(signalHandleVal, _signalHandle);
}
}
else if (arg == FORCE_V1_ARG)
{
// -ForceV1 command line switch for NTVDM support
_forceV1 = true;
s_ConsumeArg(args, i);
hr = S_OK;
}
else if (arg.substr(0, FILEPATH_LEADER_PREFIX.length()) == FILEPATH_LEADER_PREFIX)
{
// beginning of command line -- includes file path
// skipped for historical reasons.
s_ConsumeArg(args, i);
hr = S_OK;
}
else if (arg == VT_MODE_ARG)
{
hr = s_GetArgumentValue(args, i, &_vtMode);
}
else if (arg == WIDTH_ARG)
{
hr = s_GetArgumentValue(args, i, &_width);
}
else if (arg == HEIGHT_ARG)
{
hr = s_GetArgumentValue(args, i, &_height);
}
else if (arg == FEATURE_ARG)
{
hr = s_HandleFeatureValue(args, i);
}
else if (arg == HEADLESS_ARG)
{
_headless = true;
s_ConsumeArg(args, i);
hr = S_OK;
}
else if (arg == INHERIT_CURSOR_ARG)
{
_inheritCursor = true;
s_ConsumeArg(args, i);
hr = S_OK;
}
else if (arg == RESIZE_QUIRK)
{
_resizeQuirk = true;
s_ConsumeArg(args, i);
hr = S_OK;
}
else if (arg == CLIENT_COMMANDLINE_ARG)
{
// Everything after this is the explicit commandline
hr = _GetClientCommandline(args, i, true);
break;
}
// TODO: handle the rest of the possible params (MSFT:13271366, MSFT:13631640)
// TODO: handle invalid args
// e.g. "conhost --foo bar" should not make the clientCommandline "--foo bar"
else
{
// If we encounter something that doesn't match one of our other
// args, then it's the start of the commandline
hr = _GetClientCommandline(args, i, false);
break;
}
if (FAILED(hr))
{
break;
}
}
// We should have consumed every token at this point.
// if not, it is some sort of parsing error.
// If we failed to parse an arg, then no need to assert.
if (SUCCEEDED(hr))
{
FAIL_FAST_IF(!args.empty());
}
return hr;
}
// Routine Description:
// - Returns true if we already have opened handles to use for the VT server
// streams.
// - If false, try next to see if we have pipe names to open instead.
// Arguments:
// - <none> - uses internal state
// Return Value:
// - True or false (see description)
bool ConsoleArguments::HasVtHandles() const
{
return IsValidHandle(_vtInHandle) && IsValidHandle(_vtOutHandle);
}
// Routine Description:
// - Returns true if we were passed a seemingly valid signal handle on startup.
// Arguments:
// - <none> - uses internal state
// Return Value:
// - True or false (see description)
bool ConsoleArguments::HasSignalHandle() const
{
return IsValidHandle(GetSignalHandle());
}
// Routine Description:
// - Returns true if we already have at least one handle for conpty streams.
// Arguments:
// - <none> - uses internal state
// Return Value:
// - True or false (see description)
bool ConsoleArguments::InConptyMode() const noexcept
{
// If we only have a signal handle, then that's fine, they probably called
// CreatePseudoConsole with neither handle.
// If we only have one of the other handles, that's fine they're still
// invoking us by passing in pipes, so they know what they're doing.
return IsValidHandle(_vtInHandle) || IsValidHandle(_vtOutHandle) || HasSignalHandle();
}
bool ConsoleArguments::IsHeadless() const
{
return _headless;
}
bool ConsoleArguments::ShouldCreateServerHandle() const
{
return _createServerHandle;
}
HANDLE ConsoleArguments::GetServerHandle() const
{
return ULongToHandle(_serverHandle);
}
HANDLE ConsoleArguments::GetSignalHandle() const
{
return ULongToHandle(_signalHandle);
}
HANDLE ConsoleArguments::GetVtInHandle() const
{
return _vtInHandle;
}
HANDLE ConsoleArguments::GetVtOutHandle() const
{
return _vtOutHandle;
}
std::wstring ConsoleArguments::GetClientCommandline() const
{
return _clientCommandline;
}
std::wstring ConsoleArguments::GetVtMode() const
{
return _vtMode;
}
bool ConsoleArguments::GetForceV1() const
{
return _forceV1;
}
short ConsoleArguments::GetWidth() const
{
return _width;
}
short ConsoleArguments::GetHeight() const
{
return _height;
}
bool ConsoleArguments::GetInheritCursor() const
{
return _inheritCursor;
}
bool ConsoleArguments::IsResizeQuirkEnabled() const
{
return _resizeQuirk;
}
// Method Description:
// - Tell us to use a different size than the one parsed as the size of the
// console. This is called by the PtySignalInputThread when it receives a
// resize before the first client has connected. Because there's no client,
// there's also no buffer yet, so it has nothing to resize.
// However, we shouldn't just discard that first resize message. Instead,
// store it in here, so we can use the value when the first client does connect.
// Arguments:
// - dimensions: the new size in characters of the conpty buffer & viewport.
// Return Value:
// - <none>
void ConsoleArguments::SetExpectedSize(COORD dimensions) noexcept
{
_width = dimensions.X;
_height = dimensions.Y;
// Stash away the original values we parsed when this is called.
// This is to help debugging - if the signal thread DOES change these values,
// we can still recover what was given to us on the commandline.
if (!_receivedEarlySizeChange)
{
_originalWidth = _width;
_originalHeight = _height;
// Mark that we've changed size from what our commandline values were
_receivedEarlySizeChange = true;
}
}
#ifdef UNIT_TESTING
// Method Description:
// - This is a test helper method. It can be used to trick us into thinking
// we're headless (in conpty mode), even without parsing any arguments.
// Arguments:
// - <none>
// Return Value:
// - <none>
void ConsoleArguments::EnableConptyModeForTests()
{
_headless = true;
}
#endif