Инженерная статья

Как исправить слишком длинный путь Unix Socket в Cloud Mac CI

Как исправить слишком длинный путь Unix Socket в Cloud Mac CI

Логи CI показывают, что процесс службы запущен, однако клиент продолжает выдавать ошибки connect failed и No such file or directory, а повторные попытки не помогают. Если права доступа и состояние процесса в норме, реальная причина сбоя может скрываться в рабочем каталоге: инструмент создаёт Unix Socket внутри многоуровневого пути к репозиторию, каталога с идентификатором задания и временной директории, из-за чего итоговый абсолютный путь превышает длину, допустимую в macOS.

Такая проблема часто возникает в параллельных тестовых процессах, инструментах Ruby, службах Node.js, вспомогательных программах сборки и локальных механизмах межпроцессного взаимодействия. В сообщении об ошибке не обязательно будет прямо указано, что путь слишком длинный. Поэтому вместо увеличения числа повторных попыток нужно выяснить, где именно создаётся Socket и сколько байт занимает его путь в кодировке UTF-8.

Сначала убедитесь, что сбой вызван путём к Socket

Одновременно проверьте процессы, файлы Socket и открытые Unix-соединения. Не ориентируйтесь только на код завершения стартового скрипта: успешный запуск родительского процесса ещё не означает, что дочерний процесс, отвечающий за прослушивание, успел выполнить привязку.

pgrep -fl 'xcodebuild|swift|ruby|node'
find "${TMPDIR:-/private/tmp}" -type s -print 2>/dev/null
lsof -U -n -P | grep "$USER"

Если в логах указан путь к файлу .sock, сразу проверьте его родительский каталог. Отсутствие файла может объясняться тремя причинами: процесс прослушивания ещё не создал его, файл был удалён слишком рано либо вызов bind() завершился ошибкой уже на этапе проверки пути. Попробуйте запустить тот же инструмент с коротким путём. Если с коротким путём всё работает, а с исходным — нет, это обычно гораздо убедительнее, чем многократное изменение прав доступа.

В macOS ограничение на длину пути Unix Socket рассчитывается в байтах, а не в количестве отображаемых символов. Каталоги с кириллицей, комбинируемые символы и длинные идентификаторы заданий увеличивают число байт особенно быстро.

Измерьте длину абсолютного пути в байтах UTF-8

