The Battle for Wesnoth  1.19.5+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, NULL, _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 std::string format_timespan(const std::chrono::seconds& span)
405 {
406  if(span <= std::chrono::seconds{0}) {
407  return "expired";
408  }
409 
410  auto [days, hours, minutes, seconds] = chrono::deconstruct_duration(span);
411  std::vector<std::string> formatted_values;
412 
413  // TODO C++20: see if we can use the duration stream operators
414  const auto format_time = [&formatted_values](const auto& val, const std::string& suffix) {
415  if(val > std::decay_t<decltype(val)>{0}) {
416  formatted_values.push_back(formatter{} << val.count() << " " << suffix);
417  }
418  };
419 
420  format_time(days, "days");
421  format_time(hours, "hours");
422  format_time(minutes, "minutes");
423  format_time(seconds, "seconds");
424 
425  return utils::join(formatted_values, ", ");
426 }
427 
428 void set_log_sanitize(bool sanitize) {
429  log_sanitization = sanitize;
430 }
431 
432 std::string sanitize_log(const std::string& logstr)
433 {
434  if(!log_sanitization) {
435  return logstr;
436  }
437 
438  std::string str = logstr;
439 
440 #ifdef _WIN32
441  const char* user_name = getenv("USERNAME");
442 #else
443  const char* user_name = getenv("USER");
444 #endif
445 
446  if(user_name != nullptr) {
447  boost::replace_all(str, std::string("/") + user_name + "/", "/USER/");
448  boost::replace_all(str, std::string("\\") + user_name + "\\", "\\USER\\");
449  }
450 
451  return str;
452 }
453 
455  const log_domain& domain,
456  bool show_names,
457  bool do_indent,
458  bool show_timestamps,
459  bool break_strict,
460  bool auto_newline) const
461 {
462  if (severity_ > domain.domain_->second) {
463  return null_ostream;
464  } else {
465  log_in_progress stream = output();
466  if(do_indent) {
467  stream.set_indent(indent);
468  }
469  if (timestamp && show_timestamps) {
470  stream.enable_timestamp();
471  }
472  if (show_names) {
473  stream.set_prefix(formatter() << name_ << ' ' << domain.domain_->first << ": ");
474  }
475  if (!strict_threw_ && severity_ <= strict_level_ && break_strict) {
476  stream | formatter() << "Error (strict mode, strict_level = " << strict_level_ << "): wesnoth reported on channel " << name_ << " " << domain.domain_->first << std::endl;
477  strict_threw_ = true;
478  }
479  stream.set_auto_newline(auto_newline);
480  return stream;
481  }
482 }
483 
484 log_in_progress::log_in_progress(std::ostream& stream)
485  : stream_(stream)
486 {}
487 
489 {
490  std::scoped_lock lock(log_mutex);
491  for(int i = 0; i < indent; ++i)
492  stream_ << " ";
493  if(timestamp_) {
494  auto now = std::chrono::system_clock::now();
495  stream_ << chrono::format_local_timestamp(now); // Truncates precision to seconds
496  if(precise_timestamp) {
497  auto as_seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
498  auto fractional = std::chrono::duration_cast<std::chrono::microseconds>(now - as_seconds);
499  stream_ << "." << std::setw(6) << fractional.count();
500  }
501  }
502  stream_ << prefix_ << sanitize_log(message.str());
503  if(auto_newline_) {
504  stream_ << std::endl;
505  }
506 }
507 
509  indent_ = level;
510 }
511 
513  timestamp_ = true;
514 }
515 
516 void log_in_progress::set_prefix(const std::string& prefix) {
517  prefix_ = prefix;
518 }
519 
520 void log_in_progress::set_auto_newline(bool auto_newline) {
521  auto_newline_ = auto_newline;
522 }
523 
524 void scope_logger::do_log_entry(const std::string& str) noexcept
525 {
526  str_ = str;
527  start_ = std::chrono::steady_clock::now();
528  debug()(domain_, false, true) | formatter() << "{ BEGIN: " << str_;
529  ++indent;
530 }
531 
533 {
534  --indent;
535  auto output = debug()(domain_, false, true);
536  output.set_indent(indent);
537  if(timestamp) output.enable_timestamp();
538  auto now = std::chrono::steady_clock::now();
539  auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - start_);
540  output | formatter() << "} END: " << str_ << " (took " << elapsed.count() << "us)"; // FIXME c++20 stream: operator
541 }
542 
543 std::stringstream& log_to_chat()
544 {
545  static std::stringstream lg;
546  return lg;
547 }
548 
549 } // 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:488
std::string prefix_
Definition: log.hpp:196
std::ostream & stream_
Definition: log.hpp:193
void set_auto_newline(bool enabled)
Definition: log.cpp:520
void set_prefix(const std::string &prefix)
Definition: log.cpp:516
void enable_timestamp()
Definition: log.cpp:512
bool auto_newline_
Definition: log.hpp:197
void set_indent(int level)
Definition: log.cpp:508
log_in_progress(std::ostream &stream)
Definition: log.cpp:484
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:454
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:244
void do_log_entry(const std::string &str) noexcept
Definition: log.cpp:524
std::string str_
Definition: log.hpp:245
std::chrono::steady_clock::time_point start_
Definition: log.hpp:243
void do_log_exit() noexcept
Definition: log.cpp:532
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:1028
#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
std::chrono::duration< int, std::ratio< 86400 > > days
Definition: chrono.hpp:39
constexpr auto deconstruct_duration(const std::chrono::duration< Rep, Period > &span)
Definition: chrono.hpp:76
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:444
bool delete_file(const std::string &filename)
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:325
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:832
bool make_directory(const std::string &dirname)
std::string path
Definition: filesystem.cpp:90
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:428
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 format_timespan(const std::chrono::seconds &span)
TODO: we also have utils::format_timespan, which does something very similar...
Definition: log.cpp:404
std::string sanitize_log(const std::string &logstr)
Definition: log.cpp:432
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:543
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:100
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
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