terminal/src/host/writeData.cpp
Dustin L. Howett 1bf4c082b4
Reintroduce a color compatibility hack, but only for PowerShells (#6810)
There is going to be a very long tail of applications that will
explicitly request VT SGR 40/37 when what they really want is to
SetConsoleTextAttribute() with a black background/white foreground.
Instead of making those applications look bad (and therefore making us
look bad, because we're releasing this as an update to something that
"looks good" already), we're introducing this compatibility quirk.
Before the color reckoning in #6698 + #6506, *every* color was subject
to being spontaneously and erroneously turned into the default color.
Now, only the 16-color palette value that matches the active console
background/foreground color will be destroyed, and only when received
from specific applications.

Removal will be tracked by #6807.

Michael and I discussed what layer this quirk really belonged in. I
originally believed it would be sufficient to detect a background color
that matched the legacy default background, but @j4james provided an
example of where that wouldn't work out (powershell setting the
foreground color to white/gray). In addition, it was too heavyhanded: it
re-broke black backgrounds for every application.

Michael thought that it should live in the server, as a small VT parser
that righted the wrongs coming directly out of the application. On
further investigation, however, I realized that we'd need to push more
information up into the server (so that it could make the decision about
which VT was wrong and which was right) than should be strictly
necessary.

The host knows which colors are right and wrong, and it gets final say
in what ends up in the buffer.

Because of that, I chose to push the quirk state down through
WriteConsole to DoWriteConsole and toggle state on the
SCREEN_INFORMATION that indicates whether the colors coming out of the
application are to be distrusted. This quirk _only applies to pwsh.exe
and powershell.exe._

NOTE: This doesn't work for PowerShell the .NET Global tool, because it
is run as an assembly through dotnet.exe. I have no opinion on how to
fix this, or whether it is worth fixing.

VALIDATION
----------
I configured my terminals to have an incredibly garish color scheme to
show exactly what's going to happen as a result of this. The _default
terminal background_ is purple or red, and the foreground green. I've
printed out a heap of test colors to see how black interacts with them.

Pull request #6810 contains the images generated from this test.

The only color lines that change are the ones where black as a
background or white as a foreground is selected out of the 16-color
palette explicitly. Reverse video still works fine (because black is in
the foreground!), and it's even possible to represent "black on default"
and reverse it into "default on black", despite the black in question
having been `40`.

Fixes #6767.
2020-07-10 15:25:39 -07:00

