diff --git a/app.py b/app.py index 77c771d..7b0349e 100755 --- a/app.py +++ b/app.py @@ -98,11 +98,17 @@ user_accounts = {} # {user_id: {account_id: Account对象}} active_tasks = {} # {account_id: Thread对象} task_status = {} # {account_id: {"user_id": x, "username": y, "status": "排队中/运行中", "detail_status": "具体状态", "browse_type": z, "start_time": t, "source": s, "progress": {...}, "is_vip": bool}} +# 线程安全锁 - 修复Bug #12 +active_tasks_lock = threading.Lock() # 保护 active_tasks 字典 +task_status_lock = threading.Lock() # 保护 task_status 字典 +user_accounts_lock = threading.Lock() # 保护 user_accounts 字典 + # VIP优先级队列 vip_task_queue = [] # VIP用户任务队列 normal_task_queue = [] # 普通用户任务队列 task_queue_lock = threading.Lock() log_cache = {} # {user_id: [logs]} 每个用户独立的日志缓存 +log_cache_lock = threading.Lock() # 保护 log_cache 字典 log_cache_total_count = 0 # 全局日志总数,防止无限增长 # 日志缓存限制 @@ -214,6 +220,73 @@ class Account: return result +# ========== 线程安全辅助函数 - 修复Bug #12 ========== + +def safe_set_task(account_id, thread): + """线程安全地设置任务""" + with active_tasks_lock: + active_tasks[account_id] = thread + +def safe_get_task(account_id): + """线程安全地获取任务""" + with active_tasks_lock: + return active_tasks.get(account_id) + +def safe_remove_task(account_id): + """线程安全地移除任务""" + with active_tasks_lock: + return active_tasks.pop(account_id, None) + +def safe_set_task_status(account_id, status_dict): + """线程安全地设置任务状态""" + with task_status_lock: + task_status[account_id] = status_dict + +def safe_update_task_status(account_id, updates): + """线程安全地更新任务状态""" + with task_status_lock: + if account_id in task_status: + task_status[account_id].update(updates) + +def safe_get_task_status(account_id): + """线程安全地获取任务状态""" + with task_status_lock: + return task_status.get(account_id, {}).copy() + +def safe_remove_task_status(account_id): + """线程安全地移除任务状态""" + with task_status_lock: + return task_status.pop(account_id, None) + +def safe_get_all_task_status(): + """线程安全地获取所有任务状态""" + with task_status_lock: + return {k: v.copy() for k, v in task_status.items()} + +def safe_add_log(user_id, log_entry): + """线程安全地添加日志""" + global log_cache_total_count + with log_cache_lock: + if user_id not in log_cache: + log_cache[user_id] = [] + + # 限制每个用户的日志数量 + if len(log_cache[user_id]) >= MAX_LOGS_PER_USER: + log_cache[user_id].pop(0) + log_cache_total_count = max(0, log_cache_total_count - 1) + + log_cache[user_id].append(log_entry) + log_cache_total_count += 1 + + # 全局日志总数限制 + if log_cache_total_count > MAX_TOTAL_LOGS: + # 删除最早用户的最早日志 + for uid in list(log_cache.keys()): + if log_cache[uid]: + log_cache[uid].pop(0) + log_cache_total_count -= 1 + break + @login_manager.user_loader def load_user(user_id): """Flask-Login 用户加载""" @@ -2078,6 +2151,10 @@ def serve_screenshot(filename): if not filename.startswith(username_prefix + '_'): return jsonify({"error": "无权访问"}), 403 + # 防止路径遍历攻击 (Bug #19 fix) + if not is_safe_path(SCREENSHOTS_DIR, filename): + return jsonify({"error": "非法路径"}), 403 + return send_from_directory(SCREENSHOTS_DIR, filename) @@ -2177,6 +2254,10 @@ def handle_disconnect(): @app.route('/static/') def serve_static(filename): """提供静态文件访问""" + # 防止路径遍历攻击 (Bug #19 fix) + if not is_safe_path('static', filename): + return jsonify({"error": "非法路径"}), 403 + response = send_from_directory('static', filename) # 禁用缓存,强制浏览器每次都重新加载 response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' diff --git a/playwright_automation.py b/playwright_automation.py index 8b9af25..6393855 100755 --- a/playwright_automation.py +++ b/playwright_automation.py @@ -149,6 +149,7 @@ class PlaywrightAutomation: self.page: Optional[Page] = None self.main_page: Optional[Page] = None self._closed = False # 防止重复关闭 + self._lock = threading.Lock() # Bug #13 fix: 保护浏览器资源访问 # 注册退出清理函数,确保进程异常退出时也能关闭浏览器 atexit.register(self._cleanup_on_exit) @@ -434,14 +435,16 @@ class PlaywrightAutomation: """ for attempt in range(max_retries if retry else 1): try: - # 先检查main_page是否有效 - if not self.main_page: - self.log("⚠ main_page无效") - return None + # Bug #13 fix: 使用锁保护main_page访问 + with self._lock: + # 先检查main_page是否有效 + if not self.main_page: + self.log("⚠ main_page无效") + return None - iframe = self.main_page.frame('mainframe') - if iframe: - return iframe + iframe = self.main_page.frame('mainframe') + if iframe: + return iframe except Exception as e: error_msg = str(e) if self.is_context_error(error_msg): @@ -1327,49 +1330,51 @@ class PlaywrightAutomation: def close(self): """完全关闭浏览器进程(每个账号独立)并确保资源释放""" - # 防止重复关闭 - if self._closed: - return - self._closed = True + # Bug #13 fix: 使用锁保护close操作 + with self._lock: + # 防止重复关闭 + if self._closed: + return + self._closed = True - errors = [] + errors = [] - # 第一步:关闭上下文 - if self.context: - try: - self.context.close() - # self.log("上下文已关闭") # 精简日志 - except Exception as e: - error_msg = f"关闭上下文时出错: {str(e)}" - self.log(error_msg) - errors.append(error_msg) + # 第一步:关闭上下文 + if self.context: + try: + self.context.close() + # self.log("上下文已关闭") # 精简日志 + except Exception as e: + error_msg = f"关闭上下文时出错: {str(e)}" + self.log(error_msg) + errors.append(error_msg) - # 第二步:关闭浏览器进程 - if self.browser: - try: - self.browser.close() - # self.log("浏览器进程已关闭") # 精简日志 - except Exception as e: - error_msg = f"关闭浏览器时出错: {str(e)}" - self.log(error_msg) - errors.append(error_msg) + # 第二步:关闭浏览器进程 + if self.browser: + try: + self.browser.close() + # self.log("浏览器进程已关闭") # 精简日志 + except Exception as e: + error_msg = f"关闭浏览器时出错: {str(e)}" + self.log(error_msg) + errors.append(error_msg) - # 第三步:停止Playwright - if self.playwright: - try: - self.playwright.stop() - # self.log("Playwright已停止") # 精简日志 - except Exception as e: - error_msg = f"停止Playwright时出错: {str(e)}" - self.log(error_msg) - errors.append(error_msg) + # 第三步:停止Playwright + if self.playwright: + try: + self.playwright.stop() + # self.log("Playwright已停止") # 精简日志 + except Exception as e: + error_msg = f"停止Playwright时出错: {str(e)}" + self.log(error_msg) + errors.append(error_msg) - # 第四步:清空引用,确保垃圾回收 - self.context = None - self.page = None - self.main_page = None - self.browser = None - self.playwright = None + # 第四步:清空引用,确保垃圾回收 + self.context = None + self.page = None + self.main_page = None + self.browser = None + self.playwright = None # 第五步:强制等待,确保进程完全退出 time.sleep(0.5) @@ -1383,18 +1388,24 @@ class PlaywrightAutomation: def _cleanup_on_exit(self): """进程退出时的清理函数(由atexit调用)""" - if not self._closed: - try: - # 静默关闭,避免在退出时产生过多日志 - if self.context: - self.context.close() - if self.browser: - self.browser.close() - if self.playwright: - self.playwright.stop() - self._closed = True - except: - pass # 退出时忽略所有错误 + # Bug #13 fix: 尝试获取锁,但不阻塞(避免退出时死锁) + acquired = self._lock.acquire(blocking=False) + try: + if not self._closed: + try: + # 静默关闭,避免在退出时产生过多日志 + if self.context: + self.context.close() + if self.browser: + self.browser.close() + if self.playwright: + self.playwright.stop() + self._closed = True + except: + pass # 退出时忽略所有错误 + finally: + if acquired: + self._lock.release() def __enter__(self): """Context manager支持 - 进入"""