Skip to content

feat(python/launcher): データ分離した Resonite 多重起動と terminate-all を再実装#71

Merged
Geson-anko merged 8 commits into
mainfrom
feature/20260625/multi-instance-launch
Jun 25, 2026
Merged

feat(python/launcher): データ分離した Resonite 多重起動と terminate-all を再実装#71
Geson-anko merged 8 commits into
mainfrom
feature/20260625/multi-instance-launch

Conversation

@Geson-anko

Copy link
Copy Markdown
Member

背景

Resonite の多重起動は以前 PR #66 で実装したが commit b8f51c6 で撤回された。撤回理由は (1) data path (WINEPREFIX / DataPath) を分離し忘れて Resonite DB が競合(2) renderer との Camera IPC queue 名が固定 (resonite-io-camera-frames) で多重起動時に衝突・クロストークする設計欠陥。撤回時は Python launcher のみ巻き戻し、C# 側の IPC 名分離は未着手だった。

本 PR はこの 2 つのブロッカーを正しく解消して多重起動を再実装する。

確定済みの方針 (ユーザー合意)

  1. 一括停止は別コマンド: 単一 terminate (engine/renderer pid を返す pid-only) は不変。一括停止は新コマンド terminate-all で list を返す。
  2. ラベルで自動データ分離: launch --name <label>~/.resonite-io/instances/<label>/ 配下に WINEPREFIX / DataPath / CachePath / LogsPath を自動採番・分離。--name 無しは単一起動挙動を温存。
  3. renderer queue 名は engine 生成→子継承: engine mod が起動最早期に token を生成し env へ set、renderer は子プロセスとして継承。env 未設定時は従来固定名に fallback。

Summary

C# (mod/camera)

  • IpcSocketPaths.QueueNameconst から env-override (RESONITE_IO_CAMERA_QUEUE) 付き static プロパティ化 (GrpcHost.ResolveSocketPath と同じパターン)。受信側 RendererFrameInterprocessReceiver / 送信側 FrameSender は参照のみで自動分離。
  • ResoniteIOPlugin.Load() で env 未設定時のみインスタンス固有 token を生成・set。renderer spawn より前の最早期 Load() で行う必要がある (decompiled で RenderSystem.StartRendererOnEngineReady より前と確認)。
  • renderer prebuilt を再生成 (just renderer-prebuild)。

Python (launcher / CLI)

  • launch(name=...): ラベル配下に 4 dir を自動採番、prefix/data_path/cache_path/logs_path を分離 (明示値 > ラベル自動 > 既定)、token env 注入。--name 無しは global already-running guard を温存、--name 時は per-label lock で同ラベル二重起動のみ拒否。
  • _wait_for_single_wait_for_new (PID 集合差分) に復元 (他インスタンス稼働中でも本 launch の個体だけ返す)。
  • terminate_all() -> list[LaunchResult] を復元 (engine と子 renderer をペアリング、孤立 renderer も kill)。新コマンド resoio terminate-all (--format human|json)。
  • CLI: resoio launch --name <label>

docs

  • docs/cli.md / CHANGELOG.md に多重起動・terminate-all を反映。

Test plan

  • just run green (format → gen-proto → build → test → type → check-renderer-prebuilt)
  • C# 単体: IpcSocketPaths.QueueName の env-override (real env)
  • Python 単体 (real OS プロセス, mock 禁止): _wait_for_new / ラベル分離 / per-label lock guard / terminate_all / CLI 配線・出力
  • e2e 多重起動 (@mark_e2e, tests/e2e/multi_instance.py): real 2 インスタンスを別 socket で ping/screenshot → Camera クロストーク無し → terminate-all で両方 gone ※実機 Resonite 環境で要実行

🤖 Generated with Claude Code

Geson-anko and others added 5 commits June 25, 2026 10:41
engine ↔ renderer の Camera frame queue 名が固定 (resonite-io-camera-frames) で、
多重起動すると engine 同士が同名 queue を奪い合い renderer がクロス接続する問題を解消。

- IpcSocketPaths.QueueName を const から env-override 付き static プロパティへ
  (RESONITE_IO_CAMERA_QUEUE。GrpcHost.ResolveSocketPath と同じ「env → 既定」パターン)。
  未設定時は従来固定名に fallback し単一起動は完全後方互換。
