The Battle for Wesnoth  1.17.0-dev
log_windows.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2021
3  by Iris Morelle <shadowm2006@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 // For some reason, it became necessary to include this before the header
17 // after switching to c++11
18 #include <cstdio>
19 
20 #include "log_windows.hpp"
21 
22 #include "filesystem.hpp"
23 #include "libc_error.hpp"
24 #include "log.hpp"
26 
27 #include <ctime>
28 #include <iomanip>
29 
30 #include <boost/algorithm/string/predicate.hpp>
31 
32 #ifndef UNICODE
33 #define UNICODE
34 #endif
35 
36 #define WIN32_LEAN_AND_MEAN
37 
38 #include <windows.h>
39 
40 static lg::log_domain log_setup("logsetup");
41 #define ERR_LS LOG_STREAM(err, log_setup)
42 #define WRN_LS LOG_STREAM(warn, log_setup)
43 #define LOG_LS LOG_STREAM(info, log_setup)
44 #define DBG_LS LOG_STREAM(debug, log_setup)
45 
46 namespace filesystem
47 {
48 
49 std::string get_logs_dir()
50 {
51  return filesystem::get_user_data_dir() + "/logs";
52 }
53 
54 }
55 
56 namespace lg
57 {
58 
59 namespace
60 {
61 
62 // Prefix and extension for log files. This is used both to generate the unique
63 // log file name during startup and to find old files to delete.
64 const std::string log_file_prefix = "wesnoth-";
65 const std::string log_file_suffix = ".log";
66 
67 // Maximum number of older log files to keep intact. Other files are deleted.
68 // Note that this count does not include the current log file!
69 const unsigned max_logs = 8;
70 
71 /** Helper function for rotate_logs. */
72 bool is_not_log_file(const std::string& fn)
73 {
74  return !(boost::algorithm::istarts_with(fn, log_file_prefix) &&
75  boost::algorithm::iends_with(fn, log_file_suffix));
76 }
77 
78 /**
79  * Deletes old log files from the log directory.
80  */
81 void rotate_logs(const std::string& log_dir)
82 {
83  std::vector<std::string> files;
84  filesystem::get_files_in_dir(log_dir, &files);
85 
86  files.erase(std::remove_if(files.begin(), files.end(), is_not_log_file), files.end());
87 
88  if(files.size() <= max_logs) {
89  return;
90  }
91 
92  // Sorting the file list and deleting all but the last max_logs items
93  // should hopefully be faster than stat'ing every single file for its
94  // time attributes (which aren't very reliable to begin with.
95 
96  std::sort(files.begin(), files.end());
97 
98  for(std::size_t j = 0; j < files.size() - max_logs; ++j) {
99  const std::string path = log_dir + '/' + files[j];
100  LOG_LS << "rotate_logs(): delete " << path << '\n';
101  if(!filesystem::delete_file(path)) {
102  WRN_LS << "rotate_logs(): failed to delete " << path << "!\n";
103  }
104  }
105 }
106 
107 /**
108  * Generates a "unique" log file name.
109  *
110  * This is really not guaranteed to be unique, but it's close enough, since
111  * the odds of having multiple Wesnoth instances spawn with the same PID within
112  * a second span are close to zero.
113  *
114  * The file name includes a timestamp in order to satisfy the requirements of
115  * the rotate_logs logic.
116  */
117 std::string unique_log_filename()
118 {
119  std::ostringstream o;
120 
121  o << log_file_prefix;
122 
123  const std::time_t cur = std::time(nullptr);
124  o << std::put_time(std::localtime(&cur), "%Y%m%d-%H%M%S-");
125 
126  o << GetCurrentProcessId() << log_file_suffix;
127 
128  return o.str();
129 }
130 
131 /**
132  * Returns the path to a system-defined temporary files dir.
133  */
134 std::string temp_dir()
135 {
136  wchar_t tmpdir[MAX_PATH + 1];
137 
138  if(GetTempPath(MAX_PATH + 1, tmpdir) == 0) {
139  return ".";
140  }
141 
142  return unicode_cast<std::string>(std::wstring(tmpdir));
143 }
144 
145 /**
146  * Display an alert box to warn about log initialization errors, and exit.
147  */
148 void log_init_panic(const std::string& msg)
149 {
150  ERR_LS << "Log initialization panic call: " << msg << '\n';
151 
152  const std::string full_msg = msg + "\n\n" + "This may indicate an issue with your Wesnoth launch configuration. If the problem persists, contact the development team for technical support, including the full contents of this message (copy with CTRL+C).";
153 
154  // It may not be useful to write to stderr at this point, so warn the user
155  // in a failsafe fashion via Windows UI API.
156  MessageBox(nullptr,
157  unicode_cast<std::wstring>(full_msg).c_str(),
158  L"Battle for Wesnoth",
159  MB_ICONEXCLAMATION | MB_OK);
160 
161  // It may seem excessive to quit over something like this, but it's a good
162  // indicator of possible configuration issues with the user data dir that
163  // may cause much weirder symptoms later (see https://r.wesnoth.org/t42970
164  // for an example).
165  exit(1);
166 }
167 
168 /**
169  * Display an alert box to warn about log initialization errors, and exit.
170  */
171 void log_init_panic(const libc_error& e,
172  const std::string& new_log_path,
173  const std::string& old_log_path = std::string())
174 {
175  std::ostringstream msg;
176 
177  if(old_log_path.empty()) {
178  msg << "Early log initialization failed.";
179  } else {
180  msg << "Log relocation failed.";
181  }
182 
183  msg << "\n\n"
184  << "Runtime error: " << e.desc() << " (" << e.num() << ")\n";
185 
186  if(old_log_path.empty()) {
187  msg << "Log file path: " << new_log_path << '\n';
188  } else {
189  msg << "New log file path: " << new_log_path << '\n'
190  << "Old log file path: " << old_log_path;
191  }
192 
193  log_init_panic(msg.str());
194 }
195 
196 /**
197  * Singleton class that deals with the intricacies of log file redirection.
198  */
199 class log_file_manager
200 {
201 public:
202  log_file_manager(const log_file_manager&) = delete;
203  log_file_manager& operator=(const log_file_manager&) = delete;
204 
205  log_file_manager(bool native_console = false);
206  ~log_file_manager();
207 
208  /**
209  * Returns the path to the current log file.
210  */
211  std::string log_file_path() const;
212 
213  /**
214  * Moves the log file to a new directory.
215  *
216  * This causes the associated streams to closed momentarily in order to be
217  * able to move the log file, because Windows does not allow move/rename
218  * operations on currently-open files.
219  *
220  * @param log_dir Log directory path.
221  *
222  * @throw libc_error If the log file cannot be opened or relocated.
223  */
224  void move_log_file(const std::string& log_dir);
225 
226  /**
227  * Switches to using a native console instead of log file redirection.
228  *
229  * This is an irreversible operation right now. This might change later if
230  * someone deems it useful.
231  */
233 
234  /**
235  * Returns whether we are using a native console instead of a log file.
236  */
237  bool console_enabled() const;
238 
239  /**
240  * Returns whether we are attached to a native console right now.
241  *
242  * Note that being attached to a console does not necessarily mean that the
243  * standard streams are pointing to it. Use console_enabled to check that
244  * instead.
245  */
246  bool console_attached() const;
247 
248  /**
249  * Returns whether we own the console we are attached to, if any.
250  */
251  bool owns_console() const;
252 
253 private:
254  std::string fn_;
255  std::string cur_path_;
256  bool use_wincon_, created_wincon_;
257 
258  enum STREAM_ID {
259  STREAM_STDOUT = 1,
260  STREAM_STDERR = 2
261  };
262 
263  /**
264  * Opens the log file for the current session in the specified directory.
265  *
266  * @param file_path Log file path.
267  * @param truncate Whether to truncate an existing log file or append
268  * to it instead.
269  *
270  * @throw libc_error If the log file cannot be opened.
271  */
272  void open_log_file(const std::string& file_path,
273  bool truncate);
274 
275  /**
276  * Takes care of any tasks required for redirecting a log stream.
277  *
278  * @param file_path Log file path.
279  * @param stream Stream identifier.
280  * @param truncate Whether to truncate an existing log file or append
281  * to it instead.
282  *
283  * @throw libc_error If the log file cannot be opened.
284  *
285  * @note This does not set cur_path_ to the new path.
286  */
287  void do_redirect_single_stream(const std::string& file_path,
288  STREAM_ID stream,
289  bool truncate);
290 };
291 
292 log_file_manager::log_file_manager(bool native_console)
293  : fn_(unique_log_filename())
294  , cur_path_()
295  , use_wincon_(console_attached())
296  , created_wincon_(false)
297 {
298  DBG_LS << "Early init message\n";
299 
300  if(use_wincon_) {
301  // Someone already attached a console to us. Assume we were compiled
302  // with the console subsystem flag and that the standard streams are
303  // already pointing to the console.
304  LOG_LS << "Console already attached at startup, log file disabled.\n";
305  return;
306  }
307 
308  if(native_console) {
310  return;
311  }
312 
313  //
314  // We use the Windows temp dir on startup,
315  //
316  const std::string new_path = temp_dir() + "/" + fn_;
317 
318  try {
319  open_log_file(new_path, true);
320  } catch(const libc_error& e) {
321  log_init_panic(e, new_path, cur_path_);
322  }
323 
324  LOG_LS << "Opened log file at " << new_path << '\n';
325 }
326 
327 log_file_manager::~log_file_manager()
328 {
329  if(cur_path_.empty()) {
330  // No log file, nothing to do.
331  return;
332  }
333 
334  fclose(stdout);
335  fclose(stderr);
336 }
337 
338 std::string log_file_manager::log_file_path() const
339 {
340  return cur_path_;
341 }
342 
343 void log_file_manager::move_log_file(const std::string& log_dir)
344 {
345  const std::string new_path = log_dir + "/" + fn_;
346 
347  try {
348  if(!cur_path_.empty()) {
349  const std::string old_path = cur_path_;
350 
351  // Need to close files before moving or renaming. This will replace
352  // cur_path_ with NUL, hence the backup above.
353  open_log_file("NUL", false);
354 
355  const std::wstring old_path_w
356  = unicode_cast<std::wstring>(old_path);
357  const std::wstring new_path_w
358  = unicode_cast<std::wstring>(new_path);
359 
360  if(_wrename(old_path_w.c_str(), new_path_w.c_str()) != 0) {
361  throw libc_error();
362  }
363  }
364 
365  // Reopen.
366  open_log_file(new_path, false);
367  } catch(const libc_error& e) {
368  log_init_panic(e, new_path, cur_path_);
369  }
370 
371  LOG_LS << "Moved log file to " << new_path << '\n';
372 }
373 
374 void log_file_manager::open_log_file(const std::string& file_path, bool truncate)
375 {
376  do_redirect_single_stream(file_path, STREAM_STDERR, truncate);
377  do_redirect_single_stream(file_path, STREAM_STDOUT, false);
378 
379  cur_path_ = file_path;
380 }
381 
382 void log_file_manager::do_redirect_single_stream(const std::string& file_path,
383  log_file_manager::STREAM_ID stream,
384  bool truncate)
385 {
386  DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side A]\n";
387 
388  FILE* crts = stream == STREAM_STDERR ? stderr : stdout;
389  std::ostream& cxxs = stream == STREAM_STDERR ? std::cerr : std::cout;
390 
391  fflush(crts);
392  cxxs.flush();
393 
394  const std::wstring file_path_w = unicode_cast<std::wstring>(file_path);
395 
396  if(!_wfreopen(file_path_w.c_str(), (truncate ? L"w" : L"a"), crts))
397  {
398  throw libc_error();
399  }
400 
401  //setbuf(crts, nullptr);
402 
403  DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side B]\n";
404 }
405 
406 bool log_file_manager::console_enabled() const
407 {
408  return use_wincon_;
409 }
410 
411 bool log_file_manager::console_attached() const
412 {
413  return GetConsoleWindow() != nullptr;
414 }
415 
416 bool log_file_manager::owns_console() const
417 {
418  return created_wincon_;
419 }
420 
422 {
423  if(use_wincon_) {
424  // We either went over this already or the console was set up by
425  // Windows itself (console subsystem flag in executable).
426  return;
427  }
428 
429  if(AttachConsole(ATTACH_PARENT_PROCESS)) {
430  LOG_LS << "Attached parent process console.\n";
431  created_wincon_ = false;
432  } else if(AllocConsole()) {
433  LOG_LS << "Allocated own console.\n";
434  created_wincon_ = true;
435  } else {
436  // Wine as of version 4.21 just goes ERROR_ACCESS_DENIED when trying
437  // to allocate a console for a GUI subsystem application. We can ignore
438  // this since the user purportedly knows what they're doing and if they
439  // get radio silence from Wesnoth and no log files they'll realize that
440  // something went wrong.
441  WRN_LS << "Cannot attach or allocate a console, continuing anyway (is this Wine?)\n";
442  }
443 
444  DBG_LS << "stderr to console\n";
445  fflush(stderr);
446  std::cerr.flush();
447  assert(freopen("CONOUT$", "wb", stderr) == stderr);
448 
449  DBG_LS << "stdout to console\n";
450  fflush(stdout);
451  std::cout.flush();
452  assert(freopen("CONOUT$", "wb", stdout) == stdout);
453 
454  DBG_LS << "stdin from console\n";
455  assert(freopen("CONIN$", "rb", stdin) == stdin);
456 
457  // At this point the log file has been closed and it's no longer our
458  // responsibility to clean up anything; Windows will figure out what to do
459  // when the time comes for the process to exit.
460  cur_path_.clear();
461  use_wincon_ = true;
462 
463  LOG_LS << "Console streams handover complete!\n";
464 }
465 
466 std::unique_ptr<log_file_manager> lfm;
467 
468 } // end anonymous namespace
469 
470 std::string log_file_path()
471 {
472  if(lfm) {
473  return lfm->log_file_path();
474  }
475 
476  return "";
477 }
478 
479 static bool disable_redirect;
480 
481 void early_log_file_setup(bool disable)
482 {
483  if(lfm) {
484  return;
485  }
486 
487  if(disable) {
488  disable_redirect = true;
489  return;
490  }
491 
492  lfm.reset(new log_file_manager());
493 }
494 
496 {
497  if(lfm) {
498  lfm->enable_native_console_output();
499  return;
500  }
501 
502  lfm.reset(new log_file_manager(true));
503 }
504 
506 {
507  return lfm && lfm->owns_console();
508 }
509 
511 {
512  if(disable_redirect) return;
513  // Make sure the LFM is actually set up just in case.
514  early_log_file_setup(false);
515 
516  if(lfm->console_enabled()) {
517  // Nothing to do if running in console mode.
518  return;
519  }
520 
521  static bool setup_complete = false;
522 
523  if(setup_complete) {
524  ERR_LS << "finish_log_file_setup() called more than once!\n";
525  return;
526  }
527 
528  const std::string log_dir = filesystem::get_logs_dir();
529  if(!filesystem::file_exists(log_dir) && !filesystem::make_directory(log_dir)) {
530  log_init_panic(std::string("Could not create logs directory at ") +
531  log_dir + ".");
532  } else {
533  rotate_logs(log_dir);
534  }
535 
536  lfm->move_log_file(log_dir);
537 
538  setup_complete = true;
539 }
540 
541 } // end namespace lg
bool using_own_console()
Returns true if a console was allocated by the Wesnoth process.
std::string log_file_path()
Returns the path to the current log file.
bool delete_file(const std::string &filename)
Definition: filesystem.cpp:987
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:263
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
void finish_log_file_setup()
Relocates the stdout+stderr log file to the user data directory.
std::string get_logs_dir()
Returns the path to the permanent log storage directory.
Definition: log_windows.cpp:49
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:110
void early_log_file_setup(bool disable)
Sets up the initial temporary log file.
static lg::log_domain log_setup("logsetup")
Exception type used to propagate C runtime errors across functions.
Definition: libc_error.hpp:19
std::string get_user_data_dir()
Definition: filesystem.cpp:792
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:349
void enable_native_console_output()
Switches to using a native console instead of log file redirection.
std::string & truncate(std::string &str, const std::size_t size)
Truncates a UTF-8 string to the specified number of characters.
Definition: unicode.cpp:118
const std::string & desc() const
Returns an explanatory string describing the runtime error alone.
Definition: libc_error.hpp:40
std::string path
Definition: game_config.cpp:39
#define DBG_LS
Definition: log_windows.cpp:44
Definition: pump.hpp:40
Log file control routines for Windows.
#define WRN_LS
Definition: log_windows.cpp:42
#define ERR_LS
Definition: log_windows.cpp:41
static bool disable_redirect
bool make_directory(const std::string &dirname)
Definition: filesystem.cpp:937
Declarations for File-IO.
static int sort(lua_State *L)
Definition: ltablib.cpp:397
Standard logging facilities (interface).
#define e
#define LOG_LS
Definition: log_windows.cpp:43
int num() const
Returns the value of errno at the time the exception was thrown.
Definition: libc_error.hpp:34