Не делайте выводы на основании ${#path}: обычно это выражение возвращает количество символов. Следующий скрипт сначала получает абсолютный путь, а затем вычисляет его длину в байтах с учётом кодировки файловой системы:

candidate="$PWD/.ci/runtime/worker/session/control.sock"

python3 - "$candidate" <<'PY'
import os
import sys

path = os.path.abspath(sys.argv[1])
encoded = os.fsencode(path)
print(f"bytes={len(encoded)}")
print(f"path={path}")
PY

В Darwin поле sockaddr_un.sun_path имеет ограниченный размер, поэтому не следует проектировать структуру каталогов вплотную к пределу. В качестве порога предупреждения можно использовать 90 байт, оставляя запас для идентификатора процесса, случайного суффикса и дополнительных подкаталогов, которые может добавлять инструмент. Это внутренний безопасный порог, а не официальный общий лимит для всех инструментов: некоторые фреймворки дополнительно дописывают имя файла к переданному пути.

Также зафиксируйте все компоненты проблемного пути: корневой каталог рабочей области, имя репозитория, название задания в конвейере, имя параллельного сегмента и собственный рабочий каталог инструмента. Простое сокращение имени control.sock в конце пути обычно не позволяет освободить достаточно места.

Создайте короткий корневой каталог для каждого задания

Надёжное решение — запускать задание непосредственно из короткого корневого каталога, а не добавлять символическую ссылку поверх уже существующего глубокого пути. Каждому заданию следует выделить отдельный каталог с правами 700, явно направив туда временные файлы, кеши и результаты сборки.

job_root="$(mktemp -d /private/tmp/vmown-ci.XXXXXX)"
chmod 700 "$job_root"

export TMPDIR="$job_root/tmp"
export XDG_CACHE_HOME="$job_root/cache"
export DERIVED_DATA="$job_root/dd"

mkdir -p "$TMPDIR" "$XDG_CACHE_HOME" "$DERIVED_DATA"

cleanup() {
  rm -rf -- "$job_root"
}

trap cleanup EXIT HUP INT TERM

Для сборки Xcode можно явно задать каталог DerivedData, а для SwiftPM — отдельный scratch-каталог. Это не только сокращает пути, но и предотвращает конкуренцию параллельных заданий за одну базу данных сборки.

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED_DATA" \
  build

swift build --scratch-path "$job_root/spm"

Сам репозиторий также следует клонировать в короткий путь, например $job_root/src. Если команда по-прежнему запускается из исходного глубокого каталога, одного переноса TMPDIR может быть недостаточно: некоторые инструменты формируют путь к Socket на основе абсолютного пути проекта.

Избегайте ловушек с символическими ссылками и общими каталогами

Символические ссылки подходят для быстрой проверки гипотезы, но не должны быть единственным исправлением. Некоторые программы вызывают realpath и в итоге всё равно используют длинный путь, скрытый за ссылкой. Другие записывают реальный путь в ключ кеша, из-за чего в рамках одного задания одновременно появляются короткая и длинная версии каталога.

Общий каталог /private/tmp/ci также не подходит. Параллельные задания могут создавать Socket с одинаковыми именами, а оставшийся после старого задания файл прослушивания способен заставить новое задание ошибочно считать службу готовой. Более безопасный принцип — «одно задание, один корневой каталог», при этом имена файлов Socket должны оставаться короткими и предсказуемыми.

При очистке не используйте широкие шаблоны и не удаляйте целиком общие префиксы в /private/tmp. Путь, возвращённый mktemp, следует сохранить в переменной текущего процесса и удалить из того же задания с помощью trap. Если задание было принудительно остановлено и не смогло выполнить очистку, отдельное задание уборки может удалять каталоги с учётом владельца и времени изменения, но сначала необходимо убедиться, что внутри них нет активных процессов.

Добавьте проверку в качестве предварительного барьера CI

Однократного исправления недостаточно. Проблема может вернуться, если увеличится длина имени репозитория, ветки или шаблона задания. Перед запуском тестовой службы рекомендуется проверять все предполагаемые пути к Socket и немедленно завершать задание с ошибкой при превышении внутреннего порога предупреждения.

check_socket_path() {
  python3 - "$1" <<'PY'
import os
import sys

limit = 90
path = os.path.abspath(sys.argv[1])
size = len(os.fsencode(path))

if size > limit:
    print(f"socket path exceeds guard: {size} bytes")
    raise SystemExit(1)

print(f"socket path accepted: {size} bytes")
PY
}

check_socket_path "$TMPDIR/test-worker.sock"

При приёмочной проверке охватите как минимум четыре условия: путь короче порога предупреждения; параллельные задания используют разные корневые каталоги; после ошибки или прерывания каталоги корректно удаляются; путь в логах инструмента совпадает с ожидаемым. Затем запустите два задания параллельно и убедитесь, что имена Socket не пересекаются, кеши не смешиваются, а одно задание не удаляет каталог другого.

Цель управления путями не в том, чтобы перенести все файлы во временный каталог, а в том, чтобы предоставить компонентам, использующим межпроцессное взаимодействие, короткий и приватный корневой путь с чётко определённым жизненным циклом. После внедрения этого ограничения многие кажущиеся случайными ошибки подключения превращаются в воспроизводимые ошибки конфигурации, которые можно блокировать заранее.

Часто задаваемые вопросы

Почему сокращение имени файла Socket не устраняет ошибку?

Ядро проверяет число байтов UTF-8 во всём абсолютном пути. Каталог репозитория, идентификатор задания, временная папка и созданные инструментом подкаталоги тоже учитываются.

Можно ли окончательно решить проблему символической ссылкой?

Не всегда. Некоторые инструменты раскрывают ссылку и создают Socket по длинному реальному пути. Надёжнее сразу размещать рабочий каталог под коротким корнем.

Можно ли параллельным заданиям использовать один короткий каталог?

Не следует. Для каждого задания создавайте отдельный каталог через mktemp, задавайте права 700 и удаляйте его при штатном завершении и получении сигнала остановки.

Облачный Mac с эксклюзивным доступом

Выберите выделенную физическую машину для разработки, сборки и удалённого рабочего стола

Сравните три конфигурации на Apple Silicon и при оформлении заказа выберите узел, срок аренды и дополнительные параметры хранилища.

Выбрать тариф аренды