From 9793733cd5d163971be08347803ebadcd469b8fc Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 21 Sep 2026 18:59:04 +0300 Subject: [PATCH] =?UTF-8?q?chatgui:=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=B0=20=D0=B2=D0=BE=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B8=D0=B7=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D1=81=D0=BE=D0=B2=D1=8B=D1=85=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20(Windows)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sound_manager: ma_engine_init/playRawPcm логируют через DEBUG_* (qWarning/qDebug терялись в GUI-приложении без консоли); детальный результат старта PCM (rc/is_playing/cursor) - main: ранняя инициализация лога в GUI-потоке до SoundManager::init, чтобы диагностика инициализации попала в файл - messagedelegate: лог клика по Play/timeline (filePath/dlState/существует ли файл) - voiceplayback: лог заголовка opus (sr/ch/frameSamples) Попутные правки: - u_async: фикс двойного close wakeup_pipe (eventfd/Windows) - chat_core: NULL-guard в get_db/is_initialized - android: sqlite открывается в utun_instance_init, cleanup stale-потока без утечки fd - android SettingsScreen: компактная раскладка имени интерфейса --- lib/u_async.c | 7 ++- src/chat/chat_core.c | 4 +- .../utun/chat/ui/screens/SettingsScreen.kt | 4 +- .../libutun_lite/instance_lite.c | 63 ++++++++----------- tools/chatgui/src/main.cpp | 10 +++ tools/chatgui/src/messagedelegate.cpp | 4 ++ tools/chatgui/src/sound_manager.cpp | 32 +++++++--- tools/chatgui/src/voiceplayback.cpp | 3 + 8 files changed, 76 insertions(+), 51 deletions(-) diff --git a/lib/u_async.c b/lib/u_async.c index 5d8465f7..fdbdf209 100644 --- a/lib/u_async.c +++ b/lib/u_async.c @@ -1783,10 +1783,13 @@ void uasync_destroy(struct UASYNC* ua, int close_fds) { if (ua->wakeup_initialized) { #ifdef _WIN32 closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[0]); - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]); + if (ua->wakeup_pipe[1] != ua->wakeup_pipe[0]) + closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]); #else close(ua->wakeup_pipe[0]); - close(ua->wakeup_pipe[1]); + // eventfd на Linux: wakeup_pipe[0] == wakeup_pipe[1] — избегаем двойного close + if (ua->wakeup_pipe[1] != ua->wakeup_pipe[0]) + close(ua->wakeup_pipe[1]); #endif } diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index aea6c124..f1290b88 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -241,8 +241,8 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID); } -sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->db : NULL; } -int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->initialized : 0; } +sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->db : NULL; } +int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->initialized : 0; } /* ─── chat settings (GUI → uasync) ─── */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 9e8933e0..6fe4d239 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -249,8 +249,6 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool val stateColor = when (s.state) { 2 -> Color(0xFF27AE60); 1 -> Color(0xFFF1C40F); else -> Color(0xFFE53935) } Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Text(s.ifName, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) - Spacer(Modifier.width(4.dp)) Text("${s.bindIp}:${s.port}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall) Spacer(Modifier.width(6.dp)) Surface(shape = MaterialTheme.shapes.extraSmall, color = stateColor, @@ -265,6 +263,8 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool color = MaterialTheme.colorScheme.onSurfaceVariant) } } + Text(s.ifName, style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth()) } } } diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 95d8184a..e676e1d4 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -261,6 +261,8 @@ static void install_crash_handlers(void) { static void* instance_thread(void* arg) { char* config_text = (char*)arg; int my_gen = g_generation; + struct UASYNC* my_ua = NULL; /* поток-локальная копия — для cleanup stale-потока */ + struct UTUN_INSTANCE* my_inst = NULL; struct utun_config* config = parse_config_from_buf(config_text, strlen(config_text), "android"); u_free(config_text); @@ -269,6 +271,7 @@ static void* instance_thread(void* arg) { install_crash_handlers(); g_ua = uasync_create(); + my_ua = g_ua; if (!g_ua) { IL_LOGE("uasync_create failed"); free_config(config); __atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE); pthread_detach(pthread_self()); return NULL; } standby_init(g_ua); @@ -280,6 +283,7 @@ static void* instance_thread(void* arg) { ensure_keys(config); g_inst = utun_instance_create_from_config(g_ua, config); + my_inst = g_inst; if (!g_inst) { IL_LOGE("utun_instance_create_from_config failed"); standby_deinit(); @@ -294,25 +298,8 @@ static void* instance_thread(void* arg) { IL_LOGI("instance created, node_id=0x%016llx", (unsigned long long)g_inst->node_id); - /* Open shared SQLite DB */ - if (g_db_path[0] && !g_inst->topo_sqlite_db) { - char db_file[512]; - snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); - int rc = sqlite3_open_v2(db_file, &g_inst->topo_sqlite_db, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); - if (rc == SQLITE_OK && g_inst->topo_sqlite_db) { - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL); - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA wal_autocheckpoint=10000", NULL, NULL, NULL); - topo_node_sqlite_init(g_inst->topo_sqlite_db); - IL_LOGI("SQLite opened %s", db_file); - } else { - IL_LOGE("SQLite open failed rc=%d", rc); - if (g_inst->topo_sqlite_db) { sqlite3_close(g_inst->topo_sqlite_db); g_inst->topo_sqlite_db = NULL; } - } - } - + /* Shared SQLite DB открывается внутри utun_instance_init() → topo_groups_init() + (topo_group.c). Двойное открытие здесь давало утечку соединения на каждый рестарт. */ /* Full init: db_sync, chat_core, chat_sync, init_connections */ if (utun_instance_init(g_inst) != 0) { IL_LOGE("utun_instance_init failed"); @@ -369,13 +356,17 @@ static void* instance_thread(void* arg) { g_inst ? g_inst->connections : NULL); uasync_mark_stopped(g_ua); __atomic_store_n(&g_running, 0, __ATOMIC_RELEASE); - utun_instance_destroy(g_inst); + struct UTUN_INSTANCE* old_inst = g_inst; g_inst = NULL; + utun_instance_destroy(old_inst); + my_inst = NULL; uasync_print_resources(g_ua, "AFTER_DESTROY"); u_report_unfreed_blocks(); standby_deinit(); uasync_destroy(g_ua, 0); + my_ua = NULL; g_ua = uasync_create(); + my_ua = g_ua; if (!g_ua) { IL_LOGE("poll exit: uasync_create failed"); break; } standby_init(g_ua); @@ -389,20 +380,11 @@ static void* instance_thread(void* arg) { if (!config) { IL_LOGE("poll exit: parse_config failed"); break; } g_inst = utun_instance_create_from_config(g_ua, config); + my_inst = g_inst; if (!g_inst) { IL_LOGE("poll exit: create_from_config failed"); break; } chat_event_set_handler(g_inst, chat_event_forward); - char db_file[512]; - snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); - int rc = sqlite3_open_v2(db_file, &g_inst->topo_sqlite_db, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); - if (rc == SQLITE_OK && g_inst->topo_sqlite_db) { - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); - sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL); - topo_node_sqlite_init(g_inst->topo_sqlite_db); - } else { IL_LOGE("poll exit: SQLite open failed rc=%d", rc); break; } - + /* Shared SQLite DB открывается внутри utun_instance_init() (topo_group.c). */ if (utun_instance_init(g_inst) != 0) { IL_LOGE("poll exit: utun_instance_init failed"); break; } chat_core_sync_my_addresses(g_inst); fire_local_sockets_event(); @@ -424,22 +406,29 @@ static void* instance_thread(void* arg) { if (my_gen == g_generation) { __atomic_store_n(&g_running, 0, __ATOMIC_RELEASE); - if (g_inst) { - chat_event_post(g_inst, CHAT_EVT_SERVICE_STOPPED, NULL, 0); - utun_instance_destroy(g_inst); + if (my_inst) { + chat_event_post(my_inst, CHAT_EVT_SERVICE_STOPPED, NULL, 0); g_inst = NULL; + utun_instance_destroy(my_inst); + my_inst = NULL; } - if (g_ua) { + if (my_ua) { standby_deinit(); - uasync_destroy(g_ua, 0); + uasync_destroy(my_ua, 0); g_ua = NULL; + my_ua = NULL; } char* stale_cfg = (char*)__atomic_exchange_n(&g_restart_config, NULL, __ATOMIC_ACQUIRE); u_free(stale_cfg); u_report_unfreed_blocks(); IL_LOGI("cleanup complete (gen=%d)", my_gen); } else { - IL_LOGI("cleanup skipped — thread gen=%d but current gen=%d (stale thread)", my_gen, g_generation); + /* stale-поток: глобальные g_inst/g_ua уже принадлежат новому потоку, + но собственные объекты этого потока надо закрыть, иначе течёт fd. */ + IL_LOGI("cleanup (stale thread gen=%d vs %d): destroying own inst=%p ua=%p to avoid fd leak", + my_gen, g_generation, (void*)my_inst, (void*)my_ua); + if (my_inst) { utun_instance_destroy(my_inst); my_inst = NULL; } + if (my_ua) { uasync_destroy(my_ua, 0); my_ua = NULL; } } __atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE); pthread_mutex_lock(&g_stop_mutex); diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 89a56826..cba63e25 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -148,6 +148,16 @@ int main(int argc, char *argv[]) { if (!QDir::isAbsolutePath(debugFile)) debugFile = QCoreApplication::applicationDirPath() + "/" + debugFile; + /* ранняя инициализация лога в GUI-потоке: SoundManager::init() вызывается в + * конструкторе MainWindow ДО старта utun-потока, поэтому без этого его диагностика + * (ma_engine_init и т.п.) в лог-файл не попадёт. */ + debug_config_init(); + debug_enable_function_name(0); + debug_enable_file_output(debugFile.toUtf8().constData(), 0); + QString lvl = cfg.debugLevel().trimmed(); + if (lvl.isEmpty()) lvl = QStringLiteral("info"); + debug_apply_global_level(lvl.toUtf8().constData()); + DbManager db; MainWindow w(nullptr, &db, cfgPath, dbPath, debugFile, cfg.debugLevel(), cfg.debugCategories()); diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index d0157491..10e306a8 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -174,6 +174,10 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, qint64 msgId = index.data(MsgIdRole).toLongLong(); QString chId = index.data(MsgChannelIdRole).toString(); + GUI_DEBUG("editorEvent voice: playBtn=%d wave=%d file='%s' exists=%d dlState=%d msgId=%lld", + (int)onPlayBtn, (int)onWaveform, qPrintable(filePath), QFile::exists(filePath), + dlState, (long long)msgId); + /* not downloaded: start download (play will start on completion) */ if (filePath.isEmpty() && dlState != 1) { if (chId.isEmpty() || msgId <= 0) return false; diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index eb5cb93d..8e593f21 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -21,14 +21,21 @@ bool SoundManager::init() { ma_engine* engine = new ma_engine; ma_result result = ma_engine_init(NULL, engine); if (result != MA_SUCCESS) { - qWarning("SoundManager: ma_engine_init failed (%d)", result); + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "SoundManager: ma_engine_init failed (%d)", result); delete engine; return false; } m_engine = engine; m_context = ma_device_get_context(ma_engine_get_device(engine)); m_initialized = true; - qDebug("SoundManager: initialized (engine=%p, context=%p)", (void*)engine, (void*)m_context); + + ma_device* dev = ma_engine_get_device(engine); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "SoundManager: initialized engine=%p ctx=%p dev='%s' fmt=%d ch=%u sr=%u internalSr=%u", + (void*)engine, (void*)m_context, + dev ? dev->playback.name : "?", dev ? (int)dev->playback.format : -1, + dev ? dev->playback.channels : 0, dev ? dev->sampleRate : 0, + dev ? dev->playback.internalSampleRate : 0); return true; } @@ -78,7 +85,8 @@ void SoundManager::setEvent(const QString& name, bool enabled, float volume) { es.enabled = enabled; es.volume = volume; m_eventSettings[name] = es; - qDebug("SoundManager: event '%s' enabled=%d vol=%.2f", qPrintable(name), enabled, volume); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: event '%s' enabled=%d vol=%.2f", + qPrintable(name), enabled, volume); } SoundManager::EventSettings SoundManager::eventSettings(const QString& name) const { @@ -191,12 +199,14 @@ ma_context* SoundManager::context() { void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels) { if (!m_engine || !m_initialized) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: not initialized"); + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "SoundManager::playRawPcm: not initialized (engine=%p init=%d)", + (void*)m_engine, (int)m_initialized); return; } clearCurrentPcm(); m_currentPcmSampleRate = sampleRate; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: %d samples %dHz %dch", sampleCount, sampleRate, channels); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: %d samples %dHz %dch volume=%.2f", + sampleCount, sampleRate, channels, m_volume); ma_audio_buffer_config bufCfg = ma_audio_buffer_config_init( ma_format_s16, (ma_uint32)channels, (ma_uint64)sampleCount, pcm, NULL); @@ -204,7 +214,7 @@ void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRat m_currentPcmBuffer = buf; ma_result r = ma_audio_buffer_init_copy(&bufCfg, buf); if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager: audio_buffer_init_copy failed (%d)", r); + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "SoundManager: audio_buffer_init_copy failed (%d)", r); delete buf; m_currentPcmBuffer = nullptr; return; } @@ -212,14 +222,20 @@ void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRat m_currentPcmSound = new ma_sound; r = ma_sound_init_from_data_source(m_engine, (ma_data_source*)buf, 0, nullptr, m_currentPcmSound); if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager: PCM sound init failed (%d)", r); + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "SoundManager: PCM sound init failed (%d)", r); ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr; delete m_currentPcmSound; m_currentPcmSound = nullptr; return; } ma_sound_set_volume(m_currentPcmSound, m_volume); - ma_sound_start(m_currentPcmSound); + r = ma_sound_start(m_currentPcmSound); + ma_uint64 cursor = 0; + ma_sound_get_cursor_in_pcm_frames(m_currentPcmSound, &cursor); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "SoundManager::playRawPcm: started rc=%d is_playing=%d cursor=%llu/%d", + (int)r, (int)ma_sound_is_playing(m_currentPcmSound), + (unsigned long long)cursor, sampleCount); } void SoundManager::pauseCurrentPcm() { diff --git a/tools/chatgui/src/voiceplayback.cpp b/tools/chatgui/src/voiceplayback.cpp index 798bfc54..5ca7490f 100644 --- a/tools/chatgui/src/voiceplayback.cpp +++ b/tools/chatgui/src/voiceplayback.cpp @@ -41,6 +41,9 @@ int VoicePlayer::decodeOpusFile(const QString& filePath, std::vector& o outSampleRate = sampleRate; outChannels = channels; + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer: header sr=%d ch=%d frameSamples=%u file=%s", + sampleRate, channels, (unsigned)frameSamples, qPrintable(filePath)); + opus_codec_decoder_t* dec = opus_codec_decoder_create(sampleRate, channels); if (!dec) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer: decoder create failed");