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