Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1870,7 +1870,8 @@ editing the `conf` file in a text editor. Use the examples as reference.
<tr>
<td>Description</td>
<td colspan="2">
The path where the Sunshine log is stored.
The path where the current Sunshine log is stored. Each time Sunshine starts, up to five previous
logs are retained by appending <code>.1</code> through <code>.5</code> to this path.
</td>
</tr>
<tr>
Expand Down
5 changes: 5 additions & 0 deletions src/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ namespace logging {
deinit();
}

const auto log_path = std::filesystem::path {std::u8string {log_file.begin(), log_file.end()}};
if (const auto rotation_error = rotate_log_file(log_path)) {
std::cerr << "Failed to rotate log file '" << log_file << "': " << rotation_error.message() << '\n';
}

#ifndef __ANDROID__
setup_av_logging(min_log_level);
setup_libdisplaydevice_logging(min_log_level);
Expand Down
50 changes: 49 additions & 1 deletion src/logging.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
*/
#pragma once

// standard includes
#include <cstddef>
#include <filesystem>
#include <format>
#include <string>
#include <system_error>

// lib includes
#include <boost/log/common.hpp>
#include <boost/log/sinks.hpp>
Expand All @@ -30,6 +37,47 @@ extern boost::log::sources::severity_logger<int> tests;
* @brief Handles the initialization and deinitialization of the logging system.
*/
namespace logging {
/**
* @brief The number of previous log files retained during rotation.
*/
inline constexpr std::size_t retained_log_file_count {5};

/**
* @brief Rotate a log file while retaining up to five previous logs.
*
* The current log is renamed with a `.1` suffix, existing rotated logs are
* advanced by one generation, and the previous `.5` log is removed.
*
* @param log_file Path to the current log file.
* @return An error code when rotation fails, or a clear error code on success.
*/
inline std::error_code rotate_log_file(const std::filesystem::path &log_file) noexcept {
const auto rotated_log_path = [&log_file](std::size_t generation) {
auto rotated_path = log_file;
rotated_path += std::format(".{}", generation);
return rotated_path;
};

try {
std::filesystem::remove(rotated_log_path(retained_log_file_count));

for (auto generation = retained_log_file_count; generation > 1; --generation) {
const auto previous_path = rotated_log_path(generation - 1);
if (std::filesystem::exists(previous_path)) {
std::filesystem::rename(previous_path, rotated_log_path(generation));
}
}

if (std::filesystem::exists(log_file)) {
std::filesystem::rename(log_file, rotated_log_path(1));
}

return {};
} catch (const std::filesystem::filesystem_error &filesystem_error) {
return filesystem_error.code();
}
}

/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
Expand Down Expand Up @@ -58,7 +106,7 @@ namespace logging {
void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os);

/**
* @brief Initialize the logging system.
* @brief Rotate the current log file and initialize the logging system.
* @param min_log_level The minimum log level to output.
* @param log_file The log file to write to.
* @return An object that will deinitialize the logging system when it goes out of scope.
Expand Down
4 changes: 0 additions & 4 deletions src/system_tray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ namespace system_tray {
platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher");
}

#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
/**
* @brief Forwards Qt log messages to Sunshine's BOOST_LOG logger.
* @param level Log level: 0=debug, 1=info, 2=warning, 3=error.
Expand All @@ -109,7 +108,6 @@ namespace system_tray {
break;
}
}
#endif

void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Resetting display device config from system tray"sv;
Expand Down Expand Up @@ -318,9 +316,7 @@ namespace system_tray {
tray.icon = tray.allIconPaths[0];
#endif

#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
tray_set_log_callback(qt_log_to_boost);
#endif

tray_set_app_info(PROJECT_NAME, PROJECT_NAME, PROJECT_FQDN);

Expand Down
2 changes: 1 addition & 1 deletion src_assets/common/assets/web/public/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@
"locale": "Locale",
"locale_desc": "The locale used for Sunshine's user interface.",
"log_path": "Logfile Path",
"log_path_desc": "The file where the current logs of Sunshine are stored.",
"log_path_desc": "The file where the current Sunshine log is stored. At startup, up to five previous logs are retained with .1 through .5 suffixes.",
"max_bitrate": "Maximum Bitrate",
"max_bitrate_desc": "The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.",
"minimum_fps_target": "Minimum FPS Target",
Expand Down
100 changes: 100 additions & 0 deletions tests/unit/test_logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
#include "../tests_common.h"
#include "../tests_log_checker.h"

#include <filesystem>
#include <format>
#include <fstream>
#include <iterator>
#include <random>
#include <src/logging.h>
#include <string>
#include <string_view>

namespace {
std::array log_levels = {
Expand All @@ -20,8 +25,103 @@ namespace {
};

constexpr auto log_file = "test_sunshine.log";

/**
* @brief Write test content to a log file.
*
* @param path Path to write.
* @param content Content to write.
*/
void write_log_file(const std::filesystem::path &path, std::string_view content) {
std::ofstream output {path};
output << content;
}

/**
* @brief Read all content from a test log file.
*
* @param path Path to read.
* @return File content.
*/
std::string read_log_file(const std::filesystem::path &path) {
std::ifstream input {path};
return {std::istreambuf_iterator<char> {input}, std::istreambuf_iterator<char> {}};
}
} // namespace

