def startup() -> None: process_list = [] try: # 部署<单进程多线程>定时任务 if ENABLE_DEPLOY['global']: process_list.append( multiprocessing.Process(target=_SystemEngine.run_deploy, name='deploymentTimingTask')) # 部署flask if ENABLE_SERVER: process_list.append( multiprocessing.Process(target=_SystemEngine.run_server, name='deploymentFlaskAPI')) # 执行多进程任务 for process_ in process_list: logger.success(f'<SystemProcess> Startup -- {process_.name}') process_.start() # 添加阻塞 for process_ in process_list: process_.join() except TypeError or AttributeError as e: logger.exception(e) send_email(f"[程序异常终止]{str(e)}", to_='self') except KeyboardInterrupt: # FIXME 确保进程间不产生通信的情况下终止 logger.debug('<SystemProcess> Received keyboard interrupt signal') for process_ in process_list: process_.terminate() finally: logger.success('<SystemProcess> End the V2RayCloudSpider')
def push_info(self, user: dict or List[dict]): if isinstance(user, dict): user = [ user, ] elif not isinstance(user, list): logger.warning('MySQL add_user 调用格式有误') try: for user_ in user: try: sql = f'INSERT INTO v2raycs (' \ f'domain, subs, class_,end_life,res_time,passable,username,password,email,uuid) VALUES (' \ f'%s, %s, %s,%s, %s, %s,%s, %s, %s,%s)' val = (user_["domain"], user_["subs"], user_['class_'], user_['end_life'], user_["res_time"], user_['passable'], user_['username'], user_["password"], user_['email'], user_['uuid']) self.cursor.execute(sql, val) except KeyError as e: logger.warning( f"MySQL数据解析出错,user:dict必须同时包含username、password以及email的键值对{e}" ) # return 702 except pymysql.err.IntegrityError as e: logger.warning( f'{user_["username"]} -- 用户已在库,若需修改用户信息,请使用更新指令{e}') # return 701 else: logger.success(f'{user_["username"]} -- 用户添加成功') # return 700 finally: self.conn.commit() self.conn.close()
def sever_chan(title: str = None, message: str = None) -> bool: """ 调用SERVER酱微信提示 @param title: 标题最大256 @param message: 正文,支持markdown,最大64kb @return: """ if not isinstance(title, str) or not isinstance(message, str): return False import requests url = f"http://sc.ftqq.com/{SERVER_CHAN_SCKEY}.send" params = { 'text': title, 'desp': message } try: res = requests.get(url, params=params) res.raise_for_status() if res.status_code == 200 and res.json().get("errmsg") == 'success': logger.success("Server酱设备通知已发送~") return True except requests.exceptions.HTTPError: err_ = "Server酱404!!!可能原因为您的SCKEY未填写或已重置,请访问 http://sc.ftqq.com/3.version 查看解决方案\n" \ "工作流将保存此漏洞数据至error.log 并继续运行,希望您常来看看……" logger.error(err_) send_email(err_, to_='self')
def pop_subs_to_admin(class_: str): """ @param class_: @return: """ logger.debug("<SuperAdmin> -- 获取订阅") from BusinessLogicLayer.cluster.sailor import manage_task try: # 获取该类型订阅剩余链接 remain_subs: list = RedisClient().sync_remain_subs( REDIS_SECRET_KEY.format(class_)) while True: # 若无可用链接则返回错误信息 if remain_subs.__len__() == 0: logger.error(f'<SuperAdmin> -- 无可用<{class_}>订阅') return {'msg': 'failed', 'info': f"无可用<{class_}>订阅"} else: # 从池中获取(最新加入的)订阅s-e subs, end_life = remain_subs.pop() # 将s-e加入缓冲队列,该队列将被ddt的refresh工作流同过期链接一同删除 # 使用缓冲队列的方案保证节拍同步,防止过热操作/失误操作贯穿Redis # 既当管理员通过此接口获取链接时,被返回的链接不会直接从池中删去 # 而是触发缓冲机制,既将该链接标记后加入apollo缓冲队列 # apollo队列内的元素都是欲删除缓存,当ddt发动后会一次性情况当前所有的缓存 # 对订阅进行质量粗检 if subs2node(subs=subs, cache_path=False, timeout=2)['node'].__len__() <= 3: logger.debug(f"<check> BadLink -- {subs}") continue # 使用节拍同步线程锁发起连接池回滚指令,仅生成/同步一枚原子任务 threading.Thread(target=manage_task, kwargs={ "class_": class_, "only_sync": True }).start() logger.success('管理员模式--链接分发成功') # 立即执行链接解耦,将同一账号的所有订阅移除 # beat_sync =True立即刷新,False延迟刷新(节拍同步) threading.Thread(target=detach, kwargs={ "subscribe": subs, 'beat_sync': True }).start() return { 'msg': 'success', 'subscribe': subs, 'subsType': class_ } except Exception as e: logger.exception(e) return {'msg': 'failed', 'info': str(e)}
def run(beat_sync=True, force_run=None) -> None: """ 本地运行--检查队列残缺 # 所有类型任务的节点行为的同时发起 or 所有类型任务的节点行为按序执行,node任务之间互不影响 --v2rayChain --vNode_1 --vNode_2 --.... --ssrChain --sNode_1 --sNode_2 --... --.. -----> runtime v2rayChain IF USE vsu -> runtime allTask =====> runtime ... -----> runtime ssrChain ELSE -> runtime allTask -> Chain_1 -> Chain_2 -> ... -----> runtime node_1 IF USE go -> runtime allNode =====> runtime ... -----> runtime node_N ELSE -> runtime allNode-> the_node_1 -> the_node_2 -> ... @return: """ # 同步任务队列(广度优先) # 这是一次越权执行,无论本机是否具备collector权限都将执行一轮协程空间的创建任务 for class_ in CRAWLER_SEQUENCE: sailor.manage_task(class_=class_, beat_sync=beat_sync, force_run=force_run) # FIXME 节拍同步 if not beat_sync: from BusinessCentralLayer.middleware.subscribe_io import FlexibleDistribute FlexibleDistribute().start() # 执行一次数据迁移 # TODO 将集群接入多哨兵模式,减轻原生数据拷贝的额外CPU资源开销 _cd.startup_ddt_overdue() # 任务结束 logger.success('<Gevent>任务结束')
def send_email(msg, to_: List[str] or str or set, headers: str = None): """ 发送运维信息,该函数仅用于发送简单文本信息 :param msg: 正文内容 :param to_: 发送对象 1. str to_ == 'self',发送给“自己” 2. List[str] 传入邮箱列表,群发邮件(内容相同)。 :param headers: :@todo 加入日志读取功能(open file)以及富文本信息功能(html邮件) :return: 默认为'<V2Ray云彩姬>运维日志' """ headers = headers if headers else '<V2Ray云彩姬>运维日志' sender = SMTP_ACCOUNT.get('email') password = SMTP_ACCOUNT.get('sid') smtp_server = 'smtp.qq.com' message = MIMEText(msg, 'plain', 'utf-8') message['From'] = Header('ARAI.DM', 'utf-8') # 发送者 message['Subject'] = Header(f"{headers}", 'utf-8') server = smtplib.SMTP_SSL(smtp_server, 465) # 输入转换 if to_ == 'self': to_ = set(sender, ) if isinstance(to_, str): to_ = [to_, ] if isinstance(to_, list): to_ = set(to_) if not isinstance(to_, set): return False try: server.login(sender, password) for to in to_: try: message['To'] = Header(to, 'utf-8') # 接收者 server.sendmail(sender, to, message.as_string()) logger.success("发送成功->{}".format(to)) except smtplib.SMTPRecipientsRefused: logger.warning('邮箱填写错误或不存在->{}'.format(to)) except Exception as e: logger.error('>>> 发送失败 || {}'.format(e)) finally: server.quit()
def refresh(self, key_name: str, cross_threshold: int = None) -> None: """ 原子级链接池刷新,一次性删去所有过期的key_name subscribe @param cross_threshold: 越过阈值删除订阅 @param key_name:secret_key @return: """ docker: dict = self.db.hgetall(key_name) # 管理员指令获取的链接 if self.__len__(key_name) != 0: for subscribe, end_life in docker.items(): if self.is_stale(end_life, cross_threshold): logger.debug(f'del-({key_name})--{subscribe}') self.db.hdel(key_name, subscribe) logger.success('<{}> UPDATE - {}({})'.format(self.__class__.__name__, key_name, self.__len__(key_name))) else: logger.warning('<{}> EMPTY - {}({})'.format(self.__class__.__name__, key_name, self.__len__(key_name)))
def __init__(self) -> None: logger.info(f'<系统初始化> SystemEngine -> {platform}') # 读取配置序列 logger.info(f'<定位配置> check_sequence:{CRAWLER_SEQUENCE}') # 默认linux下自动部署 logger.info(f'<部署设置> enable_deploy:{ENABLE_DEPLOY}') # 协程加速配置 logger.info(f"<协程加速> Coroutine:{enable_coroutine}") # 解压接口容器 logger.info("<解压容器> DockerEngineInterface") # 初始化进程 logger.info(f'<加载队列> IndexQueue:{actions.__all__}') logger.success('<Gevent> 工程核心准备就绪 任务即将开始')
def load_any_subscribe(self, api: Chrome, element_xpath_str: str, href_xpath_str: str, class_: str, retry=0): """ @param api: ChromeDriverObject @param element_xpath_str: 用于定位链接所在的标签 @param href_xpath_str: 用于取出目标标签的属性值,既subscribe @param class_: 该subscribe类型,如`ssr`/`v2ray`/`trojan` @param retry: 失败重试 @todo 使用 retrying 模块替代retry参数实现的功能(引入断网重连,断言重试,行为回滚...) @return: """ self.subscribe = WebDriverWait(api, 30).until(expected_conditions.presence_of_element_located(( By.XPATH, element_xpath_str ))).get_attribute(href_xpath_str) if self.subscribe: for x in range(3): # ['domain', 'subs', 'class_', 'end_life', 'res_time', 'passable','username', 'password', 'email'] try: domain = urlparse(self.register_url).netloc res_time = str(datetime.now(TIME_ZONE_CN)).split('.')[0] passable = 'true' docker = [domain, self.subscribe, class_, self.end_life, res_time, passable, self.username, self.password, self.email] FlexibleDistribute(docker=docker, beat_sync=self.beat_sync) # flexible_distribute(self.subscribe, class_, self.end_life, driver_name=self.__class__.__name__) logger.success(">> GET <{}> -> {}:{}".format(self.__class__.__name__, class_, self.subscribe)) break except Exception as e: logger.debug(">> FAILED <{}> -> {}:{}".format(self.__class__.__name__, class_, e)) time.sleep(1) continue else: return None else: if retry >= 3: raise TimeoutException retry += 1 self.load_any_subscribe(api, element_xpath_str, href_xpath_str, class_, retry)
def interface(self, power: int = 8) -> None: """ @param power: 协程功率 @return: """ # 任务重载 self.offload_task() # 任务启动 task_list = [] power_ = self.power if self.power else power for x in range(power_): task = gevent.spawn(self.launch) task_list.append(task) gevent.joinall(task_list) self.killer() logger.success( f'<Gevent> mission completed -- <{self.__class__.__name__}>')
def quick_deploy_(docker=GeventSchedule, interface: str = 'interface', crontab_seconds: int = 100): """ @param crontab_seconds: 每间隔多少秒执行一次任务 @param interface: 接口函数名 @param docker: Python 类对象指针,如 SubscribesCleaner,而不是SubscribesCleaner() @return: """ logger.success(f'<GeventSchedule>启动成功 -- {docker.__name__}') def release_docker(): """ 由接口解压容器主线功能 @return: """ logger.info(f'<GeventSchedule> Release docker || Do {docker.__name__}') exec(f'docker().{interface}()') schedule.every(crontab_seconds).seconds.do(release_docker) while True: schedule.run_pending() time.sleep(1)
def _sync_actions( class_: str, mode_sync: str = None, only_sync=False, beat_sync=True, ): """ @param class_: @param mode_sync: 是否同步消息队列。False:同步本机任务队列,True:同步Redis订阅任务 @param only_sync: @param beat_sync: @return: """ logger.info( f"<TaskManager> Sync{mode_sync.title()} || 正在同步<{class_}>任务队列...") # TODO 原子化同步行为 rc = RedisClient() # 拷贝生成队列,需使用copy()完成拷贝,否则pop()会影响actions-list本体 task_list: list = actions.__all__.copy() random.shuffle(task_list) # 在本机环境中生成任务并加入消息队列 if mode_sync == 'upload': # 持续实例化采集任务 while True: if task_list.__len__() == 0: logger.success("<TaskManager> EmptyList -- 本机任务为空或已完全生成") break else: slave_ = task_list.pop() # 将相应的任务执行语句转换成exec语法 expr = f'from BusinessLogicLayer.cluster.slavers.actions import {slave_}\n' \ f'{slave_}(beat_sync={beat_sync}).run()' # 将执行语句同步至消息队列 rc.sync_message_queue(mode='upload', message=expr) # 节拍同步线程锁 if only_sync: logger.warning( "<TaskManager> OnlySync -- 触发节拍同步线程锁,仅上传一枚原子任务") break logger.info( f"<TaskManager> 本节点任务({actions.__all__.__len__()})已同步至消息队列," f"待集群接收订阅后既可完成后续任务") # 同步分布式消息队列的任务 elif mode_sync == 'download': while True: # 判断同步状态 # 防止过载。当本地缓冲任务即将突破容载极限时停止同步 # _state 状态有三,continue/offload/stop _state = _is_overflow(task_name=class_, rc=rc) if _state != 'continue': return _state # 获取原子任务,该任务应已封装为exec语法 # todo 将入队操作封装到redis里,以获得合理的循环退出条件 atomic = rc.sync_message_queue(mode='download') # 若原子有效则同步数据 if atomic: # 将执行语句推送至Poseidon本机消息队列 Middleware.poseidon.put_nowait(atomic) logger.info(f'<TaskManager> offload atomic<{class_}>') # 节拍同步线程锁 if only_sync: logger.warning( f"<TaskManager> OnlySync -- <{class_}>触发节拍同步线程锁,仅下载一枚原子任务" ) return 'offload' # 否则打印警告日志并提前退出同步 else: logger.warning(f"<TaskManager> SyncFinish -- <{class_}>无可同步任务") break elif mode_sync == 'force_run': for slave_ in task_list: # force_run :适用于单机部署或单步调试下 _state = _is_overflow(task_name=class_, rc=rc) # 需要确保无溢出风险,故即使是force_run的启动模式,任务执行数也不应逾越任务容载数 if _state == 'stop': return 'stop' # 将相应的任务执行语句转换成exec语法 expr = f'from BusinessLogicLayer.cluster.slavers.actions import {slave_}\n' \ f'{slave_}(beat_sync={beat_sync}).run()' # 将执行语句推送至Poseidon本机消息队列 Middleware.poseidon.put_nowait(expr) # 在force_run模式下仍制约于节拍同步线程锁 # 此举服务于主机的订阅补充操作 # 优先级更高,不受队列可用容载影响强制中断同步操作 if only_sync: logger.warning( f"<TaskManager> OnlySync -- <{class_}>触发节拍同步线程锁,仅下载一枚原子任务") return 'stop' else: logger.success(f"<TaskManager> ForceCollect" f" -- 已将本地预设任务({actions.__all__.__len__()})录入待执行队列") return 'offload'
def manage_task(class_: str = 'v2ray', speedup: bool = True, only_sync=False, startup=None, beat_sync=True, force_run=None) -> bool: """ 加载任务 @param force_run: debug模式下的强制运行,可逃逸队列满载检测 @param startup:创建协程工作空间,并开始并发执行队列任务。 @param only_sync:节拍同步线程锁。当本机任务数大于0时,将1枚原子任务推送至Poseidon协程空间。 @param class_: 任务类型,必须在 crawler seq内,如 ssr,v2ray or trojan。 @param speedup: 使用加速插件。默认使用coroutine-speedup。 @param beat_sync: @return: """ # ---------------------------------------------------- # 参数审查与转译 # ---------------------------------------------------- # 检查输入 if class_ not in CRAWLER_SEQUENCE or not isinstance(class_, str): return False # 审核采集权限,允许越权传参。当手动指定参数时,可授予本机采集权限,否则使用配置权限 local_work: bool = startup if startup else ENABLE_DEPLOY.get('tasks').get( 'collector') # 强制运行:指定参数优先级更高,若不指定则以是否单机部署模式决定运行force_run是否开启 # 默认单机模式下开启force_run # 若未传参时也未定义部署形式(null),则默认不使用force_run force_run = force_run if force_run else SINGLE_DEPLOYMENT # ---------------------------------------------------- # 解析同步模式 # ---------------------------------------------------- # 以本机是否有采集权限来区分download 以及upload两种同步模式 mode_sync = "download" if local_work else "upload" # 以更高优先级的`force_run` 替代传统同步模式,执行强制采集方案 mode_sync = "force_run" if force_run else mode_sync # ---------------------------------------------------- # 同步消息(任务)队列 # ---------------------------------------------------- # 当本机可采集时,将任务同步至本机执行,若消息队列为空则 # 若本机不可采集,则生成任务加入消息队列 response: str or bool = _sync_actions( class_=class_, only_sync=only_sync, beat_sync=beat_sync, mode_sync=mode_sync, ) # ---------------------------------------------------- # 初始化协程空间(执行任务) # ---------------------------------------------------- # 若本机开启了采集器权限则创建协程空间 # 若从control-deploy进入此函数,则说明本机必定具备创建协程空间权限 if force_run: if response == 'offload': logger.info(f'<TaskManager> ForceRun || <{class_}>采集任务启动') vsu(core=PuppetCore(), docker=Middleware.poseidon).run(speedup) logger.success(f'<TaskManager> ForceWorkFinish || <{class_}>采集任务结束') return True # if 'force_run' is False and the node has the permissions of collector if local_work: # if task queue can be work if response == 'offload': logger.info(f'<TaskManager> Run || <{class_}>采集任务启动') vsu(core=PuppetCore(), docker=Middleware.poseidon).run(speedup) logger.success(f'<TaskManager> Finish || <{class_}>采集任务结束') return True else: logger.warning(f"<TaskManager> Hijack<{class_}> || 当前节点不具备采集权限") return False