#include "ppsspp_config.h"
#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
#include <atomic>
#include <array>
#include <cstring>
#include <string>
#include <math.h>
#include <process.h>
#include "Common/CommonWindows.h"
#ifndef _MSC_VER
#include <unistd.h>
#endif
#include "Common/Thread/ThreadUtil.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/CommonTypes.h"
#include "Common/Log/LogManager.h"
#include "Common/Log/ConsoleListener.h"
#include "Common/StringUtils.h"
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;
static bool g_Initialized;
ConsoleListener::ConsoleListener() : hidden_(true) {
useColor_ = true;
if (useThread_ && !hTriggerEvent) {
hTriggerEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
InitializeCriticalSection(&criticalSection);
logPending_ = new char[LOG_PENDING_MAX];
}
_dbg_assert_(!g_Initialized);
g_Initialized = true;
logPendingReadPos_.store(0);
logPendingWritePos_.store(0);
}
ConsoleListener::~ConsoleListener() {
g_Initialized = false;
Close();
}
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;
}
return FALSE;
}
void ConsoleListener::Init(bool AutoOpen, int Width, int Height) {
openWidth_ = Width;
openHeight_ = Height;
if (AutoOpen) {
Open();
hidden_ = false;
}
}
void ConsoleListener::Open() {
if (!GetConsoleWindow()) {
AllocConsole();
hWnd = GetConsoleWindow();
ShowWindow(hWnd, SW_SHOWDEFAULT);
HMENU hMenu = GetSystemMenu(hWnd, false);
EnableMenuItem(hMenu,SC_CLOSE,MF_GRAYED|MF_BYCOMMAND);
hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
if (SetConsoleCtrlHandler((PHANDLER_ROUTINE)ConsoleHandler, TRUE)) {
OutputDebugStringA("Console handler is installed!\n");
}
SetConsoleTitle(L"PPSSPP Debug Console");
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
LetterSpace(openWidth_, LOG_MAX_DISPLAY_LINES);
} else {
hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
}
if (useThread_ && hTriggerEvent != NULL && !thread_.joinable()) {
thread_ = std::thread([&] {
SetCurrentThreadName("Console");
LogWriterThread();
});
}
}
void ConsoleListener::Show(bool bShow) {
if (bShow && hidden_) {
if (!IsOpen()) {
Open();
}
ShowWindow(GetConsoleWindow(), SW_SHOW);
hidden_ = false;
} else if (!bShow && !hidden_) {
ShowWindow(GetConsoleWindow(), SW_HIDE);
hidden_ = true;
}
}
void ConsoleListener::UpdateHandle() {
hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
}
void ConsoleListener::Close() {
if (thread_.joinable()) {
logPendingWritePos_.store((u32)-1, std::memory_order_release);
SetEvent(hTriggerEvent);
thread_.join();
}
if (hTriggerEvent) {
DeleteCriticalSection(&criticalSection);
CloseHandle(hTriggerEvent);
hTriggerEvent = nullptr;
}
if (logPending_) {
delete [] logPending_;
logPending_ = nullptr;
}
if (hConsole) {
FreeConsole();
hConsole = nullptr;
}
}
bool ConsoleListener::IsOpen() {
return (hConsole != nullptr);
}
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.");
BOOL SB, SW;
if (BufferFirst) {
COORD Co = {(SHORT)BufferWidth, (SHORT)BufferHeight};
SB = SetConsoleScreenBufferSize(hConsole, Co);
SMALL_RECT coo = {(SHORT)0, (SHORT)0, (SHORT)ScreenWidth, (SHORT)ScreenHeight};
SW = SetConsoleWindowInfo(hConsole, TRUE, &coo);
} else {
SMALL_RECT coo = {(SHORT)0, (SHORT)0, (SHORT)ScreenWidth, (SHORT)ScreenHeight};
SW = SetConsoleWindowInfo(hConsole, TRUE, &coo);
COORD Co = {(SHORT)BufferWidth, (SHORT)BufferHeight};
SB = SetConsoleScreenBufferSize(hConsole, Co);
}
}
void ConsoleListener::LetterSpace(int Width, int Height) {
_dbg_assert_msg_(IsOpen(), "Don't call this before opening the console.");
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;
BufferWidthHeight(NewBufferWidth, OldBufferHeight, NewScreenWidth, OldScreenHeight, (NewBufferWidth > OldScreenWidth-1));
BufferWidthHeight(NewBufferWidth, NewBufferHeight, NewScreenWidth, NewScreenHeight, (NewBufferHeight > OldScreenHeight-1));
}
COORD ConsoleListener::GetCoordinates(int BytesRead, int BufferWidth) {
COORD Ret = {0, 0};
int Step = (int)floor((float)BytesRead / (float)BufferWidth);
Ret.Y += Step;
Ret.X = BytesRead - (BufferWidth * Step);
return Ret;
}
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);
if (logPendingWritePos_ == (u32)-1) {
break;
}
logLocalSize = start + count;
}
for (char *Text = logLocal, *End = logLocal + logLocalSize; Text < End; ) {
LogLevel Level = LogLevel::LINFO;
char *next = (char *) memchr(Text + 1, '\033', End - Text);
size_t Len = next - Text;
if (!next) {
Len = End - Text;
}
if (Text[0] == '\033' && Text + 1 < End) {
Level = (LogLevel)(Text[1] - '0');
Len -= 2;
Text += 2;
}
if (logPendingWritePos_ == (u32)-1) {
break;
}
WriteToConsole(Level, Text, Len);
Text += Len;
}
}
delete [] logLocal;
}
void ConsoleListener::SendToThread(LogLevel Level, const char *Text) {
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 (useColor_) {
snprintf(ColorAttr, 16, "\033%d", Level);
_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;
}
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);
if (nextNewline != NULL)
logPendingReadPos_ = (u32)(nextNewline - logPending_ + 1);
}
if (logPendingWritePos_ == (u32) -1) {
LeaveCriticalSection(&criticalSection);
return;
}
logPendingWritePos_.store(logWritePos, std::memory_order::memory_order_release);
LeaveCriticalSection(&criticalSection);
SetEvent(hTriggerEvent);
}
void ConsoleListener::WriteToConsole(LogLevel Level, const char *Text, size_t Len) {
_dbg_assert_msg_(IsOpen(), "Don't call this before opening the console.");
DWORD cCharsWritten;
WORD Color;
static wchar_t tempBuf[2048];
switch (Level) {
case LogLevel::LNOTICE:
Color = FOREGROUND_GREEN | FOREGROUND_INTENSITY;
break;
case LogLevel::LERROR:
Color = FOREGROUND_RED | FOREGROUND_INTENSITY;
break;
case LogLevel::LWARNING:
Color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY;
break;
case LogLevel::LINFO:
Color = FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY;
break;
case LogLevel::LDEBUG:
Color = FOREGROUND_INTENSITY;
break;
default:
Color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
break;
}
if (Len > 10) {
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
int wlen = MultiByteToWideChar(CP_UTF8, 0, Text, (int)Len, NULL, 0);
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, 0);
MultiByteToWideChar(CP_UTF8, 0, Text, (int)Len, tempBuf, wlen);
WriteConsole(hConsole, tempBuf, (DWORD)wlen, &cCharsWritten, NULL);
}
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 (Width < 8 || Height < 12) return;
std::string SLog = "";
CONSOLE_SCREEN_BUFFER_INFO ConInfo;
GetConsoleScreenBufferInfo(hConsole, &ConInfo);
DWORD BufferSize = ConInfo.dwSize.X * ConInfo.dwSize.Y;
DWORD cCharsRead = 0;
COORD coordScreen = { 0, 0 };
static const int MAX_BYTES = 1024 * 16;
std::vector<std::array<wchar_t, MAX_BYTES>> Str;
std::vector<std::array<WORD, MAX_BYTES>> Attr;
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");
if (cAttrRead == 0) break;
BytesRead += cAttrRead;
coordScreen = GetCoordinates(BytesRead, ConInfo.dwSize.X);
}
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);
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 (Resize) {
MoveWindow(GetConsoleWindow(), Left, Top, (Width + 100), Height, true);
}
}
void ConsoleListener::Log(const LogMessage &msg) {
char buf[2048];
snprintf(buf, sizeof(buf), "%s %s %s", msg.timestamp, msg.header, msg.msg.c_str());
buf[sizeof(buf) - 2] = '\n';
buf[sizeof(buf) - 1] = '\0';
if (!useThread_ && IsOpen())
WriteToConsole(msg.level, buf, strlen(buf));
else
SendToThread(msg.level, buf);
}
void ConsoleListener::ClearScreen(bool Cursor) {
_dbg_assert_msg_(IsOpen(), "Don't call this before opening the console.");
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(hConsole, &csbi);
DWORD dwConSize = csbi.dwSize.X * csbi.dwSize.Y;
DWORD cCharsWritten;
COORD coordScreen = { 0, 0 };
FillConsoleOutputCharacter(hConsole, TEXT(' '), dwConSize, coordScreen, &cCharsWritten);
GetConsoleScreenBufferInfo(hConsole, &csbi);
FillConsoleOutputAttribute(hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten);
if (Cursor) {
SetConsoleCursorPosition(hConsole, coordScreen);
}
}
#endif