From 6269a01b4e9963ffdaf98ddf5d5f2a90d49e58ff Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 2 Jul 2018 11:10:41 -0600 Subject: Add configurable logging backends --- src/common/common_paths.h | 3 + src/common/file_util.cpp | 17 +++-- src/common/file_util.h | 11 ++- src/common/logging/backend.cpp | 157 ++++++++++++++++++++++++++++++++++++++--- src/common/logging/backend.h | 87 ++++++++++++++++++++++- 5 files changed, 257 insertions(+), 18 deletions(-) (limited to 'src/common') diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 0a6132dab..d03fca314 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -32,12 +32,15 @@ #define SDMC_DIR "sdmc" #define NAND_DIR "nand" #define SYSDATA_DIR "sysdata" +#define LOG_DIR "log" // Filenames // Files in the directory returned by GetUserPath(D_CONFIG_IDX) #define EMU_CONFIG "emu.ini" #define DEBUGGER_CONFIG "debugger.ini" #define LOGGER_CONFIG "logger.ini" +// Files in the directory returned by GetUserPath(D_LOGS_IDX) +#define LOG_FILE "citra_log.txt" // Sys files #define SHARED_FONT "shared_font.bin" diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 2152e3fea..b9e1fd1f6 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -713,6 +713,8 @@ const std::string& GetUserPath(const unsigned int DirIDX, const std::string& new paths[D_SDMC_IDX] = paths[D_USER_IDX] + SDMC_DIR DIR_SEP; paths[D_NAND_IDX] = paths[D_USER_IDX] + NAND_DIR DIR_SEP; paths[D_SYSDATA_IDX] = paths[D_USER_IDX] + SYSDATA_DIR DIR_SEP; + // TODO: Put the logs in a better location for each OS + paths[D_LOGS_IDX] = paths[D_USER_IDX] + LOG_DIR DIR_SEP; } if (!newPath.empty()) { @@ -799,8 +801,8 @@ void SplitFilename83(const std::string& filename, std::array& short_nam IOFile::IOFile() {} -IOFile::IOFile(const std::string& filename, const char openmode[]) { - Open(filename, openmode); +IOFile::IOFile(const std::string& filename, const char openmode[], int flags) { + Open(filename, openmode, flags); } IOFile::~IOFile() { @@ -821,11 +823,16 @@ void IOFile::Swap(IOFile& other) noexcept { std::swap(m_good, other.m_good); } -bool IOFile::Open(const std::string& filename, const char openmode[]) { +bool IOFile::Open(const std::string& filename, const char openmode[], int flags) { Close(); #ifdef _WIN32 - _wfopen_s(&m_file, Common::UTF8ToUTF16W(filename).c_str(), - Common::UTF8ToUTF16W(openmode).c_str()); + if (flags != 0) { + m_file = _wfsopen(Common::UTF8ToUTF16W(filename).c_str(), + Common::UTF8ToUTF16W(openmode).c_str(), flags); + } else { + _wfopen_s(&m_file, Common::UTF8ToUTF16W(filename).c_str(), + Common::UTF8ToUTF16W(openmode).c_str()); + } #else m_file = fopen(filename.c_str(), openmode); #endif diff --git a/src/common/file_util.h b/src/common/file_util.h index fc6b3ea46..5bc7fbf7c 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -156,7 +156,10 @@ void SplitFilename83(const std::string& filename, std::array& short_nam class IOFile : public NonCopyable { public: IOFile(); - IOFile(const std::string& filename, const char openmode[]); + // flags is used for windows specific file open mode flags, which + // allows yuzu to open the logs in shared write mode, so that the file + // isn't considered "locked" while yuzu is open and people can open the log file and view it + IOFile(const std::string& filename, const char openmode[], int flags = 0); ~IOFile(); @@ -165,7 +168,7 @@ public: void Swap(IOFile& other) noexcept; - bool Open(const std::string& filename, const char openmode[]); + bool Open(const std::string& filename, const char openmode[], int flags = 0); bool Close(); template @@ -220,6 +223,10 @@ public: return WriteArray(&object, 1); } + size_t WriteString(const std::string& str) { + return WriteArray(str.c_str(), str.length()); + } + bool IsOpen() const { return nullptr != m_file; } diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index c26b20062..242914c6a 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -2,16 +2,145 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include // For _SH_DENYWR +#else +#define _SH_DENYWR 0 +#endif #include "common/assert.h" +#include "common/common_funcs.h" // snprintf compatibility define #include "common/logging/backend.h" -#include "common/logging/filter.h" #include "common/logging/log.h" #include "common/logging/text_formatter.h" #include "common/string_util.h" +#include "common/threadsafe_queue.h" namespace Log { +/** + * Static state as a singleton. + */ +class Impl { +public: + static Impl& Instance() { + static Impl backend; + return backend; + } + + Impl(Impl const&) = delete; + const Impl& operator=(Impl const&) = delete; + + void PushEntry(Entry e) { + std::lock_guard lock(message_mutex); + message_queue.Push(std::move(e)); + message_cv.notify_one(); + } + + void AddBackend(std::unique_ptr backend) { + std::lock_guard lock(writing_mutex); + backends.push_back(std::move(backend)); + } + + void RemoveBackend(const std::string& backend_name) { + std::lock_guard lock(writing_mutex); + auto it = std::remove_if(backends.begin(), backends.end(), [&backend_name](const auto& i) { + return !strcmp(i->GetName(), backend_name.c_str()); + }); + backends.erase(it, backends.end()); + } + + const Filter& GetGlobalFilter() const { + return filter; + } + + void SetGlobalFilter(const Filter& f) { + filter = f; + } + + Backend* GetBackend(const std::string& backend_name) { + auto it = std::find_if(backends.begin(), backends.end(), [&backend_name](const auto& i) { + return !strcmp(i->GetName(), backend_name.c_str()); + }); + if (it == backends.end()) + return nullptr; + return it->get(); + } + +private: + Impl() { + backend_thread = std::thread([&] { + Entry entry; + auto write_logs = [&](Entry& e) { + std::lock_guard lock(writing_mutex); + for (const auto& backend : backends) { + backend->Write(e); + } + }; + while (true) { + std::unique_lock lock(message_mutex); + message_cv.wait(lock, [&] { return !running || message_queue.Pop(entry); }); + if (!running) { + break; + } + write_logs(entry); + } + // Drain the logging queue. Only writes out up to MAX_LOGS_TO_WRITE to prevent a case + // where a system is repeatedly spamming logs even on close. + constexpr int MAX_LOGS_TO_WRITE = 100; + int logs_written = 0; + while (logs_written++ < MAX_LOGS_TO_WRITE && message_queue.Pop(entry)) { + write_logs(entry); + } + }); + } + + ~Impl() { + running = false; + message_cv.notify_one(); + backend_thread.join(); + } + + std::atomic_bool running{true}; + std::mutex message_mutex, writing_mutex; + std::condition_variable message_cv; + std::thread backend_thread; + std::vector> backends; + Common::MPSCQueue message_queue; + Filter filter; +}; + +void ConsoleBackend::Write(const Entry& entry) { + PrintMessage(entry); +} + +void ColorConsoleBackend::Write(const Entry& entry) { + PrintColoredMessage(entry); +} + +// _SH_DENYWR allows read only access to the file for other programs. +// It is #defined to 0 on other platforms +FileBackend::FileBackend(const std::string& filename) + : file(filename, "w", _SH_DENYWR), bytes_written(0) {} + +void FileBackend::Write(const Entry& entry) { + // prevent logs from going over the maximum size (in case its spamming and the user doesn't + // know) + constexpr size_t MAX_BYTES_WRITTEN = 50 * 1024L * 1024L; + if (!file.IsOpen() || bytes_written > MAX_BYTES_WRITTEN) { + return; + } + bytes_written += file.WriteString(FormatLogMessage(entry) + '\n'); + if (entry.log_level >= Level::Error) { + file.Flush(); + } +} + /// Macro listing all log classes. Code should define CLS and SUB as desired before invoking this. #define ALL_LOG_CLASSES() \ CLS(Log) \ @@ -125,20 +254,32 @@ Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsign return entry; } -static Filter* filter = nullptr; +void SetGlobalFilter(const Filter& filter) { + Impl::Instance().SetGlobalFilter(filter); +} + +void AddBackend(std::unique_ptr backend) { + Impl::Instance().AddBackend(std::move(backend)); +} -void SetFilter(Filter* new_filter) { - filter = new_filter; +void RemoveBackend(const std::string& backend_name) { + Impl::Instance().RemoveBackend(backend_name); +} + +Backend* GetBackend(const std::string& backend_name) { + return Impl::Instance().GetBackend(backend_name); } void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, const char* format, const fmt::format_args& args) { - if (filter && !filter->CheckMessage(log_class, log_level)) + auto filter = Impl::Instance().GetGlobalFilter(); + if (!filter.CheckMessage(log_class, log_level)) return; + Entry entry = CreateEntry(log_class, log_level, filename, line_num, function, fmt::vformat(format, args)); - PrintColoredMessage(entry); + Impl::Instance().PushEntry(std::move(entry)); } -} // namespace Log +} // namespace Log \ No newline at end of file diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h index 7e81efb23..57cdf6b2d 100644 --- a/src/common/logging/backend.h +++ b/src/common/logging/backend.h @@ -1,13 +1,15 @@ // Copyright 2014 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. - #pragma once #include #include +#include #include #include +#include "common/file_util.h" +#include "common/logging/filter.h" #include "common/logging/log.h" namespace Log { @@ -34,6 +36,80 @@ struct Entry { Entry& operator=(const Entry& o) = default; }; +/** + * Interface for logging backends. As loggers can be created and removed at runtime, this can be + * used by a frontend for adding a custom logging backend as needed + */ +class Backend { +public: + virtual ~Backend() = default; + virtual void SetFilter(const Filter& new_filter) { + filter = new_filter; + } + virtual const char* GetName() const = 0; + virtual void Write(const Entry& entry) = 0; + +private: + Filter filter; +}; + +/** + * Backend that writes to stderr without any color commands + */ +class ConsoleBackend : public Backend { +public: + static const char* Name() { + return "console"; + } + const char* GetName() const override { + return Name(); + } + void Write(const Entry& entry) override; +}; + +/** + * Backend that writes to stderr and with color + */ +class ColorConsoleBackend : public Backend { +public: + static const char* Name() { + return "color_console"; + } + + const char* GetName() const override { + return Name(); + } + void Write(const Entry& entry) override; +}; + +/** + * Backend that writes to a file passed into the constructor + */ +class FileBackend : public Backend { +public: + explicit FileBackend(const std::string& filename); + + static const char* Name() { + return "file"; + } + + const char* GetName() const override { + return Name(); + } + + void Write(const Entry& entry) override; + +private: + FileUtil::IOFile file; + size_t bytes_written; +}; + +void AddBackend(std::unique_ptr backend); + +void RemoveBackend(const std::string& backend_name); + +Backend* GetBackend(const std::string& backend_name); + /** * Returns the name of the passed log class as a C-string. Subclasses are separated by periods * instead of underscores as in the enumeration. @@ -49,5 +125,10 @@ const char* GetLevelName(Level log_level); Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr, const char* function, std::string message); -void SetFilter(Filter* filter); -} // namespace Log +/** + * The global filter will prevent any messages from even being processed if they are filtered. Each + * backend can have a filter, but if the level is lower than the global filter, the backend will + * never get the message + */ +void SetGlobalFilter(const Filter& filter); +} // namespace Log \ No newline at end of file -- cgit v1.2.3