diff --git a/configure.ac b/configure.ac index 87faa26f..def460b3 100644 --- a/configure.ac +++ b/configure.ac @@ -79,6 +79,16 @@ AC_CHECK_FUNCS([memset strdup strerror]) PKG_CHECK_MODULES([LOG4C], [log4c >= 1.2.3]) + +dnl Log suppression feature +AC_ARG_ENABLE([log-suppression], + [AS_HELP_STRING([--enable-log-suppression], [Enable duplicate log suppression (default: no)])], + [enable_log_suppression=$enableval], + [enable_log_suppression=no]) +if test "x$enable_log_suppression" = "xyes"; then + AC_DEFINE([HAVE_LOG_SUPPRESSION], [1], [Define to enable log duplicate suppression]) +fi +AM_CONDITIONAL([HAVE_LOG_SUPPRESSION], [test "x$enable_log_suppression" = "xyes"]) AC_CONFIG_FILES([Makefile cfg/Makefile src/Makefile diff --git a/include/rdk_log_suppression.h b/include/rdk_log_suppression.h new file mode 100644 index 00000000..ece5ece4 --- /dev/null +++ b/include/rdk_log_suppression.h @@ -0,0 +1,119 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RDK_LOG_SUPPRESSION_H +#define RDK_LOG_SUPPRESSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** Maximum number of tracked patterns per category */ +#define RDK_SUPPRESS_RING_SIZE 8 + +/** Default suppression time window in seconds */ +#define RDK_SUPPRESS_DEFAULT_WINDOW_SEC 30 + +/** Default number of repeats before suppression activates */ +#define RDK_SUPPRESS_DEFAULT_THRESHOLD 3 + +/** Result of a suppression check */ +typedef enum { + RDK_SUPPRESS_PASS = 0, /**< Message should be logged normally */ + RDK_SUPPRESS_BLOCK = 1, /**< Message is suppressed (duplicate) */ + RDK_SUPPRESS_EMIT_SUMMARY = 2 /**< Emit summary then log current message */ +} rdk_SuppressResult; + +/** Suppression statistics for a module */ +typedef struct { + uint64_t total_suppressed; /**< Total messages suppressed */ + uint64_t total_summaries; /**< Total summary messages emitted */ + uint32_t current_repeat_count; /**< Current consecutive repeat count */ +} rdk_SuppressStats; + +/** + * Initialize the log suppression subsystem. + * Must be called after rdk_dbg_priv_config() during logger initialization. + * @return 0 on success, -1 on failure + */ +int rdk_log_suppression_init(void); + +/** + * Deinitialize the log suppression subsystem. + * Frees all allocated suppression state. + */ +void rdk_log_suppression_deinit(void); + +/** + * Check if a log message should be suppressed. + * Called from rdk_dbg_priv_log_msg() after priority check. + * + * @param module The log category name (e.g., "LOG.RDK.TR69") + * @param level The log level of the message + * @param fmt The format string (before printf expansion) + * @param[out] repeat_count If result is EMIT_SUMMARY, set to number of suppressed messages + * @return RDK_SUPPRESS_PASS, RDK_SUPPRESS_BLOCK, or RDK_SUPPRESS_EMIT_SUMMARY + */ +rdk_SuppressResult rdk_log_suppression_check(const char *module, int level, + const char *fmt, + uint32_t *repeat_count); + +/** + * Configure suppression for a specific module. + * + * @param module Module name (NULL for global default) + * @param enabled true to enable, false to disable + * @param window_sec Time window in seconds (0 = use default) + * @param threshold Messages before suppression kicks in (0 = use default) + * @return 0 on success, -1 on failure + */ +int rdk_log_suppression_configure(const char *module, bool enabled, + uint32_t window_sec, uint32_t threshold); + +/** + * Get suppression statistics for a module. + * + * @param module Module name (NULL for global stats) + * @param[out] stats Filled with current statistics + * @return 0 on success, -1 if module not found + */ +int rdk_log_suppression_get_stats(const char *module, rdk_SuppressStats *stats); + +/** + * Reset suppression state for a module (or all if module is NULL). + * Useful after configuration changes or for testing. + * + * @param module Module name (NULL to reset all) + */ +void rdk_log_suppression_reset(const char *module); + +/** + * Check if suppression is globally enabled. + * @return true if suppression is active + */ +bool rdk_log_suppression_is_enabled(void); + +#ifdef __cplusplus +} +#endif + +#endif /* RDK_LOG_SUPPRESSION_H */ diff --git a/scripts/logDedup.sh b/scripts/logDedup.sh new file mode 100755 index 00000000..b5c5fe09 --- /dev/null +++ b/scripts/logDedup.sh @@ -0,0 +1,65 @@ +#!/bin/sh +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2026 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +# logDedup.sh - Deduplicate log files before upload to reduce bandwidth +# Usage: logDedup.sh [log_directory] [threshold] +# +# Processes all .log and .txt files in the specified directory, +# replacing them with deduplicated versions. + +LOG_DIR="${1:-/rdklogs/logs}" +THRESHOLD="${2:-2}" +DEDUP_BIN="${DEDUP_BIN:-/usr/bin/rdkLogDedupSync}" +DEDUP_COUNT=0 + +if [ ! -d "$LOG_DIR" ]; then + echo "Error: Log directory not found: $LOG_DIR" + exit 1 +fi + +if [ ! -x "$DEDUP_BIN" ]; then + echo "Error: Dedup binary not found or not executable: $DEDUP_BIN" + exit 1 +fi + +for logfile in "$LOG_DIR"/*.log "$LOG_DIR"/*.txt "$LOG_DIR"/*.txt.*; do + [ -f "$logfile" ] || continue + + tmpfile="${logfile}.dedup.tmp" + + "$DEDUP_BIN" "$logfile" "$tmpfile" "$THRESHOLD" + ret=$? + + if [ $ret -eq 0 ]; then + # Dedup was applied - replace original + mv "$tmpfile" "$logfile" + DEDUP_COUNT=$((DEDUP_COUNT + 1)) + elif [ $ret -eq 2 ]; then + # No dedup needed - tmp already removed by utility + : + else + # Error + rm -f "$tmpfile" 2>/dev/null + echo "Warning: Dedup failed for $logfile" + fi +done + +echo "Log dedup complete: $DEDUP_COUNT files deduplicated in $LOG_DIR" +exit 0 diff --git a/src/Makefile.am b/src/Makefile.am index 6ca2fd2b..abfb2a47 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -20,6 +20,9 @@ AM_CFLAGS = -pthread -Wall -Werror lib_LTLIBRARIES = librdkloggers.la librdkloggers_la_SOURCES = rdk_debug.c rdk_debug_priv.c rdk_dynamic_logger.c rdk_logger_init.c rdk_logger_milestone.c rdk_logger_onboard.c +if HAVE_LOG_SUPPRESSION +librdkloggers_la_SOURCES += rdk_log_suppression.c +endif librdkloggers_la_CFLAGS = $(LOG4C_CFLAGS) $(GLIB_CFLAGS) $(SYSTEMD_CFLAGS) -I$(top_srcdir)/include -I$(top_srcdir)/src/include -DDEBUG_CONF_FILE="\"debug.ini\"" librdkloggers_la_LDFLAGS = $(GLIB_LIBS) $(LOG4C_LIBS) $(SYSTEMD_LIBS) diff --git a/src/rdk_debug_priv.c b/src/rdk_debug_priv.c index 5775c90a..46e9ec92 100644 --- a/src/rdk_debug_priv.c +++ b/src/rdk_debug_priv.c @@ -49,14 +49,19 @@ #include "rdk_debug_priv.h" #include "rdk_dynamic_logger.h" #include "log4c.h" -#include -#include -#include #ifdef HAVE_CONFIG_H #include #endif +#ifdef HAVE_LOG_SUPPRESSION +#include "rdk_log_suppression.h" +#endif +#include +#include +#include + + #ifdef HAVE_SYSTEMD #include #endif //HAVE_SYSTEMD @@ -649,6 +654,19 @@ void rdk_dbg_priv_log_msg(rdk_LogLevel level, const char *module_name, const cha int log4cPriority = rdk_logLevel_to_log4c_priority(level); if (log4c_category_is_priority_enabled(cat, log4cPriority)) { +#ifdef HAVE_LOG_SUPPRESSION + uint32_t repeat_count = 0; + rdk_SuppressResult suppress_result = rdk_log_suppression_check(module_name, level, format, &repeat_count); + if (suppress_result == RDK_SUPPRESS_BLOCK) + { + pthread_mutex_unlock(&gLoggingMutex); + return; + } + if (suppress_result == RDK_SUPPRESS_EMIT_SUMMARY) + { + log4c_category_log(cat, log4cPriority, "Previous message repeated %u times", repeat_count); + } +#endif log4c_category_vlog(cat, log4cPriority, format, args); } } diff --git a/src/rdk_log_suppression.c b/src/rdk_log_suppression.c new file mode 100644 index 00000000..74ef6277 --- /dev/null +++ b/src/rdk_log_suppression.c @@ -0,0 +1,358 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "rdk_log_suppression.h" +#include +#include +#include +#include + +/* Maximum number of categories tracked for suppression */ +#define MAX_SUPPRESS_CATEGORIES 64 + +/* FNV-1a hash parameters (32-bit) */ +#define FNV_OFFSET_BASIS 0x811c9dc5u +#define FNV_PRIME 0x01000193u + +/** Single entry in the per-category ring buffer */ +typedef struct { + uint32_t pattern_hash; /**< FNV-1a hash of (module + level + fmt) */ + uint32_t repeat_count; /**< Number of consecutive repeats */ + struct timespec first_seen; /**< Timestamp of first occurrence */ + struct timespec last_seen; /**< Timestamp of most recent occurrence */ + bool active; /**< Slot is in use */ +} suppress_entry_t; + +/** Per-category suppression state */ +typedef struct { + char module[128]; /**< Module/category name */ + suppress_entry_t ring[RDK_SUPPRESS_RING_SIZE]; /**< Ring buffer of tracked patterns */ + uint32_t ring_head; /**< Next slot to write */ + bool enabled; /**< Suppression enabled for this category */ + uint32_t window_sec; /**< Time window in seconds */ + uint32_t threshold; /**< Repeats before suppression */ + uint64_t total_suppressed; /**< Lifetime counter */ + uint64_t total_summaries; /**< Lifetime summary count */ +} suppress_category_t; + +/** Global suppression state */ +static struct { + bool initialized; + bool globally_enabled; + uint32_t default_window_sec; + uint32_t default_threshold; + suppress_category_t categories[MAX_SUPPRESS_CATEGORIES]; + int num_categories; +} g_suppress = { + .initialized = false, + .globally_enabled = false, + .default_window_sec = RDK_SUPPRESS_DEFAULT_WINDOW_SEC, + .default_threshold = RDK_SUPPRESS_DEFAULT_THRESHOLD, + .num_categories = 0 +}; + +/* fnv1a_hash() removed (unused) */ + +/** + * Compute pattern hash from module + level + format string. + */ +static uint32_t compute_pattern_hash(const char *module, int level, const char *fmt) +{ + uint32_t hash = FNV_OFFSET_BASIS; + + /* Hash module name */ + if (module) { + const unsigned char *p = (const unsigned char *)module; + while (*p) { + hash ^= *p++; + hash *= FNV_PRIME; + } + } + + /* Hash level */ + hash ^= (uint32_t)level; + hash *= FNV_PRIME; + + /* Hash format string */ + if (fmt) { + const unsigned char *p = (const unsigned char *)fmt; + while (*p) { + hash ^= *p++; + hash *= FNV_PRIME; + } + } + + return hash; +} + +/** + * Get elapsed seconds between two timespecs. + */ +static double timespec_diff_sec(const struct timespec *end, const struct timespec *start) +{ + return (double)(end->tv_sec - start->tv_sec) + + (double)(end->tv_nsec - start->tv_nsec) / 1e9; +} + +/** + * Find or create a category entry. + */ +static suppress_category_t *find_or_create_category(const char *module) +{ + if (!module) return NULL; + + /* Search existing */ + for (int i = 0; i < g_suppress.num_categories; i++) { + if (strcmp(g_suppress.categories[i].module, module) == 0) { + return &g_suppress.categories[i]; + } + } + + /* Create new if space available */ + if (g_suppress.num_categories >= MAX_SUPPRESS_CATEGORIES) { + return NULL; + } + + suppress_category_t *cat = &g_suppress.categories[g_suppress.num_categories]; + memset(cat, 0, sizeof(*cat)); + strncpy(cat->module, module, sizeof(cat->module) - 1); + cat->module[sizeof(cat->module) - 1] = '\0'; + cat->enabled = true; /* inherit global setting */ + cat->window_sec = g_suppress.default_window_sec; + cat->threshold = g_suppress.default_threshold; + g_suppress.num_categories++; + + return cat; +} + +int rdk_log_suppression_init(void) +{ + if (g_suppress.initialized) { + return 0; + } + + memset(g_suppress.categories, 0, sizeof(g_suppress.categories)); + g_suppress.num_categories = 0; + g_suppress.globally_enabled = false; + g_suppress.default_window_sec = RDK_SUPPRESS_DEFAULT_WINDOW_SEC; + g_suppress.default_threshold = RDK_SUPPRESS_DEFAULT_THRESHOLD; + g_suppress.initialized = true; + + return 0; +} + +void rdk_log_suppression_deinit(void) +{ + g_suppress.initialized = false; + g_suppress.globally_enabled = false; + g_suppress.num_categories = 0; +} + +rdk_SuppressResult rdk_log_suppression_check(const char *module, int level, + const char *fmt, + uint32_t *repeat_count) +{ + if (!g_suppress.initialized || !g_suppress.globally_enabled) { + return RDK_SUPPRESS_PASS; + } + + if (!module || !fmt) { + return RDK_SUPPRESS_PASS; + } + + suppress_category_t *cat = find_or_create_category(module); + if (!cat || !cat->enabled) { + return RDK_SUPPRESS_PASS; + } + + uint32_t hash = compute_pattern_hash(module, level, fmt); + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + /* Search ring buffer for matching pattern */ + for (int i = 0; i < RDK_SUPPRESS_RING_SIZE; i++) { + suppress_entry_t *entry = &cat->ring[i]; + + if (!entry->active || entry->pattern_hash != hash) { + continue; + } + + /* Found matching pattern - check time window */ + double elapsed = timespec_diff_sec(&now, &entry->first_seen); + + if (elapsed > (double)cat->window_sec) { + /* Window expired - emit summary if we suppressed, then reset */ + if (entry->repeat_count > cat->threshold) { + uint32_t suppressed = entry->repeat_count - cat->threshold; + *repeat_count = suppressed; + } + cat->total_summaries++; + /* Reset entry for new window */ + entry->first_seen = now; + entry->last_seen = now; + entry->repeat_count = 1; + return RDK_SUPPRESS_EMIT_SUMMARY; + } + /* Window expired but below threshold - reset */ + entry->first_seen = now; + entry->last_seen = now; + entry->repeat_count = 1; + return RDK_SUPPRESS_PASS; + } + + /* Within window - increment count */ + entry->repeat_count++; + entry->last_seen = now; + + if (entry->repeat_count > cat->threshold) { + /* Suppress this message */ + cat->total_suppressed++; + if (repeat_count) { + *repeat_count = entry->repeat_count - cat->threshold; + } + return RDK_SUPPRESS_BLOCK; + } + + /* Below threshold - let it through */ + return RDK_SUPPRESS_PASS; + } + + /* Pattern not found - add to ring buffer */ + suppress_entry_t *slot = &cat->ring[cat->ring_head]; + + /* If overwriting an active entry that was suppressing, emit summary */ + if (slot->active && slot->repeat_count >= cat->threshold) { + /* We lose this suppression context - acceptable trade-off for bounded memory */ + } + + slot->pattern_hash = hash; + slot->repeat_count = 1; + slot->first_seen = now; + slot->last_seen = now; + slot->active = true; + + cat->ring_head = (cat->ring_head + 1) % RDK_SUPPRESS_RING_SIZE; + + return RDK_SUPPRESS_PASS; +} + +int rdk_log_suppression_configure(const char *module, bool enabled, + uint32_t window_sec, uint32_t threshold) +{ + if (!g_suppress.initialized) { + return -1; + } + + if (!module) { + /* Global configuration */ + g_suppress.globally_enabled = enabled; + if (window_sec > 0) { + g_suppress.default_window_sec = window_sec; + } + if (threshold > 0) { + g_suppress.default_threshold = threshold; + } + return 0; + } + + suppress_category_t *cat = find_or_create_category(module); + if (!cat) { + return -1; + } + + cat->enabled = enabled; + if (window_sec > 0) { + cat->window_sec = window_sec; + } + if (threshold > 0) { + cat->threshold = threshold; + } + + return 0; +} + +int rdk_log_suppression_get_stats(const char *module, rdk_SuppressStats *stats) +{ + if (!g_suppress.initialized || !stats) { + return -1; + } + + if (!module) { + /* Aggregate stats */ + memset(stats, 0, sizeof(*stats)); + for (int i = 0; i < g_suppress.num_categories; i++) { + stats->total_suppressed += g_suppress.categories[i].total_suppressed; + stats->total_summaries += g_suppress.categories[i].total_summaries; + } + return 0; + } + + for (int i = 0; i < g_suppress.num_categories; i++) { + if (strcmp(g_suppress.categories[i].module, module) == 0) { + stats->total_suppressed = g_suppress.categories[i].total_suppressed; + stats->total_summaries = g_suppress.categories[i].total_summaries; + /* Find current active repeat count for this module */ + stats->current_repeat_count = 0; + for (int j = 0; j < RDK_SUPPRESS_RING_SIZE; j++) { + if (g_suppress.categories[i].ring[j].active && + g_suppress.categories[i].ring[j].repeat_count > stats->current_repeat_count) { + stats->current_repeat_count = g_suppress.categories[i].ring[j].repeat_count; + } + } + return 0; + } + } + + return -1; +} + +void rdk_log_suppression_reset(const char *module) +{ + if (!g_suppress.initialized) { + return; + } + + if (!module) { + /* Reset all */ + for (int i = 0; i < g_suppress.num_categories; i++) { + memset(g_suppress.categories[i].ring, 0, + sizeof(g_suppress.categories[i].ring)); + g_suppress.categories[i].ring_head = 0; + g_suppress.categories[i].total_suppressed = 0; + g_suppress.categories[i].total_summaries = 0; + } + return; + } + + for (int i = 0; i < g_suppress.num_categories; i++) { + if (strcmp(g_suppress.categories[i].module, module) == 0) { + memset(g_suppress.categories[i].ring, 0, + sizeof(g_suppress.categories[i].ring)); + g_suppress.categories[i].ring_head = 0; + g_suppress.categories[i].total_suppressed = 0; + g_suppress.categories[i].total_summaries = 0; + return; + } + } +} + +bool rdk_log_suppression_is_enabled(void) +{ + return g_suppress.initialized && g_suppress.globally_enabled; +} diff --git a/src/rdk_logger_init.c b/src/rdk_logger_init.c index c983751e..2bf3ffda 100644 --- a/src/rdk_logger_init.c +++ b/src/rdk_logger_init.c @@ -35,9 +35,15 @@ #include #include #include +#ifdef HAVE_CONFIG_H +#include +#endif #include "rdk_logger.h" #include "rdk_debug_priv.h" #include "rdk_dynamic_logger.h" +#ifdef HAVE_LOG_SUPPRESSION +#include "rdk_log_suppression.h" +#endif static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER; @@ -73,6 +79,9 @@ rdk_Error rdk_logger_init(const char* debugConfigFile) { /* Perform Dynamic Logger Internal Init */ rdk_dyn_log_init(); +#ifdef HAVE_LOG_SUPPRESSION + rdk_log_suppression_init(); +#endif isLogInited = true; /** @@ -118,6 +127,9 @@ rdk_Error rdk_logger_deinit() if (isLogInited) { rdk_dyn_log_deinit(); +#ifdef HAVE_LOG_SUPPRESSION + rdk_log_suppression_deinit(); +#endif } pthread_mutex_unlock(&gInitMutex); diff --git a/utils/rdk_log_dedup_sync.c b/utils/rdk_log_dedup_sync.c new file mode 100644 index 00000000..00d23e11 --- /dev/null +++ b/utils/rdk_log_dedup_sync.c @@ -0,0 +1,161 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file rdk_log_dedup_sync.c + * @brief Pre-upload log deduplication utility. + * + * Reads a log file, identifies consecutive duplicate lines, + * and writes a deduplicated version with "repeated N times" annotations. + * Intended to be run before log files are uploaded to reduce bandwidth. + * + * Usage: rdk_log_dedup_sync [threshold] + * Exit codes: 0 = success, 1 = error, 2 = no changes needed + +#include +#include +#include +#include +#include +#include +#include + +#define MAX_LINE_LEN 4096 +#define DEFAULT_THRESHOLD 2 + +static void print_usage(const char *progname) +{ + fprintf(stderr, "Usage: %s [threshold]\n", progname); + fprintf(stderr, " input_file: Path to log file to deduplicate\n"); + fprintf(stderr, " output_file: Path to write deduplicated output\n"); + fprintf(stderr, " threshold: Minimum repeats before suppression (default: %d)\n", + DEFAULT_THRESHOLD); +} + +int main(int argc, char *argv[]) +{ + if (argc < 3 || argc > 4) { + print_usage(argv[0]); + return 1; + } + + const char *input_path = argv[1]; + const char *output_path = argv[2]; + int threshold = DEFAULT_THRESHOLD; + + if (strcmp(input_path, output_path) == 0) { + fprintf(stderr, "Error: input_file and output_file must be different paths\n"); + return 1; + } + + if (argc == 4) { + threshold = atoi(argv[3]); + if (threshold < 1) { + fprintf(stderr, "Error: threshold must be >= 1\n"); + return 1; + } + } + + FILE *fin = fopen(input_path, "r"); + if (!fin) { + fprintf(stderr, "Error: Cannot open input file: %s\n", input_path); + return 1; + } + + struct stat in_st; + mode_t out_mode = S_IRUSR | S_IWUSR; + if (stat(input_path, &in_st) == 0) { + out_mode = in_st.st_mode & 0777; + } + + int fd_out = open(output_path, O_WRONLY | O_CREAT | O_TRUNC, out_mode); + if (fd_out < 0) { + fprintf(stderr, "Error: Cannot open output file: %s\n", output_path); + fclose(fin); + return 1; + } + + FILE *fout = fdopen(fd_out, "w"); + if (!fout) { + fprintf(stderr, "Error: Cannot open output file stream: %s\n", output_path); + close(fd_out); + fclose(fin); + return 1; + } + + char current_line[MAX_LINE_LEN]; + char prev_line[MAX_LINE_LEN] = {0}; + int repeat_count = 0; + bool has_prev = false; + bool any_dedup = false; + + while (fgets(current_line, sizeof(current_line), fin)) { + if (has_prev && strcmp(current_line, prev_line) == 0) { + /* Duplicate line */ + repeat_count++; + } else { + /* Different line - flush previous */ + if (has_prev) { + fputs(prev_line, fout); + if (repeat_count >= threshold) { + fprintf(fout, "--- Previous message repeated %d times ---\n", + repeat_count); + any_dedup = true; + } else { + /* Below threshold: write out all repeats */ + for (int i = 0; i < repeat_count; i++) { + fputs(prev_line, fout); + } + } + } + strncpy(prev_line, current_line, sizeof(prev_line) - 1); + prev_line[sizeof(prev_line) - 1] = '\0'; + repeat_count = 0; + has_prev = true; + } + } + + /* Flush last line */ + if (has_prev) { + fputs(prev_line, fout); + if (repeat_count >= threshold) { + fprintf(fout, "--- Previous message repeated %d times ---\n", + repeat_count); + any_dedup = true; + } else { + for (int i = 0; i < repeat_count; i++) { + fputs(prev_line, fout); + } + } + } + + fclose(fin); + fclose(fout); + + if (!any_dedup) { + /* No deduplication was needed - remove output and signal caller */ + if (remove(output_path) != 0) { + fprintf(stderr, "Error: Failed to remove unchanged output file: %s\n", output_path); + return 1; + } + return 2; + } + + return 0; +}