From dafd3a2adc5503bd58ff6f616136524efd6a7dd7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 03:19:03 +0100 Subject: [PATCH] Reject malformed audio diagnostic snapshots --- .../chatgui/tests/test_audio_diagnostics.cpp | 35 ++++++++++++++- tools/chatgui/transport/audio_diagnostics.cpp | 18 ++++++-- tools/chatgui/transport/audio_diagnostics.h | 44 ++++++++++++++----- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/tools/chatgui/tests/test_audio_diagnostics.cpp b/tools/chatgui/tests/test_audio_diagnostics.cpp index 060f9238..80e43102 100644 --- a/tools/chatgui/tests/test_audio_diagnostics.cpp +++ b/tools/chatgui/tests/test_audio_diagnostics.cpp @@ -5,7 +5,7 @@ #include int main() { - debug_config_init(); debug_enable_console(0); + debug_config_init(); debug_enable_console(1); debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_DEBUG); constexpr int producers = 4, perProducer = 20000; std::vector seen(producers * perProducer); std::atomic done{0}; @@ -19,6 +19,22 @@ int main() { assert(take(e)); assert(e.value == int64_t(i) && e.detail == 7); } assert(!take(e)); lost = 0; + // Повреждённый вызов не должен выдавать прежний снимок слота за новые часы. + audio_diag_capture_clock(nullptr, nullptr); + audio_diag_playback_clock(nullptr, nullptr); + audio_diag_wasapi_playback(nullptr, nullptr); + audio_diag_event(nullptr, AUDIO_DIAG_CAPTURE_CLOCK, AUDIO_DIAG_READ, 1, 0); + audio_diag_event(nullptr, AUDIO_DIAG_ENTER, AUDIO_DIAG_STAGE_COUNT, 1, 0); + audio_diag_event(nullptr, -1, AUDIO_DIAG_READ, 1, 0); + std::map invalidDevices; + for (int i = 0; i < 6; ++i) { + assert(take(e)); assert(e.kind == AUDIO_DIAG_ERROR && e.value == 1 && e.text[0]); + consume(e, invalidDevices); + } + assert(!take(e)); + assert(invalidDevices[0].faults[AUDIO_DIAG_ERROR] == 6); + assert(!invalidDevices[0].captureBlocks && !invalidDevices[0].playbackBlocks && !invalidDevices[0].wasapiSeen); + report(invalidDevices, wallNow(), 1.0); // Граница секундного отчёта не обрывает карту времени; hole отделён от скачка оценки latency. std::map devices; audio_capture_timing timing{1000000, 100000, 0, 480, 48000, 1, 0}; @@ -56,6 +72,23 @@ int main() { assert(capture.playbackLatency.average() == 14500 && capture.playbackBlock.low == 20000); report(devices, wallNow(), 1.0); assert(capture.playbackBlocks == 0 && capture.playbackLatency.count == 0 && capture.playbackBlock.high == 0); + // Снимок принадлежит producer только до возврата; exclusive padding не является измеренной очередью. + audio_wasapi_timing wasapi{}; + wasapi.frames = 480; wasapi.rate = 48000; wasapi.device_rate = 96000; + wasapi.shared = 1; wasapi.padding_frames = 960; wasapi.stream_latency_us = 5000; wasapi.converter_us = 1000; + audio_diag_wasapi_playback(nullptr, &wasapi); + wasapi.shared = 0; wasapi.padding_frames = 96000; wasapi.stream_latency_us = 8000; + assert(take(e)); consume(e, devices); + assert(capture.wasapiPadding.count == 1 && capture.wasapiPadding.high == 10000); + assert(capture.wasapiLatency.high == 5000 && capture.wasapi.shared == 1); + audio_diag_wasapi_playback(nullptr, &wasapi); + wasapi.padding_result = -1; wasapi.latency_result = -1; + audio_diag_wasapi_playback(nullptr, &wasapi); + while (take(e)) consume(e, devices); + assert(capture.wasapiBlocks == 3 && capture.wasapiFailures == 1 && capture.wasapiPadding.count == 1); + assert(capture.wasapiLatency.count == 2 && capture.wasapiLatency.average() == 6500); + report(devices, wallNow(), 1.0); + assert(!capture.wasapiBlocks && !capture.wasapiFailures && !capture.wasapiPadding.count); std::vector workers; for (int p = 0; p < producers; ++p) workers.emplace_back([p, &done]() { for (int i = 0; i < perProducer; ++i) diff --git a/tools/chatgui/transport/audio_diagnostics.cpp b/tools/chatgui/transport/audio_diagnostics.cpp index b7aa645d..09bbeef1 100644 --- a/tools/chatgui/transport/audio_diagnostics.cpp +++ b/tools/chatgui/transport/audio_diagnostics.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include "../../../lib/platform_compat.h" #endif extern "C" { @@ -253,10 +253,11 @@ void consume(const Event& e, std::map& devices) { // Первая аномалия за окно, остальные попадут в сводку. if (d.faults[e.kind] == uint64_t(std::max(e.value, 1))) DEBUG_WARN(DEBUG_CATEGORY_CALL, - "AudioDiag: fault dev=%p role=%s id=%016llx kind=%s stage=%s value=%lld detail=%lld tid=%ld mono_ms=%llu", + "AudioDiag: fault dev=%p role=%s id=%016llx kind=%s stage=%s value=%lld detail=%lld tid=%ld mono_ms=%llu%s%s", (void*)e.device, d.label.c_str(), (unsigned long long)d.id, kFaults[e.kind - AUDIO_DIAG_HOLE], e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT ? kStages[e.stage] : "none", - (long long)e.value, (long long)e.detail, e.tid, (unsigned long long)(e.wall / 1000000)); + (long long)e.value, (long long)e.detail, e.tid, (unsigned long long)(e.wall / 1000000), + e.text[0] ? " reason=" : "", e.text); } } @@ -388,6 +389,14 @@ void audio_diag_stop(void) { DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: stopped"); } void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail) { + if (kind < AUDIO_DIAG_ENTER || kind > AUDIO_DIAG_ERROR) { + publish(device, AUDIO_DIAG_ERROR, stage, 1, kind, "invalid generic event kind"); + return; + } + if ((kind == AUDIO_DIAG_ENTER || kind == AUDIO_DIAG_LEAVE) && (stage < 0 || stage >= AUDIO_DIAG_STAGE_COUNT)) { + publish(device, AUDIO_DIAG_ERROR, -1, 1, stage, "invalid stage for enter/leave"); + return; + } publish(device, kind, stage, value, detail, nullptr); } void audio_diag_label(const void* device, uint64_t id, const char* name) { @@ -397,12 +406,15 @@ void audio_diag_log(const void* log, unsigned int level, const char* message) { publish(log, AUDIO_DIAG_LOG, -1, level, 0, message); } void audio_diag_capture_clock(const void* device, const audio_capture_timing* timing) { + if (!timing) { publish(device, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, 0, "NULL capture clock snapshot"); return; } publish(device, AUDIO_DIAG_CAPTURE_CLOCK, AUDIO_DIAG_READ, 0, 0, nullptr, timing); } void audio_diag_playback_clock(const void* device, const audio_playback_timing* timing) { + if (!timing) { publish(device, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, 0, "NULL playback clock snapshot"); return; } publish(device, AUDIO_DIAG_PLAYBACK_CLOCK, AUDIO_DIAG_WRITE, 0, 0, nullptr, nullptr, timing); } void audio_diag_wasapi_playback(const void* device, const audio_wasapi_timing* timing) { + if (!timing) { publish(device, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, 0, "NULL WASAPI snapshot"); return; } publish(device, AUDIO_DIAG_WASAPI_PLAYBACK, AUDIO_DIAG_WRITE, 0, 0, nullptr, nullptr, nullptr, timing); } #else diff --git a/tools/chatgui/transport/audio_diagnostics.h b/tools/chatgui/transport/audio_diagnostics.h index 102035c3..4e5e6545 100644 --- a/tools/chatgui/transport/audio_diagnostics.h +++ b/tools/chatgui/transport/audio_diagnostics.h @@ -2,8 +2,15 @@ #include -/* Desktop-аудиодиагностика: callbacks передают числовые снимки в ограниченную очередь. - * Отдельный reporter пишет сводки в категорию call; start/stop принадлежат владельцу устройств. */ +/* Desktop-аудиодиагностика Linux/Windows; на других платформах API ничего не делает. + * После start несколько producers передают метаданные в MPSC-кольцо на 8192 события. + * Публикация без allocation/логирования/ожидания, не более четырёх попыток резервирования; + * перегрузка/конкуренция дают потерю события, которую reporter отмечает в сводке. + * Отдельный поток опрашивает очередь каждые 20 мс, пишет секундные сводки в call: + * штатные стадии TRACE, часы DEBUG, аномалии WARN (первая каждого типа за интервал + счётчики). + * Указатели device/log — только числовые ключи, могут быть NULL; reporter их не разыменовывает. + * Строки копируются до 191 байта плюс NUL, timing копируется целиком до возврата вызова. + * PCM и содержимое сообщений не принимаются. До start/после stop события игнорируются. */ #ifdef __cplusplus extern "C" { #endif @@ -22,15 +29,18 @@ enum audio_diag_kind { }; struct audio_capture_timing { - int64_t first_us, latency_us, converter_us; - unsigned frames, rate; - int measured, missing; + int64_t first_us; // оценка monotonic-времени первого кадра блока, микросекунды + int64_t latency_us; // знаковая backend latency, учитывается только при measured!=0 + int64_t converter_us; // задержка converter/cache, микросекунды + unsigned frames, rate; // размер блока и его частота, Гц; rate=0 исключает расчёт длительности + int measured; // backend-оценка доступна; 0 означает оценочный блок без измеренной latency + int missing; // блок с hole: в missing_frames попадают все frames этого блока }; struct audio_playback_timing { - int64_t latency_us, converter_us; - unsigned frames, rate; - int measured; + int64_t latency_us, converter_us; // backend latency и задержка converter/cache, микросекунды + unsigned frames, rate; // размер блока и частота, Гц; rate=0 исключает расчёт длительности + int measured; // ненулевой: latency_us участвует в диапазоне; отрицательная latency допустима }; /* Снимок перед заполнением client PCM. Padding — текущая очередь, stream_latency — отдельное @@ -44,15 +54,29 @@ struct audio_wasapi_timing { int32_t padding_result, latency_result; // HRESULT каждого запроса; отрицательное значение — ошибка. }; -/* start/stop — управляющий поток; перед stop все producers должны быть остановлены. */ +/* Один управляющий владелец сериализует start/stop. Повторный start активного reporter — no-op; + * отказ создания потока логируется, диагностика остаётся выключенной (возврата статуса нет). + * Перед stop остановить и дождаться всех producers, включая незавершённые публикации. + * stop дренирует очередь, пишет последнюю сводку и join; повторный stop допустим. + * Следующий start очищает статистику и очередь. Не вызывать stop из reporter. */ void audio_diag_start(void); void audio_diag_stop(void); +/* kind — ENTER..ERROR; LABEL/LOG/CLOCK передавать специализированными функциями. + * ENTER/LEAVE требуют stage=0..STAGE_COUNT-1; остальные события могут использовать -1. + * value/detail — backend-метрики: ENTER value=frames, OPEN value=тип/detail=частота; + * для аномалий value даёт max(value,1) в счётчик. Неверные kind/stage дают ERROR-событие. */ void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail); +/* Метка ресурса; id без интерпретации, NULL name означает пустую строку. */ void audio_diag_label(const void* device, uint64_t id, const char* name); +/* miniaudio level 1/2/3/остальные -> ERROR/WARN/DEBUG/TRACE; NULL message — пустая строка. + * Строки с Hole. подавляются: число hole-кадров собирает отдельное событие. */ void audio_diag_log(const void* log, unsigned int level, const char* message); +/* Снимки из потока устройства; timing обязателен, NULL даёт ERROR-событие без снимка. */ void audio_diag_capture_clock(const void* device, const struct audio_capture_timing* timing); void audio_diag_playback_clock(const void* device, const struct audio_playback_timing* timing); -/* Вызывать из playback callback; снимок копируется, устройством reporter не пользуется. */ +/* Снимок перед заполнением playback buffer; timing обязателен, NULL даёт ERROR-событие. + * Только shared padding с успешным HRESULT и device_rate>0 даёт оценку очереди; + * stream latency учитывается отдельно, даже в exclusive mode. */ void audio_diag_wasapi_playback(const void* device, const struct audio_wasapi_timing* timing); #ifdef __cplusplus