The Battle for Wesnoth  1.15.9+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  , created_wincon_(false)
286 {
287  DBG_LS << "Early init message\n";
288 
289  if(use_wincon_) {
290  // Someone already attached a console to us. Assume we were compiled
291  // with the console subsystem flag and that the standard streams are
292  // already pointing to the console.
293  LOG_LS << "Console already attached at startup, log file disabled.\n";
294  return;
295  }
296 
297  if(native_console) {
299  return;
300  }
301 
302  //
303  // We use the Windows temp dir on startup,
304  //
305  const std::string new_path = temp_dir() + "/" + fn_;
306 
307  try {
308  open_log_file(new_path, true);
309  } catch(const libc_error& e) {
310  log_init_panic(e, new_path, cur_path_);
311  }
312 
313  LOG_LS << "Opened log file at " << new_path << '\n';
314 }
315 
316 log_file_manager::~log_file_manager()
317 {
318  if(cur_path_.empty()) {
319  // No log file, nothing to do.
320  return;
321  }
322 
323  fclose(stdout);
324  fclose(stderr);
325 }
326 
327 std::string log_file_manager::log_file_path() const
328 {
329  return cur_path_;
330 }
331 
332 void log_file_manager::move_log_file(const std::string& log_dir)
333 {
334  const std::string new_path = log_dir + "/" + fn_;
335 
336  try {
337  if(!cur_path_.empty()) {
338  const std::string old_path = cur_path_;
339 
340  // Need to close files before moving or renaming. This will replace
341  // cur_path_ with NUL, hence the backup above.
342  open_log_file("NUL", false);
343 
344  const std::wstring old_path_w
345  = unicode_cast<std::wstring>(old_path);
346  const std::wstring new_path_w
347  = unicode_cast<std::wstring>(new_path);
348 
349  if(_wrename(old_path_w.c_str(), new_path_w.c_str()) != 0) {
350  throw libc_error();
351  }
352  }
353 
354  // Reopen.
355  open_log_file(new_path, false);
356  } catch(const libc_error& e) {
357  log_init_panic(e, new_path, cur_path_);
358  }
359 
360  LOG_LS << "Moved log file to " << new_path << '\n';
361 }
362 
363 void log_file_manager::open_log_file(const std::string& file_path, bool truncate)
364 {
365  do_redirect_single_stream(file_path, STREAM_STDERR, truncate);
366  do_redirect_single_stream(file_path, STREAM_STDOUT, false);
367 
368  cur_path_ = file_path;
369 }
370 
371 void log_file_manager::do_redirect_single_stream(const std::string& file_path,
372  log_file_manager::STREAM_ID stream,
373  bool truncate)
374 {
375  DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side A]\n";
376 
377  FILE* crts = stream == STREAM_STDERR ? stderr : stdout;
378  std::ostream& cxxs = stream == STREAM_STDERR ? std::cerr : std::cout;
379 
380  fflush(crts);
381  cxxs.flush();
382 
383  const std::wstring file_path_w = unicode_cast<std::wstring>(file_path);
384 
385  if(!_wfreopen(file_path_w.c_str(), (truncate ? L"w" : L"a"), crts))
386  {
387  throw libc_error();
388  }
389 
390  //setbuf(crts, nullptr);
391 
392  DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side B]\n";
393 }
394 
395 bool log_file_manager::console_enabled() const
396 {
397  return use_wincon_;
398 }
399 
400 bool log_file_manager::console_attached() const
401 {
402  return GetConsoleWindow() != nullptr;
403 }
404 
405 bool log_file_manager::owns_console() const
406 {
407  return created_wincon_;
408 }
409 
411 {
412  if(use_wincon_) {
413  // We either went over this already or the console was set up by
414  // Windows itself (console subsystem flag in executable).
415  return;
416  }
417 
418  if(AttachConsole(ATTACH_PARENT_PROCESS)) {
419  LOG_LS << "Attached parent process console.\n";
420  created_wincon_ = false;
421  } else if(AllocConsole()) {
422  LOG_LS << "Allocated own console.\n";
423  created_wincon_ = true;
424  } else {
425  // Wine as of version 4.21 just goes ERROR_ACCESS_DENIED when trying
426  // to allocate a console for a GUI subsystem application. We can ignore
427  // this since the user purportedly knows what they're doing and if they
428  // get radio silence from Wesnoth and no log files they'll realize that
429  // something went wrong.
430  WRN_LS << "Cannot attach or allocate a console, continuing anyway (is this Wine?)\n";
431  }
432 
433  DBG_LS << "stderr to console\n";
434  fflush(stderr);
435  std::cerr.flush();
436  assert(freopen("CONOUT$", "wb", stderr) == stderr);
437 
438  DBG_LS << "stdout to console\n";
439  fflush(stdout);
440  std::cout.flush();
441  assert(freopen("CONOUT$", "wb", stdout) == stdout);
442 
443  DBG_LS << "stdin from console\n";
444  assert(freopen("CONIN$", "rb", stdin) == stdin);
445 
446  // At this point the log file has been closed and it's no longer our
447  // responsibility to clean up anything; Windows will figure out what to do
448  // when the time comes for the process to exit.
449  cur_path_.clear();
450  use_wincon_ = true;
451 
452  LOG_LS << "Console streams handover complete!\n";
453 }
454 
455 std::unique_ptr<log_file_manager> lfm;
456 
457 } // end anonymous namespace
458 
459 std::string log_file_path()
460 {
461  if(lfm) {
462  return lfm->log_file_path();
463  }
464 
465  return "";
466 }
467 
469 {
470  if(lfm) {
471  return;
472  }
473 
474  lfm.reset(new log_file_manager());
475 }
476 
478 {
479  if(lfm) {
480  lfm->enable_native_console_output();
481  return;
482  }
483 
484  lfm.reset(new log_file_manager(true));
485 }
486 
488 {
489  return lfm && lfm->owns_console();
490 }
491 
493 {
494  // Make sure the LFM is actually set up just in case.
496 
497  if(lfm->console_enabled()) {
498  // Nothing to do if running in console mode.
499  return;
500  }
501 
502  static bool setup_complete = false;
503 
504  if(setup_complete) {
505  ERR_LS << "finish_log_file_setup() called more than once!\n";
506  return;
507  }
508 
509  const std::string log_dir = filesystem::get_user_data_dir() + "/logs";
510  if(!filesystem::file_exists(log_dir) && !filesystem::make_directory(log_dir)) {
511  log_init_panic(std::string("Could not create logs directory at ") +
512  log_dir + ".");
513  } else {
514  rotate_logs(log_dir);
515  }
516 
517  lfm->move_log_file(log_dir);
518 
519  setup_complete = true;
520 }
521 
522 } // 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:986
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.
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:791
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:39
#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
bool make_directory(const std::string &dirname)
Definition: filesystem.cpp:936
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