Unity Package Manager の一括インストールが Domain Reload で止まる:SessionState で再開する
2件以上選んでも1件目しか入らない。原因は Domain Reload で待ち行列が消えること。EditorWindow のシリアライズでは足りず、Service 側の SessionState へ移すまでの経緯。
何に困っていたか
MimiCatHub には「パッケージ管理」Window があり、追加パッケージを 1 件ずつインストール・更新・削除できた。別プロジェクトへ持ち込むときは、Core を入れてから十数個の追加パッケージを順に入れる。しかも順番を人間が覚えていなければならない。Localization は先に Addressables、ChatCommand は先に YouTube、Steam は Steamworks.NET を manifest.json へ手で追記してから、という具合である。
これをチェックボックスで選んで一括インストールできるようにした。依存はカタログのデータ(DependencyPackageIds と ExternalDependencies)として持ち、McPackageManagerInstallPlanner が再帰で依存を先に並べた手順列を作る。
visit(id):
if 訪問済み or 導入済み: return
訪問済みに追加
for dep in entry.DependencyPackageIds: visit(dep)
for ext in entry.ExternalDependencies:
if 未導入かつ未追加: 手順「外部依存の取得」を追加
手順「インストール」を追加
Unity の Package Manager は同時に 1 要求しか扱えないので、手順は直列に実行し、各手順のあとに状態を再取得してから次へ進む。
何が起きたか
1 つ目:自分で自分を止めた。 操作完了後の状態再取得を公開 API の RequestRefresh で行うと、IsBusy のガードで自分自身が弾かれ、次の手順が永久に始まらなかった。内部用の再取得メソッドを分けて解決した。
2 つ目:2 件以上選んでも 1 件目しか入らない。 1 件目のパッケージが入るとスクリプトのコンパイル、つまり Domain Reload が走る。Window も Service も作り直され、メモリ上の待ち行列が丸ごと消える。残りの Package ID を EditorWindow の [SerializeField] に保存し、リロード後に続きから再開する対策を入れた。
3 つ目:その対策が実機で効かなかった。 フェイクのクライアントでは通ったが、実際の Package Manager では相変わらず 1 件目で止まった。そこで検証プロジェクトへ一時的に [InitializeOnLoad] のプローブを置き、-executeMethod で 2 パッケージを一括インストールしてリロードごとに Service の状態をログへ書いた。すると、1 件の Client.Add のあと十数回の Domain Reload が起き、そのたびに Service が作り直されていた。EditorWindow のシリアライズは、この回数と生成タイミングでは当てにならなかった。
どう直したか
再開の責務を Window から Service へ移し、保存先を SessionState に変えた。
public void Save(IReadOnlyList<string> pInstallPackageIds, IReadOnlyList<string> pUpdatePackageIds)
{
SessionState.SetString(c_pInstallKey, _Join(pInstallPackageIds));
SessionState.SetString(c_pUpdateKey, _Join(pUpdatePackageIds));
}
手順の開始と完了のたびに保存し、Service は生成後の最初の状態取得完了時に再開を試みる。SessionState は Editor セッション中だけ残り、Editor を閉じると消える。永続化しすぎないので、翌日に古い予定が蘇ることもない。
保存先は IMcPackageManagerResumeStore として注入し、テストではメモリ実装を共有したまま Service を作り直すことで Domain Reload を再現する。実 UPM でも、外部依存 → DebugConsole → Watchdog の 3 手順が全部入ることを確認した。
失敗時は、失敗したパッケージにエラーを出し、そのパッケージに依存する残りの予定だけを見送る。無関係な手順は続ける。全部止めると成功できるものまで再操作になり、依存が入っていないものを続けても失敗するだけだからである。
後から出た追加の教訓
Window を閉じても SessionState の残り手順が生き残り、次に Window を開くと中止したはずの手順が再開された。保存する状態には必ず「取り消す入口」が要る。
また、依存が散文からデータになったことで、package.json の依存とカタログの一致をテストで検査できるようになり、Watchdog → Diagnostics の登録漏れを CI が見つけた。
教訓
- Editor ツールの状態は Domain Reload で消える前提で設計する。 多段の非同期処理の待ち行列はメモリではなく
SessionState相当に置き、各ステップ境界で保存する。 - フェイクだけの検証で「直った」と言わない。 実 UPM を
[InitializeOnLoad]のプローブと-executeMethodで再現する手順が原因特定の決定打になった。 - リロードの回数は想像より多い。 1 件の Add で十数回。「1 回耐えればよい」という前提は破綻する。
- 状態の寿命が合う層に置く。 再開は UI の仕事ではなく Service の仕事だった。
- 公開 API を内部処理に再利用しない。 ガード条件が自分の進行を止める。
- 散文の手順書はデータ化する。 データになった時点でテスト対象になる。
Related Articles