fix(tci): пара «клиент+приёмник» только по факту передачи; перебор имён .part

[P1] FTxClient/FTxRx писались ДО Invoke, и команда, которая ничего не сделала,
всё равно их перебивала. Клиент, уже передающий с приёмника 1, шлёт
trx:0,true,tci — передатчик занят, SyncSetTRX не делает ничего (Started=False),
а маркеры ИДУЩЕЙ передачи с этого мига уходят под номером 0. MSHV такие блоки
отбрасывает (network.cpp:231), то есть передача просто замолкает. Теперь пара
назначается после Invoke и только при Started — тем же признаком, по которому
назначается хозяин эфира. Тот же гейт закрывает близнеца: trx:<N>,true без
',tci' поверх своей же передачи больше не снимает источник модуляции. Снятие
(',false') работает как прежде.

[P2] Имя временного файла (pid + счётчик) уникально внутри процесса, но не
между запусками: «.part», оставшийся от прошлой жизни (публиковать было
нечем), плюс повторно выданный системой pid дают EEXIST на создании — и
задание пропадало молча. TCICreateTempNear перебирает до 64 имён, но только
пока ошибка — «имя занято»: нет прав или каталога перебором не лечится.

Стенд test/tci: 219 проверок (было 217). Новое — занятое имя «.part» записи не
теряет (стенд занимает ровно то имя, которое возьмёт писатель) и пустая
команда TRX не меняет номер приёмника в маркерах. Негативный контроль на обе
правки. Попутно в тесте поправлены два комментария, описывавшие прежнюю
реализацию публикации.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 22:02:40 +03:00
co-authored by Claude Opus 5
parent ecd42c32f8
commit 66892fc7e0
4 changed files with 143 additions and 24 deletions
+38 -3
View File
@@ -353,6 +353,11 @@ function TCIRecordPath(const BaseDir, Req: string): string;
имя приходит из сети. THandle(-1) — не вышло. }
function TCICreateNewFile(const Path: string): THandle;
{ Очередное имя временного файла рядом с целью: <путь>.<pid>-<N>.part. Каждый
вызов даёт следующее. Наружу открыто ради стенда — ему нужно занять ровно то
имя, которое возьмёт писатель. }
function TCIRecTempName(const Path: string): string;
{ Погасить писателя: закрыть приём заданий, дождаться ВСЕЙ очереди и
освободить объект (Destroy = Terminate + WaitFor). Зовут при гибели
адаптера, ради этого писатель и стал управляемым. W обнуляется в любом
@@ -972,6 +977,37 @@ begin
[Path, Integer(GetProcessID), InterLockedIncrement(RecTmpSeq)]);
end;
function TCILastErrorIsExists: Boolean;
// «Такое имя уже есть» — единственная ошибка создания, которую лечит другое имя.
begin
{$IFDEF UNIX}
Result := fpGetErrno = ESysEEXIST;
{$ELSE}
Result := (GetLastError = ERROR_ALREADY_EXISTS) or
(GetLastError = ERROR_FILE_EXISTS);
{$ENDIF}
end;
function TCICreateTempNear(const Path: string; out Tmp: string): THandle;
// Временный файл рядом с целью, с перебором имён.
// ★pid + счётчик уникальны внутри процесса, но не между запусками: «.part»,
// оставшийся от прошлой жизни (публиковать было нечем — см. TCIPublishFile),
// плюс повторно выданный системой pid дают EEXIST, и задание пропадало бы
// молча. Занятое имя — не повод терять запись, берём следующее.
var i: Integer;
begin
for i := 1 to 64 do
begin
Tmp := TCIRecTempName(Path);
Result := TCICreateNewFile(Tmp);
if Result <> THandle(-1) then Exit;
// Не «имя занято» (нет прав, нет каталога) — перебор не поможет.
if not TCILastErrorIsExists then Break;
end;
Tmp := '';
Result := THandle(-1);
end;
function TCIWriteAll(H: THandle; const Buf; Count: Integer): Boolean;
// ★Результат записи проверяем, и не «= Count», а циклом. FileWrite (как и
// THandleStream.Write под ним) возвращает ЧИСЛО записанных байт и при ошибке
@@ -1276,9 +1312,8 @@ begin
// Так клиент, увидевший файл, всегда прав: раньше по имени сначала
// появлялась пустышка на 0 байт, и на медленном диске её было видно всю
// запись, а до того — огрызок с заголовком на полную длину.
Ok := False;
Tmp := TCIRecTempName(J.Path);
HT := TCICreateNewFile(Tmp);
Ok := False;
HT := TCICreateTempNear(J.Path, Tmp);
if HT <> THandle(-1) then
begin
try