バックエンドプロセスのライフサイクル
ランチャー終了時によく発生する不具合には、終了すべきプロセスが残存する(gui.py のプロセスリーク)ことと、終了してはならないプロセスが巻き添えで終了する(エミュレーターが道連れ終了される)ことの2つがあります。このページでは、両者を正確に分離制御する仕組みについて解説します。
プロセスツリー構造
alas-launcher(ランチャー本体)
└── gui.py(バックエンド)
├── instance worker(スケジューラー)
└── エミュレーター(worker から起動されるが、バックエンドとは独立して生存すべきプロセス)達成すべき制御目標:
| プロセス | ランチャー終了時の期待動作 |
|---|---|
gui.py | 必ず終了 |
| instance worker | 必ず終了 |
| エミュレーター | 生存・保持 |
Windows:Job Object による制御
backend_process.rs はバックエンド向けに Windows Job Object を作成し、以下の制限フラグを設定します:
| 制限フラグ | 役割 |
|---|---|
JOB_OBJECT_LIMIT_BREAKAWAY_OK | 子プロセスが明示的に Job からの脱離(Breakaway)を要求することを許可 |
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | 最後の Job ハンドルが閉じられた際、配下の全通常ワーカーを一括終了 |
SILENT_BREAKAWAY は使用厳禁です
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK を設定すると、通常の Python ワーカープロセスまでもが自動的に脱離してしまい、終了時にプロセスリークが発生します。必ず「明示的に要求した場合のみ脱離可能」とする必要があります。
なぜエミュレーターが道連れ終了されてしまうのか
Windows の仕様上、Job Object の所属属性は子孫プロセスへ自動継承されます。バックエンドが短命な cmd /c start を経由してプロセスツリーを切り離そうとしても、生成されたエミュレーターは依然として元の Job Object に属しているため、TerminateJobObject が実行された際に巻き添えで強制終了されます。
これを解決するための相互プロトコル:
- ランチャーはバックエンド起動時に
ALAS_LAUNCHER_JOB_BREAKAWAY=1を注入し、「脱離機能に対応している」ことを通知します。 - バックエンドの
PlatformWindows.execute(wait=False)はこの環境変数を検知した場合、エミュレーターの起動コマンドにCREATE_BREAKAWAY_FROM_JOBフラグを付与します。 - 同時に、複製される環境変数テーブルから
ALAS_LAUNCHER_PID、ALAS_LAUNCHER_JOB_BREAKAWAY、ALAS_WEBUI_TRUST_SECRETを明示的に削除します。
これにより、エミュレーターはバックエンドの Job Object に属さなくなり、かつランチャーのフォールバックプロセススキャンにもヒットしなくなります。
双方の更新が不可欠です
古いバックエンドは脱離を要求せず、古いランチャーは脱離を許可しません。片側だけを修正しても「エミュレーターが道連れ終了される」か「ワーカーがリークする」かのどちらかの不具合が再発します。
ルートプロセスの生成順序
- Python ルートプロセスをサスペンド(一時停止)状態で生成。
- プロセスを Job Object に登録。
- プロセスの実行を再開(Resume)。
登録や再開に失敗した場合はルートプロセスを即時終了・回収し、起動時の競合やゾンビ化を防ぎます。Job ハンドルは OwnedHandle によって管理され、正常終了、初期化失敗、パニック発生時のいずれにおいても確実に解放されます。
明示的終了処理
ルートプロセスの終了待機に加え、Job Object 内のアクティブなプロセス数を監視し、通常のワーカーがすべて終了するまで最大5秒間待機します。タイムアウトまたは監視失敗時はエラーを返します。ルートプロセスの終了をもってグループ全体のクリーンアップ完了と誤認してはなりません。
Unix:プロセスグループによる制御
Unix 環境には Job Object が存在しないため、プロセスグループを利用します:
- バックエンドは独立したプロセスグループ内で起動されます。
- 終了時はプロセスグループ全体に対してシグナルを送信し、回収を待機します。
- エミュレーターの脱離処理は起動方式(
setsid等)に依存し、Windows の Breakaway 宣言とは別系統のロジックとなります。
フォールバック機構:環境変数マーカー
万が一 Job Object やプロセスグループによる回収が機能しなかった場合に備え、ランチャーは終了時にシステム全体のプロセス一覧をスキャンし、ALAS_LAUNCHER_PID を継承している残存プロセスを検出して強制終了します。
エミュレーター起動前に当該環境変数を削除しなければならないのはこのためです(削除を怠るとフォールバック回収によって強制終了されます)。
テスト仕様
cargo test --locked backend_process::testsテストコードは自身をバックエンド、ワーカー、エミュレーターに見立てて以下の3系統のパスを検証します:
- 明示的終了 → ルートプロセスと通常ワーカーは終了し、脱離したエミュレーターは生存。
- Job ハンドルの破棄 → 同上の動作。
- ルートプロセスの先行異常終了 → 子孫ワーカープロセスが確実に回収されること。
テスト用エミュレータープロセスの終了はテスト専用ハンドルを経由して行われ、ユーザーの実エミュレーターには影響しません。
コアリポジトリ側の tests.test_emulator_process_lifetime では、機能判定、環境変数クリーンアップ、同期管理コマンドの挙動が検証されます。
Windows ローカル環境での単体テスト
マニフェストにより管理者権限が要求されます。非特権環境ではテストコード自体がエラー 740 で起動できません。一時的なテストプロジェクトで #[path] を介して backend_process.rs と window_util.rs を読み込んで実行することも可能ですが、全結合テストの完全な代替にはなりません。
改修時チェックリスト
- [ ] 新規子プロセスに意図しない環境変数が継承されていないか
- [ ] エミュレーターの起動パイプラインで breakaway が正しく要求されているか
- [ ] 終了処理においてルートプロセスと子孫ワーカーの双方が待機されているか
- [ ] すべての分岐パスで Job ハンドルが確実に解放されているか
- [ ] Unix 側の分岐ロジックが破損していないか
- [ ] 3プラットフォームすべてでテストが通過しているか
- [ ] 実機上で「ランチャー終了後もエミュレーターが継続稼働すること」を確認したか
参考文献
- Microsoft Job Objects 公式ドキュメント
- リポジトリ内
docs/backend-lifecycle.md