mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): жизненный цикл, синхронизация слайсов и WebSocket по RFC
Разбор ревью ветки. Критичное — четыре отказа жизненного цикла и один пробел синхронизации. Use-after-free стора спотов: FTCIAdapter освобождается ДО FDXStore. Команда SPOT/SPOT_DELETE, пришедшая между их гибелью, обращалась к освобождённой памяти. Bind-адрес: TCIParseIPv4 стал строгим (out + Boolean, ровно четыре октета 0..255). Кривой адрес — отказ поднимать сокет, а не молчаливый INADDR_ANY: авторизации в TCI нет. В UI порт и адрес применяются по уходу фокуса и по Close, а не на каждую букву — набор «127.0.0.1» по дороге проходил через «127.0.0.» и открывал порт наружу. Остановка при висящем Synchronize: флаг Stopping (адаптер не начинает новых Invoke), прокачка CheckSynchronize в цикле ожидания Stop и запрет освобождать клиента, чей поток не вышел. Владение переделано: клиента освобождает только тик-поток (ReapClients), клиентский лишь помечает себя закрытым. Отправка больше не блокирует вызывающего: Send/Broadcast кладут строку в очередь клиента, в сокет пишет тик-поток вне общего лока, склеивая очередь в общие кадры. Медленный клиент морозил UI на таймаут отправки за каждое движение ручки VFO; теперь он просто вылетает. Слайсы: в контроллере появилось rfSliceState (нагрузка — FSliceFreqId), его шлют сами сеттеры слайса; SyncSetVfo зовёт SliceFreqChanged, как CAT. Адаптер разворачивает Id в пару (приёмник, канал) и рассылает состояние именно этого канала, а не канала 0 каждого пана. WebSocket по RFC 6455: маска обязательна, FIN/continuation собираются, RSV и незнакомые opcode рвут соединение, control-кадры ≤125 и только целиком, 64-битная длина не сворачивается в отрицательный Integer, пустой Sec-WebSocket-Key получает 400. Хвост пакета handshake больше не выбрасывается — первая команда не теряется. Клиент после исключения в разборе не остаётся висеть в массиве. Ещё: DSP и squelch доп. приёмников читаются и пишутся из TCtrlSlice (парные сеттеры сохраняли соседние поля значениями главного тракта); параметры потоков — в TTCIClient, они клиентские по спецификации; эхо под своим локом; ApplySettings возвращает результат, отказ старта виден оператору; инициализация объявляет только существующие каналы; SET_IN_FOCUS реализован через OnFocusRequest. Осознанно не сделано и записано в doc/TCI.md §3.1: AGC_GAIN для приёмников >0 (AGC-T один на тракт), цвет спота, KEYER, TX_FOOTSWITCH, арбитраж нескольких клиентов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+29
-3
@@ -844,6 +844,7 @@ type
|
||||
// отдавать клавиатуру).
|
||||
procedure TCIFormActivate(Sender: TObject);
|
||||
procedure TCIFormDeactivate(Sender: TObject);
|
||||
procedure TCIFocusRequest;
|
||||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
// Пушит активные grid-параметры (RX или TX в зависимости от FTransmitting)
|
||||
// в FSpecView и сбрасывает кэш сетки. Вызывается при смене RX↔TX и при
|
||||
@@ -1428,6 +1429,11 @@ begin
|
||||
FDXPending := False;
|
||||
FController.FSettings.SaveDXClusterSettings(FDXPendingCfg);
|
||||
end;
|
||||
// TCI гасим ПЕРЕД базой спотов: его клиенты кладут споты (SPOT/SPOT_DELETE/
|
||||
// SPOT_CLEAR) прямо в FDXStore из своих потоков, и команда, пришедшая между
|
||||
// гибелью стора и гибелью адаптера, обратилась бы к освобождённой памяти.
|
||||
// Destroy останавливает сервер и дожидается его потоков.
|
||||
FreeAndNil(FTCIAdapter);
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPans[i].DetachDXSpots;
|
||||
FDXSpotOverlay := nil;
|
||||
@@ -1456,7 +1462,7 @@ begin
|
||||
FreeAndNil(FWebAdapter); // адаптер не владеет сервером — освобождаем после Stop
|
||||
FWebServer.Free;
|
||||
FreeAndNil(FCATAdapter); // его Destroy останавливает+освобождает CAT движок/транспорты
|
||||
FreeAndNil(FTCIAdapter); // Destroy останавливает TCI-сервер и его потоки
|
||||
// (TCI освобождён выше — до базы спотов)
|
||||
// Ядро освобождаем последним — его Destroy закрывает и освобождает движки
|
||||
// (FreeEngines) и FSettings.
|
||||
FreeAndNil(FController);
|
||||
@@ -2719,7 +2725,13 @@ begin
|
||||
// InitDXCluster: споты от TCI-клиентов кладутся в тот же стор.
|
||||
FController.FSettings.LoadTCISettings(FTCICfg);
|
||||
FTCIAdapter := TTCIAdapter.Create(FController, FDXStore);
|
||||
FTCIAdapter.ApplySettings(FTCICfg);
|
||||
FTCIAdapter.OnFocusRequest := TCIFocusRequest;
|
||||
// Порт может быть занят (второй экземпляр, чужая программа) — молчать об
|
||||
// этом нельзя: галка стоит, а сервера нет.
|
||||
if not FTCIAdapter.ApplySettings(FTCICfg) then
|
||||
ShowMessage('TCI server failed to start on ' + FTCICfg.BindAddr + ':' +
|
||||
IntToStr(FTCICfg.Port) + '.' + LineEnding +
|
||||
'Port busy or address invalid — check Settings → Advanced.');
|
||||
OnActivate := TCIFormActivate;
|
||||
OnDeactivate := TCIFormDeactivate;
|
||||
|
||||
@@ -6576,7 +6588,21 @@ begin
|
||||
FTCICfg.Port := Port;
|
||||
FTCICfg.BindAddr := BindAddr;
|
||||
FController.FSettings.SaveTCISettings(FTCICfg);
|
||||
if FTCIAdapter <> nil then FTCIAdapter.ApplySettings(FTCICfg);
|
||||
if FTCIAdapter = nil then Exit;
|
||||
// Отказ (порт занят, адрес не разобран) показываем сразу: иначе оператор
|
||||
// останется с галкой «включено» и мёртвым сервером.
|
||||
if not FTCIAdapter.ApplySettings(FTCICfg) then
|
||||
ShowMessage('TCI server failed to start on ' + BindAddr + ':' +
|
||||
IntToStr(Port) + '.' + LineEnding +
|
||||
'Port busy or address invalid.');
|
||||
end;
|
||||
|
||||
// SET_IN_FOCUS от TCI-клиента: логгер просит поднять окно программы.
|
||||
// Вызывается в UI-потоке (адаптер маршалит через FController.Invoke).
|
||||
procedure TMainForm.TCIFocusRequest;
|
||||
begin
|
||||
if WindowState = wsMinimized then WindowState := wsNormal;
|
||||
BringToFront;
|
||||
end;
|
||||
|
||||
procedure TMainForm.TCIFormActivate(Sender: TObject);
|
||||
|
||||
Reference in New Issue
Block a user