diff --git a/Common/Cpp/Logging/FileLogger.cpp b/Common/Cpp/Logging/FileLogger.cpp index a087453243..361d5609a8 100644 --- a/Common/Cpp/Logging/FileLogger.cpp +++ b/Common/Cpp/Logging/FileLogger.cpp @@ -133,6 +133,17 @@ void FileLogger::thread_loop(){ return m_stopping || !m_queue.empty(); }); if (m_stopping){ + if (m_config.flush_when_exit && m_file.is_open()){ + while (m_queue.size() > 0){ + std::string msg = std::move(m_queue.front().first); + // Write to file + std::string line = normalize_newlines(msg); + std::string file_str = to_file_str(line); + m_file.write(file_str.c_str(), file_str.size()); + m_queue.pop_front(); + } + m_file.flush(); + } break; } auto& item = m_queue.front(); diff --git a/Common/Cpp/Logging/FileLogger.h b/Common/Cpp/Logging/FileLogger.h index 9f08fe7d79..ef71c63742 100644 --- a/Common/Cpp/Logging/FileLogger.h +++ b/Common/Cpp/Logging/FileLogger.h @@ -28,10 +28,11 @@ struct FileLoggerConfig{ size_t max_queue_size = 10000; // Max pending log entries before blocking size_t max_file_size_bytes = 50 * 1024 * 1024; // Max file size before rotation (50MB default) size_t last_log_max_lines = 10000; // Max lines to keep in memory for get_last() + bool flush_when_exit = true; // Whether to save remaining logs to file when program exists }; -// A Qt-free file logger that: +// A file logger that: // 1. Writes log messages to a file asynchronously via a background thread // 2. Supports log rotation when the file exceeds a configured size // 3. Notifies registered listeners when a log message is written (for UI integration) diff --git a/Common/Cpp/Logging/GlobalLogger.cpp b/Common/Cpp/Logging/GlobalLogger.cpp new file mode 100644 index 0000000000..f6d1886120 --- /dev/null +++ b/Common/Cpp/Logging/GlobalLogger.cpp @@ -0,0 +1,36 @@ +/* Global Logger + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "GlobalLogger.h" + +namespace PokemonAutomation{ + + +// We should define this function at Main.cpp of each executable +// that uses the Logging/ library. +// +// This function is required by Common/Cpp/Logging/GlobalLogger.h:global_logger_raw() to initialize +// the global file logger. +// This function is called the first time `global_logger_raw()` is called to initialize the static +// local global file logger object. +// +// The advantage of such design is that: +// - This forces each executable's main.cpp to define how the logger is configured. +// - This ensures thread-safety on the global logger object. The static local `FileLogger` defined +// in `global_logger_raw()` is guaranteed by C++ to be thread-safe when constructed. +FileLoggerConfig make_global_config(); + + +Logger& global_logger_raw(){ + // Call a function `make_global_config()` that is not defined in + // this Logging/ library! To use this global logger, you must + // define `make_global_config()` in the Main.cpp. + static FileLogger logger(make_global_config()); + return logger; +} + + +} diff --git a/Common/Cpp/Logging/GlobalLogger.h b/Common/Cpp/Logging/GlobalLogger.h new file mode 100644 index 0000000000..9fa2a0cd9a --- /dev/null +++ b/Common/Cpp/Logging/GlobalLogger.h @@ -0,0 +1,27 @@ +/* Global Logger + * + * From: https://github.com/PokemonAutomation/ + * + * Provides a global file logger instance for application-wide logging. + * This is a Qt-free logger that can be used before Qt is initialized. + */ + +#ifndef PokemonAutomation_Logging_GlobalLogger_H +#define PokemonAutomation_Logging_GlobalLogger_H + +#include "FileLogger.h" + +namespace PokemonAutomation{ + + +// Return a global raw `FileLogger`. `FileLogger` is defined in FileLogger.h. +// "raw" here means the logger does not add any timestamp or tags to the incoming log lines. +// +// To initialize the global logger, implement `FileLoggerConfig make_global_config()` at +// Main.cpp to return the config for the global logger. If this function is not implemented, +// the program will not compile. +Logger& global_logger_raw(); + + +} +#endif diff --git a/SerialPrograms/Source/CommandLine/CommandLine_Main.cpp b/SerialPrograms/Source/CommandLine/CommandLine_Main.cpp index 15e36cc01a..97ab7fd531 100644 --- a/SerialPrograms/Source/CommandLine/CommandLine_Main.cpp +++ b/SerialPrograms/Source/CommandLine/CommandLine_Main.cpp @@ -16,6 +16,20 @@ using namespace PokemonAutomation; using namespace PokemonAutomation::NintendoSwitch; +namespace PokemonAutomation{ + +// This function is required by Common/Cpp/Logging/GlobalLogger.h:global_logger_raw() to initialize +// the global file logger. +// This function is called the first time `global_logger_raw()` is called to initialize the static +// local global file logger object. +FileLoggerConfig make_global_config(){ + return FileLoggerConfig{ + .file_path = "./SerialProgramsCommandLine.log" + }; +} + +} + int main(int argc, char* argv[]){ // // Set up output redirection for logging // OutputRedirector redirect_stdout(std::cout, "stdout", Color()); diff --git a/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.cpp b/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.cpp index 7a5c3f1df0..ac4f96a296 100644 --- a/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.cpp +++ b/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "Common/Cpp/Logging/GlobalLogger.h" #include "CommonFramework/Globals.h" #include "CommonFramework/GlobalSettingsPanel.h" #include "CommonFramework/Windows/DpiScaler.h" @@ -22,70 +23,13 @@ using std::endl; namespace PokemonAutomation{ -Logger& global_logger_raw(){ - auto get_log_filepath = [&](){ - QString application_name(QCoreApplication::applicationName()); - if (application_name.size() == 0){ - application_name = "SerialPrograms"; - } - return USER_FILE_PATH() + (application_name + ".log").toStdString(); - }; - - static FileWindowLogger logger(get_log_filepath(), LOG_HISTORY_LINES); - return logger; -} - - -FileWindowLogger::~FileWindowLogger(){ - m_file_logger.remove_listener(*this); -} - -FileWindowLogger::FileWindowLogger(const std::string& path, size_t max_queue_size) - : m_file_logger(FileLoggerConfig{ - .file_path = path, - .max_queue_size = max_queue_size, - .max_file_size_bytes = 50 * 1024 * 1024, // 50MB - .last_log_max_lines = max_queue_size, - }) -{ - m_file_logger.add_listener(*this); -} - -void FileWindowLogger::operator+=(FileWindowLoggerWindow& widget){ - std::lock_guard lg(m_window_lock); - m_windows.insert(&widget); -} - -void FileWindowLogger::operator-=(FileWindowLoggerWindow& widget){ - std::lock_guard lg(m_window_lock); - m_windows.erase(&widget); -} - -void FileWindowLogger::log(const std::string& msg, Color color){ - m_file_logger.log(msg, color); -} - -void FileWindowLogger::log(std::string&& msg, Color color){ - m_file_logger.log(std::move(msg), color); -} - -std::vector FileWindowLogger::get_last() const{ - return m_file_logger.get_last(); -} - -void FileWindowLogger::on_log(const std::string& msg, Color color){ +void FileWindowLoggerWindow::on_log(const std::string& msg, Color color){ // This is called from FileLogger's background thread. // Format the message for Qt display and send to all windows. - std::lock_guard lg(m_window_lock); - if (!m_windows.empty()){ - QString str = to_window_str(msg, color); - for (FileWindowLoggerWindow* window : m_windows){ - window->log(str); - } - } + emit signal_log(to_window_str(msg, color)); } -QString FileWindowLogger::to_window_str(const std::string& msg, Color color){ +QString FileWindowLoggerWindow::to_window_str(const std::string& msg, Color color){ // Convert message to HTML for display in QTextEdit. // Replace spaces with   and newlines with
. std::string str; @@ -111,10 +55,11 @@ QString FileWindowLogger::to_window_str(const std::string& msg, Color color){ } -FileWindowLoggerWindow::FileWindowLoggerWindow(FileWindowLogger& logger, QWidget* parent) +FileWindowLoggerWindow::FileWindowLoggerWindow(QWidget* parent) : QMainWindow(parent) - , m_logger(logger) + , m_logger(dynamic_cast(global_logger_raw())) { + m_logger.add_listener(*this); if (objectName().isEmpty()){ setObjectName(QString::fromUtf8("TextWindow")); } @@ -149,7 +94,6 @@ FileWindowLoggerWindow::FileWindowLoggerWindow(FileWindowLogger& logger, QWidget GlobalSettings::instance().LOG_WINDOW_SIZE->X_POS.add_listener(*this); GlobalSettings::instance().LOG_WINDOW_SIZE->Y_POS.add_listener(*this); - m_logger += *this; log("================================================================================"); log("Window Startup..."); log("Current path: " + QDir::currentPath()); @@ -161,7 +105,7 @@ FileWindowLoggerWindow::FileWindowLoggerWindow(FileWindowLogger& logger, QWidget FileWindowLoggerWindow::~FileWindowLoggerWindow(){ remove_window(*this); - m_logger -= *this; + m_logger.remove_listener(*this); GlobalSettings::instance().LOG_WINDOW_SIZE->WIDTH.remove_listener(*this); GlobalSettings::instance().LOG_WINDOW_SIZE->HEIGHT.remove_listener(*this); GlobalSettings::instance().LOG_WINDOW_SIZE->X_POS.remove_listener(*this); diff --git a/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.h b/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.h index bd75a8bfbf..d5ac9077a7 100644 --- a/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.h +++ b/SerialPrograms/Source/CommonFramework/Logging/FileWindowLogger.h @@ -18,61 +18,25 @@ namespace PokemonAutomation{ -class FileWindowLoggerWindow; - - -// A logger that writes to a file (via FileLogger) and can also display -// log messages in Qt GUI windows (FileWindowLoggerWindow). -// -// This class acts as a thin Qt wrapper around the Qt-free FileLogger, -// adding the ability to manage multiple Qt windows that display log output. -class FileWindowLogger : public Logger, private FileLogger::Listener{ -public: - ~FileWindowLogger(); - - // Construct a FileWindowLogger that writes to the given file path. - // The max_queue_size parameter controls how many log messages can be - // queued before the log() call blocks. - FileWindowLogger(const std::string& path, size_t max_queue_size); - - // Add/remove Qt windows that will display log messages. - void operator+=(FileWindowLoggerWindow& widget); - void operator-=(FileWindowLoggerWindow& widget); - - // Logger interface implementation - forwards to FileLogger. - virtual void log(const std::string& msg, Color color = Color()) override; - virtual void log(std::string&& msg, Color color = Color()) override; - virtual std::vector get_last() const override; - -private: - // FileLogger::Listener implementation - called when a message is logged. - // Formats the message for Qt display and sends to all registered windows. - virtual void on_log(const std::string& msg, Color color) override; - - // Convert a log message to HTML for display in QTextEdit. - static QString to_window_str(const std::string& msg, Color color); - -private: - FileLogger m_file_logger; - - std::mutex m_window_lock; - std::set m_windows; -}; - // A Qt window that displays log output from a FileWindowLogger. // Uses Qt signals/slots for thread-safe updates from the logger's background thread. -class FileWindowLoggerWindow : public QMainWindow, public ConfigOption::Listener{ +class FileWindowLoggerWindow : public QMainWindow, public ConfigOption::Listener, public FileLogger::Listener{ Q_OBJECT public: - FileWindowLoggerWindow(FileWindowLogger& logger, QWidget* parent = nullptr); + FileWindowLoggerWindow(QWidget* parent = nullptr); virtual ~FileWindowLoggerWindow(); // Called by FileWindowLogger to display a log message. - // Thread-safe: emits a signal that is handled on the UI thread. + void log(QString msg); + // Callback function registered to the global logger. + // The global logger's background thread call it to display a log to the window. + // Thread-safe: emits a signal that is handled on the UI thread. + void on_log(const std::string& msg, Color color) override; + virtual void resizeEvent(QResizeEvent* event) override; virtual void moveEvent(QMoveEvent* event) override; @@ -82,7 +46,9 @@ class FileWindowLoggerWindow : public QMainWindow, public ConfigOption::Listener private: virtual void on_config_value_changed(void* object) override; - FileWindowLogger& m_logger; + static QString to_window_str(const std::string& msg, Color color); + + FileLogger& m_logger; QMenuBar* m_menubar; QTextEdit* m_text; bool m_pending_resize = false; diff --git a/SerialPrograms/Source/CommonFramework/Logging/Logger.cpp b/SerialPrograms/Source/CommonFramework/Logging/Logger.cpp index bd20640b79..3284421ad1 100644 --- a/SerialPrograms/Source/CommonFramework/Logging/Logger.cpp +++ b/SerialPrograms/Source/CommonFramework/Logging/Logger.cpp @@ -6,6 +6,7 @@ #include #include "Common/Cpp/Logging/TaggedLogger.h" +#include "Common/Cpp/Logging/GlobalLogger.h" #include "Logger.h" namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/CommonFramework/Logging/Logger.h b/SerialPrograms/Source/CommonFramework/Logging/Logger.h index 13fe92ecab..5b6a57adfc 100644 --- a/SerialPrograms/Source/CommonFramework/Logging/Logger.h +++ b/SerialPrograms/Source/CommonFramework/Logging/Logger.h @@ -10,16 +10,11 @@ #define PokemonAutomation_Logging_Logger_H #include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/Logging/GlobalLogger.h" namespace PokemonAutomation{ -// The base logger for the application. Use this to build other loggers. -// Its implementation is defined in FileWindowLogger.cpp, writing each input -// log into a log file named "/.log". -// It prints each input log string as is with no tag or timestamp. -Logger& global_logger_raw(); - // This logger wraps around `global_logger_raw()` to print each log with a // timestamp and a default tag "Global". Use this logger directly in the // application codebase. diff --git a/SerialPrograms/Source/CommonFramework/Main.cpp b/SerialPrograms/Source/CommonFramework/Main.cpp index 3b48c7fd69..44c9a54d29 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -51,6 +51,22 @@ void set_working_directory(){ } } +namespace PokemonAutomation{ + +// This function is required by Common/Cpp/Logging/GlobalLogger.h:global_logger_raw() to initialize +// the global file logger. +// This function is called the first time `global_logger_raw()` is called to initialize the static +// local global file logger object. +// Note: in order to make sure `USER_FILE_PATH()` and `QCoreApplication::applicationName()` work +// correctly you need to define `QApplication` before `make_global_config()` is called. +FileLoggerConfig make_global_config(){ + return FileLoggerConfig{ + .file_path = USER_FILE_PATH() + QCoreApplication::applicationName().toStdString() + ".log", + }; +} + +} + int run_program(int argc, char *argv[]){ QApplication application(argc, argv); diff --git a/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp b/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp index 75a530645b..449b077ece 100644 --- a/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp +++ b/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp @@ -209,7 +209,7 @@ MainWindow::MainWindow(QWidget* parent) ); } { - m_output_window.reset(new FileWindowLoggerWindow((FileWindowLogger&)global_logger_raw())); + m_output_window.reset(new FileWindowLoggerWindow); QPushButton* output = new QPushButton("Output Window", support_box); buttons->addWidget(output); connect( diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index bdb91e44be..266bc69a33 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -98,6 +98,8 @@ file(GLOB LIBRARY_SOURCES ../Common/Cpp/Logging/AbstractLogger.h ../Common/Cpp/Logging/FileLogger.cpp ../Common/Cpp/Logging/FileLogger.h + ../Common/Cpp/Logging/GlobalLogger.cpp + ../Common/Cpp/Logging/GlobalLogger.h ../Common/Cpp/Logging/LastLogTracker.cpp ../Common/Cpp/Logging/LastLogTracker.h ../Common/Cpp/Logging/OutputRedirector.cpp