Browse Source

Starting implementation of the forking webserver

The server is listening, fork on new request, and then loads the context from URL.
Now we have to call properly the webui plugin in order to answer the request
Yann Weber 7 years ago
parent
commit
c5522816fe
2 changed files with 230 additions and 0 deletions
  1. 18
    0
      plugins/multisite/__init__.py
  2. 212
    0
      plugins/multisite/main.py

+ 18
- 0
plugins/multisite/__init__.py View File

@@ -0,0 +1,18 @@
1
+from lodel.context import LodelContext
2
+LodelContext.expose_modules(globals(), {
3
+    'lodel.settings.validator': ['SettingValidator']})
4
+
5
+__plugin_name__ = "multisite"
6
+__version__ = '0.0.1' #or __version__ = [0,0,1]
7
+__loader__ = "main.py"
8
+__author__ = "Lodel2 dev team"
9
+__fullname__ = "Multisite plugin"
10
+__name__ = 'yweber.dummy'
11
+__plugin_type__ = 'extension'
12
+
13
+CONFSPEC = {
14
+    'lodel2.server': {
15
+        'port': (80,SettingValidator('int')),
16
+        'listen_addr': ('', SettingValidator('string')),
17
+    }
18
+}

+ 212
- 0
plugins/multisite/main.py View File

