details: https://github.com/nginx/njs/commit/04f6dfb91991a855095c41df760149d9e3847bd9 branches: master commit: 04f6dfb91991a855095c41df760149d9e3847bd9 user: Dmitry Volyntsev <xei...@nginx.com> date: Tue, 26 Aug 2025 14:22:19 -0700 description: QuickJS: added njs.on('exit') API support.
This closes #955 issue on Github. --- external/njs_shell.c | 2 + nginx/ngx_http_js_module.c | 4 +- nginx/ngx_js.c | 8 ++ nginx/t/js_exit.t | 76 ++++++++++++++++++ nginx/t/stream_js_exit.t | 2 - src/qjs.c | 194 +++++++++++++++++++++++++++++++++++++++++---- src/qjs.h | 2 + 7 files changed, 269 insertions(+), 19 deletions(-) diff --git a/external/njs_shell.c b/external/njs_shell.c index c2294f75..b29ccee2 100644 --- a/external/njs_shell.c +++ b/external/njs_shell.c @@ -2703,6 +2703,8 @@ njs_engine_qjs_destroy(njs_engine_t *engine) njs_queue_link_t *link; njs_rejected_promise_t *rejected_promise; + qjs_call_exit_hook(engine->u.qjs.ctx); + console = JS_GetRuntimeOpaque(engine->u.qjs.rt); if (console->rejected_promises != NULL) { diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index c73171b8..286a0a78 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1609,7 +1609,7 @@ static ngx_int_t ngx_http_js_init_vm(ngx_http_request_t *r, njs_int_t proto_id) { ngx_http_js_ctx_t *ctx; - ngx_pool_cleanup_t *cln; + ngx_http_cleanup_t *cln; ngx_http_js_loc_conf_t *jlcf; jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); @@ -1644,7 +1644,7 @@ ngx_http_js_init_vm(ngx_http_request_t *r, njs_int_t proto_id) "http js vm clone %s: %p from: %p", jlcf->engine->name, ctx->engine, jlcf->engine); - cln = ngx_pool_cleanup_add(r->pool, 0); + cln = ngx_http_cleanup_add(r, 0); if (cln == NULL) { return NGX_ERROR; } diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index d2bb781c..c385e16e 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -1130,6 +1130,7 @@ ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, uint32_t i, length; ngx_str_t exception; JSRuntime *rt; + JSValue ret; JSContext *cx; JSClassID class_id; JSMemoryUsage stats; @@ -1142,6 +1143,13 @@ ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, cx = e->u.qjs.ctx; if (ctx != NULL) { + ret = qjs_call_exit_hook(cx); + if (JS_IsException(ret)) { + ngx_qjs_exception(e, &exception); + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exit hook exception: %V", &exception); + } + node = njs_rbtree_min(&ctx->waiting_events); while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) { diff --git a/nginx/t/js_exit.t b/nginx/t/js_exit.t new file mode 100644 index 00000000..c0ea90ee --- /dev/null +++ b/nginx/t/js_exit.t @@ -0,0 +1,76 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, njs.on('exit', ...). + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /test { + js_content test.test; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test(r) { + njs.on('exit', function() { + ngx.log(ngx.WARN, `exit hook: bs: \${r.variables.bytes_sent}`); + }); + + r.return(200, `bs: \${r.variables.bytes_sent}`); + } + + export default { test }; + +EOF + +$t->try_run('no njs')->plan(2); + +############################################################################### + +like(http_get('/test'), qr/bs: 0/, 'response'); + +$t->stop(); + +like($t->read_file('error.log'), qr/\[warn\].*exit hook: bs: \d+/, 'exit hook logged'); + +############################################################################### diff --git a/nginx/t/stream_js_exit.t b/nginx/t/stream_js_exit.t index 01778f0f..41fbe1b3 100644 --- a/nginx/t/stream_js_exit.t +++ b/nginx/t/stream_js_exit.t @@ -108,8 +108,6 @@ EOF $t->try_run('no stream njs available'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(2); $t->run_daemon(\&stream_daemon, port(8090)); diff --git a/src/qjs.c b/src/qjs.c index 9c0fcdb4..b7899158 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -19,6 +19,12 @@ typedef struct { } qjs_signal_entry_t; +typedef struct { +#define QJS_NJS_HOOK_EXIT 0 + JSValue hooks[1]; +} qjs_njs_t; + + typedef enum { QJS_ENCODING_UTF8, } qjs_encoding_t; @@ -42,7 +48,13 @@ typedef struct { extern char **environ; -static JSValue qjs_njs_getter(JSContext *ctx, JSValueConst this_val); +static int qjs_add_intrinsic_njs(JSContext *cx, JSValueConst global); +static JSValue qjs_njs_on(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv); +static void qjs_njs_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); +static void qjs_njs_finalizer(JSRuntime *rt, JSValue val); + static JSValue qjs_process_env(JSContext *ctx, JSValueConst this_val); static JSValue qjs_process_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); @@ -99,10 +111,6 @@ static qjs_encoding_label_t qjs_encoding_labels[] = }; -static const JSCFunctionListEntry qjs_global_proto[] = { - JS_CGETSET_DEF("njs", qjs_njs_getter, NULL), -}; - static const JSCFunctionListEntry qjs_text_decoder_proto[] = { JS_PROP_STRING_DEF("[Symbol.toStringTag]", "TextDecoder", JS_PROP_CONFIGURABLE), @@ -126,6 +134,7 @@ static const JSCFunctionListEntry qjs_njs_proto[] = { JS_PROP_INT32_DEF("version_number", NJS_VERSION_NUMBER, JS_PROP_C_W_E), JS_PROP_STRING_DEF("engine", "QuickJS", JS_PROP_C_W_E), + JS_CFUNC_DEF("on", 2, qjs_njs_on), }; static const JSCFunctionListEntry qjs_process_proto[] = { @@ -137,6 +146,13 @@ static const JSCFunctionListEntry qjs_process_proto[] = { }; +static JSClassDef qjs_njs_class = { + "njs", + .finalizer = qjs_njs_finalizer, + .gc_mark = qjs_njs_mark, +}; + + static JSClassDef qjs_text_decoder_class = { "TextDecoder", .finalizer = qjs_text_decoder_finalizer, @@ -186,6 +202,10 @@ qjs_new_context(JSRuntime *rt, qjs_module_t **addons) global_obj = JS_GetGlobalObject(ctx); + if (qjs_add_intrinsic_njs(ctx, global_obj) < 0) { + return NULL; + } + if (qjs_add_intrinsic_text_decoder(ctx, global_obj) < 0) { return NULL; } @@ -194,9 +214,6 @@ qjs_new_context(JSRuntime *rt, qjs_module_t **addons) return NULL; } - JS_SetPropertyFunctionList(ctx, global_obj, qjs_global_proto, - njs_nitems(qjs_global_proto)); - prop = JS_NewAtom(ctx, "eval"); if (prop == JS_ATOM_NULL) { return NULL; @@ -225,20 +242,167 @@ qjs_new_context(JSRuntime *rt, qjs_module_t **addons) } -static JSValue -qjs_njs_getter(JSContext *ctx, JSValueConst this_val) +JSValue +qjs_call_exit_hook(JSContext *ctx) { - JSValue obj; + JSValue global, obj, ret; + qjs_njs_t *njs; - obj = JS_NewObject(ctx); + global = JS_GetGlobalObject(ctx); + + obj = JS_GetPropertyStr(ctx, global, "njs"); + if (JS_IsException(obj) || JS_IsUndefined(obj)) { + goto done; + } + + njs = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_NJS); + if (njs != NULL && JS_IsFunction(ctx, njs->hooks[QJS_NJS_HOOK_EXIT])) { + ret = JS_Call(ctx, njs->hooks[QJS_NJS_HOOK_EXIT], JS_UNDEFINED, + 0, NULL); + + JS_FreeValue(ctx, njs->hooks[QJS_NJS_HOOK_EXIT]); + njs->hooks[QJS_NJS_HOOK_EXIT] = JS_UNDEFINED; + + if (JS_IsException(ret)) { + JS_FreeValue(ctx, obj); + JS_FreeValue(ctx, global); + return ret; + } + + JS_FreeValue(ctx, ret); + } + +done: + + JS_FreeValue(ctx, obj); + JS_FreeValue(ctx, global); + + return JS_UNDEFINED; +} + + +static int +qjs_add_intrinsic_njs(JSContext *cx, JSValueConst global) +{ + JSValue obj, proto; + + if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_NJS, + &qjs_njs_class) < 0) + { + return -1; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return -1; + } + + JS_SetPropertyFunctionList(cx, proto, qjs_njs_proto, + njs_nitems(qjs_njs_proto)); + + JS_SetClassProto(cx, QJS_CORE_CLASS_ID_NJS, proto); + + obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_NJS); if (JS_IsException(obj)) { + return -1; + } + + if (JS_SetPropertyStr(cx, global, "njs", obj) < 0) { + JS_FreeValue(cx, obj); + return -1; + } + + return 0; +} + + +static void +qjs_njs_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) +{ + unsigned i; + qjs_njs_t *njs; + + njs = JS_GetOpaque(val, QJS_CORE_CLASS_ID_NJS); + if (njs != NULL) { + for (i = 0; i < njs_nitems(njs->hooks); i++) { + JS_MarkValue(rt, njs->hooks[i], mark_func); + } + } +} + + +static void +qjs_njs_finalizer(JSRuntime *rt, JSValue val) +{ + unsigned i; + qjs_njs_t *njs; + + njs = JS_GetOpaque(val, QJS_CORE_CLASS_ID_NJS); + if (njs != NULL) { + for (i = 0; i < njs_nitems(njs->hooks); i++) { + JS_FreeValueRT(rt, njs->hooks[i]); + } + + js_free_rt(rt, njs); + } +} + + +static JSValue +qjs_njs_on(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + unsigned i, n; + qjs_njs_t *njs; + njs_str_t name; + + static const njs_str_t hooks[] = { + njs_str("exit"), + }; + + njs = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_NJS); + if (njs == NULL) { + njs = js_mallocz(ctx, sizeof(qjs_njs_t)); + if (njs == NULL) { + return JS_ThrowOutOfMemory(ctx); + } + + JS_SetOpaque(this_val, njs); + } + + name.start = (u_char *) JS_ToCStringLen(ctx, &name.length, argv[0]); + if (name.start == NULL) { return JS_EXCEPTION; } - JS_SetPropertyFunctionList(ctx, obj, qjs_njs_proto, - njs_nitems(qjs_njs_proto)); + i = 0; + n = njs_nitems(hooks); - return obj; + while (i < n) { + if (njs_strstr_eq(&name, &hooks[i])) { + break; + } + + i++; + } + + if (i == n) { + JS_ThrowTypeError(ctx, "unknown hook \"%s\"", name.start); + JS_FreeCString(ctx, (const char *) name.start); + return JS_EXCEPTION; + } + + JS_FreeCString(ctx, (const char *) name.start); + + if (!JS_IsFunction(ctx, argv[1]) && !JS_IsNull(argv[1])) { + JS_ThrowTypeError(ctx, "callback is not a function or null"); + return JS_EXCEPTION; + } + + JS_FreeValue(ctx, njs->hooks[i]); + njs->hooks[i] = JS_DupValue(ctx, argv[1]); + + return JS_UNDEFINED; } diff --git a/src/qjs.h b/src/qjs.h index 3b50143e..df5bfd11 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -27,6 +27,7 @@ enum { QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR, QJS_CORE_CLASS_ID_TEXT_DECODER, QJS_CORE_CLASS_ID_TEXT_ENCODER, + QJS_CORE_CLASS_ID_NJS, QJS_CORE_CLASS_ID_FS_STATS, QJS_CORE_CLASS_ID_FS_DIRENT, QJS_CORE_CLASS_ID_FS_FILEHANDLE, @@ -49,6 +50,7 @@ typedef struct { JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons); +JSValue qjs_call_exit_hook(JSContext *ctx); JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv); _______________________________________________ nginx-devel mailing list nginx-devel@nginx.org https://mailman.nginx.org/mailman/listinfo/nginx-devel