- ResoniteIOPlugin.Load() で env 未設定時のみインスタンス固有 token を生成・set。
  renderer は engine の子プロセス (RenderSystem.StartRenderer の Process.Start) として
  env を継承するため両側が同じ token を読む。token set は renderer spawn より前の
  最早期 Load() で行う必要がある (OnEngineReady では間に合わない)。
- renderer prebuilt を再生成 (IpcSocketPaths は prebuilt hash の入力)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
以前撤回 (b8f51c6) した多重起動を、データ分離忘れと IPC 衝突を解消して再実装。

launch:
- name=<label> で instances/<label>/{prefix,data,cache,logs} を自動採番し、
  WINEPREFIX / -DataPath / -CachePath / -LogsPath を分離 (明示値 > ラベル自動 > 既定)。
  RESONITE_IO_CAMERA_QUEUE にラベル由来 token を注入し Camera queue を分離。
- name 無しは従来の単一起動挙動を温存 (global already-running guard)。
- name 指定時は per-label lock (instances/<label>/launch.lock) で同ラベル二重起動のみ拒否。
- _wait_for_single を _wait_for_new (PID 集合差分) に戻し、他インスタンス稼働中でも
  本 launch が spawn した個体だけを返す。label はサニタイズ (path traversal 防止)。
- CLI: resoio launch --name <label>。

terminate-all:
- terminate_all() を list[LaunchResult] で復元 (engine と子 renderer をペアリング、
  孤立 renderer も kill)。単一 terminate は pid-only のまま不変。
- 新コマンド resoio terminate-all (--format human|json)。

real OS プロセス (mock 禁止) で _wait_for_new / ラベル分離 / lock guard / terminate_all /
CLI 配線を検証。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
real な 2 インスタンス (--name a / --name b) を別 WINEPREFIX/DataPath で起動し、
各 engine の resonite-{pid}.sock へ個別接続して ping + Camera screenshot を撮り、
両方がフレームを返す (Camera queue がクロストークしない) ことを検証。最後に
terminate-all で全インスタンスが host から消えることを確認する @mark_e2e シナリオ。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs/cli.md の launch/terminate 節と例に --name によるインスタンス分離、別 socket
ターゲティング、terminate-all を追記。CHANGELOG の Added に多重起動と terminate-all
を再掲。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Geson-anko

Copy link
Copy Markdown
Member Author

✅ e2e 多重起動を実機で実行 (green)

mod を再デプロイ (engine + renderer plugin、token 変更込み) のうえ just e2e-test multi_instance を実機 Resonite で実走し PASSED

検証できたこと:

  • 別 WINEPREFIX / DataPath で 2 インスタンス同時起動: instances/a/prefix instances/b/prefix が各 ~677M で個別に Proton bootstrap、data も別 (2.7M / 1.7M)。4 PID すべて distinct。
  • 別 socket で個別接続: resonite-{pid}.sock 2 つに ConnectionClient で ping 成功。
  • Camera クロストーク無し (核心): 両インスタンスから独立に screenshot 取得。instance_a.png (mean=3.4 std=23.7) と instance_b.png (mean=0.5 std=6.6) は shape 同一だがピクセル非一致 = 各 engine が自分の renderer queue から別フレームを受信。固定 queue 名なら衝突して片方が starve するはずが、token 分離で正しく独立。
  • terminate-all で全 gone: 実行後に socket / engine / renderer プロセスのリーク無し。

実行ログ抜粋:

tests/e2e/multi_instance.py::TestMultiInstance::test_two_named_instances_run_isolated_then_terminate_all PASSED
1 passed in 21.15s

--data-path を別々に渡せば多重起動できるため、PID 検出で launch を拒否するのは誤り
だった。launch は「既に起動中」で二度と拒否しない:

- name=None の global already-running guard を削除。
- per-label lock guard (launch.lock + _reject_if_instance_running /
  _write_instance_lock / _instance_lock_path) を削除。--name はデータツリーの自動採番
  と Camera queue token のみを担い、起動をゲートしない。同ラベル再起動も許可。
