// https://github.com/Const-me/vis_avs_dx/blob/master/avs_dx/DxVisuals/Interop/ConsoleLogger.cpp #include "stdafx.h" #include "DebugConsole.h" #include "miscUtils.h" #include "../AppState.h" #include "logger.h" namespace { using Whisper::eLogLevel; constexpr uint16_t defaultAttributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; inline uint16_t textAttributes( eLogLevel lvl ) { switch( lvl ) { case eLogLevel::Error: return FOREGROUND_RED | FOREGROUND_INTENSITY; case eLogLevel::Warning: return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; case eLogLevel::Info: return FOREGROUND_GREEN | FOREGROUND_INTENSITY; case eLogLevel::Debug: return FOREGROUND_BLUE | FOREGROUND_INTENSITY; } return defaultAttributes; } // Background stuff: accumulate messages in a small buffer, in case user will want to see them in the console. // Ideally, we should accumulate them in a more efficient data structure, maybe a circular buffer. // However, we don't have that many messages per second, this simple solution that uses std::deque is probably good enough for the job. static constexpr uint16_t bufferSize = 64; using Lock = CComCritSecLock; #define LOCK() Lock __lock{ critSec } thread_local CStringA threadError; } HRESULT DebugConsole::Entry::print( HANDLE hConsole, CString& tempString ) const { if( !SetConsoleTextAttribute( hConsole, textAttributes( level ) ) ) return getLastHr(); makeUtf16( tempString, message ); tempString += L"\r\n"; if( !WriteConsoleW( hConsole, tempString, (DWORD)tempString.GetLength(), nullptr, nullptr ) ) return getLastHr(); return S_OK; } void clearLastError() { threadError = ""; } bool getLastError( CString& rdi ) { if( threadError.GetLength() <= 0 ) { rdi = L""; return false; } else { makeUtf16( rdi, threadError ); threadError = ""; return true; } } inline void DebugConsole::logSink( eLogLevel lvl, const char* message ) { LOCK(); // Add to the buffer while( buffer.size() >= bufferSize ) buffer.pop_front(); buffer.emplace_back( Entry{ lvl, message } ); // If the console window is shown, print there, too. if( output ) buffer.rbegin()->print( output, tempString ); } void __stdcall DebugConsole::logSinkStatic( void* context, eLogLevel lvl, const char* message ) { if( lvl == eLogLevel::Error ) threadError = message; DebugConsole* con = (DebugConsole*)context; con->logSink( lvl, message ); } HRESULT DebugConsole::initialize( Whisper::eLogLevel level ) { if( nullptr != pGlobalInstance ) return HRESULT_FROM_WIN32( ERROR_ALREADY_INITIALIZED ); pGlobalInstance = this; Whisper::sLoggerSetup setup; setup.sink = &logSinkStatic; setup.context = this; setup.level = level; setup.flags = Whisper::eLoggerFlags::SkipFormatMessage; return Whisper::setupLogger( setup ); } DebugConsole::~DebugConsole() { hide(); Whisper::sLoggerSetup setup; memset( &setup, 0, sizeof( setup ) ); Whisper::setupLogger( setup ); pGlobalInstance = nullptr; } DebugConsole* DebugConsole::pGlobalInstance = nullptr; void DebugConsole::windowClosed() { LOCK(); if( FreeConsole() ) { // Apparently, FreeConsole already closes that handle: https://stackoverflow.com/q/12676312/126995 output.Detach(); } output.Close(); for( CButton* b : checkboxes ) { if( !*b ) continue; if( !b->IsWindow() ) continue; PostMessage( *b, BM_SETCHECK, BST_UNCHECKED, 0 ); } } BOOL __stdcall DebugConsole::consoleHandlerRoutine( DWORD dwCtrlType ) { switch( dwCtrlType ) { case CTRL_CLOSE_EVENT: case CTRL_C_EVENT: case CTRL_BREAK_EVENT: pGlobalInstance->windowClosed(); return TRUE; } return TRUE; } HRESULT DebugConsole::show() { HWND hWnd = GetConsoleWindow(); if( nullptr != hWnd ) { ShowWindow( hWnd, SW_RESTORE ); SetForegroundWindow( hWnd ); return S_FALSE; } if( !AllocConsole() ) return getLastHr(); output.Close(); output.Attach( GetStdHandle( STD_OUTPUT_HANDLE ) ); if( !output ) return getLastHr(); constexpr UINT cp = CP_UTF8; if( IsValidCodePage( cp ) ) SetConsoleOutputCP( cp ); // Enable ANSI color coding DWORD mode = 0; if( !GetConsoleMode( output, &mode ) ) return getLastHr(); if( 0 == ( mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING ) ) { mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; if( !SetConsoleMode( output, mode ) ) return getLastHr(); } SetConsoleTitle( L"Whisper Desktop Debug Console" ); SetConsoleCtrlHandler( &consoleHandlerRoutine, TRUE ); // Disable close command in the sys.menu of the new console, otherwise the whole process will quit: https://stackoverflow.com/a/12015131/126995 HWND hwnd = ::GetConsoleWindow(); if( hwnd != nullptr ) { HMENU hMenu = ::GetSystemMenu( hwnd, FALSE ); if( hMenu != NULL ) DeleteMenu( hMenu, SC_CLOSE, MF_BYCOMMAND ); } // Print old log entries for( const auto& e : buffer ) CHECK( e.print( output, tempString ) ); const CStringA msg = "Press Control+C or Control+Break to close this window\r\n"; if( !SetConsoleTextAttribute( output, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY ) ) return getLastHr(); if( !WriteConsoleA( output, cstr( msg ), msg.GetLength(), nullptr, nullptr ) ) return getLastHr(); return S_OK; } HRESULT DebugConsole::hide() { LOCK(); if( !output ) return S_FALSE; windowClosed(); return S_OK; } void DebugConsole::addCheckbox( CButton& cb ) { checkboxes.emplace( &cb ); } void DebugConsole::removeCheckbox( CButton& cb ) { checkboxes.erase( &cb ); } HRESULT ConsoleCheckbox::initialize( HWND dialog, int idc, AppState& state ) { control = GetDlgItem( dialog, idc ); assert( control ); console = &state.console; if( state.console.isVisible() ) control.SetCheck( BST_CHECKED ); state.console.addCheckbox( control ); return S_OK; } void ConsoleCheckbox::click() { const int state = control.GetCheck(); if( state == BST_CHECKED ) console->show(); else console->hide(); } void ConsoleCheckbox::ensureChecked() { const int state = control.GetCheck(); if( state == BST_CHECKED ) return; control.SetCheck( BST_CHECKED ); console->show(); } void DebugConsole::log( eLogLevel lvl, const char* pszFormat, va_list args ) { LOCK(); // Add to the buffer while( buffer.size() >= bufferSize ) buffer.pop_front(); tempStringA.FormatV( pszFormat, args ); buffer.emplace_back( Entry{ lvl, tempStringA } ); // If the console window is shown, print there, too. if( output ) buffer.rbegin()->print( output, tempString ); } void DebugConsole::logMessage( eLogLevel lvl, const char* pszFormat, va_list args ) { if( nullptr == pGlobalInstance ) return; pGlobalInstance->log( lvl, pszFormat, args ); } void logMessage( Whisper::eLogLevel lvl, const char8_t* pczFormat, va_list args ) { DebugConsole::logMessage( lvl, (const char*)pczFormat, args ); }