@@ -0,0 +1,212 @@
1
+#Known bugs : 
2
+#Sockets are not closed properly leading in a listening socket leak
3
+#
4
+
5
+
6
+import wsgiref
7
+import wsgiref.simple_server
8
+from wsgiref.simple_server import make_server
9
+import http.server
10
+import multiprocessing as mp
11
+import socketserver
12
+import socket
13
+import os
14
+from io import BufferedWriter
15
+import urllib
16
+import threading
17
+
18
+import sys, signal
19
+
20
+from lodel.context import LodelContext
21
+from lodel.context import ContextError
22
+
23
+LISTEN_ADDR = ''
24
+LISTEN_PORT = 1337
25
+
26
+SHUTDOWN_POLL_INTERVAL = 0.1
27
+
28
+class HtppHandler(wsgiref.simple_server.WSGIRequestHandler):
29
+    def handle(self):
30
+        print("addr : %s %s\n" % (self.client_address, type(self.request)))
31
+        #Dirty copy & past from Lib/http/server.py in Cpython sources       
32
+        try:
33
+            self.raw_requestline = self.rfile.readline(65537)
34
+            if len(self.raw_requestline) > 65536:
35
+                self.requestline = ''
36
+                self.request_version = ''
37
+                self.command = ''
38
+                self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
39
+                return
40
+            if not self.raw_requestline:
41
+                self.close_connection = True
42
+                return
43
+            if not self.parse_request():
44
+                return
45
+            #Here begin custom code
46
+            env = self.get_environ()
47
+            stdout = BufferedWriter(self.wfile)
48
+            try:
49
+                handler = wsgiref.handlers.SimpleHandler(
50
+                    self.rfile, stdout, self.get_stderr(), env)
51
+                handler.request_handler = self      # backpointer for logging
52
+                handler.run(self.server.get_app())
53
+            finally:
54
+                stdout.detach()
55
+        except socket.timeout as e:
56
+            self.log_error("Request timed out: %r", e)
57
+            self.close_connection = True
58
+            return
59
+    
60
+    ##@brief An attempt to solve the socket leak problem
61
+    def close(self):
62
+        print("Closing request from handler : %s" % self.request)
63
+        self.request.close()
64
+        super().close()
65
+
66
+    ##@brief Copy of wsgiref.simple_server.WSGIRequestHandler.get_environ method
67
+    def get_environ(self):
68
+        env = self.server.base_environ.copy()
69
+        env['SERVER_PROTOCOL'] = self.request_version
70
+        env['SERVER_SOFTWARE'] = self.server_version
71
+        env['REQUEST_METHOD'] = self.command
72
+        if '?' in self.path:
73
+            path,query = self.path.split('?',1)
74
+        else:
75
+            path,query = self.path,''
76
+
77
+        env['PATH_INFO'] = urllib.parse.unquote(path, 'iso-8859-1')
78
+        env['QUERY_STRING'] = query
79
+
80
+        host = self.address_string()
81
+        if host != self.client_address[0]:
82
+            env['REMOTE_HOST'] = host
83
+        env['REMOTE_ADDR'] = self.client_address[0]
84
+
85
+        if self.headers.get('content-type') is None:
86
+            env['CONTENT_TYPE'] = self.headers.get_content_type()
87
+        else:
88
+            env['CONTENT_TYPE'] = self.headers['content-type']
89
+
90
+        length = self.headers.get('content-length')
91
+        if length:
92
+            env['CONTENT_LENGTH'] = length
93
+
94
+        for k, v in self.headers.items():
95
+            k=k.replace('-','_').upper(); v=v.strip()
96
+            if k in env:
97
+                continue                    # skip content length, type,etc.
98
+            if 'HTTP_'+k in env:
99
+                env['HTTP_'+k] += ','+v     # comma-separate multiple headers
100
+            else:
101
+                env['HTTP_'+k] = v
102
+        return env
103
+
104
+##@brief Speciallized ForkingTCPServer to fit specs of WSGIHandler
105
+class HttpServer(socketserver.ForkingTCPServer):
106
+    
107
+    ##@brief Onverwritting of ForkingTCPServer.server_bind method
108
+    #to fit the wsgiref specs
109
+    def server_bind(self):
110
+        super().server_bind()
111
+        #Copy & paste from Lib/http/server.py
112
+        host, port = self.socket.getsockname()[:2]
113
+        self.server_name = socket.getfqdn(host)
114
+        self.server_port = port
115
+        # Copy&paste from Lib/wsgiref/simple_server.py
116
+        # Set up base environment 
117
+        env = self.base_environ = {}
118
+        env['SERVER_NAME'] = self.server_name
119
+        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
120
+        env['SERVER_PORT'] = str(self.server_port)
121
+        env['REMOTE_HOST']=''
122
+        env['CONTENT_LENGTH']=''
123
+        env['SCRIPT_NAME'] = ''
124
+
125
+    ##@brief Hardcoded callback function
126
+    def get_app(self):
127
+        return wsgi_router
128
+
129
+    ##@brief An attempt to solve the socket leak problem
130
+    def close_request(self, request):
131
+        print("Closing client socket in server : %s" % request)
132
+        request.close()
133
+
134
+    ##@brief An attempt to solve the socket leak problem
135
+    def server_close(self):
136
+        print("Closing listening socket")
137
+        self.socket.close()
138
+
139
+##@brief utility function to extract site id from an url
140
+def site_id_from_url(url):
141
+    res = ''
142
+    for c in url[1:]:
143
+        if c == '/':
144
+            break
145
+        res += c
146
+    if len(res) == 0:
147
+        return None
148
+    return res
149
+
150
+##@brief Utility function to return quickly an error
151
+def http_error(env, start_response, status = '500 internal server error', \
152
+        extra = None):
153
+    headers = [('Content-type', 'text/plain; charset=utf-8')]
154
+    start_response(status, headers)
155
+    msg = status
156
+    if extra is not None:
157
+        msg = extra
158
+    return [msg.encode('utf-8')]
159
+
160
+##@brief This method is run in a child process by the handler
161
+def wsgi_router(env, start_response):
162
+    print("\n\nCPROCPID = %d\n\n" % os.getpid()) #<-- print PID (for debug)
163
+    #Attempt to load a context
164
+    site_id = site_id_from_url(env['PATH_INFO'])
165
+    if site_id is None:
166
+        #It can be nice to provide a list of instances here
167
+        return http_error(env, start_response, '404 Not Found')
168
+    try:
169
+        LodelContext.set(site_id)
170
+        #We are in the good context
171
+
172
+    except ContextError as e:
173
+        print(e)
174
+        return http_error(env, start_response, '404 Not found',
175
+            "No site named '%s'" % site_id)
176
+    #
177
+    #   Here we have to put the code that run the request
178
+    #
179
+    
180
+    #Testing purpose
181
+    rep = "Woot '%s'" % site_id
182
+    print(rep)
183
+    start_response('200 ok', [('Content-type', 'text/plain; charset=utf-8')])
184
+    return [rep.encode('utf-8')]
185
+
186
+    #mp.Process(target=foo, args=(env,start_response))
187
+    return child_proc(env, start_response)
188
+
189
+def main_loop():
190
+    
191
+    #Set the start method for multiprocessing
192
+    mp.set_start_method('forkserver')
193
+    print("\n\nPID = %d\n\n" % os.getpid())
194
+
195
+    listen_addr = LISTEN_ADDR
196
+    listen_port = LISTEN_PORT
197
+    
198
+    #server = socketserver.ForkingTCPServer((listen_addr, listen_port),
199
+    #    HtppHandler)
200
+    server = HttpServer((listen_addr, listen_port),
201
+        HtppHandler)
202
+    
203
+    #Signal handler to close server properly on sigint
204
+    def sigint_handler(signal, frame):
205
+        print("Ctrl-c pressed, exiting")
206
+        server.shutdown() # <-- Do not work for unkonwn reasons
207
+        server.server_close()
208
+        sys.exit(0)
209
+    #signal.signal(signal.SIGINT, sigint_handler)
210
+
211
+    server.serve_forever(SHUTDOWN_POLL_INTERVAL)
212
+

Loading…
Cancel
Save