ascript 1.3.27__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. ascript/__init__.py +8 -0
  2. ascript/hooks/__init__.py +0 -0
  3. ascript/hooks/hook-ascript.py +2 -0
  4. ascript/windows/__init__.py +29 -0
  5. ascript/windows/daos/__init__.py +0 -0
  6. ascript/windows/daos/screen.py +143 -0
  7. ascript/windows/hardware/__init__.py +5 -0
  8. ascript/windows/hardware/keyboard.py +279 -0
  9. ascript/windows/hardware/logitech_driver.py +179 -0
  10. ascript/windows/hardware/mouse.py +308 -0
  11. ascript/windows/license/__init__.py +16 -0
  12. ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
  13. ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
  14. ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
  15. ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
  16. ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
  17. ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
  18. ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
  19. ascript/windows/media/__init__.py +0 -0
  20. ascript/windows/resources/__init__.py +0 -0
  21. ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
  22. ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
  23. ascript/windows/resources/templates/__init__.py +0 -0
  24. ascript/windows/resources/templates/__init__py.win.template +51 -0
  25. ascript/windows/resources/templates/build.as.win.template +14 -0
  26. ascript/windows/resources/templates/build.py +71 -0
  27. ascript/windows/resources/templates/logo_windows.ico +0 -0
  28. ascript/windows/resources/templates/windows/main.py +50 -0
  29. ascript/windows/resources/templates/windows/pyproject.toml +44 -0
  30. ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
  31. ascript/windows/screen/__init__.py +13 -0
  32. ascript/windows/screen/as_cv.py +382 -0
  33. ascript/windows/screen/canny.py +128 -0
  34. ascript/windows/screen/code_scanner.py +72 -0
  35. ascript/windows/screen/color_algorithms.py +277 -0
  36. ascript/windows/screen/find_colors.py +251 -0
  37. ascript/windows/screen/find_images.py +217 -0
  38. ascript/windows/screen/ocr/__init__.py +4 -0
  39. ascript/windows/screen/ocr/ocr.py +223 -0
  40. ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
  41. ascript/windows/screen/ocrx.py +169 -0
  42. ascript/windows/screen/screen_core.py +139 -0
  43. ascript/windows/store/__init__.py +21 -0
  44. ascript/windows/system/__init__.py +8 -0
  45. ascript/windows/system/device.py +355 -0
  46. ascript/windows/system/easy_print.py +115 -0
  47. ascript/windows/system/env.py +492 -0
  48. ascript/windows/system/exception/__init__.py +1 -0
  49. ascript/windows/system/key_value.py +145 -0
  50. ascript/windows/system/pip.py +175 -0
  51. ascript/windows/system/r.py +165 -0
  52. ascript/windows/system/runtime.py +62 -0
  53. ascript/windows/tools/__init__.py +27 -0
  54. ascript/windows/tools/__main__.py +21 -0
  55. ascript/windows/tools/action.json +33 -0
  56. ascript/windows/tools/action.py +168 -0
  57. ascript/windows/tools/colors_tool.py +391 -0
  58. ascript/windows/tools/config.py +26 -0
  59. ascript/windows/tools/dumptool.py +18 -0
  60. ascript/windows/tools/eel_api.py +495 -0
  61. ascript/windows/tools/gpplug/__init__.py +0 -0
  62. ascript/windows/tools/gpplug/plug.py +70 -0
  63. ascript/windows/tools/mouse_catch.py +168 -0
  64. ascript/windows/tools/plug_manager.py +38 -0
  65. ascript/windows/tools/tool_api.py +906 -0
  66. ascript/windows/tools/tool_window.py +56 -0
  67. ascript/windows/tools/uia_worker.py +365 -0
  68. ascript/windows/tools/web/app_store_window.html +481 -0
  69. ascript/windows/tools/web/colors.html +532 -0
  70. ascript/windows/tools/web/css/screen.css +1342 -0
  71. ascript/windows/tools/web/css/viewtree.css +357 -0
  72. ascript/windows/tools/web/flot_control.html +195 -0
  73. ascript/windows/tools/web/img/ico/favicon.png +0 -0
  74. ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
  75. ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
  76. ascript/windows/tools/web/img/svg/find_images.svg +1 -0
  77. ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
  78. ascript/windows/tools/web/img/svg/img_default.svg +1 -0
  79. ascript/windows/tools/web/img/svg/ocr.svg +1 -0
  80. ascript/windows/tools/web/js/screen/colors.js +466 -0
  81. ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
  82. ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
  83. ascript/windows/tools/web/js/screen/img_view.js +558 -0
  84. ascript/windows/tools/web/js/screen/plug.js +246 -0
  85. ascript/windows/tools/web/js/screen/screen.js +690 -0
  86. ascript/windows/tools/web/js/screen/screen_api.js +778 -0
  87. ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
  88. ascript/windows/tools/web/js/utils/map.js +73 -0
  89. ascript/windows/tools/web/js/utils/upload.js +157 -0
  90. ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
  91. ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
  92. ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
  93. ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
  94. ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
  95. ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
  96. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
  97. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
  98. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
  99. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
  100. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
  101. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
  102. ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
  103. ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
  104. ascript/windows/tools/web/libs/jquery/core.js +399 -0
  105. ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
  106. ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
  107. ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
  108. ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
  109. ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
  110. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
  111. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
  112. ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
  113. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
  114. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
  115. ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
  116. ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
  117. ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
  118. ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
  119. ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
  120. ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
  121. ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
  122. ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
  123. ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
  124. ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
  125. ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
  126. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
  127. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
  128. ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
  129. ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
  130. ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
  131. ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
  132. ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
  133. ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
  134. ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
  135. ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
  136. ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
  137. ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
  138. ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
  139. ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
  140. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
  141. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
  142. ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
  143. ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
  144. ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
  145. ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
  146. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
  147. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
  148. ascript/windows/tools/web/license_window.html +255 -0
  149. ascript/windows/tools/web/plugs/a.html +653 -0
  150. ascript/windows/tools/web/plugs/demo.html +52 -0
  151. ascript/windows/tools/web/plugs/find_colors.html +629 -0
  152. ascript/windows/tools/web/plugs/find_images.html +699 -0
  153. ascript/windows/tools/web/plugs/rapidocr.html +954 -0
  154. ascript/windows/tools/web/vtree.html +496 -0
  155. ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
  156. ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
  157. ascript/windows/tools/webview/index.html +13 -0
  158. ascript/windows/ui/__init__.py +6 -0
  159. ascript/windows/ui/app_store_window.py +272 -0
  160. ascript/windows/ui/draw_window.py +231 -0
  161. ascript/windows/ui/flot_test.py +297 -0
  162. ascript/windows/ui/flot_window.py +488 -0
  163. ascript/windows/ui/license_window.py +111 -0
  164. ascript/windows/ui/web_window.py +466 -0
  165. ascript/windows/utils/__init__.py +0 -0
  166. ascript/windows/utils/node_utils.py +20 -0
  167. ascript/windows/utils/screen_utils.py +280 -0
  168. ascript/windows/window/Selector.py +1015 -0
  169. ascript/windows/window/Window.py +747 -0
  170. ascript/windows/window/__init__.py +5 -0
  171. ascript/windows/window/launcher.py +213 -0
  172. ascript/windows/window/uia_runtime.py +26 -0
  173. ascript-1.3.27.dist-info/METADATA +41 -0
  174. ascript-1.3.27.dist-info/RECORD +177 -0
  175. ascript-1.3.27.dist-info/WHEEL +5 -0
  176. ascript-1.3.27.dist-info/entry_points.txt +2 -0
  177. ascript-1.3.27.dist-info/top_level.txt +1 -0
