// Copyright (C) 2003 Dolphin Project. // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 2.0 or later versions. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License 2.0 for more details. // A copy of the GPL 2.0 should have been included with the program. // If not, see http://www.gnu.org/licenses/ // Official SVN repository and contact information can be found at // http://code.google.com/p/dolphin-emu/ #include #include // min #include #include // System: To be able to add strings with "+" #include #ifdef _WIN32 #include #include "CommonWindows.h" #include #else #include #endif #ifndef _MSC_VER #include #endif #include "ppsspp_config.h" #include "Common/Thread/ThreadUtil.h" #include "Common/Data/Encoding/Utf8.h" #include "Common/CommonTypes.h" #include "Common/ConsoleListener.h" #include "Common/StringUtils.h" #if defined(USING_WIN_UI) const int LOG_PENDING_MAX = 120 * 10000; const int LOG_LATENCY_DELAY_MS = 20; const int LOG_SHUTDOWN_DELAY_MS = 250; const int LOG_MAX_DISPLAY_LINES = 4000; int ConsoleListener::refCount = 0; HANDLE ConsoleListener::hThread = NULL; HANDLE ConsoleListener::hTriggerEvent = NULL; CRITICAL_SECTION ConsoleListener::criticalSection; char *ConsoleListener::logPending = NULL; std::atomic ConsoleListener::logPendingReadPos; std::atomic ConsoleListener::logPendingWritePos; #endif ConsoleListener::ConsoleListener() : bHidden(true) { #if defined(USING_WIN_UI) hConsole = NULL; bUseColor = true; if (hTriggerEvent == NULL) { hTriggerEvent = CreateEvent(NULL, FALSE, FALSE, NULL); InitializeCriticalSection(&criticalSection); logPending = new char[LOG_PENDING_MAX]; } ++refCount; #elif PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(UWP) || PPSSPP_PLATFORM(SWITCH) bUseColor = false; #elif defined(_MSC_VER) bUseColor = false; #elif defined(__APPLE__) // Xcode builtin terminal used for debugging does not support colours. // Fortunately it can be detected with a TERM env variable. bUseColor = isatty(fileno(stdout)) && getenv("TERM") != NULL; #else bUseColor = isatty(fileno(stdout)); #endif } ConsoleListener::~ConsoleListener() { Close(); } #if defined(_WIN32) // Handle console event bool WINAPI ConsoleHandler(DWORD msgType) { if (msgType == CTRL_C_EVENT) { OutputDebugString(L"Ctrl-C!\n"); return TRUE; } else if (msgType == CTRL_CLOSE_EVENT) { OutputDebugString(L"Close console window!\n"); return TRUE; } /* Other messages: CTRL_BREAK_EVENT Ctrl-Break pressed CTRL_LOGOFF_EVENT User log off CTRL_SHUTDOWN_EVENT System shutdown */ return FALSE; } #endif // 100, 100, "Dolphin Log Console" // Open console window - width and height is the size of console window // Name is the window title void ConsoleListener::Init(bool AutoOpen, int Width, int Height, const char *Title) { #if defined(USING_WIN_UI) openWidth_ = Width; openHeight_ = Height; title_ = ConvertUTF8ToWString(Title); if (AutoOpen) Open(); #endif } void ConsoleListener::Open() { #if defined(USING_WIN_UI) if (!GetConsoleWindow()) { // Open the console window and create the window handle for GetStdHandle() AllocConsole(); hWnd = GetConsoleWindow(); ShowWindow(hWnd, SW_SHOWDEFAULT); // disable console close button HMENU hMenu=GetSystemMenu(hWnd, false); EnableMenuItem(hMenu,SC_CLOSE,MF_GRAYED|MF_BYCOMMAND); // Save the window handle that AllocConsole() created hConsole = GetStdHandle(STD_OUTPUT_HANDLE); // Set console handler if(SetConsoleCtrlHandler((PHANDLER_ROUTINE)ConsoleHandler, TRUE)){OutputDebugString(L"Console handler is installed!\n");} // Set the console window title SetConsoleTitle(title_.c_str()); SetConsoleCP(CP_UTF8); SetConsoleOutputCP(CP_UTF8); // Set letter space LetterSpace(openWidth_, LOG_MAX_DISPLAY_LINES); //MoveWindow(GetConsoleWindow(), 200,200, 800,800, true); } else { hConsole = GetStdHandle(STD_OUTPUT_HANDLE); } if (hTriggerEvent != NULL && hThread == NULL) hThread = (HANDLE)_beginthreadex(NULL, 0, &ConsoleListener::RunThread, this, 0, NULL); #endif } void ConsoleListener::Show(bool bShow) { #if defined(USING_WIN_UI) if (bShow && bHidden) { if (!IsOpen()) Open(); ShowWindow(GetConsoleWindow(), SW_SHOW); bHidden = false; } else if (!bShow && !bHidden) { ShowWindow(GetConsoleWindow(), SW_HIDE); bHidden = true; } #endif } void ConsoleListener::UpdateHandle() { #if defined(USING_WIN_UI) hConsole = GetStdHandle(STD_OUTPUT_HANDLE); #endif } // Close the console window and close the eventual file handle void ConsoleListener::Close() { #if defined(USING_WIN_UI) if (--refCount <= 0) { if (hThread != NULL) { logPendingWritePos.store((u32)-1, std::memory_order_release); SetEvent(hTriggerEvent); WaitForSingleObject(hThread, LOG_SHUTDOWN_DELAY_MS); CloseHandle(hThread); hThread = NULL; } if (hTriggerEvent != NULL) { DeleteCriticalSection(&criticalSection); CloseHandle(hTriggerEvent); hTriggerEvent = NULL; } if (logPending != NULL) { delete [] logPending; logPending = NULL; } refCount = 0; } if (hConsole != NULL) { FreeConsole(); hConsole = NULL; } #else fflush(NULL); #endif } bool ConsoleListener::IsOpen() { #if defined(USING_WIN_UI) return (hConsole != NULL); #else return true; #endif } /* LetterSpace: SetConsoleScreenBufferSize and SetConsoleWindowInfo are dependent on each other, that's the reason for the additional checks. */ void ConsoleListener::BufferWidthHeight(int BufferWidth, int BufferHeight, int ScreenWidth, int ScreenHeight, bool BufferFirst) { _dbg_assert_msg_(IsOpen(), "Don't call this before opening the console."); #if defined(USING_WIN_UI) BOOL SB, SW; if (BufferFirst) { // Change screen buffer size COORD Co = {(SHORT)BufferWidth, (SHORT)BufferHeight}; SB = SetConsoleScreenBufferSize(hConsole, Co); // Change the screen buffer window size SMALL_RECT coo = {(SHORT)0, (SHORT)0, (SHORT)ScreenWidth, (SHORT)ScreenHeight}; // top, left, right, bottom SW = SetConsoleWindowInfo(hConsole, TRUE, &coo); } else { // Change the screen buffer window size SMALL_RECT coo = {(SHORT)0, (SHORT)0, (SHORT)ScreenWidth, (SHORT)ScreenHeight}; // top, left, right, bottom SW = SetConsoleWindowInfo(hConsole, TRUE, &coo); // Change screen buffer size COORD Co = {(SHORT)BufferWidth, (SHORT)BufferHeight}; SB = SetConsoleScreenBufferSize(hConsole, Co); } #endif } void ConsoleListener::LetterSpace(int Width, int Height) { _dbg_assert_msg_(IsOpen(), "Don't call this before opening the console."); #if defined(USING_WIN_UI) // Get console info CONSOLE_SCREEN_BUFFER_INFO ConInfo; GetConsoleScreenBufferInfo(hConsole, &ConInfo); // int OldBufferWidth = ConInfo.dwSize.X; int OldBufferHeight = ConInfo.dwSize.Y; int OldScreenWidth = (ConInfo.srWindow.Right - ConInfo.srWindow.Left); int OldScreenHeight = (ConInfo.srWindow.Bottom - ConInfo.srWindow.Top); // int NewBufferWidth = Width; int NewBufferHeight = Height; int NewScreenWidth = NewBufferWidth - 1; int NewScreenHeight = OldScreenHeight; // Width BufferWidthHeight(NewBufferWidth, OldBufferHeight, NewScreenWidth, OldScreenHeight, (NewBufferWidth > OldScreenWidth-1)); // Height BufferWidthHeight(NewBufferWidth, NewBufferHeight, NewScreenWidth, NewScreenHeight, (NewBufferHeight > OldScreenHeight-1)); // Resize the window too //MoveWindow(GetConsoleWindow(), 200,200, (Width*8 + 50),(NewScreenHeight*12 + 200), true); #endif } #if defined(USING_WIN_UI) COORD ConsoleListener::GetCoordinates(int BytesRead, int BufferWidth) { COORD Ret = {0, 0}; // Full rows int Step = (int)floor((float)BytesRead / (float)BufferWidth); Ret.Y += Step; // Partial row Ret.X = BytesRead - (BufferWidth * Step); return Ret; } unsigned int WINAPI ConsoleListener::RunThread(void *lpParam) { SetCurrentThreadName("Console"); ConsoleListener *consoleLog = (ConsoleListener *)lpParam; consoleLog->LogWriterThread(); return 0; } void ConsoleListener::LogWriterThread() { char *logLocal = new char[LOG_PENDING_MAX]; int logLocalSize = 0; while (true) { WaitForSingleObject(hTriggerEvent, INFINITE); Sleep(LOG_LATENCY_DELAY_MS); u32 logRemotePos = logPendingWritePos.load(std::memory_order_acquire); if (logRemotePos == (u32) -1) break; else if (logRemotePos == logPendingReadPos) continue; else { EnterCriticalSection(&criticalSection); logRemotePos = logPendingWritePos.load(std::memory_order_acquire); int start = 0; if (logRemotePos < logPendingReadPos) { const int count = LOG_PENDING_MAX - logPendingReadPos; memcpy(logLocal + start, logPending + logPendingReadPos, count); start = count; logPendingReadPos = 0; } const int count = logRemotePos - logPendingReadPos; memcpy(logLocal + start, logPending + logPendingReadPos, count); logPendingReadPos += count; LeaveCriticalSection(&criticalSection); // Double check. if (logPendingWritePos == (u32) -1) break; logLocalSize = start + count; } for (char *Text = logLocal, *End = logLocal + logLocalSize; Text < End; ) { LogTypes::LOG_LEVELS Level = LogTypes::LINFO; char *next = (char *) memchr(Text + 1, '\033', End - Text); size_t Len = next - Text; if (next == NULL) Len = End - Text; if (Text[0] == '\033' && Text + 1 < End) { Level = (LogTypes::LOG_LEVELS) (Text[1] - '0'); Len -= 2; Text += 2; } // Make sure we didn't start quitting. This is kinda slow. if (logPendingWritePos == (u32) -1) break; WriteToConsole(Level, Text, Len); Text += Len; } } delete [] logLocal; } void ConsoleListener::SendToThread(LogTypes::LOG_LEVELS Level, const char *Text) { // Oops, we're already quitting. Just do nothing. if (logPendingWritePos == (u32) -1) return; int Len = (int)strlen(Text); if (Len > LOG_PENDING_MAX) Len = LOG_PENDING_MAX - 16; char ColorAttr[16] = ""; int ColorLen = 0; if (bUseColor) { // Not ANSI, since the console doesn't support it, but ANSI-like. snprintf(ColorAttr, 16, "\033%d", Level); // For now, rather than properly support it. _dbg_assert_msg_(strlen(ColorAttr) == 2, "Console logging doesn't support > 9 levels."); ColorLen = (int)strlen(ColorAttr); } EnterCriticalSection(&criticalSection); u32 logWritePos = logPendingWritePos.load(); u32 prevLogWritePos = logWritePos; if (logWritePos + ColorLen + Len >= LOG_PENDING_MAX) { for (int i = 0; i < ColorLen; ++i) logPending[(logWritePos + i) % LOG_PENDING_MAX] = ColorAttr[i]; logWritePos += ColorLen; if (logWritePos >= LOG_PENDING_MAX) logWritePos -= LOG_PENDING_MAX; int start = 0; if (logWritePos < LOG_PENDING_MAX && logWritePos + Len >= LOG_PENDING_MAX) { const int count = LOG_PENDING_MAX - logWritePos; memcpy(logPending + logWritePos, Text, count); start = count; logWritePos = 0; } const int count = Len - start; if (count > 0) { memcpy(logPending + logWritePos, Text + start, count); logWritePos += count; } } else { memcpy(logPending + logWritePos, ColorAttr, ColorLen); memcpy(logPending + logWritePos + ColorLen, Text, Len); logWritePos += ColorLen + Len; } // Oops, we passed the read pos. if (prevLogWritePos < logPendingReadPos && logWritePos >= logPendingReadPos) { char *nextNewline = (char *) memchr(logPending + logWritePos, '\n', LOG_PENDING_MAX - logWritePos); if (nextNewline == NULL && logWritePos > 0) nextNewline = (char *) memchr(logPending, '\n', logWritePos); // Okay, have it go right after the next newline. if (nextNewline != NULL) logPendingReadPos = (u32)(nextNewline - logPending + 1); } // Double check we didn't start quitting. if (logPendingWritePos == (u32) -1) { LeaveCriticalSection(&criticalSection); return; } logPendingWritePos.store(logWritePos, std::memory_order::memory_order_release); LeaveCriticalSection(&criticalSection); SetEvent(hTriggerEvent); } void ConsoleListener::WriteToConsole(LogTypes::LOG_LEVELS Level, const char *Text, size_t Len) { _dbg_assert_msg_(IsOpen(), "Don't call this before opening the console."); /* const int MAX_BYTES = 1024*10; char Str[MAX_BYTES]; va_list ArgPtr; int Cnt; va_start(ArgPtr, Text); Cnt = vsnprintf(Str, MAX_BYTES, Text, ArgPtr); va_end(ArgPtr); */ DWORD cCharsWritten; WORD Color; static wchar_t tempBuf[2048]; switch (Level) { case NOTICE_LEVEL: // light green Color = FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; case ERROR_LEVEL: // light red Color = FOREGROUND_RED | FOREGROUND_INTENSITY; break; case WARNING_LEVEL: // light yellow Color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; case INFO_LEVEL: // cyan Color = FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY; break; case DEBUG_LEVEL: // gray Color = FOREGROUND_INTENSITY; break; default: // off-white Color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; break; } if (Len > 10) { // First 10 chars white SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY); int wlen = MultiByteToWideChar(CP_UTF8, 0, Text, (int)Len, NULL, NULL); MultiByteToWideChar(CP_UTF8, 0, Text, (int)Len, tempBuf, wlen); WriteConsole(hConsole, tempBuf, 10, &cCharsWritten, NULL); Text += 10; Len -= 10; } SetConsoleTextAttribute(hConsole, Color); int wlen = MultiByteToWideChar(CP_UTF8, 0, Text, (int)Len, NULL, NULL); MultiByteToWideChar(CP_UTF8, 0, Text, (int)Len, tempBuf, wlen); WriteConsole(hConsole, tempBuf, (DWORD)wlen, &cCharsWritten, NULL); } #endif void ConsoleListener::PixelSpace(int Left, int Top, int Width, int Height, bool Resize) { _dbg_assert_msg_(IsOpen(), "Don't call this before opening the console."); #if defined(USING_WIN_UI) // Check size if (Width < 8 || Height < 12) return; std::string SLog = ""; // Get console info CONSOLE_SCREEN_BUFFER_INFO ConInfo; GetConsoleScreenBufferInfo(hConsole, &ConInfo); DWORD BufferSize = ConInfo.dwSize.X * ConInfo.dwSize.Y; // --------------------------------------------------------------------- // Save the current text // ------------------------ DWORD cCharsRead = 0; COORD coordScreen = { 0, 0 }; static const int MAX_BYTES = 1024 * 16; std::vector> Str; std::vector> Attr; // ReadConsoleOutputAttribute seems to have a limit at this level static const int ReadBufferSize = MAX_BYTES - 32; DWORD cAttrRead = ReadBufferSize; DWORD BytesRead = 0; while (BytesRead < BufferSize) { Str.resize(Str.size() + 1); if (!ReadConsoleOutputCharacter(hConsole, Str.back().data(), ReadBufferSize, coordScreen, &cCharsRead)) SLog += StringFromFormat("WriteConsoleOutputCharacter error"); Attr.resize(Attr.size() + 1); if (!ReadConsoleOutputAttribute(hConsole, Attr.back().data(), ReadBufferSize, coordScreen, &cAttrRead)) SLog += StringFromFormat("WriteConsoleOutputAttribute error"); // Break on error if (cAttrRead == 0) break; BytesRead += cAttrRead; coordScreen = GetCoordinates(BytesRead, ConInfo.dwSize.X); } // Letter space int LWidth = (int)(floor((float)Width / 8.0f) - 1.0f); int LHeight = (int)(floor((float)Height / 12.0f) - 1.0f); int LBufWidth = LWidth + 1; int LBufHeight = (int)floor((float)BufferSize / (float)LBufWidth); // Change screen buffer size LetterSpace(LBufWidth, LBufHeight); ClearScreen(true); coordScreen.Y = 0; coordScreen.X = 0; DWORD cCharsWritten = 0; int BytesWritten = 0; DWORD cAttrWritten = 0; for (size_t i = 0; i < Attr.size(); i++) { if (!WriteConsoleOutputCharacter(hConsole, Str[i].data(), ReadBufferSize, coordScreen, &cCharsWritten)) SLog += StringFromFormat("WriteConsoleOutputCharacter error"); if (!WriteConsoleOutputAttribute(hConsole, Attr[i].data(), ReadBufferSize, coordScreen, &cAttrWritten)) SLog += StringFromFormat("WriteConsoleOutputAttribute error"); BytesWritten += cAttrWritten; coordScreen = GetCoordinates(BytesWritten, LBufWidth); } const int OldCursor = ConInfo.dwCursorPosition.Y * ConInfo.dwSize.X + ConInfo.dwCursorPosition.X; COORD Coo = GetCoordinates(OldCursor, LBufWidth); SetConsoleCursorPosition(hConsole, Coo); // if (SLog.length() > 0) Log(LogTypes::LNOTICE, SLog.c_str()); // Resize the window too if (Resize) MoveWindow(GetConsoleWindow(), Left,Top, (Width + 100),Height, true); #endif } void ConsoleListener::Log(const LogMessage &msg) { char Text[2048]; snprintf(Text, sizeof(Text), "%s %s %s", msg.timestamp, msg.header, msg.msg.c_str()); Text[sizeof(Text) - 2] = '\n'; Text[sizeof(Text) - 1] = '\0'; #if defined(USING_WIN_UI) if (hThread == NULL && IsOpen()) WriteToConsole(msg.level, Text, strlen(Text)); else SendToThread(msg.level, Text); #else char ColorAttr[16] = ""; char ResetAttr[16] = ""; if (bUseColor) { strcpy(ResetAttr, "\033[0m"); switch (msg.level) { case NOTICE_LEVEL: // light green strcpy(ColorAttr, "\033[92m"); break; case ERROR_LEVEL: // light red strcpy(ColorAttr, "\033[91m"); break; case WARNING_LEVEL: // light yellow strcpy(ColorAttr, "\033[93m"); break; default: break; } } fprintf(stderr, "%s%s%s", ColorAttr, Text, ResetAttr); #endif } // Clear console screen void ConsoleListener::ClearScreen(bool Cursor) { _dbg_assert_msg_(IsOpen(), "Don't call this before opening the console."); #if defined(USING_WIN_UI) COORD coordScreen = { 0, 0 }; DWORD cCharsWritten; CONSOLE_SCREEN_BUFFER_INFO csbi; DWORD dwConSize; GetConsoleScreenBufferInfo(hConsole, &csbi); dwConSize = csbi.dwSize.X * csbi.dwSize.Y; // Write space to the entire console FillConsoleOutputCharacter(hConsole, TEXT(' '), dwConSize, coordScreen, &cCharsWritten); GetConsoleScreenBufferInfo(hConsole, &csbi); FillConsoleOutputAttribute(hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten); // Reset cursor if (Cursor) SetConsoleCursorPosition(hConsole, coordScreen); #endif }