1 Commits

Author SHA1 Message Date
Laan Tungir
c08fae7ba8 v0.0.15 - Fix static musl URL signer segfault and open log pager at newest entry 2026-05-07 10:20:16 -04:00
10 changed files with 408 additions and 96 deletions

View File

@@ -31,6 +31,17 @@ int nt_publish(const char *event_json,
int relay_count,
int timeout_ms);
/*
* Same as nt_publish_detailed(), but skips db relay-post logging.
* Intended for background worker usage when DB handle is thread-affine.
*/
int nt_publish_detailed_nodb(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
nt_publish_result_t **results_out,
int *result_count_out);
/* Free result array returned by nt_publish_detailed(). */
void nt_publish_results_free(nt_publish_result_t *results, int result_count);

View File

@@ -13,4 +13,14 @@ int publish_signed_event_with_report(const char *label,
cJSON *tags,
int timeout_ms);
/*
* Fire-and-forget publish wrapper.
* Returns 0 when background job is started, -1 on immediate setup failure.
*/
int publish_signed_event_with_report_async(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms);
#endif /* PUBLISH_H */

View File

@@ -34,6 +34,7 @@ extern nt_state_t g_state;
void state_init(void);
void state_cleanup(void);
int state_load_user_info(void);
int state_load_user_info_async(void);
void state_parse_relay_list(void);
const char **state_get_read_relays(int *count);
const char **state_get_write_relays(int *count);

View File

@@ -11,8 +11,8 @@
*/
#define NT_VERSION_MAJOR 0
#define NT_VERSION_MINOR 0
#define NT_VERSION_PATCH 14
#define NT_VERSION "v0.0.14"
#define NT_VERSION_PATCH 15
#define NT_VERSION "v0.0.15"
#include <ncurses.h>
#include <stdio.h>

View File

