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