-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprocess_manager.py
More file actions
277 lines (233 loc) · 12.6 KB
/
process_manager.py
File metadata and controls
277 lines (233 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""
Process Manager - 进程管理器
负责管理区域选择器子进程和程序重启等功能
"""
import logging
import os
import sys
import subprocess
import threading
import tempfile
import json
from typing import Optional
import config_loader
from modeswitch import AppMode
from log_manager import get_module_logger
class ProcessManager:
"""进程管理器 - 负责子进程管理和程序重启功能"""
def __init__(self, config, stop_event):
self.config = config
self.stop_event = stop_event
self.region_selector_process: Optional[subprocess.Popen] = None
self.mode_switch = None # 将在外部注入
self.action_queue = None # 将在外部注入
# 设置模块专用日志记录器
self.logger = get_module_logger('process_manager')
def _handle_region_selector_error(self, message: str, error: Exception):
"""通用的区域选择器错误处理方法"""
self.logger.error(f"{message}: {error}", exc_info=True)
self.mode_switch.return_from_region_select()
self.mode_switch.resume_keyboard_hook()
@staticmethod
def restart_application():
"""静态重启方法,无需创建实例"""
import sys
import os
import subprocess
from tkinter import messagebox
# 为静态方法创建临时日志记录器
logger = get_module_logger('process_manager_static')
try:
logger.info("[Restart] Starting application restart...")
# 检测是否为打包后的环境
if getattr(sys, 'frozen', False):
# 打包后的环境:使用当前可执行文件
current_executable = sys.executable
logger.info(f"[Restart] Detected frozen environment, executable: {current_executable}")
if not os.path.exists(current_executable):
messagebox.showerror("重启失败", "找不到可执行文件。请手动重启程序。")
return
# 直接重启可执行文件,保留命令行参数
args = [current_executable] + sys.argv[1:]
logger.info(f"[Restart] Starting new process: {' '.join(args)}")
subprocess.Popen(args, creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == 'nt' else 0)
else:
# 开发环境:使用Python解释器运行脚本
executable = sys.executable
current_script = os.path.abspath(sys.argv[0])
if not os.path.exists(executable):
messagebox.showerror("重启失败", "找不到Python解释器。请手动重启程序。")
return
args = [executable, current_script] + sys.argv[1:]
logger.info(f"[Restart] Starting new process: {' '.join(args)}")
subprocess.Popen(args, creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == 'nt' else 0)
logger.info("[Restart] New process started, exiting current process...")
os._exit(0)
except Exception as e:
logger.error(f"[Restart] Restart failed: {e}")
messagebox.showerror("重启失败", f"重启过程中发生错误:{str(e)}")
def _handle_region_select(self) -> None:
"""处理区域选择功能。
启动区域选择器进程并等待其完成。
"""
# 暂停键盘钩子,使键盘事件不再被拦截和处理
self.logger.info("[RegionSelect] 暂停键盘钩子...")
self.mode_switch.pause_keyboard_hook()
self.logger.info(f"[RegionSelect] 键盘钩子暂停后状态: is_active={hasattr(self.mode_switch, 'keyboard_hook_active') and self.mode_switch.keyboard_hook_active}")
try:
# 如果区域选择器进程存在且仍在运行,则终止它
if self.region_selector_process and self.region_selector_process.poll() is None:
self.region_selector_process.terminate()
# 将应用程序模式切换到区域选择模式
# 为什么这样写风格不统一,因为区域选择模式是一个单独的进程,需要和主进程分离
self.mode_switch.set_mode(AppMode.REGION_SELECT)
# 获取当前进程ID
pid = os.getpid()
# 获取系统临时目录路径
temp_dir = tempfile.gettempdir()
# 创建临时文件路径,用于存储布局和坐标信息
layout_file = os.path.join(temp_dir, f"keymouse_layout_{pid}.tmp")
coords_file = os.path.join(temp_dir, f"keymouse_coords_{pid}.tmp")
# 将区域选择布局配置写入临时文件
with open(layout_file, 'w', encoding='utf-8') as f:
# 将配置中的区域选择布局以JSON格式写入文件
# json.dump()方法将Python对象序列化为JSON格式字符串并写入文件
# 第一个参数是要序列化的Python对象(这里是区域选择布局配置)
# 第二个参数是文件对象f
json.dump(self.config.REGION_SELECT_LAYOUT, f)
# 获取应用程序基础路径
base_path = config_loader.get_base_path()
command = []
# 判断是否是打包后的可执行文件
if getattr(sys, 'frozen', False):
# 如果是打包后的程序,使用RegionSelector.exe
command = [os.path.join(base_path, "RegionSelector.exe"),
layout_file, coords_file]
else:
# 如果是源码运行,使用Python解释器执行region_selector.py
command = [sys.executable,
os.path.join(base_path, "region_selector.py"),
layout_file, coords_file]
# 创建启动信息对象,用于控制子进程窗口的显示方式
# 创建一个STARTUPINFO对象,用于控制子进程的窗口显示方式
startupinfo = subprocess.STARTUPINFO()
# 设置dwFlags标志,通过位运算添加STARTF_USESHOWWINDOW标志
# 这样可以让子进程窗口以隐藏方式启动,不会显示命令行窗口
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
# 启动区域选择器子进程
self.logger.info(f"[RegionSelect] 启动子进程: {' '.join(command)}")
self.region_selector_process = subprocess.Popen(command,
startupinfo=startupinfo)
self.logger.info(f"[RegionSelect] 子进程启动成功,PID: {self.region_selector_process.pid}")
# 创建守护线程等待区域选择器进程完成
threading.Thread(target=self.wait_for_region_selector,
args=(layout_file, coords_file),
daemon=True).start()
except FileNotFoundError as e:
self._handle_region_selector_error("找不到区域选择器程序", e)
except PermissionError as e:
self._handle_region_selector_error("没有权限访问区域选择器", e)
except subprocess.SubprocessError as e:
self._handle_region_selector_error("启动区域选择器进程失败", e)
except Exception as e:
self._handle_region_selector_error("启动区域选择器时发生未知错误", e)
def wait_for_region_selector(self, layout_file: str, coords_file: str) -> None:
"""等待区域选择器进程完成。
Args:
layout_file: 布局文件路径
coords_file: 坐标文件路径
"""
if not self.region_selector_process:
return
try:
self.region_selector_process.wait()
if os.path.exists(coords_file):
with open(coords_file, 'r', encoding='utf-8') as f:
content = f.read().strip()
if content:
try:
x_str, y_str = content.split(',')
target_x, target_y = float(x_str), float(y_str)
self.action_queue.put(('move_mouse_to',
(target_x, target_y)))
except Exception as e:
self.logger.error(f"解析坐标文件内容失败: {e}")
except Exception as e:
self.logger.error(f"等待区域选择器时发生错误: {e}", exc_info=True)
finally:
for f in [layout_file, coords_file]:
if os.path.exists(f):
try:
os.remove(f)
except Exception as e:
self.logger.error(f"删除临时文件 {f} 失败: {e}")
self.mode_switch.return_from_region_select()
# 恢复键盘钩子,重新开始拦截和处理键盘事件
self.logger.info("[RegionSelect] 恢复键盘钩子...")
self.mode_switch.resume_keyboard_hook()
self.logger.info(f"[RegionSelect] 键盘钩子恢复后状态: is_active={hasattr(self.mode_switch, 'keyboard_hook_active') and self.mode_switch.keyboard_hook_active}")
self.region_selector_process = None
def restart_program(self, tray_icon=None, keyboard_listener=None):
"""重启当前程序
使用当前Python解释器和相同参数重新启动程序实例,
然后退出当前进程。这个函数是GUI调用的入口点。
Args:
tray_icon: 托盘图标实例
keyboard_listener: 键盘监听器实例
"""
try:
self.logger.info("[Restart] Starting application restart...")
# 获取当前Python解释器路径和参数
executable = sys.executable
args = sys.argv
self.logger.info(f"[Restart] Executable path: {executable}")
self.logger.info(f"[Restart] Arguments: {args}")
# 1. 停止托盘图标
try:
if tray_icon is not None:
self.logger.info("[Restart] Cleaning up tray icon...")
if hasattr(tray_icon, 'force_cleanup'):
tray_icon.force_cleanup()
else:
self.logger.warning("[Restart] Tray icon does not have a force_cleanup method.")
self.logger.info("[Restart] Tray icon cleaned up.")
except Exception as e:
self.logger.error(f"[Restart] Error cleaning up tray icon: {e}", exc_info=True)
# 2. 停止键盘监听器
try:
if keyboard_listener is not None:
self.logger.info("[Restart] Stopping keyboard listener...")
keyboard_listener.stop()
if keyboard_listener.is_alive():
keyboard_listener.join(timeout=1.0)
self.logger.info("[Restart] Keyboard listener stopped.")
except Exception as e:
self.logger.error(f"[Restart] Error stopping keyboard listener: {e}", exc_info=True)
# 3. 触发全局停止事件
try:
if self.stop_event is not None:
self.logger.info("[Restart] Setting global stop event...")
self.stop_event.set()
except Exception as e:
self.logger.error(f"[Restart] Error setting stop event: {e}", exc_info=True)
# 4. 启动新进程
self.logger.info("[Restart] Starting new process...")
subprocess.Popen([executable] + args)
self.logger.info("[Restart] New process started. Current process will now hard exit.")
# 5. 使用 os._exit(0) 强制退出,避免被 pystray 捕获
os._exit(0)
except FileNotFoundError:
self.logger.error("[Restart] Restart failed: Python interpreter not found.")
import tkinter as tk
from tkinter import messagebox
messagebox.showerror("Restart Failed", "Python interpreter not found. Please restart the program manually.")
except PermissionError:
self.logger.error("[Restart] Restart failed: Insufficient permissions.")
import tkinter as tk
from tkinter import messagebox
messagebox.showerror("Restart Failed", "Insufficient permissions to restart the program. Please restart manually.")
except Exception as e:
self.logger.error(f"[Restart] Restart failed: {e}")
import tkinter as tk
from tkinter import messagebox
messagebox.showerror("重启失败", f"重启过程中发生错误:{str(e)}")