terminal/src/host/VtIo.cpp
James Holderness ddbe370d22
Improve the propagation of color attributes over ConPTY (#6506)
This PR reimplements the VT rendering engines to do a better job of
preserving the original color types when propagating attributes over
ConPTY. For the 16-color renderers it provides better support for
default colors and improves the efficiency of the color narrowing
conversions. It also fixes problems with the ordering of character
renditions that could result in attributes being dropped.

Originally the base renderer would calculate the RGB color values and
legacy/extended attributes up front, passing that data on to the active
engine's `UpdateDrawingBrushes` method. With this new implementation,
the renderer now just passes through the original `TextAttribute` along
with an `IRenderData` interface, and leaves it to the engines to extract
the information they need.

The GDI and DirectX engines now have to lookup the RGB colors themselves
(via simple `IRenderData` calls), but have no need for the other
attributes. The VT engines extract the information that they need from
the `TextAttribute`, instead of having to reverse engineer it from
`COLORREF`s.

The process for the 256-color Xterm engine starts with a check for
default colors. If both foreground and background are default, it
outputs a SGR 0 reset, and clears the `_lastTextAttribute` completely to
make sure any reset state is reapplied. With that out the way, the
foreground and background are updated (if changed) in one of 4 ways.
They can either be a default value (SGR 39 and 49), a 16-color index
(using ANSI or AIX sequences), a 256-color index, or a 24-bit RGB value
(both using SGR 38 and 48 sequences).

Then once the colors are accounted for, there is a separate step that
handles the character rendition attributes (bold, italics, underline,
etc.) This step must come _after_ the color sequences, in case a SGR
reset is required, which would otherwise have cleared any character
rendition attributes if it came last (which is what happened in the
original implementation).

The process for the 16-color engines is a little different. The target
client in this case (Windows telnet) is incapable of setting default
colors individually, so we need to output an SGR 0 reset if _either_
color has changed to default. With that out the way, we use the
`TextColor::GetLegacyIndex` method to obtain an approximate 16-color
index for each color, and apply the bold attribute by brightening the
foreground index (setting bit 8) if the color type permits that.

However, since Windows telnet only supports the 8 basic ANSI colors, the
best we can do for bright colors is to output an SGR 1 attribute to get
a bright foreground. There is nothing we can do about a bright
background, so after that we just have to drop the high bit from the
colors. If the resulting index values have changed from what they were
before, we then output ANSI 8-color SGR sequences to update them.

As with the 256-color engine, there is also a final step to handle the
character rendition attributes. But in this case, the only supported
attributes are underline and reversed video.

Since the VT engines no longer depend on the active color table and
default color values, there was quite a lot of code that could now be
removed. This included the `IDefaultColorProvider` interface and
implementations, the `Find(Nearest)TableIndex` functions, and also the
associated HLS conversion and difference calculations.

VALIDATION

Other than simple API parameter changes, the majority of updates
required in the unit tests were to correct assumptions about the way the
colors should be rendered, which were the source of the narrowing bugs
this PR was trying to fix. Like passing white on black to the
`UpdateDrawingBrushes` API, and expecting it to output the default `SGR
0` sequence, or passing an RGB color and expecting an indexed SGR
sequence.

In addition to that, I've added some VT renderer tests to make sure the
rendition attributes (bold, underline, etc) are correctly retained when
a default color update causes an `SGR 0` sequence to be generated (the
source of bug #3076). And I've extended the VT renderer color tests
(both 256-color and 16-color) to make sure we're covering all of the
different color types (default, RGB, and both forms of indexed colors).

I've also tried to manually verify that all of the test cases in the
linked bug reports (and their associated duplicates) are now fixed when
this PR is applied.

Closes #2661
Closes #3076
Closes #3717
Closes #5384
Closes #5864

This is only a partial fix for #293, but I suspect the remaining cases
are unfixable.
2020-07-01 11:10:36 -07:00

476 lines
16 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "VtIo.hpp"
#include "../interactivity/inc/ServiceLocator.hpp"
#include "../renderer/vt/XtermEngine.hpp"
#include "../renderer/vt/Xterm256Engine.hpp"
#include "../renderer/base/renderer.hpp"
#include "../types/inc/utils.hpp"
#include "input.h" // ProcessCtrlEvents
#include "output.h" // CloseConsoleProcessState
using namespace Microsoft::Console;
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::VirtualTerminal;
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::Utils;
using namespace Microsoft::Console::Interactivity;
VtIo::VtIo() :
_initialized(false),
_objectsCreated(false),
_lookingForCursorPosition(false),
_IoMode(VtIoMode::INVALID)
{
}
// Routine Description:
// Tries to get the VtIoMode from the given string. If it's not one of the
// *_STRING constants in VtIoMode.hpp, then it returns E_INVALIDARG.
// Arguments:
// VtIoMode: A string containing the console's requested VT mode. This can be
// any of the strings in VtIoModes.hpp
// pIoMode: receives the VtIoMode that the string represents if it's a valid
// IO mode string
// Return Value:
// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating failure.
[[nodiscard]] HRESULT VtIo::ParseIoMode(const std::wstring& VtMode, _Out_ VtIoMode& ioMode)
{
ioMode = VtIoMode::INVALID;
if (VtMode == XTERM_256_STRING)
{
ioMode = VtIoMode::XTERM_256;
}
else if (VtMode == XTERM_STRING)
{
ioMode = VtIoMode::XTERM;
}
else if (VtMode == XTERM_ASCII_STRING)
{
ioMode = VtIoMode::XTERM_ASCII;
}
else if (VtMode == DEFAULT_STRING)
{
ioMode = VtIoMode::XTERM_256;
}
else
{
return E_INVALIDARG;
}
return S_OK;
}
[[nodiscard]] HRESULT VtIo::Initialize(const ConsoleArguments* const pArgs)
{
_lookingForCursorPosition = pArgs->GetInheritCursor();
_resizeQuirk = pArgs->IsResizeQuirkEnabled();
_win32InputMode = pArgs->IsWin32InputModeEnabled();
// If we were already given VT handles, set up the VT IO engine to use those.
if (pArgs->InConptyMode())
{
return _Initialize(pArgs->GetVtInHandle(), pArgs->GetVtOutHandle(), pArgs->GetVtMode(), pArgs->GetSignalHandle());
}
// Didn't need to initialize if we didn't have VT stuff. It's still OK, but report we did nothing.
else
{
return S_FALSE;
}
}
// Routine Description:
// Tries to initialize this VtIo instance from the given pipe handles and
// VtIoMode. The pipes should have been created already (by the caller of
// conhost), in non-overlapped mode.
// The VtIoMode string can be the empty string as a default value.
// Arguments:
// InHandle: a valid file handle. The console will
// read VT sequences from this pipe to generate INPUT_RECORDs and other
// input events.
// OutHandle: a valid file handle. The console
// will be "rendered" to this pipe using VT sequences
// VtIoMode: A string containing the console's requested VT mode. This can be
// any of the strings in VtIoModes.hpp
// SignalHandle: an optional file handle that will be used to send signals into the console.
// This represents the ability to send signals to a *nix tty/pty.
// Return Value:
// S_OK if we initialized successfully, otherwise an appropriate HRESULT
// indicating failure.
[[nodiscard]] HRESULT VtIo::_Initialize(const HANDLE InHandle,
const HANDLE OutHandle,
const std::wstring& VtMode,
_In_opt_ const HANDLE SignalHandle)
{
FAIL_FAST_IF_MSG(_initialized, "Someone attempted to double-_Initialize VtIo");
RETURN_IF_FAILED(ParseIoMode(VtMode, _IoMode));
_hInput.reset(InHandle);
_hOutput.reset(OutHandle);
_hSignal.reset(SignalHandle);
// The only way we're initialized is if the args said we're in conpty mode.
// If the args say so, then at least one of in, out, or signal was specified
_initialized = true;
return S_OK;
}
// Method Description:
// - Create the VtRenderer and the VtInputThread for this console.
// MUST BE DONE AFTER CONSOLE IS INITIALIZED, to make sure we've gotten the
// buffer size from the attached client application.
// Arguments:
// - <none>
// Return Value:
// S_OK if we initialized successfully,
// S_FALSE if VtIo hasn't been initialized (or we're not in conpty mode)
// otherwise an appropriate HRESULT indicating failure.
[[nodiscard]] HRESULT VtIo::CreateIoHandlers() noexcept
{
if (!_initialized)
{
return S_FALSE;
}
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
try
{
if (IsValidHandle(_hInput.get()))
{
_pVtInputThread = std::make_unique<VtInputThread>(std::move(_hInput), _lookingForCursorPosition);
}
if (IsValidHandle(_hOutput.get()))
{
Viewport initialViewport = Viewport::FromDimensions({ 0, 0 },
gci.GetWindowSize().X,
gci.GetWindowSize().Y);
switch (_IoMode)
{
case VtIoMode::XTERM_256:
_pVtRenderEngine = std::make_unique<Xterm256Engine>(std::move(_hOutput),
initialViewport);
break;
case VtIoMode::XTERM:
_pVtRenderEngine = std::make_unique<XtermEngine>(std::move(_hOutput),
initialViewport,
false);
break;
case VtIoMode::XTERM_ASCII:
_pVtRenderEngine = std::make_unique<XtermEngine>(std::move(_hOutput),
initialViewport,
true);
break;
default:
return E_FAIL;
}
if (_pVtRenderEngine)
{
_pVtRenderEngine->SetTerminalOwner(this);
_pVtRenderEngine->SetResizeQuirk(_resizeQuirk);
}
}
}
CATCH_RETURN();
_objectsCreated = true;
return S_OK;
}
bool VtIo::IsUsingVt() const
{
return _objectsCreated;
}
// Routine Description:
// Potentially starts this VtIo's input thread and render engine.
// If the VtIo hasn't yet been given pipes, then this function will
// silently do nothing. It's the responsibility of the caller to make sure
// that the pipes are initialized first with VtIo::Initialize
// Arguments:
// <none>
// Return Value:
// S_OK if we started successfully or had nothing to start, otherwise an
// appropriate HRESULT indicating failure.
[[nodiscard]] HRESULT VtIo::StartIfNeeded()
{
// If we haven't been set up, do nothing (because there's nothing to start)
if (!_objectsCreated)
{
return S_FALSE;
}
Globals& g = ServiceLocator::LocateGlobals();
if (_pVtRenderEngine)
{
try
{
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
}
CATCH_RETURN();
}
// GH#4999 - Send a sequence to the connected terminal to request
// win32-input-mode from them. This will enable the connected terminal to
// send us full INPUT_RECORDs as input. If the terminal doesn't understand
// this sequence, it'll just ignore it.
if (_win32InputMode)
{
LOG_IF_FAILED(_pVtRenderEngine->RequestWin32Input());
}
// MSFT: 15813316
// If the terminal application wants us to inherit the cursor position,
// we're going to emit a VT sequence to ask for the cursor position, then
// read input until we get a response. Terminals who request this behavior
// but don't respond will hang.
// If we get a response, the InteractDispatch will call SetCursorPosition,
// which will call to our VtIo::SetCursorPosition method.
// We need both handles for this initialization to work. If we don't have
// both, we'll skip it. They either aren't going to be reading output
// (so they can't get the DSR) or they can't write the response to us.
if (_lookingForCursorPosition && _pVtRenderEngine && _pVtInputThread)
{
LOG_IF_FAILED(_pVtRenderEngine->RequestCursor());
while (_lookingForCursorPosition)
{
_pVtInputThread->DoReadInput(false);
}
}
if (_pVtInputThread)
{
LOG_IF_FAILED(_pVtInputThread->Start());
}
if (_pPtySignalInputThread)
{
// Let the signal thread know that the console is connected
_pPtySignalInputThread->ConnectConsole();
}
return S_OK;
}
// Method Description:
// - Create and start the signal thread. The signal thread can be created
// independent of the i/o threads, and doesn't require a client first
// attaching to the console. We need to create it first and foremost,
// because it's possible that a terminal application could
// CreatePseudoConsole, then ClosePseudoConsole without ever attaching a
// client. Should that happen, we still need to exit.
// Arguments:
// - <none>
// Return Value:
// - S_FALSE if we're not in VtIo mode,
// S_OK if we succeeded,
// otherwise an appropriate HRESULT indicating failure.
[[nodiscard]] HRESULT VtIo::CreateAndStartSignalThread() noexcept
{
if (!_initialized)
{
return S_FALSE;
}
// If we were passed a signal handle, try to open it and make a signal reading thread.
if (IsValidHandle(_hSignal.get()))
{
try
{
_pPtySignalInputThread = std::make_unique<PtySignalInputThread>(std::move(_hSignal));
// Start it if it was successfully created.
RETURN_IF_FAILED(_pPtySignalInputThread->Start());
}
CATCH_RETURN();
}
return S_OK;
}
// Method Description:
// - Prevent the renderer from emitting output on the next resize. This prevents
// the host from echoing a resize to the terminal that requested it.
// Arguments:
// - <none>
// Return Value:
// - S_OK if the renderer successfully suppressed the next repaint, otherwise an
// appropriate HRESULT indicating failure.
[[nodiscard]] HRESULT VtIo::SuppressResizeRepaint()
{
HRESULT hr = S_OK;
if (_pVtRenderEngine)
{
hr = _pVtRenderEngine->SuppressResizeRepaint();
}
return hr;
}
// Method Description:
// - Attempts to set the initial cursor position, if we're looking for it.
// If we're not trying to inherit the cursor, does nothing.
// Arguments:
// - coordCursor: The initial position of the cursor.
// Return Value:
// - S_OK if we successfully inherited the cursor or did nothing, else an
// appropriate HRESULT
[[nodiscard]] HRESULT VtIo::SetCursorPosition(const COORD coordCursor)
{
HRESULT hr = S_OK;
if (_lookingForCursorPosition)
{
if (_pVtRenderEngine)
{
hr = _pVtRenderEngine->InheritCursor(coordCursor);
}
_lookingForCursorPosition = false;
}
return hr;
}
void VtIo::CloseInput()
{
// This will release the lock when it goes out of scope
std::lock_guard<std::mutex> lk(_shutdownLock);
_pVtInputThread = nullptr;
_ShutdownIfNeeded();
}
void VtIo::CloseOutput()
{
// This will release the lock when it goes out of scope
std::lock_guard<std::mutex> lk(_shutdownLock);
Globals& g = ServiceLocator::LocateGlobals();
// DON'T RemoveRenderEngine, as that requires the engine list lock, and this
// is usually being triggered on a paint operation, when the lock is already
// owned by the paint.
// Instead we're releasing the Engine here. A pointer to it has already been
// given to the Renderer, so we don't want the unique_ptr to delete it. The
// Renderer will own its lifetime now.
_pVtRenderEngine.release();
g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(nullptr);
_ShutdownIfNeeded();
}
void VtIo::_ShutdownIfNeeded()
{
// The callers should have both acquired the _shutdownLock at this point -
// we dont want a race on who is actually responsible for closing it.
if (_objectsCreated && _pVtInputThread == nullptr && _pVtRenderEngine == nullptr)
{
// At this point, we no longer have a renderer or inthread. So we've
// effectively been disconnected from the terminal.
// If we have any remaining attached processes, this will prepare us to send a ctrl+close to them
// if we don't, this will cause us to rundown and exit.
CloseConsoleProcessState();
// If we haven't terminated by now, that's because there's a client that's still attached.
// Force the handling of the control events by the attached clients.
// As of MSFT:19419231, CloseConsoleProcessState will make sure this
// happens if this method is called outside of lock, but if we're
// currently locked, we want to make sure ctrl events are handled
// _before_ we RundownAndExit.
ProcessCtrlEvents();
// Make sure we terminate.
ServiceLocator::RundownAndExit(ERROR_BROKEN_PIPE);
}
}
// Method Description:
// - Tell the vt renderer to begin a resize operation. During a resize
// operation, the vt renderer should _not_ request to be repainted during a
// text buffer circling event. Any callers of this method should make sure to
// call EndResize to make sure the renderer returns to normal behavior.
// See GH#1795 for context on this method.
// Arguments:
// - <none>
// Return Value:
// - <none>
void VtIo::BeginResize()
{
if (_pVtRenderEngine)
{
_pVtRenderEngine->BeginResizeRequest();
}
}
// Method Description:
// - Tell the vt renderer to end a resize operation.
// See BeginResize for more details.
// See GH#1795 for context on this method.
// Arguments:
// - <none>
// Return Value:
// - <none>
void VtIo::EndResize()
{
if (_pVtRenderEngine)
{
_pVtRenderEngine->EndResizeRequest();
}
}
#ifdef UNIT_TESTING
// Method Description:
// - This is a test helper method. It can be used to trick VtIo into responding
// true to `IsUsingVt`, which will cause the console host to act in conpty
// mode.
// Arguments:
// - vtRenderEngine: a VT renderer that our VtIo should use as the vt engine during these tests
// Return Value:
// - <none>
void VtIo::EnableConptyModeForTests(std::unique_ptr<Microsoft::Console::Render::VtEngine> vtRenderEngine)
{
_objectsCreated = true;
_pVtRenderEngine = std::move(vtRenderEngine);
}
#endif
// Method Description:
// - Returns true if the Resize Quirk is enabled. This changes the behavior of
// conpty to _not_ InvalidateAll the entire viewport on a resize operation.
// This is used by the Windows Terminal, because it is prepared to be
// connected to a conpty, and handles it's own buffer specifically for a
// conpty scenario.
// - See also: GH#3490, #4354, #4741
// Arguments:
// - <none>
// Return Value:
// - true iff we were started with the `--resizeQuirk` flag enabled.
bool VtIo::IsResizeQuirkEnabled() const
{
return _resizeQuirk;
}
// Method Description:
// - Manually tell the renderer that it should emit a "Erase Scrollback"
// sequence to the connected terminal. We need to do this in certain cases
// that we've identified where we believe the client wanted the entire
// terminal buffer cleared, not just the viewport. For more information, see
// GH#3126.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we wrote the sequences successfully, otherwise an appropriate HRESULT
[[nodiscard]] HRESULT VtIo::ManuallyClearScrollback() const noexcept
{
if (_pVtRenderEngine)
{
return _pVtRenderEngine->ManuallyClearScrollback();
}
return S_OK;
}