Refactor Game Monitor into Game Manager with Setup.py integration and full process control
- Replaced legacy `game_monitor.py` with a new modular `game_manager.py`. - Introduced `GameMonitor` class to encapsulate: - Game window detection, focus enforcement, and resize enforcement. - Timed game restarts based on configuration interval. - Callback system to notify Setup.py on restart completion. - Cross-platform game launching (Windows/Unix). - Process termination using `psutil` if available. - `Setup.py` now acts as the control hub: - Instantiates and manages `GameMonitor`. - Provides live configuration updates (e.g., window title, restart timing). - Coordinates bot lifecycle with game restarts. - Maintains standalone execution mode for `game_manager.py` (for testing or CLI use). - Replaces older “always-on-top” logic with foreground window activation. - Dramatically improves control, flexibility, and automation reliability for game-based workflows.
This commit is contained in:
parent
a5b6a44164
commit
51a99ee5ad
184
ClaudeCode.md
184
ClaudeCode.md
@ -15,72 +15,66 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
|
||||
### 核心元件
|
||||
|
||||
1. **主控模塊 (main.py)**
|
||||
- 協調各模塊的工作
|
||||
- 初始化 MCP 連接
|
||||
- **容錯處理**:即使 `config.py` 中未配置 MCP 伺服器,或所有伺服器連接失敗,程式現在也會繼續執行,僅打印警告訊息,MCP 功能將不可用。 (Added 2025-04-21)
|
||||
- **伺服器子進程管理 (修正 2025-05-02)**:使用 `mcp.client.stdio.stdio_client` 啟動和連接 `config.py` 中定義的每個 MCP 伺服器。`stdio_client` 作為一個異步上下文管理器,負責管理其啟動的子進程的生命週期。
|
||||
- **Windows 特定處理 (修正 2025-05-02)**:在 Windows 上,如果 `pywin32` 可用,會註冊一個控制台事件處理程序 (`win32api.SetConsoleCtrlHandler`)。此處理程序主要用於輔助觸發正常的關閉流程(最終會調用 `AsyncExitStack.aclose()`),而不是直接終止進程。伺服器子進程的實際終止依賴於 `stdio_client` 上下文管理器在 `AsyncExitStack.aclose()` 期間的清理操作。
|
||||
- **記憶體系統初始化 (新增 2025-05-02)**:在啟動時調用 `chroma_client.initialize_memory_system()`,根據 `config.py` 中的 `ENABLE_PRELOAD_PROFILES` 設定決定是否啟用記憶體預載入。
|
||||
- 設置並管理主要事件循環
|
||||
- **記憶體預載入 (新增 2025-05-02)**:在主事件循環中,如果預載入已啟用,則在每次收到 UI 觸發後、調用 LLM 之前,嘗試從 ChromaDB 預先獲取用戶資料 (`get_entity_profile`)、相關記憶 (`get_related_memories`) 和潛在相關的機器人知識 (`get_bot_knowledge`)。
|
||||
- 處理程式生命週期管理和資源清理(通過 `AsyncExitStack` 間接管理 MCP 伺服器子進程的終止)
|
||||
1. **主控模塊 (main.py)**
|
||||
- 協調各模塊的工作
|
||||
- 初始化 MCP 連接
|
||||
- **容錯處理**:即使 `config.py` 中未配置 MCP 伺服器,或所有伺服器連接失敗,程式現在也會繼續執行,僅打印警告訊息,MCP 功能將不可用。 (Added 2025-04-21)
|
||||
- **伺服器子進程管理 (修正 2025-05-02)**:使用 `mcp.client.stdio.stdio_client` 啟動和連接 `config.py` 中定義的每個 MCP 伺服器。`stdio_client` 作為一個異步上下文管理器,負責管理其啟動的子進程的生命週期。
|
||||
- **Windows 特定處理 (修正 2025-05-02)**:在 Windows 上,如果 `pywin32` 可用,會註冊一個控制台事件處理程序 (`win32api.SetConsoleCtrlHandler`)。此處理程序主要用於輔助觸發正常的關閉流程(最終會調用 `AsyncExitStack.aclose()`),而不是直接終止進程。伺服器子進程的實際終止依賴於 `stdio_client` 上下文管理器在 `AsyncExitStack.aclose()` 期間的清理操作。
|
||||
- **記憶體系統初始化 (新增 2025-05-02)**:在啟動時調用 `chroma_client.initialize_memory_system()`,根據 `config.py` 中的 `ENABLE_PRELOAD_PROFILES` 設定決定是否啟用記憶體預載入。
|
||||
- 設置並管理主要事件循環
|
||||
- **記憶體預載入 (新增 2025-05-02)**:在主事件循環中,如果預載入已啟用,則在每次收到 UI 觸發後、調用 LLM 之前,嘗試從 ChromaDB 預先獲取用戶資料 (`get_entity_profile`)、相關記憶 (`get_related_memories`) 和潛在相關的機器人知識 (`get_bot_knowledge`)。
|
||||
- 處理程式生命週期管理和資源清理(通過 `AsyncExitStack` 間接管理 MCP 伺服器子進程的終止)
|
||||
|
||||
2. **LLM 交互模塊 (llm_interaction.py)**
|
||||
- 與語言模型 API 通信
|
||||
- 管理系統提示與角色設定
|
||||
- **條件式提示 (新增 2025-05-02)**:`get_system_prompt` 函數現在接受預載入的用戶資料、相關記憶和機器人知識。根據是否有預載入數據,動態調整系統提示中的記憶體檢索協議說明。
|
||||
- 處理語言模型的工具調用功能
|
||||
- 格式化 LLM 回應
|
||||
- 提供工具結果合成機制
|
||||
2. **LLM 交互模塊 (llm_interaction.py)**
|
||||
- 與語言模型 API 通信
|
||||
- 管理系統提示與角色設定
|
||||
- **條件式提示 (新增 2025-05-02)**:`get_system_prompt` 函數現在接受預載入的用戶資料、相關記憶和機器人知識。根據是否有預載入數據,動態調整系統提示中的記憶體檢索協議說明。
|
||||
- 處理語言模型的工具調用功能
|
||||
- 格式化 LLM 回應
|
||||
- 提供工具結果合成機制
|
||||
|
||||
3. **UI 互動模塊 (ui_interaction.py)**
|
||||
- 使用圖像辨識技術監控遊戲聊天視窗
|
||||
- 檢測聊天泡泡與關鍵字
|
||||
- 複製聊天內容和獲取發送者姓名
|
||||
- 將生成的回應輸入到遊戲中
|
||||
3. **UI 互動模塊 (ui_interaction.py)**
|
||||
- 使用圖像辨識技術監控遊戲聊天視窗
|
||||
- 檢測聊天泡泡與關鍵字
|
||||
- 複製聊天內容和獲取發送者姓名
|
||||
- 將生成的回應輸入到遊戲中
|
||||
|
||||
4. **MCP 客戶端模塊 (mcp_client.py)**
|
||||
- 管理與 MCP 服務器的通信
|
||||
- 列出和調用可用工具
|
||||
- 處理工具調用的結果和錯誤
|
||||
4. **MCP 客戶端模塊 (mcp_client.py)**
|
||||
- 管理與 MCP 服務器的通信
|
||||
- 列出和調用可用工具
|
||||
- 處理工具調用的結果和錯誤
|
||||
|
||||
5. **配置模塊 (config.py)**
|
||||
- 集中管理系統參數和設定
|
||||
- 整合環境變數
|
||||
- 配置 API 密鑰和服務器設定
|
||||
5. **配置模塊 (config.py)**
|
||||
- 集中管理系統參數和設定
|
||||
- 整合環境變數
|
||||
- 配置 API 密鑰和服務器設定
|
||||
|
||||
6. **角色定義 (persona.json)**
|
||||
- 詳細定義機器人的人格特徵
|
||||
- 包含外觀、說話風格、個性特點等資訊
|
||||
- 提供給 LLM 以確保角色扮演一致性
|
||||
6. **角色定義 (persona.json)**
|
||||
- 詳細定義機器人的人格特徵
|
||||
- 包含外觀、說話風格、個性特點等資訊
|
||||
- 提供給 LLM 以確保角色扮演一致性
|
||||
|
||||
7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py)
|
||||
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
|
||||
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
|
||||
- **確保視窗保持活躍**:如果遊戲視窗不是目前的前景視窗,則嘗試將其帶到前景並啟用 (Bring to Foreground/Activate),取代了之前的強制置頂 (Always on Top) 邏輯 (修改於 2025-05-12)。
|
||||
- **定時遊戲重啟** (如果 `config.ENABLE_SCHEDULED_RESTART` 為 True):
|
||||
- 根據 `config.RESTART_INTERVAL_MINUTES` 設定的間隔執行。
|
||||
- **簡化流程 (2025-04-25)**:
|
||||
1. 通過 `stdout` 向 `main.py` 發送 JSON 訊號 (`{'action': 'pause_ui'}`),請求暫停 UI 監控。
|
||||
2. 等待固定時間(30 秒)。
|
||||
3. 調用 `restart_game_process` 函數,**嘗試**終止 (`terminate`/`kill`) `LastWar.exe` 進程(**無驗證**)。
|
||||
4. 等待固定時間(2 秒)。
|
||||
5. **嘗試**使用 `os.startfile` 啟動 `config.GAME_EXECUTABLE_PATH`(**無驗證**)。
|
||||
6. 等待固定時間(30 秒)。
|
||||
7. 使用 `try...finally` 結構確保**總是**執行下一步。
|
||||
8. 通過 `stdout` 向 `main.py` 發送 JSON 訊號 (`{'action': 'resume_ui'}`),請求恢復 UI 監控。
|
||||
- **視窗調整**:遊戲視窗的位置/大小/置頂狀態的調整完全由 `monitor_game_window` 的主循環持續負責,重啟流程不再進行立即調整。
|
||||
- **作為獨立進程運行**:由 `main.py` 使用 `subprocess.Popen` 啟動,捕獲其 `stdout` (用於 JSON 訊號) 和 `stderr` (用於日誌)。
|
||||
- **進程間通信**:
|
||||
- `game_monitor.py` -> `main.py`:通過 `stdout` 發送 JSON 格式的 `pause_ui` 和 `resume_ui` 訊號。
|
||||
- **日誌處理**:`game_monitor.py` 的日誌被配置為輸出到 `stderr`,以保持 `stdout` 清潔,確保訊號傳遞可靠性。`main.py` 會讀取 `stderr` 並可能顯示這些日誌。
|
||||
- **生命週期管理**:由 `main.py` 在啟動時創建,並在 `shutdown` 過程中嘗試終止 (`terminate`)。
|
||||
7. **遊戲管理器模組 (game_manager.py)** (取代舊的 `game_monitor.py`)
|
||||
- **核心類 `GameMonitor`**:封裝所有遊戲視窗監控、自動重啟和進程管理功能。
|
||||
- **由 `Setup.py` 管理**:
|
||||
- 在 `Setup.py` 的 "Start Managed Bot & Game" 流程中被實例化和啟動。
|
||||
- 在停止會話時由 `Setup.py` 停止。
|
||||
- 設定(如視窗標題、路徑、重啟間隔等)通過 `Setup.py` 傳遞,並可在運行時通過 `update_config` 方法更新。
|
||||
- **功能**:
|
||||
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
|
||||
- 確保視窗維持在設定檔中指定的位置和大小。
|
||||
- 確保視窗保持活躍(帶到前景並獲得焦點)。
|
||||
- **定時遊戲重啟**:根據設定檔中的間隔執行。
|
||||
- **回調機制**:重啟完成後,通過回調函數通知 `Setup.py`(例如,`restart_complete`),`Setup.py` 隨後處理機器人重啟。
|
||||
- **進程管理**:使用 `psutil`(如果可用)查找和終止遊戲進程。
|
||||
- **跨平台啟動**:使用 `os.startfile` (Windows) 或 `subprocess.Popen` (其他平台) 啟動遊戲。
|
||||
- **獨立運行模式**:`game_manager.py` 仍然可以作為獨立腳本運行 (類似舊的 `game_monitor.py`),此時它會從 `config.py` 加載設定,並通過 `stdout` 發送 JSON 訊號。
|
||||
|
||||
8. **ChromaDB 客戶端模塊 (chroma_client.py)** (新增 2025-05-02)
|
||||
- 處理與本地 ChromaDB 向量數據庫的連接和互動。
|
||||
- 提供函數以初始化客戶端、獲取/創建集合,以及查詢用戶資料、相關記憶和機器人知識。
|
||||
- 使用 `chromadb.PersistentClient` 連接持久化數據庫。
|
||||
8. **ChromaDB 客戶端模塊 (chroma_client.py)** (新增 2025-05-02)
|
||||
- 處理與本地 ChromaDB 向量數據庫的連接和互動。
|
||||
- 提供函數以初始化客戶端、獲取/創建集合,以及查詢用戶資料、相關記憶和機器人知識。
|
||||
- 使用 `chromadb.PersistentClient` 連接持久化數據庫。
|
||||
|
||||
### 資料流程
|
||||
|
||||
@ -638,6 +632,39 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
- 添加主題識別與記憶功能
|
||||
- 探索多輪對話中的上下文理解能力
|
||||
|
||||
## 最近改進(2025-05-13)
|
||||
|
||||
### 遊戲監控模組重構
|
||||
|
||||
- **目的**:將遊戲監控功能從獨立的 `game_monitor.py` 腳本重構為一個更健壯、更易於管理的 `game_manager.py` 模組,並由 `Setup.py` 統一控制其生命週期和配置。
|
||||
- **`game_manager.py` (新模組)**:
|
||||
- 創建了 `GameMonitor` 類,封裝了所有遊戲視窗監控、自動重啟和進程管理邏輯。
|
||||
- 提供了 `create_game_monitor` 工廠函數。
|
||||
- 支持通過構造函數和 `update_config` 方法進行配置。
|
||||
- 使用回調函數 (`callback`) 與調用者(即 `Setup.py`)通信,例如在遊戲重啟完成時。
|
||||
- 保留了獨立運行模式,以便在直接執行時仍能工作(主要用於測試或舊版兼容)。
|
||||
- 程式碼註解和日誌訊息已更新為英文。
|
||||
- **`Setup.py` (修改)**:
|
||||
- 導入 `game_manager`。
|
||||
- 在 `WolfChatSetup` 類的 `__init__` 方法中初始化 `self.game_monitor = None`。
|
||||
- 在 `start_managed_session` 方法中:
|
||||
- 創建 `game_monitor_callback` 函數以處理來自 `GameMonitor` 的動作(特別是 `restart_complete`)。
|
||||
- 使用 `game_manager.create_game_monitor` 創建 `GameMonitor` 實例。
|
||||
- 啟動 `GameMonitor`。
|
||||
- 新增 `_handle_game_restart_complete` 方法,用於在收到 `GameMonitor` 的重啟完成回調後,處理機器人的重啟。
|
||||
- 在 `stop_managed_session` 方法中,調用 `self.game_monitor.stop()` 並釋放實例。
|
||||
- 修改 `_restart_game_managed` 方法,使其在 `self.game_monitor` 存在且運行時,調用 `self.game_monitor.restart_now()` 來執行遊戲重啟。
|
||||
- 在 `save_settings` 方法中,如果 `self.game_monitor` 實例存在,則調用其 `update_config` 方法以更新運行時配置。
|
||||
- **`main.py` (修改)**:
|
||||
- 移除了所有對舊 `game_monitor.py` 的導入、子進程啟動、訊號讀取和生命週期管理相關的程式碼。遊戲監控現在完全由 `Setup.py` 在受管會話模式下處理。
|
||||
- **舊檔案刪除**:
|
||||
- 刪除了原來的 `game_monitor.py` 文件。
|
||||
- **效果**:
|
||||
- 遊戲監控邏輯更加內聚和模塊化。
|
||||
- `Setup.py` 現在完全控制遊戲監控的啟動、停止和配置,簡化了 `main.py` 的職責。
|
||||
- 通過回調機制實現了更清晰的模塊間通信。
|
||||
- 提高了程式碼的可維護性和可擴展性。
|
||||
|
||||
### 注意事項
|
||||
|
||||
1. **圖像模板**:確保所有必要的 UI 元素模板都已截圖並放置在 templates 目錄
|
||||
@ -741,3 +768,42 @@ ClaudeCode.md
|
||||
# Current Mode
|
||||
ACT MODE
|
||||
</environment_details>
|
||||
|
||||
</file_content>
|
||||
|
||||
Now that you have the latest state of the file, try the operation again with fewer, more precise SEARCH blocks. For large files especially, it may be prudent to try to limit yourself to <5 SEARCH/REPLACE blocks at a time, then wait for the user to respond with the result of the operation before following up with another replace_in_file call to make additional edits.
|
||||
(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback.)
|
||||
</error><environment_details>
|
||||
# VSCode Visible Files
|
||||
ClaudeCode.md
|
||||
|
||||
# VSCode Open Tabs
|
||||
config_template.py
|
||||
test/llm_debug_script.py
|
||||
llm_interaction.py
|
||||
wolf_control.py
|
||||
.gitignore
|
||||
chroma_client.py
|
||||
batch_memory_record.py
|
||||
memory_manager.py
|
||||
game_monitor.py
|
||||
game_manager.py
|
||||
Setup.py
|
||||
main.py
|
||||
ClaudeCode.md
|
||||
reembedding tool.py
|
||||
config.py
|
||||
memory_backup.py
|
||||
tools/chroma_view.py
|
||||
ui_interaction.py
|
||||
remote_config.json
|
||||
|
||||
# Current Time
|
||||
5/13/2025, 3:31:34 AM (Asia/Taipei, UTC+8:00)
|
||||
|
||||
# Context Window Usage
|
||||
429,724 / 1,048.576K tokens used (41%)
|
||||
|
||||
# Current Mode
|
||||
ACT MODE
|
||||
</environment_details>
|
||||
|
||||
88
Setup.py
88
Setup.py
@ -29,6 +29,7 @@ import schedule
|
||||
import psutil
|
||||
import random # Added for exponential backoff jitter
|
||||
import urllib3 # Added for SSL warning suppression
|
||||
import game_manager # Added for new game monitoring module
|
||||
try:
|
||||
import socketio
|
||||
HAS_SOCKETIO = True
|
||||
@ -605,6 +606,9 @@ class WolfChatSetup(tk.Tk):
|
||||
# Initialize scheduler process tracker
|
||||
self.scheduler_process = None
|
||||
|
||||
# Initialize game monitor instance (will be created in start_managed_session)
|
||||
self.game_monitor = None
|
||||
|
||||
|
||||
# Set initial states based on loaded data
|
||||
self.update_ui_from_data()
|
||||
@ -747,7 +751,39 @@ class WolfChatSetup(tk.Tk):
|
||||
|
||||
|
||||
# Start Monitoring Thread
|
||||
self._start_monitoring_thread()
|
||||
self._start_monitoring_thread() # This is the old general monitoring thread
|
||||
|
||||
# Initialize and start GameMonitor (new specific game monitor)
|
||||
try:
|
||||
# Create callback function for game_monitor
|
||||
def game_monitor_callback(action):
|
||||
logger.info(f"Received action from game_manager: {action}")
|
||||
if action == "restart_complete":
|
||||
# Schedule _handle_game_restart_complete to run in the main thread
|
||||
self.after(0, self._handle_game_restart_complete)
|
||||
# Add other actions if needed, e.g., "restart_begin", "restart_error"
|
||||
|
||||
# Create GameMonitor instance if it doesn't exist
|
||||
if not self.game_monitor:
|
||||
self.game_monitor = game_manager.create_game_monitor(
|
||||
config_data=self.config_data,
|
||||
remote_data=self.remote_data,
|
||||
logger=logger, # Use the main Setup logger
|
||||
callback=game_monitor_callback
|
||||
)
|
||||
|
||||
# Start the game monitor
|
||||
if self.game_monitor.start(): # Ensure start() returns a boolean
|
||||
logger.info("Game monitor (game_manager) started successfully.")
|
||||
else:
|
||||
logger.error("Failed to start game_manager's GameMonitor.")
|
||||
messagebox.showwarning("Warning", "Game window monitoring (game_manager) could not be started.")
|
||||
# Continue execution, not a fatal error for the whole session
|
||||
|
||||
except Exception as gm_err:
|
||||
logger.exception(f"Error setting up game_manager's GameMonitor: {gm_err}")
|
||||
messagebox.showwarning("Warning", "Failed to initialize game_manager's GameMonitor.")
|
||||
# Continue execution
|
||||
|
||||
# Start Scheduler Thread
|
||||
self._start_scheduler_thread()
|
||||
@ -756,6 +792,23 @@ class WolfChatSetup(tk.Tk):
|
||||
# messagebox.showinfo("Session Started", "Managed bot and game session started. Check console for logs.") # Removed popup
|
||||
logger.info("Managed bot and game session started. Check console for logs.") # Log instead of popup
|
||||
|
||||
def _handle_game_restart_complete(self):
|
||||
"""Handles the callback from GameMonitor when a game restart is complete."""
|
||||
logger.info("Game restart completed (callback from game_manager). Handling bot restart...")
|
||||
try:
|
||||
# Ensure we are in the main thread (already handled by self.after)
|
||||
# Wait a bit for the game to stabilize
|
||||
time.sleep(10)
|
||||
|
||||
logger.info("Restarting bot after game restart (triggered by game_manager)...")
|
||||
if self._restart_bot_managed():
|
||||
logger.info("Bot restarted successfully after game_manager's game restart.")
|
||||
else:
|
||||
logger.error("Failed to restart bot after game_manager's game restart!")
|
||||
messagebox.showwarning("Warning", "Failed to restart bot after game_manager's game restart.")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in _handle_game_restart_complete: {e}")
|
||||
|
||||
def stop_managed_session(self):
|
||||
logger.info("Attempting to stop managed session...")
|
||||
self.keep_monitoring_flag.clear() # Signal threads to stop
|
||||
@ -779,6 +832,18 @@ class WolfChatSetup(tk.Tk):
|
||||
logger.warning("Monitor thread did not stop in time.")
|
||||
self.monitor_thread_instance = None
|
||||
|
||||
# Stop GameMonitor (from game_manager)
|
||||
if self.game_monitor:
|
||||
try:
|
||||
if self.game_monitor.stop(): # Ensure stop() returns a boolean
|
||||
logger.info("Game monitor (game_manager) stopped successfully.")
|
||||
else:
|
||||
logger.warning("Game monitor (game_manager) stop may have failed.")
|
||||
except Exception as gm_err:
|
||||
logger.exception(f"Error stopping game_manager's GameMonitor: {gm_err}")
|
||||
finally:
|
||||
self.game_monitor = None # Release the instance
|
||||
|
||||
self._stop_bot_managed()
|
||||
self._stop_game_managed()
|
||||
|
||||
@ -1063,9 +1128,16 @@ class WolfChatSetup(tk.Tk):
|
||||
|
||||
def _restart_game_managed(self):
|
||||
logger.info("Restarting game (managed)...")
|
||||
self._stop_game_managed()
|
||||
time.sleep(2) # Give it time to fully stop
|
||||
return self._start_game_managed()
|
||||
# If GameMonitor (from game_manager) exists and is running, use it to restart
|
||||
if self.game_monitor and self.game_monitor.running:
|
||||
logger.info("Using game_manager's GameMonitor to restart game.")
|
||||
return self.game_monitor.restart_now()
|
||||
else:
|
||||
# Fallback to the original method if game_monitor is not active
|
||||
logger.info("game_manager's GameMonitor not active, using default method to restart game.")
|
||||
self._stop_game_managed()
|
||||
time.sleep(2) # Give it time to fully stop
|
||||
return self._start_game_managed()
|
||||
|
||||
def _restart_bot_managed(self):
|
||||
logger.info("Restarting bot (managed)...")
|
||||
@ -2415,6 +2487,14 @@ class WolfChatSetup(tk.Tk):
|
||||
generate_config_file(self.config_data, self.env_data)
|
||||
save_remote_config(self.remote_data) # Save remote config
|
||||
|
||||
# If GameMonitor (from game_manager) exists, update its configuration
|
||||
if self.game_monitor:
|
||||
try:
|
||||
self.game_monitor.update_config(self.config_data, self.remote_data)
|
||||
logger.info("Game monitor (game_manager) configuration updated.")
|
||||
except Exception as gm_update_err:
|
||||
logger.error(f"Failed to update game_manager's GameMonitor configuration: {gm_update_err}")
|
||||
|
||||
if show_success_message:
|
||||
messagebox.showinfo("Success", "Settings saved successfully.\nRestart managed session for changes to take effect.")
|
||||
|
||||
|
||||
494
game_manager.py
Normal file
494
game_manager.py
Normal file
@ -0,0 +1,494 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Game Manager Module
|
||||
|
||||
Provides game window monitoring, automatic restart, and process management features.
|
||||
Designed to be imported and controlled by setup.py or other management scripts.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import threading
|
||||
import subprocess
|
||||
import logging
|
||||
import pygetwindow as gw
|
||||
|
||||
# Attempt to import platform-specific modules that might be needed
|
||||
try:
|
||||
import win32gui
|
||||
import win32con
|
||||
HAS_WIN32 = True
|
||||
except ImportError:
|
||||
HAS_WIN32 = False
|
||||
print("Warning: win32gui/win32con modules not installed, some window management features may be unavailable")
|
||||
|
||||
try:
|
||||
import psutil
|
||||
HAS_PSUTIL = True
|
||||
except ImportError:
|
||||
HAS_PSUTIL = False
|
||||
print("Warning: psutil module not installed, process management features may be unavailable")
|
||||
|
||||
|
||||
class GameMonitor:
|
||||
"""
|
||||
Game window monitoring class.
|
||||
Responsible for monitoring game window position, scheduled restarts, and providing window management functions.
|
||||
"""
|
||||
def __init__(self, config_data, remote_data=None, logger=None, callback=None):
|
||||
# Use the provided logger or create a new one
|
||||
self.logger = logger or logging.getLogger("GameMonitor")
|
||||
if not self.logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
self.logger.addHandler(handler)
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
self.config_data = config_data
|
||||
self.remote_data = remote_data or {}
|
||||
self.callback = callback # Callback function to notify the caller
|
||||
|
||||
# Read settings from configuration
|
||||
self.window_title = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("WINDOW_TITLE", "Last War-Survival Game")
|
||||
self.enable_restart = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("ENABLE_SCHEDULED_RESTART", True)
|
||||
self.restart_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("RESTART_INTERVAL_MINUTES", 60)
|
||||
self.game_path = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_EXECUTABLE_PATH", "")
|
||||
self.window_x = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_X", 50)
|
||||
self.window_y = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_Y", 30)
|
||||
self.window_width = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_WIDTH", 600)
|
||||
self.window_height = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_HEIGHT", 1070)
|
||||
self.monitor_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("MONITOR_INTERVAL_SECONDS", 5)
|
||||
|
||||
# Read game process name from remote_data, use default if not found
|
||||
self.game_process_name = self.remote_data.get("GAME_PROCESS_NAME", "LastWar.exe")
|
||||
|
||||
# Internal state
|
||||
self.running = False
|
||||
self.next_restart_time = None
|
||||
self.monitor_thread = None
|
||||
self.stop_event = threading.Event()
|
||||
|
||||
self.logger.info(f"GameMonitor initialized. Game window: '{self.window_title}', Process: '{self.game_process_name}'")
|
||||
self.logger.info(f"Position: ({self.window_x}, {self.window_y}), Size: {self.window_width}x{self.window_height}")
|
||||
self.logger.info(f"Scheduled Restart: {'Enabled' if self.enable_restart else 'Disabled'}, Interval: {self.restart_interval} minutes")
|
||||
|
||||
def start(self):
|
||||
"""Start game window monitoring"""
|
||||
if self.running:
|
||||
self.logger.info("Game window monitoring is already running")
|
||||
return True # Return True if already running
|
||||
|
||||
self.logger.info("Starting game window monitoring...")
|
||||
self.stop_event.clear()
|
||||
|
||||
# Set next restart time
|
||||
if self.enable_restart and self.restart_interval > 0:
|
||||
self.next_restart_time = time.time() + (self.restart_interval * 60)
|
||||
self.logger.info(f"Scheduled restart enabled. First restart in {self.restart_interval} minutes")
|
||||
else:
|
||||
self.next_restart_time = None
|
||||
self.logger.info("Scheduled restart is disabled")
|
||||
|
||||
# Start monitoring thread
|
||||
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self.monitor_thread.start()
|
||||
self.running = True
|
||||
self.logger.info("Game window monitoring started")
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
"""Stop game window monitoring"""
|
||||
if not self.running:
|
||||
self.logger.info("Game window monitoring is not running")
|
||||
return True # Return True if already stopped
|
||||
|
||||
self.logger.info("Stopping game window monitoring...")
|
||||
self.stop_event.set()
|
||||
|
||||
# Wait for monitoring thread to finish
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
self.logger.info("Waiting for monitoring thread to finish...")
|
||||
self.monitor_thread.join(timeout=5)
|
||||
if self.monitor_thread.is_alive():
|
||||
self.logger.warning("Game window monitoring thread did not stop within the timeout period")
|
||||
|
||||
self.running = False
|
||||
self.monitor_thread = None
|
||||
self.logger.info("Game window monitoring stopped")
|
||||
return True
|
||||
|
||||
def _monitor_loop(self):
|
||||
"""Main monitoring loop"""
|
||||
self.logger.info("Game window monitoring loop started")
|
||||
last_adjustment_message = "" # Avoid logging repetitive adjustment messages
|
||||
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
# Check for scheduled restart
|
||||
if self.next_restart_time and time.time() >= self.next_restart_time:
|
||||
self.logger.info("Scheduled restart time reached. Performing restart...")
|
||||
self._perform_restart()
|
||||
# Reset next restart time
|
||||
self.next_restart_time = time.time() + (self.restart_interval * 60)
|
||||
self.logger.info(f"Restart timer reset. Next restart in {self.restart_interval} minutes")
|
||||
# Continue to next loop iteration
|
||||
time.sleep(self.monitor_interval)
|
||||
continue
|
||||
|
||||
# Find game window
|
||||
window = self._find_game_window()
|
||||
adjustment_made = False
|
||||
current_message = ""
|
||||
|
||||
if window:
|
||||
try:
|
||||
# Use win32gui functions only on Windows
|
||||
if HAS_WIN32:
|
||||
# Get window handle
|
||||
hwnd = window._hWnd
|
||||
|
||||
# 1. Check and adjust position/size
|
||||
current_pos = (window.left, window.top)
|
||||
current_size = (window.width, window.height)
|
||||
target_pos = (self.window_x, self.window_y)
|
||||
target_size = (self.window_width, self.window_height)
|
||||
|
||||
if current_pos != target_pos or current_size != target_size:
|
||||
window.moveTo(target_pos[0], target_pos[1])
|
||||
window.resizeTo(target_size[0], target_size[1])
|
||||
# Verify if move and resize were successful
|
||||
time.sleep(0.1)
|
||||
window.activate() # Try activating to ensure changes apply
|
||||
time.sleep(0.1)
|
||||
new_pos = (window.left, window.top)
|
||||
new_size = (window.width, window.height)
|
||||
if new_pos == target_pos and new_size == target_size:
|
||||
current_message += f"Adjusted game window to position ({target_pos[0]},{target_pos[1]}) size {target_size[0]}x{target_size[1]}. "
|
||||
adjustment_made = True
|
||||
else:
|
||||
self.logger.warning(f"Attempted to adjust window pos/size, but result mismatch. Target: {target_pos}/{target_size}, Actual: {new_pos}/{new_size}")
|
||||
|
||||
|
||||
# 2. Check and bring to foreground
|
||||
current_foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
|
||||
if current_foreground_hwnd != hwnd:
|
||||
try:
|
||||
# Use HWND_TOP to bring window to top, not HWND_TOPMOST
|
||||
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
|
||||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
||||
|
||||
# Set as foreground window (gain focus)
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
|
||||
# Verify if window is active
|
||||
time.sleep(0.1)
|
||||
foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
|
||||
if foreground_hwnd == hwnd:
|
||||
current_message += "Brought game window to foreground and set focus. "
|
||||
adjustment_made = True
|
||||
else:
|
||||
# Use fallback method
|
||||
self.logger.warning("SetForegroundWindow failed, trying fallback window.activate()")
|
||||
try:
|
||||
window.activate()
|
||||
time.sleep(0.1)
|
||||
if win32gui.GetForegroundWindow() == hwnd:
|
||||
current_message += "Set game window focus using fallback method. "
|
||||
adjustment_made = True
|
||||
except Exception as activate_err:
|
||||
self.logger.warning(f"Fallback method window.activate() failed: {activate_err}")
|
||||
except Exception as focus_err:
|
||||
self.logger.warning(f"Error setting window focus: {focus_err}")
|
||||
else:
|
||||
# Use basic functions on non-Windows platforms
|
||||
current_pos = (window.left, window.top)
|
||||
current_size = (window.width, window.height)
|
||||
target_pos = (self.window_x, self.window_y)
|
||||
target_size = (self.window_width, self.window_height)
|
||||
|
||||
if current_pos != target_pos or current_size != target_size:
|
||||
window.moveTo(target_pos[0], target_pos[1])
|
||||
window.resizeTo(target_size[0], target_size[1])
|
||||
current_message += f"Adjusted game window to position {target_pos} size {target_size[0]}x{target_size[1]}. "
|
||||
adjustment_made = True
|
||||
|
||||
# Try activating the window (may have limited effect on non-Windows)
|
||||
try:
|
||||
window.activate()
|
||||
current_message += "Attempted to activate game window. "
|
||||
adjustment_made = True
|
||||
except Exception as activate_err:
|
||||
self.logger.warning(f"Error activating window: {activate_err}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error while monitoring game window: {e}")
|
||||
|
||||
# Log only if adjustments were made and the message changed
|
||||
if adjustment_made and current_message and current_message != last_adjustment_message:
|
||||
self.logger.info(f"[GameMonitor] {current_message.strip()}")
|
||||
last_adjustment_message = current_message
|
||||
elif not window:
|
||||
# Reset last message if window disappears
|
||||
last_adjustment_message = ""
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in monitoring loop: {e}")
|
||||
|
||||
# Wait for the next check
|
||||
time.sleep(self.monitor_interval)
|
||||
|
||||
self.logger.info("Game window monitoring loop finished")
|
||||
|
||||
def _find_game_window(self):
|
||||
"""Find the game window with the specified title"""
|
||||
try:
|
||||
windows = gw.getWindowsWithTitle(self.window_title)
|
||||
if windows:
|
||||
return windows[0]
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error finding game window: {e}")
|
||||
return None
|
||||
|
||||
def _find_game_process(self):
|
||||
"""Find the game process"""
|
||||
if not HAS_PSUTIL:
|
||||
self.logger.warning("psutil is not available, cannot perform process lookup")
|
||||
return None
|
||||
|
||||
try:
|
||||
for proc in psutil.process_iter(['pid', 'name', 'exe']):
|
||||
try:
|
||||
proc_info = proc.info
|
||||
proc_name = proc_info.get('name')
|
||||
|
||||
if proc_name and proc_name.lower() == self.game_process_name.lower():
|
||||
self.logger.info(f"Found game process '{proc_name}' (PID: {proc.pid})")
|
||||
return proc
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
continue
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finding game process: {e}")
|
||||
|
||||
self.logger.info(f"Game process '{self.game_process_name}' not found")
|
||||
return None
|
||||
|
||||
def _perform_restart(self):
|
||||
"""Execute the game restart process"""
|
||||
self.logger.info("Starting game restart process")
|
||||
|
||||
try:
|
||||
# 1. Notify that restart has begun (optional)
|
||||
if self.callback:
|
||||
self.callback("restart_begin")
|
||||
|
||||
# 2. Terminate existing game process
|
||||
self._terminate_game_process()
|
||||
time.sleep(2) # Short wait to ensure process termination
|
||||
|
||||
# 3. Start new game process
|
||||
if self._start_game_process():
|
||||
self.logger.info("Game restarted successfully")
|
||||
else:
|
||||
self.logger.error("Failed to start game")
|
||||
|
||||
# 4. Wait for game to launch
|
||||
restart_wait_time = 30 # seconds
|
||||
self.logger.info(f"Waiting for game to start ({restart_wait_time} seconds)...")
|
||||
time.sleep(restart_wait_time)
|
||||
|
||||
# 5. Notify restart completion
|
||||
self.logger.info("Game restart process completed, sending notification")
|
||||
if self.callback:
|
||||
self.callback("restart_complete")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during game restart process: {e}")
|
||||
# Attempt to notify error
|
||||
if self.callback:
|
||||
self.callback("restart_error")
|
||||
return False
|
||||
|
||||
def _terminate_game_process(self):
|
||||
"""Terminate the game process"""
|
||||
self.logger.info(f"Attempting to terminate game process '{self.game_process_name}'")
|
||||
|
||||
if not HAS_PSUTIL:
|
||||
self.logger.warning("psutil is not available, cannot terminate process")
|
||||
return False
|
||||
|
||||
process = self._find_game_process()
|
||||
terminated = False
|
||||
|
||||
if process:
|
||||
try:
|
||||
self.logger.info(f"Found game process PID: {process.pid}, terminating...")
|
||||
process.terminate()
|
||||
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
self.logger.info(f"Process {process.pid} terminated successfully (terminate)")
|
||||
terminated = True
|
||||
except psutil.TimeoutExpired:
|
||||
self.logger.warning(f"Process {process.pid} did not terminate within 5s (terminate), attempting force kill")
|
||||
process.kill()
|
||||
process.wait(timeout=5)
|
||||
self.logger.info(f"Process {process.pid} force killed (kill)")
|
||||
terminated = True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error terminating process: {e}")
|
||||
else:
|
||||
self.logger.warning(f"No running process found with name '{self.game_process_name}'")
|
||||
|
||||
return terminated
|
||||
|
||||
def _start_game_process(self):
|
||||
"""Start the game process"""
|
||||
if not self.game_path:
|
||||
self.logger.error("Game executable path not set, cannot start")
|
||||
return False
|
||||
|
||||
self.logger.info(f"Starting game: {self.game_path}")
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.startfile(self.game_path)
|
||||
self.logger.info("Called os.startfile to launch game")
|
||||
return True
|
||||
else:
|
||||
# Use subprocess.Popen for non-Windows platforms
|
||||
# Ensure it runs detached if possible, or handle appropriately
|
||||
subprocess.Popen([self.game_path], start_new_session=True) # Attempt detached start
|
||||
self.logger.info("Called subprocess.Popen to launch game")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
self.logger.error(f"Startup error: Game launcher '{self.game_path}' not found")
|
||||
except OSError as ose:
|
||||
self.logger.error(f"Startup error (OSError): {ose} - Check path and permissions", exc_info=True)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error starting game: {e}", exc_info=True)
|
||||
|
||||
return False
|
||||
|
||||
def restart_now(self):
|
||||
"""Perform an immediate restart"""
|
||||
self.logger.info("Manually triggering game restart")
|
||||
result = self._perform_restart()
|
||||
|
||||
# Reset the timer if scheduled restart is enabled
|
||||
if self.enable_restart and self.restart_interval > 0:
|
||||
self.next_restart_time = time.time() + (self.restart_interval * 60)
|
||||
self.logger.info(f"Restart timer reset. Next restart in {self.restart_interval} minutes")
|
||||
|
||||
return result
|
||||
|
||||
def update_config(self, config_data=None, remote_data=None):
|
||||
"""Update configuration settings"""
|
||||
if config_data:
|
||||
old_config = self.config_data
|
||||
self.config_data = config_data
|
||||
|
||||
# Update key settings
|
||||
self.window_title = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("WINDOW_TITLE", self.window_title)
|
||||
self.enable_restart = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("ENABLE_SCHEDULED_RESTART", self.enable_restart)
|
||||
self.restart_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("RESTART_INTERVAL_MINUTES", self.restart_interval)
|
||||
self.game_path = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_EXECUTABLE_PATH", self.game_path)
|
||||
self.window_x = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_X", self.window_x)
|
||||
self.window_y = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_Y", self.window_y)
|
||||
self.window_width = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_WIDTH", self.window_width)
|
||||
self.window_height = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_HEIGHT", self.window_height)
|
||||
self.monitor_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("MONITOR_INTERVAL_SECONDS", self.monitor_interval)
|
||||
|
||||
# Reset scheduled restart timer if parameters changed
|
||||
if self.running and self.enable_restart and self.restart_interval > 0:
|
||||
old_interval = old_config.get("GAME_WINDOW_CONFIG", {}).get("RESTART_INTERVAL_MINUTES", 60)
|
||||
if self.restart_interval != old_interval:
|
||||
self.next_restart_time = time.time() + (self.restart_interval * 60)
|
||||
self.logger.info(f"Restart interval updated to {self.restart_interval} minutes, next restart reset")
|
||||
|
||||
if remote_data:
|
||||
self.remote_data = remote_data
|
||||
old_process_name = self.game_process_name
|
||||
self.game_process_name = self.remote_data.get("GAME_PROCESS_NAME", old_process_name)
|
||||
if self.game_process_name != old_process_name:
|
||||
self.logger.info(f"Game process name updated to '{self.game_process_name}'")
|
||||
|
||||
self.logger.info("GameMonitor configuration updated")
|
||||
|
||||
|
||||
# Provide simple external API functions
|
||||
def create_game_monitor(config_data, remote_data=None, logger=None, callback=None):
|
||||
"""Create a game monitor instance"""
|
||||
return GameMonitor(config_data, remote_data, logger, callback)
|
||||
|
||||
def stop_all_monitors():
|
||||
"""Attempt to stop all created monitors (global cleanup)"""
|
||||
# This function could be implemented if instance references are stored.
|
||||
# In the current design, each monitor needs to be stopped individually.
|
||||
pass
|
||||
|
||||
|
||||
# Functionality when run standalone (similar to original game_monitor.py)
|
||||
if __name__ == "__main__":
|
||||
# Set up basic logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("GameManagerStandalone")
|
||||
|
||||
# Load settings from config.py
|
||||
try:
|
||||
import config
|
||||
logger.info("Loaded config.py")
|
||||
|
||||
# Build basic configuration dictionary
|
||||
config_data = {
|
||||
"GAME_WINDOW_CONFIG": {
|
||||
"WINDOW_TITLE": config.WINDOW_TITLE,
|
||||
"ENABLE_SCHEDULED_RESTART": config.ENABLE_SCHEDULED_RESTART,
|
||||
"RESTART_INTERVAL_MINUTES": config.RESTART_INTERVAL_MINUTES,
|
||||
"GAME_EXECUTABLE_PATH": config.GAME_EXECUTABLE_PATH,
|
||||
"GAME_WINDOW_X": config.GAME_WINDOW_X,
|
||||
"GAME_WINDOW_Y": config.GAME_WINDOW_Y,
|
||||
"GAME_WINDOW_WIDTH": config.GAME_WINDOW_WIDTH,
|
||||
"GAME_WINDOW_HEIGHT": config.GAME_WINDOW_HEIGHT,
|
||||
"MONITOR_INTERVAL_SECONDS": config.MONITOR_INTERVAL_SECONDS
|
||||
}
|
||||
}
|
||||
|
||||
# Define a callback for standalone execution
|
||||
def standalone_callback(action):
|
||||
"""Send JSON signal via standard output"""
|
||||
logger.info(f"Sending signal: {action}")
|
||||
signal_data = {'action': action}
|
||||
try:
|
||||
json_signal = json.dumps(signal_data)
|
||||
print(json_signal, flush=True)
|
||||
logger.info(f"Signal sent: {action}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send signal '{action}': {e}")
|
||||
|
||||
# Create and start the monitor
|
||||
monitor = GameMonitor(config_data, logger=logger, callback=standalone_callback)
|
||||
monitor.start()
|
||||
|
||||
# Keep the program running
|
||||
try:
|
||||
logger.info("Game monitoring started. Press Ctrl+C to stop.")
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Ctrl+C received, stopping...")
|
||||
finally:
|
||||
monitor.stop()
|
||||
logger.info("Game monitoring stopped")
|
||||
|
||||
except ImportError:
|
||||
logger.error("Could not load config.py. Ensure it exists and contains necessary settings.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting game monitoring: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
303
game_monitor.py
303
game_monitor.py
@ -1,303 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Game Window Monitor Module
|
||||
|
||||
Continuously monitors the game window specified in the config,
|
||||
ensuring it stays at the configured position, size, and remains topmost.
|
||||
"""
|
||||
|
||||
import time
|
||||
import datetime # Added
|
||||
import subprocess # Added
|
||||
import psutil # Added
|
||||
import sys # Added
|
||||
import json # Added
|
||||
import os # Added for basename
|
||||
import pygetwindow as gw
|
||||
import win32gui
|
||||
import win32con
|
||||
import config
|
||||
import logging
|
||||
# import multiprocessing # Keep for Pipe/Queue if needed later, though using stdio now
|
||||
# NOTE: config.py should handle dotenv loading. This script only imports values.
|
||||
|
||||
# --- Setup Logging ---
|
||||
monitor_logger = logging.getLogger('GameMonitor')
|
||||
monitor_logger.setLevel(logging.INFO) # Set level for the logger
|
||||
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
# Create handler for stderr
|
||||
stderr_handler = logging.StreamHandler(sys.stderr) # Explicitly use stderr
|
||||
stderr_handler.setFormatter(log_formatter)
|
||||
# Add handler to the logger
|
||||
if not monitor_logger.hasHandlers(): # Avoid adding multiple handlers if run multiple times
|
||||
monitor_logger.addHandler(stderr_handler)
|
||||
monitor_logger.propagate = False # Prevent propagation to root logger if basicConfig was called elsewhere
|
||||
|
||||
# --- Helper Functions ---
|
||||
|
||||
def restart_game_process():
|
||||
"""Finds and terminates the existing game process, then restarts it."""
|
||||
monitor_logger.info("嘗試重啟遊戲進程。(Attempting to restart game process.)")
|
||||
game_path = config.GAME_EXECUTABLE_PATH
|
||||
if not game_path or not os.path.exists(os.path.dirname(game_path)): # Basic check
|
||||
monitor_logger.error(f"遊戲執行檔路徑 '{game_path}' 無效或目錄不存在,無法重啟。(Game executable path '{game_path}' is invalid or directory does not exist, cannot restart.)")
|
||||
return
|
||||
|
||||
target_process_name = "LastWar.exe" # Correct process name
|
||||
launcher_path = config.GAME_EXECUTABLE_PATH # Keep launcher path for restarting
|
||||
monitor_logger.info(f"尋找名稱為 '{target_process_name}' 的遊戲進程。(Looking for game process named '{target_process_name}')")
|
||||
|
||||
terminated = False
|
||||
process_found = False
|
||||
for proc in psutil.process_iter(['pid', 'name', 'exe']):
|
||||
try:
|
||||
proc_info = proc.info
|
||||
proc_name = proc_info.get('name')
|
||||
|
||||
if proc_name == target_process_name:
|
||||
process_found = True
|
||||
monitor_logger.info(f"找到遊戲進程 PID: {proc_info['pid']},名稱: {proc_name}。正在終止...(Found game process PID: {proc_info['pid']}, Name: {proc_name}. Terminating...)")
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
monitor_logger.info(f"進程 {proc_info['pid']} 已成功終止 (terminate)。(Process {proc_info['pid']} terminated successfully (terminate).)")
|
||||
terminated = True
|
||||
except psutil.TimeoutExpired:
|
||||
monitor_logger.warning(f"進程 {proc_info['pid']} 未能在 5 秒內終止 (terminate),嘗試強制結束 (kill)。(Process {proc_info['pid']} did not terminate in 5s (terminate), attempting kill.)")
|
||||
proc.kill()
|
||||
proc.wait(timeout=5) # Wait for kill with timeout
|
||||
monitor_logger.info(f"進程 {proc_info['pid']} 已強制結束 (kill)。(Process {proc_info['pid']} killed.)")
|
||||
terminated = True
|
||||
except Exception as wait_kill_err:
|
||||
monitor_logger.error(f"等待進程 {proc_info['pid']} 強制結束時出錯: {wait_kill_err}", exc_info=False)
|
||||
|
||||
# Removed Termination Verification - Rely on main loop for eventual state correction
|
||||
monitor_logger.info(f"已處理匹配的進程 PID: {proc_info['pid']},停止搜索。(Processed matching process PID: {proc_info['pid']}, stopping search.)")
|
||||
break # Exit the loop once a process is handled
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass # Process might have already exited, access denied, or is a zombie
|
||||
except Exception as e:
|
||||
pid_str = proc.pid if hasattr(proc, 'pid') else 'N/A'
|
||||
monitor_logger.error(f"檢查或終止進程 PID:{pid_str} 時出錯: {e}", exc_info=False)
|
||||
|
||||
if process_found and not terminated:
|
||||
monitor_logger.error("找到遊戲進程但未能成功終止它。(Found game process but failed to terminate it successfully.)")
|
||||
elif not process_found:
|
||||
monitor_logger.warning(f"未找到名稱為 '{target_process_name}' 的正在運行的進程。(No running process named '{target_process_name}' was found.)")
|
||||
|
||||
# Wait a moment before restarting, use the launcher path from config
|
||||
time.sleep(2)
|
||||
if not launcher_path or not os.path.exists(os.path.dirname(launcher_path)):
|
||||
monitor_logger.error(f"遊戲啟動器路徑 '{launcher_path}' 無效或目錄不存在,無法啟動。(Game launcher path '{launcher_path}' is invalid or directory does not exist, cannot launch.)")
|
||||
return
|
||||
|
||||
monitor_logger.info(f"正在使用啟動器啟動遊戲: {launcher_path} (Launching game using launcher: {launcher_path})")
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.startfile(launcher_path)
|
||||
monitor_logger.info("已調用 os.startfile 啟動遊戲。(os.startfile called to launch game.)")
|
||||
else:
|
||||
subprocess.Popen([launcher_path])
|
||||
monitor_logger.info("已調用 subprocess.Popen 啟動遊戲。(subprocess.Popen called to launch game.)")
|
||||
except FileNotFoundError:
|
||||
monitor_logger.error(f"啟動錯誤:找不到遊戲啟動器 '{launcher_path}'。(Launch Error: Game launcher not found at '{launcher_path}'.)")
|
||||
except OSError as ose:
|
||||
monitor_logger.error(f"啟動錯誤 (OSError): {ose} - 檢查路徑和權限。(Launch Error (OSError): {ose} - Check path and permissions.)", exc_info=True)
|
||||
except Exception as e:
|
||||
monitor_logger.error(f"啟動遊戲時發生未預期錯誤: {e}", exc_info=True)
|
||||
# Don't return False here, let the process continue to send resume signal
|
||||
# Removed Startup Verification - Rely on main loop for eventual state correction
|
||||
# Always return True (or nothing) to indicate the attempt was made
|
||||
return # Or return True, doesn't matter much now
|
||||
|
||||
def perform_scheduled_restart():
|
||||
"""Handles the sequence of pausing UI, restarting game, resuming UI."""
|
||||
monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)")
|
||||
|
||||
# Removed pause_ui signal - UI will handle its own pause/resume based on restart_complete
|
||||
|
||||
try:
|
||||
# 1. Attempt to restart the game (no verification)
|
||||
monitor_logger.info("嘗試執行遊戲重啟。(Attempting game restart process.)")
|
||||
restart_game_process() # Fire and forget restart attempt
|
||||
monitor_logger.info("遊戲重啟嘗試已執行。(Game restart attempt executed.)")
|
||||
|
||||
# 2. Wait fixed time after restart attempt
|
||||
monitor_logger.info("等待 30 秒讓遊戲啟動(無驗證)。(Waiting 30 seconds for game to launch (no verification)...)")
|
||||
time.sleep(30) # Fixed wait
|
||||
|
||||
except Exception as restart_err:
|
||||
monitor_logger.error(f"執行 restart_game_process 時發生未預期錯誤: {restart_err}", exc_info=True)
|
||||
# Continue to finally block even on error
|
||||
|
||||
finally:
|
||||
# 3. Signal main process that restart attempt is complete via stdout
|
||||
monitor_logger.info("發送重啟完成訊號。(Sending restart complete signal.)")
|
||||
restart_complete_signal_data = {'action': 'restart_complete'}
|
||||
try:
|
||||
json_signal = json.dumps(restart_complete_signal_data)
|
||||
print(json_signal, flush=True)
|
||||
monitor_logger.info("已發送重啟完成訊號。(Sent restart complete signal.)")
|
||||
except Exception as e:
|
||||
monitor_logger.error(f"發送重啟完成訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
|
||||
|
||||
monitor_logger.info("定時重啟流程(包括 finally 塊)執行完畢。(Scheduled restart sequence (including finally block) finished.)")
|
||||
# Configure logger (basic example, adjust as needed)
|
||||
# (Logging setup moved earlier)
|
||||
|
||||
def find_game_window(title=config.WINDOW_TITLE):
|
||||
"""Attempts to find the game window by its title."""
|
||||
try:
|
||||
windows = gw.getWindowsWithTitle(title)
|
||||
if windows:
|
||||
return windows[0]
|
||||
except Exception as e:
|
||||
# Log errors if a logger was configured
|
||||
# monitor_logger.error(f"Error finding window '{title}': {e}")
|
||||
pass # Keep silent if window not found during normal check
|
||||
return None
|
||||
|
||||
def monitor_game_window():
|
||||
"""The main monitoring loop. Now runs directly, not in a thread."""
|
||||
monitor_logger.info("遊戲視窗監控腳本已啟動。(Game window monitoring script started.)")
|
||||
last_adjustment_message = "" # Track last message to avoid spam
|
||||
next_restart_time = None
|
||||
|
||||
# Initialize scheduled restart timer if enabled
|
||||
if config.ENABLE_SCHEDULED_RESTART and config.RESTART_INTERVAL_MINUTES > 0:
|
||||
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
|
||||
next_restart_time = time.time() + interval_seconds
|
||||
monitor_logger.info(f"已啟用定時重啟,首次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Scheduled restart enabled. First restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
|
||||
else:
|
||||
monitor_logger.info("未啟用定時重啟功能。(Scheduled restart is disabled.)")
|
||||
|
||||
|
||||
while True: # Run indefinitely until terminated externally
|
||||
# --- Scheduled Restart Check ---
|
||||
if next_restart_time and time.time() >= next_restart_time:
|
||||
monitor_logger.info("到達預定重啟時間。(Scheduled restart time reached.)")
|
||||
perform_scheduled_restart()
|
||||
# Reset timer for the next interval
|
||||
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
|
||||
next_restart_time = time.time() + interval_seconds
|
||||
monitor_logger.info(f"重啟計時器已重置,下次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Restart timer reset. Next restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
|
||||
# Continue to next loop iteration after restart sequence
|
||||
time.sleep(config.MONITOR_INTERVAL_SECONDS) # Add a small delay before next check
|
||||
continue
|
||||
|
||||
# --- Regular Window Monitoring ---
|
||||
window = find_game_window()
|
||||
adjustment_made = False
|
||||
current_message = ""
|
||||
|
||||
if window:
|
||||
try:
|
||||
hwnd = window._hWnd # Get the window handle for win32 functions
|
||||
|
||||
# 1. Check and Adjust Position/Size
|
||||
current_pos = (window.left, window.top)
|
||||
current_size = (window.width, window.height)
|
||||
target_pos = (config.GAME_WINDOW_X, config.GAME_WINDOW_Y)
|
||||
target_size = (config.GAME_WINDOW_WIDTH, config.GAME_WINDOW_HEIGHT)
|
||||
|
||||
if current_pos != target_pos or current_size != target_size:
|
||||
window.moveTo(target_pos[0], target_pos[1])
|
||||
window.resizeTo(target_size[0], target_size[1])
|
||||
# Verify if move/resize was successful before logging
|
||||
time.sleep(0.1) # Give window time to adjust
|
||||
window.activate() # Bring window to foreground before checking again
|
||||
time.sleep(0.1)
|
||||
new_pos = (window.left, window.top)
|
||||
new_size = (window.width, window.height)
|
||||
if new_pos == target_pos and new_size == target_size:
|
||||
current_message += f"已將遊戲視窗調整至位置 ({target_pos[0]},{target_pos[1]}) 大小 {target_size[0]}x{target_size[1]}。(Adjusted game window to position {target_pos} size {target_size}.) "
|
||||
adjustment_made = True
|
||||
else:
|
||||
# Log failure if needed
|
||||
# monitor_logger.warning(f"Failed to adjust window. Current: {new_pos} {new_size}, Target: {target_pos} {target_size}")
|
||||
pass # Keep silent on failure for now
|
||||
|
||||
# 2. Check and Bring to Foreground/Activate (Improved Logic)
|
||||
current_foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
|
||||
if current_foreground_hwnd != hwnd:
|
||||
try:
|
||||
# Use HWND_TOP instead of HWND_TOPMOST to bring it above others without forcing always-on-top
|
||||
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
|
||||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
||||
|
||||
# Make window the foreground window (with focus)
|
||||
# Note: This might still fail due to Windows foreground restrictions
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
|
||||
# Verify window is active by checking foreground window
|
||||
time.sleep(0.1) # Brief pause to let operation complete
|
||||
foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
|
||||
if foreground_hwnd == hwnd:
|
||||
current_message += "已將遊戲視窗提升到前景並設為焦點。(Brought game window to foreground with focus.) "
|
||||
adjustment_made = True
|
||||
else:
|
||||
# Optional: Add a fallback for versions of Windows with stricter foreground rules
|
||||
monitor_logger.warning("SetForegroundWindow 未能成功,嘗試備用方法 window.activate()。(SetForegroundWindow failed, trying fallback window.activate())")
|
||||
try:
|
||||
window.activate() # Try pygetwindow's activate method as backup
|
||||
time.sleep(0.1) # Pause after activate
|
||||
if win32gui.GetForegroundWindow() == hwnd:
|
||||
current_message += "已透過備用方法將遊戲視窗設為焦點。(Set game window focus via fallback method.) "
|
||||
adjustment_made = True
|
||||
else:
|
||||
monitor_logger.warning("備用方法 window.activate() 也未能成功。(Fallback window.activate() also failed.)")
|
||||
except Exception as activate_err:
|
||||
monitor_logger.warning(f"備用方法 window.activate() 出錯: {activate_err}")
|
||||
|
||||
except Exception as focus_err:
|
||||
# Log errors during the focus attempt
|
||||
monitor_logger.warning(f"設置視窗焦點時出錯: {focus_err}")
|
||||
|
||||
except gw.PyGetWindowException as e:
|
||||
# Log PyGetWindowException specifically, might indicate window closed during check
|
||||
monitor_logger.warning(f"監控循環中無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties in monitor loop (may be closed): {e})")
|
||||
except Exception as e:
|
||||
# Log other exceptions during monitoring
|
||||
monitor_logger.error(f"監控遊戲視窗時發生未預期錯誤: {e} (Unexpected error during game window monitoring: {e})", exc_info=True)
|
||||
|
||||
# Log adjustment message only if an adjustment was made and it's different from the last one
|
||||
# This should NOT print JSON signals
|
||||
if adjustment_made and current_message and current_message != last_adjustment_message:
|
||||
# Log the adjustment message instead of printing to stdout
|
||||
monitor_logger.info(f"[GameMonitor] {current_message.strip()}")
|
||||
last_adjustment_message = current_message
|
||||
elif not window:
|
||||
# Reset last message if window disappears
|
||||
last_adjustment_message = ""
|
||||
|
||||
# Wait before the next check
|
||||
time.sleep(config.MONITOR_INTERVAL_SECONDS)
|
||||
|
||||
# This part is theoretically unreachable in the new design as the loop is infinite
|
||||
# and termination is handled externally by the parent process (main.py).
|
||||
# monitor_logger.info("遊戲視窗監控腳本已停止。(Game window monitoring script stopped.)")
|
||||
|
||||
|
||||
# Example usage (if run directly)
|
||||
if __name__ == '__main__':
|
||||
monitor_logger.info("直接運行 game_monitor.py。(Running game_monitor.py directly.)")
|
||||
monitor_logger.info(f"將監控標題為 '{config.WINDOW_TITLE}' 的視窗。(Will monitor window with title '{config.WINDOW_TITLE}')")
|
||||
monitor_logger.info(f"目標位置: ({config.GAME_WINDOW_X}, {config.GAME_WINDOW_Y}), 目標大小: {config.GAME_WINDOW_WIDTH}x{config.GAME_WINDOW_HEIGHT}")
|
||||
monitor_logger.info(f"檢查間隔: {config.MONITOR_INTERVAL_SECONDS} 秒。(Check interval: {config.MONITOR_INTERVAL_SECONDS} seconds.)")
|
||||
if config.ENABLE_SCHEDULED_RESTART:
|
||||
monitor_logger.info(f"定時重啟已啟用,間隔: {config.RESTART_INTERVAL_MINUTES} 分鐘。(Scheduled restart enabled, interval: {config.RESTART_INTERVAL_MINUTES} minutes.)")
|
||||
else:
|
||||
monitor_logger.info("定時重啟已禁用。(Scheduled restart disabled.)")
|
||||
monitor_logger.info("腳本將持續運行,請從啟動它的終端使用 Ctrl+C 或由父進程終止。(Script will run continuously. Stop with Ctrl+C from the launching terminal or termination by parent process.)")
|
||||
|
||||
try:
|
||||
monitor_game_window() # Start the main loop directly
|
||||
except KeyboardInterrupt:
|
||||
monitor_logger.info("收到 Ctrl+C,正在退出...(Received Ctrl+C, exiting...)")
|
||||
except Exception as e:
|
||||
monitor_logger.critical(f"監控過程中發生致命錯誤: {e}", exc_info=True)
|
||||
sys.exit(1) # Exit with error code
|
||||
finally:
|
||||
monitor_logger.info("Game Monitor 腳本執行完畢。(Game Monitor script finished.)")
|
||||
152
main.py
152
main.py
@ -30,7 +30,6 @@ import llm_interaction
|
||||
# Import UI module
|
||||
import ui_interaction
|
||||
import chroma_client
|
||||
# import game_monitor # No longer importing, will run as subprocess
|
||||
import subprocess # Import subprocess module
|
||||
import signal
|
||||
import platform
|
||||
@ -65,9 +64,6 @@ trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # UI Thread -> Main Loop
|
||||
command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread
|
||||
# --- End Change ---
|
||||
ui_monitor_task: asyncio.Task | None = None # To track the UI monitor task
|
||||
game_monitor_process: subprocess.Popen | None = None # To store the game monitor subprocess
|
||||
monitor_reader_task: asyncio.Future | None = None # Store the future from run_in_executor
|
||||
stop_reader_event = threading.Event() # Event to signal the reader thread to stop
|
||||
|
||||
# --- Keyboard Shortcut State ---
|
||||
script_paused = False
|
||||
@ -149,70 +145,6 @@ def keyboard_listener():
|
||||
# --- End Keyboard Shortcut Handlers ---
|
||||
|
||||
|
||||
# --- Game Monitor Signal Reader (Threaded Blocking Version) ---
|
||||
def read_monitor_output(process: subprocess.Popen, queue: ThreadSafeQueue, loop: asyncio.AbstractEventLoop, stop_event: threading.Event):
|
||||
"""Runs in a separate thread, reads stdout blocking, parses JSON, and puts commands in the queue."""
|
||||
print("Game monitor output reader thread started.")
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
if not process.stdout:
|
||||
print("[Monitor Reader Thread] Subprocess stdout is None. Exiting thread.")
|
||||
break
|
||||
|
||||
try:
|
||||
# Blocking read - this is fine in a separate thread
|
||||
line = process.stdout.readline()
|
||||
except ValueError:
|
||||
# Can happen if the pipe is closed during readline
|
||||
print("[Monitor Reader Thread] ValueError on readline (pipe likely closed). Exiting thread.")
|
||||
break
|
||||
|
||||
if not line:
|
||||
# EOF reached (process terminated)
|
||||
print("[Monitor Reader Thread] EOF reached on stdout. Exiting thread.")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if line:
|
||||
# Log raw line immediately
|
||||
print(f"[Monitor Reader Thread] Received raw line: '{line}'")
|
||||
try:
|
||||
data = json.loads(line)
|
||||
action = data.get('action')
|
||||
print(f"[Monitor Reader Thread] Parsed action: '{action}'") # Log parsed action
|
||||
if action == 'pause_ui':
|
||||
command = {'action': 'pause'}
|
||||
print(f"[Monitor Reader Thread] Preparing to queue command: {command}") # Log before queueing
|
||||
loop.call_soon_threadsafe(queue.put_nowait, command)
|
||||
print("[Monitor Reader Thread] Pause command queued.") # Log after queueing
|
||||
elif action == 'resume_ui':
|
||||
# Removed direct resume_ui handling - ui_interaction will handle pause/resume based on restart_complete
|
||||
print("[Monitor Reader Thread] Received old 'resume_ui' signal, ignoring.")
|
||||
elif action == 'restart_complete':
|
||||
command = {'action': 'handle_restart_complete'}
|
||||
print(f"[Monitor Reader Thread] Received 'restart_complete' signal, preparing to queue command: {command}")
|
||||
try:
|
||||
loop.call_soon_threadsafe(queue.put_nowait, command)
|
||||
print("[Monitor Reader Thread] 'handle_restart_complete' command queued.")
|
||||
except Exception as q_err:
|
||||
print(f"[Monitor Reader Thread] Error putting 'handle_restart_complete' command in queue: {q_err}")
|
||||
else:
|
||||
print(f"[Monitor Reader Thread] Received unknown action from monitor: {action}")
|
||||
except json.JSONDecodeError:
|
||||
print(f"[Monitor Reader Thread] ERROR: Could not decode JSON from monitor: '{line}'")
|
||||
# Log the raw line that failed to parse
|
||||
# print(f"[Monitor Reader Thread] Raw line that failed JSON decode: '{line}'") # Already logged raw line earlier
|
||||
except Exception as e:
|
||||
print(f"[Monitor Reader Thread] Error processing monitor output: {e}")
|
||||
# No sleep needed here as readline() is blocking
|
||||
except Exception as e:
|
||||
# Catch broader errors in the thread loop itself
|
||||
print(f"[Monitor Reader Thread] Thread loop error: {e}")
|
||||
finally:
|
||||
print("Game monitor output reader thread stopped.")
|
||||
# --- End Game Monitor Signal Reader ---
|
||||
|
||||
|
||||
# --- Chat Logging Function ---
|
||||
def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str, bot_thoughts: str | None = None):
|
||||
"""Logs the chat interaction, including optional bot thoughts, to a date-stamped file if enabled."""
|
||||
@ -318,7 +250,7 @@ if platform.system() == "Windows" and win32api and win32con:
|
||||
# --- Cleanup Function ---
|
||||
async def shutdown():
|
||||
"""Gracefully closes connections and stops monitoring tasks/processes."""
|
||||
global wolfhart_persona_details, ui_monitor_task, shutdown_requested, game_monitor_process, monitor_reader_task # Add monitor_reader_task
|
||||
global wolfhart_persona_details, ui_monitor_task, shutdown_requested
|
||||
# Ensure shutdown is requested if called externally (e.g., Ctrl+C)
|
||||
if not shutdown_requested:
|
||||
print("Shutdown initiated externally (e.g., Ctrl+C).")
|
||||
@ -338,42 +270,7 @@ async def shutdown():
|
||||
except Exception as e:
|
||||
print(f"Error while waiting for UI monitoring task cancellation: {e}")
|
||||
|
||||
# 1b. Signal and Wait for Monitor Reader Thread
|
||||
if monitor_reader_task: # Check if the future exists
|
||||
if not stop_reader_event.is_set():
|
||||
print("Signaling monitor output reader thread to stop...")
|
||||
stop_reader_event.set()
|
||||
|
||||
# Wait for the thread to finish (the future returned by run_in_executor)
|
||||
# This might block briefly, but it's necessary to ensure clean thread shutdown
|
||||
# We don't await it directly in the async shutdown, but check if it's done
|
||||
# A better approach might be needed if the thread blocks indefinitely
|
||||
print("Waiting for monitor output reader thread to finish (up to 2s)...")
|
||||
try:
|
||||
# Wait for the future to complete with a timeout
|
||||
await asyncio.wait_for(monitor_reader_task, timeout=2.0)
|
||||
print("Monitor output reader thread finished.")
|
||||
except asyncio.TimeoutError:
|
||||
print("Warning: Monitor output reader thread did not finish within timeout.")
|
||||
except asyncio.CancelledError:
|
||||
print("Monitor output reader future was cancelled.") # Should not happen if we don't cancel it
|
||||
except Exception as e:
|
||||
print(f"Error waiting for monitor reader thread future: {e}")
|
||||
|
||||
# 2. Terminate Game Monitor Subprocess (after signaling reader thread)
|
||||
if game_monitor_process:
|
||||
print("Terminating game monitor subprocess...")
|
||||
try:
|
||||
game_monitor_process.terminate()
|
||||
# Optionally wait for a short period or check return code
|
||||
# game_monitor_process.wait(timeout=1)
|
||||
print("Game monitor subprocess terminated.")
|
||||
except Exception as e:
|
||||
print(f"Error terminating game monitor subprocess: {e}")
|
||||
finally:
|
||||
game_monitor_process = None # Clear the reference
|
||||
|
||||
# 3. Close MCP connections via AsyncExitStack
|
||||
# 2. Close MCP connections via AsyncExitStack
|
||||
# This will trigger the __aexit__ method of stdio_client contexts,
|
||||
# which we assume handles terminating the server subprocesses it started.
|
||||
print(f"Closing MCP Server connections (via AsyncExitStack)...")
|
||||
@ -555,7 +452,7 @@ def initialize_memory_system():
|
||||
# --- Main Async Function ---
|
||||
async def run_main_with_exit_stack():
|
||||
"""Initializes connections, loads persona, starts UI monitor and main processing loop."""
|
||||
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue, game_monitor_process, monitor_reader_task # Add monitor_reader_task to globals
|
||||
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue
|
||||
try:
|
||||
# 1. Load Persona Synchronously (before async loop starts)
|
||||
load_persona_from_file() # Corrected function
|
||||
@ -594,48 +491,7 @@ async def run_main_with_exit_stack():
|
||||
ui_monitor_task = monitor_task # Store task reference for shutdown
|
||||
# Note: UI task cancellation is handled in shutdown()
|
||||
|
||||
# 5b. Start Game Window Monitoring as a Subprocess
|
||||
# global game_monitor_process, monitor_reader_task # Already declared global at function start
|
||||
print("\n--- Starting Game Window monitoring as a subprocess ---")
|
||||
try:
|
||||
# Use sys.executable to ensure the same Python interpreter is used
|
||||
# Capture stdout to read signals
|
||||
game_monitor_process = subprocess.Popen(
|
||||
[sys.executable, 'game_monitor.py'],
|
||||
stdout=subprocess.PIPE, # Capture stdout
|
||||
stderr=subprocess.PIPE, # Capture stderr for logging/debugging
|
||||
text=True, # Decode stdout/stderr as text (UTF-8 by default)
|
||||
bufsize=1, # Line buffered
|
||||
# Ensure process creation flags are suitable for Windows if needed
|
||||
# creationflags=subprocess.CREATE_NO_WINDOW # Example: Hide console window
|
||||
)
|
||||
print(f"Game monitor subprocess started (PID: {game_monitor_process.pid}).")
|
||||
|
||||
# Start the thread to read monitor output if process started successfully
|
||||
if game_monitor_process.stdout:
|
||||
# Run the blocking reader function in a separate thread using the default executor
|
||||
monitor_reader_task = loop.run_in_executor(
|
||||
None, # Use default ThreadPoolExecutor
|
||||
read_monitor_output, # The function to run
|
||||
game_monitor_process, # Arguments for the function...
|
||||
command_queue,
|
||||
loop,
|
||||
stop_reader_event # Pass the stop event
|
||||
)
|
||||
print("Monitor output reader thread submitted to executor.")
|
||||
else:
|
||||
print("Error: Could not access game monitor subprocess stdout.")
|
||||
monitor_reader_task = None
|
||||
|
||||
# Optionally, start a task to read stderr as well for debugging
|
||||
# stderr_reader_task = loop.create_task(read_stderr(game_monitor_process), name="monitor_stderr_reader")
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Error: 'game_monitor.py' not found. Cannot start game monitor subprocess.")
|
||||
game_monitor_process = None
|
||||
except Exception as e:
|
||||
print(f"Error starting game monitor subprocess: {e}")
|
||||
game_monitor_process = None
|
||||
# 5b. Game Window Monitoring is now handled by Setup.py
|
||||
|
||||
|
||||
# 6. Start the main processing loop (non-blocking check on queue)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user