Redirect using C output instead of C++ output (#8391)
Apparently redirecting stdout/stderr also results in std::cout/std::cerr being redirected, but not the reverse. This is not compatible with using boost's tee. Fixes #8108 Fixes #8255
This commit is contained in:
parent
8d3a52eb3f
commit
7a3a72e8bd
3 changed files with 153 additions and 37 deletions
|
@ -640,6 +640,8 @@ static void setup_user_data_dir()
|
|||
create_directory_if_missing(get_saves_dir());
|
||||
create_directory_if_missing(get_wml_persist_dir());
|
||||
create_directory_if_missing(get_logs_dir());
|
||||
|
||||
lg::move_log_file();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
|
138
src/log.cpp
138
src/log.cpp
|
@ -21,19 +21,23 @@
|
|||
*/
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include "filesystem.hpp"
|
||||
#include "mt_rng.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include <boost/iostreams/tee.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
static lg::log_domain log_setup("logsetup");
|
||||
#define ERR_LS LOG_STREAM(err, log_setup)
|
||||
|
@ -58,9 +62,14 @@ static bool timestamp = true;
|
|||
static bool precise_timestamp = false;
|
||||
static std::mutex log_mutex;
|
||||
|
||||
/** whether the current logs directory is writable */
|
||||
static std::optional<bool> is_log_dir_writable_ = std::nullopt;
|
||||
/** alternative stream to write data to */
|
||||
static std::ostream *output_stream_ = nullptr;
|
||||
|
||||
/**
|
||||
* @return std::cerr if the redirect_output_setter isn't being used, output_stream_ if it is
|
||||
*/
|
||||
static std::ostream& output()
|
||||
{
|
||||
if(output_stream_) {
|
||||
|
@ -69,15 +78,10 @@ static std::ostream& output()
|
|||
return std::cerr;
|
||||
}
|
||||
|
||||
// custom deleter needed to reset cerr and cout
|
||||
// otherwise wesnoth segfaults on closing (such as clicking the Quit button on the main menu)
|
||||
// seems to be that there's a final flush done outside of wesnoth's code just before exiting
|
||||
// but at that point the output_file_ has already been cleaned up
|
||||
static std::unique_ptr<std::ostream, void(*)(std::ostream*)> output_file_(nullptr, [](std::ostream*){
|
||||
std::cerr.rdbuf(nullptr);
|
||||
std::cout.rdbuf(nullptr);
|
||||
});
|
||||
/** path to the current log file; does not include the extension */
|
||||
static std::string output_file_path_ = "";
|
||||
/** path to the current logs directory; may change after being initially set if a custom userdata directory is given on the command line */
|
||||
static std::string logs_dir_ = "";
|
||||
|
||||
namespace lg {
|
||||
|
||||
|
@ -87,16 +91,12 @@ std::ostringstream& operator<<(std::ostringstream& oss, const lg::severity sever
|
|||
return oss;
|
||||
}
|
||||
|
||||
/** Helper function for rotate_logs. */
|
||||
bool is_not_log_file(const std::string& fn)
|
||||
{
|
||||
return !(boost::algorithm::istarts_with(fn, lg::log_file_prefix) &&
|
||||
boost::algorithm::iends_with(fn, lg::log_file_suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes old log files from the log directory.
|
||||
*/
|
||||
void rotate_logs(const std::string& log_dir)
|
||||
{
|
||||
// if logging to file is disabled, don't rotate the logs
|
||||
|
@ -128,9 +128,6 @@ void rotate_logs(const std::string& log_dir)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique log file name.
|
||||
*/
|
||||
std::string unique_log_filename()
|
||||
{
|
||||
std::ostringstream o;
|
||||
|
@ -139,8 +136,7 @@ std::string unique_log_filename()
|
|||
|
||||
o << lg::log_file_prefix
|
||||
<< std::put_time(std::localtime(&cur), "%Y%m%d-%H%M%S-")
|
||||
<< rng.get_next_random()
|
||||
<< lg::log_file_suffix;
|
||||
<< rng.get_next_random();
|
||||
|
||||
return o.str();
|
||||
}
|
||||
|
@ -177,6 +173,62 @@ void check_log_dir_writable()
|
|||
is_log_dir_writable_ = true;
|
||||
}
|
||||
|
||||
void move_log_file()
|
||||
{
|
||||
if(logs_dir_ == filesystem::get_logs_dir() || logs_dir_ == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
check_log_dir_writable();
|
||||
|
||||
if(is_log_dir_writable_.value_or(false)) {
|
||||
#ifdef _WIN32
|
||||
std::string old_path = output_file_path_;
|
||||
output_file_path_ = filesystem::get_logs_dir()+"/"+unique_log_filename();
|
||||
|
||||
// flush and close existing log files, since Windows doesn't allow moving open files
|
||||
std::fflush(stderr);
|
||||
std::cerr.flush();
|
||||
if(!std::freopen("NUL", "a", stderr)) {
|
||||
std::cerr << "Failed to close stderr log file: '" << old_path << "'";
|
||||
// stderr is where basically all output goes through, so if that fails then don't attempt anything else
|
||||
// moving just the stdout log would be pointless
|
||||
return;
|
||||
}
|
||||
std::fflush(stdout);
|
||||
std::cout.flush();
|
||||
if(!std::freopen("NUL", "a", stdout)) {
|
||||
std::cerr << "Failed to close stdout log file: '" << old_path << "'";
|
||||
}
|
||||
|
||||
// move the .log and .out.log files
|
||||
// stdout and stderr are set to NUL currently so nowhere to send info on failure
|
||||
if(rename((old_path+lg::log_file_suffix).c_str(), (output_file_path_+lg::log_file_suffix).c_str()) == -1) {
|
||||
return;
|
||||
}
|
||||
rename((old_path+lg::out_log_file_suffix).c_str(), (output_file_path_+lg::out_log_file_suffix).c_str());
|
||||
|
||||
// reopen to log files at new location
|
||||
// stdout and stderr are still NUL if freopen fails, so again nowhere to send info on failure
|
||||
std::fflush(stderr);
|
||||
std::cerr.flush();
|
||||
std::freopen((output_file_path_+lg::log_file_suffix).c_str(), "a", stderr);
|
||||
|
||||
std::fflush(stdout);
|
||||
std::cout.flush();
|
||||
std::freopen((output_file_path_+lg::out_log_file_suffix).c_str(), "a", stdout);
|
||||
#else
|
||||
std::string old_path = get_log_file_path();
|
||||
output_file_path_ = filesystem::get_logs_dir()+"/"+unique_log_filename();
|
||||
|
||||
// non-Windows can just move the file
|
||||
if(rename(old_path.c_str(), get_log_file_path().c_str()) == -1) {
|
||||
std::cerr << "Failed to rename log file from '" << old_path << "' to '" << output_file_path_ << "'";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void set_log_to_file()
|
||||
{
|
||||
check_log_dir_writable();
|
||||
|
@ -185,14 +237,40 @@ void set_log_to_file()
|
|||
// if the optional isn't set, then logging to file has been disabled, so don't try to do anything
|
||||
if(is_log_dir_writable_.value_or(false)) {
|
||||
// get the log file stream and assign cerr+cout to it
|
||||
logs_dir_ = filesystem::get_logs_dir();
|
||||
output_file_path_ = filesystem::get_logs_dir()+"/"+unique_log_filename();
|
||||
static std::unique_ptr<std::ostream> logfile { filesystem::ostream_file(output_file_path_) };
|
||||
static std::ostream cerr_stream{std::cerr.rdbuf()};
|
||||
//static std::ostream cout_stream{std::cout.rdbuf()};
|
||||
auto cerr_tee { boost::iostreams::tee(*logfile, cerr_stream) };
|
||||
output_file_.reset(new boost::iostreams::stream<decltype(cerr_tee)>{cerr_tee, 4096, 0});
|
||||
std::cerr.rdbuf(output_file_.get()->rdbuf());
|
||||
std::cout.rdbuf(output_file_.get()->rdbuf());
|
||||
|
||||
// IMPORTANT: apparently redirecting stderr/stdout will also redirect std::cerr/std::cout, but the reverse is not true
|
||||
// redirecting std::cerr/std::cout will *not* redirect stderr/stdout
|
||||
|
||||
// redirect stderr to file
|
||||
std::fflush(stderr);
|
||||
std::cerr.flush();
|
||||
if(!std::freopen((output_file_path_+lg::log_file_suffix).c_str(), "w", stderr)) {
|
||||
std::cerr << "Failed to redirect stderr to a file!";
|
||||
}
|
||||
|
||||
// redirect stdout to file
|
||||
// separate handling for Windows since dup2() just... doesn't work for GUI apps there apparently
|
||||
// redirect to a separate file on Windows as well, since otherwise two streams independently writing to the same file can cause weirdness
|
||||
#ifdef _WIN32
|
||||
std::fflush(stdout);
|
||||
std::cout.flush();
|
||||
if(!std::freopen((output_file_path_+lg::out_log_file_suffix).c_str(), "w", stdout)) {
|
||||
std::cerr << "Failed to redirect stdout to a file!";
|
||||
}
|
||||
#else
|
||||
if(dup2(STDERR_FILENO, STDOUT_FILENO) == -1) {
|
||||
std::cerr << "Failed to redirect stdout to a file!";
|
||||
}
|
||||
#endif
|
||||
|
||||
// make stdout unbuffered - otherwise some output might be lost
|
||||
// in practice shouldn't make much difference either way, given how little output goes through stdout/std::cout
|
||||
if(setvbuf(stdout, NULL, _IONBF, 2) == -1) {
|
||||
std::cerr << "Failed to set stdout to be unbuffered";
|
||||
}
|
||||
|
||||
rotate_logs(filesystem::get_logs_dir());
|
||||
}
|
||||
}
|
||||
|
@ -202,13 +280,9 @@ std::optional<bool> log_dir_writable()
|
|||
return is_log_dir_writable_;
|
||||
}
|
||||
|
||||
std::string& get_log_file_path()
|
||||
std::string get_log_file_path()
|
||||
{
|
||||
return output_file_path_;
|
||||
}
|
||||
void set_log_file_path(const std::string& path)
|
||||
{
|
||||
output_file_path_ = path;
|
||||
return output_file_path_+lg::log_file_suffix;
|
||||
}
|
||||
|
||||
redirect_output_setter::redirect_output_setter(std::ostream& stream)
|
||||
|
|
50
src/log.hpp
50
src/log.hpp
|
@ -62,14 +62,21 @@
|
|||
|
||||
namespace lg {
|
||||
|
||||
// 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.
|
||||
// Prefix and extension for log files.
|
||||
// This is used to find old files to delete.
|
||||
const std::string log_file_prefix = "wesnoth-";
|
||||
const std::string log_file_suffix = ".log";
|
||||
// stdout file for Windows
|
||||
const std::string out_log_file_suffix = ".out.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;
|
||||
// double for Windows due to the separate .log and .out.log files
|
||||
const unsigned max_logs = 8
|
||||
#ifdef _WIN32
|
||||
*2
|
||||
#endif
|
||||
;
|
||||
|
||||
enum class severity
|
||||
{
|
||||
|
@ -129,12 +136,46 @@ std::string list_log_domains(const std::string& filter);
|
|||
void set_strict_severity(severity severity);
|
||||
void set_strict_severity(const logger &lg);
|
||||
bool broke_strict();
|
||||
|
||||
/**
|
||||
* Do the initial redirection to a log file if the logs directory is writable.
|
||||
* Also performs log rotation to delete old logs.
|
||||
* NOTE: This runs before command line arguments are processed.
|
||||
* Therefore the log file is initially written under the default userdata directory
|
||||
*/
|
||||
void set_log_to_file();
|
||||
/**
|
||||
* Move the log file to another directory.
|
||||
* Used if a custom userdata directory is given as a command line option to move it to the new location.
|
||||
*/
|
||||
void move_log_file();
|
||||
/**
|
||||
* Checks that a dummy file can be written to and deleted from the logs directory.
|
||||
*/
|
||||
void check_log_dir_writable();
|
||||
/**
|
||||
* Returns the result set by check_log_dir_writable().
|
||||
* Will not be set if called before log redirection is done.
|
||||
*
|
||||
* @return true if the log directory is writable, false otherwise.
|
||||
*/
|
||||
std::optional<bool> log_dir_writable();
|
||||
|
||||
/**
|
||||
* Use the defined prefix and suffix to determine if a filename is a log file.
|
||||
*
|
||||
* @return true if it's a log file, false otherwise
|
||||
*/
|
||||
bool is_not_log_file(const std::string& filename);
|
||||
/**
|
||||
* Check how many log files exist and delete the oldest when there's too many.
|
||||
*/
|
||||
void rotate_logs(const std::string& log_dir);
|
||||
/**
|
||||
* Generate a unique file name using the current timestamp and a randomly generated number.
|
||||
*
|
||||
* @return A unique file name to use for the current log file.
|
||||
*/
|
||||
std::string unique_log_filename();
|
||||
|
||||
// A little "magic" to surround the logging operation in a mutex.
|
||||
|
@ -188,8 +229,7 @@ void precise_timestamps(bool);
|
|||
std::string get_timestamp(const std::time_t& t, const std::string& format="%Y%m%d %H:%M:%S ");
|
||||
std::string get_timespan(const std::time_t& t);
|
||||
std::string sanitize_log(const std::string& logstr);
|
||||
std::string& get_log_file_path();
|
||||
void set_log_file_path(const std::string& path);
|
||||
std::string get_log_file_path();
|
||||
|
||||
logger &err(), &warn(), &info(), &debug();
|
||||
log_domain& general();
|
||||
|
|
Loading…
Add table
Reference in a new issue