From 8c30c8e6ce0c3ffab47a4353bc88e54139eaa42f Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 12:19:31 +0100 Subject: [PATCH] Isolate avatar preparation generations and guard reentrant signals --- tools/chatgui/src/avatars/avatarmanager.cpp | 22 ++++++++++++++++----- tools/chatgui/src/avatars/avatarmanager.h | 7 ++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/tools/chatgui/src/avatars/avatarmanager.cpp b/tools/chatgui/src/avatars/avatarmanager.cpp index ce47469d..1b2af062 100644 --- a/tools/chatgui/src/avatars/avatarmanager.cpp +++ b/tools/chatgui/src/avatars/avatarmanager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -270,12 +271,14 @@ void AvatarManager::saveDefault() { const QByteArray value = m_defaultServer ? (QString::number(m_defaultServer, 16) + '/' + m_defaultHash + ".json").toLatin1() : QByteArray("-"); const char name[] = "avatar_default"; memcpy(q->data, name, sizeof(name)); memcpy(q->data + sizeof(name), value.constData(), value.size()); - gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, q); + if (gui_bridge_post_uasync_owned_fn(chat_core_save_ui_state_trampoline, q) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "avatar: default preference queue rejected"); u_free(q); + } } void AvatarManager::uploadDefault(const QString& source, quint64 server, const QRectF& crop) { if (!m_ready || m_preparing || !server || !gui_bridge_is_uasync_ready()) return; - m_preparing = true; emit statusChanged(text("Preparing avatar…")); + m_preparing = true; const QString directory = m_directory; const quint64 generation = ++m_generation; cancelUploads(); QStringList pinned; if (!m_defaultHash.isEmpty()) pinned << m_defaultHash; for (const auto& job : m_jobs) if (job.upload) pinned << job.ref.hash; @@ -283,8 +286,11 @@ void AvatarManager::uploadDefault(const QString& source, quint64 server, const Q const auto result = AvatarCache::prepare(source, directory, crop); if (!result.image.isNull()) { auto keep = pinned; keep << result.hash; AvatarCache::prune(directory, keep); } QMetaObject::invokeMethod(this, [this, result, server, generation] { + if (generation != m_generation) { + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "avatar: discard stale preparation generation=%llu current=%llu", + (unsigned long long)generation, (unsigned long long)m_generation); return; + } m_preparing = false; - if (generation != m_generation) return; if (result.image.isNull()) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: preparation failed reason=%s", qPrintable(result.error)); emit statusChanged(text("Cannot prepare avatar: %1").arg(result.error)); return; @@ -292,11 +298,17 @@ void AvatarManager::uploadDefault(const QString& source, quint64 server, const Q m_defaultHash = result.hash; m_defaultServer = server; saveDefault(); m_records[result.hash] = result.record; m_images.insert(result.hash, new QImage(result.image), qMax(1, int(result.image.sizeInBytes() / 1024))); - m_missing.remove(result.hash); emit serversChanged(); emit statusChanged(text("Uploading avatar…")); + m_missing.remove(result.hash); + QPointer self(this); + emit serversChanged(); + if (!self || generation != m_generation) return; + emit statusChanged(text("Uploading avatar…")); + if (!self || generation != m_generation) return; for (auto group : m_servers.keys()) { m_retry.remove(key(group, m_db->myNodeId())); scheduleOwn(group); } pump(); }, Qt::QueuedConnection); })); + emit statusChanged(text("Preparing avatar…")); } void AvatarManager::cancelUploads() { @@ -309,7 +321,7 @@ void AvatarManager::cancelUploads() { void AvatarManager::clearDefault() { if (!m_ready || !gui_bridge_is_uasync_ready()) return; - ++m_generation; cancelUploads(); m_defaultHash.clear(); m_defaultServer = 0; saveDefault(); + ++m_generation; m_preparing = false; cancelUploads(); m_defaultHash.clear(); m_defaultServer = 0; saveDefault(); for (const auto& ref : m_refs) if (ref.owner == m_db->myNodeId()) { Job job; job.ref = ref; job.ref.server = 0; job.ref.hash.clear(); job.upload = true; job.generation = m_generation; enqueue(job); diff --git a/tools/chatgui/src/avatars/avatarmanager.h b/tools/chatgui/src/avatars/avatarmanager.h index f4deb04a..62a80ab0 100644 --- a/tools/chatgui/src/avatars/avatarmanager.h +++ b/tools/chatgui/src/avatars/avatarmanager.h @@ -1,4 +1,5 @@ -/* Один менеджер на аккаунт: подписанные ссылки, общий кеш и ограниченная очередь передач. */ +/* Один менеджер на аккаунт: подписанные ссылки, общий кеш и ограниченная очередь передач. + * Методы/сигналы — GUI-поток; DbManager заимствуется и должен переживать менеджер. */ #pragma once #include "avatarcache.h" #include @@ -26,6 +27,10 @@ public: void onPublished(const uint8_t* data, int length); QImage image(const QString& channel, quint64 owner); QMap servers() const; + /* Начинает worker-подготовку локального source; требует ready, ненулевой server и готовое ядро. + * crop — нормализованный после EXIF квадрат из selectAvatarCrop, пустой — центральный квадрат. + * Пока текущая подготовка занята, повтор игнорируется; clearDefault отменяет поколение и позволяет новый upload. + * Устаревший worker не меняет default/preparing. Из обработчиков сигналов подготовки можно удалить manager. */ void uploadDefault(const QString& source, quint64 server, const QRectF& crop = {}); void openGallery(const QString& channel, quint64 owner); void closeGallery();