The Battle for Wesnoth  1.19.7+dev
log.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2004 - 2024
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 <ctime>
34 #include <mutex>
35 #include <iostream>
36 #include <iomanip>
37 
38 #ifdef _WIN32
39 #include <io.h>
40 #endif
41 
42 static lg::log_domain log_setup("logsetup");
43 #define ERR_LS LOG_STREAM(err, log_setup)
44 #define WRN_LS LOG_STREAM(warn, log_setup)
45 #define LOG_LS LOG_STREAM(info, log_setup)
46 #define DBG_LS LOG_STREAM(debug, log_setup)
47 
48 namespace {
49 
50 class null_streambuf : public std::streambuf
51 {
52  virtual int overflow(int c) { return std::char_traits< char >::not_eof(c); }
53 public:
54  null_streambuf() {}
55 };
56 
57 } // end anonymous namespace
58 
59 static std::ostream null_ostream(new null_streambuf);
60 static int indent = 0;
61 static bool timestamp = true;
62 static bool precise_timestamp = false;
63 static std::mutex log_mutex;
64 
65 static bool log_sanitization = true;
66 
67 /** whether the current logs directory is writable */
68 static utils::optional<bool> is_log_dir_writable_ = utils::nullopt;
69 /** alternative stream to write data to */
70 static std::ostream *output_stream_ = nullptr;
71 
72 /**
73  * @return std::cerr if the redirect_output_setter isn't being used, output_stream_ if it is
74  */
75 static std::ostream& output()
76 {
77  if(output_stream_) {
78  return *output_stream_;
79  }
80  return std::cerr;
81 }
82 
83 /** path to the current log file; does not include the extension */
84 static std::string output_file_path_ = "";
85 /** path to the current logs directory; may change after being initially set if a custom userdata directory is given on the command line */
86 static std::string logs_dir_ = "";
87 
88 namespace lg {
89 
90 std::ostringstream& operator<<(std::ostringstream& oss, const lg::severity severity)
91 {
92  oss << static_cast<int>(severity);
93  return oss;
94 }
95 
96 bool is_not_log_file(const std::string& fn)
97 {
98  return !(boost::algorithm::istarts_with(fn, lg::log_file_prefix) &&
99  boost::algorithm::iends_with(fn, lg::log_file_suffix));
100 }
101 
102 void rotate_logs(const std::string& log_dir)
103 {
104  // if logging to file is disabled, don't rotate the logs
105  if(output_file_path_.empty()) {
106  return;
107  }
108 
109  std::vector<std::string> files;
110  filesystem::get_files_in_dir(log_dir, &files);
111 
113 
114  if(files.size() <= lg::max_logs) {
115  return;
116  }
117 
118  // Sorting the file list and deleting all but the last max_logs items
119  // should hopefully be faster than stat'ing every single file for its
120  // time attributes (which aren't very reliable to begin with).
121 
122  std::sort(files.begin(), files.end());
123 
124  for(std::size_t j = 0; j < files.size() - lg::max_logs; ++j) {
125  const std::string path = log_dir + '/' + files[j];
126  LOG_LS << "rotate_logs(): delete " << path;
128  ERR_LS << "rotate_logs(): failed to delete " << path << "!";
129  }
130  }
131 }
132 
133 std::string unique_log_filename()
134 {
135  std::ostringstream o;
136  const std::time_t cur = std::time(nullptr);
137  randomness::mt_rng rng;
138 
140  << std::put_time(std::localtime(&cur), "%Y%m%d-%H%M%S-")
141  << rng.get_next_random();
142 
143  return o.str();
144 }
145 
147 {
148  std::string dummy_log = filesystem::get_logs_dir()+"/dummy.log";
149 
150  // log directory doesn't exist and can't be created
152  is_log_dir_writable_ = false;
153  return;
154  }
155 
156  // can't create and write new log files
157  try {
158  filesystem::write_file(dummy_log, " ");
159  } catch(const filesystem::io_exception&) {
160  is_log_dir_writable_ = false;
161  return;
162  }
163 
164  // confirm that file exists and was written to
165  if(filesystem::file_size(dummy_log) != 1) {
166  is_log_dir_writable_ = false;
167  }
168 
169  // can't delete files - prevents log rotation
170  if(filesystem::file_exists(dummy_log) && !filesystem::delete_file(dummy_log)) {
171  is_log_dir_writable_ = false;
172  return;
173  }
174 
175  is_log_dir_writable_ = true;
176 }
177 
179 {
180  if(logs_dir_ == filesystem::get_logs_dir() || logs_dir_ == "") {
181  return;
182  }
183 
185 
186  if(is_log_dir_writable_.value_or(false)) {
187 #ifdef _WIN32
188  std::string old_path = output_file_path_;
190 
191  // flush and close existing log files, since Windows doesn't allow moving open files
192  std::fflush(stderr);
193  std::cerr.flush();
194  if(!std::freopen("NUL", "a", stderr)) {
195  std::cerr << "Failed to close stderr log file: '" << old_path << "'";
196  // stderr is where basically all output goes through, so if that fails then don't attempt anything else
197  // moving just the stdout log would be pointless
198  return;
199  }
200  std::fflush(stdout);
201  std::cout.flush();
202  if(!std::freopen("NUL", "a", stdout)) {
203  std::cerr << "Failed to close stdout log file: '" << old_path << "'";
204  }
205 
206  // move the .log and .out.log files
207  // stdout and stderr are set to NUL currently so nowhere to send info on failure
208  if(rename((old_path+lg::log_file_suffix).c_str(), (output_file_path_+lg::log_file_suffix).c_str()) == -1) {
209  return;
210  }
211  rename((old_path+lg::out_log_file_suffix).c_str(), (output_file_path_+lg::out_log_file_suffix).c_str());
212 
213  // reopen to log files at new location
214  // stdout and stderr are still NUL if freopen fails, so again nowhere to send info on failure
215  std::fflush(stderr);
216  std::cerr.flush();
217  std::freopen((output_file_path_+lg::log_file_suffix).c_str(), "a", stderr);
218 
219  std::fflush(stdout);
220  std::cout.flush();
221  std::freopen((output_file_path_+lg::out_log_file_suffix).c_str(), "a", stdout);
222 #else
223  std::string old_path = get_log_file_path();
225 
226  // non-Windows can just move the file
227  if(rename(old_path.c_str(), get_log_file_path().c_str()) == -1) {
228  std::cerr << "Failed to rename log file from '" << old_path << "' to '" << output_file_path_ << "'";
229  }
230 #endif
231  }
232 }
233 
235 {
237  // if the log directory is not writable, then don't try to do anything.
238  // if the log directory is writable, then setup logging and rotate the logs.
239  // if the optional isn't set, then logging to file has been disabled, so don't try to do anything
240  if(is_log_dir_writable_.value_or(false)) {
241  // get the log file stream and assign cerr+cout to it
244 
245  // IMPORTANT: apparently redirecting stderr/stdout will also redirect std::cerr/std::cout, but the reverse is not true
246  // redirecting std::cerr/std::cout will *not* redirect stderr/stdout
247 
248  // redirect stderr to file
249  std::fflush(stderr);
250  std::cerr.flush();
251  if(!std::freopen((output_file_path_+lg::log_file_suffix).c_str(), "w", stderr)) {
252  std::cerr << "Failed to redirect stderr to a file!";
253  }
254 
255  // redirect stdout to file
256  // separate handling for Windows since dup2() just... doesn't work for GUI apps there apparently
257  // redirect to a separate file on Windows as well, since otherwise two streams independently writing to the same file can cause weirdness
258 #ifdef _WIN32
259  std::fflush(stdout);
260  std::cout.flush();
261  if(!std::freopen((output_file_path_+lg::out_log_file_suffix).c_str(), "w", stdout)) {
262  std::cerr << "Failed to redirect stdout to a file!";
263  }
264 #else
265  if(dup2(STDERR_FILENO, STDOUT_FILENO) == -1) {
266  std::cerr << "Failed to redirect stdout to a file!";
267  }
268 #endif
269 
270  // make stdout unbuffered - otherwise some output might be lost
271  // in practice shouldn't make much difference either way, given how little output goes through stdout/std::cout
272  if(setvbuf(stdout, nullptr, _IONBF, 2) == -1) {
273  std::cerr << "Failed to set stdout to be unbuffered";
274  }
275 
277  }
278 }
279 
280 utils::optional<bool> log_dir_writable()
281 {
282  return is_log_dir_writable_;
283 }
284 
285 std::string get_log_file_path()
286 {
288 }
289 
291  : old_stream_(output_stream_)
292 {
293  output_stream_ = &stream;
294 }
295 
297 {
299 }
300 
301 typedef std::map<std::string, severity> domain_map;
304 void timestamps(bool t) { timestamp = t; }
305 void precise_timestamps(bool pt) { precise_timestamp = pt; }
306 
308 {
309  static logger lg("error", severity::LG_ERROR);
310  return lg;
311 }
312 
314 {
315  static logger lg("warning", severity::LG_WARN);
316  return lg;
317 }
318 
320 {
321  static logger lg("info", severity::LG_INFO);
322  return lg;
323 }
324 
326 {
327  static logger lg("debug", severity::LG_DEBUG);
328  return lg;
329 }
330 
331 static log_domain dom("general");
332 
334 {
335  return dom;
336 }
337 
339  : domain_(nullptr)
340 {
341  // Indirection to prevent initialization depending on link order.
342  if (!domains) domains = new domain_map;
343  domain_ = &*domains->insert(logd(name, severity)).first;
344  domain_->second = severity;
345 }
346 
347 bool set_log_domain_severity(const std::string& name, severity severity)
348 {
349  std::string::size_type s = name.size();
350  if (name == "all") {
351  for(logd &l : *domains) {
352  l.second = severity;
353  }
354  } else if (s > 2 && name.compare(s - 2, 2, "/*") == 0) {
355  for(logd &l : *domains) {
356  if (l.first.compare(0, s - 1, name, 0, s - 1) == 0)
357  l.second = severity;
358  }
359  } else {
360  domain_map::iterator it = domains->find(name);
361  if (it == domains->end())
362  return false;
363  it->second = severity;
364  }
365  return true;
366 }
367 bool set_log_domain_severity(const std::string& name, const logger &lg) {
368  return set_log_domain_severity(name, lg.get_severity());
369 }
370 
371 bool get_log_domain_severity(const std::string& name, severity &severity)
372 {
373  domain_map::iterator it = domains->find(name);
374  if (it == domains->end())
375  return false;
376  severity = it->second;
377  return true;
378 }
379 
380 std::string list_log_domains(const std::string& filter)
381 {
382  std::ostringstream res;
383  for(logd &l : *domains) {
384  if(l.first.find(filter) != std::string::npos)
385  res << l.first << "\n";
386  }
387  return res.str();
388 }
389 
392 }
393 
395  set_strict_severity(lg.get_severity());
396 }
397 
398 static bool strict_threw_ = false;
399 
400 bool broke_strict() {
401  return strict_threw_;
402 }
403 
404 void set_log_sanitize(bool sanitize) {
405  log_sanitization = sanitize;
406 }
407 
408 std::string sanitize_log(const std::string& logstr)
409 {
410  if(!log_sanitization) {
411  return logstr;
412  }
413 
414  std::string str = logstr;
415 
416 #ifdef _WIN32
417  const char* user_name = getenv("USERNAME");
418 #else
419  const char* user_name = getenv("USER");
420 #endif
421 
422  if(user_name != nullptr) {
423  boost::replace_all(str, std::string("/") + user_name + "/", "/USER/");
424  boost::replace_all(str, std::string("\\") + user_name + "\\", "\\USER\\");
425  }
426 
427  return str;
428 }
429 
431  const log_domain& domain,
432  bool show_names,
433  bool do_indent,
434  bool show_timestamps,
435  bool break_strict,
436  bool auto_newline) const
437 {
438  if (severity_ > domain.domain_->second) {
439  return null_ostream;
440  } else {
441  log_in_progress stream = output();
442  if(do_indent) {
443  stream.set_indent(indent);
444  }
445  if (timestamp && show_timestamps) {
446  stream.enable_timestamp();
447  }
448  if (show_names) {
449  stream.set_prefix(formatter() << name_ << ' ' << domain.domain_->first << ": ");
450  }
451  if (!strict_threw_ && severity_ <= strict_level_ && break_strict) {
452  stream | formatter() << "Error (strict mode, strict_level = " << strict_level_ << "): wesnoth reported on channel " << name_ << " " << domain.domain_->first << std::endl;
453  strict_threw_ = true;
454  }
455  stream.set_auto_newline(auto_newline);
456  return stream;
457  }
458 }
459 
460 log_in_progress::log_in_progress(std::ostream& stream)
461  : stream_(stream)
462 {}
463 
465 {
466  std::scoped_lock lock(log_mutex);
467  for(int i = 0; i < indent; ++i)
468  stream_ << " ";
469  if(timestamp_) {
470  auto now = std::chrono::system_clock::now();
471  stream_ << chrono::format_local_timestamp(now); // Truncates precision to seconds
472  if(precise_timestamp) {
473  auto as_seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
474  auto fractional = std::chrono::duration_cast<std::chrono::microseconds>(now - as_seconds);
475  stream_ << "." << std::setw(6) << fractional.count();
476  }
477  stream_ << " ";
478  }
479  stream_ << prefix_ << sanitize_log(message.str());
480  if(auto_newline_) {
481  stream_ << std::endl;
482  }
483 }
484 
486  indent_ = level;
487 }
488 
490  timestamp_ = true;
491 }
492 
493 void log_in_progress::set_prefix(const std::string& prefix) {
494  prefix_ = prefix;
495 }
496 
497 void log_in_progress::set_auto_newline(bool auto_newline) {
498  auto_newline_ = auto_newline;
499 }
500 
501 void scope_logger::do_log_entry(const std::string& str) noexcept
502 {
503  str_ = str;
504  start_ = std::chrono::steady_clock::now();
505  debug()(domain_, false, true) | formatter() << "{ BEGIN: " << str_;
506  ++indent;
507 }
508 
510 {
511  --indent;
512  auto output = debug()(domain_, false, true);
513  output.set_indent(indent);
514  if(timestamp) output.enable_timestamp();
515  auto now = std::chrono::steady_clock::now();
516  auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - start_);
517  output | formatter() << "} END: " << str_ << " (took " << elapsed.count() << "us)"; // FIXME c++20 stream: operator
518 }
519 
520 std::stringstream& log_to_chat()
521 {
522  static std::stringstream lg;
523  return lg;
524 }
525 
526 } // end namespace lg
double t
Definition: astarsearch.cpp:63
std::ostringstream wrapper.
Definition: formatter.hpp:40
logd * domain_
Definition: log.hpp:126
log_domain(char const *name, severity severity=severity::LG_WARN)
Definition: log.cpp:338
void operator|(formatter &&message)
Definition: log.cpp:464
std::string prefix_
Definition: log.hpp:196
std::ostream & stream_
Definition: log.hpp:193
void set_auto_newline(bool enabled)
Definition: log.cpp:497
void set_prefix(const std::string &prefix)
Definition: log.cpp:493
void enable_timestamp()
Definition: log.cpp:489
bool auto_newline_
Definition: log.hpp:197
void set_indent(int level)
Definition: log.cpp:485
log_in_progress(std::ostream &stream)
Definition: log.cpp:460
char const * name_
Definition: log.hpp:208
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:430
severity severity_
Definition: log.hpp:209
std::ostream * old_stream_
The previously set redirection.
Definition: log.hpp:118
redirect_output_setter(std::ostream &stream)
Constructor.
Definition: log.cpp:290
const log_domain & domain_
Definition: log.hpp:242
void do_log_entry(const std::string &str) noexcept
Definition: log.cpp:501
std::string str_
Definition: log.hpp:243
std::chrono::steady_clock::time_point start_
Definition: log.hpp:241
void do_log_exit() noexcept
Definition: log.cpp:509
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:1029
#define ERR_LS
Definition: log.cpp:43
static bool precise_timestamp
Definition: log.cpp:62
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:86
static utils::optional< bool > is_log_dir_writable_
whether the current logs directory is writable
Definition: log.cpp:68
#define LOG_LS
Definition: log.cpp:45
static std::string output_file_path_
path to the current log file; does not include the extension
Definition: log.cpp:84
static std::ostream null_ostream(new null_streambuf)
static std::mutex log_mutex
Definition: log.cpp:63
static int indent
Definition: log.cpp:60
static std::ostream & output()
Definition: log.cpp:75
static bool log_sanitization
Definition: log.cpp:65
static std::ostream * output_stream_
alternative stream to write data to
Definition: log.cpp:70
static lg::log_domain log_setup("logsetup")
static bool timestamp
Definition: log.cpp:61
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:61
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:445
bool delete_file(const std::string &filename)
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:326
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:833
bool make_directory(const std::string &dirname)
std::string path
Definition: filesystem.cpp:91
Definition: pump.hpp:41
bool get_log_domain_severity(const std::string &name, severity &severity)
Definition: log.cpp:371
logger & err()
Definition: log.cpp:307
severity
Definition: log.hpp:83
std::string list_log_domains(const std::string &filter)
Definition: log.cpp:380
log_domain & general()
Definition: log.cpp:333
bool broke_strict()
Definition: log.cpp:400
logger & debug()
Definition: log.cpp:325
const std::string out_log_file_suffix
Definition: log.hpp:71
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:102
std::string unique_log_filename()
Generate a unique file name using the current timestamp and a randomly generated number.
Definition: log.cpp:133
void set_log_to_file()
Do the initial redirection to a log file if the logs directory is writable.
Definition: log.cpp:234
static log_domain dom("general")
void set_log_sanitize(bool sanitize)
toggle log sanitization
Definition: log.cpp:404
utils::optional< bool > log_dir_writable()
Returns the result set by check_log_dir_writable().
Definition: log.cpp:280
void move_log_file()
Move the log file to another directory.
Definition: log.cpp:178
logger & warn()
Definition: log.cpp:313
void timestamps(bool t)
Definition: log.cpp:304
std::string sanitize_log(const std::string &logstr)
Definition: log.cpp:408
static severity strict_level_
Definition: log.cpp:303
const unsigned max_logs
Definition: log.hpp:76
void check_log_dir_writable()
Checks that a dummy file can be written to and deleted from the logs directory.
Definition: log.cpp:146
std::stringstream & log_to_chat()
Use this to show WML errors in the ingame chat.
Definition: log.cpp:520
const std::string log_file_prefix
Definition: log.hpp:68
std::map< std::string, severity > domain_map
Definition: log.cpp:301
static domain_map * domains
Definition: log.cpp:302
std::pair< const std::string, severity > logd
Definition: log.hpp:121
static bool strict_threw_
Definition: log.cpp:398
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:96
void precise_timestamps(bool pt)
Definition: log.cpp:305
logger & info()
Definition: log.cpp:319
bool set_log_domain_severity(const std::string &name, severity severity)
Definition: log.cpp:347
void set_strict_severity(severity severity)
Definition: log.cpp:390
std::string get_log_file_path()
Definition: log.cpp:285
const std::string log_file_suffix
Definition: log.hpp:69
std::ostringstream & operator<<(std::ostringstream &oss, const lg::severity severity)
Definition: log.cpp:90
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 s