From 960cde3eda6fc250a0d64fd9dd2f7a3beb432b55 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 17 Jul 2026 16:25:55 +0300 Subject: [PATCH] chatgui: add Status page in settings (NTP + connections status) --- src/config_parser.c | 35 ++- src/config_parser.h | 7 +- src/ntp_time.c | 144 +++++++---- src/ntp_time.h | 7 +- tests/Makefile.am | 5 + tests/test_ntp.c | 251 ++++++++++++++++++++ tools/chatgui/CMakeLists.txt | 1 + tools/chatgui/src/settingsdialog.cpp | 7 + tools/chatgui/src/settingsdialog.h | 2 + tools/chatgui/src/statuspage.cpp | 68 ++++++ tools/chatgui/src/statuspage.h | 22 ++ tools/chatgui/transport/chat_core.c | 150 +++++++++++- tools/chatgui/transport/chat_core.h | 3 + tools/chatgui/transport/gui_bridge.h | 5 + tools/chatgui/transport/gui_bridge_impl.cpp | 8 + 15 files changed, 652 insertions(+), 63 deletions(-) create mode 100644 tests/test_ntp.c create mode 100644 tools/chatgui/src/statuspage.cpp create mode 100644 tools/chatgui/src/statuspage.h diff --git a/src/config_parser.c b/src/config_parser.c index c04a0cbf..4a5ed5aa 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -807,6 +807,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename) cfg->global.db_sync_enabled = 0; cfg->global.db_sync_ttl = 86400; cfg->global.ntp_enabled = 0; + cfg->global.ntp_servers = NULL; cfg->global.ntp_server_count = 0; cfg->global.ntp_resync_interval = 3600; @@ -983,13 +984,15 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename) if (strcmp(key, "enabled") == 0) { cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; } else if (strcmp(key, "server") == 0) { - if (cfg->global.ntp_server_count >= 5) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many NTP servers (max 5)", filename, line_num); - } else { - int idx = cfg->global.ntp_server_count; - strncpy(cfg->global.ntp_servers[idx], value, sizeof(cfg->global.ntp_servers[idx]) - 1); - cfg->global.ntp_servers[idx][sizeof(cfg->global.ntp_servers[idx]) - 1] = '\0'; + struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); + if (ns) { + strncpy(ns->name, value, sizeof(ns->name) - 1); + ns->name[sizeof(ns->name) - 1] = '\0'; + ns->next = cfg->global.ntp_servers; + cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; + } else { + DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, "%s:%d: Failed to allocate NTP server entry", filename, line_num); } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); @@ -1078,6 +1081,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha cfg->global.db_sync_enabled = 0; cfg->global.db_sync_ttl = 86400; cfg->global.ntp_enabled = 0; + cfg->global.ntp_servers = NULL; cfg->global.ntp_server_count = 0; cfg->global.ntp_resync_interval = 3600; @@ -1120,7 +1124,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha case SECTION_TCP_PROXY_SERVER: cfg->global.tcp_proxy_server_enabled = 1; if (strcmp(key, "tcp_recv_buf") == 0) cfg->global.tcp_recv_buf = atoi(value); break; case SECTION_MSG_TRANSPORT: parse_msg_transport(key, value, &cfg->global, filename, line_num); break; case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break; - case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0 && cfg->global.ntp_server_count < 5) { int idx = cfg->global.ntp_server_count; strncpy(cfg->global.ntp_servers[idx], value, sizeof(cfg->global.ntp_servers[idx]) - 1); cfg->global.ntp_servers[idx][sizeof(cfg->global.ntp_servers[idx]) - 1] = '\0'; cfg->global.ntp_server_count++; } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break; + case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break; case SECTION_GUI: break; default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break; } @@ -1198,6 +1202,14 @@ void free_config(struct utun_config *config) { ak = next; } + // Free NTP servers + struct CFG_NTP_SERVER *ns = config->global.ntp_servers; + while (ns) { + struct CFG_NTP_SERVER *next = ns->next; + u_free(ns); + ns = next; + } + u_free(config); } @@ -1304,6 +1316,15 @@ void print_config(const struct utun_config *cfg) { ak = ak->next; } } + + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NTP: enabled=%d, servers=%d, interval=%ds", g->ntp_enabled, g->ntp_server_count, g->ntp_resync_interval); + if (g->ntp_server_count > 0) { + struct CFG_NTP_SERVER *ns = g->ntp_servers; + while (ns) { + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " server=%s", ns->name); + ns = ns->next; + } + } } int update_config_keys(const char *filename, const char *priv_key, const char *pub_key) { diff --git a/src/config_parser.h b/src/config_parser.h index 89f46062..35408c74 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -92,6 +92,11 @@ struct CFG_ALLOWED_KEY { struct CFG_ALLOWED_KEY *next; }; +struct CFG_NTP_SERVER { + char name[256]; + struct CFG_NTP_SERVER *next; +}; + #define MAX_TCP_PROXY_CLIENT_MAPPINGS 32 struct tcp_proxy_client_mapping_config { uint16_t local_port; @@ -185,7 +190,7 @@ struct global_config { // NTP configuration ([ntp] section) int ntp_enabled; - char ntp_servers[5][256]; // up to 5 NTP server hostnames + struct CFG_NTP_SERVER *ntp_servers; // linked list, unlimited int ntp_server_count; int ntp_resync_interval; // seconds, default 3600 }; diff --git a/src/ntp_time.c b/src/ntp_time.c index 6ca755af..c5eb4ac2 100644 --- a/src/ntp_time.c +++ b/src/ntp_time.c @@ -15,6 +15,7 @@ #include #include #include + #include #endif #define NTP_DELTA 2208988800ULL // seconds from 1900 to 1970 @@ -54,24 +55,10 @@ static int64_t ntp64_to_us(uint64_t ntp) { return sec * 1000000LL + frac; } -static int ntp_query_server(const char* server, int64_t* offset_us_out, int* stratum_out) { - struct addrinfo hints, *result = NULL; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_DGRAM; - - char port_str[8]; - snprintf(port_str, sizeof(port_str), "%d", NTP_PORT); - int gai_err = getaddrinfo(server, port_str, &hints, &result); - if (gai_err != 0 || !result) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to resolve %s: %s", server, gai_strerror(gai_err)); - return -1; - } - +static int ntp_do_query(const struct sockaddr_in* sin, const char* server_name, int64_t* offset_us_out, int* stratum_out) { socket_t sock = socket_create_udp(AF_INET); if (sock == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: socket() failed: %s", socket_strerror(socket_get_error())); - freeaddrinfo(result); return -1; } @@ -87,14 +74,13 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str uint64_t t1_ntp = timeval_to_ntp(&t1_tv); request.xmit_ts = htobe64(t1_ntp); - struct sockaddr_in* sin = (struct sockaddr_in*)result->ai_addr; int send_ok = 0; int tries; for (tries = 0; tries < NTP_MAX_TRIES; tries++) { ssize_t sent = sendto(sock, (const char*)&request, sizeof(request), 0, - (struct sockaddr*)sin, sizeof(*sin)); + (const struct sockaddr*)sin, sizeof(*sin)); if (sent < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto(%s) failed: %s", server, socket_strerror(socket_get_error())); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto(%s) failed: %s", server_name, socket_strerror(socket_get_error())); break; } @@ -110,9 +96,8 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str if (r > 0) { send_ok = 1; break; } } if (!send_ok) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", server, tries); + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", server_name, tries); socket_close_wrapper(sock); - freeaddrinfo(result); return -1; } @@ -121,9 +106,8 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str socklen_t from_len = sizeof(from); ssize_t n = recvfrom(sock, (char*)&reply, sizeof(reply), 0, (struct sockaddr*)&from, &from_len); if (n < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: recvfrom(%s) failed: %s", server, socket_strerror(socket_get_error())); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: recvfrom(%s) failed: %s", server_name, socket_strerror(socket_get_error())); socket_close_wrapper(sock); - freeaddrinfo(result); return -1; } @@ -134,28 +118,27 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str gettimeofday(&t4_tv, NULL); #endif socket_close_wrapper(sock); - freeaddrinfo(result); if (n < (ssize_t)sizeof(reply)) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: short reply from %s (got %zd, expected %zu)", server, n, sizeof(reply)); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: short reply from %s (got %zd, expected %zu)", server_name, n, sizeof(reply)); return -1; } uint8_t mode = reply.li_vn_mode & 0x07; if (mode != 4) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: unexpected mode %d from %s (expected 4)", mode, server); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: unexpected mode %d from %s (expected 4)", mode, server_name); return -1; } int stratum = reply.stratum; if (stratum == 0) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: kiss-o-death from %s", server); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: kiss-o-death from %s", server_name); return -1; } uint64_t reply_orig = be64toh(reply.orig_ts); if (reply_orig != t1_ntp) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: originate timestamp mismatch from %s", server); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: originate timestamp mismatch from %s", server_name); return -1; } @@ -168,13 +151,32 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str int64_t rtt_us = (t4_us - t1_us) - (t3_us - t2_us); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: synced from %s offset=%lldus rtt=%lldus stratum=%d", - server, (long long)offset_us, (long long)rtt_us, stratum); + server_name, (long long)offset_us, (long long)rtt_us, stratum); *offset_us_out = offset_us; *stratum_out = stratum; return 0; } +static int ntp_query_server(const char* server, int64_t* offset_us_out, int* stratum_out) { + struct addrinfo hints, *result = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + + char port_str[8]; + snprintf(port_str, sizeof(port_str), "%d", NTP_PORT); + int gai_err = getaddrinfo(server, port_str, &hints, &result); + if (gai_err != 0 || !result) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to resolve %s: %s", server, gai_strerror(gai_err)); + return -1; + } + + int ret = ntp_do_query((const struct sockaddr_in*)result->ai_addr, server, offset_us_out, stratum_out); + freeaddrinfo(result); + return ret; +} + static void ntp_time_sync_cb(void* arg) { struct UTUN_INSTANCE* instance = (struct UTUN_INSTANCE*)arg; if (!instance) return; @@ -187,25 +189,37 @@ static void ntp_time_sync_cb(void* arg) { return; } - const char* server = ntp->servers[ntp->server_current]; int64_t offset_us = 0; int stratum = 0; - - if (ntp_query_server(server, &offset_us, &stratum) == 0) { - ntp->offset_us = offset_us; - ntp->synced = 1; - ntp->last_sync_tb = get_time_tb(); - ntp->server_current = (ntp->server_current + 1) % ntp->server_count; + int synced = 0; + + if (ntp->test_addr.sin_port != 0) { + if (ntp_do_query(&ntp->test_addr, "test-server", &offset_us, &stratum) == 0) { + ntp->offset_us = offset_us; + ntp->synced = 1; + ntp->last_sync_tb = get_time_tb(); + synced = 1; + } } else { - for (int i = 0; i < ntp->server_count; i++) { + const char* server = ntp->servers[ntp->server_current]; + if (ntp_query_server(server, &offset_us, &stratum) == 0) { + ntp->offset_us = offset_us; + ntp->synced = 1; + ntp->last_sync_tb = get_time_tb(); ntp->server_current = (ntp->server_current + 1) % ntp->server_count; - const char* next_server = ntp->servers[ntp->server_current]; - if (strcmp(next_server, server) == 0) break; - if (ntp_query_server(next_server, &offset_us, &stratum) == 0) { - ntp->offset_us = offset_us; - ntp->synced = 1; - ntp->last_sync_tb = get_time_tb(); - break; + synced = 1; + } else { + for (int i = 0; i < ntp->server_count; i++) { + ntp->server_current = (ntp->server_current + 1) % ntp->server_count; + const char* next_server = ntp->servers[ntp->server_current]; + if (strcmp(next_server, server) == 0) break; + if (ntp_query_server(next_server, &offset_us, &stratum) == 0) { + ntp->offset_us = offset_us; + ntp->synced = 1; + ntp->last_sync_tb = get_time_tb(); + synced = 1; + break; + } } } } @@ -233,12 +247,34 @@ int ntp_time_init(struct UTUN_INSTANCE* instance) { ntp->offset_us = 0; ntp->last_sync_tb = 0; ntp->timer = NULL; + ntp->servers = NULL; ntp->server_count = g->ntp_server_count; ntp->server_current = 0; ntp->resync_interval_sec = g->ntp_resync_interval; - for (int i = 0; i < g->ntp_server_count && i < NTP_MAX_SERVERS; i++) { - strncpy(ntp->servers[i], g->ntp_servers[i], sizeof(ntp->servers[i]) - 1); - ntp->servers[i][sizeof(ntp->servers[i]) - 1] = '\0'; + memset(&ntp->test_addr, 0, sizeof(ntp->test_addr)); + + if (g->ntp_server_count > 0) { + ntp->servers = u_malloc(g->ntp_server_count * sizeof(char*)); + if (!ntp->servers) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, "NTP: failed to allocate servers array"); + ntp->server_count = 0; + return -1; + } + struct CFG_NTP_SERVER *ns = g->ntp_servers; + int i = 0; + while (ns && i < g->ntp_server_count) { + ntp->servers[i] = u_strdup(ns->name); + if (!ntp->servers[i]) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, "NTP: failed to duplicate server name"); + while (--i >= 0) u_free(ntp->servers[i]); + u_free(ntp->servers); + ntp->servers = NULL; + ntp->server_count = 0; + return -1; + } + i++; + ns = ns->next; + } } if (!ntp->enabled || ntp->server_count == 0) { @@ -266,6 +302,11 @@ void ntp_time_destroy(struct UTUN_INSTANCE* instance) { uasync_cancel_timeout(instance->ua, ntp->timer); ntp->timer = NULL; } + if (ntp->servers) { + for (int i = 0; i < ntp->server_count; i++) u_free(ntp->servers[i]); + u_free(ntp->servers); + ntp->servers = NULL; + } ntp->enabled = 0; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: destroyed (synced=%d, offset=%lldus)", ntp->synced, (long long)ntp->offset_us); @@ -298,3 +339,14 @@ time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance) { int ntp_time_is_synced(struct UTUN_INSTANCE* instance) { return instance && instance->ntp.synced; } + +void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port) { + if (!ntp || !ip) return; + memset(&ntp->test_addr, 0, sizeof(ntp->test_addr)); + ntp->test_addr.sin_family = AF_INET; + ntp->test_addr.sin_port = htons(port); + if (inet_pton(AF_INET, ip, &ntp->test_addr.sin_addr) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: test addr parse failed for %s", ip); + memset(&ntp->test_addr, 0, sizeof(ntp->test_addr)); + } +} diff --git a/src/ntp_time.h b/src/ntp_time.h index 9632ec69..2be56644 100644 --- a/src/ntp_time.h +++ b/src/ntp_time.h @@ -3,6 +3,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -10,7 +11,6 @@ extern "C" { struct UTUN_INSTANCE; -#define NTP_MAX_SERVERS 5 #define NTP_DEFAULT_INTERVAL_SEC 3600 #define NTP_FIRST_SYNC_DELAY_SEC 0 @@ -20,15 +20,18 @@ struct NTP_TIME { int64_t offset_us; // local_time - ntp_time (positive = local clock ahead) uint64_t last_sync_tb; // monotonic time of last successful sync (0.1ms units) void* timer; // uasync timer handle - char servers[NTP_MAX_SERVERS][256]; + char **servers; // dynamic array, allocated at init int server_count; int server_current; int resync_interval_sec; + struct sockaddr_in test_addr; // test mode: direct address when sin_port != 0 }; int ntp_time_init(struct UTUN_INSTANCE* instance); void ntp_time_destroy(struct UTUN_INSTANCE* instance); +void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port); + int64_t ntp_time_get_us(struct UTUN_INSTANCE* instance); time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance); int ntp_time_is_synced(struct UTUN_INSTANCE* instance); diff --git a/tests/Makefile.am b/tests/Makefile.am index f664ba3f..3e97bedc 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -56,6 +56,7 @@ check_PROGRAMS = \ test_intensive_memory_pool \ test_tcp_io \ test_uasync_socket_race \ + test_ntp \ bench_timeout_heap \ bench_uasync_timeouts @@ -223,6 +224,10 @@ test_uasync_socket_race_SOURCES = test_uasync_socket_race.c test_uasync_socket_race_CFLAGS = -I$(top_srcdir)/lib test_uasync_socket_race_LDADD = $(COMMON_LIBS) +test_ntp_SOURCES = test_ntp.c +test_ntp_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_ntp_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_memory_pool_and_config_SOURCES = test_memory_pool_and_config.c test_memory_pool_and_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_memory_pool_and_config_LDADD = $(COMMON_LIBS) diff --git a/tests/test_ntp.c b/tests/test_ntp.c new file mode 100644 index 00000000..7a307b2f --- /dev/null +++ b/tests/test_ntp.c @@ -0,0 +1,251 @@ +/** + * @file test_ntp.c + * @brief NTP client test with local fake NTP server + * + * Starts a local NTP server in a separate thread that returns a known + * time offset. Verifies the NTP client syncs correctly: synced flag, + * offset computation, and ntp_time_get_us()/ntp_time_is_synced(). + */ +#include +#include +#include +#include +#include +#include +#include +#include "../lib/debug_config.h" +#include "../lib/u_async.h" +#include "../src/utun_instance.h" +#include "../src/ntp_time.h" +#include "../src/config_parser.h" + +#define TEST_KNOWN_OFFSET_SEC 5 +#define TEST_KNOWN_OFFSET_US (TEST_KNOWN_OFFSET_SEC * 1000000LL) +#define TEST_TIMEOUT_SEC 10 +#define OFFSET_TOLERANCE_US 500000 // 500ms — generous for scheduling jitter on localhost +#define SYNC_WAIT_POLL_MS 10 + +#define NTP_DELTA 2208988800ULL + +#pragma pack(push, 1) +struct ntp_packet { + uint8_t li_vn_mode; + uint8_t stratum; + uint8_t poll; + uint8_t precision; + uint32_t root_delay; + uint32_t root_dispersion; + uint32_t ref_id; + uint64_t ref_ts; + uint64_t orig_ts; + uint64_t recv_ts; + uint64_t xmit_ts; +}; +#pragma pack(pop) +_Static_assert(sizeof(struct ntp_packet) == 48, "bad ntp_packet size"); + +static volatile int g_server_running = 1; +static volatile uint16_t g_server_port = 0; + +static uint64_t timeval_to_ntp(struct timeval *tv) { + uint64_t sec = (uint64_t)(tv->tv_sec + NTP_DELTA); + uint64_t frac = ((uint64_t)tv->tv_usec << 32) / 1000000ULL; + return (sec << 32) | frac; +} + +static void* test_server_thread(void* arg) { + (void)arg; + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { g_server_running = 0; return NULL; } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = 0; + + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { close(sock); g_server_running = 0; return NULL; } + + socklen_t addr_len = sizeof(addr); + getsockname(sock, (struct sockaddr*)&addr, &addr_len); + g_server_port = ntohs(addr.sin_port); + + struct timeval tv = {1, 0}; + while (g_server_running) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + tv.tv_sec = 1; tv.tv_usec = 0; + int r = select(sock + 1, &fds, NULL, NULL, &tv); + if (r <= 0) continue; + + struct ntp_packet request; + struct sockaddr_in client; + socklen_t client_len = sizeof(client); + ssize_t n = recvfrom(sock, (char*)&request, sizeof(request), 0, (struct sockaddr*)&client, &client_len); + if (n < (ssize_t)sizeof(request)) continue; + + uint8_t mode = request.li_vn_mode & 0x07; + if (mode != 3) continue; // only respond to client-mode + + struct timeval now; + gettimeofday(&now, NULL); + now.tv_sec += TEST_KNOWN_OFFSET_SEC; + uint64_t fake_ntp = timeval_to_ntp(&now); + + struct ntp_packet reply; + memset(&reply, 0, sizeof(reply)); + reply.li_vn_mode = (0 << 6) | (4 << 3) | 4; // stratum 0=unspec, v4, server + reply.stratum = 1; + reply.ref_id = htonl(0x7F000001); // 127.0.0.1 + reply.orig_ts = request.xmit_ts; // echo client's transmit + reply.recv_ts = htobe64(fake_ntp); + reply.xmit_ts = htobe64(fake_ntp); + + sendto(sock, (const char*)&reply, sizeof(reply), 0, (struct sockaddr*)&client, sizeof(client)); + } + close(sock); + return NULL; +} + +static int wait_for_port(void) { + for (int i = 0; i < 200; i++) { + if (g_server_port != 0) return 0; + usleep(5000); + } + return -1; +} + +int main(void) { + int result = 1; + + debug_config_init(); + debug_set_level(DEBUG_LEVEL_WARN); + debug_set_categories(DEBUG_CATEGORY_GENERAL); + + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== NTP Test ==="); + + /* ── 1. Start fake NTP server thread ── */ + pthread_t server_thread; + if (pthread_create(&server_thread, NULL, test_server_thread, NULL) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create server thread"); + return 1; + } + if (wait_for_port() != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Server thread did not bind"); + g_server_running = 0; + pthread_join(server_thread, NULL); + return 1; + } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Fake NTP server on 127.0.0.1:%u, offset=+%ds", g_server_port, TEST_KNOWN_OFFSET_SEC); + + /* ── 2. Create uTun instance with minimal config ── */ + struct UASYNC* ua = uasync_create(); + if (!ua) { g_server_running = 0; pthread_join(server_thread, NULL); return 1; } + + utun_instance_set_tun_init_enabled(0); + + /* Parse config from buffer and inject keys (avoid config_ensure_keys_and_node_id rewriting) */ + const char *config_text = + "[global]\n" + "my_node_name=ntp_test\n" + "my_private_key=e8d4d14943ca3732d7802d4010ea4f6f8bdb71e2304f78d68f4f3dc31e760c79\n" + "my_public_key=8bac8ff3b3344cd3c1168b4e55bb4673f4063b5d42afb05fc70658f5da27907c\n" + "my_node_id=721256a8d204e355\n" + "\n" + "[ntp]\n" + "enabled=yes\n" + "server=placeholder\n" + "interval=3600\n"; + + struct utun_config* cfg = parse_config_from_buf(config_text, strlen(config_text), "inline"); + if (!cfg) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to parse config from buffer"); + uasync_destroy(ua, 0); + g_server_running = 0; + pthread_join(server_thread, NULL); + return 1; + } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Parsed config: ntp_enabled=%d server_count=%d", + cfg->global.ntp_enabled, cfg->global.ntp_server_count); + + struct UTUN_INSTANCE* inst = utun_instance_create_from_config(ua, cfg); + if (!inst) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create instance from config"); + free_config(cfg); + uasync_destroy(ua, 0); + g_server_running = 0; + pthread_join(server_thread, NULL); + return 1; + } + + /* utun_instance_create only calls instance_init_common (basic inits). + utun_instance_init is needed for NTP, connections, etc. */ + utun_instance_init(inst); + + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP state after init: enabled=%d server_count=%d interval=%d servers[0]=%s config_enabled=%d config_count=%d", + inst->ntp.enabled, inst->ntp.server_count, inst->ntp.resync_interval_sec, + inst->ntp.server_count > 0 ? inst->ntp.servers[0] : "NONE", + inst->config ? inst->config->global.ntp_enabled : -1, + inst->config ? inst->config->global.ntp_server_count : -1); + + /* ── 3. Set test address BEFORE first uasync poll (timer fires at delay 0) ── */ + ntp_time_set_test_addr(&inst->ntp, "127.0.0.1", g_server_port); + + /* ── 4. Poll until synced or timeout ── */ + uint64_t start_tb = get_time_tb(); + while (get_time_tb() - start_tb < (uint64_t)(TEST_TIMEOUT_SEC * 10000)) { + uasync_poll(ua, SYNC_WAIT_POLL_MS); + if (inst->ntp.synced) break; + } + + /* ── 5. Verify ── */ + if (!inst->ntp.synced) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: NTP did not sync within %ds", TEST_TIMEOUT_SEC); + goto cleanup; + } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP synced, offset=%lldus", (long long)inst->ntp.offset_us); + + // Check offset is close to expected (formula gives server - client ≈ KNOWN_OFFSET) + { + int64_t diff = inst->ntp.offset_us - TEST_KNOWN_OFFSET_US; + if (diff < 0) diff = -diff; + if (diff > OFFSET_TOLERANCE_US) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: offset %lldus too far from expected %lldus (diff=%lldus)", + (long long)inst->ntp.offset_us, (long long)TEST_KNOWN_OFFSET_US, (long long)diff); + goto cleanup; + } + } + + // Check ntp_time_is_synced + if (!ntp_time_is_synced(inst)) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: ntp_time_is_synced returned 0 after sync"); + goto cleanup; + } + + // Check ntp_time_get_us internal consistency + { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t raw_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; + int64_t corrected_us = ntp_time_get_us(inst); + int64_t expected_us = raw_us - inst->ntp.offset_us; + int64_t diff = corrected_us - expected_us; + if (diff < 0) diff = -diff; + if (diff > 10000) { // 10ms — time passes between gettimeofday calls + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: ntp_time_get_us(%lld) != gettimeofday(%lld) - offset(%lld), diff=%lldus", + (long long)corrected_us, (long long)raw_us, (long long)inst->ntp.offset_us, (long long)diff); + goto cleanup; + } + } + + result = 0; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP Test PASSED"); + +cleanup: + utun_instance_destroy(inst); + uasync_destroy(ua, 0); + g_server_running = 0; + pthread_join(server_thread, NULL); + return result; +} diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 37bd6546..94b04e9f 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -69,6 +69,7 @@ add_executable(chatgui src/settingsdialog.cpp src/networksettingspage.cpp src/databasesettingspage.cpp + src/statuspage.cpp transport/utun_node.cpp transport/node_config.cpp transport/config_updater.cpp diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index fd6dc177..02e42335 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -2,6 +2,7 @@ #include "settingsdialog.h" #include "networksettingspage.h" #include "databasesettingspage.h" +#include "statuspage.h" #include "../transport/gui_bridge.h" #include "../../lib/mem.h" #include "../db/db_manager.h" @@ -40,6 +41,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget m_categoryList->addItem("Profile"); m_categoryList->addItem("Network"); m_categoryList->addItem("Database"); + m_categoryList->addItem("Status"); mainLayout->addWidget(m_categoryList); // --- Right panel: stacked pages --- @@ -71,6 +73,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget // Database page m_databasePage = new DatabaseSettingsPage(db, m_pages); m_pages->addWidget(m_databasePage); + + // Status page + m_statusPage = new StatusPage(m_pages); + m_pages->addWidget(m_statusPage); rightLayout->addWidget(m_pages, 1); // --- Bottom buttons --- @@ -127,6 +133,7 @@ void SettingsDialog::loadProfileFromConfig(const QString& path) { void SettingsDialog::onCategoryChanged(int row) { m_pages->setCurrentIndex(row); + if (row == 3) m_statusPage->refreshStatus(); } void SettingsDialog::onSave() { diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index edde94a9..4f2aa4d3 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -10,6 +10,7 @@ class NetworkSettingsPage; class DatabaseSettingsPage; +class StatusPage; class DbManager; class SettingsDialog : public QDialog { @@ -32,4 +33,5 @@ private: QString m_oldNickname; NetworkSettingsPage* m_networkPage; DatabaseSettingsPage* m_databasePage; + StatusPage* m_statusPage; }; diff --git a/tools/chatgui/src/statuspage.cpp b/tools/chatgui/src/statuspage.cpp new file mode 100644 index 00000000..19892fd3 --- /dev/null +++ b/tools/chatgui/src/statuspage.cpp @@ -0,0 +1,68 @@ +// statuspage.cpp — Status page (NTP + connections) in settings +#include "statuspage.h" + +extern "C" { +#include "../transport/gui_bridge.h" +#include "../transport/chat_core.h" +} + +#include +#include +#include +#include + +static StatusPage* g_activeStatusPage = nullptr; + +static void statusRefreshCallback(const char* text, int len) { + if (g_activeStatusPage) + g_activeStatusPage->setText(QString::fromUtf8(text, len)); +} + +StatusPage::StatusPage(QWidget* parent) + : QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + layout->setSpacing(8); + layout->setContentsMargins(0, 0, 0, 0); + + auto* title = new QLabel("System Status", this); + title->setStyleSheet("font-weight: bold; font-size: 13px;"); + layout->addWidget(title); + + m_textEdit = new QPlainTextEdit(this); + m_textEdit->setReadOnly(true); + m_textEdit->setFont(QFont("monospace", 10)); + m_textEdit->setStyleSheet( + "QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;" + " padding: 8px; background: palette(base); }"); + m_textEdit->setPlaceholderText("Click Refresh to load status"); + layout->addWidget(m_textEdit, 1); + + auto* btnLayout = new QHBoxLayout(); + btnLayout->addStretch(); + m_refreshBtn = new QPushButton("Refresh", this); + m_refreshBtn->setFixedWidth(90); + m_refreshBtn->setStyleSheet( + "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" + " padding: 6px 16px; font-size: 13px; background: palette(button); }" + "QPushButton:hover { background: palette(light); }"); + connect(m_refreshBtn, &QPushButton::clicked, this, &StatusPage::onRefreshClicked); + btnLayout->addWidget(m_refreshBtn); + layout->addLayout(btnLayout); + + g_activeStatusPage = this; + gui_bridge_set_status_refresh_cb(statusRefreshCallback); +} + +StatusPage::~StatusPage() { + if (g_activeStatusPage == this) g_activeStatusPage = nullptr; +} + +void StatusPage::refreshStatus() { + m_textEdit->setPlainText("Loading..."); + gui_bridge_post_uasync_fn(chat_core_collect_status_trampoline, NULL); +} + +void StatusPage::onRefreshClicked() { + refreshStatus(); +} diff --git a/tools/chatgui/src/statuspage.h b/tools/chatgui/src/statuspage.h new file mode 100644 index 00000000..2572336c --- /dev/null +++ b/tools/chatgui/src/statuspage.h @@ -0,0 +1,22 @@ +// statuspage.h — Status page (NTP + connections) in settings +#pragma once + +#include +#include +#include + +class StatusPage : public QWidget { + Q_OBJECT +public: + explicit StatusPage(QWidget* parent = nullptr); + ~StatusPage() override; + void refreshStatus(); + void setText(const QString& text) { m_textEdit->setPlainText(text); } + +private slots: + void onRefreshClicked(); + +private: + QPlainTextEdit* m_textEdit; + QPushButton* m_refreshBtn; +}; diff --git a/tools/chatgui/transport/chat_core.c b/tools/chatgui/transport/chat_core.c index f2f5067f..e47dad65 100644 --- a/tools/chatgui/transport/chat_core.c +++ b/tools/chatgui/transport/chat_core.c @@ -307,8 +307,27 @@ void chat_core_update_my_name_trampoline(void* arg) { u_free(arg); } +static void chat_core_collect_status(void); + +void chat_core_collect_status_trampoline(void* arg) { + (void)arg; + chat_core_collect_status(); +} + /* ─── отправка сообщения (GUI → uasync) ─── */ +static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) { + char sql[128]; snprintf(sql, sizeof(sql), + "SELECT chain_hash FROM \"%s\" ORDER BY timestamp DESC LIMIT 1", tbl); + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; } + if (sqlite3_step(st) == SQLITE_ROW) { + const void* blob = sqlite3_column_blob(st, 0); int blen = sqlite3_column_bytes(st, 0); + if (blob && blen == 32) memcpy(out, blob, 32); else memset(out, 0, 32); + } else { memset(out, 0, 32); } + sqlite3_finalize(st); return 0; +} + void chat_core_submit_message(struct chat_msg_submit* req) { if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: submit before init", CC_ID); return; } if (!req) return; @@ -342,14 +361,26 @@ void chat_core_submit_message(struct chat_msg_submit* req) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret); /* Insert directly to msg_ table as fallback (dual-write not triggered by callback) */ char tbl[80]; msg_table_name(req->channel_id, tbl, sizeof(tbl)); - char sql[512]; snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,signature,is_outgoing,is_read) VALUES(?,?,?,?,?,1,1)", tbl); + + /* compute datahash and chain_hash */ + uint8_t dh_buf2[32]; SHA256(req->data, req->data_len, dh_buf2); + uint64_t datahash2; memcpy(&datahash2, dh_buf2, 8); + uint8_t prev_ch2[32]; msg_get_prev_chain_hash(tbl, prev_ch2); + uint8_t ch_buf2[48]; memcpy(ch_buf2, prev_ch2, 32); memcpy(ch_buf2+32, &req->timestamp, 8); memcpy(ch_buf2+40, &datahash2, 8); + uint8_t ch_hash2[32]; SHA256(ch_buf2, 48, ch_hash2); + + char sql[512]; snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,datahash,chain_hash,signature,is_outgoing,is_read) VALUES(?,?,?,?,?,?,?,?,?)", tbl); sqlite3_stmt* st=NULL; if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)==SQLITE_OK) { sqlite3_bind_int64(st,1,(sqlite3_int64)g_cc.my_node_id); sqlite3_bind_text(st,2,req->content_type,-1,SQLITE_STATIC); sqlite3_bind_blob(st,3,req->data,(int)req->data_len,SQLITE_STATIC); sqlite3_bind_int64(st,4,(sqlite3_int64)req->timestamp); - sqlite3_bind_blob(st,5,sig,64,SQLITE_STATIC); + sqlite3_bind_int64(st,5,(sqlite3_int64)datahash2); + sqlite3_bind_blob(st,6,ch_hash2,32,SQLITE_STATIC); + sqlite3_bind_blob(st,7,sig,64,SQLITE_STATIC); + sqlite3_bind_int(st,8,1); + sqlite3_bind_int(st,9,1); sqlite3_step(st); sqlite3_finalize(st); } /* notify GUI anyway */ @@ -977,11 +1008,18 @@ void chat_core_ensure_channel_ready(const char* ch_id) { " content_type TEXT NOT NULL," " data BLOB NOT NULL," " timestamp INTEGER NOT NULL," + " datahash INTEGER DEFAULT 0," + " chain_hash BLOB," " signature BLOB," " is_outgoing INTEGER DEFAULT 0," " is_read INTEGER DEFAULT 0," " UNIQUE(timestamp, node_id))", tbl_msg); db_exec(sql); + /* migration for existing tables without datahash/chain_hash columns */ + snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN datahash INTEGER DEFAULT 0", tbl_msg); + sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL); + snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN chain_hash BLOB", tbl_msg); + sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL); snprintf(sql, sizeof(sql), "CREATE INDEX IF NOT EXISTS \"idx_%s_ts_node\" ON \"%s\"(timestamp, node_id)", tbl_msg, tbl_msg); @@ -1103,6 +1141,7 @@ static void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) { g_cc.si[g_cc.si_count]=si; g_cc.si_ch_id[g_cc.si_count]=u_strdup(ch_id); g_cc.si_count++; } + static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) { const char* ch_id = (const char*)arg; static int insert_count = 0; @@ -1123,18 +1162,28 @@ static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, c if (!jd_start) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert JSON parse fail: no '\"d\"' field ch=%s", CC_ID, ch_id); return; } char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + + /* compute datahash and chain_hash */ + uint8_t dh_buf[32]; SHA256((const uint8_t*)jd_start, jd_len, dh_buf); + uint64_t datahash; memcpy(&datahash, dh_buf, 8); + uint8_t prev_ch[32]; msg_get_prev_chain_hash(tbl, prev_ch); + uint8_t ch_buf[48]; memcpy(ch_buf, prev_ch, 32); memcpy(ch_buf+32, &record_ts, 8); memcpy(ch_buf+40, &datahash, 8); + uint8_t ch_hash[32]; SHA256(ch_buf, 48, ch_hash); + char sql[512]; snprintf(sql, sizeof(sql), - "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,signature,is_outgoing,is_read)" - " VALUES(?,?,?,?,?,?,?)", tbl); + "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,datahash,chain_hash,signature,is_outgoing,is_read)" + " VALUES(?,?,?,?,?,?,?,?,?)", tbl); sqlite3_stmt* st=NULL; if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)!=SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert prepare failed ch=%s", CC_ID, ch_id); return; } sqlite3_bind_int64(st,1,(sqlite3_int64)jn); sqlite3_bind_text(st,2,jct,(int)jct_len,SQLITE_STATIC); sqlite3_bind_blob(st,3,jd_start,(int)jd_len,SQLITE_STATIC); sqlite3_bind_int64(st,4,(sqlite3_int64)record_ts); - { static const uint8_t z64[64]={0}; sqlite3_bind_blob(st,5,z64,64,SQLITE_STATIC); } - sqlite3_bind_int(st,6,(uint64_t)jn==g_cc.my_node_id?1:0); - sqlite3_bind_int(st,7,1); + sqlite3_bind_int64(st,5,(sqlite3_int64)datahash); + sqlite3_bind_blob(st,6,ch_hash,32,SQLITE_STATIC); + { static const uint8_t z64[64]={0}; sqlite3_bind_blob(st,7,z64,64,SQLITE_STATIC); } + sqlite3_bind_int(st,8,(uint64_t)jn==g_cc.my_node_id?1:0); + sqlite3_bind_int(st,9,1); int rc=sqlite3_step(st); sqlite3_finalize(st); if (rc==SQLITE_DONE) { insert_count++; @@ -1152,3 +1201,90 @@ static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, c CC_ID, rc, ch_id); } } + +/* ─── сбор статуса (NTP + connections) и отправка в GUI ─── */ + +static const char* conn_state_str(uint8_t state) { + switch (state) { case 0: return "DISCONNECTED"; case 1: return "CONNECTING"; case 2: return "CONNECTED"; default: return "?"; } +} +static const char* conn_type_str(uint8_t t) { + switch (t) { case 0: return "NONE"; case 1: return "DIRECT"; case 2: return "REVERSE"; case 3: return "INDIRECT"; default: return "?"; } +} +static const char* nat_type_str(uint8_t t) { + switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; } +} + +static void chat_core_collect_status(void) { + char buf[8192]; int off = 0; + + if (!g_cc.initialized || !g_cc.inst) { + off = snprintf(buf, sizeof(buf), "uTun not initialized\n"); + gui_bridge_post(GUI_EVT_STATUS_REFRESH, (const uint8_t*)buf, off); + return; + } + + /* ─── NTP ─── */ + struct NTP_TIME* ntp = &g_cc.inst->ntp; + off += snprintf(buf + off, sizeof(buf) - off, "=== NTP ===\n"); + off += snprintf(buf + off, sizeof(buf) - off, "Enabled: %s\n", ntp->enabled ? "yes" : "no"); + off += snprintf(buf + off, sizeof(buf) - off, "Synced: %s\n", ntp->synced ? "yes" : "no"); + off += snprintf(buf + off, sizeof(buf) - off, "Offset: %lld us\n", (long long)ntp->offset_us); + if (ntp->server_count > 0 && ntp->servers) { + off += snprintf(buf + off, sizeof(buf) - off, "Servers: %d\n", ntp->server_count); + for (int i = 0; i < ntp->server_count && ntp->servers[i]; i++) + off += snprintf(buf + off, sizeof(buf) - off, " [%d] %s%s\n", i, ntp->servers[i], + i == ntp->server_current ? " (current)" : ""); + } + if (ntp->synced && ntp->last_sync_tb > 0) { + uint64_t now_tb = get_time_tb(); + uint64_t ago_sec = (now_tb - ntp->last_sync_tb) / 10000; + off += snprintf(buf + off, sizeof(buf) - off, "Last sync: %llu sec ago\n", (unsigned long long)ago_sec); + } + off += snprintf(buf + off, sizeof(buf) - off, "\n"); + + /* ─── Connections ─── */ + int conn_count = 0; + struct ll_entry* entry = g_cc.inst->connections->head; + while (entry) { conn_count++; entry = entry->next; } + off += snprintf(buf + off, sizeof(buf) - off, "=== Connections (%d) ===\n", conn_count); + + entry = g_cc.inst->connections->head; + while (entry) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; + if (!ce || !ce->conn) { entry = entry->next; continue; } + + uint64_t pid = ce->peer_node_id; + struct ETCP_CONN* conn = ce->conn; + + uint8_t state = 0, ctype = 0; + conn_mgr_get_status(g_cc.inst->conn_mgr, pid, &state, &ctype); + + off += snprintf(buf + off, sizeof(buf) - off, + "Peer 0x%016llx links_up=%d initialized=%d\n", + (unsigned long long)pid, conn->links_up, conn->initialized); + off += snprintf(buf + off, sizeof(buf) - off, " State: %s Type: %s\n", + conn_state_str(state), conn_type_str(ctype)); + + int link_idx = 0; + struct ETCP_LINK* link = conn->links; + while (link) { + off += snprintf(buf + off, sizeof(buf) - off, + " Link #%d: local_id=%d remote_id=%d status=%s recv=%s remote=%s NAT=%s", + link_idx, link->local_link_id, link->remote_link_id, + link->link_status ? "UP" : "DOWN", + link->recv_keepalive ? "UP" : "DOWN", + link->remote_keepalive ? "UP" : "DOWN", + nat_type_str(link->nat_type)); + if (link->rtt_last > 0) + off += snprintf(buf + off, sizeof(buf) - off, " rtt=%ums", link->rtt_last); + if (link->bandwidth > 0) + off += snprintf(buf + off, sizeof(buf) - off, " bw=%uKbps", link->bandwidth); + off += snprintf(buf + off, sizeof(buf) - off, "\n"); + link = link->next; link_idx++; + } + + entry = entry->next; + } + + gui_bridge_post(GUI_EVT_STATUS_REFRESH, (const uint8_t*)buf, off); +} diff --git a/tools/chatgui/transport/chat_core.h b/tools/chatgui/transport/chat_core.h index a957a218..b38a4720 100644 --- a/tools/chatgui/transport/chat_core.h +++ b/tools/chatgui/transport/chat_core.h @@ -92,4 +92,7 @@ void chat_core_update_my_name_trampoline(void* arg); /* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ void chat_core_submit_trampoline(void* arg); +/* Сбор статуса (NTP + connections) и отправка в GUI */ +void chat_core_collect_status_trampoline(void* arg); + #endif /* CHAT_CORE_H */ diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index c1c4d2bb..8f44ace9 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -21,6 +21,7 @@ struct UASYNC; #define GUI_EVT_AUTO_CONNECT_STATUS 7 /* data: [status:1][total_tried:2][node_count:2][success:2] */ #define GUI_EVT_CHANNEL_PEERS_ONLINE 8 /* data: [ch_id_len:1][ch_id:var][online_count:2] */ #define GUI_EVT_DB_READY 9 /* data: none — DB is open, tables created */ +#define GUI_EVT_STATUS_REFRESH 10 /* data: status text (null-terminated string) */ /* ── API ── */ @@ -72,6 +73,10 @@ void gui_bridge_set_channel_peers_online_cb(gui_channel_peers_online_fn cb); typedef void (*gui_db_ready_fn)(void); void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb); +/* Callback для обновления статуса (NTP + connections) — text без \0, вызывается из GUI-потока */ +typedef void (*gui_status_refresh_fn)(const char* text, int len); +void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 85ce30d7..e8c4fda7 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -32,6 +32,7 @@ static gui_my_node_id_fn g_my_node_id_cb = nullptr; static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr; static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr; static gui_db_ready_fn g_db_ready_cb = nullptr; +static gui_status_refresh_fn g_status_refresh_cb = nullptr; static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ @@ -126,6 +127,9 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: DB_READY"); if (g_db_ready_cb) g_db_ready_cb(); break; + case GUI_EVT_STATUS_REFRESH: + if (g_status_refresh_cb) g_status_refresh_cb((const char*)d, dlen); + break; default: DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); break; @@ -204,6 +208,10 @@ void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb) { g_db_ready_cb = cb; } +void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb) { + g_status_refresh_cb = cb; +} + } /* extern "C" */ #include "gui_bridge_impl.moc"