Improve game window topmost handling and add forced reconnection for remote control stability
This commit is contained in:
parent
b33ea85768
commit
59471b62ce
@ -58,7 +58,7 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py)
|
7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py)
|
||||||
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
|
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
|
||||||
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
|
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
|
||||||
- 確保視窗維持在最上層 (Always on Top)。
|
- **確保視窗保持活躍**:如果遊戲視窗不是目前的前景視窗,則嘗試將其帶到前景並啟用 (Bring to Foreground/Activate),取代了之前的強制置頂 (Always on Top) 邏輯 (修改於 2025-05-12)。
|
||||||
- **定時遊戲重啟** (如果 `config.ENABLE_SCHEDULED_RESTART` 為 True):
|
- **定時遊戲重啟** (如果 `config.ENABLE_SCHEDULED_RESTART` 為 True):
|
||||||
- 根據 `config.RESTART_INTERVAL_MINUTES` 設定的間隔執行。
|
- 根據 `config.RESTART_INTERVAL_MINUTES` 設定的間隔執行。
|
||||||
- **簡化流程 (2025-04-25)**:
|
- **簡化流程 (2025-04-25)**:
|
||||||
@ -598,6 +598,18 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
- **依賴項**:Windows 上的控制台事件處理仍然依賴 `pywin32` 套件。如果未安裝,程式會打印警告,關閉時的可靠性可能略有降低(但 `stdio_client` 的正常清理機制應在多數情況下仍然有效)。
|
- **依賴項**:Windows 上的控制台事件處理仍然依賴 `pywin32` 套件。如果未安裝,程式會打印警告,關閉時的可靠性可能略有降低(但 `stdio_client` 的正常清理機制應在多數情況下仍然有效)。
|
||||||
- **效果**:恢復了與 `mcp` 庫的兼容性,同時通過標準的上下文管理和輔助性的 Windows 事件處理,實現了在主程式退出時關閉 MCP 伺服器子進程的目標。
|
- **效果**:恢復了與 `mcp` 庫的兼容性,同時通過標準的上下文管理和輔助性的 Windows 事件處理,實現了在主程式退出時關閉 MCP 伺服器子進程的目標。
|
||||||
|
|
||||||
|
## 最近改進(2025-05-12)
|
||||||
|
|
||||||
|
### 遊戲視窗置頂邏輯修改
|
||||||
|
|
||||||
|
- **目的**:將 `game_monitor.py` 中強制遊戲視窗「永遠在最上層」(Always on Top) 的行為,修改為「臨時置頂並獲得焦點」(Bring to Foreground/Activate),以解決原方法僅覆蓋其他視窗的問題。
|
||||||
|
- **`game_monitor.py`**:
|
||||||
|
- 在 `monitor_game_window` 函數的監控循環中,移除了使用 `win32gui.SetWindowPos` 和 `win32con.HWND_TOPMOST` 來檢查和設定 `WS_EX_TOPMOST` 樣式的程式碼。
|
||||||
|
- 替換為檢查當前前景視窗 (`win32gui.GetForegroundWindow()`) 是否為目標遊戲視窗 (`hwnd`)。
|
||||||
|
- 如果不是,則調用 `win32gui.BringWindowToTop(hwnd)` 和 `win32gui.SetForegroundWindow(hwnd)` 來嘗試將遊戲視窗帶到前景並啟用。
|
||||||
|
- 更新了相關的日誌訊息以反映新的行為。
|
||||||
|
- **效果**:監控腳本現在會嘗試將失去焦點的遊戲視窗重新激活並帶到前景,而不是強制其覆蓋所有其他視窗。這更符合一般視窗的行為模式。
|
||||||
|
|
||||||
## 開發建議
|
## 開發建議
|
||||||
|
|
||||||
### 優化方向
|
### 優化方向
|
||||||
|
|||||||
82
Setup.py
82
Setup.py
@ -2443,6 +2443,7 @@ if HAS_SOCKETIO:
|
|||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
self.should_exit_flag = threading.Event() # Use an event for thread control
|
self.should_exit_flag = threading.Event() # Use an event for thread control
|
||||||
self.client_thread = None
|
self.client_thread = None
|
||||||
|
self.last_successful_connection_time = None # Track last successful connection/auth
|
||||||
|
|
||||||
self.registered_commands = [
|
self.registered_commands = [
|
||||||
"restart bot", "restart game", "restart all",
|
"restart bot", "restart game", "restart all",
|
||||||
@ -2473,15 +2474,21 @@ if HAS_SOCKETIO:
|
|||||||
last_heartbeat = time.time() # For heartbeat
|
last_heartbeat = time.time() # For heartbeat
|
||||||
retry_delay = 1.0 # Start with 1 second delay for exponential backoff
|
retry_delay = 1.0 # Start with 1 second delay for exponential backoff
|
||||||
max_delay = 300.0 # Maximum delay of 5 minutes for exponential backoff
|
max_delay = 300.0 # Maximum delay of 5 minutes for exponential backoff
|
||||||
|
hourly_refresh_interval = 3600 # 1 hour in seconds
|
||||||
|
|
||||||
while not self.should_exit_flag.is_set():
|
while not self.should_exit_flag.is_set():
|
||||||
|
current_time = time.time() # Get current time at the start of the loop iteration
|
||||||
|
|
||||||
if not self.sio.connected:
|
if not self.sio.connected:
|
||||||
|
# Reset connection time tracker when attempting to connect
|
||||||
|
self.last_successful_connection_time = None
|
||||||
try:
|
try:
|
||||||
logger.info(f"ControlClient: Attempting to connect to {self.server_url}...")
|
logger.info(f"ControlClient: Attempting to connect to {self.server_url}...")
|
||||||
self.sio.connect(self.server_url)
|
self.sio.connect(self.server_url)
|
||||||
logger.info("ControlClient: Successfully connected.")
|
# Connection successful, wait for authentication to set last_successful_connection_time
|
||||||
retry_delay = 1.0 # Reset delay on successful connection
|
logger.info("ControlClient: Successfully established socket connection. Waiting for authentication.")
|
||||||
last_heartbeat = time.time() # Reset heartbeat timer on new connection
|
retry_delay = 1.0 # Reset delay on successful connection attempt
|
||||||
|
# last_heartbeat = time.time() # Reset heartbeat timer only after authentication? Or here? Let's keep it after auth.
|
||||||
except socketio.exceptions.ConnectionError as e:
|
except socketio.exceptions.ConnectionError as e:
|
||||||
logger.error(f"ControlClient: Connection failed: {e}. Retrying in {retry_delay:.2f}s.")
|
logger.error(f"ControlClient: Connection failed: {e}. Retrying in {retry_delay:.2f}s.")
|
||||||
self.should_exit_flag.wait(retry_delay)
|
self.should_exit_flag.wait(retry_delay)
|
||||||
@ -2496,23 +2503,51 @@ if HAS_SOCKETIO:
|
|||||||
retry_delay = max(1.0, retry_delay) # Ensure it's at least 1s
|
retry_delay = max(1.0, retry_delay) # Ensure it's at least 1s
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If connected, manage heartbeat and check for exit signal
|
# If connected (socket established, maybe not authenticated yet)
|
||||||
if self.sio.connected:
|
if self.sio.connected:
|
||||||
current_time = time.time()
|
# Check for hourly refresh ONLY if authenticated and timer is set
|
||||||
if current_time - last_heartbeat > 60: # Send heartbeat every 60 seconds
|
if self.authenticated and self.last_successful_connection_time and (current_time - self.last_successful_connection_time > hourly_refresh_interval):
|
||||||
|
logger.info(f"ControlClient: Hourly session refresh triggered (Connected for > {hourly_refresh_interval}s). Disconnecting for refresh...")
|
||||||
|
try:
|
||||||
|
self.sio.disconnect()
|
||||||
|
# Reset flags immediately after intentional disconnect
|
||||||
|
self.connected = False
|
||||||
|
self.authenticated = False
|
||||||
|
self.last_successful_connection_time = None
|
||||||
|
logger.info("ControlClient: Disconnected for hourly refresh. Will attempt reconnect in next cycle.")
|
||||||
|
# Continue to the start of the loop to handle reconnection logic
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"ControlClient: Error during planned hourly disconnect: {e}")
|
||||||
|
# Reset flags anyway and let the loop retry
|
||||||
|
self.connected = False
|
||||||
|
self.authenticated = False
|
||||||
|
self.last_successful_connection_time = None
|
||||||
|
|
||||||
|
|
||||||
|
# Manage heartbeat if authenticated
|
||||||
|
if self.authenticated and current_time - last_heartbeat > 60: # Send heartbeat every 60 seconds
|
||||||
try:
|
try:
|
||||||
self.sio.emit('heartbeat', {'timestamp': current_time})
|
self.sio.emit('heartbeat', {'timestamp': current_time})
|
||||||
last_heartbeat = current_time
|
last_heartbeat = current_time
|
||||||
logger.debug("ControlClient: Sent heartbeat to keep connection alive.")
|
logger.debug("ControlClient: Sent heartbeat.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"ControlClient: Error sending heartbeat: {e}. Connection might be lost.")
|
logger.error(f"ControlClient: Error sending heartbeat: {e}. Connection might be lost.")
|
||||||
|
# Consider triggering disconnect/reconnect logic here if heartbeat fails repeatedly
|
||||||
|
|
||||||
|
# Wait before next loop iteration, checking for exit signal
|
||||||
self.should_exit_flag.wait(1) # Check for exit signal every second
|
self.should_exit_flag.wait(1) # Check for exit signal every second
|
||||||
else:
|
|
||||||
# Fallback if not connected after attempt block (should be rare with current logic)
|
else: # Not connected (e.g., after a disconnect, or failed connection attempt)
|
||||||
logger.debug(f"ControlClient: Not connected (unexpected state in loop), waiting {retry_delay:.2f}s before next cycle.")
|
# This path is hit after disconnects (intentional or unintentional)
|
||||||
|
# Reset connection time tracker if not already None
|
||||||
|
if self.last_successful_connection_time is not None:
|
||||||
|
logger.debug("ControlClient: Resetting connection timer as client is not connected.")
|
||||||
|
self.last_successful_connection_time = None
|
||||||
|
|
||||||
|
logger.debug(f"ControlClient: Not connected, waiting {retry_delay:.2f}s before next connection attempt.")
|
||||||
self.should_exit_flag.wait(retry_delay)
|
self.should_exit_flag.wait(retry_delay)
|
||||||
# Optionally re-calculate retry_delay here if this path is hit, to maintain backoff progression
|
# Exponential backoff for reconnection attempts
|
||||||
retry_delay = min(retry_delay * 2, max_delay) * (0.8 + 0.4 * random.random())
|
retry_delay = min(retry_delay * 2, max_delay) * (0.8 + 0.4 * random.random())
|
||||||
retry_delay = max(1.0, retry_delay)
|
retry_delay = max(1.0, retry_delay)
|
||||||
|
|
||||||
@ -2522,6 +2557,7 @@ if HAS_SOCKETIO:
|
|||||||
|
|
||||||
def _on_connect(self):
|
def _on_connect(self):
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
# Don't reset timer here, wait for authentication
|
||||||
logger.info("ControlClient: Connected to server. Authenticating...")
|
logger.info("ControlClient: Connected to server. Authenticating...")
|
||||||
self.sio.emit('authenticate', {
|
self.sio.emit('authenticate', {
|
||||||
'type': 'client',
|
'type': 'client',
|
||||||
@ -2530,26 +2566,30 @@ if HAS_SOCKETIO:
|
|||||||
})
|
})
|
||||||
|
|
||||||
def _on_disconnect(self):
|
def _on_disconnect(self):
|
||||||
|
was_connected = self.connected # Store previous state
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
|
self.last_successful_connection_time = None # Reset timer on any disconnect
|
||||||
|
if was_connected: # Only log if it was previously connected
|
||||||
logger.info("ControlClient: Disconnected from server.")
|
logger.info("ControlClient: Disconnected from server.")
|
||||||
|
else:
|
||||||
|
logger.debug("ControlClient: Received disconnect event, but was already marked as disconnected.")
|
||||||
|
|
||||||
# Force reconnection if not intentionally stopping
|
# Remove the immediate reconnection attempt here, let _run_forever handle it with backoff
|
||||||
if not self.should_exit_flag.is_set():
|
# if not self.should_exit_flag.is_set():
|
||||||
logger.info("ControlClient: Attempting immediate reconnection from _on_disconnect...")
|
# logger.info("ControlClient: Disconnected. Reconnection will be handled by the main loop.")
|
||||||
try:
|
|
||||||
# This is an immediate attempt; _run_forever handles sustained retries.
|
|
||||||
if not self.sio.connected: # Check before trying to connect
|
|
||||||
self.sio.connect(self.server_url)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"ControlClient: Immediate reconnection from _on_disconnect failed: {e}")
|
|
||||||
|
|
||||||
def _on_authenticated(self, data):
|
def _on_authenticated(self, data):
|
||||||
if data.get('success'):
|
if data.get('success'):
|
||||||
self.authenticated = True
|
self.authenticated = True
|
||||||
logger.info("ControlClient: Authentication successful.")
|
self.last_successful_connection_time = time.time() # Start timer on successful auth
|
||||||
|
# Reset heartbeat timer upon successful authentication
|
||||||
|
# Find where last_heartbeat is accessible or make it accessible (e.g., self.last_heartbeat)
|
||||||
|
# For now, assume last_heartbeat is managed within _run_forever and will naturally reset timing
|
||||||
|
logger.info("ControlClient: Authentication successful. Hourly refresh timer started.")
|
||||||
else:
|
else:
|
||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
|
self.last_successful_connection_time = None # Ensure timer is reset if auth fails
|
||||||
logger.error(f"ControlClient: Authentication failed: {data.get('error', 'Unknown error')}")
|
logger.error(f"ControlClient: Authentication failed: {data.get('error', 'Unknown error')}")
|
||||||
self.sio.disconnect() # Disconnect if auth fails
|
self.sio.disconnect() # Disconnect if auth fails
|
||||||
|
|
||||||
|
|||||||
@ -217,24 +217,28 @@ def monitor_game_window():
|
|||||||
# monitor_logger.warning(f"Failed to adjust window. Current: {new_pos} {new_size}, Target: {target_pos} {target_size}")
|
# 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
|
pass # Keep silent on failure for now
|
||||||
|
|
||||||
# 2. Check and Set Topmost
|
# 2. Check and Bring to Foreground/Activate
|
||||||
style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
|
current_foreground_hwnd = win32gui.GetForegroundWindow()
|
||||||
is_topmost = style & win32con.WS_EX_TOPMOST
|
|
||||||
|
|
||||||
if not is_topmost:
|
if current_foreground_hwnd != hwnd:
|
||||||
# Set topmost, -1 for HWND_TOPMOST, flags = SWP_NOMOVE | SWP_NOSIZE
|
try:
|
||||||
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
|
# Attempt to bring to top and set foreground
|
||||||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
# Note: SetForegroundWindow might fail if the calling process doesn't have foreground rights
|
||||||
# Verify
|
win32gui.BringWindowToTop(hwnd)
|
||||||
|
win32gui.SetForegroundWindow(hwnd)
|
||||||
|
# Short delay to allow window manager to process
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
new_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
|
# Verify if it became the foreground window
|
||||||
if new_style & win32con.WS_EX_TOPMOST:
|
if win32gui.GetForegroundWindow() == hwnd:
|
||||||
current_message += "已將遊戲視窗設為最上層。(Set game window to topmost.)"
|
current_message += "已將遊戲視窗帶到前景並啟用。(Brought game window to foreground and activated.) "
|
||||||
adjustment_made = True
|
adjustment_made = True
|
||||||
else:
|
else:
|
||||||
# Log failure if needed
|
# Optional: Log if setting foreground failed, might happen if another app steals focus quickly
|
||||||
# monitor_logger.warning("Failed to set window to topmost.")
|
# monitor_logger.warning("嘗試將視窗設為前景後,它並未成為前景視窗。(Attempted to set window foreground, but it did not become the foreground window.)")
|
||||||
pass # Keep silent
|
pass
|
||||||
|
except Exception as fg_err:
|
||||||
|
# This can happen if the window handle is invalid or other win32 errors occur
|
||||||
|
monitor_logger.warning(f"嘗試將視窗設為前景時出錯: {fg_err} (Error trying to set window foreground: {fg_err})")
|
||||||
|
|
||||||
except gw.PyGetWindowException as e:
|
except gw.PyGetWindowException as e:
|
||||||
# Log PyGetWindowException specifically, might indicate window closed during check
|
# Log PyGetWindowException specifically, might indicate window closed during check
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user