From 6aa4db3cb84d5c2b23918ec6322e89ee90afd1bd Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 20 Aug 2026 10:44:28 +0300 Subject: [PATCH] =?UTF-8?q?fix(tci):=20Stop=20=D0=BF=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=20Synchronize=20?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20accept/=D1=82=D0=B8=D0=BA-=D0=BF=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=D0=B0=D1=85,=20=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BD=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D1=81=D0=BA=D0=B8=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Шаг 4 остановки сервера намеренно ждёт клиентские потоки с прокачкой CheckSynchronize — Stop зовут из потока контроллера, и клиентский поток может как раз висеть на FController.Invoke, то есть на TThread.Synchronize к этому самому потоку. Шаги 2 и 3 при этом делали глухой WaitFor, хотя тик-поток ходит той же дорогой: ReapClients → Disconnected → HandleDisconnect → StopTxOf → Invoke. Проверка CanInvoke у адаптера от этого не спасает: она читает Stopping ДО входа в Synchronize, а FStopping поднимается в начале Stop — значит клиент, отвалившийся ровно в момент снятия галки «Enable TCI server» (или закрытия приложения), успевает проскочить в это окно. Дальше тик-поток стоит в Synchronize, главный — в FTickThread.WaitFor, и таймаута ни у того, ни у другого нет: приложение висит намертво. Общий JoinPumped: ждём Finished, прокачивая очередь (Sleep, если зовут не из главного потока), и только потом WaitFor + FreeAndNil. Finished в FPC взводится ПОСЛЕ DoTerminate, поэтому финальный WaitFor уже не может застать чужой Synchronize и не блокирует. Co-Authored-By: Claude Opus 5 --- TCIServer.pas | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/TCIServer.pas b/TCIServer.pas index 496558e..c639b7c 100644 --- a/TCIServer.pas +++ b/TCIServer.pas @@ -868,6 +868,22 @@ begin Result := True; end; +procedure JoinPumped(var T: TThread); +// Ожидание выхода потока с прокачкой очереди Synchronize — то же, что делает +// шаг 4 в Stop, но для accept- и тик-потока. Глухой WaitFor тут — взаимный +// клин: тик-поток из ReapClients зовёт Disconnected → адаптер → Invoke, а +// Invoke это TThread.Synchronize к потоку контроллера, то есть ровно к тому, +// кто сейчас ждёт в WaitFor. Проверка CanInvoke у адаптера не спасает: она +// читает Stopping ДО входа в Synchronize, и клиент, отвалившийся в момент +// остановки сервера, успевает проскочить в это окно. +begin + if T = nil then Exit; + while not T.Finished do + if GetCurrentThreadId = MainThreadID then CheckSynchronize(5) else Sleep(5); + T.WaitFor; // Finished взводится уже после DoTerminate — не блокирует + FreeAndNil(T); +end; + procedure TTCIServer.KillAll; var i: Integer; begin @@ -897,11 +913,11 @@ begin end; // Шаг 2: дожидаемся accept-потока — после него новых клиентов не появится. - if FAcceptThread <> nil then begin FAcceptThread.WaitFor; FreeAndNil(FAcceptThread); end; + JoinPumped(FAcceptThread); // Шаг 3: будим клиентские потоки, висящие в recv, и останавливаем тик. KillAll; - if FTickThread <> nil then begin FTickThread.WaitFor; FreeAndNil(FTickThread); end; + JoinPumped(FTickThread); // Шаг 4: ждём выхода клиентских потоков — БЕЗ таймаута. Прокачивая очередь // Synchronize: Stop зовёт поток контроллера (UI), а клиентский поток может