/*** Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: ConsoleStd.cpp Date: 2021-6-8 Author: Reece ***/ #include #include "ConsoleStd.hpp" #include "Source/Locale/Locale.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 #include #include #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) #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(); 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::SpinLock gRingLock; #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; } Console::Hooks::AddFunctionalHook([](const Aurora::Console::ConsoleMessage &string) -> void { #if (defined(DEBUG) || defined(STAGING)) && defined(AURORA_IS_MODERNNT_DERIVED) auto debugLine = string.ToSimplified() + "\r\n"; OutputDebugStringW(Locale::ConvertFromUTF8(debugLine).c_str()); #endif if (!gRuntimeConfig.console.enableStdOut) { return; } auto writeLine = string.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 }); } void Start() { static bool gConsoleStarted = false; if (std::exchange(gConsoleStarted, true)) return; #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD dwMode; bool ok; // Obtain a win32 file HANDLE of STDIN auto fileHandle = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); SysAssert(fileHandle != INVALID_HANDLE_VALUE, "Couldn't open CONIN"); gInputStream = fileHandle; // Obtain a win32 file HANDLE of STDOUT fileHandle = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); SysAssert(fileHandle != INVALID_HANDLE_VALUE, "Couldn't open CONOUT"); gOutputStream = fileHandle; // 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) { 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, "Couldn't allocate input stream handler"); SysAssert(gOutputStream, "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) { return; } 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 if (gRuntimeConfig.console.disableAllConsoles) { return; } #endif Start(); } AuUInt32 WriteStdOut(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; } 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(); gRingLock.Lock(); auto readable = std::min(AuUInt32(length), AuUInt32(gEncodedIndex)); std::memcpy(data, gLineEncodedBuffer, readable); const auto remainingBytes = gEncodedIndex - readable; if (remainingBytes) { std::memmove(gLineEncodedBuffer, &gLineEncodedBuffer[readable], remainingBytes); } gEncodedIndex = remainingBytes; gRingLock.Unlock(); 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) { std::memmove(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) { std::memmove(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 Pump() { 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() { } #endif }