Article technique

Corriger les chemins de socket Unix trop longs en CI Mac

Corriger les chemins de socket Unix trop longs en CI Mac

Les journaux de CI indiquent que le processus de service a démarré, mais le client signale sans cesse connect failed ou No such file or directory, sans qu’aucune nouvelle tentative ne résolve le problème. Si les autorisations et l’état des processus sont normaux, la véritable cause peut se cacher dans le répertoire de travail : l’outil crée son socket Unix sous une arborescence profonde comprenant le chemin du dépôt, l’identifiant de la tâche et un répertoire temporaire, si bien que le chemin absolu final dépasse la longueur acceptée par macOS.

Ce problème touche fréquemment les processus de test parallèles, les outils Ruby, les services Node.js, les utilitaires de build et les communications interprocessus locales. Le message d’erreur ne précise pas nécessairement que le chemin est trop long. Plutôt que de multiplier les nouvelles tentatives, il faut déterminer où le socket est réellement créé et mesurer la longueur de son chemin après encodage en UTF-8.

Confirmer que l’échec vient du chemin du socket

Commencez par examiner simultanément les processus, les fichiers de socket et les connexions Unix ouvertes. Ne vous fiez pas uniquement au code de sortie du script de démarrage : la réussite du processus parent ne garantit pas que le processus enfant chargé de l’écoute a terminé son opération de liaison.

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

Si les journaux indiquent un chemin se terminant par .sock, inspectez directement son répertoire parent. L’absence du fichier peut avoir trois causes : le processus d’écoute ne l’a pas encore créé, le fichier a été supprimé trop tôt après sa création, ou bind() a échoué pendant la validation du chemin. Essayez alors de reproduire le comportement du même outil avec un chemin court. Si le chemin court fonctionne contrairement au chemin d’origine, ce résultat est généralement plus probant que des modifications répétées des autorisations.

Sous macOS, la limite des chemins de socket Unix est calculée en octets, et non d’après le nombre de caractères affichés à l’écran. Les répertoires contenant des caractères chinois ou des caractères combinés, ainsi que les identifiants de tâche longs, font augmenter le nombre d’octets beaucoup plus rapidement.

Mesurer le chemin absolu en octets UTF-8

