You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

run.py 10 kB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # Copyright 2019 Huawei Technologies Co., Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # ============================================================================
  15. """Web service entrance."""
  16. import os
  17. import re
  18. import shlex
  19. import stat
  20. import subprocess
  21. import sys
  22. import time
  23. from enum import Enum, unique
  24. from gunicorn.glogging import Logger
  25. from mindinsight.backend.config import WEB_CONFIG_DIR
  26. from mindinsight.conf import settings
  27. from mindinsight.utils.log import setup_logger
  28. MINDBOARD_APP_MODULE = "mindinsight.backend.application:APP"
  29. GUNICORN_LOGGER = "mindinsight.backend.run.GunicornLogger"
  30. _MIN_PORT = 1
  31. _MAX_PORT = 65535
  32. @unique
  33. class ServerStateEnum(Enum):
  34. """
  35. The service startup status are as follows: "unknown", "failed" and "success"
  36. """
  37. UNKNOWN = "unknown"
  38. FAILED = "failed"
  39. SUCCESS = "success"
  40. def _get_file_size(file_path):
  41. """
  42. Get the file size.
  43. Args:
  44. file_path (str): The file path.
  45. Returns:
  46. int, the file size. If file is not existed, then return 0.
  47. """
  48. try:
  49. file_size = os.path.getsize(file_path)
  50. except FileNotFoundError:
  51. file_size = 0
  52. return file_size
  53. def _is_match_one(sub_string_list, src_string):
  54. """
  55. Whether the sub-string in the list can match the source string.
  56. Args:
  57. sub_string_list (list): The sub-string list.
  58. src_string (str): The source string.
  59. Returns:
  60. bool, if matched return True, else return False.
  61. """
  62. for match_info in sub_string_list:
  63. if match_info in src_string:
  64. return True
  65. return False
  66. def _check_stat_from_log(log_info):
  67. """
  68. Determine the service startup status based on the log information.
  69. Args:
  70. log_info (str): The output log of service startup.
  71. Returns:
  72. str, the state value that is one of the follows: "unknown", "failed" and "success".
  73. """
  74. server_state = ServerStateEnum.UNKNOWN.value
  75. match_success_info = "Listening at: http://%s:%d" % \
  76. (settings.HOST, int(settings.PORT))
  77. common_failed_info_list = [
  78. "[ERROR] Retrying in 1 second",
  79. "[INFO] Reason: App failed to load",
  80. "[ERROR] Exception in worker process"
  81. ]
  82. re_pattern = "\\[ERROR\\].+%s.+%d" % \
  83. (settings.HOST, int(settings.PORT))
  84. # matched failed output log by fuzzy match
  85. if re.search(re_pattern, log_info) or \
  86. _is_match_one(common_failed_info_list, log_info):
  87. server_state = ServerStateEnum.FAILED.value
  88. if match_success_info in log_info:
  89. server_state = ServerStateEnum.SUCCESS.value
  90. return server_state
  91. def _get_error_log_path():
  92. """
  93. Get gunicorn error log path.
  94. Returns:
  95. str, the path of error log.
  96. """
  97. path = os.path.join(settings.WORKSPACE, 'log/gunicorn/error.{}.log'.format(settings.PORT))
  98. errorlog_abspath = os.path.realpath(path)
  99. return errorlog_abspath
  100. def _get_access_log_path():
  101. """Get gunicorn access log path."""
  102. access_log_path = os.path.join(settings.WORKSPACE, 'log/gunicorn/access.{}.log'.format(settings.PORT))
  103. access_log_path = os.path.realpath(access_log_path)
  104. return access_log_path
  105. def _check_state_from_log(log_abspath, start_pos=0):
  106. """
  107. Check the service startup status based on the log file.
  108. Args:
  109. log_abspath (str): Absolute path of the log file.
  110. start_pos (int): Offset position of the log file.
  111. Returns:
  112. dict, a dict with "state" and "prompt_message" key.
  113. The value of the "state" key is as follows:"unknown", "failed" and "success".
  114. The value of the "prompt_message" key is a list of prompt messages.
  115. """
  116. server_is_start = False
  117. state_result = {"state": ServerStateEnum.UNKNOWN.value, "prompt_message": []}
  118. prompt_messages = []
  119. match_start_log = "Starting gunicorn"
  120. with open(log_abspath) as f_log:
  121. f_log.seek(start_pos)
  122. for line in f_log.readlines():
  123. if match_start_log in line:
  124. if server_is_start:
  125. break
  126. server_is_start = True
  127. continue
  128. if server_is_start:
  129. log_result = _check_stat_from_log(line)
  130. # ignore "unknown" result
  131. if log_result != ServerStateEnum.UNKNOWN.value:
  132. state_result["state"] = log_result
  133. if log_result == ServerStateEnum.FAILED.value:
  134. prompt_messages.append(line.strip())
  135. prompt_messages.append(
  136. "more failed details in log: %s" % log_abspath)
  137. break
  138. state_result["prompt_message"].append(
  139. "service start state: %s" % state_result["state"])
  140. for prompt_message in prompt_messages:
  141. state_result["prompt_message"].append(prompt_message)
  142. return state_result
  143. def _check_server_start_stat(log_abspath, start_pos=None):
  144. """
  145. Checking the Server Startup Status.
  146. Args:
  147. log_abspath (str): The log file path.
  148. start_pos (int): The log file start position.
  149. Returns:
  150. dict, an dict object that contains the state and prompt_message fields.
  151. The state values are as follows: "unknown", "failed" and "success".
  152. """
  153. state_result = {"state": ServerStateEnum.UNKNOWN.value, "prompt_message": []}
  154. # return unknown when not config gunicorn error log file
  155. if not log_abspath:
  156. return state_result
  157. log_pos = _get_file_size(log_abspath) if start_pos is None else start_pos
  158. try_cnt = 0
  159. try_cnt_max = 2
  160. while try_cnt < try_cnt_max:
  161. try_cnt += 1
  162. time.sleep(1)
  163. if _get_file_size(log_abspath) > log_pos:
  164. state_result.update(_check_state_from_log(log_abspath, log_pos))
  165. break
  166. if not state_result['prompt_message']:
  167. state_result["prompt_message"].append(
  168. "service start state: %s" % state_result["state"])
  169. return state_result
  170. class GunicornLogger(Logger):
  171. """Rewrite gunicorn default logger."""
  172. def __init__(self, cfg):
  173. self.cfg = cfg
  174. self.access_log = setup_logger('gunicorn', 'access', formatter='%(message)s')
  175. self.error_log = setup_logger('gunicorn', 'error', formatter=self.error_fmt)
  176. access_log_path = _get_access_log_path()
  177. error_log_path = _get_error_log_path()
  178. os.chmod(access_log_path, stat.S_IREAD | stat.S_IWRITE)
  179. os.chmod(error_log_path, stat.S_IREAD | stat.S_IWRITE)
  180. super(GunicornLogger, self).__init__(cfg)
  181. def now(self):
  182. """Get log format."""
  183. return time.strftime('[%Y-%m-%d-%H:%M:%S %z]')
  184. def rewrite_stdout(self, message):
  185. if message.strip():
  186. self.error_log.info(message)
  187. def rewrite_stderr(self, message):
  188. if message.strip():
  189. self.error_log.error(message)
  190. def setup(self, cfg):
  191. """Rewrite the setup method of Logger, and we don't need to do anything"""
  192. sys.stdout.write = self.rewrite_stdout
  193. sys.stderr.write = self.rewrite_stderr
  194. def _get_all_ip_addresses(host):
  195. """Get all the accessible IP address."""
  196. yield host
  197. if host == '127.0.0.1':
  198. return
  199. # The format of the environment variable SSH_CONNECTION is:
  200. # '<client_ip> <client_port> <server_ip> <server_port>'
  201. connection = os.getenv('SSH_CONNECTION', '')
  202. if len(connection) > 128:
  203. # The length should be less than 128 bytes, or the variable may be corruped.
  204. # And 128 bytes should be enough to hold two IPs and two ports.
  205. return
  206. connection = connection.split()
  207. if len(connection) == 4 and connection[2] != host:
  208. # 'connection' should hold 4 components and '<server_ip>' should be in index 2.
  209. yield connection[2]
  210. def start():
  211. """Start web service."""
  212. errorlog_abspath = _get_error_log_path()
  213. gunicorn_conf_file = os.path.join(WEB_CONFIG_DIR, "gunicorn_conf.py")
  214. cmd = "gunicorn " \
  215. "-b {host}:{port} {app_module} " \
  216. "-c {conf_file} " \
  217. "--logger-class {logger_class} " \
  218. "--access-logformat {log_format}"\
  219. .format(host=settings.HOST,
  220. port=settings.PORT,
  221. conf_file=gunicorn_conf_file,
  222. app_module=MINDBOARD_APP_MODULE,
  223. logger_class=GUNICORN_LOGGER,
  224. log_format=settings.GUNICORN_ACCESS_FORMAT
  225. )
  226. log_size = _get_file_size(errorlog_abspath)
  227. console = setup_logger('mindinsight', 'console', console=True, logfile=False, formatter='%(message)s')
  228. # start server
  229. process = subprocess.Popen(
  230. shlex.split(cmd),
  231. shell=False,
  232. # Change stdout to DEVNULL to prevent broken pipe error when creating new processes.
  233. stdin=subprocess.DEVNULL,
  234. stdout=None,
  235. stderr=subprocess.STDOUT
  236. )
  237. # sleep 1 second for gunicorn appplication to load modules
  238. time.sleep(1)
  239. # check if gunicorn application is running
  240. if process.poll() is not None:
  241. console.error("Start MindInsight failed. See log for details.")
  242. sys.exit(1)
  243. else:
  244. state_result = _check_server_start_stat(errorlog_abspath, log_size)
  245. # print gunicorn start state to stdout
  246. label = 'Web address:'
  247. for ip in _get_all_ip_addresses(settings.HOST):
  248. format_args = label, ip, str(settings.PORT), settings.URL_PATH_PREFIX
  249. console.info('%s http://%s:%s%s', *format_args)
  250. label = '.' * len(label)
  251. for line in state_result["prompt_message"]:
  252. console.info(line)
  253. if state_result["state"] == ServerStateEnum.FAILED.value:
  254. sys.exit(1)
  255. if __name__ == '__main__':
  256. start()