terminal/src/host/stream.cpp

814 lines
32 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "_stream.h"
#include "stream.h"
#include "dbcs.h"
#include "handle.h"
#include "misc.h"
#include "readDataRaw.hpp"
#include "ApiRoutines.h"
#include "../types/inc/GlyphWidth.hpp"
#include "../interactivity/inc/ServiceLocator.hpp"
#pragma hdrstop
using Microsoft::Console::Interactivity::ServiceLocator;
// Routine Description:
// - This routine is used in stream input. It gets input and filters it for unicode characters.
// Arguments:
// - pInputBuffer - The InputBuffer to read from
// - pwchOut - On a successful read, the char data read
// - Wait - true if a waited read should be performed
// - pCommandLineEditingKeys - if present, arrow keys will be
// returned. on output, if true, pwchOut contains virtual key code for
// arrow key.
// - pPopupKeys - if present, arrow keys will be
// returned. on output, if true, pwchOut contains virtual key code for
// arrow key.
// Return Value:
// - STATUS_SUCCESS on success or a relevant error code on failure.
[[nodiscard]] NTSTATUS GetChar(_Inout_ InputBuffer* const pInputBuffer,
_Out_ wchar_t* const pwchOut,
const bool Wait,
_Out_opt_ bool* const pCommandLineEditingKeys,
_Out_opt_ bool* const pPopupKeys,
_Out_opt_ DWORD* const pdwKeyState) noexcept
{
if (nullptr != pCommandLineEditingKeys)
{
*pCommandLineEditingKeys = false;
}
if (nullptr != pPopupKeys)
{
*pPopupKeys = false;
}
if (nullptr != pdwKeyState)
{
*pdwKeyState = 0;
}
NTSTATUS Status;
for (;;)
{
std::unique_ptr<IInputEvent> inputEvent;
Status = pInputBuffer->Read(inputEvent,
false, // peek
Wait,
true, // unicode
true); // stream
if (!NT_SUCCESS(Status))
{
return Status;
}
else if (inputEvent.get() == nullptr)
{
FAIL_FAST_IF(Wait);
return STATUS_UNSUCCESSFUL;
}
if (inputEvent->EventType() == InputEventType::KeyEvent)
{
std::unique_ptr<KeyEvent> keyEvent = std::unique_ptr<KeyEvent>(static_cast<KeyEvent*>(inputEvent.release()));
bool commandLineEditKey = false;
if (pCommandLineEditingKeys)
{
commandLineEditKey = keyEvent->IsCommandLineEditingKey();
}
else if (pPopupKeys)
{
commandLineEditKey = keyEvent->IsPopupKey();
}
if (pdwKeyState)
{
*pdwKeyState = keyEvent->GetActiveModifierKeys();
}
if (keyEvent->GetCharData() != 0 && !commandLineEditKey)
{
// chars that are generated using alt + numpad
if (!keyEvent->IsKeyDown() && keyEvent->GetVirtualKeyCode() == VK_MENU)
{
if (keyEvent->IsAltNumpadSet())
{
if (HIBYTE(keyEvent->GetCharData()))
{
char chT[2] = {
static_cast<char>(HIBYTE(keyEvent->GetCharData())),
static_cast<char>(LOBYTE(keyEvent->GetCharData())),
};
*pwchOut = CharToWchar(chT, 2);
}
else
{
// Because USER doesn't know our codepage,
// it gives us the raw OEM char and we
// convert it to a Unicode character.
char chT = LOBYTE(keyEvent->GetCharData());
*pwchOut = CharToWchar(&chT, 1);
}
}
else
{
*pwchOut = keyEvent->GetCharData();
}
return STATUS_SUCCESS;
}
// Ignore Escape and Newline chars
else if (keyEvent->IsKeyDown() &&
(WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT) ||
(keyEvent->GetVirtualKeyCode() != VK_ESCAPE &&
keyEvent->GetCharData() != UNICODE_LINEFEED)))
{
*pwchOut = keyEvent->GetCharData();
return STATUS_SUCCESS;
}
}
if (keyEvent->IsKeyDown())
{
if (pCommandLineEditingKeys && commandLineEditKey)
{
*pCommandLineEditingKeys = true;
*pwchOut = static_cast<wchar_t>(keyEvent->GetVirtualKeyCode());
return STATUS_SUCCESS;
}
else if (pPopupKeys && commandLineEditKey)
{
*pPopupKeys = true;
*pwchOut = static_cast<char>(keyEvent->GetVirtualKeyCode());
return STATUS_SUCCESS;
}
else
{
const short zeroVkeyData = ServiceLocator::LocateInputServices()->VkKeyScanW(0);
const byte zeroVKey = LOBYTE(zeroVkeyData);
const byte zeroControlKeyState = HIBYTE(zeroVkeyData);
try
{
// Convert real Windows NT modifier bit into bizarre Console bits
std::unordered_set<ModifierKeyState> consoleModKeyState = FromVkKeyScan(zeroControlKeyState);
if (zeroVKey == keyEvent->GetVirtualKeyCode() &&
keyEvent->DoActiveModifierKeysMatch(consoleModKeyState))
{
// This really is the character 0x0000
*pwchOut = keyEvent->GetCharData();
return STATUS_SUCCESS;
}
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
}
}
}
}
}
}
// Routine Description:
// - This routine returns the total number of screen spaces the characters up to the specified character take up.
size_t RetrieveTotalNumberOfSpaces(const SHORT sOriginalCursorPositionX,
_In_reads_(ulCurrentPosition) const WCHAR* const pwchBuffer,
_In_ size_t ulCurrentPosition)
{
SHORT XPosition = sOriginalCursorPositionX;
size_t NumSpaces = 0;
for (size_t i = 0; i < ulCurrentPosition; i++)
{
WCHAR const Char = pwchBuffer[i];
size_t NumSpacesForChar;
if (Char == UNICODE_TAB)
{
NumSpacesForChar = NUMBER_OF_SPACES_IN_TAB(XPosition);
}
else if (IS_CONTROL_CHAR(Char))
{
NumSpacesForChar = 2;
}
else if (IsGlyphFullWidth(Char))
{
NumSpacesForChar = 2;
}
else
{
NumSpacesForChar = 1;
}
XPosition = (SHORT)(XPosition + NumSpacesForChar);
NumSpaces += NumSpacesForChar;
}
return NumSpaces;
}
// Routine Description:
// - This routine returns the number of screen spaces the specified character takes up.
size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX,
_In_reads_(ulCurrentPosition + 1) const WCHAR* const pwchBuffer,
_In_ size_t ulCurrentPosition)
{
WCHAR Char = pwchBuffer[ulCurrentPosition];
if (Char == UNICODE_TAB)
{
size_t NumSpaces = 0;
SHORT XPosition = sOriginalCursorPositionX;
for (size_t i = 0; i <= ulCurrentPosition; i++)
{
Char = pwchBuffer[i];
if (Char == UNICODE_TAB)
{
NumSpaces = NUMBER_OF_SPACES_IN_TAB(XPosition);
}
else if (IS_CONTROL_CHAR(Char))
{
NumSpaces = 2;
}
else if (IsGlyphFullWidth(Char))
{
NumSpaces = 2;
}
else
{
NumSpaces = 1;
}
XPosition = (SHORT)(XPosition + NumSpaces);
}
return NumSpaces;
}
else if (IS_CONTROL_CHAR(Char))
{
return 2;
}
else if (IsGlyphFullWidth(Char))
{
return 2;
}
else
{
return 1;
}
}
// Routine Description:
// - if we have leftover input, copy as much fits into the user's
// buffer and return. we may have multi line input, if a macro
// has been defined that contains the $T character.
// Arguments:
// - inputBuffer - Pointer to input buffer to read from.
// - buffer - buffer to place read char data into
// - bytesRead - number of bytes read and filled into the buffer
// - readHandleState - input read handle data associated with this read operation
// - unicode - true if read should be unicode, false otherwise
// Return Value:
// - STATUS_NO_MEMORY in low memory situation
// - other relevant NTSTATUS codes
[[nodiscard]] static NTSTATUS _ReadPendingInput(InputBuffer& inputBuffer,
gsl::span<char> buffer,
size_t& bytesRead,
INPUT_READ_HANDLE_DATA& readHandleState,
const bool unicode)
{
// TODO: MSFT: 18047766 - Correct this method to not play byte counting games.
BOOL fAddDbcsLead = FALSE;
size_t NumToWrite = 0;
size_t NumToBytes = 0;
wchar_t* pBuffer = reinterpret_cast<wchar_t*>(buffer.data());
size_t bufferRemaining = buffer.size_bytes();
bytesRead = 0;
if (buffer.size_bytes() < sizeof(wchar_t))
{
return STATUS_BUFFER_TOO_SMALL;
}
const auto pending = readHandleState.GetPendingInput();
size_t pendingBytes = pending.size() * sizeof(wchar_t);
auto Tmp = pending.cbegin();
if (readHandleState.IsMultilineInput())
{
if (!unicode)
{
if (inputBuffer.IsReadPartialByteSequenceAvailable())
{
std::unique_ptr<IInputEvent> event = inputBuffer.FetchReadPartialByteSequence(false);
const KeyEvent* const pKeyEvent = static_cast<const KeyEvent* const>(event.get());
*pBuffer = static_cast<char>(pKeyEvent->GetCharData());
++pBuffer;
bufferRemaining -= sizeof(wchar_t);
pendingBytes -= sizeof(wchar_t);
fAddDbcsLead = TRUE;
}
if (pendingBytes == 0 || bufferRemaining == 0)
{
readHandleState.CompletePending();
bytesRead = 1;
return STATUS_SUCCESS;
}
else
{
for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0;
NumToBytes < pendingBytes &&
NumToBytes < bufferRemaining / sizeof(wchar_t) &&
*Tmp != UNICODE_LINEFEED;
Tmp++, NumToWrite += sizeof(wchar_t))
{
NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
}
}
}
NumToWrite = 0;
Tmp = pending.cbegin();
while (NumToWrite < pendingBytes &&
*Tmp != UNICODE_LINEFEED)
{
++Tmp;
NumToWrite += sizeof(wchar_t);
}
NumToWrite += sizeof(wchar_t);
if (NumToWrite > bufferRemaining)
{
NumToWrite = bufferRemaining;
}
}
else
{
if (!unicode)
{
if (inputBuffer.IsReadPartialByteSequenceAvailable())
{
std::unique_ptr<IInputEvent> event = inputBuffer.FetchReadPartialByteSequence(false);
const KeyEvent* const pKeyEvent = static_cast<const KeyEvent* const>(event.get());
*pBuffer = static_cast<char>(pKeyEvent->GetCharData());
++pBuffer;
bufferRemaining -= sizeof(wchar_t);
pendingBytes -= sizeof(wchar_t);
fAddDbcsLead = TRUE;
}
if (pendingBytes == 0)
{
readHandleState.CompletePending();
bytesRead = 1;
return STATUS_SUCCESS;
}
else
{
for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0;
NumToBytes < pendingBytes && NumToBytes < bufferRemaining / sizeof(wchar_t);
Tmp++, NumToWrite += sizeof(wchar_t))
{
NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
}
}
}
NumToWrite = (bufferRemaining < pendingBytes) ? bufferRemaining : pendingBytes;
}
memmove(pBuffer, pending.data(), NumToWrite);
pendingBytes -= NumToWrite;
if (pendingBytes != 0)
{
std::wstring_view remainingPending{ pending.data() + (NumToWrite / sizeof(wchar_t)), pendingBytes / sizeof(wchar_t) };
readHandleState.UpdatePending(remainingPending);
}
else
{
readHandleState.CompletePending();
}
if (!unicode)
{
// if ansi, translate string. we allocated the capture buffer
// large enough to handle the translated string.
std::unique_ptr<char[]> tempBuffer = std::make_unique<char[]>(NumToBytes);
std::unique_ptr<IInputEvent> partialEvent;
NumToWrite = TranslateUnicodeToOem(pBuffer,
gsl::narrow<ULONG>(NumToWrite / sizeof(wchar_t)),
tempBuffer.get(),
gsl::narrow<ULONG>(NumToBytes),
partialEvent);
if (partialEvent.get())
{
inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent));
}
// clang-format off
#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine but prefast can't follow it, evidently")
// clang-format on
memmove(pBuffer, tempBuffer.get(), NumToWrite);
if (fAddDbcsLead)
{
NumToWrite++;
}
}
bytesRead = NumToWrite;
return STATUS_SUCCESS;
}
// Routine Description:
// - read in characters until the buffer is full or return is read.
// since we may wait inside this loop, store all important variables
// in the read data structure. if we do wait, a read data structure
// will be allocated from the heap and its pointer will be stored
// in the wait block. the CookedReadData will be copied into the
// structure. the data is freed when the read is completed.
// Arguments:
// - inputBuffer - input buffer to read data from
// - processData - process handle of process making read request
// - buffer - buffer to place read char data
// - bytesRead - on output, the number of bytes read into pwchBuffer
// - controlKeyState - set by a cooked read
// - initialData - text of initial data found in the read message
// - ctrlWakeupMask - used by COOKED_READ_DATA
// - readHandleState - input read handle data associated with this read operation
// - exeName - name of the exe requesting the read
// - unicode - true if read should be unicode, false otherwise
// - waiter - If a wait is necessary this will contain the wait
// object on output
// Return Value:
// - STATUS_UNSUCCESSFUL if not able to access current screen buffer
// - STATUS_NO_MEMORY in low memory situation
// - other relevant HRESULT codes
[[nodiscard]] static HRESULT _ReadLineInput(InputBuffer& inputBuffer,
const HANDLE processData,
gsl::span<char> buffer,
size_t& bytesRead,
DWORD& controlKeyState,
const std::string_view initialData,
const DWORD ctrlWakeupMask,
INPUT_READ_HANDLE_DATA& readHandleState,
const std::wstring_view exeName,
const bool unicode,
std::unique_ptr<IWaitRoutine>& waiter) noexcept
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
RETURN_HR_IF(E_FAIL, !gci.HasActiveOutputBuffer());
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
CommandHistory* const pCommandHistory = CommandHistory::s_Find(processData);
try
{
auto cookedReadData = std::make_unique<COOKED_READ_DATA>(&inputBuffer, // pInputBuffer
&readHandleState, // pInputReadHandleData
screenInfo, // pScreenInfo
buffer.size_bytes(), // UserBufferSize
reinterpret_cast<wchar_t*>(buffer.data()), // UserBuffer
ctrlWakeupMask, // CtrlWakeupMask
pCommandHistory, // CommandHistory
exeName, // exe name
initialData);
gci.SetCookedReadData(cookedReadData.get());
bytesRead = buffer.size_bytes(); // This parameter on the way in is the size to read, on the way out, it will be updated to what is actually read.
if (CONSOLE_STATUS_WAIT == cookedReadData->Read(unicode, bytesRead, controlKeyState))
{
// memory will be cleaned up by wait queue
waiter.reset(cookedReadData.release());
}
else
{
gci.SetCookedReadData(nullptr);
}
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - Character (raw) mode. Read at least one character in. After one
// character has been read, get any more available characters and
// return. The first call to GetChar may block. If we do wait, a read
// data structure will be allocated from the heap and its pointer will
// be stored in the wait block. The RawReadData will be copied into
// the structure. The data is freed when the read is completed.
// Arguments:
// - inputBuffer - input buffer to read data from
// - buffer - on output, the amount of data read, in bytes
// - bytesRead - number of bytes read and placed into buffer
// - readHandleState - input read handle data associated with this read operation
// - unicode - true if read should be unicode, false otherwise
// - waiter - if a wait is necessary, on output this will contain
// the associated wait object
// Return Value:
// - CONSOLE_STATUS_WAIT if a wait is necessary. ppWaiter will be
// populated.
// - STATUS_SUCCESS on success
// - Other NTSTATUS codes as necessary
[[nodiscard]] static NTSTATUS _ReadCharacterInput(InputBuffer& inputBuffer,
gsl::span<char> buffer,
size_t& bytesRead,
INPUT_READ_HANDLE_DATA& readHandleState,
const bool unicode,
std::unique_ptr<IWaitRoutine>& waiter)
{
size_t NumToWrite = 0;
bool addDbcsLead = false;
NTSTATUS Status = STATUS_SUCCESS;
wchar_t* pBuffer = reinterpret_cast<wchar_t*>(buffer.data());
size_t bufferRemaining = buffer.size_bytes();
bytesRead = 0;
if (buffer.size() < 1)
{
return STATUS_BUFFER_TOO_SMALL;
}
if (bytesRead < bufferRemaining)
{
wchar_t* pwchBufferTmp = pBuffer;
NumToWrite = 0;
if (!unicode && inputBuffer.IsReadPartialByteSequenceAvailable())
{
std::unique_ptr<IInputEvent> event = inputBuffer.FetchReadPartialByteSequence(false);
const KeyEvent* const pKeyEvent = static_cast<const KeyEvent* const>(event.get());
*pBuffer = static_cast<char>(pKeyEvent->GetCharData());
++pBuffer;
bufferRemaining -= sizeof(wchar_t);
addDbcsLead = true;
if (bufferRemaining == 0)
{
bytesRead = 1;
return STATUS_SUCCESS;
}
}
else
{
Status = GetChar(&inputBuffer,
pBuffer,
true,
nullptr,
nullptr,
nullptr);
}
if (Status == CONSOLE_STATUS_WAIT)
{
waiter = std::make_unique<RAW_READ_DATA>(&inputBuffer,
&readHandleState,
gsl::narrow<ULONG>(buffer.size_bytes()),
reinterpret_cast<wchar_t*>(buffer.data()));
}
if (!NT_SUCCESS(Status))
{
bytesRead = 0;
return Status;
}
if (!addDbcsLead)
{
bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1;
NumToWrite += sizeof(wchar_t);
pBuffer++;
}
while (NumToWrite < static_cast<ULONG>(bufferRemaining))
{
Status = GetChar(&inputBuffer,
pBuffer,
false,
nullptr,
nullptr,
nullptr);
if (!NT_SUCCESS(Status))
{
break;
}
bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1;
NumToWrite += sizeof(wchar_t);
pBuffer++;
}
// if ansi, translate string. we allocated the capture buffer large enough to handle the translated string.
if (!unicode)
{
std::unique_ptr<char[]> tempBuffer;
try
{
tempBuffer = std::make_unique<char[]>(bytesRead);
}
catch (...)
{
return STATUS_NO_MEMORY;
}
pBuffer = pwchBufferTmp;
std::unique_ptr<IInputEvent> partialEvent;
bytesRead = TranslateUnicodeToOem(pBuffer,
gsl::narrow<ULONG>(NumToWrite / sizeof(wchar_t)),
tempBuffer.get(),
gsl::narrow<ULONG>(bytesRead),
partialEvent);
if (partialEvent.get())
{
inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent));
}
#pragma prefast(suppress : 26053 26015, "PREfast claims read overflow. *pReadByteCount is the exact size of tempBuffer as allocated above.")
memmove(pBuffer, tempBuffer.get(), bytesRead);
if (addDbcsLead)
{
++bytesRead;
}
}
else
{
// We always return the byte count for A & W modes, so in
// the Unicode case where we didn't translate back, set
// the return to the byte count that we assembled while
// pulling characters from the internal buffers.
bytesRead = NumToWrite;
}
}
return STATUS_SUCCESS;
}
// Routine Description:
// - This routine reads in characters for stream input and does the
// required processing based on the input mode (line, char, echo).
// - This routine returns UNICODE characters.
// Arguments:
// - inputBuffer - Pointer to input buffer to read from.
// - processData - process handle of process making read request
// - buffer - buffer to place read char data into
// - bytesRead - the length of data placed in buffer. Measured in bytes.
// - controlKeyState - set by a cooked read
// - initialData - text of initial data found in the read message
// - ctrlWakeupMask - used by COOKED_READ_DATA
// - readHandleState - read handle data associated with this read
// - exeName- name of the exe requesting the read
// - unicode - true for a unicode read, false for ascii
// - waiter - If a wait is necessary this will contain the wait
// object on output
// Return Value:
// - STATUS_BUFFER_TOO_SMALL if pdwNumBytes is too small to store char
// data.
// - CONSOLE_STATUS_WAIT if a wait is necessary. ppWaiter will be
// populated.
// - STATUS_SUCCESS on success
// - Other NSTATUS codes as necessary
[[nodiscard]] NTSTATUS DoReadConsole(InputBuffer& inputBuffer,
const HANDLE processData,
gsl::span<char> buffer,
size_t& bytesRead,
ULONG& controlKeyState,
const std::string_view initialData,
const DWORD ctrlWakeupMask,
INPUT_READ_HANDLE_DATA& readHandleState,
const std::wstring_view exeName,
const bool unicode,
std::unique_ptr<IWaitRoutine>& waiter) noexcept
{
try
{
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
waiter.reset();
bytesRead = 0;
if (buffer.size() < 1)
{
return STATUS_BUFFER_TOO_SMALL;
}
const size_t OutputBufferSize = buffer.size_bytes();
if (readHandleState.IsInputPending())
{
return _ReadPendingInput(inputBuffer,
buffer,
bytesRead,
readHandleState,
unicode);
}
else if (WI_IsFlagSet(inputBuffer.InputMode, ENABLE_LINE_INPUT))
{
return NTSTATUS_FROM_HRESULT(_ReadLineInput(inputBuffer,
processData,
buffer,
bytesRead,
controlKeyState,
initialData,
ctrlWakeupMask,
readHandleState,
exeName,
unicode,
waiter));
}
else
{
return _ReadCharacterInput(inputBuffer,
buffer,
bytesRead,
readHandleState,
unicode,
waiter);
}
}
CATCH_RETURN();
}
[[nodiscard]] HRESULT ApiRoutines::ReadConsoleAImpl(IConsoleInputObject& context,
gsl::span<char> buffer,
size_t& written,
std::unique_ptr<IWaitRoutine>& waiter,
const std::string_view initialData,
const std::wstring_view exeName,
INPUT_READ_HANDLE_DATA& readHandleState,
const HANDLE clientHandle,
const DWORD controlWakeupMask,
DWORD& controlKeyState) noexcept
{
try
{
return HRESULT_FROM_NT(DoReadConsole(context,
clientHandle,
buffer,
written,
controlKeyState,
initialData,
controlWakeupMask,
readHandleState,
exeName,
false,
waiter));
}
CATCH_RETURN();
}
[[nodiscard]] HRESULT ApiRoutines::ReadConsoleWImpl(IConsoleInputObject& context,
gsl::span<char> buffer,
size_t& written,
std::unique_ptr<IWaitRoutine>& waiter,
const std::string_view initialData,
const std::wstring_view exeName,
INPUT_READ_HANDLE_DATA& readHandleState,
const HANDLE clientHandle,
const DWORD controlWakeupMask,
DWORD& controlKeyState) noexcept
{
try
{
return HRESULT_FROM_NT(DoReadConsole(context,
clientHandle,
buffer,
written,
controlKeyState,
initialData,
controlWakeupMask,
readHandleState,
exeName,
true,
waiter));
}
CATCH_RETURN();
}
void UnblockWriteConsole(const DWORD dwReason)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.Flags &= ~dwReason;
if (WI_AreAllFlagsClear(gci.Flags, (CONSOLE_SUSPENDED | CONSOLE_SELECTING | CONSOLE_SCROLLBAR_TRACKING)))
{
// There is no longer any reason to suspend output, so unblock it.
gci.OutputQueue.NotifyWaiters(true);
}
}