Files
ewsdr/DXClusterClient.pas
T
ew8bakandClaude Opus 5 17fa965aec feat(dxcluster): споты на всех панадаптерах + кнопка DX в блоке RX
Подписи спотов рисовались только на главном пане. Теперь их видит каждый пан,
в чьё окно попадает частота спота.

Оверлей — ЭКЗЕМПЛЯР НА ПАН, а не один общий: кэш полосы подписей ключуется
центром/спаном/шириной, а у панов они свои — общий оверлей пересобирался бы на
каждый пан каждый кадр, то есть ровно то, ради чего кэш и заводился. Живёт в
TPanafallPanel (AttachDXSpots/DetachDXSpots/DXOverlay, Owner=панель), база
спотов по-прежнему одна на всех. FDXSpotOverlay в MainForm стал алиасом на
оверлей пана 0 — как FSpecView для FPan.View: настройки, тема и тик затухания
идут общим циклом по FPans, и главный пан перестал быть особым случаем (иначе
тумблер DX гасил бы подписи только на нём).

★ Version стора глобальна, а окно у пана своё, поэтому в EnsureRendered на
смену версии сначала берётся снимок окна и сверяется его подпись (FNV-1a по
позывному, частоте, моде и метке времени). Совпала — ни пересборки битмапа, ни
перезаливки GL-текстуры: спот, севший на чужой диапазон, до этого пана не
доходит. Без этого цена пересборки множилась бы на число панов. Снимок берётся
один раз и переиспользуется раскладкой — стор второй раз не дёргаем.

Клик по подписи на пане N = QSY слайса ЭТОГО пана (активного, если он здесь,
иначе первого; нет ни одного — создаём в точке) с модой по комментарию
кластера и полосой пресета этой моды. Своего VFO у панов N нет, а ретюнить их
DDC под спот нельзя — увезло бы весь пан. Мода спота → режим вынесена в общую
DXSpotRadioMode для обоих путей.

Низкий пан (грид): полоса подписей и штрихи пропускаются целиком — раньше в
GL-пути полоса легла бы поверх спектра, а штрихи пошли бы снизу вверх.

Кнопка DX переехала из тулбара в блок RX левой панели, сразу после CTUN (ряд
стал четырёхколоночным: CTUN | DX | Channel | BEACON): споты — часть приёмного
вида, а не глобальная команда уровня DISCOVER/START/SETUP. Заодно снят расчёт
SafeLeft, резервировавший под неё место в шапке.

Подписи по умолчанию ВЫКЛЮЧЕНЫ (show_spots: дефолт и фолбэк чтения были True),
состояние переживает перезапуск. Клик по кнопке подливает состояние в открытый
SETUP: там своя копия конфига, и первая же правка любого поля страницы вернула
бы подписи обратно.

fix: Stop клиента кластера больше не держит главный поток. Он делал Terminate +
WaitFor, а поток в этот момент сидит в блокирующем recv и просыпается лишь по
своему кванту (до 1 с), на застрявшем send — до SO_SNDTIMEO. Всё это время
окно висело на выходе и на переподключении после правки настроек. Теперь Stop
рвёт живой сокет SockShutdown (хэндл — зеркало под тем же локом, поток снимает
его ДО close, поэтому shutdown по закрытому fd невозможен), а FormDestroy зовёт
неблокирующий RequestStop первой строкой: поток доживает параллельно с
разборкой радио, и join в конце уже никого не ждёт.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено: доп. паны
требуют подключённого радио.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:00:08 +03:00

