Adds the --log-to-file command line option.

This sets up writing both the cerr and cout streams to a log file, as well as pulls the log rotation logic out of log_windows.*pp into the general logger.

Note that this is meant for usage on macOS and Linux, not Windows. log_windows.*pp has additional functionality in it, as well as a fair bit related to the --wconsole option.
This commit is contained in:
Pentarctagon 2022-07-20 21:57:54 -05:00 committed by Pentarctagon
parent 178a09c2cc
commit d1833b5cf7
9 changed files with 123 additions and 103 deletions

View File

@ -190,6 +190,9 @@ lists defined log domains (only the ones containing
.I filter
if used) and exits
.TP
.B --log-to-file
redirects logged output to a file. Log files are created in the logs directory under the userdata folder.
.TP
.BI --max-fps \ fps
the number of frames per second the game can show, the value should be between
.B 1

View File

@ -257,6 +257,7 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
("log-debug", po::value<std::string>(), "sets the severity level of the specified log domain(s) to 'debug'. Similar to --log-error.")
("log-none", po::value<std::string>(), "sets the severity level of the specified log domain(s) to 'none'. Similar to --log-error.")
("log-precise", "shows the timestamps in log output with more precision.")
("log-to-file", "log output is written to a file rather than to standard error.")
;
po::options_description multiplayer_opts("Multiplayer options");
@ -268,7 +269,7 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
("era", po::value<std::string>(), "selects the era to be played in by its id.")
("exit-at-end", "exit Wesnoth at the end of the scenario.")
("ignore-map-settings", "do not use map settings.")
("label", po::value<std::string>(), "sets the label for AIs.") //TODO is the description precise? this option was undocumented before.
("label", po::value<std::string>(), "sets the label for AIs.") // TODO: is the description precise? this option was undocumented before.
("multiplayer-repeat", po::value<unsigned int>(), "repeats a multiplayer game after it is finished <arg> times.")
("nogui", "runs the game without the GUI.")
("parm", po::value<std::vector<std::string>>()->composing(), "sets additional parameters for this side. <arg> should have format side:name:value.")

View File

