Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/common/log.cpp
7328 views
1
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "log.h"
5
#include "assert.h"
6
#include "file_system.h"
7
#include "small_string.h"
8
#include "timer.h"
9
10
#include "fmt/format.h"
11
12
#include <array>
13
#include <bitset>
14
#include <cstdio>
15
#include <mutex>
16
#include <vector>
17
18
#if defined(_WIN32)
19
#include "windows_headers.h"
20
#elif defined(__ANDROID__)
21
#include <android/log.h>
22
#else
23
#include <sys/ioctl.h>
24
#include <termios.h>
25
#include <unistd.h>
26
#endif
27
28
using namespace std::string_view_literals;
29
30
namespace Log {
31
namespace {
32
struct RegisteredCallback
33
{
34
Log::CallbackFunctionType Function;
35
void* Parameter;
36
};
37
} // namespace
38
39
using ChannelBitSet = std::bitset<static_cast<size_t>(Channel::MaxCount)>;
40
41
static void RegisterCallback(CallbackFunctionType callbackFunction, void* pUserParam,
42
const std::unique_lock<std::mutex>& lock);
43
static void UnregisterCallback(CallbackFunctionType callbackFunction, void* pUserParam,
44
const std::unique_lock<std::mutex>& lock);
45
static void UpdateEffectiveLevel();
46
47
static bool FilterTest(Channel channel, Level level);
48
static void ExecuteCallbacks(MessageCategory cat, const char* functionName, std::string_view message);
49
static void FormatLogMessageForDisplay(fmt::memory_buffer& buffer, MessageCategory cat, const char* functionName,
50
std::string_view message, bool timestamp, bool ansi_color_code);
51
static void ConsoleOutputLogCallback(void* pUserParam, MessageCategory cat, const char* functionName,
52
std::string_view message);
53
static void DebugOutputLogCallback(void* pUserParam, MessageCategory cat, const char* functionName,
54
std::string_view message);
55
static void FileOutputLogCallback(void* pUserParam, MessageCategory cat, const char* functionName,
56
std::string_view message);
57
template<typename T>
58
static void FormatLogMessageAndPrint(MessageCategory cat, const char* functionName, std::string_view message,
59
bool timestamp, bool ansi_color_code, const T& callback);
60
#ifdef _WIN32
61
template<typename T>
62
static void FormatLogMessageAndPrintW(MessageCategory cat, const char* functionName, std::string_view message,
63
bool timestamp, bool ansi_color_code, const T& callback);
64
#endif
65
66
static constexpr const std::array<char, static_cast<size_t>(Level::MaxCount)> s_log_level_characters = {
67
{'X', 'E', 'W', 'I', 'V', 'D', 'B', 'T'}};
68
69
static constexpr const std::array<const char*, static_cast<size_t>(Channel::MaxCount)> s_log_channel_names = {{
70
#define LOG_CHANNEL_NAME(X) #X,
71
ENUMERATE_LOG_CHANNELS(LOG_CHANNEL_NAME)
72
#undef LOG_CHANNEL_NAME
73
}};
74
75
namespace {
76
77
struct State
78
{
79
Level effective_log_level = Level::None;
80
Level requested_log_level = Level::Trace;
81
ChannelBitSet log_channels_enabled = ChannelBitSet().set();
82
83
std::vector<RegisteredCallback> callbacks;
84
std::mutex callbacks_mutex;
85
86
Timer::Value start_timestamp = Timer::GetCurrentValue();
87
88
FileSystem::ManagedCFilePtr file_handle;
89
90
bool console_output_enabled = false;
91
bool console_output_timestamps = false;
92
bool file_output_enabled = false;
93
bool file_output_timestamp = false;
94
bool debug_output_enabled = false;
95
96
#ifdef _WIN32
97
HANDLE hConsoleStdIn = NULL;
98
HANDLE hConsoleStdOut = NULL;
99
HANDLE hConsoleStdErr = NULL;
100
#endif
101
};
102
103
} // namespace
104
105
ALIGN_TO_CACHE_LINE static State s_state;
106
107
} // namespace Log
108
109
void Log::RegisterCallback(CallbackFunctionType callbackFunction, void* pUserParam)
110
{
111
std::unique_lock lock(s_state.callbacks_mutex);
112
RegisterCallback(callbackFunction, pUserParam, lock);
113
}
114
115
void Log::RegisterCallback(CallbackFunctionType callbackFunction, void* pUserParam,
116
const std::unique_lock<std::mutex>& lock)
117
{
118
RegisteredCallback Callback;
119
Callback.Function = callbackFunction;
120
Callback.Parameter = pUserParam;
121
122
s_state.callbacks.push_back(std::move(Callback));
123
UpdateEffectiveLevel();
124
}
125
126
void Log::UnregisterCallback(CallbackFunctionType callbackFunction, void* pUserParam)
127
{
128
std::unique_lock lock(s_state.callbacks_mutex);
129
UnregisterCallback(callbackFunction, pUserParam, lock);
130
}
131
132
void Log::UnregisterCallback(CallbackFunctionType callbackFunction, void* pUserParam,
133
const std::unique_lock<std::mutex>& lock)
134
{
135
for (auto iter = s_state.callbacks.begin(); iter != s_state.callbacks.end(); ++iter)
136
{
137
if (iter->Function == callbackFunction && iter->Parameter == pUserParam)
138
{
139
s_state.callbacks.erase(iter);
140
UpdateEffectiveLevel();
141
break;
142
}
143
}
144
}
145
146
void Log::UpdateEffectiveLevel()
147
{
148
s_state.effective_log_level = s_state.callbacks.empty() ? Level::None : s_state.requested_log_level;
149
}
150
151
const std::array<const char*, static_cast<size_t>(Log::Channel::MaxCount)>& Log::GetChannelNames()
152
{
153
return s_log_channel_names;
154
}
155
156
float Log::GetCurrentMessageTime()
157
{
158
return static_cast<float>(Timer::ConvertValueToSeconds(Timer::GetCurrentValue() - s_state.start_timestamp));
159
}
160
161
bool Log::AreConsoleOutputTimestampsEnabled()
162
{
163
return s_state.console_output_timestamps;
164
}
165
166
bool Log::IsConsoleOutputCurrentlyAvailable()
167
{
168
#ifdef _WIN32
169
const HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
170
return (h != NULL && h != INVALID_HANDLE_VALUE);
171
#elif defined(__ANDROID__)
172
return false;
173
#else
174
// standard output isn't really reliable because it could be redirected to a file. check standard input for tty.
175
struct termios attr;
176
return (tcgetattr(STDIN_FILENO, &attr) == 0);
177
#endif
178
}
179
180
bool Log::IsConsoleOutputEnabled()
181
{
182
return s_state.console_output_enabled;
183
}
184
185
bool Log::IsDebugOutputEnabled()
186
{
187
return s_state.debug_output_enabled;
188
}
189
190
Log::Color Log::GetColorForLevel(Level level)
191
{
192
static constexpr const std::array s_level_colours = {
193
Color::Default, // None
194
Color::StrongRed, // Error
195
Color::StrongYellow, // Warning
196
Color::StrongWhite, // Info
197
Color::StrongGreen, // Verbose
198
Color::White, // Dev
199
Color::Green, // Debug
200
Color::Blue, // Trace
201
};
202
203
return s_level_colours[static_cast<size_t>(level)];
204
}
205
206
void Log::ExecuteCallbacks(MessageCategory cat, const char* function_name, std::string_view message)
207
{
208
for (RegisteredCallback& callback : s_state.callbacks)
209
callback.Function(callback.Parameter, cat, function_name, message);
210
}
211
212
ALWAYS_INLINE_RELEASE void Log::FormatLogMessageForDisplay(fmt::memory_buffer& buffer, MessageCategory cat,
213
const char* function_name, std::string_view message,
214
bool timestamp, bool ansi_color_code)
215
{
216
static constexpr const std::array s_ansi_color_codes = {
217
"\033[0m"sv, // default
218
"\033[30m\033[1m"sv, // black
219
"\033[31m"sv, // red
220
"\033[32m"sv, // green
221
"\033[34m"sv, // blue
222
"\033[35m"sv, // magenta
223
"\033[38;5;217m"sv, // orange
224
"\033[36m"sv, // cyan
225
"\033[33m"sv, // yellow
226
"\033[37m"sv, // white
227
"\033[30m\033[1m"sv, // strong black
228
"\033[31m\033[1m"sv, // strong red
229
"\033[32m\033[1m"sv, // strong green
230
"\033[34m\033[1m"sv, // strong blue
231
"\033[35m\033[1m"sv, // strong magenta
232
"\033[38;5;202m"sv, // strong orange
233
"\033[36m\033[1m"sv, // strong cyan
234
"\033[33m\033[1m"sv, // strong yellow
235
"\033[37m\033[1m"sv, // strong white
236
};
237
238
const Level level = UnpackLevel(cat);
239
const Color color = (UnpackColor(cat) == Color::Default) ? GetColorForLevel(level) : UnpackColor(cat);
240
const char* channel_name = GetChannelName(UnpackChannel(cat));
241
242
std::string_view color_start = ansi_color_code ? s_ansi_color_codes[static_cast<size_t>(color)] : ""sv;
243
std::string_view color_end = ansi_color_code ? s_ansi_color_codes[0] : ""sv;
244
245
auto appender = std::back_inserter(buffer);
246
247
if (timestamp)
248
{
249
// find time since start of process
250
const float message_time = Log::GetCurrentMessageTime();
251
252
// have to break it up into lines
253
std::string_view::size_type start = 0;
254
for (;;)
255
{
256
const std::string_view::size_type pos = message.find('\n', start);
257
const std::string_view sub_message =
258
(pos == std::string_view::npos) ? message.substr(start) : message.substr(start, pos - start);
259
const std::string_view end_message = sub_message.ends_with('\n') ? ""sv : "\n"sv;
260
261
if (function_name)
262
{
263
fmt::format_to(appender, "[{:10.4f}] {}{}({}): {}{}{}", message_time, color_start,
264
s_log_level_characters[static_cast<size_t>(level)], function_name, sub_message, color_end,
265
end_message);
266
}
267
else
268
{
269
fmt::format_to(appender, "[{:10.4f}] {}{}/{}: {}{}{}", message_time, color_start,
270
s_log_level_characters[static_cast<size_t>(level)], channel_name, sub_message, color_end,
271
end_message);
272
}
273
274
if (pos != std::string_view::npos)
275
start = pos + 1;
276
else
277
break;
278
}
279
}
280
else
281
{
282
if (function_name)
283
{
284
fmt::format_to(appender, "{}{}({}): {}{}\n", color_start, s_log_level_characters[static_cast<size_t>(level)],
285
function_name, message, color_end);
286
}
287
else
288
{
289
fmt::format_to(appender, "{}{}/{}: {}{}\n", color_start, s_log_level_characters[static_cast<size_t>(level)],
290
channel_name, message, color_end);
291
}
292
}
293
}
294
295
template<typename T>
296
ALWAYS_INLINE_RELEASE void Log::FormatLogMessageAndPrint(MessageCategory cat, const char* function_name,
297
std::string_view message, bool timestamp, bool ansi_color_code,
298
const T& callback)
299
{
300
fmt::memory_buffer buffer;
301
FormatLogMessageForDisplay(buffer, cat, function_name, message, timestamp, ansi_color_code);
302
callback(std::string_view(buffer.data(), buffer.size()));
303
}
304
305
#ifdef _WIN32
306
307
template<typename T>
308
ALWAYS_INLINE_RELEASE void Log::FormatLogMessageAndPrintW(MessageCategory cat, const char* function_name,
309
std::string_view message, bool timestamp,
310
bool ansi_color_code, const T& callback)
311
{
312
fmt::memory_buffer buffer;
313
FormatLogMessageForDisplay(buffer, cat, function_name, message, timestamp, ansi_color_code);
314
315
// Convert to UTF-16 first so unicode characters display correctly. NT is going to do it
316
// anyway...
317
wchar_t wbuf[512];
318
wchar_t* wmessage_buf = wbuf;
319
int wmessage_buflen = static_cast<int>(std::size(wbuf) - 1);
320
if (buffer.size() >= std::size(wbuf))
321
{
322
wmessage_buflen = static_cast<int>(buffer.size());
323
wmessage_buf = static_cast<wchar_t*>(std::malloc((buffer.size() + 1) * sizeof(wchar_t)));
324
}
325
326
wmessage_buflen =
327
MultiByteToWideChar(CP_UTF8, 0, buffer.data(), static_cast<int>(buffer.size()), wmessage_buf, wmessage_buflen);
328
if (wmessage_buflen > 0) [[likely]]
329
{
330
wmessage_buf[wmessage_buflen] = '\0';
331
callback(std::wstring_view(wmessage_buf, wmessage_buflen));
332
}
333
334
if (wmessage_buf != wbuf)
335
std::free(wmessage_buf);
336
}
337
338
static bool EnableVirtualTerminalProcessing(HANDLE hConsole)
339
{
340
DWORD old_mode;
341
if (!GetConsoleMode(hConsole, &old_mode))
342
return false;
343
344
// already enabled?
345
if (old_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
346
return true;
347
348
return SetConsoleMode(hConsole, old_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
349
}
350
351
#endif
352
353
void Log::ConsoleOutputLogCallback(void* pUserParam, MessageCategory cat, const char* function_name,
354
std::string_view message)
355
{
356
if (!s_state.console_output_enabled)
357
return;
358
359
#if defined(_WIN32)
360
FormatLogMessageAndPrintW(
361
cat, function_name, message, s_state.console_output_timestamps, true, [cat](const std::wstring_view& message) {
362
HANDLE hOutput = (UnpackLevel(cat) <= Level::Warning) ? s_state.hConsoleStdErr : s_state.hConsoleStdOut;
363
DWORD chars_written;
364
WriteConsoleW(hOutput, message.data(), static_cast<DWORD>(message.length()), &chars_written, nullptr);
365
});
366
#elif !defined(__ANDROID__)
367
FormatLogMessageAndPrint(
368
cat, function_name, message, s_state.console_output_timestamps, true, [cat](std::string_view message) {
369
const int outputFd = (UnpackLevel(cat) <= Log::Level::Warning) ? STDERR_FILENO : STDOUT_FILENO;
370
write(outputFd, message.data(), message.length());
371
});
372
#endif
373
}
374
375
void Log::DebugOutputLogCallback(void* pUserParam, MessageCategory cat, const char* function_name,
376
std::string_view message)
377
{
378
if (!s_state.debug_output_enabled)
379
return;
380
381
#if defined(_WIN32)
382
FormatLogMessageAndPrintW(cat, function_name, message, false, false,
383
[](const std::wstring_view& message) { OutputDebugStringW(message.data()); });
384
#elif defined(__ANDROID__)
385
if (message.empty())
386
return;
387
388
static constexpr int logPriority[static_cast<size_t>(Level::MaxCount)] = {
389
ANDROID_LOG_INFO, // None
390
ANDROID_LOG_ERROR, // Error
391
ANDROID_LOG_WARN, // Warning
392
ANDROID_LOG_INFO, // Info
393
ANDROID_LOG_INFO, // Verbose
394
ANDROID_LOG_DEBUG, // Dev
395
ANDROID_LOG_DEBUG, // Debug
396
ANDROID_LOG_DEBUG, // Trace
397
};
398
399
__android_log_print(logPriority[static_cast<size_t>(UnpackLevel(cat))], GetChannelName(UnpackChannel(cat)), "%.*s",
400
static_cast<int>(message.length()), message.data());
401
#endif
402
}
403
404
void Log::SetConsoleOutputParams(bool enabled, bool timestamps)
405
{
406
std::unique_lock lock(s_state.callbacks_mutex);
407
408
s_state.console_output_timestamps = timestamps;
409
if (s_state.console_output_enabled == enabled)
410
return;
411
412
s_state.console_output_enabled = enabled;
413
414
#if defined(_WIN32)
415
// On windows, no console is allocated by default on a windows based application
416
static bool console_was_allocated = false;
417
static HANDLE old_stdin = NULL;
418
static HANDLE old_stdout = NULL;
419
static HANDLE old_stderr = NULL;
420
421
if (enabled)
422
{
423
old_stdin = GetStdHandle(STD_INPUT_HANDLE);
424
old_stdout = GetStdHandle(STD_OUTPUT_HANDLE);
425
old_stderr = GetStdHandle(STD_ERROR_HANDLE);
426
427
if (!old_stdout)
428
{
429
// Attach to the parent console if we're running from a command window
430
if (!AttachConsole(ATTACH_PARENT_PROCESS) && !AllocConsole())
431
return;
432
433
s_state.hConsoleStdIn = GetStdHandle(STD_INPUT_HANDLE);
434
s_state.hConsoleStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
435
s_state.hConsoleStdErr = GetStdHandle(STD_ERROR_HANDLE);
436
437
EnableVirtualTerminalProcessing(s_state.hConsoleStdOut);
438
EnableVirtualTerminalProcessing(s_state.hConsoleStdErr);
439
440
std::FILE* fp;
441
freopen_s(&fp, "CONIN$", "r", stdin);
442
freopen_s(&fp, "CONOUT$", "w", stdout);
443
freopen_s(&fp, "CONOUT$", "w", stderr);
444
445
console_was_allocated = true;
446
}
447
else
448
{
449
s_state.hConsoleStdIn = old_stdin;
450
s_state.hConsoleStdOut = old_stdout;
451
s_state.hConsoleStdErr = old_stderr;
452
}
453
}
454
else
455
{
456
if (console_was_allocated)
457
{
458
console_was_allocated = false;
459
460
std::FILE* fp;
461
freopen_s(&fp, "NUL:", "w", stderr);
462
freopen_s(&fp, "NUL:", "w", stdout);
463
freopen_s(&fp, "NUL:", "w", stdin);
464
465
SetStdHandle(STD_ERROR_HANDLE, old_stderr);
466
SetStdHandle(STD_OUTPUT_HANDLE, old_stdout);
467
SetStdHandle(STD_INPUT_HANDLE, old_stdin);
468
469
s_state.hConsoleStdIn = NULL;
470
s_state.hConsoleStdOut = NULL;
471
s_state.hConsoleStdErr = NULL;
472
473
FreeConsole();
474
}
475
}
476
#endif
477
478
if (enabled)
479
RegisterCallback(ConsoleOutputLogCallback, nullptr, lock);
480
else
481
UnregisterCallback(ConsoleOutputLogCallback, nullptr, lock);
482
}
483
484
void Log::SetDebugOutputParams(bool enabled)
485
{
486
std::unique_lock lock(s_state.callbacks_mutex);
487
if (s_state.debug_output_enabled == enabled)
488
return;
489
490
s_state.debug_output_enabled = enabled;
491
if (enabled)
492
RegisterCallback(DebugOutputLogCallback, nullptr, lock);
493
else
494
UnregisterCallback(DebugOutputLogCallback, nullptr, lock);
495
}
496
497
void Log::FileOutputLogCallback(void* pUserParam, MessageCategory cat, const char* function_name,
498
std::string_view message)
499
{
500
if (!s_state.file_output_enabled)
501
return;
502
503
FormatLogMessageAndPrint(cat, function_name, message, s_state.file_output_timestamp, false,
504
[](std::string_view message) {
505
std::fwrite(message.data(), 1, message.size(), s_state.file_handle.get());
506
std::fflush(s_state.file_handle.get());
507
});
508
}
509
510
void Log::SetFileOutputParams(bool enabled, const char* filename, bool timestamps /* = true */)
511
{
512
std::unique_lock lock(s_state.callbacks_mutex);
513
514
s_state.file_output_timestamp = timestamps;
515
516
if (s_state.file_output_enabled == enabled)
517
return;
518
519
if (enabled)
520
{
521
s_state.file_handle = FileSystem::OpenManagedSharedCFile(filename, "wb", FileSystem::FileShareMode::DenyWrite);
522
if (!s_state.file_handle) [[unlikely]]
523
{
524
ExecuteCallbacks(PackCategory(Channel::Log, Level::Error, Color::Default), nullptr,
525
TinyString::from_format("Failed to open log file '{}'", filename));
526
return;
527
}
528
529
RegisterCallback(FileOutputLogCallback, nullptr, lock);
530
}
531
else
532
{
533
UnregisterCallback(FileOutputLogCallback, nullptr, lock);
534
s_state.file_handle.reset();
535
}
536
537
s_state.file_output_enabled = enabled;
538
}
539
540
Log::Level Log::GetLogLevel()
541
{
542
return s_state.effective_log_level;
543
}
544
545
bool Log::IsLogVisible(Level level, Channel channel)
546
{
547
return FilterTest(channel, level);
548
}
549
550
void Log::SetLogLevel(Level level)
551
{
552
std::unique_lock lock(s_state.callbacks_mutex);
553
DebugAssert(level < Level::MaxCount);
554
s_state.requested_log_level = level;
555
UpdateEffectiveLevel();
556
}
557
558
void Log::SetLogChannelEnabled(Channel channel, bool enabled)
559
{
560
std::unique_lock lock(s_state.callbacks_mutex);
561
DebugAssert(channel < Channel::MaxCount);
562
s_state.log_channels_enabled[static_cast<size_t>(channel)] = enabled;
563
}
564
565
const char* Log::GetChannelName(Channel channel)
566
{
567
return s_log_channel_names[static_cast<size_t>(channel)];
568
}
569
570
ALWAYS_INLINE_RELEASE bool Log::FilterTest(Channel channel, Level level)
571
{
572
return (level <= s_state.effective_log_level && s_state.log_channels_enabled[static_cast<size_t>(channel)]);
573
}
574
575
void Log::Write(MessageCategory cat, std::string_view message)
576
{
577
if (!FilterTest(UnpackChannel(cat), UnpackLevel(cat)))
578
return;
579
580
std::unique_lock lock(s_state.callbacks_mutex);
581
ExecuteCallbacks(cat, nullptr, message);
582
}
583
584
void Log::WriteFuncName(MessageCategory cat, const char* function_name, std::string_view message)
585
{
586
if (!FilterTest(UnpackChannel(cat), UnpackLevel(cat)))
587
return;
588
589
std::unique_lock lock(s_state.callbacks_mutex);
590
ExecuteCallbacks(cat, function_name, message);
591
}
592
593
void Log::WriteFmtArgs(MessageCategory cat, fmt::string_view fmt, fmt::format_args args)
594
{
595
if (!FilterTest(UnpackChannel(cat), UnpackLevel(cat)))
596
return;
597
598
fmt::memory_buffer buffer;
599
fmt::vformat_to(std::back_inserter(buffer), fmt, args);
600
601
std::unique_lock lock(s_state.callbacks_mutex);
602
ExecuteCallbacks(cat, nullptr, std::string_view(buffer.data(), buffer.size()));
603
}
604
605
void Log::WriteFuncNameFmtArgs(MessageCategory cat, const char* function_name, fmt::string_view fmt,
606
fmt::format_args args)
607
{
608
if (!FilterTest(UnpackChannel(cat), UnpackLevel(cat)))
609
return;
610
611
fmt::memory_buffer buffer;
612
fmt::vformat_to(std::back_inserter(buffer), fmt, args);
613
614
std::unique_lock lock(s_state.callbacks_mutex);
615
ExecuteCallbacks(cat, function_name, std::string_view(buffer.data(), buffer.size()));
616
}
617
618