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