From 6812220d8f0a40fb0fa9498bdcd077bc092f2dbc Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Sun, 16 Aug 2026 17:05:38 +0500 Subject: [PATCH] =?UTF-8?q?=D0=95=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D0=BB=D0=BE=D0=B3=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D0=BD=D1=8F=D1=82=D0=BD=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BD=D1=8F=D1=82=D0=BE=D0=BC=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uvicorn заводил свои обработчики с propagate=False, поэтому его строки шли мимо нашего формата и рвались посреди слова. log_config=None отдаёт всё общему обработчику. Занятый порт сервис теперь замечает до запуска и объясняет, что делать, вместо строки uvicorn про Errno 10048. Co-Authored-By: Claude Opus 5 (1M context) --- app/main.py | 35 +++++++++++++++++++++++++++++++---- tests/test_console.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index faca491..bfd589f 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ import logging import os import shutil +import socket import sys import tempfile import threading @@ -386,12 +387,36 @@ def _setup_console() -> None: pass +def _port_busy(host: str, port: int) -> bool: + """Проверяет, слушает ли кто-то уже этот порт. + + Проверяем подключением, а не пробной привязкой: привязка оставляет сокет + в TIME_WAIT и может сама помешать запуску через секунду. + """ + target = "127.0.0.1" if host in ("0.0.0.0", "::", "") else host + with socket.socket() as probe: + probe.settimeout(0.5) + try: + return probe.connect_ex((target, port)) == 0 + except OSError: + return False + + def run() -> None: import uvicorn _setup_console() install_log_buffer() - colored = setup_logging(logging.INFO) + setup_logging(logging.INFO) + + if _port_busy(settings.host, settings.port): + # Uvicorn напишет про это «[Errno 10048] ... bind on address», из чего + # неочевидно, что виноват уже работающий экземпляр сервиса. + print(f"\n Порт {settings.port} уже занят - похоже, сервис уже запущен." + "\n Проверьте свёрнутые окна talkscore-asr либо найдите процесс:" + f"\n netstat -ano | findstr :{settings.port}" + "\n taskkill /PID <номер из последней колонки> /F\n") + raise SystemExit(1) if not settings.token: print("\n В config.toml пустой токен - сервис никого не пустит." @@ -414,10 +439,12 @@ def run() -> None: f"\n Потоков: {settings.effective_threads()}" f"\n Проверка: curl http://localhost:{settings.port}/health" "\n Остановить: Ctrl+C\n") - # Красим только если разбор ANSI удалось включить: иначе консоль напечатает - # сами последовательности вместо цвета. + # log_config=None отключает собственную настройку логов uvicorn. Иначе он + # заводит свои обработчики с propagate=False, его строки идут мимо нашего + # формата и рвутся посреди слова, а наши выглядят иначе - в одном окне два + # разных формата. uvicorn.run(app, host=settings.host, port=settings.port, log_level="info", - use_colors=colored) + log_config=None) if __name__ == "__main__": diff --git a/tests/test_console.py b/tests/test_console.py index 1f62a6d..0a9cc84 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -73,3 +73,41 @@ class TestSetup: assert isinstance(root.handlers[0].formatter, ConsoleFormatter) finally: root.handlers[:] = before + + +class TestPortCheck: + """Занятый порт должен диагностироваться до запуска, а не строкой Errno 10048.""" + + def test_free_port_is_not_busy(self): + import socket as sk + + from app.main import _port_busy + + with sk.socket() as probe: + probe.bind(("127.0.0.1", 0)) + free = probe.getsockname()[1] + # Сокет закрыт, порт снова свободен. + assert _port_busy("127.0.0.1", free) is False + + def test_listening_port_is_busy(self): + import socket as sk + + from app.main import _port_busy + + with sk.socket() as server: + server.bind(("127.0.0.1", 0)) + server.listen(1) + port = server.getsockname()[1] + assert _port_busy("127.0.0.1", port) is True + + def test_wildcard_host_is_probed_via_loopback(self): + """При host=0.0.0.0 подключаться к 0.0.0.0 нельзя, проверяем петлю.""" + import socket as sk + + from app.main import _port_busy + + with sk.socket() as server: + server.bind(("127.0.0.1", 0)) + server.listen(1) + port = server.getsockname()[1] + assert _port_busy("0.0.0.0", port) is True