The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
log.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2004 - 2025
3 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
4 Copyright (C) 2003 by David White <dave@whitevine.net>
5 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY.
13
14 See the COPYING file for more details.
15*/
16
17/**
18 * @file
19 * Standard logging facilities (implementation).
20 * See also the command line switches --logdomains and --log-@<level@>="domain".
21 */
22
23#include "log.hpp"
24#include "filesystem.hpp"
25#include "mt_rng.hpp"
28#include "utils/general.hpp"
29
30#include <boost/algorithm/string.hpp>
31
32#include <map>
33#include <mutex>
34#include <iostream>
35#include <iomanip>
36
37#ifdef __ANDROID__
38#include <android/log.h>
39#endif
40
41#ifdef _WIN32
42#include <io.h>
43#endif
44
45static lg::log_domain log_setup("logsetup");
46#define ERR_LS LOG_STREAM(err, log_setup)
47#define WRN_LS LOG_STREAM(warn, log_setup)
48#define LOG_LS LOG_STREAM(info, log_setup)
49#define DBG_LS LOG_STREAM(debug, log_setup)
50
51namespace {
52
53class null_streambuf : public std::streambuf
54{
55 virtual int overflow(int c) { return std::char_traits< char >::not_eof(c); }
56public:
57 null_streambuf() {}
58};
59
60#ifdef __ANDROID__
61class android_log_buf : public std::streambuf
62{
63 std::string output;
64 void write() {
65 auto newline { output.find_first_of("\n") };
66 if(newline != std::string::npos) {
67 __android_log_write(ANDROID_LOG_INFO, "wesnoth", output.substr(0, newline).c_str());
68 output = output.substr(newline+1);
69 }
70 }
71protected:
72 virtual std::streamsize xsputn(const char_type* s, std::streamsize n) override {
73 output.append(s, n);
74 write();
75 return n;
76 }
77 virtual int_type overflow(int_type ch) override {
78 output.push_back(ch);
79 write();
80 return 1;
81 }
82};
83#endif
84
85} // end anonymous namespace
86
87static std::ostream null_ostream(new null_streambuf);
88static int indent = 0;
89static bool timestamp = true;
90static bool precise_timestamp = false;
91static std::mutex log_mutex;
92
93static bool log_sanitization = true;
94
95/** whether the current logs directory is writable */
96static utils::optional<bool> is_log_dir_writable_ = utils::nullopt;
97/** alternative stream to write data to */
98static std::ostream *output_stream_ = nullptr;
99
100/**
101 * @return std::cerr if the redirect_output_setter isn't being used, output_stream_ if it is
102 */
103static std::ostream& output()
104{
105#ifdef __ANDROID__
106 static std::ostream android_ostream(new android_log_buf);
107 return android_ostream;
108#endif
109 if(output_stream_) {
110 return *output_stream_;
111 }
112 return std::cerr;
113}
114
115/** path to the current log file; does not include the extension */
116static std::string output_file_path_ = "";
117/** path to the current logs directory; may change after being initially set if a custom userdata directory is given on the command line */
118static std::string logs_dir_ = "";
119
120namespace lg {
121
122std::ostringstream& operator<<(std::ostringstream& oss, const lg::severity severity)
123{
124 oss << static_cast<int>(severity);
125 return oss;
126}
127
128bool is_not_log_file(const std::string& fn)
129{
130 return !(boost::algorithm::istarts_with(fn, lg::log_file_prefix) &&
131 boost::algorithm::iends_with(fn, lg::log_file_suffix));
132}
133
134void rotate_logs(const std::string& log_dir)
135{
136 // if logging to file is disabled, don't rotate the logs
137 if(output_file_path_.empty()) {
138 return;
139 }
140
141 std::vector<std::string> files;
142 filesystem::get_files_in_dir(log_dir, &files);
143
145
146 if(files.size() <= lg::max_logs) {
147 return;
148 }
149
150 // Sorting the file list and deleting all but the last max_logs items
151 // should hopefully be faster than stat'ing every single file for its
152 // time attributes (which aren't very reliable to begin with).
153
154 std::sort(files.begin(), files.end());
155
156 for(std::size_t j = 0; j < files.size() - lg::max_logs; ++j) {
157 const std::string path = log_dir + '/' + files[j];
158 LOG_LS << "rotate_logs(): delete " << path;
159 if(!filesystem::delete_file(path)) {
160 ERR_LS << "rotate_logs(): failed to delete " << path << "!";
161 }
162 }
163}
164
166{
167 std::ostringstream o;
168 const auto now = std::chrono::system_clock::now();
170
172 << chrono::format_local_timestamp(now, "%Y%m%d-%H%M%S-")
173 << rng.get_next_random();
174
175 return o.str();
176}
177
179{
180 std::string dummy_log = filesystem::get_logs_dir()+"/dummy.log";
181
182 // log directory doesn't exist and can't be created
184 is_log_dir_writable_ = false;
185 return;
186 }
187
188 // can't create and write new log files
189 try {
190 filesystem::write_file(dummy_log, " ");
191 } catch(const filesystem::io_exception&) {
192 is_log_dir_writable_ = false;
193 return;
194 }
195
196 // confirm that file exists and was written to
197 if(filesystem::file_size(dummy_log) != 1) {
198 is_log_dir_writable_ = false;
199 }
200
201 // can't delete files - prevents log rotation
202 if(filesystem::file_exists(dummy_log) && !filesystem::delete_file(dummy_log)) {
203 is_log_dir_writable_ = false;
204 return;
205 }
206
208}
209
211{
213 return;
214 }
215
217
218 if(is_log_dir_writable_.value_or(false)) {
219#ifdef _WIN32
220 std::string old_path = output_file_path_;
222
223 // flush and close existing log files, since Windows doesn't allow moving open files
224 std::fflush(stderr);
225 std::cerr.flush();
226 if(!std::freopen("NUL", "a", stderr)) {
227 std::cerr << "Failed to close stderr log file: '" << old_path << "'";
228 // stderr is where basically all output goes through, so if that fails then don't attempt anything else
229 // moving just the stdout log would be pointless
230 return;
231 }
232 std::fflush(stdout);
233 std::cout.flush();
234 if(!std::freopen("NUL", "a", stdout)) {
235 std::cerr << "Failed to close stdout log file: '" << old_path << "'";
236 }
237
238 // move the .log and .out.log files
239 // stdout and stderr are set to NUL currently so nowhere to send info on failure
240 if(rename((old_path+lg::log_file_suffix).c_str(), (output_file_path_+lg::log_file_suffix).c_str()) == -1) {
241 return;
242 }
243 rename((old_path+lg::out_log_file_suffix).c_str(), (output_file_path_+lg::out_log_file_suffix).c_str());
244
245 // reopen to log files at new location
246 // stdout and stderr are still NUL if freopen fails, so again nowhere to send info on failure
247 std::fflush(stderr);
248 std::cerr.flush();
249 std::freopen((output_file_path_+lg::log_file_suffix).c_str(), "a", stderr);
250
251 std::fflush(stdout);
252 std::cout.flush();
253 std::freopen((output_file_path_+lg::out_log_file_suffix).c_str(), "a", stdout);
254#else
255 std::string old_path = get_log_file_path();
257
258 // non-Windows can just move the file
259 if(rename(old_path.c_str(), get_log_file_path().c_str()) == -1) {
260 std::cerr << "Failed to rename log file from '" << old_path << "' to '" << output_file_path_ << "'";
261 }
262#endif
263 }
264}
265
267{
269 // if the log directory is not writable, then don't try to do anything.
270 // if the log directory is writable, then setup logging and rotate the logs.
271 // if the optional isn't set, then logging to file has been disabled, so don't try to do anything
272 if(is_log_dir_writable_.value_or(false)) {
273 // get the log file stream and assign cerr+cout to it
276
277 // IMPORTANT: apparently redirecting stderr/stdout will also redirect std::cerr/std::cout, but the reverse is not true
278 // redirecting std::cerr/std::cout will *not* redirect stderr/stdout
279
280 // redirect stderr to file
281 std::fflush(stderr);
282 std::cerr.flush();
283 if(!std::freopen((output_file_path_+lg::log_file_suffix).c_str(), "w", stderr)) {
284 std::cerr << "Failed to redirect stderr to a file!";
285 }
286
287 // redirect stdout to file
288 // separate handling for Windows since dup2() just... doesn't work for GUI apps there apparently
289 // redirect to a separate file on Windows as well, since otherwise two streams independently writing to the same file can cause weirdness
290#ifdef _WIN32
291 std::fflush(stdout);
292 std::cout.flush();
293 if(!std::freopen((output_file_path_+lg::out_log_file_suffix).c_str(), "w", stdout)) {
294 std::cerr << "Failed to redirect stdout to a file!";
295 }
296#else
297 if(dup2(STDERR_FILENO, STDOUT_FILENO) == -1) {
298 std::cerr << "Failed to redirect stdout to a file!";
299 }
300#endif
301
302 // make stdout unbuffered - otherwise some output might be lost
303 // in practice shouldn't make much difference either way, given how little output goes through stdout/std::cout
304 if(setvbuf(stdout, nullptr, _IONBF, 2) == -1) {
305 std::cerr << "Failed to set stdout to be unbuffered";
306 }
307
309 }
310}
311
312utils::optional<bool> log_dir_writable()
313{
315}
316
317std::string get_log_file_path()
318{
320}
321
323 : old_stream_(output_stream_)
324{
325 output_stream_ = &stream;
326}
327
332
333typedef std::map<std::string, severity> domain_map;
336void timestamps(bool t) { timestamp = t; }
338
340{
341 static logger lg("error", severity::LG_ERROR);
342 return lg;
343}
344
346{
347 static logger lg("warning", severity::LG_WARN);
348 return lg;
349}
350
352{
353 static logger lg("info", severity::LG_INFO);
354 return lg;
355}
356
358{
359 static logger lg("debug", severity::LG_DEBUG);
360 return lg;
361}
362
363static log_domain dom("general");
364
366{
367 return dom;
368}
369
371 : domain_(nullptr)
372{
373 // Indirection to prevent initialization depending on link order.
374 if (!domains) domains = new domain_map;
375 domain_ = &*domains->insert(logd(name, severity)).first;
376 domain_->second = severity;
377}
378
379bool set_log_domain_severity(const std::string& name, severity severity)
380{
381 if (name == "all") {
382 for(logd &l : *domains) {
383 l.second = severity;
384 }
385 return true;
386 } else {
387 bool any_matched = false;
388 for (logd &l : *domains) {
389 if (utils::wildcard_string_match(l.first, name)) {
390 l.second = severity;
391 any_matched = true;
392 }
393 }
394 return any_matched;
395 }
396}
397bool set_log_domain_severity(const std::string& name, const logger &lg) {
398 return set_log_domain_severity(name, lg.get_severity());
399}
400
401bool get_log_domain_severity(const std::string& name, severity &severity)
402{
403 domain_map::iterator it = domains->find(name);
404 if (it == domains->end())
405 return false;
406 severity = it->second;
407 return true;
408}
409
410std::string list_log_domains(const std::string& filter)
411{
412 std::ostringstream res;
413 for(logd &l : *domains) {
414 if(l.first.find(filter) != std::string::npos)
415 res << l.first << "\n";
416 }
417 return res.str();
418}
419
423
425 set_strict_severity(lg.get_severity());
426}
427
428static bool strict_threw_ = false;
429
431 return strict_threw_;
432}
433
434void set_log_sanitize(bool sanitize) {
435 log_sanitization = sanitize;
436}
437
438std::string sanitize_log(const std::string& logstr)
439{
440 if(!log_sanitization) {
441 return logstr;
442 }
443
444 std::string str = logstr;
445
446#ifdef _WIN32
447 const char* user_name = getenv("USERNAME");
448#else
449 const char* user_name = getenv("USER");
450#endif
451
452 if(user_name != nullptr) {
453 boost::replace_all(str, std::string("/") + user_name + "/", "/USER/");
454 boost::replace_all(str, std::string("\\") + user_name + "\\", "\\USER\\");
455 }
456
457 return str;
458}
459
461 const log_domain& domain,
462 bool show_names,
463 bool do_indent,
464 bool show_timestamps,
465 bool break_strict,
466 bool auto_newline) const
467{
468 if (severity_ > domain.domain_->second) {
469 return null_ostream;
470 } else {
471 log_in_progress stream = output();
472 if(do_indent) {
473 stream.set_indent(indent);
474 }
475 if (timestamp && show_timestamps) {
476 stream.enable_timestamp();
477 }
478 if (show_names) {
479 stream.set_prefix(formatter() << name_ << ' ' << domain.domain_->first << ": ");
480 }
481 if (!strict_threw_ && severity_ <= strict_level_ && break_strict) {
482 stream | formatter() << "Error (strict mode, strict_level = " << strict_level_ << "): wesnoth reported on channel " << name_ << " " << domain.domain_->first << std::endl;
483 strict_threw_ = true;
484 }
485 stream.set_auto_newline(auto_newline);
486 return stream;
487 }
488}
489
491 : stream_(stream)
492{}
493
495{
496 std::scoped_lock lock(log_mutex);
497 for(int i = 0; i < indent; ++i)
498 stream_ << " ";
499 if(timestamp_) {
500 auto now = std::chrono::system_clock::now();
501 stream_ << chrono::format_local_timestamp(now, "%Y%m%d %H:%M:%S"); // Truncates precision to seconds
503 auto as_seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
504 auto fractional = std::chrono::duration_cast<std::chrono::microseconds>(now - as_seconds);
505 stream_ << "." << std::setw(6) << fractional.count();
506 }
507 stream_ << " ";
508 }
509 stream_ << prefix_ << sanitize_log(message.str());
510 if(auto_newline_) {
511 stream_ << std::endl;
512 }
513}
514
516 indent_ = level;
517}
518
522
523void log_in_progress::set_prefix(const std::string& prefix) {
524 prefix_ = prefix;
525}
526
527void log_in_progress::set_auto_newline(bool auto_newline) {
528 auto_newline_ = auto_newline;
529}
530
531void scope_logger::do_log_entry(const std::string& str) noexcept
532{
533 str_ = str;
534 start_ = std::chrono::steady_clock::now();
535 debug()(domain_, false, true) | formatter() << "{ BEGIN: " << str_;
536 ++indent;
537}
538
540{
541 --indent;
542 auto output = debug()(domain_, false, true);
543 output.set_indent(indent);
544 if(timestamp) output.enable_timestamp();
545 auto now = std::chrono::steady_clock::now();
546 auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - start_);
547 output | formatter() << "} END: " << str_ << " (took " << elapsed.count() << "us)"; // FIXME c++20 stream: operator
548}
549
550std::stringstream& log_to_chat()
551{
552 static std::stringstream lg;
553 return lg;
554}
555
556} // end namespace lg
double t
std::ostringstream wrapper.
Definition formatter.hpp:40
logd * domain_
Definition log.hpp:125
log_domain(char const *name, severity severity=severity::LG_WARN)
Definition log.cpp:370
std::string prefix_
Definition log.hpp:195
std::ostream & stream_
Definition log.hpp:192
void set_auto_newline(bool enabled)
Definition log.cpp:527
void set_prefix(const std::string &prefix)
Definition log.cpp:523
void enable_timestamp()
Definition log.cpp:519
void operator|(const formatter &message)
Definition log.cpp:494
void set_indent(int level)
Definition log.cpp:515
log_in_progress(std::ostream &stream)
Definition log.cpp:490
char const * name_
Definition log.hpp:207
log_in_progress operator()(const log_domain &domain, bool show_names=true, bool do_indent=false, bool show_timestamps=true, bool break_strict=true, bool auto_newline=true) const
Definition log.cpp:460
severity severity_
Definition log.hpp:208
std::ostream * old_stream_
The previously set redirection.
Definition log.hpp:117
redirect_output_setter(std::ostream &stream)
Constructor.
Definition log.cpp:322
const log_domain & domain_
Definition log.hpp:241
void do_log_entry(const std::string &str) noexcept
Definition log.cpp:531
std::string str_
Definition log.hpp:242
std::chrono::steady_clock::time_point start_
Definition log.hpp:240
void do_log_exit() noexcept
Definition log.cpp:539
uint32_t get_next_random()
Get a new random number.
Definition mt_rng.cpp:62
Declarations for File-IO.
#define ERR_LS
#define LOG_LS
static std::ostream & output()
Definition log.cpp:103
static bool precise_timestamp
Definition log.cpp:90
static std::string logs_dir_
path to the current logs directory; may change after being initially set if a custom userdata directo...
Definition log.cpp:118
static utils::optional< bool > is_log_dir_writable_
whether the current logs directory is writable
Definition log.cpp:96
static std::string output_file_path_
path to the current log file; does not include the extension
Definition log.cpp:116
static std::ostream null_ostream(new null_streambuf)
static std::mutex log_mutex
Definition log.cpp:91
static int indent
Definition log.cpp:88
static bool log_sanitization
Definition log.cpp:93
static std::ostream * output_stream_
alternative stream to write data to
Definition log.cpp:98
static lg::log_domain log_setup("logsetup")
static bool timestamp
Definition log.cpp:89
Standard logging facilities (interface).
auto format_local_timestamp(const std::chrono::system_clock::time_point &time, std::string_view format="%F %T")
Definition chrono.hpp:72
bool file_exists(const std::string &name)
Returns true if a file or directory with such name already exists.
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs=nullptr, name_mode mode=name_mode::FILE_NAME_ONLY, filter_mode filter=filter_mode::NO_FILTER, reorder_mode reorder=reorder_mode::DONT_REORDER, file_tree_checksum *checksum=nullptr)
Get a list of all files and/or directories in a given directory.
bool delete_file(const std::string &filename)
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn't exist.
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode=std::ios_base::binary)
Throws io_exception if an error occurs.
std::string get_logs_dir()
bool make_directory(const std::string &dirname)
Definition pump.hpp:41
bool get_log_domain_severity(const std::string &name, severity &severity)
Definition log.cpp:401
logger & err()
Definition log.cpp:339
severity
Definition log.hpp:82
std::string list_log_domains(const std::string &filter)
Definition log.cpp:410
log_domain & general()
Definition log.cpp:365
bool broke_strict()
Definition log.cpp:430
logger & debug()
Definition log.cpp:357
const std::string out_log_file_suffix
Definition log.hpp:70
void rotate_logs(const std::string &log_dir)
Check how many log files exist and delete the oldest when there's too many.
Definition log.cpp:134
std::string unique_log_filename()
Generate a unique file name using the current timestamp and a randomly generated number.
Definition log.cpp:165
void set_log_to_file()
Do the initial redirection to a log file if the logs directory is writable.
Definition log.cpp:266
static log_domain dom("general")
void set_log_sanitize(bool sanitize)
toggle log sanitization
Definition log.cpp:434
utils::optional< bool > log_dir_writable()
Returns the result set by check_log_dir_writable().
Definition log.cpp:312
void move_log_file()
Move the log file to another directory.
Definition log.cpp:210
logger & warn()
Definition log.cpp:345
void timestamps(bool t)
Definition log.cpp:336
std::string sanitize_log(const std::string &logstr)
Definition log.cpp:438
static severity strict_level_
Definition log.cpp:335
const unsigned max_logs
Definition log.hpp:75
void check_log_dir_writable()
Checks that a dummy file can be written to and deleted from the logs directory.
Definition log.cpp:178
std::stringstream & log_to_chat()
Use this to show WML errors in the ingame chat.
Definition log.cpp:550
const std::string log_file_prefix
Definition log.hpp:67
std::map< std::string, severity > domain_map
Definition log.cpp:333
static domain_map * domains
Definition log.cpp:334
std::pair< const std::string, severity > logd
Definition log.hpp:122
static bool strict_threw_
Definition log.cpp:428
bool is_not_log_file(const std::string &fn)
Use the defined prefix and suffix to determine if a filename is a log file.
Definition log.cpp:128
void precise_timestamps(bool pt)
Definition log.cpp:337
logger & info()
Definition log.cpp:351
bool set_log_domain_severity(const std::string &name, severity severity)
Definition log.cpp:379
void set_strict_severity(severity severity)
Definition log.cpp:420
std::string get_log_file_path()
Definition log.cpp:317
const std::string log_file_suffix
Definition log.hpp:68
std::ostringstream & operator<<(std::ostringstream &oss, const lg::severity severity)
Definition log.cpp:122
bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
Performs pattern matching with wildcards.
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition general.hpp:108
An exception object used when an IO error occurs.
static map_location::direction n
static map_location::direction s
#define i
#define c