192 lines
8.6 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "writeData.hpp"
#include "_stream.h"
#include "..\types\inc\convert.hpp"
#include "..\interactivity\inc\ServiceLocator.hpp"
// Routine Description:
// - Creates a new write data object for used in servicing write console requests
// Arguments:
// - siContext - The output buffer to write text data to
// - pwchContext - The string information that the client application sent us to be written
// - cbContext - Byte count of the string above
// - uiOutputCodepage - When the wait is completed, we *might* have to convert the byte count
// back into a specific codepage if the initial call was an A call.
// We need to remember what output codepage was set at the moment in time
// when the write was delayed as it might change by the time it is serviced.
// Return Value:
// - THROW: Throws if space cannot be allocated to copy the given string
WriteData::WriteData(SCREEN_INFORMATION& siContext,
_In_reads_bytes_(cbContext) wchar_t* const pwchContext,
const size_t cbContext,
const UINT uiOutputCodepage,
const bool requiresVtQuirk) :
IWaitRoutine(ReplyDataType::Write),
_siContext(siContext),
_pwchContext(THROW_IF_NULL_ALLOC(reinterpret_cast<wchar_t*>(new byte[cbContext]))),
_cbContext(cbContext),
_uiOutputCodepage(uiOutputCodepage),
_requiresVtQuirk(requiresVtQuirk),
_fLeadByteCaptured(false),
_fLeadByteConsumed(false),
_cchUtf8Consumed(0)
{
memmove(_pwchContext, pwchContext, _cbContext);
}
// Routine Description:
// - Destroys the write data object
// - Frees the string copy we made on creation
WriteData::~WriteData()
{
if (nullptr != _pwchContext)
{
delete[] _pwchContext;
}
}
// Routine Description:
// - Stores some additional information about lead byte adjustments from the conversion
// in WriteConsoleA before the real WriteConsole processing (always W) is reached
// so we can restore an accurate A byte count at the very end when the wait is serviced.
// Arguments:
// - fLeadByteCaptured - A lead byte was removed from the string before converted it and saved it.
// We need to report to the original caller that we "wrote" the byte
// even though it is held in escrow for the next call because it was
// the last character in the stream.
// - fLeadByteConsumed - We had a lead byte in escrow from the previous call that we stitched onto the
// front of the input string even though the caller didn't write it in this call.
// We need to report the byte count back to the caller without including this byte
// in the calculation as it wasn't a part of what was given in this exact call.
// Return Value:
// - <none>
void WriteData::SetLeadByteAdjustmentStatus(const bool fLeadByteCaptured,
const bool fLeadByteConsumed)
{
_fLeadByteCaptured = fLeadByteCaptured;
_fLeadByteConsumed = fLeadByteConsumed;
}
// Routine Description:
// - For UTF-8 codepages, remembers how many bytes that the UTF-8 parser said it consumed from the input stream.
// This will allow us to give back the correct value after the wait routine Notify services the data later.
// Arguments:
// - cchUtf8Consumed - Count of characters consumed by the UTF-8 parser off the input stream to generate the
// wide character string that is stowed in this object for consumption in the notify routine later.
// Return Value:
// - <none>
void WriteData::SetUtf8ConsumedCharacters(const size_t cchUtf8Consumed)
{
_cchUtf8Consumed = cchUtf8Consumed;
}
// Routine Description:
// - Called back at a later time to resume the writing operation when the output object becomes unblocked.
// Arguments:
// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument
// contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise 0.
// - fIsUnicode - Input data was in UCS-2 unicode or it needs to be converted with the current Output Codepage
// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait)
// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process
// - pControlKeyState - Unused for write operations. Set to 0.
// - pOutputData - not used.
// - true if the wait is done and result buffer/status code can be sent back to the client.
// - false if we need to continue to wait because the output object blocked again
bool WriteData::Notify(const WaitTerminationReason TerminationReason,
const bool fIsUnicode,
_Out_ NTSTATUS* const pReplyStatus,
_Out_ size_t* const pNumBytes,
_Out_ DWORD* const pControlKeyState,
_Out_ void* const /*pOutputData*/)
{
*pNumBytes = _cbContext;
*pControlKeyState = 0;
if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
{
*pReplyStatus = STATUS_THREAD_IS_TERMINATING;
return true;
}
// if we get to here, this routine was called by the input
// thread, which grabs the current console lock.
// This routine should be called by a thread owning the same lock on the
// same console as we're reading from.
FAIL_FAST_IF(!(Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()));
std::unique_ptr<WriteData> waiter;
size_t cbContext = _cbContext;
NTSTATUS Status = DoWriteConsole(_pwchContext,
&cbContext,
_siContext,
_requiresVtQuirk,
waiter);
if (Status == CONSOLE_STATUS_WAIT)
{
// an extra waiter will be created by DoWriteConsole, but we're already a waiter so discard it.
waiter.reset();
return false;
}
// There's extra work to do to correct the byte counts if the original call was an A-version call.
// We always process and hold text in the waiter as W-version text, but the A call is expecting
// a byte value in its own codepage of how much we have written in that codepage.
if (!fIsUnicode)
{
if (CP_UTF8 != _uiOutputCodepage)
{
// At this level with WriteConsole, everything is byte counts, so change back to char counts for
// GetALengthFromW to work correctly.
const size_t cchContext = cbContext / sizeof(wchar_t);
// For non-UTF-8 codepages, we need to back convert the amount consumed and then
// correlate that with any lead bytes we may have kept for later or reintroduced
// from previous calls.
size_t cchTextBufferRead = 0;
// Start by counting the number of A bytes we used in printing our W string to the screen.
try
{
cchTextBufferRead = GetALengthFromW(_uiOutputCodepage, { _pwchContext, cchContext });
}
CATCH_LOG();
// If we captured a byte off the string this time around up above, it means we didn't feed
// it into the WriteConsoleW above, and therefore its consumption isn't accounted for
// in the count we just made. Add +1 to compensate.
if (_fLeadByteCaptured)
{
cchTextBufferRead++;
}
// If we consumed an internally-stored lead byte this time around up above, it means that we
// fed a byte into WriteConsoleW that wasn't a part of this particular call's request.
// We need to -1 to compensate and tell the caller the right number of bytes consumed this request.
if (_fLeadByteConsumed)
{
cchTextBufferRead--;
}
cbContext = cchTextBufferRead;
}
else
{
// For UTF-8, we were told exactly how many valid bytes were consumed before we got into the wait state.
// Just give that value back now.
cbContext = _cchUtf8Consumed;
}
}
*pNumBytes = cbContext;
*pReplyStatus = Status;
return true;
}