gendesign/tradein-mvp/browser/Dockerfile
bot-backend 14f2518fc4 fix(tradein/browser): проверка шрифтов в сборке ловила угаданный порог, а не суть
Первая версия требовала >=30 файлов в fc-list и покраснела на фактических
20 — порог был взят из головы, а не измерен. Значение имеет не число
файлов (оно зависит от того, что притащили соседние пакеты), а то, что
запрос Arial отдаёт метрически совместимый шрифт. Проверяем ровно это,
по всем пяти алиасам.

Замер образа 30.08: 20 файлов, 5 семейств (Liberation Sans/Serif/Mono,
Carlito, Caladea); Arial→Liberation Sans, Times New Roman→Liberation
Serif, Courier New→Liberation Mono, Calibri→Carlito, Cambria→Caladea.
2026-08-30 01:47:42 +03:00

128 lines
9.1 KiB
Docker
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.

# tradein-browser — camoufox Playwright WS server (изолированный контейнер Firefox).
# Backend подключается к этому контейнеру через playwright.firefox.connect.
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Firefox runtime libs + ca-certificates для TLS
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
libgtk-3-0 \
libasound2 \
libdbus-glib-1-2 \
libx11-xcb1 \
libxtst6 \
ca-certificates \
fontconfig \
fonts-liberation \
fonts-crosextra-carlito \
fonts-crosextra-caladea \
&& useradd --create-home --uid 1000 app
# Замер прод-контейнера (2026-08-29): `fc-list` отдавал 8 файлов / 3 семейства
# (только DejaVu Sans/Sans Mono/Serif), а камуфокс запускается с os="windows"
# (см. browser/server.py, _launch_browser) — браузер, называющий себя Windows,
# но не знающий Arial/Times New Roman/Calibri, легко ловится перечислением
# шрифтов (стандартная анти-бот проверка). Набор шрифтов подменить нельзя,
# как navigator/UA/экран/таймзону — он меряется рендерингом текста, поэтому
# нужны настоящие файлы шрифтов в системе. Это устранение отдельной нестыковки
# фингерпринта сама по себе, безотносительно к какому-либо конкретному замеру
# отказов площадок — с текущими отказами Домклика (домен инжектируемых кук) это
# не связано и их не чинит.
#
# fonts-liberation/fonts-crosextra-carlito/fonts-crosextra-caladea — метрически
# совместимые клоны Arial/Times New Roman/Courier New (Liberation), Calibri
# (Carlito) и Cambria (Caladea): те же ширины символов на кегль, просто другой
# рисунок. Детект по перечню шрифтов чаще всего меряет именно ширину текста
# (measureText), а не байты файла шрифта — метрический клон с алиасом
# fontconfig проходит проверку «Arial есть» неотличимо от настоящего Arial.
# Алиасы (Arial→Liberation Sans, Calibri→Carlito, Cambria→Caladea) кладёт сам
# пакет fontconfig файлом 30-metric-aliases.conf — отдельно их не пишем.
#
# Настоящие шрифты Microsoft (ttf-mscorefonts-installer) сознательно НЕ ставим:
# пакет качает их с SourceForge на этапе сборки и требует интерактивного
# принятия EULA — хрупко и недетерминированно для CI-сборки образа.
# Проверка сборки намеренно смотрит НЕ на число файлов, а на алиасы: счёт файлов
# зависит от того, что притащили соседние пакеты (первая версия этой проверки
# требовала >=30 файлов и покраснела на фактических 20 — порог был угадан, а не
# измерен). Значение имеет ровно одно: спрашиваешь Arial — получаешь метрически
# совместимый шрифт, а не «шрифта нет». Замер образа 2026-08-30: 20 файлов,
# 5 семейств (Liberation Sans/Serif/Mono, Carlito, Caladea).
RUN fc-cache -f \
&& echo "tradein-browser: fc-list = $(fc-list | wc -l) файлов, семейств: $(fc-list : family | tr , '\n' | sort -u | wc -l)" \
&& fc-match Arial | grep -qi "Liberation Sans" \
&& fc-match "Times New Roman" | grep -qi "Liberation Serif" \
&& fc-match "Courier New" | grep -qi "Liberation Mono" \
&& fc-match Calibri | grep -qi "Carlito" \
&& fc-match Cambria | grep -qi "Caladea" \
&& echo "tradein-browser: метрические алиасы Arial/TNR/Courier/Calibri/Cambria на месте"
WORKDIR /app
# Пины, а не плавающие версии: единственный лок этого сайдкара — вот эти строки.
# Без них любая пересборка образа (в том числе автоматическая, на не связанном
# коммите) втягивала свежайший camoufox — а вместе с ним другой playwright, под
# который написан sed-патч coreBundle.js ниже. Версии сняты с работающего
# прод-контейнера tradein-browser (2026-08-29): camoufox 0.5.5, playwright
# 1.60.0, aiohttp 3.14.3. playwright запинен явно, хотя и приходит транзитивно
# (camoufox 0.5.5 требует playwright<1.61) — патч завязан на конкретную сборку
# драйвера, диапазон тут не годится.
RUN pip install --no-cache-dir \
"camoufox[geoip]==0.5.5" \
"playwright==1.60.0" \
"aiohttp==3.14.3"
# ── #915/#918: null-guard + ВАЛИДНЫЕ дефолты для pageError.location (playwright 1.60.0) ─
# Firefox/juggler-бэкенд иногда отдаёт uncaught JS-ошибку страницы (реклама /
# cross-origin скрипты / anti-bot challenge на avito) БЕЗ поля location.
# playwright при доставке pageError-события собирает
# { url: pageError.location.url, line: ...lineNumber, column: ...columnNumber }
# (coreBundle.js, 2 места: BrowserContextDispatcher + trace event).
#
# Двухслойный баг (оба рвут protocol-соединение → "Connection closed while reading
# from the driver" / TargetClosedError → server.py 500 + relaunch браузера):
# 1. без guard → `pageError.location.url` бросает
# `TypeError: Cannot read properties of undefined (reading 'url')`;
# 2. ОДНОГО optional-chaining (`?.url`) НЕДОСТАТОЧНО (это была дыра #918):
# url станет undefined, а protocol-валидатор `tString` требует string →
# `ValidationError: location.url: expected string, got undefined`.
# Поэтому даём валидные дефолты: url → '', line/column → 0 — проходят валидатор,
# событие доставляется без краша. Профит: browser возвращает HTML challenge-
# страницы вместо краша → pipeline видит firewall и штатно ротирует IP, а не
# уходит в crash-storm (подтверждено прод-дебагом локального стека 2026-05-31).
#
# Апгрейд playwright не заблокирован, но связан: camoufox тянет playwright сам
# (0.5.5 → playwright<1.61), поэтому поднимать его можно только вместе с
# camoufox и только проверив, что паттерны sed ниже ещё есть в coreBundle.js.
# Сборка падает (exit 1), если паттерн не найден — это и есть сигнал, что bump
# сломал патч.
RUN PW_BUNDLE="$(python -c 'import os,playwright;print(os.path.join(os.path.dirname(playwright.__file__),"driver","package","lib","coreBundle.js"))')" \
&& sed -i -E "s/pageError\.location\.url,/pageError.location?.url ?? '',/g" "$PW_BUNDLE" \
&& sed -i -E 's/pageError\.location\.lineNumber,/pageError.location?.lineNumber ?? 0,/g' "$PW_BUNDLE" \
&& sed -i -E 's/pageError\.location\.columnNumber/pageError.location?.columnNumber ?? 0/g' "$PW_BUNDLE" \
&& N="$(grep -cF "pageError.location?.url ?? ''" "$PW_BUNDLE")" \
&& echo "tradein-browser: patched pageError.location?.url ?? '' sites: $N" \
&& [ "$N" -ge 2 ]
COPY server.py ./server.py
# camoufox fetch качает (а) Firefox-сборку и (б) GeoLite2 mmdb (geoip=True).
# Firefox идёт в $HOME/.cache → ENV HOME=/home/app кладёт его в кэш app-юзера,
# куда рантайм его ищет (фикс #899). НО mmdb пишется в site-packages пакета
# camoufox (root-owned после pip install) — под app это PermissionError на
# GeoLite2-City.mmdb. Поэтому fetch выполняем под ROOT (пишет и mmdb, и —
# благодаря ENV HOME — Firefox в /home/app/.cache), затем chown'им Firefox-кэш
# на app. mmdb остаётся root-owned в site-packages (world-readable → app читает
# на рантайме). Verified реальной сборкой образа 2026-05-31.
ENV HOME=/home/app
RUN python -m camoufox fetch && chown -R app:app /home/app/.cache
USER app
EXPOSE 3000
CMD ["python", "server.py"]