Technischer Artikel

Zu lange Unix-Socket-Pfade in Cloud-Mac-CI beheben

Zu lange Unix-Socket-Pfade in Cloud-Mac-CI beheben

Laut CI-Protokoll wurde der Dienstprozess erfolgreich gestartet, doch der Client meldet wiederholt connect failed oder No such file or directory. Auch weitere Verbindungsversuche helfen nicht. Wenn Berechtigungen und Prozessstatus unauffällig sind, kann die eigentliche Ursache im Arbeitsverzeichnis liegen: Das Tool legt seinen Unix Socket unterhalb eines tief verschachtelten Repository-Pfads, einer Job-ID und mehrerer temporärer Verzeichnisse an. Dadurch überschreitet der endgültige absolute Pfad die unter macOS zulässige Länge.

Dieses Problem tritt häufig bei parallelen Testprozessen, Ruby-Tools, Node.js-Diensten, Build-Hilfsprogrammen und lokaler Interprozesskommunikation auf. Da die Fehlermeldung nicht unbedingt ausdrücklich auf einen zu langen Pfad hinweist, sollten nicht einfach weitere Wiederholungsversuche hinzugefügt werden. Entscheidend ist vielmehr, den tatsächlichen Speicherort des Sockets zu ermitteln und die Länge des Pfads in UTF-8-Bytes zu messen.

Zuerst den Socket-Pfad als Fehlerursache bestätigen

Prüfen Sie zunächst gleichzeitig die Prozesse, die Socket-Dateien und die geöffneten Unix-Verbindungen. Verlassen Sie sich nicht allein auf den Exit-Code des Startskripts: Ein erfolgreich beendeter Elternprozess bedeutet nicht, dass der zuständige Kindprozess seinen Socket bereits gebunden hat.

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

Wenn das Protokoll einen .sock-Pfad enthält, prüfen Sie direkt dessen übergeordnetes Verzeichnis. Eine fehlende Datei kann drei Ursachen haben: Der Listener-Prozess hat sie noch nicht erstellt, sie wurde nach dem Erstellen vorzeitig entfernt oder bind() ist bereits bei der Pfadprüfung fehlgeschlagen. Testen Sie dasselbe Tool dann mit einem kurzen Pfad. Funktioniert der kurze Pfad, während der ursprüngliche Pfad fehlschlägt, ist das in der Regel aussagekräftiger als wiederholte Änderungen an den Berechtigungen.

Unter macOS wird die Begrenzung für Unix-Socket-Pfade in Bytes berechnet, nicht anhand der auf dem Bildschirm sichtbaren Zeichen. Verzeichnisnamen mit chinesischen Zeichen, kombinierte Zeichen und lange Job-Kennungen lassen die Bytezahl besonders schnell steigen.

UTF-8-Bytezahl des absoluten Pfads messen

