terminal/src/host/readDataCooked.cpp
David Kaplan 31e58809cc
Add cooked data read tracing (#10166)
Added trace to conhost to instrument buffers that are cooked prior to being passed to the console.
* [x] I've discussed this with core contributors already (internal)

VALIDATION
- Ensured trace is correctly logged in ETL (via TraceLog)
2021-05-24 16:24:01 -05:00

1211 lines
44 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "readDataCooked.hpp"
#include "dbcs.h"
#include "stream.h"
#include "misc.h"
#include "_stream.h"
#include "inputBuffer.hpp"
#include "cmdline.h"
#include "../types/inc/GlyphWidth.hpp"
#include "../types/inc/convert.hpp"
#include "../interactivity/inc/ServiceLocator.hpp"
#define LINE_INPUT_BUFFER_SIZE (256 * sizeof(WCHAR))
using Microsoft::Console::Interactivity::ServiceLocator;
// Routine Description:
// - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'.
// Arguments:
// - pInputBuffer - Buffer that data will be read from.
// - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately.
// - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it
// - BufferSize -
// - BytesRead -
// - CurrentPosition -
// - BufPtr -
// - BackupLimit -
// - UserBufferSize - The byte count of the buffer presented by the client
// - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host.
// - OriginalCursorPosition -
// - NumberOfVisibleChars
// - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application
// - CommandHistory -
// - Echo -
// - InsertMode -
// - Processed -
// - Line -
// - pTempHandle - A handle to the output buffer to prevent it from being destroyed while we're using it to present 'edit line' text.
// - initialData - any text data that should be prepopulated into the buffer
// Return Value:
// - THROW: Throws E_INVALIDARG for invalid pointers.
COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
_In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
SCREEN_INFORMATION& screenInfo,
_In_ size_t UserBufferSize,
_In_ PWCHAR UserBuffer,
_In_ ULONG CtrlWakeupMask,
_In_ CommandHistory* CommandHistory,
const std::wstring_view exeName,
const std::string_view initialData) :
ReadData(pInputBuffer, pInputReadHandleData),
_screenInfo{ screenInfo },
_bytesRead{ 0 },
_currentPosition{ 0 },
_userBufferSize{ UserBufferSize },
_userBuffer{ UserBuffer },
_tempHandle{ nullptr },
_exeName{ exeName },
_pdwNumBytes{ nullptr },
_commandHistory{ CommandHistory },
_controlKeyState{ 0 },
_ctrlWakeupMask{ CtrlWakeupMask },
_visibleCharCount{ 0 },
_originalCursorPosition{ -1, -1 },
_beforeDialogCursorPosition{ 0, 0 },
_echoInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_ECHO_INPUT) },
_lineInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_LINE_INPUT) },
_processedInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) },
_insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() },
_unicode{ false }
{
#ifndef UNIT_TESTING
THROW_IF_FAILED(screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output,
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
_tempHandle));
#endif
// to emulate OS/2 KbdStringIn, we read into our own big buffer
// (256 bytes) until the user types enter. then return as many
// chars as will fit in the user's buffer.
_bufferSize = std::max(UserBufferSize, LINE_INPUT_BUFFER_SIZE);
_buffer = std::make_unique<byte[]>(_bufferSize);
_backupLimit = reinterpret_cast<wchar_t*>(_buffer.get());
_bufPtr = reinterpret_cast<wchar_t*>(_buffer.get());
// Initialize the user's buffer to spaces. This is done so that
// moving in the buffer via cursor doesn't do strange things.
std::fill_n(_bufPtr, _bufferSize / sizeof(wchar_t), UNICODE_SPACE);
if (!initialData.empty())
{
memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size());
_bytesRead += initialData.size();
const size_t cchInitialData = initialData.size() / sizeof(wchar_t);
VisibleCharCount() = cchInitialData;
_bufPtr += cchInitialData;
_currentPosition = cchInitialData;
OriginalCursorPosition() = screenInfo.GetTextBuffer().GetCursor().GetPosition();
OriginalCursorPosition().X -= (SHORT)_currentPosition;
const SHORT sScreenBufferSizeX = screenInfo.GetBufferSize().Width();
while (OriginalCursorPosition().X < 0)
{
OriginalCursorPosition().X += sScreenBufferSizeX;
OriginalCursorPosition().Y -= 1;
}
}
// TODO MSFT:11285829 find a better way to manage the lifetime of this object in relation to gci
}
// Routine Description:
// - Destructs a read data class.
// - Decrements count of readers waiting on the given handle.
COOKED_READ_DATA::~COOKED_READ_DATA()
{
CommandLine::Instance().EndAllPopups();
}
gsl::span<wchar_t> COOKED_READ_DATA::SpanWholeBuffer()
{
return gsl::make_span(_backupLimit, (_bufferSize / sizeof(wchar_t)));
}
gsl::span<wchar_t> COOKED_READ_DATA::SpanAtPointer()
{
auto wholeSpan = SpanWholeBuffer();
return wholeSpan.subspan(_bufPtr - _backupLimit);
}
bool COOKED_READ_DATA::HasHistory() const noexcept
{
return _commandHistory != nullptr;
}
CommandHistory& COOKED_READ_DATA::History() noexcept
{
return *_commandHistory;
}
const size_t& COOKED_READ_DATA::VisibleCharCount() const noexcept
{
return _visibleCharCount;
}
size_t& COOKED_READ_DATA::VisibleCharCount() noexcept
{
return _visibleCharCount;
}
SCREEN_INFORMATION& COOKED_READ_DATA::ScreenInfo() noexcept
{
return _screenInfo;
}
const COORD& COOKED_READ_DATA::OriginalCursorPosition() const noexcept
{
return _originalCursorPosition;
}
COORD& COOKED_READ_DATA::OriginalCursorPosition() noexcept
{
return _originalCursorPosition;
}
COORD& COOKED_READ_DATA::BeforeDialogCursorPosition() noexcept
{
return _beforeDialogCursorPosition;
}
bool COOKED_READ_DATA::IsEchoInput() const noexcept
{
return _echoInput;
}
bool COOKED_READ_DATA::IsInsertMode() const noexcept
{
return _insertMode;
}
void COOKED_READ_DATA::SetInsertMode(const bool mode) noexcept
{
_insertMode = mode;
}
bool COOKED_READ_DATA::IsUnicode() const noexcept
{
return _unicode;
}
// Routine Description:
// - gets the size of the user buffer
// Return Value:
// - the size of the user buffer in bytes
size_t COOKED_READ_DATA::UserBufferSize() const noexcept
{
return _userBufferSize;
}
// Routine Description:
// - gets a pointer to the beginning of the prompt storage
// Return Value:
// - pointer to the first char in the internal prompt storage array
wchar_t* COOKED_READ_DATA::BufferStartPtr() noexcept
{
return _backupLimit;
}
// Routine Description:
// - gets a pointer to where the next char will be inserted into the prompt storage
// Return Value:
// - pointer to the current insertion point of the prompt storage array
wchar_t* COOKED_READ_DATA::BufferCurrentPtr() noexcept
{
return _bufPtr;
}
// Routine Description:
// - Set the location of the next char insert into the prompt storage to be at
// ptr. ptr must point into a valid portion of the internal prompt storage array
// Arguments:
// - ptr - the new char insertion location
void COOKED_READ_DATA::SetBufferCurrentPtr(wchar_t* ptr) noexcept
{
_bufPtr = ptr;
}
// Routine Description:
// - gets the number of bytes read so far into the prompt buffer
// Return Value:
// - the number of bytes read
const size_t& COOKED_READ_DATA::BytesRead() const noexcept
{
return _bytesRead;
}
// Routine Description:
// - gets the number of bytes read so far into the prompt buffer
// Return Value:
// - the number of bytes read
size_t& COOKED_READ_DATA::BytesRead() noexcept
{
return _bytesRead;
}
// Routine Description:
// - gets the index for the current insertion point of the prompt
// Return Value:
// - the index of the current insertion point
const size_t& COOKED_READ_DATA::InsertionPoint() const noexcept
{
return _currentPosition;
}
// Routine Description:
// - gets the index for the current insertion point of the prompt
// Return Value:
// - the index of the current insertion point
size_t& COOKED_READ_DATA::InsertionPoint() noexcept
{
return _currentPosition;
}
// Routine Description:
// - sets the number of bytes that will be reported when this read block completes its read
// Arguments:
// - count - the number of bytes to report
void COOKED_READ_DATA::SetReportedByteCount(const size_t count) noexcept
{
FAIL_FAST_IF_NULL(_pdwNumBytes);
*_pdwNumBytes = count;
}
// Routine Description:
// - resets the prompt to be as if it was erased
void COOKED_READ_DATA::Erase() noexcept
{
_bufPtr = _backupLimit;
_bytesRead = 0;
_currentPosition = 0;
_visibleCharCount = 0;
}
// Routine Description:
// - This routine is called to complete a cooked read that blocked in ReadInputBuffer.
// - The context of the read was saved in the CookedReadData structure.
// - This routine is called when events have been written to the input buffer.
// - It is called in the context of the writing thread.
// - It may be called more than once.
// 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 - Whether to convert the final data to A (using Console Input CP) at the end or treat everything as Unicode (UCS-2)
// - 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 - For certain types of reads, this specifies which modifier keys were held.
// - pOutputData - not used
// Return Value:
// - 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 until more data is available.
bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason,
const bool fIsUnicode,
_Out_ NTSTATUS* const pReplyStatus,
_Out_ size_t* const pNumBytes,
_Out_ DWORD* const pControlKeyState,
_Out_ void* const /*pOutputData*/)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// this routine should be called by a thread owning the same
// lock on the same console as we're reading from.
FAIL_FAST_IF(!gci.IsConsoleLocked());
*pNumBytes = 0;
*pControlKeyState = 0;
*pReplyStatus = STATUS_SUCCESS;
FAIL_FAST_IF(_pInputReadHandleData->IsInputPending());
// this routine should be called by a thread owning the same lock on the same console as we're reading from.
FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0);
// if ctrl-c or ctrl-break was seen, terminate read.
if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak)))
{
*pReplyStatus = STATUS_ALERTED;
gci.SetCookedReadData(nullptr);
return true;
}
// See if we were called because the thread that owns this wait block is exiting.
if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
{
*pReplyStatus = STATUS_THREAD_IS_TERMINATING;
gci.SetCookedReadData(nullptr);
return true;
}
// We must see if we were woken up because the handle is being closed. If
// so, we decrement the read count. If it goes to zero, we wake up the
// close thread. Otherwise, we wake up any other thread waiting for data.
if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
{
*pReplyStatus = STATUS_ALERTED;
gci.SetCookedReadData(nullptr);
return true;
}
// If we get to here, this routine was called either by the input thread
// or a write routine. Both of these callers grab the current console
// lock.
// MSFT:13994975 This is REALLY weird.
// When we're doing cooked reading for popups, we come through this method
// twice. Once when we press F7 to bring up the popup, then again when we
// press enter to input the selected command.
// The first time, there is no popup, and we go to CookedRead. We pass into
// CookedRead `pNumBytes`, which is passed to us as the address of the
// stack variable dwNumBytes, in ConsoleWaitBlock::Notify.
// CookedRead sets this->_pdwNumBytes to that value, and starts the popup,
// which returns all the way up, and pops the ConsoleWaitBlock::Notify
// stack frame containing the address we're pointing at.
// Then on the second time through this function, we hit this if block,
// because there is a popup to get input from.
// However, pNumBytes is now the address of a different stack frame, and not
// necessarily the same as before (presumably not at all). The
// Callback would try and write the number of bytes read to the
// value in _pdwNumBytes, and then we'd return up to ConsoleWaitBlock::Notify,
// who's dwNumBytes had nothing in it.
// To fix this, when we hit this with a popup, we're going to make sure to
// refresh the value of _pdwNumBytes to the current address we want to put
// the out value into.
// It's still really weird, but limits the potential fallout of changing a
// piece of old spaghetti code.
if (_commandHistory)
{
if (CommandLine::Instance().HasPopup())
{
// (see above comment, MSFT:13994975)
// Make sure that the popup writes the dwNumBytes to the right place
if (pNumBytes)
{
_pdwNumBytes = pNumBytes;
}
auto& popup = CommandLine::Instance().GetPopup();
*pReplyStatus = popup.Process(*this);
if (*pReplyStatus == CONSOLE_STATUS_READ_COMPLETE ||
(*pReplyStatus != CONSOLE_STATUS_WAIT && *pReplyStatus != CONSOLE_STATUS_WAIT_NO_BLOCK))
{
*pReplyStatus = S_OK;
gci.SetCookedReadData(nullptr);
return true;
}
return false;
}
}
*pReplyStatus = Read(fIsUnicode, *pNumBytes, *pControlKeyState);
if (*pReplyStatus != CONSOLE_STATUS_WAIT)
{
gci.SetCookedReadData(nullptr);
return true;
}
else
{
return false;
}
}
bool COOKED_READ_DATA::AtEol() const noexcept
{
return _bytesRead == (_currentPosition * 2);
}
// Routine Description:
// - Method that actually retrieves a character/input record from the buffer (key press form)
// and determines the next action based on the various possible cooked read modes.
// - Mode options include the F-keys popup menus, keyboard manipulation of the edit line, etc.
// - This method also does the actual copying of the final manipulated data into the return buffer.
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// - controlKeyState - For some types of reads, this is the modifier key state with the last button press.
[[nodiscard]] HRESULT COOKED_READ_DATA::Read(const bool isUnicode,
size_t& numBytes,
ULONG& controlKeyState) noexcept
{
controlKeyState = 0;
NTSTATUS Status = _readCharInputLoop(isUnicode, numBytes);
// if the read was completed (status != wait), free the cooked read
// data. also, close the temporary output handle that was opened to
// echo the characters read.
if (Status != CONSOLE_STATUS_WAIT)
{
Status = _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState);
}
return Status;
}
void COOKED_READ_DATA::ProcessAliases(DWORD& lineCount)
{
Alias::s_MatchAndCopyAliasLegacy(_backupLimit,
_bytesRead,
_backupLimit,
_bufferSize,
_bytesRead,
_exeName,
lineCount);
}
// Routine Description:
// - This method handles the various actions that occur on the edit line like pressing keys left/right/up/down, paging, and
// the final ENTER key press that will end the wait and finally return the data.
// Arguments:
// - pCookedReadData - Pointer to cooked read data information (edit line, client buffer, etc.)
// - wch - The most recently pressed/retrieved character from the input buffer (keystroke)
// - keyState - Modifier keys/state information with the pressed key/character
// - status - The return code to pass to the client
// Return Value:
// - true if read is completed. false if we need to keep waiting and be called again with the user's next keystroke.
bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
const DWORD keyState,
NTSTATUS& status)
{
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
size_t NumSpaces = 0;
SHORT ScrollY = 0;
size_t NumToWrite;
WCHAR wch = wchOrig;
bool fStartFromDelim;
status = STATUS_SUCCESS;
if (_bytesRead >= (_bufferSize - (2 * sizeof(WCHAR))) && wch != UNICODE_CARRIAGERETURN && wch != UNICODE_BACKSPACE)
{
return false;
}
if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch)))
{
*_bufPtr = wch;
_bytesRead += sizeof(WCHAR);
_bufPtr += 1;
_currentPosition += 1;
_controlKeyState = keyState;
return true;
}
if (wch == EXTKEY_ERASE_PREV_WORD)
{
wch = UNICODE_BACKSPACE;
}
if (AtEol())
{
// If at end of line, processing is relatively simple. Just store the character and write it to the screen.
if (wch == UNICODE_BACKSPACE2)
{
wch = UNICODE_BACKSPACE;
}
if (wch != UNICODE_BACKSPACE || _bufPtr != _backupLimit)
{
fStartFromDelim = IsWordDelim(_bufPtr[-1]);
bool loop = true;
while (loop)
{
loop = false;
if (_echoInput)
{
NumToWrite = sizeof(WCHAR);
status = WriteCharsLegacy(_screenInfo,
_backupLimit,
_bufPtr,
&wch,
&NumToWrite,
&NumSpaces,
_originalCursorPosition.X,
WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
&ScrollY);
if (NT_SUCCESS(status))
{
_originalCursorPosition.Y += ScrollY;
}
else
{
RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status);
}
}
_visibleCharCount += NumSpaces;
if (wch == UNICODE_BACKSPACE && _processedInput)
{
_bytesRead -= sizeof(WCHAR);
// clang-format off
#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine")
// clang-format on
*_bufPtr = (WCHAR)' ';
_bufPtr -= 1;
_currentPosition -= 1;
// Repeat until it hits the word boundary
if (wchOrig == EXTKEY_ERASE_PREV_WORD &&
_bufPtr != _backupLimit &&
fStartFromDelim ^ !IsWordDelim(_bufPtr[-1]))
{
loop = true;
}
}
else
{
*_bufPtr = wch;
_bytesRead += sizeof(WCHAR);
_bufPtr += 1;
_currentPosition += 1;
}
}
}
}
else
{
bool CallWrite = true;
const SHORT sScreenBufferSizeX = _screenInfo.GetBufferSize().Width();
// processing in the middle of the line is more complex:
// calculate new cursor position
// store new char
// clear the current command line from the screen
// write the new command line to the screen
// update the cursor position
if (wch == UNICODE_BACKSPACE && _processedInput)
{
// for backspace, use writechars to calculate the new cursor position.
// this call also sets the cursor to the right position for the
// second call to writechars.
if (_bufPtr != _backupLimit)
{
fStartFromDelim = IsWordDelim(_bufPtr[-1]);
bool loop = true;
while (loop)
{
loop = false;
// we call writechar here so that cursor position gets updated
// correctly. we also call it later if we're not at eol so
// that the remainder of the string can be updated correctly.
if (_echoInput)
{
NumToWrite = sizeof(WCHAR);
status = WriteCharsLegacy(_screenInfo,
_backupLimit,
_bufPtr,
&wch,
&NumToWrite,
nullptr,
_originalCursorPosition.X,
WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
nullptr);
if (!NT_SUCCESS(status))
{
RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status);
}
}
_bytesRead -= sizeof(WCHAR);
_bufPtr -= 1;
_currentPosition -= 1;
memmove(_bufPtr,
_bufPtr + 1,
_bytesRead - (_currentPosition * sizeof(WCHAR)));
{
PWCHAR buf = (PWCHAR)((PBYTE)_backupLimit + _bytesRead);
*buf = (WCHAR)' ';
}
NumSpaces = 0;
// Repeat until it hits the word boundary
if (wchOrig == EXTKEY_ERASE_PREV_WORD &&
_bufPtr != _backupLimit &&
fStartFromDelim ^ !IsWordDelim(_bufPtr[-1]))
{
loop = true;
}
}
}
else
{
CallWrite = false;
}
}
else
{
// store the char
if (wch == UNICODE_CARRIAGERETURN)
{
_bufPtr = (PWCHAR)((PBYTE)_backupLimit + _bytesRead);
*_bufPtr = wch;
_bufPtr += 1;
_bytesRead += sizeof(WCHAR);
_currentPosition += 1;
}
else
{
bool fBisect = false;
if (_echoInput)
{
if (CheckBisectProcessW(_screenInfo,
_backupLimit,
_currentPosition + 1,
sScreenBufferSizeX - _originalCursorPosition.X,
_originalCursorPosition.X,
TRUE))
{
fBisect = true;
}
}
if (_insertMode)
{
memmove(_bufPtr + 1,
_bufPtr,
_bytesRead - (_currentPosition * sizeof(WCHAR)));
_bytesRead += sizeof(WCHAR);
}
*_bufPtr = wch;
_bufPtr += 1;
_currentPosition += 1;
// calculate new cursor position
if (_echoInput)
{
NumSpaces = RetrieveNumberOfSpaces(_originalCursorPosition.X,
_backupLimit,
_currentPosition - 1);
if (NumSpaces > 0 && fBisect)
NumSpaces--;
}
}
}
if (_echoInput && CallWrite)
{
COORD CursorPosition;
// save cursor position
CursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition();
CursorPosition.X = (SHORT)(CursorPosition.X + NumSpaces);
// clear the current command line from the screen
// clang-format off
#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast doesn't like this call.")
// clang-format on
DeleteCommandLine(*this, FALSE);
// write the new command line to the screen
NumToWrite = _bytesRead;
DWORD dwFlags = WC_DESTRUCTIVE_BACKSPACE | WC_PRINTABLE_CONTROL_CHARS;
if (wch == UNICODE_CARRIAGERETURN)
{
dwFlags |= WC_KEEP_CURSOR_VISIBLE;
}
status = WriteCharsLegacy(_screenInfo,
_backupLimit,
_backupLimit,
_backupLimit,
&NumToWrite,
&_visibleCharCount,
_originalCursorPosition.X,
dwFlags,
&ScrollY);
if (!NT_SUCCESS(status))
{
RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status);
_bytesRead = 0;
return true;
}
// update cursor position
if (wch != UNICODE_CARRIAGERETURN)
{
if (CheckBisectProcessW(_screenInfo,
_backupLimit,
_currentPosition + 1,
sScreenBufferSizeX - _originalCursorPosition.X,
_originalCursorPosition.X,
TRUE))
{
if (CursorPosition.X == (sScreenBufferSizeX - 1))
{
CursorPosition.X++;
}
}
// adjust cursor position for WriteChars
_originalCursorPosition.Y += ScrollY;
CursorPosition.Y += ScrollY;
status = AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr);
if (!NT_SUCCESS(status))
{
_bytesRead = 0;
return true;
}
}
}
}
// in cooked mode, enter (carriage return) is converted to
// carriage return linefeed (0xda). carriage return is always
// stored at the end of the buffer.
if (wch == UNICODE_CARRIAGERETURN)
{
if (_processedInput)
{
if (_bytesRead < _bufferSize)
{
*_bufPtr = UNICODE_LINEFEED;
if (_echoInput)
{
NumToWrite = sizeof(WCHAR);
status = WriteCharsLegacy(_screenInfo,
_backupLimit,
_bufPtr,
_bufPtr,
&NumToWrite,
nullptr,
_originalCursorPosition.X,
WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
nullptr);
if (!NT_SUCCESS(status))
{
RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status);
}
}
_bytesRead += sizeof(WCHAR);
_bufPtr++;
_currentPosition += 1;
}
}
// reset the cursor back to 25% if necessary
if (_lineInput)
{
if (_insertMode != gci.GetInsertMode())
{
// Make cursor small.
LOG_IF_FAILED(CommandLine::Instance().ProcessCommandLine(*this, VK_INSERT, 0));
}
status = STATUS_SUCCESS;
return true;
}
}
return false;
}
// Routine Description:
// - Writes string to current position in prompt line. can overwrite text to the right of the cursor.
// Arguments:
// - wstr - the string to write
// Return Value:
// - The number of chars written
size_t COOKED_READ_DATA::Write(const std::wstring_view wstr)
{
auto end = wstr.end();
const size_t charsRemaining = (_bufferSize / sizeof(wchar_t)) - (_bufPtr - _backupLimit);
if (wstr.size() > charsRemaining)
{
end = std::next(wstr.begin(), charsRemaining);
}
std::copy(wstr.begin(), end, _bufPtr);
const size_t charsInserted = end - wstr.begin();
size_t bytesInserted = charsInserted * sizeof(wchar_t);
_currentPosition += charsInserted;
_bytesRead += bytesInserted;
if (IsEchoInput())
{
size_t NumSpaces = 0;
SHORT ScrollY = 0;
FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(ScreenInfo(),
_backupLimit,
_bufPtr,
_bufPtr,
&bytesInserted,
&NumSpaces,
OriginalCursorPosition().X,
WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
&ScrollY));
OriginalCursorPosition().Y += ScrollY;
VisibleCharCount() += NumSpaces;
}
_bufPtr += charsInserted;
return charsInserted;
}
// Routine Description:
// - saves data in the prompt buffer to the outgoing user buffer
// Arguments:
// - cch - the number of chars to write to the user buffer
// Return Value:
// - the number of bytes written to the user buffer
size_t COOKED_READ_DATA::SavePromptToUserBuffer(const size_t cch)
{
size_t bytesToWrite = 0;
const HRESULT hr = SizeTMult(cch, sizeof(wchar_t), &bytesToWrite);
if (FAILED(hr))
{
return 0;
}
memmove(_userBuffer, _backupLimit, bytesToWrite);
if (!IsUnicode())
{
try
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const std::wstring wstr = ConvertToW(gci.CP, { reinterpret_cast<char*>(_userBuffer), cch });
const size_t copyAmount = std::min(wstr.size(), _userBufferSize / sizeof(wchar_t));
std::copy_n(wstr.begin(), copyAmount, _userBuffer);
return copyAmount * sizeof(wchar_t);
}
CATCH_LOG();
}
return bytesToWrite;
}
// Routine Description:
// - saves data in the prompt buffer as pending input
// Arguments:
// - index - the index of what wchar to start the saving
// - multiline - whether the pending input should be saved as multiline or not
void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline)
{
INPUT_READ_HANDLE_DATA& inputReadHandleData = *GetInputReadHandleData();
const std::wstring_view pending{ _backupLimit + index,
BytesRead() / sizeof(wchar_t) - index };
if (multiline)
{
inputReadHandleData.SaveMultilinePendingInput(pending);
}
else
{
inputReadHandleData.SavePendingInput(pending);
}
}
// Routine Description:
// - saves data in the prompt buffer as pending input
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// Return Value:
// - Status code that indicates success, wait, etc.
[[nodiscard]] NTSTATUS COOKED_READ_DATA::_readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept
{
NTSTATUS Status = STATUS_SUCCESS;
while (_bytesRead < _bufferSize)
{
wchar_t wch = UNICODE_NULL;
bool commandLineEditingKeys = false;
DWORD keyState = 0;
// This call to GetChar may block.
Status = GetChar(_pInputBuffer,
&wch,
true,
&commandLineEditingKeys,
nullptr,
&keyState);
if (!NT_SUCCESS(Status))
{
if (Status != CONSOLE_STATUS_WAIT)
{
_bytesRead = 0;
}
break;
}
// we should probably set these up in GetChars, but we set them
// up here because the debugger is multi-threaded and calls
// read before outputting the prompt.
if (_originalCursorPosition.X == -1)
{
_originalCursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition();
}
if (commandLineEditingKeys)
{
// TODO: this is super weird for command line popups only
_unicode = isUnicode;
_pdwNumBytes = &numBytes;
Status = CommandLine::Instance().ProcessCommandLine(*this, wch, keyState);
if (Status == CONSOLE_STATUS_READ_COMPLETE || Status == CONSOLE_STATUS_WAIT)
{
break;
}
if (!NT_SUCCESS(Status))
{
if (Status == CONSOLE_STATUS_WAIT_NO_BLOCK)
{
Status = CONSOLE_STATUS_WAIT;
}
else
{
_bytesRead = 0;
}
break;
}
}
else
{
if (ProcessInput(wch, keyState, Status))
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP;
break;
}
}
}
return Status;
}
// Routine Description:
// - handles any tasks that need to be completed after the read input loop finishes
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// - controlKeyState - For some types of reads, this is the modifier key state with the last button press.
// Return Value:
// - Status code that indicates success, out of memory, etc.
[[nodiscard]] NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept
{
DWORD LineCount = 1;
if (_echoInput)
{
// Figure out where real string ends (at carriage return or end of buffer).
PWCHAR StringPtr = _backupLimit;
size_t StringLength = _bytesRead;
bool FoundCR = false;
for (size_t i = 0; i < (_bytesRead / sizeof(WCHAR)); i++)
{
if (*StringPtr++ == UNICODE_CARRIAGERETURN)
{
StringLength = i * sizeof(WCHAR);
FoundCR = true;
break;
}
}
if (FoundCR)
{
if (_commandHistory)
{
// add to command line recall list if we have a history list.
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, StringLength / sizeof(wchar_t) },
WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
}
Tracing::s_TraceCookedRead(_backupLimit);
// check for alias
ProcessAliases(LineCount);
}
}
bool fAddDbcsLead = false;
size_t NumBytes = 0;
// at this point, a->NumBytes contains the number of bytes in
// the UNICODE string read. UserBufferSize contains the converted
// size of the app's buffer.
if (_bytesRead > _userBufferSize || LineCount > 1)
{
if (LineCount > 1)
{
PWSTR Tmp;
if (!isUnicode)
{
if (_pInputBuffer->IsReadPartialByteSequenceAvailable())
{
fAddDbcsLead = true;
std::unique_ptr<IInputEvent> event = GetInputBuffer()->FetchReadPartialByteSequence(false);
const KeyEvent* const pKeyEvent = static_cast<const KeyEvent* const>(event.get());
*_userBuffer = static_cast<char>(pKeyEvent->GetCharData());
_userBuffer++;
_userBufferSize -= sizeof(wchar_t);
}
NumBytes = 0;
for (Tmp = _backupLimit;
*Tmp != UNICODE_LINEFEED && _userBufferSize / sizeof(WCHAR) > NumBytes;
Tmp++)
{
NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
}
}
// clang-format off
#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "LineCount > 1 means there's a UNICODE_LINEFEED")
// clang-format on
for (Tmp = _backupLimit; *Tmp != UNICODE_LINEFEED; Tmp++)
{
FAIL_FAST_IF(!(Tmp < (_backupLimit + _bytesRead)));
}
numBytes = (ULONG)(Tmp - _backupLimit + 1) * sizeof(*Tmp);
}
else
{
if (!isUnicode)
{
PWSTR Tmp;
if (_pInputBuffer->IsReadPartialByteSequenceAvailable())
{
fAddDbcsLead = true;
std::unique_ptr<IInputEvent> event = GetInputBuffer()->FetchReadPartialByteSequence(false);
const KeyEvent* const pKeyEvent = static_cast<const KeyEvent* const>(event.get());
*_userBuffer = static_cast<char>(pKeyEvent->GetCharData());
_userBuffer++;
_userBufferSize -= sizeof(wchar_t);
}
NumBytes = 0;
size_t NumToWrite = _bytesRead;
for (Tmp = _backupLimit;
NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes;
Tmp++, NumToWrite -= sizeof(WCHAR))
{
NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
}
}
numBytes = _userBufferSize;
}
__analysis_assume(numBytes <= _userBufferSize);
memmove(_userBuffer, _backupLimit, numBytes);
INPUT_READ_HANDLE_DATA* const pInputReadHandleData = GetInputReadHandleData();
const std::wstring_view pending{ _backupLimit + (numBytes / sizeof(wchar_t)), (_bytesRead - numBytes) / sizeof(wchar_t) };
if (LineCount > 1)
{
pInputReadHandleData->SaveMultilinePendingInput(pending);
}
else
{
pInputReadHandleData->SavePendingInput(pending);
}
}
else
{
if (!isUnicode)
{
PWSTR Tmp;
if (_pInputBuffer->IsReadPartialByteSequenceAvailable())
{
fAddDbcsLead = true;
std::unique_ptr<IInputEvent> event = GetInputBuffer()->FetchReadPartialByteSequence(false);
const KeyEvent* const pKeyEvent = static_cast<const KeyEvent* const>(event.get());
*_userBuffer = static_cast<char>(pKeyEvent->GetCharData());
_userBuffer++;
_userBufferSize -= sizeof(wchar_t);
if (_userBufferSize == 0)
{
numBytes = 1;
return STATUS_SUCCESS;
}
}
NumBytes = 0;
size_t NumToWrite = _bytesRead;
for (Tmp = _backupLimit;
NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes;
Tmp++, NumToWrite -= sizeof(WCHAR))
{
NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
}
}
numBytes = _bytesRead;
if (numBytes > _userBufferSize)
{
return STATUS_BUFFER_OVERFLOW;
}
memmove(_userBuffer, _backupLimit, numBytes);
}
controlKeyState = _controlKeyState;
if (!isUnicode)
{
// if ansi, translate string.
std::unique_ptr<char[]> tempBuffer;
try
{
tempBuffer = std::make_unique<char[]>(NumBytes);
}
catch (...)
{
return STATUS_NO_MEMORY;
}
std::unique_ptr<IInputEvent> partialEvent;
numBytes = TranslateUnicodeToOem(_userBuffer,
gsl::narrow<ULONG>(numBytes / sizeof(wchar_t)),
tempBuffer.get(),
gsl::narrow<ULONG>(NumBytes),
partialEvent);
if (partialEvent.get())
{
GetInputBuffer()->StoreReadPartialByteSequence(std::move(partialEvent));
}
if (numBytes > _userBufferSize)
{
return STATUS_BUFFER_OVERFLOW;
}
memmove(_userBuffer, tempBuffer.get(), numBytes);
if (fAddDbcsLead)
{
numBytes++;
}
}
return STATUS_SUCCESS;
}
void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer)
{
// See the comment in WaitBlock.cpp for more information.
if (_userBuffer == reinterpret_cast<const wchar_t*>(oldBuffer))
{
_userBuffer = reinterpret_cast<wchar_t*>(newBuffer);
}
}