mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
3dc7035f9e6731076151c809768c1bb1444d698c
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3dc7035f9e |
fix(tci): писатель WAV — один поток с очередью, файл публикуется атомарно
SAVE запускал поток на каждую запись с FreeOnTerminate: его никто не держал
и никто не ждал. Замерено отдельным процессом — при штатном выходе сразу
после сохранения от ожидаемых 100000044 байт на диске оставалось 40960, а
заголовок заявлял полную длину; на медленном каталоге идущие подряд SAVE
плодили сотни потоков, чья память стеков в 128-МБ бюджет не входила.
Теперь писатель один и принадлежит адаптеру (лениво на первом SAVE), очередь
ограничена TCI_RECORD_MAX_JOBS = 16 (сверх — клиенту writer busy и возврат
резерва), а деструктор адаптера гасит его через TCIStopWriter: Close →
WaitDrained (без срока) → Free. Срока здесь нет намеренно: поток, стоящий в
write/fsync, изнутри процесса не останавливается (Terminate не указ, Free
обязан WaitFor, бросить живой TThread нельзя — он ходит в общий бюджет),
поэтому срок не ограничивал выход, а только терял подтверждённые клиенту
записи. Ограниченный выход = писатель отдельным процессом, одним TThread не
делается; это записано в коде и в доке.
Результат записи больше не игнорируется: FileWrite возвращает число байт и
при ошибке даёт 0/-1 без исключения, поэтому на полном диске файл спокойно
дописывался до конца огрызком. TCIWriteAll — цикл с проверкой каждого вызова,
плюс FileFlush перед публикацией (на ext4 с отложенным размещением ENOSPC
приходит именно там).
Файл появляется под целевым именем целиком или не появляется вовсе: данные
пишутся во временный файл рядом (эксклюзивно, не по симлинку), а публикует
их TCIPublishFile — renameat2(RENAME_NOREPLACE) напрямую через Do_SysCall,
если его нет — link + unlink, если нет и ссылок (FAT/exFAT, часть CIFS/SMB и
FUSE) — отказ с сохранением данных в .part. FileExists + rename не делается
нигде: это тот самый TOCTOU. На Windows — MoveFileW без REPLACE_EXISTING.
doc/TCI.md: §2.5 переписан (писатель-очередь, остановка, публикация); заодно
исправлено устаревшее описание склейки кусков в Take (её нет с
|
||
|
|
7aae0fdfcd |
fix(tci): SAVE отдаёт писателю сами куски записи, а не сплошную копию
Потолок 128 МБ всё ещё пробивался примерно вдвое на пике: Take собирал
линейную копию ДО освобождения FChunks, поэтому при полном бюджете рядом
жили ~128 МБ кусков и ~128 МБ копии, а счётчик показывал 128. Передача
резерва писателю (
|
||
|
|
9b77809a73 |
fix(tci,cat): резерв бюджета переезжает писателю; строгий разбор полей ZZ-команд
Три замечания по |
||
|
|
79132f133c |
fix(tci): рекордер линейного выхода — память под потолком, файл только в своём каталоге
Два дефекта уровня P1 в LINE_OUT_RECORDER_*. Авторизации в протоколе нет
(§3.1), bind наружу разрешён — значит «сколько стоит одна строка из сети»
это вопрос живучести процесса, а не аккуратности.
1. Неограниченное выделение памяти. CmdRecorder проверял только ValidRx
(номер в потолке), а конструктор выделял буфер целиком: 300 с × 48 кГц
× 2 канала × int16 = 57.6 МБ на команду, приёмников 1+MAX_SLICES=7, то
есть 403 МБ семью строками. У мёртвого приёмника Feed не зовут — значит
срок записи никто не проверял; уход клиента рекордеры не трогал вовсе;
DropDeadRxStreams бежит только на rfDevice/rfConnected и смене карты
слайсов, в покое не срабатывает. Память жила до остановки сервера.
Лечение тремя замками:
- память набирается кусками по секунде, START не стоит ни байта;
куски не перевыделяются (никакого realloc в DSP-потоке) и склеиваются
один раз в Take — уже после того, как рекордер вынут из таблицы;
- общий бюджет TCI_RECORD_MAX_BYTES (128 МБ) на все рекордеры сразу,
спрашивается на каждый кусок; отказ не рушит запись, набранное
остаётся сохраняемым;
- освобождение по трём событиям: START требует живого приёмника
(RxActive, ответ receiver is not running), тик сервера подметает
истёкшие окна по часам (SweepRecorders — окно закрывается от START
и без единого блока звука), уход клиента забирает его записи
(DropClientRecorders; рекордер живёт на приёмнике, но платит за него
тот, кто нажал START).
2. Перезапись произвольного файла. Путь из сети уходил в fmCreate почти
как пришёл — вместе с '..' и абсолютными путями. Теперь TCIRecordPath
берёт из строки ТОЛЬКО имя файла, каталог — настроенный tci.record_dir
(пусто = <каталог конфигурации>/records). Каталог из просьбы
отбрасывается молча: полный путь на сервере клиенту всё равно
бесполезен, файл ложится не на его машину. Имя валидируется (пусто,
'.', '..', управляющие, ':', длиннее 120, расширение не .wav →
bad file name); после ExtractFileName выйти за каталог нечем. Файл
создаётся эксклюзивно (TCICreateNewFile: O_EXCL|O_NOFOLLOW на Unix,
CREATE_NEW на Windows) — ни перезаписи, ни симлинка, без окна между
FileExists и созданием; клиенту заранее file exists.
Попутно: MainForm.ApplyTCISettings собирал TTCISettings по полям с
чистого листа — новое поле RecordDir обнулялось бы при каждом применении
вкладки CAT. В uses TCIStreams Windows стоит первым намеренно: иначе его
TCriticalSection перекрыл бы SyncObjs (та же грабля, что в DX-кластере).
Стенд 189/189 (было 169): 11 проверок имени файла (/etc/passwd.wav,
../../.., D|\rec\a.wav), бюджет (START не выделяет, растёт кусками,
потолок, отказ не рушит запись, срок истекает без Feed), существующий WAV
не перезаписывается, START на мёртвом приёмнике и с чужим номером, и
сквозная проверка в части E — клиент стартует запись, набирает память
живым звуком через WDSP, рвёт TCP, бюджет возвращается к нулю. Без фиксов
новые проверки краснеют. GUI (--ws=qt6) и демон зелёные.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
adbb8d02ac |
fix(tci): Stream.length — вещественные отсчёты всего блока, а не на канал
MSHV не декодировал FT4 по TCI (по виртуальному кабелю — декодировал).
Регрессия из
|
||
|
|
bb48f5d3fa |
feat(tci): приёмник = слот слайса; TRX/TUNE адресуют передатчик; стенд «как MSHV»
Модель приёмников переделана: приёмник TCI — это СЛОТ СЛАЙСА, а не панадаптер.
rx0 — главный тракт (каналы A/B = VFO A/B), rx N — слайс слота N−1, то есть
буквы B..G с флага, на каком бы пане он ни стоял. Панорама стала свойством
приёмника: от неё берутся DDS (у слайса только чтение) и поток IQ.
Почему: у Pluto панорама ровно одна (MaxPans = 1), и второй приёмник там
существует ТОЛЬКО как слайс главного пана — при нумерации по панам он был
недоступен вовсе, а TRX_COUNT навсегда равнялся единице. С другого конца —
клиенты: у MSHV в настройках всего «TCI Client rx1/rx2», то есть приёмники 0 и
1, третьего номера ввести некуда. Со слотами правило «первый созданный слайс =
приёмник 1» держится на любом железе: на openHPSDR слайс второго пана и на
Pluto слайс главного одинаково занимают слот B. Номер совпадает с буквой на
экране и с портом слайс-CAT. Цена: панорама без слайсов из TCI пропала, а
второй слайс пана перестал быть «каналом B» и стал своим приёмником — у канала
B в протоколе только частота, IF и громкость, у приёмника же всё.
TRX/TUNE раньше игнорировали arg1 (номер передатчика) целиком: клиент доп.
приёмника уводил в эфир слайс ОПЕРАТОРА — чужая частота, а с кросс-бандовым
мультислайс-TX и чужой диапазон, с чужими антенной и фильтрами; trx:9,true жал
PTT. Теперь номер разбирается и проверяется, приёмник N > 0 идёт через
RequestSliceTx (та же дверь, что у CAT-порта слайса: «в эфире только один» и
Auto TX), у контроллера появился параметр Tune для TUN тем же путём. Чужую
передачу не трогаем вовсе — ни источник модуляции, ни тон: SetMOX(True) поверх
идущей передачи не выходит рано, а заново выбирает микрофон. Хозяином эфира
клиент становится, только если передача началась именно от его команды, и
решает это Sync-метод в потоке контроллера (снимок «шла ли передача», взятый в
потоке клиента, врал: между разбором и исполнением влезает PTT оператора).
Разбор исходников MSHV (он фильтрует ВСЕ строки и бинарные блоки по номеру
приёмника) дал ещё три правки:
* ответ на TRX/TUNE адресуется номером АВТОРА, состояние в нём — «в эфире
именно твой слайс»; в рассылку идёт номер реально передающего;
* tx_enable рассылается каждому живому приёмнику со своим номером и входит в
картину нового приёмника — без этого у MSHV молча мёртвая PTT
(set_ptt начинается с `if (!tci_tx_enable) return;`);
* про несуществующий приёмник молчим целиком (LiveRx), в том числе на чтение:
ответ «vfo:1,0,0» MSHV принимал бы за конец инициализации.
Попутно, вне TCI: SendDUCSpecificFromSettings трогала FNetwork без Assigned, а
зовут её по любому PTT/TUN (SetMOX → SyncCWKeyer → она) — до подключения
устройства это была Access violation, у TCI её глотал обработчик команды.
Стенды: новый test/tci/mshv_sim.py — точная копия логики клиента MSHV, отвечает
на вопрос «почему он не подключается» одной строкой (на живом приложении
воспроизвёл ошибку инициализации для rx2 до правки). tcitest — 158/158, в
сквозном прогоне добавлено создание слайса на главном пане: он становится
приёмником 1, отвечает на vfo:1,0, слушается командой, отдаёт аудио с
receiver = 1 и замолкает после удаления.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dc6f996e70 |
fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника: NFM, DIGU, FMRAW) и его отчётом. 1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq). 2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор TX-аудио в HandleBinary. 3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO: движку сообщают SetAudioTapsActive. 5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf; TCIMicRequested снимается и по окончании любой передачи. 6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock). 7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный буфер с окном по часам от START. 8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS. Обычный TCP-разрыв без close-кадра не освобождал слот до остановки сервера, и после нескольких аварийных отключений новые клиенты упирались в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается только при R < 0. Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял внутренние колбэки контроллера — введён AttachEngineCallbacks. Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая сквозной прогон через живой WDSP), доп. проверки на оба пути отключения клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1, единицы Stream.length, линейный буфер рекордера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |