From 5fd2933d2e54a7b5781698a670abf89b1031db44 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 10 Nov 2020 22:27:08 +0300 Subject: Python: supporting ASGI legacy protocol. Introducing manual protocol selection for 'universal' apps and frameworks. --- src/python/nxt_python.c | 12 +++- src/python/nxt_python_asgi.c | 121 +++++++++++++++++++++++++++------- src/python/nxt_python_asgi.h | 2 + src/python/nxt_python_asgi_lifespan.c | 43 +++++++++++- 4 files changed, 151 insertions(+), 27 deletions(-) (limited to 'src/python') diff --git a/src/python/nxt_python.c b/src/python/nxt_python.c index 26a6f093..faf0c0e1 100644 --- a/src/python/nxt_python.c +++ b/src/python/nxt_python.c @@ -68,6 +68,7 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) char *nxt_py_module; size_t len; PyObject *obj, *pypath, *module; + nxt_str_t proto; const char *callable; nxt_unit_ctx_t *unit_ctx; nxt_unit_init_t python_init; @@ -82,6 +83,9 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) static const char bin_python[] = "/bin/python"; #endif + static const nxt_str_t wsgi = nxt_string("wsgi"); + static const nxt_str_t asgi = nxt_string("asgi"); + app_conf = data->app; c = &app_conf->u.python; @@ -244,7 +248,13 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) python_init.shm_limit = data->app->shm_limit; python_init.callbacks.ready_handler = nxt_python_ready_handler; - if (nxt_python_asgi_check(nxt_py_application)) { + proto = c->protocol; + + if (proto.length == 0) { + proto = nxt_python_asgi_check(nxt_py_application) ? asgi : wsgi; + } + + if (nxt_strstr_eq(&proto, &asgi)) { rc = nxt_python_asgi_init(&python_init, &nxt_py_proto); } else { diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c index cab73239..e11a3b6c 100644 --- a/src/python/nxt_python_asgi.c +++ b/src/python/nxt_python_asgi.c @@ -16,6 +16,7 @@ #include +static PyObject *nxt_python_asgi_get_func(PyObject *obj); static int nxt_python_asgi_ctx_data_alloc(void **pdata); static void nxt_python_asgi_ctx_data_free(void *data); static int nxt_python_asgi_startup(void *data); @@ -42,6 +43,7 @@ static PyObject *nxt_py_asgi_port_read(PyObject *self, PyObject *args); static void nxt_python_asgi_done(void); +int nxt_py_asgi_legacy; static PyObject *nxt_py_port_read; static nxt_unit_port_t *nxt_py_shared_port; @@ -64,56 +66,78 @@ int nxt_python_asgi_check(PyObject *obj) { int res; - PyObject *call; + PyObject *func; PyCodeObject *code; - if (PyFunction_Check(obj)) { - code = (PyCodeObject *) PyFunction_GET_CODE(obj); + func = nxt_python_asgi_get_func(obj); + + if (func == NULL) { + return 0; + } + + code = (PyCodeObject *) PyFunction_GET_CODE(func); + + nxt_unit_debug(NULL, "asgi_check: callable is %sa coroutine function with " + "%d argument(s)", + (code->co_flags & CO_COROUTINE) != 0 ? "" : "not ", + code->co_argcount); + + res = (code->co_flags & CO_COROUTINE) != 0 || code->co_argcount == 1; + + Py_DECREF(func); + + return res; +} + + +static PyObject * +nxt_python_asgi_get_func(PyObject *obj) +{ + PyObject *call; - return (code->co_flags & CO_COROUTINE) != 0; + if (PyFunction_Check(obj)) { + Py_INCREF(obj); + return obj; } if (PyMethod_Check(obj)) { obj = PyMethod_GET_FUNCTION(obj); - code = (PyCodeObject *) PyFunction_GET_CODE(obj); - - return (code->co_flags & CO_COROUTINE) != 0; + Py_INCREF(obj); + return obj; } call = PyObject_GetAttrString(obj, "__call__"); if (call == NULL) { - return 0; + return NULL; } if (PyFunction_Check(call)) { - code = (PyCodeObject *) PyFunction_GET_CODE(call); - - res = (code->co_flags & CO_COROUTINE) != 0; - - } else { - if (PyMethod_Check(call)) { - obj = PyMethod_GET_FUNCTION(call); + return call; + } - code = (PyCodeObject *) PyFunction_GET_CODE(obj); + if (PyMethod_Check(call)) { + obj = PyMethod_GET_FUNCTION(call); - res = (code->co_flags & CO_COROUTINE) != 0; + Py_INCREF(obj); + Py_DECREF(call); - } else { - res = 0; - } + return obj; } Py_DECREF(call); - return res; + return NULL; } int nxt_python_asgi_init(nxt_unit_init_t *init, nxt_python_proto_t *proto) { + PyObject *func; + PyCodeObject *code; + nxt_unit_debug(NULL, "asgi_init"); if (nxt_slow_path(nxt_py_asgi_str_init() != NXT_UNIT_OK)) { @@ -136,6 +160,22 @@ nxt_python_asgi_init(nxt_unit_init_t *init, nxt_python_proto_t *proto) return NXT_UNIT_ERROR; } + func = nxt_python_asgi_get_func(nxt_py_application); + if (nxt_slow_path(func == NULL)) { + nxt_unit_alert(NULL, "Python cannot find function for callable"); + return NXT_UNIT_ERROR; + } + + code = (PyCodeObject *) PyFunction_GET_CODE(func); + + if ((code->co_flags & CO_COROUTINE) == 0) { + nxt_unit_debug(NULL, "asgi: callable is not a coroutine function " + "switching to legacy mode"); + nxt_py_asgi_legacy = 1; + } + + Py_DECREF(func); + init->callbacks.request_handler = nxt_py_asgi_request_handler; init->callbacks.data_handler = nxt_py_asgi_http_data_handler; init->callbacks.websocket_handler = nxt_py_asgi_websocket_handler; @@ -366,6 +406,7 @@ static void nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) { PyObject *scope, *res, *task, *receive, *send, *done, *asgi; + PyObject *stage2; nxt_py_asgi_ctx_data_t *ctx_data; if (req->request->websocket_handshake) { @@ -415,8 +456,42 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) req->data = asgi; - res = PyObject_CallFunctionObjArgs(nxt_py_application, - scope, receive, send, NULL); + if (!nxt_py_asgi_legacy) { + nxt_unit_req_debug(req, "Python call ASGI 3.0 application"); + + res = PyObject_CallFunctionObjArgs(nxt_py_application, + scope, receive, send, NULL); + + } else { + nxt_unit_req_debug(req, "Python call legacy application"); + + res = PyObject_CallFunctionObjArgs(nxt_py_application, scope, NULL); + + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(req, "Python failed to call legacy app stage1"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_scope; + } + + if (nxt_slow_path(PyCallable_Check(res) == 0)) { + nxt_unit_req_error(req, + "Legacy ASGI application returns not a callable"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + Py_DECREF(res); + + goto release_scope; + } + + stage2 = res; + + res = PyObject_CallFunctionObjArgs(stage2, receive, send, NULL); + + Py_DECREF(stage2); + } + if (nxt_slow_path(res == NULL)) { nxt_unit_req_error(req, "Python failed to call the application"); nxt_python_print_exception(); diff --git a/src/python/nxt_python_asgi.h b/src/python/nxt_python_asgi.h index 69d58477..c3c3e17a 100644 --- a/src/python/nxt_python_asgi.h +++ b/src/python/nxt_python_asgi.h @@ -68,4 +68,6 @@ int nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data); int nxt_py_asgi_lifespan_shutdown(nxt_unit_ctx_t *ctx); +extern int nxt_py_asgi_legacy; + #endif /* _NXT_PYTHON_ASGI_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi_lifespan.c b/src/python/nxt_python_asgi_lifespan.c index 6910d2e8..506eaf4d 100644 --- a/src/python/nxt_python_asgi_lifespan.c +++ b/src/python/nxt_python_asgi_lifespan.c @@ -71,6 +71,7 @@ nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data) { int rc; PyObject *scope, *res, *py_task, *receive, *send, *done; + PyObject *stage2; nxt_py_asgi_lifespan_t *lifespan; if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_lifespan_type) != 0)) { @@ -129,8 +130,43 @@ nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data) goto release_future; } - res = PyObject_CallFunctionObjArgs(nxt_py_application, - scope, receive, send, NULL); + if (!nxt_py_asgi_legacy) { + nxt_unit_req_debug(NULL, "Python call ASGI 3.0 application"); + + res = PyObject_CallFunctionObjArgs(nxt_py_application, + scope, receive, send, NULL); + + } else { + nxt_unit_req_debug(NULL, "Python call legacy application"); + + res = PyObject_CallFunctionObjArgs(nxt_py_application, scope, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_log(NULL, NXT_UNIT_LOG_INFO, + "ASGI Lifespan processing exception"); + nxt_python_print_exception(); + + lifespan->disabled = 1; + rc = NXT_UNIT_OK; + + goto release_scope; + } + + if (nxt_slow_path(PyCallable_Check(res) == 0)) { + nxt_unit_req_error(NULL, + "Legacy ASGI application returns not a callable"); + + Py_DECREF(res); + + goto release_scope; + } + + stage2 = res; + + res = PyObject_CallFunctionObjArgs(stage2, receive, send, NULL); + + Py_DECREF(stage2); + } + if (nxt_slow_path(res == NULL)) { nxt_unit_error(NULL, "Python failed to call the application"); nxt_python_print_exception(); @@ -143,7 +179,8 @@ nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data) goto release_scope; } - py_task = PyObject_CallFunctionObjArgs(ctx_data->loop_create_task, res, NULL); + py_task = PyObject_CallFunctionObjArgs(ctx_data->loop_create_task, res, + NULL); if (nxt_slow_path(py_task == NULL)) { nxt_unit_alert(NULL, "Python failed to call the create_task"); nxt_python_print_exception(); -- cgit