From aae8e70c7aff84117f48271dedbd1f2189fd7509 Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Sun, 22 Nov 2015 08:01:18 -0300 Subject: [PATCH 1/6] log/windows: Add our own stdout/stderr redirection on Windows This replaces SDL 1.2's built-in stdout/stderr redirection, which writes the log file to the process working directory (usually an admin-restricted location) instead of a more accessible user-writable location. My approach combines stdout and stderr into a single log file which includes the process id and timestamp in its filename in order to accomodate multiple instances and (coming later) log rotation. The log file is created in the user's temporary directory defined by Windows, and then relocated to the game user data dir as soon as it is set-up the first time, placed in the Windows-specific logs/ subdir. The most pressing issues this solves are the lack of built-in stdout/stderr redirection in SDL 2's SDL2main.lib entry point, and Unicode path issues with SDL 1.2 (bug #22897). Additionally, it allows us to not have to rely on UAC virtualization anymore; this is arguably far more important because it has been known to break on occasion (e.g. ), and starting with version 1.13.2 we want to declare Windows Vista - 10 compatibility in our side-by-side manifest (see commit e119f4071f047c6d740ebec4636985fdf39349bc). Currently missing features coming later: * Log rotation (otherwise the logs/ dir may grow forever) * wesnothd support (although the code is already required to be linked into wesnothd due to it being required by the FS API) * Integration with the version info dialog --- projectfiles/CodeBlocks/wesnoth.cbp | 3 + projectfiles/CodeBlocks/wesnothd.cbp | 3 + src/CMakeLists.txt | 6 + src/SConscript | 5 + src/filesystem_boost.cpp | 7 + src/libc_error.hpp | 42 ++++ src/log_windows.cpp | 344 +++++++++++++++++++++++++++ src/log_windows.hpp | 73 ++++++ src/wesnoth.cpp | 3 + 9 files changed, 486 insertions(+) create mode 100644 src/libc_error.hpp create mode 100644 src/log_windows.cpp create mode 100644 src/log_windows.hpp diff --git a/projectfiles/CodeBlocks/wesnoth.cbp b/projectfiles/CodeBlocks/wesnoth.cbp index a364e497fb70..0e318a815524 100644 --- a/projectfiles/CodeBlocks/wesnoth.cbp +++ b/projectfiles/CodeBlocks/wesnoth.cbp @@ -868,12 +868,15 @@ + + + diff --git a/projectfiles/CodeBlocks/wesnothd.cbp b/projectfiles/CodeBlocks/wesnothd.cbp index adba546fe564..5afc9616ff7f 100644 --- a/projectfiles/CodeBlocks/wesnothd.cbp +++ b/projectfiles/CodeBlocks/wesnothd.cbp @@ -70,10 +70,13 @@ + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b56aeac3ceeb..896e861ac271 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -288,6 +288,12 @@ else() ) endif() +if(WIN32) + set(libwesnoth-core_STAT_SRC + ${libwesnoth-core_STAT_SRC} + log_windows.cpp + ) + # a 'lib' is automatically set in front when creating the library (as in the filename) # internal reference is the name given here add_library(wesnoth-core ${LIBRARY_TYPE} EXCLUDE_FROM_ALL ${libwesnoth-core_STAT_SRC}) diff --git a/src/SConscript b/src/SConscript index 9667ed215212..f95ecef3b25f 100644 --- a/src/SConscript +++ b/src/SConscript @@ -69,6 +69,11 @@ libwesnoth_core_sources.extend([ filesystem_env.Object("filesystem_boost.cpp") ]) +if env["PLATFORM"] == "win32": + libwesnoth_core_sources.extend([ + filesystem_env.Object("log_windows.cpp") + ]) + if env["libintl"]: libwesnoth_core_sources.extend([ filesystem_env.Object("gettext.cpp") diff --git a/src/filesystem_boost.cpp b/src/filesystem_boost.cpp index 1c25f0b19551..db37528fdd58 100644 --- a/src/filesystem_boost.cpp +++ b/src/filesystem_boost.cpp @@ -34,7 +34,10 @@ using boost::uintmax_t; #ifdef _WIN32 +#include "log_windows.hpp" + #include + #include #include #endif /* !_WIN32 */ @@ -478,6 +481,10 @@ static void setup_user_data_dir() create_directory_if_missing(user_data_dir / "data" / "add-ons"); create_directory_if_missing(user_data_dir / "saves"); create_directory_if_missing(user_data_dir / "persist"); + +#ifdef _WIN32 + lg::finish_log_file_setup(); +#endif } #ifdef _WIN32 diff --git a/src/libc_error.hpp b/src/libc_error.hpp new file mode 100644 index 000000000000..6035a5aac16d --- /dev/null +++ b/src/libc_error.hpp @@ -0,0 +1,42 @@ +/* + By Ignacio Riquelme Morelle + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + The contents of this file are placed in the public domain. + */ + +#include +#include +#include + +#ifndef LIBC_ERROR_HPP_INCLUDED +#define LIBC_ERROR_HPP_INCLUDED + +/** + * Exception type used to propagate C runtime errors across functions. + */ +class libc_error +{ +public: + libc_error(): e_(errno), msg_(strerror(e_)) + { + } + + /** Returns the value of @a errno at the time the exception was thrown. */ + int num() const + { + return e_; + } + + /** Returns an explanatory string describing the runtime error. */ + const std::string& desc() const + { + return msg_; + } + +private: + int e_; + std::string msg_; +}; + +#endif diff --git a/src/log_windows.cpp b/src/log_windows.cpp new file mode 100644 index 000000000000..a32949a9ac05 --- /dev/null +++ b/src/log_windows.cpp @@ -0,0 +1,344 @@ +/* + Copyright (C) 2015 by Ignacio Riquelme Morelle + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY. + + See the COPYING file for more details. +*/ + +#include "log_windows.hpp" + +#include "filesystem.hpp" +#include "libc_error.hpp" +#include "log.hpp" +#include "serialization/unicode.hpp" + +#include +#include + +#include +#include + +#ifndef UNICODE +#define UNICODE +#endif + +#define WIN32_LEAN_AND_MEAN +#define _WIN32_WINNT 0x0501 // XP and later + +#include + +#if 1 +static lg::log_domain log_setup("logsetup"); +#define ERR_LS LOG_STREAM(err, log_setup) +#define WRN_LS LOG_STREAM(warn, log_setup) +#define LOG_LS LOG_STREAM(info, log_setup) +#define DBG_LS LOG_STREAM(debug, log_setup) + +#else + +std::stringstream foo; +#define ERR_LS foo +#define WRN_LS foo +#define LOG_LS foo +#define DBG_LS foo + +#endif + +namespace lg +{ + +namespace +{ + +/** + * Generates a "unique" log file name. + * + * This is really not guaranteed to be unique, but it's close enough, since + * the odds of having multiple Wesnoth instances spawn with the same PID within + * a second span are close to zero. + */ +std::string unique_log_filename() +{ + std::ostringstream o; + + o << "wesnoth-"; + + const time_t cur = time(NULL); + const tm* const lt = localtime(&cur); + + if(lt) { + char ts_buf[128] = { 0 }; + strftime(ts_buf, 128, "%Y%m%d-%H%M%S-", lt); + o << ts_buf; + } + + o << GetCurrentProcessId() << ".log"; + + return o.str(); +} + +/** + * Returns the path to a system-defined temporary files dir. + */ +std::string temp_dir() +{ + wchar_t tmpdir[MAX_PATH + 1]; + + if(GetTempPath(MAX_PATH + 1, tmpdir) == 0) { + return "."; + } + + return unicode_cast(std::wstring(tmpdir)); +} + +/** + * Display an alert box to warn about log initialization errors, and exit. + */ +void log_init_panic(const std::string& msg) +{ + ERR_LS << "Log initialization panic call: " << msg << '\n'; + + 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)."; + + // It may not be useful to write to stderr at this point, so warn the user + // in a failsafe fashion via Windows UI API. + MessageBox(NULL, + unicode_cast(full_msg).c_str(), + L"Battle for Wesnoth", + MB_ICONEXCLAMATION | MB_OK); + + // It may seem excessive to quit over something like this, but it's a good + // indicator of possible configuration issues with the user data dir that + // may cause much weirder symptoms later (see http://r.wesnoth.org/t42970 + // for an example). + exit(1); +} + +/** + * Display an alert box to warn about log initialization errors, and exit. + */ +void log_init_panic(const libc_error& e, + const std::string& new_log_path, + const std::string& old_log_path = std::string()) +{ + std::ostringstream msg; + + if(old_log_path.empty()) { + msg << "Early log initialization failed."; + } else { + msg << "Log relocation failed."; + } + + msg << "\n\n" + << "Runtime error: " << e.desc() << " (" << e.num() << ")\n" + << "New log file path: " << new_log_path << '\n' + << "Old log file path: "; + + if(old_log_path.empty()) { + msg << "Log file path: " << new_log_path << '\n'; + } else { + msg << "New log file path: " << new_log_path << '\n' + << "Old log file path: "; + } + + log_init_panic(msg.str()); +} + +/** + * Singleton class that deals with the intricacies of log file redirection. + */ +class log_file_manager : private boost::noncopyable +{ +public: + log_file_manager(); + + /** + * Returns the path to the current log file. + */ + std::string log_file_path() const; + + /** + * Moves the log file to a new directory. + * + * This causes the associated streams to closed momentarily in order to be + * able to move the log file, because Windows does not allow move/rename + * operations on currently-open files. + * + * @param log_dir Log directory path. + * + * @throw libc_error If the log file cannot be opened or relocated. + */ + void move_log_file(const std::string& log_dir); + +private: + std::string fn_; + std::string cur_path_; + + enum STREAM_ID { + STREAM_STDOUT = 1, + STREAM_STDERR = 2 + }; + + /** + * Opens the log file for the current session in the specified directory. + * + * @param file_path Log file path. + * @param truncate Whether to truncate an existing log file or append + * to it instead. + * + * @throw libc_error If the log file cannot be opened. + */ + void open_log_file(const std::string& file_path, + bool truncate); + + /** + * Takes care of any tasks required for redirecting a log stream. + * + * @param file_path Log file path. + * @param stream Stream identifier. + * @param truncate Whether to truncate an existing log file or append + * to it instead. + * + * @throw libc_error If the log file cannot be opened. + * + * @note This does not set cur_path_ to the new path. + */ + void do_redirect_single_stream(const std::string& file_path, + STREAM_ID stream, + bool truncate); +}; + +log_file_manager::log_file_manager() + : fn_(unique_log_filename()) + , cur_path_() +{ + DBG_LS << "Early init message\n"; + + // + // We use the Windows temp dir on startup, + // + const std::string new_path = temp_dir() + "/" + fn_; + + try { + open_log_file(new_path, true); + } catch(const libc_error& e) { + log_init_panic(e, new_path, cur_path_); + } + + LOG_LS << "Opened log file at " << new_path << '\n'; +} + +std::string log_file_manager::log_file_path() const +{ + return cur_path_; +} + +void log_file_manager::move_log_file(const std::string& log_dir) +{ + const std::string new_path = log_dir + "/" + fn_; + + try { + if(!cur_path_.empty()) { + const std::string old_path = cur_path_; + + // Need to close files before moving or renaming. This will replace + // cur_path_ with NUL, hence the backup above. + open_log_file("NUL", false); + + if(rename(old_path.c_str(), new_path.c_str()) != 0) { + throw libc_error(); + } + } + + // Reopen. + open_log_file(new_path, false); + } catch(const libc_error& e) { + log_init_panic(e, new_path, cur_path_); + } + + LOG_LS << "Moved log file to " << new_path << '\n'; +} + +void log_file_manager::open_log_file(const std::string& file_path, bool truncate) +{ + do_redirect_single_stream(file_path, STREAM_STDERR, truncate); + do_redirect_single_stream(file_path, STREAM_STDOUT, false); + + cur_path_ = file_path; +} + +void log_file_manager::do_redirect_single_stream(const std::string& file_path, + log_file_manager::STREAM_ID stream, + bool truncate) +{ + DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side A]\n"; + + FILE* crts = stream == STREAM_STDERR ? stderr : stdout; + std::ostream& cxxs = stream == STREAM_STDERR ? std::cerr : std::cout; + + fflush(crts); + cxxs.flush(); + + if(!freopen(file_path.c_str(), (truncate ? "w" : "a"), crts)) + { + throw libc_error(); + } + + //setbuf(crts, NULL); + + DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side B]\n"; +} + +boost::scoped_ptr lfm; + +} // end anonymous namespace + +std::string log_file_path() +{ + if(lfm) { + return lfm->log_file_path(); + } + + return ""; +} + +void early_log_file_setup() +{ + if(lfm) { + return; + } + + lfm.reset(new log_file_manager()); +} + +void finish_log_file_setup() +{ + // Make sure the LFM is actually set up just in case. + early_log_file_setup(); + + static bool setup_complete = false; + + if(setup_complete) { + ERR_LS << "finish_log_file_setup() called more than once!\n"; + return; + } + + const std::string log_dir = filesystem::get_user_data_dir() + "/logs"; + if(!filesystem::file_exists(log_dir) && !filesystem::make_directory(log_dir)) { + log_init_panic(std::string("Could not create logs directory at ") + + log_dir + "."); + } + + lfm->move_log_file(log_dir); + + setup_complete = true; +} + +} // end namespace lg diff --git a/src/log_windows.hpp b/src/log_windows.hpp new file mode 100644 index 000000000000..e4a4c11328b9 --- /dev/null +++ b/src/log_windows.hpp @@ -0,0 +1,73 @@ +/* + Copyright (C) 2015 by Ignacio Riquelme Morelle + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY. + + See the COPYING file for more details. +*/ + +#ifndef DESKTOP_WINDOWS_LOG_HPP_INCLUDED +#define DESKTOP_WINDOWS_LOG_HPP_INCLUDED + +#include + +/** + * @file + * Log file control routines for Windows. + * + * During static object initialization, stdout and stderr are redirected to a + * uniquely-named log file located in the user's temporary directory as defined + * by the platform (e.g. C:/Users/username/AppData/Local/Temp/wesnoth-XXXX.log). + * Later, a request may be issued to relocate the log file to a more permanent + * and user-accessible location (such as the Wesnoth user data directory). + * + * Because Wesnoth is normally built with the GUI subsystem option, there is no + * console on startup and thus no way to see stdout/stderr output. Since + * version 1.13.1, we can allocate a console during initialization when started + * with the --wconsole option, but that is a somewhat clunky hack that does not + * help with post mortem debugging. + * + * SDL 1.2 used to redirect stdout and stderr to stdout.txt and stderr.txt in + * the process working directory automatically, but this approach too had its + * own shortcomings by assuming the pwd was writable by the process (or in Vista + * and later versions, requiring UAC virtualization to be enabled). + */ + +namespace lg +{ + +/** + * Returns the path to the current log file. + * + * An empty string is returned if the log file has not been set up yet or it + * was disabled (e.g. by --wconsole). + */ +std::string log_file_path(); + +/** + * Sets up the initial temporary log file. + * + * This has to be done on demand (preferably as early as possible) from a + * function rather than during static initialization, otherwise things go + * horribly wrong as soon as we try to use the logging facilities internally + * for debug messages. + */ +void early_log_file_setup(); + +/** + * Relocates the stdout+stderr log file to the user data directory. + * + * This function exits the process if something goes wrong (including calling + * it when the user data directory isn't known yet). + */ +void finish_log_file_setup(); + +} + +#endif diff --git a/src/wesnoth.cpp b/src/wesnoth.cpp index b6e29bf35f9d..fd46acb2bf64 100644 --- a/src/wesnoth.cpp +++ b/src/wesnoth.cpp @@ -63,6 +63,7 @@ #ifdef _WIN32 #include "desktop/windows_console.hpp" +#include "log_windows.hpp" #endif // _WIN32 #include // for SDL_Init, SDL_INIT_TIMER @@ -909,6 +910,8 @@ int main(int argc, char** argv) #endif #endif //_OPENMP #ifdef _WIN32 + lg::early_log_file_setup(); + (void)argc; (void)argv; From 900dc5b07017bfcf6ea08fc06a09a5f1cfb712db Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Sun, 22 Nov 2015 21:58:14 -0300 Subject: [PATCH 2/6] log/windows: Implement log file rotation A maximum of 8 previous log files are kept around in the logs directory. Older files are automatically deleted during initialization. I could've achieved this with stat calls, really, but the log filenames are simpler and faster to process. --- src/log_windows.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/log_windows.cpp b/src/log_windows.cpp index a32949a9ac05..bf4dd3ac964e 100644 --- a/src/log_windows.cpp +++ b/src/log_windows.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include #include @@ -57,18 +59,66 @@ namespace lg namespace { +// Prefix and extension for log files. This is used both to generate the unique +// log file name during startup and to find old files to delete. +const std::string log_file_prefix = "wesnoth-"; +const std::string log_file_suffix = ".log"; + +// Maximum number of older log files to keep intact. Other files are deleted. +// Note that this count does not include the current log file! +const unsigned max_logs = 8; + +/** Helper function for rotate_logs. */ +bool is_not_log_file(const std::string& fn) +{ + return !(boost::algorithm::istarts_with(fn, log_file_prefix) && + boost::algorithm::iends_with(fn, log_file_suffix)); +} + +/** + * Deletes old log files from the log directory. + */ +void rotate_logs(const std::string& log_dir) +{ + std::vector files; + filesystem::get_files_in_dir(log_dir, &files); + + files.erase(std::remove_if(files.begin(), files.end(), is_not_log_file), files.end()); + + if(files.size() <= max_logs) { + return; + } + + // Sorting the file list and deleting all but the last max_logs items + // should hopefully be faster than stat'ing every single file for its + // time attributes (which aren't very reliable to begin with. + + std::sort(files.begin(), files.end()); + + for(size_t j = 0; j < files.size() - max_logs; ++j) { + const std::string path = log_dir + '/' + files[j]; + LOG_LS << "rotate_logs(): delete " << path << '\n'; + if(!filesystem::delete_file(path)) { + WRN_LS << "rotate_logs(): failed to delete " << path << "!\n"; + } + } +} + /** * Generates a "unique" log file name. * * This is really not guaranteed to be unique, but it's close enough, since * the odds of having multiple Wesnoth instances spawn with the same PID within * a second span are close to zero. + * + * The file name includes a timestamp in order to satisfy the requirements of + * the rotate_logs logic. */ std::string unique_log_filename() { std::ostringstream o; - o << "wesnoth-"; + o << log_file_prefix; const time_t cur = time(NULL); const tm* const lt = localtime(&cur); @@ -79,7 +129,7 @@ std::string unique_log_filename() o << ts_buf; } - o << GetCurrentProcessId() << ".log"; + o << GetCurrentProcessId() << log_file_suffix; return o.str(); } @@ -334,6 +384,8 @@ void finish_log_file_setup() if(!filesystem::file_exists(log_dir) && !filesystem::make_directory(log_dir)) { log_init_panic(std::string("Could not create logs directory at ") + log_dir + "."); + } else { + rotate_logs(log_dir); } lfm->move_log_file(log_dir); From bc9fb0c1110027b7fddaf7860cd0fded652e91a2 Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Mon, 23 Nov 2015 00:24:57 -0300 Subject: [PATCH 3/6] log/windows: Close stdout and stderr on destruction if redirected No real reason to do this since these resources should be automatically released on process exit anyway, but, better safe than sorry I guess. --- src/log_windows.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/log_windows.cpp b/src/log_windows.cpp index bf4dd3ac964e..3e5b60d77fc9 100644 --- a/src/log_windows.cpp +++ b/src/log_windows.cpp @@ -208,6 +208,7 @@ class log_file_manager : private boost::noncopyable { public: log_file_manager(); + ~log_file_manager(); /** * Returns the path to the current log file. @@ -285,6 +286,18 @@ log_file_manager::log_file_manager() LOG_LS << "Opened log file at " << new_path << '\n'; } +log_file_manager::~log_file_manager() +{ + if(cur_path_.empty()) { + // No log file, nothing to do. + return; + } + + DBG_LS << "Closing log file...\n"; + fclose(stdout); + fclose(stderr); +} + std::string log_file_manager::log_file_path() const { return cur_path_; From 53ed94da251b231a41e399bcee634be3fa813343 Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Mon, 23 Nov 2015 01:59:36 -0300 Subject: [PATCH 4/6] log/windows: Integrate implementation of --wconsole This makes it so we no longer try to steal the console back to a log file when using --wconsole with the new redirection code. Now the --wconsole switch triggers a special mode of the log file manager that uses a native console instead of log files. As a necessary bonus to appease compilers, the GUI2 version info dialog now uses the correct log file path when not started with the --wconsole switch. Yay! --- projectfiles/CodeBlocks/wesnoth.cbp | 2 - src/CMakeLists.txt | 1 - src/SConscript | 1 - src/desktop/windows_console.cpp | 101 ---------------------------- src/desktop/windows_console.hpp | 29 -------- src/gui/dialogs/game_version.cpp | 10 +-- src/log_windows.cpp | 79 +++++++++++++++++++++- src/log_windows.hpp | 14 +++- src/wesnoth.cpp | 7 +- 9 files changed, 97 insertions(+), 147 deletions(-) delete mode 100644 src/desktop/windows_console.cpp delete mode 100644 src/desktop/windows_console.hpp diff --git a/projectfiles/CodeBlocks/wesnoth.cbp b/projectfiles/CodeBlocks/wesnoth.cbp index 0e318a815524..18dcb0e90619 100644 --- a/projectfiles/CodeBlocks/wesnoth.cbp +++ b/projectfiles/CodeBlocks/wesnoth.cbp @@ -251,8 +251,6 @@ - - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 896e861ac271..1b3daf550c90 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1148,7 +1148,6 @@ set(libwesnoth-game_STAT_SRC if(WIN32) set(libwesnoth-game_STAT_SRC ${libwesnoth-game_STAT_SRC} - desktop/windows_console.cpp desktop/windows_tray_notification.cpp ) endif(WIN32) diff --git a/src/SConscript b/src/SConscript index f95ecef3b25f..2269f9fd218d 100644 --- a/src/SConscript +++ b/src/SConscript @@ -635,7 +635,6 @@ wesnoth_sources = Split(""" if env["PLATFORM"] == "win32": wesnoth_sources.append("desktop/windows_tray_notification.cpp") - wesnoth_sources.append("desktop/windows_console.cpp") if env["PLATFORM"] == 'darwin': wesnoth_sources.append("desktop/apple_notification.mm") diff --git a/src/desktop/windows_console.cpp b/src/desktop/windows_console.cpp deleted file mode 100644 index 8bdece2e484f..000000000000 --- a/src/desktop/windows_console.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - Copyright (C) 2014 - 2015 by Ignacio Riquelme Morelle - Part of the Battle for Wesnoth Project http://www.wesnoth.org/ - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY. - - See the COPYING file for more details. -*/ - -#include "desktop/windows_console.hpp" - -#include "log.hpp" - -#ifndef UNICODE -#define UNICODE -#endif - -#define WIN32_LEAN_AND_MEAN -#define _WIN32_WINNT 0x0501 // XP and later - -#include - -#include -#include - -static lg::log_domain log_desktop("desktop"); -#define ERR_DU LOG_STREAM(err, log_desktop) -#define LOG_DU LOG_STREAM(info, log_desktop) - -namespace { - -class win32_console_manager -{ -public: - win32_console_manager() - { -#if 0 - // Because this runs before cmdline processing, enable this block for - // debugging purposes if necessary. - lg::set_log_domain_severity("desktop", lg::debug); -#endif - if(AttachConsole(ATTACH_PARENT_PROCESS)) { - LOG_DU << "win32_console: attached to parent process console\n"; - } else if(AllocConsole()) { - LOG_DU << "win32_console: attached to own console\n"; - } else { - ERR_DU << "win32_console: failed to attach or allocate console!"; - return; - } - - LOG_DU << "win32_console: stdin to console\n"; - freopen("CONIN$", "rb", stdin); - LOG_DU << "win32_console: stdout to console\n"; - std::cout.flush(); - freopen("CONOUT$", "wb", stdout); - LOG_DU << "win32_console: stderr to console\n"; - std::cerr.flush(); - freopen("CONOUT$", "wb", stderr); - - LOG_DU << "win32_console: init complete\n"; - } - - ~win32_console_manager() - { - FreeConsole(); - - LOG_DU << "win32_console: uninit complete\n"; - } -}; - -boost::scoped_ptr conman; - -} // end anonymous namespace - -namespace desktop { - -void enable_win32_console() -{ - if(!conman) { - conman.reset(new win32_console_manager()); - } else { - ERR_DU << "win32_console: Console already enabled!\n"; - } -} - -void disable_win32_console() -{ - conman.reset(NULL); -} - -bool is_win32_console_enabled() -{ - return conman != NULL; -} - -} // end namespace desktop diff --git a/src/desktop/windows_console.hpp b/src/desktop/windows_console.hpp deleted file mode 100644 index d25a99952f3f..000000000000 --- a/src/desktop/windows_console.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/* - Copyright (C) 2014 - 2015 by Ignacio Riquelme Morelle - Part of the Battle for Wesnoth Project http://www.wesnoth.org/ - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY. - - See the COPYING file for more details. -*/ - -#ifndef DESKTOP_WIN32_CONSOLE_HPP_INCLUDED -#define DESKTOP_WIN32_CONSOLE_HPP_INCLUDED - -namespace desktop { - -void enable_win32_console(); - -void disable_win32_console(); - -bool is_win32_console_enabled(); - -} - - -#endif // DESKTOP_WIN32_CONSOLE_HPP_INCLUDED diff --git a/src/gui/dialogs/game_version.cpp b/src/gui/dialogs/game_version.cpp index 7c3d37b7cc7d..c38e02a73db9 100644 --- a/src/gui/dialogs/game_version.cpp +++ b/src/gui/dialogs/game_version.cpp @@ -20,9 +20,6 @@ #include "desktop/clipboard.hpp" #include "desktop/open.hpp" #include "desktop/version.hpp" -#ifdef _WIN32 -#include "desktop/windows_console.hpp" -#endif #include "filesystem.hpp" #include "formula_string_utils.hpp" #include "game_config.hpp" @@ -41,6 +38,9 @@ #include "gui/widgets/stacked_widget.hpp" #include "gui/widgets/text.hpp" #include "gui/widgets/window.hpp" +#ifdef _WIN32 +#include "log_windows.hpp" +#endif #include "serialization/string_utils.hpp" #include "gettext.hpp" @@ -93,7 +93,7 @@ tgame_version::tgame_version() , browse_wid_stem_("browse_") , path_map_() #ifdef _WIN32 - , log_path_(game_config::wesnoth_program_dir + "\\stderr.txt") + , log_path_(lg::log_file_path()) #endif , deps_() , opts_(game_config::optional_features_table()) @@ -199,7 +199,7 @@ void tgame_version::pre_show(CVideo& /*video*/, twindow& window) boost::bind(&tgame_version::browse_directory_callback, this, log_path_)); - stderr_button.set_active(!desktop::is_win32_console_enabled()); + stderr_button.set_active(!log_path_.empty()); #endif // diff --git a/src/log_windows.cpp b/src/log_windows.cpp index 3e5b60d77fc9..d06077aa1761 100644 --- a/src/log_windows.cpp +++ b/src/log_windows.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2015 by Ignacio Riquelme Morelle + Copyright (C) 2014 - 2015 by Ignacio Riquelme Morelle Part of the Battle for Wesnoth Project http://www.wesnoth.org/ This program is free software; you can redistribute it and/or modify @@ -207,7 +207,7 @@ void log_init_panic(const libc_error& e, class log_file_manager : private boost::noncopyable { public: - log_file_manager(); + log_file_manager(bool native_console = false); ~log_file_manager(); /** @@ -228,9 +228,23 @@ class log_file_manager : private boost::noncopyable */ void move_log_file(const std::string& log_dir); + /** + * Switches to using a native console instead of log file redirection. + * + * This is an irreversible operation right now. This might change later if + * someone deems it useful. + */ + void enable_native_console_output(); + + /** + * Returns whether we are using a native console instead of a log file. + */ + bool console_enabled() const; + private: std::string fn_; std::string cur_path_; + bool use_wincon_; enum STREAM_ID { STREAM_STDOUT = 1, @@ -266,12 +280,18 @@ class log_file_manager : private boost::noncopyable bool truncate); }; -log_file_manager::log_file_manager() +log_file_manager::log_file_manager(bool native_console) : fn_(unique_log_filename()) , cur_path_() + , use_wincon_() { DBG_LS << "Early init message\n"; + if(native_console) { + enable_native_console_output(); + return; + } + // // We use the Windows temp dir on startup, // @@ -359,6 +379,44 @@ void log_file_manager::do_redirect_single_stream(const std::string& file_path, DBG_LS << stream << ' ' << cur_path_ << " -> " << file_path << " [side B]\n"; } +bool log_file_manager::console_enabled() const +{ + return use_wincon_; +} + +void log_file_manager::enable_native_console_output() +{ + if(AttachConsole(ATTACH_PARENT_PROCESS)) { + LOG_LS << "Attached parent process console.\n"; + } else if(AllocConsole()) { + LOG_LS << "Allocated own console.\n"; + } else { + ERR_LS << "Console attachment or allocation failed!\n"; + return; + } + + DBG_LS << "stderr to console\n"; + fflush(stderr); + std::cerr.flush(); + freopen("CONOUT$", "wb", stderr); + + DBG_LS << "stdout to console\n"; + fflush(stdout); + std::cout.flush(); + freopen("CONOUT$", "wb", stdout); + + DBG_LS << "stdin from console\n"; + freopen("CONIN$", "rb", stdin); + + // At this point the log file has been closed and it's no longer our + // responsibility to clean up anything; Windows will figure out what to do + // when the time comes for the process to exit. + cur_path_.clear(); + use_wincon_ = true; + + LOG_LS << "Console streams handover complete!\n"; +} + boost::scoped_ptr lfm; } // end anonymous namespace @@ -381,11 +439,26 @@ void early_log_file_setup() lfm.reset(new log_file_manager()); } +void enable_native_console_output() +{ + if(lfm) { + lfm->enable_native_console_output(); + return; + } + + lfm.reset(new log_file_manager(true)); +} + void finish_log_file_setup() { // Make sure the LFM is actually set up just in case. early_log_file_setup(); + if(lfm->console_enabled()) { + // Nothing to do if running in console mode. + return; + } + static bool setup_complete = false; if(setup_complete) { diff --git a/src/log_windows.hpp b/src/log_windows.hpp index e4a4c11328b9..8040cb3a71cb 100644 --- a/src/log_windows.hpp +++ b/src/log_windows.hpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2015 by Ignacio Riquelme Morelle + Copyright (C) 2014 - 2015 by Ignacio Riquelme Morelle Part of the Battle for Wesnoth Project http://www.wesnoth.org/ This program is free software; you can redistribute it and/or modify @@ -68,6 +68,18 @@ void early_log_file_setup(); */ void finish_log_file_setup(); +/** + * Switches to using a native console instead of log file redirection. + * + * In this mode, the log file is closed (if it was created in the first place) + * and output is sent directly to an attached or allocated console instead. + * This is used to implement the --wconsole command line option. + * + * Using a native console instead of a file has the benefit of allowing to see + * output in real time or redirecting it to a user-specified file. + */ +void enable_native_console_output(); + } #endif diff --git a/src/wesnoth.cpp b/src/wesnoth.cpp index fd46acb2bf64..54bd00bffad6 100644 --- a/src/wesnoth.cpp +++ b/src/wesnoth.cpp @@ -62,7 +62,6 @@ #include "wml_exception.hpp" // for twml_exception #ifdef _WIN32 -#include "desktop/windows_console.hpp" #include "log_windows.hpp" #endif // _WIN32 @@ -910,8 +909,6 @@ int main(int argc, char** argv) #endif #endif //_OPENMP #ifdef _WIN32 - lg::early_log_file_setup(); - (void)argc; (void)argv; @@ -924,10 +921,12 @@ int main(int argc, char** argv) // here and let program_options ignore the switch later. for(size_t k = 0; k < args.size(); ++k) { if(args[k] == "--wconsole") { - desktop::enable_win32_console(); + lg::enable_native_console_output(); break; } } + + lg::early_log_file_setup(); #else std::vector args; for(int i = 0; i < argc; ++i) From baae0bd708d3b61ff0440a4b4d22519c230a0c32 Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Mon, 23 Nov 2015 03:13:50 -0300 Subject: [PATCH 5/6] log/windows: Auto-disable log file if a console is attached at startup This change only actually concerns binaries built with the console subsystem flag, which isn't the case with our official project files or SCons recipe. --- src/log_windows.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/log_windows.cpp b/src/log_windows.cpp index d06077aa1761..b2e6fda7cc5f 100644 --- a/src/log_windows.cpp +++ b/src/log_windows.cpp @@ -241,6 +241,15 @@ class log_file_manager : private boost::noncopyable */ bool console_enabled() const; + /** + * Returns whether we are attached to a native console right now. + * + * Note that being attached to a console does not necessarily mean that the + * standard streams are pointing to it. Use console_enabled to check that + * instead. + */ + bool console_attached() const; + private: std::string fn_; std::string cur_path_; @@ -283,10 +292,18 @@ class log_file_manager : private boost::noncopyable log_file_manager::log_file_manager(bool native_console) : fn_(unique_log_filename()) , cur_path_() - , use_wincon_() + , use_wincon_(console_attached()) { DBG_LS << "Early init message\n"; + if(use_wincon_) { + // Someone already attached a console to us. Assume we were compiled + // with the console subsystem flag and that the standard streams are + // already pointing to the console. + LOG_LS << "Console already attached at startup, log file disabled.\n"; + return; + } + if(native_console) { enable_native_console_output(); return; @@ -384,8 +401,19 @@ bool log_file_manager::console_enabled() const return use_wincon_; } +bool log_file_manager::console_attached() const +{ + return GetConsoleWindow() != NULL; +} + void log_file_manager::enable_native_console_output() { + if(use_wincon_) { + // We either went over this already or the console was set up by + // Windows itself (console subsystem flag in executable). + return; + } + if(AttachConsole(ATTACH_PARENT_PROCESS)) { LOG_LS << "Attached parent process console.\n"; } else if(AllocConsole()) { From 758f929a7bca73c9272746292b1367b4945654c2 Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Mon, 23 Nov 2015 03:41:19 -0300 Subject: [PATCH 6/6] Update changelog and RELEASE_NOTES --- RELEASE_NOTES | 6 ++++++ changelog | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 05a4589c1c58..1dad34239c26 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -57,6 +57,12 @@ See also bug [bug]23753[/bug]. [/list] [/rasection] +[rasection="New log files location on Windows"] +From this release onwards, Wesnoth no longer writes its stdout/stderr logs to [tt]stdout.txt[/tt]/[tt]stderr.txt[/tt] in the installation path. Instead, a single combined log file is written to [tt]\logs\wesnoth--.log[/tt] containing all stdout [b]and[/b] stderr output. Up to 8 older log files are kept around, the rest being automatically deleted at game startup. Note that if an early startup failure occurs, the log file will be found at [tt]%TEMP%\wesnoth--.log[/tt] instead. + +Since this replaces SDL 1.2's buggy built-in stdout/stderr redirection code with a Unicode-aware alternative, this also fixes bug [bug]22897[/bug]. +[/rasection] + [rasection="New game version dialog"] Describe how awesome the new game version dialog is and how it replaces the old game paths dialog from 1.12.x [/rasection] diff --git a/changelog b/changelog index 88534c9fb321..58eeb96f608b 100644 --- a/changelog +++ b/changelog @@ -162,6 +162,12 @@ Version 1.13.1+dev: explicitly use . or .. (e.g. `--config-dir .\userdata`) to force the user config+data dir path to be relative to the current working dir (see also bug #23753). + * Wesnoth now uses combined stdout+stderr log files on Windows, moved to + \logs during initialization and first created in the user's + temporary files directory defined by Windows. Log files are named like + "wesnoth--.log" and up to 8 log files are kept along with + the latest session's log file. This avoids issues caused by SDL 1.2's + built-in redirection code not being Unicode-aware (fixes bug #22897). * Removed legacy filesystem API implementation. * Fixed Generate Map dialog bug that caused last choice of map generator to not be actually selected (bug #23711).