diff --git a/Galactic.py b/Galactic.py index 920c226..09a0848 100644 --- a/Galactic.py +++ b/Galactic.py @@ -40,7 +40,7 @@ from selenium.common.exceptions import NoSuchWindowException from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings, QWebEngineProfile from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor from PyQt5.QtNetwork import QNetworkProxyFactory -from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QSystemTrayIcon, QMenu, QAction, QDesktopWidget, QShortcut +from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QSystemTrayIcon, QMenu, QAction, QDesktopWidget, QShortcut, QFileDialog from PyQt5.QtCore import Qt, QCoreApplication, QTimer, QUrl from PyQt5.QtGui import QIcon, QKeySequence from typing import List @@ -1206,22 +1206,38 @@ class BrowserPluginParent: def __init__(self): self.app_id = None self.thread = None + self.result_color = None self.result = None - self.run_task_exception = None + self.exception = None self.user_id = None self.instant_message = None + self.batch_mode = None + self._rs = 0 + self._re = 0 + + @property + def running_time(self): + return (int(time.time()) - self._rs) if self._rs > self._re else (self._re - self._rs) def _run_task(self, *args, **kwargs): try: - self.result = self.running(*args, **kwargs) or '' + self._rs = int(time.time()) + res = self.running(*args, **kwargs) + if isinstance(res, (tuple, list)): + self.result_color = res[0] + self.result = res[1] except Exception as e: - self.run_task_exception = e + self.exception = e self.message_except(e) + finally: + self._re = int(time.time()) def message(self, message=None): print('[%s]: %s' % (str(self.app_id), message), file=sys.stderr) if len(str(message)) <= 4096: notification_send(app_id=self.app_id, title=self.name, message='%s' % (message,)) + if (self.batch_mode is True) == 1: + return None self.instant_message and self.instant_message(json.dumps({ 'time': int(time.time()), 'type': 'message', 'data': {'user_id': str(self.user_id), 'content': str(message), 'title': str(self.app_id)} })) @@ -1241,7 +1257,7 @@ class BrowserPluginParent: params = BrowserPluginRunningParam({'driver': driver, 'message': self.message, 'logging': self.logging, 'requirements': requirements}) thread = threading.Thread(target=self._run_task, args=(params,)) self.thread = thread - thread.daemon = False + thread.daemon = True thread.start() return thread @@ -1254,7 +1270,7 @@ class BrowserPluginParent: self.thread = None def state(self): - return self.thread.is_alive() if self.thread else None + return self.thread.is_alive() if self.thread else False @staticmethod def running(param: BrowserPluginRunningParam): @@ -1273,6 +1289,7 @@ class BrowserPluginFileTest(BrowserPluginParent): @staticmethod def running(param): param.message(param.requirements) + return '#FFFFFF', '已完成' class BrowserPluginMultFileTest(BrowserPluginParent): @@ -1287,6 +1304,7 @@ class BrowserPluginMultFileTest(BrowserPluginParent): @staticmethod def running(param): param.message(param.requirements) + return '#FFFFFF', '已完成' class BrowserPluginTextTest(BrowserPluginParent): @@ -1301,6 +1319,7 @@ class BrowserPluginTextTest(BrowserPluginParent): @staticmethod def running(param): param.message(param.requirements) + return '#FFFFFF', '已完成' class BrowserPluginMultTextTest(BrowserPluginParent): @@ -1315,6 +1334,7 @@ class BrowserPluginMultTextTest(BrowserPluginParent): @staticmethod def running(param): param.message(param.requirements) + return '#FFFFFF', '已完成' class BrowserPluginLoggingTest(BrowserPluginParent): @@ -1329,6 +1349,7 @@ class BrowserPluginLoggingTest(BrowserPluginParent): @staticmethod def running(param): param.logging(param.requirements[0]) + return '#FFFFFF', '已完成' class BrowserPluginMessageTest(BrowserPluginParent): @@ -1343,6 +1364,7 @@ class BrowserPluginMessageTest(BrowserPluginParent): @staticmethod def running(param): param.message(param.requirements[0]) + return '#FFFFFF', '已完成' class BrowserPluginFileCommandDebug(BrowserPluginParent): @@ -1358,6 +1380,7 @@ class BrowserPluginFileCommandDebug(BrowserPluginParent): def running(param): file = param.requirements[0][0]['path'] exec(open(file, mode='r', encoding='utf-8').read()) + return '#FFFFFF', '已完成' class BrowserPluginTextCommandDebug(BrowserPluginParent): @@ -1373,6 +1396,7 @@ class BrowserPluginTextCommandDebug(BrowserPluginParent): def running(param): code = param.requirements[0] code and exec(code) + return '#FFFFFF', '已完成' class BrowserManagerDataStorage(dict): @@ -1455,14 +1479,6 @@ class BrowserManagerUserRunning: return False except PermissionError: return True - # s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # s.settimeout(0.01) - # try: - # return s.connect_ex(('127.0.0.1', self.remote_debugging_port)) == 0 - # except socket.timeout: - # return False - # finally: - # s.close() @func_set_timeout(0.05) def _get_window_handles(self): @@ -1533,11 +1549,14 @@ class BrowserManagerUserRunning: 'user_data_dir': self.user_data_dir, 'remote_debugging_port': self.remote_debugging_port, 'is_running': self.is_running, - 'current_plugin_executing': self.plugin.id if self.plugin and self.plugin.state() else None, 'url': self.url, 'title': self.title, 'alert': self.alert, - 'window_handles': self.window_handles + 'window_handles': self.window_handles, + 'plugin_running': self.plugin.state() if self.plugin else False, + 'plugin_running_time': self.plugin.running_time if self.plugin else None, + 'plugin_execution_result': self.plugin.result if self.plugin else None, + 'plugin_execution_result_color': self.plugin.result_color if self.plugin else None } def app_id(self): @@ -1553,6 +1572,46 @@ class BrowserManagerUserRunning: self.user_name = user_name +class BrowserManagerBusy(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) + + +class BrowserPluginBatchExecution: + def __init__(self): + self.plugin_id = None + self.plugin_name = None + self.plugin_time = None + self.users = None + self.plugins = None + self.results = None + self.running = None + + def set_plugin_for_monitor(self, users: list, plugins: list, plugin_id: str, plugin_name: str): + if self.running is True: + raise Exception('Running.') + self.plugin_id = plugin_id + self.plugin_name = plugin_name + self.plugin_time = int(time.time()) + self.users = users + self.plugins = plugins + threading.Thread(target=self._monitor_until_all_completed).start() + + def _monitor_until_all_completed(self): + if not isinstance(self.plugins, list): + return None + self.results = [] + self.running = True + while True: + time.sleep(2.5) + if all(not plugin.state() for plugin in self.plugins): + break + for plugin in self.plugins: + self.results.append([plugin.app_id, plugin.running_time, plugin.result]) + self.running = False + + class BrowserManager: def __init__(self, runner, driver: str, binary: str, plugin_result_dir: str, manager_data_file: str, browser_data_home: str, browser_init_home: str, use_selenium_wire: int): self.runner = runner @@ -1580,6 +1639,7 @@ class BrowserManager: self.user_in_operation = [] self.plugins_int = {} self.plugins_ext = {} + self.plugin_batch_execution = BrowserPluginBatchExecution() def _generate_remote_debugging_port(self): exist_ports = [value['remote_debugging_port'] for key, value in self.data_storage['browser_user'].items()] @@ -1589,8 +1649,7 @@ class BrowserManager: for user_id, running in self.user_running.items(): if (not running) == 1: continue - run = BrowserManagerUserRunning(running) - run.update_status_running() + running.update_status_running() def _get_user_name(self, user_id: str): return str(self.data_storage['browser_user'][user_id]['user_name'] or '') @@ -1731,21 +1790,15 @@ class BrowserManager: def user_operate_starting(self, user_id: str): user_id = str(user_id) - # with self.threading_lock: - # if (user_id not in self.user_in_operation) == 1: - # self.user_in_operation.append(user_id) - # else: - # raise RuntimeError('Busy.') if (user_id not in self.user_in_operation) == 1: self.user_in_operation.append(user_id) else: - raise RuntimeError('Busy.') + raise BrowserManagerBusy('Busy.') def user_operate_complete(self, user_id: str): user_id = str(user_id) - with self.threading_lock: - while user_id in self.user_in_operation: - self.user_in_operation.remove(user_id) + while user_id in self.user_in_operation: + self.user_in_operation.remove(user_id) def is_user_data_occupied(self, user_id: str): try: @@ -1783,8 +1836,12 @@ class BrowserManager: plugin = self.plugins[plugin_id]() plugin.run(self._get_user_app_id(user_id), user_id, driver, requirements, self.runner.web_server.websocket_connection_manager.send_broadcast_use_sync) running.set_plugin(plugin) - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise @calculate_execution_time def plugin_shutdown(self, user_id: str): @@ -1797,8 +1854,45 @@ class BrowserManager: raise Exception('The plugin is not running.') plugin = running.plugin plugin.interrupt() - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise + + def plugin_all_users_run(self, plugin_id: str, requirements=None): + try: + if (self.plugin_batch_execution.running is True) == 1: + raise Exception('Still in batch execution.') + users = [] + plugins = [] + for user_id in self.user_ids_online(): + try: + self.plugin_run(user_id, plugin_id, requirements) + users.append(user_id) + plugin = BrowserManagerUserRunning(self.user_running[user_id]).plugin + plugin.batch_mode = True + plugins.append(plugin) + time.sleep(0.1) + except Exception: + pass + self.plugin_batch_execution.set_plugin_for_monitor(users, plugins, plugin_id, self.plugins[plugin_id].name) + except Exception: + raise + + def plugin_all_users_shutdown(self): + try: + for user_id in self.plugin_batch_execution.users: + try: + self.plugin_shutdown(user_id) + except Exception: + pass + except Exception: + pass + + def plugin_list(self): + return {plugin_id: {'name': plugin_class.name, 'requirements': plugin_class.requirements} for plugin_id, plugin_class in self.plugins.items()} def user_ids(self): return [user_id for user_id in self.data_storage['browser_user'].keys()] @@ -1808,14 +1902,17 @@ class BrowserManager: return [user_id for user_id, running in self.user_running.items() if running and running.is_running] @calculate_execution_time - def user_all_details(self, user_id: str = None): - plugins = {plugin_id: {'name': plugin_class.name, 'requirements': plugin_class.requirements} for plugin_id, plugin_class in self.plugins.items()} + def user_all_details(self): details = {user_id: self.user_running[user_id].status() for user_id in self.user_ids() if user_id in self.user_running.keys()} - details = {user_id: {**detail, **{'plugins': plugins}} for user_id, detail in details.items()} - if (user_id and user_id in details.keys()) == 1: - return details[user_id] - else: - return details + return { + 'user_online_count': len(self.user_ids_online()), + 'user_details': details, + 'plugin_batch_execution_id': self.plugin_batch_execution.plugin_id, + 'plugin_batch_execution_name': self.plugin_batch_execution.plugin_name, + 'plugin_batch_execution_time': self.plugin_batch_execution.plugin_time, + 'plugin_batch_execution_status': self.plugin_batch_execution.running, + 'plugin_batch_execution_result': self.plugin_batch_execution.results + } @calculate_execution_time def user_set_name(self, user_id: str, user_name: str): @@ -1829,8 +1926,12 @@ class BrowserManager: self.data_storage['browser_user'][user_id]['user_name'] = user_name self.data_storage.save() self.user_running[user_id].set_user_name(user_name) - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise @func_set_timeout(0.55) @calculate_execution_time @@ -1871,8 +1972,12 @@ class BrowserManager: os.path.exists(data_dir) or shutil.copytree(self.browser_init_home, data_dir) else: os.path.exists(data_dir) or os.makedirs(data_dir) - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise @calculate_execution_time def user_del(self, user_id: str): @@ -1889,8 +1994,12 @@ class BrowserManager: self.data_storage['browser_user'].pop(user_id) self.data_storage.save() self.user_running.pop(user_id) - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise @calculate_execution_time def user_run(self, user_id: str): @@ -1917,8 +2026,12 @@ class BrowserManager: res_interceptor=self.use_selenium_wire and self.res_interceptor ) running.set_driver(driver) - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise @calculate_execution_time def user_die(self, user_id: str): @@ -1944,8 +2057,45 @@ class BrowserManager: pass else: raise Exception('The debug port is not in listening.') - finally: self.user_operate_complete(user_id) + except BrowserManagerBusy: + raise + except Exception: + self.user_operate_complete(user_id) + raise + + +class ApplicationSetting(dict): + def __init__(self, file: str): + self._data_file = os.path.abspath(file) + super().__init__(self._get_json_data(self._data_file, {})) + + @staticmethod + def _get_json_data(file: str, data=None): + if os.path.exists(file): + try: + return json.loads(open(file=file, mode='r', encoding='utf-8').read()) + except json.decoder.JSONDecodeError: + return data + return data + + @staticmethod + def _put_json_data(file: str, data=None): + with open(file=file, mode='w', encoding='utf-8') as f: + flock(f, LOCK_EX) + res = f.write(json.dumps(data, indent=4, ensure_ascii=True)) + flock(f, LOCK_UN) + return res + + def __getitem__(self, item): + if item in self: + return super().__getitem__(item) + else: + return None + + def __setitem__(self, key, value): + super().__setitem__(key, value) + self._put_json_data(self._data_file, self) class MainWindow(QMainWindow): @@ -1968,10 +2118,11 @@ class MainWindow(QMainWindow): self.setWindowFlags(self.windowFlags() | Qt.WindowMinMaxButtonsHint) screen = QDesktopWidget().screenGeometry() self.screen_w, self.screen_h = screen.width(), screen.height() - self.window_w, self.window_h = round(480 * self.scale_rate), round(868 * self.scale_rate) + self.window_w, self.window_h = round(455 * self.scale_rate), round(855 * self.scale_rate) self.window_x, self.window_y = 0, 0 self.setFixedSize(self.window_w, self.window_h) QNetworkProxyFactory.setUseSystemConfiguration(False) + self.setting = ApplicationSetting(os.path.join(runner.app_data, 'application.json')) self.webview = WebEngineView() self.webview_cache_dir = os.path.join(tempfile.gettempdir(), hashlib.md5(bytes('%s_cache' % (__file__,), encoding='utf-8')).hexdigest()[:32]) web_settings = self.webview.settings() @@ -1996,8 +2147,8 @@ class MainWindow(QMainWindow): ['调试:打印环境变量信息', self.on_debug_print_environment], ['调试:打印模块搜索路径', self.on_debug_print_module_path], ['调试:重新加载外部插件', self.on_debug_reload_external_plugins], + ['配置首选文件', self.on_config_preferred_file], ['重载面板', self.webview.reload], - ['显示窗口', self.window_show], ['退出', self.exit] ] for action_item in action_list: @@ -2028,7 +2179,8 @@ class MainWindow(QMainWindow): window_x=self.pos().x(), window_y=self.pos().y() ) - self.webview.load(QUrl('http://%s:%s/' % ('127.0.0.1' if self.web_listen_host == '0.0.0.0' else self.web_listen_host, self.web_listen_port))) + self.webview.load(QUrl('http://%s:%s/index.html' % ('127.0.0.1' if self.web_listen_host == '0.0.0.0' else self.web_listen_host, self.web_listen_port))) + self.webview.reload() def exit(self): self.webview.deleteLater() @@ -2053,6 +2205,14 @@ class MainWindow(QMainWindow): def window_position_reset(self): self.setGeometry(self.window_x, self.window_y + (self.frameGeometry().height() - self.geometry().height()), self.window_w, self.window_h) + def on_config_preferred_file(self): + self.window_show() + filename, _ = QFileDialog.getOpenFileName(self, '选择文件', '/', 'All Files (*)') + if (filename and os.path.exists(filename)) == 1: + filename = os.path.abspath(filename) + self.setting['plugin_preferred_file'] = filename + QMessageBox.information(self, '提示', 'The preferred file has changed to %s' % (filename,)) + def on_tray_icon_update(self): self.tray_icon.setToolTip('活跃用户:%s/%s' % (len(self.runner.web_server.browser_manager.user_ids_online()), len(self.runner.web_server.browser_manager.user_ids()))) @@ -2169,6 +2329,14 @@ class WebServer: pass await self.websocket_connection_manager.connect(websocket, 1) + @self.app.api_route(methods=['GET', 'POST'], path='/plugin_preferred_file') + def api_plugin_preferred_file(): + file = self.runner.window and self.runner.window.setting['plugin_preferred_file'] + if (file and os.path.exists(file)) == 1: + return JSONResponse(status_code=200, content=self.message(0, '', file)) + else: + return JSONResponse(status_code=200, content=self.message(0, '', None)) + @self.app.api_route(methods=['POST'], path='/plugin_run') def api_plugin_run(data: WebServerAPIJSONData): try: @@ -2185,6 +2353,26 @@ class WebServer: except Exception as e: return self.message(1, '%s' % (e,)) + @self.app.api_route(methods=['POST'], path='/plugin_all_users_run') + def api_plugin_all_users_run(data: WebServerAPIJSONData): + try: + self.browser_manager.plugin_all_users_run(data.data['plugin_id'], data.data['requirements']) + return self.message(0) + except Exception as e: + return self.message(1, '%s' % (e,)) + + @self.app.api_route(methods=['POST'], path='/plugin_all_users_shutdown') + def api_plugin_all_users_shutdown(): + try: + self.browser_manager.plugin_all_users_shutdown() + return self.message(0) + except Exception as e: + return self.message(1, '%s' % (e,)) + + @self.app.api_route(methods=['GET', 'POST'], path='/plugin_list') + def api_plugin_list(): + return self.browser_manager.plugin_list() + @self.app.api_route(methods=['POST'], path='/upload_files') def api_upload_files(files: List[UploadFile] = File(...)): if (len(files) > 25) == 1: @@ -2197,7 +2385,7 @@ class WebServer: file_save_path = os.path.join(self.upload_dir, '%s_%s' % (int(time.time() * 1000), file.filename)) with open(file_save_path, 'wb') as f: f.write(file.file.read()) - uploaded_files.append({'filename': file.filename, 'size': file.size, 'type': file.content_type, 'path': file_save_path}) + uploaded_files.append(file_save_path) return JSONResponse(status_code=200, content=self.message(0, '', uploaded_files)) @self.app.api_route(methods=['GET', 'POST'], path='/user_all_details') @@ -2265,7 +2453,7 @@ class WebServer: @self.app.api_route(methods=['GET'], path='/') def index(): - return self.statics(os.path.join(self.www, 'index.html')) + raise HTTPException(status_code=403) @self.app.api_route(methods=['GET'], path='/200') def state(): @@ -2435,6 +2623,7 @@ class MainRunner: sys.path.append(os.path.join(os.path.dirname(__file__), 'site-packages.zip')) sys.path.append(os.path.join(self.app_data, 'Packages')) sys.path.append(os.path.join(self.app_data, 'site-packages.zip')) + print('Startup.', file=sys.stderr) try: self.preprocessing() except Exception: diff --git a/www/index.html b/www/index.html index a18f5a1..fc1c08a 100644 --- a/www/index.html +++ b/www/index.html @@ -1,6 +1,5 @@ - @@ -16,34 +15,47 @@ - - + -
-
+
+

用户管理

+
+
+
+
+

{{ pluginBatchExecutionStatus ? '正在执行:' + pluginBatchExecutionName : '批量执行' }}

+
+
+
+ +
+ +
+
+
-
-
+
+
-
-
-

未命名

-

{{ item.user_name }}

-

+
+
+

未命名

+

{{ item.user_name }}

+

{{ item.plugin_execution_result }}

- +
-
+

{{ userLoggingList[item.user_id].data }}

-

未运行

+

用户未运行

{{ item.alert || item.title }}

{{ !item.window_handles ? '0个页面' : item.window_handles.length + '个页面' }}

@@ -51,27 +63,28 @@
- - - + + +
-
-
- + +
+
+ +
-
+
+

暂时还没有用户,请先添加~

+
-

暂时还没有用户,请先添加~

-
- + - \ No newline at end of file