928 lines
29 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit DXClusterClient;
{
DXClusterClient.pas — telnet-клиент DX-кластера (одно соединение).
Один рабочий поток: резолв → connect (неблокирующий + select, чтобы не висеть
минуту на мёртвом хосте) → логин позывным → чтение строк. Разобранные споты
кладутся ПРЯМО в TDXSpotStore (он потокобезопасен), лог соединения — во
внутреннее кольцо. В главный поток ничего не маршалим: UI сам замечает
изменения по Version стора и по LogVersion — как оверлеи замечают смену вида
по ключам кэша. Никаких LCL-зависимостей.
Разрыв → реконнект с backoff (RECONNECT_MIN..RECONNECT_MAX). Stop() делает
SockShutdown, что разблокирует recv в потоке (на Linux одного close мало —
см. WebUtils.SockShutdown).
Формат спота (общий для DXSpider / AR-Cluster / CC Cluster):
DX de UA3XYZ: 14025.0 DL1ABC CW 599 tnx qso 1832Z
Всё, что не начинается с 'DX de ', в стор не идёт — только в лог.
}
{$IFDEF FPC}
{$MODE Delphi}
{$LONGSTRINGS ON}
{$ENDIF}
interface
uses
Classes, SysUtils, SyncObjs, StrUtils, DateUtils,
WebUtils, // SockClose/SockShutdown/SockRecv/SockSend/SOCK_INVALID
DXSpotStore
{$IFDEF MSWINDOWS}
, Windows, WinSock2
{$ELSE}
, BaseUnix, Sockets, netdb
{$ENDIF};
const
DX_DEFAULT_HOST = 'cluster.dxfun.com';
DX_DEFAULT_PORT = 8000;
DX_CONNECT_MS = 8000; // потолок ожидания connect
DX_CONNECT_SLICE_MS = 200; // квант ожидания connect (чтобы Stop не ждал 8 с)
DX_RECV_MS = 1000; // квант recv: просыпаемся проверить Terminated/очередь
DX_RECONNECT_MIN = 5; // с
DX_RECONNECT_MAX = 60; // с
DX_LOGIN_GRACE_S = 6; // не увидели приглашение — шлём позывной сами
DX_POSTLOGIN_S = 2; // пауза после позывного перед post-login командами
DX_LOG_LINES = 300; // кольцо лога соединения
DX_SEND_RETRIES = 20; // повторов send по таймауту буфера, потом разрыв
type
TDXClusterState = (dxsOff, dxsConnecting, dxsLogin, dxsOnline, dxsRetry, dxsError);
TDXClusterClient = class
private
FThread: TThread;
FLock: TCriticalSection; // защищает конфиг, лог, очередь, состояние
FStopEvent: TEvent;
// конфиг (копия под FLock — поток читает её через GetConfig)
FHost: string;
FPort: Integer;
FLogin: string;
FPassword: string;
FPostLogin: string; // строки через LineEnding
FStore: TDXSpotStore; // не владеет
// Сокет живого соединения — зеркало под FLock. Нужен ровно для того, чтобы
// Stop() мог сделать SockShutdown и вывести поток из блокирующего recv:
// иначе выход из программы ждал бы до конца кванта recv (и до конца
// таймаута send, если кластер перестал забирать данные). Поток снимает
// зеркало ДО close, поэтому под локом хэндл гарантированно ещё открыт.
FActiveSock: TSocket;
FState: TDXClusterState;
FStatusMsg: string;
FSpotCount: Int64; // сколько спотов принято за сессию
FLog: array[0..DX_LOG_LINES-1] of string;
FLogHead: Integer; // индекс следующей записи
FLogCount: Integer;
FLogVer: Int64;
FOutQueue: TStringList; // строки на отправку в кластер
procedure SetState(St: TDXClusterState; const Msg: string);
procedure PublishSock(S: TSocket); // зеркало хэндла для Stop (см. FActiveSock)
public
constructor Create(AStore: TDXSpotStore);
destructor Destroy; override;
procedure Configure(const AHost: string; APort: Integer;
const ALogin, APassword, APostLogin: string);
procedure Start;
// Неблокирующая половина Stop: разбудить поток и порвать сокет, не дожидаясь
// его конца. Зовётся в начале выхода из программы, чтобы поток доживал
// ПАРАЛЛЕЛЬНО с разборкой радио, а join в Stop уже никого не ждал.
procedure RequestStop;
procedure Stop; // RequestStop + join
function Running: Boolean;
// Отправить сырую строку в кластер (окно списка: sh/dx, set/filter, …).
procedure SendCommand(const S: string);
// Лог соединения — снимок в порядке приёма (старые → новые).
procedure GetLog(Dst: TStrings);
function LogVersion: Int64;
procedure AddLog(const S: string);
function State: TDXClusterState;
function StateText: string;
function StatusMessage: string;
function SpotsReceived: Int64;
property Store: TDXSpotStore read FStore;
end;
// Разбор строки кластера. True — это спот (Spot заполнен).
function ParseDXSpot(const Line: string; out Spot: TDXSpot): Boolean;
function DXStateName(St: TDXClusterState): string;
implementation
type
TDXClusterThread = class(TThread)
private
FOwner: TDXClusterClient;
FSocket: TSocket;
FRxBuf: string; // хвост неполной строки
// конфиг сессии (снимок на время соединения)
FHost: string;
FPort: Integer;
FLogin: string;
FPassword: string;
FPostLogin: string;
FLoginSent: Boolean;
FPassSent: Boolean;
FPostSent: Boolean;
FLoginAt: TDateTime;
FConnectAt: TDateTime;
function Resolve(const Host: string; out Addr: LongWord): Boolean;
function ConnectSock: Boolean;
procedure CloseSock;
function SendAll(const Data: string): Boolean;
function SendLine(const S: string): Boolean;
procedure FlushOutQueue;
procedure HandleChunk(const Data: string);
procedure HandleLine(const Line: string);
procedure CheckPrompts(const Tail: string);
function Session: Boolean; // одно соединение; False — выйти совсем
protected
procedure Execute; override;
public
constructor Create(AOwner: TDXClusterClient);
end;
function DXStateName(St: TDXClusterState): string;
begin
case St of
dxsOff: Result := 'OFF';
dxsConnecting: Result := 'CONNECTING';
dxsLogin: Result := 'LOGIN';
dxsOnline: Result := 'ONLINE';
dxsRetry: Result := 'RETRY';
else Result := 'ERROR';
end;
end;
function SockWouldBlock: Boolean;
// Последняя ошибка сокета означает «данных пока нет / истёк SO_RCVTIMEO /
// прерван сигналом» — соединение живо. Всё остальное (ECONNRESET, ENOTCONN,
// EPIPE…) — разрыв: без этой проверки поток крутил бы recv в пустом цикле,
// вместо того чтобы уйти на реконнект.
var E: Integer;
begin
{$IFDEF MSWINDOWS}
E := WSAGetLastError;
Result := (E = WSAEWOULDBLOCK) or (E = WSAETIMEDOUT) or (E = WSAEINTR);
{$ELSE}
E := fpgeterrno;
Result := (E = ESysEAGAIN) or (E = ESysEWOULDBLOCK) or (E = ESysEINTR);
{$ENDIF}
end;
{ ── Разбор строки спота ──────────────────────────────────────────────────── }
function ParseDXSpot(const Line: string; out Spot: TDXSpot): Boolean;
// 'DX de <spotter>: <кГц> <позывной> <комментарий> <HHMM>Z [грид]'
var
S, Rest, FreqStr, TimeTok: string;
P, i: Integer;
KHz: Double;
FS: TFormatSettings;
begin
Result := False;
FillChar(Spot, SizeOf(Spot), 0);
Spot.Call := ''; Spot.Spotter := ''; Spot.Comment := ''; Spot.TimeUTC := '';
S := Trim(Line);
if Length(S) < 12 then Exit;
if not SameText(Copy(S, 1, 6), 'DX de ') then Exit;
Rest := Copy(S, 7, MaxInt);
P := Pos(':', Rest);
if P <= 1 then Exit;
Spot.Spotter := Trim(Copy(Rest, 1, P - 1));
Rest := Trim(Copy(Rest, P + 1, MaxInt));
// частота (кГц) — первый токен
P := Pos(' ', Rest);
if P <= 1 then Exit;
FreqStr := Copy(Rest, 1, P - 1);
Rest := TrimLeft(Copy(Rest, P + 1, MaxInt));
FS := DefaultFormatSettings;
FS.DecimalSeparator := '.';
FreqStr := StringReplace(FreqStr, ',', '.', [rfReplaceAll]);
if not TryStrToFloat(FreqStr, KHz, FS) then Exit;
if (KHz <= 0) or (KHz > 100000000.0) then Exit; // мусор/битая строка
Spot.FreqHz := KHz * 1000.0;
// позывной DX — второй токен
P := Pos(' ', Rest);
if P > 0 then
begin
Spot.Call := Copy(Rest, 1, P - 1);
Rest := TrimLeft(Copy(Rest, P + 1, MaxInt));
end
else
begin
Spot.Call := Rest;
Rest := '';
end;
if Spot.Call = '' then Exit;
// Время: последний токен вида HHMMZ. Всё до него — комментарий, всё после
// (обычно грид спотера) приклеиваем к комментарию — терять жалко.
Rest := TrimRight(Rest);
P := 0;
for i := Length(Rest) - 4 downto 1 do
if (Rest[i] in ['0'..'9']) and (Rest[i+1] in ['0'..'9']) and
(Rest[i+2] in ['0'..'9']) and (Rest[i+3] in ['0'..'9']) and
(UpCase(Rest[i+4]) = 'Z') and
((i = 1) or (Rest[i-1] = ' ')) then
begin
P := i;
Break;
end;
if P > 0 then
begin
TimeTok := Copy(Rest, P, 4);
Spot.TimeUTC := TimeTok;
Spot.Comment := Trim(Copy(Rest, 1, P - 1));
Rest := Trim(Copy(Rest, P + 5, MaxInt));
if Rest <> '' then Spot.Comment := Trim(Spot.Comment + ' ' + Rest);
end
else
Spot.Comment := Trim(Rest);
Spot.Mode := DXModeFromComment(Spot.Comment);
Spot.Stamp := Now;
Result := True;
end;
{ ── TDXClusterClient ─────────────────────────────────────────────────────── }
constructor TDXClusterClient.Create(AStore: TDXSpotStore);
{$IFDEF MSWINDOWS}
var wsa: TWSAData;
{$ENDIF}
begin
inherited Create;
FStore := AStore;
FLock := TCriticalSection.Create;
FStopEvent := TEvent.Create(nil, True, False, '');
FOutQueue := TStringList.Create;
FHost := DX_DEFAULT_HOST;
FPort := DX_DEFAULT_PORT;
FState := dxsOff;
FLogHead := 0;
FLogCount := 0;
FLogVer := 0;
FSpotCount := 0;
FActiveSock := SOCK_INVALID;
{$IFDEF MSWINDOWS}
WSAStartup($0202, wsa);
{$ENDIF}
end;
destructor TDXClusterClient.Destroy;
begin
Stop;
FOutQueue.Free;
FStopEvent.Free;
FLock.Free;
{$IFDEF MSWINDOWS}
WSACleanup;
{$ENDIF}
inherited Destroy;
end;
procedure TDXClusterClient.Configure(const AHost: string; APort: Integer;
const ALogin, APassword, APostLogin: string);
begin
FLock.Enter;
try
// Смена адреса/учётки — команды, набранные для ПРЕЖНЕГО кластера, теряют
// смысл (и ушли бы на новый). Чистим очередь.
if (FHost <> Trim(AHost)) or (FPort <> APort) or (FLogin <> Trim(ALogin)) then
FOutQueue.Clear;
FHost := Trim(AHost);
FPort := APort;
FLogin := Trim(ALogin);
FPassword := APassword;
FPostLogin := APostLogin;
finally
FLock.Leave;
end;
end;
procedure TDXClusterClient.Start;
begin
if FThread <> nil then Exit;
FLock.Enter;
try
if (FHost = '') or (FLogin = '') then
begin
FState := dxsError;
FStatusMsg := 'host or callsign not set';
Exit;
end;
finally
FLock.Leave;
end;
FStopEvent.ResetEvent;
SetState(dxsConnecting, '');
FThread := TDXClusterThread.Create(Self);
end;
procedure TDXClusterClient.RequestStop;
begin
// Всё, что не успели отправить, к следующему соединению уже неактуально.
FLock.Enter;
try
FOutQueue.Clear;
finally
FLock.Leave;
end;
if FThread = nil then Exit;
FThread.Terminate;
FStopEvent.SetEvent;
// Терминации мало: поток стоит в блокирующем recv и проснулся бы только по
// SO_RCVTIMEO (до DX_RECV_MS), а на застрявшем send — до SO_SNDTIMEO. Всё это
// время join держал бы главный поток, т.е. окно висело бы на выходе и на
// переподключении после правки настроек. SockShutdown рвёт обе стороны
// немедленно (на Linux одного close мало — см. WebUtils.SockShutdown).
// Закрывает сокет по-прежнему сам поток: закрыть чужой хэндл значило бы
// гонку с его же recv.
FLock.Enter;
try
if FActiveSock <> SOCK_INVALID then SockShutdown(FActiveSock);
finally
FLock.Leave;
end;
end;
procedure TDXClusterClient.Stop;
var T: TThread;
begin
RequestStop;
T := FThread;
if T = nil then
begin
SetState(dxsOff, '');
Exit;
end;
FThread := nil;
T.WaitFor;
T.Free;
SetState(dxsOff, '');
end;
function TDXClusterClient.Running: Boolean;
begin
Result := FThread <> nil;
end;
procedure TDXClusterClient.PublishSock(S: TSocket);
// Зеркало хэндла для Stop. Поток зовёт с валидным хэндлом сразу после connect
// и с SOCK_INVALID ПЕРЕД close — так Stop под локом видит либо живой сокет,
// либо ничего, но не закрытый.
begin
FLock.Enter;
try
FActiveSock := S;
finally
FLock.Leave;
end;
end;
procedure TDXClusterClient.SetState(St: TDXClusterState; const Msg: string);
begin
FLock.Enter;
try
FState := St;
FStatusMsg := Msg;
Inc(FLogVer); // UI перечитает и статус тоже
finally
FLock.Leave;
end;
end;
function TDXClusterClient.State: TDXClusterState;
begin
FLock.Enter;
try
Result := FState;
finally
FLock.Leave;
end;
end;
function TDXClusterClient.StateText: string;
begin
Result := DXStateName(State);
end;
function TDXClusterClient.StatusMessage: string;
begin
FLock.Enter;
try
Result := FStatusMsg;
finally
FLock.Leave;
end;
end;
function TDXClusterClient.SpotsReceived: Int64;
begin
FLock.Enter;
try
Result := FSpotCount;
finally
FLock.Leave;
end;
end;
procedure TDXClusterClient.SendCommand(const S: string);
begin
if Trim(S) = '' then Exit;
FLock.Enter;
try
FOutQueue.Add(S);
finally
FLock.Leave;
end;
end;
procedure TDXClusterClient.AddLog(const S: string);
begin
FLock.Enter;
try
FLog[FLogHead] := S;
FLogHead := (FLogHead + 1) mod DX_LOG_LINES;
if FLogCount < DX_LOG_LINES then Inc(FLogCount);
Inc(FLogVer);
finally
FLock.Leave;
end;
end;
procedure TDXClusterClient.GetLog(Dst: TStrings);
var i, Start: Integer;
begin
if Dst = nil then Exit;
FLock.Enter;
try
Start := (FLogHead - FLogCount + DX_LOG_LINES) mod DX_LOG_LINES;
for i := 0 to FLogCount - 1 do
Dst.Add(FLog[(Start + i) mod DX_LOG_LINES]);
finally
FLock.Leave;
end;
end;
function TDXClusterClient.LogVersion: Int64;
begin
FLock.Enter;
try
Result := FLogVer;
finally
FLock.Leave;
end;
end;
{ ── TDXClusterThread ─────────────────────────────────────────────────────── }
constructor TDXClusterThread.Create(AOwner: TDXClusterClient);
begin
FOwner := AOwner;
FSocket := SOCK_INVALID;
FreeOnTerminate := False;
inherited Create(False);
end;
function TDXClusterThread.Resolve(const Host: string; out Addr: LongWord): Boolean;
// Возвращает адрес в СЕТЕВОМ порядке байт (готов для sin_addr).
{$IFNDEF MSWINDOWS}
var HE: THostEntry; HA: THostAddr;
{$ELSE}
var PH: PHostEnt; L: LongWord;
{$ENDIF}
begin
Result := False;
Addr := 0;
if Host = '' then Exit;
{$IFDEF MSWINDOWS}
L := inet_addr(PChar(Host));
if L <> INADDR_NONE then // литеральный IP (уже в сетевом порядке)
begin
Addr := L;
Exit(True);
end;
PH := gethostbyname(PChar(Host));
if (PH = nil) or (PH^.h_addr_list = nil) or (PH^.h_addr_list^ = nil) then Exit;
Move(PH^.h_addr_list^^, Addr, 4); // hostent отдаёт сетевой порядок
Result := True;
{$ELSE}
// Литеральный IP: TryStrToHostAddr отдаёт ХОСТОВЫЙ порядок — разворачиваем.
if TryStrToHostAddr(Host, HA) then
begin
Addr := htonl(HA.s_addr);
Exit(True);
end;
// DNS (netdb читает /etc/resolv.conf и /etc/hosts). Здесь адрес приходит
// прямо из DNS-ответа, т.е. УЖЕ в сетевом порядке — второй раз не вертим.
if ResolveHostByName(Host, HE) then
begin
Addr := HE.Addr.s_addr;
Exit(True);
end;
{$ENDIF}
end;
function TDXClusterThread.ConnectSock: Boolean;
// Неблокирующий connect + select: мёртвый хост не держит поток минуту на
// системном таймауте TCP, и Stop() отрабатывает быстро.
var
Addr: {$IFDEF MSWINDOWS}TSockAddrIn{$ELSE}TInetSockAddr{$ENDIF};
IP: LongWord;
R, SoErr: Integer;
FDS: TFDSet;
TV: TTimeVal;
One, Waited: Integer;
{$IFDEF MSWINDOWS}ErrLen: Integer;{$ELSE}ErrLen: TSockLen;{$ENDIF}
begin
Result := False;
if not Resolve(FHost, IP) then
begin
FOwner.SetState(dxsRetry, 'cannot resolve ' + FHost);
FOwner.AddLog('*** DNS: cannot resolve ' + FHost);
Exit;
end;
{$IFDEF MSWINDOWS}
FSocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
{$ELSE}
FSocket := fpSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
{$ENDIF}
if FSocket = SOCK_INVALID then
begin
FOwner.SetState(dxsRetry, 'cannot create socket');
Exit;
end;
FillChar(Addr, SizeOf(Addr), 0);
Addr.sin_family := AF_INET;
Addr.sin_port := htons(FPort);
Addr.sin_addr.s_addr := IP;
SockSetNonBlock(FSocket, True);
{$IFDEF MSWINDOWS}
R := WinSock2.connect(FSocket, @Addr, SizeOf(Addr));
{$ELSE}
R := fpConnect(FSocket, @Addr, SizeOf(Addr));
{$ENDIF}
if R <> 0 then
begin
// ожидаемо: EINPROGRESS / WSAEWOULDBLOCK — ждём готовности на запись.
// Ждём НЕ одним select на 8 с, а квантами по DX_CONNECT_SLICE_MS: иначе
// Stop() (закрытие программы) висел бы до конца таймаута соединения.
Waited := 0;
R := 0;
while (Waited < DX_CONNECT_MS) and (not Terminated) do
begin
TV.tv_sec := DX_CONNECT_SLICE_MS div 1000;
TV.tv_usec := (DX_CONNECT_SLICE_MS mod 1000) * 1000;
{$IFDEF MSWINDOWS}
FD_ZERO(FDS);
FD_SET(FSocket, FDS);
R := select(0, nil, @FDS, nil, @TV);
{$ELSE}
fpFD_ZERO(FDS);
fpFD_SET(FSocket, FDS);
R := fpSelect(FSocket + 1, nil, @FDS, nil, @TV);
{$ENDIF}
if R <> 0 then Break; // готов или ошибка select
Inc(Waited, DX_CONNECT_SLICE_MS);
end;
if Terminated then
begin
CloseSock;
Exit;
end;
if R <= 0 then
begin
CloseSock;
FOwner.SetState(dxsRetry, 'connect timeout: ' + FHost);
FOwner.AddLog('*** no answer from ' + FHost + ':' + IntToStr(FPort));
Exit;
end;
SoErr := 0; ErrLen := SizeOf(SoErr);
{$IFDEF MSWINDOWS}
getsockopt(FSocket, SOL_SOCKET, SO_ERROR, PChar(@SoErr), ErrLen);
{$ELSE}
fpGetSockOpt(FSocket, SOL_SOCKET, SO_ERROR, @SoErr, @ErrLen);
{$ENDIF}
if SoErr <> 0 then
begin
CloseSock;
FOwner.SetState(dxsRetry, 'connection refused');
FOwner.AddLog('*** refused: ' + FHost + ':' + IntToStr(FPort));
Exit;
end;
end;
SockSetNonBlock(FSocket, False);
SockSetRcvTimeout(FSocket, DX_RECV_MS);
SockSetSndTimeout(FSocket, 5000);
One := 1;
{$IFDEF MSWINDOWS}
setsockopt(FSocket, SOL_SOCKET, SO_KEEPALIVE, PChar(@One), SizeOf(One));
{$ELSE}
fpSetSockOpt(FSocket, SOL_SOCKET, SO_KEEPALIVE, @One, SizeOf(One));
{$ENDIF}
FOwner.PublishSock(FSocket); // с этого момента Stop может рвать нас shutdown'ом
Result := True;
end;
procedure TDXClusterThread.CloseSock;
begin
if FSocket <> SOCK_INVALID then
begin
FOwner.PublishSock(SOCK_INVALID); // до close: Stop не должен увидеть мёртвый хэндл
SockShutdown(FSocket);
SockClose(FSocket);
FSocket := SOCK_INVALID;
end;
end;
function TDXClusterThread.SendAll(const Data: string): Boolean;
// TCP вправе отправить меньше запрошенного — дописываем остаток. Таймаут
// (SO_SNDTIMEO) и EINTR — не ошибка соединения, пробуем ещё, но не бесконечно.
var
Sent, N, Tries: Integer;
begin
Result := False;
if (FSocket = SOCK_INVALID) or (Data = '') then Exit;
Sent := 0;
Tries := 0;
while Sent < Length(Data) do
begin
if Terminated then Exit;
N := SockSend(FSocket, @Data[Sent + 1], Length(Data) - Sent, 0);
if N > 0 then
begin
Inc(Sent, N);
Tries := 0;
end
else
begin
if not SockWouldBlock then Exit; // настоящая ошибка сокета
Inc(Tries);
if Tries > DX_SEND_RETRIES then Exit; // не отдаёт буфер — считаем разрывом
end;
end;
Result := True;
end;
function TDXClusterThread.SendLine(const S: string): Boolean;
begin
Result := SendAll(S + #13#10);
if not Result then
FOwner.AddLog('*** send failed, dropping connection');
end;
procedure TDXClusterThread.FlushOutQueue;
var
Pending: TStringList;
i: Integer;
begin
Pending := nil;
FOwner.FLock.Enter;
try
if FOwner.FOutQueue.Count > 0 then
begin
Pending := TStringList.Create;
Pending.Assign(FOwner.FOutQueue);
FOwner.FOutQueue.Clear;
end;
finally
FOwner.FLock.Leave;
end;
if Pending = nil then Exit;
try
for i := 0 to Pending.Count - 1 do
begin
if not SendLine(Pending[i]) then Break; // сокет умер — остальное на реконнект
FOwner.AddLog('> ' + Pending[i]);
end;
finally
Pending.Free;
end;
end;
procedure TDXClusterThread.CheckPrompts(const Tail: string);
// Приглашения логина И пароля обычно приходят БЕЗ перевода строки, поэтому
// ищем их и в незавершённом хвосте буфера, а не только в целых строках.
// Флаги FLoginSent/FPassSent держат отправку однократной: хвост проверяется
// заново с каждым пришедшим куском.
var U: string;
begin
if Tail = '' then Exit;
U := LowerCase(Tail);
if not FLoginSent then
begin
if (Pos('login:', U) > 0) or (Pos('call:', U) > 0) or
(Pos('callsign', U) > 0) or (Pos('your call', U) > 0) or
(Pos('enter your', U) > 0) then
begin
if not SendLine(FLogin) then Exit;
FOwner.AddLog('> ' + FLogin);
FLoginSent := True;
FLoginAt := Now;
FOwner.SetState(dxsLogin, '');
end;
Exit;
end;
if (FPassword <> '') and (not FPassSent) and (Pos('password', U) > 0) then
begin
if not SendLine(FPassword) then Exit;
FOwner.AddLog('> ********');
FPassSent := True;
end;
end;
procedure TDXClusterThread.HandleLine(const Line: string);
var
Spot: TDXSpot;
begin
if Line = '' then Exit;
FOwner.AddLog(Line);
if ParseDXSpot(Line, Spot) then
begin
FOwner.FStore.Add(Spot);
FOwner.FLock.Enter;
try
Inc(FOwner.FSpotCount);
finally
FOwner.FLock.Leave;
end;
if FOwner.State <> dxsOnline then FOwner.SetState(dxsOnline, '');
Exit;
end;
CheckPrompts(Line);
end;
procedure TDXClusterThread.HandleChunk(const Data: string);
var
P: Integer;
Line: string;
begin
FRxBuf := FRxBuf + Data;
while True do
begin
P := 1;
while (P <= Length(FRxBuf)) and not (FRxBuf[P] in [#10, #13]) do Inc(P);
if P > Length(FRxBuf) then Break; // перевода строки ещё нет
Line := Copy(FRxBuf, 1, P - 1);
// съедаем весь конец строки (CR, LF или CRLF)
while (P <= Length(FRxBuf)) and (FRxBuf[P] in [#10, #13]) do Inc(P);
Delete(FRxBuf, 1, P - 1);
HandleLine(TrimRight(Line));
end;
// Хвост без перевода строки — возможно, это приглашение логина или пароля.
if FRxBuf <> '' then CheckPrompts(FRxBuf);
// Защита от мусора без переводов строк.
if Length(FRxBuf) > 8192 then FRxBuf := '';
end;
function TDXClusterThread.Session: Boolean;
var
Buf: array[0..4095] of Byte;
N: Integer;
Chunk: string;
PL: TStringList;
i: Integer;
begin
Result := True;
FRxBuf := '';
FLoginSent := False;
FPassSent := False;
FPostSent := False;
FConnectAt := Now;
FOwner.SetState(dxsConnecting, FHost + ':' + IntToStr(FPort));
FOwner.AddLog('*** connecting to ' + FHost + ':' + IntToStr(FPort));
if not ConnectSock then Exit;
FOwner.AddLog('*** connected');
FOwner.SetState(dxsLogin, '');
while not Terminated do
begin
N := SockRecv(FSocket, @Buf[0], SizeOf(Buf), 0);
if N > 0 then
begin
SetLength(Chunk, N);
Move(Buf[0], Chunk[1], N);
HandleChunk(Chunk);
end
else if N = 0 then
begin
FOwner.AddLog('*** connection closed by peer');
Break;
end
else if not SockWouldBlock then
begin
// Не таймаут кванта recv, а настоящая ошибка сокета — уходим на реконнект.
FOwner.AddLog('*** connection lost (socket error)');
Break;
end;
if Terminated then Break;
// Приглашения не дождались — шлём позывной сами (часть кластеров молчит).
if (not FLoginSent) and (SecondsBetween(Now, FConnectAt) >= DX_LOGIN_GRACE_S) then
begin
if not SendLine(FLogin) then Break; // сокет умер — на реконнект
FOwner.AddLog('> ' + FLogin + ' (no prompt seen)');
FLoginSent := True;
FLoginAt := Now;
end;
// Post-login команды — один раз, чуть погодя после позывного.
if FLoginSent and (not FPostSent) and
(SecondsBetween(Now, FLoginAt) >= DX_POSTLOGIN_S) then
begin
FPostSent := True;
if Trim(FPostLogin) <> '' then
begin
PL := TStringList.Create;
try
PL.Text := FPostLogin;
for i := 0 to PL.Count - 1 do
if Trim(PL[i]) <> '' then
begin
if not SendLine(Trim(PL[i])) then Break;
FOwner.AddLog('> ' + Trim(PL[i]));
end;
finally
PL.Free;
end;
end;
if FOwner.State <> dxsOnline then FOwner.SetState(dxsOnline, '');
end;
FlushOutQueue;
end;
CloseSock;
Result := not Terminated;
end;
procedure TDXClusterThread.Execute;
var
Backoff: Integer;
begin
Backoff := DX_RECONNECT_MIN;
// Снимок конфига на всю жизнь потока: Configure во время работы применяется
// следующим Start (так же ведёт себя веб-сервер при смене порта).
FOwner.FLock.Enter;
try
FHost := FOwner.FHost;
FPort := FOwner.FPort;
FLogin := FOwner.FLogin;
FPassword := FOwner.FPassword;
FPostLogin := FOwner.FPostLogin;
finally
FOwner.FLock.Leave;
end;
while not Terminated do
begin
if not Session then Break;
if Terminated then Break;
FOwner.SetState(dxsRetry, 'reconnecting in ' + IntToStr(Backoff) + ' s');
FOwner.AddLog('*** reconnecting in ' + IntToStr(Backoff) + ' s');
if FOwner.FStopEvent.WaitFor(Backoff * 1000) = wrSignaled then Break;
Backoff := Backoff * 2;
if Backoff > DX_RECONNECT_MAX then Backoff := DX_RECONNECT_MAX;
end;
CloseSock;
end;
end.