@@ -364,12 +364,10 @@ void menu_login(void) {
free(local_items);
free(labels);
(void)state_load_user_info();
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
nt_log("No profile found on relays. Setting up new user...");
menu_add_new_user();
(void)state_load_user_info();
if (state_load_user_info_async() != 0) {
nt_log("Failed to start background user-info load.");
} else {
nt_log("Loading profile/relay data in background...");
}
return;
@@ -418,7 +416,7 @@ void menu_login(void) {
snprintf(endpoint_url, sizeof(endpoint_url), "%s", (saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : "");
(void)menu_prompt("> Login > URL Signer",
"Signer URL (http://<npub>.fips:8080)",
endpoint_url,
(saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : "",
endpoint_url,
sizeof(endpoint_url));
@@ -486,12 +484,10 @@ void menu_login(void) {
nt_log("Signed in via n_signer URL (%s) npub: %s", endpoint_url, g_state.npub_bech32);
(void)state_load_user_info();
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
nt_log("No profile found on relays. Setting up new user...");
menu_add_new_user();
(void)state_load_user_info();
if (state_load_user_info_async() != 0) {
nt_log("Failed to start background user-info load.");
} else {
nt_log("Loading profile/relay data in background...");
}
return;
@@ -511,7 +507,7 @@ void menu_login(void) {
(void)menu_prompt("> Login > Qrexec Signer",
"Target signer qube (nsigner-vault)",
target_qube,
(saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault",
target_qube,
sizeof(target_qube));
if (target_qube[0] == '\0') {
@@ -520,7 +516,7 @@ void menu_login(void) {
(void)menu_prompt("> Login > Qrexec Signer",
"Qrexec service (qubes.NsignerRpc)",
service_name,
"qubes.NsignerRpc",
service_name,
sizeof(service_name));
if (service_name[0] == '\0') {
@@ -567,12 +563,10 @@ void menu_login(void) {
nt_log("Signed in via qrexec n_signer (%s:%s) npub: %s", target_qube, service_name, g_state.npub_bech32);
(void)state_load_user_info();
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
nt_log("No profile found on relays. Setting up new user...");
menu_add_new_user();
(void)state_load_user_info();
if (state_load_user_info_async() != 0) {
nt_log("Failed to start background user-info load.");
} else {
nt_log("Loading profile/relay data in background...");
}
return;

View File

@@ -11,7 +11,7 @@ void menu_tweet(void) {
TuiFrame frame = nt_frame("> Main Menu > Tweet");
TuiStatus status = nt_status();
char text[4096];
int posted;
int queued;
tuin_render_header(&frame);
nt_print_reset();
@@ -28,11 +28,12 @@ void menu_tweet(void) {
return;
}
posted = publish_signed_event_with_report("Tweet", 1, text, NULL, 8000);
queued = publish_signed_event_with_report_async("Tweet", 1, text, NULL, 8000);
if (posted < 0) {
tuin_notice("Tweet publish failed.");
} else {
nt_log("Tweet published.");
if (queued != 0) {
tuin_notice("Tweet publish failed to start.");
return;
}
nt_log("Tweet queued; publishing in background.");
}

View File

@@ -3,6 +3,7 @@
#include "db.h"
#include "state.h"
#include "signer.h"
#include "tui.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip042.h"
@@ -396,12 +397,13 @@ static int nt_wait_publish_ok(nostr_ws_client_t *client,
}
/* Publish one event to each relay: connect -> EVENT -> OK -> close. */
int nt_publish_detailed(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
nt_publish_result_t **results_out,
int *result_count_out) {
static int nt_publish_detailed_impl(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
int with_db_log,
nt_publish_result_t **results_out,
int *result_count_out) {
cJSON *event = NULL;
char *event_id = NULL;
nt_publish_result_t *results = NULL;
@@ -456,7 +458,9 @@ int nt_publish_detailed(const char *event_json,
client = nostr_ws_connect(relay);
if (!client) {
response = nt_strdup_local("connect_failed");
db_log_relay_post(relay, event_id, 0, response ? response : "connect_failed");
if (with_db_log) {
db_log_relay_post(relay, event_id, 0, response ? response : "connect_failed");
}
slot->posted = 0;
slot->response = response;
continue;
@@ -466,7 +470,9 @@ int nt_publish_detailed(const char *event_json,
if (nostr_relay_send_event(client, event) != NOSTR_WS_SUCCESS) {
response = nt_strdup_local("send_failed");
db_log_relay_post(relay, event_id, 0, response ? response : "send_failed");
if (with_db_log) {
db_log_relay_post(relay, event_id, 0, response ? response : "send_failed");
}
slot->posted = 0;
slot->response = response;
nostr_ws_close(client);
@@ -482,7 +488,9 @@ int nt_publish_detailed(const char *event_json,
accepted_count++;
}
db_log_relay_post(relay, event_id, posted, response ? response : "no_response");
if (with_db_log) {
db_log_relay_post(relay, event_id, posted, response ? response : "no_response");
}
slot->posted = posted;
slot->response = response;
@@ -498,6 +506,36 @@ int nt_publish_detailed(const char *event_json,
return accepted_count;
}
int nt_publish_detailed(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
nt_publish_result_t **results_out,
int *result_count_out) {
return nt_publish_detailed_impl(event_json,
relay_urls,
relay_count,
timeout_ms,
1,
results_out,
result_count_out);
}
int nt_publish_detailed_nodb(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
nt_publish_result_t **results_out,
int *result_count_out) {
return nt_publish_detailed_impl(event_json,
relay_urls,
relay_count,
timeout_ms,
0,
results_out,
result_count_out);
}
int nt_publish(const char *event_json,
const char **relay_urls,
int relay_count,
@@ -569,11 +607,10 @@ static __attribute__((unused)) int nt_query_one_relay(const char *relay_url,
cJSON_AddItemToArray(req_msg, cJSON_Duplicate(filter, 1));
{
char *req_json = cJSON_PrintUnformatted(req_msg);
fprintf(stderr,
"%s: request to %s: %s\n",
phase_label ? phase_label : "query",
relay_url ? relay_url : "(unknown relay)",
req_json ? req_json : "(serialize_failed)");
nt_log("%s: request to %s: %s",
phase_label ? phase_label : "query",
relay_url ? relay_url : "(unknown relay)",
req_json ? req_json : "(serialize_failed)");
free(req_json);
}
cJSON_Delete(req_msg);
@@ -601,11 +638,10 @@ static __attribute__((unused)) int nt_query_one_relay(const char *relay_url,
}
if (verbose) {
fprintf(stderr,
"%s: response from %s: %s\n",
phase_label ? phase_label : "query",
relay_url ? relay_url : "(unknown relay)",
buffer);
nt_log("%s: response from %s: %s",
phase_label ? phase_label : "query",
relay_url ? relay_url : "(unknown relay)",
buffer);
}
if (nostr_parse_relay_message(buffer, &message_type, &parsed) != NOSTR_WS_SUCCESS) {
@@ -619,11 +655,10 @@ static __attribute__((unused)) int nt_query_one_relay(const char *relay_url,
if (cJSON_IsObject(event_obj)) {
if (verbose) {
char *received_event = cJSON_PrintUnformatted(event_obj);
fprintf(stderr,
"%s: event from %s: %s\n",
phase_label ? phase_label : "query",
relay_url ? relay_url : "(unknown relay)",
received_event ? received_event : "(serialize_failed)");
nt_log("%s: event from %s: %s",
phase_label ? phase_label : "query",
relay_url ? relay_url : "(unknown relay)",
received_event ? received_event : "(serialize_failed)");
free(received_event);
}
@@ -716,10 +751,9 @@ int nt_query_sync_verbose(const char *filter_json,
}
if (verbose && phase_label) {
fprintf(stderr,
"%s: querying %d relays via nostr_core relay pool\n",
phase_label,
relay_count);
nt_log("%s: querying %d relays via nostr_core relay pool",
phase_label,
relay_count);
}
pool_events = nostr_relay_pool_query_sync(pool,
@@ -757,10 +791,9 @@ int nt_query_sync_verbose(const char *filter_json,
free(pool_events);
if (verbose && phase_label) {
fprintf(stderr,
"%s: relay pool collected %d unique events\n",
phase_label,
events_n);
nt_log("%s: relay pool collected %d unique events",
phase_label,
events_n);
}
nostr_relay_pool_destroy(pool);

View File

@@ -1,6 +1,7 @@
#include "tui.h"
#include <ncurses.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
@@ -16,9 +17,11 @@ static int g_body_row = 0;
static char g_app_name[128] = "NOSTR TERMINAL";
static char g_app_version[64] = "";
static char g_user[256] = "";
static char g_status_line[512] = "";
static NtLogEntry g_log_entries[NT_LOG_MAX];
static int g_log_head = 0;
static int g_log_count = 0;
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
static WINDOW *nt_body_window(void) {
return (WINDOW *)tuin_body_window();
@@ -86,17 +89,6 @@ int nt_run_external_editor(int (*spawn)(void *user), void *user) {
return result;
}
static const char *nt_log_latest(void) {
int idx;
if (g_log_count <= 0) {
return "";
}
idx = (g_log_head - 1 + NT_LOG_MAX) % NT_LOG_MAX;
return g_log_entries[idx].text;
}
TuiFrame nt_frame(const char *breadcrumb) {
static char composed_title[512];
TuiFrame frame;
@@ -115,7 +107,15 @@ TuiFrame nt_frame(const char *breadcrumb) {
TuiStatus nt_status(void) {
TuiStatus status;
status.text = nt_log_latest();
pthread_mutex_lock(&g_log_mutex);
if (g_status_line[0] == '\0' && g_log_count > 0) {
int idx = (g_log_head - 1 + NT_LOG_MAX) % NT_LOG_MAX;
snprintf(g_status_line, sizeof(g_status_line), "%s", g_log_entries[idx].text);
}
status.text = g_status_line;
pthread_mutex_unlock(&g_log_mutex);
return status;
}
@@ -160,13 +160,18 @@ void nt_log(const char *fmt, ...) {
snprintf(ts, sizeof(ts), "[--:--:--]");
}
pthread_mutex_lock(&g_log_mutex);
idx = g_log_head;
snprintf(g_log_entries[idx].text, sizeof(g_log_entries[idx].text), "%s %s", ts, msg);
snprintf(g_status_line, sizeof(g_status_line), "%s", g_log_entries[idx].text);
g_log_head = (g_log_head + 1) % NT_LOG_MAX;
if (g_log_count < NT_LOG_MAX) {
g_log_count++;
}
pthread_mutex_unlock(&g_log_mutex);
}
void nt_log_show(void) {
@@ -177,6 +182,7 @@ void nt_log_show(void) {
int i;
log_text[0] = '\0';
pthread_mutex_lock(&g_log_mutex);
for (i = 0; i < g_log_count; ++i) {
int idx = (g_log_head - g_log_count + i + NT_LOG_MAX) % NT_LOG_MAX;
int n = snprintf(log_text + pos, sizeof(log_text) - (size_t)pos, "%s%s",
@@ -191,13 +197,17 @@ void nt_log_show(void) {
}
pos += n;
}
pthread_mutex_unlock(&g_log_mutex);
tuin_pager_run(&frame, log_text, &status);
tuin_pager_run_ex(&frame, log_text, &status, 1);
}
void nt_log_clear(void) {
pthread_mutex_lock(&g_log_mutex);
g_log_head = 0;
g_log_count = 0;
g_status_line[0] = '\0';
pthread_mutex_unlock(&g_log_mutex);
}
void nt_pager_show_text(const char *breadcrumb, const char *text) {

View File

@@ -5,15 +5,109 @@
#include "tui.h"
#include "signer.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int publish_signed_event_with_report(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms) {
typedef struct {
char *label;
char *event_json;
char **relay_urls;
int relay_count;
int timeout_ms;
} NtPublishJob;
static int publish_signed_event_with_report_impl(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms,
int with_db_log);
static char *publish_strdup_local(const char *s) {
size_t n;
char *out;
if (!s) {
return NULL;
}
n = strlen(s);
out = (char *)malloc(n + 1U);
if (!out) {
return NULL;
}
memcpy(out, s, n + 1U);
return out;
}
static void publish_job_free(NtPublishJob *job) {
if (!job) {
return;
}
free(job->label);
free(job->event_json);
if (job->relay_urls) {
int i;
for (i = 0; i < job->relay_count; i++) {
free(job->relay_urls[i]);
}
free(job->relay_urls);
}
free(job);
}
static void *publish_worker_main(void *arg) {
NtPublishJob *job = (NtPublishJob *)arg;
nt_publish_result_t *results = NULL;
int result_count = 0;
int accepted_count = 0;
int i;
if (!job) {
return NULL;
}
nt_log("%s: posting to %d relay(s)...", job->label, job->relay_count);
accepted_count = nt_publish_detailed_nodb(job->event_json,
(const char **)job->relay_urls,
job->relay_count,
job->timeout_ms,
&results,
&result_count);
if (result_count <= 0) {
nt_log("%s: no relay attempts were made.", job->label);
publish_job_free(job);
return NULL;
}
for (i = 0; i < result_count; i++) {
const char *relay = results[i].relay_url ? results[i].relay_url : "(unknown relay)";
const char *response = results[i].response ? results[i].response : "(no response)";
if (results[i].posted) {
nt_log("[OK] %s", relay);
} else {
nt_log("[FAIL] %s", relay);
nt_log(" %s", response);
}
}
nt_log("%s: accepted by %d/%d relay(s).", job->label, accepted_count, result_count);
nt_publish_results_free(results, result_count);
publish_job_free(job);
return NULL;
}
static int publish_signed_event_with_report_impl(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms,
int with_db_log) {
char *event_json = NULL;
const char **write_relays;
int relay_count = 0;
@@ -38,12 +132,19 @@ int publish_signed_event_with_report(const char *label,
nt_log("%s", event_json);
nt_log("Posting to %d relay(s)...", relay_count);
accepted_count = nt_publish_detailed(event_json,
write_relays,
relay_count,
timeout_ms,
&results,
&result_count);
accepted_count = with_db_log
? nt_publish_detailed(event_json,
write_relays,
relay_count,
timeout_ms,
&results,
&result_count)
: nt_publish_detailed_nodb(event_json,
write_relays,
relay_count,
timeout_ms,
&results,
&result_count);
free(event_json);
@@ -68,3 +169,103 @@ int publish_signed_event_with_report(const char *label,
nt_publish_results_free(results, result_count);
return accepted_count;
}
int publish_signed_event_with_report(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms) {
return publish_signed_event_with_report_impl(label,
kind,
content,
tags,
timeout_ms,
1);
}
int publish_signed_event_with_report_async(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms) {
NtPublishJob *job;
pthread_t tid;
char *event_json = NULL;
const char **write_relays = NULL;
int relay_count = 0;
int i;
if (!label || label[0] == '\0') {
label = "Event";
}
/* Sign synchronously on UI thread; only network publish happens in background. */
if (signer_create_and_sign(kind, content ? content : "", tags, time(NULL), &event_json) != 0 || !event_json) {
nt_log("%s: failed to serialize event.", label);
return -1;
}
write_relays = state_get_write_relays(&relay_count);
job = (NtPublishJob *)calloc(1, sizeof(*job));
if (!job) {
free(event_json);
return -1;
}
job->label = publish_strdup_local(label);
job->event_json = event_json;
job->relay_count = relay_count;
job->timeout_ms = timeout_ms;
if (relay_count > 0) {
job->relay_urls = (char **)calloc((size_t)relay_count, sizeof(char *));
if (!job->relay_urls) {
publish_job_free(job);
return -1;
}
for (i = 0; i < relay_count; i++) {
const char *src = (write_relays && write_relays[i]) ? write_relays[i] : "";
job->relay_urls[i] = publish_strdup_local(src);
if (!job->relay_urls[i]) {
publish_job_free(job);
return -1;
}
}
}
if (!job->label || !job->event_json) {
publish_job_free(job);
return -1;
}
{
pthread_attr_t attr;
int has_attr = 0;
if (pthread_attr_init(&attr) == 0) {
has_attr = 1;
(void)pthread_attr_setstacksize(&attr, 1024U * 1024U);
}
if (pthread_create(&tid, has_attr ? &attr : NULL, publish_worker_main, job) != 0) {
if (has_attr) {
(void)pthread_attr_destroy(&attr);
}
publish_job_free(job);
return -1;
}
if (has_attr) {
(void)pthread_attr_destroy(&attr);
}
}
if (pthread_detach(tid) != 0) {
publish_job_free(job);
return -1;
}
return 0;
}

View File

@@ -10,13 +10,19 @@
#include "../resources/nostr_core_lib/nostr_core/nip044.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "tui.h"
nt_state_t g_state;
static pthread_mutex_t g_user_info_load_mutex = PTHREAD_MUTEX_INITIALIZER;
static int g_user_info_loading = 0;
static char *state_strdup_local(const char *s) {
size_t n;
char *out;
@@ -337,7 +343,7 @@ int state_load_user_info(void) {
free(g_state.user_settings_json);
g_state.user_settings_json = NULL;
fprintf(stderr, "Attempting to fetch kind 10002 relays from bootstrap relays\n");
nt_log("Attempting to fetch kind 10002 relays from bootstrap relays");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[10002],\"limit\":1}",
@@ -357,7 +363,7 @@ int state_load_user_info(void) {
state_parse_relay_list();
relays = state_get_read_relays(&relay_count);
fprintf(stderr, "Attempting to fetch kinds [0,3,10096,17375] from user relays\n");
nt_log("Attempting to fetch kinds [0,3,10096,17375] from user relays");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[0,3,10096,17375],\"limit\":200}",
@@ -424,7 +430,7 @@ int state_load_user_info(void) {
free(kind10096_event);
free(kind17375_event);
fprintf(stderr, "Attempting to fetch user-settings (kind 30078, d=user-settings)\n");
nt_log("Attempting to fetch user-settings (kind 30078, d=user-settings)");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"#d\":[\"user-settings\"],\"limit\":1}",
@@ -453,14 +459,13 @@ int state_load_user_info(void) {
free(settings_event_json);
}
fprintf(stderr,
"Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s user_settings=%s\n",
g_state.kind10002_json ? "ok" : "missing",
(g_state.kind0_json && strcmp(g_state.kind0_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind3_json && strstr(g_state.kind3_json, "\"tags\"") != NULL) ? "ok" : "missing",
(g_state.kind10096_json && strcmp(g_state.kind10096_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind17375_json && strcmp(g_state.kind17375_json, "{}") != 0) ? "ok" : "missing",
g_state.user_settings_json ? "ok" : "missing");
nt_log("Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s user_settings=%s",
g_state.kind10002_json ? "ok" : "missing",
(g_state.kind0_json && strcmp(g_state.kind0_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind3_json && strstr(g_state.kind3_json, "\"tags\"") != NULL) ? "ok" : "missing",
(g_state.kind10096_json && strcmp(g_state.kind10096_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind17375_json && strcmp(g_state.kind17375_json, "{}") != 0) ? "ok" : "missing",
g_state.user_settings_json ? "ok" : "missing");
snprintf(g_state.user_name, sizeof(g_state.user_name), "%s", "not logged in");
if (g_state.kind0_json) {
@@ -481,6 +486,52 @@ int state_load_user_info(void) {
return 0;
}
static void *state_user_info_load_worker(void *arg) {
(void)arg;
(void)state_load_user_info();
pthread_mutex_lock(&g_user_info_load_mutex);
g_user_info_loading = 0;
pthread_mutex_unlock(&g_user_info_load_mutex);
return NULL;
}
int state_load_user_info_async(void) {
pthread_t tid;
pthread_attr_t attr;
int has_attr = 0;
pthread_mutex_lock(&g_user_info_load_mutex);
if (g_user_info_loading) {
pthread_mutex_unlock(&g_user_info_load_mutex);
return 0;
}
g_user_info_loading = 1;
pthread_mutex_unlock(&g_user_info_load_mutex);
if (pthread_attr_init(&attr) == 0) {
has_attr = 1;
(void)pthread_attr_setstacksize(&attr, 1024U * 1024U);
}
if (pthread_create(&tid, has_attr ? &attr : NULL, state_user_info_load_worker, NULL) != 0) {
if (has_attr) {
(void)pthread_attr_destroy(&attr);
}
pthread_mutex_lock(&g_user_info_load_mutex);
g_user_info_loading = 0;
pthread_mutex_unlock(&g_user_info_load_mutex);
return -1;
}
if (has_attr) {
(void)pthread_attr_destroy(&attr);
}
(void)pthread_detach(tid);
return 0;
}
const char **state_get_read_relays(int *count) {
if (g_state.read_relay_count > 0 && g_state.read_relays) {
if (count) {