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:
Pentarctagon 2024-02-17 09:28:08 -06:00 committed by GitHub
parent 8d3a52eb3f
commit 7a3a72e8bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 153 additions and 37 deletions

View file

@ -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

View file

@ -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)

View file

@ -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();