/**
* @brief Test fixture for startup log rotation.
*/
class LogRotationTest: public BaseTest {
protected:
/**
* @brief Create an empty directory for the current test.
*/
void SetUp() override {
BaseTest::SetUp();
std::filesystem::remove_all(test_directory);
std::filesystem::create_directories(test_directory);
}

/**
* @brief Remove files created by the current test.
*/
void TearDown() override {
std::filesystem::remove_all(test_directory);
BaseTest::TearDown();
}

/**
* @brief Build the path for a rotated test log.
*
* @param generation Rotated log generation number.
* @return Path with the generation suffix appended.
*/
std::filesystem::path rotated_log_path(std::size_t generation) const {
auto path = log_path;
path += std::format(".{}", generation);
return path;
}

const std::filesystem::path test_directory {std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "log_rotation_tests"}; ///< Directory containing log rotation test files.
const std::filesystem::path log_path {test_directory / "custom.log"}; ///< Path to the current test log.
};

TEST_F(LogRotationTest, RotatesCurrentLogAndRetainsFivePreviousLogs) {
write_log_file(log_path, "current");
for (std::size_t generation = 1; generation <= logging::retained_log_file_count; ++generation) {
write_log_file(rotated_log_path(generation), std::to_string(generation));
}

EXPECT_FALSE(logging::rotate_log_file(log_path));

EXPECT_FALSE(std::filesystem::exists(log_path));
EXPECT_EQ(read_log_file(rotated_log_path(1)), "current");
EXPECT_EQ(read_log_file(rotated_log_path(2)), "1");
EXPECT_EQ(read_log_file(rotated_log_path(3)), "2");
EXPECT_EQ(read_log_file(rotated_log_path(4)), "3");
EXPECT_EQ(read_log_file(rotated_log_path(5)), "4");
}

TEST_F(LogRotationTest, SupportsMissingLogGenerations) {
write_log_file(rotated_log_path(2), "second");

EXPECT_FALSE(logging::rotate_log_file(log_path));

EXPECT_FALSE(std::filesystem::exists(log_path));
EXPECT_FALSE(std::filesystem::exists(rotated_log_path(1)));
EXPECT_FALSE(std::filesystem::exists(rotated_log_path(2)));
EXPECT_EQ(read_log_file(rotated_log_path(3)), "second");
}

TEST_F(LogRotationTest, ReportsFilesystemErrors) {
std::filesystem::create_directories(rotated_log_path(logging::retained_log_file_count) / "child");
write_log_file(log_path, "current");

EXPECT_TRUE(logging::rotate_log_file(log_path));
EXPECT_EQ(read_log_file(log_path), "current");
}

struct LogLevelsTest: BaseTest, testing::WithParamInterface<decltype(log_levels)::value_type> {};

INSTANTIATE_TEST_SUITE_P(
Expand Down
8 changes: 7 additions & 1 deletion tools/sunshinesvc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include <Windows.h>
#include <WtsApi32.h>

// local includes
#include "src/logging.h"

// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers
#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
Expand Down Expand Up @@ -123,10 +126,13 @@ HANDLE OpenLogFileHandle() {
GetTempPathW(_countof(log_file_name), log_file_name);
wcscat_s(log_file_name, L"sunshine.log");

// Preserve previous service output before opening the current log.
logging::rotate_log_file(log_file_name);

// The file handle must be inheritable for our child process to use it
SECURITY_ATTRIBUTES security_attributes = {sizeof(security_attributes), nullptr, TRUE};

// Overwrite the old sunshine.log
// Create the current sunshine.log
return CreateFileW(log_file_name, GENERIC_WRITE, FILE_SHARE_READ, &security_attributes, CREATE_ALWAYS, 0, nullptr);
}

Expand Down
Loading