1+ """
2+ 协议管理器 - 跨平台URL协议注册管理
3+ 支持Windows和Linux系统的自定义协议注册
4+ """
5+ import os
6+ import sys
7+ import winreg
8+ import subprocess
9+ from pathlib import Path
10+ from typing import Optional
11+ from loguru import logger
12+
13+
14+
15+ class ProtocolManager :
16+ """协议管理器 - 处理跨平台的URL协议注册"""
17+
18+ def __init__ (self , app_name : str , protocol_name : str ):
19+ """
20+ 初始化协议管理器
21+
22+ Args:
23+ app_name: 应用程序名称
24+ protocol_name: 自定义协议名称(不含://)
25+ """
26+ self .app_name = app_name
27+ self .protocol_name = protocol_name
28+ self .is_windows = sys .platform .startswith ('win' )
29+ self .is_linux = sys .platform .startswith ('linux' )
30+
31+ def register_protocol (self ) -> bool :
32+ """
33+ 注册自定义协议
34+
35+ Returns:
36+ 注册成功返回True,失败返回False
37+ """
38+ if self .is_windows :
39+ return self ._register_windows_protocol ()
40+ elif self .is_linux :
41+ return self ._register_linux_protocol ()
42+ else :
43+ logger .error (f"不支持的操作系统: { sys .platform } " )
44+ return False
45+
46+ def unregister_protocol (self ) -> bool :
47+ """
48+ 注销自定义协议
49+
50+ Returns:
51+ 注销成功返回True,失败返回False
52+ """
53+ if self .is_windows :
54+ return self ._unregister_windows_protocol ()
55+ elif self .is_linux :
56+ return self ._unregister_linux_protocol ()
57+ else :
58+ logger .error (f"不支持的操作系统: { sys .platform } " )
59+ return False
60+
61+ def is_protocol_registered (self ) -> bool :
62+ """
63+ 检查协议是否已注册
64+
65+ Returns:
66+ 已注册返回True,未注册返回False
67+ """
68+ if self .is_windows :
69+ return self ._is_windows_protocol_registered ()
70+ elif self .is_linux :
71+ return self ._is_linux_protocol_registered ()
72+ else :
73+ return False
74+
75+ def _register_windows_protocol (self ) -> bool :
76+ """Windows系统注册协议"""
77+ try :
78+ # 获取当前可执行文件路径
79+ exe_path = self ._get_executable_path ()
80+
81+ # 首先尝试注册到HKEY_CLASSES_ROOT
82+ try :
83+ # 注册协议到HKEY_CLASSES_ROOT
84+ with winreg .CreateKey (winreg .HKEY_CLASSES_ROOT , self .protocol_name ) as key :
85+ winreg .SetValueEx (key , "" , 0 , winreg .REG_SZ , f"URL:{ self .app_name } Protocol" )
86+ winreg .SetValueEx (key , "URL Protocol" , 0 , winreg .REG_SZ , "" )
87+
88+ # 注册命令
89+ command_key_path = f"{ self .protocol_name } \\ shell\\ open\\ command"
90+ with winreg .CreateKey (winreg .HKEY_CLASSES_ROOT , command_key_path ) as key :
91+ winreg .SetValueEx (key , "" , 0 , winreg .REG_SZ , f'"{ exe_path } " --url "%1"' )
92+
93+ return True
94+
95+ except (OSError , PermissionError ) as e :
96+ # 如果HKEY_CLASSES_ROOT失败,尝试注册到HKEY_CURRENT_USER
97+ if e .errno == 5 or "Access is denied" in str (e ): # WinError 5
98+ logger .error (f"管理员权限不足,尝试注册到当前用户: { e } " )
99+ return self ._register_windows_protocol_current_user (exe_path )
100+ else :
101+ raise
102+
103+ except Exception as e :
104+ logger .error (f"Windows协议注册失败: { e } " )
105+ return False
106+
107+ def _register_windows_protocol_current_user (self , exe_path : str ) -> bool :
108+ """注册到当前用户的Windows协议(无需管理员权限)"""
109+ try :
110+ # 注册协议到HKEY_CURRENT_USER\Software\Classes
111+ key_path = f"Software\\ Classes\\ { self .protocol_name } "
112+ with winreg .CreateKey (winreg .HKEY_CURRENT_USER , key_path ) as key :
113+ winreg .SetValueEx (key , "" , 0 , winreg .REG_SZ , f"URL:{ self .app_name } Protocol" )
114+ winreg .SetValueEx (key , "URL Protocol" , 0 , winreg .REG_SZ , "" )
115+
116+ # 注册命令
117+ command_key_path = f"Software\\ Classes\\ { self .protocol_name } \\ shell\\ open\\ command"
118+ with winreg .CreateKey (winreg .HKEY_CURRENT_USER , command_key_path ) as key :
119+ winreg .SetValueEx (key , "" , 0 , winreg .REG_SZ , f'"{ exe_path } " --url "%1"' )
120+
121+ return True
122+
123+ except Exception as e :
124+ logger .error (f"Windows当前用户协议注册失败: { e } " )
125+ return False
126+
127+ def _unregister_windows_protocol (self ) -> bool :
128+ """Windows系统注销协议"""
129+ try :
130+ # 首先尝试删除系统级注册(HKEY_CLASSES_ROOT)
131+ try :
132+ winreg .DeleteKey (winreg .HKEY_CLASSES_ROOT , f"{ self .protocol_name } \\ shell\\ open\\ command" )
133+ winreg .DeleteKey (winreg .HKEY_CLASSES_ROOT , f"{ self .protocol_name } \\ shell\\ open" )
134+ winreg .DeleteKey (winreg .HKEY_CLASSES_ROOT , f"{ self .protocol_name } \\ shell" )
135+ winreg .DeleteKey (winreg .HKEY_CLASSES_ROOT , self .protocol_name )
136+ return True
137+ except (OSError , PermissionError ) as e :
138+ if e .errno == 5 or "Access is denied" in str (e ): # WinError 5
139+ # 如果系统级删除失败,尝试删除当前用户注册
140+ return self ._unregister_windows_protocol_current_user ()
141+ else :
142+ raise
143+
144+ except Exception as e :
145+ logger .error (f"Windows协议注销失败: { e } " )
146+ return False
147+
148+ def _unregister_windows_protocol_current_user (self ) -> bool :
149+ """注销当前用户的Windows协议"""
150+ try :
151+ # 删除当前用户注册表项
152+ key_path = f"Software\\ Classes\\ { self .protocol_name } \\ shell\\ open\\ command"
153+ winreg .DeleteKey (winreg .HKEY_CURRENT_USER , key_path )
154+ winreg .DeleteKey (winreg .HKEY_CURRENT_USER , f"Software\\ Classes\\ { self .protocol_name } \\ shell\\ open" )
155+ winreg .DeleteKey (winreg .HKEY_CURRENT_USER , f"Software\\ Classes\\ { self .protocol_name } \\ shell" )
156+ winreg .DeleteKey (winreg .HKEY_CURRENT_USER , f"Software\\ Classes\\ { self .protocol_name } " )
157+ return True
158+
159+ except Exception as e :
160+ logger .error (f"Windows当前用户协议注销失败: { e } " )
161+ return False
162+
163+ def _is_windows_protocol_registered (self ) -> bool :
164+ """检查Windows协议是否已注册"""
165+ try :
166+ # 首先检查HKEY_CLASSES_ROOT(系统级)
167+ with winreg .OpenKey (winreg .HKEY_CLASSES_ROOT , self .protocol_name ) as key :
168+ return True
169+ except (OSError , FileNotFoundError ):
170+ try :
171+ # 如果没找到,检查HKEY_CURRENT_USER(用户级)
172+ key_path = f"Software\\ Classes\\ { self .protocol_name } "
173+ with winreg .OpenKey (winreg .HKEY_CURRENT_USER , key_path ) as key :
174+ return True
175+ except (OSError , FileNotFoundError ):
176+ return False
177+
178+ def _register_linux_protocol (self ) -> bool :
179+ """Linux系统注册协议"""
180+ try :
181+ # 获取当前可执行文件路径
182+ exe_path = self ._get_executable_path ()
183+
184+ # 创建.desktop文件
185+ desktop_file_path = self ._get_linux_desktop_file_path ()
186+ desktop_content = self ._generate_linux_desktop_file (exe_path )
187+
188+ # 写入.desktop文件
189+ with open (desktop_file_path , 'w' , encoding = 'utf-8' ) as f :
190+ f .write (desktop_content )
191+
192+ # 设置可执行权限
193+ os .chmod (desktop_file_path , 0o755 )
194+
195+ # 注册协议处理程序
196+ self ._register_linux_mime_handler ()
197+
198+ # 更新桌面数据库
199+ self ._update_linux_desktop_database ()
200+
201+ return True
202+
203+ except Exception as e :
204+ logger .error (f"Linux协议注册失败: { e } " )
205+ return False
206+
207+ def _unregister_linux_protocol (self ) -> bool :
208+ """Linux系统注销协议"""
209+ try :
210+ desktop_file_path = self ._get_linux_desktop_file_path ()
211+
212+ if os .path .exists (desktop_file_path ):
213+ os .remove (desktop_file_path )
214+
215+ # 更新桌面数据库
216+ self ._update_linux_desktop_database ()
217+
218+ return True
219+
220+ except Exception as e :
221+ logger .error (f"Linux协议注销失败: { e } " )
222+ return False
223+
224+ def _is_linux_protocol_registered (self ) -> bool :
225+ """检查Linux协议是否已注册"""
226+ desktop_file_path = self ._get_linux_desktop_file_path ()
227+ return os .path .exists (desktop_file_path )
228+
229+ def _get_linux_desktop_file_path (self ) -> str :
230+ """获取Linux桌面文件路径"""
231+ applications_dir = Path .home () / '.local' / 'share' / 'applications'
232+ applications_dir .mkdir (parents = True , exist_ok = True )
233+ return str (applications_dir / f"{ self .app_name .lower ()} -url-handler.desktop" )
234+
235+ def _generate_linux_desktop_file (self , exe_path : str ) -> str :
236+ """生成Linux桌面文件内容"""
237+ return f"""[Desktop Entry]
238+ Name={ self .app_name } URL Handler
239+ Comment=Handle { self .protocol_name } :// URLs for { self .app_name }
240+ Exec={ exe_path } --url %u
241+ Icon={ self .app_name .lower ()}
242+ Terminal=false
243+ Type=Application
244+ Categories=Utility;
245+ MimeType=x-scheme-handler/{ self .protocol_name } ;
246+ """
247+
248+ def _register_linux_mime_handler (self ):
249+ """注册Linux MIME处理程序"""
250+ try :
251+ # 使用xdg-mime注册协议处理程序
252+ subprocess .run ([
253+ 'xdg-mime' , 'default' ,
254+ f'{ self .app_name .lower ()} -url-handler.desktop' ,
255+ f'x-scheme-handler/{ self .protocol_name } '
256+ ], check = True , capture_output = True )
257+ except (subprocess .CalledProcessError , FileNotFoundError ):
258+ # 如果xdg-mime不可用,尝试直接创建mimeapps.list
259+ self ._create_linux_mimeapps_list ()
260+
261+ def _create_linux_mimeapps_list (self ):
262+ """创建Linux mimeapps.list文件"""
263+ config_dir = Path .home () / '.config'
264+ config_dir .mkdir (parents = True , exist_ok = True )
265+
266+ mimeapps_file = config_dir / 'mimeapps.list'
267+ content = f"""[Default Applications]
268+ x-scheme-handler/{ self .protocol_name } ={ self .app_name .lower ()} -url-handler.desktop
269+
270+ [Added Associations]
271+ x-scheme-handler/{ self .protocol_name } ={ self .app_name .lower ()} -url-handler.desktop
272+ """
273+
274+ with open (mimeapps_file , 'a' , encoding = 'utf-8' ) as f :
275+ f .write (content )
276+
277+ def _update_linux_desktop_database (self ):
278+ """更新Linux桌面数据库"""
279+ try :
280+ applications_dir = Path .home () / '.local' / 'share' / 'applications'
281+ subprocess .run ([
282+ 'update-desktop-database' , str (applications_dir )
283+ ], check = True , capture_output = True )
284+ except (subprocess .CalledProcessError , FileNotFoundError ):
285+ # 如果update-desktop-database不可用,忽略错误
286+ pass
287+
288+ def _get_executable_path (self ) -> str :
289+ """获取当前可执行文件路径"""
290+ if getattr (sys , 'frozen' , False ):
291+ # PyInstaller打包后的可执行文件
292+ exe_path = sys .executable
293+ else :
294+ # 普通Python脚本
295+ exe_path = sys .argv [0 ]
296+
297+ return os .path .abspath (exe_path )
0 commit comments