From 569e48b60eeb4bebfa10b47982b426dfab79a30d Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Sun, 8 Feb 2026 23:15:20 +0800 Subject: [PATCH 1/6] Add log caching mechanism with atomic variables Implement log caching with atomic operations to avoid unnecessary file reads and ensure thread-safe access. --- src/confighttp.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 85d66077e49..f34cb696658 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -11,6 +11,7 @@ #include #include #include +#include // lib includes #include @@ -1107,12 +1108,72 @@ namespace confighttp { print_req(request); - std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); + // Log caching: avoid reading disk unnecessarily when file hasn't changed + // Use std::atomic to ensure thread-safe access (no locks) + static std::atomic> cached_log; + static std::atomic cached_log_size { 0 }; + static std::atomic cached_log_mtime_ns { 0 }; + + const auto &log_path = config::sunshine.log_file; + + // Check file status + std::error_code ec; + auto current_size = std::filesystem::file_size(log_path, ec); + if (ec) { + response->write(SimpleWeb::StatusCode::server_error_internal_server_error, "Failed to read log file"); + return; + } + auto current_mtime = std::filesystem::last_write_time(log_path, ec); + auto current_mtime_ns = current_mtime.time_since_epoch().count(); + + // Read file again if it has changed + if (current_size != cached_log_size.load() || current_mtime_ns != cached_log_mtime_ns.load()) { + auto new_content = std::make_shared(file_handler::read_file(log_path.c_str())); + cached_log.store(new_content); + cached_log_size.store(current_size); + cached_log_mtime_ns.store(current_mtime_ns); + } + + // Atomic load shared_ptr, subsequent operations based on this snapshot + auto content = cached_log.load(); + if (!content) { + response->write(SimpleWeb::StatusCode::server_error_internal_server_error, "Log not available"); + return; + } + + // Read client's offset from request header + std::uintmax_t client_offset = 0; + auto it = request->header.find("X-Log-Offset"); + if (it != request->header.end()) { + try { + client_offset = std::stoull(it->second); + } + catch (...) { + client_offset = 0; + } + } + SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/plain"); + headers.emplace("X-Log-Size", std::to_string(content->size())); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); - response->write(SimpleWeb::StatusCode::success_ok, content, headers); + + // offset equals current size: no change in logs, return 304 + if (client_offset > 0 && client_offset == content->size()) { + response->write(SimpleWeb::StatusCode::redirection_not_modified, headers); + return; + } + + // Valid offset and within range: return increment + if (client_offset > 0 && client_offset < content->size()) { + auto delta = content->substr(client_offset); + response->write(SimpleWeb::StatusCode::success_ok, delta, headers); + } + else { + // Invalid offset (file rotation/first request): return full content + response->write(SimpleWeb::StatusCode::success_ok, *content, headers); + } } /** From e3b4f3ee41719bff393ec8a371af81d819f62d8f Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Sun, 8 Feb 2026 23:37:41 +0800 Subject: [PATCH 2/6] Enhance log fetching and refresh mechanism --- src_assets/common/assets/web/index.html | 9 ++- .../common/assets/web/troubleshooting.html | 62 ++++++++++++++++--- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src_assets/common/assets/web/index.html b/src_assets/common/assets/web/index.html index 7a02b34fc36..eae05be8516 100644 --- a/src_assets/common/assets/web/index.html +++ b/src_assets/common/assets/web/index.html @@ -205,9 +205,14 @@
{{ githubVersion.release.name }}
console.error(e); } try { - this.logs = (await fetch("./api/logs").then(r => r.text())) + const response = await fetch("./api/logs"); + if (!response.ok) { + console.error('Failed to fetch logs: HTTP', response.status); + } else { + this.logs = await response.text(); + } } catch (e) { - console.error(e); + console.error('Failed to fetch logs:', e); } this.loading = false; }, diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index c33b3db24bc..beea841dbd6 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -237,6 +237,7 @@

{{ $t('troubleshooting.logs') }}

logs: 'Loading...', logFilter: null, logInterval: null, + logOffset: 0, restartPressed: false, showApplyMessage: false, platform: "", @@ -386,22 +387,63 @@

