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

Reply via email to