Ziehen Sie aus ${#path} keine direkten Schlüsse, denn dieser Ausdruck liefert normalerweise die Anzahl der Zeichen. Das folgende Skript löst den absoluten Pfad auf und berechnet anschließend seine Länge in Bytes anhand der Dateisystemkodierung:

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

Der Platz in Darwins sockaddr_un.sun_path ist begrenzt. Verzeichnisstrukturen sollten deshalb nicht bis nahe an das technische Limit ausgereizt werden. Als interne Warnschwelle eignen sich 90 Bytes, damit noch genügend Spielraum für Prozess-IDs, zufällige Suffixe und zusätzliche Unterverzeichnisse bleibt. Dieser Wert ist eine interne Sicherheitsgrenze und kein offizielles, für alle Tools identisches Limit. Manche Frameworks hängen an den übergebenen Pfad außerdem weitere Dateinamen an.

Erfassen Sie zugleich alle Bestandteile des fehlerhaften Pfads: das Stammverzeichnis des Workspace, den Repository-Namen, den Namen des Pipeline-Jobs, die Bezeichnung des parallelen Shards und das Laufzeitverzeichnis des Tools. Nur den abschließenden Namen control.sock zu kürzen, spart meist nicht genügend Platz.

Für jeden Job ein kurzes Stammverzeichnis anlegen

Eine zuverlässige Lösung besteht darin, jeden Job direkt in einem kurzen Stammverzeichnis auszuführen, statt lediglich einen symbolischen Link um die bestehende tiefe Verzeichnisstruktur zu legen. Jeder Job erhält ein eigenes Verzeichnis mit den Berechtigungen 700. Temporäre Dateien, Caches und Build-Ausgaben werden ausdrücklich dorthin umgeleitet.

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

Für Xcode-Builds lässt sich DerivedData explizit angeben, während SwiftPM ein separates Scratch-Verzeichnis verwendet. Dadurch werden nicht nur die Pfade kürzer; parallele Jobs greifen außerdem nicht mehr auf dieselbe Build-Datenbank zu.

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

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

Auch das Repository selbst sollte in einen kurzen Pfad wie $job_root/src ausgecheckt werden. Wird der Build weiterhin aus einem tief verschachtelten ursprünglichen Verzeichnis gestartet, reicht es möglicherweise nicht aus, nur TMPDIR zu verschieben. Einige Tools erzeugen ihre Sockets nämlich auf Grundlage des absoluten Projektpfads.

Fallstricke durch symbolische Links und gemeinsam genutzte Verzeichnisse vermeiden

Symbolische Links eignen sich für einen schnellen Test, sollten aber nicht als alleinige Fehlerbehebung dienen. Manche Programme rufen realpath auf und verwenden dadurch letztlich weiterhin den langen Pfad hinter dem Link. Andere schreiben den realen Pfad in Cache-Schlüssel, sodass innerhalb desselben Jobs gleichzeitig eine kurze und eine lange Verzeichnisstruktur entsteht.

Auch ein gemeinsam genutztes Verzeichnis wie /private/tmp/ci ist ungeeignet. Parallele Jobs könnten gleichnamige Sockets anlegen. Von älteren Jobs zurückgelassene Socket-Dateien können zudem dazu führen, dass ein neuer Job den Dienst irrtümlich als betriebsbereit einstuft. Sicherer ist das Prinzip „ein Stammverzeichnis pro Job“, kombiniert mit kurzen und vorhersehbaren Socket-Dateinamen.

Verwenden Sie beim Aufräumen keine weit gefassten Platzhalter und löschen Sie keine gemeinsam genutzten Verzeichnispräfixe unter /private/tmp. Das von mktemp zurückgegebene Verzeichnis sollte in einer Variablen des aktuellen Prozesses gespeichert und von demselben Job gezielt per trap entfernt werden. Wird ein Job zwangsweise beendet und kann seine Bereinigung deshalb nicht ausführen, kann ein separater Bereinigungsjob die Verzeichnisse anhand ihres Eigentümers und Änderungszeitpunkts entfernen. Zuvor muss jedoch geprüft werden, dass darin keine aktiven Prozesse mehr laufen.

Die Prüfung als vorgeschaltete Pipeline-Sperre einrichten

Eine einmalige Fehlerbehebung genügt nicht. Wenn Repository-Namen, Branch-Namen oder Job-Vorlagen länger werden, kann das Problem erneut auftreten. Prüfen Sie deshalb vor dem Start des Testdienstes alle infrage kommenden Socket-Pfade und lassen Sie die Pipeline sofort fehlschlagen, sobald die interne Warnschwelle überschritten wird.

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"

Die Abnahme sollte mindestens vier Punkte abdecken: Der Pfad liegt unterhalb der Warnschwelle; parallele Jobs verwenden unterschiedliche Stammverzeichnisse; nach Fehlern oder Abbrüchen werden die Verzeichnisse bereinigt; und der in den Tool-Protokollen verwendete Pfad entspricht dem erwarteten Pfad. Führen Sie anschließend zwei Jobs parallel aus und stellen Sie sicher, dass keine gleichnamigen Sockets, gemeinsam verwendeten Caches oder versehentlichen Löschvorgänge im Verzeichnis des jeweils anderen Jobs auftreten.

Ziel der Pfadverwaltung ist nicht, sämtliche Dateien in temporäre Verzeichnisse zu verschieben. Stattdessen sollen genau die Komponenten, die Interprozesskommunikation benötigen, einen kurzen, privaten Stammverzeichnispfad mit klar definiertem Lebenszyklus erhalten. Sobald diese Vorgabe umgesetzt ist, werden viele scheinbar zufällige Verbindungsfehler zu reproduzierbaren Konfigurationsfehlern, die sich bereits vor dem Start erkennen und blockieren lassen.

Häufig gestellte Fragen

Warum genügt ein kürzerer Socket-Dateiname nicht?

Der Kernel bewertet die UTF-8-Bytezahl des vollständigen absoluten Pfads. Repository, Auftragskennung, temporäres Verzeichnis und automatisch erzeugte Unterordner zählen ebenfalls.

Löst ein symbolischer Link das Problem dauerhaft?

Nicht zuverlässig. Manche Werkzeuge lösen den Link vor dem Erstellen des Sockets zum realen, längeren Pfad auf. Ein direkt unter einer kurzen Wurzel angelegter Arbeitsbereich ist robuster.

Dürfen parallele CI-Aufträge dasselbe kurze Verzeichnis verwenden?

Nein. Jeder Auftrag sollte mit mktemp ein privates Verzeichnis erhalten, dessen Modus auf 700 gesetzt und das bei normalem Ende sowie bei Abbruch entfernt wird.

Dedizierter Cloud-Mac

Wählen Sie für Entwicklung, Builds und Remote-Desktop einen dedizierten physischen Rechner.

Vergleichen Sie drei Apple-Silicon-Konfigurationen und wählen Sie beim Bestellen Knoten, Mietdauer und zusätzliche Speicheroptionen aus.

Mietoption auswählen