From f2025b4bafead29d4544ad3cff83498eaec16463 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 7 Aug 2026 19:45:01 -0400
Subject: [PATCH 1/2] feat(logging): rotating log files
---
docs/configuration.md | 3 +-
src/logging.cpp | 5 +
src/logging.h | 50 ++++++++-
.../assets/web/public/assets/locale/en.json | 2 +-
tests/unit/test_logging.cpp | 100 ++++++++++++++++++
tools/sunshinesvc.cpp | 8 +-
6 files changed, 164 insertions(+), 4 deletions(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index ed503973a4b..e03d9eccedf 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1870,7 +1870,8 @@ editing the `conf` file in a text editor. Use the examples as reference.
| Description |
- 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 .1 through .5 to this path.
|
diff --git a/src/logging.cpp b/src/logging.cpp
index f5f9eaf7b41..814c168eb3a 100644
--- a/src/logging.cpp
+++ b/src/logging.cpp
@@ -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);
diff --git a/src/logging.h b/src/logging.h
index 7cfa381948c..cb33988b4dd 100644
--- a/src/logging.h
+++ b/src/logging.h
@@ -4,6 +4,13 @@
*/
#pragma once
+// standard includes
+#include
+#include
+#include
+#include
+#include
+
// lib includes
#include
#include
@@ -30,6 +37,47 @@ extern boost::log::sources::severity_logger 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.
*/
@@ -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.
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 8bb1005c7d1..7de1459e7e1 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -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",
diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp
index e1ffb6adc63..1b11703638f 100644
--- a/tests/unit/test_logging.cpp
+++ b/tests/unit/test_logging.cpp
@@ -5,9 +5,14 @@
#include "../tests_common.h"
#include "../tests_log_checker.h"
+#include
#include
+#include
+#include
#include
#include
+#include
+#include
namespace {
std::array log_levels = {
@@ -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 {input}, std::istreambuf_iterator {}};
+ }
} // 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 {};
INSTANTIATE_TEST_SUITE_P(
diff --git a/tools/sunshinesvc.cpp b/tools/sunshinesvc.cpp
index cbc9d597450..e23ecf9add5 100644
--- a/tools/sunshinesvc.cpp
+++ b/tools/sunshinesvc.cpp
@@ -8,6 +8,9 @@
#include
#include
+// 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)
@@ -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);
}
From d76b9ef3e1256fe9dcd927a29c85b86a73af45d9 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 7 Aug 2026 20:18:25 -0400
Subject: [PATCH 2/2] fix(tray): use qt logging callback on all platforms
---
src/system_tray.cpp | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/system_tray.cpp b/src/system_tray.cpp
index 5f5287883d1..55fca243ea8 100644
--- a/src/system_tray.cpp
+++ b/src/system_tray.cpp
@@ -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.
@@ -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;
@@ -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);