@ -583,9 +583,12 @@ 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");
create_directory_if_missing(filesystem::get_logs_dir());
#ifdef _WIN32
lg::finish_log_file_setup();
#else
lg::rotate_logs(filesystem::get_logs_dir());
#endif
}
@ -792,6 +795,11 @@ std::string get_user_data_dir()
return get_user_data_path().string();
}
std::string get_logs_dir()
{
return filesystem::get_user_data_dir() + "/logs";
}
std::string get_cache_dir()
{
if(cache_dir.empty()) {
@ -1042,7 +1050,6 @@ filesystem::scoped_istream istream_file(const std::string& fname, bool treat_fai
filesystem::scoped_ostream ostream_file(const std::string& fname, std::ios_base::openmode mode, bool create_directory)
{
LOG_FS << "streaming " << fname << " for writing.";
#if 1
try {
boost::iostreams::file_descriptor_sink fd(bfs::path(fname), mode);
return std::make_unique<boost::iostreams::stream<boost::iostreams::file_descriptor_sink>>(fd, 4096, 0);
@ -1056,9 +1063,6 @@ filesystem::scoped_ostream ostream_file(const std::string& fname, std::ios_base:
throw filesystem::io_exception(e.what());
}
#else
return new bfs::ofstream(bfs::path(fname), mode);
#endif
}
// Throws io_exception if an error occurs

View File

@ -165,6 +165,7 @@ void set_user_data_dir(std::string path);
std::string get_user_config_dir();
std::string get_user_data_dir();
std::string get_logs_dir();
std::string get_cache_dir();
struct other_version_dir

View File

@ -21,6 +21,10 @@
*/
#include "log.hpp"
#include "filesystem.hpp"
#include "mt_rng.hpp"
#include <boost/algorithm/string.hpp>
#include <map>
@ -30,6 +34,12 @@
#include <iostream>
#include <iomanip>
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)
namespace {
class null_streambuf : public std::streambuf
@ -47,27 +57,97 @@ static bool timestamp = true;
static bool precise_timestamp = false;
static std::mutex log_mutex;
static std::ostream *output_stream = nullptr;
static std::ostream *output_stream_ = nullptr;
static std::ostream& output()
{
if(output_stream) {
return *output_stream;
if(output_stream_) {
return *output_stream_;
}
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);
});
namespace lg {
redirect_output_setter::redirect_output_setter(std::ostream& stream)
: old_stream_(output_stream)
/** Helper function for rotate_logs. */
bool is_not_log_file(const std::string& fn)
{
output_stream = &stream;
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)
{
std::vector<std::string> 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() <= lg::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(std::size_t j = 0; j < files.size() - lg::max_logs; ++j) {
const std::string path = log_dir + '/' + files[j];
LOG_LS << "rotate_logs(): delete " << path;
if(!filesystem::delete_file(path)) {
ERR_LS << "rotate_logs(): failed to delete " << path << "!";
}
}
}
/**
* Generates a unique log file name.
*/
std::string unique_log_filename()
{
std::ostringstream o;
const std::time_t cur = std::time(nullptr);
randomness::mt_rng rng;
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;
return o.str();
}
void set_log_to_file()
{
// get the log file stream and assign cerr+cout to it
output_file_.reset(filesystem::ostream_file(filesystem::get_logs_dir()+"/"+unique_log_filename()).release());
std::cerr.rdbuf(output_file_.get()->rdbuf());
std::cout.rdbuf(output_file_.get()->rdbuf());
}
redirect_output_setter::redirect_output_setter(std::ostream& stream)
: old_stream_(output_stream_)
{
output_stream_ = &stream;
}
redirect_output_setter::~redirect_output_setter()
{
output_stream = old_stream_;
output_stream_ = old_stream_;
}
typedef std::map<std::string, int> domain_map;

View File

@ -61,6 +61,15 @@
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.
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;
enum severity
{
LG_ERROR=0,
@ -117,6 +126,11 @@ std::string list_logdomains(const std::string& filter);
void set_strict_severity(int severity);
void set_strict_severity(const logger &lg);
bool broke_strict();
void set_log_to_file();
bool is_not_log_file(const std::string& filename);
void rotate_logs(const std::string& log_dir);
std::string unique_log_filename();
// A little "magic" to surround the logging operation in a mutex.
// This works by capturing the output first to a stringstream formatter, then

View File

@ -44,91 +44,12 @@ static lg::log_domain log_setup("logsetup");
#define LOG_LS LOG_STREAM(info, log_setup)
#define DBG_LS LOG_STREAM(debug, log_setup)
namespace filesystem
{
std::string get_logs_dir()
{
return filesystem::get_user_data_dir() + "/logs";
}
}
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<std::string> 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(std::size_t j = 0; j < files.size() - max_logs; ++j) {
const std::string path = log_dir + '/' + files[j];
LOG_LS << "rotate_logs(): delete " << path;
if(!filesystem::delete_file(path)) {
WRN_LS << "rotate_logs(): failed to delete " << path << "!";
}
}
}
/**
* 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 << log_file_prefix;
const std::time_t cur = std::time(nullptr);
o << std::put_time(std::localtime(&cur), "%Y%m%d-%H%M%S-");
o << GetCurrentProcessId() << log_file_suffix;
return o.str();
}
/**
* Returns the path to a system-defined temporary files dir.
*/
@ -291,7 +212,7 @@ private:
};
log_file_manager::log_file_manager(bool native_console)
: fn_(unique_log_filename())
: fn_(lg::unique_log_filename())
, cur_path_()
, use_wincon_(console_attached())
, created_wincon_(false)
@ -531,7 +452,7 @@ void finish_log_file_setup()
log_init_panic(std::string("Could not create logs directory at ") +
log_dir + ".");
} else {
rotate_logs(log_dir);
lg::rotate_logs(log_dir);
}
lfm->move_log_file(log_dir);

View File

@ -39,16 +39,6 @@
* and later versions, requiring UAC virtualization to be enabled).
*/
namespace filesystem
{
/**
* Returns the path to the permanent log storage directory.
*/
std::string get_logs_dir();
}
namespace lg
{

View File

@ -986,12 +986,18 @@ int main(int argc, char** argv)
assert(!args.empty());
// --nobanner needs to be detected before the main command-line parsing happens
// --log-to needs to be detected so the logging output location is set before any actual logging happens
bool nobanner = false;
for(const auto& arg : args) {
if(arg == "--nobanner") {
nobanner = true;
break;
}
#ifndef _WIN32
else if(arg == "--log-to-file") {
lg::set_log_to_file();
}
#endif
}
#ifdef _WIN32