@@ -0,0 +1,492 @@
1
+ import os
2
+ import shutil
3
+ import threading
4
+ import subprocess
5
+ import sys
6
+ import ctypes
7
+ import psutil
8
+ import time
9
+ import zipfile
10
+ from email.parser import Parser
11
+
12
+ from .pip import Pip
13
+
14
+
15
+ CREATE_NEW_CONSOLE = 0x00000010
16
+ CREATE_NO_WINDOW = 0x08000000
17
+
18
+ # 这些环境变量如果泄漏进 venv 的 python.exe, 会让它去加载用户机器上
19
+ # 另一个 Python 安装的 stdlib / site-packages, ABI 不匹配直接 SEH 崩溃, 表现就是
20
+ # "Python 已停止工作" 弹窗 (无 stderr, 无退出码可追). 调用 venv 子进程前一律剥掉.
21
+ _PYTHON_ENV_VARS_TO_STRIP = (
22
+ "PYTHONHOME",
23
+ "PYTHONPATH",
24
+ "PYTHONSTARTUP",
25
+ "PYTHONUSERBASE",
26
+ "PYTHONEXECUTABLE",
27
+ "__PYVENV_LAUNCHER__",
28
+ )
29
+
30
+
31
+ def _clean_env() -> dict:
32
+ env = os.environ.copy()
33
+ for k in _PYTHON_ENV_VARS_TO_STRIP:
34
+ env.pop(k, None)
35
+ return env
36
+
37
+
38
+ # 进程全局: 让子进程崩溃以退出码体现, 而不是 Windows 接管弹"调试/关闭程序"对话框
39
+ # (那对话框会把 stderr 一并埋掉). SetErrorMode 影响整个进程, 在模块加载时设一次即可.
40
+ if sys.platform == "win32":
41
+ # SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX
42
+ ctypes.windll.kernel32.SetErrorMode(0x0001 | 0x0002 | 0x8000)
43
+
44
+
45
+ class EnvManager:
46
+ """用户虚拟环境管理器。
47
+
48
+ 负责为每个用户创建独立的 Python 虚拟环境,支持 wheel 包的自动部署、
49
+ 应用启动与停止、运行状态监控等功能。每个用户通过 user_id 隔离环境。
50
+ """
51
+
52
+ def __init__(self, root_path=None):
53
+ """初始化环境管理器。
54
+
55
+ Args:
56
+ root_path: 环境根目录路径,默认为当前工作目录下的 "user_envs" 文件夹。
57
+ 所有用户的虚拟环境和模块将存放在此目录下。
58
+ """
59
+ self.root_path = root_path or os.path.abspath(os.path.join(os.getcwd(), "user_envs"))
60
+ self.module_dir = os.path.join(self.root_path, "module")
61
+ if not os.path.exists(self.root_path):
62
+ os.makedirs(self.root_path)
63
+
64
+ if not os.path.exists(self.module_dir):
65
+ os.makedirs(self.module_dir)
66
+
67
+ def _get_pkg_name(self, wheel_path):
68
+ """从 wheel 文件中提取 Python 包名。
69
+
70
+ Args:
71
+ wheel_path: .whl 文件的路径。
72
+
73
+ Returns:
74
+ str | None: 提取到的包名,失败时返回 None。
75
+ """
76
+ try:
77
+ with zipfile.ZipFile(wheel_path) as wheel:
78
+ metadata_path = next(
79
+ (name for name in wheel.namelist() if name.endswith(".dist-info/METADATA")),
80
+ None,
81
+ )
82
+ if metadata_path is None:
83
+ return None
84
+ metadata = Parser().parsestr(wheel.read(metadata_path).decode("utf-8", errors="replace"))
85
+ return metadata.get("Name")
86
+ except Exception as e:
87
+ print(f"提取包名失败: {e}")
88
+ return None
89
+
90
+ def get_env_dir(self, user_id):
91
+ """获取指定用户的虚拟环境目录路径。
92
+
93
+ Args:
94
+ user_id: 用户唯一标识。
95
+
96
+ Returns:
97
+ str: 虚拟环境目录的绝对路径。
98
+ """
99
+ return os.path.join(self.root_path, str(user_id))
100
+
101
+ def get_model_dir(self, user_id):
102
+ """获取指定用户的模块目录路径。
103
+
104
+ Args:
105
+ user_id: 用户唯一标识。
106
+
107
+ Returns:
108
+ str: 模块目录的绝对路径。
109
+ """
110
+ return os.path.join(self.module_dir, str(user_id))
111
+
112
+ def get_python_exe(self, user_id):
113
+ """获取指定用户虚拟环境中的 Python 可执行文件路径。
114
+
115
+ Args:
116
+ user_id: 用户唯一标识。
117
+
118
+ Returns:
119
+ str: Python 可执行文件的绝对路径。
120
+ """
121
+ base = self.get_env_dir(user_id)
122
+ suffix = "Scripts\\python.exe" if sys.platform == "win32" else "bin/python"
123
+ return os.path.join(base, suffix)
124
+
125
+ def get_pip_exe(self, user_id):
126
+ """获取指定用户虚拟环境中的 pip 可执行文件路径。
127
+
128
+ Args:
129
+ user_id: 用户唯一标识。
130
+
131
+ Returns:
132
+ str: pip 可执行文件的绝对路径。
133
+ """
134
+ base = self.get_env_dir(user_id)
135
+ suffix = "Scripts\\pip.exe" if sys.platform == "win32" else "bin/pip"
136
+ return os.path.join(base, suffix)
137
+
138
+ def get_create_cmd(self, env_dir):
139
+ """生成创建虚拟环境的命令字符串。
140
+
141
+ Args:
142
+ env_dir: 虚拟环境目标目录路径。
143
+
144
+ Returns:
145
+ str: 用于创建虚拟环境的 shell 命令。
146
+ """
147
+ return f'"{sys.executable}" -m virtualenv "{env_dir}"'
148
+
149
+ def smart_deploy(self, user_id, wheel_path, file_md5, show_console=True):
150
+ """智能部署用户环境。
151
+
152
+ 根据 MD5 校验判断环境是否已存在且完整,若匹配则跳过部署;
153
+ 否则清理旧环境并重新创建虚拟环境、安装 wheel 包。
154
+
155
+ Args:
156
+ user_id: 用户唯一标识。
157
+ wheel_path: .whl 安装包的文件路径。
158
+ file_md5: wheel 文件的 MD5 校验值,用于判断环境是否需要更新。
159
+ show_console: 是否显示安装过程的控制台窗口,默认 True。
160
+
161
+ Returns:
162
+ bool: 部署成功返回 True,失败返回 False。
163
+ """
164
+ env_dir = self.get_env_dir(user_id)
165
+ md5_file = os.path.join(env_dir, "env_identity.md5")
166
+ name_info_file = os.path.join(env_dir, "pkg_name.info") # 记录包名的文件
167
+ python_exe = self.get_python_exe(user_id)
168
+
169
+ # wheel_path = r"D:\workspace\python\ascript-windows\apps\module\1085\8bb2a42175a3d4f473f58333020aebda\ascriptproject2-0.1.0-py3-none-any.whl"
170
+
171
+ is_ready = False
172
+ # 增加对 pkg_name.info 的存在性检查
173
+ if os.path.exists(md5_file) and os.path.exists(python_exe) and os.path.exists(name_info_file):
174
+ try:
175
+ with open(md5_file, "r", encoding="utf-8") as f:
176
+ saved_md5 = f.read().strip()
177
+ if saved_md5 == file_md5:
178
+ is_ready = True
179
+ except Exception:
180
+ is_ready = False
181
+
182
+ if is_ready:
183
+ print(f"[{user_id}] 环境匹配且完整。")
184
+ return True
185
+
186
+ print(f"[{user_id}] 环境重建中...")
187
+
188
+ # 提前提取包名
189
+ current_pkg_name = self._get_pkg_name(wheel_path)
190
+ if not current_pkg_name:
191
+ self._show_error_box(f"无法从文件解析包名: {wheel_path}", "部署异常")
192
+ return False
193
+
194
+ try:
195
+ if os.path.exists(env_dir):
196
+ self.stop_app(user_id)
197
+ time.sleep(0.8)
198
+
199
+ if os.path.exists(env_dir):
200
+ shutil.rmtree(env_dir, ignore_errors=True)
201
+
202
+ os.makedirs(self.root_path, exist_ok=True)
203
+
204
+ # 执行安装
205
+ self._execute_combined_install(user_id, wheel_path, env_dir, show_console)
206
+
207
+ # 写入校验信息
208
+ with open(md5_file, "w", encoding="utf-8") as f:
209
+ f.write(file_md5)
210
+ # 关键修改:持久化包名,供 run_app 使用
211
+ with open(name_info_file, "w", encoding="utf-8") as f:
212
+ f.write(current_pkg_name)
213
+
214
+ return True
215
+
216
+ except RuntimeError as re:
217
+ error_text = str(re)
218
+ print(f"[{user_id}] 部署中断: {error_text}")
219
+ log_path = os.path.join(self.root_path, f"deploy_error_{user_id}.log")
220
+ try:
221
+ with open(log_path, "w", encoding="utf-8") as f:
222
+ f.write(error_text)
223
+ except Exception:
224
+ log_path = None
225
+
226
+ # MessageBox 没有滚动条,只展示末尾最有价值的部分;完整内容保存在日志中。
227
+ display_text = error_text[-3000:]
228
+ if log_path:
229
+ display_text += f"\n\n完整日志:{log_path}"
230
+ self._show_error_box(display_text, f"插件 {user_id} 部署失败")
231
+ if os.path.exists(env_dir):
232
+ shutil.rmtree(env_dir, ignore_errors=True)
233
+ return False
234
+ except Exception as e:
235
+ self._show_error_box(f"用户 {user_id} 部署失败!\n原因: {str(e)}", "环境部署中断")
236
+ if os.path.exists(env_dir):
237
+ shutil.rmtree(env_dir, ignore_errors=True)
238
+ return False
239
+
240
+ def _execute_combined_install(self, user_id, wheel_path, env_dir, show_console):
241
+ """分三步部署: 创建 venv → 自检 venv python.exe → pip 安装 wheel。
242
+
243
+ 关键是把曾经的 "cmd /s /c virtualenv && pip install" 拆开 —— 那种串法任一步
244
+ 崩了 stderr 都会被外层 cmd 吞掉, 用户只看到 "Python 已停止工作" 这种无法定位
245
+ 的提示。拆开后每一步独立 subprocess + capture, 用户能拿到具体退出码 + stderr。
246
+
247
+ Raises:
248
+ RuntimeError: 任一步骤失败,message 含退出码和完整输出。
249
+ """
250
+ abs_env_dir = os.path.abspath(env_dir)
251
+ abs_wheel_path = os.path.abspath(wheel_path)
252
+ venv_python = self.get_python_exe(user_id)
253
+ env = _clean_env()
254
+
255
+ r = subprocess.run(
256
+ [sys.executable, "-m", "virtualenv", abs_env_dir],
257
+ env=env, capture_output=True, text=True,
258
+ creationflags=CREATE_NO_WINDOW,
259
+ )
260
+ if r.returncode != 0:
261
+ raise RuntimeError(
262
+ f"虚拟环境创建失败 (exit {r.returncode}):\n"
263
+ f"{(r.stderr or r.stdout).strip() or '(no output)'}"
264
+ )
265
+
266
+ # 把"看不见的 SEH 崩溃"变成"看得见的退出码", 让用户能据此自查或截屏反馈。
267
+ r = subprocess.run(
268
+ [venv_python, "-c", "import sys; print(sys.version)"],
269
+ env=env, capture_output=True, text=True, timeout=15,
270
+ )
271
+ if r.returncode != 0:
272
+ code_hex = f"0x{r.returncode & 0xFFFFFFFF:08X}"
273
+ hints = {
274
+ "0xC0000142": "进程初始化失败。常见原因: 安装路径在网络盘 / 外置盘 / OneDrive 同步盘, 或被企业 EDR 拦截。",
275
+ "0xC0000135": "缺少必要 DLL。请检查 VC++ 运行库或安全软件是否隔离了 python.exe 的依赖。",
276
+ "0xC0000005": "访问违例。常见于安全软件 (Defender ASR / 360 / 火绒) 拦截了进程启动。",
277
+ "0xC000007B": "DLL 位数错配。系统 PATH 上可能存在与 AScript 自带 Python 冲突的同名 DLL。",
278
+ }
279
+ hint = hints.get(code_hex, "请把以上完整信息发给开发者排查。")
280
+ raise RuntimeError(
281
+ f"虚拟环境自检失败 (exit {code_hex}):\n"
282
+ f" 路径: {venv_python}\n"
283
+ f" stdout: {r.stdout.strip() or '(empty)'}\n"
284
+ f" stderr: {r.stderr.strip() or '(empty)'}\n"
285
+ f" 提示: {hint}"
286
+ )
287
+
288
+ index_url = Pip.get_best_index()
289
+ print(f"[{user_id}] 自动择源完成,选用: {index_url}")
290
+ timeout = getattr(self, 'config', {}).get('timeout', 15)
291
+ pip_args = [
292
+ # 使用已经通过自检的 python.exe 启动 pip,避开 pip.exe 启动器静默崩溃。
293
+ venv_python, "-m", "pip", "install", "--no-cache-dir",
294
+ "-i", index_url,
295
+ "--default-timeout", str(timeout),
296
+ abs_wheel_path,
297
+ ]
298
+
299
+ r = subprocess.run(
300
+ pip_args,
301
+ env=env,
302
+ capture_output=True,
303
+ text=True,
304
+ errors="replace",
305
+ creationflags=CREATE_NO_WINDOW,
306
+ )
307
+ output = "\n".join(part for part in (r.stdout, r.stderr) if part).strip()
308
+ if r.returncode != 0:
309
+ detail = output or "(pip 未输出任何错误信息)"
310
+ raise RuntimeError(
311
+ f"插件依赖安装失败 (exit {r.returncode}):\n{detail}"
312
+ )
313
+ if output:
314
+ print(output)
315
+
316
+ def run_app(self, user_id, entry_module="main", cwd=None, show_console=False):
317
+ """启动用户应用程序。
318
+
319
+ 在指定用户的虚拟环境中运行应用入口模块,自动从环境配置中
320
+ 读取包名。启动后会创建后台监控线程跟踪进程状态。
321
+
322
+ Args:
323
+ user_id: 用户唯一标识。
324
+ entry_module: 入口模块名,默认 "main"。
325
+ cwd: 工作目录,默认为用户环境目录。
326
+ show_console: 是否显示控制台窗口,默认 False。
327
+
328
+ Returns:
329
+ int | None: 启动成功返回进程 PID,失败返回 None。
330
+ """
331
+ python_exe = self.get_python_exe(user_id)
332
+ env_dir = self.get_env_dir(user_id)
333
+ name_info_file = os.path.join(env_dir, "pkg_name.info")
334
+
335
+ # 自动获取包名
336
+ if not os.path.exists(name_info_file):
337
+ self._show_error_box("找不到环境包名配置,请重新部署环境。", "启动失败")
338
+ return None
339
+
340
+ with open(name_info_file, "r", encoding="utf-8") as f:
341
+ package_name = f.read().strip()
342
+
343
+ work_dir = cwd or env_dir
344
+ last_error_log = os.path.join(env_dir, "last_error.log")
345
+
346
+ if os.path.exists(last_error_log):
347
+ try:
348
+ os.remove(last_error_log)
349
+ except:
350
+ pass
351
+
352
+ target_module = f"{package_name}.{entry_module}"
353
+ cmd = [python_exe, "-u", "-m", target_module]
354
+ pid_file = os.path.join(env_dir, "app.pid")
355
+
356
+ try:
357
+ flags = 0 if show_console else CREATE_NO_WINDOW
358
+ with open(last_error_log, "w", encoding="utf-8") as f_err:
359
+ process = subprocess.Popen(
360
+ cmd,
361
+ cwd=work_dir,
362
+ creationflags=flags,
363
+ stdout=subprocess.DEVNULL,
364
+ stderr=f_err,
365
+ text=True,
366
+ encoding='utf-8',
367
+ env=_clean_env(),
368
+ )
369
+
370
+ with open(pid_file, "w") as f:
371
+ f.write(str(process.pid))
372
+
373
+ monitor_thread = threading.Thread(
374
+ target=self._wait_and_handle_error,
375
+ args=(process, user_id, package_name, pid_file, show_console, last_error_log),
376
+ daemon=True
377
+ )
378
+ monitor_thread.start()
379
+ return process.pid
380
+ except Exception as e:
381
+ self._show_error_box(f"启动失败: {str(e)}", "致命错误")
382
+ return None
383
+
384
+ def _wait_and_handle_error(self, process, user_id, name, pid_file, show_console, log_path):
385
+ """等待进程结束并处理异常退出。
386
+
387
+ 作为后台监控线程的目标函数运行,在进程异常退出时
388
+ 读取错误日志并弹出错误提示框。
389
+
390
+ Args:
391
+ process: subprocess.Popen 进程对象。
392
+ user_id: 用户唯一标识。
393
+ name: 包名称,用于错误提示。
394
+ pid_file: PID 文件路径,用于判断是否为手动停止。
395
+ show_console: 是否显示了控制台窗口。
396
+ log_path: 错误日志文件路径。
397
+ """
398
+ try:
399
+ process.wait()
400
+ is_manual_stop = not os.path.exists(pid_file)
401
+ if process.returncode != 0 and not show_console and not is_manual_stop:
402
+ stderr_content = ""
403
+ if os.path.exists(log_path):
404
+ with open(log_path, "r", encoding="utf-8") as f:
405
+ stderr_content = f.read()
406
+ self._handle_runtime_error(name, stderr_content, process.returncode)
407
+ finally:
408
+ if os.path.exists(pid_file):
409
+ try:
410
+ os.remove(pid_file)
411
+ except:
412
+ pass
413
+
414
+ def _handle_runtime_error(self, name, stderr, code):
415
+ """处理运行时错误:记录崩溃日志并弹出错误提示。
416
+
417
+ Args:
418
+ name: 模块/包名称。
419
+ stderr: 标准错误输出内容。
420
+ code: 进程退出码。
421
+ """
422
+ if not stderr: stderr = f"进程异常退出,退出码: {code}"
423
+ lines = [line.strip() for line in stderr.splitlines() if line.strip()]
424
+ error_summary = "\n".join(lines[-10:])
425
+ try:
426
+ log_path = os.path.join(os.getcwd(), f"crash_{int(time.time())}.log")
427
+ with open(log_path, "w", encoding="utf-8") as f:
428
+ f.write(f"Time: {time.ctime()}\nModule: {name}\nCode: {code}\n{stderr}")
429
+ except:
430
+ pass
431
+ self._show_error_box(f"【代码崩溃】核心摘要:\n{error_summary}", f"运行异常 - {name}")
432
+
433
+ def _show_error_box(self, content, title):
434
+ """在 Windows 上弹出错误消息框。
435
+
436
+ Args:
437
+ content: 消息框内容文本。
438
+ title: 消息框标题。
439
+ """
440
+ if sys.platform == "win32":
441
+ ctypes.windll.user32.MessageBoxW(0, content, title, 16)
442
+
443
+ def is_running(self, user_id):
444
+ """检查指定用户的应用是否正在运行。
445
+
446
+ 通过 PID 文件和进程信息双重验证运行状态。
447
+
448
+ Args:
449
+ user_id: 用户唯一标识。
450
+
451
+ Returns:
452
+ bool: 应用正在运行返回 True,否则返回 False。
453
+ """
454
+ env_dir = self.get_env_dir(user_id)
455
+ pid_file = os.path.join(env_dir, "app.pid")
456
+ if not os.path.exists(pid_file): return False
457
+ try:
458
+ with open(pid_file, "r") as f:
459
+ saved_pid = int(f.read().strip())
460
+ if not psutil.pid_exists(saved_pid):
461
+ if os.path.exists(pid_file): os.remove(pid_file)
462
+ return False
463
+ proc = psutil.Process(saved_pid)
464
+ expected_python = os.path.normcase(os.path.normpath(self.get_python_exe(user_id)))
465
+ actual_python = os.path.normcase(os.path.normpath(proc.exe()))
466
+ return expected_python == actual_python
467
+ except Exception:
468
+ return False
469
+
470
+ def stop_app(self, user_id):
471
+ """停止指定用户的应用进程。
472
+
473
+ 先删除 PID 文件(标记为手动停止),然后使用 taskkill 强制终止进程树。
474
+
475
+ Args:
476
+ user_id: 用户唯一标识。
477
+ """
478
+ pid_file = os.path.join(self.get_env_dir(user_id), "app.pid")
479
+ if not os.path.exists(pid_file): return
480
+ try:
481
+ with open(pid_file, "r") as f:
482
+ pid = int(f.read().strip())
483
+ if os.path.exists(pid_file):
484
+ try:
485
+ os.remove(pid_file)
486
+ except:
487
+ pass
488
+ if psutil.pid_exists(pid):
489
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], creationflags=CREATE_NO_WINDOW,
490
+ capture_output=True)
491
+ except:
492
+ pass
@@ -0,0 +1,145 @@
1
+ import json
2
+ import os
3
+ import pathlib
4
+ import threading
5
+
6
+ class KeyValueMeta(type):
7
+ """KeyValue 的元类,支持类级别的索引操作。
8
+
9
+ 通过该元类,可以直接使用 KeyValue["key"] 语法进行键值的
10
+ 读取、设置和删除操作。
11
+ """
12
+
13
+ def __getitem__(cls, key):
14
+ """通过索引语法获取值。
15
+
16
+ Args:
17
+ key: 键名。
18
+
19
+ Returns:
20
+ 对应键的值,不存在时返回 None。
21
+ """
22
+ return cls.get(key)
23
+
24
+ def __setitem__(cls, key, value):
25
+ """通过索引语法设置值。
26
+
27
+ Args:
28
+ key: 键名。
29
+ value: 要保存的值。
30
+ """
31
+ cls.save(key, value)
32
+
33
+ def __delitem__(cls, key):
34
+ """通过索引语法删除键值对。
35
+
36
+ Args:
37
+ key: 要删除的键名。
38
+ """
39
+ cls.remove(key)
40
+
41
+
42
+ class KeyValue(metaclass=KeyValueMeta):
43
+ """基于 JSON 文件的持久化键值存储。
44
+
45
+ 提供线程安全的键值对读写操作,数据自动持久化到 JSON 文件。
46
+ 支持类级别的索引操作(如 KeyValue["key"] = value)。
47
+
48
+ Attributes:
49
+ FILENAME: 存储文件名,默认为 "config.json"。
50
+ """
51
+ _data = None
52
+ _file_path = None
53
+ _lock = threading.Lock() # 线程锁,防止并发写入冲突
54
+ FILENAME = "config.json"
55
+
56
+ @classmethod
57
+ def _initialize(cls):
58
+ """延迟初始化:确定存储路径并加载已有数据。
59
+
60
+ 首次访问时自动调用,确保数据文件路径和内存数据已就绪。
61
+ """
62
+ with cls._lock:
63
+ if cls._data is None:
64
+ cls._file_path = cls._determine_path()
65
+ cls._data = cls._load_data()
66
+
67
+ @classmethod
68
+ def _determine_path(cls):
69
+ """确定配置文件的存储路径。
70
+
71
+ 优先使用当前工作目录,若无写权限则回退到用户家目录下的隐藏文件夹。
72
+
73
+ Returns:
74
+ pathlib.Path: 配置文件的完整路径。
75
+ """
76
+ # 1. 尝试根目录
77
+ root_path = pathlib.Path(os.getcwd())
78
+ if os.access(root_path, os.W_OK):
79
+ return root_path / cls.FILENAME
80
+
81
+ # 2. 回退到用户家目录 (隐藏文件夹)
82
+ home_dir = pathlib.Path.home() / ".app_data"
83
+ home_dir.mkdir(parents=True, exist_ok=True)
84
+ return home_dir / cls.FILENAME
85
+
86
+ @classmethod
87
+ def _load_data(cls):
88
+ """从 JSON 文件加载数据到内存。
89
+
90
+ Returns:
91
+ dict: 解析后的键值数据字典,文件不存在或解析失败时返回空字典。
92
+ """
93
+ if cls._file_path.exists():
94
+ try:
95
+ return json.loads(cls._file_path.read_text(encoding='utf-8'))
96
+ except:
97
+ return {}
98
+ return {}
99
+
100
+ @classmethod
101
+ def _commit(cls):
102
+ """将内存数据持久化写入 JSON 文件(线程安全)。"""
103
+ with cls._lock:
104
+ cls._file_path.write_text(
105
+ json.dumps(cls._data, indent=4, ensure_ascii=False),
106
+ encoding='utf-8'
107
+ )
108
+
109
+ @classmethod
110
+ def save(cls, key, value):
111
+ """保存键值对并持久化到文件。
112
+
113
+ Args:
114
+ key: 键名。
115
+ value: 要保存的值,需可 JSON 序列化。
116
+ """
117
+ cls._initialize()
118
+ cls._data[key] = value
119
+ cls._commit()
120
+
121
+ @classmethod
122
+ def get(cls, key, default=None):
123
+ """获取指定键的值。
124
+
125
+ Args:
126
+ key: 键名。
127
+ default: 键不存在时的默认返回值,默认 None。
128
+
129
+ Returns:
130
+ 对应键的值,不存在时返回 default。
131
+ """
132
+ cls._initialize()
133
+ return cls._data.get(key, default)
134
+
135
+ @classmethod
136
+ def remove(cls, key):
137
+ """删除指定键值对并持久化更新。
138
+
139
+ Args:
140
+ key: 要删除的键名。若键不存在则不执行任何操作。
141
+ """
142
+ cls._initialize()
143
+ if key in cls._data:
144
+ del cls._data[key]
145
+ cls._commit()