Skip to content

Electron Main Process ​

electron/main.ts を中心とした Electron メインプロセスの起動シーケンスと、electron/main/ 配下の helper の構成。

3 プロセス構成(Manager / Recorder / Viewer) ​

このアプリは同じ electron/main.ts を 3 つの実行ファイルとして配布し、exe 名 / 起動フラグでモードを切り替える(detectRecorderMode / detectViewerMode)。役割を分離することで「記録は裏で軽く常駐、確認や重い分析は必要なときだけ」という運用ができる。

exeモード役割UI
WingStats Manager.exe通常オーバーレイ設定・戦績分析のメイン画面フル UI
WingStats Recorder.exe--recorder-mode画面を持たない記録専用サービス。ログ監視 → DB へ自動保存トレイのみ
WingStats Viewer.exe--viewer-mode記録状況をすばやく確認する軽量ウィンドウ(DB は read-only 接続)軽量 UI

単一 writer と委譲 ​

DB / 設定への書き込みは常に 1 プロセスだけが行う(single-writer)。userData/process.lock(electron/core/processLock.ts)に { role, pid, startedAt, appVersion } を排他作成し、Recorder が常駐している間は Manager が起動時に lock を検出して 委譲モード(electron/core/writerDelegation.ts)に入り、自前の log watcher / auto-fetch / startup-import を起動しない。

ライブ連携(オーバーレイのリアルタイム反映) ​

