mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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>
This commit is contained in:
+64
-2
@@ -136,9 +136,16 @@ TCI-клиенты ──WebSocket──► TTCIServer ──► TTCIAdapter ─
|
||||
Секция `tci` в корне `settings.json`:
|
||||
|
||||
```json
|
||||
"tci": { "enabled": false, "port": 40001, "bind_addr": "127.0.0.1" }
|
||||
"tci": { "enabled": false, "port": 40001, "bind_addr": "127.0.0.1",
|
||||
"record_dir": "" }
|
||||
```
|
||||
|
||||
`record_dir` — единственный каталог, куда TCI пишет записи линейного выхода;
|
||||
пусто = `<каталог конфигурации>/records`, каталог создаётся при первом `SAVE`.
|
||||
Поля в UI у него нет намеренно: это не рабочая настройка, а граница, и менять
|
||||
её осмысленно только руками в файле. Почему каталог вообще существует и почему
|
||||
путь из команды в него не попадает — §2.5, «Имя файла: каталог всегда наш».
|
||||
|
||||
UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server» (галка, порт,
|
||||
интерфейс): TCI — такой же канал внешнего управления трансивером, что и CAT,
|
||||
и оператор ищет его там, а не в «Advanced». В протоколе
|
||||
@@ -506,6 +513,50 @@ ExpertSDR3 давно бы не было.
|
||||
это время не читает свой сокет. MP3 не поддержан — кодера в проекте нет,
|
||||
и на `.mp3` уходит честный `tci_error`.
|
||||
|
||||
**★Память рекордера — три замка, и все три нужны.** Авторизации в протоколе
|
||||
нет (§3.1), поэтому «сколько памяти займёт одна строка из сети» — это вопрос
|
||||
не об аккуратности, а о живучести процесса. Раньше `START` выделял буфер
|
||||
целиком: 300 с × 48 кГц × 2 канала × int16 = 57.6 МБ на команду, а приёмников
|
||||
1 + `MAX_SLICES` = 7, то есть 403 МБ семью строками, и держать их можно было
|
||||
сколько угодно.
|
||||
|
||||
1. **Память набирается кусками по секунде, а не вся сразу.** `START` не стоит
|
||||
ни байта; молчащий, замьюченный или просто не звучащий приёмник не стоит
|
||||
ничего вовсе. Куски не перевыделяются (никакого `realloc` в DSP-потоке) и
|
||||
склеиваются один раз — в `Take`, а он идёт уже после того, как рекордер
|
||||
вынут из таблицы, то есть без DSP-потока на плечах.
|
||||
2. **Общий бюджет `TCI_RECORD_MAX_BYTES` (128 МБ) на все рекордеры сразу.**
|
||||
Спрашивается при выделении каждого куска. Отказ не рушит запись: набранное
|
||||
остаётся сохраняемым, просто дальше она не растёт — иначе клиент терял бы
|
||||
уже записанное из-за чужой записи на другом приёмнике.
|
||||
3. **Освобождение по трём событиям, а не по одному.** Раньше срок проверял
|
||||
только `Feed`, то есть DSP-поток — а к мёртвому приёмнику он не приходит
|
||||
никогда, и `START` на несуществующий номер оставлял память навсегда.
|
||||
Теперь: `START` требует **живого** приёмника (`RxActive`, ответ
|
||||
`receiver is not running` — как у потоков); тик сервера подметает истёкшие
|
||||
окна по часам (`SweepRecorders`, окно закрывается от `START` и без единого
|
||||
блока звука); уход клиента забирает его записи (`DropClientRecorders` —
|
||||
рекордер живёт на приёмнике, но платит за него тот, кто нажал `START`, иначе
|
||||
пары «подключился, `START`, отключился» набивали бы бюджет до потолка).
|
||||
|
||||
**★Имя файла: каталог всегда наш.** `LINE_OUT_RECORDER_SAVE` в §4.3 принимает
|
||||
«полное имя файла», и раньше оно почти без изменений уходило в `fmCreate` —
|
||||
то есть любой, кто дотянулся до порта (а bind наружу разрешён), писал файлы от
|
||||
имени EWSDR куда угодно и затирал существующие. Теперь из строки берётся
|
||||
**только имя файла** (`TCIRecordPath`), а каталог — настроенный каталог
|
||||
записей (`tci.record_dir`, пусто = `<каталог конфигурации>/records`). Полный
|
||||
путь на СЕРВЕРЕ клиенту всё равно бесполезен: файл ложится не на его машину, а
|
||||
на нашу, поэтому каталог из просьбы отбрасывается молча — клиент получает
|
||||
разумный файл, а не ошибку. Само имя проверяется: пусто, `.`, `..`,
|
||||
управляющие символы, `:`, длиннее 120 и расширение не `.wav` — отказ
|
||||
(`bad file name`). Выйти за каталог после `ExtractFileName` нечем:
|
||||
разделителей в имени уже не осталось, а `..` не проходит проверку. Файл
|
||||
создаётся **эксклюзивно** (`TCICreateNewFile`: `O_EXCL or O_NOFOLLOW` на Unix,
|
||||
`CREATE_NEW` на Windows) — существующий не перезаписывается и симлинк не
|
||||
уводит наружу, причём одним вызовом ядра, без окна между `FileExists` и
|
||||
созданием. Клиенту про уже занятое имя отвечаем `file exists` до постановки
|
||||
задачи писателю.
|
||||
|
||||
**Потолок кадра.** Приёмный буфер соединения (`WsClient.WS_BUF_SIZE`) поднят
|
||||
с 4 до 32 КБ: блок TX-аудио — это 64 байта заголовка плюс `data[16384]`, а
|
||||
кадр крупнее буфера не собирается никогда (BufLen упирается в потолок и разбор
|
||||
@@ -806,11 +857,22 @@ ExpertSDR3 давно бы не было.
|
||||
- **Рекордер и WAV:** буфер ограничен запрошенным временем и пишется с начала
|
||||
окна (переполнение отбрасывает новое, а не затирает старое), до срока запись
|
||||
есть, после срока её нет и истёкший рекордер не оживает, `Take` завершает
|
||||
запись, файл получает верные RIFF/fmt/data и длину.
|
||||
запись, файл получает верные RIFF/fmt/data и длину. ★Память: `START` не
|
||||
выделяет ни байта, бюджет растёт кусками по мере звука и возвращается по
|
||||
`Free`, потолок берётся целиком и сверх него следует отказ, при отказе запись
|
||||
не рушится, окно истекает по часам **без единого `Feed`** и отдаёт память
|
||||
само. ★Имя файла: простое имя ложится в каталог записей, а каталог из
|
||||
просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не
|
||||
выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога
|
||||
записей дают отказ; существующий файл писатель не перезаписывает.
|
||||
- **Команды на живом сервере** (настоящий `TRadioController`, WS-клиент на
|
||||
сыром сокете): отказ на несуществующий приёмник и на нечисловой аргумент,
|
||||
отказ на старт потока с незапущенного пана, подтверждение и отбраковка
|
||||
параметров, `SAVE` без записи и `SAVE` в `.mp3` отвечают ошибкой,
|
||||
`LINE_OUT_RECORDER_START` на мёртвом приёмнике и с чужим номером отвечает
|
||||
ошибкой и **не стоит памяти**, `SAVE` с чужим путём отбивается по имени;
|
||||
запись, начатая ушедшим клиентом, освобождается вместе с ним (видно насквозь
|
||||
по общему бюджету в части E);
|
||||
`TRX:0,true,tci` без аудиопотока модуляцию не берёт, а с потоком берёт,
|
||||
реально поднимает передачу и снимает её по `TRX:0,false` и по уходу клиента;
|
||||
чужой бинарный блок не рвёт соединение; без передачи маркеров `TX_CHRONO`
|
||||
|
||||
Reference in New Issue
Block a user