Wenn auf demselben Cloud-Mac mehrere CI-Läufe nacheinander ausgeführt werden, sind die schwersten Fehler oft keine Compilerfehler. Stattdessen ist ein Lauf bereits beendet, doch der nächste kann nicht starten: Ein Testdienst meldet einen belegten Port, die Zahl der Simulatoren wächst stetig, Build-Verzeichnisse lassen sich nicht löschen oder mehrere gleichnamige Hintergrundprozesse schreiben gleichzeitig in dieselbe Protokolldatei. Ein Neustart behebt das Problem vorübergehend, verschleiert jedoch, welchem Lauf die Ressourcen gehören. Zuverlässiger ist es, zunächst Beweise zu sichern und anschließend die von jedem Lauf erzeugten Ressourcen klar abzugrenzen, damit sie eindeutig zugeordnet und gezielt entfernt werden können.
Zuerst den Eigentümer verbliebener Ressourcen ermitteln
Bei der Meldung Address already in use sollte nicht sofort ein globales killall ausgeführt werden. Ermitteln Sie zuerst den lauschenden Prozess und verfolgen Sie anschließend dessen Elternprozesse, um festzustellen, ob er zu einem bereits beendeten Auftrag gehört.
PORT=8080
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN
ps -o pid=,ppid=,user=,lstart=,command= -p 41872
ps -o pid=,ppid=,user=,command= -p 41801
Nachdem lsof die PID geliefert hat, sollten mindestens Benutzer, Startzeit, vollständiger Befehl und PPID geprüft werden. Ist der Elternprozess weiterhin der aktuelle Runner, kann der Port zu einem noch laufenden parallelen Auftrag gehören. Ist der Elternprozess inzwischen ein Systemprozess, bedeutet das allein ebenfalls nicht, dass der Prozess entfernt werden darf, denn Hintergrundaufgaben können sich absichtlich von ihrer ursprünglichen Sitzung lösen.
Es empfiehlt sich, für jeden Lauf eine RUN_ID zu erzeugen und sie in Prozessargumenten, Protokollpfaden oder Umgebungsvariablen zu hinterlegen. Bei der Fehlersuche lässt sich der Suchbereich mit den folgenden Befehlen eingrenzen:
RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
export XCODEVM_RUN_ID="$RUN_ID"
pgrep -afil "$RUN_ID"
Eine Bereinigung darf nur erfolgen, wenn die Ressourcenkennung zu einem nachweislich beendeten Auftrag gehört – nicht bloß, weil der Prozessname vertraut aussieht. Parallele Aufträge auf derselben Maschine können exakt dieselben Befehle ausführen.
Jedem Lauf eigene Grenzen zuweisen
Gemeinsam genutzte Verzeichnisse und feste Ports können aus einer gelegentlichen Unterbrechung einen dauerhaften Fehler machen. Build-Verzeichnisse, Ergebnispakete, temporäre Dateien und Dienstports sollten deshalb pro Lauf isoliert werden.
| Ressource | Instabile Vorgehensweise | Empfohlene Abgrenzung |
|---|---|---|
| Derived Data | Alle Aufträge verwenden das Standardverzeichnis gemeinsam | $WORK_ROOT/$RUN_ID/DerivedData |
| Testergebnisse | Festes result.xcresult |
Nach der Laufkennung benennen |
| Temporäres Verzeichnis | Direkt nach /tmp/build schreiben |
Mit mktemp -d erstellen |
| Lokaler Dienst | Alle Aufträge verwenden fest Port 8080 | Aus einem kontrollierten Portpool zuweisen |
| Simulator | Wiederholt nach Namen suchen | UDID des in diesem Lauf erstellten Geräts speichern |
Für xcodebuild lassen sich die Pfade für Build-Daten und Ergebnisse explizit festlegen:
WORK_ROOT="${CI_WORK_ROOT:-$HOME/ci-work}"
RUN_ROOT="$WORK_ROOT/$RUN_ID"
DERIVED_DATA="$RUN_ROOT/DerivedData"
RESULT_PATH="$RUN_ROOT/Test.xcresult"
mkdir -p "$RUN_ROOT"
xcodebuild test \
-scheme App \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath "$RESULT_PATH"
Zwei gleichzeitig ausgeführte Aufträge dürfen niemals dasselbe Derived Data verwenden. Selbst bei identischem Projekt und Branch können Zwischenprodukte, Indexdatenbanken und Bereinigungsvorgänge miteinander konkurrieren.
Fehler und Unterbrechungen mit einem Exit-Trap abdecken
Eine Bereinigung ausschließlich am Ende des Skripts reicht nicht aus. Build-Fehler, Zeitüberschreitungen und manuelle Abbrüche können die letzten Zeilen überspringen. Mit trap in der Shell lassen sich normale Beendigungen und gängige Unterbrechungspfade einheitlich behandeln.
set -euo pipefail
RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
RUN_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/xcodevm-${RUN_ID}.XXXXXX")"
SIM_UDID=""
SERVICE_PID=""
cleanup() {
if [[ -n "$SERVICE_PID" ]] && kill -0 "$SERVICE_PID" 2>/dev/null; then
kill -TERM "$SERVICE_PID" 2>/dev/null || true
for _ in 1 2 3 4 5; do
kill -0 "$SERVICE_PID" 2>/dev/null || break
sleep 1
done
kill -KILL "$SERVICE_PID" 2>/dev/null || true
fi
if [[ -n "$SIM_UDID" ]]; then
xcrun simctl shutdown "$SIM_UDID" 2>/dev/null || true
xcrun simctl delete "$SIM_UDID" 2>/dev/null || true
fi
rm -rf "$RUN_ROOT"
}
trap cleanup EXIT INT TERM
Gespeichert werden hier die PID und UDID, die im aktuellen Lauf erzeugt wurden. Es werden nicht sämtliche Ressourcen mit demselben Namen gesucht und gelöscht. TERM gibt einem Prozess Gelegenheit, Dateihandles freizugeben und Protokolle vollständig zu schreiben. Erst wenn er nach einer Wartezeit weiterhin läuft, wird auf KILL eskaliert.
Schäden durch wiederverwendete PIDs vermeiden
Bei lang laufenden Aufträgen kann das System eine alte PID erneut vergeben. Vor dem Beenden eines Prozesses sollte daher die Befehlszeile nochmals gelesen und auf die Kennung des aktuellen Laufs geprüft werden. Noch robuster ist es, wenn der Hintergrunddienst PID, Startzeit und eine Zusammenfassung des Befehls in $RUN_ROOT/manifest schreibt. Bei der Bereinigung werden dann alle diese Felder gemeinsam abgeglichen.
Simulatoren und Dienstmanager behandeln
Simulatoren sollten nicht ausschließlich anhand ihres Namens verwaltet werden, da gleichnamige Geräte zu anderen Aufträgen gehören können. Speichern Sie die UDID unmittelbar nach der Erstellung:
SIM_UDID="$(xcrun simctl create "ci-$RUN_ID" \
"iPhone 16" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0")"
xcrun simctl boot "$SIM_UDID"
Ist die entsprechende Runtime in der Ausführungsumgebung nicht installiert, ermitteln Sie zunächst mit xcrun simctl list runtimes die tatsächlich verfügbaren Kennungen. Die Version sollte nicht fest in gemeinsam genutzte Skripte geschrieben werden.
Taucht ein Prozess nach seiner Beendigung automatisch wieder auf, sollte geprüft werden, ob er von einem benutzerspezifischen Dienst verwaltet wird. Untersuchen Sie zunächst die Domäne des aktuellen Benutzers, statt einen Dienst unbekannter Herkunft direkt zu entfernen:
USER_ID="$(id -u)"
launchctl print "gui/$USER_ID" | grep -B 3 -A 6 "$RUN_ID"
Hat der Auftrag tatsächlich einen temporären Dienst erstellt, muss dessen Label gespeichert und beim Beenden mit demselben Label gezielt entfernt werden. Nur die Prozess-PID zu beenden, bleibt häufig wirkungslos, weil der Dienstmanager den Prozess erneut startet.
Eine reproduzierbare Abnahme-Checkliste erstellen
Eine abgeschlossene Bereinigung bedeutet noch nicht, dass das Problem gelöst ist. Führen Sie denselben Auftrag zweimal hintereinander aus und prüfen Sie nach jedem Lauf, ob die Ressourcen auf ihren Ausgangszustand zurückgekehrt sind:
- Mit
lsofbestätigen, dass der Testport nicht mehr überwacht wird. pgrep -afil "$RUN_ID"darf keine Hintergrundprozesse des aktuellen Laufs zurückgeben.- In
xcrun simctl list devicesdürfen Name und UDID des in diesem Lauf erstellten Geräts nicht mehr vorhanden sein. - Das temporäre Laufverzeichnis wurde gelöscht; zu archivierende Protokolle und Ergebnispakete wurden zuvor in ein dauerhaftes Verzeichnis kopiert.
- Der zweite Lauf verwendet eine neue Laufkennung und hängt weder von Caches noch von Diensten des ersten Laufs ab.
- Bei parallelen Aufträgen werden ausschließlich die im Manifest des aktuellen Auftrags eingetragenen Ressourcen bereinigt.
Ist der Port weiterhin belegt, sichern Sie die Ausgabe von lsof sowie zwei Ebenen von Elternprozessen, Startzeit und Befehlszeile. Entscheiden Sie anschließend, ob die Bereinigung unvollständig war oder ein legitimer paralleler Auftrag läuft. Tritt das Problem nur bei einem erzwungenen Abbruch auf, sollte vor allem geprüft werden, ob Signale an das Wrapper-Skript weitergegeben werden und ob Hintergrundprozesse eigene Sitzungen erstellt haben. Diese Prüfungen fest in die Abschlussphase des Runners zu integrieren, ist sicherer und leichter reproduzierbar als regelmäßige Scans mit anschließendem massenweisen Beenden von Prozessen.
Häufig gestellte Fragen
Soll ein Prozess sofort beendet werden, wenn er einen benötigten Port belegt?
Nein. Zuerst müssen PID, Elternprozess, Startzeit, Befehlszeile und Laufkennung geprüft werden. Gehört der Prozess eindeutig zu einem beendeten Job, sollte zunächst TERM und erst nach einem kontrollierten Timeout KILL verwendet werden.
Warum überlebt ein Unterprozess das Ende des CI-Skripts?
Ein Hintergrundprozess kann eine neue Sitzung eröffnen oder von einem Dienstmanager übernommen werden. Deshalb sollten erzeugte PIDs und Ressourcen gespeichert und über einen Trap für EXIT, INT und TERM gezielt aufgeräumt werden.
XcodeVM macOS-Cloud-Hosts
Wählen Sie einen exklusiven physischen Server passend zu Ihrer aktuellen Arbeitslast.
Prüfen Sie Arbeitsspeicher, Speicherplatz, Node und Abrechnungszeitraum, bevor Sie mit der Konfiguration fortfahren. Alle Nodes laufen 365 Tage im Jahr stabil; maßgeblich ist die in der Konsole in Echtzeit angezeigte Verfügbarkeit.