Recorder が writer のときも、オーバーレイのリアルタイム表示は維持される。Recorder は LiveEventWsServer(ws://127.0.0.1:47831/live)でライブイベントを配信し、Manager は LiveEventWsClient で受信して自身の LiveEventBus へ再注入する。renderer 側は writer が誰かを意識せず、従来どおり IPC チャンネル(bp-opponent:update / log-watcher-phase 等)で受け取るため、既存のオーバーレイ実装は変更不要。この接続は起動ごとの乱数トークン(<userData>/local-ws-token-<port>)による相互認証付きで、トークンを持たないプロセスは繋げない(詳細は Sync and State の「ローカル WebSocket の認証」)。

管理画面をしまったときのメモリ解放 ​

委譲モードの Manager は、管理画面を × でトレイにしまって 5 分たつと窓(renderer)を破棄してメモリを返す (electron/main/controlWindowRelease.ts)。トレイの「ウィンドウを表示」・二重起動で作り直し、IPC の送り先も張り替える。 作り直した窓は control.html?restored=1 で開き、未保存ログの確認・起動時の戦績取得・サポートのお願いは繰り返さない。 own-writer の Manager は試合終了時の自動取得を renderer が担っているので破棄しない。待ち時間は WINGSTATS_CONTROL_WINDOW_RELEASE_DELAY_MS で変えられる(検証用)。spec: electron-memory-release

オーバーレイ窓を隠しているときのメモリ解放 ​

オーバーレイ窓(透過・最前面のレイアウト編集用の窓)は、既定では画面外に置いたまま起動する(UserSettings.overlayWindowOpenOnStart === true のときだけ、保存済みの表示状態に従う。OverlayWindowManager.create の openOnStart)。Manager は、 この窓を画面外に隠したまま 5 分たつと窓(renderer)を破棄してメモリを返す(electron/main/overlayWindowRelease.ts。 管理画面の破棄と同じ ControlWindowReleaser を、位置で「隠れている」を見るようにして使う)。own-writer / 委譲のどちらでも破棄する。 オーバーレイ設定の正本・勝敗の自動加算・4 キャラ反映・OBS 配信はどれも main にあるので、破棄している間も止まらない。 「オーバーレイを表示」・サイズ変更で画面外に作り直し(DevTools は自動で開かない)、ライブイベントの送り先も張り替える。 作り直した窓は起動時と同じく正本の全量(get-config)と DB から最新の表示を取り直す。縮小表示(開発用)中と DevTools を 開いている間は破棄しない。窓を画面内に出している人(旧ウィンドウキャプチャ運用)には影響しない。 管理画面とオーバーレイ窓の両方を破棄して全窓が閉じても Manager は終了せず、トレイに残る(quitOnAllWindowsClosed を関数で判定)。 待ち時間は WINGSTATS_OVERLAY_WINDOW_RELEASE_DELAY_MS で変えられる(検証用)。E2E では破棄しない。 spec: dev-tooling-and-overlay-window-release

常駐設定 ​

記録サービスの常駐(Windows ログイン時起動)は AutoLaunchService(setRecorderEnabled → Run キー登録)で管理する。UI からは初回セットアップの「アプリ構成」ステップ、または設定タブの「自動起動」で切り替えられる。

全体構成 ​

electron/
├── main.ts                     # エントリ(起動の順番と配線だけ。中身は main/ へ)
├── main/                       # main.ts から抽出した helper(主なものは下の表)
├── core/                       # ライフサイクル骨格
│   ├── AppLifecycle.ts         # 単一インスタンスロック / グローバル例外 / app events
│   ├── PathResolver.ts         # userData パス解決
│   ├── ProcessManager.ts
│   └── StartupSequencer.ts     # critical-path / deferred-boot の 2 段階起動
├── windows/                    # BrowserWindow ラッパ
│   ├── ControlWindowManager.ts
│   ├── OverlayWindowManager.ts
│   ├── BPOpponentWindow.ts
│   └── WindowStateStore.ts
├── ipc/                        # IPC ハンドラ群
│   ├── IPCRegistry.ts          # core / features を分けた段階登録
│   ├── singleFlightIpc.ts      # handleSingleFlight(戦績 API 系 IPC の連打対策)
│   └── *Handlers.ts            # 機能別ハンドラ
├── services/                   # アプリサービス(recorder/・viewer/・obsBrowserSource/・liveEvents/ ほか)
│   ├── AutoUpdateService.ts
│   ├── LogWatcherService.ts
│   ├── BrowserAutomationService.ts
│   ├── browserAutomation/      # browser detection / response listener
│   ├── ResponseStorageService.ts
│   └── UserSettingsService.ts
├── preload/                    # preload.ts の実装本体
│   ├── api/                    # window.electronAPI に expose する API 群
│   ├── eventChannels.ts
│   └── types.ts
├── database.ts                 # 薄い facade
├── database/                   # Repository 8 つ + StatsService(Database Design 参照)
└── logger.ts

electron/main/ の主なモジュール ​

ファイル役割
launchRoleDetection.tsこのプロセスが Manager / Recorder / Viewer のどれかを判定し、Logger のエラー出力先を配線する
instanceLocks.tsprocess.lock(Recorder の writer 宣言。2 個目の Recorder は即終了)と manager.lock(Manager 存命宣言。Viewer の BP 窓自動表示を抑える)の取得
dbInit.tsDB 初期化と失敗時のダイアログ(下記)。Viewer・委譲 Manager は read-only で開く
delegatedManagerDb.ts委譲中の Manager が matches.db を read-only で開く(Recorder と 2 writer にしない)
recorderModeBootstrap.tsRecorder の deferred boot。RecorderAutoStart・トレイ・recorder-state.json の定期書き出し・CLI トリガの polling・ゲーム起動検知(GameProcessWatcher)・戦績窓用 snapshot の生成
viewerModeBootstrap.tsViewer の起動。DB は読み取り専用・WS サーバは持たず LiveEventWsClient で Recorder に繋ぐ・戦績窓と BP 窓を持つ。IPCRegistry は登録しない
viewerReadOnlyIpc.tsViewer が登録する読み取り専用 IPC(BP 窓の renderer が読む設定・アクティブアカウント・プロファイル・直近の試合・対戦成績)。BP 窓が呼ぶ IPC が Viewer で全部受けられているかは __tests__/viewerWindowIpcContract.test.ts がソースから拾って検査する(Manager 側にだけ足すと落ちる)
managerRecorderControl.tsManager から記録サービスを操作する配線。Recorder の起動、WS 操作コマンド(RecorderControlClient)、トレイの「記録サービス制御」、recorder-control:* IPC
recorderMatchWrite.ts委譲 Manager から振り替えられた試合の書き込みを Recorder(writer)側で行う
recorderStartupImport.ts / startupLogReanalysis.tswriter の起動時に 1 回: 過去ログの取り込み / 保存済みログ試合を今の解析器で読み直す
obsBrowserSourceBootstrap.tsOBS ブラウザソース配信サーバの配線。起動ロールで所有者を決め、正本 overlay-config.json を開く(起動時の勝敗リセットもここ)
controlWindowRelease.ts / overlayWindowRelease.tsしまった管理画面・隠したオーバーレイ窓を一定時間後に破棄してメモリを返す(上記)
startMenuShortcuts.tsdev / 未インストールのビルドが作る同じ AUMID のショートカットで、インストール版のスタートメニュー項目が隠れないようにする
backgroundWindows.tsAI エージェント・自動テストが起動したときの窓を画面外・フォーカスなしで出す
resourcePackBootstrap.ts配信されたキャラデータ(リソースパック)の配線。wingstats-res:// と resources:* IPC
telemetryBootstrap.ts / resourceUsageSampling.ts / troubleshootWiring.tstelemetry・診断の配線 / メモリと CPU の定期記録 / 「問題の診断」に出す直近エラーの読み出し
bpOpponentVisibility.ts / perfRelay.tsBP 窓の表示判定 / 起動計測(下記)

毎秒の照会を減らす仕組み ​

戦績窓と Manager は記録状態を 1 秒ごとに取りにくるので、そのたびに当日の試合を SQL で集計しないようにしている。

  • Recorder の snapshot(recorderModeBootstrap.ts): createTtlMemo で 1 秒メモ化し、bp-data / phase-changed / match-completed / match-detail-updated で捨てる。中の今日の戦績とプロファイル一覧は RecorderAutoStart が dbChangeToken(接続の通し番号・ PRAGMA data_version・total_changes())が変わったときだけ引き直す。目印が取れないときは毎回計算する
  • Viewer のローカル集計(electron/services/viewer/localRecentCache.ts): Recorder に未接続のときの「今日の戦績」を、UID・ローカル日付・ matches.db / -wal の更新時刻とサイズ・最大保持時間のどれかが変わったときだけ作り直す

API 系 IPC の連打対策(handleSingleFlight) ​

戦績 API を叩く IPC(試合終了時の取得・シーズン取得・全件取得・プロファイル単体の取得・ログイン確認・所持キャラ一覧の取得など)と委譲 Manager から Recorder への操作コマンド(recorder-control:send)は ipcMain.handle の代わりに electron/ipc/singleFlightIpc.ts の handleSingleFlight で登録する。同じチャネル・同じ引数の呼び出しが走っている間に次が来たら、 新しく API を叩かず走っている方の結果を返す(複数の画面から同時に来ても 1 本にまとまる)。終われば次の呼び出しは新しく走る。

起動シーケンス ​

StartupSequencer が critical path と deferred boot に分けて初期化を進める。first paint までの時間を最短化するため、UI を先に出して DB / Service / feature IPC は後で実行する。

Critical Path の責務 ​

Step役割なぜ critical補足
loadMinimalUserSettingsbpWindowOnlyInBP 等、ウィンドウ表示判断に必要な設定だけ読むウィンドウ生成前に値が必要失敗時はデフォルト値で続行
createWindowscontrol + overlay の BrowserWindow を生成画面を出すコア処理overlay は OBS が読む URL
registerCoreIpccore IPC(ウィンドウ操作 / 設定 / アップデート等)を登録renderer の最初の IPC が成立する必要があるbpOpponentWindow は null で OK(後で updateWindows で反映)

Deferred Boot の責務 ​

Step役割失敗時の振る舞い
initDatabaseinitDatabaseWithFailureDialog() で SQLite を開くダイアログ → 削除 / DB なし続行 / 終了
initServicesBPOpponentWindow 生成 / IPCRegistry に reflectDB 不可でも UI は使えるよう続行
registerFeatureIpcDB / 統計 / log watcher / browser automation 系 IPC を登録ユーザーが終了を選んだ場合は no-op
startStartupImportPreviewrenderer 駆動なので no-op (将来の拡張点)—
startLogWatcherManager(own-writer)だけが起動時に監視を始める。既定 ON で、UserSettings.autoStartLogWatcher === false のときだけ止める。Recorder(RecorderAutoStart が別に始める)・Viewer・委譲中の Manager では始めない。E2E でも始めない(E2E_LOG_WATCHER=1 のときだけ、ほかの条件を満たせば始める)直前に Recorder の生存を読み直し、いれば始めない

DB 初期化失敗の対処(dbInit.ts) ​

initDatabaseWithFailureDialog() は initDatabase() を呼び、失敗時はダイアログでユーザに選ばせる。

  • ダイアログのボタンラベル: 🗑️ DBファイルを削除して再試行 / ▶️ DBなしで続行 / ❌ アプリを終了
  • 終了選択時は呼び出し側の electron/main.ts で app.quit() を行い、deferred boot の残りステップを skip
  • 終了選択時の挙動は electron/main.ts 側の deferred boot skip ロジックで検証

BP 対戦相手ウィンドウ表示制御(bpOpponentVisibility.ts) ​

makeBPOpponentVisibility(getCtx) は closure factory。getCtx で「現在の window / onlyInBP 設定 / 試合フェーズ」を毎回読み直す。

ts
const { applyVisibility } = makeBPOpponentVisibility(
  () => ({ window: bpOpponentWindow, onlyInBP: bpWindowOnlyInBP, currentPhase })
);
  • bpWindowOnlyInBP=false → 常に表示
  • bpWindowOnlyInBP=true → currentPhase === 'BP' のときだけ表示
  • 試合フェーズの切り替えで applyVisibility() を呼ぶ責務は IPC ハンドラ側

起動計測 (perfRelay.ts) ​

関数役割
reportBinAndModuleLoad()PERF_RUN_ELECTRON_T0_MS 環境変数(spawn 時刻)と現在時刻の差分を [perf] electron.binAndModuleLoad=Xms として stdout に書く
registerPerfMarkerRelay()ELECTRON_PERF_MEASURE か ELECTRON_PERF_DEV_MEASURE が '1' のときだけ、renderer からの perf-marker IPC を stdout にリレー

利用側スクリプト: scripts/perf/measure-startup.ts / measure-dev-startup.ts

IPC レジストリ(段階登録) ​

IPCRegistry は 2 段階登録 を行う。registerCore() は critical path で実行し、registerFeatures() は DB 初期化後の deferred boot で実行する。updateWindows / updateDatabaseAvailability で後から参照を差し替える。

単一インスタンスロック ​

AppLifecycle.setupSingleInstanceLock(onSecondInstance):

  • 二重起動時、後発プロセスは即 process.exit(0)
  • 既存プロセスでは onSecondInstance callback で control window をフォーカスする

グローバル例外ハンドラ ​

AppLifecycle.setupGlobalErrorHandlers():

  • process.on('uncaughtException') / unhandledRejection をキャッチして Logger 出力
  • 開発時は dev tools で詳細確認できるよう error の stack を残す

関連ドキュメント ​


Last verified: 2026-10-24 / commit e111fbeb