暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

main.py 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #Known bugs :
  2. #Sockets are not closed properly leading in a listening socket leak, a patch
  3. #will be submitted for cpython 3.6
  4. #
  5. import os
  6. import sys
  7. import signal
  8. import socket
  9. import socketserver
  10. import wsgiref
  11. import wsgiref.simple_server
  12. from wsgiref.simple_server import make_server
  13. from io import BufferedWriter
  14. from lodel.context import LodelContext
  15. from lodel.context import ContextError
  16. ##@brief Set the poll interval to detect shutdown requests (do not work)
  17. SHUTDOWN_POLL_INTERVAL = 0.1 # <-- No impact because of ForkingTCPServer bug
  18. ##@brief Stores the signal we uses to kill childs
  19. KILLING_CHILDS_SIGNAL = signal.SIGTERM
  20. FAST_APP_EXPOSAL_CACHE = dict()
  21. ##@brief Reimplementation of WSGIRequestHandler
  22. #
  23. #Handler class designed to be called by socketserver child classes to handle
  24. #a request.
  25. #We inherit from wsgiref.simple_server.WSGIRequestHandler to avoid writing
  26. #all the construction of the wsgi variables
  27. class LodelWSGIHandler(wsgiref.simple_server.WSGIRequestHandler):
  28. ##@brief Method called by the socketserver to handle a request
  29. def handle(self):
  30. #Register a signal handler for sigint in the child process
  31. req_ref = self.request
  32. def sigstop_handler_client(signal, frame):
  33. req_ref.close()
  34. print("Client %d stopping by signal" % os.getpid())
  35. os._exit(0)
  36. signal.signal(KILLING_CHILDS_SIGNAL, sigstop_handler_client)
  37. #Dirty copy & past from Lib/http/server.py in Cpython sources
  38. try:
  39. self.raw_requestline = self.rfile.readline(65537)
  40. if len(self.raw_requestline) > 65536:
  41. self.requestline = ''
  42. self.request_version = ''
  43. self.command = ''
  44. self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
  45. return
  46. if not self.raw_requestline:
  47. self.close_connection = True
  48. return
  49. if not self.parse_request():
  50. return
  51. #Here begin custom code
  52. env = self.get_environ()
  53. stdout = BufferedWriter(self.wfile)
  54. try:
  55. handler = wsgiref.handlers.SimpleHandler(
  56. self.rfile, stdout, self.get_stderr(), env)
  57. handler.request_handler = self # backpointer for logging
  58. handler.run(self.server.get_app())
  59. finally:
  60. stdout.detach()
  61. except socket.timeout as e:
  62. self.log_error("Request timed out: %r", e)
  63. self.close_connection = True
  64. return
  65. ##@brief An attempt to solve the socket leak problem
  66. def close(self):
  67. self.request.close()
  68. super().close()
  69. class CustomForkingTCPServer(socketserver.ForkingTCPServer):
  70. ##@brief static property indicating the max number of childs allowed
  71. max_children = 40
  72. def __init__(self, *args, **kwargs):
  73. super().__init__(*args, **kwargs)
  74. if self.__class__.active_children is None:
  75. self.__class__.active_childer = set()
  76. ##@brief Implements max_children limitations
  77. def process_request(self, request, client_address):
  78. while self.active_children is not None and \
  79. len(self.active_children) > self.__class__.max_children:
  80. self.collect_children()
  81. super().process_request(request, client_address)
  82. ##@brief Custom reimplementation of shutdown method in order to ensure
  83. #that we close all listening sockets
  84. #
  85. #This method is here because of a bug (or a missing feature) :
  86. #The socketserver implementation force to call the shutdown method
  87. #from another thread/process else it leads in a deadlock.
  88. #The problem is that the implementation of shutdown set a private attribute
  89. #__shutdown_request to true. So we cannot reimplement a method that will
  90. #just set the flag to True, we have to manually collect each actives
  91. #childs. A patch is prepared and will be proposed for cpython upstream.
  92. def shutdown(self):
  93. if self.active_children is not None:
  94. for pid in self.active_children.copy():
  95. print("Killing : %d"%pid)
  96. os.kill(pid, KILLING_CHILDS_SIGNAL)
  97. try:
  98. pid, _ = os.waitpid(pid, 0)
  99. self.active_children.discard(pid)
  100. except ChildProcessError:
  101. self.active_children.discard(pid)
  102. self.server_close()
  103. ##@brief WSGIServer implementing ForkingTCPServer.
  104. #
  105. #Same features than wsgiref.simple_server.WSGIServer but process each requests
  106. #in a child process
  107. class ForkingWSGIServer(
  108. wsgiref.simple_server.WSGIServer, CustomForkingTCPServer):
  109. pass
  110. ##@brief utility function to extract site id from an url
  111. def site_id_from_url(url):
  112. res = ''
  113. for c in url[1:]:
  114. if c == '/':
  115. break
  116. res += c
  117. if len(res) == 0:
  118. return None
  119. return res
  120. ##@brief Utility function to return quickly an error
  121. def http_error(env, start_response, status = '500 internal server error', \
  122. extra = None):
  123. headers = [('Content-type', 'text/plain; charset=utf-8')]
  124. start_response(status, headers)
  125. msg = status
  126. if extra is not None:
  127. msg = extra
  128. return [msg.encode('utf-8')]
  129. ##@brief This method is run in a child process by the handler
  130. def wsgi_router(env, start_response):
  131. #Attempt to load a context
  132. site_id = site_id_from_url(env['PATH_INFO'])
  133. if site_id is None:
  134. #It can be nice to provide a list of instances here
  135. return http_error(env, start_response, '404 Not Found')
  136. try:
  137. LodelContext.set(site_id)
  138. #We are in the good context
  139. except ContextError as e:
  140. print(e)
  141. return http_error(env, start_response, '404 Not found',
  142. "No site named '%s'" % site_id)
  143. #Calling webui
  144. return FAST_APP_EXPOSAL_CACHE[site_id].application(env, start_response)
  145. #LodelContext.expose_modules(globals(), {
  146. # 'lodel.plugins.webui.run': ['application']})
  147. #return application(env, start_response)
  148. ##@brief Starts the server until a SIGINT is received
  149. def main_loop():
  150. LodelContext.expose_modules(globals(), {'lodel.settings': ['Settings']})
  151. ForkingWSGIServer.max_children = Settings.server.max_children
  152. listen_addr = Settings.server.listen_address
  153. listen_port = Settings.server.listen_port
  154. server = wsgiref.simple_server.make_server(
  155. listen_addr, listen_port, wsgi_router,
  156. server_class=ForkingWSGIServer, handler_class = LodelWSGIHandler)
  157. #Signal handler to close server properly on sigint
  158. def sigint_handler(signal, frame):
  159. print("Ctrl-c pressed, exiting")
  160. server.shutdown()
  161. server.server_close()
  162. exit(0)
  163. signal.signal(signal.SIGINT, sigint_handler)
  164. #Listen until SIGINT
  165. server.serve_forever(SHUTDOWN_POLL_INTERVAL)