diff --git a/doc/tasks.md b/doc/tasks.md index 6e5953a4..b4194633 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -4,6 +4,12 @@ ## Текущие задачи +[ ] **Разобрать нестабильное ожидание ACK в test_local_transport** — 19.10.2026: + Общий check.sh: 161/162, chat_adapter упал на строке 173 при ожидании LOCAL_DELIVERY_FORBIDDEN для request=508. + В логе получатель дважды отклонил приглашение и отправил ACK; отправитель до проверки получил только late ACK request=507. + Отдельный повтор прошёл. Причина не установлена; проверить доставку UDP и шаги ускоренных часов под параллельной нагрузкой. + Логи: /tmp/config-name-local-transport-failure.log, /tmp/config-name-local-transport-retry.log. + [ ] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — 17.10.2026: При проверке зума test_stream_view::realH264RenderingAndClose получил silenceGaps=3 вместо gapsBefore=2. В логе есть desktop audio gap cause=pcm_short, got=96/480; видео продолжает выводиться (50 кадров к stop). diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 6e7f8a67..526e7fb1 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -145,7 +145,7 @@ struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; }; void chat_core_update_my_name_trampoline(void* arg); /* Обновить адреса topo_node и собственные member-записи каналов. */ void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst); -/* Обновить своё userinfo во всех каналах: увеличить update_ts, подписать и запустить sync. +/* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync. * Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */ void chat_core_update_my_member(struct UTUN_INSTANCE* inst); diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index fe12d725..23f04cd3 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -143,7 +143,7 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in if (profile_pair(&profile, "name", inst->name) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: display name exceeds profile limit channel=%s", ch); return -1; } - char query[256]; snprintf(query, sizeof(query), "SELECT update_ts,userinfo FROM \"%s\" WHERE node_id=?", tbl); + char query[256]; snprintf(query, sizeof(query), "SELECT update_ts,userinfo,update_sig FROM \"%s\" WHERE node_id=?", tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(cc->db, query, -1, &st, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: query failed channel=%s: %s", ch, sqlite3_errmsg(cc->db)); return -1; @@ -154,6 +154,8 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in sqlite3_finalize(st); return -1; } uint64_t local_uts = (uint64_t)sqlite3_column_int64(st, 0); + int signed_profile = local_uts && sqlite3_column_bytes(st, 2) == 64 + && memcmp(sqlite3_column_blob(st, 2), (uint8_t[64]){0}, 64); const char* old = (const char*)sqlite3_column_text(st, 1); char before[MS_USERINFO_MAX + 1] = ""; if (old && strlen(old) > MS_USERINFO_MAX) { @@ -182,8 +184,9 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: attributes exceed profile limit channel=%s", ch); return -1; } profile.text[profile.used++] = '}'; profile.text[profile.used] = 0; - if (option_count && !strcmp(before, profile.text)) { - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "profile: options unchanged channel=%s", ch); return 0; + if (signed_profile && !strcmp(before, profile.text)) { + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "profile: unchanged channel=%s name=%s ts=%llu", ch, inst->name, + (unsigned long long)local_uts); return 0; } if (local_uts >= INT64_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: version exhausted channel=%s", ch); return -1; } time_t seconds = ntp_time_get_seconds(inst); @@ -198,7 +201,8 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in int rc = member_sync_put(inst, ch, myid, inst->my_keys.public_key, inst->my_ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, profile.text, NULL, NULL, 0, 0, NULL); if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: update failed channel=%s result=%d", ch, rc); return -1; } - DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch, (unsigned long long)update_ts); + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s name=%s ts=%llu", CC_ID, ch, inst->name, + (unsigned long long)update_ts); return 0; } diff --git a/src/utun_instance.c b/src/utun_instance.c index 0250287e..f2aca9fa 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -791,6 +791,8 @@ int chat_service_start(struct UTUN_INSTANCE* instance) { if (db_sync_enable(instance) < 0) return -1; topo_group_invite_init(instance); if (chat_core_init(instance, db_file) < 0 || chat_sync_init(instance) < 0 || stream_service_init(instance) < 0) goto fail; + /* Имя из конфига публикуем подписанным member-блоком после восстановления Merkle. */ + if (instance->config->global.chatserver_enabled) chat_core_update_my_member(instance); instance->media_async = media_async_create(); if (!instance->media_async || media_delivery_init(instance) < 0) goto fail; if (chat_attachment_init(instance) < 0) goto fail; diff --git a/tests/test_services.c b/tests/test_services.c index 679b0f18..5254f7c8 100644 --- a/tests/test_services.c +++ b/tests/test_services.c @@ -14,8 +14,11 @@ #include "node_conn_direct.h" #include "chat/chat_core.h" #include "chat/db_sync.h" +#include "chat/member_sync.h" #include "media_async/media_async.h" #include "../lib/debug_config.h" +#include "../lib/json_flat.h" +#include "test_utils.h" struct job { int worked, done, error; }; static void work(void* arg) { ((struct job*)arg)->worked = 1; } @@ -57,10 +60,69 @@ static struct TOPO_GROUP* chat_group(struct UTUN_INSTANCE* inst) { return NULL; } +/* Полный restart с той же БД: новое имя подписано во всех каналах, повторный старт не меняет версии. */ +static void check_config_name(struct UASYNC* ua) { + char directory[512] = "/tmp/utun-config-name-XXXXXX"; + assert(test_mkdtemp(directory) == 0); + char channels[2][64], config[2048]; + uint8_t previous_hash[2][32], previous_signature[2][64]; + uint64_t previous_version[2] = {0}; + const char* names[] = {"Old server", "New \"server\"", "New \"server\"", "Other name"}; + for (unsigned run = 0; run < 4; run++) { + snprintf(config, sizeof(config), "[global]\nmy_node_name=%s\ndb_path=%s\n" + "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" + "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" + "[server: udp]\naddr=127.0.0.1:0\ntype=public\n%s", names[run], directory, + run < 3 ? "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n" : ""); + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst); + assert(utun_core_start(inst) == 0 && chat_service_start(inst) == 0); + if (!run) { + chat_core_create_channel_auto(inst, "config name 1"); + chat_core_create_channel_auto(inst, "config name 2"); + unsigned count = 0; + for (struct ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) { + struct TOPO_GROUP* group = (struct TOPO_GROUP*)e; + assert(group->group_type == TOPO_GROUP_TYPE_CHAT && count < 2); + snprintf(channels[count], sizeof(channels[count]), "%s", group->channel_id); + struct chat_profile_option option = {"extra", "retained"}; + assert(chat_core_set_my_profile_options(inst, channels[count], &option, 1) == 0); + count++; + } + assert(count == 2 && strcmp(channels[0], channels[1])); + } + for (unsigned i = 0; i < 2; i++) { + char profile[MS_USERINFO_MAX + 1], value[128]; + uint8_t signature[64], join[64], hash[32], message[MS_USERINFO_MAX + 73]; + uint64_t version, join_ts; + assert(chat_core_get_my_profile(inst, channels[i], profile, sizeof(profile), signature, &version) == 0); + assert(json_flat_get(profile, "name", value, sizeof(value)) == 0 && !strcmp(value, names[run == 3 ? 2 : run])); + assert(json_flat_get(profile, "extra", value, sizeof(value)) == 0 && !strcmp(value, "retained")); + assert(topo_node_sqlite_member_get_join(inst->topo_sqlite_db, channels[i], inst->node_id, join, &join_ts) == 0); + int length = member_sync_build_update_msg(join, version, profile, message, sizeof(message)); assert(length > 0); + assert(sc_ed25519_verify(inst->my_ed25519_pubkey, message, length, signature) == SC_OK); + assert(merkle_sync_read_hash(inst, MT_DATA_MEMBERS, channels[i], 0, 0, hash) == 0); + if (run == 1) { + assert(version > previous_version[i] && memcmp(signature, previous_signature[i], 64)); + assert(memcmp(hash, previous_hash[i], 32)); + } else if (run > 1) { + assert(version == previous_version[i] && !memcmp(signature, previous_signature[i], 64)); + assert(!memcmp(hash, previous_hash[i], 32)); + } + previous_version[i] = version; + memcpy(previous_hash[i], hash, 32); memcpy(previous_signature[i], signature, 64); + } + utun_instance_destroy(inst); uasync_poll(ua, 0); + } + char path[1024]; snprintf(path, sizeof(path), "%s/chats.db", directory); test_unlink(path); test_rmdir(directory); + DEBUG_INFO(DEBUG_CATEGORY_CHAT, + "[PASS] config name: restart updates signed profiles and Merkle in all channels; unchanged/no chatserver keeps versions"); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); struct UASYNC* ua = uasync_create(); assert(ua); check_client_activity(ua); + check_config_name(ua); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, "[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 513ad603..1ab22edc 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -341,6 +341,16 @@ add_executable(vibechat WIN32 ) add_dependencies(vibechat chatgui_translations) +# Проводник и ярлыки Windows читают значок из EXE до запуска Qt. +if(WIN32) + enable_language(RC) + configure_file(resources/vibechat.rc.in "${CMAKE_CURRENT_BINARY_DIR}/vibechat.rc" @ONLY) + target_sources(vibechat PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/vibechat.rc") + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/vibechat.rc" PROPERTIES + OBJECT_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/app_icon.ico") + message(STATUS "Windows executable icon: resources/icons/app_icon.ico") +endif() + target_include_directories(vibechat PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS}) target_compile_definitions(vibechat PRIVATE SQLITE_THREADSAFE=1 USE_SQLITE VIBECHAT_VERSION="${PROJECT_VERSION}") diff --git a/tools/chatgui/resources/icons/app_icon.ico b/tools/chatgui/resources/icons/app_icon.ico new file mode 100644 index 00000000..9f302500 Binary files /dev/null and b/tools/chatgui/resources/icons/app_icon.ico differ diff --git a/tools/chatgui/resources/vibechat.rc.in b/tools/chatgui/resources/vibechat.rc.in new file mode 100644 index 00000000..27dec854 --- /dev/null +++ b/tools/chatgui/resources/vibechat.rc.in @@ -0,0 +1,2 @@ +// Значок EXE для меню «Пуск», ярлыков и Проводника; путь подставляет CMake. +1 ICON "@CMAKE_CURRENT_SOURCE_DIR@/resources/icons/app_icon.ico"