- _resolve_instance_paths は instance_dir を内部算出 (lock 用の外出し不要に)。
- docs/cli.md・CHANGELOG を「多重起動は --data-path で分離、--name は便宜」に更新。

real OS プロセスで「稼働中インスタンスの隣で named/unnamed launch が拒否されず新規個体を
返す」「同ラベル再起動が通る」を検証。撤回したガードのテストは削除。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Geson-anko

Copy link
Copy Markdown
Member Author

🔧 設計修正: 多重起動ガードを全廃

レビュー指摘を反映し、PID 検出による launch ガードを global / per-label とも全廃しました。--data-path を別々に渡せば多重起動できるため、稼働中インスタンスの存在で launch を拒否するのは誤りでした。

  • launch は「既に起動中」で二度と拒否しない(global already-running guard 削除)。
  • per-label lock (launch.lock + _reject_if_instance_running / _write_instance_lock) を削除。--nameデータツリーの自動採番 + Camera queue token のみを担う純粋な便宜機能で、起動をゲートしない。同ラベル再起動も許可。
  • docs / CHANGELOG を「多重起動は --data-path 分離が基本、--name は便宜」に更新。

just run 全 green。terminate-all と Camera token 分離 (本 PR の核心) は不変です。

Geson-anko and others added 2 commits June 25, 2026 12:01
`resoio launch` を --name なしで起動すると Renderer/Engine 起動直後にフリーズする
問題と、`resoio terminate-all` が単一インスタンスを 2 つ (pid=0) と誤判定する問題を
修正する。両方とも engine↔renderer が共有する Camera IPC queue token に起因する。

- フリーズ: --name なしの launch は token を注入せず、engine mod が runtime に
  自己生成していたが、runtime set した env は Wine 子プロセス (renderer) に継承
  されず両者が別 queue を掴んでいた。launch() が --name の有無に関わらず一意の
  token を exec 前に必ず env 注入するようにし、engine と renderer が同じ queue で
  合意するようにする (--name が動いていたのは exec 前注入だったため)。
- terminate-all の誤判定: renderer は engine の host プロセス subtree に居ない
  ことがある (Wine が reparent する) ため process-tree ペアリングが取りこぼし、
  1 インスタンスが 2 行に割れていた。engine/renderer が共有する token (/proc/environ)
  でペアリングし、process-tree を fallback に残す。

env 名を `_CAMERA_QUEUE_ENV` 定数に集約。回帰テスト 2 件追加 (unnamed launch の
token 注入 / token によるペアリング)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… camera を成立させる

engine が ResoniteIOPlugin.Load() で runtime に Environment.SetEnvironmentVariable した
Camera IPC queue token は、別プロセスとして起動する renderer に伝播しない。このため
resoio を介さない直接起動 (Gale/Steam) では engine が Guid token・renderer が固定デフォルト名
を掴んで別 queue になり camera がフリーズしていた (resoio 経由は launcher が exec 前に
注入するので前commitで解決済み)。

self-gen を撤去し、token は常に exec 前に env へ載せる設計へ統一する:
- resoio launch 経由: launcher が起動前に instance 固有 token を注入 (済)。
- Gale/Steam 直接起動: 何も set されず engine/renderer 双方が IpcSocketPaths の
  固定デフォルト名に fallback して一致する (単一インスタンス)。

- mod/src/ResoniteIO/ResoniteIOPlugin.cs: Load() の self-gen if-block を撤去。
- mod/src/ResoniteIO.RendererShared/IpcSocketPaths.cs: QueueNameEnvVar の remarks を
  実態 (launcher 注入 / デフォルト fallback) に更新。renderer prebuilt を再生成。
- python/tests/e2e/gale_launch.py: queue env 無しの mod launch が camera frame を返す
  e2e 回帰シナリオを追加 (_build_command(camera_queue=None) で Gale 起動を faithful に再現)。

実機 e2e で検証済み: gale_launch / launcher (no-name) / multi_instance すべて green。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Geson-anko
Geson-anko merged commit 2c53ca1 into main Jun 25, 2026
7 checks passed
@Geson-anko
Geson-anko deleted the feature/20260625/multi-instance-launch branch June 25, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant