feat: アプリ終了時の app_closed イベント処理を追加

This commit is contained in:
misyaguziya
2026-01-05 22:38:17 +09:00
parent 300c0d4648
commit 4eb61fa4b7

View File

@@ -771,6 +771,213 @@ class TelemetryCore:
---
## App Closed イベント処理の詳細
### 処理タイミング
`app_closed` イベントはアプリケーション終了時に送信する **最後のイベント** です。以下のタイミングで発火します:
1. **Watchdog タイムアウト**(異常終了)
2. **ユーザーがアプリを閉じる**(正常終了)
3. **KeyboardInterruptCtrl+C**(強制終了)
### 既存コード構造への組み込み
#### パターン1: mainloop.py の stop() メソッド
```python
# mainloop.py
from models.telemetry import telemetry
class Main:
def stop(self, wait: float = 2.0) -> None:
"""Signal threads to stop and wait for them to finish.
Args:
wait: maximum seconds to wait for threads to join.
"""
# ここで telemetry を終了(最後のイベント送信)
try:
telemetry.shutdown() # app_closed 送信
except Exception:
pass # 握りつぶし
self._stop_event.set()
# give threads a chance to exit
start = time.time()
for th in self._threads:
remaining = max(0.0, wait - (time.time() - start))
th.join(timeout=remaining)
```
**重要**: `telemetry.shutdown()`**スレッド停止前** に呼び出す必要があります。そうしないと heartbeat スレッドが動作していない状態で shutdown に失敗する可能性があります。
#### パターン2: Controller.setWatchdogCallback
Watchdog がタイムアウトした場合も `stop()` 経由で終了するため、上記の実装で自動的に `app_closed` が送信されます。
```python
# mainloop.py
if __name__ == "__main__":
main_instance.startReceiver()
main_instance.startHandler()
# Watchdog タイムアウト時に main_instance.stop() を呼び出し
# → 自動的に telemetry.shutdown() が実行される
main_instance.controller.setWatchdogCallback(main_instance.stop)
main_instance.controller.init()
# ...
```
#### パターン3: KeyboardInterrupt 時
```python
# mainloop.py の Main.start()
def start(self) -> None:
"""Start the main loop to keep the program running."""
try:
while not self._stop_event.is_set():
time.sleep(1)
except KeyboardInterrupt:
self.stop() # telemetry.shutdown() が呼ばれる
```
### app_closed 送信前の確認事項
`telemetry.shutdown()` が呼ばれる直前に、以下の処理を完了しておくべきです:
| 処理 | タイミング | 理由 |
|------|----------|------|
| **設定保存** | telemetry.shutdown() **前** | 設定が失われるのを防ぐ |
| **ファイルクローズ** | telemetry.shutdown() **前** | ファイルディスクリプタをリソースリークから守る |
| **オーバーレイ終了** | telemetry.shutdown() **前** | VR ウィンドウを正常にクローズ |
| **OSC ハンドラー停止** | telemetry.shutdown() **前** | ネットワークリソースを解放 |
| **モデルクリーンアップ** | telemetry.shutdown() **前** | GPU メモリを解放 |
| **テレメトリ終了** | telemetry.shutdown() | 最後 |
### 実装フローチャート
```
ユーザーがアプリを閉じる / Watchdog タイムアウト / Ctrl+C
main_instance.stop() 呼び出し
1. try:
telemetry.shutdown() # app_closed 送信 + heartbeat停止
except:
pass # 握りつぶし(通信失敗でも続行)
2. self._stop_event.set()
3. スレッド停止待機(最大 2.0秒)
4. モデルクリーンアップif model が初期化されていれば)
5. メモリ解放
プロセス終了
```
### コード例model.py でのクリーンアップ
`model.shutdown()` メソッドを追加することで、モデルのリソース解放を統一管理できます:
```python
# model.py
def shutdown(self) -> None:
"""Model cleanup on application shutdown."""
try:
# オーバーレイ終了
if hasattr(self, 'overlay') and self.overlay:
self.shutdownOverlay()
# Watchdog 停止
if hasattr(self, 'th_watchdog'):
self.stopWatchdog()
# WebSocket サーバー停止
if hasattr(self, 'websocket_server_alive') and self.websocket_server_alive:
self.stopWebSocketServer()
# OSC ハンドラー停止
if hasattr(self, 'osc_handler'):
self.stopReceiveOSC()
# オーディオ停止
self.stopMicTranscript()
self.stopSpeakerTranscript()
self.stopCheckMicEnergy()
self.stopCheckSpeakerEnergy()
# ロガー停止
if self.logger:
self.stopLogger()
# メモリ解放
gc.collect()
except Exception:
errorLogging()
```
その後、mainloop.py で呼び出し:
```python
# mainloop.py
def stop(self, wait: float = 2.0) -> None:
"""Signal threads to stop and wait for them to finish."""
# テレメトリ終了app_closed 送信)
try:
telemetry.shutdown()
except Exception:
pass
# モデルクリーンアップ
try:
model.shutdown()
except Exception:
pass
self._stop_event.set()
start = time.time()
for th in self._threads:
remaining = max(0.0, wait - (time.time() - start))
th.join(timeout=remaining)
```
### オフライン時の動作
テレメトリが OFF の場合でも、以下のようにコードは安全に機能します:
```python
# telemetry.shutdown() 内の処理OFF時
def shutdown(self):
if self.state.is_enabled():
try:
self.core.send_event("app_closed") # ← 実行されない
except Exception:
pass
self.heartbeat.stop() # OFF時も停止するスレッド安全
self.state.reset() # 状態リセット
```
### 通信失敗時の処理
ネットワーク障害で `app_closed` 送信に失敗した場合:
```python
try:
telemetry.shutdown() # Aptabase への通信失敗
except Exception:
pass # 握りつぶし:エラーメッセージ出力なし
```
**アプリは normal exit します。再送・バッファリング・ログ出力は行いません。**
---
## テスト計画
### ユニットテスト