Klaus Demo bjoern / a090b0b
Merge branch 'rewrite-socket-initialization' Jonas Haag 6 years ago
16 changed file(s) with 179 addition(s) and 254 deletion(s). Raw diff Collapse all Expand all
3838 CFLAGS='-Os' make
3939
4040 bjoernmodule:
41 @$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(objects) -o $(BUILD_DIR)/bjoern.so
41 @$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(objects) -o $(BUILD_DIR)/_bjoern.so
4242 @PYTHONPATH=$$PYTHONPATH:$(BUILD_DIR) python2 -c "import bjoern"
4343
4444 again: clean all
7979 wget:
8080 wget -O - -q -S $(TEST_URL)
8181
82 test:
83 cd tests && python2 ~/dev/wsgitest/runner.py
84
8582 valgrind:
8683 valgrind --leak-check=full --show-reachable=yes python2 tests/empty.py
8784
44 #include "filewrapper.h"
55
66
7 PyDoc_STRVAR(listen_doc,
8 "listen(wsgi_app, host, [port, [reuse_port=False]]) -> None\n\n \
9 \
10 Makes bjoern listen to 'host:port' and use 'wsgi_app' as WSGI application. \
11 (This does not run the server mainloop.)\n\n \
12 \
13 'reuse_port' -- whether to set SO_REUSEPORT (if available on platform)");
147 static PyObject*
15 listen(PyObject* self, PyObject* args, PyObject* kwds)
8 run(PyObject* self, PyObject* args)
169 {
17 static char* keywords[] = {"wsgi_app", "host", "port", "reuse_port", NULL};
10 ServerInfo info;
1811
19 const char* host;
20 int port = 0;
21 int reuse_port = false; // see footnote (1)
12 PyObject* socket;
2213
23 if(wsgi_app) {
24 PyErr_SetString(
25 PyExc_RuntimeError,
26 "Only one bjoern server per Python interpreter is allowed"
27 );
14 if(!PyArg_ParseTuple(args, "OO:server_run", &socket, &info.wsgi_app)) {
2815 return NULL;
2916 }
3017
31 if(!PyArg_ParseTupleAndKeywords(args, kwds, "Os|ii:run/listen", keywords,
32 &wsgi_app, &host, &port, &reuse_port))
18 info.sockfd = PyObject_AsFileDescriptor(socket);
19 if (info.sockfd < 0) {
3320 return NULL;
34
35 _initialize_request_module(host, port);
36
37 if(!port) {
38 /* Unix socket: "unix:/tmp/foo.sock" or "unix:@abstract-socket" (Linux) */
39 if(strncmp("unix:", host, 5)) {
40 PyErr_Format(PyExc_ValueError, "'port' missing but 'host' is not a Unix socket");
41 goto err;
42 }
43 host += 5;
4421 }
4522
46 if(!server_init(host, port, reuse_port)) {
47 if(port)
48 PyErr_Format(PyExc_RuntimeError, "Could not start server on %s:%d", host, port);
49 else
50 PyErr_Format(PyExc_RuntimeError, "Could not start server on %s", host);
51 goto err;
23 PyObject* sockname = PyObject_CallMethod(socket, "getsockname", NULL);
24 if (sockname == NULL) {
25 return NULL;
26 }
27 if (PyTuple_CheckExact(sockname) && PyTuple_GET_SIZE(sockname) == 2) {
28 /* Standard (ipaddress, port) case */
29 info.host = PyTuple_GET_ITEM(sockname, 0);
30 info.port = PyTuple_GET_ITEM(sockname, 1);
31 } else {
32 info.host = NULL;
5233 }
5334
54 Py_RETURN_NONE;
35 _initialize_request_module();
36 server_run(&info);
5537
56 err:
57 wsgi_app = NULL;
58 return NULL;
59 }
60
61 PyDoc_STRVAR(run_doc,
62 "run(*args, **kwargs) -> None\n \
63 Calls listen(*args, **kwargs) and starts the server mainloop.\n \
64 \n\
65 run() -> None\n \
66 Starts the server mainloop. listen(...) has to be called before calling \
67 run() without arguments.");
68 static PyObject*
69 run(PyObject* self, PyObject* args, PyObject* kwds)
70 {
71 if(PyTuple_GET_SIZE(args) == 0) {
72 /* bjoern.run() */
73 if(!wsgi_app) {
74 PyErr_SetString(
75 PyExc_RuntimeError,
76 "Must call bjoern.listen(wsgi_app, host, ...) before "
77 "calling bjoern.run() without arguments."
78 );
79 return NULL;
80 }
81 } else {
82 /* bjoern.run(wsgi_app, host, ...) */
83 if(!listen(self, args, kwds))
84 return NULL;
85 }
86
87 server_run();
88 wsgi_app = NULL;
8938 Py_RETURN_NONE;
9039 }
9140
9241 static PyMethodDef Bjoern_FunctionTable[] = {
93 {"run", (PyCFunction) run, METH_VARARGS | METH_KEYWORDS, run_doc},
94 {"listen", (PyCFunction) listen, METH_VARARGS | METH_KEYWORDS, listen_doc},
42 {"server_run", (PyCFunction) run, METH_VARARGS, NULL},
9543 {NULL, NULL, 0, NULL}
9644 };
9745
98 PyMODINIT_FUNC initbjoern(void)
46 PyMODINIT_FUNC init_bjoern(void)
9947 {
10048 _init_common();
10149 _init_filewrapper();
10553 PyType_Ready(&StartResponse_Type);
10654 assert(StartResponse_Type.tp_flags & Py_TPFLAGS_READY);
10755
108 PyObject* bjoern_module = Py_InitModule("bjoern", Bjoern_FunctionTable);
56 PyObject* bjoern_module = Py_InitModule("_bjoern", Bjoern_FunctionTable);
10957 PyModule_AddObject(bjoern_module, "version", Py_BuildValue("(iii)", 1, 3, 3));
11058 }
111
112 /* (1) Don't use bool here because we use the "i" type specifier in the call to
113 * PyArg_ParseTuple below which always writes sizeof(int) bytes (and thus
114 * might write to memory regions we don't expect it to write to. */
0 PyObject* wsgi_app;
2727
2828 void _init_common()
2929 {
30 #define _(name) _##name = PyString_FromString(#name)
31 _(REMOTE_ADDR); _(PATH_INFO); _(QUERY_STRING); _(close);
32 _(REQUEST_METHOD); _(SERVER_PROTOCOL); _(GET);
33 _(HTTP_CONTENT_LENGTH); _(CONTENT_LENGTH); _(HTTP_CONTENT_TYPE); _(CONTENT_TYPE);
30 #define _(name) _##name = PyString_FromString(#name)
31 _(REMOTE_ADDR);
32 _(PATH_INFO);
33 _(QUERY_STRING);
34 _(close);
35
36 _(REQUEST_METHOD);
37 _(SERVER_PROTOCOL);
38 _(SERVER_NAME);
39 _(SERVER_PORT);
40 _(GET);
41 _(HTTP_CONTENT_LENGTH);
42 _(CONTENT_LENGTH);
43 _(HTTP_CONTENT_TYPE);
44 _(CONTENT_TYPE);
45 #undef _
46
3447 _HTTP_1_1 = PyString_FromString("HTTP/1.1");
3548 _HTTP_1_0 = PyString_FromString("HTTP/1.0");
3649 _wsgi_input = PyString_FromString("wsgi.input");
3750 _empty_string = PyString_FromString("");
38 #undef _
3951 }
2323 void _init_common(void);
2424
2525 PyObject *_REMOTE_ADDR, *_PATH_INFO, *_QUERY_STRING, *_REQUEST_METHOD, *_GET,
26 *_HTTP_CONTENT_LENGTH, *_CONTENT_LENGTH, *_HTTP_CONTENT_TYPE, *_CONTENT_TYPE,
27 *_SERVER_PROTOCOL, *_HTTP_1_1, *_HTTP_1_0, *_wsgi_input, *_close, *_empty_string;
26 *_HTTP_CONTENT_LENGTH, *_CONTENT_LENGTH, *_HTTP_CONTENT_TYPE,
27 *_CONTENT_TYPE, *_SERVER_PROTOCOL, *_SERVER_NAME, *_SERVER_PORT,
28 *_HTTP_1_1, *_HTTP_1_0, *_wsgi_input, *_close, *_empty_string;
2829
2930 #ifdef DEBUG
3031 #define DBG_REQ(request, ...) \
00 #include <stdlib.h>
1
2 #include "portable_sendfile.h"
13
24 #define SENDFILE_CHUNK_SIZE 16*1024
35
810 #include <sys/socket.h>
911 #include <sys/types.h>
1012
11 ssize_t portable_sendfile(int out_fd, int in_fd) {
13 Py_ssize_t portable_sendfile(int out_fd, int in_fd) {
1214 off_t len = SENDFILE_CHUNK_SIZE;
1315 if(sendfile(in_fd, out_fd, 0, &len, NULL, 0) == -1)
1416 return -1;
2224 #include <sys/socket.h>
2325 #include <sys/types.h>
2426
25 ssize_t portable_sendfile(int out_fd, int in_fd) {
27 Py_ssize_t portable_sendfile(int out_fd, int in_fd) {
2628 off_t len;
2729 if (sendfile(in_fd, out_fd, 0, SENDFILE_CHUNK_SIZE, NULL, &len, 0) == -1) {
2830 return -1;
3638
3739 #include <sys/sendfile.h>
3840
39 ssize_t portable_sendfile(int out_fd, int in_fd) {
41 Py_ssize_t portable_sendfile(int out_fd, int in_fd) {
4042 return sendfile(out_fd, in_fd, NULL, SENDFILE_CHUNK_SIZE);
4143 }
4244
0 ssize_t portable_sendfile(int out_fd, int in_fd);
0 #include <Python.h> /* for Py_ssize_t */
1
2 Py_ssize_t portable_sendfile(int out_fd, int in_fd);
1515 PyObject *pbuf;
1616 } Iobject;
1717
18 Request* Request_new(int client_fd, const char* client_addr)
18 Request* Request_new(ServerInfo* server_info, int client_fd, const char* client_addr)
1919 {
2020 Request* request = malloc(sizeof(Request));
2121 #ifdef DEBUG
2222 static unsigned long request_id = 0;
2323 request->id = request_id++;
2424 #endif
25 request->server_info = server_info;
2526 request->client_fd = client_fd;
2627 request->client_addr = PyString_FromString(client_addr);
2728 http_parser_init((http_parser*)&request->parser, HTTP_REQUEST);
208209 /* SERVER_PROTOCOL (REQUEST_PROTOCOL) */
209210 _set_header(_SERVER_PROTOCOL, parser->http_minor == 1 ? _HTTP_1_1 : _HTTP_1_0);
210211
212 /* SERVER_NAME and SERVER_PORT */
213 if (REQUEST->server_info->host) {
214 _set_header(_SERVER_NAME, REQUEST->server_info->host);
215 _set_header(_SERVER_PORT, REQUEST->server_info->port);
216 }
217
211218 /* REQUEST_METHOD */
212219 if(parser->method == HTTP_GET) {
213220 /* I love useless micro-optimizations. */
281288 on_header_value, on_headers_complete, on_body, on_message_complete
282289 };
283290
284 void _initialize_request_module(const char* server_host, const int server_port)
291 void _initialize_request_module()
285292 {
286293 if(wsgi_base_dict == NULL) {
287294 PycString_IMPORT;
347354 Py_False
348355 );
349356 }
350
351 PyDict_SetItemString(
352 wsgi_base_dict,
353 "SERVER_NAME",
354 PyString_FromString(server_host)
355 );
356
357 PyDict_SetItemString(
358 wsgi_base_dict,
359 "SERVER_PORT",
360 server_port ? PyString_FromFormat("%d", server_port) : _empty_string
361 );
362 }
357 }
33 #include <ev.h>
44 #include "http_parser.h"
55 #include "common.h"
6 #include "server.h"
67
7 void _initialize_request_module(const char* host, const int port);
8 void _initialize_request_module();
89
910 typedef struct {
1011 unsigned error_code : 2;
3132 bj_parser parser;
3233 ev_io ev_watcher;
3334
35 ServerInfo* server_info;
3436 int client_fd;
3537 PyObject* client_addr;
3638
4749 #define REQUEST_FROM_WATCHER(watcher) \
4850 (Request*)((size_t)watcher - (size_t)(&(((Request*)NULL)->ev_watcher)));
4951
50 Request* Request_new(int client_fd, const char* client_addr);
52 Request* Request_new(ServerInfo*, int client_fd, const char* client_addr);
5153 void Request_parse(Request*, const char*, const size_t);
5254 void Request_reset(Request*);
5355 void Request_clean(Request*);
0 #include <sys/socket.h>
1 #include <sys/un.h>
2 #include <netinet/in.h>
3 #include <arpa/inet.h>
4 #include <fcntl.h>
50 #ifdef WANT_SIGINT_HANDLING
61 # include <sys/signal.h>
72 #endif
3 #include <arpa/inet.h>
4 #include <fcntl.h>
85 #include <ev.h>
96 #include "portable_sendfile.h"
107 #include "common.h"
118 #include "wsgi.h"
129 #include "server.h"
1310
14 #define LISTEN_BACKLOG 1024
11 #define SERVER_INFO(loop) ((ServerInfo*)ev_userdata(loop))
1512 #define READ_BUFFER_SIZE 64*1024
1613 #define Py_XCLEAR(obj) do { if(obj) { Py_DECREF(obj); obj = NULL; } } while(0)
1714 #define GIL_LOCK(n) PyGILState_STATE _gilstate_##n = PyGILState_Ensure()
3633 static bool do_sendfile(Request*);
3734 static bool handle_nonzero_errno(Request*);
3835
39 static struct { int fd; const char* filename; } sockinfo;
40
41 void server_run(void)
42 {
43 struct ev_loop* mainloop = ev_default_loop(0);
36
37 void server_run(ServerInfo* server_info)
38 {
39 struct ev_loop* mainloop = ev_loop_new(0);
40 ev_set_userdata(mainloop, server_info);
4441
4542 ev_io accept_watcher;
46 ev_io_init(&accept_watcher, ev_io_on_request, sockinfo.fd, EV_READ);
43 ev_io_init(&accept_watcher, ev_io_on_request, server_info->sockfd, EV_READ);
4744 ev_io_start(mainloop, &accept_watcher);
4845
4946 #if WANT_SIGINT_HANDLING
5956 Py_END_ALLOW_THREADS
6057 }
6158
62 static void cleanup(void) {
63 close(sockinfo.fd);
64 if(sockinfo.filename)
65 unlink(sockinfo.filename);
66 }
67
6859 #if WANT_SIGINT_HANDLING
6960 static void
7061 ev_signal_on_sigint(struct ev_loop* mainloop, ev_signal* watcher, const int events)
7162 {
7263 /* Clean up and shut down this thread.
7364 * (Shuts down the Python interpreter if this is the main thread) */
74 cleanup();
7565 ev_unloop(mainloop, EVUNLOOP_ALL);
7666 PyErr_SetInterrupt();
7767 }
7868 #endif
79
80 bool server_init(const char* hostaddr, const int port, const int reuse_port)
81 {
82 if(!port) {
83 /* Unix socket */
84 if((sockinfo.fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
85 return false;
86
87 struct sockaddr_un sockaddr;
88 sockaddr.sun_family = PF_UNIX;
89 strcpy(sockaddr.sun_path, hostaddr);
90
91 /* Use @ for abstract (Linux) */
92 if(hostaddr[0] == '@')
93 sockaddr.sun_path[0] = '\0';
94 else
95 sockinfo.filename = hostaddr;
96
97 if(bind(sockinfo.fd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0)
98 goto err;
99 }
100 else {
101 /* IP bind */
102 if((sockinfo.fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
103 return false;
104
105 struct sockaddr_in sockaddr;
106 memset(&sockaddr, 0, sizeof(sockaddr));
107 sockaddr.sin_family = PF_INET;
108 inet_pton(AF_INET, hostaddr, &sockaddr.sin_addr);
109 sockaddr.sin_port = htons(port);
110
111 /* Socket options */
112 int true_ref = true;
113
114 /* Set SO_REUSEADDR t make the IP address available for reuse */
115 setsockopt(sockinfo.fd, SOL_SOCKET, SO_REUSEADDR, &true_ref, sizeof(true_ref));
116
117 /* Enable "receive steering" on FreeBSD and Linux >=3.9. This allows
118 * multiple independent bjoerns to bind to the same port (and ideally also
119 * set their CPU affinity), resulting in more efficient load distribution.
120 * https://lwn.net/Articles/542629/
121 */
122 if(reuse_port) {
123 #ifdef SO_REUSEPORT
124 setsockopt(sockinfo.fd, SOL_SOCKET, SO_REUSEPORT, &true_ref, sizeof(true_ref));
125 #else
126 PyErr_SetString(PyExc_RuntimeError, "SO_REUSEPORT is not available.");
127 goto err;
128 #endif
129 }
130
131 if(bind(sockinfo.fd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0)
132 goto err;
133 }
134
135 if(listen(sockinfo.fd, LISTEN_BACKLOG) < 0)
136 goto err;
137
138 DBG("Listening on %s:%d...", hostaddr, port);
139 return true;
140
141 err:
142 cleanup();
143 return false;
144 }
14569
14670 static void
14771 ev_io_on_request(struct ev_loop* mainloop, ev_io* watcher, const int events)
16387 return;
16488 }
16589
166 GIL_LOCK(0);
167 Request* request = Request_new(client_fd, inet_ntoa(sockaddr.sin_addr));
168 GIL_UNLOCK(0);
90 Request* request = Request_new(
91 SERVER_INFO(mainloop),
92 client_fd,
93 inet_ntoa(sockaddr.sin_addr)
94 );
16995
17096 DBG_REQ(request, "Accepted client %s:%d on fd %d",
17197 inet_ntoa(sockaddr.sin_addr), ntohs(sockaddr.sin_port), client_fd);
0 #include "request.h"
0 #ifndef __server_h__
1 #define __server_h__
12
2 bool server_init(const char* hostaddr, const int port, const int reuseport);
3 void server_run(void);
3 typedef struct {
4 int sockfd;
5 PyObject* wsgi_app;
6 PyObject* host;
7 PyObject* port;
8 } ServerInfo;
9
10 void server_run(ServerInfo*);
11
12 #endif
2424
2525 /* application(environ, start_response) call */
2626 PyObject* retval = PyObject_CallFunctionObjArgs(
27 wsgi_app,
27 request->server_info->wsgi_app,
2828 request_headers,
2929 start_response,
3030 NULL /* sentinel */
0 import os
1 import socket
2 import _bjoern
3
4
5 _default_instance = None
6 LISTEN_BACKLOG = 1024
7
8
9 def bind_and_listen(host, port=None, reuse_port=False):
10 if host.startswith("unix:@"):
11 # Abstract UNIX socket: "unix:@foobar"
12 sock = socket.socket(socket.AF_UNIX)
13 sock.bind('\0' + host[6:])
14 elif host.startswith("unix:"):
15 # UNIX socket: "unix:/tmp/foobar.sock"
16 sock = socket.socket(socket.AF_UNIX)
17 sock.bind(host[5:])
18 else:
19 # IP socket
20 sock = socket.socket(socket.AF_INET)
21 # Set SO_REUSEADDR t make the IP address available for reuse
22 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
23 if reuse_port:
24 # Enable "receive steering" on FreeBSD and Linux >=3.9. This allows
25 # multiple independent bjoerns to bind to the same port (and ideally
26 # also set their CPU affinity), resulting in more efficient load
27 # distribution. https://lwn.net/Articles/542629/
28 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
29 sock.bind((host, port))
30
31 sock.listen(LISTEN_BACKLOG)
32
33 return sock
34
35
36 def server_run(sock, wsgi_app):
37 _bjoern.server_run(sock, wsgi_app)
38
39
40 # Backwards compatibility API
41 def listen(wsgi_app, host, port=None, reuse_port=False):
42 """
43 Makes bjoern listen to 'host:port' and use 'wsgi_app' as WSGI application.
44 (This does not run the server mainloop.)
45
46 'reuse_port' -- whether to set SO_REUSEPORT (if available on platform)
47 """
48 global _default_instance
49 if _default_instance:
50 raise RuntimeError("Only one global server instance possible")
51 sock = bind_and_listen(host, port, reuse_port)
52 _default_instance = (sock, wsgi_app)
53
54 def run(*args, **kwargs):
55 """
56 run(*args, **kwargs):
57 Calls listen(*args, **kwargs) and starts the server mainloop.
58
59 run():
60 Starts the server mainloop. listen(...) has to be called before calling
61 run() without arguments."
62 """
63 global _default_instance
64
65 if args or kwargs:
66 # Called as `bjoern.run(wsgi_app, host, ...)`
67 listen(*args, **kwargs)
68 else:
69 # Called as `bjoern.run()`
70 if not _default_instance:
71 raise RuntimeError("Must call bjoern.listen(wsgi_app, host, ...) "
72 "before calling bjoern.run() without arguments.")
73
74 sock, wsgi_app = _default_instance
75 try:
76 server_run(sock, wsgi_app)
77 finally:
78 if sock.type == socket.AF_UNIX:
79 filename = sock.getsockname()
80 if filename[0] != '\0':
81 os.unlink(sock.getsockname())
82 sock.close()
83 _default_instance = None
11 from random import choice
22
33 def app1(env, sr):
4 print env
45 sr('200 ok', [('Foo', 'Bar'), ('Blah', 'Blubb'), ('Spam', 'Eggs'), ('Blurg', 'asdasjdaskdasdjj asdk jaks / /a jaksdjkas jkasd jkasdj '),
56 ('asd2easdasdjaksdjdkskjkasdjka', 'oasdjkadk kasdk k k k k k ')])
67 return ['hello', 'world']
78
89 def app2(env, sr):
10 print env
911 sr('200 ok', [])
1012 return 'hello'
1113
1214 def app3(env, sr):
15 print env
1316 sr('200 abc', [('Content-Length', '12')])
1417 yield 'Hello'
1518 yield ' World'
1619 yield '\n'
1720
1821 def app4(e, s):
22 print e
1923 s('200 ok', [])
2024 return ['hello\n']
2125
+0
-45
tests/wsgitest-round-robin.py less more
0 import sys
1 import random
2 import wsgitest
3 import wsgitest.run
4
5 tests = wsgitest.run.find_tests([wsgitest.DEFAULT_TESTS_DIR]).tests
6 test = None
7
8 if len(sys.argv) > 1:
9 try:
10 index = int(sys.argv[1])
11 except ValueError:
12 module_name, test_name = sys.argv[1:]
13 for module, tests_ in tests.iteritems():
14 if module.__name__ == module_name:
15 for test_ in tests_:
16 if test_.app.__name__ == test_name:
17 test = test_
18 break
19 else:
20 continue
21 break
22 else:
23 iterator = iter(tests.chainvalues())
24 index -= 1
25 while index:
26 iterator.next()
27 index -= 1
28 test = iterator.next()
29
30 assert test is not None
31 def choose_test():
32 return test.app
33 else:
34 testlist = list(tests.chainvalues())
35 def choose_test():
36 return random.choice(testlist).app
37
38 def app(environ, start_response):
39 app = choose_test()
40 print 'Testing %s...' % app.__name__
41 return app(environ, start_response)
42
43 import bjoern
44 bjoern.run(app, '127.0.0.1', 8080)
+0
-7
tests/wsgitest_config.py less more
0 import bjoern
1
2 SERVER_PORT = 9998
3 SERVER_BOOT_DURATION = 0.1
4
5 def run_server(app, host, port):
6 bjoern.run(app, host, port)