Ne tirez pas de conclusion à partir de ${#path}, qui renvoie généralement un nombre de caractères. Le script suivant développe d’abord le chemin absolu, puis calcule sa longueur en octets avec l’encodage du système de fichiers :

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

Sous Darwin, l’espace disponible dans sockaddr_un.sun_path est limité. Il ne faut donc pas concevoir une arborescence qui s’approche de cette limite. En pratique, une alerte interne à 90 octets permet de conserver une marge pour les identifiants de processus, les suffixes aléatoires et les sous-répertoires ajoutés par les outils. Cette valeur constitue une marge de sécurité interne, et non une limite officielle commune à tous les outils ; certains frameworks ajoutent encore un nom de fichier au chemin fourni.

Consignez également chaque composant du chemin en échec : la racine de l’espace de travail, le nom du dépôt, le nom de la tâche du pipeline, le nom du fragment d’exécution parallèle et le répertoire d’exécution propre à l’outil. Raccourcir uniquement le composant final control.sock permet rarement de gagner assez d’espace.

Créer une racine courte pour chaque tâche

La solution fiable consiste à exécuter directement chaque tâche sous une racine courte, plutôt qu’à ajouter un lien symbolique autour de l’arborescence profonde existante. Attribuez un répertoire distinct à chaque tâche, réglez ses autorisations sur 700 et redirigez explicitement les fichiers temporaires, les caches et les sorties de build vers ce répertoire.

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

Pour les builds Xcode, transmettez explicitement le chemin de DerivedData. Pour SwiftPM, utilisez un répertoire scratch distinct. Cette organisation raccourcit les chemins tout en empêchant les tâches parallèles de se disputer la même base de données de build.

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

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

Le dépôt lui-même doit également être extrait dans un chemin court, par exemple $job_root/src. Déplacer uniquement TMPDIR ne suffit pas toujours si les commandes continuent à être exécutées depuis un répertoire d’origine très profond, car certains outils génèrent leur socket à partir du chemin absolu du projet.

Éviter les pièges des liens symboliques et des répertoires partagés

Les liens symboliques sont utiles pour effectuer une validation rapide, mais ils ne doivent pas constituer l’unique correctif. Certains programmes appellent realpath et finissent malgré tout par utiliser le chemin long situé derrière le lien. D’autres enregistrent le chemin réel dans leurs clés de cache, ce qui fait coexister des variantes longues et courtes du répertoire au sein d’une même tâche.

Le partage de /private/tmp/ci est également à éviter. Des tâches parallèles peuvent créer des sockets portant le même nom, tandis qu’un fichier de socket laissé par une ancienne tâche peut amener une nouvelle tâche à considérer à tort que le service est prêt. La règle la plus sûre consiste à attribuer une racine à chaque tâche et à conserver des noms de socket courts et prévisibles.

Lors du nettoyage, n’utilisez pas de caractères génériques trop larges et ne supprimez pas un préfixe partagé entier sous /private/tmp. Le répertoire renvoyé par mktemp doit être conservé dans une variable du processus courant, puis supprimé précisément par la même tâche au moyen de trap. Si une tâche est interrompue de force et ne peut pas effectuer son nettoyage, une tâche indépendante peut traiter les répertoires selon leur propriétaire et leur date de modification, mais elle doit d’abord vérifier qu’aucun processus actif ne les utilise.

Ajouter un contrôle préalable au pipeline

Un correctif ponctuel ne suffit pas. Le problème peut réapparaître lorsque les noms de dépôt, de branche ou les modèles de tâche s’allongent. Avant de démarrer les services de test, vérifiez tous les chemins de socket possibles et faites immédiatement échouer la tâche si l’un d’eux dépasse le seuil d’alerte interne.

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"

La validation doit couvrir au moins quatre points : les chemins restent sous le seuil d’alerte ; les tâches parallèles utilisent des répertoires racine distincts ; les répertoires sont supprimés après un échec ou une interruption ; et les chemins consignés dans les journaux des outils correspondent aux emplacements attendus. Exécutez enfin deux tâches en parallèle pour vérifier qu’elles ne réutilisent pas les mêmes noms de socket, ne partagent pas accidentellement leurs caches et ne suppriment pas les répertoires l’une de l’autre.

La gestion des chemins ne consiste pas à placer tous les fichiers dans un répertoire temporaire. Son objectif est de fournir aux composants qui ont besoin de communications interprocessus une racine courte, privée et dotée d’un cycle de vie clairement défini. Une fois cette contrainte appliquée, de nombreuses erreurs de connexion apparemment aléatoires deviennent des erreurs de configuration reproductibles que le pipeline peut bloquer en amont.

Questions fréquentes

Pourquoi raccourcir seulement le nom du socket ne suffit-il pas ?

Le noyau contrôle le nombre d’octets UTF-8 du chemin absolu complet. Le dépôt, l’identifiant de tâche, le dossier temporaire et les sous-dossiers générés sont tous comptabilisés.

Un lien symbolique règle-t-il définitivement le problème ?

Non. Certains outils résolvent le lien avant de créer le socket et retrouvent donc le chemin réel trop long. Il vaut mieux créer directement le travail sous une racine courte.

Plusieurs tâches CI peuvent-elles partager le même dossier court ?

Ce n’est pas recommandé. Chaque tâche doit obtenir un dossier privé créé avec mktemp, protégé en mode 700 et supprimé à la sortie afin d’éviter collisions et fuites de données.

Cloud Mac dédié

Choisissez un serveur physique dédié pour le développement, les builds et le bureau à distance

Comparez trois configurations Apple Silicon et choisissez le nœud, la durée de location et les options de stockage lors de la commande.

Choisir une formule de location