Après plusieurs exécutions CI successives sur un même Mac cloud, les incidents les plus difficiles à expliquer ne sont souvent pas des erreurs de compilation, mais des situations où « l’exécution précédente est terminée, pourtant la suivante ne démarre pas » : le service de test signale qu’un port est occupé, le nombre de simulateurs continue d’augmenter, le dossier de build ne peut pas être supprimé, voire plusieurs processus d’arrière-plan portant le même nom écrivent simultanément dans un seul journal. Redémarrer la machine rétablit temporairement la situation, mais masque le problème d’appartenance des ressources. Une approche plus fiable consiste à commencer par recueillir des éléments de diagnostic, puis à cantonner les ressources produites par chaque exécution dans un périmètre identifiable et nettoyable.
Commencer par identifier le propriétaire des ressources résiduelles
Face à Address already in use, n’exécutez pas immédiatement un killall global. Identifiez d’abord le processus à l’écoute, puis remontez sa chaîne de processus parents pour vérifier s’il appartient à une tâche déjà terminée.
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
Une fois le PID obtenu avec lsof, vérifiez au minimum l’utilisateur, l’heure de démarrage, la commande complète et le PPID. Si le processus parent est toujours le Runner actuel, le port peut appartenir à une tâche parallèle encore en cours. Si le processus parent est désormais un processus système, cela ne suffit pas non plus pour conclure qu’il peut être supprimé : une tâche d’arrière-plan peut s’être volontairement détachée de sa session d’origine.
Il est recommandé de générer un RUN_ID pour chaque exécution et de l’intégrer aux arguments des processus, aux chemins des journaux ou aux variables d’environnement. Pour cibler le diagnostic, utilisez les commandes suivantes :
RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
export XCODEVM_RUN_ID="$RUN_ID"
pgrep -afil "$RUN_ID"
Une ressource ne doit être nettoyée que si son identifiant correspond à celui d’une tâche terminée, et non parce que le nom du processus paraît familier. Des tâches parallèles exécutées sur la même machine peuvent lancer exactement les mêmes commandes.
Attribuer un périmètre indépendant à chaque exécution
Les dossiers partagés et les ports fixes transforment les interruptions ponctuelles en pannes persistantes. Les dossiers de build, les bundles de résultats, les fichiers temporaires et les ports de service doivent être isolés par exécution.
| Ressource | Pratique peu fiable | Périmètre recommandé |
|---|---|---|
| Derived Data | Toutes les tâches partagent le dossier par défaut | $WORK_ROOT/$RUN_ID/DerivedData |
| Résultats des tests | Chemin fixe result.xcresult |
Nom basé sur l’identifiant d’exécution |
| Dossier temporaire | Écriture directe dans /tmp/build |
Création avec mktemp -d |
| Service local | Port 8080 fixe pour toutes les tâches | Attribution depuis un pool de ports contrôlé |
| Simulateur | Recherche répétée par nom | Conservation de l’UDID créé pour l’exécution |
Avec xcodebuild, les chemins des données de build et des résultats peuvent être définis explicitement :
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"
Deux tâches concurrentes ne doivent jamais partager le même dossier Derived Data. Même si le projet et la branche sont identiques, les artefacts intermédiaires, les bases d’indexation et les opérations de nettoyage peuvent entrer en concurrence.
Utiliser un piège de sortie pour couvrir les échecs et les interruptions
Un nettoyage exécuté uniquement à la fin du script ne suffit pas. Un échec de compilation, un signal d’expiration ou une annulation manuelle peuvent empêcher l’exécution des dernières lignes. La commande Shell trap permet de traiter de façon uniforme les sorties normales et les interruptions courantes.
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
Le script conserve ici le PID et l’UDID créés pour l’exécution en cours, au lieu de rechercher puis de supprimer toutes les ressources portant le même nom. Le signal TERM laisse au processus le temps de libérer ses descripteurs de fichiers et de terminer l’écriture de ses journaux. Le passage à KILL ne doit intervenir que si le processus ne s’est toujours pas arrêté après le délai d’attente.
Éviter les suppressions accidentelles dues à la réutilisation des PID
Pendant une tâche de longue durée, un ancien PID peut être réattribué par le système. Avant de terminer un processus, relisez sa ligne de commande et vérifiez qu’elle contient l’identifiant d’exécution. Une solution encore plus robuste consiste à faire enregistrer par le service d’arrière-plan son PID, son heure de démarrage et un résumé de sa commande dans $RUN_ROOT/manifest, puis à vérifier simultanément tous ces champs lors du nettoyage.
Gérer les simulateurs et les gestionnaires de services
Les simulateurs ne doivent pas être gérés uniquement par leur nom, car des appareils homonymes peuvent appartenir à d’autres tâches. Enregistrez l’UDID dès la création :
SIM_UDID="$(xcrun simctl create "ci-$RUN_ID" \
"iPhone 16" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0")"
xcrun simctl boot "$SIM_UDID"
Si le Runtime correspondant n’est pas installé dans l’environnement d’exécution, commencez par obtenir les identifiants réellement disponibles avec xcrun simctl list runtimes. N’imposez pas une version fixe dans un script partagé.
Lorsqu’un processus réapparaît automatiquement après avoir été arrêté, vérifiez s’il est géré par un service au niveau utilisateur. Inspectez d’abord le domaine de l’utilisateur actuel et ne déchargez pas directement un service dont l’origine est inconnue :
USER_ID="$(id -u)"
launchctl print "gui/$USER_ID" | grep -B 3 -A 6 "$RUN_ID"
Si la tâche crée effectivement un service temporaire, conservez son label et utilisez ce même label pour le supprimer de façon ciblée dans le chemin de sortie. Supprimer uniquement le processus à partir de son PID est généralement inefficace, car le gestionnaire de services le relancera.
Mettre en place une liste de contrôle reproductible
La fin du nettoyage ne signifie pas que le problème est résolu. Exécutez deux fois de suite la même tâche et vérifiez après chaque passage que les ressources sont revenues à leur état de référence :
- Utilisez
lsofpour confirmer que le port de test n’est plus en écoute. - La commande
pgrep -afil "$RUN_ID"ne doit retourner aucun processus d’arrière-plan appartenant à l’exécution en cours. - Le nom et l’UDID créés pour cette exécution ne doivent plus apparaître dans
xcrun simctl list devices. - Le dossier temporaire de l’exécution doit être supprimé, après copie dans un dossier persistant des journaux et bundles de résultats à archiver.
- La deuxième exécution doit utiliser un nouvel identifiant et ne pas dépendre des caches ou services laissés par la première.
- En présence de tâches concurrentes, seules les ressources enregistrées dans l’inventaire de la tâche actuelle doivent être nettoyées.
Si le port reste occupé, conservez la sortie de lsof, les informations sur deux niveaux de processus parents, l’heure de démarrage et la ligne de commande, puis déterminez s’il s’agit d’un oubli de nettoyage ou d’une exécution concurrente légitime. Si le problème ne survient qu’en cas d’annulation forcée, vérifiez en priorité que le signal est transmis au script d’encapsulation et que les processus d’arrière-plan n’ont pas créé une session indépendante. Intégrer ces contrôles à la phase finale du Runner est plus sûr qu’une analyse périodique suivie d’un arrêt massif des processus, et facilite également la reproduction des incidents.
Questions fréquentes
Faut-il arrêter immédiatement le processus qui occupe un port requis ?
Non. Il faut d’abord vérifier son PID, son parent, son heure de démarrage, sa commande et l’identifiant de l’exécution. Une fois l’origine confirmée, envoyez TERM, attendez un délai contrôlé, puis utilisez KILL uniquement si nécessaire.
Pourquoi un processus enfant survit-il à la fin du script CI ?
Il peut avoir été lancé en arrière-plan, avoir créé une nouvelle session ou être passé sous le contrôle d’un gestionnaire de services. Enregistrez ses identifiants et utilisez un trap couvrant EXIT, INT et TERM.
XcodeVM Mac dans le cloud
Choisissez une machine physique dédiée adaptée à votre charge de travail
Vérifiez la mémoire, le stockage, le nœud et la période de facturation avant de passer à la configuration de la commande. Tous les nœuds fonctionnent normalement 365 jours par an ; la disponibilité réelle est celle renvoyée en temps réel par la console.