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