AuroraRuntime/Source/Console/ConsoleStd/ConsoleStd.cpp

1746 lines
53 KiB
C++
Executable File

/***
Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved.
File: ConsoleStd.cpp
Date: 2021-6-8
Author: Reece
***/
#include <Source/RuntimeInternal.hpp>
#include "ConsoleStd.hpp"
#include <Source/Locale/Locale.hpp>
#include <Source/Console/Console.hpp>
#include <Source/Logging/Sinks/StdConsole.hpp>
#include <Source/IO/Loop/ILoopSourceEx.hpp>
#include <Source/IO/Loop/LSHandle.hpp>
#include <Source/IO/Loop/LSEvent.hpp>
#include <Source/Console/ConsoleTTY/ConsoleTTY.hpp>
#include "../ColorConvert.hpp"
#if defined(AURORA_IS_MODERNNT_DERIVED) || defined(AURORA_IS_POSIX_DERIVED)
#if defined(AURORA_IS_MODERNNT_DERIVED)
// nothing yet
#elif defined(AURORA_IS_POSIX_DERIVED)
#define IO_POSIX_STREAMS
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
#include <termios.h>
#endif
// compile time preprocessor definition to strip std stream based IO
#if !(defined(AURORA_DISABLE_STD_CONSOLE))
#define ENABLE_STD_CONSOLE
#endif
#endif
namespace Aurora::Console::ConsoleStd
{
#if defined(ENABLE_STD_CONSOLE)
struct ConsoleHasDataLoopSource :
#if defined(AURORA_IS_MODERNNT_DERIVED)
virtual AuLoop::ILoopSource,
#endif
virtual AuLoop::LSEvent
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
ConsoleHasDataLoopSource() : LSEvent(false, false, true)
{
}
#else
ConsoleHasDataLoopSource(int fd) : LSEvent(false, false, true)
{
handles = { AuUInt(fd), LSEvent::GetHandle() };
}
#endif
bool IsSignaled() override;
AuLoop::ELoopSource GetType() override;
bool OnTrigger(AuUInt handle) override;
bool WaitOn(AuUInt32 timeout) override;
private:
#if !defined(AURORA_IS_MODERNNT_DERIVED)
AuList<AuUInt> handles;
virtual const AuList<AuUInt>& GetHandles() override;
virtual bool Singular() override;
#endif
};
struct ConsoleSecondaryDataLoopSource :
virtual AuLoop::ILoopSource,
virtual AuLoop::LSEvent
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
ConsoleSecondaryDataLoopSource(LSEvent *pAltHandle) : LSEvent(false, true, true)
{
handles = { /*pAltHandle->GetHandle()*/AuUInt(GetStdHandle(STD_INPUT_HANDLE)), LSEvent::GetHandle()};
}
#else
ConsoleSecondaryDataLoopSource(int fd) : LSEvent(false, true, true)
{
handles = { AuUInt(fd), LSEvent::GetHandle() };
}
#endif
bool IsSignaled() override;
AuLoop::ELoopSource GetType() override;
bool OnTrigger(AuUInt handle) override;
bool WaitOn(AuUInt32 timeout) override;
private:
AuList<AuUInt> handles;
virtual const AuList<AuUInt>& GetHandles() override;
virtual bool Singular() override;
};
static AuByteBuffer gStdoutBuffer;
#if defined(AURORA_IS_MODERNNT_DERIVED)
#define DEFAULT_HANDLE_VAL INVALID_HANDLE_VALUE
using StreamHandle_t = HANDLE;
static StreamHandle_t gWin32Thread = INVALID_HANDLE_VALUE;
static DWORD gCanonicalBackup {};
#elif defined(IO_POSIX_STREAMS)
#define DEFAULT_HANDLE_VAL 0xFFFFFFFF
using StreamHandle_t = int;
#endif
#define IS_STREAM_HANDLE_VALID(h) (h != DEFAULT_HANDLE_VAL)
static bool AsyncReadAnyOrReadStreamBlock();
void WriteStdOut(AuUInt8 level, const ConsoleMessage &msg, const AuString &formatted);
static const AuMach kLineBufferMax = 2048;
static AuUInt8 gLineEncodedBuffer[kLineBufferMax];
static AuUInt gEncodedIndex = 0;
static AuString gLineBuffer(kLineBufferMax, 0);
static AuUInt gLineIndex = 0;
static StreamHandle_t gTerminateConsole;
static StreamHandle_t gInputStream = DEFAULT_HANDLE_VAL;
static StreamHandle_t gOutputStream = DEFAULT_HANDLE_VAL;
static AuSPtr<ConsoleHasDataLoopSource> gLoopSource;
static AuSPtr<ConsoleSecondaryDataLoopSource> gLoopSourceConTTY;
static bool gCanonicalEnabled {};
static AuList<NoncanonicalInput> gCanonicalBuffer;
//static AuThreadPrimitives::MutexUnique_t gRingLock = AuThreadPrimitives::MutexUnique();
static AuThreadPrimitives::SpinLock gRingLock;// = AuThreadPrimitives::MutexUnique();
static bool gBufferMode {};
static bool gCanonicalUnixOn {};
#if defined(AURORA_IS_MODERNNT_DERIVED)
}
#include "../ConsoleTTY/ConsoleTTY.hpp"
#include "../ConsoleTTY/ConsoleTTY.NT.hpp"
namespace Aurora::Console::ConsoleStd
{
static DWORD WINAPI StdInWin32Thread(void*)
{
HANDLE a[2] = {gInputStream, gTerminateConsole};
while (!gCanonicalEnabled)
{
WaitForMultipleObjectsEx(2, a, false, 25, 0);
if (WaitForSingleObject(gTerminateConsole, 0) == WAIT_OBJECT_0)
{
break;
}
AsyncReadAnyOrReadStreamBlock();
}
return 1;
}
static void PumpClipboardCheck()
{
static AuUInt64 gLastHash {};
AuString str;
if (!pOpenClipboard ||
!pCloseClipboard ||
!pGetClipboardData)
{
return;
}
if (!pOpenClipboard(nullptr))
{
return;
}
auto hClipboardData = pGetClipboardData(CF_UNICODETEXT);
if (hClipboardData == nullptr)
{
pCloseClipboard();
return;
}
auto pLpwStr = (LPWSTR)GlobalLock(hClipboardData);
if (pLpwStr != nullptr)
{
str = AuLocale::ConvertFromWChar(pLpwStr);
::GlobalUnlock(hClipboardData);
}
auto newHash = AuFnv1a64Runtime(str.data(), str.length());
if (newHash != gLastHash)
{
if (gLastHash)
{
ConsoleTTY::EnterScrollMode();
}
gLastHash = newHash;
}
pCloseClipboard();
}
static void ProcessLines(AuList<AuString> &lines);
static void TryPasteFromClipboard()
{
AuString str;
if (!pOpenClipboard ||
!pCloseClipboard ||
!pGetClipboardData)
{
return;
}
if (!pOpenClipboard(nullptr))
{
return;
}
auto hClipboardData = pGetClipboardData(CF_UNICODETEXT);
if (hClipboardData == nullptr)
{
pCloseClipboard();
return;
}
auto pLpwStr = (LPWSTR)GlobalLock(hClipboardData);
if (pLpwStr != nullptr)
{
str = AuLocale::ConvertFromWChar(pLpwStr);
::GlobalUnlock(hClipboardData);
}
{
auto remaining = AuParse::SplitNewlines(str, [](const AuString &line)
{
NoncanonicalInput canInput {};
canInput.type = ENoncanonicalInput::eInput;
canInput.string = line;
if (auto uMax = ::strnlen(line.data(), line.size()))
{
canInput.string.resize(uMax);
}
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
canInput.type = ENoncanonicalInput::eEnter;
canInput.string.clear();
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}, true);
if (remaining.size())
{
NoncanonicalInput canInput {};
canInput.type = ENoncanonicalInput::eInput;
canInput.string = remaining;
if (auto uMax = ::strnlen(remaining.data(), remaining.size()))
{
canInput.string.resize(uMax);
}
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
}
pCloseClipboard();
}
void ProcessCanonical(HANDLE h)
{
INPUT_RECORD records[4096];
DWORD dwRecords;
void *data = &gLineEncodedBuffer[gEncodedIndex];
auto length = kLineBufferMax - gEncodedIndex;
PumpClipboardCheck();
if (!IS_STREAM_HANDLE_VALID(h))
{
h = gInputStream;
}
if (length == 0)
{
return;
}
if (!GetNumberOfConsoleInputEvents(h,
&dwRecords))
{
return;
}
dwRecords = AuMin(dwRecords, DWORD(AuArraySize(records)));
if (!dwRecords)
{
return;
}
if (!ReadConsoleInputW(h,
records,
dwRecords,
&dwRecords))
{
return;
}
AU_LOCK_GUARD(gRingLock);
gCanonicalBuffer.reserve(4096);
if (dwRecords)
{
if (gLoopSource)
{
gLoopSource->Set();
}
}
for (auto i = 0u; i < dwRecords; i++)
{
int z;
bool dBreak = false;
NoncanonicalInput canInput;
canInput.type = ENoncanonicalInput::eEnumInvalid;
canInput.iScrollDeltaY = 0;
auto &record = records[i];
AuString key;
switch (record.EventType)
{
case KEY_EVENT:
{
if (!record.Event.KeyEvent.bKeyDown)
{
dBreak = true;
break;
}
switch (record.Event.KeyEvent.wVirtualKeyCode)
{
case VK_UP:
canInput.type = ENoncanonicalInput::eArrowUp;
break;
case VK_DOWN:
canInput.type = ENoncanonicalInput::eArrowDown;
break;
case VK_LEFT:
canInput.type = ENoncanonicalInput::eArrowLeft;
break;
case VK_RIGHT:
canInput.type = ENoncanonicalInput::eArrowRight;
break;
case VK_HOME:
canInput.type = ENoncanonicalInput::eHome;
break;
case VK_BACK:
canInput.type = ENoncanonicalInput::eBackspace;
break;
case VK_PRIOR:
canInput.type = ENoncanonicalInput::ePageUp;
break;
case VK_NEXT:
canInput.type = ENoncanonicalInput::ePageDown;
break;
case VK_RETURN:
canInput.type = ENoncanonicalInput::eEnter;
break;
case VK_TAB:
canInput.type = ENoncanonicalInput::eTab;
break;
case VK_DELETE:
canInput.type = ENoncanonicalInput::eDelete;
break;
case VK_ESCAPE:
canInput.type = ENoncanonicalInput::eEscape;
break;
case VK_END:
canInput.type = ENoncanonicalInput::eEnd;
break;
case VK_F1:
canInput.type = ENoncanonicalInput::eFunction1;
break;
case VK_F2:
canInput.type = ENoncanonicalInput::eFunction2;
break;
case VK_F3:
canInput.type = ENoncanonicalInput::eFunction3;
break;
case VK_F4:
canInput.type = ENoncanonicalInput::eFunction4;
break;
case VK_F5:
canInput.type = ENoncanonicalInput::eFunction5;
break;
case VK_F6:
canInput.type = ENoncanonicalInput::eFunction6;
break;
case VK_F7:
canInput.type = ENoncanonicalInput::eFunction7;
break;
case VK_F8:
canInput.type = ENoncanonicalInput::eFunction8;
break;
case VK_F9:
canInput.type = ENoncanonicalInput::eFunction9;
break;
case VK_F10:
canInput.type = ENoncanonicalInput::eFunction10;
break;
case VK_F11:
canInput.type = ENoncanonicalInput::eFunction11;
break;
case VK_F12:
canInput.type = ENoncanonicalInput::eFunction12;
break;
case VK_NUMPAD0:
case VK_NUMPAD1:
case VK_NUMPAD2:
case VK_NUMPAD3:
case VK_NUMPAD4:
case VK_NUMPAD5:
case VK_NUMPAD6:
case VK_NUMPAD7:
case VK_NUMPAD8:
case VK_NUMPAD9:
{
canInput.type = ENoncanonicalInput::eInput;
canInput.string = '0' + (record.Event.KeyEvent.wVirtualKeyCode - VK_NUMPAD0);
break;
}
case VK_MULTIPLY:
{
canInput.type = ENoncanonicalInput::eInput;
canInput.string = '*';
break;
}
case VK_ADD:
{
canInput.type = ENoncanonicalInput::eInput;
canInput.string = '+';
break;
}
case VK_SUBTRACT:
{
canInput.type = ENoncanonicalInput::eInput;
canInput.string = '-';
break;
}
case VK_DIVIDE:
{
canInput.type = ENoncanonicalInput::eInput;
canInput.string = '/';
break;
}
case VK_DECIMAL:
{
canInput.type = ENoncanonicalInput::eInput;
canInput.string = '.';
break;
}
default:
dBreak = true;
}
canInput.bIsControlSequence = record.Event.KeyEvent.dwControlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED);
canInput.bIsAltSequence = record.Event.KeyEvent.dwControlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED);
canInput.bIsShiftSequence = record.Event.KeyEvent.dwControlKeyState & (SHIFT_PRESSED);
// TODO: numpad
if (!dBreak || ((record.Event.KeyEvent.wVirtualKeyCode <= VK_HELP &&
record.Event.KeyEvent.wVirtualKeyCode != VK_SPACE &&
(record.Event.KeyEvent.wVirtualKeyCode < VK_NUMPAD0 || record.Event.KeyEvent.wVirtualKeyCode > VK_NUMPAD9))))
{
}
else
{
canInput.type = ENoncanonicalInput::eInput;
key = AuLocale::ConvertFromWChar(&record.Event.KeyEvent.uChar.UnicodeChar, 1);
if (key.empty())
{
return;
}
for (z = 0; z < record.Event.KeyEvent.wRepeatCount; z++)
{
key.resize(::strnlen(key.data(), key.size()));
canInput.string += key;
if (key.size())
{
dBreak = false;
}
}
}
break;
}
case MOUSE_EVENT:
{
dBreak = true;
if (record.Event.MouseEvent.dwEventFlags == MOUSE_WHEELED)
{
canInput.type = ENoncanonicalInput::eScroll;
canInput.iScrollDeltaY = AuStaticCast<AuInt16>(AuBitsToHigher((AuUInt32)record.Event.MouseEvent.dwButtonState));
if (canInput.iScrollDeltaY > 1)
canInput.iScrollDeltaY = 1;
else if (canInput.iScrollDeltaY < -1)
canInput.iScrollDeltaY = -1;
dBreak = false;
}
else if ((record.Event.MouseEvent.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) != 0)
{
ConsoleTTY::LeaveScrollMode();
}
else if ((record.Event.MouseEvent.dwButtonState & RIGHTMOST_BUTTON_PRESSED) != 0 && record.Event.MouseEvent.dwEventFlags != 1)
{
TryPasteFromClipboard();
}
else
{
// NOOP
}
break;
}
default:
{
dBreak = true;
break;
}
};
if (dBreak)
{
continue;
}
if (!AuTryInsert(gCanonicalBuffer, canInput))
{
return;
}
}
}
void NoncanonicalTick()
{
ProcessCanonical(INVALID_HANDLE_VALUE);
}
void SignalKillNT()
{
if (IS_STREAM_HANDLE_VALID(gWin32Thread) &&
pCancelSynchronousIo)
{
pCancelSynchronousIo(gWin32Thread);
}
if (IS_STREAM_HANDLE_VALID(gTerminateConsole))
{
SetEvent(gTerminateConsole);
}
if (WaitForSingleObject(gWin32Thread, 200) != WAIT_OBJECT_0)
{
TerminateThread(gWin32Thread, 0);
}
AuWin32CloseHandle(gWin32Thread);
}
bool EnterNoncanonicalMode()
{
DWORD mode;
if (!GetConsoleMode(gInputStream, &mode))
{
return false;
}
gCanonicalBackup = mode;
mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_QUICK_EDIT_MODE);
mode |= ENABLE_PROCESSED_INPUT | ENABLE_MOUSE_INPUT | ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS;
SetConsoleMode(gInputStream, mode);
gCanonicalEnabled = true;
SignalKillNT();
INPUT_RECORD niceWorkMicrosoft[2] {};
niceWorkMicrosoft[0].EventType = KEY_EVENT;
niceWorkMicrosoft[0].Event.KeyEvent.bKeyDown = TRUE;
niceWorkMicrosoft[0].Event.KeyEvent.dwControlKeyState = 0;
niceWorkMicrosoft[0].Event.KeyEvent.wRepeatCount = 1;
niceWorkMicrosoft[0].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
niceWorkMicrosoft[0].Event.KeyEvent.uChar.UnicodeChar = '\r';
niceWorkMicrosoft[0].Event.KeyEvent.wVirtualScanCode = pMapVirtualKeyA(VK_RETURN, MAPVK_VK_TO_VSC);
niceWorkMicrosoft[1].EventType = KEY_EVENT;
niceWorkMicrosoft[1].Event.KeyEvent.bKeyDown = FALSE;
niceWorkMicrosoft[1].Event.KeyEvent.dwControlKeyState = 0;
niceWorkMicrosoft[1].Event.KeyEvent.wRepeatCount = 1;
niceWorkMicrosoft[1].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
niceWorkMicrosoft[1].Event.KeyEvent.uChar.UnicodeChar = '\r';
niceWorkMicrosoft[1].Event.KeyEvent.wVirtualScanCode = pMapVirtualKeyA(VK_RETURN, MAPVK_VK_TO_VSC);
DWORD idc;
WriteConsoleInputW(gInputStream, niceWorkMicrosoft, 2, &idc);
WriteConsoleInputW(gInputStream, niceWorkMicrosoft, 2, &idc);
return true;
}
void LeaveNoncanonicalMode()
{
if (!AuExchange(gCanonicalEnabled, false))
{
return;
}
SetConsoleMode(gInputStream, gCanonicalBackup);
gWin32Thread = CreateThread(nullptr, 0, StdInWin32Thread, nullptr, 0, nullptr);
}
#else
struct termios gTermSavedAttributes;
bool EnterNoncanonicalMode()
{
struct termios tattr;
::tcgetattr(STDIN_FILENO, &gTermSavedAttributes);
::tcgetattr(STDIN_FILENO, &tattr);
::cfmakeraw(&tattr);
tattr.c_iflag |= BRKINT | IGNBRK;//~(BRKINT | IGNBRK );//ISIG | BRKINT | VINTR;
tattr.c_lflag |= ISIG ;//~(ISIG | IEXTEN );//ISIG | BRKINT | VINTR;
tattr.c_cc[VMIN] = 1;
tattr.c_cc[VTIME] = 0;
if (::tcsetattr(STDIN_FILENO, TCSANOW, &tattr))
{
return false;
}
gCanonicalUnixOn = true;
return true;
}
void LeaveNoncanonicalMode()
{
::tcsetattr(STDIN_FILENO, TCSANOW, &gTermSavedAttributes);
}
static void ProcessLinesSysCP();
static void FlushStdOut();
static bool AsyncReadAnyOrReadStreamBlock();
static bool InputStreamAvailable();
static AuUInt32 SyncReadConsole();
void NoncanonicalTick()
{
FlushStdOut();
if (!InputStreamAvailable())
{
return;
}
SyncReadConsole();
ProcessLinesSysCP();
}
#endif
bool IsStdOutTTY()
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
HANDLE hConsole;
DWORD dwType;
hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
if (!(dwType = GetFileType(hConsole)))
{
return false;
}
return dwType == FILE_TYPE_CHAR;
#else
return IsStdOutTTY(STDOUT_FILENO);
#endif
}
bool IsStdOutTTY(AuUInt handle)
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
HANDLE hConsole {(HANDLE)handle};
DWORD dwType;
if (handle == 0 || (AuUInt)INVALID_HANDLE_VALUE == handle)
{
return false;
}
if (!(dwType = GetFileType(hConsole)))
{
return false;
}
return dwType == FILE_TYPE_CHAR;
#else
return ::isatty((int)handle);
#endif
}
AuList<NoncanonicalInput> DequeueNoncanonicalInput()
{
AU_LOCK_GUARD(gRingLock);
if (gLoopSource)
{
gLoopSource->Reset();
}
return AuExchange(gCanonicalBuffer, {});
}
bool ConsoleHasDataLoopSource::IsSignaled()
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
return gLineIndex || (gRuntimeConfig.console.enableStdPassthrough && gRuntimeConfig.console.enableStdIn && gEncodedIndex);
#else
return LSHandle::IsSignaled();
#endif
}
AuLoop::ELoopSource ConsoleHasDataLoopSource::GetType()
{
return AuLoop::ELoopSource::eProcessStdIn;
}
bool ConsoleHasDataLoopSource::OnTrigger(AuUInt handle)
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
return IsSignaled();
#else
return true;
#endif
}
bool ConsoleHasDataLoopSource::WaitOn(AuUInt32 timeout)
{
return LSHandle::WaitOn(timeout);
}
#if !defined(AURORA_IS_MODERNNT_DERIVED)
const AuList<AuUInt> &ConsoleHasDataLoopSource::GetHandles()
{
return this->handles;
}
bool ConsoleHasDataLoopSource::Singular()
{
return false;
}
#endif
bool ConsoleSecondaryDataLoopSource::IsSignaled()
{
return LSHandle::IsSignaled() ||
gLineIndex;
}
AuLoop::ELoopSource ConsoleSecondaryDataLoopSource::GetType()
{
return AuLoop::ELoopSource::eProcessStdIn;
}
bool ConsoleSecondaryDataLoopSource::OnTrigger(AuUInt handle)
{
return true;
}
bool ConsoleSecondaryDataLoopSource::WaitOn(AuUInt32 timeout)
{
return LSHandle::WaitOn(timeout);
}
const AuList<AuUInt>& ConsoleSecondaryDataLoopSource::GetHandles()
{
return this->handles;
}
bool ConsoleSecondaryDataLoopSource::Singular()
{
return false;
}
static void StartLogger()
{
if (gRuntimeConfig.console.enableStdPassthrough == gRuntimeConfig.console.enableStdOut)
{
return;
}
Console::AddDefaultLogger(AuUnsafeRaiiToShared(&Logging::Sinks::gStdConsoleSink));
}
static bool gConsoleStarted = false;
void Start()
{
if (AuExchange(gConsoleStarted, true)) return;
#if defined(AURORA_IS_MODERNNT_DERIVED)
DWORD dwMode;
bool ok;
// Obtain a win32 file HANDLE of STDIN
auto fileHandle =
#if defined(AURORA_PLATFORM_WIN32)
GetStdHandle(STD_INPUT_HANDLE);
#else
Win32Open(L"CONIN$",
GENERIC_READ,
FILE_SHARE_READ,
false,
OPEN_EXISTING,
0,
0);
#endif
SysAssert(fileHandle != INVALID_HANDLE_VALUE, "Couldn't open CONIN");
gInputStream = fileHandle;
// Obtain a win32 file HANDLE of STDOUT
fileHandle =
#if defined(AURORA_PLATFORM_WIN32)
GetStdHandle(STD_OUTPUT_HANDLE);
#else
Win32Open(L"CONOUT$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
false,
OPEN_EXISTING,
0,
0);
#endif
SysAssert(fileHandle != INVALID_HANDLE_VALUE, "Couldn't open CONOUT");
gOutputStream = fileHandle;
bool isNotPipe = GetFileType(fileHandle) != FILE_TYPE_PIPE;
if (isNotPipe)
{
// Get current console flags
if (GetConsoleMode(gOutputStream, &dwMode))
{
if (gRuntimeConfig.console.enableStdPassthrough ^ gRuntimeConfig.console.enableStdOut)
{
if (AuSwInfo::IsWindows10OrGreater() && AuSwInfo::GetPlatformInfo().uKernelPatch >= 10586)
{
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
}
else
{
gSupportsColorOutput = false;
}
// Enable escape processing; enable colored output
dwMode |= ENABLE_PROCESSED_OUTPUT;
ok = SetConsoleMode(gOutputStream, dwMode);
SysAssert(ok, "Couldn't set console mode");
// Set the output stream to use UTF-8
ok = SetConsoleOutputCP(CP_UTF8);
SysAssert(ok, "Couldn't maintain UTF-8 stdout stream");
}
}
// Set binary mode if redirecting stdin to user
if (gRuntimeConfig.console.enableStdPassthrough && gRuntimeConfig.console.enableStdOut)
{
SetConsoleMode(gOutputStream, 0);
}
if (gRuntimeConfig.console.enableStdPassthrough ^ gRuntimeConfig.console.enableStdIn)
{
ok = SetConsoleCP(CP_UTF8);
#if 0
SysAssert(ok, "Couldn't maintain UTF-8 stdin stream");
#endif
}
}
if (gRuntimeConfig.console.enableStdPassthrough && gRuntimeConfig.console.enableStdIn && isNotPipe)
{
if (!GetConsoleMode(gInputStream, &dwMode))
{
ok = SetConsoleMode(gInputStream, dwMode & ~(ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT));
SysAssert(ok, "Couldn't maintain binary stdin stream");
}
}
//else
{
gTerminateConsole = CreateEvent(nullptr, true, false, nullptr);
gWin32Thread = CreateThread(nullptr, 0, StdInWin32Thread, nullptr, 0, nullptr);
}
#elif defined(AURORA_IS_POSIX_DERIVED)
gInputStream = STDIN_FILENO;
gOutputStream = STDOUT_FILENO;
#endif
gLoopSource = AuMakeSharedPanic<ConsoleHasDataLoopSource>(
#if !defined(AURORA_IS_MODERNNT_DERIVED)
gInputStream
#endif
);
gLoopSourceConTTY = AuMakeSharedPanic<ConsoleSecondaryDataLoopSource>(
#if defined(AURORA_IS_MODERNNT_DERIVED)
gLoopSource.get()
#else
gInputStream
#endif
);
SysAssert(gInputStream != DEFAULT_HANDLE_VAL, "Couldn't allocate input stream handler");
SysAssert(gOutputStream != DEFAULT_HANDLE_VAL, "Couldn't allocate output stream handler");
StartLogger();
}
void Init()
{
#if defined(AURORA_PLATFORM_WIN32)
if (GetConsoleWindow() == NULL)
{
if (!gRuntimeConfig.console.enableConsole)
{
return;
}
if (gRuntimeConfig.console.forceConsoleWindow)
{
auto ok = AllocConsole();
SysAssert(ok, "Request of a Win32 console yielded nada, forceConsole wasn't respected");
}
}
else
{
// Under windows, applications linked with the console subsystem flag should have output no matter what disableAll/forceConsoleWindow says
// The amount of effort it takes to link as console app for a conhost window far exceeds whatever 'forceConsoleWindow' is defined as
// One does not simply switch over to conapp for the fun of it. We can't or at least shouldn't destroy or hide conhost with hacks
// Assuming stdconsole is wanted - DO NOT RETURN - LINK AS A WINDOWED APP IF YOU DO NOT WANT A CONSOLE WINDOW
}
#elif defined(AURORA_IS_POSIX_DERIVED)
// no always means no under UNIX targets
// we *know* stdin/out will be available under these standards, dont fuck with what might be an IPC channel
if (!gRuntimeConfig.console.enableConsole)
{
return;
}
#endif
Start();
}
static AuUInt32 WriteStdOutBlocking(const void *data, AuUInt32 length)
{
if (gBufferMode)
{
return gStdoutBuffer.Write(data, length);
}
if (!IS_STREAM_HANDLE_VALID(gOutputStream))
{
return 0;
}
if (length == 0)
{
return {};
}
#if defined(IO_POSIX_STREAMS)
AuInt32 written = ::write(gOutputStream, data, length);
if (written < 0)
{
return 0;
}
#elif defined(AURORA_IS_MODERNNT_DERIVED)
DWORD written;
if (!::WriteFile(gOutputStream, data, length, &written, NULL))
{
return 0;
}
#endif
return written;
}
void FlushStdOutNb()
{
if (gBufferMode)
{
return;
}
AuUInt32 written {};
written = WriteStdOutBlocking(gStdoutBuffer.begin(), gStdoutBuffer.size());
gStdoutBuffer.clear();
#if defined(AURORA_IS_POSIX_DERIVED)
::fsync(gOutputStream);
#endif
}
AuUInt32 WriteStdOut(const void *data, AuUInt32 length)
{
AU_LOCK_GUARD(gRingLock);
return gStdoutBuffer.Write(data, length);
}
AuUInt32 WriteStdOutBlocking2(const void *data, AuUInt32 length)
{
AU_LOCK_GUARD(gRingLock);
return WriteStdOutBlocking(data, length);
}
void Lock()
{
AU_LOCK_GUARD(gRingLock);
gBufferMode = true;
}
void Unlock()
{
AU_LOCK_GUARD(gRingLock);
gBufferMode = false;
FlushStdOutNb();
}
AuUInt32 WriteStdOutNonBlocking(const void *data, AuUInt32 length)
{
return gStdoutBuffer.Write(data, length);
}
static void FlushStdOut()
{
AU_LOCK_GUARD(gRingLock);
FlushStdOutNb();
}
void WriteStdOut(AuUInt8 level, const ConsoleMessage &msg, const AuString &formatted)
{
bool bUseAlt {};
auto writeLine = formatted;
if (!gSupportsColorOutput)
{
if (IsStdOutTTY())
{
bUseAlt = true;
}
}
else
{
const char *pAnsiCode =
static_cast<EAnsiColor>(msg.color) <= EAnsiColor::eEnumCount ?
kAnsiColorForegroundToVirtualEscape[static_cast<AuUInt>(msg.color)] :
"";
writeLine = pAnsiCode + writeLine + kAnsiColorForegroundToVirtualEscape[static_cast<AuUInt>(EAnsiColor::eReset)];
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
writeLine += '\r';
#endif
writeLine += '\n';
if (bUseAlt)
{
ConsoleTTY::TTYWrite(writeLine.c_str(), msg.color);
return;
}
#if defined(IO_POSIX_STREAMS)
if (Locale::GetInternalCodePage() == Locale::ECodePage::eUTF8)
{
WriteStdOut(writeLine.data(), writeLine.size());
}
else
{
AuString slow;
slow.resize(writeLine.size() * 4);
auto len = Locale::Encoding::EncodeUTF8(writeLine, Aurora::Memory::MemoryViewWrite {slow.data(), slow.size()}, Locale::ECodePage::eSysUnk);
if (len.first != 0)
{
WriteStdOut(slow.data(), len.second);
}
else
{
// better write this than nothing
WriteStdOut(writeLine.data(), writeLine.size());
}
}
#elif defined(AURORA_IS_MODERNNT_DERIVED)
WriteStdOut(writeLine.data(), AuUInt32(writeLine.size()));
#endif
}
static bool InputStreamAvailable()
{
#if defined(IO_POSIX_STREAMS)
timeval tv {};
fd_set fds {};
FD_ZERO(&fds);
FD_SET(gInputStream, &fds);
select(gInputStream + 1, &fds, NULL, NULL, &tv);
return (FD_ISSET(0, &fds));
#elif defined(AURORA_IS_MODERNNT_DERIVED)
// Inline non-blocking is not legal on windows.
// One can force it with the overlapped flag, seemingly valid since win8, but defined as illegal since the 90s
return false;
#else
return false;
#endif
}
AuUInt32 ReadStdIn(void *data, AuUInt32 length)
{
Pump();
{
AU_LOCK_GUARD(gRingLock);
auto readable = AuMin(AuUInt32(length), AuUInt32(gEncodedIndex));
AuMemcpy(data, gLineEncodedBuffer, readable);
const auto remainingBytes = gEncodedIndex - readable;
if (remainingBytes)
{
AuMemmove(gLineEncodedBuffer, &gLineEncodedBuffer[readable], remainingBytes);
}
gEncodedIndex = remainingBytes;
if (gLoopSource && !gEncodedIndex)
{
gLoopSource->Reset();
}
return readable;
}
}
static void ProcessLines(AuList<AuString> &lines)
{
AuMach index = 0, startIdx = 0;
if (gLineIndex == 0)
{
return;
}
auto end = gLineBuffer.data() + gLineIndex;
while (true)
{
auto a = std::find(gLineBuffer.data() + startIdx, end, '\n');
if (a == end) break;
index = a - gLineBuffer.data();
auto line = gLineBuffer.substr(startIdx, index - startIdx);
startIdx = index + 1;
if (line[line.size() - 1] == '\r')
{
line.pop_back();
}
if (line.size())
{
lines.push_back(line);
}
}
if (index != 0)
{
const auto remainingBytes = gLineIndex - startIdx;
if (remainingBytes)
{
AuMemmove(gLineBuffer.data(), &gLineBuffer.data()[startIdx], remainingBytes);
gLineIndex -= startIdx;
}
else
{
gLineIndex = 0;
}
}
}
static void CanFlushLines()
{
AuList<AuString> binaryLines;
gLineBuffer[gLineIndex++] = '\n';
ProcessLines(binaryLines);
for (const auto &str : binaryLines)
{
NoncanonicalInput canInput {};
canInput.type = ENoncanonicalInput::eInput;
canInput.string += str;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
}
// TODO (Reece): This is disgusting.
// I need to rewrite this frame
static void ConsumeCanInputUnixEsc()
{
static Locale::Encoding::TextStreamEncoder stream(Locale::GetInternalCodePage());
{
AU_LOCK_GUARD(gRingLock);
int i {};
for (i = 0;
i < gEncodedIndex;
)
{
auto c = gLineEncodedBuffer[i];
NoncanonicalInput canInput {};
canInput.iScrollDeltaY = 0;
canInput.bIsControlSequence = canInput.bIsControlSequence = canInput.bIsControlSequence = false;
if (c == 127)
{
CanFlushLines();
canInput.type = ENoncanonicalInput::eBackspace;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (c == '\n' || c == 13)
{
CanFlushLines();
canInput.type = ENoncanonicalInput::eEnter;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if ((c == 27 && gEncodedIndex == 1))
{
CanFlushLines();
canInput.type = ENoncanonicalInput::eEscape;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (c == '\t')
{
CanFlushLines();
canInput.type = ENoncanonicalInput::eTab;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (c == '\r')
{
CanFlushLines();
}
else if (c < 27)
{
CanFlushLines();
canInput.type = ENoncanonicalInput::eInput;
canInput.string = 'a' + c - 1;
canInput.bIsControlSequence = true;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (c == 27)
{
CanFlushLines();
i++;
c = gLineEncodedBuffer[i];
if (c == '[')
{
i++;
auto esc = gLineEncodedBuffer[i];
if (esc == 'A')
{
canInput.type = ENoncanonicalInput::eArrowUp;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (esc == 'B')
{
canInput.type = ENoncanonicalInput::eArrowDown;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (esc == 'D')
{
canInput.type = ENoncanonicalInput::eArrowLeft;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (esc == 'C')
{
canInput.type = ENoncanonicalInput::eArrowRight;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
#define HANDLE_ALTS_OF_NEXT_OP(nestedChar, op) \
if (next == nestedChar) \
{ \
canInput.type = ENoncanonicalInput::op; \
SysAssert(AuTryInsert(gCanonicalBuffer, canInput)); \
} \
else if (next == ';') \
{ \
i++; \
auto esc = gLineEncodedBuffer[i]; \
if (esc == '5') \
{ \
i++; \
auto next = gLineEncodedBuffer[i]; \
\
if (next == nestedChar) \
{ \
canInput.type = ENoncanonicalInput::op; \
canInput.bIsControlSequence = true; \
SysAssert(AuTryInsert(gCanonicalBuffer, canInput)); \
} \
} \
else if (esc == '6') \
{ \
i++; \
auto next = gLineEncodedBuffer[i]; \
\
if (next == nestedChar) \
{ \
canInput.type = ENoncanonicalInput::op; \
canInput.bIsShiftSequence = true; \
canInput.bIsControlSequence = true; \
SysAssert(AuTryInsert(gCanonicalBuffer, canInput)); \
} \
} \
else if (esc == '2') \
{ \
i++; \
auto next = gLineEncodedBuffer[i]; \
\
if (next == nestedChar) \
{ \
canInput.type = ENoncanonicalInput::op; \
canInput.bIsShiftSequence = true; \
SysAssert(AuTryInsert(gCanonicalBuffer, canInput)); \
} \
} \
else if (esc == '8') \
{ \
i++; \
auto next = gLineEncodedBuffer[i]; \
\
if (next == nestedChar) \
{ \
canInput.type = ENoncanonicalInput::op; \
canInput.bIsShiftSequence = true; \
canInput.bIsControlSequence = true; \
canInput.bIsAltSequence = true; \
SysAssert(AuTryInsert(gCanonicalBuffer, canInput)); \
} \
} \
}
#define HANDLE_PAGE_OPS(no, op) \
else if (esc == no) \
{ \
i++; \
auto next = gLineEncodedBuffer[i]; \
HANDLE_ALTS_OF_NEXT_OP('~', op) \
}
HANDLE_PAGE_OPS('6', ePageDown)
HANDLE_PAGE_OPS('5', ePageUp)
HANDLE_PAGE_OPS('2', eInsert)
HANDLE_PAGE_OPS('3', eDelete)
else if (esc == '1')
{
i++;
auto next = gLineEncodedBuffer[i];
if (next == ';')
{
i++;
auto esc = gLineEncodedBuffer[i];
if (esc == '5')
{
i++;
auto esc = gLineEncodedBuffer[i];
if (esc == 'A')
{
canInput.type = ENoncanonicalInput::eScroll;
canInput.iScrollDeltaY = 4;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
else if (esc == 'B')
{
canInput.type = ENoncanonicalInput::eScroll;
canInput.iScrollDeltaY = -4;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
}
}
}
}
else if (c == '?')
{
i++;
auto esc = gLineEncodedBuffer[i];
}
#if 0
else if (c == ';')
{
i++;
auto next = gLineEncodedBuffer[i];
if (next == '~')
{
canInput.type = ENoncanonicalInput::ePageUp;
SysAssert(AuTryInsert(gCanonicalBuffer, canInput));
}
}
#endif
}
else
{
gLineBuffer[gLineIndex++] = c;
}
i++;
}
CanFlushLines();
{
const auto remainingBytes = gEncodedIndex - i;
if (remainingBytes)
{
AuMemmove(gLineEncodedBuffer, &gLineEncodedBuffer[i], remainingBytes);
}
gEncodedIndex = remainingBytes;
}
}
}
static void ProcessLinesSysCP()
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
static Locale::Encoding::TextStreamEncoder stream(Locale::ECodePage::eUTF8);
#else
static Locale::Encoding::TextStreamEncoder stream(Locale::GetInternalCodePage());
#endif
AuList<AuString> lines;
{
if (gCanonicalUnixOn)
{
if (gLoopSource)
{
gLoopSource->Set();
}
ConsumeCanInputUnixEsc();
}
else
{
auto ret = stream.DecodeUTF8(gLineEncodedBuffer, gEncodedIndex, gLineBuffer.data() + gLineIndex, AuUInt32(gLineBuffer.size() - gLineIndex));
// increment backline buffer
{
const auto remainingBytes = gEncodedIndex - ret.first;
if (remainingBytes)
{
AuMemmove(gLineEncodedBuffer, &gLineEncodedBuffer[ret.first], remainingBytes);
}
gEncodedIndex = remainingBytes;
}
// increment frontline buffer
{
gLineIndex += ret.second;
}
if (gLoopSource)
{
gLoopSource->Set();
}
ProcessLines(lines);
}
}
if (lines.size())
{
for (const auto &line : lines)
{
Console::DispatchRawLine(line);
}
ConsoleTTY::OnEnter();
}
}
static AuUInt32 SyncReadConsole()
{
void *data = &gLineEncodedBuffer[gEncodedIndex];
auto length = kLineBufferMax - gEncodedIndex;
if (!IS_STREAM_HANDLE_VALID(gInputStream))
{
return 0;
}
if (length == 0)
{
return 0;
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
DWORD read = length;
if (!ReadFile(gInputStream, data, read, &read, NULL))
{
return 0;
}
gRingLock.Lock();
gEncodedIndex += read;
gRingLock.Unlock();
return read;
#elif defined(IO_POSIX_STREAMS)
auto bread = ::read(gInputStream, data, length);
if (bread < 0)
{
return 0;
}
gRingLock.Lock();
gEncodedIndex += bread;
gRingLock.Unlock();
return bread;
#else
return 0;
#endif
}
static bool AsyncReadAnyOrReadStreamBlock()
{
if (!SyncReadConsole())
{
return false;
}
if (gRuntimeConfig.console.enableStdPassthrough && gRuntimeConfig.console.enableStdIn)
{
if (gLoopSource)
{
gLoopSource->Set();
}
return true;
}
ProcessLinesSysCP();
return true;
}
void Flush()
{
FlushStdOut();
}
void Pump()
{
FlushStdOut();
if (!gRuntimeConfig.console.enableStdPassthrough && !gRuntimeConfig.console.enableStdIn)
{
return;
}
if (!IS_STREAM_HANDLE_VALID(gInputStream))
{
return;
}
if (!InputStreamAvailable())
{
return;
}
AsyncReadAnyOrReadStreamBlock();
}
void Exit()
{
if (IS_STREAM_HANDLE_VALID(gTerminateConsole))
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
SetEvent(gTerminateConsole);
#endif
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
gCanonicalEnabled = false;
Pump();
::FlushFileBuffers(gOutputStream);
SignalKillNT();
AuWin32CloseHandle(gTerminateConsole);
// Note: CloseHandle in the middle of a ReadFile blocks
#if !defined(AURORA_PLATFORM_WIN32)
AuWin32CloseHandle(gInputStream);
AuWin32CloseHandle(gOutputStream);
#endif
gConsoleStarted = false;
#endif
AuResetMember(gLoopSource);
AuResetMember(gLoopSourceConTTY);
}
AuLoop::ILSEvent* GetConTTYEvent()
{
return gLoopSourceConTTY.get();
}
AuSPtr<AuLoop::ILoopSource> GetLoopSource()
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (gCanonicalEnabled)
{
return gLoopSourceConTTY;
}
#endif
return gLoopSource;
}
AuSPtr<AuLoop::ILoopSource> GetLoopSourceConTTY()
{
return gLoopSourceConTTY;
}
#else
void Pump()
{
}
void Init()
{
}
void Exit()
{
}
void Start()
{
}
void Flush()
{
}
AuSPtr<AuLoop::ILoopSource> GetLoopSource()
{
return {};
}
AuSPtr<AuLoop::ILoopSource> GetLoopSourceConTTY()
{
return {};
}
AuLoop::ILSEvent* GetConTTYEvent()
{
return {};
}
#endif
}