mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 09:45:24 +02:00
log.channel.set is meant for temporary, session-only diagnostic tweaks (e.g. quieting a noisy channel while investigating something over the WebSocket debugger). It was going through the same SetLogLevel/SetEnabled calls the UI settings use, so a normal app exit would persist whatever channels/levels the debugger last left set, silently overwriting the user's real saved log preferences for future runs - discovered when a later session's default logging looked "off" for no apparent reason. LogManager now tracks whether the debugger has touched channel config this run and skips SaveConfig() entirely if so, leaving whatever's already on disk untouched.
510 lines
12 KiB
C++
510 lines
12 KiB
C++
// 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 "ppsspp_config.h"
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
|
|
#include <android/log.h>
|
|
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
#include "Common/Data/Encoding/Utf8.h"
|
|
#include "Common/Log/LogManager.h"
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
#include <io.h>
|
|
#include "Common/Log/ConsoleListener.h"
|
|
#endif
|
|
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/Data/Format/IniFile.h"
|
|
#include "Common/StringUtils.h"
|
|
|
|
LogChannel g_log[(size_t)Log::NUMBER_OF_LOGS];
|
|
LogManager g_logManager;
|
|
|
|
const char *hleCurrentThreadName = nullptr;
|
|
|
|
bool g_bDummySetting = true;
|
|
bool *g_bLogEnabledSetting = &g_bDummySetting;
|
|
|
|
static const char level_to_char[8] = "-NEWIDV";
|
|
|
|
#if PPSSPP_PLATFORM(UWP) && defined(_DEBUG)
|
|
#define LOG_MSC_OUTPUTDEBUG true
|
|
#else
|
|
#define LOG_MSC_OUTPUTDEBUG false
|
|
#endif
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
void AndroidLog(const LogMessage &message);
|
|
#endif
|
|
|
|
// TODO: Get rid of this wrapper, not much point.
|
|
void GenericLog(Log type, LogLevel level, const char *file, int line, const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
g_logManager.LogLine(level, type, file, line, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
// NOTE: Needs to be kept in sync with the Log enum.
|
|
static const char * const g_logTypeNames[] = {
|
|
"System",
|
|
"Config",
|
|
"Boot",
|
|
"Common",
|
|
"CPU",
|
|
"FileSystem",
|
|
"G3D",
|
|
"TexCache",
|
|
"HLE",
|
|
"JIT",
|
|
"Loader",
|
|
"Mpeg",
|
|
"Atrac",
|
|
"ME",
|
|
"MemMap",
|
|
"SasMix",
|
|
"SaveState",
|
|
"FrameBuf",
|
|
"Audio",
|
|
"IO",
|
|
"Achievements",
|
|
"HTTP",
|
|
"Printf",
|
|
"TexReplacement",
|
|
"Debugger",
|
|
"GeDebugger",
|
|
"UI",
|
|
"IAP",
|
|
"CwCheats",
|
|
"Net",
|
|
"sceAudio",
|
|
"sceCtrl",
|
|
"sceDisplay",
|
|
"sceFont",
|
|
"sceGe",
|
|
"sceIntc",
|
|
"sceIo",
|
|
"sceKernel",
|
|
"sceModule",
|
|
"sceNet",
|
|
"sceRtc",
|
|
"sceSas",
|
|
"sceUtility",
|
|
"sceMisc",
|
|
"sceReg",
|
|
};
|
|
|
|
const char *LogManager::GetLogTypeName(Log type) {
|
|
return g_logTypeNames[(size_t)type];
|
|
}
|
|
|
|
// Ultra plain output, for CI and stuff.
|
|
void PrintfLog(const LogMessage &message);
|
|
|
|
void LogManager::Init(bool *enabledSetting, bool headless) {
|
|
g_bLogEnabledSetting = enabledSetting;
|
|
if (initialized_) {
|
|
// Just update the pointer, already done above.
|
|
return;
|
|
}
|
|
initialized_ = true;
|
|
|
|
_dbg_assert_(ARRAY_SIZE(g_logTypeNames) == (size_t)Log::NUMBER_OF_LOGS);
|
|
_dbg_assert_(ARRAY_SIZE(g_logTypeNames) == ARRAY_SIZE(g_log));
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(g_log); i++) {
|
|
g_log[i].enabled = true;
|
|
g_log[i].level = LogLevel::LINFO;
|
|
}
|
|
}
|
|
|
|
void LogManager::Shutdown() {
|
|
if (!initialized_) {
|
|
// already done
|
|
return;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lk(logFileLock_);
|
|
if (fp_) {
|
|
fclose(fp_);
|
|
fp_ = nullptr;
|
|
}
|
|
}
|
|
|
|
outputs_ = (LogOutput)0;
|
|
|
|
ringLog_.Clear();
|
|
initialized_ = false;
|
|
}
|
|
|
|
LogManager::LogManager() {
|
|
#if PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(UWP) || PPSSPP_PLATFORM(SWITCH)
|
|
stdioUseColor_ = false;
|
|
#elif defined(_MSC_VER)
|
|
stdioUseColor_ = false;
|
|
#elif defined(__APPLE__)
|
|
// Xcode builtin terminal used for debugging does not support colours.
|
|
// Fortunately it can be detected with a TERM env variable.
|
|
stdioUseColor_ = isatty(fileno(stdout)) && getenv("TERM") != NULL;
|
|
#else
|
|
stdioUseColor_ = isatty(fileno(stdout));
|
|
#endif
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
if (IsDebuggerPresent()) {
|
|
outputs_ |= LogOutput::DebugString;
|
|
}
|
|
#if !PPSSPP_PLATFORM(UWP)
|
|
if (!consoleLog_) {
|
|
consoleLog_ = new ConsoleListener();
|
|
}
|
|
outputs_ |= LogOutput::WinConsole;
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
LogManager::~LogManager() {
|
|
Shutdown();
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
|
|
delete consoleLog_;
|
|
consoleLog_ = nullptr;
|
|
#endif
|
|
}
|
|
|
|
void LogManager::SetFileLogPath(const Path &filename) {
|
|
std::lock_guard<std::mutex> lk(logFileLock_);
|
|
if (fp_ && filename == logFilename_) {
|
|
// All good
|
|
return;
|
|
}
|
|
|
|
if (fp_) {
|
|
fclose(fp_);
|
|
fp_ = nullptr;
|
|
}
|
|
|
|
if (!filename.empty()) {
|
|
logFilename_ = Path(filename);
|
|
|
|
if (outputs_ & LogOutput::File) {
|
|
File::CreateFullPath(logFilename_.NavigateUp());
|
|
fp_ = File::OpenCFile(logFilename_, "at");
|
|
logFileOpenFailed_ = fp_ == nullptr;
|
|
if (logFileOpenFailed_) {
|
|
printf("Failed to open log file %s\n", logFilename_.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LogManager::SaveConfig(Section *section) {
|
|
if (channelsChangedByDebugger_) {
|
|
// Leave the section as whatever was already on disk - see the doc comment on
|
|
// NotifyChannelsChangedByDebugger().
|
|
return;
|
|
}
|
|
for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; i++) {
|
|
section->Set((std::string(g_logTypeNames[i]) + "Enabled"), g_log[i].enabled);
|
|
section->Set((std::string(g_logTypeNames[i]) + "Level"), (int)g_log[i].level);
|
|
}
|
|
}
|
|
|
|
void LogManager::LoadConfig(const Section *section) {
|
|
for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; i++) {
|
|
// Defaults. Get now doesn't write the output if it fails.
|
|
bool enabled = true;
|
|
int level = (int)LogLevel::LERROR;
|
|
section->Get((std::string(g_logTypeNames[i]) + "Enabled"), &enabled);
|
|
section->Get((std::string(g_logTypeNames[i]) + "Level"), &level);
|
|
g_log[i].enabled = enabled;
|
|
g_log[i].level = (LogLevel)level;
|
|
}
|
|
}
|
|
|
|
void LogManager::SetOutputsEnabled(LogOutput outputs) {
|
|
outputs_ = outputs;
|
|
if (outputs & LogOutput::File) {
|
|
SetFileLogPath(logFilename_);
|
|
}
|
|
}
|
|
|
|
void LogManager::LogLine(LogLevel level, Log type, const char *file, int line, const char *format, va_list args) {
|
|
char msgBuf[1024];
|
|
|
|
const LogChannel &log = g_log[(size_t)type];
|
|
if (level > log.level || !log.enabled || outputs_ == (LogOutput)0) {
|
|
// If we get here, it should have been caught earlier.
|
|
return;
|
|
}
|
|
|
|
LogMessage message;
|
|
message.level = level;
|
|
message.log = g_logTypeNames[(size_t)type];
|
|
|
|
#ifdef _WIN32
|
|
static const char sep = '\\';
|
|
#else
|
|
static const char sep = '/';
|
|
#endif
|
|
const char *fileshort = strrchr(file, sep);
|
|
if (fileshort) {
|
|
do
|
|
--fileshort;
|
|
while (fileshort > file && *fileshort != sep);
|
|
if (fileshort != file)
|
|
file = fileshort + 1;
|
|
}
|
|
|
|
const char *threadName;
|
|
#if PPSSPP_PLATFORM(WINDOWS) || PPSSPP_PLATFORM(MAC)
|
|
const char *hostThreadName = GetCurrentThreadName();
|
|
if ((hostThreadName && strcmp(hostThreadName, "EmuThread") != 0) || !hleCurrentThreadName) {
|
|
// Use the host thread name.
|
|
threadName = hostThreadName ? hostThreadName : "unknown";
|
|
} else {
|
|
// Use the PSP HLE thread name.
|
|
threadName = hleCurrentThreadName;
|
|
}
|
|
#else
|
|
threadName = hleCurrentThreadName;
|
|
#endif
|
|
|
|
if (threadName) {
|
|
snprintf(message.header, sizeof(message.header), "%-12.12s %c[%s]: %s:%d",
|
|
threadName, level_to_char[(int)level],
|
|
message.log,
|
|
file, line);
|
|
} else {
|
|
snprintf(message.header, sizeof(message.header), "%s:%d %c[%s]:",
|
|
file, line, level_to_char[(int)level],
|
|
message.log);
|
|
}
|
|
|
|
GetCurrentTimeFormatted(message.timestamp);
|
|
|
|
va_list args_copy;
|
|
|
|
va_copy(args_copy, args);
|
|
size_t neededBytes = vsnprintf(msgBuf, sizeof(msgBuf), format, args);
|
|
message.msg.resize(neededBytes + 1);
|
|
if (neededBytes > sizeof(msgBuf)) {
|
|
// Needed more space? Re-run vsnprintf.
|
|
vsnprintf(&message.msg[0], neededBytes + 1, format, args_copy);
|
|
} else {
|
|
memcpy(&message.msg[0], msgBuf, neededBytes);
|
|
}
|
|
message.msg[neededBytes] = '\n';
|
|
va_end(args_copy);
|
|
|
|
if (outputs_ & LogOutput::Stdio) {
|
|
// This has its own mutex.
|
|
StdioLog(message);
|
|
}
|
|
|
|
// OK, now go through the possible listeners in order.
|
|
if (outputs_ & LogOutput::File) {
|
|
// Lock covers the fp_ check too - SetFileLogPath()/Shutdown() can close it concurrently.
|
|
std::lock_guard<std::mutex> lk(logFileLock_);
|
|
if (fp_) {
|
|
fprintf(fp_, "%s %s %s", message.timestamp, message.header, message.msg.c_str());
|
|
// Is this really necessary to do every time? I guess to catch the last message before a crash..
|
|
fflush(fp_);
|
|
}
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
if (outputs_ & LogOutput::DebugString) {
|
|
// No mutex needed
|
|
char buffer[4096];
|
|
// We omit the timestamp for easy copy-paste-diffing.
|
|
snprintf(buffer, sizeof(buffer), "%s %s", message.header, message.msg.c_str());
|
|
OutputDebugStringUTF8(buffer);
|
|
}
|
|
#endif
|
|
|
|
if (outputs_ & LogOutput::RingBuffer) {
|
|
ringLog_.Log(message);
|
|
}
|
|
|
|
if (outputs_ & LogOutput::Printf) {
|
|
PrintfLog(message);
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
|
|
if (outputs_ & LogOutput::WinConsole) {
|
|
if (consoleLog_) {
|
|
consoleLog_->Log(message);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (outputs_ & LogOutput::ExternalCallback) {
|
|
if (externalCallback_) {
|
|
externalCallback_(message, externalUserData_);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RingbufferLog::Log(const LogMessage &message) {
|
|
std::lock_guard<std::mutex> lock(ringLock_);
|
|
messages_[curMessage_] = message;
|
|
curMessage_++;
|
|
if (curMessage_ >= MAX_LOGS)
|
|
curMessage_ -= MAX_LOGS;
|
|
count_++;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
|
|
void OutputDebugStringUTF8(const char *p) {
|
|
wchar_t *temp = new wchar_t[65536];
|
|
|
|
int len = std::min(16383*4, (int)strlen(p));
|
|
int size = (int)MultiByteToWideChar(CP_UTF8, 0, p, len, NULL, 0);
|
|
MultiByteToWideChar(CP_UTF8, 0, p, len, temp, size);
|
|
temp[size] = 0;
|
|
|
|
OutputDebugString(temp);
|
|
delete[] temp;
|
|
}
|
|
|
|
#else
|
|
|
|
void OutputDebugStringUTF8(const char *p) {
|
|
INFO_LOG(Log::System, "%s", p);
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef HAVE_LIBRETRO_VFS
|
|
#undef fprintf
|
|
#endif
|
|
|
|
void LogManager::StdioLog(const LogMessage &message) {
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
#ifndef LOG_APP_NAME
|
|
#define LOG_APP_NAME "PPSSPP"
|
|
#endif
|
|
int mode;
|
|
switch (message.level) {
|
|
case LogLevel::LWARNING:
|
|
mode = ANDROID_LOG_WARN;
|
|
break;
|
|
case LogLevel::LERROR:
|
|
mode = ANDROID_LOG_ERROR;
|
|
break;
|
|
default:
|
|
mode = ANDROID_LOG_INFO;
|
|
break;
|
|
}
|
|
|
|
// Long log messages need splitting up.
|
|
// Not sure what the actual limit is (seems to vary), but let's be conservative.
|
|
const size_t maxLogLength = 512;
|
|
if (message.msg.length() < maxLogLength) {
|
|
// Log with simplified headers as Android already provides timestamp etc.
|
|
__android_log_print(mode, LOG_APP_NAME, "[%s] %s", message.log, message.msg.c_str());
|
|
} else {
|
|
std::string_view msg = message.msg;
|
|
|
|
// Ideally we should split at line breaks, but it's at least fairly usable anyway.
|
|
std::string_view first_part = msg.substr(0, maxLogLength);
|
|
__android_log_print(mode, LOG_APP_NAME, "[%s] %.*s", message.log, (int)first_part.size(), first_part.data());
|
|
msg = msg.substr(maxLogLength);
|
|
|
|
while (msg.length() > maxLogLength) {
|
|
std::string_view next_part = msg.substr(0, maxLogLength);
|
|
__android_log_print(mode, LOG_APP_NAME, "%.*s", (int)next_part.size(), next_part.data());
|
|
msg = msg.substr(maxLogLength);
|
|
}
|
|
// Print the final part.
|
|
__android_log_print(mode, LOG_APP_NAME, "%.*s", (int)msg.size(), msg.data());
|
|
}
|
|
#else
|
|
char text[2048];
|
|
snprintf(text, sizeof(text), "%s %s %s", message.timestamp, message.header, message.msg.c_str());
|
|
text[sizeof(text) - 2] = '\n';
|
|
text[sizeof(text) - 1] = '\0';
|
|
|
|
const char *colorAttr = "";
|
|
const char *resetAttr = "";
|
|
|
|
if (stdioUseColor_) {
|
|
resetAttr = "\033[0m";
|
|
switch (message.level) {
|
|
case LogLevel::LNOTICE: // light green
|
|
colorAttr = "\033[92m";
|
|
break;
|
|
case LogLevel::LERROR: // light red
|
|
colorAttr = "\033[91m";
|
|
break;
|
|
case LogLevel::LWARNING: // light yellow
|
|
colorAttr = "\033[93m";
|
|
break;
|
|
case LogLevel::LINFO: // cyan
|
|
colorAttr = "\033[96m";
|
|
break;
|
|
case LogLevel::LDEBUG: // gray
|
|
colorAttr = "\033[90m";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(stdioLock_);
|
|
fprintf(stderr, "%s%s%s", colorAttr, text, resetAttr);
|
|
#endif
|
|
}
|
|
|
|
void PrintfLog(const LogMessage &message) {
|
|
const char *category = message.log;
|
|
|
|
switch (message.level) {
|
|
case LogLevel::LVERBOSE:
|
|
fprintf(stderr, "V %s: %s", category, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LDEBUG:
|
|
fprintf(stderr, "D %s: %s", category, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LINFO:
|
|
fprintf(stderr, "I %s: %s", category, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LERROR:
|
|
fprintf(stderr, "E %s: %s", category, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LWARNING:
|
|
fprintf(stderr, "W %s: %s", category, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LNOTICE:
|
|
default:
|
|
fprintf(stderr, "N %s: %s", category, message.msg.c_str());
|
|
break;
|
|
}
|
|
}
|