エンジニアリング記事

クラウドMac CIのUnixソケットパス長エラーを直す

クラウドMac CIのUnixソケットパス長エラーを直す

CIログではサービスプロセスが起動済みなのに、クライアントでは connect failedNo such file or directory が繰り返し発生し、再試行しても改善しないことがあります。権限とプロセスの状態に問題がなければ、実際の原因は作業ディレクトリに隠れている可能性があります。ツールが多階層のリポジトリパス、ジョブ番号、一時ディレクトリの下にUnixソケットを作成し、最終的な絶対パスがmacOSで扱える長さを超えているケースです。

この問題は、並列テストプロセス、Rubyツール、Node.jsサービス、ビルド補助プログラム、ローカルのプロセス間通信でよく発生します。エラーメッセージに「パスが長すぎる」と明示されるとは限りません。そのため、再試行回数を増やすのではなく、ソケットが実際にどこに作成されているか、パスをUTF-8でエンコードすると何バイトになるかを確認することが重要です。

まずソケットパスが原因か確認する

最初に、プロセス、ソケットファイル、開かれているUnix接続を同時に確認します。起動スクリプトの終了コードだけを見てはいけません。親プロセスが正常終了していても、待ち受けを担当する子プロセスがバインドを完了しているとは限りません。

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

ログに .sock パスが出力されている場合は、その親ディレクトリを直接確認します。ファイルが存在しない理由は3つ考えられます。待ち受けプロセスがまだ作成していない、作成後に早期削除された、またはパス検証の段階で bind() が失敗した場合です。この時点で、同じツールを短いパスで再現してみます。短いパスでは成功し、元のパスでは失敗するなら、権限を繰り返し調整するよりも原因を明確に切り分けられます。

macOSのUnixソケットパス制限は、画面に表示される文字数ではなくバイト数で決まります。日本語を含むディレクトリ名、結合文字、長いジョブ識別子があると、バイト数はより速く増加します。

絶対パスのUTF-8バイト数を計測する

${#path} だけで判断してはいけません。通常、これで得られるのは文字数です。次のスクリプトは、最初に絶対パスへ展開し、ファイルシステムのエンコーディングに従ってバイト数を計算します。

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

Darwinの sockaddr_un.sun_path が保持できる領域には限りがあるため、ディレクトリを上限ぎりぎりの長さに設計すべきではありません。ツールが追加するプロセスID、ランダムなサフィックス、サブディレクトリの余裕を確保するため、90バイトを警告ラインとして設定できます。この数値は内部的な安全基準であり、すべてのツールに共通する正式な上限ではありません。フレームワークによっては、渡されたパスの末尾にさらにファイル名を追加します。

同時に、失敗したパスの構成要素も記録します。ワークスペースのルート、リポジトリ名、パイプラインのジョブ名、並列シャード名、ツール固有の実行ディレクトリです。末尾の control.sock だけを短くしても、十分な長さを削減できないことがほとんどです。

ジョブごとに短いルートディレクトリを作成する

安定した解決策は、既存の深いディレクトリにシンボリックリンクをかぶせるのではなく、ジョブ自体を短いルートディレクトリの下で実行することです。ジョブごとに独立したディレクトリを用意し、権限を 700 に設定したうえで、一時ファイル、キャッシュ、ビルド出力の保存先を明示的にそのディレクトリへ向けます。

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

XcodeビルドではDerivedDataを明示的に指定でき、SwiftPMでは独立したscratchディレクトリを使用できます。これによりパスを短縮できるだけでなく、並列ジョブが同じビルドデータベースを奪い合うことも避けられます。

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

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

リポジトリ自体も、$job_root/src などの短いパスへチェックアウトしてください。深い元ディレクトリから実行し続ける場合、TMPDIR だけを移動しても効果がないことがあります。一部のツールは、プロジェクトの絶対パスを基にソケットを生成するためです。

シンボリックリンクと共有ディレクトリの落とし穴を避ける

シンボリックリンクは簡単な検証には適していますが、唯一の修正方法にすべきではありません。プログラムによっては realpath を呼び出し、最終的にリンク先の長いパスを使用します。また、実パスをキャッシュキーに書き込むプログラムもあり、同じジョブ内に長短2種類のディレクトリが混在する原因になります。

共有の /private/tmp/ci を使う方法も適切ではありません。並列ジョブが同名のソケットを作成する可能性があり、以前のジョブが残した待ち受けファイルによって、新しいジョブがサービスを準備済みと誤認することもあります。より安全なのは「1ジョブにつき1つのルートディレクトリ」という原則を守り、ソケットのファイル名を短く予測可能にすることです。

クリーンアップでは、広範なワイルドカードを使用したり、/private/tmp 配下の共有プレフィックス全体を削除したりしてはいけません。mktemp が返したディレクトリを現在のプロセスの変数に保存し、同じジョブの trap で正確に回収します。ジョブが強制終了され、クリーンアップを実行できなかった場合は、独立した清掃ジョブでディレクトリの所有者と更新時刻を基準に処理できます。ただし、ディレクトリ内にアクティブなプロセスが存在しないことを事前に確認する必要があります。

チェックをパイプラインの事前ゲートにする

一度修正するだけでは不十分です。リポジトリ名、ブランチ名、ジョブテンプレートが長くなると、問題が再発する可能性があります。テストサービスを起動する前に、候補となるすべてのソケットを確認し、内部の警告ラインを超えていれば即座に失敗させることを推奨します。

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"

検証では、少なくとも4項目を確認します。パスが警告ラインを下回っていること、並列ジョブのルートディレクトリが異なること、失敗時や中断後にディレクトリをクリーンアップできること、ツールのログに出力されたパスが想定どおりであることです。最後に2つのジョブを並列実行し、ソケット名の衝突、キャッシュの混在、別ジョブのディレクトリを削除する問題がないことを確認します。

パス管理の目的は、すべてのファイルを一時ディレクトリに押し込むことではありません。プロセス間通信が必要な部分に対して、短く、プライベートで、ライフサイクルが明確なルートパスを提供することです。この制約を整備すれば、ランダムに見えていた多くの接続失敗を、再現可能かつ事前に阻止できる設定エラーへ変えられます。

よくある質問

ソケットのファイル名を短くしても直らないのはなぜですか?

判定対象はファイル名ではなく、絶対パス全体のUTF-8バイト数です。リポジトリ名、ジョブID、一時領域、ツールが作る階層もすべて含まれます。

シンボリックリンクでパス長問題を解決できますか?

確実ではありません。リンクを実体パスへ解決してからソケットを作るツールがあります。短いルートの直下に作業領域を直接作る方法が安全です。

並列ジョブで短い一時ディレクトリを共有してもよいですか?

共有は避けます。ジョブごとにmktempで専用領域を作り、権限を700に設定し、正常終了時と中断時の両方で削除してください。

専用クラウドMac

開発、ビルド、リモートデスクトップに専用物理マシンを選択

3種類のApple Silicon構成を比較し、注文時にノード、利用期間、ストレージの追加オプションを選択できます。

レンタルプランを選ぶ