nostr_core_lib/tests/pool_test.c

692 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Interactive Relay Pool Test Program
*
* Interactive command-line interface for testing nostr_relay_pool functionality.
* All output is logged to pool.log while the menu runs in the terminal.
*
* Usage: ./pool_test
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
// Global variables
volatile sig_atomic_t running = 1;
nostr_relay_pool_t* pool = NULL;
nostr_pool_subscription_t** subscriptions = NULL;
int subscription_count = 0;
int subscription_capacity = 0;
pthread_t poll_thread;
int log_fd = -1;
// Signal handler for clean shutdown
void signal_handler(int signum) {
(void)signum;
running = 0;
}
// Event callback - called when an event is received
void on_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data;
// Extract basic event information
cJSON* id = cJSON_GetObjectItem(event, "id");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
cJSON* kind = cJSON_GetObjectItem(event, "kind");
cJSON* content = cJSON_GetObjectItem(event, "content");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0'; // Remove newline
dprintf(log_fd, "[%s] 📨 EVENT from %s\n", timestamp, relay_url);
dprintf(log_fd, "├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
dprintf(log_fd, "├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
dprintf(log_fd, "├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
dprintf(log_fd, "├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
// Truncate content if too long
if (content && cJSON_IsString(content)) {
const char* content_str = cJSON_GetStringValue(content);
size_t content_len = strlen(content_str);
if (content_len > 100) {
dprintf(log_fd, "└── Content: %.97s...\n", content_str);
} else {
dprintf(log_fd, "└── Content: %s\n", content_str);
}
} else {
dprintf(log_fd, "└── Content: (empty)\n");
}
dprintf(log_fd, "\n");
}
// EOSE callback - called when End of Stored Events is received
void on_eose(cJSON** events, int event_count, void* user_data) {
(void)user_data;
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 📋 EOSE received - %d events collected\n", timestamp, event_count);
// Log collected events if any
for (int i = 0; i < event_count; i++) {
cJSON* id = cJSON_GetObjectItem(events[i], "id");
if (id && cJSON_IsString(id)) {
dprintf(log_fd, " Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
}
}
dprintf(log_fd, "\n");
}
// Background polling thread
void* poll_thread_func(void* arg) {
(void)arg;
while (running) {
if (pool) {
nostr_relay_pool_poll(pool, 100);
}
struct timespec ts = {0, 10000000}; // 10ms
nanosleep(&ts, NULL);
}
return NULL;
}
// Print menu
void print_menu() {
printf("\n=== NOSTR Relay Pool Test Menu ===\n");
printf("1. Start Pool (wss://relay.laantungir.net)\n");
printf("2. Stop Pool\n");
printf("3. Add relay to pool\n");
printf("4. Remove relay from pool\n");
printf("5. Add subscription\n");
printf("6. Remove subscription\n");
printf("7. Show pool status\n");
printf("8. Test reconnection (simulate disconnect)\n");
printf("9. Exit\n");
printf("Choice: ");
}
// Get user input with default
char* get_input(const char* prompt, const char* default_value) {
static char buffer[1024];
printf("%s", prompt);
if (default_value) {
printf(" [%s]", default_value);
}
printf(": ");
if (!fgets(buffer, sizeof(buffer), stdin)) {
return NULL;
}
// Remove newline
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
// Return default if empty
if (strlen(buffer) == 0 && default_value) {
return strdup(default_value);
}
return strdup(buffer);
}
// Parse comma-separated list into cJSON array
cJSON* parse_comma_list(const char* input, int is_number) {
if (!input || strlen(input) == 0) {
return NULL;
}
cJSON* array = cJSON_CreateArray();
if (!array) return NULL;
char* input_copy = strdup(input);
char* token = strtok(input_copy, ",");
while (token) {
// Trim whitespace
while (*token == ' ') token++;
char* end = token + strlen(token) - 1;
while (end > token && *end == ' ') *end-- = '\0';
if (is_number) {
int num = atoi(token);
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
} else {
cJSON_AddItemToArray(array, cJSON_CreateString(token));
}
token = strtok(NULL, ",");
}
free(input_copy);
return array;
}
// Add subscription interactively
void add_subscription() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
printf("\n--- Add Subscription ---\n");
printf("Enter filter values (press Enter for no value):\n");
cJSON* filter = cJSON_CreateObject();
// ids
char* ids_input = get_input("ids (comma-separated event ids)", NULL);
if (ids_input && strlen(ids_input) > 0) {
cJSON* ids = parse_comma_list(ids_input, 0);
if (ids) cJSON_AddItemToObject(filter, "ids", ids);
}
free(ids_input);
// authors
char* authors_input = get_input("authors (comma-separated pubkeys)", NULL);
if (authors_input && strlen(authors_input) > 0) {
cJSON* authors = parse_comma_list(authors_input, 0);
if (authors) cJSON_AddItemToObject(filter, "authors", authors);
}
free(authors_input);
// kinds
char* kinds_input = get_input("kinds (comma-separated numbers)", NULL);
if (kinds_input && strlen(kinds_input) > 0) {
cJSON* kinds = parse_comma_list(kinds_input, 1);
if (kinds) cJSON_AddItemToObject(filter, "kinds", kinds);
}
free(kinds_input);
// #e tag
char* e_input = get_input("#e (comma-separated event ids)", NULL);
if (e_input && strlen(e_input) > 0) {
cJSON* e_array = parse_comma_list(e_input, 0);
if (e_array) cJSON_AddItemToObject(filter, "#e", e_array);
}
free(e_input);
// #p tag
char* p_input = get_input("#p (comma-separated pubkeys)", NULL);
if (p_input && strlen(p_input) > 0) {
cJSON* p_array = parse_comma_list(p_input, 0);
if (p_array) cJSON_AddItemToObject(filter, "#p", p_array);
}
free(p_input);
// since
char* since_input = get_input("since (unix timestamp or 'n' for now)", NULL);
if (since_input && strlen(since_input) > 0) {
if (strcmp(since_input, "n") == 0) {
// Use current timestamp
time_t now = time(NULL);
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber((int)now));
printf("Using current timestamp: %ld\n", now);
} else {
int since = atoi(since_input);
if (since > 0) cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
}
}
free(since_input);
// until
char* until_input = get_input("until (unix timestamp)", NULL);
if (until_input && strlen(until_input) > 0) {
int until = atoi(until_input);
if (until > 0) cJSON_AddItemToObject(filter, "until", cJSON_CreateNumber(until));
}
free(until_input);
// limit
char* limit_input = get_input("limit (max events)", "10");
if (limit_input && strlen(limit_input) > 0) {
int limit = atoi(limit_input);
if (limit > 0) cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(limit));
}
free(limit_input);
// Get relay URLs from pool
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
cJSON_Delete(filter);
return;
}
// Ask about close_on_eose behavior
char* close_input = get_input("Close subscription on EOSE? (y/n)", "n");
int close_on_eose = (close_input && strcmp(close_input, "y") == 0) ? 1 : 0;
free(close_input);
// Create subscription with new parameters
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool,
(const char**)relay_urls,
relay_count,
filter,
on_event,
on_eose,
NULL,
close_on_eose,
1, // enable_deduplication
NOSTR_POOL_EOSE_FULL_SET, // result_mode
30, // relay_timeout_seconds
60 // eose_timeout_seconds
);
// Free relay URLs
for (int i = 0; i < relay_count; i++) {
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
if (!sub) {
printf("❌ Failed to create subscription\n");
cJSON_Delete(filter);
return;
}
// Store subscription
if (subscription_count >= subscription_capacity) {
subscription_capacity = subscription_capacity == 0 ? 10 : subscription_capacity * 2;
subscriptions = realloc(subscriptions, subscription_capacity * sizeof(nostr_pool_subscription_t*));
}
subscriptions[subscription_count++] = sub;
printf("✅ Subscription created (ID: %d)\n", subscription_count);
// Log the filter
char* filter_json = cJSON_Print(filter);
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🔍 New subscription created (ID: %d)\n", timestamp, subscription_count);
dprintf(log_fd, "Filter: %s\n\n", filter_json);
free(filter_json);
}
// Remove subscription
void remove_subscription() {
if (subscription_count == 0) {
printf("❌ No subscriptions to remove\n");
return;
}
printf("\n--- Remove Subscription ---\n");
printf("Available subscriptions:\n");
for (int i = 0; i < subscription_count; i++) {
printf("%d. Subscription %d\n", i + 1, i + 1);
}
char* choice_input = get_input("Enter subscription number to remove", NULL);
if (!choice_input || strlen(choice_input) == 0) {
free(choice_input);
return;
}
int choice = atoi(choice_input) - 1;
free(choice_input);
if (choice < 0 || choice >= subscription_count) {
printf("❌ Invalid subscription number\n");
return;
}
nostr_pool_subscription_close(subscriptions[choice]);
// Shift remaining subscriptions
for (int i = choice; i < subscription_count - 1; i++) {
subscriptions[i] = subscriptions[i + 1];
}
subscription_count--;
printf("✅ Subscription removed\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🗑️ Subscription removed (was ID: %d)\n\n", timestamp, choice + 1);
}
// Show pool status
void show_pool_status() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
// Give polling thread time to establish connections
printf("⏳ Waiting for connections to establish...\n");
sleep(3);
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
printf("\n📊 POOL STATUS\n");
printf("Relays: %d\n", relay_count);
printf("Subscriptions: %d\n", subscription_count);
if (relay_count > 0) {
printf("\nRelay Details:\n");
for (int i = 0; i < relay_count; i++) {
const char* status_str;
switch (statuses[i]) {
case NOSTR_POOL_RELAY_CONNECTED: status_str = "🟢 CONNECTED"; break;
case NOSTR_POOL_RELAY_CONNECTING: status_str = "🟡 CONNECTING"; break;
case NOSTR_POOL_RELAY_DISCONNECTED: status_str = "⚪ DISCONNECTED"; break;
case NOSTR_POOL_RELAY_ERROR: status_str = "🔴 ERROR"; break;
default: status_str = "❓ UNKNOWN"; break;
}
printf("├── %s: %s\n", relay_urls[i], status_str);
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
if (stats) {
printf("│ ├── Events received: %d\n", stats->events_received);
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
printf("│ ├── Connection failures: %d\n", stats->connection_failures);
printf("│ ├── Ping latency: %.2f ms\n", stats->ping_latency_current);
printf("│ └── Query latency: %.2f ms\n", stats->query_latency_avg);
}
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
}
printf("\n");
}
int main() {
// Setup logging to file
log_fd = open("pool.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (log_fd == -1) {
fprintf(stderr, "❌ Failed to open pool.log for writing\n");
return 1;
}
// Initialize NOSTR library
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
close(log_fd);
return 1;
}
// Setup signal handler
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Start polling thread
if (pthread_create(&poll_thread, NULL, poll_thread_func, NULL) != 0) {
fprintf(stderr, "❌ Failed to create polling thread\n");
nostr_cleanup();
close(log_fd);
return 1;
}
printf("🔗 NOSTR Relay Pool Interactive Test\n");
printf("=====================================\n");
printf("All event output is logged to pool.log\n");
printf("Press Ctrl+C to exit\n\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🚀 Pool test started\n\n", timestamp);
// Main menu loop
while (running) {
print_menu();
char choice;
if (scanf("%c", &choice) != 1) {
break;
}
// Consume newline
int c;
while ((c = getchar()) != '\n' && c != EOF);
switch (choice) {
case '1': { // Start Pool
if (pool) {
printf("❌ Pool already started\n");
break;
}
// Create pool with custom reconnection configuration for faster testing
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
config->ping_interval_seconds = 5; // Ping every 5 seconds for testing
pool = nostr_relay_pool_create(config);
if (!pool) {
printf("❌ Failed to create pool\n");
break;
}
if (nostr_relay_pool_add_relay(pool, "wss://relay.laantungir.net") != NOSTR_SUCCESS) {
printf("❌ Failed to add default relay\n");
nostr_relay_pool_destroy(pool);
pool = NULL;
break;
}
printf("✅ Pool started with wss://relay.laantungir.net\n");
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🏊 Pool started with default relay\n\n", timestamp);
break;
}
case '2': { // Stop Pool
if (!pool) {
printf("❌ Pool not started\n");
break;
}
// Close all subscriptions
for (int i = 0; i < subscription_count; i++) {
if (subscriptions[i]) {
nostr_pool_subscription_close(subscriptions[i]);
}
}
free(subscriptions);
subscriptions = NULL;
subscription_count = 0;
subscription_capacity = 0;
nostr_relay_pool_destroy(pool);
pool = NULL;
printf("✅ Pool stopped\n");
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🛑 Pool stopped\n\n", timestamp);
break;
}
case '3': { // Add relay
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char* url = get_input("Enter relay URL", "wss://relay.example.com");
if (url && strlen(url) > 0) {
if (nostr_relay_pool_add_relay(pool, url) == NOSTR_SUCCESS) {
printf("✅ Relay added: %s\n", url);
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] Relay added: %s\n\n", timestamp, url);
} else {
printf("❌ Failed to add relay\n");
}
}
free(url);
break;
}
case '4': { // Remove relay
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char* url = get_input("Enter relay URL to remove", NULL);
if (url && strlen(url) > 0) {
if (nostr_relay_pool_remove_relay(pool, url) == NOSTR_SUCCESS) {
printf("✅ Relay removed: %s\n", url);
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] Relay removed: %s\n\n", timestamp, url);
} else {
printf("❌ Failed to remove relay\n");
}
}
free(url);
break;
}
case '5': // Add subscription
add_subscription();
break;
case '6': // Remove subscription
remove_subscription();
break;
case '7': // Show status
show_pool_status();
break;
case '8': { // Test reconnection
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
break;
}
printf("\n--- Test Reconnection ---\n");
printf("Available relays:\n");
for (int i = 0; i < relay_count; i++) {
printf("%d. %s (%s)\n", i + 1, relay_urls[i],
statuses[i] == NOSTR_POOL_RELAY_CONNECTED ? "CONNECTED" : "NOT CONNECTED");
}
char* choice_input = get_input("Enter relay number to test reconnection with", NULL);
if (!choice_input || strlen(choice_input) == 0) {
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
free(choice_input);
break;
}
int choice = atoi(choice_input) - 1;
free(choice_input);
if (choice < 0 || choice >= relay_count) {
printf("❌ Invalid relay number\n");
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
break;
}
printf("🔄 Testing reconnection with %s...\n", relay_urls[choice]);
printf(" The pool is configured with automatic reconnection enabled.\n");
printf(" If the connection drops, it will automatically attempt to reconnect\n");
printf(" with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s max).\n");
printf(" Connection health is monitored with ping/pong every 30 seconds.\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🔄 TEST: Testing reconnection behavior with %s\n", timestamp, relay_urls[choice]);
dprintf(log_fd, " Pool configured with: auto-reconnect=ON, max_attempts=10, ping_interval=30s\n\n");
printf("✅ Reconnection test initiated. Monitor the status and logs for reconnection activity.\n");
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
break;
}
case '9': // Exit
running = 0;
break;
default:
printf("❌ Invalid choice\n");
break;
}
}
printf("\n🧹 Cleaning up...\n");
// Stop polling thread
running = 0;
pthread_join(poll_thread, NULL);
// Clean up pool and subscriptions
if (pool) {
for (int i = 0; i < subscription_count; i++) {
if (subscriptions[i]) {
nostr_pool_subscription_close(subscriptions[i]);
}
}
free(subscriptions);
nostr_relay_pool_destroy(pool);
printf("✅ Pool destroyed\n");
}
// Cleanup
nostr_cleanup();
close(log_fd);
printf("👋 Test completed\n");
return 0;
}