From ca176e1f24d41433e9aa73202f49895e076d09cd Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 8 Aug 2026 16:56:32 +0300 Subject: [PATCH] =?UTF-8?q?encrypt=20overhead=20fix=20(33=E2=86=9236,=20no?= =?UTF-8?q?encrypt=5Flen),=20mtu=20default=201280,=20connmonitor=20window,?= =?UTF-8?q?=20total=5Fdecrypted=20for=20TCP,=20check.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + AGENTS.md | 4 +- check.sh | 21 ++ src/chat/chat_core.h | 6 + src/chat/chat_event.c | 4 +- src/chat/chat_event.h | 2 + src/chat/chat_status.c | 172 ++++++++++++++++ src/config_parser.c | 4 +- src/routing_layer/topo_group.c | 2 +- src/transport_layer/auto_socket.c | 2 +- src/transport_layer/etcp.c | 7 +- src/transport_layer/etcp_connections.c | 18 +- src/transport_layer/etcp_connections.h | 7 +- src/transport_layer/node_conn_direct.c | 20 +- src/transport_layer/pkt_normalizer.c | 7 +- src/transport_layer/stcp_link.c | 2 + tools/chatgui-android/AGENTS.md | 6 + tools/chatgui-android/build.sh | 19 ++ tools/chatgui/CMakeLists.txt | 1 + tools/chatgui/src/animtimer.cpp | 4 +- tools/chatgui/src/audiodevicesettingspage.cpp | 2 +- tools/chatgui/src/audiorecorder.cpp | 8 +- tools/chatgui/src/channellist.cpp | 13 +- tools/chatgui/src/channellist.h | 1 + tools/chatgui/src/connmonitorwindow.cpp | 189 ++++++++++++++++++ tools/chatgui/src/connmonitorwindow.h | 34 ++++ tools/chatgui/src/main.cpp | 6 +- tools/chatgui/src/mainwindow.cpp | 27 ++- tools/chatgui/src/mainwindow.h | 3 + tools/chatgui/src/media_blocks.cpp | 2 +- tools/chatgui/src/messagedelegate.cpp | 19 +- tools/chatgui/src/messagedelegate.h | 2 +- tools/chatgui/src/messagelist.cpp | 6 +- tools/chatgui/src/settingsdialog.cpp | 40 +++- tools/chatgui/src/settingsdialog.h | 3 + tools/chatgui/src/sound_manager.cpp | 6 +- tools/chatgui/src/voiceplayback.cpp | 10 +- tools/chatgui/src/voiceplayback.h | 8 +- tools/chatgui/transport/gui_bridge.h | 10 + tools/chatgui/transport/gui_bridge_impl.cpp | 18 +- tools/chatgui/transport/utun_node.cpp | 18 +- 41 files changed, 643 insertions(+), 91 deletions(-) create mode 100755 check.sh create mode 100755 tools/chatgui-android/build.sh create mode 100644 tools/chatgui/src/connmonitorwindow.cpp create mode 100644 tools/chatgui/src/connmonitorwindow.h diff --git a/.gitignore b/.gitignore index f188cce8..bf5461d7 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,4 @@ build_win.log src/lwip_orig/ lib/timeout/ utun.pid +.check_time diff --git a/AGENTS.md b/AGENTS.md index 7d1ad107..62839536 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,7 +96,7 @@ ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /t ### Run All Tests ```bash -make check # Run all tests via automake, логи в tests/logs/ - запускай тесты с ПОЛНЫМ ВЫОДОМ, не урезая его ( grep итд) - иначе будешь 10 раз повторять зпауск что жрёт много времени +./check.sh # сборка + тесты (только результаты, при ошибке — хвост) ``` ```powershell powershell check.bat # Windows, запускает каждый .exe из tests/ @@ -695,7 +695,7 @@ C-ядро: `libutun_lite` (выборочная компиляция нужны 1. Add new source file to `src/Makefile.am` under `utun_SOURCES` 2. Add test file to `tests/Makefile.am` under `check_PROGRAMS` 3. Use existing patterns from similar modules -4. Run `make check` after changes +5. Run `./check.sh` after changes 5. Commit with descriptive message in appropriate language ## chatgui: отправка сообщений через xdotool diff --git a/check.sh b/check.sh new file mode 100755 index 00000000..ce0e53ab --- /dev/null +++ b/check.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -eo pipefail +cd "$(dirname "$0")" + +LAST=0; [ -f .check_time ] && read -r LAST < .check_time +TMO=$(( LAST + 30 )) +[ $TMO -lt 90 ] && TMO=90 + +START=$(date +%s) +TMP=$(mktemp) +timeout $TMO make check -j4 > "$TMP" 2>&1 || { + tail -40 "$TMP"; rm -f "$TMP" + [ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2 + exit 1 +} +grep -E '\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP" +rm -f "$TMP" + +ELAPSED=$(( $(date +%s) - START )) +echo "Time: ${ELAPSED}s" +echo "$ELAPSED" > .check_time diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index fcf3ef93..a76108ad 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -103,6 +103,12 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts, /* Сбор статуса (NTP + connections) и отправка в GUI */ void chat_core_collect_status_trampoline(void* arg); +/* Сбор списка ETCP-соединений и отправка в GUI (binary packet) */ +void chat_core_collect_conn_list_trampoline(void* arg); + +/* Сбор полных метрик одного соединения (arg = u_malloc uint64_t peer_node_id) */ +void chat_core_collect_conn_metrics_trampoline(void* arg); + /* ── Headless CLI API ── */ /** Вариант для сборки без UTUN_INSTANCE (использует sqlite3* и node_id) */ diff --git a/src/chat/chat_event.c b/src/chat/chat_event.c index bd3f3cb2..b8ef904f 100644 --- a/src/chat/chat_event.c +++ b/src/chat/chat_event.c @@ -33,8 +33,10 @@ void chat_event_post(int type, const uint8_t* data, int len) { [12]="SERVICE_STARTED", [13]="SERVICE_STOPPED", [14]="ATTACHMENT_DOWNLOADED", [15]="DOWNLOAD_PROGRESS", [16]="NODEINFO_UPDATED", [17]="LINK_STATUS_CHANGED", + [18]="INVITE_RECEIVED", [19]="LOCAL_SOCKETS", + [20]="CONN_LIST", [21]="CONN_METRICS", }; - const char* n = (type >= 1 && type <= 17) ? names[type] : "?"; + const char* n = (type >= 1 && type <= 21) ? names[type] : "?"; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "chat_event: %s(%d) data=%d bytes", n, type, len); if (data && len > 0) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DEBUG, " ", data, len); } diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 2aeeb6f0..a87e7a1d 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -38,6 +38,8 @@ extern "C" { #define CHAT_EVT_LINK_STATUS_CHANGED 17 /* [peer_id:8][link_id:1][state:1][status:1][links_up:1][initialized:1] */ #define CHAT_EVT_INVITE_RECEIVED 18 /* [ch_id_len:1][ch_id][name_len:1][name][inviter_node_id:8][inviter_name_len:1][inviter_name] */ #define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */ +#define CHAT_EVT_CONN_LIST 20 /* [count:2][entry:46B]* — список ETCP-соединений */ +#define CHAT_EVT_CONN_METRICS 21 /* текст — полный дамп метрик выбранного соединения */ typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len); diff --git a/src/chat/chat_status.c b/src/chat/chat_status.c index 76e24d3a..acaec267 100644 --- a/src/chat/chat_status.c +++ b/src/chat/chat_status.c @@ -10,9 +10,11 @@ #include "../utun_instance.h" #include "../ntp_time.h" #include "../../lib/ll_queue.h" +#include "../../lib/mem.h" #include "../../lib/platform_compat.h" #include "../transport_layer/etcp.h" #include "../transport_layer/etcp_connections.h" +#include "../transport_layer/pkt_normalizer.h" 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 "?"; } @@ -112,7 +114,177 @@ static void chat_core_collect_status(void) { chat_event_post(CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off); } +/* ── Бинарный список соединений: [count:2][entry:46B]* ── */ +/* entry: peer_node_id:8 flags:1 link_count:1 rtt:2 inflight_kb:2 name:32 */ +#define CONN_LIST_ENTRY_SIZE 46 + +static void collect_conn_list(void) { + if (!g_cc.initialized || !g_cc.inst || !g_cc.inst->connections) return; + + uint16_t count = 0; + struct ll_entry* entry = g_cc.inst->connections->head; + while (entry) { count++; entry = entry->next; } + if (count > 250) count = 250; + + size_t buf_sz = 2 + (size_t)count * CONN_LIST_ENTRY_SIZE; + uint8_t* buf = (uint8_t*)u_malloc(buf_sz); + if (!buf) return; + + uint16_t* hdr = (uint16_t*)buf; + *hdr = count; + uint8_t* p = buf + 2; + + entry = g_cc.inst->connections->head; + for (uint16_t i = 0; i < count && entry; i++, entry = entry->next) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; + if (!ce || !ce->conn) { memset(p, 0, CONN_LIST_ENTRY_SIZE); p += CONN_LIST_ENTRY_SIZE; continue; } + struct ETCP_CONN* conn = ce->conn; + + memcpy(p, &ce->peer_node_id, 8); p += 8; + + uint8_t flags = 0; + if (conn->links_up) flags |= 1; + if (conn->initialized) flags |= 2; + *p++ = flags; + + uint8_t lc = 0; + struct ETCP_LINK* tl = conn->links; while (tl) { lc++; tl = tl->next; } + *p++ = lc; + + uint16_t rtt = 0; + if (conn->links) rtt = conn->links->rtt_last; + memcpy(p, &rtt, 2); p += 2; + + uint16_t inflight_kb = (uint16_t)(conn->unacked_bytes / 1024); + memcpy(p, &inflight_kb, 2); p += 2; + + char name[32]; memset(name, 0, 32); + get_node_name(ce->peer_node_id, name, 31); + if (!name[0]) snprintf(name, 32, "%016llX", (unsigned long long)ce->peer_node_id); + memcpy(p, name, 32); p += 32; + } + + chat_event_post(CHAT_EVT_CONN_LIST, buf, (int)buf_sz); + u_free(buf); +} + +/* ── Полный текстовый дамп одного соединения ── */ +static void collect_conn_metrics(uint64_t peer_node_id) { + char buf[8192]; int off = 0; + + if (!g_cc.initialized || !g_cc.inst || !g_cc.inst->connections) { + off = snprintf(buf, sizeof(buf), "uTun not initialized\n"); + chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off); + return; + } + + struct ll_entry* e = queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&peer_node_id); + if (!e) { + off = snprintf(buf, sizeof(buf), "Connection %016llX not found\n", (unsigned long long)peer_node_id); + chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off); + return; + } + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + if (!ce || !ce->conn) { + off = snprintf(buf, sizeof(buf), "Connection %016llX has null data\n", (unsigned long long)peer_node_id); + chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off); + return; + } + struct ETCP_CONN* conn = ce->conn; + uint64_t my_id = g_cc.inst->node_id; + + char peername[64]; get_node_name(peer_node_id, peername, sizeof(peername)); + if (!peername[0]) snprintf(peername, sizeof(peername), "%016llX", (unsigned long long)peer_node_id); + + /* Count links */ + int link_count = 0, links_up_count = 0; + struct ETCP_LINK* tl = conn->links; while (tl) { if (tl->link_status) links_up_count++; link_count++; tl = tl->next; } + + off += snprintf(buf + off, sizeof(buf) - off, + "=== PEER: %04llX\u2192%04llX [%s] ===\n" + "Status: %s Links: %d/%d Initialized: %s Session: 0x%X MTU: %d\n" + "Reinit: %u Reset: %u Tx_state: %d Routing_exchange: %d\n" + "\n--- ETCP ---\n" + "RTT last/avg10: %.1f/%.1f ms Jitter: %.1f ms\n" + "Bytes sent: %u Retrans: %u ACKs: %u\n" + "Unacked bytes: %u Max inflight: %u\n" + "RX dup: %u TX dup: %u\n" + "IDs: next_tx=%u last_rx=%u last_del=%u rx_ack_till=%u\n" + "\n--- Queues ---\n", + (unsigned long long)(my_id & 0xFFFF), (unsigned long long)(peer_node_id & 0xFFFF), peername, + conn->links_up ? "UP" : "DOWN", links_up_count, link_count, + conn->initialized ? "yes" : "no", conn->session_id, conn->mtu, + conn->reinit_count, conn->reset_count, conn->tx_state, conn->routing_exchange_active, + conn->rtt_last / 10.0f, conn->rtt_avg_10 / 10.0f, conn->jitter / 10.0f, + conn->bytes_sent_total, conn->retransmissions_count, conn->ack_packets_count, + conn->unacked_bytes, conn->max_inflight, + conn->rx_dup_count, conn->tx_dup_count, + conn->next_tx_id, conn->last_rx_id, conn->last_delivered_id, conn->rx_ack_till); + + if (conn->input_queue) off += snprintf(buf + off, sizeof(buf) - off, "input_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_queue), queue_entry_count(conn->input_queue)); + if (conn->input_send_q) off += snprintf(buf + off, sizeof(buf) - off, "send_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_send_q), queue_entry_count(conn->input_send_q)); + if (conn->input_wait_ack) off += snprintf(buf + off, sizeof(buf) - off, "wait_ack: %uB/%dp\n", (unsigned)queue_total_bytes(conn->input_wait_ack), queue_entry_count(conn->input_wait_ack)); + if (conn->ack_q) off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->ack_q), queue_entry_count(conn->ack_q)); + if (conn->recv_q) off += snprintf(buf + off, sizeof(buf) - off, "recv_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->recv_q), queue_entry_count(conn->recv_q)); + if (conn->output_queue) off += snprintf(buf + off, sizeof(buf) - off, "out_q: %uB/%dp\n", (unsigned)queue_total_bytes(conn->output_queue), queue_entry_count(conn->output_queue)); + + off += snprintf(buf + off, sizeof(buf) - off, "\n--- ACK Debug ---\n" + "hit_inf=%u hit_sndq=%u miss=%u link_wait=%u\n", + conn->cnt_ack_hit_inf, conn->cnt_ack_hit_sndq, conn->cnt_ack_miss, conn->cnt_link_wait); + + /* Normalizer */ + if (conn->normalizer) { + struct PKTNORM* pn = conn->normalizer; + off += snprintf(buf + off, sizeof(buf) - off, + "\n--- Normalizer ---\n" + "in: %llu pkts / %llu B out: %llu pkts / %llu B\n" + "frag_size=%u data_ptr=%u data_size=%u alloc_err=%u logic_err=%u\n", + (unsigned long long)pn->in_total_pkts, (unsigned long long)pn->in_total_bytes, + (unsigned long long)pn->out_total_pkts, (unsigned long long)pn->out_total_bytes, + pn->frag_size, pn->data_ptr, pn->data_size, pn->alloc_errors, pn->logic_errors); + } + + /* Links */ + int link_idx = 0; + struct ETCP_LINK* link = conn->links; + while (link) { + const char* is_tcp_str = link->is_tcp ? "TCP" : "UDP"; + char local_str[54], remote_str[54]; + strncpy(local_str, link->conn ? sockaddr_storage_to_str(&link->conn->local_addr).str : "stcp", sizeof(local_str) - 1); + strncpy(remote_str, sockaddr_storage_to_str(&link->remote_addr).str, sizeof(remote_str) - 1); + off += snprintf(buf + off, sizeof(buf) - off, + "\n--- LINK#%d: %s %s NAT=%s ---\n" + "addr: %s -> %s\n" + "RTT: %.1f ms TT: %.1f ms\n" + "Keepalive: sent=%u recv=%u period=%ums\n" + "Inflight: %u / %u B (%u pkts)\n" + "Encrypt: sent=%zu err=%zu Decrypt: rcvd=%zu err=%zu\n", + link_idx, link->link_status ? "UP" : "DOWN", is_tcp_str, nat_type_str(link->nat_type), + local_str, remote_str, + link->rtt_last / 10.0f, link->tt_last / 10.0f, + link->keepalive_sent_count, link->keepalive_recv_count, link->keepalive_interval, + link->inflight_bytes, link->inflight_lim_bytes, link->inflight_packets, + link->total_encrypted, link->encrypt_errors, link->total_decrypted, link->decrypt_errors); + + link = link->next; link_idx++; + } + + chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off); +} + void chat_core_collect_status_trampoline(void* arg) { (void)arg; chat_core_collect_status(); } + +void chat_core_collect_conn_list_trampoline(void* arg) { + (void)arg; + collect_conn_list(); +} + +void chat_core_collect_conn_metrics_trampoline(void* arg) { + uint64_t peer_id; + memcpy(&peer_id, arg, sizeof(peer_id)); + u_free(arg); + collect_conn_metrics(peer_id); +} diff --git a/src/config_parser.c b/src/config_parser.c index 2eb2df23..d795ce2e 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -846,7 +846,8 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename) // Set default values cfg->global.name[0] = '\0'; - cfg->global.keepalive_timeout = 2000; // Default 2 seconds + cfg->global.mtu = 1280; // Default MTU: safe for IPv6 tunnels + cfg->global.keepalive_timeout = 2000; // Default 2 seconds cfg->global.keepalive_interval = 200; // Default 0.2 s cfg->global.keepalive_adaptive = 1; // Default: adaptive enabled cfg->global.bbr_max_cwnd = 1048576; // Default 1MB (was INFLIGHT_LIM_MAX) @@ -1131,6 +1132,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config)); if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure"); return NULL; } cfg->global.name[0] = '\0'; + cfg->global.mtu = 1280; cfg->global.keepalive_timeout = 2000; cfg->global.keepalive_interval = 200; cfg->global.keepalive_adaptive = 1; diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index 98835257..a3dd82a0 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -510,7 +510,7 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { struct ETCP_CONN* topo_group_find_conn_for_node(struct TOPO_GROUP* group, uint64_t node_id) { if (!group) return NULL; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "find_conn_for_node %016llx: nq=%p paths=%p head=%p", + DEBUG_TRACE(DEBUG_CATEGORY_DEBUG, "find_conn_for_node %016llx: nq=%p paths=%p head=%p", (unsigned long long)node_id, (void*)nq, nq ? (void*)nq->paths : NULL, (nq && nq->paths) ? (void*)nq->paths->head : NULL); if (!nq || !nq->paths || !nq->paths->head) return NULL; struct ll_entry* e = nq->paths->head; diff --git a/src/transport_layer/auto_socket.c b/src/transport_layer/auto_socket.c index a31c0ef5..a1530cc1 100644 --- a/src/transport_layer/auto_socket.c +++ b/src/transport_layer/auto_socket.c @@ -204,7 +204,7 @@ static int create_iface_udp_socket(struct AUTO_SOCKET* as, uint32_t ifindex, con server.type = type; server.netif_index = ifindex; server.transport = 0; - server.mtu = inst->config ? inst->config->global.mtu : 1500; + server.mtu = inst->config ? inst->config->global.mtu : 1280; if (family == AF_INET) { struct sockaddr_in* sin = (struct sockaddr_in*)&server.ip; diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index a2e2f1e5..9de93c20 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -1832,7 +1832,12 @@ void etcp_update_mtu(struct ETCP_CONN* etcp) { if (!has_links) new_mtu = ETCP_RFC791_MIN_MTU; if (new_mtu != etcp->mtu) { etcp->mtu = new_mtu; - if (etcp->normalizer) etcp->normalizer->frag_size = etcp->mtu - ACK_REZERV - UDP_HDR_SIZE - UDP_SC_HDR_SIZE; + if (etcp->normalizer) { + int max_udp = IP_UDP_OVERHEAD_V4; + for (struct ETCP_LINK* l = etcp->links; l; l = l->next) + if (l->remote_addr.ss_family == AF_INET6) max_udp = IP_UDP_OVERHEAD_V6; + etcp->normalizer->frag_size = etcp->mtu - ACK_REZERV - max_udp - UDP_SC_HDR_SIZE; + } } } diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 21534c7d..528cb466 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -924,7 +924,7 @@ struct ETCP_LINK* etcp_link_new(struct ETCP_CONN* etcp, struct ETCP_SOCKET* conn int mtu; mtu = conn ? conn->mtu : STCP_MAX_MSG_SIZE; if (is_tcp) { mtu = STCP_MAX_MSG_SIZE; link->mtu_local = mtu; link->mtu = mtu; link->inflight_lim_bytes = mtu * 4; link->handshake_minsize = 100; link->handshake_maxsize = mtu; } - else { if (mtu == 0) mtu = 1500; if (mtu > PACKET_DATA_MAX_MTU) mtu = PACKET_DATA_MAX_MTU; link->mtu_local = mtu; link->mtu = mtu; link->inflight_lim_bytes = mtu * 4; link->handshake_minsize = 100; link->handshake_maxsize = mtu; } + else { if (mtu == 0) mtu = 1280; if (mtu > PACKET_DATA_MAX_MTU) mtu = PACKET_DATA_MAX_MTU; link->mtu_local = mtu; link->mtu = mtu; link->inflight_lim_bytes = mtu * 4; link->handshake_minsize = 100; link->handshake_maxsize = mtu; } /* mtu init moved above */ etcp_update_mtu(etcp); link->initialized = 0; @@ -1155,6 +1155,7 @@ static int etcp_tcp_send(struct ETCP_DGRAM* dgram) { if (link->send_hook) rc = (int)link->send_hook(0, dgram->data, dgram->data_len, NULL, 0, link, link->send_hook_ctx); else if (link->tcp_link) rc = stcp_link_send(link->tcp_link, dgram->data, dgram->data_len) == 0 ? (int)dgram->data_len : -1; else rc = -1; + if (rc > 0) link->total_encrypted += (size_t)rc; DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] TCP send: link=%d dlen=%d rc=%d via=%s", link->etcp->log_name, link->local_link_id, dgram->data_len, rc, link->send_hook ? "hook" : (link->tcp_link ? "stcp" : "NONE")); @@ -1182,9 +1183,10 @@ int etcp_encrypt_send(struct ETCP_DGRAM* dgram) { // Размер зашифрованной части не должен превышать UDP payload = MTU - заголовки int errcode=0; sc_context_t* sc = &dgram->link->etcp->crypto_ctx; - int len=dgram->data_len-dgram->noencrypt_len;// не забываем добавить timestamp (2 bytes) - int wire_ov = WIRE_OVERHEAD(dgram->link->remote_addr.ss_family); - if (len<0 || len>dgram->link->mtu - wire_ov) { dgram->link->send_errors++; errcode=1; goto es_err; } + int len=dgram->data_len-dgram->noencrypt_len; + int udp_ov = (dgram->link->remote_addr.ss_family == AF_INET6) ? IP_UDP_OVERHEAD_V6 : IP_UDP_OVERHEAD_V4; + int wire_ov = udp_ov + SC_ENCRYPT_OVERHEAD; + if (len<0 || len + (int)dgram->noencrypt_len > (int)(dgram->link->mtu - wire_ov)) { dgram->link->send_errors++; errcode=1; goto es_err; } uint8_t enc_buf[1600]; size_t enc_buf_len=0; dgram->timestamp=get_current_timestamp(); @@ -1195,8 +1197,8 @@ int etcp_encrypt_send(struct ETCP_DGRAM* dgram) { if (enc_buf_len == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "eencryption failed for node %016llx", (unsigned long long)dgram->link->etcp->instance->node_id); dgram->link->send_errors++; errcode=2; goto es_err; } - if (enc_buf_len + dgram->noencrypt_len > (size_t)(dgram->link->mtu - UDP_HDR_SIZE)) { - DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "packet too long len=%d ne_len=%d", enc_buf_len, dgram->noencrypt_len); + if (enc_buf_len + dgram->noencrypt_len > (size_t)(dgram->link->mtu - udp_ov)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "packet too long enc=%zu ne=%d mtu=%d", enc_buf_len, dgram->noencrypt_len, dgram->link->mtu); dgram->link->send_errors++; errcode=3; goto es_err; } memcpy(enc_buf+enc_buf_len, dgram->data+len, dgram->noencrypt_len); @@ -1229,7 +1231,7 @@ static int etcp_send_ping_raw(struct ETCP_DGRAM* dgram, socket_t fd, sc_context_ return -1; } int len = dgram->data_len - dgram->noencrypt_len; - if (len < 0 || len > PACKET_DATA_SIZE - UDP_HDR_SIZE) { + if (len < 0 || len > PACKET_DATA_SIZE - IP_UDP_OVERHEAD_V4) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "ping packet data invalid len=%d", len); return -1; } @@ -1242,7 +1244,7 @@ static int etcp_send_ping_raw(struct ETCP_DGRAM* dgram, socket_t fd, sc_context_ DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "encryption failed for ping"); return -1; } - if (enc_buf_len + dgram->noencrypt_len > (size_t)(PACKET_DATA_SIZE - UDP_HDR_SIZE)) { + if (enc_buf_len + dgram->noencrypt_len > (size_t)(PACKET_DATA_SIZE - IP_UDP_OVERHEAD_V4)) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "ping packet too long enc=%zu ne=%d", enc_buf_len, dgram->noencrypt_len); return -1; } diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 1e98a195..2bd7d8ab 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -16,12 +16,11 @@ extern "C" { #include #include -#define UDP_HDR_SIZE 28// размер udp header + ethernet заголовков (ipv4) [для ipv6 = 48 байт] -#define UDP_SC_HDR_SIZE (13+8+4 + 5)// 13+8+4 - sc_nonce+tag size+crc, 5 - payload hdr -#define ACK_REZERV 100// сколько байт резервировать под ack и прочие заголовки -#define SC_ENCRYPT_OVERHEAD (3 + SC_TAG_SIZE) // timestamp(2)+flag_up(1)+AES-CCM tag(16) — поверх plaintext #define IP_UDP_OVERHEAD_V4 (20 + 8) // IP(20)+UDP(8) #define IP_UDP_OVERHEAD_V6 (40 + 8) // IP(40)+UDP(8) +#define SC_ENCRYPT_OVERHEAD (ETCP_ENCRYPTED_HDR_SIZE + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE) // 3+13+4+16=36 +#define UDP_SC_HDR_SIZE (SC_NONCE_SIZE + SC_TAG_SIZE + SC_CRC32_SIZE + 5) // 13+16+4+5=38 — sc overhead без ETCP_HDR + etcp framing +#define ACK_REZERV 100// сколько байт резервировать под ack и прочие заголовки #define WIRE_OVERHEAD(fam) ((fam) == AF_INET6 ? IP_UDP_OVERHEAD_V6 + SC_ENCRYPT_OVERHEAD : IP_UDP_OVERHEAD_V4 + SC_ENCRYPT_OVERHEAD) #define INFLIGHT_LIM_MIN 8192 // 8K diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index 02adb21d..2b9662e2 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -885,7 +885,7 @@ void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) { h->entry = NULL; u_free(h); return; } - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close node=0x%016llx remaining=%d", + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close node=0x%016llx remaining=%d", (unsigned long long)node_id, entry->handle_count); if (entry->handle_count == 0) { @@ -898,28 +898,28 @@ void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) { if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; } if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; } if (conn && conn->state != 2 && conn->instance) { - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: cleaning conn=%p", conn); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: cleaning conn=%p", conn); conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL; - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_init_cb"); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_init_cb"); etcp_conn_remove_cbk(conn, ncd_init_cb, entry); - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_up_cb"); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_up_cb"); etcp_conn_remove_cbk(conn, ncd_up_cb, entry); - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_down_cb"); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_down_cb"); etcp_conn_remove_cbk(conn, ncd_down_cb, entry); - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: scheduling deferred close"); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: scheduling deferred close"); if (conn->state != 2) uasync_call_soon(entry->ua, conn, ncd_deferred_close_conn); - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing from registry"); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing from registry"); ncd_registry_remove(conn->instance, entry); } - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing entry=%p", entry); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing entry=%p", entry); u_free(entry); } h->entry = NULL; - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing h=%p", h); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing h=%p", h); u_free(h); - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close: done"); + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: done"); } /* diff --git a/src/transport_layer/pkt_normalizer.c b/src/transport_layer/pkt_normalizer.c index fa38cf2c..e831a100 100644 --- a/src/transport_layer/pkt_normalizer.c +++ b/src/transport_layer/pkt_normalizer.c @@ -39,8 +39,11 @@ struct PKTNORM* pn_init(struct ETCP_CONN* etcp) { pn->etcp = etcp; pn->ua = etcp->instance->ua; - pn->frag_size = etcp->mtu - ACK_REZERV - UDP_HDR_SIZE - UDP_SC_HDR_SIZE; - int min_frag = UDP_HDR_SIZE + UDP_SC_HDR_SIZE + ACK_REZERV; + int max_udp = IP_UDP_OVERHEAD_V4; + for (struct ETCP_LINK* l = etcp->links; l; l = l->next) + if (l->remote_addr.ss_family == AF_INET6) max_udp = IP_UDP_OVERHEAD_V6; + pn->frag_size = etcp->mtu - ACK_REZERV - max_udp - UDP_SC_HDR_SIZE; + int min_frag = max_udp + UDP_SC_HDR_SIZE + ACK_REZERV; if (etcp->mtu < min_frag || pn->frag_size > etcp->mtu) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "MTU %d too small (min %d)", etcp->mtu, min_frag); pn->frag_size = etcp->mtu; diff --git a/src/transport_layer/stcp_link.c b/src/transport_layer/stcp_link.c index 2b82f979..ddd94121 100644 --- a/src/transport_layer/stcp_link.c +++ b/src/transport_layer/stcp_link.c @@ -54,6 +54,7 @@ static void link_rx_cb(struct ll_queue *q, void *arg) { struct ETCP_LINK *l = link->etcp_link; if (e->len >= 3) { uint16_t pp = e->dgram[1] | ((uint16_t)e->dgram[2] << 8); l->keepalive_timeout = (uint32_t)pp * KA_TIMEOUT_MULT; } l->recv_keepalive = 1; l->remote_keepalive = 1; l->link_status = 1; l->keepalive_recv_count++; + l->total_decrypted += e->len; l->last_recv_local_time = get_time_tb(); queue_dgram_free(e); queue_entry_free(e); } else { @@ -62,6 +63,7 @@ static void link_rx_cb(struct ll_queue *q, void *arg) { if (pkt) { pkt->link = link->etcp_link; pkt->data_len = (uint16_t)e->len; pkt->noencrypt_len = 0; if (e->len > 0) memcpy(pkt->data, e->dgram, e->len); + link->etcp_link->total_decrypted += e->len; etcp_conn_input(pkt); } queue_dgram_free(e); queue_entry_free(e); diff --git a/tools/chatgui-android/AGENTS.md b/tools/chatgui-android/AGENTS.md index 9c32c068..506199e2 100644 --- a/tools/chatgui-android/AGENTS.md +++ b/tools/chatgui-android/AGENTS.md @@ -137,6 +137,12 @@ sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" ## Сборка +``` +./build.sh # APK +./build.sh install # сборка + прошивка на подключённый телефон +./build.sh install # на конкретный девайс +``` + ### APK (Android) ```bash diff --git a/tools/chatgui-android/build.sh b/tools/chatgui-android/build.sh new file mode 100755 index 00000000..bb7d45aa --- /dev/null +++ b/tools/chatgui-android/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -eo pipefail +cd "$(dirname "$0")" +export ANDROID_HOME="${ANDROID_HOME:-/home/user/android}" +echo "Building..." +TMP=$(mktemp) +if ! ./gradlew assembleDebug > "$TMP" 2>&1; then + cat "$TMP" + rm -f "$TMP" + exit 1 +fi +rm -f "$TMP" +echo "Build OK" +if [ "$1" = "install" ]; then + DEV=${2:-$(adb devices 2>/dev/null | awk 'NR==2{print $1}')} + ADB=adb; [ -n "$DEV" ] && ADB="adb -s $DEV" + $ADB install -r app/build/outputs/apk/debug/app-debug.apk + echo "Install OK" +fi diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 46f41ed7..6e9fdf31 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -122,6 +122,7 @@ add_executable(chatgui src/media_blocks.cpp src/audiodevicesettingspage.cpp src/storagesettingspage.cpp + src/connmonitorwindow.cpp transport/utun_node.cpp transport/node_config.cpp transport/config_updater.cpp diff --git a/tools/chatgui/src/animtimer.cpp b/tools/chatgui/src/animtimer.cpp index bfdd6760..a4ef9a30 100644 --- a/tools/chatgui/src/animtimer.cpp +++ b/tools/chatgui/src/animtimer.cpp @@ -16,7 +16,7 @@ AnimTimer *AnimTimer::instance() { void AnimTimer::shutdown() { instance()->m_timer.stop(); instance()->m_icons.clear(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "AnimTimer: shutdown"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "AnimTimer: shutdown"); } void AnimTimer::registerIcon(const QString &key, LottieIcon *icon) { @@ -53,7 +53,7 @@ void AnimTimer::onTimer() { void AnimTimer::logRegisteredIcons() const { for (auto it = m_icons.constBegin(); it != m_icons.constEnd(); ++it) { LottieIcon *icon = it.value(); - GUI_INFO("LottieIcon: path=%s frames=%d size=%dx%d tgsSize=%d valid=%d", + GUI_DEBUG("LottieIcon: path=%s frames=%d size=%dx%d tgsSize=%d valid=%d", qPrintable(it.key()), icon->frameCount(), icon->originalSize().width(), icon->originalSize().height(), icon->tgsSize(), (int)icon->isValid()); diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp index 9abf0837..0378b3b9 100644 --- a/tools/chatgui/src/audiodevicesettingspage.cpp +++ b/tools/chatgui/src/audiodevicesettingspage.cpp @@ -236,7 +236,7 @@ void AudioDeviceSettingsPage::applyAndSave() { m_savedCompressorEnabled = compEnabled; m_savedCompressorMaxGainDb = compMaxGain; m_savedCompressorRiseRate = compRiseRate; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB rise=%.1fx", + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB rise=%.1fx", outIdx, inIdx, codecPreset, compEnabled, compMaxGain, (float)compRiseRate); if (m_recorder) { diff --git a/tools/chatgui/src/audiorecorder.cpp b/tools/chatgui/src/audiorecorder.cpp index bd2ccab5..19660487 100644 --- a/tools/chatgui/src/audiorecorder.cpp +++ b/tools/chatgui/src/audiorecorder.cpp @@ -78,7 +78,7 @@ bool AudioRecorder::init() { m_device = new ma_device; std::memset(m_device, 0, sizeof(*m_device)); m_initialized = true; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "initialized (sampleRate=%d, channels=%d)", m_sampleRate, m_channels); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "initialized (sampleRate=%d, channels=%d)", m_sampleRate, m_channels); return true; } @@ -91,7 +91,7 @@ void AudioRecorder::shutdown() { } if (m_device) { delete m_device; m_device = nullptr; } m_initialized = false; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "shutdown"); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "shutdown"); } void AudioRecorder::startRecording() { @@ -122,7 +122,7 @@ void AudioRecorder::startRecording() { ma_result r = ma_context_get_devices(ctx, nullptr, nullptr, &pCaptureDevices, &captureCount); if (r == MA_SUCCESS && (ma_uint32)m_captureDeviceIndex < captureCount) { config.capture.pDeviceID = &pCaptureDevices[m_captureDeviceIndex].id; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "using capture device[%d]: %s", m_captureDeviceIndex, + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "using capture device[%d]: %s", m_captureDeviceIndex, pCaptureDevices[m_captureDeviceIndex].name); } } @@ -196,5 +196,5 @@ size_t AudioRecorder::bufferSize() const { void AudioRecorder::setCompressorEnabled(bool enabled) { m_compressorEnabled = enabled; if (m_compressor) audio_compressor_set_enabled(m_compressor, enabled); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "compressor %s", enabled ? "enabled" : "disabled"); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "compressor %s", enabled ? "enabled" : "disabled"); } diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 0bc6610b..4924ebdf 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -155,6 +155,15 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) connect(gearBtn, &QPushButton::clicked, this, [this] { emit settingsRequested(); }); + auto *monitorBtn = new QPushButton(QString::fromUtf8("\xF0\x9F\x94\x8D"), footer); /* 🔍 */ + monitorBtn->setFixedSize(28, 28); + monitorBtn->setToolTip(QString::fromUtf8("Connection Monitor")); + monitorBtn->setStyleSheet( + "QPushButton { border: none; font-size: 16px; background: transparent; color: gray; }" + "QPushButton:hover { color: palette(text); }"); + fl->addWidget(monitorBtn); + connect(monitorBtn, &QPushButton::clicked, this, [this] { emit connMonitorRequested(); }); + fl->addStretch(); auto *createBtn = new QPushButton("GRP", footer); @@ -222,7 +231,7 @@ void ChannelList::loadChannels() { item->setData(ch.lastMsgAt, ChannelLastMsgAtRole); item->setData(makeChannelIcon(colors[ci % 6], letter), ChannelIconRole); m_model->appendRow(item); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels row=%d cid='%s' name='%s' unread=%d lastMsgAt=%lld", + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels row=%d cid='%s' name='%s' unread=%d lastMsgAt=%lld", m_model->rowCount()-1, qPrintable(ch.channelId), qPrintable(ch.name), ch.unreadCount, (long long)ch.lastMsgAt); ci++; @@ -266,7 +275,7 @@ void ChannelList::setChannelPeersOnline(const QString& channelId, int count) { } } if (!found) - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, " NOT FOUND in model"); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, " NOT FOUND in model"); } void ChannelList::setAutoConnectActive(bool active) { diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index 86209818..f2fd5d36 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -27,6 +27,7 @@ signals: void joinRequested(); void createGroupRequested(); void settingsRequested(); + void connMonitorRequested(); private: QListView *m_listView; diff --git a/tools/chatgui/src/connmonitorwindow.cpp b/tools/chatgui/src/connmonitorwindow.cpp new file mode 100644 index 00000000..48d3cef6 --- /dev/null +++ b/tools/chatgui/src/connmonitorwindow.cpp @@ -0,0 +1,189 @@ +// connmonitorwindow.cpp — ETCP connection monitor (standalone, non-modal window) +#include "connmonitorwindow.h" +#include "../transport/gui_bridge.h" + +extern "C" { +#include "chat/chat_core.h" +#include "../../lib/mem.h" +} + +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONN_LIST_ENTRY_SIZE 46 + +static ConnMonitorWindow* g_activeConnMonitor = nullptr; + +static void connListCb(const uint8_t* data, int len) { + if (g_activeConnMonitor) g_activeConnMonitor->onConnList(data, len); +} + +static void connMetricsCb(const uint8_t* data, int len) { + if (g_activeConnMonitor) g_activeConnMonitor->onConnMetrics(data, len); +} + +ConnMonitorWindow::ConnMonitorWindow(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle("Connection Monitor"); + resize(900, 550); + setAttribute(Qt::WA_DeleteOnClose); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + auto* mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(4); + mainLayout->setContentsMargins(8, 8, 8, 8); + + auto* title = new QLabel("Active ETCP Connections (10 fps)", this); + title->setStyleSheet("font-weight: bold; font-size: 13px;"); + mainLayout->addWidget(title); + + auto* splitter = new QSplitter(Qt::Horizontal, this); + splitter->setStyleSheet( + "QSplitter::handle { background: palette(mid); width: 2px; }"); + + m_connList = new QListWidget(splitter); + m_connList->setFixedWidth(280); + m_connList->setFont(QFont("monospace", 9)); + m_connList->setStyleSheet( + "QListWidget { border: 1px solid palette(mid); border-radius: 4px;" + " background: palette(base); }" + "QListWidget::item { padding: 4px 6px; }" + "QListWidget::item:selected { background: #3390EC; color: white; }"); + + m_details = new QPlainTextEdit(splitter); + m_details->setReadOnly(true); + m_details->setFont(QFont("monospace", 9)); + m_details->setStyleSheet( + "QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;" + " padding: 8px; background: palette(base); }"); + m_details->setPlaceholderText("Select a connection to view metrics"); + + splitter->addWidget(m_connList); + splitter->addWidget(m_details); + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + + mainLayout->addWidget(splitter, 1); + + m_timer = new QTimer(this); + m_timer->setInterval(100); + connect(m_timer, &QTimer::timeout, this, &ConnMonitorWindow::onTimerTick); + connect(m_connList, &QListWidget::currentItemChanged, + this, &ConnMonitorWindow::onConnSelected); + + g_activeConnMonitor = this; + gui_bridge_set_conn_list_cb(connListCb); + gui_bridge_set_conn_metrics_cb(connMetricsCb); + + m_timer->start(); +} + +ConnMonitorWindow::~ConnMonitorWindow() { + m_timer->stop(); + if (g_activeConnMonitor == this) g_activeConnMonitor = nullptr; +} + +void ConnMonitorWindow::showEvent(QShowEvent* event) { + QDialog::showEvent(event); + m_timer->start(); +} + +void ConnMonitorWindow::hideEvent(QHideEvent* event) { + m_timer->stop(); + QDialog::hideEvent(event); +} + +void ConnMonitorWindow::onTimerTick() { + if (m_details->textCursor().hasSelection()) return; + if (QApplication::mouseButtons()) return; + gui_bridge_post_uasync_fn(chat_core_collect_conn_list_trampoline, nullptr); + if (m_selectedNodeId) requestMetricsForSelected(); +} + +void ConnMonitorWindow::onConnSelected(QListWidgetItem* current, QListWidgetItem* previous) { + (void)previous; + if (!current) { m_selectedNodeId = 0; m_details->clear(); return; } + m_selectedNodeId = (uint64_t)current->data(Qt::UserRole).toULongLong(); + requestMetricsForSelected(); +} + +void ConnMonitorWindow::requestMetricsForSelected() { + void* arg = u_malloc(sizeof(uint64_t)); + if (!arg) return; + memcpy(arg, &m_selectedNodeId, sizeof(uint64_t)); + gui_bridge_post_uasync_fn(chat_core_collect_conn_metrics_trampoline, arg); +} + +void ConnMonitorWindow::onConnList(const uint8_t* data, int len) { + if (len < 2) return; + uint16_t count; memcpy(&count, data, 2); + const uint8_t* p = data + 2; + + QHash existing; + for (int i = 0; i < m_connList->count(); i++) { + auto* item = m_connList->item(i); + existing[(uint64_t)item->data(Qt::UserRole).toULongLong()] = item; + } + + m_connList->blockSignals(true); + + for (uint16_t i = 0; i < count && (p + CONN_LIST_ENTRY_SIZE) <= (data + len); i++) { + uint64_t nodeId; memcpy(&nodeId, p, 8); p += 8; + uint8_t flags = *p++; + uint8_t linkCount = *p++; + uint16_t rtt; memcpy(&rtt, p, 2); p += 2; + p += 2; /* inflightKb */ + char name[33]; memcpy(name, p, 32); name[32] = '\0'; p += 32; + + if (!nodeId) continue; + + bool linksUp = (flags & 1) != 0; + bool initialized = (flags & 2) != 0; + QString color = linksUp ? "#2E7D32" : "#C62828"; + QString status = linksUp ? "UP" : (initialized ? "init" : "DOWN"); + + QString nodeHex = QString("%1").arg(nodeId, 16, 16, QChar('0')).right(16).left(4); + QString rttStr = rtt > 0 ? QString::number(rtt / 10.0f, 'f', 1) : "-"; + QString text = QString("[%1] %2 %3 rtt=%4ms") + .arg(nodeHex) + .arg(QString::fromUtf8(name).left(16), -16) + .arg(status + QString(" %1L").arg(linkCount), -10) + .arg(rttStr); + + auto it = existing.find(nodeId); + if (it != existing.end()) { + auto* item = *it; + item->setText(text); + item->setForeground(QColor(color)); + existing.erase(it); + } else { + auto* item = new QListWidgetItem(text, m_connList); + item->setData(Qt::UserRole, QVariant((qulonglong)nodeId)); + item->setForeground(QColor(color)); + } + } + + for (auto it = existing.begin(); it != existing.end(); ++it) + delete *it; + + m_connList->blockSignals(false); + + if (!m_selectedNodeId && m_connList->count() > 0) { + m_connList->setCurrentRow(0); + onConnSelected(m_connList->item(0), nullptr); + } +} + +void ConnMonitorWindow::onConnMetrics(const uint8_t* data, int len) { + if (len <= 0) return; + int scrollVal = m_details->verticalScrollBar()->value(); + m_details->setPlainText(QString::fromUtf8((const char*)data, len)); + m_details->verticalScrollBar()->setValue(scrollVal); +} diff --git a/tools/chatgui/src/connmonitorwindow.h b/tools/chatgui/src/connmonitorwindow.h new file mode 100644 index 00000000..64201839 --- /dev/null +++ b/tools/chatgui/src/connmonitorwindow.h @@ -0,0 +1,34 @@ +// connmonitorwindow.h — ETCP connection monitor (standalone, non-modal window) +#pragma once + +#include +#include +#include +#include +#include + +class ConnMonitorWindow : public QDialog { + Q_OBJECT +public: + explicit ConnMonitorWindow(QWidget* parent = nullptr); + ~ConnMonitorWindow() override; + + void onConnList(const uint8_t* data, int len); + void onConnMetrics(const uint8_t* data, int len); + +protected: + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + +private slots: + void onTimerTick(); + void onConnSelected(QListWidgetItem* current, QListWidgetItem* previous); + +private: + void requestMetricsForSelected(); + + QListWidget* m_connList; + QPlainTextEdit* m_details; + QTimer* m_timer; + uint64_t m_selectedNodeId = 0; +}; diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index e621540e..168209eb 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -40,10 +40,10 @@ static BOOL WINAPI console_ctrl_handler(DWORD dwCtrlType) { static struct ShutdownSentinel { ~ShutdownSentinel() { - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "main: static destructors done"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "main: static destructors done"); #ifdef _WIN32 DWORD threads = count_threads(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "main: threads at exit=%lu", (unsigned long)threads); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "main: threads at exit=%lu", (unsigned long)threads); if (threads > 4) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "main: %lu hanging threads, TerminateProcess", (unsigned long)threads); fflush(nullptr); @@ -128,6 +128,6 @@ int main(int argc, char *argv[]) { #endif int ret = app.exec(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "main: app.exec() returned %d", ret); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "main: app.exec() returned %d", ret); return ret; } diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 5b2b3ba2..bdea7632 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -8,6 +8,7 @@ #include "joindialog.h" #include "settingsdialog.h" #include "channelsettingsdialog.h" +#include "connmonitorwindow.h" #include "creategroupdialog.h" #include "invite_link.h" #include "sound_manager.h" @@ -249,7 +250,7 @@ void MainWindow::setupSoundFromConfig() { audio_compressor_configure(m_recorder->compressor(), &compCfg); } } - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)", + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)", m_recorder->captureDeviceIndex(), VoiceEncoder::preset(), m_recorder->isCompressorEnabled() ? 1 : 0); } @@ -308,6 +309,7 @@ void MainWindow::setupConnects() { connect(m_channelList, &ChannelList::joinRequested, this, &MainWindow::showJoinDialog); connect(m_channelList, &ChannelList::createGroupRequested, this, &MainWindow::onCreateGroupRequested); connect(m_channelList, &ChannelList::settingsRequested, this, &MainWindow::showSettings); + connect(m_channelList, &ChannelList::connMonitorRequested, this, &MainWindow::showConnMonitor); connect(m_channelList, &ChannelList::channelSettingsRequested, this, &MainWindow::showChannelSettings); connect(m_accountList, &AccountList::memberRenamed, m_messageList, &MessageList::reloadLocalNicks); @@ -428,6 +430,17 @@ void MainWindow::showSettings() { reloadChannels(); } +void MainWindow::showConnMonitor() { + if (m_connMonitor) { + m_connMonitor->raise(); + m_connMonitor->activateWindow(); + return; + } + m_connMonitor = new ConnMonitorWindow(nullptr); + connect(m_connMonitor, &QDialog::destroyed, this, [this] { m_connMonitor = nullptr; }); + m_connMonitor->show(); +} + void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author) { QString cid = QString::fromUtf8(ch_id, ch_id_len); if (m_db && author != m_db->myNodeId()) @@ -598,14 +611,16 @@ void MainWindow::autoPlayTimerTick() { QString fullPath = autoPlayMediaDir() + QStringLiteral("/") + item.channelId + QStringLiteral("/") + fp; if (QFile::exists(fullPath)) { + QString chId = item.channelId; + qint64 msgId = item.msgId; + float dur = item.durationSec; m_autoPlayQueue.dequeue(); - m_autoPlayDownloading.remove(item.msgId); + m_autoPlayDownloading.remove(msgId); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: playing ch=%s msg=%lld file=%s dur=%.1fs", - qPrintable(item.channelId), (long long)item.msgId, qPrintable(fullPath), - (double)item.durationSec); + qPrintable(chId), (long long)msgId, qPrintable(fullPath), (double)dur); MessageDelegate::stopAllAutoPlay(); - VoicePlayer::playOpusFile(fullPath); - MessageDelegate::startAutoPlay(fullPath, item.durationSec); + float actualDur = VoicePlayer::playOpusFile(fullPath); + MessageDelegate::startAutoPlay(fullPath, actualDur); return; } } diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index 401e8227..47d671bf 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -13,6 +13,7 @@ class AccountList; class DbManager; class UtunNode; class AudioRecorder; +class ConnMonitorWindow; struct AutoPlayItem { QString channelId; @@ -41,6 +42,7 @@ private slots: void showJoinDialog(); void onCreateGroupRequested(); void showSettings(); + void showConnMonitor(); void showChannelSettings(const QString& channelId); void autoPlayTimerTick(); @@ -79,6 +81,7 @@ private: QString m_pendingChannelSelect; QString m_currentChannelId; AudioRecorder *m_recorder = nullptr; + ConnMonitorWindow *m_connMonitor = nullptr; QQueue m_autoPlayQueue; QTimer* m_autoPlayTimer = nullptr; diff --git a/tools/chatgui/src/media_blocks.cpp b/tools/chatgui/src/media_blocks.cpp index 10951cd6..0436a0f6 100644 --- a/tools/chatgui/src/media_blocks.cpp +++ b/tools/chatgui/src/media_blocks.cpp @@ -284,7 +284,7 @@ MediaBlockInfo media_split_to_blocks(const QString& srcFile, if (removeSrc) QFile::remove(srcFile); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: %s -> %d blocks (%lld bytes each) in %s", + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: %s -> %d blocks (%lld bytes each) in %s", qPrintable(fi.fileName()), info.numBlocks, (long long)info.blockSize, qPrintable(mediaDir)); return info; } diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 8707a86d..241cc237 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -153,14 +153,16 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, if (!st.playing && !st.paused) { /* Start playback then seek */ + float actualDur = VoicePlayer::playOpusFile(filePath); + if (actualDur <= 0.0f) return false; + targetSec = clickFrac * actualDur; st.playing = true; st.paused = false; - st.durationSec = dur; + st.durationSec = actualDur; st.startMs = now - (qint64)(targetSec * 1000.0); st.pausedPosSec = targetSec; - st.displayFrac = (targetSec > 0 && dur > 0) ? (targetSec / dur) : 0; + st.displayFrac = (targetSec > 0 && actualDur > 0) ? (targetSec / actualDur) : 0; st.displayElapsed = targetSec; - VoicePlayer::playOpusFile(filePath); int sr = SoundManager::instance()->currentPcmSampleRate(); if (sr > 0) SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr)); @@ -222,15 +224,16 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, SoundManager::instance()->stopCurrentPcm(); } + float actualDur = VoicePlayer::playOpusFile(filePath); + if (actualDur <= 0.0f) return false; st.playing = true; st.paused = false; st.pausedPosSec = 0; st.startMs = QDateTime::currentMSecsSinceEpoch(); - st.durationSec = dur; + st.durationSec = actualDur; st.displayFrac = 0; st.displayElapsed = 0; - VoicePlayer::playOpusFile(filePath); s_voiceTimer->start(); model->setData(index, 1, MsgVoicePlayStateRole); return true; @@ -850,8 +853,6 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio QColor idle(0x70, 0x70, 0x78, 130); painter->setBrush((isPlaying || isPaused) ? (before ? played : unplayed) : idle); painter->drawRect(QRect(x, y, bw, barH)); - if (i == 0) GUI_DEBUG("voice paint: wfN=%d wfW=%d logV=%.2f barH=%d", - wfN, wfR.width(), logV, barH); } } @@ -1187,12 +1188,12 @@ void MessageDelegate::drawAvatar(QPainter *p, const Layout &L, p->restore(); } -void MessageDelegate::startAutoPlay(const QString& filePath, float durationSec) { +void MessageDelegate::startAutoPlay(const QString& filePath, float actualDurationSec) { ensureVoiceTimer(); VoicePlayState& st = s_voiceStates[filePath]; st.playing = true; st.paused = false; - st.durationSec = (durationSec > 0) ? durationSec : 1.0f; + st.durationSec = (actualDurationSec > 0) ? actualDurationSec : 1.0f; st.startMs = QDateTime::currentMSecsSinceEpoch(); st.pausedPosSec = 0.0f; st.displayFrac = 0.0f; diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index 3b263864..5eae8ae1 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -48,7 +48,7 @@ public: const QStyleOptionViewItem &option, const QModelIndex &index) override; - static void startAutoPlay(const QString& filePath, float durationSec); + static void startAutoPlay(const QString& filePath, float actualDurationSec); static void stopAllAutoPlay(); private: diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index bf53f9be..f1bc979b 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -166,8 +166,6 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data, wf.append((float)idx / 63.0f); } item->setData(wf, MsgVoiceWaveformRole); - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "setVoiceMessageRoles: voice dur=%dms wf=%d exists=%d", - durMs, wf.size(), QFile::exists(fp)); } } if (ct == "application/octet-stream") { @@ -659,9 +657,9 @@ void MessageList::updateReadPosition() { void MessageList::cleanupAnimations() { size_t n = s_icons.size(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MessageList: cleanupAnimations clearing %zu icons", n); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MessageList: cleanupAnimations clearing %zu icons", n); s_icons.clear(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MessageList: cleanupAnimations done"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MessageList: cleanupAnimations done"); } void MessageList::updateMessageProgress(const QString& chId, int64_t msgId, diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index b86be409..9a02fc48 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -7,6 +7,7 @@ #include "soundsettingspage.h" #include "audiodevicesettingspage.h" #include "storagesettingspage.h" +#include "connmonitorwindow.h" #include "../transport/gui_bridge.h" #include "../../lib/mem.h" #include "../db/db_manager.h" @@ -31,7 +32,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod { setWindowTitle("Settings"); setMinimumSize(760, 460); - setModal(true); + setWindowModality(Qt::WindowModal); auto* mainLayout = new QHBoxLayout(this); mainLayout->setSpacing(0); @@ -48,6 +49,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod m_categoryList->addItem("Profile"); m_categoryList->addItem("Storage"); m_categoryList->addItem("Network"); + m_categoryList->addItem("Connections"); m_categoryList->addItem("Database"); m_categoryList->addItem("Status"); m_categoryList->addItem("Nodes"); @@ -105,6 +107,22 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod m_networkPage = new NetworkSettingsPage(m_pages); m_pages->addWidget(m_networkPage); + // Connections page — button to open standalone window + { + auto* connPage = new QWidget(m_pages); + auto* connLayout = new QVBoxLayout(connPage); + connLayout->setAlignment(Qt::AlignCenter); + auto* hintLabel = new QLabel("Live ETCP connection monitor\nwith per-link BBR statistics", connPage); + hintLabel->setAlignment(Qt::AlignCenter); + hintLabel->setStyleSheet("font-size: 12px; color: palette(text); padding: 20px;"); + connLayout->addWidget(hintLabel); + auto* openBtn = new QPushButton("Open Connection Monitor", connPage); + openBtn->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 13px; }"); + connect(openBtn, &QPushButton::clicked, this, &SettingsDialog::onOpenConnMonitor); + connLayout->addWidget(openBtn); + m_pages->addWidget(connPage); + } + // Database page m_databasePage = new DatabaseSettingsPage(db, m_pages); m_pages->addWidget(m_databasePage); @@ -187,11 +205,11 @@ void SettingsDialog::loadProfileFromConfig(const QString& path) { void SettingsDialog::onCategoryChanged(int row) { m_pages->setCurrentIndex(row); if (row == 1) m_storagePage->loadFromDb(); - if (row == 3) m_databasePage->refreshStatus(); - if (row == 4) m_statusPage->refreshStatus(); - if (row == 5) m_nodesPage->refreshNodes(); - if (row == 6) m_soundPage->loadFromDb(); - if (row == 7) m_audioDevicePage->loadFromDb(); + if (row == 4) m_databasePage->refreshStatus(); + if (row == 5) m_statusPage->refreshStatus(); + if (row == 6) m_nodesPage->refreshNodes(); + if (row == 7) m_soundPage->loadFromDb(); + if (row == 8) m_audioDevicePage->loadFromDb(); } void SettingsDialog::onSave() { @@ -268,3 +286,13 @@ void SettingsDialog::onSave() { } accept(); } + +void SettingsDialog::onOpenConnMonitor() { + if (m_connMonitorWindow) { + m_connMonitorWindow->raise(); + m_connMonitorWindow->activateWindow(); + return; + } + m_connMonitorWindow = new ConnMonitorWindow(nullptr); + m_connMonitorWindow->show(); +} diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index 86e33938..8204dfca 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -13,6 +13,7 @@ class NetworkSettingsPage; class DatabaseSettingsPage; class StatusPage; class NodesPage; +class ConnMonitorWindow; class SoundSettingsPage; class AudioDeviceSettingsPage; class StorageSettingsPage; @@ -29,6 +30,7 @@ public: private slots: void onCategoryChanged(int row); void onSave(); + void onOpenConnMonitor(); private: void loadProfileFromConfig(const QString& path); @@ -45,6 +47,7 @@ private: DatabaseSettingsPage* m_databasePage; StatusPage* m_statusPage; NodesPage* m_nodesPage; + ConnMonitorWindow* m_connMonitorWindow = nullptr; SoundSettingsPage* m_soundPage; AudioDeviceSettingsPage* m_audioDevicePage; StorageSettingsPage* m_storagePage; diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index 913925e1..b345735b 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -11,7 +11,7 @@ SoundManager* SoundManager::instance() { } SoundManager::~SoundManager() { - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "SoundManager: static destructor entered"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: static destructor entered"); shutdown(); } @@ -43,14 +43,14 @@ void SoundManager::shutdown() { } m_sounds.clear(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine); if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; } if (m_currentPcmSound) { delete m_currentPcmSound; m_currentPcmSound = nullptr; } if (m_currentPcmBuffer) { ma_audio_buffer* buf = (ma_audio_buffer*)m_currentPcmBuffer; ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr; } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit"); m_initialized = false; } diff --git a/tools/chatgui/src/voiceplayback.cpp b/tools/chatgui/src/voiceplayback.cpp index 4105a34a..798bfc54 100644 --- a/tools/chatgui/src/voiceplayback.cpp +++ b/tools/chatgui/src/voiceplayback.cpp @@ -75,7 +75,7 @@ int VoicePlayer::decodeOpusFile(const QString& filePath, std::vector& o return frameCount; } -void VoicePlayer::playOpusFile(const QString& filePath) { +float VoicePlayer::playOpusFile(const QString& filePath) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFile: %s", qPrintable(filePath)); std::vector pcm; int sampleRate, channels; @@ -83,26 +83,28 @@ void VoicePlayer::playOpusFile(const QString& filePath) { if (decodeOpusFile(filePath, pcm, sampleRate, channels, duration) <= 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFile: decode failed for %s", qPrintable(filePath)); - return; + return 0.0f; } DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFile: decoded %zu samples %dHz %dch %.1fs", pcm.size(), sampleRate, channels, duration); SoundManager::instance()->playRawPcm(pcm.data(), pcm.size() / channels, sampleRate, channels); + return duration; } -void VoicePlayer::playOpusFromReader(MediaBlockReader& reader) { +float VoicePlayer::playOpusFromReader(MediaBlockReader& reader) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFromReader: %s", qPrintable(reader.debugPath())); std::vector pcm; int sampleRate, channels; float duration; if (decodeOpusFromReader(reader, pcm, sampleRate, channels, duration) <= 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFromReader: decode failed"); - return; + return 0.0f; } DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFromReader: decoded %zu samples %dHz %dch %.1fs", pcm.size(), sampleRate, channels, duration); SoundManager::instance()->playRawPcm(pcm.data(), pcm.size() / channels, sampleRate, channels); + return duration; } int VoicePlayer::decodeOpusFromReader(MediaBlockReader& reader, std::vector& outPcm, diff --git a/tools/chatgui/src/voiceplayback.h b/tools/chatgui/src/voiceplayback.h index 1a9f9fc2..34aa41f2 100644 --- a/tools/chatgui/src/voiceplayback.h +++ b/tools/chatgui/src/voiceplayback.h @@ -8,10 +8,10 @@ class MediaBlockReader; class VoicePlayer { public: - static void playOpusFile(const QString& filePath); + static float playOpusFile(const QString& filePath); static int decodeOpusFile(const QString& filePath, std::vector& outPcm, - int& outSampleRate, int& outChannels, float& outDurationSec); - static void playOpusFromReader(MediaBlockReader& reader); + int& outSampleRate, int& outChannels, float& outDurationSec); + static float playOpusFromReader(MediaBlockReader& reader); static int decodeOpusFromReader(MediaBlockReader& reader, std::vector& outPcm, - int& outSampleRate, int& outChannels, float& outDurationSec); + int& outSampleRate, int& outChannels, float& outDurationSec); }; diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 8d8d6c31..68aa9715 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -28,6 +28,8 @@ struct TOPO_GROUP_NODE; #define GUI_EVT_ATTACHMENT_DOWNLOADED 14 /* data: [ch_id_len:1][ch_id:var][msg_id:8] */ #define GUI_EVT_DOWNLOAD_PROGRESS 15 /* data: [ch_id_len:1][ch_id:var][msg_id:8][blocks_done:4][num_blocks:4] */ #define GUI_EVT_NODEINFO_UPDATE 16 /* data: node_id:8 conn_presence:1 conn_up:1 best_rtt_packed:2 */ +#define GUI_EVT_CONN_LIST 20 /* data: [count:2][entry:46B]* — binary conn list */ +#define GUI_EVT_CONN_METRICS 21 /* data: text dump of selected connection metrics */ /* ── API ── */ @@ -100,6 +102,14 @@ void gui_bridge_set_nodeinfo_update_cb(gui_nodeinfo_update_fn cb); typedef void (*gui_invite_received_fn)(const uint8_t* data, int len); void gui_bridge_set_invite_received_cb(gui_invite_received_fn cb); +/* Callback для списка ETCP-соединений (вызывается из GUI-потока) */ +typedef void (*gui_conn_list_fn)(const uint8_t* data, int len); +void gui_bridge_set_conn_list_cb(gui_conn_list_fn cb); + +/* Callback для метрик выбранного соединения (вызывается из GUI-потока) */ +typedef void (*gui_conn_metrics_fn)(const uint8_t* data, int len); +void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb); + /* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */ void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 98b738c4..ebe3dd52 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -39,6 +39,8 @@ static gui_attachment_downloaded_fn g_attachment_downloaded_cb = nullptr; static gui_download_progress_fn g_download_progress_cb = nullptr; static gui_nodeinfo_update_fn g_nodeinfo_update_cb = nullptr; static gui_invite_received_fn g_invite_received_cb = nullptr; +static gui_conn_list_fn g_conn_list_cb = nullptr; +static gui_conn_metrics_fn g_conn_metrics_cb = nullptr; static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ @@ -139,7 +141,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { } break; case GUI_EVT_DB_READY: - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: DB_READY (chat DB tables created)"); + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: DB_READY (chat DB tables created)"); if (g_db_ready_cb) g_db_ready_cb(); break; case GUI_EVT_STATUS_REFRESH: @@ -180,6 +182,12 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { case 18: /* CHAT_EVT_INVITE_RECEIVED */ if (g_invite_received_cb) g_invite_received_cb(d, dlen); break; + case GUI_EVT_CONN_LIST: + if (g_conn_list_cb) g_conn_list_cb(d, dlen); + break; + case GUI_EVT_CONN_METRICS: + if (g_conn_metrics_cb) g_conn_metrics_cb(d, dlen); + break; default: DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); break; @@ -277,6 +285,14 @@ void gui_bridge_set_invite_received_cb(gui_invite_received_fn cb) { g_invite_received_cb = cb; } +void gui_bridge_set_conn_list_cb(gui_conn_list_fn cb) { + g_conn_list_cb = cb; +} + +void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb) { + g_conn_metrics_cb = cb; +} + } /* extern "C" */ /* ── uTun nodeinfo callback (called from uasync thread) ── */ diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 9e206dfd..5e955634 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -102,7 +102,7 @@ void UtunNode::stop() { void UtunNode::finalize() { if (!m_instance) return; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "finalize: cleanup start"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup start"); etcp_router_bind(m_instance, ETCP_RT_ID_CHAT, nullptr); chat_sync_destroy(m_instance); chat_core_destroy(m_instance); @@ -110,7 +110,7 @@ void UtunNode::finalize() { m_instance = nullptr; uasync_destroy(m_ua, 0); m_ua = nullptr; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done"); } void UtunNode::send(uint64_t dstNodeId, const QByteArray& data) { @@ -302,7 +302,7 @@ void UtunNode::runLoop() { debug_level_t lvl = debug_level_from_name(lvl_name); if (cat != DEBUG_CATEGORY_NONE && lvl != DEBUG_LEVEL_NONE) { g_debug_config.category_levels[cat] = lvl; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "debug category: %s=%s", + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "debug category: %s=%s", debug_get_category_name(cat), lvl_name); } @@ -359,7 +359,7 @@ void UtunNode::runLoop() { sqlite3_exec(m_instance->topo_sqlite_db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL); sqlite3_exec(m_instance->topo_sqlite_db, "PRAGMA wal_autocheckpoint=10000", NULL, NULL, NULL); topo_node_sqlite_init(m_instance->topo_sqlite_db); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "SQLite (shared): %s db=%p", db_file, (void*)m_instance->topo_sqlite_db); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SQLite (shared): %s db=%p", db_file, (void*)m_instance->topo_sqlite_db); } else { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "SQLite open failed: %s rc=%d", db_file, rc); if (m_instance->topo_sqlite_db) { sqlite3_close(m_instance->topo_sqlite_db); m_instance->topo_sqlite_db = NULL; } @@ -380,7 +380,7 @@ void UtunNode::runLoop() { } { int sock_count = 0; for (struct ETCP_SOCKET* s = m_instance->etcp_sockets; s; s = s->next) sock_count++; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: after instance_init, etcp_sockets=%d conns=%d", + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "utun_node: after instance_init, etcp_sockets=%d conns=%d", sock_count, queue_entry_count(m_instance->connections)); } etcp_router_bind(m_instance, ETCP_RT_ID_CHAT, recvCallback); @@ -397,8 +397,8 @@ void UtunNode::runLoop() { chat_sync_init(m_instance); chat_core_sync_my_addresses(); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: entering poll loop"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "utun_node: entering poll loop"); signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); @@ -407,11 +407,11 @@ void UtunNode::runLoop() { uasync_poll(ua, 100); } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "runLoop: poll exit m_stop=%d", (int)m_stop); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: poll exit m_stop=%d", (int)m_stop); signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "runLoop: emitting stopped"); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: emitting stopped"); QMetaObject::invokeMethod(this, [this] { emit stopped(); }); }