Skip to content

Bug fixes: Adds cache so reports aren't reported multiple times. Finally fixed #23#27

Merged
SimonCahill merged 6 commits into
masterfrom
feat/fix-multiple-reports
Jan 26, 2026
Merged

Bug fixes: Adds cache so reports aren't reported multiple times. Finally fixed #23#27
SimonCahill merged 6 commits into
masterfrom
feat/fix-multiple-reports

Conversation

@SimonCahill
Copy link
Copy Markdown
Owner

Fixes #23.
Fixes issue where automated reports reported entries multiple times.

@SimonCahill SimonCahill requested a review from Copilot January 26, 2026 20:32
@SimonCahill SimonCahill self-assigned this Jan 26, 2026
@SimonCahill SimonCahill added the bug Something isn't working label Jan 26, 2026
@SimonCahill SimonCahill merged commit 3829045 into master Jan 26, 2026
7 of 9 checks passed
@SimonCahill SimonCahill deleted the feat/fix-multiple-reports branch January 26, 2026 20:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes issue #23 where the endlessh-report tool incorrectly calculated connection counts and reported entries multiple times. The fix introduces a caching mechanism to track previously reported IPs and ensure they are only reported when their statistics change.

Changes:

  • Fixed the critical bug where CLOSE connections were always counted as 0 (line 378 in src/main.cpp)
  • Added a new ReportCache class to persistently track IP statistics and prevent duplicate reports
  • Introduced new command-line options: --strict, --cache-file, --sort, and --dns-lookup
  • Refactored IP normalization and DNS lookup functionality into a separate endlessh_core library
  • Added comprehensive unit and end-to-end tests with coverage reporting infrastructure

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/main.cpp Core bug fix for connection counting; integrated caching mechanism; refactored IP normalization
src/core.cpp New file with extracted IP normalization and DNS lookup functions
include/report_cache.hpp Cache implementation for tracking reported IP statistics
include/public_api.hpp Public API declarations for testable core functions
include/options.hpp Added new command-line options for cache control and sorting
include/extensions.hpp Refactored trim functions to use string_view and modern C++
tests/report_cache_test.cpp Unit tests for cache functionality
tests/main_extensions_test.cpp Unit tests for utility functions
tests/end_to_end_cache_test.cpp End-to-end tests verifying cache behavior
tests/CMakeLists.txt Test build configuration with GoogleTest integration
CMakeLists.txt Build system updates for library extraction and coverage support
scripts/coverage.sh Script for generating code coverage reports
Dockerfile Docker image for running tests and coverage in CI
.github/workflows/coverage.yml GitHub Actions workflow for automated coverage reporting
.dockerignore Docker build optimizations
README.md Updated documentation with new command-line options

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core.cpp
Comment on lines +14 to +19
string normalizeIp(string ip) {
const auto offset = ip.find("::ffff:");
if (offset != string::npos) {
ip = ip.substr(offset + 7);
}
return ip;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizeIp function takes its parameter by value (line 14), which causes an unnecessary copy of the string. Since the function modifies and returns the string, this is acceptable but could be optimized. However, changing this would affect the public API. Consider documenting this design choice or potentially offering an overload that takes a const reference if performance becomes a concern for large-scale usage.

Copilot uses AI. Check for mistakes.
Comment thread src/main.cpp
Comment on lines 556 to 575
for (const auto& connection : connectionList) {
totalAcceptedConnections += connection.second.first;
totalClosedConnections += connection.second.second;

if (g_printIpStatistics) {
string lastSpacer;
string tmpString;
const auto offset = connection.first.find("::ffff:");
if (offset != string::npos) {
tmpString = connection.first.substr(offset + 7);

tmpString = normalizeIp(connection.first);

if (g_strict && g_reportCache) {
IpStatsSnapshot snap;
snap.accepted = connection.second.first;
snap.closed = connection.second.second;
if (g_reportCache->hasUnchanged(tmpString, snap)) {
continue;
}
g_reportCache->update(tmpString, snap);
g_cacheDirty = true;
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When strict mode is enabled and an IP is skipped because its stats haven't changed (line 571), the connection totals are still accumulated (lines 557-558). This creates an inconsistency where the total counters include skipped IPs while the table only shows changed IPs. Consider moving the counter accumulation after the cache check, or documenting this behavior if intentional.

Copilot uses AI. Check for mistakes.
Comment thread scripts/coverage.sh
find . -name '*.gcda' -print0 | xargs -0 -n1 dirname | sort -u | while read -r dir; do
echo "Processing $dir"
# run gcov for all source files in the directory
(cd "$dir" && for f in $(find . -name '*.cpp' -o -name '*.c' 2>/dev/null) ; do true; done)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gcov fallback loop (line 40) doesn't actually run gcov on the found files. The loop body only contains true, which does nothing. This should be replaced with an actual gcov command, such as gcov "$f" or similar, to generate coverage information.

Suggested change
(cd "$dir" && for f in $(find . -name '*.cpp' -o -name '*.c' 2>/dev/null) ; do true; done)
(cd "$dir" && find . \( -name '*.cpp' -o -name '*.c' \) -print0 2>/dev/null | while IFS= read -r -d '' f; do gcov "$f"; done)

Copilot uses AI. Check for mistakes.
Comment thread include/extensions.hpp
Comment on lines +183 to 187
inline string& trimStart(string& nonTrimmed, const string_view trimChars) {
nonTrimmed.erase(nonTrimmed.begin(), std::find_if(nonTrimmed.begin(), nonTrimmed.end(), [&trimChars](int ch) { return trimChars.find(static_cast<char>(ch)) == string_view::npos; }));

return nonTrimmed;
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment for the trimChar parameter still references a single character and mentions a default value, but the function signature now uses trimChars (plural) of type string_view with no default value. The documentation should be updated to match the implementation.

Copilot uses AI. Check for mistakes.
Comment thread CMakeLists.txt
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

option(ENABLE_COVERAGE "Enable compilation flags for gcov coverage" ON)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ENABLE_COVERAGE option is set to ON by default (line 8), which means coverage instrumentation is always enabled unless explicitly disabled. This will cause a performance penalty in production builds. Consider setting the default to OFF and enabling it only for development or CI builds.

Suggested change
option(ENABLE_COVERAGE "Enable compilation flags for gcov coverage" ON)
option(ENABLE_COVERAGE "Enable compilation flags for gcov coverage" OFF)

Copilot uses AI. Check for mistakes.
Comment thread tests/CMakeLists.txt
${GTEST_MAIN_TARGET}
${GMOCK_TARGET}
fmt
endlessh_core
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endlessh_core library is listed outside the PRIVATE scope in the target_link_libraries command. It should be moved inside the PRIVATE section on line 43 to maintain consistency with the other dependencies.

Suggested change
endlessh_core
endlessh_core

Copilot uses AI. Check for mistakes.
Comment thread src/core.cpp
string host;
// By default the application runs without DNS lookups; keep behaviour
// identical to original implementation when lookups are disabled.
extern bool g_performDnsLookup; // declared in main.cpp
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extern declaration here is redundant and confusing. The global variable g_performDnsLookup is already defined in this file at line 12, and it's declared in the header file public_api.hpp at line 16. The extern declaration inside this function serves no purpose and may cause confusion about where the variable is actually defined.

Suggested change
extern bool g_performDnsLookup; // declared in main.cpp

Copilot uses AI. Check for mistakes.
Comment thread include/extensions.hpp
Comment on lines +196 to 200
inline string& trimEnd(string& nonTrimmed, const string_view trimChars) {
nonTrimmed.erase(std::find_if(nonTrimmed.rbegin(), nonTrimmed.rend(), [&trimChars](int ch) { return trimChars.find(static_cast<char>(ch)) == string_view::npos; }).base(), nonTrimmed.end());

return nonTrimmed;
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment for the trimChar parameter still references a single character and mentions a default value, but the function signature now uses trimChars (plural) of type string_view with no default value. The documentation should be updated to match the implementation.

Copilot uses AI. Check for mistakes.
Comment thread include/extensions.hpp
* @return The trimmed string.
*/
inline string trim(const string& nonTrimmed, const string& trimChar) { return trimStart(trimEnd(nonTrimmed, trimChar), trimChar); }
inline string& trim(string& nonTrimmed, const string_view trimChars) { return trimStart(trimEnd(nonTrimmed, trimChars), trimChars); }
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment for the trimChar parameter still references a single character and mentions a default value, but the function signature now uses trimChars (plural) of type string_view with no default value. The documentation should be updated to match the implementation.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +8
#include <string>

#include "extensions.hpp"
#include "public_api.hpp"

using std::string;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vector type is used without including the necessary header or using declaration. While the extensions.hpp header likely includes <vector> and has a using declaration, it's better practice to include the header and declare the using directive explicitly in the test file for clarity and to avoid relying on transitive includes.

Suggested change
#include <string>
#include "extensions.hpp"
#include "public_api.hpp"
using std::string;
#include <string>
#include <vector>
#include "extensions.hpp"
#include "public_api.hpp"
using std::string;
using std::vector;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue in connection calculation

2 participants