From 8882401497968f300a8a625704c9af48d021f63c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 06:59:34 +0000 Subject: [PATCH 1/4] Initial plan From 4ca9beab7ab5dda57c259113b66152c378b3c06a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:04:53 +0000 Subject: [PATCH 2/4] Implement Phase 17 enhanced mDNS discovery with peer caching and statistics Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- include/rootstream.h | 48 ++++- src/discovery.c | 357 +++++++++++++++++++++++++++++++++----- src/discovery_broadcast.c | 39 ++++- src/discovery_manual.c | 3 +- 4 files changed, 401 insertions(+), 46 deletions(-) diff --git a/include/rootstream.h b/include/rootstream.h index b30e3f9..55541f6 100644 --- a/include/rootstream.h +++ b/include/rootstream.h @@ -399,7 +399,7 @@ typedef struct { } peer_t; /* ============================================================================ - * DISCOVERY - mDNS/Avahi service discovery + * DISCOVERY - mDNS/Avahi service discovery (PHASE 17 Enhanced) * ============================================================================ */ /* Peer history entry for quick reconnection (PHASE 5) */ @@ -410,11 +410,43 @@ typedef struct { char rootstream_code[ROOTSTREAM_CODE_MAX_LEN]; } peer_history_entry_t; +/* Enhanced peer cache entry with TTL and statistics (PHASE 17) */ +typedef struct { + char hostname[256]; + char ip_address[64]; /* String IP address */ + uint16_t port; + char rootstream_code[ROOTSTREAM_CODE_MAX_LEN]; + char capability[32]; /* "host", "client", or "both" */ + char version[16]; /* Protocol version */ + uint32_t max_peers; /* Advertised max peer capacity */ + char bandwidth[32]; /* Advertised bandwidth */ + uint64_t discovered_time_us; /* When first discovered */ + uint64_t last_seen_time_us; /* Last advertisement/update */ + uint32_t ttl_seconds; /* Time-to-live */ + bool is_online; /* Currently online */ + uint32_t contact_count; /* Times successfully contacted */ + uint32_t failure_count; /* Connection failures */ +} peer_cache_entry_t; + +#define MAX_CACHED_PEERS 64 + typedef struct { void *avahi_client; /* Avahi client (opaque) */ void *avahi_group; /* Avahi entry group (opaque) */ void *avahi_browser; /* Avahi service browser (opaque) */ bool running; /* Discovery active? */ + + /* Enhanced discovery features (PHASE 17) */ + peer_cache_entry_t peer_cache[MAX_CACHED_PEERS]; + int num_cached_peers; + uint64_t last_cache_cleanup_us; /* Last cache expiry check */ + + /* Discovery statistics */ + uint64_t total_discoveries; + uint64_t total_losses; + uint64_t mdns_discoveries; + uint64_t broadcast_discoveries; + uint64_t manual_discoveries; } discovery_ctx_t; /* ============================================================================ @@ -843,6 +875,20 @@ int discovery_parse_address(const char *address, char *hostname, uint16_t *port) int discovery_parse_rootstream_code(rootstream_ctx_t *ctx, const char *code, char *hostname, uint16_t *port); +/* Discovery - Enhanced Cache (PHASE 17) */ +int discovery_cache_add_peer(rootstream_ctx_t *ctx, const peer_cache_entry_t *entry); +int discovery_cache_update_peer(rootstream_ctx_t *ctx, const char *hostname, + uint64_t last_seen_time_us); +int discovery_cache_remove_peer(rootstream_ctx_t *ctx, const char *hostname); +peer_cache_entry_t* discovery_cache_get_peer(rootstream_ctx_t *ctx, const char *hostname); +int discovery_cache_get_all(rootstream_ctx_t *ctx, peer_cache_entry_t *entries, + int max_entries); +int discovery_cache_get_online(rootstream_ctx_t *ctx, peer_cache_entry_t *entries, + int max_entries); +void discovery_cache_expire_old_entries(rootstream_ctx_t *ctx); +void discovery_cache_cleanup(rootstream_ctx_t *ctx); +void discovery_print_stats(rootstream_ctx_t *ctx); + /* --- Input (existing, polished) --- */ int rootstream_input_init(rootstream_ctx_t *ctx); diff --git a/src/discovery.c b/src/discovery.c index 503d38f..3450592 100644 --- a/src/discovery.c +++ b/src/discovery.c @@ -76,7 +76,7 @@ static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, } /* - * Service resolver callback + * Service resolver callback (enhanced PHASE 17) * Called when a discovered service has been resolved */ static void resolve_callback(AvahiServiceResolver *r, @@ -109,7 +109,17 @@ static void resolve_callback(AvahiServiceResolver *r, printf("✓ Resolved RootStream host: %s at %s:%u\n", name, addr_str, port); - /* Extract RootStream code from TXT records */ + /* Extract enhanced TXT records (PHASE 17) */ + peer_cache_entry_t cache_entry = {0}; + strncpy(cache_entry.hostname, name, sizeof(cache_entry.hostname) - 1); + strncpy(cache_entry.ip_address, addr_str, sizeof(cache_entry.ip_address) - 1); + cache_entry.port = port; + cache_entry.discovered_time_us = get_timestamp_us(); + cache_entry.last_seen_time_us = cache_entry.discovered_time_us; + cache_entry.ttl_seconds = 3600; /* Default 1 hour TTL */ + cache_entry.is_online = true; + + /* Extract RootStream code */ AvahiStringList *code_txt = avahi_string_list_find(txt, "code"); if (code_txt) { char *key = NULL; @@ -117,41 +127,102 @@ static void resolve_callback(AvahiServiceResolver *r, size_t value_len = 0; if (avahi_string_list_get_pair(code_txt, &key, &value, &value_len) >= 0) { - /* Add discovered peer to context */ - if (ctx->num_peers < MAX_PEERS) { - peer_t *peer = &ctx->peers[ctx->num_peers]; - - /* Parse address */ - struct sockaddr_in *addr = (struct sockaddr_in*)&peer->addr; - addr->sin_family = AF_INET; - addr->sin_port = htons(port); - if (avahi_address_parse(addr_str, AVAHI_PROTO_INET, - (AvahiAddress*)&addr->sin_addr) != NULL) { - /* Store peer info */ - strncpy(peer->hostname, name, sizeof(peer->hostname) - 1); - peer->hostname[sizeof(peer->hostname) - 1] = '\0'; - - strncpy(peer->rootstream_code, value, - sizeof(peer->rootstream_code) - 1); - peer->rootstream_code[sizeof(peer->rootstream_code) - 1] = '\0'; - - peer->state = PEER_DISCOVERED; - peer->last_seen = get_timestamp_ms(); - - ctx->num_peers++; - - printf(" → Added peer: %s (code: %.8s...)\n", - peer->hostname, peer->rootstream_code); - } - } else { - fprintf(stderr, "WARNING: Max peers reached, cannot add %s\n", name); - } - + strncpy(cache_entry.rootstream_code, value, + sizeof(cache_entry.rootstream_code) - 1); + cache_entry.rootstream_code[sizeof(cache_entry.rootstream_code) - 1] = '\0'; + avahi_free(key); + avahi_free(value); + } + } + + /* Extract capability */ + AvahiStringList *capability_txt = avahi_string_list_find(txt, "capability"); + if (capability_txt) { + char *key = NULL; + char *value = NULL; + size_t value_len = 0; + if (avahi_string_list_get_pair(capability_txt, &key, &value, &value_len) >= 0) { + strncpy(cache_entry.capability, value, + sizeof(cache_entry.capability) - 1); avahi_free(key); avahi_free(value); } } else { - fprintf(stderr, "WARNING: No RootStream code in TXT records for %s\n", name); + strncpy(cache_entry.capability, "unknown", sizeof(cache_entry.capability) - 1); + } + + /* Extract version */ + AvahiStringList *version_txt = avahi_string_list_find(txt, "version"); + if (version_txt) { + char *key = NULL; + char *value = NULL; + size_t value_len = 0; + if (avahi_string_list_get_pair(version_txt, &key, &value, &value_len) >= 0) { + strncpy(cache_entry.version, value, + sizeof(cache_entry.version) - 1); + avahi_free(key); + avahi_free(value); + } + } + + /* Extract max_peers */ + AvahiStringList *max_peers_txt = avahi_string_list_find(txt, "max_peers"); + if (max_peers_txt) { + char *key = NULL; + char *value = NULL; + size_t value_len = 0; + if (avahi_string_list_get_pair(max_peers_txt, &key, &value, &value_len) >= 0) { + cache_entry.max_peers = (uint32_t)atoi(value); + avahi_free(key); + avahi_free(value); + } + } + + /* Extract bandwidth */ + AvahiStringList *bandwidth_txt = avahi_string_list_find(txt, "bandwidth"); + if (bandwidth_txt) { + char *key = NULL; + char *value = NULL; + size_t value_len = 0; + if (avahi_string_list_get_pair(bandwidth_txt, &key, &value, &value_len) >= 0) { + strncpy(cache_entry.bandwidth, value, + sizeof(cache_entry.bandwidth) - 1); + avahi_free(key); + avahi_free(value); + } + } + + /* Add to cache */ + discovery_cache_add_peer(ctx, &cache_entry); + + /* Add discovered peer to context (backwards compatibility) */ + if (strlen(cache_entry.rootstream_code) > 0 && ctx->num_peers < MAX_PEERS) { + peer_t *peer = &ctx->peers[ctx->num_peers]; + + /* Parse address */ + struct sockaddr_in *addr = (struct sockaddr_in*)&peer->addr; + addr->sin_family = AF_INET; + addr->sin_port = htons(port); + if (avahi_address_parse(addr_str, AVAHI_PROTO_INET, + (AvahiAddress*)&addr->sin_addr) != NULL) { + /* Store peer info */ + strncpy(peer->hostname, name, sizeof(peer->hostname) - 1); + peer->hostname[sizeof(peer->hostname) - 1] = '\0'; + + strncpy(peer->rootstream_code, cache_entry.rootstream_code, + sizeof(peer->rootstream_code) - 1); + peer->rootstream_code[sizeof(peer->rootstream_code) - 1] = '\0'; + + peer->state = PEER_DISCOVERED; + peer->last_seen = get_timestamp_ms(); + + ctx->num_peers++; + + printf(" → Added peer: %s (code: %.8s..., %s)\n", + peer->hostname, peer->rootstream_code, cache_entry.capability); + + ctx->discovery.mdns_discoveries++; + } } } else { fprintf(stderr, "WARNING: Failed to resolve service %s: %s\n", @@ -163,7 +234,7 @@ static void resolve_callback(AvahiServiceResolver *r, } /* - * Service browser callback + * Service browser callback (enhanced PHASE 17) * Called when services are found or lost */ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, @@ -198,7 +269,10 @@ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, case AVAHI_BROWSER_REMOVE: printf("INFO: RootStream service removed: %s\n", name); - /* Find and remove peer */ + /* Remove from cache */ + discovery_cache_remove_peer(ctx, name); + + /* Find and remove peer (backwards compatibility) */ for (int i = 0; i < ctx->num_peers; i++) { if (strcmp(ctx->peers[i].hostname, name) == 0) { /* Disconnect if connected */ @@ -334,7 +408,7 @@ int discovery_init(rootstream_ctx_t *ctx) { } /* - * Announce service on network (with fallback support - PHASE 5) + * Announce service on network (with fallback support - PHASE 5, enhanced PHASE 17) */ int discovery_announce(rootstream_ctx_t *ctx) { if (!ctx) return -1; @@ -354,9 +428,10 @@ int discovery_announce(rootstream_ctx_t *ctx) { } } - /* Prepare TXT records */ + /* Prepare enhanced TXT records (PHASE 17) */ AvahiStringList *txt = NULL; - char version_txt[64], pubkey_txt[256]; + char version_txt[64], pubkey_txt[256], capability_txt[64]; + char max_peers_txt[64], bandwidth_txt[64]; snprintf(version_txt, sizeof(version_txt), "version=%s", ROOTSTREAM_VERSION); txt = avahi_string_list_add(txt, version_txt); @@ -364,6 +439,20 @@ int discovery_announce(rootstream_ctx_t *ctx) { snprintf(pubkey_txt, sizeof(pubkey_txt), "code=%s", ctx->keypair.rootstream_code); txt = avahi_string_list_add(txt, pubkey_txt); + + /* Add capability: host if we can encode, client if we can decode */ + const char *capability = ctx->is_host ? "host" : "client"; + snprintf(capability_txt, sizeof(capability_txt), "capability=%s", capability); + txt = avahi_string_list_add(txt, capability_txt); + + /* Add max peers capacity */ + snprintf(max_peers_txt, sizeof(max_peers_txt), "max_peers=%d", MAX_PEERS); + txt = avahi_string_list_add(txt, max_peers_txt); + + /* Add bandwidth estimate (simplified) */ + uint32_t bitrate_mbps = ctx->settings.video_bitrate / 1000000; + snprintf(bandwidth_txt, sizeof(bandwidth_txt), "bandwidth=%uMbps", bitrate_mbps); + txt = avahi_string_list_add(txt, bandwidth_txt); /* Add service */ int ret = avahi_entry_group_add_service_strlst( @@ -394,7 +483,7 @@ int discovery_announce(rootstream_ctx_t *ctx) { goto try_broadcast; } - printf("→ Announcing service on network (mDNS)\n"); + printf("→ Announcing service on network (mDNS) [%s]\n", capability); return 0; } @@ -488,5 +577,195 @@ void discovery_cleanup(rootstream_ctx_t *ctx) { ctx->discovery.avahi_client = NULL; #endif + /* Cleanup cache */ + discovery_cache_cleanup(ctx); + ctx->discovery.running = false; } + +/* ============================================================================ + * PHASE 17: Enhanced Discovery Cache Management + * ============================================================================ */ + +/* + * Add peer to discovery cache + */ +int discovery_cache_add_peer(rootstream_ctx_t *ctx, const peer_cache_entry_t *entry) { + if (!ctx || !entry) return -1; + + /* Check if peer already exists */ + for (int i = 0; i < ctx->discovery.num_cached_peers; i++) { + if (strcmp(ctx->discovery.peer_cache[i].hostname, entry->hostname) == 0) { + /* Update existing entry */ + ctx->discovery.peer_cache[i] = *entry; + ctx->discovery.peer_cache[i].contact_count++; + return 0; + } + } + + /* Add new entry if space available */ + if (ctx->discovery.num_cached_peers >= MAX_CACHED_PEERS) { + fprintf(stderr, "WARNING: Peer cache full, cannot add %s\n", entry->hostname); + return -1; + } + + ctx->discovery.peer_cache[ctx->discovery.num_cached_peers] = *entry; + ctx->discovery.num_cached_peers++; + ctx->discovery.total_discoveries++; + + printf("✓ Cached peer: %s (%s:%u)\n", entry->hostname, entry->ip_address, entry->port); + return 0; +} + +/* + * Update peer's last seen time + */ +int discovery_cache_update_peer(rootstream_ctx_t *ctx, const char *hostname, + uint64_t last_seen_time_us) { + if (!ctx || !hostname) return -1; + + for (int i = 0; i < ctx->discovery.num_cached_peers; i++) { + if (strcmp(ctx->discovery.peer_cache[i].hostname, hostname) == 0) { + ctx->discovery.peer_cache[i].last_seen_time_us = last_seen_time_us; + ctx->discovery.peer_cache[i].is_online = true; + return 0; + } + } + + return -1; /* Peer not found */ +} + +/* + * Remove peer from cache + */ +int discovery_cache_remove_peer(rootstream_ctx_t *ctx, const char *hostname) { + if (!ctx || !hostname) return -1; + + for (int i = 0; i < ctx->discovery.num_cached_peers; i++) { + if (strcmp(ctx->discovery.peer_cache[i].hostname, hostname) == 0) { + /* Shift remaining entries */ + for (int j = i; j < ctx->discovery.num_cached_peers - 1; j++) { + ctx->discovery.peer_cache[j] = ctx->discovery.peer_cache[j + 1]; + } + ctx->discovery.num_cached_peers--; + ctx->discovery.total_losses++; + return 0; + } + } + + return -1; /* Peer not found */ +} + +/* + * Get peer from cache + */ +peer_cache_entry_t* discovery_cache_get_peer(rootstream_ctx_t *ctx, const char *hostname) { + if (!ctx || !hostname) return NULL; + + for (int i = 0; i < ctx->discovery.num_cached_peers; i++) { + if (strcmp(ctx->discovery.peer_cache[i].hostname, hostname) == 0) { + return &ctx->discovery.peer_cache[i]; + } + } + + return NULL; +} + +/* + * Get all cached peers + */ +int discovery_cache_get_all(rootstream_ctx_t *ctx, peer_cache_entry_t *entries, + int max_entries) { + if (!ctx || !entries || max_entries <= 0) return -1; + + int count = ctx->discovery.num_cached_peers; + if (count > max_entries) count = max_entries; + + for (int i = 0; i < count; i++) { + entries[i] = ctx->discovery.peer_cache[i]; + } + + return count; +} + +/* + * Get only online cached peers + */ +int discovery_cache_get_online(rootstream_ctx_t *ctx, peer_cache_entry_t *entries, + int max_entries) { + if (!ctx || !entries || max_entries <= 0) return -1; + + int count = 0; + for (int i = 0; i < ctx->discovery.num_cached_peers && count < max_entries; i++) { + if (ctx->discovery.peer_cache[i].is_online) { + entries[count++] = ctx->discovery.peer_cache[i]; + } + } + + return count; +} + +/* + * Expire old cache entries based on TTL + */ +void discovery_cache_expire_old_entries(rootstream_ctx_t *ctx) { + if (!ctx) return; + + uint64_t now_us = get_timestamp_us(); + ctx->discovery.last_cache_cleanup_us = now_us; + + /* Iterate backwards to safely remove entries */ + for (int i = ctx->discovery.num_cached_peers - 1; i >= 0; i--) { + peer_cache_entry_t *entry = &ctx->discovery.peer_cache[i]; + uint64_t age_us = now_us - entry->last_seen_time_us; + uint64_t ttl_us = (uint64_t)entry->ttl_seconds * 1000000ULL; + + if (age_us > ttl_us) { + printf("INFO: Expiring cached peer: %s (age: %llu sec)\n", + entry->hostname, age_us / 1000000ULL); + discovery_cache_remove_peer(ctx, entry->hostname); + } else if (age_us > ttl_us / 2) { + /* Mark as potentially offline if not seen in half TTL */ + entry->is_online = false; + } + } +} + +/* + * Cleanup cache + */ +void discovery_cache_cleanup(rootstream_ctx_t *ctx) { + if (!ctx) return; + + ctx->discovery.num_cached_peers = 0; + memset(ctx->discovery.peer_cache, 0, sizeof(ctx->discovery.peer_cache)); +} + +/* + * Print discovery statistics + */ +void discovery_print_stats(rootstream_ctx_t *ctx) { + if (!ctx) return; + + printf("\n=== Discovery Statistics ===\n"); + printf(" Total discoveries: %llu\n", ctx->discovery.total_discoveries); + printf(" Total losses: %llu\n", ctx->discovery.total_losses); + printf(" mDNS discoveries: %llu\n", ctx->discovery.mdns_discoveries); + printf(" Broadcast discoveries: %llu\n", ctx->discovery.broadcast_discoveries); + printf(" Manual discoveries: %llu\n", ctx->discovery.manual_discoveries); + printf(" Cached peers: %d\n", ctx->discovery.num_cached_peers); + + if (ctx->discovery.num_cached_peers > 0) { + printf("\n=== Cached Peers ===\n"); + for (int i = 0; i < ctx->discovery.num_cached_peers; i++) { + peer_cache_entry_t *entry = &ctx->discovery.peer_cache[i]; + printf(" %d. %s (%s:%u) - %s %s\n", i + 1, + entry->hostname, + entry->ip_address, + entry->port, + entry->capability, + entry->is_online ? "[online]" : "[offline]"); + } + } + printf("\n"); +} diff --git a/src/discovery_broadcast.c b/src/discovery_broadcast.c index 4fd72cd..305254f 100644 --- a/src/discovery_broadcast.c +++ b/src/discovery_broadcast.c @@ -27,6 +27,9 @@ typedef struct { char hostname[256]; /* Sender hostname */ uint16_t listen_port; /* Port peer is listening on */ char rootstream_code[ROOTSTREAM_CODE_MAX_LEN]; /* User's RootStream code */ + char capability[32]; /* "host", "client", or "both" (PHASE 17) */ + uint32_t max_peers; /* Max peer capacity (PHASE 17) */ + char bandwidth[32]; /* Bandwidth estimate (PHASE 17) */ } discovery_broadcast_packet_t; /* @@ -69,7 +72,7 @@ static int get_local_ip(char *ip_buf, size_t ip_len, char *bcast_buf, size_t bca } /* - * Broadcast discovery query + * Broadcast discovery query (enhanced PHASE 17) */ int discovery_broadcast_announce(rootstream_ctx_t *ctx) { if (!ctx) return -1; @@ -98,7 +101,7 @@ int discovery_broadcast_announce(rootstream_ctx_t *ctx) { return -1; } - /* Create announcement packet */ + /* Create enhanced announcement packet (PHASE 17) */ discovery_broadcast_packet_t pkt; memset(&pkt, 0, sizeof(pkt)); memcpy(pkt.magic, DISCOVERY_MAGIC, strlen(DISCOVERY_MAGIC)); @@ -107,6 +110,13 @@ int discovery_broadcast_announce(rootstream_ctx_t *ctx) { pkt.listen_port = ctx->port; strncpy(pkt.rootstream_code, ctx->keypair.rootstream_code, sizeof(pkt.rootstream_code) - 1); + + /* Add enhanced fields */ + const char *capability = ctx->is_host ? "host" : "client"; + strncpy(pkt.capability, capability, sizeof(pkt.capability) - 1); + pkt.max_peers = MAX_PEERS; + uint32_t bitrate_mbps = ctx->settings.video_bitrate / 1000000; + snprintf(pkt.bandwidth, sizeof(pkt.bandwidth), "%uMbps", bitrate_mbps); struct sockaddr_in bcast_addr = { .sin_family = AF_INET, @@ -124,7 +134,7 @@ int discovery_broadcast_announce(rootstream_ctx_t *ctx) { return -1; } - printf("✓ Broadcast discovery announced (%s:%u)\n", local_ip, ctx->port); + printf("✓ Broadcast discovery announced (%s:%u) [%s]\n", local_ip, ctx->port, capability); return 0; } @@ -190,8 +200,27 @@ int discovery_broadcast_listen(rootstream_ctx_t *ctx, int timeout_ms) { char peer_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &from_addr.sin_addr, peer_ip, sizeof(peer_ip)); - printf("✓ Discovered peer: %s (%s:%u, code: %.16s...)\n", - pkt.hostname, peer_ip, ntohs(pkt.listen_port), pkt.rootstream_code); + printf("✓ Discovered peer: %s (%s:%u, code: %.16s..., %s)\n", + pkt.hostname, peer_ip, ntohs(pkt.listen_port), pkt.rootstream_code, + strlen(pkt.capability) > 0 ? pkt.capability : "unknown"); + + /* Add to cache (PHASE 17) */ + peer_cache_entry_t cache_entry = {0}; + strncpy(cache_entry.hostname, pkt.hostname, sizeof(cache_entry.hostname) - 1); + strncpy(cache_entry.ip_address, peer_ip, sizeof(cache_entry.ip_address) - 1); + cache_entry.port = ntohs(pkt.listen_port); + strncpy(cache_entry.rootstream_code, pkt.rootstream_code, + sizeof(cache_entry.rootstream_code) - 1); + strncpy(cache_entry.capability, pkt.capability, sizeof(cache_entry.capability) - 1); + cache_entry.max_peers = pkt.max_peers; + strncpy(cache_entry.bandwidth, pkt.bandwidth, sizeof(cache_entry.bandwidth) - 1); + cache_entry.discovered_time_us = get_timestamp_us(); + cache_entry.last_seen_time_us = cache_entry.discovered_time_us; + cache_entry.ttl_seconds = 3600; + cache_entry.is_online = true; + + discovery_cache_add_peer(ctx, &cache_entry); + ctx->discovery.broadcast_discoveries++; /* Add to peer list if not already present */ bool already_exists = false; diff --git a/src/discovery_manual.c b/src/discovery_manual.c index 63d5c4a..1875aa2 100644 --- a/src/discovery_manual.c +++ b/src/discovery_manual.c @@ -75,7 +75,7 @@ int discovery_parse_address(const char *address, char *hostname, uint16_t *port) } /* - * Connect to manually specified peer + * Connect to manually specified peer (enhanced PHASE 17) */ int discovery_manual_add_peer(rootstream_ctx_t *ctx, const char *address_or_code) { if (!ctx || !address_or_code) return -1; @@ -139,6 +139,7 @@ int discovery_manual_add_peer(rootstream_ctx_t *ctx, const char *address_or_code peer->last_seen = get_timestamp_ms(); ctx->num_peers++; + ctx->discovery.manual_discoveries++; /* Track manual discovery (PHASE 17) */ printf("✓ Manually added peer: %s (%s:%u)\n", hostname, hostname, port); From 714120baa3568a0e1c86b8e12d73130e8c6f8043 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:09:20 +0000 Subject: [PATCH 3/4] Add unit tests for Phase 17 discovery cache with full passing test suite Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- src/discovery.c | 10 +- tests/CMakeLists.txt | 12 ++ tests/unit/test_discovery_cache.c | 312 ++++++++++++++++++++++++++++++ 3 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_discovery_cache.c diff --git a/src/discovery.c b/src/discovery.c index 3450592..feaea61 100644 --- a/src/discovery.c +++ b/src/discovery.c @@ -748,11 +748,11 @@ void discovery_print_stats(rootstream_ctx_t *ctx) { if (!ctx) return; printf("\n=== Discovery Statistics ===\n"); - printf(" Total discoveries: %llu\n", ctx->discovery.total_discoveries); - printf(" Total losses: %llu\n", ctx->discovery.total_losses); - printf(" mDNS discoveries: %llu\n", ctx->discovery.mdns_discoveries); - printf(" Broadcast discoveries: %llu\n", ctx->discovery.broadcast_discoveries); - printf(" Manual discoveries: %llu\n", ctx->discovery.manual_discoveries); + printf(" Total discoveries: %lu\n", (unsigned long)ctx->discovery.total_discoveries); + printf(" Total losses: %lu\n", (unsigned long)ctx->discovery.total_losses); + printf(" mDNS discoveries: %lu\n", (unsigned long)ctx->discovery.mdns_discoveries); + printf(" Broadcast discoveries: %lu\n", (unsigned long)ctx->discovery.broadcast_discoveries); + printf(" Manual discoveries: %lu\n", (unsigned long)ctx->discovery.manual_discoveries); printf(" Cached peers: %d\n", ctx->discovery.num_cached_peers); if (ctx->discovery.num_cached_peers > 0) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 93af1ff..fddf056 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -80,5 +80,17 @@ if(ENABLE_UNIT_TESTS) add_test(NAME InputManagerUnit COMMAND test_input_manager) set_tests_properties(InputManagerUnit PROPERTIES LABELS "unit") + # PHASE 17: Discovery Cache tests + add_executable(test_discovery_cache unit/test_discovery_cache.c + ${CMAKE_SOURCE_DIR}/src/discovery.c + ) + target_include_directories(test_discovery_cache PRIVATE ${CMAKE_SOURCE_DIR}/include) + target_link_libraries(test_discovery_cache test_harness pthread m) + if(AVAHI_FOUND) + target_link_libraries(test_discovery_cache ${AVAHI_LIBRARIES}) + endif() + add_test(NAME DiscoveryCacheUnit COMMAND test_discovery_cache) + set_tests_properties(DiscoveryCacheUnit PROPERTIES LABELS "unit") + message(STATUS "Unit tests enabled") endif() diff --git a/tests/unit/test_discovery_cache.c b/tests/unit/test_discovery_cache.c new file mode 100644 index 0000000..e89aea7 --- /dev/null +++ b/tests/unit/test_discovery_cache.c @@ -0,0 +1,312 @@ +/* + * test_discovery_cache.c - Unit tests for PHASE 17 discovery cache + * + * Tests peer cache management, TTL expiry, and statistics tracking. + */ + +#include "../../include/rootstream.h" +#include +#include +#include +#include +#include +#include +#include + +/* Stub functions needed for linking */ +uint64_t get_timestamp_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; +} + +uint64_t get_timestamp_us(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)ts.tv_nsec / 1000ULL; +} + +/* Stub discovery functions */ +int discovery_broadcast_announce(rootstream_ctx_t *ctx) { (void)ctx; return 0; } +int discovery_broadcast_listen(rootstream_ctx_t *ctx, int timeout_ms) { (void)ctx; (void)timeout_ms; return 0; } +int discovery_save_peer_to_history(rootstream_ctx_t *ctx, const char *hostname, + uint16_t port, const char *rootstream_code) { + (void)ctx; (void)hostname; (void)port; (void)rootstream_code; return 0; +} + +/* Test helper: Create a mock peer cache entry */ +static peer_cache_entry_t create_test_peer(const char *hostname, const char *ip, + uint16_t port, const char *capability) { + peer_cache_entry_t entry = {0}; + strncpy(entry.hostname, hostname, sizeof(entry.hostname) - 1); + strncpy(entry.ip_address, ip, sizeof(entry.ip_address) - 1); + entry.port = port; + snprintf(entry.rootstream_code, sizeof(entry.rootstream_code), + "TESTCODE%s", hostname); + strncpy(entry.capability, capability, sizeof(entry.capability) - 1); + strncpy(entry.version, "1.0.0", sizeof(entry.version) - 1); + entry.max_peers = 10; + strncpy(entry.bandwidth, "100Mbps", sizeof(entry.bandwidth) - 1); + entry.discovered_time_us = get_timestamp_us(); + entry.last_seen_time_us = entry.discovered_time_us; + entry.ttl_seconds = 3600; + entry.is_online = true; + return entry; +} + +/* Test 1: Add peer to cache */ +void test_cache_add_peer() { + printf("TEST: Add peer to cache...\n"); + + rootstream_ctx_t ctx = {0}; + peer_cache_entry_t peer = create_test_peer("test-host-1", "192.168.1.100", + 9876, "host"); + + int ret = discovery_cache_add_peer(&ctx, &peer); + assert(ret == 0); + assert(ctx.discovery.num_cached_peers == 1); + assert(strcmp(ctx.discovery.peer_cache[0].hostname, "test-host-1") == 0); + assert(strcmp(ctx.discovery.peer_cache[0].ip_address, "192.168.1.100") == 0); + assert(ctx.discovery.peer_cache[0].port == 9876); + assert(strcmp(ctx.discovery.peer_cache[0].capability, "host") == 0); + + printf(" ✓ Peer added successfully\n"); +} + +/* Test 2: Update existing peer */ +void test_cache_update_peer() { + printf("TEST: Update existing peer...\n"); + + rootstream_ctx_t ctx = {0}; + peer_cache_entry_t peer = create_test_peer("test-host-1", "192.168.1.100", + 9876, "host"); + + discovery_cache_add_peer(&ctx, &peer); + uint32_t original_contact_count = ctx.discovery.peer_cache[0].contact_count; + + /* Wait a bit */ + usleep(10000); /* 10ms */ + + /* Add same peer again (should update) */ + peer.last_seen_time_us = get_timestamp_us(); + int ret = discovery_cache_add_peer(&ctx, &peer); + assert(ret == 0); + assert(ctx.discovery.num_cached_peers == 1); /* Still only one peer */ + assert(ctx.discovery.peer_cache[0].contact_count > original_contact_count); + + printf(" ✓ Peer updated successfully\n"); +} + +/* Test 3: Get peer from cache */ +void test_cache_get_peer() { + printf("TEST: Get peer from cache...\n"); + + rootstream_ctx_t ctx = {0}; + peer_cache_entry_t peer1 = create_test_peer("test-host-1", "192.168.1.100", + 9876, "host"); + peer_cache_entry_t peer2 = create_test_peer("test-host-2", "192.168.1.101", + 9877, "client"); + + discovery_cache_add_peer(&ctx, &peer1); + discovery_cache_add_peer(&ctx, &peer2); + + peer_cache_entry_t *found = discovery_cache_get_peer(&ctx, "test-host-2"); + assert(found != NULL); + assert(strcmp(found->hostname, "test-host-2") == 0); + assert(strcmp(found->ip_address, "192.168.1.101") == 0); + assert(found->port == 9877); + assert(strcmp(found->capability, "client") == 0); + + /* Try to get non-existent peer */ + peer_cache_entry_t *not_found = discovery_cache_get_peer(&ctx, "nonexistent"); + assert(not_found == NULL); + + printf(" ✓ Peer retrieval works correctly\n"); +} + +/* Test 4: Remove peer from cache */ +void test_cache_remove_peer() { + printf("TEST: Remove peer from cache...\n"); + + rootstream_ctx_t ctx = {0}; + peer_cache_entry_t peer1 = create_test_peer("test-host-1", "192.168.1.100", + 9876, "host"); + peer_cache_entry_t peer2 = create_test_peer("test-host-2", "192.168.1.101", + 9877, "client"); + + discovery_cache_add_peer(&ctx, &peer1); + discovery_cache_add_peer(&ctx, &peer2); + assert(ctx.discovery.num_cached_peers == 2); + + int ret = discovery_cache_remove_peer(&ctx, "test-host-1"); + assert(ret == 0); + assert(ctx.discovery.num_cached_peers == 1); + assert(strcmp(ctx.discovery.peer_cache[0].hostname, "test-host-2") == 0); + + /* Try to remove non-existent peer */ + ret = discovery_cache_remove_peer(&ctx, "nonexistent"); + assert(ret == -1); + + printf(" ✓ Peer removal works correctly\n"); +} + +/* Test 5: Get all cached peers */ +void test_cache_get_all() { + printf("TEST: Get all cached peers...\n"); + + rootstream_ctx_t ctx = {0}; + peer_cache_entry_t entries[10]; + + /* Add multiple peers */ + for (int i = 0; i < 5; i++) { + char hostname[64]; + char ip[32]; + snprintf(hostname, sizeof(hostname), "test-host-%d", i); + snprintf(ip, sizeof(ip), "192.168.1.%d", 100 + i); + peer_cache_entry_t peer = create_test_peer(hostname, ip, 9876 + i, "host"); + discovery_cache_add_peer(&ctx, &peer); + } + + int count = discovery_cache_get_all(&ctx, entries, 10); + assert(count == 5); + assert(strcmp(entries[0].hostname, "test-host-0") == 0); + assert(strcmp(entries[4].hostname, "test-host-4") == 0); + + printf(" ✓ Get all peers works correctly\n"); +} + +/* Test 6: Get only online peers */ +void test_cache_get_online() { + printf("TEST: Get only online peers...\n"); + + rootstream_ctx_t ctx = {0}; + peer_cache_entry_t entries[10]; + + /* Add peers with different online status */ + for (int i = 0; i < 5; i++) { + char hostname[64]; + char ip[32]; + snprintf(hostname, sizeof(hostname), "test-host-%d", i); + snprintf(ip, sizeof(ip), "192.168.1.%d", 100 + i); + peer_cache_entry_t peer = create_test_peer(hostname, ip, 9876 + i, "host"); + peer.is_online = (i % 2 == 0); /* Only even-indexed peers are online */ + discovery_cache_add_peer(&ctx, &peer); + } + + int count = discovery_cache_get_online(&ctx, entries, 10); + assert(count == 3); /* Peers 0, 2, 4 are online */ + + /* Verify all returned peers are online */ + for (int i = 0; i < count; i++) { + assert(entries[i].is_online == true); + } + + printf(" ✓ Get online peers works correctly\n"); +} + +/* Test 7: Cache expiry */ +void test_cache_expiry() { + printf("TEST: Cache expiry...\n"); + + rootstream_ctx_t ctx = {0}; + + /* Add peers with different ages */ + for (int i = 0; i < 3; i++) { + char hostname[64]; + char ip[32]; + snprintf(hostname, sizeof(hostname), "test-host-%d", i); + snprintf(ip, sizeof(ip), "192.168.1.%d", 100 + i); + peer_cache_entry_t peer = create_test_peer(hostname, ip, 9876 + i, "host"); + peer.ttl_seconds = 1; /* Very short TTL for testing */ + + if (i == 0) { + /* Make first peer very old */ + peer.last_seen_time_us = get_timestamp_us() - 2000000ULL; /* 2 seconds ago */ + } else { + peer.last_seen_time_us = get_timestamp_us(); + } + + discovery_cache_add_peer(&ctx, &peer); + } + + assert(ctx.discovery.num_cached_peers == 3); + + /* Expire old entries */ + discovery_cache_expire_old_entries(&ctx); + + /* First peer should be removed */ + assert(ctx.discovery.num_cached_peers == 2); + assert(strcmp(ctx.discovery.peer_cache[0].hostname, "test-host-1") == 0); + + printf(" ✓ Cache expiry works correctly\n"); +} + +/* Test 8: Statistics tracking */ +void test_discovery_stats() { + printf("TEST: Discovery statistics tracking...\n"); + + rootstream_ctx_t ctx = {0}; + + assert(ctx.discovery.total_discoveries == 0); + assert(ctx.discovery.total_losses == 0); + + /* Add peers */ + for (int i = 0; i < 3; i++) { + char hostname[64]; + char ip[32]; + snprintf(hostname, sizeof(hostname), "test-host-%d", i); + snprintf(ip, sizeof(ip), "192.168.1.%d", 100 + i); + peer_cache_entry_t peer = create_test_peer(hostname, ip, 9876 + i, "host"); + discovery_cache_add_peer(&ctx, &peer); + } + + assert(ctx.discovery.total_discoveries == 3); + + /* Remove one peer */ + discovery_cache_remove_peer(&ctx, "test-host-1"); + assert(ctx.discovery.total_losses == 1); + + printf(" ✓ Statistics tracking works correctly\n"); +} + +/* Test 9: Cache cleanup */ +void test_cache_cleanup() { + printf("TEST: Cache cleanup...\n"); + + rootstream_ctx_t ctx = {0}; + + /* Add multiple peers */ + for (int i = 0; i < 5; i++) { + char hostname[64]; + char ip[32]; + snprintf(hostname, sizeof(hostname), "test-host-%d", i); + snprintf(ip, sizeof(ip), "192.168.1.%d", 100 + i); + peer_cache_entry_t peer = create_test_peer(hostname, ip, 9876 + i, "host"); + discovery_cache_add_peer(&ctx, &peer); + } + + assert(ctx.discovery.num_cached_peers == 5); + + discovery_cache_cleanup(&ctx); + + assert(ctx.discovery.num_cached_peers == 0); + + printf(" ✓ Cache cleanup works correctly\n"); +} + +int main() { + printf("\n=== Discovery Cache Unit Tests ===\n\n"); + + test_cache_add_peer(); + test_cache_update_peer(); + test_cache_get_peer(); + test_cache_remove_peer(); + test_cache_get_all(); + test_cache_get_online(); + test_cache_expiry(); + test_discovery_stats(); + test_cache_cleanup(); + + printf("\n=== All Discovery Cache Tests Passed ✓ ===\n"); + return 0; +} From 44b8a83eaf4acf6e4c07ea8dd3dbd7f99db8cb9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:10:24 +0000 Subject: [PATCH 4/4] Address code review feedback: fix cache expiry iteration and remove timing dependency in tests Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- src/discovery.c | 22 +++++++++++++++++----- tests/unit/test_discovery_cache.c | 7 ++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/discovery.c b/src/discovery.c index feaea61..ff054fe 100644 --- a/src/discovery.c +++ b/src/discovery.c @@ -714,19 +714,31 @@ void discovery_cache_expire_old_entries(rootstream_ctx_t *ctx) { uint64_t now_us = get_timestamp_us(); ctx->discovery.last_cache_cleanup_us = now_us; - /* Iterate backwards to safely remove entries */ - for (int i = ctx->discovery.num_cached_peers - 1; i >= 0; i--) { + /* Collect indices to remove (iterate backwards for safe removal) */ + int i = ctx->discovery.num_cached_peers - 1; + while (i >= 0) { peer_cache_entry_t *entry = &ctx->discovery.peer_cache[i]; uint64_t age_us = now_us - entry->last_seen_time_us; uint64_t ttl_us = (uint64_t)entry->ttl_seconds * 1000000ULL; if (age_us > ttl_us) { - printf("INFO: Expiring cached peer: %s (age: %llu sec)\n", - entry->hostname, age_us / 1000000ULL); - discovery_cache_remove_peer(ctx, entry->hostname); + /* Remove expired entry */ + printf("INFO: Expiring cached peer: %s (age: %lu sec)\n", + entry->hostname, (unsigned long)(age_us / 1000000ULL)); + + /* Shift remaining entries */ + for (int j = i; j < ctx->discovery.num_cached_peers - 1; j++) { + ctx->discovery.peer_cache[j] = ctx->discovery.peer_cache[j + 1]; + } + ctx->discovery.num_cached_peers--; + ctx->discovery.total_losses++; + /* Note: Don't decrement i here as we've shifted entries */ } else if (age_us > ttl_us / 2) { /* Mark as potentially offline if not seen in half TTL */ entry->is_online = false; + i--; /* Move to next entry */ + } else { + i--; /* Move to next entry */ } } } diff --git a/tests/unit/test_discovery_cache.c b/tests/unit/test_discovery_cache.c index e89aea7..4cc65da 100644 --- a/tests/unit/test_discovery_cache.c +++ b/tests/unit/test_discovery_cache.c @@ -84,11 +84,8 @@ void test_cache_update_peer() { discovery_cache_add_peer(&ctx, &peer); uint32_t original_contact_count = ctx.discovery.peer_cache[0].contact_count; - /* Wait a bit */ - usleep(10000); /* 10ms */ - - /* Add same peer again (should update) */ - peer.last_seen_time_us = get_timestamp_us(); + /* Set an explicitly newer timestamp instead of relying on sleep */ + peer.last_seen_time_us = get_timestamp_us() + 1000000ULL; /* 1 second in the future */ int ret = discovery_cache_add_peer(&ctx, &peer); assert(ret == 0); assert(ctx.discovery.num_cached_peers == 1); /* Still only one peer */