クラウドMacでiOS Simulatorの起動失敗を復旧する

クラウドMacでiOS Simulatorの起動失敗を復旧する

リモートCIでは、再現が難しい障害が発生することがあります。Xcodeでのビルドは完了しているのにiOS Simulatorが Booting のまま止まったり、デバイスが Shutdown と表示されている状態でインストールコマンドを実行すると、サービスへの接続エラーが返されたりするケースです。クラウドMacを再起動すれば一時的に復旧することはありますが、障害発生時の情報が失われるうえ、次に同じ問題が起きる理由も分かりません。より確実に対処するには、Runtime、デバイスの状態、ジョブの並行実行を切り分けて確認します。

まず障害がどの層にあるかを判断する

Simulatorの障害は通常、Xcode Runtimeが利用できない、デバイスディレクトリが破損している、複数のジョブが同じデバイスを同時に操作している、という三つの層に分類できます。最初にXcodeのパスとデバイス一覧を記録し、いきなり erase all を実行しないでください。

set -euo pipefail

xcode-select -p
xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices
xcrun simctl list devices unavailable

Runtimeが unavailable と表示される場合は、現在選択されているXcodeに、プロジェクトが必要とするiOS Runtimeが含まれているかを確認します。複数のXcodeがインストールされているマシンでは、開発者ディレクトリを明示的に固定してから再確認します。

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcrun simctl list runtimes

Xcodeアプリケーションの名前からRuntime識別子を推測してはいけません。自動化スクリプトでは simctl list -j から実際に利用可能な項目を取得し、isAvailable=false のRuntimeを使用しないようにします。

破壊的なクリーンアップの前に証拠を収集する

デバイスが停止した場合は、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の添付ファイルに残らないようにします。

erasedelete、デバイスディレクトリの直接削除はいずれも破壊的な操作です。証拠を収集する前に実行すると、多くの場合は「再試行したら直った」という結果しか残らず、修正可能な根本原因を特定できません。

ジョブごとに独立したデバイスセットを作成する

デフォルトのデバイスセットはユーザーディレクトリにあり、並行して動作するすべてのジョブからアクセスできます。あるジョブが 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エラーが、単に長い待ち時間へ置き換わってしまいます。

段階的な復旧手順を定める

復旧操作は、影響の小さい手順から開始します。

  1. DEVELOPER_DIR、Runtime、デバイスタイプの組み合わせが正しいかを再確認します。
  2. 現在のジョブが所有するデバイスを停止し、再起動します。
  3. 同じ独立デバイスセット内で障害のあるデバイスを削除し、再作成します。
  4. simctl list devices unavailable に含まれる、無効であることを確認済みのレコードを削除します。
  5. 他のジョブが実行されていない場合に限り、CoreSimulator関連サービスまたはマシン全体の再起動を検討します。

毎回第五段階まで進む必要がある場合は、ランナーが共有のデフォルトデバイスセットを使い続けていないか、ディスク容量が上限に近づいていないか、ジョブのキャンセル後にSimulatorの子プロセスが残っていないかを確認してください。RunnerVM上のリモートジョブでも同じ原則に従います。コンソールで現在選択可能な構成を確認したうえで、デバイスセットのパスをジョブのライフサイクルに組み込み、Simulatorの状態をマシン単位の永続リソースとして扱わないようにします。

障害を比較可能な形で記録する

最後に、Xcodeのバージョン、Runtime識別子、デバイスタイプ、UDID、起動時間、失敗した段階を構造化された結果として記録します。障害が連続する場合にこれらのフィールドで集計すれば、特定のRuntime、特定のデバイス種別、特定の並行ジョブのどこに問題が集中しているかを判断できます。

信頼できるSimulatorの復旧は、「ようやく画面が開いた」時点で完了するものではありません。コマンドを再現可能な形で実行でき、アプリをインストールして起動でき、障害の証拠がアーカイブされ、ジョブ固有の状態が回収されて初めて完了します。そうしておけば、問題が再発しても最初から推測をやり直すのではなく、既知の段階から調査を続けられます。

よくある質問

Bootingで止まった場合は最初にeraseすべきですか?

先にデバイス一覧と関連ログを保存してください。その後、独立したデバイスセットに同種の端末を再作成し、旧状態が不要な場合だけeraseを使います。

複数のCIジョブで既定のデバイスセットを共有できますか?

共有は避けるべきです。別ジョブのshutdownやdeleteが実行中の端末へ影響するため、ジョブごとに専用のデバイスセットを作成します。

bootstatusが成功すればテストを開始できますか?

それだけでは不十分です。アプリのインストール、bundle identifierによる起動、終了ステータスの確認までを起動前検査に含めます。

Runner M4

次のビルドを専用の物理Macで実行

利用期間に合わせてクラウドMacノードを選び、コンソールから注文、接続情報、サポート依頼を管理できます。

プランを選んで注文