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

main.py 6.2KB

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