GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 86.8% 210 / 0 / 242
Functions: 100.0% 20 / 0 / 20
Branches: 46.6% 235 / 0 / 504

src/logger.cpp
Line Branch Exec Source
1 #include "DetourModKit/logger.hpp"
2 #include "DetourModKit/async_logger.hpp"
3 #include "DetourModKit/filesystem.hpp"
4 #include "DetourModKit/format.hpp"
5 #include "platform.hpp"
6
7 #include <algorithm>
8 #include <cstdio>
9 #include <ctime>
10 #include <filesystem>
11 #include <chrono>
12 #include <iomanip>
13 #include <iostream>
14 #include <new>
15 #include <stdexcept>
16 #include <array>
17 #include <type_traits>
18
19 namespace DetourModKit
20 {
21
22 namespace
23 {
24 234 std::atomic<std::shared_ptr<const Logger::StaticConfig>> &static_config_atom()
25 {
26 static std::atomic<std::shared_ptr<const Logger::StaticConfig>> instance{
27 1 std::make_shared<const Logger::StaticConfig>(
28
4/8
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 11 taken 233 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 11 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 13 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
234 DEFAULT_LOG_PREFIX, DEFAULT_LOG_FILE_NAME, DEFAULT_TIMESTAMP_FORMAT)};
29 234 return instance;
30 }
31 } // anonymous namespace
32
33 1 std::shared_ptr<const Logger::StaticConfig> Logger::get_static_config()
34 {
35 1 return static_config_atom().load(std::memory_order_acquire);
36 }
37
38 233 void Logger::set_static_config(std::shared_ptr<const StaticConfig> config)
39 {
40 466 static_config_atom().store(std::move(config), std::memory_order_release);
41 233 }
42
43 1172 LogLevel Logger::string_to_log_level(std::string_view level_str)
44 {
45
1/2
✓ Branch 4 → 5 taken 1172 times.
✗ Branch 4 → 35 not taken.
1172 std::string upper_level_str(level_str);
46 1172 std::transform(upper_level_str.begin(), upper_level_str.end(), upper_level_str.begin(),
47 15038 [](unsigned char c)
48 15038 { return static_cast<char>(std::toupper(c)); });
49
50
3/4
✓ Branch 10 → 11 taken 1172 times.
✗ Branch 10 → 38 not taken.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1169 times.
1172 if (upper_level_str == "TRACE")
51 3 return LogLevel::Trace;
52
3/4
✓ Branch 13 → 14 taken 1169 times.
✗ Branch 13 → 38 not taken.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 1165 times.
1169 if (upper_level_str == "DEBUG")
53 4 return LogLevel::Debug;
54
3/4
✓ Branch 16 → 17 taken 1165 times.
✗ Branch 16 → 38 not taken.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 1161 times.
1165 if (upper_level_str == "INFO")
55 4 return LogLevel::Info;
56
3/4
✓ Branch 19 → 20 taken 1161 times.
✗ Branch 19 → 38 not taken.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 22 taken 1158 times.
1161 if (upper_level_str == "WARNING")
57 3 return LogLevel::Warning;
58
3/4
✓ Branch 22 → 23 taken 1158 times.
✗ Branch 22 → 38 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 25 taken 1155 times.
1158 if (upper_level_str == "ERROR")
59 3 return LogLevel::Error;
60
61 std::cerr << "[" << DEFAULT_LOG_PREFIX << " Logger WARNING] Unrecognized log level string '" << level_str
62
6/12
✓ Branch 25 → 26 taken 1155 times.
✗ Branch 25 → 38 not taken.
✓ Branch 26 → 27 taken 1155 times.
✗ Branch 26 → 38 not taken.
✓ Branch 27 → 28 taken 1155 times.
✗ Branch 27 → 38 not taken.
✓ Branch 28 → 29 taken 1155 times.
✗ Branch 28 → 38 not taken.
✓ Branch 29 → 30 taken 1155 times.
✗ Branch 29 → 38 not taken.
✓ Branch 30 → 31 taken 1155 times.
✗ Branch 30 → 38 not taken.
1155 << "'. Defaulting to INFO." << '\n';
63 1155 return LogLevel::Info;
64 1172 }
65
66 233 void Logger::configure(std::string_view prefix, std::string_view file_name, std::string_view timestamp_fmt)
67 {
68
5/10
✓ Branch 4 → 5 taken 233 times.
✗ Branch 4 → 40 not taken.
✓ Branch 7 → 8 taken 233 times.
✗ Branch 7 → 34 not taken.
✓ Branch 10 → 11 taken 233 times.
✗ Branch 10 → 28 not taken.
✓ Branch 11 → 12 taken 233 times.
✗ Branch 11 → 26 not taken.
✓ Branch 12 → 13 taken 233 times.
✗ Branch 12 → 24 not taken.
1165 set_static_config(std::make_shared<const StaticConfig>(std::string(prefix), std::string(file_name), std::string(timestamp_fmt)));
69
70 233 Logger &instance = get_instance();
71
72 // configure() is the authoritative reset path — allow reconfiguration
73 // even after shutdown to support reuse (e.g., test fixtures).
74 233 instance.shutdown_called_.store(false, std::memory_order_release);
75
76 // Comparison is done inside reconfigure() under the lock to prevent
77 // reading string members while another thread is modifying them.
78 233 instance.reconfigure(prefix, file_name, timestamp_fmt);
79 233 }
80
81 270 void Logger::reconfigure(std::string_view prefix, std::string_view file_name, std::string_view timestamp_fmt)
82 {
83
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 270 times.
270 if (shutdown_called_.load(std::memory_order_acquire))
84 {
85 2 return;
86 }
87
88 // Acquire both async_mutex_ and log_mutex_ to prevent concurrent log() calls
89 // from reading partially-updated string members during reconfiguration
90
1/2
✓ Branch 6 → 7 taken 270 times.
✗ Branch 6 → 113 not taken.
270 std::scoped_lock lock(async_mutex_, *log_mutex_ptr_);
91
92 // Skip reconfiguration only when all parameters match AND the stream is usable.
93 // After shutdown or a prior open failure the stream may be closed, so we must
94 // fall through to reopen even if the strings are identical.
95
2/2
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 20 taken 251 times.
524 if (log_file_stream_ptr_->is_open() &&
96
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 20 taken 1 time.
257 log_prefix_ == prefix &&
97
5/6
✓ Branch 9 → 10 taken 254 times.
✓ Branch 9 → 20 taken 16 times.
✓ Branch 18 → 19 taken 2 times.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 268 times.
527 log_file_name_ == file_name &&
98 2 timestamp_format_ == timestamp_fmt)
99 {
100 2 return;
101 }
102
103
6/8
✓ Branch 25 → 26 taken 252 times.
✓ Branch 25 → 30 taken 16 times.
✓ Branch 27 → 28 taken 252 times.
✗ Branch 27 → 111 not taken.
✓ Branch 28 → 29 taken 252 times.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 252 times.
✓ Branch 31 → 51 taken 16 times.
268 if (log_file_stream_ptr_->is_open() && log_file_stream_ptr_->good())
104 {
105 504 *log_file_stream_ptr_ << "[" << get_timestamp() << "] "
106
6/12
✓ Branch 33 → 34 taken 252 times.
✗ Branch 33 → 111 not taken.
✓ Branch 34 → 35 taken 252 times.
✗ Branch 34 → 99 not taken.
✓ Branch 35 → 36 taken 252 times.
✗ Branch 35 → 97 not taken.
✓ Branch 36 → 37 taken 252 times.
✗ Branch 36 → 97 not taken.
✓ Branch 37 → 38 taken 252 times.
✗ Branch 37 → 97 not taken.
✓ Branch 40 → 41 taken 252 times.
✗ Branch 40 → 97 not taken.
252 << "[" << std::setw(7) << std::left << "INFO" << "] :: "
107
5/10
✓ Branch 41 → 42 taken 252 times.
✗ Branch 41 → 97 not taken.
✓ Branch 42 → 43 taken 252 times.
✗ Branch 42 → 97 not taken.
✓ Branch 43 → 44 taken 252 times.
✗ Branch 43 → 97 not taken.
✓ Branch 44 → 45 taken 252 times.
✗ Branch 44 → 97 not taken.
✓ Branch 45 → 46 taken 252 times.
✗ Branch 45 → 97 not taken.
252 << "Logger reconfiguring. New file: " << file_name << '\n';
108
1/2
✓ Branch 48 → 49 taken 252 times.
✗ Branch 48 → 111 not taken.
252 log_file_stream_ptr_->flush();
109
1/2
✓ Branch 50 → 51 taken 252 times.
✗ Branch 50 → 111 not taken.
252 log_file_stream_ptr_->close();
110 }
111
112
1/2
✓ Branch 51 → 52 taken 268 times.
✗ Branch 51 → 111 not taken.
268 log_prefix_ = prefix;
113
1/2
✓ Branch 52 → 53 taken 268 times.
✗ Branch 52 → 111 not taken.
268 log_file_name_ = file_name;
114
1/2
✓ Branch 53 → 54 taken 268 times.
✗ Branch 53 → 111 not taken.
268 timestamp_format_ = timestamp_fmt;
115
116
1/2
✓ Branch 54 → 55 taken 268 times.
✗ Branch 54 → 111 not taken.
268 std::wstring log_file_full_path = generate_log_file_path();
117
1/2
✓ Branch 57 → 58 taken 268 times.
✗ Branch 57 → 109 not taken.
268 log_file_stream_ptr_->open(log_file_full_path, std::ios::out | std::ios::trunc);
118
119
2/2
✓ Branch 60 → 61 taken 4 times.
✓ Branch 60 → 73 taken 264 times.
268 if (!log_file_stream_ptr_->is_open())
120 {
121 4 std::cerr << "[" << log_prefix_ << " Logger CRITICAL ERROR] "
122 << "Failed to open log file: "
123
1/2
✓ Branch 66 → 67 taken 4 times.
✗ Branch 66 → 102 not taken.
8 << std::filesystem::path(log_file_full_path).string()
124
8/16
✓ Branch 61 → 62 taken 4 times.
✗ Branch 61 → 109 not taken.
✓ Branch 62 → 63 taken 4 times.
✗ Branch 62 → 109 not taken.
✓ Branch 63 → 64 taken 4 times.
✗ Branch 63 → 109 not taken.
✓ Branch 64 → 65 taken 4 times.
✗ Branch 64 → 109 not taken.
✓ Branch 65 → 66 taken 4 times.
✗ Branch 65 → 104 not taken.
✓ Branch 67 → 68 taken 4 times.
✗ Branch 67 → 100 not taken.
✓ Branch 68 → 69 taken 4 times.
✗ Branch 68 → 100 not taken.
✓ Branch 69 → 70 taken 4 times.
✗ Branch 69 → 100 not taken.
8 << ". Subsequent logs to file will fail." << '\n';
125 }
126 else
127 {
128 528 *log_file_stream_ptr_ << "[" << get_timestamp() << "] "
129
6/12
✓ Branch 74 → 75 taken 264 times.
✗ Branch 74 → 109 not taken.
✓ Branch 75 → 76 taken 264 times.
✗ Branch 75 → 108 not taken.
✓ Branch 76 → 77 taken 264 times.
✗ Branch 76 → 106 not taken.
✓ Branch 77 → 78 taken 264 times.
✗ Branch 77 → 106 not taken.
✓ Branch 78 → 79 taken 264 times.
✗ Branch 78 → 106 not taken.
✓ Branch 81 → 82 taken 264 times.
✗ Branch 81 → 106 not taken.
264 << "[" << std::setw(7) << std::left << "INFO" << "] :: "
130
5/10
✓ Branch 82 → 83 taken 264 times.
✗ Branch 82 → 106 not taken.
✓ Branch 83 → 84 taken 264 times.
✗ Branch 83 → 106 not taken.
✓ Branch 84 → 85 taken 264 times.
✗ Branch 84 → 106 not taken.
✓ Branch 85 → 86 taken 264 times.
✗ Branch 85 → 106 not taken.
✓ Branch 86 → 87 taken 264 times.
✗ Branch 86 → 106 not taken.
264 << "Logger reconfigured. Now logging to: " << file_name << '\n';
131 }
132
2/2
✓ Branch 92 → 93 taken 268 times.
✓ Branch 92 → 95 taken 2 times.
270 }
133
134 1 Logger::Logger()
135
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 78 not taken.
1 : log_file_stream_ptr_(std::make_shared<WinFileStream>()),
136
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 76 not taken.
2 log_mutex_ptr_(std::make_shared<std::mutex>())
137 {
138 {
139
1/2
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 57 not taken.
1 auto config = get_static_config();
140
1/2
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 55 not taken.
1 log_prefix_ = config->log_prefix;
141
1/2
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 55 not taken.
1 log_file_name_ = config->log_file_name;
142
1/2
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 55 not taken.
1 timestamp_format_ = config->timestamp_format;
143 1 }
144
145
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 69 not taken.
1 const std::wstring log_file_full_path = generate_log_file_path();
146
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 67 not taken.
1 log_file_stream_ptr_->open(log_file_full_path, std::ios::out | std::ios::trunc);
147
148
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 39 taken 1 time.
1 if (!log_file_stream_ptr_->is_open())
149 {
150 std::cerr << "[" << log_prefix_ << " Logger CRITICAL ERROR] "
151 << "Failed to open log file: "
152 << std::filesystem::path(log_file_full_path).string()
153 << ". Subsequent logs to file will fail." << '\n';
154 }
155 else
156 {
157 2 *log_file_stream_ptr_ << "[" << get_timestamp() << "] ["
158
5/10
✓ Branch 40 → 41 taken 1 time.
✗ Branch 40 → 67 not taken.
✓ Branch 41 → 42 taken 1 time.
✗ Branch 41 → 66 not taken.
✓ Branch 42 → 43 taken 1 time.
✗ Branch 42 → 64 not taken.
✓ Branch 43 → 44 taken 1 time.
✗ Branch 43 → 64 not taken.
✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 64 not taken.
1 << std::setw(7) << std::left << "INFO"
159
4/8
✓ Branch 47 → 48 taken 1 time.
✗ Branch 47 → 64 not taken.
✓ Branch 48 → 49 taken 1 time.
✗ Branch 48 → 64 not taken.
✓ Branch 49 → 50 taken 1 time.
✗ Branch 49 → 64 not taken.
✓ Branch 50 → 51 taken 1 time.
✗ Branch 50 → 64 not taken.
1 << "] :: Logger initialized. Logging to: " << log_file_name_ << '\n';
160 }
161 1 }
162
163 1 Logger::~Logger() noexcept
164 {
165 1 bool expected = false;
166
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
1 if (!shutdown_called_.compare_exchange_strong(expected, true,
167 std::memory_order_acq_rel))
168 {
169 1 return;
170 }
171 shutdown_internal();
172
7/14
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 1 time.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1 time.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 1 time.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 1 time.
7 }
173
174 38 void Logger::shutdown()
175 {
176 38 bool expected = false;
177
2/2
✓ Branch 3 → 4 taken 25 times.
✓ Branch 3 → 5 taken 13 times.
38 if (!shutdown_called_.compare_exchange_strong(expected, true,
178 std::memory_order_acq_rel))
179 {
180 25 return;
181 }
182
1/2
✓ Branch 5 → 6 taken 13 times.
✗ Branch 5 → 8 not taken.
13 shutdown_internal();
183 }
184
185 13 void Logger::shutdown_internal()
186 {
187 13 std::shared_ptr<AsyncLogger> local_logger;
188
189 {
190
1/2
✓ Branch 2 → 3 taken 13 times.
✗ Branch 2 → 52 not taken.
13 std::lock_guard<std::mutex> lock(async_mutex_);
191
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 15 taken 7 times.
13 if (async_mode_enabled_.load(std::memory_order_acquire))
192 {
193 6 local_logger = async_logger_.exchange(nullptr, std::memory_order_acq_rel);
194 6 async_mode_enabled_.store(false, std::memory_order_release);
195
1/2
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 15 not taken.
6 if (local_logger)
196 {
197 6 local_logger->shutdown();
198 }
199 }
200 13 }
201
202 // If the writer thread was detached under loader lock, it may still
203 // be accessing AsyncLogger members (queue_, flush_mutex_, etc.).
204 // Transfer ownership to permanent heap storage so the object
205 // outlives the detached thread; pin the module so the code pages
206 // it executes from also remain mapped.
207 //
208 // The transfer is unconditional when loader lock is held: concurrent
209 // log() callers may still own temporary shared_ptrs obtained from
210 // async_logger_ before exchange(), so use_count() is not a reliable
211 // proxy for "no other owners". Dropping local_logger's ref while
212 // a temporary outlives us would let the last temporary's destructor
213 // race the detached writer thread.
214 //
215 // The leak is per-call and append-only: each invocation allocates
216 // its own heap cell, so a process that re-attaches after shutdown
217 // (e.g. hot-reload) and hits loader lock again cannot drop a prior
218 // handle whose writer thread may still be running. Mirrors the
219 // HookManager loader-lock discipline: new (std::nothrow) keeps the
220 // noexcept destructor honest by returning nullptr on OOM rather
221 // than turning a std::vector::emplace_back bad_alloc into
222 // std::terminate inside this noexcept context.
223
4/6
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 21 taken 7 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 6 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 35 taken 13 times.
13 if (local_logger && detail::is_loader_lock_held())
224 {
225 static_assert(std::is_nothrow_move_constructible_v<std::shared_ptr<AsyncLogger>>,
226 "Leak cell must be nothrow-move-constructible to keep ~Logger noexcept honest.");
227
228 detail::pin_current_module();
229
230 auto *leaked = new (std::nothrow)
231 std::shared_ptr<AsyncLogger>(std::move(local_logger));
232 static_cast<void>(leaked);
233 }
234
235 {
236 // Acquire both mutexes to prevent configure()/reconfigure() from
237 // opening a new stream in the gap after BLOCK 1 releases async_mutex_.
238
1/2
✓ Branch 36 → 37 taken 13 times.
✗ Branch 36 → 55 not taken.
13 std::scoped_lock lock(async_mutex_, *log_mutex_ptr_);
239
3/6
✓ Branch 38 → 39 taken 13 times.
✗ Branch 38 → 43 not taken.
✓ Branch 41 → 42 taken 13 times.
✗ Branch 41 → 43 not taken.
✓ Branch 44 → 45 taken 13 times.
✗ Branch 44 → 49 not taken.
13 if (log_file_stream_ptr_ && log_file_stream_ptr_->is_open())
240 {
241
1/2
✓ Branch 46 → 47 taken 13 times.
✗ Branch 46 → 53 not taken.
13 log_file_stream_ptr_->flush();
242
1/2
✓ Branch 48 → 49 taken 13 times.
✗ Branch 48 → 53 not taken.
13 log_file_stream_ptr_->close();
243 }
244 13 }
245 13 }
246
247 96 void Logger::set_log_level(LogLevel level)
248 {
249 96 auto level_int = static_cast<std::underlying_type_t<LogLevel>>(level);
250
3/4
✓ Branch 2 → 3 taken 96 times.
✗ Branch 2 → 4 not taken.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 94 times.
96 if (level_int < 0 || level_int > static_cast<std::underlying_type_t<LogLevel>>(LogLevel::Error))
251 {
252
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 16 not taken.
2 log(LogLevel::Warning, "Attempted to set an invalid log level value ({}). Keeping current level.", level_int);
253 21 return;
254 }
255
256 94 auto old_level = current_log_level_.load(std::memory_order_acquire);
257
2/2
✓ Branch 7 → 8 taken 19 times.
✓ Branch 7 → 9 taken 75 times.
94 if (old_level == level)
258 {
259 19 return;
260 }
261
262 75 current_log_level_.store(level, std::memory_order_release);
263
264
1/2
✓ Branch 12 → 13 taken 75 times.
✗ Branch 12 → 17 not taken.
75 log(LogLevel::Info, "Log level changed from {} to {}",
265 150 log_level_to_string(old_level), log_level_to_string(level));
266 }
267
268 3461 void Logger::log(LogLevel level, std::string_view message)
269 {
270
2/2
✓ Branch 3 → 4 taken 3465 times.
✓ Branch 3 → 65 taken 7 times.
3461 if (level >= current_log_level_.load(std::memory_order_acquire))
271 {
272 // Fast path: lock-free check via atomic shared_ptr
273
2/2
✓ Branch 5 → 6 taken 936 times.
✓ Branch 5 → 18 taken 2529 times.
3465 if (async_mode_enabled_.load(std::memory_order_acquire))
274 {
275 936 auto local_logger = async_logger_.load(std::memory_order_acquire);
276
1/2
✓ Branch 8 → 9 taken 938 times.
✗ Branch 8 → 12 not taken.
938 if (local_logger)
277 {
278 938 static_cast<void>(local_logger->enqueue(level, message));
279 938 return;
280 }
281
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 938 times.
938 }
282
283 2529 const auto level_str = log_level_to_string(level);
284
1/2
✓ Branch 20 → 21 taken 2530 times.
✗ Branch 20 → 74 not taken.
2527 std::lock_guard<std::mutex> lock(*log_mutex_ptr_);
285
286
6/8
✓ Branch 23 → 24 taken 2380 times.
✓ Branch 23 → 28 taken 150 times.
✓ Branch 25 → 26 taken 2380 times.
✗ Branch 25 → 72 not taken.
✓ Branch 26 → 27 taken 2380 times.
✗ Branch 26 → 28 not taken.
✓ Branch 29 → 30 taken 2380 times.
✓ Branch 29 → 47 taken 150 times.
2530 if (log_file_stream_ptr_->is_open() && log_file_stream_ptr_->good())
287 {
288 4760 *log_file_stream_ptr_ << "[" << get_timestamp() << "] "
289
6/12
✓ Branch 31 → 32 taken 2380 times.
✗ Branch 31 → 72 not taken.
✓ Branch 32 → 33 taken 2380 times.
✗ Branch 32 → 68 not taken.
✓ Branch 33 → 34 taken 2380 times.
✗ Branch 33 → 66 not taken.
✓ Branch 34 → 35 taken 2380 times.
✗ Branch 34 → 66 not taken.
✓ Branch 35 → 36 taken 2380 times.
✗ Branch 35 → 66 not taken.
✓ Branch 38 → 39 taken 2380 times.
✗ Branch 38 → 66 not taken.
2380 << "[" << std::setw(7) << std::left << level_str << "] :: "
290
4/8
✓ Branch 39 → 40 taken 2380 times.
✗ Branch 39 → 66 not taken.
✓ Branch 40 → 41 taken 2380 times.
✗ Branch 40 → 66 not taken.
✓ Branch 41 → 42 taken 2380 times.
✗ Branch 41 → 66 not taken.
✓ Branch 42 → 43 taken 2380 times.
✗ Branch 42 → 66 not taken.
2380 << message << '\n';
291
292 // Flush on warnings/errors to ensure critical messages survive crashes
293
2/2
✓ Branch 44 → 45 taken 792 times.
✓ Branch 44 → 63 taken 1588 times.
2380 if (level >= LogLevel::Warning)
294 {
295
1/2
✓ Branch 46 → 63 taken 792 times.
✗ Branch 46 → 72 not taken.
792 log_file_stream_ptr_->flush();
296 }
297 }
298
2/2
✓ Branch 47 → 48 taken 22 times.
✓ Branch 47 → 63 taken 128 times.
150 else if (level >= LogLevel::Error)
299 {
300 22 std::cerr << "[" << log_prefix_ << " LOG_FILE_WRITE_ERROR] [" << get_timestamp() << "] ["
301
7/14
✓ Branch 48 → 49 taken 22 times.
✗ Branch 48 → 72 not taken.
✓ Branch 49 → 50 taken 22 times.
✗ Branch 49 → 72 not taken.
✓ Branch 50 → 51 taken 22 times.
✗ Branch 50 → 72 not taken.
✓ Branch 51 → 52 taken 22 times.
✗ Branch 51 → 71 not taken.
✓ Branch 52 → 53 taken 22 times.
✗ Branch 52 → 69 not taken.
✓ Branch 53 → 54 taken 22 times.
✗ Branch 53 → 69 not taken.
✓ Branch 56 → 57 taken 22 times.
✗ Branch 56 → 69 not taken.
22 << std::setw(7) << std::left << level_str << "] :: "
302
4/8
✓ Branch 57 → 58 taken 22 times.
✗ Branch 57 → 69 not taken.
✓ Branch 58 → 59 taken 22 times.
✗ Branch 58 → 69 not taken.
✓ Branch 59 → 60 taken 22 times.
✗ Branch 59 → 69 not taken.
✓ Branch 60 → 61 taken 22 times.
✗ Branch 60 → 69 not taken.
22 << message << '\n';
303 }
304 2530 }
305 }
306
307 2919 std::string Logger::get_timestamp() const
308 {
309 try
310 {
311 2919 const auto now = std::chrono::system_clock::now();
312 2919 const auto in_time_t = std::chrono::system_clock::to_time_t(now);
313 2919 std::tm timeinfo_struct = {};
314
315 #if defined(_MSC_VER)
316 if (localtime_s(&timeinfo_struct, &in_time_t) != 0)
317 {
318 throw std::runtime_error("localtime_s failed to convert time.");
319 }
320 #elif defined(__MINGW32__) || defined(__MINGW64__)
321 // MinGW: localtime_s has reversed parameter order (ISO C11 Annex K)
322
2/4
✓ Branch 8 → 9 taken 2919 times.
✗ Branch 8 → 43 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 2919 times.
2919 if (localtime_s(&timeinfo_struct, &in_time_t) != 0)
323 {
324 throw std::runtime_error("localtime_s failed to convert time.");
325 }
326 #else
327 if (localtime_r(&in_time_t, &timeinfo_struct) == nullptr)
328 {
329 throw std::runtime_error("localtime_r failed to convert time.");
330 }
331 #endif
332 // Single stack buffer for timestamp + milliseconds, no heap allocation
333 char buf[134];
334 2919 const size_t len = std::strftime(buf, sizeof(buf) - 5, timestamp_format_.c_str(), &timeinfo_struct);
335
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 2919 times.
2919 if (len == 0)
336 {
337 return "TIMESTAMP_FORMAT_ERROR";
338 }
339
340
1/2
✓ Branch 21 → 22 taken 2919 times.
✗ Branch 21 → 37 not taken.
2919 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
341 now.time_since_epoch()) %
342
1/2
✓ Branch 22 → 23 taken 2919 times.
✗ Branch 22 → 37 not taken.
5838 1000;
343
1/2
✓ Branch 24 → 25 taken 2919 times.
✗ Branch 24 → 43 not taken.
2919 const int ms_len = std::snprintf(buf + len, 5, ".%03d", static_cast<int>(ms.count()));
344
1/2
✓ Branch 27 → 28 taken 2919 times.
✗ Branch 27 → 40 not taken.
5838 return std::string(buf, len + static_cast<size_t>(ms_len));
345 }
346 catch (const std::exception &e)
347 {
348 std::cerr << "[" << log_prefix_ << " Logger TIMESTAMP_ERROR] Failed to generate timestamp: " << e.what() << '\n';
349 return "TIMESTAMP_GENERATION_ERROR";
350 }
351 catch (...)
352 {
353 std::cerr << "[" << log_prefix_ << " Logger TIMESTAMP_ERROR] Unknown exception during timestamp generation." << '\n';
354 return "TIMESTAMP_GENERATION_ERROR";
355 }
356 }
357
358 269 std::wstring Logger::generate_log_file_path() const
359 {
360
1/2
✓ Branch 2 → 3 taken 269 times.
✗ Branch 2 → 81 not taken.
269 std::filesystem::path log_file_path_obj(log_file_name_);
361
2/2
✓ Branch 4 → 5 taken 265 times.
✓ Branch 4 → 7 taken 4 times.
269 if (log_file_path_obj.is_absolute())
362 {
363
1/2
✓ Branch 5 → 6 taken 265 times.
✗ Branch 5 → 79 not taken.
265 return log_file_path_obj.wstring();
364 }
365
366 try
367 {
368
1/2
✓ Branch 7 → 8 taken 4 times.
✗ Branch 7 → 51 not taken.
4 std::wstring module_dir = Filesystem::get_runtime_directory();
369
4/8
✓ Branch 9 → 10 taken 4 times.
✗ Branch 9 → 12 not taken.
✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 49 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 4 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 23 taken 4 times.
4 if (module_dir.empty() || module_dir == L".")
370 {
371 std::cerr << "[" << log_prefix_ << " Logger PATH_WARNING] "
372 << "Could not determine module directory. Using relative path: " << log_file_name_ << '\n';
373 return log_file_path_obj.wstring();
374 }
375
376
3/6
✓ Branch 23 → 24 taken 4 times.
✗ Branch 23 → 43 not taken.
✓ Branch 24 → 25 taken 4 times.
✗ Branch 24 → 40 not taken.
✓ Branch 25 → 26 taken 4 times.
✗ Branch 25 → 38 not taken.
4 const std::filesystem::path final_log_path = std::filesystem::path(module_dir) / log_file_name_;
377
2/4
✓ Branch 28 → 29 taken 4 times.
✗ Branch 28 → 46 not taken.
✓ Branch 29 → 30 taken 4 times.
✗ Branch 29 → 44 not taken.
4 return final_log_path.lexically_normal().wstring();
378 4 }
379 catch (const std::exception &e)
380 {
381 std::cerr << "[" << log_prefix_ << " Logger PATH_WARNING] Failed to determine module directory for log file: "
382 << e.what() << ". Using relative path for log file: " << log_file_name_ << '\n';
383 return log_file_path_obj.wstring();
384 }
385 catch (...)
386 {
387 std::cerr << "[" << log_prefix_ << " Logger PATH_WARNING] Unknown exception while determining module directory for log file."
388 << " Using relative path: " << log_file_name_ << '\n';
389 return log_file_path_obj.wstring();
390 }
391 269 }
392
393 27 void Logger::enable_async_mode(const AsyncLoggerConfig &config)
394 {
395 27 bool should_log_error = false;
396 27 bool should_log_success = false;
397 27 std::string error_msg;
398 27 size_t queue_cap = 0;
399 27 size_t batch_sz = 0;
400
401 {
402
1/2
✓ Branch 3 → 4 taken 27 times.
✗ Branch 3 → 59 not taken.
27 std::lock_guard<std::mutex> lock(async_mutex_);
403
404
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 26 times.
27 if (async_mode_enabled_.load(std::memory_order_acquire))
405 {
406 1 return;
407 }
408
409
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 26 times.
26 if (!log_file_stream_ptr_->is_open())
410 {
411 should_log_error = true;
412 error_msg = "Cannot enable async mode: log file is not open.";
413 }
414 else
415 {
416 try
417 {
418 24 async_logger_.store(
419
2/2
✓ Branch 11 → 12 taken 24 times.
✓ Branch 11 → 34 taken 2 times.
50 std::make_shared<AsyncLogger>(config, log_file_stream_ptr_, log_mutex_ptr_),
420 std::memory_order_release);
421 24 async_mode_enabled_.store(true, std::memory_order_release);
422 24 should_log_success = true;
423 24 queue_cap = config.queue_capacity;
424 24 batch_sz = config.batch_size;
425 }
426
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 2 times.
2 catch (const std::exception &e)
427 {
428 2 should_log_error = true;
429
2/4
✓ Branch 41 → 42 taken 2 times.
✗ Branch 41 → 50 not taken.
✓ Branch 42 → 43 taken 2 times.
✗ Branch 42 → 48 not taken.
4 error_msg = std::string("Failed to enable async mode: ") + e.what();
430 2 }
431 }
432
2/2
✓ Branch 18 → 19 taken 26 times.
✓ Branch 18 → 21 taken 1 time.
27 }
433
434
2/2
✓ Branch 20 → 22 taken 2 times.
✓ Branch 20 → 24 taken 24 times.
26 if (should_log_error)
435 {
436
1/2
✓ Branch 22 → 23 taken 2 times.
✗ Branch 22 → 60 not taken.
2 log(LogLevel::Error, "{}", error_msg);
437 }
438
1/2
✓ Branch 24 → 25 taken 24 times.
✗ Branch 24 → 27 not taken.
24 else if (should_log_success)
439 {
440
1/2
✓ Branch 25 → 26 taken 24 times.
✗ Branch 25 → 61 not taken.
24 log(LogLevel::Info, "Async logging mode enabled. Queue capacity: {}, Batch size: {}",
441 queue_cap, batch_sz);
442 }
443
2/2
✓ Branch 29 → 30 taken 26 times.
✓ Branch 29 → 32 taken 1 time.
27 }
444
445 20 void Logger::enable_async_mode()
446 {
447
1/2
✓ Branch 3 → 4 taken 20 times.
✗ Branch 3 → 5 not taken.
20 enable_async_mode(AsyncLoggerConfig{});
448 20 }
449
450 24 void Logger::disable_async_mode()
451 {
452 24 bool should_log = false;
453
454 {
455
1/2
✓ Branch 2 → 3 taken 24 times.
✗ Branch 2 → 25 not taken.
24 std::lock_guard<std::mutex> lock(async_mutex_);
456
457
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 6 taken 18 times.
24 if (!async_mode_enabled_.load(std::memory_order_acquire))
458 {
459 6 return;
460 }
461
462 18 auto local_async = async_logger_.exchange(nullptr, std::memory_order_acq_rel);
463
1/2
✓ Branch 10 → 11 taken 18 times.
✗ Branch 10 → 13 not taken.
18 if (local_async)
464 {
465 18 local_async->shutdown();
466 }
467
468 18 async_mode_enabled_.store(false, std::memory_order_release);
469 18 should_log = true;
470
2/2
✓ Branch 17 → 18 taken 18 times.
✓ Branch 17 → 20 taken 6 times.
24 }
471
472
1/2
✓ Branch 19 → 21 taken 18 times.
✗ Branch 19 → 24 not taken.
18 if (should_log)
473 {
474
1/2
✓ Branch 22 → 23 taken 18 times.
✗ Branch 22 → 26 not taken.
18 log(LogLevel::Info, "Async logging mode disabled. Switched to synchronous mode.");
475 }
476 }
477
478 38 bool Logger::is_async_mode_enabled() const
479 {
480 38 return async_mode_enabled_.load(std::memory_order_acquire);
481 }
482
483 84 void Logger::flush()
484 {
485
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 16 taken 78 times.
84 if (async_mode_enabled_.load(std::memory_order_acquire))
486 {
487 6 auto local_logger = async_logger_.load(std::memory_order_acquire);
488
1/2
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 10 not taken.
6 if (local_logger)
489 {
490 6 local_logger->flush();
491 6 return;
492 }
493
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 6 times.
6 }
494
495
1/2
✓ Branch 17 → 18 taken 78 times.
✗ Branch 17 → 28 not taken.
78 std::lock_guard<std::mutex> lock(*log_mutex_ptr_);
496
2/2
✓ Branch 20 → 21 taken 77 times.
✓ Branch 20 → 23 taken 1 time.
78 if (log_file_stream_ptr_->is_open())
497 {
498
1/2
✓ Branch 22 → 23 taken 77 times.
✗ Branch 22 → 26 not taken.
77 log_file_stream_ptr_->flush();
499 }
500 78 }
501
502 } // namespace DetourModKit
503