GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 78.5% 215 / 0 / 274
Functions: 95.5% 21 / 0 / 22
Branches: 46.6% 245 / 0 / 526

src/logger.cpp
Line Branch Exec Source
1 #include "DetourModKit/logger.hpp"
2 #include "DetourModKit/diagnostics.hpp"
3 #include "DetourModKit/async_logger.hpp"
4 #include "DetourModKit/filesystem.hpp"
5 #include "DetourModKit/format.hpp"
6 #include "platform.hpp"
7
8 #include <algorithm>
9 #include <cstdio>
10 #include <ctime>
11 #include <filesystem>
12 #include <chrono>
13 #include <iomanip>
14 #include <iostream>
15 #include <new>
16 #include <stdexcept>
17 #include <type_traits>
18
19 namespace DetourModKit
20 {
21
22 namespace
23 {
24 constexpr std::size_t EMERGENCY_ASYNC_LOGGER_LEAK_SLOTS = 16;
25
26 struct AsyncLoggerLeakSlot
27 {
28 alignas(std::shared_ptr<AsyncLogger>) unsigned char storage[sizeof(std::shared_ptr<AsyncLogger>)];
29 std::atomic<bool> occupied{false};
30 };
31
32 1352 std::atomic<std::shared_ptr<const Logger::StaticConfig>> &static_config_atom()
33 {
34 static std::atomic<std::shared_ptr<const Logger::StaticConfig>> instance{
35 1067 std::make_shared<const Logger::StaticConfig>(DEFAULT_LOG_PREFIX, DEFAULT_LOG_FILE_NAME,
36
4/8
✓ Branch 2 → 3 taken 1067 times.
✓ Branch 2 → 11 taken 285 times.
✓ Branch 4 → 5 taken 1067 times.
✗ Branch 4 → 11 not taken.
✓ Branch 5 → 6 taken 1067 times.
✗ Branch 5 → 13 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
1352 DEFAULT_TIMESTAMP_FORMAT)};
37 1352 return instance;
38 }
39
40 void leak_async_logger_handle(std::shared_ptr<AsyncLogger> &logger) noexcept
41 {
42 static_assert(std::is_nothrow_move_constructible_v<std::shared_ptr<AsyncLogger>>,
43 "AsyncLogger leak cells must be nothrow-move-constructible.");
44
45 if (!logger)
46 {
47 return;
48 }
49
50 detail::pin_current_module();
51
52 auto *leaked = new (std::nothrow) std::shared_ptr<AsyncLogger>(std::move(logger));
53 if (leaked != nullptr)
54 {
55 (void)leaked;
56 DetourModKit::Diagnostics::record_intentional_leak(DetourModKit::Diagnostics::LeakSubsystem::Logger);
57 return;
58 }
59
60 void *virtual_cell =
61 VirtualAlloc(nullptr, sizeof(std::shared_ptr<AsyncLogger>), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
62 if (virtual_cell != nullptr)
63 {
64 new (virtual_cell) std::shared_ptr<AsyncLogger>(std::move(logger));
65 DetourModKit::Diagnostics::record_intentional_leak(DetourModKit::Diagnostics::LeakSubsystem::Logger);
66 return;
67 }
68
69 static AsyncLoggerLeakSlot emergency_slots[EMERGENCY_ASYNC_LOGGER_LEAK_SLOTS];
70 for (auto &slot : emergency_slots)
71 {
72 bool expected = false;
73 if (slot.occupied.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
74 {
75 new (static_cast<void *>(slot.storage)) std::shared_ptr<AsyncLogger>(std::move(logger));
76 DetourModKit::Diagnostics::record_intentional_leak(
77 DetourModKit::Diagnostics::LeakSubsystem::Logger);
78 return;
79 }
80 }
81
82 std::terminate();
83 }
84 } // anonymous namespace
85
86 1067 std::shared_ptr<const Logger::StaticConfig> Logger::get_static_config()
87 {
88 1067 return static_config_atom().load(std::memory_order_acquire);
89 }
90
91 285 void Logger::set_static_config(std::shared_ptr<const StaticConfig> config)
92 {
93 570 static_config_atom().store(std::move(config), std::memory_order_release);
94 285 }
95
96 1611 LogLevel Logger::string_to_log_level(std::string_view level_str)
97 {
98
1/2
✓ Branch 4 → 5 taken 1611 times.
✗ Branch 4 → 35 not taken.
1611 std::string upper_level_str(level_str);
99 1611 std::transform(upper_level_str.begin(), upper_level_str.end(), upper_level_str.begin(),
100 20745 [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
101
102
3/4
✓ Branch 10 → 11 taken 1611 times.
✗ Branch 10 → 38 not taken.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1608 times.
1611 if (upper_level_str == "TRACE")
103 3 return LogLevel::Trace;
104
3/4
✓ Branch 13 → 14 taken 1608 times.
✗ Branch 13 → 38 not taken.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 1604 times.
1608 if (upper_level_str == "DEBUG")
105 4 return LogLevel::Debug;
106
3/4
✓ Branch 16 → 17 taken 1604 times.
✗ Branch 16 → 38 not taken.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 1600 times.
1604 if (upper_level_str == "INFO")
107 4 return LogLevel::Info;
108
3/4
✓ Branch 19 → 20 taken 1600 times.
✗ Branch 19 → 38 not taken.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 22 taken 1597 times.
1600 if (upper_level_str == "WARNING")
109 3 return LogLevel::Warning;
110
3/4
✓ Branch 22 → 23 taken 1597 times.
✗ Branch 22 → 38 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 25 taken 1594 times.
1597 if (upper_level_str == "ERROR")
111 3 return LogLevel::Error;
112
113 std::cerr << "[" << DEFAULT_LOG_PREFIX << " Logger WARNING] Unrecognized log level string '" << level_str
114
6/12
✓ Branch 25 → 26 taken 1594 times.
✗ Branch 25 → 38 not taken.
✓ Branch 26 → 27 taken 1594 times.
✗ Branch 26 → 38 not taken.
✓ Branch 27 → 28 taken 1594 times.
✗ Branch 27 → 38 not taken.
✓ Branch 28 → 29 taken 1594 times.
✗ Branch 28 → 38 not taken.
✓ Branch 29 → 30 taken 1594 times.
✗ Branch 29 → 38 not taken.
✓ Branch 30 → 31 taken 1594 times.
✗ Branch 30 → 38 not taken.
1594 << "'. Defaulting to INFO." << '\n';
115 1594 return LogLevel::Info;
116 1611 }
117
118 285 void Logger::configure(std::string_view prefix, std::string_view file_name, std::string_view timestamp_fmt)
119 {
120
4/8
✓ Branch 7 → 8 taken 285 times.
✗ Branch 7 → 34 not taken.
✓ Branch 10 → 11 taken 285 times.
✗ Branch 10 → 28 not taken.
✓ Branch 11 → 12 taken 285 times.
✗ Branch 11 → 26 not taken.
✓ Branch 12 → 13 taken 285 times.
✗ Branch 12 → 24 not taken.
855 set_static_config(std::make_shared<const StaticConfig>(std::string(prefix), std::string(file_name),
121
1/2
✓ Branch 4 → 5 taken 285 times.
✗ Branch 4 → 40 not taken.
570 std::string(timestamp_fmt)));
122
123 285 Logger &instance = get_instance();
124
125 // configure() is the authoritative reset path -- allow reconfiguration even after shutdown to support reuse
126 // (e.g., test fixtures).
127 285 instance.m_shutdown_called.store(false, std::memory_order_release);
128
129 // Comparison is done inside reconfigure() under the lock to prevent reading string members while another thread
130 // is modifying them.
131 285 instance.reconfigure(prefix, file_name, timestamp_fmt);
132 285 }
133
134 290 void Logger::reconfigure(std::string_view prefix, std::string_view file_name, std::string_view timestamp_fmt)
135 {
136
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 290 times.
290 if (m_shutdown_called.load(std::memory_order_acquire))
137 {
138 94 return;
139 }
140
141 // Acquire both m_async_mutex and m_log_mutex_ptr to prevent concurrent log() calls from reading
142 // partially-updated string members during reconfiguration
143
1/2
✓ Branch 6 → 7 taken 290 times.
✗ Branch 6 → 113 not taken.
290 std::scoped_lock lock(m_async_mutex, *m_log_mutex_ptr);
144
145 // Skip reconfiguration only when all parameters match AND the stream is usable. After shutdown or a prior open
146 // failure the stream may be closed, so we must fall through to reopen even if the strings are identical.
147
9/10
✓ Branch 9 → 10 taken 276 times.
✓ Branch 9 → 20 taken 14 times.
✓ Branch 12 → 13 taken 95 times.
✓ Branch 12 → 20 taken 181 times.
✓ Branch 15 → 16 taken 94 times.
✓ Branch 15 → 20 taken 1 time.
✓ Branch 18 → 19 taken 94 times.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 94 times.
✓ Branch 21 → 23 taken 196 times.
384 if (m_log_file_stream_ptr->is_open() && m_log_prefix == prefix && m_log_file_name == file_name &&
148 94 m_timestamp_format == timestamp_fmt)
149 {
150 94 return;
151 }
152
153
6/8
✓ Branch 25 → 26 taken 182 times.
✓ Branch 25 → 30 taken 14 times.
✓ Branch 27 → 28 taken 182 times.
✗ Branch 27 → 111 not taken.
✓ Branch 28 → 29 taken 182 times.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 182 times.
✓ Branch 31 → 51 taken 14 times.
196 if (m_log_file_stream_ptr->is_open() && m_log_file_stream_ptr->good())
154 {
155 364 *m_log_file_stream_ptr << "[" << get_timestamp() << "] "
156
6/12
✓ Branch 33 → 34 taken 182 times.
✗ Branch 33 → 111 not taken.
✓ Branch 34 → 35 taken 182 times.
✗ Branch 34 → 99 not taken.
✓ Branch 35 → 36 taken 182 times.
✗ Branch 35 → 97 not taken.
✓ Branch 36 → 37 taken 182 times.
✗ Branch 36 → 97 not taken.
✓ Branch 37 → 38 taken 182 times.
✗ Branch 37 → 97 not taken.
✓ Branch 40 → 41 taken 182 times.
✗ Branch 40 → 97 not taken.
182 << "[" << std::setw(7) << std::left << "INFO" << "] :: "
157
5/10
✓ Branch 41 → 42 taken 182 times.
✗ Branch 41 → 97 not taken.
✓ Branch 42 → 43 taken 182 times.
✗ Branch 42 → 97 not taken.
✓ Branch 43 → 44 taken 182 times.
✗ Branch 43 → 97 not taken.
✓ Branch 44 → 45 taken 182 times.
✗ Branch 44 → 97 not taken.
✓ Branch 45 → 46 taken 182 times.
✗ Branch 45 → 97 not taken.
182 << "Logger reconfiguring. New file: " << file_name << '\n';
158
1/2
✓ Branch 48 → 49 taken 182 times.
✗ Branch 48 → 111 not taken.
182 m_log_file_stream_ptr->flush();
159 182 m_log_file_stream_ptr->close();
160 }
161
162
1/2
✓ Branch 51 → 52 taken 196 times.
✗ Branch 51 → 111 not taken.
196 m_log_prefix = prefix;
163
1/2
✓ Branch 52 → 53 taken 196 times.
✗ Branch 52 → 111 not taken.
196 m_log_file_name = file_name;
164
1/2
✓ Branch 53 → 54 taken 196 times.
✗ Branch 53 → 111 not taken.
196 m_timestamp_format = timestamp_fmt;
165
166
1/2
✓ Branch 54 → 55 taken 196 times.
✗ Branch 54 → 111 not taken.
196 std::wstring log_file_full_path = generate_log_file_path();
167
1/2
✓ Branch 57 → 58 taken 196 times.
✗ Branch 57 → 109 not taken.
196 m_log_file_stream_ptr->open(log_file_full_path, std::ios::out | std::ios::trunc);
168
169
2/2
✓ Branch 60 → 61 taken 4 times.
✓ Branch 60 → 73 taken 192 times.
196 if (!m_log_file_stream_ptr->is_open())
170 {
171 4 std::cerr << "[" << m_log_prefix << " Logger CRITICAL ERROR] "
172
1/2
✓ Branch 66 → 67 taken 4 times.
✗ Branch 66 → 102 not taken.
8 << "Failed to open log file: " << std::filesystem::path(log_file_full_path).string()
173
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';
174 }
175 else
176 {
177 384 *m_log_file_stream_ptr << "[" << get_timestamp() << "] "
178
6/12
✓ Branch 74 → 75 taken 192 times.
✗ Branch 74 → 109 not taken.
✓ Branch 75 → 76 taken 192 times.
✗ Branch 75 → 108 not taken.
✓ Branch 76 → 77 taken 192 times.
✗ Branch 76 → 106 not taken.
✓ Branch 77 → 78 taken 192 times.
✗ Branch 77 → 106 not taken.
✓ Branch 78 → 79 taken 192 times.
✗ Branch 78 → 106 not taken.
✓ Branch 81 → 82 taken 192 times.
✗ Branch 81 → 106 not taken.
192 << "[" << std::setw(7) << std::left << "INFO" << "] :: "
179
5/10
✓ Branch 82 → 83 taken 192 times.
✗ Branch 82 → 106 not taken.
✓ Branch 83 → 84 taken 192 times.
✗ Branch 83 → 106 not taken.
✓ Branch 84 → 85 taken 192 times.
✗ Branch 84 → 106 not taken.
✓ Branch 85 → 86 taken 192 times.
✗ Branch 85 → 106 not taken.
✓ Branch 86 → 87 taken 192 times.
✗ Branch 86 → 106 not taken.
192 << "Logger reconfigured. Now logging to: " << file_name << '\n';
180 }
181
2/2
✓ Branch 92 → 93 taken 196 times.
✓ Branch 92 → 95 taken 94 times.
290 }
182
183 1067 Logger::Logger()
184
2/4
✓ Branch 5 → 6 taken 1067 times.
✗ Branch 5 → 78 not taken.
✓ Branch 6 → 7 taken 1067 times.
✗ Branch 6 → 76 not taken.
1067 : m_log_file_stream_ptr(std::make_shared<WinFileStream>()), m_log_mutex_ptr(std::make_shared<std::mutex>())
185 {
186 {
187
1/2
✓ Branch 12 → 13 taken 1067 times.
✗ Branch 12 → 57 not taken.
1067 auto config = get_static_config();
188
1/2
✓ Branch 14 → 15 taken 1067 times.
✗ Branch 14 → 55 not taken.
1067 m_log_prefix = config->log_prefix;
189
1/2
✓ Branch 16 → 17 taken 1067 times.
✗ Branch 16 → 55 not taken.
1067 m_log_file_name = config->log_file_name;
190
1/2
✓ Branch 18 → 19 taken 1067 times.
✗ Branch 18 → 55 not taken.
1067 m_timestamp_format = config->timestamp_format;
191 1067 }
192
193
1/2
✓ Branch 20 → 21 taken 1067 times.
✗ Branch 20 → 69 not taken.
1067 const std::wstring log_file_full_path = generate_log_file_path();
194
1/2
✓ Branch 23 → 24 taken 1067 times.
✗ Branch 23 → 67 not taken.
1067 m_log_file_stream_ptr->open(log_file_full_path, std::ios::out | std::ios::trunc);
195
196
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 39 taken 1067 times.
1067 if (!m_log_file_stream_ptr->is_open())
197 {
198 std::cerr << "[" << m_log_prefix << " Logger CRITICAL ERROR] "
199 << "Failed to open log file: " << std::filesystem::path(log_file_full_path).string()
200 << ". Subsequent logs to file will fail." << '\n';
201 }
202 else
203 {
204
5/10
✓ Branch 40 → 41 taken 1067 times.
✗ Branch 40 → 67 not taken.
✓ Branch 41 → 42 taken 1067 times.
✗ Branch 41 → 66 not taken.
✓ Branch 42 → 43 taken 1067 times.
✗ Branch 42 → 64 not taken.
✓ Branch 43 → 44 taken 1067 times.
✗ Branch 43 → 64 not taken.
✓ Branch 46 → 47 taken 1067 times.
✗ Branch 46 → 64 not taken.
2134 *m_log_file_stream_ptr << "[" << get_timestamp() << "] [" << std::setw(7) << std::left << "INFO"
205
4/8
✓ Branch 47 → 48 taken 1067 times.
✗ Branch 47 → 64 not taken.
✓ Branch 48 → 49 taken 1067 times.
✗ Branch 48 → 64 not taken.
✓ Branch 49 → 50 taken 1067 times.
✗ Branch 49 → 64 not taken.
✓ Branch 50 → 51 taken 1067 times.
✗ Branch 50 → 64 not taken.
1067 << "] :: Logger initialized. Logging to: " << m_log_file_name << '\n';
206 }
207 1067 }
208
209 2123 Logger::~Logger() noexcept
210 {
211 1067 bool expected = false;
212
2/2
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 1056 times.
1067 if (!m_shutdown_called.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
213 {
214 11 return;
215 }
216 1056 shutdown_internal();
217
14/14
✓ Branch 8 → 9 taken 1056 times.
✓ Branch 8 → 10 taken 11 times.
✓ Branch 12 → 13 taken 1056 times.
✓ Branch 12 → 14 taken 11 times.
✓ Branch 16 → 17 taken 1056 times.
✓ Branch 16 → 18 taken 11 times.
✓ Branch 20 → 21 taken 1056 times.
✓ Branch 20 → 22 taken 11 times.
✓ Branch 24 → 25 taken 1056 times.
✓ Branch 24 → 26 taken 11 times.
✓ Branch 28 → 29 taken 1056 times.
✓ Branch 28 → 30 taken 11 times.
✓ Branch 32 → 33 taken 1056 times.
✓ Branch 32 → 34 taken 11 times.
1133 }
218
219 43 void Logger::shutdown() noexcept
220 {
221 43 bool expected = false;
222
2/2
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 5 taken 21 times.
43 if (!m_shutdown_called.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
223 {
224 22 return;
225 }
226 21 shutdown_internal();
227 }
228
229 1077 void Logger::shutdown_internal() noexcept
230 {
231 1077 std::shared_ptr<AsyncLogger> local_logger;
232
233 {
234 1077 std::lock_guard<std::mutex> lock(m_async_mutex);
235
2/2
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 15 taken 1070 times.
1077 if (m_async_mode_enabled.load(std::memory_order_acquire))
236 {
237 7 local_logger = m_async_logger.exchange(nullptr, std::memory_order_acq_rel);
238 7 m_async_mode_enabled.store(false, std::memory_order_release);
239
1/2
✓ Branch 12 → 13 taken 7 times.
✗ Branch 12 → 15 not taken.
7 if (local_logger)
240 {
241 7 local_logger->shutdown();
242 }
243 }
244 1077 }
245
246 // If the writer thread was detached under loader lock, it may still be accessing AsyncLogger members (m_queue,
247 // m_flush_mutex, etc.). Transfer ownership to permanent heap storage so the object outlives the detached
248 // thread; pin the module so the code pages it executes from also remain mapped.
249 //
250 // The transfer is unconditional when loader lock is held: concurrent log() callers may still own temporary
251 // shared_ptrs obtained from m_async_logger before exchange(), so use_count() is not a reliable proxy for "no
252 // other owners". Dropping local_logger's ref while a temporary outlives us would let the last temporary's
253 // destructor race the detached writer thread.
254 //
255 // The leak is per-call and append-only: each invocation retains its own shared_ptr cell, so a process that
256 // re-attaches after shutdown (e.g. hot-reload) and hits loader lock again cannot drop a prior handle whose
257 // writer thread may still be running. The helper first tries new (std::nothrow), then non-CRT permanent storage
258 // fallbacks, so the noexcept destructor never frees the logger merely because the heap cell could not be
259 // allocated.
260
4/6
✓ Branch 17 → 18 taken 7 times.
✓ Branch 17 → 21 taken 1070 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 7 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 1077 times.
1077 if (local_logger && detail::is_loader_lock_held())
261 {
262 leak_async_logger_handle(local_logger);
263 }
264
265 {
266 // Acquire both mutexes to prevent configure()/reconfigure() from opening a new stream in the gap after the
267 // async-logger teardown block above releases m_async_mutex.
268 1077 std::scoped_lock lock(m_async_mutex, *m_log_mutex_ptr);
269
3/6
✓ Branch 27 → 28 taken 1077 times.
✗ Branch 27 → 32 not taken.
✓ Branch 30 → 31 taken 1077 times.
✗ Branch 30 → 32 not taken.
✓ Branch 33 → 34 taken 1077 times.
✗ Branch 33 → 38 not taken.
1077 if (m_log_file_stream_ptr && m_log_file_stream_ptr->is_open())
270 {
271 1077 m_log_file_stream_ptr->flush();
272 1077 m_log_file_stream_ptr->close();
273 }
274 1077 }
275 1077 }
276
277 109 void Logger::set_log_level(LogLevel level)
278 {
279 109 auto level_int = static_cast<std::underlying_type_t<LogLevel>>(level);
280
3/4
✓ Branch 2 → 3 taken 109 times.
✗ Branch 2 → 4 not taken.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 107 times.
109 if (level_int < 0 || level_int > static_cast<std::underlying_type_t<LogLevel>>(LogLevel::Error))
281 {
282
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.",
283 level_int);
284 18 return;
285 }
286
287 107 auto old_level = m_current_log_level.load(std::memory_order_acquire);
288
2/2
✓ Branch 7 → 8 taken 16 times.
✓ Branch 7 → 9 taken 91 times.
107 if (old_level == level)
289 {
290 16 return;
291 }
292
293 91 m_current_log_level.store(level, std::memory_order_release);
294
295
1/2
✓ Branch 12 → 13 taken 91 times.
✗ Branch 12 → 17 not taken.
91 log(LogLevel::Info, "Log level changed from {} to {}", log_level_to_string(old_level),
296 182 log_level_to_string(level));
297 }
298
299 3328 bool Logger::log(LogLevel level, std::string_view message)
300 {
301
2/2
✓ Branch 3 → 4 taken 219 times.
✓ Branch 3 → 5 taken 3123 times.
3328 if (level < m_current_log_level.load(std::memory_order_acquire))
302 {
303 219 return false;
304 }
305
306 // Fast path: lock-free check via atomic shared_ptr
307
2/2
✓ Branch 6 → 7 taken 893 times.
✓ Branch 6 → 19 taken 2215 times.
3123 if (m_async_mode_enabled.load(std::memory_order_acquire))
308 {
309 893 auto local_logger = m_async_logger.load(std::memory_order_acquire);
310
1/2
✓ Branch 9 → 10 taken 951 times.
✗ Branch 9 → 13 not taken.
951 if (local_logger)
311 {
312 // Propagate the queue's accept/drop result so callers learn the true delivery status instead of an
313 // unconditional success.
314 951 return local_logger->enqueue(level, message);
315 }
316
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 948 times.
949 }
317
318 2215 const auto level_str = log_level_to_string(level);
319
1/2
✓ Branch 21 → 22 taken 2209 times.
✗ Branch 21 → 79 not taken.
2208 std::lock_guard<std::mutex> lock(*m_log_mutex_ptr);
320
321
6/8
✓ Branch 24 → 25 taken 2135 times.
✓ Branch 24 → 29 taken 74 times.
✓ Branch 26 → 27 taken 2135 times.
✗ Branch 26 → 77 not taken.
✓ Branch 27 → 28 taken 2135 times.
✗ Branch 27 → 29 not taken.
✓ Branch 30 → 31 taken 2135 times.
✓ Branch 30 → 51 taken 74 times.
2209 if (m_log_file_stream_ptr->is_open() && m_log_file_stream_ptr->good())
322 {
323 4270 *m_log_file_stream_ptr << "[" << get_timestamp() << "] "
324
10/20
✓ Branch 32 → 33 taken 2135 times.
✗ Branch 32 → 77 not taken.
✓ Branch 33 → 34 taken 2135 times.
✗ Branch 33 → 73 not taken.
✓ Branch 34 → 35 taken 2135 times.
✗ Branch 34 → 71 not taken.
✓ Branch 35 → 36 taken 2135 times.
✗ Branch 35 → 71 not taken.
✓ Branch 36 → 37 taken 2135 times.
✗ Branch 36 → 71 not taken.
✓ Branch 39 → 40 taken 2135 times.
✗ Branch 39 → 71 not taken.
✓ Branch 40 → 41 taken 2135 times.
✗ Branch 40 → 71 not taken.
✓ Branch 41 → 42 taken 2135 times.
✗ Branch 41 → 71 not taken.
✓ Branch 42 → 43 taken 2135 times.
✗ Branch 42 → 71 not taken.
✓ Branch 43 → 44 taken 2135 times.
✗ Branch 43 → 71 not taken.
2135 << "[" << std::setw(7) << std::left << level_str << "] :: " << message << '\n';
325
326 // Flush on warnings/errors to ensure critical messages survive crashes
327
2/2
✓ Branch 45 → 46 taken 1020 times.
✓ Branch 45 → 48 taken 1115 times.
2135 if (level >= LogLevel::Warning)
328 {
329
1/2
✓ Branch 47 → 48 taken 1020 times.
✗ Branch 47 → 77 not taken.
1020 m_log_file_stream_ptr->flush();
330 }
331
332 // Report whether the write (and any flush) left the stream healthy, so log_noexcept()/try_log() reflect
333 // actual delivery rather than just a no-throw call.
334
1/2
✓ Branch 49 → 50 taken 2135 times.
✗ Branch 49 → 77 not taken.
2135 return m_log_file_stream_ptr->good();
335 }
336
337
2/2
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 67 taken 72 times.
74 if (level >= LogLevel::Error)
338 {
339
6/12
✓ Branch 52 → 53 taken 2 times.
✗ Branch 52 → 77 not taken.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 53 → 77 not taken.
✓ Branch 54 → 55 taken 2 times.
✗ Branch 54 → 77 not taken.
✓ Branch 55 → 56 taken 2 times.
✗ Branch 55 → 76 not taken.
✓ Branch 56 → 57 taken 2 times.
✗ Branch 56 → 74 not taken.
✓ Branch 57 → 58 taken 2 times.
✗ Branch 57 → 74 not taken.
4 std::cerr << "[" << m_log_prefix << " LOG_FILE_WRITE_ERROR] [" << get_timestamp() << "] [" << std::setw(7)
340
5/10
✓ Branch 60 → 61 taken 2 times.
✗ Branch 60 → 74 not taken.
✓ Branch 61 → 62 taken 2 times.
✗ Branch 61 → 74 not taken.
✓ Branch 62 → 63 taken 2 times.
✗ Branch 62 → 74 not taken.
✓ Branch 63 → 64 taken 2 times.
✗ Branch 63 → 74 not taken.
✓ Branch 64 → 65 taken 2 times.
✗ Branch 64 → 74 not taken.
2 << std::left << level_str << "] :: " << message << '\n';
341 }
342
343 // The file sink was closed or unhealthy; the message was not delivered to it (an error-level message reached
344 // stderr only as a last resort).
345 74 return false;
346 2209 }
347
348 30 bool Logger::log_noexcept(LogLevel level, std::string_view message) noexcept
349 {
350 // The synchronous sink allocates (timestamp formatting) and a custom stream could raise, so the throwing log()
351 // is wrapped here to keep the no-throw contract for noexcept-boundary callers. The returned bool is log()'s
352 // real delivery status, not merely "did not throw".
353 try
354 {
355
1/2
✓ Branch 2 → 3 taken 30 times.
✗ Branch 2 → 5 not taken.
30 return log(level, message);
356 }
357 catch (...)
358 {
359 return false;
360 }
361 }
362
363 3578 std::string Logger::get_timestamp() const
364 {
365 try
366 {
367 3578 const auto now = std::chrono::system_clock::now();
368 3578 const auto in_time_t = std::chrono::system_clock::to_time_t(now);
369 3578 std::tm timeinfo_struct = {};
370
371 #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
372 // MinGW's localtime_s takes the same (struct tm *, const time_t *) argument order as MSVC, not the ISO C11
373 // Annex K order, so the call is identical.
374
2/4
✓ Branch 8 → 9 taken 3578 times.
✗ Branch 8 → 43 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 3578 times.
3578 if (localtime_s(&timeinfo_struct, &in_time_t) != 0)
375 {
376 throw std::runtime_error("localtime_s failed to convert time.");
377 }
378 #else
379 if (localtime_r(&in_time_t, &timeinfo_struct) == nullptr)
380 {
381 throw std::runtime_error("localtime_r failed to convert time.");
382 }
383 #endif
384 // Single stack buffer for timestamp + milliseconds, no heap allocation
385 char buf[134];
386 3578 const size_t len = std::strftime(buf, sizeof(buf) - 5, m_timestamp_format.c_str(), &timeinfo_struct);
387
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 3578 times.
3578 if (len == 0)
388 {
389 return "TIMESTAMP_FORMAT_ERROR";
390 }
391
392
2/4
✓ Branch 21 → 22 taken 3578 times.
✗ Branch 21 → 37 not taken.
✓ Branch 22 → 23 taken 3578 times.
✗ Branch 22 → 37 not taken.
3578 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
393
1/2
✓ Branch 24 → 25 taken 3578 times.
✗ Branch 24 → 43 not taken.
3578 const int ms_len = std::snprintf(buf + len, 5, ".%03d", static_cast<int>(ms.count()));
394
1/2
✓ Branch 27 → 28 taken 3578 times.
✗ Branch 27 → 40 not taken.
7156 return std::string(buf, len + static_cast<size_t>(ms_len));
395 }
396 catch (const std::exception &e)
397 {
398 std::cerr << "[" << m_log_prefix << " Logger TIMESTAMP_ERROR] Failed to generate timestamp: " << e.what()
399 << '\n';
400 return "TIMESTAMP_GENERATION_ERROR";
401 }
402 catch (...)
403 {
404 std::cerr << "[" << m_log_prefix
405 << " Logger TIMESTAMP_ERROR] Unknown exception during timestamp generation." << '\n';
406 return "TIMESTAMP_GENERATION_ERROR";
407 }
408 }
409
410 1263 std::wstring Logger::generate_log_file_path() const
411 {
412
1/2
✓ Branch 2 → 3 taken 1263 times.
✗ Branch 2 → 81 not taken.
1263 std::filesystem::path log_file_path_obj(m_log_file_name);
413
2/2
✓ Branch 4 → 5 taken 283 times.
✓ Branch 4 → 7 taken 980 times.
1263 if (log_file_path_obj.is_absolute())
414 {
415
1/2
✓ Branch 5 → 6 taken 283 times.
✗ Branch 5 → 79 not taken.
283 return log_file_path_obj.wstring();
416 }
417
418 try
419 {
420
1/2
✓ Branch 7 → 8 taken 980 times.
✗ Branch 7 → 51 not taken.
980 std::wstring module_dir = Filesystem::get_runtime_directory();
421
4/8
✓ Branch 9 → 10 taken 980 times.
✗ Branch 9 → 12 not taken.
✓ Branch 10 → 11 taken 980 times.
✗ Branch 10 → 49 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 980 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 23 taken 980 times.
980 if (module_dir.empty() || module_dir == L".")
422 {
423 std::cerr << "[" << m_log_prefix << " Logger PATH_WARNING] "
424 << "Could not determine module directory. Using relative path: " << m_log_file_name << '\n';
425 return log_file_path_obj.wstring();
426 }
427
428
3/6
✓ Branch 23 → 24 taken 980 times.
✗ Branch 23 → 43 not taken.
✓ Branch 24 → 25 taken 980 times.
✗ Branch 24 → 40 not taken.
✓ Branch 25 → 26 taken 980 times.
✗ Branch 25 → 38 not taken.
980 const std::filesystem::path final_log_path = std::filesystem::path(module_dir) / m_log_file_name;
429
2/4
✓ Branch 28 → 29 taken 980 times.
✗ Branch 28 → 46 not taken.
✓ Branch 29 → 30 taken 980 times.
✗ Branch 29 → 44 not taken.
980 return final_log_path.lexically_normal().wstring();
430 980 }
431 catch (const std::exception &e)
432 {
433 std::cerr << "[" << m_log_prefix
434 << " Logger PATH_WARNING] Failed to determine module directory for log file: " << e.what()
435 << ". Using relative path for log file: " << m_log_file_name << '\n';
436 return log_file_path_obj.wstring();
437 }
438 catch (...)
439 {
440 std::cerr << "[" << m_log_prefix
441 << " Logger PATH_WARNING] Unknown exception while determining module directory for log file."
442 << " Using relative path: " << m_log_file_name << '\n';
443 return log_file_path_obj.wstring();
444 }
445 1263 }
446
447 31 void Logger::enable_async_mode(const AsyncLoggerConfig &config)
448 {
449 31 bool should_log_error = false;
450 31 bool should_log_success = false;
451 31 std::string error_msg;
452 31 size_t queue_cap = 0;
453 31 size_t batch_sz = 0;
454
455 {
456
1/2
✓ Branch 3 → 4 taken 31 times.
✗ Branch 3 → 65 not taken.
31 std::lock_guard<std::mutex> lock(m_async_mutex);
457
458
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 29 times.
31 if (m_async_mode_enabled.load(std::memory_order_acquire))
459 {
460 2 return;
461 }
462
463
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 29 times.
29 if (!m_log_file_stream_ptr->is_open())
464 {
465 should_log_error = true;
466 error_msg = "Cannot enable async mode: log file is not open.";
467 }
468 else
469 {
470 try
471 {
472 // Override the config's timestamp format with the Logger's own so the async sink emits the same
473 // timestamps as the synchronous path; callers configure the format through the Logger, not the
474 // async config.
475
1/2
✓ Branch 11 → 12 taken 29 times.
✗ Branch 11 → 40 not taken.
29 AsyncLoggerConfig effective_config = config;
476
1/2
✓ Branch 12 → 13 taken 29 times.
✗ Branch 12 → 38 not taken.
29 effective_config.timestamp_format = m_timestamp_format;
477 27 m_async_logger.store(
478
2/2
✓ Branch 13 → 14 taken 27 times.
✓ Branch 13 → 37 taken 2 times.
56 std::make_shared<AsyncLogger>(effective_config, m_log_file_stream_ptr, m_log_mutex_ptr),
479 std::memory_order_release);
480 27 m_async_mode_enabled.store(true, std::memory_order_release);
481 27 should_log_success = true;
482 27 queue_cap = config.queue_capacity;
483 27 batch_sz = config.batch_size;
484 29 }
485
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 2 times.
2 catch (const std::exception &e)
486 {
487 2 should_log_error = true;
488
2/4
✓ Branch 47 → 48 taken 2 times.
✗ Branch 47 → 56 not taken.
✓ Branch 48 → 49 taken 2 times.
✗ Branch 48 → 54 not taken.
4 error_msg = std::string("Failed to enable async mode: ") + e.what();
489 2 }
490 }
491
2/2
✓ Branch 21 → 22 taken 29 times.
✓ Branch 21 → 24 taken 2 times.
31 }
492
493
2/2
✓ Branch 23 → 25 taken 2 times.
✓ Branch 23 → 27 taken 27 times.
29 if (should_log_error)
494 {
495
1/2
✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 66 not taken.
2 log(LogLevel::Error, "{}", error_msg);
496 }
497
1/2
✓ Branch 27 → 28 taken 27 times.
✗ Branch 27 → 30 not taken.
27 else if (should_log_success)
498 {
499
1/2
✓ Branch 28 → 29 taken 27 times.
✗ Branch 28 → 67 not taken.
27 log(LogLevel::Info, "Async logging mode enabled. Queue capacity: {}, Batch size: {}", queue_cap, batch_sz);
500 }
501
2/2
✓ Branch 32 → 33 taken 29 times.
✓ Branch 32 → 35 taken 2 times.
31 }
502
503 21 void Logger::enable_async_mode()
504 {
505
2/4
✓ Branch 5 → 6 taken 21 times.
✗ Branch 5 → 12 not taken.
✓ Branch 6 → 7 taken 21 times.
✗ Branch 6 → 10 not taken.
42 enable_async_mode(AsyncLoggerConfig{});
506 21 }
507
508 26 void Logger::disable_async_mode() noexcept
509 {
510 26 std::shared_ptr<AsyncLogger> local_async;
511 26 bool should_log = false;
512
513 {
514 26 std::lock_guard<std::mutex> lock(m_async_mutex);
515
516
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 6 taken 20 times.
26 if (!m_async_mode_enabled.load(std::memory_order_acquire))
517 {
518 6 return;
519 }
520
521 20 local_async = m_async_logger.exchange(nullptr, std::memory_order_acq_rel);
522
1/2
✓ Branch 12 → 13 taken 20 times.
✗ Branch 12 → 15 not taken.
20 if (local_async)
523 {
524 20 local_async->shutdown();
525 }
526
527 20 m_async_mode_enabled.store(false, std::memory_order_release);
528 20 should_log = true;
529
2/2
✓ Branch 18 → 19 taken 20 times.
✓ Branch 18 → 22 taken 6 times.
26 }
530
531 // If the writer thread was detached under loader lock it may still be touching AsyncLogger members (m_queue,
532 // m_flush_mutex, etc.). Mirror shutdown_internal's discipline: transfer ownership to permanent heap storage so
533 // the object outlives the detached thread, and pin the module so the code pages it runs from stay mapped.
534 // Dropping the last shared_ptr here instead would let the AsyncLogger destructor race the live writer -- a
535 // use-after-free. The transfer is unconditional under loader lock since concurrent log() callers may still own
536 // temporaries fetched before the exchange, so use_count() is an unreliable "no other owners" proxy. The leak is
537 // per-call and append-only, so a hot-reload cycle (disable -> enable -> disable) that hits loader lock again
538 // cannot drop a prior handle whose writer may still be running. The helper first tries new (std::nothrow), then
539 // non-CRT permanent storage fallbacks, so the object is retained even if the heap cell cannot be allocated.
540
2/4
✓ Branch 21 → 23 taken 20 times.
✗ Branch 21 → 26 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 20 times.
20 const bool leaked_under_loader_lock = local_async && detail::is_loader_lock_held();
541
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 20 times.
20 if (leaked_under_loader_lock)
542 {
543 leak_async_logger_handle(local_async);
544 }
545
546
2/4
✓ Branch 29 → 30 taken 20 times.
✗ Branch 29 → 34 not taken.
✓ Branch 30 → 31 taken 20 times.
✗ Branch 30 → 34 not taken.
20 if (should_log && !leaked_under_loader_lock)
547 {
548 // disable_async_mode() is noexcept; the synchronous sink can allocate and throw while formatting this
549 // line. Fail closed: drop the diagnostic rather than letting an exception escape the mode switch.
550 try
551 {
552
1/2
✓ Branch 32 → 33 taken 20 times.
✗ Branch 32 → 41 not taken.
20 log(LogLevel::Info, "Async logging mode disabled. Switched to synchronous mode.");
553 }
554 catch (...)
555 {
556 }
557 }
558
2/2
✓ Branch 36 → 37 taken 20 times.
✓ Branch 36 → 39 taken 6 times.
26 }
559
560 45 bool Logger::is_async_mode_enabled() const noexcept
561 {
562 45 return m_async_mode_enabled.load(std::memory_order_acquire);
563 }
564
565 96 void Logger::flush() noexcept
566 {
567
2/2
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 16 taken 89 times.
96 if (m_async_mode_enabled.load(std::memory_order_acquire))
568 {
569 7 auto local_logger = m_async_logger.load(std::memory_order_acquire);
570
1/2
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 10 not taken.
7 if (local_logger)
571 {
572 7 local_logger->flush();
573 7 return;
574 }
575
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 7 times.
7 }
576
577 89 std::lock_guard<std::mutex> lock(*m_log_mutex_ptr);
578
2/2
✓ Branch 20 → 21 taken 88 times.
✓ Branch 20 → 23 taken 1 time.
89 if (m_log_file_stream_ptr->is_open())
579 {
580 88 m_log_file_stream_ptr->flush();
581 }
582 89 }
583
584 } // namespace DetourModKit
585