The Battle for Wesnoth  1.19.13+dev
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"
26 #include "serialization/chrono.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 
45 static 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 
51 namespace {
52 
53 class null_streambuf : public std::streambuf
54 {
55  virtual int overflow(int c) { return std::char_traits< char >::not_eof(c); }
56 public:
57  null_streambuf() {}
58 };
59 
60 #ifdef __ANDROID__
61 class 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  }
71 protected:
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 
87 static std::ostream null_ostream(new null_streambuf);
88 static int indent = 0;
89 static bool timestamp = true;
90 static bool precise_timestamp = false;
91 static std::mutex log_mutex;
92 
93 static bool log_sanitization = true;
94 
95 /** whether the current logs directory is writable */
96 static utils::optional<bool> is_log_dir_writable_ = utils::nullopt;
97 /** alternative stream to write data to */
98 static 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  */
103 static 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 */
116 static 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 */
118 static std::string logs_dir_ = "";
119 
120 namespace lg {
121 
122 std::ostringstream& operator<<(std::ostringstream& oss, const lg::severity severity)
123 {
124  oss << static_cast<int>(severity);
125  return oss;
126 }
127 
128 bool 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 
134 void 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;
160  ERR_LS << "rotate_logs(): failed to delete " << path << "!";
161  }
162  }
163 }
164 
165 std::string unique_log_filename()
166 {
167  std::ostringstream o;
168  const auto now = std::chrono::system_clock::now();
169  randomness::mt_rng rng;
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 
207  is_log_dir_writable_ = true;
208 }
209 
211 {
212  if(logs_dir_ == filesystem::get_logs_dir() || logs_dir_ == "") {
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 
312 utils::optional<bool> log_dir_writable()
313 {
314  return is_log_dir_writable_;
315 }
316 
317 std::string get_log_file_path()
318 {
320 }
321 
323  : old_stream_(output_stream_)
324 {
325  output_stream_ = &stream;
326 }
327 
329 {
331 }
332 
333 typedef std::map<std::string, severity> domain_map;
336 void timestamps(bool t) { timestamp = t; }
337 void precise_timestamps(bool pt) { precise_timestamp = pt; }
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 
363 static 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 
379 bool 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 }
397 bool set_log_domain_severity(const std::string& name, const logger &lg) {
398  return set_log_domain_severity(name, lg.get_severity());
399 }
400 
401 bool 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 
410 std::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 
422 }
423 
425  set_strict_severity(lg.get_severity());
426 }
427 
428 static bool strict_threw_ = false;
429 
430 bool broke_strict() {
431  return strict_threw_;
432 }
433 
434 void set_log_sanitize(bool sanitize) {
435  log_sanitization = sanitize;
436 }
437 
438 std::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 
490 log_in_progress::log_in_progress(std::ostream& stream)
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
502  if(precise_timestamp) {
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 
520  timestamp_ = true;
521 }
522 
523 void log_in_progress::set_prefix(const std::string& prefix) {
524  prefix_ = prefix;
525 }
526 
527 void log_in_progress::set_auto_newline(bool auto_newline) {
528  auto_newline_ = auto_newline;
529 }
530 
531 void 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 
550 std::stringstream& log_to_chat()
551 {
552  static std::stringstream lg;
553  return lg;
554 }
555 
556 } // end namespace lg
double t
Definition: astarsearch.cpp:63
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
bool auto_newline_
Definition: log.hpp:196
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:63
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1032
#define ERR_LS
Definition: log.cpp:46
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
#define LOG_LS
Definition: log.cpp:48
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 std::ostream & output()
Definition: log.cpp:103
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:62
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:463
bool delete_file(const std::string &filename)
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:341
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)
Throws io_exception if an error occurs.
std::string get_logs_dir()
Definition: filesystem.cpp:877
bool make_directory(const std::string &dirname)
std::string path
Definition: filesystem.cpp:106
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:737
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:120
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
constexpr auto filter
Definition: ranges.hpp:38
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:106
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
An exception object used when an IO error occurs.
Definition: filesystem.hpp:67
mock_char c
static map_location::direction n
static map_location::direction s