В удалённой CI-среде иногда возникают трудно воспроизводимые сбои: Xcode уже завершил сборку, но iOS Simulator продолжает находиться в состоянии Booting; либо устройство отображается как Shutdown, а команда установки всё равно возвращает ошибку подключения к службе. Перезапуск облачного Mac часто временно устраняет проблему, но при этом уничтожает данные о сбое и не объясняет, почему он возникает снова. Надёжнее отдельно проверить среду выполнения, состояние устройств и параллельное выполнение задач.
Сначала определите уровень сбоя
Проблемы с симулятором обычно относятся к одному из трёх уровней: среда выполнения Xcode недоступна, каталог устройств повреждён либо несколько задач одновременно работают с одним устройством. Сначала зафиксируйте путь к Xcode и список устройств, а не запускайте сразу erase all.
set -euo pipefail
xcode-select -p
xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices
xcrun simctl list devices unavailable
Если среда выполнения помечена как unavailable, сначала убедитесь, что выбранная версия Xcode содержит требуемый проекту iOS Runtime. Если на машине установлено несколько версий Xcode, явно задайте каталог разработчика и повторите проверку:
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcrun simctl list runtimes
Не определяйте идентификатор Runtime по имени приложения Xcode. Скрипт автоматизации должен получать фактически доступные варианты из simctl list -j и отклонять среды выполнения с isAvailable=false.
Сначала соберите данные, затем выполняйте разрушительную очистку
Если устройство зависло, сохраните список в формате JSON и последние журналы. Тогда даже после пересоздания устройства можно будет определить, что именно произошло: сбой службы CoreSimulator, завершение процесса запуска или аварийное завершение самого приложения.
mkdir -p artifacts/simulator
xcrun simctl list -j > artifacts/simulator/list.json
xcrun simctl diagnose \
> artifacts/simulator/diagnose.txt 2>&1 || true
xcrun simctl spawn booted log show \
--last 5m \
--style compact \
--predicate 'process == "SpringBoard" OR process == "launchd_sim"' \
> artifacts/simulator/boot.log 2>&1 || true
Если в данный момент нет устройства в состоянии booted, завершение последней команды с ошибкой — нормальная ситуация, но скрипт всё равно должен сохранить первые два результата. Перед архивацией журналов также необходимо удалить конфиденциальные данные, чтобы в долговременно хранящиеся CI-артефакты не попали рабочие каталоги, параметры с токенами или пути к материалам для подписи.
erase,deleteи прямое удаление каталога устройства относятся к разрушительным операциям. Если выполнить их до сбора данных, результатом обычно будет лишь вывод «после повторной попытки заработало», а не устранимая первопричина.
Создавайте отдельный набор устройств для каждой задачи
Набор устройств по умолчанию находится в пользовательском каталоге и доступен всем параллельным задачам. Если одна задача выполняет shutdown all, другая может потерять устройство прямо во время тестирования. Решение состоит не в увеличении числа повторных попыток, а в выделении отдельного каталога каждой задаче.
JOB_KEY="${CI_JOB_ID:-local}-$$"
DEVICE_SET="$PWD/.simulators/$JOB_KEY"
mkdir -p "$DEVICE_SET"
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"
RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-18-0"
UDID="$(
xcrun simctl --set "$DEVICE_SET" create \
"ci-$JOB_KEY" "$DEVICE_TYPE" "$RUNTIME_ID"
)"
xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcrun simctl --set "$DEVICE_SET" bootstatus "$UDID" -b
Тип устройства и Runtime в примере показывают только формат параметров. Фактические значения необходимо выбирать из JSON-списка, полученного на предыдущем шаге. Если создать устройство не удалось, нельзя незаметно переключаться на набор устройств по умолчанию: иначе изоляция перестанет работать именно тогда, когда она нужнее всего.
При очистке удаляйте только собственный каталог
После завершения задачи сначала остановите принадлежащие ей устройства, а затем удалите соответствующий каталог. Не запускайте глобальную команду delete all на общем исполнителе.
xcrun simctl --set "$DEVICE_SET" shutdown all || true
rm -rf "$DEVICE_SET"
Расширьте проверку запуска до уровня приложения
bootstatus -b означает только завершение загрузки системы, но не гарантирует, что тестируемое приложение можно установить и запустить. Полная дымовая проверка должна включать как минимум четыре пункта:
| Проверка | Команда или сигнал | Что сохранить при сбое |
|---|---|---|
| Загрузка устройства | bootstatus -b |
diagnose и журналы загрузки |
| Установка App | simctl install |
Путь к App и код завершения |
| Запуск App | simctl launch |
bundle identifier и вывод процесса |
| Готовность к тестам | Файл состояния или проверка работоспособности | Крайний срок и последнее состояние |
APP_PATH="$PWD/build/Sample.app"
BUNDLE_ID="com.example.Sample"
xcrun simctl --set "$DEVICE_SET" install "$UDID" "$APP_PATH"
xcrun simctl --set "$DEVICE_SET" launch \
--console-pty "$UDID" "$BUNDLE_ID"
В конвейере следует задать явный тайм-аут для этапа запуска. При его превышении сначала соберите журналы и только затем выключайте устройство. Безусловные повторные попытки с фиксированным числом запусков лишь превращают однозначную ошибку Runtime в более продолжительное ожидание.
Выстройте поэтапный порядок восстановления
Восстановление следует начинать с действий с наименьшим воздействием:
- Повторно проверьте соответствие
DEVELOPER_DIR, Runtime и типа устройства. - Выключите и снова запустите устройство, принадлежащее текущей задаче.
- Удалите сбойное устройство в том же изолированном наборе и создайте его заново.
- Удалите подтверждённо недействительные записи из
simctl list devices unavailable. - Перезапускайте службы CoreSimulator или всю машину только тогда, когда другие задачи не выполняются.
Если каждый раз приходится доходить до пятого шага, проверьте, не использует ли исполнитель общий набор устройств по умолчанию, не заканчивается ли место на диске и не остаются ли после отмены задач дочерние процессы симулятора. Для удалённых задач в RunnerVM действуют те же принципы: подтвердите доступные конфигурации в консоли и включите путь к набору устройств в жизненный цикл задачи, вместо того чтобы считать состояние симулятора постоянным ресурсом всей машины.
Сделайте сбои сопоставимыми
Наконец, записывайте версию Xcode, идентификатор Runtime, тип устройства, UDID, время запуска и этап сбоя в структурированный результат. При повторяющихся сбоях группировка по этим полям позволит увидеть, связана ли проблема с определённым Runtime, типом устройства или конкретной параллельной задачей.
Надёжное восстановление симулятора заканчивается не тогда, когда «интерфейс наконец открылся», а когда команды можно воспроизвести, приложение устанавливается и запускается, данные о сбое сохранены, а частное состояние задачи очищено. Тогда при повторном возникновении проблемы диагностику можно продолжить с уже известного этапа, а не начинать с новых догадок.
Часто задаваемые вопросы
Что делать первым: стирать или пересоздавать зависший симулятор?
Сначала сохраните список устройств и журналы. Затем создайте новое устройство в изолированном наборе, а erase применяйте только тогда, когда старое тестовое состояние больше не требуется.
Можно ли параллельным задачам CI использовать общий набор устройств?
Это создаёт риск взаимного влияния: одна задача может выключить или удалить устройство другой. Для каждой задачи лучше создавать отдельный каталог набора устройств.
Достаточно ли успешного bootstatus для запуска тестов?
Нет. После загрузки нужно установить собранное приложение, запустить его по bundle identifier и проверить код завершения каждого шага.
Запускайте следующую сборку на выделенном физическом Mac
Выберите облачный Mac-узел под длительность задачи, а затем управляйте заказами, данными для подключения и обращениями в службу поддержки через консоль.