11#!/usr/bin/env python3
22# -*- coding: utf-8 -*-
33"""
4- ECH Workers 客户端 - Mac 版本 (Python + PyQt5)
4+ ECH Workers 客户端 - 跨平台版本 (Python + PyQt5)
5+ 支持 Windows 和 macOS
56"""
67
78import sys
@@ -34,7 +35,17 @@ class ConfigManager:
3435 """配置管理器"""
3536
3637 def __init__ (self ):
37- self .config_dir = Path .home () / "Library" / "Application Support" / "ECHWorkersClient"
38+ # 跨平台配置文件路径
39+ if sys .platform == 'win32' :
40+ # Windows: %APPDATA%\ECHWorkersClient
41+ self .config_dir = Path (os .getenv ('APPDATA' , Path .home ())) / "ECHWorkersClient"
42+ else :
43+ # macOS/Linux: ~/Library/Application Support/ECHWorkersClient 或 ~/.config/ECHWorkersClient
44+ if sys .platform == 'darwin' :
45+ self .config_dir = Path .home () / "Library" / "Application Support" / "ECHWorkersClient"
46+ else :
47+ self .config_dir = Path .home () / ".config" / "ECHWorkersClient"
48+
3849 self .config_file = self .config_dir / "config.json"
3950 self .config_dir .mkdir (parents = True , exist_ok = True )
4051 self .servers = []
@@ -189,56 +200,75 @@ def stop(self):
189200 self .process .kill ()
190201
191202 def _find_executable (self ):
192- """查找可执行文件"""
203+ """查找可执行文件(跨平台) """
193204 # 脚本所在目录
194205 script_dir = Path (__file__ ).parent .absolute ()
195206 # 当前工作目录
196207 current_dir = Path .cwd ()
197208
209+ # Windows 和 Unix 的可执行文件扩展名
210+ exe_ext = '.exe' if sys .platform == 'win32' else ''
211+
198212 # 可能的可执行文件路径(按优先级)
199213 possible_paths = [
200- script_dir / 'ech-workers' ,
201- script_dir / 'ech-workers.exe' ,
202- current_dir / 'ech-workers' ,
203- current_dir / 'ech-workers.exe' ,
204- # 尝试查找编译后的文件
205- script_dir / 'ech-workers-gui.exe' , # Windows 编译版本
206- script_dir / 'build' / 'ech-workers' , # 可能的构建目录
214+ script_dir / f'ech-workers{ exe_ext } ' ,
215+ current_dir / f'ech-workers{ exe_ext } ' ,
216+ # Windows 特定路径
217+ script_dir / 'ech-workers.exe' if sys .platform == 'win32' else None ,
218+ current_dir / 'ech-workers.exe' if sys .platform == 'win32' else None ,
219+ # Unix 路径(无扩展名)
220+ script_dir / 'ech-workers' if sys .platform != 'win32' else None ,
221+ current_dir / 'ech-workers' if sys .platform != 'win32' else None ,
207222 ]
208223
224+ # 过滤掉 None 值
225+ possible_paths = [p for p in possible_paths if p is not None ]
226+
209227 for path in possible_paths :
210228 if path .exists ():
211- # 检查是否是真正的可执行文件(不是文本文件)
212- try :
213- # 尝试读取文件头,检查是否是二进制文件
214- with open (path , 'rb' ) as f :
215- header = f .read (4 )
216- # 检查是否是 ELF、Mach-O 或 PE 可执行文件
217- is_binary = header .startswith (b'\x7f ELF' ) or \
218- header .startswith (b'\xfe \xed \xfa ' ) or \
219- header .startswith (b'MZ' ) or \
220- header .startswith (b'#!' ) # 脚本文件
221-
222- if is_binary or os .access (path , os .X_OK ):
223- # 如果是脚本文件,尝试添加执行权限
224- if not os .access (path , os .X_OK ) and header .startswith (b'#!' ):
225- try :
226- os .chmod (path , 0o755 )
227- except :
228- pass
229+ # Windows: 检查文件是否存在即可(.exe 文件)
230+ # Unix: 检查文件权限
231+ if sys .platform == 'win32' :
232+ # Windows 上,.exe 文件可以直接运行
233+ if path .suffix .lower () == '.exe' :
229234 return str (path )
230- except :
231- # 如果读取失败,至少检查执行权限
235+ # 或者检查文件是否可执行
236+ try :
237+ with open (path , 'rb' ) as f :
238+ header = f .read (2 )
239+ # PE 文件头
240+ if header == b'MZ' :
241+ return str (path )
242+ except :
243+ pass
244+ else :
245+ # Unix/Linux/macOS: 检查执行权限
232246 if os .access (path , os .X_OK ):
233247 return str (path )
248+ # 或者检查是否是二进制文件
249+ try :
250+ with open (path , 'rb' ) as f :
251+ header = f .read (4 )
252+ # ELF 或 Mach-O
253+ if (header .startswith (b'\x7f ELF' ) or
254+ header .startswith (b'\xfe \xed \xfa ' ) or
255+ header .startswith (b'#!' )):
256+ # 尝试添加执行权限
257+ try :
258+ os .chmod (path , 0o755 )
259+ except :
260+ pass
261+ return str (path )
262+ except :
263+ pass
234264
235265 # 尝试从 PATH 中查找
236266 import shutil
237267 exe = shutil .which ('ech-workers' )
238268 if exe :
239269 return exe
240270
241- # 如果都找不到,返回 None 并显示详细错误
271+ # 如果都找不到,返回 None
242272 return None
243273
244274
@@ -523,8 +553,119 @@ def on_process_finished(self):
523553
524554 def on_auto_start_changed (self ):
525555 """开机启动改变"""
526- # 简化版本,不实现开机启动
527- pass
556+ enabled = self .auto_start_check .isChecked ()
557+ if self ._set_auto_start (enabled ):
558+ self .append_log (f"[系统] { '已设置' if enabled else '已取消' } 开机启动\n " )
559+ else :
560+ self .auto_start_check .setChecked (not enabled )
561+ QMessageBox .warning (self , "错误" , "设置开机启动失败" )
562+
563+ def _set_auto_start (self , enabled ):
564+ """设置开机启动(跨平台)"""
565+ try :
566+ if sys .platform == 'win32' :
567+ # Windows: 使用注册表
568+ import winreg
569+ key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
570+ app_name = "ECHWorkersClient"
571+
572+ if enabled :
573+ # 获取当前脚本路径
574+ script_path = Path (__file__ ).absolute ()
575+ python_path = sys .executable
576+ # 创建启动命令
577+ cmd = f'"{ python_path } " "{ script_path } "'
578+
579+ try :
580+ key = winreg .OpenKey (winreg .HKEY_CURRENT_USER , key_path , 0 , winreg .KEY_SET_VALUE )
581+ winreg .SetValueEx (key , app_name , 0 , winreg .REG_SZ , cmd )
582+ winreg .CloseKey (key )
583+ return True
584+ except Exception as e :
585+ print (f"设置开机启动失败: { e } " )
586+ return False
587+ else :
588+ try :
589+ key = winreg .OpenKey (winreg .HKEY_CURRENT_USER , key_path , 0 , winreg .KEY_SET_VALUE )
590+ winreg .DeleteValue (key , app_name )
591+ winreg .CloseKey (key )
592+ return True
593+ except FileNotFoundError :
594+ # 如果值不存在,也算成功
595+ return True
596+ except Exception as e :
597+ print (f"删除开机启动失败: { e } " )
598+ return False
599+ else :
600+ # macOS/Linux: 使用 LaunchAgents 或 systemd
601+ if sys .platform == 'darwin' :
602+ # macOS
603+ plist_path = Path .home () / "Library" / "LaunchAgents" / "com.echworkers.client.plist"
604+ if enabled :
605+ script_path = Path (__file__ ).absolute ()
606+ python_path = sys .executable
607+ plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
608+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
609+ <plist version="1.0">
610+ <dict>
611+ <key>Label</key>
612+ <string>com.echworkers.client</string>
613+ <key>ProgramArguments</key>
614+ <array>
615+ <string>{ python_path } </string>
616+ <string>{ script_path } </string>
617+ <string>-autostart</string>
618+ </array>
619+ <key>RunAtLoad</key>
620+ <true/>
621+ <key>KeepAlive</key>
622+ <false/>
623+ </dict>
624+ </plist>"""
625+ try :
626+ plist_path .parent .mkdir (parents = True , exist_ok = True )
627+ with open (plist_path , 'w' ) as f :
628+ f .write (plist_content )
629+ return True
630+ except Exception as e :
631+ print (f"创建启动项失败: { e } " )
632+ return False
633+ else :
634+ try :
635+ if plist_path .exists ():
636+ plist_path .unlink ()
637+ return True
638+ except Exception as e :
639+ print (f"删除启动项失败: { e } " )
640+ return False
641+ else :
642+ # Linux: 使用 systemd user service(简化实现)
643+ return False # Linux 暂不支持
644+ except Exception as e :
645+ print (f"设置开机启动失败: { e } " )
646+ return False
647+
648+ def _is_auto_start_enabled (self ):
649+ """检查是否已启用开机启动"""
650+ try :
651+ if sys .platform == 'win32' :
652+ import winreg
653+ key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
654+ app_name = "ECHWorkersClient"
655+ try :
656+ key = winreg .OpenKey (winreg .HKEY_CURRENT_USER , key_path , 0 , winreg .KEY_READ )
657+ winreg .QueryValueEx (key , app_name )
658+ winreg .CloseKey (key )
659+ return True
660+ except FileNotFoundError :
661+ return False
662+ elif sys .platform == 'darwin' :
663+ plist_path = Path .home () / "Library" / "LaunchAgents" / "com.echworkers.client.plist"
664+ return plist_path .exists ()
665+ else :
666+ return False
667+ except :
668+ return False
528669
529670 def clear_log (self ):
530671 """清空日志"""
@@ -541,6 +682,10 @@ def append_log(self, text):
541682 cursor .movePosition (cursor .Start , cursor .KeepAnchor )
542683 cursor .removeSelectedText ()
543684
685+ def update_auto_start_checkbox (self ):
686+ """更新开机启动复选框状态"""
687+ self .auto_start_check .setChecked (self ._is_auto_start_enabled ())
688+
544689 def auto_start (self ):
545690 """自动启动"""
546691 if not (self .process_thread and self .process_thread .is_running ):
0 commit comments