{{ $t('troubleshooting.logs') }}

} }); - this.logInterval = setInterval(() => { - this.refreshLogs(); - }, 5000); this.refreshLogs(); + this.startLogRefresh(); this.refreshClients(); + + const handleVisibilityChange = () => { + if (document.hidden) { + this.stopLogRefresh(); + } else { + this.refreshLogs(); + this.startLogRefresh(); + } + }; + document.addEventListener('visibilitychange', handleVisibilityChange); + this._visibilityCleanup = () => document.removeEventListener('visibilitychange', handleVisibilityChange); }, beforeDestroy() { - clearInterval(this.logInterval); + this.stopLogRefresh(); + if (this._visibilityCleanup) this._visibilityCleanup(); }, methods: { - refreshLogs() { - fetch("./api/logs",) - .then((r) => r.text()) - .then((r) => { - this.logs = r; - }); + startLogRefresh() { + this.stopLogRefresh(); + this.logInterval = setInterval(() => this.refreshLogs(), 5000); + }, + stopLogRefresh() { + if (this.logInterval != null) { + clearInterval(this.logInterval); + this.logInterval = null; + } + }, + async refreshLogs() { + try { + const headers = this.logOffset > 0 ? { 'X-Log-Offset': String(this.logOffset) } : {}; + const response = await fetch('./api/logs', { headers }); + + if (response.status === 304) { + return; + } + + if (!response.ok) { + console.error('Failed to refresh logs: HTTP', response.status); + return; + } + + const newSize = parseInt(response.headers.get('X-Log-Size') || '0', 10); + const text = await response.text(); + + if (!this.logOffset || newSize < this.logOffset) { + this.logs = text; + } else if (text.length > 0) { + this.logs += text; + } + + this.logOffset = newSize; + } catch (e) { + console.error('Failed to refresh logs:', e); + } }, closeApp() { this.closeAppPressed = true; From 0bb6f82b3063668e64a64184cfaddf91b263986b Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 9 Feb 2026 08:33:21 +0800 Subject: [PATCH 3/6] ci lint --- src/confighttp.cpp | 6 +++--- src_assets/common/assets/web/index.html | 6 +++--- src_assets/common/assets/web/troubleshooting.html | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index f34cb696658..66808e8c9fc 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -1114,7 +1114,7 @@ namespace confighttp { static std::atomic cached_log_size { 0 }; static std::atomic cached_log_mtime_ns { 0 }; - const auto &log_path = config::sunshine.log_file; + const std::filesystem::path log_path(config::sunshine.log_file); // Check file status std::error_code ec; @@ -1128,7 +1128,7 @@ namespace confighttp { // Read file again if it has changed if (current_size != cached_log_size.load() || current_mtime_ns != cached_log_mtime_ns.load()) { - auto new_content = std::make_shared(file_handler::read_file(log_path.c_str())); + auto new_content = std::make_shared(file_handler::read_file(log_path.string().c_str())); cached_log.store(new_content); cached_log_size.store(current_size); cached_log_mtime_ns.store(current_mtime_ns); @@ -1148,7 +1148,7 @@ namespace confighttp { try { client_offset = std::stoull(it->second); } - catch (...) { + catch (const std::exception &) { client_offset = 0; } } diff --git a/src_assets/common/assets/web/index.html b/src_assets/common/assets/web/index.html index eae05be8516..57729b789cb 100644 --- a/src_assets/common/assets/web/index.html +++ b/src_assets/common/assets/web/index.html @@ -206,10 +206,10 @@
{{ githubVersion.release.name }}
} try { const response = await fetch("./api/logs"); - if (!response.ok) { - console.error('Failed to fetch logs: HTTP', response.status); - } else { + if (response.ok) { this.logs = await response.text(); + } else { + console.error('Failed to fetch logs: HTTP', response.status); } } catch (e) { console.error('Failed to fetch logs:', e); diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index beea841dbd6..b3b7377a422 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -431,7 +431,7 @@

{{ $t('troubleshooting.logs') }}

return; } - const newSize = parseInt(response.headers.get('X-Log-Size') || '0', 10); + const newSize = Number.parseInt(response.headers.get('X-Log-Size') || '0', 10); const text = await response.text(); if (!this.logOffset || newSize < this.logOffset) { From acbe1bbeb08878b3a781bd38baacfe00fa8b989c Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 9 Feb 2026 08:56:26 +0800 Subject: [PATCH 4/6] Implement incremental log reading to optimize log fetching --- src/confighttp.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 66808e8c9fc..b13cd3c8fe5 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // lib includes #include @@ -1094,6 +1095,30 @@ namespace confighttp { } } + /** + * @brief Try to read only the new tail of the log file and append to existing content. + * @return New content on success, nullptr on any failure (caller should fall back to full read). + */ + static std::shared_ptr try_incremental_log_read( + const std::filesystem::path &log_path, + std::uintmax_t prev_size, + std::uintmax_t current_size, + const std::shared_ptr &old_content) { + if (current_size <= prev_size || prev_size == 0 || !old_content) { + return nullptr; + } + std::ifstream in(log_path.string(), std::ios::binary); + if (!in || !in.seekg(static_cast(prev_size))) { + return nullptr; + } + const auto tail_len = static_cast(current_size - prev_size); + std::string tail(tail_len, '\0'); + if (!in.read(tail.data(), static_cast(tail_len))) { + return nullptr; + } + return std::make_shared(*old_content + tail); + } + /** * @brief Get the logs from the log file. * @param response The HTTP response object. @@ -1124,11 +1149,24 @@ namespace confighttp { return; } auto current_mtime = std::filesystem::last_write_time(log_path, ec); + if (ec) { + response->write(SimpleWeb::StatusCode::server_error_internal_server_error, "Failed to read log file"); + return; + } auto current_mtime_ns = current_mtime.time_since_epoch().count(); - // Read file again if it has changed - if (current_size != cached_log_size.load() || current_mtime_ns != cached_log_mtime_ns.load()) { - auto new_content = std::make_shared(file_handler::read_file(log_path.string().c_str())); + const auto prev_size = cached_log_size.load(); + const bool cache_stale = (current_size != prev_size || current_mtime_ns != cached_log_mtime_ns.load()); + if (cache_stale) { + auto new_content = try_incremental_log_read(log_path, prev_size, current_size, cached_log.load()); + if (!new_content) { + new_content = std::make_shared(file_handler::read_file(log_path.string().c_str())); + } + // If read returned empty, ensure file still exists (e.g. not deleted during read) + if (new_content->empty() && !std::filesystem::exists(log_path, ec)) { + response->write(SimpleWeb::StatusCode::server_error_internal_server_error, "Log file not available"); + return; + } cached_log.store(new_content); cached_log_size.store(current_size); cached_log_mtime_ns.store(current_mtime_ns); @@ -1148,7 +1186,10 @@ namespace confighttp { try { client_offset = std::stoull(it->second); } - catch (const std::exception &) { + catch (const std::invalid_argument &) { + client_offset = 0; + } + catch (const std::out_of_range &) { client_offset = 0; } } From 857d05f1ecaf24b4c57eb63be389fa2cc5cd5c09 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 9 Feb 2026 09:00:49 +0800 Subject: [PATCH 5/6] Implement incremental log reading to optimize log fetching --- src/confighttp.cpp | 11 +++++++++-- .../common/assets/web/troubleshooting.html | 16 +++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index b13cd3c8fe5..6c0c0bb3233 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -1179,12 +1179,16 @@ namespace confighttp { return; } - // Read client's offset from request header + // Read client's offset from request header (trim whitespace; invalid values => 0, then full response) std::uintmax_t client_offset = 0; auto it = request->header.find("X-Log-Offset"); if (it != request->header.end()) { try { - client_offset = std::stoull(it->second); + std::string offset_str(it->second); + boost::algorithm::trim(offset_str); + if (!offset_str.empty()) { + client_offset = std::stoull(offset_str); + } } catch (const std::invalid_argument &) { client_offset = 0; @@ -1202,17 +1206,20 @@ namespace confighttp { // offset equals current size: no change in logs, return 304 if (client_offset > 0 && client_offset == content->size()) { + headers.emplace("X-Log-Range", "unchanged"); response->write(SimpleWeb::StatusCode::redirection_not_modified, headers); return; } // Valid offset and within range: return increment if (client_offset > 0 && client_offset < content->size()) { + headers.emplace("X-Log-Range", "incremental"); auto delta = content->substr(client_offset); response->write(SimpleWeb::StatusCode::success_ok, delta, headers); } else { // Invalid offset (file rotation/first request): return full content + headers.emplace("X-Log-Range", "full"); response->write(SimpleWeb::StatusCode::success_ok, *content, headers); } } diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index b3b7377a422..164db272581 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -419,10 +419,14 @@

{{ $t('troubleshooting.logs') }}

}, async refreshLogs() { try { - const headers = this.logOffset > 0 ? { 'X-Log-Offset': String(this.logOffset) } : {}; + const offset = Number(this.logOffset); + const headers = (!Number.isNaN(offset) && offset > 0) ? { 'X-Log-Offset': String(offset) } : {}; const response = await fetch('./api/logs', { headers }); if (response.status === 304) { + const sizeHeader = response.headers.get('X-Log-Size'); + const size = Number.parseInt(sizeHeader || '0', 10); + this.logOffset = Number.isNaN(size) || size < 0 ? 0 : size; return; } @@ -431,13 +435,15 @@

{{ $t('troubleshooting.logs') }}

return; } - const newSize = Number.parseInt(response.headers.get('X-Log-Size') || '0', 10); + const rawSize = Number.parseInt(response.headers.get('X-Log-Size') || '0', 10); + const newSize = Number.isNaN(rawSize) || rawSize < 0 ? 0 : rawSize; + const logRange = (response.headers.get('X-Log-Range') || '').trim().toLowerCase(); const text = await response.text(); - if (!this.logOffset || newSize < this.logOffset) { - this.logs = text; - } else if (text.length > 0) { + if (logRange === 'incremental' && text.length > 0) { this.logs += text; + } else { + this.logs = text; } this.logOffset = newSize; From d8d297cfb5b60ba1cd573a4a47c81fc0520b1ef5 Mon Sep 17 00:00:00 2001 From: Yundi339 Date: Mon, 9 Feb 2026 09:10:24 +0800 Subject: [PATCH 6/6] ci lint --- src/confighttp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 6c0c0bb3233..0f4601c1e25 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -1111,7 +1111,7 @@ namespace confighttp { if (!in || !in.seekg(static_cast(prev_size))) { return nullptr; } - const auto tail_len = static_cast(current_size - prev_size); + const auto tail_len = current_size - prev_size; std::string tail(tail_len, '\0'); if (!in.read(tail.data(), static_cast(tail_len))) { return nullptr;