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