def app_start_adb(self, package_name, allow_failure=False): """ Args: package_name (str): allow_failure (bool): Returns: bool: If success to start """ result = self.adb_shell([ 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1' ]) if 'No activities found' in result: # ** No activities found to run, monkey aborted. if allow_failure: return False else: logger.error(result) possible_reasons( f'"{package_name}" not found, please check setting Emulator.PackageName' ) raise RequestHumanTakeover else: # Events injected: 1 # ## Network stats: elapsed time=4ms (0ms mobile, 0ms wifi, 4ms not connected) return True
def app_start_wsa(self, package_name=None, display=0): """ Args: package_name (str): display (int): Returns: bool: If success to start """ if not package_name: package_name = self.package self.adb_shell(['svc', 'power', 'stayon', 'true']) activity_name = self.get_main_activity_name(package_name=package_name) result = self.adb_shell([ 'am', 'start', '--display', display, f'{package_name}/{activity_name}' ]) if 'Activity not started' in result or 'does not exist' in result: # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] pkg=xxx } # Error: Activity not started, unable to resolve Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 pkg=xxx } # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.bilibili.azurlane/xxx } # Error type 3 # Error: Activity class {com.bilibili.azurlane/com.manjuu.azurlane.MainAct} does not exist. logger.error(result) raise PackageNotInstalled(package_name) else: # Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.bilibili.azurlane/com.manjuu.azurlane.MainActivity } return True
def app_start_adb(self, package_name=None, allow_failure=False): """ Args: package_name (str): allow_failure (bool): Returns: bool: If success to start """ if not package_name: package_name = self.package result = self.adb_shell([ 'monkey', '-p', package_name, '-c', 'android.intent.category.LAUNCHER', '1' ]) if 'No activities found' in result: # ** No activities found to run, monkey aborted. if allow_failure: return False else: logger.error(result) raise PackageNotInstalled(package_name) else: # Events injected: 1 # ## Network stats: elapsed time=4ms (0ms mobile, 0ms wifi, 4ms not connected) return True
def hermit_send(self, url, **kwargs): """ Args: url (str): **kwargs: Returns: dict: Usually to be {"code":0,"msg":"ok"} """ result = self.hermit_session.get(f'{self._hermit_url}{url}', params=kwargs, timeout=3).text try: result = json.loads(result, encoding='utf-8') if result['code'] != 0: # {"code":-1,"msg":"error"} raise HermitError(result) except (json.decoder.JSONDecodeError, KeyError): e = HermitError(result) if 'GestureDescription$Builder' in result: logger.error(e) logger.critical('Hermit cannot run on current device, hermit requires Android>=7.0') raise RequestHumanTakeover if 'accessibilityservice' in result: # Attempt to invoke virtual method # 'boolean android.accessibilityservice.AccessibilityService.dispatchGesture( # android.accessibilityservice.GestureDescription, # android.accessibilityservice.AccessibilityService$GestureResultCallback, # android.os.Handler # )' on a null object reference logger.error('Unable to access accessibility service') raise e # Hermit only takes 2-4ms # Add a 50ms delay because game can't response quickly. self.sleep(0.05) return result
def adb_connect(self, serial): """ Connect to a serial, try 3 times at max. If there's an old ADB server running while Alas is using a newer one, which happens on Chinese emulators, the first connection is used to kill the other one, and the second is the real connect. Args: serial (str): Returns: bool: If success """ if 'emulator' in serial: return True else: for _ in range(3): msg = self.adb_client.connect(serial) logger.info(msg) if 'connected' in msg: # Connected to 127.0.0.1:59865 # Already connected to 127.0.0.1:59865 return True elif 'bad port' in msg: # bad port number '598265' in '127.0.0.1:598265' logger.error(msg) possible_reasons('Serial incorrect, might be a typo') raise RequestHumanTakeover logger.warning(f'Failed to connect {serial} after 3 trial, assume connected') return False
def app_start_wsa(self, package_name, display, allow_failure=False): """ Args: package_name (str): display (int): allow_failure (bool): Returns: bool: If success to start """ self.adb_shell(['svc', 'power', 'stayon', 'true']) activity_name = self.get_main_activity_name(package_name=package_name) result = self.adb_shell([ 'am', 'start', '--display', display, package_name + '/' + activity_name ]) if 'No activities found' in result: # ** No activities found to run, monkey aborted. if allow_failure: return False else: logger.error(result) possible_reasons( f'"{package_name}" not found, please check setting Emulator.PackageName' ) raise RequestHumanTakeover else: # Events injected: 1 # ## Network stats: elapsed time=4ms (0ms mobile, 0ms wifi, 4ms not connected) return True
def _adb_connect(self, serial): if serial.startswith('127.0.0.1'): msg = self.adb_command(['connect', serial]).decode("utf-8") if msg.startswith('unable'): logger.error('Unable to connect %s' % serial) exit(1) else: logger.info(msg.strip())
def stop(self): if self.is_alive(): thread_id = self._get_id() res = ctypes.pythonapi.PyThreadState_SetAsyncExc( thread_id, ctypes.py_object(SystemExit)) if res > 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) logger.error('Exception raise failure')
def app_start_uiautomator2(self, package_name): try: self.u2.app_start(package_name) except u2.exceptions.BaseError as e: # BaseError: package "com.bilibili.azurlane" not found logger.error(e) possible_reasons(f'"{package_name}" not found, please check setting Emulator.PackageName') raise RequestHumanTakeover
def app_start_uiautomator2(self, package_name=None): if not package_name: package_name = self.package try: self.u2.app_start(package_name) except u2.exceptions.BaseError as e: # BaseError: package "com.bilibili.azurlane" not found logger.error(e) raise PackageNotInstalled(package_name)
def update(self): logger.hr("Run update") backup, builtins.print = builtins.print, logger.info try: self.git_install() self.pip_install() except ExecutionError: logger.error("Update failed") builtins.print = backup return False builtins.print = backup return True
def gacha_goto_pool(self, target_pool): """ Transition to appropriate build pool page. Args: target_pool (str): Name of pool, default to 'light' path if outside of acceptable range Returns: str: Current pool location based on availability Pages: in: page_build (gacha pool selection) out: page_build (gacha pool allowed) Except: May exit if 'wishing_well' but not complete configuration """ # Switch view to 'light' pool self.gacha_bottom_navbar_ensure(right=3, is_build=True) # Transition to 'target_pool' if needed, update # 'target_pool' appropriately if target_pool == 'wishing_well': if self._gacha_side_navbar.get_total(main=self) != 5: logger.warning('\'wishing_well\' is not available, ' 'default to \'light\' pool') target_pool = 'light' else: self.gacha_side_navbar_ensure(upper=2) if self.appear(BUILD_WW_CHECK): logger.error('\'wishing_well\' must be configured ' 'manually by user, cannot continue ' 'gacha_goto_pool') exit(1) elif target_pool == 'event': gacha_bottom_navbar = self._gacha_bottom_navbar(is_build=True) if gacha_bottom_navbar.get_total(main=self) == 3: logger.warning('\'event\' is not available, default ' 'to \'light\' pool') target_pool = 'light' else: self.gacha_bottom_navbar_ensure(left=1, is_build=True) elif target_pool in ['heavy', 'special']: if target_pool == 'heavy': self.gacha_bottom_navbar_ensure(right=2, is_build=True) else: self.gacha_bottom_navbar_ensure(right=1, is_build=True) return target_pool
def handle_adb_error(e): """ Args: e (Exception): Returns: bool: If should retry """ text = str(e) if 'not found' in text: # When you call `adb disconnect <serial>` # Or when adb server was killed (low possibility) # AdbError(device '127.0.0.1:59865' not found) logger.error(e) return True elif 'timeout' in text: # AdbTimeout(adb read timeout) logger.error(e) return True elif 'closed' in text: # AdbError(closed) # Usually after AdbTimeout(adb read timeout) # Disconnect and re-connect should fix this. logger.error(e) return True elif 'device offline' in text: # AdbError(device offline) # When a device that has been connected wirelessly is disconnected passively, # it does not disappear from the adb device list, # but will be displayed as offline. # In many cases, such as disconnection and recovery caused by network fluctuations, # or after VMOS reboot when running Alas on a phone, # the device is still available, but it needs to be disconnected and re-connected. logger.error(e) return True elif 'unknown host service' in text: # AdbError(unknown host service) # Another version of ADB service started, current ADB service has been killed. # Usually because user opened a Chinese emulator, which uses ADB from the Stone Age. logger.error(e) return True else: # AdbError() logger.exception(e) possible_reasons( 'If you are using BlueStacks or LD player or WSA, please enable ADB in the settings of your emulator', 'Emulator died, please restart emulator', 'Serial incorrect, no such device exists or emulator is not running' ) return False
def retry_wrapper(self, *args, **kwargs): """ Args: self (AScreenCap): """ init = None for _ in range(RETRY_TRIES): try: if callable(init): self.sleep(RETRY_DELAY) init() return func(self, *args, **kwargs) # Can't handle except RequestHumanTakeover: break # When adb server was killed except ConnectionResetError as e: logger.error(e) def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) # When ascreencap is not installed except AscreencapError as e: logger.error(e) def init(): self.ascreencap_init() # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) else: break # Unknown, probably a trucked image except Exception as e: logger.exception(e) def init(): pass logger.critical(f'Retry {func.__name__}() failed') raise RequestHumanTakeover
def report(self): report_file = os.path.abspath( os.path.join(os.path.dirname(__file__), "../report/%s") % report_file_name) if not os.path.exists(report_file_name): try: os.makedirs(os.path.dirname(report_file)) except Exception as reason: logger.error(reason) try: with open(report_file, 'w', encoding='utf-8') as report: all_result = self.header + self.juicy_result + self.footer report.write(all_result) logger.info("报告生成完毕") except Exception as reason: logger.info("报告生成失败") logger.error(reason)
def read_json(self): # 加载字典文件,在前面加上site组成完成查询语句 logger.info("正在读取 GHDB Json 文件") try: with open(file_name, "r", encoding=file_encode) as json_file: content = json.load(json_file) # 先不进行子域名查询和C段查询 # del content['basic_information'] # 生成查询队列列表 for primary_category, sub_category in content.items(): for j in sub_category: for i in range(len(j['query'])): search_query = self.basic_site + (j['query'][i]) self.search_list.append(search_query) except Exception as reason: logger.error(reason) sys.exit() logger.info("文件读取已完成")
def handle_adb_error(e): """ Args: e (Exception): Returns: bool: If should retry """ text = str(e) if 'not found' in text: # When you call `adb disconnect <serial>` # Or when adb server was killed (low possibility) # AdbError(device '127.0.0.1:59865' not found) logger.error(e) return True elif 'timeout' in text: # AdbTimeout(adb read timeout) logger.error(e) return True elif 'closed' in text: # AdbError(closed) # Usually after AdbTimeout(adb read timeout) # Disconnect and re-connect should fix this. logger.error(e) return True else: # AdbError(device offline) # AdbError() logger.exception(e) possible_reasons( 'If you are using BlueStacks or LD player or WSA, please enable ADB in the settings of your emulator', 'Emulator died, please restart emulator', 'Serial incorrect, no such device exists or emulator is not running' ) return False
def retry_wrapper(self, *args, **kwargs): """ Args: self (Hermit): """ init = None for _ in range(RETRY_TRIES): try: if callable(init): self.sleep(RETRY_DELAY) init() return func(self, *args, **kwargs) # Can't handle except RequestHumanTakeover: break # When adb server was killed except ConnectionResetError as e: logger.error(e) def init(): self.adb_reconnect() # When unable to send requests except requests.exceptions.ConnectionError as e: logger.error(e) text = str(e) if 'Connection aborted' in text: # Hermit not installed or not running # ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) def init(): self.adb_reconnect() self.hermit_init() else: # Lost connection, adb server was killed # HTTPConnectionPool(host='127.0.0.1', port=20269): # Max retries exceeded with url: /click?x=500&y=500 def init(): self.adb_reconnect() # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_reconnect() else: break # HermitError: {"code":-1,"msg":"error"} except HermitError as e: logger.error(e) def init(): self.adb_reconnect() self.hermit_init() # Unknown, probably a trucked image except Exception as e: logger.exception(e) def init(): pass logger.critical(f'Retry {func.__name__}() failed') raise RequestHumanTakeover
def start_ocr_server(port=22268): from module.ocr.al_ocr import AlOcr from module.ocr.models import OcrModel class OCRServer(OcrModel): def hello(self): return "hello" def ocr(self, lang, img_fp): img_fp = pickle.loads(img_fp) cnocr: AlOcr = self.__getattribute__(lang) return cnocr.ocr(img_fp) def ocr_for_single_line(self, lang, img_fp): img_fp = pickle.loads(img_fp) cnocr: AlOcr = self.__getattribute__(lang) return cnocr.ocr_for_single_line(img_fp) def ocr_for_single_lines(self, lang, img_list): img_list = [pickle.loads(img_fp) for img_fp in img_list] cnocr: AlOcr = self.__getattribute__(lang) return cnocr.ocr_for_single_lines(img_list) def set_cand_alphabet(self, lang, cand_alphabet): cnocr: AlOcr = self.__getattribute__(lang) return cnocr.set_cand_alphabet(cand_alphabet) def debug(self, lang, img_list): img_list = [pickle.loads(img_fp) for img_fp in img_list] cnocr: AlOcr = self.__getattribute__(lang) return cnocr.debug(img_list) server = zerorpc.Server(OCRServer()) try: server.bind(f"tcp://*:{port}") except zmq.error.ZMQError: logger.error(f"Ocr server cannot bind on port {port}") return logger.info(f"Ocr server listen on port {port}") server.run()
def bind(self) -> socket.socket: logger = uvicorn.config.logger logger_args: List[Union[str, int]] if self.uds: # pragma: py-win32 path = self.uds sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: sock.bind(path) uds_perms = 0o666 os.chmod(self.uds, uds_perms) except OSError as exc: logger.error(exc) sys.exit(1) message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)" sock_name_format = "%s" color_message = ("Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)") logger_args = [self.uds] elif self.fd: # pragma: py-win32 sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM) message = "Uvicorn running on socket %s (Press CTRL+C to quit)" fd_name_format = "%s" color_message = ("Uvicorn running on " + click.style(fd_name_format, bold=True) + " (Press CTRL+C to quit)") logger_args = [sock.getsockname()] else: family = socket.AF_INET addr_format = "%s://%s:%d" if self.host and ":" in self.host: # pragma: py-win32 # It's an IPv6 address. family = socket.AF_INET6 addr_format = "%s://[%s]:%d" sock = socket.socket(family=family) if reuseaddr: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((self.host, self.port)) except OSError as exc: logger.error(exc) logger.error(f"Already bind on address ({self.host}, {self.port})") sys.exit(1) message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" color_message = ("Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)") protocol_name = "https" if self.is_ssl else "http" logger_args = [protocol_name, self.host, self.port] logger.info(message, *logger_args, extra={"color_message": color_message}) sock.set_inheritable(True) return sock
def search(self, query, num_results=search_num_results, prefetch_pages=True, prefetch_threads=10, language=search_language, proxy_list=None): # 初始化搜索结果为空 search_results = [] # 计算返回结果的页数 pages = int(math.ceil(num_results / self.RESULTS_PER_PAGE)) # 双初始化端队列 fetcher_threads = deque([]) total = None for i in range(pages): start = i * self.RESULTS_PER_PAGE # 设置通过代理发包 if proxy_list: proxy = random.choice(proxy_list) proxy_handler = urllib.request.ProxyHandler(proxy) else: proxy_handler = urllib.request.ProxyHandler({}) try: opener = urllib.request.build_opener(proxy_handler) # 覆盖 addheaders 值 opener.addheaders = self.DEFAULT_HEADERS # 使用 urllib.request 按页数获取 Google 查询返回的结果 response = opener.open(self.SEARCH_URL + "?q=" + urllib.request.quote(query) + "&hl=" + language + ("" if start == 0 else ("&start=" + str(start)))) # 使用BeautifulSoup lxml 解析器处理返回结果 soup = BeautifulSoup(response.read(), 'lxml') response.close() except Exception as reason: logger.error(reason) sys.exit() if total is None: # 处理总共返回结果的数量 if soup.select(self.TOTAL_SELECTOR): # 查询返回结果数目 total_text = soup.select( self.TOTAL_SELECTOR)[0].children.__next__().encode( 'utf-8') # 使用正则匹配总共返回多少条结果 total = int( re.sub( "[', ]", "", re.search("(([0-9]+[', ])*[0-9]+)", total_text.decode('utf-8')).group(1))) else: logger.info("查询结果为空") total = 0 # 从结果中选择 href 便签下的结果链接 results = self.parse_results(soup.select(self.RESULT_SELECTOR)) # 去除脏数据 if len(results) > total: del results[total - len(results):] # 截取查询结果长度 if len(search_results) + len(results) > num_results: del results[num_results - len(search_results):] # 将结果加入搜索结果列表中 search_results += results # 采用多线程处理返回结果 if prefetch_pages: for result in results: while True: running = 0 for thread in fetcher_threads: if thread.is_alive(): running += 1 if running < prefetch_threads: break sleep(1) fetcher_thread = Thread(target=result.get_text_) fetcher_thread.start() fetcher_threads.append(fetcher_thread) for thread in fetcher_threads: thread.join() return SearchResponse(search_results, total)
def retry_wrapper(self, *args, **kwargs): """ Args: self (Minitouch): """ init = None sleep = True for _ in range(RETRY_TRIES): try: if callable(init): if sleep: self.sleep(RETRY_DELAY) sleep = True init() return func(self, *args, **kwargs) # Can't handle except RequestHumanTakeover: break # When adb server was killed except ConnectionResetError as e: logger.error(e) def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) # Emulator closed except ConnectionAbortedError as e: logger.error(e) def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) # MinitouchNotInstalledError: Received empty data from minitouch except MinitouchNotInstalledError as e: logger.error(e) sleep = False def init(): self.install_uiautomator2() if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') del_cached_property(self, 'minitouch_builder') # MinitouchOccupiedError: Timeout when connecting to minitouch except MinitouchOccupiedError as e: logger.error(e) sleep = False def init(): self.restart_atx() if self._minitouch_port: self.adb_forward_remove(f'tcp:{self._minitouch_port}') del_cached_property(self, 'minitouch_builder') # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) else: break except BrokenPipeError as e: logger.error(e) def init(): del_cached_property(self, 'minitouch_builder') # Unknown, probably a trucked image except Exception as e: logger.exception(e) def init(): pass logger.critical(f'Retry {func.__name__}() failed') raise RequestHumanTakeover
def retry_wrapper(self, *args, **kwargs): """ Args: self (Uiautomator2): """ init = None sleep = True for _ in range(RETRY_TRIES): try: if callable(init): if sleep: self.sleep(RETRY_DELAY) sleep = True init() return func(self, *args, **kwargs) # Can't handle except RequestHumanTakeover: break # When adb server was killed except ConnectionResetError as e: logger.error(e) def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) # In `device.set_new_command_timeout(604800)` # json.decoder.JSONDecodeError: Expecting value: line 1 column 2 (char 1) except JSONDecodeError as e: logger.error(e) sleep = False def init(): self.install_uiautomator2() # AdbError except AdbError as e: if handle_adb_error(e): def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) else: break # RuntimeError: USB device 127.0.0.1:5555 is offline except RuntimeError as e: if handle_adb_error(e): def init(): self.adb_disconnect(self.serial) self.adb_connect(self.serial) else: break # In `assert c.read string(4) == _OKAY` # ADB on emulator not enabled except AssertionError as e: logger.exception(e) possible_reasons( 'If you are using BlueStacks or LD player, ' 'please enable ADB in the settings of your emulator') break # Unknown, probably a trucked image except Exception as e: logger.exception(e) def init(): pass logger.critical(f'Retry {func.__name__}() failed') raise RequestHumanTakeover
def gacha_prep(self, target, skip_first_screenshot=True): """ Initiate preparation to submit build orders. Args: target (int): Number of build orders to submit skip_first_screenshot (bool): Returns: bool: True if prep complete otherwise False. Pages: in: page_build (any) out: submit pop up Except: May exit if unable to process prep """ # Nothing to prep if 'target' = 0 if not target: return False # Ensure correct page to be able to prep in if not self.appear(BUILD_SUBMIT_ORDERS) \ and not self.appear(BUILD_SUBMIT_WW_ORDERS): return False # Use 'appear' to update actual position of assets # for ui_ensure_index confirm_timer = Timer(1, count=2).start() ocr_submit = None index_offset = (60, 20) while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear_then_click(BUILD_SUBMIT_ORDERS, interval=3): ocr_submit = OCR_BUILD_SUBMIT_COUNT confirm_timer.reset() continue if self.appear_then_click(BUILD_SUBMIT_WW_ORDERS, interval=3): ocr_submit = OCR_BUILD_SUBMIT_WW_COUNT confirm_timer.reset() continue # End if self.appear(BUILD_PLUS, offset=index_offset) \ and self.appear(BUILD_MINUS, offset=index_offset): if confirm_timer.reached(): break # Check for exception, exited prematurely # Apply appropriate submission count if ocr_submit is None: logger.error('Failed to identify ocr asset required, ' 'cannot continue prep work') exit(1) area = ocr_submit.buttons[0] ocr_submit.buttons = [(BUILD_MINUS.button[2] + 3, area[1], BUILD_PLUS.button[0] - 3, area[3])] self.ui_ensure_index(target, letter=ocr_submit, prev_button=BUILD_MINUS, next_button=BUILD_PLUS, skip_first_screenshot=True) return True
def remote_access_service( local_host="127.0.0.1", local_port=22267, server="app.pywebio.online", server_port=1022, remote_port="/", setup_timeout=60, ): """ Wait at most one minute to get the ssh output, if it gets a normal out, the connection is successfully established. Otherwise report error and kill ssh process. :param local_port: ssh local listen port :param server: ssh server domain :param server_port: ssh server port :param setup_timeout: If the service can't setup successfully in `setup_timeout` seconds, then exit. """ global _ssh_process, _ssh_notfound bin = State.deploy_config.SSHExecutable cmd = f"{bin} -oStrictHostKeyChecking=no -R {remote_port}:{local_host}:{local_port} -p {server_port} {server} -- --output json" args = shlex.split(cmd) logger.debug(f"remote access service command: {cmd}") if _ssh_process is not None and _ssh_process.poll() is None: logger.warning(f"Kill previous ssh process [{_ssh_process.pid}]") _ssh_process.kill() try: _ssh_process = Popen(args, stdout=PIPE, stderr=PIPE) except FileNotFoundError as e: logger.critical( f"Cannot find SSH executable {bin}, please install OpenSSH or specify SSHExecutable in deploy.yaml" ) _ssh_notfound = True return logger.info(f"remote access process pid: {_ssh_process.pid}") success = False def timeout_killer(wait_sec): time.sleep(wait_sec) if not success and _ssh_process.poll() is None: logger.info("Connection timeout, kill ssh process") _ssh_process.kill() threading.Thread(target=timeout_killer, kwargs=dict(wait_sec=setup_timeout), daemon=True).start() stdout = _ssh_process.stdout.readline().decode("utf8") logger.info(f"ssh server stdout: {stdout}") connection_info = {} try: connection_info = json.loads(stdout) success = True except json.decoder.JSONDecodeError: if not success and _ssh_process.poll() is None: _ssh_process.kill() if success: if connection_info.get("status", "fail") != "success": logger.info( f"Failed to establish remote access, this is the error message from service provider: {connection_info.get('message', '')}" ) new_username = connection_info.get("change_username", None) if new_username: logger.info( f"Server requested to change username, change it to: {new_username}" ) State.deploy_config.SSHUser = new_username else: global address address = connection_info["address"] logger.debug(f"Remote access url: {address}") # wait ssh or main thread exit while not am_i_the_only_thread() and _ssh_process.poll() is None: # while _ssh_process.poll() is None: time.sleep(1) if _ssh_process.poll() is None: # main thread exit, kill ssh process logger.info("App process exit, killing ssh process") _ssh_process.kill() else: # ssh process exit by itself or by timeout killer stderr = _ssh_process.stderr.read().decode("utf8") if stderr: logger.error( f"PyWebIO application remote access service error: {stderr}") else: logger.info("PyWebIO application remote access service exit.") address = None