From 03afaf3083265001bb06e57f1ec62badee8566ed Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 24 Aug 2026 22:31:04 +0300 Subject: [PATCH] =?UTF-8?q?fix(serial):=20=D0=BD=D0=BE=D0=BB=D1=8C=20?= =?UTF-8?q?=E2=80=94=20=D0=B7=D0=B0=D0=BA=D0=BE=D0=BD=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B4=D0=B5=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D1=80,?= =?UTF-8?q?=20=D0=B0=20=D0=B8=D0=BC=D1=8F=20=D0=BF=D0=BE=D1=80=D1=82=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D0=B8=D0=B7=D1=83=D0=B5?= =?UTF-8?q?=D0=BC=20=D1=86=D0=B5=D0=BB=D0=B8=D0=BA=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Два замечания по обёртке, оба верные. 1. SerValid считал ноль невалидным на всех платформах. На Unix дескриптор 0 — обычный номер: если стандартный ввод закрыт (демон, запуск из службы), fpOpen отдаст именно его. Порт при этом объявлялся неоткрытым И оставался незакрытым — SerClose смотрит на ту же проверку. Относительно прежней unix-проверки `h < 0` это была регрессия. Теперь признак неудачи ровно один и на всех платформах — SER_INVALID_HANDLE; ноль, которым RTL под Windows сообщает об отказе, переводится в него внутри SerOpen (как и было). 2. SerWinDeviceName обрезала пробелы только по пути COM10+. Для ' COM3 ' и ' \\.\COM12 ' возвращалось исходное имя с пробелами, а CreateFile их не прощает: оператор, скопировавший имя с хвостовым пробелом, получал «не удалось открыть» на ровном месте. Теперь опознанное имя возвращается нормализованным; неопознанное (путь на Unix, COM3: с двоеточием) — по-прежнему без единого изменения, трогать чужой путь мы не вправе. Стенд: 52 проверки. Новая часть C2 закрывает стандартный ввод, открывает порт как дескриптор 0 и гоняет через него байты — ★негативный контроль: с прежним `and (Handle <> 0)` она падает двумя проверками. Плюс нормализация пробелов у короткого имени и у полной формы. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV --- SerialPort.pas | 28 ++++++++++++++++------- test/serial/README.md | 5 +++++ test/serial/serialtest.pas | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/SerialPort.pas b/SerialPort.pas index f647605..3279251 100644 --- a/SerialPort.pas +++ b/SerialPort.pas @@ -398,10 +398,16 @@ var S: string; i, N: Integer; begin + // ★Имя, которое не опознано как COM-порт, возвращаем БЕЗ изменений: это может + // быть путь, и обрезать у него что-либо мы не вправе. А вот опознанное имя + // отдаём нормализованным — пробелы вокруг ' COM3 ' CreateFile не прощает, и + // оператор, скопировавший имя из документации с хвостовым пробелом, получал + // бы «не удалось открыть» на ровном месте. Result := Name; S := Trim(Name); - // Уже полная форма, или это вовсе не COM-порт (например, путь на Unix). - if (Length(S) < 4) or (Copy(S, 1, 4) = '\\.\') then Exit; + if Length(S) < 4 then Exit; + // Уже полная форма — только снимаем пробелы. + if Copy(S, 1, 4) = '\\.\' then Exit(S); if UpperCase(Copy(S, 1, 3)) <> 'COM' then Exit; N := 0; for i := 4 to Length(S) do @@ -410,16 +416,22 @@ begin N := N * 10 + (Ord(S[i]) - Ord('0')); if N > 999 then Exit; end; - // COM1..COM9 открываются и коротким именем — оставляем как ввёл оператор, - // чтобы не менять поведение там, где оно и так работало. - if N >= 10 then Result := '\\.\' + S; + // COM1..COM9 открываются и коротким именем — префикс им не нужен. + if N >= 10 then + Result := '\\.\' + S + else + Result := S; end; function SerValid(Handle: TSerialHandle): Boolean; begin - // Ноль сюда попадает от RTL под Windows (там это признак неудачи) и не может - // быть настоящим портом на Unix: дескриптор 0 занят стандартным вводом. - Result := (Handle <> SER_INVALID_HANDLE) and (Handle <> 0); + // ★Ноль — ЗАКОННЫЙ дескриптор на Unix: если стандартный ввод закрыт (демон, + // запуск из службы), fpOpen отдаст именно 0, и объявлять такой порт ошибкой + // нельзя — он не только не заработает, но и не закроется, потому что + // SerClose тоже смотрит сюда. Признак неудачи ровно один и на всех + // платформах: SER_INVALID_HANDLE. Ноль, который RTL под Windows отдаёт при + // отказе, переводится в него внутри SerOpen. + Result := Handle <> SER_INVALID_HANDLE; end; function SerDefaultPortName(Index: Integer): string; diff --git a/test/serial/README.md b/test/serial/README.md index 45569df..b4ac347 100644 --- a/test/serial/README.md +++ b/test/serial/README.md @@ -12,6 +12,11 @@ test/serial/run.sh * **Открытие**: несуществующий порт даёт `SER_INVALID_HANDLE` (а не ноль и не случайное число), живой — годный хендл. +* ★**Дескриптор 0 — законный порт**: стенд закрывает стандартный ввод, после + чего порт открывается именно как `0`, и проверяет, что он считается годным и + через него идут данные. Признак неудачи ровно один — `SER_INVALID_HANDLE`. + Проверка вида «ноль = ошибка» теряла бы рабочий порт у демона и вдобавок не + закрывала дескриптор (`SerClose` смотрит на ту же проверку). * ★**Имя устройства для Windows**: `COM10` и выше `CreateFile` открывает только в форме `\\.\COM10` — короткое имя система резолвит лишь для `COM1..COM9`, а у USB-переходников номер за десяток заезжает легко. Правило чистое diff --git a/test/serial/serialtest.pas b/test/serial/serialtest.pas index 7e1dfa0..953f446 100644 --- a/test/serial/serialtest.pas +++ b/test/serial/serialtest.pas @@ -137,6 +137,13 @@ begin SerWinDeviceName('com12') = '\\.\com12', SerWinDeviceName('com12')); Check('пробелы вокруг имени не мешают', SerWinDeviceName(' COM12 ') = '\\.\COM12', SerWinDeviceName(' COM12 ')); + // ★Пробелы CreateFile не прощает и коротким именам тоже: опознанное имя + // отдаём нормализованным, а не «как ввели». + Check('пробелы снимаются и у короткого имени', + SerWinDeviceName(' COM3 ') = 'COM3', '[' + SerWinDeviceName(' COM3 ') + ']'); + Check('пробелы снимаются и у полной формы', + SerWinDeviceName(' \\.\COM12 ') = '\\.\COM12', + '[' + SerWinDeviceName(' \\.\COM12 ') + ']'); Check('уже полная форма не удваивается', SerWinDeviceName('\\.\COM12') = '\\.\COM12'); Check('путь Unix не трогаем', @@ -295,6 +302,44 @@ begin fpClose(Master); end; +// ── C2. Законный дескриптор 0 ────────────────────────────────────────────── +procedure TestFdZero; +// ★Регрессия, которую легко внести и невозможно заметить на рабочем столе: на +// Unix дескриптор 0 — обычный номер, и если стандартный ввод закрыт (демон, +// запуск из службы), порт откроется именно как 0. Проверка вида «хендл ноль = +// ошибка» тогда не только теряет рабочий порт, но и оставляет дескриптор +// незакрытым: SerClose смотрит на ту же проверку. +var + Master, H: cint; + Slave: string; + B: Byte; + N: LongInt; +begin + WriteLn('C2. Дескриптор 0 — законный порт (стандартный ввод закрыт)'); + if not OpenPty(Master, Slave) then + begin + Check('псевдотерминал создан', False); + Exit; + end; + fpClose(0); // освобождаем нулевой дескриптор + H := SerOpen(Slave); + if H <> 0 then + WriteLn(' .. дескриптор 0 занял не порт (', H, ') — проверка ослаблена') + else + Check('порт открылся как дескриптор 0', True); + Check('★нулевой дескриптор считается годным', SerValid(H), IntToStr(H)); + SerSetParams(H, 9600, 8, NoneParity, 1, []); + MasterWrite(Master, 'X'); + N := SerReadTimeout(H, B, 500); + Check('через него идут данные', (N = 1) and (B = Ord('X')), + Format('n=%d b=%d', [N, B])); + SerClose(H); + fpClose(Master); + // Возвращаем стандартный ввод на место: иначе следующий открытый файл снова + // сядет на нулевой дескриптор, и это уже будет случайностью, а не проверкой. + fpOpen('/dev/null', O_RDONLY); +end; + // ── D. Сквозной прогон: CAT поверх обёртки ───────────────────────────────── procedure TestCatThrough; // Байты на мастере → поток CATSerial → разбор в TCATEngine → ответ обратно. @@ -350,6 +395,7 @@ begin TestWinName; TestParams; TestIO; + TestFdZero; TestCatThrough; WriteLn; WriteLn(Format('Итого: %d проверок, провалено %d', [Passed + Failed, Failed]));