Branch data Line data Source code
1 : : #ifndef LOGGER_H
2 : : #define LOGGER_H
3 : :
4 : : #include <string>
5 : : #include <fstream>
6 : : #include <mutex>
7 : : #include <memory>
8 : : #include <iostream>
9 : : #include <sstream>
10 : : #include <iomanip>
11 : : #include <chrono>
12 : : #include <ctime>
13 : : #include <cstdarg>
14 : : #include <algorithm> // For std::max, std::min
15 : : #include <filesystem>
16 : : #include "StringUtils.h" // Include StringUtils.h
17 : :
18 : : // Simple synchronous logger that flushes every line for crash debugging
19 : : class Logger {
20 : : public:
21 : 8696 : static Logger& Get() {
22 [ + + + - : 8696 : static Logger instance;
+ - - - ]
23 : 8696 : return instance;
24 : : }
25 : :
26 : : /**
27 : : * Initialize the logger.
28 : : * @param base_filename The base name of the log file (e.g. "debug.log")
29 : : * @param log_path Optional directory to store the log file.
30 : : * @param use_timestamp If true (default), appends a unique timestamp to the filename to avoid overwriting.
31 : : */
32 : 22 : void Init(const std::string& base_filename, const std::string& log_path = "", bool use_timestamp = true) {
33 [ + - ]: 22 : std::lock_guard<std::mutex> lock(m_mutex);
34 : :
35 [ + - ]: 22 : std::string final_filename = base_filename;
36 : 22 : std::string timestamp_str;
37 : :
38 [ + + ]: 22 : if (use_timestamp) {
39 : : // Generate timestamped filename
40 : 15 : auto now = std::chrono::system_clock::now();
41 : 15 : auto in_time_t = std::chrono::system_clock::to_time_t(now);
42 : : std::tm time_info;
43 : : #ifdef _WIN32
44 : : localtime_s(&time_info, &in_time_t);
45 : : #else
46 : 15 : localtime_r(&in_time_t, &time_info);
47 : : #endif
48 : :
49 [ + - ]: 15 : std::stringstream ss;
50 [ + - ]: 15 : ss << std::put_time(&time_info, "%Y-%m-%d_%H-%M-%S");
51 [ + - ]: 15 : timestamp_str = ss.str();
52 : :
53 : : // Extract extension if present
54 [ + - ]: 15 : std::string name_part = base_filename;
55 [ + - ]: 15 : std::string ext = ".log";
56 : 15 : size_t dot_pos = base_filename.find_last_of('.');
57 [ + + ]: 15 : if (dot_pos != std::string::npos) {
58 [ + - ]: 14 : name_part = base_filename.substr(0, dot_pos);
59 [ + - ]: 14 : ext = base_filename.substr(dot_pos);
60 : : }
61 [ + - + - : 15 : final_filename = name_part + "_" + timestamp_str + ext;
+ - ]
62 : 15 : }
63 : :
64 [ + - ]: 22 : std::filesystem::path path = log_path;
65 : 22 : std::string new_filename;
66 [ + + ]: 22 : if (!path.empty()) {
67 : : try {
68 [ + - ]: 13 : std::filesystem::create_directories(path);
69 : 0 : } catch (...) {
70 : : // Ignore, let file open fail if necessary
71 [ - - ]: 0 : }
72 [ + - + - : 13 : new_filename = (path / final_filename).string();
+ - ]
73 : : } else {
74 [ + - ]: 9 : new_filename = final_filename;
75 : : }
76 : :
77 : : // If we are re-initializing to the SAME file, don't close and reopen (to avoid truncation)
78 : : // Or if we must reopen, use append mode if use_timestamp is true to avoid losing startup lines
79 : : // that happened in the same second.
80 [ + + + + : 22 : if (m_file.is_open() && m_filename == new_filename) {
+ + ]
81 [ + - + - ]: 6 : _LogNoLock("Logger re-initialized to same file. Continuing...", true);
82 : 6 : return;
83 : : }
84 : :
85 [ + + ]: 16 : if (m_file.is_open()) {
86 [ + - ]: 12 : m_file.close();
87 : : }
88 : :
89 [ + - ]: 16 : m_filename = new_filename;
90 : :
91 : : // Open mode: if timestamped, use append to be safe against multi-init in same second.
92 : : // If not timestamped (tests), use trunc to ensure clean file.
93 : 16 : std::ios_base::openmode mode = std::ios::out;
94 [ + + ]: 16 : if (use_timestamp) {
95 : 9 : mode |= std::ios::app;
96 : : } else {
97 : 7 : mode |= std::ios::trunc;
98 : : }
99 : :
100 [ + - ]: 16 : m_file.open(m_filename, mode);
101 [ + + ]: 16 : if (m_file.is_open()) {
102 : 15 : m_initialized = true;
103 [ + - + - : 30 : _LogNoLock("Logger Initialized. Version: " + std::string(LMUFFB_VERSION), true);
+ - ]
104 : : }
105 [ + + + + : 46 : }
+ + + + +
+ ]
106 : :
107 : 4 : void Close() {
108 [ + - ]: 4 : std::lock_guard<std::mutex> lock(m_mutex);
109 [ + + ]: 4 : if (m_file.is_open()) {
110 [ + - ]: 3 : m_file << "Logger Closed Explicitly.\n";
111 [ + - ]: 3 : m_file.close();
112 : : }
113 : 4 : m_initialized = false;
114 : 4 : }
115 : :
116 : 6735 : void Log(const char* fmt, ...) {
117 : : char buffer[2048];
118 : : va_list args;
119 : 6735 : va_start(args, fmt);
120 : 6735 : StringUtils::vSafeFormat(buffer, sizeof(buffer), fmt, args);
121 : 6735 : va_end(args);
122 : :
123 [ + - ]: 6735 : std::string message(buffer);
124 : :
125 [ + - ]: 6735 : std::lock_guard<std::mutex> lock(m_mutex);
126 [ + - ]: 6735 : _LogNoLock(message, true);
127 : 6735 : }
128 : :
129 : : // Log to file only, not to console
130 : 1916 : void LogFile(const char* fmt, ...) {
131 : : char buffer[2048];
132 : : va_list args;
133 : 1916 : va_start(args, fmt);
134 : 1916 : StringUtils::vSafeFormat(buffer, sizeof(buffer), fmt, args);
135 : 1916 : va_end(args);
136 : :
137 [ + - ]: 1916 : std::string message(buffer);
138 : :
139 [ + - ]: 1916 : std::lock_guard<std::mutex> lock(m_mutex);
140 [ + - ]: 1916 : _LogNoLock(message, false);
141 : 1916 : }
142 : :
143 : : // Helper for std::string
144 : 2 : void LogStr(const std::string& msg) {
145 : 2 : Log("%s", msg.c_str());
146 : 2 : }
147 : :
148 : : // Helper for std::string (file only)
149 : : void LogFileStr(const std::string& msg) {
150 : : LogFile("%s", msg.c_str());
151 : : }
152 : :
153 : : // Helper for error logging with GetLastError()
154 : 2 : void LogWin32Error(const char* context, unsigned long errorCode) {
155 : 2 : Log("Error in %s: Code %lu", context, errorCode);
156 : 2 : }
157 : :
158 : 5 : const std::string& GetFilename() const { return m_filename; }
159 : :
160 : : // For testing
161 : 14 : void SetTestStream(std::ostream* os) {
162 [ + - ]: 14 : std::lock_guard<std::mutex> lock(m_mutex);
163 : 14 : m_testStream = os;
164 : 14 : }
165 : :
166 : : private:
167 [ + - ]: 1 : Logger() {}
168 : 1 : ~Logger() noexcept {
169 : : try {
170 [ - + ]: 1 : if (m_file.is_open()) {
171 [ # # ]: 0 : m_file << "Logger Shutdown.\n";
172 [ # # ]: 0 : m_file.close();
173 : : }
174 : 0 : } catch (...) {
175 : : // Destructor must not throw
176 : 0 : }
177 : 1 : }
178 : :
179 : 8672 : void _LogNoLock(const std::string& message, bool toConsole) {
180 [ + + ]: 8672 : if (m_testStream) {
181 : 60 : *m_testStream << message << "\n";
182 : : }
183 : :
184 [ + + ]: 8672 : if (m_file.is_open()) {
185 : : // Timestamp
186 : 8344 : auto now = std::chrono::system_clock::now();
187 : 8344 : auto in_time_t = std::chrono::system_clock::to_time_t(now);
188 : : std::tm time_info;
189 : : #ifdef _WIN32
190 : : localtime_s(&time_info, &in_time_t);
191 : : #else
192 : 8344 : localtime_r(&in_time_t, &time_info);
193 : : #endif
194 : :
195 [ + - + - : 8344 : m_file << "[" << std::put_time(&time_info, "%H:%M:%S") << "] " << message << "\n";
+ - + - +
- ]
196 [ + - ]: 8344 : m_file.flush(); // Critical for crash debugging
197 : : }
198 : :
199 [ + + ]: 8672 : if (toConsole) {
200 : : // Also print to console for consistency
201 : : // Re-calculate timestamp if not already done (file logging also calculates it)
202 : 6756 : auto now = std::chrono::system_clock::now();
203 : 6756 : auto in_time_t = std::chrono::system_clock::to_time_t(now);
204 : : std::tm time_info;
205 : : #ifdef _WIN32
206 : : localtime_s(&time_info, &in_time_t);
207 : : #else
208 : 6756 : localtime_r(&in_time_t, &time_info);
209 : : #endif
210 : :
211 [ + - + - : 6756 : std::cout << "[" << std::put_time(&time_info, "%H:%M:%S") << "] [Log] " << message << std::endl;
+ - + - +
- ]
212 : : }
213 : 8672 : }
214 : :
215 : : std::string m_filename;
216 : : std::ofstream m_file;
217 : : std::mutex m_mutex;
218 : : bool m_initialized = false;
219 : : std::ostream* m_testStream = nullptr;
220 : : };
221 : :
222 : : #endif // LOGGER_H
|