15 changed files with 652 additions and 63 deletions
@ -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 <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <unistd.h> |
||||
#include <pthread.h> |
||||
#include <arpa/inet.h> |
||||
#include <sys/time.h> |
||||
#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; |
||||
} |
||||
@ -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 <QVBoxLayout> |
||||
#include <QHBoxLayout> |
||||
#include <QLabel> |
||||
#include <QFont> |
||||
|
||||
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(); |
||||
} |
||||
@ -0,0 +1,22 @@
|
||||
// statuspage.h — Status page (NTP + connections) in settings
|
||||
#pragma once |
||||
|
||||
#include <QWidget> |
||||
#include <QPlainTextEdit> |
||||
#include <QPushButton> |
||||
|
||||
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; |
||||
}; |
||||
Loading…
Reference in new issue