AuroraRuntime/Source/Console/ConsoleStd/ConsoleStd.cpp
2022-04-15 11:01:43 +01:00

584 lines
16 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>
#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>
#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)
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;
#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);
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 AuThreadPrimitives::MutexUnique_t gRingLock = AuThreadPrimitives::MutexUnique();
static AuThreadPrimitives::SpinLock gRingLock;// = AuThreadPrimitives::MutexUnique();
#if defined(AURORA_IS_MODERNNT_DERIVED)
static DWORD WINAPI StdInWin32Thread(void*)
{
HANDLE a[2] = {gInputStream, gTerminateConsole};
while (true)
{
WaitForMultipleObjectsEx(2, a, false, 25, 0);
if (WaitForSingleObject(gTerminateConsole, 0) == WAIT_OBJECT_0)
{
break;
}
AsyncReadAnyOrReadStreamBlock();
}
return 1;
}
#endif
static void StartLogger()
{
if (gRuntimeConfig.console.enableStdPassthrough && gRuntimeConfig.console.enableStdOut)
{
return;
}
if (!gRuntimeConfig.console.enableStdOut)
{
return;
}
Console::AddDefaultLogger(AuUnsafeRaiiToShared(&Logging::Sinks::gStdConsoleSink));
}
void Start()
{
static bool gConsoleStarted = false;
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
CreateFileW(L"CONIN$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
#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
CreateFileW(L"CONOUT$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
#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)
{
// Enable escape processing; enable colored output
dwMode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
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
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 (!IS_STREAM_HANDLE_VALID(gOutputStream))
{
return 0;
}
#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;
}
AuUInt32 WriteStdOut(const void *data, AuUInt32 length)
{
AU_LOCK_GUARD(gRingLock);
return gStdoutBuffer.Write(data, length);
}
static void FlushStdOut()
{
AU_LOCK_GUARD(gRingLock);
AuUInt32 written {};
// TODO: finish me
written = WriteStdOutBlocking(gStdoutBuffer.begin(), gStdoutBuffer.size());
gStdoutBuffer.clear();
}
void WriteStdOut(AuUInt8 level, const ConsoleMessage &msg)
{
auto writeLine = msg.ToConsole();
#if defined(AURORA_IS_MODERNNT_DERIVED)
writeLine += '\r';
#endif
writeLine += '\n';
#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(), 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;
return readable;
}
}
static void ProcessLines()
{
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())
{
Console::DispatchRawLine(line);
}
}
if (index != 0)
{
const auto remainingBytes = gLineIndex - startIdx;
if (remainingBytes)
{
AuMemmove(gLineBuffer.data(), &gLineBuffer.data()[startIdx], remainingBytes);
gLineIndex -= startIdx;
}
}
}
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
AU_LOCK_GUARD(&gRingLock);
auto ret = stream.DecodeUTF8(gLineEncodedBuffer, gEncodedIndex, gLineBuffer.data() + gLineIndex, 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;
}
ProcessLines();
}
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)
{
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)
if (IS_STREAM_HANDLE_VALID(gWin32Thread))
{
CancelSynchronousIo(gWin32Thread);
if (WaitForSingleObject(gWin32Thread, 200) != WAIT_OBJECT_0)
{
TerminateThread(gWin32Thread, 0);
}
}
AuWin32CloseHandle(gWin32Thread);
AuWin32CloseHandle(gTerminateConsole);
// Note: CloseHandle in the middle of a ReadFile blocks
AuWin32CloseHandle(gInputStream);
AuWin32CloseHandle(gOutputStream);
#endif
}
#else
void Pump()
{
}
void Init()
{
}
void Exit()
{
}
void Start()
{
}
void Flush()
{
}
#endif
}