From fe291de024125a40b0a137c06b2bb724a023aac1 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 06:30:15 +0800 Subject: [PATCH 01/13] feat(http): add QuickJS script handler --- CMakeLists.txt | 61 +- Makefile | 26 +- Makefile.in | 33 + Makefile.vars | 5 + config.ini | 2 + configure | 2 + docs/PLAN.md | 1 + docs/cn/HttpJsHandler.md | 226 ++++ docs/cn/HttpLuaHandler.md | 6 +- docs/cn/README.md | 4 + examples/http_server_test.cpp | 33 +- examples/scripts/hello.js | 26 + hconfig.h.in | 1 + http/server/HttpJsHandler.cpp | 1812 +++++++++++++++++++++++++++++ http/server/HttpJsHandler.h | 47 + http/server/HttpScriptHandler.cpp | 28 +- http/server/HttpService.cpp | 26 +- http/server/HttpService.h | 2 +- redis/AsyncRedisClient.cpp | 8 +- scripts/unittest.sh | 12 + unittest/CMakeLists.txt | 26 + unittest/http_js_handler_test.cpp | 137 +++ unittest/http_js_mqtt_test.cpp | 71 ++ unittest/http_js_redis_test.cpp | 113 ++ unittest/http_js_ws_test.cpp | 83 ++ 25 files changed, 2759 insertions(+), 32 deletions(-) create mode 100644 docs/cn/HttpJsHandler.md create mode 100644 examples/scripts/hello.js create mode 100644 http/server/HttpJsHandler.cpp create mode 100644 http/server/HttpJsHandler.h create mode 100644 unittest/http_js_handler_test.cpp create mode 100644 unittest/http_js_mqtt_test.cpp create mode 100644 unittest/http_js_redis_test.cpp create mode 100644 unittest/http_js_ws_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e1ab1cfd..35eb2947a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ option(WITH_GNUTLS "with gnutls library" OFF) option(WITH_MBEDTLS "with mbedtls library" OFF) option(WITH_LUA "with lua library" OFF) +option(WITH_JS "with quickjs library" OFF) option(WITH_KCP "compile event/kcp" OFF) @@ -213,6 +214,49 @@ if(WITH_LUA) endif() endif() +if(WITH_JS) + add_definitions(-DWITH_JS) + find_path(QUICKJS_INCLUDE_DIR + NAMES quickjs.h + HINTS + ${QUICKJS_ROOT}/include/quickjs + ${QUICKJS_ROOT}/include + /opt/homebrew/opt/quickjs/include/quickjs + /opt/homebrew/opt/quickjs/include + /usr/local/opt/quickjs/include/quickjs + /usr/local/opt/quickjs/include + /usr/local/include/quickjs + /usr/local/include + /usr/include/quickjs + /usr/include) + find_library(QUICKJS_LIBRARY + NAMES quickjs libquickjs + HINTS + ${QUICKJS_ROOT}/lib/quickjs + ${QUICKJS_ROOT}/lib + /opt/homebrew/opt/quickjs/lib/quickjs + /opt/homebrew/opt/quickjs/lib + /usr/local/opt/quickjs/lib/quickjs + /usr/local/opt/quickjs/lib + /usr/local/lib/quickjs + /usr/local/lib + /usr/lib) + if(NOT QUICKJS_INCLUDE_DIR OR NOT QUICKJS_LIBRARY) + message(FATAL_ERROR "WITH_JS requires QuickJS. Set QUICKJS_ROOT or QUICKJS_INCLUDE_DIR and QUICKJS_LIBRARY.") + endif() + include_directories(${QUICKJS_INCLUDE_DIR}) + set(LIBS ${LIBS} ${QUICKJS_LIBRARY}) + if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT) + add_definitions(-DHVJS_WITH_HTTP) + endif() + if(WITH_EVPP AND WITH_REDIS) + add_definitions(-DHVJS_WITH_REDIS) + endif() + if(WITH_EVPP AND WITH_MQTT) + add_definitions(-DHVJS_WITH_MQTT) + endif() +endif() + if(WIN32 OR MINGW) add_definitions(-DWIN32_LEAN_AND_MEAN -D_CRT_SECURE_NO_WARNINGS -D_WIN32_WINNT=0x0600) set(LIBS ${LIBS} secur32 crypt32 winmm iphlpapi ws2_32) @@ -286,8 +330,14 @@ if(WITH_EVPP) endif() if(WITH_HTTP_SERVER) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${HTTP_SERVER_HEADERS}) + if(WITH_LUA OR WITH_JS) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpScriptHandler.h) + endif() if(WITH_LUA) - set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpScriptHandler.h http/server/HttpLuaHandler.h) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpLuaHandler.h) + endif() + if(WITH_JS) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpJsHandler.h) endif() set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} http/server) endif() @@ -308,6 +358,15 @@ if(WITH_MQTT) endif() list_source_directories(LIBHV_SRCS ${LIBHV_SRCDIRS}) +if(NOT WITH_LUA) + list(FILTER LIBHV_SRCS EXCLUDE REGEX "(^|/)HttpLuaHandler\\.cpp$") +endif() +if(NOT WITH_JS) + list(FILTER LIBHV_SRCS EXCLUDE REGEX "(^|/)HttpJsHandler\\.cpp$") +endif() +if(NOT WITH_LUA AND NOT WITH_JS) + list(FILTER LIBHV_SRCS EXCLUDE REGEX "(^|/)HttpScriptHandler\\.cpp$") +endif() if(WIN32) set(CMAKE_RC_FLAGS_DEBUG -D_DEBUG) configure_file(${PROJECT_SOURCE_DIR}/${PROJECT_NAME}.rc.in ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.rc) diff --git a/Makefile b/Makefile index 4f573da45..60875c196 100644 --- a/Makefile +++ b/Makefile @@ -49,8 +49,14 @@ endif ifeq ($(WITH_HTTP_SERVER), yes) LIBHV_HEADERS += $(HTTP_SERVER_HEADERS) LIBHV_SRCDIRS += http/server +ifneq ($(filter yes,$(WITH_LUA) $(WITH_JS)),) +LIBHV_HEADERS += http/server/HttpScriptHandler.h +endif ifeq ($(WITH_LUA), yes) -LIBHV_HEADERS += http/server/HttpScriptHandler.h http/server/HttpLuaHandler.h +LIBHV_HEADERS += http/server/HttpLuaHandler.h +endif +ifeq ($(WITH_JS), yes) +LIBHV_HEADERS += http/server/HttpJsHandler.h endif endif @@ -424,6 +430,24 @@ ifeq ($(WITH_REDIS), yes) endif endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_SERVER), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_handler_test unittest/http_js_handler_test.cpp -Llib -lhv -pthread $(JS_LIBS) +ifeq ($(WITH_REDIS), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_REDIS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Iredis -o bin/http_js_redis_test unittest/http_js_redis_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(JS_LIBS) +endif + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_ws_test unittest/http_js_ws_test.cpp -Llib -lhv -pthread $(JS_LIBS) +ifeq ($(WITH_MQTT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_MQTT $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Imqtt -o bin/http_js_mqtt_test unittest/http_js_mqtt_test.cpp -Llib -lhv -pthread $(JS_LIBS) +endif +endif +endif +endif +endif +endif run-unittest: unittest bash scripts/unittest.sh diff --git a/Makefile.in b/Makefile.in index 25ea81d54..bcb5fce5a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -129,6 +129,17 @@ ifeq ($(ALL_SRCS), ) ALL_SRCS = $(wildcard *.c *.cc *.cpp) endif override SRCS += $(filter-out %_test.c %_test.cc %_test.cpp, $(ALL_SRCS)) +ifeq ($(filter clean,$(MAKECMDGOALS)),) +ifneq ($(WITH_LUA), yes) +override SRCS := $(filter-out %/HttpLuaHandler.cpp HttpLuaHandler.cpp, $(SRCS)) +endif +ifneq ($(WITH_JS), yes) +override SRCS := $(filter-out %/HttpJsHandler.cpp HttpJsHandler.cpp, $(SRCS)) +endif +ifeq ($(filter yes,$(WITH_LUA) $(WITH_JS)),) +override SRCS := $(filter-out %/HttpScriptHandler.cpp HttpScriptHandler.cpp, $(SRCS)) +endif +endif # OBJS += $(patsubst %.c, %.o, $(SRCS)) # OBJS += $(patsubst %.cc, %.o, $(SRCS)) # OBJS += $(patsubst %.cpp, %.o, $(SRCS)) @@ -185,6 +196,28 @@ endif endif endif +ifeq ($(WITH_JS), yes) + CPPFLAGS += -DWITH_JS $(JS_CFLAGS) + LDFLAGS += $(JS_LIBS) +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + CPPFLAGS += -DHVJS_WITH_HTTP +endif +endif +endif +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_REDIS), yes) + CPPFLAGS += -DHVJS_WITH_REDIS +endif +endif +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_MQTT), yes) + CPPFLAGS += -DHVJS_WITH_MQTT +endif +endif +endif + CPPFLAGS += $(addprefix -D, $(DEFINES)) CPPFLAGS += $(addprefix -I, $(INCDIRS)) CPPFLAGS += $(addprefix -I, $(SRCDIRS)) diff --git a/Makefile.vars b/Makefile.vars index f2d3a13c0..89f3fda04 100644 --- a/Makefile.vars +++ b/Makefile.vars @@ -13,6 +13,11 @@ LUA_PREFIX ?= $(shell for dir in /opt/homebrew/opt/lua /usr/local/opt/lua /usr; LUA_INCLUDE_DIR ?= $(shell if [ -n "$(LUA_PREFIX)" ]; then for dir in "$(LUA_PREFIX)/include/lua" "$(LUA_PREFIX)/include/lua5.5" "$(LUA_PREFIX)/include/lua5.4" "$(LUA_PREFIX)/include/lua5.3" "$(LUA_PREFIX)/include"; do if [ -f "$$dir/lua.h" ]; then echo $$dir; break; fi; done; fi) LUA_CFLAGS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --cflags $(LUA_PKG_CONFIG); elif [ -n "$(LUA_INCLUDE_DIR)" ]; then echo -I$(LUA_INCLUDE_DIR); fi) LUA_LIBS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --libs $(LUA_PKG_CONFIG); elif [ -n "$(LUA_PREFIX)" ]; then echo -L$(LUA_PREFIX)/lib -llua; else echo -llua; fi) +QUICKJS_ROOT ?= $(shell for dir in /opt/homebrew/opt/quickjs /usr/local/opt/quickjs /usr; do if [ -f "$$dir/include/quickjs/quickjs.h" ] || [ -f "$$dir/include/quickjs.h" ]; then echo $$dir; break; fi; done) +QUICKJS_INCLUDE_DIR ?= $(shell if [ -n "$(QUICKJS_ROOT)" ]; then for dir in "$(QUICKJS_ROOT)/include/quickjs" "$(QUICKJS_ROOT)/include"; do if [ -f "$$dir/quickjs.h" ]; then echo $$dir; break; fi; done; fi) +QUICKJS_LIB_DIR ?= $(shell if [ -n "$(QUICKJS_ROOT)" ]; then for dir in "$(QUICKJS_ROOT)/lib/quickjs" "$(QUICKJS_ROOT)/lib"; do if [ -f "$$dir/libquickjs.a" ] || [ -f "$$dir/libquickjs.dylib" ] || [ -f "$$dir/libquickjs.so" ]; then echo $$dir; break; fi; done; fi) +JS_CFLAGS ?= $(shell if [ -n "$(QUICKJS_INCLUDE_DIR)" ]; then echo -I$(QUICKJS_INCLUDE_DIR); fi) +JS_LIBS ?= $(shell if [ -n "$(QUICKJS_LIB_DIR)" ]; then echo -L$(QUICKJS_LIB_DIR) -lquickjs; else echo -lquickjs; fi) BASE_HEADERS = base/hplatform.h\ \ diff --git a/config.ini b/config.ini index 6f5a13d93..e4a87fe9e 100644 --- a/config.ini +++ b/config.ini @@ -40,6 +40,8 @@ WITH_GNUTLS=no WITH_MBEDTLS=no # for http lua handler WITH_LUA=no +# for http js handler (QuickJS) +WITH_JS=no # rudp WITH_KCP=no diff --git a/configure b/configure index ea7b7d407..aa7a66d4c 100755 --- a/configure +++ b/configure @@ -33,6 +33,7 @@ modules: --with-redis compile redis module? (DEFAULT: $WITH_REDIS) --with-rpc compile hrpc (libhrpc, needs protobuf)? (DEFAULT: $WITH_RPC) --with-lua compile lua module? (DEFAULT: $WITH_LUA) + --with-js compile js module? (DEFAULT: $WITH_JS) features: --enable-uds enable Unix Domain Socket? (DEFAULT: $ENABLE_UDS) @@ -305,6 +306,7 @@ option=ENABLE_UDS && check_option option=USE_MULTIMAP && check_option option=WITH_KCP && check_option option=WITH_IO_URING && check_option +option=WITH_JS && check_option # end confile cat << END >> $confile diff --git a/docs/PLAN.md b/docs/PLAN.md index 2f6450a7d..ce5743ed8 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,6 +12,7 @@ - redis client - async DNS - lua binding +- http js script handler - hrpc = libhv + protobuf ## Plan diff --git a/docs/cn/HttpJsHandler.md b/docs/cn/HttpJsHandler.md new file mode 100644 index 000000000..b7a7c8a8c --- /dev/null +++ b/docs/cn/HttpJsHandler.md @@ -0,0 +1,226 @@ +# Http JS Handler + +`HttpScriptHandler` 支持把 `.js` 脚本作为 HTTP 请求处理器执行。JS handler 基于 QuickJS,适合把少量业务逻辑从 C++ 编译周期里解耦出来,并且可以在脚本中使用 `async` / `await` 调用 libhv 的异步能力。 + +该功能是可选模块,默认不编译。 + +## 编译 + +需要 QuickJS 开发库。 + +Makefile: + +```bash +make libhv WITH_JS=yes WITH_HTTP=yes +make http_server_test WITH_JS=yes WITH_HTTP=yes +make unittest WITH_JS=yes WITH_HTTP=yes WITH_REDIS=yes WITH_MQTT=yes +``` + +如果 QuickJS 安装在自定义路径,可以显式指定: + +```bash +make libhv WITH_JS=yes \ + QUICKJS_ROOT=/opt/homebrew/opt/quickjs +``` + +或: + +```bash +make libhv WITH_JS=yes \ + JS_CFLAGS="-I/usr/local/include/quickjs" \ + JS_LIBS="-L/usr/local/lib/quickjs -lquickjs" +``` + +CMake: + +```bash +cmake -S . -B build -DWITH_JS=ON -DWITH_HTTP=ON -DWITH_HTTP_CLIENT=ON -DBUILD_UNITTEST=ON +cmake --build build +``` + +如果 CMake 没有自动找到 QuickJS: + +```bash +cmake -S . -B build -DWITH_JS=ON -DQUICKJS_ROOT=/opt/homebrew/opt/quickjs +``` + +## 基本用法 + +C++: + +```cpp +#include "HttpServer.h" +#include "HttpScriptHandler.h" + +using namespace hv; + +int main() { + HttpService router; + router.GET("/hello", HttpScriptHandler("scripts/hello.js")); + + HttpServer server; + server.port = 8080; + server.service = &router; + server.run(); + return 0; +} +``` + +JS: + +```js +async function get(ctx) { + const hv = require("hv"); + await hv.sleep(100); + return { + ok: true, + id: ctx.query("id", ""), + path: ctx.path() + }; +} +``` + +如果需要明确指定 JS 引擎,也可以直接使用 `HttpJsHandler("scripts/hello.js")`。推荐用户代码优先使用 `HttpScriptHandler`,这样同一个路由入口可以按脚本后缀分发到不同脚本引擎。 + +## 目录映射 + +`HttpService::Script(path, script_dir)` 可以把 URL 前缀映射到脚本目录,内部同样使用 `HttpScriptHandler`: + +```cpp +router.Script("/script/", "scripts"); +``` + +访问 `/script/user?id=42` 时会调用 `scripts/user.js`。访问 `/script/` 时会调用 `scripts/index.js`。如果同时启用了 Lua 和 JS,未带后缀的脚本路径会优先匹配 `.lua`,再匹配 `.js`。 + +目录映射默认支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`。路径中包含 `..` 路径段时返回 `403`。 + +## ctx API + +```js +ctx.method() // GET/POST/... +ctx.path() // URL path +ctx.param(name, defaultValue) +ctx.query(name, defaultValue) +ctx.header(name, defaultValue) +ctx.body() + +ctx.status(code) +ctx.setHeader(name, value) +ctx.set_header(name, value) +ctx.text(str) +ctx.json(value) +``` + +handler 可以直接调用 `ctx.text` / `ctx.json`,也可以返回字符串、数字状态码或 JS 对象: + +```js +function post(ctx) { + ctx.status(201); + ctx.setHeader("X-From", "js"); + return ctx.text("created"); +} +``` + +## 内置模块 + +JS handler 提供受控的内置模块,不兼容 Node.js,也不支持 npm 包加载。也就是说,首版不支持 `require("axios")`;请使用 libhv 提供的内置模块。 + +```js +const hv = require("hv"); +hv.version() // libhv 版本串 +hv.log("hello") // INFO 日志 +await hv.sleep(1000) +``` + +### hv/http + +需同时启用 `WITH_HTTP` 和 `WITH_HTTP_CLIENT`。 + +```js +const http = require("hv/http"); + +const resp = await http.get("http://127.0.0.1:8080/ping"); +// resp: { status, body, headers } + +await http.post("http://127.0.0.1:8080/echo", "body", { + "Content-Type": "text/plain" +}); + +await http.request("GET", "http://127.0.0.1:8080/ping"); +``` + +### hv/ws + +需同时启用 `WITH_HTTP` 和 `WITH_HTTP_CLIENT`。 + +```js +const wsmod = require("hv/ws"); + +const ws = await wsmod.connect("ws://127.0.0.1:8888/"); +ws.send("hello"); +const msg = await ws.recv(); +ws.close(); +``` + +`recv()` 在收到消息前保持 pending;连接关闭时会 reject。 + +### hv/redis + +需启用 `WITH_REDIS`。 + +```js +const redis = require("hv/redis"); + +const r = redis.new({ host: "127.0.0.1", port: 6379, timeout: 3000 }); +await r.set("k", "v"); +const v = await r.get("k"); +const n = await r.incr("c"); +const pong = await r.command(["PING"]); +``` + +Redis 回复映射:string -> string,integer -> number,nil -> null,array -> array,error reply -> rejected Promise。 + +### hv/mqtt + +需启用 `WITH_MQTT`。 + +```js +const mqtt = require("hv/mqtt"); + +const client = await mqtt.connect({ + host: "127.0.0.1", + port: 1883, + id: "client-1", + username: "", + password: "", + keepalive: 60, + clean_session: true, + ssl: false, + timeout: 3000, + reconnect: { + min_delay: 1000, + max_delay: 10000, + delay_policy: 2, + max_retry: 0 + } +}); + +client.subscribe("topic", 1); +client.publish("topic", "payload", 1, false); +const msg = await client.recv(); // { topic, payload, qos } +client.disconnect(); +``` + +## 异步模型 + +每次 HTTP 请求会创建独立 QuickJS runtime/context。脚本可以返回普通值,也可以返回 Promise;`HttpJsHandler` 会等待 Promise fulfilled/rejected 后再发送 HTTP 响应。`await hv.sleep()`、`await http.get()`、`await ws.recv()`、`await redis.command()`、`await mqtt.connect()` 都在当前 IO 线程的 event loop 上推进,不会阻塞 loop。 + +`HttpJsHandler` 会缓存脚本文本,并在 `reload_on_change=true` 时根据文件 `mtime` 自动重新读取;每个请求仍使用独立 QuickJS runtime/context,因此脚本里的全局变量不会跨请求共享。 + +## 示例 + +```bash +make http_server_test WITH_JS=yes WITH_HTTP=yes +bin/http_server_test 8080 +curl "http://127.0.0.1:8080/script/hello?id=42" +``` diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md index d77a32518..4c75ec419 100644 --- a/docs/cn/HttpLuaHandler.md +++ b/docs/cn/HttpLuaHandler.md @@ -1,6 +1,6 @@ # Http Lua Handler -`HttpScriptHandler` 允许 `HttpService` 调用脚本里的 `handle(ctx)` 方法处理 HTTP 请求。当前支持 `.lua` 脚本,适合把少量业务逻辑从 C++ 编译周期里解耦出来:修改脚本后无需重新编译服务,下一次请求会自动加载新脚本。 +`HttpScriptHandler` 允许 `HttpService` 调用脚本里的 `handle(ctx)` 方法处理 HTTP 请求。启用 `WITH_LUA` 时支持 `.lua` 脚本,启用 `WITH_JS` 时也支持 `.js` 脚本。它适合把少量业务逻辑从 C++ 编译周期里解耦出来:修改脚本后无需重新编译服务,下一次请求会自动加载新脚本。 该功能是可选模块,默认不编译。 @@ -76,7 +76,7 @@ end router.Script("/script/", "scripts"); ``` -访问 `/script/user?id=42` 时会调用 `scripts/user.lua`。访问 `/script/` 时会调用 `scripts/index.lua`。当前目录映射只自动补 `.lua` 后缀。 +访问 `/script/user?id=42` 时会调用 `scripts/user.lua`。访问 `/script/` 时会调用 `scripts/index.lua`。如果同时启用了 Lua 和 JS,未带后缀的脚本路径会优先匹配 `.lua`,再匹配 `.js`。 目录映射默认支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`。路径中包含 `..` 路径段时返回 `403`。 @@ -168,7 +168,7 @@ end ## 热更新 -`HttpScriptHandler` 当前会把 `.lua` 文件转给 `HttpLuaHandler`。`HttpLuaHandler` 会记录脚本文件的 `mtime`。每次请求前,如果文件被修改,会重新加载脚本。 +`HttpScriptHandler` 当前会把 `.lua` 文件转给 `HttpLuaHandler`,把 `.js` 文件转给 `HttpJsHandler`。`HttpLuaHandler` 会记录脚本文件的 `mtime`。每次请求前,如果文件被修改,会重新加载脚本。 重新加载失败时: diff --git a/docs/cn/README.md b/docs/cn/README.md index e6d625d41..10e3de9ca 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -9,6 +9,10 @@ - [Lua Binding: hv.* Lua 绑定](lua.md) +## js接口 + +- [Http JS Handler: HTTP JS脚本处理器](HttpJsHandler.md) + ## c++接口 - [class EventLoop: 事件循环类](EventLoop.md) diff --git a/examples/http_server_test.cpp b/examples/http_server_test.cpp index b6854008d..402afe77e 100644 --- a/examples/http_server_test.cpp +++ b/examples/http_server_test.cpp @@ -5,10 +5,10 @@ */ #include "HttpServer.h" -#include "hthread.h" // import hv_gettid -#include "hasync.h" // import hv::async +#include "hthread.h" // import hv_gettid +#include "hasync.h" // import hv::async -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) #include "HttpScriptHandler.h" #endif @@ -55,9 +55,7 @@ int main(int argc, char** argv) { /* API handlers */ // curl -v http://ip:port/ping - router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { - return resp->String("pong"); - }); + router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { return resp->String("pong"); }); // curl -v http://ip:port/data router.GET("/data", [](HttpRequest* req, HttpResponse* resp) { @@ -66,9 +64,7 @@ int main(int argc, char** argv) { }); // curl -v http://ip:port/paths - router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) { - return resp->Json(router.Paths()); - }); + router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) { return resp->Json(router.Paths()); }); // curl -v http://ip:port/get?env=1 router.GET("/get", [](const HttpContextPtr& ctx) { @@ -81,9 +77,7 @@ int main(int argc, char** argv) { }); // curl -v http://ip:port/echo -d "hello,world!" - router.POST("/echo", [](const HttpContextPtr& ctx) { - return ctx->send(ctx->body(), ctx->type()); - }); + router.POST("/echo", [](const HttpContextPtr& ctx) { return ctx->send(ctx->body(), ctx->type()); }); // curl -v http://ip:port/user/123 router.GET("/user/{id}", [](const HttpContextPtr& ctx) { @@ -95,8 +89,14 @@ int main(int argc, char** argv) { #ifdef WITH_LUA // curl -v "http://ip:port/lua/hello?id=42" router.GET("/lua/hello", HttpScriptHandler("examples/scripts/hello.lua")); +#endif +#ifdef WITH_JS + // curl -v "http://ip:port/js/hello?id=42" + router.GET("/js/hello", HttpScriptHandler("examples/scripts/hello.js")); +#endif +#if defined(WITH_LUA) || defined(WITH_JS) // curl -v "http://ip:port/script/hello?id=42" - // curl -v "http://ip:port/script/async?host=example.com" (coroutine sync-style async) + // curl -v "http://ip:port/script/async?host=example.com" (sync-style async) router.Script("/script/", "examples/scripts"); #endif @@ -111,9 +111,7 @@ int main(int argc, char** argv) { // curl -v http://ip:port/close // Test HTTP_STATUS_CLOSE: closes connection without sending any response - router.GET("/close", [](HttpRequest* req, HttpResponse* resp) { - return HTTP_STATUS_CLOSE; - }); + router.GET("/close", [](HttpRequest* req, HttpResponse* resp) { return HTTP_STATUS_CLOSE; }); // middleware router.AllowCORS(); @@ -146,7 +144,8 @@ int main(int argc, char** argv) { server.start(); // press Enter to stop - while (getchar() != '\n'); + while (getchar() != '\n') + ; hv::async::cleanup(); return 0; } diff --git a/examples/scripts/hello.js b/examples/scripts/hello.js new file mode 100644 index 000000000..fd6799282 --- /dev/null +++ b/examples/scripts/hello.js @@ -0,0 +1,26 @@ +function get(ctx) { + const hv = require("hv"); + hv.log("js get", ctx.path()); + return { + ok: true, + method: "GET", + path: ctx.path(), + id: ctx.query("id", "") + }; +} + +function post(ctx) { + const hv = require("hv"); + hv.log("js post", ctx.path()); + return ctx.text("POST " + ctx.body()); +} + +function handle(ctx) { + const hv = require("hv"); + hv.log("js fallback", ctx.method(), ctx.path()); + return { + ok: true, + method: ctx.method(), + path: ctx.path() + }; +} diff --git a/hconfig.h.in b/hconfig.h.in index 2bef47660..37af2e788 100644 --- a/hconfig.h.in +++ b/hconfig.h.in @@ -101,5 +101,6 @@ #cmakedefine WITH_IO_URING 1 #cmakedefine WITH_LUA 1 +#cmakedefine WITH_JS 1 #endif // HV_CONFIG_H_ diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp new file mode 100644 index 000000000..7a16dc1e0 --- /dev/null +++ b/http/server/HttpJsHandler.cpp @@ -0,0 +1,1812 @@ +#ifdef WITH_JS + +#include "HttpJsHandler.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "EventLoop.h" +#include "hfile.h" +#include "hlog.h" +#include "hpath.h" +#include "hstring.h" +#include "htime.h" +#include "hversion.h" +#ifdef HVJS_WITH_HTTP +#include "AsyncHttpClient.h" +#include "WebSocketClient.h" +#endif +#ifdef HVJS_WITH_REDIS +#include "AsyncRedisClient.h" +#endif +#ifdef HVJS_WITH_MQTT +#include "mqtt_client.h" +#endif + +namespace hv { + +namespace { + +struct JsHttpTask; + +static const int JS_HTTP_METHOD_REQUEST = -1; + +struct JsHttpTask { + JSRuntime* rt; + JSContext* js; + hloop_t* loop; + EventLoopPtr loop_ptr; + HttpContextPtr ctx; + JSValue promise; + bool async; + bool finished; + bool in_call; + bool closing; + int refcount; + std::string error; + + JsHttpTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), async(false), finished(false), in_call(false), closing(false), refcount(1) {} +}; + +struct JsPromiseOp { + JsHttpTask* task; + JSValue resolve; + JSValue reject; + bool completed; + bool defer_delete; + + JsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} + + virtual ~JsPromiseOp() {} +}; + +struct JsSleep : public JsPromiseOp { + htimer_t* timer; + TimerID timer_id; + + JsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} +}; + +struct JsImmediatePromise : public JsPromiseOp {}; + +static std::mutex& js_class_id_mutex() { + static std::mutex mutex; + return mutex; +} + +static void js_new_class_id(JSClassID* class_id) { + std::lock_guard lock(js_class_id_mutex()); + JS_NewClassID(class_id); +} + +static void task_ref(JsHttpTask* task) { + ++task->refcount; +} + +static void task_unref(JsHttpTask* task) { + if (--task->refcount != 0) return; + task->closing = true; + if (!JS_IsUndefined(task->promise)) { + JS_FreeValue(task->js, task->promise); + task->promise = JS_UNDEFINED; + } + if (task->js) { + if (task->rt) { + JS_RunGC(task->rt); + } + JS_FreeContext(task->js); + task->js = NULL; + } + if (task->rt) { + JS_RunGC(task->rt); + JS_FreeRuntime(task->rt); + task->rt = NULL; + } + delete task; +} + +static void drain_jobs(JsHttpTask* task); +static std::string js_to_string(JSContext* ctx, JSValueConst value); +static std::string js_exception_string(JSContext* ctx); +static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok); + +static void drain_event_cb(hevent_t* ev) { + JsHttpTask* task = (JsHttpTask*)hevent_userdata(ev); + drain_jobs(task); + task_unref(task); +} + +static void schedule_drain(JsHttpTask* task) { + if (task == NULL || task->closing) return; + task_ref(task); + if (task->loop_ptr) { + task->loop_ptr->queueInLoop([task]() { + drain_jobs(task); + task_unref(task); + }); + } + else if (task->loop) { + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = drain_event_cb; + ev.userdata = task; + hloop_post_event(task->loop, &ev); + } + else { + task_unref(task); + } +} + +template static JSValue js_new_promise(JSContext* js, JsHttpTask* task, T** out) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + T* op = new T(); + op->task = task; + op->resolve = funcs[0]; + op->reject = funcs[1]; + task_ref(task); + *out = op; + return promise; +} + +static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok) { + JsHttpTask* task = op->task; + if (op->completed) { + JS_FreeValue(task->js, value); + return; + } + op->completed = true; + if (!task->closing) { + JSValue func = ok ? op->resolve : op->reject; + JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); + if (JS_IsException(ret) && task->error.empty()) { + task->error = js_exception_string(task->js); + } + JS_FreeValue(task->js, ret); + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + if (task->in_call) { + op->defer_delete = true; + schedule_drain(task); + return; + } + schedule_drain(task); + } + else { + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + } + delete op; + task_unref(task); +} + +static void js_promise_resolve(JsPromiseOp* op, JSValue value) { + js_promise_complete(op, value, true); +} + +static void js_promise_reject(JsPromiseOp* op, const char* message) { + js_promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); +} + +static JSValue js_rejected_promise(JSContext* js, const char* message) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + JSValue reason = JS_NewString(js, message ? message : "error"); + JSValue ret = JS_Call(js, funcs[1], JS_UNDEFINED, 1, &reason); + JS_FreeValue(js, ret); + JS_FreeValue(js, reason); + JS_FreeValue(js, funcs[0]); + JS_FreeValue(js, funcs[1]); + return promise; +} + +static JSValue js_async_resolved_promise(JSContext* js, JsHttpTask* task, JSValue value) { + if (task == NULL) { + JS_FreeValue(js, value); + return JS_ThrowInternalError(js, "invalid HttpJsHandler task"); + } + JsImmediatePromise* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) { + JS_FreeValue(js, value); + return promise; + } + js_promise_resolve(op, value); + return promise; +} + +static void js_finish_deferred_op(JsPromiseOp* op) { + if (op == NULL || !op->completed || !op->defer_delete) return; + JsHttpTask* task = op->task; + delete op; + task_unref(task); +} + +static std::string js_to_string(JSContext* ctx, JSValueConst value) { + size_t len = 0; + const char* str = JS_ToCStringLen(ctx, &len, value); + if (str == NULL) return std::string(); + std::string out(str, len); + JS_FreeCString(ctx, str); + return out; +} + +static bool js_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out) { + *out = JS_UNDEFINED; + if (!JS_IsObject(obj)) return false; + *out = JS_GetPropertyStr(js, obj, name); + return !JS_IsUndefined(*out) && !JS_IsException(*out); +} + +static std::string js_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue = "") { + JSValue value; + if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + std::string out = js_to_string(js, value); + JS_FreeValue(js, value); + return out; +} + +static int js_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue = 0) { + JSValue value; + if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + int32_t out = defvalue; + JS_ToInt32(js, &out, value); + JS_FreeValue(js, value); + return out; +} + +static bool js_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue = false) { + JSValue value; + if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + bool out = JS_ToBool(js, value) != 0; + JS_FreeValue(js, value); + return out; +} + +static std::string js_exception_string(JSContext* ctx) { + JSValue exception = JS_GetException(ctx); + std::string msg = js_to_string(ctx, exception); + JS_FreeValue(ctx, exception); + return msg.empty() ? "javascript exception" : msg; +} + +static JsHttpTask* js_get_task(JSContext* js) { + return (JsHttpTask*)JS_GetContextOpaque(js); +} + +static JSValue js_ctx_method(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || !task->ctx->request) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + return JS_NewString(js, http_method_str(task->ctx->request->method)); +} + +static JSValue js_ctx_path(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string path = task->ctx->path(); + return JS_NewStringLen(js, path.data(), path.size()); +} + +static JSValue js_ctx_query(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string value = task->ctx->param(key.c_str(), defvalue); + return JS_NewStringLen(js, value.data(), value.size()); +} + +static JSValue js_ctx_header(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string value = task->ctx->header(key.c_str(), defvalue); + return JS_NewStringLen(js, value.data(), value.size()); +} + +static JSValue js_ctx_body(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string& body = task->ctx->body(); + return JS_NewStringLen(js, body.data(), body.size()); +} + +static JSValue js_ctx_status(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 1) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + int32_t status = 0; + if (JS_ToInt32(js, &status, argv[0]) != 0) return JS_EXCEPTION; + task->ctx->response->status_code = (http_status)status; + return JS_NewInt32(js, status); +} + +static JSValue js_ctx_set_header(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 2) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string key = js_to_string(js, argv[0]); + std::string value = js_to_string(js, argv[1]); + task->ctx->setHeader(key.c_str(), value); + return JS_UNDEFINED; +} + +static JSValue js_ctx_text(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 1) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string text = js_to_string(js, argv[0]); + task->ctx->response->String(text); + return JS_NewInt32(js, task->ctx->response->status_code); +} + +static JSValue js_ctx_json(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 1) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + JSValue json = JS_JSONStringify(js, argv[0], JS_UNDEFINED, JS_UNDEFINED); + if (JS_IsException(json)) return json; + std::string body = js_to_string(js, json); + JS_FreeValue(js, json); + task->ctx->response->SetContentType(APPLICATION_JSON); + task->ctx->response->body = body; + return JS_NewInt32(js, task->ctx->response->status_code); +} + +static JSValue js_new_ctx(JSContext* js, const HttpContextPtr& ctx) { + (void)ctx; + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "method", JS_NewCFunction(js, js_ctx_method, "method", 0)); + JS_SetPropertyStr(js, obj, "path", JS_NewCFunction(js, js_ctx_path, "path", 0)); + JS_SetPropertyStr(js, obj, "param", JS_NewCFunction(js, js_ctx_query, "param", 1)); + JS_SetPropertyStr(js, obj, "query", JS_NewCFunction(js, js_ctx_query, "query", 1)); + JS_SetPropertyStr(js, obj, "header", JS_NewCFunction(js, js_ctx_header, "header", 1)); + JS_SetPropertyStr(js, obj, "body", JS_NewCFunction(js, js_ctx_body, "body", 0)); + JS_SetPropertyStr(js, obj, "status", JS_NewCFunction(js, js_ctx_status, "status", 1)); + JS_SetPropertyStr(js, obj, "setHeader", JS_NewCFunction(js, js_ctx_set_header, "setHeader", 2)); + JS_SetPropertyStr(js, obj, "set_header", JS_NewCFunction(js, js_ctx_set_header, "set_header", 2)); + JS_SetPropertyStr(js, obj, "text", JS_NewCFunction(js, js_ctx_text, "text", 1)); + JS_SetPropertyStr(js, obj, "json", JS_NewCFunction(js, js_ctx_json, "json", 1)); + return obj; +} + +static void task_finish(JsHttpTask* task, JSValue result); + +static void drain_jobs(JsHttpTask* task) { + JSContext* job_ctx = NULL; + while (JS_IsJobPending(task->rt)) { + int rc = JS_ExecutePendingJob(task->rt, &job_ctx); + if (rc < 0) { + task->error = js_exception_string(job_ctx ? job_ctx : task->js); + break; + } + } + if (!task->finished && !JS_IsUndefined(task->promise)) { + JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); + if (state != JS_PROMISE_PENDING) { + JSValue value = JS_PromiseResult(task->js, task->promise); + task_finish(task, value); + return; + } + } + if (!task->error.empty()) { + task_finish(task, JS_UNDEFINED); + } +} + +static void sleep_timer_cb(htimer_t* timer) { + JsSleep* sleep = (JsSleep*)hevent_userdata(timer); + js_promise_resolve(sleep, JS_UNDEFINED); +} + +static JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = (JsHttpTask*)JS_GetContextOpaque(js); + if (task == NULL || argc < 1) return JS_EXCEPTION; + int32_t ms = 0; + if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + + JsSleep* sleep = new JsSleep(); + sleep->task = task; + sleep->resolve = funcs[0]; + JS_FreeValue(js, funcs[1]); + task_ref(task); + if (task->loop_ptr) { + sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { js_promise_resolve(sleep, JS_UNDEFINED); }); + } + else { + sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); + if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); + } + if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { + task_unref(task); + JS_FreeValue(js, sleep->resolve); + delete sleep; + JS_FreeValue(js, promise); + return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); + } + return promise; +} + +static JSValue js_hv_version(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + return JS_NewString(js, HV_VERSION_STRING); +} + +static JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + std::string line; + for (int i = 0; i < argc; ++i) { + if (i != 0) line += "\\t"; + line += js_to_string(js, argv[i]); + } + hlogi("%s", line.c_str()); + return JS_UNDEFINED; +} +#ifdef HVJS_WITH_HTTP +struct JsHttpRequest : public JsPromiseOp { + std::shared_ptr client; +}; + +static JSValue js_push_headers(JSContext* js, const http_headers& headers) { + JSValue obj = JS_NewObject(js); + for (auto& kv : headers) { + JS_SetPropertyStr(js, obj, kv.first.c_str(), JS_NewStringLen(js, kv.second.data(), kv.second.size())); + } + return obj; +} + +static JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "status", JS_NewInt32(js, resp ? resp->status_code : 0)); + if (resp) { + JS_SetPropertyStr(js, obj, "body", JS_NewStringLen(js, resp->body.data(), resp->body.size())); + JS_SetPropertyStr(js, obj, "headers", js_push_headers(js, resp->headers)); + } + else { + JS_SetPropertyStr(js, obj, "body", JS_NewString(js, "")); + JS_SetPropertyStr(js, obj, "headers", JS_NewObject(js)); + } + return obj; +} + +static int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { + if (argc <= url_index) { + JS_ThrowTypeError(js, "missing url"); + return -1; + } + std::string url = js_to_string(js, argv[url_index]); + auto req = std::make_shared(); + req->method = method; + req->url = url; + if (argc > url_index + 1 && !JS_IsUndefined(argv[url_index + 1]) && !JS_IsNull(argv[url_index + 1])) { + std::string body = js_to_string(js, argv[url_index + 1]); + req->body = body; + } + if (argc > url_index + 2 && JS_IsObject(argv[url_index + 2])) { + JSPropertyEnum* tab = NULL; + uint32_t len = 0; + if (JS_GetOwnPropertyNames(js, &tab, &len, argv[url_index + 2], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { + for (uint32_t i = 0; i < len; ++i) { + JSValue key = JS_AtomToString(js, tab[i].atom); + JSValue value = JS_GetProperty(js, argv[url_index + 2], tab[i].atom); + std::string k = js_to_string(js, key); + std::string v = js_to_string(js, value); + if (!k.empty()) req->headers[k] = v; + JS_FreeValue(js, value); + JS_FreeValue(js, key); + } + JS_FreePropertyEnum(js, tab, len); + } + } + *out = req; + return 0; +} + +static JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->loop_ptr) { + return js_rejected_promise(js, "hv.http: no shared event loop on this thread"); + } + http_method method = (http_method)magic; + int url_index = 0; + if (magic == JS_HTTP_METHOD_REQUEST) { + if (argc < 2) return js_rejected_promise(js, "hv.http: request needs method and url"); + std::string m = js_to_string(js, argv[0]); + toupper(m); + method = http_method_enum(m.c_str()); + url_index = 1; + } + if (method == HTTP_CUSTOM_METHOD) { + return js_rejected_promise(js, "hv.http: unsupported method"); + } + + HttpRequestPtr req; + if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { + return JS_EXCEPTION; + } + + JsHttpRequest* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->client = std::make_shared(task->loop_ptr); + std::shared_ptr client = op->client; + task->in_call = true; + int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { + if (op->task->loop_ptr) { + op->task->loop_ptr->queueInLoop([client]() {}); + } + JSContext* js = op->task->js; + if (resp) { + js_promise_resolve(op, js_push_http_response(js, resp)); + } + else { + js_promise_reject(op, "hv.http: request failed"); + } + }); + if (ret != 0) { + js_promise_reject(op, "hv.http: request failed"); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_require_http(JSContext* js) { + JSValue http = JS_NewObject(js); + JS_SetPropertyStr(js, http, "request", JS_NewCFunctionMagic(js, js_http_request, "request", 2, JS_CFUNC_generic_magic, JS_HTTP_METHOD_REQUEST)); + JS_SetPropertyStr(js, http, "get", JS_NewCFunctionMagic(js, js_http_request, "get", 1, JS_CFUNC_generic_magic, HTTP_GET)); + JS_SetPropertyStr(js, http, "post", JS_NewCFunctionMagic(js, js_http_request, "post", 2, JS_CFUNC_generic_magic, HTTP_POST)); + JS_SetPropertyStr(js, http, "put", JS_NewCFunctionMagic(js, js_http_request, "put", 2, JS_CFUNC_generic_magic, HTTP_PUT)); + JS_SetPropertyStr(js, http, "delete", JS_NewCFunctionMagic(js, js_http_request, "delete", 1, JS_CFUNC_generic_magic, HTTP_DELETE)); + return http; +} +#endif +#ifdef HVJS_WITH_REDIS +static JSClassID s_redis_class_id; +static std::once_flag s_redis_class_once; + +struct JsRedisState { + std::shared_ptr client; + bool destroyed; + + JsRedisState() : destroyed(false) {} + + ~JsRedisState() { + destroyed = true; + if (client) { + client->stop(true); + client.reset(); + } + } +}; + +struct JsRedisClient { + std::shared_ptr state; +}; + +struct JsRedisCommand : public JsPromiseOp { + std::shared_ptr redis; +}; + +static void js_redis_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + JsRedisClient* box = (JsRedisClient*)JS_GetOpaque(val, s_redis_class_id); + if (box) { + delete box; + } +} + +static JsRedisClient* js_redis_client(JSContext* js, JSValueConst this_val) { + JsRedisClient* box = (JsRedisClient*)JS_GetOpaque2(js, this_val, s_redis_class_id); + return box; +} + +static void js_redis_register_class(JSContext* js) { + std::call_once(s_redis_class_once, []() { js_new_class_id(&s_redis_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_redis_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.redis.client"; + def.finalizer = js_redis_finalizer; + JS_NewClass(rt, s_redis_class_id, &def); + } +} + +static JSValue js_push_redis_reply(JSContext* js, const RedisReply& reply) { + switch (reply.type) { + case REDIS_REPLY_STRING: return JS_NewStringLen(js, reply.str.data(), reply.str.size()); + case REDIS_REPLY_INTEGER: return JS_NewInt64(js, reply.integer); + case REDIS_REPLY_ARRAY: { + if (reply.null_array) return JS_NULL; + JSValue arr = JS_NewArray(js); + for (uint32_t i = 0; i < reply.elements.size(); ++i) { + JSValue item = reply.elements[i].isNil() ? JS_NULL : js_push_redis_reply(js, reply.elements[i]); + JS_SetPropertyUint32(js, arr, i, item); + } + return arr; + } + case REDIS_REPLY_NIL: + default: return JS_NULL; + } +} + +static void js_redis_resolve_result(JsRedisCommand* op, const RedisResult& result) { + JSContext* js = op->task->js; + if (!op->redis || op->redis->destroyed) { + js_promise_reject(op, "hv.redis: client closed"); + return; + } + if (result.code != 0) { + char err[64]; + snprintf(err, sizeof(err), "hv.redis: request failed (%d)", result.code); + js_promise_reject(op, err); + return; + } + if (result.reply.isError()) { + js_promise_reject(op, result.reply.error().c_str()); + return; + } + js_promise_resolve(op, js_push_redis_reply(js, result.reply)); +} + +static bool js_build_redis_command(JSContext* js, JSValueConst* argv, int argc, int first, RedisCommand* cmd) { + if (argc <= first) return false; + if (JS_IsArray(js, argv[first]) && argc == first + 1) { + JSValue lenv = JS_GetPropertyStr(js, argv[first], "length"); + uint32_t len = 0; + JS_ToUint32(js, &len, lenv); + JS_FreeValue(js, lenv); + for (uint32_t i = 0; i < len; ++i) { + JSValue item = JS_GetPropertyUint32(js, argv[first], i); + cmd->push_back(js_to_string(js, item)); + JS_FreeValue(js, item); + } + } + else { + for (int i = first; i < argc; ++i) { + cmd->push_back(js_to_string(js, argv[i])); + } + } + return !cmd->empty(); +} + +static const char* js_redis_verb_name(int magic) { + switch (magic) { + case 1: return "GET"; + case 2: return "SET"; + case 3: return "DEL"; + case 4: return "INCR"; + case 5: return "DECR"; + case 6: return "EXPIRE"; + case 7: return "EXISTS"; + default: return NULL; + } +} + +static JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + JsRedisClient* box = js_redis_client(js, this_val); + JsRedisState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || state->destroyed) { + return js_rejected_promise(js, "hv.redis: client closed"); + } + RedisCommand cmd; + if (magic != 0) { + const char* verb = js_redis_verb_name(magic); + if (verb == NULL) { + return js_rejected_promise(js, "hv.redis: unknown command"); + } + cmd.push_back(verb); + for (int i = 0; i < argc; ++i) { + cmd.push_back(js_to_string(js, argv[i])); + } + } + else if (!js_build_redis_command(js, argv, argc, 0, &cmd)) { + return js_rejected_promise(js, "hv.redis: empty or invalid command"); + } + + JsHttpTask* task = js_get_task(js); + JsRedisCommand* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->redis = box->state; + task->in_call = true; + int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); + if (ret != 0) { + js_promise_reject(op, "hv.redis: request failed"); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_redis_new(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->loop_ptr) { + return JS_ThrowTypeError(js, "hv.redis: no shared event loop on this thread"); + } + js_redis_register_class(js); + + std::string host = "127.0.0.1"; + int port = 6379; + std::string auth; + int db = 0; + int timeout = 0; + if (argc > 0 && JS_IsObject(argv[0])) { + host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); + port = js_get_int_property(js, argv[0], "port", 6379); + auth = js_get_string_property(js, argv[0], "auth", ""); + db = js_get_int_property(js, argv[0], "db", 0); + timeout = js_get_int_property(js, argv[0], "timeout", 0); + } + + JSValue obj = JS_NewObjectClass(js, s_redis_class_id); + if (JS_IsException(obj)) return obj; + JsRedisClient* box = new JsRedisClient(); + box->state = std::make_shared(); + box->state->client = std::make_shared(task->loop_ptr); + box->state->client->setHost(host); + box->state->client->setPort(port); + if (!auth.empty()) box->state->client->setAuth(auth); + if (db > 0) box->state->client->setDb(db); + if (timeout > 0) box->state->client->setTimeout(timeout); + box->state->client->start(false); + JS_SetOpaque(obj, box); + + JS_SetPropertyStr(js, obj, "command", JS_NewCFunctionMagic(js, js_redis_command, "command", 1, JS_CFUNC_generic_magic, 0)); + static const char* verbs[] = {"GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL}; + for (int i = 0; verbs[i]; ++i) { + std::string name = verbs[i]; + for (char& c : name) c = (char)::tolower((unsigned char)c); + JS_SetPropertyStr(js, obj, name.c_str(), JS_NewCFunctionMagic(js, js_redis_command, name.c_str(), 1, JS_CFUNC_generic_magic, i + 1)); + } + return obj; +} + +static JSValue js_require_redis(JSContext* js) { + JSValue redis = JS_NewObject(js); + JS_SetPropertyStr(js, redis, "new", JS_NewCFunction(js, js_redis_new, "new", 1)); + return redis; +} +#endif +#ifdef HVJS_WITH_HTTP +static JSClassID s_ws_class_id; +static std::once_flag s_ws_class_once; + +struct JsWsState { + std::shared_ptr client; + std::deque inbox; + JsPromiseOp* connect_op; + JsPromiseOp* recv_op; + bool js_alive; + bool connected; + bool closed; + + JsWsState() : connect_op(NULL), recv_op(NULL), js_alive(false), connected(false), closed(false) {} + + void detach() { + closed = true; + connected = false; + if (client) { + client->onopen = NULL; + client->onmessage = NULL; + client->onclose = NULL; + client->close(); + client.reset(); + } + } + + ~JsWsState() { detach(); } +}; + +struct JsWsClient { + std::shared_ptr state; +}; + +struct JsWsConnect : public JsPromiseOp { + std::shared_ptr state; +}; + +struct JsWsRecv : public JsPromiseOp { + std::shared_ptr state; +}; + +static JsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { + return (JsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); +} + +static void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { + if (!state) return; + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else { + state->detach(); + } +} + +static void js_ws_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + JsWsClient* box = (JsWsClient*)JS_GetOpaque(val, s_ws_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +static void js_ws_register_class(JSContext* js) { + std::call_once(s_ws_class_once, []() { js_new_class_id(&s_ws_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_ws_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.ws.client"; + def.finalizer = js_ws_finalizer; + JS_NewClass(rt, s_ws_class_id, &def); + } +} + +static void js_ws_try_deliver(const std::shared_ptr& state) { + if (!state || state->recv_op == NULL) return; + JsWsRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + js_promise_resolve(op, JS_NewStringLen(op->task->js, msg.data(), msg.size())); + } + else if (state->closed) { + state->recv_op = NULL; + js_promise_reject(op, "closed"); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_ws_detach_after_callback(loop, hold); + } +} + +static JSValue js_ws_send(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsWsClient* box = js_ws_client(js, this_val); + JsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || !state->connected) { + return JS_ThrowTypeError(js, "hv.ws: closed"); + } + std::string msg = argc > 0 ? js_to_string(js, argv[0]) : std::string(); + enum ws_opcode opcode = WS_OPCODE_TEXT; + if (argc > 1 && js_to_string(js, argv[1]) == "binary") { + opcode = WS_OPCODE_BINARY; + } + int ret = state->client->send(msg.data(), (int)msg.size(), opcode); + if (ret < 0) { + return JS_ThrowInternalError(js, "hv.ws: send failed"); + } + return JS_NewInt32(js, ret); +} + +static JSValue js_ws_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsWsClient* box = js_ws_client(js, this_val); + JsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client) { + return js_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return js_async_resolved_promise(js, js_get_task(js), JS_NewStringLen(js, msg.data(), msg.size())); + } + if (state->closed || !state->connected) { + return js_rejected_promise(js, "closed"); + } + if (state->recv_op != NULL) { + return js_rejected_promise(js, "hv.ws: recv already pending"); + } + JsHttpTask* task = js_get_task(js); + JsWsRecv* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = box->state; + state->recv_op = op; + return promise; +} + +static JSValue js_ws_close(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsWsClient* box = js_ws_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + if (state->connect_op) { + JsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + js_promise_reject(op, "closed"); + } + if (state->recv_op) { + JsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + js_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +static JSValue js_ws_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_ws_class_id); + if (JS_IsException(obj)) return obj; + JsWsClient* box = new JsWsClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "send", JS_NewCFunction(js, js_ws_send, "send", 1)); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_ws_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "close", JS_NewCFunction(js, js_ws_close, "close", 0)); + return obj; +} + +static JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->loop_ptr) { + return js_rejected_promise(js, "hv.ws: no shared event loop on this thread"); + } + if (argc < 1) { + return js_rejected_promise(js, "hv.ws: connect needs url"); + } + std::string url = js_to_string(js, argv[0]); + js_ws_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = std::make_shared(task->loop_ptr); + + JsWsConnect* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + state->connect_op = op; + op->state = state; + state->client->onopen = [state]() { + state->connected = true; + state->closed = false; + if (state->connect_op) { + JsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + JSValue obj = js_ws_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + js_promise_reject(op, "hv.ws: create client failed"); + js_ws_detach_after_callback(loop, hold); + } + else { + js_promise_resolve(op, obj); + } + } + }; + state->client->onmessage = [state](const std::string& msg) { + state->inbox.push_back(msg); + js_ws_try_deliver(state); + }; + state->client->onclose = [state]() { + state->connected = false; + state->closed = true; + if (state->connect_op) { + JsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + js_promise_reject(op, "closed"); + js_ws_detach_after_callback(loop, hold); + } + js_ws_try_deliver(state); + }; + task->in_call = true; + int ret = state->client->open(url.c_str()); + if (ret != 0) { + state->connect_op = NULL; + js_promise_reject(op, "hv.ws: open failed"); + state->detach(); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_require_ws(JSContext* js) { + JSValue ws = JS_NewObject(js); + JS_SetPropertyStr(js, ws, "connect", JS_NewCFunction(js, js_ws_connect, "connect", 1)); + return ws; +} +#endif +#ifdef HVJS_WITH_MQTT +static JSClassID s_mqtt_class_id; +static std::once_flag s_mqtt_class_once; + +struct JsMqttMessage { + std::string topic; + std::string payload; + int qos; +}; + +struct JsMqttState { + mqtt_client_t* client; + std::deque inbox; + JsPromiseOp* connect_op; + JsPromiseOp* recv_op; + bool js_alive; + bool closed; + bool reconnect; + + JsMqttState() : client(NULL), connect_op(NULL), recv_op(NULL), js_alive(false), closed(false), reconnect(false) {} + + void detach() { + closed = true; + if (client) { + mqtt_client_set_callback(client, NULL); + mqtt_client_set_userdata(client, NULL); + mqtt_client_free(client); + client = NULL; + } + } + + ~JsMqttState() { detach(); } +}; + +struct JsMqttClient { + std::shared_ptr state; +}; + +struct JsMqttConnect : public JsPromiseOp { + std::shared_ptr state; +}; + +struct JsMqttRecv : public JsPromiseOp { + std::shared_ptr state; +}; + +struct JsMqttDetachEvent { + std::shared_ptr state; +}; + +static JsMqttClient* js_mqtt_client(JSContext* js, JSValueConst this_val) { + return (JsMqttClient*)JS_GetOpaque2(js, this_val, s_mqtt_class_id); +} + +static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); + +static void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + JsMqttClient* box = (JsMqttClient*)JS_GetOpaque(val, s_mqtt_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +static void js_mqtt_register_class(JSContext* js) { + std::call_once(s_mqtt_class_once, []() { js_new_class_id(&s_mqtt_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_mqtt_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.mqtt.client"; + def.finalizer = js_mqtt_finalizer; + JS_NewClass(rt, s_mqtt_class_id, &def); + } +} + +static JSValue js_mqtt_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_mqtt_class_id); + if (JS_IsException(obj)) return obj; + JsMqttClient* box = new JsMqttClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_mqtt_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "publish", JS_NewCFunction(js, js_mqtt_publish, "publish", 2)); + JS_SetPropertyStr(js, obj, "subscribe", JS_NewCFunction(js, js_mqtt_subscribe, "subscribe", 1)); + JS_SetPropertyStr(js, obj, "unsubscribe", JS_NewCFunction(js, js_mqtt_unsubscribe, "unsubscribe", 1)); + JS_SetPropertyStr(js, obj, "disconnect", JS_NewCFunction(js, js_mqtt_disconnect, "disconnect", 0)); + return obj; +} + +static JSValue js_push_mqtt_message(JSContext* js, const JsMqttMessage& msg) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "topic", JS_NewStringLen(js, msg.topic.data(), msg.topic.size())); + JS_SetPropertyStr(js, obj, "payload", JS_NewStringLen(js, msg.payload.data(), msg.payload.size())); + JS_SetPropertyStr(js, obj, "qos", JS_NewInt32(js, msg.qos)); + return obj; +} + +static const char* js_mqtt_closed_reason(const JsMqttState* state) { + return state && state->reconnect ? "reconnecting" : "closed"; +} + +static void js_mqtt_try_deliver(JsMqttState* state) { + if (!state || state->recv_op == NULL) return; + JsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + if (!state->inbox.empty()) { + JsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + js_promise_resolve(op, js_push_mqtt_message(op->task->js, msg)); + } + else if (state->closed) { + state->recv_op = NULL; + js_promise_reject(op, js_mqtt_closed_reason(state)); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } +} + +static void js_mqtt_detach_event_cb(hevent_t* ev) { + JsMqttDetachEvent* detach = (JsMqttDetachEvent*)hevent_userdata(ev); + if (detach) { + detach->state->detach(); + delete detach; + } +} + +static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { + if (!state) return; + state->reconnect = false; + if (state->client) { + mqtt_client_set_reconnect(state->client, NULL); + } + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else if (raw_loop) { + JsMqttDetachEvent* detach = new JsMqttDetachEvent(); + detach->state = state; + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = js_mqtt_detach_event_cb; + ev.userdata = detach; + hloop_post_event(raw_loop, &ev); + } + else { + state->detach(); + } +} + +static void js_mqtt_on_event(mqtt_client_t* client, int type) { + JsMqttState* state = (JsMqttState*)mqtt_client_get_userdata(client); + if (state == NULL) return; + switch (type) { + case MQTT_TYPE_CONNACK: + state->closed = false; + if (state->connect_op) { + JsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + JSValue obj = js_mqtt_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + js_promise_reject(op, "hv.mqtt: create client failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + else { + js_promise_resolve(op, obj); + } + } + break; + case MQTT_TYPE_PUBLISH: { + JsMqttMessage msg; + if (client->message.topic && client->message.topic_len > 0) { + msg.topic.assign(client->message.topic, client->message.topic_len); + } + if (client->message.payload && client->message.payload_len > 0) { + msg.payload.assign(client->message.payload, client->message.payload_len); + } + msg.qos = client->message.qos; + state->inbox.push_back(std::move(msg)); + js_mqtt_try_deliver(state); + break; + } + case MQTT_TYPE_DISCONNECT: + state->closed = true; + if (state->connect_op) { + JsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + js_promise_reject(op, "connect failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + if (state->recv_op) { + JsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->recv_op = NULL; + js_promise_reject(op, js_mqtt_closed_reason(state)); + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + } + break; + default: break; + } +} + +static bool js_parse_reconnect(JSContext* js, JSValueConst obj, reconn_setting_t* out) { + JSValue reconnect; + if (!js_get_property(js, obj, "reconnect", &reconnect) || !JS_IsObject(reconnect)) { + if (!JS_IsUndefined(reconnect) && !JS_IsException(reconnect)) JS_FreeValue(js, reconnect); + return false; + } + reconn_setting_init(out); + out->min_delay = (uint32_t)js_get_int_property(js, reconnect, "min_delay", (int)out->min_delay); + out->max_delay = (uint32_t)js_get_int_property(js, reconnect, "max_delay", (int)out->max_delay); + out->delay_policy = (uint32_t)js_get_int_property(js, reconnect, "delay_policy", (int)out->delay_policy); + out->max_retry_cnt = (uint32_t)js_get_int_property(js, reconnect, "max_retry", (int)out->max_retry_cnt); + if (out->max_retry_cnt == 0) out->max_retry_cnt = INFINITE; + if (out->min_delay == 0) out->min_delay = 1; + if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; + if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { + out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; + } + JS_FreeValue(js, reconnect); + return true; +} + +static JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || task->loop == NULL) { + return js_rejected_promise(js, "hv.mqtt: no event loop on this thread"); + } + if (argc < 1 || !JS_IsObject(argv[0])) { + return js_rejected_promise(js, "hv.mqtt: connect needs options"); + } + + std::string host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); + int port = js_get_int_property(js, argv[0], "port", DEFAULT_MQTT_PORT); + int ssl = js_get_bool_property(js, argv[0], "ssl", false) ? 1 : 0; + std::string id = js_get_string_property(js, argv[0], "id", ""); + std::string username = js_get_string_property(js, argv[0], "username", ""); + std::string password = js_get_string_property(js, argv[0], "password", ""); + int keepalive = js_get_int_property(js, argv[0], "keepalive", 0); + int timeout = js_get_int_property(js, argv[0], "connect_timeout", 0); + if (timeout <= 0) timeout = js_get_int_property(js, argv[0], "timeout", 0); + bool clean_session = js_get_bool_property(js, argv[0], "clean_session", true); + + js_mqtt_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = mqtt_client_new(task->loop); + if (state->client == NULL) { + return js_rejected_promise(js, "hv.mqtt: create client failed"); + } + mqtt_client_set_userdata(state->client, state.get()); + mqtt_client_set_callback(state->client, js_mqtt_on_event); + if (!id.empty()) mqtt_client_set_id(state->client, id.c_str()); + if (!username.empty() || !password.empty()) { + mqtt_client_set_auth(state->client, username.c_str(), password.c_str()); + } + if (keepalive > 0) state->client->keepalive = (unsigned short)keepalive; + state->client->clean_session = clean_session ? 1 : 0; + if (timeout > 0) mqtt_client_set_connect_timeout(state->client, timeout); + reconn_setting_t reconn; + if (js_parse_reconnect(js, argv[0], &reconn)) { + mqtt_client_set_reconnect(state->client, &reconn); + state->reconnect = true; + } + + JsMqttConnect* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) { + state->detach(); + return promise; + } + state->connect_op = op; + op->state = state; + task->in_call = true; + int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); + if (ret != 0) { + state->connect_op = NULL; + js_promise_reject(op, "hv.mqtt: connect failed"); + state->detach(); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsMqttClient* box = js_mqtt_client(js, this_val); + std::shared_ptr state = box ? box->state : std::shared_ptr(); + if (!state || state->client == NULL) { + return js_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + JsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return js_async_resolved_promise(js, js_get_task(js), js_push_mqtt_message(js, msg)); + } + if (state->closed) { + return js_rejected_promise(js, js_mqtt_closed_reason(state.get())); + } + if (state->recv_op != NULL) { + return js_rejected_promise(js, "hv.mqtt: recv already pending"); + } + JsHttpTask* task = js_get_task(js); + JsMqttRecv* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = state; + state->recv_op = op; + return promise; +} + +static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsMqttClient* box = js_mqtt_client(js, this_val); + JsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 2) { + return JS_ThrowTypeError(js, "hv.mqtt: publish needs topic and payload"); + } + std::string topic = js_to_string(js, argv[0]); + std::string payload = js_to_string(js, argv[1]); + int32_t qos = 0; + if (argc > 2 && JS_ToInt32(js, &qos, argv[2]) != 0) return JS_EXCEPTION; + int retain = argc > 3 ? JS_ToBool(js, argv[3]) : 0; + mqtt_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.topic = topic.c_str(); + msg.topic_len = (unsigned int)topic.size(); + msg.payload = payload.c_str(); + msg.payload_len = (unsigned int)payload.size(); + msg.qos = (unsigned char)qos; + msg.retain = (unsigned char)retain; + int mid = mqtt_client_publish(state->client, &msg); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: publish failed"); + } + return JS_NewInt32(js, mid); +} + +static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsMqttClient* box = js_mqtt_client(js, this_val); + JsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: subscribe needs topic"); + } + std::string topic = js_to_string(js, argv[0]); + int32_t qos = 0; + if (argc > 1 && JS_ToInt32(js, &qos, argv[1]) != 0) return JS_EXCEPTION; + int mid = mqtt_client_subscribe(state->client, topic.c_str(), qos); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: subscribe failed"); + } + return JS_NewInt32(js, mid); +} + +static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsMqttClient* box = js_mqtt_client(js, this_val); + JsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: unsubscribe needs topic"); + } + std::string topic = js_to_string(js, argv[0]); + int mid = mqtt_client_unsubscribe(state->client, topic.c_str()); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: unsubscribe failed"); + } + return JS_NewInt32(js, mid); +} + +static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsMqttClient* box = js_mqtt_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + state->reconnect = false; + if (state->connect_op) { + JsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + js_promise_reject(op, "closed"); + } + if (state->recv_op) { + JsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + js_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +static JSValue js_require_mqtt(JSContext* js) { + JSValue mqtt = JS_NewObject(js); + JS_SetPropertyStr(js, mqtt, "connect", JS_NewCFunction(js, js_mqtt_connect, "connect", 1)); + return mqtt; +} +#endif + +static JSValue js_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + if (argc < 1) { + return JS_ThrowTypeError(js, "require needs a module name"); + } + std::string name = js_to_string(js, argv[0]); + if (name == "hv") { + JSValue hv = JS_NewObject(js); + JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); + JS_SetPropertyStr(js, hv, "log", JS_NewCFunction(js, js_hv_log, "log", 1)); + JS_SetPropertyStr(js, hv, "sleep", JS_NewCFunction(js, js_hv_sleep, "sleep", 1)); + return hv; + } +#ifdef HVJS_WITH_HTTP + if (name == "hv/http") { + return js_require_http(js); + } +#endif +#ifdef HVJS_WITH_REDIS + if (name == "hv/redis") { + return js_require_redis(js); + } +#endif +#ifdef HVJS_WITH_HTTP + if (name == "hv/ws") { + return js_require_ws(js); + } +#endif +#ifdef HVJS_WITH_MQTT + if (name == "hv/mqtt") { + return js_require_mqtt(js); + } +#endif + return JS_ThrowReferenceError(js, "module '%s' is not available", name.c_str()); +} + +static bool load_file(const std::string& filepath, std::string* out, std::string* err) { + HFile file; + if (file.open(filepath.c_str(), "rb") != 0) { + if (err) *err = strerror(errno); + return false; + } + size_t size = hv_filesize(filepath.c_str()); + out->resize(size); + if (size > 0) { + int nread = file.read(&(*out)[0], (int)size); + if (nread < 0 || (size_t)nread != size) { + if (err) *err = "read script failed"; + return false; + } + } + return true; +} + +static time_t file_mtime(const std::string& filepath) { + struct stat st; + if (stat(filepath.c_str(), &st) != 0) { + return 0; + } + return st.st_mtime; +} + +static bool push_handler_fn(JSContext* js, JSValueConst global, http_method method, JSValue* fn) { + std::string name = http_method_str(method); + tolower(name); + *fn = JS_GetPropertyStr(js, global, name.c_str()); + if (JS_IsFunction(js, *fn)) return true; + JS_FreeValue(js, *fn); + *fn = JS_GetPropertyStr(js, global, "handle"); + if (JS_IsFunction(js, *fn)) return true; + JS_FreeValue(js, *fn); + *fn = JS_UNDEFINED; + return false; +} + +static bool apply_result(JSContext* js, JSValueConst value, const HttpContextPtr& ctx, std::string* err) { + if (JS_IsUndefined(value) || JS_IsNull(value)) { + return true; + } + if (JS_IsNumber(value)) { + int32_t status = 0; + if (JS_ToInt32(js, &status, value) == 0 && ctx->response->status_code == HTTP_STATUS_OK) { + ctx->response->status_code = (http_status)status; + } + return true; + } + if (JS_IsString(value)) { + std::string body = js_to_string(js, value); + ctx->response->String(body); + return true; + } + JSValue json = JS_JSONStringify(js, value, JS_UNDEFINED, JS_UNDEFINED); + if (!JS_IsException(json)) { + std::string body = js_to_string(js, json); + ctx->response->SetContentType(APPLICATION_JSON); + ctx->response->body = body; + JS_FreeValue(js, json); + return true; + } + if (err) *err = js_exception_string(js); + return false; +} + +static void task_finish(JsHttpTask* task, JSValue result) { + if (task->finished) return; + task->finished = true; + if (!task->error.empty()) { + hloge("[js] http handler error: %s", task->error.c_str()); + task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + task->ctx->response->String(task->error); + } + else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + std::string err = js_to_string(task->js, result); + hloge("[js] http handler rejected: %s", err.c_str()); + task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + task->ctx->response->String(err); + } + else { + std::string err; + if (!apply_result(task->js, result, task->ctx, &err)) { + hloge("[js] http handler error: %s", err.c_str()); + task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + task->ctx->response->String(err); + } + } + JS_FreeValue(task->js, result); + if (task->async) { + task->ctx->send(); + } + task_unref(task); +} + +} // namespace + +struct HttpJsHandler::State { + std::mutex mutex; + std::string code; + time_t mtime; + bool loaded; + + State() : mtime(0), loaded(false) {} +}; + +HttpJsHandler::HttpJsHandler(const char* filepath, const HttpJsHandlerOptions& options) + : filepath_(filepath ? filepath : ""), options_(options), state_(std::make_shared()) {} + +bool HttpJsHandler::loadScript(std::string* code, std::string* err) { + std::lock_guard lock(state_->mutex); + if (state_->loaded && !options_.reload_on_change) { + if (code) *code = state_->code; + return true; + } + + time_t mtime = file_mtime(filepath_); + if (mtime == 0) { + if (err) *err = strerror(errno); + return false; + } + if (state_->loaded && state_->mtime == mtime) { + if (code) *code = state_->code; + return true; + } + + std::string latest; + if (!load_file(filepath_, &latest, err)) { + return false; + } + state_->code = latest; + state_->mtime = mtime; + state_->loaded = true; + if (code) *code = state_->code; + return true; +} + +int HttpJsHandler::operator()(const HttpContextPtr& ctx) { + if (!ctx || !ctx->request || !ctx->response) { + if (ctx && ctx->response) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("js handler: invalid http context"); + } + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + std::string code, err; + if (!loadScript(&code, &err)) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(err); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JsHttpTask* task = new JsHttpTask(); + task->ctx = ctx; + task->rt = JS_NewRuntime(); + task->js = task->rt ? JS_NewContext(task->rt) : NULL; + if (task->rt == NULL || task->js == NULL) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("js handler: failed to create quickjs runtime"); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + JS_SetContextOpaque(task->js, task); + if (ctx->writer && ctx->writer->io()) { + task->loop = hevent_loop(ctx->writer->io()); + } + task->loop_ptr = currentThreadEventLoopPtr; + if (task->loop == NULL && task->loop_ptr) { + task->loop = task->loop_ptr->loop(); + } + if (task->loop == NULL) { + EventLoop* loop = currentThreadEventLoop; + if (loop) { + task->loop = loop->loop(); + } + } + if (task->loop == NULL) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("js handler: no event loop on this thread"); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, js_require, "require", 1)); + + JSValue eval = JS_Eval(task->js, code.c_str(), code.size(), filepath_.c_str(), JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + std::string msg = js_exception_string(task->js); + JS_FreeValue(task->js, global); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(msg); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + JS_FreeValue(task->js, eval); + + JSValue fn; + if (!push_handler_fn(task->js, global, ctx->request->method, &fn)) { + JS_FreeValue(task->js, global); + ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; + ctx->response->String("no js handler function"); + task_unref(task); + return HTTP_STATUS_NOT_IMPLEMENTED; + } + + JSValue js_ctx = js_new_ctx(task->js, ctx); + JSValue ret = JS_Call(task->js, fn, JS_UNDEFINED, 1, &js_ctx); + JS_FreeValue(task->js, js_ctx); + JS_FreeValue(task->js, fn); + if (JS_IsException(ret)) { + std::string msg = js_exception_string(task->js); + JS_FreeValue(task->js, global); + JS_FreeValue(task->js, ret); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(msg); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = ret; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, ret); + if (JS_IsException(task->promise)) { + std::string msg = js_exception_string(task->js); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(msg); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + task_ref(task); + drain_jobs(task); + bool finished = task->finished; + int status = ctx->response->status_code; + if (!finished) { + task->async = true; + } + task_unref(task); + if (finished) { + return status; + } + return HTTP_STATUS_NEXT; +} + +} // namespace hv +#endif // WITH_JS diff --git a/http/server/HttpJsHandler.h b/http/server/HttpJsHandler.h new file mode 100644 index 000000000..8c9a5f7b3 --- /dev/null +++ b/http/server/HttpJsHandler.h @@ -0,0 +1,47 @@ +#ifndef HV_HTTP_JS_HANDLER_H_ +#define HV_HTTP_JS_HANDLER_H_ + +#include +#include + +#include "hexport.h" +#include "HttpService.h" + +namespace hv { + +struct HV_EXPORT HttpJsHandlerOptions { + bool reload_on_change; + + HttpJsHandlerOptions() { reload_on_change = true; } +}; + +// HttpJsHandler runs a QuickJS script to handle an HTTP request. +// +// The first implementation uses one QuickJS runtime/context per request. This +// keeps request lifetime, Promise continuations and loop-thread affinity simple; +// scripts can use async functions and await hv.sleep() without blocking the +// server IO loop. The public route surface mirrors HttpLuaHandler: a per-method +// function (get/post/...) takes precedence over handle(ctx). +class HV_EXPORT HttpJsHandler { +public: + HttpJsHandler(const char* filepath, const HttpJsHandlerOptions& options = HttpJsHandlerOptions()); + + int operator()(const HttpContextPtr& ctx); + + const std::string& filepath() const { return filepath_; } + +private: + struct State; + + bool loadScript(std::string* code, std::string* err); + + std::string filepath_; + HttpJsHandlerOptions options_; + std::shared_ptr state_; +}; + +typedef std::shared_ptr HttpJsHandlerPtr; + +} // namespace hv + +#endif // HV_HTTP_JS_HANDLER_H_ diff --git a/http/server/HttpScriptHandler.cpp b/http/server/HttpScriptHandler.cpp index 83ee2bae7..c66e958eb 100644 --- a/http/server/HttpScriptHandler.cpp +++ b/http/server/HttpScriptHandler.cpp @@ -1,17 +1,28 @@ #include "HttpScriptHandler.h" -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) #include "hbase.h" #include "hstring.h" +#ifdef WITH_JS +#include "HttpJsHandler.h" +#endif +#ifdef WITH_LUA #include "HttpLuaHandler.h" +#endif #include namespace hv { struct HttpScriptHandler::State { +#ifdef WITH_LUA std::once_flag lua_once; HttpLuaHandlerPtr lua_handler; +#endif +#ifdef WITH_JS + std::once_flag js_once; + HttpJsHandlerPtr js_handler; +#endif }; namespace { @@ -44,6 +55,7 @@ HttpScriptHandler& HttpScriptHandler::operator=(const HttpScriptHandler& rhs) { } int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { +#ifdef WITH_LUA if (filepath_has_suffix(filepath_, "lua")) { std::call_once(state_->lua_once, [this]() { HttpLuaHandlerOptions lua_options; @@ -52,6 +64,18 @@ int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { }); return (*state_->lua_handler)(ctx); } +#endif + +#ifdef WITH_JS + if (filepath_has_suffix(filepath_, "js")) { + std::call_once(state_->js_once, [this]() { + HttpJsHandlerOptions js_options; + js_options.reload_on_change = options_.reload_on_change; + state_->js_handler = std::make_shared(filepath_.c_str(), js_options); + }); + return (*state_->js_handler)(ctx); + } +#endif if (ctx && ctx->response) { ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; @@ -62,4 +86,4 @@ int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { } // namespace hv -#endif // WITH_LUA +#endif // WITH_LUA || WITH_JS diff --git a/http/server/HttpService.cpp b/http/server/HttpService.cpp index da7b946c4..c28710e70 100644 --- a/http/server/HttpService.cpp +++ b/http/server/HttpService.cpp @@ -1,7 +1,7 @@ #include "HttpService.h" #include "HttpMiddleware.h" #include "HttpRouter.h" -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) #include "HttpScriptHandler.h" #include "hpath.h" #include "hstring.h" @@ -115,7 +115,7 @@ std::string HttpService::GetStaticFilepath(const char* path) { return filepath; } -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) void HttpService::Script(const char* path, const char* script_dir) { std::string route_path(path ? path : ""); if (route_path.empty()) return; @@ -147,7 +147,27 @@ void HttpService::Script(const char* path, const char* script_dir) { } std::string script = HPath::join(root, name); if (HPath::suffixname(script).empty()) { - script += ".lua"; + std::string base_script = script; + script.clear(); +#if defined(WITH_LUA) && defined(WITH_JS) + std::string lua_script = base_script + ".lua"; + if (HPath::exists(lua_script.c_str())) { + script = lua_script; + } + if (script.empty()) { + std::string js_script = base_script + ".js"; + if (HPath::exists(js_script.c_str())) { + script = js_script; + } + } + if (script.empty()) { + script = lua_script; + } +#elif defined(WITH_LUA) + script = base_script + ".lua"; +#else + script = base_script + ".js"; +#endif } HttpScriptHandlerPtr script_handler; diff --git a/http/server/HttpService.h b/http/server/HttpService.h index 37073d637..c6a35f0b4 100644 --- a/http/server/HttpService.h +++ b/http/server/HttpService.h @@ -201,7 +201,7 @@ struct HV_EXPORT HttpService { // @retval / => /var/www/html/index.html std::string GetStaticFilepath(const char* path); -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) void Script(const char* path, const char* script_dir); #endif diff --git a/redis/AsyncRedisClient.cpp b/redis/AsyncRedisClient.cpp index f6e9f8e3e..99d38e92b 100644 --- a/redis/AsyncRedisClient.cpp +++ b/redis/AsyncRedisClient.cpp @@ -352,10 +352,10 @@ struct AsyncRedisClient::Impl { void failPending(int code) { while (!pending.empty()) { - const std::shared_ptr& request = pending.front(); + std::shared_ptr request = pending.front(); + pending.pop_front(); cancelTimeout(request); invokeRequestCallback(request, code); - pending.pop_front(); } } @@ -411,14 +411,14 @@ struct AsyncRedisClient::Impl { handleClientError(ERR_RESPONSE); return; } - const std::shared_ptr& request = pending.front(); + std::shared_ptr request = pending.front(); request->replies.push_back(reply); if (request->replies.size() < request->expected_replies) { return; } + pending.pop_front(); cancelTimeout(request); invokeRequestCallback(request, 0); - pending.pop_front(); } void handleClientError(int code) { diff --git a/scripts/unittest.sh b/scripts/unittest.sh index d765500ab..ebffce7cd 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -47,6 +47,18 @@ fi if [ -x bin/http_lua_handler_test ]; then bin/http_lua_handler_test fi +if [ -x bin/http_js_handler_test ]; then + bin/http_js_handler_test || exit $? +fi +if [ -x bin/http_js_redis_test ]; then + bin/http_js_redis_test || exit $? +fi +if [ -x bin/http_js_ws_test ]; then + bin/http_js_ws_test || exit $? +fi +if [ -x bin/http_js_mqtt_test ]; then + bin/http_js_mqtt_test || exit $? +fi if [ -x bin/lua_http_test ]; then bin/lua_http_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 2b62d081d..47b5b4bdd 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -133,6 +133,31 @@ set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_redis_test) endif() endif() +if(WITH_JS AND WITH_EVPP AND WITH_HTTP AND WITH_HTTP_SERVER AND WITH_HTTP_CLIENT) +add_executable(http_js_handler_test http_js_handler_test.cpp) +target_include_directories(http_js_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) +target_link_libraries(http_js_handler_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS http_js_handler_test) +if(WITH_REDIS) +add_executable(http_js_redis_test http_js_redis_test.cpp redis_test_server.cpp) +target_include_directories(http_js_redis_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client ../redis) +target_link_libraries(http_js_redis_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_redis_test) +endif() +if(WITH_HTTP_CLIENT) +add_executable(http_js_ws_test http_js_ws_test.cpp) +target_include_directories(http_js_ws_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) +target_link_libraries(http_js_ws_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_ws_test) +endif() +if(WITH_MQTT) +add_executable(http_js_mqtt_test http_js_mqtt_test.cpp) +target_include_directories(http_js_mqtt_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client ../mqtt) +target_link_libraries(http_js_mqtt_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_mqtt_test) +endif() +endif() + # ------event: async dns------ add_executable(hdns_test hdns_test.c) target_include_directories(hdns_test PRIVATE .. ../base ../ssl ../event) @@ -199,6 +224,7 @@ add_custom_target(unittest DEPENDS sendmail http_router_test ${HTTP_LUA_UNITTEST_TARGETS} + ${HTTP_JS_UNITTEST_TARGETS} hdns_test hdns_benchmark ${REDIS_UNITTEST_TARGETS} diff --git a/unittest/http_js_handler_test.cpp b/unittest/http_js_handler_test.cpp new file mode 100644 index 000000000..e5b28dc29 --- /dev/null +++ b/unittest/http_js_handler_test.cpp @@ -0,0 +1,137 @@ +/* + * http_js_handler_test - integration test for HttpJsHandler. + * + * Starts a real HttpServer whose JS route awaits hv.sleep(ms). Concurrent + * requests on a single IO thread must complete faster than serial execution, + * proving the QuickJS promise continuation is driven by the event loop without + * blocking it. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "htime.h" +#include "HttpJsHandler.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "requests.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_handler_test"); + std::string path = HPath::join("tmp/http_js_handler_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + std::string script = write_script("sleep.js", "const hv = require('hv');\n" + "const http = require('hv/http');\n" + "async function get(ctx) {\n" + " await hv.sleep(300);\n" + " const resp = await http.get('http://' + ctx.header('Host') + '/ping');\n" + " return { ok: true, id: ctx.query('id'), upstream: resp.body };\n" + "}\n"); + std::string direct_script = write_script("direct.js", "function get(ctx) {\n" + " ctx.setHeader('X-From', 'js');\n" + " return ctx.text('direct:' + ctx.query('id', ''));\n" + "}\n"); + std::string circular_script = write_script("circular.js", "function get(ctx) {\n" + " const data = { ok: true };\n" + " data.self = data;\n" + " return data;\n" + "}\n"); + + HttpService service; + service.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { + (void)req; + resp->body = "pong"; + return 200; + }); + service.GET("/sleep", hv::HttpScriptHandler(script.c_str())); + service.GET("/direct", hv::HttpJsHandler(direct_script.c_str())); + service.GET("/circular", hv::HttpJsHandler(circular_script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + const int N = 5; + std::vector threads; + std::atomic ok_count(0); + + uint64_t start = gettimeofday_ms(); + const int server_port = server.port; + for (int i = 0; i < N; ++i) { + threads.emplace_back([i, server_port, &ok_count]() { + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/sleep?id=%d", server_port, i); + auto resp = requests::get(url); + if (resp == NULL) { + fprintf(stderr, "request %d failed: null response\n", i); + return; + } + if (resp->status_code != 200) { + fprintf(stderr, "request %d failed: status=%d body=%s\n", i, resp->status_code, resp->body.c_str()); + return; + } + char needle[32]; + snprintf(needle, sizeof(needle), "\"id\":\"%d\"", i); + if (resp->body.find("\"ok\":true") != std::string::npos && resp->body.find(needle) != std::string::npos && + resp->body.find("\"upstream\":\"pong\"") != std::string::npos) { + ok_count++; + } + else { + fprintf(stderr, "request %d failed: body=%s\n", i, resp->body.c_str()); + } + }); + } + for (auto& t : threads) t.join(); + uint64_t elapsed = gettimeofday_ms() - start; + + char direct_url[128]; + snprintf(direct_url, sizeof(direct_url), "http://127.0.0.1:%d/direct?id=7", server_port); + auto direct_resp = requests::get(direct_url); + char circular_url[128]; + snprintf(circular_url, sizeof(circular_url), "http://127.0.0.1:%d/circular", server_port); + auto circular_resp = requests::get(circular_url); + + server.stop(); + hv_msleep(100); + + printf("ok_count=%d/%d elapsed=%llums (each handler awaits 300ms)\n", ok_count.load(), N, (unsigned long long)elapsed); + CHECK(ok_count.load() == N); + CHECK(elapsed < 1200); + CHECK(direct_resp != NULL); + CHECK(direct_resp->status_code == 200); + CHECK(direct_resp->body == "direct:7"); + CHECK(direct_resp->GetHeader("X-From") == "js"); + CHECK(circular_resp != NULL); + CHECK(circular_resp->status_code == 500); + CHECK(circular_resp->body.find("circular") != std::string::npos); + printf("ALL http_js_handler_test PASSED\n"); + return 0; +} diff --git a/unittest/http_js_mqtt_test.cpp b/unittest/http_js_mqtt_test.cpp new file mode 100644 index 000000000..c480ce90f --- /dev/null +++ b/unittest/http_js_mqtt_test.cpp @@ -0,0 +1,71 @@ +/* + * http_js_mqtt_test - HttpJsHandler + hv/mqtt Promise binding smoke test. + * + * No live MQTT broker is required: this verifies the module registers and its + * connect() failure path rejects without crashing or hanging. + */ + +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "requests.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_mqtt_test"); + std::string path = HPath::join("tmp/http_js_mqtt_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + std::string script = write_script("mqtt.js", "const mqtt = require('hv/mqtt');\n" + "async function get(ctx) {\n" + " let err = '';\n" + " try { await mqtt.connect({ host: '127.0.0.1', port: 1, timeout: 500 }); }\n" + " catch (e) { err = String(e); }\n" + " return { ok: true, err };\n" + "}\n"); + + HttpService service; + service.GET("/mqtt", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/mqtt", server.port); + auto resp = requests::get(url); + server.stop(); + hv_msleep(100); + + CHECK(resp != NULL); + CHECK(resp->status_code == 200); + CHECK(resp->body.find("\"ok\":true") != std::string::npos); + CHECK(resp->body.find("\"err\":\"") != std::string::npos); + printf("ALL http_js_mqtt_test PASSED\n"); + return 0; +} diff --git a/unittest/http_js_redis_test.cpp b/unittest/http_js_redis_test.cpp new file mode 100644 index 000000000..967de8ead --- /dev/null +++ b/unittest/http_js_redis_test.cpp @@ -0,0 +1,113 @@ +/* + * http_js_redis_test - HttpJsHandler + hv/redis Promise binding. + * + * Uses the in-process FakeRedisServer so the test does not depend on a local + * Redis daemon. The JS handler awaits Redis command promises and returns JSON. + */ + +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "requests.h" +#include "redis_test_server.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_redis_test"); + std::string path = HPath::join("tmp/http_js_redis_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + FakeRedisServer redis_server; + redis_server.setCommandHandler([](const hv::RedisCommand& cmd) { + hv::RedisReply reply; + if (cmd[0] == "PING") { + reply.type = hv::REDIS_REPLY_STRING; + reply.str = "PONG"; + } + else if (cmd[0] == "SET") { + reply.type = hv::REDIS_REPLY_STRING; + reply.str = "OK"; + } + else if (cmd[0] == "GET") { + reply.type = hv::REDIS_REPLY_STRING; + reply.str = "v"; + reply.bulk = true; + } + else if (cmd[0] == "INCR") { + reply.type = hv::REDIS_REPLY_INTEGER; + reply.integer = 1; + } + else { + reply.type = hv::REDIS_REPLY_ERROR; + reply.str = "ERR unsupported"; + } + return reply; + }); + redis_server.start(); + CHECK(redis_server.port() > 0); + + char script_buf[2048]; + snprintf(script_buf, sizeof(script_buf), + "const redis = require('hv/redis');\n" + "async function get(ctx) {\n" + " const r = redis.new({ host: '127.0.0.1', port: %d, timeout: 3000 });\n" + " const ok = await r.set('k', 'v');\n" + " const v = await r.get('k');\n" + " const n = await r.incr('c');\n" + " const pong = await r.command(['PING']);\n" + " let err = '';\n" + " try { await r.command('BADCMD'); } catch (e) { err = String(e); }\n" + " return { ok, v, n, pong, err };\n" + "}\n", + redis_server.port()); + std::string script = write_script("redis.js", script_buf); + + HttpService service; + service.GET("/redis", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/redis", server.port); + auto resp = requests::get(url); + server.stop(); + redis_server.stop(); + hv_msleep(100); + + CHECK(resp != NULL); + CHECK(resp->status_code == 200); + CHECK(resp->body.find("\"ok\":\"OK\"") != std::string::npos); + CHECK(resp->body.find("\"v\":\"v\"") != std::string::npos); + CHECK(resp->body.find("\"n\":1") != std::string::npos); + CHECK(resp->body.find("\"pong\":\"PONG\"") != std::string::npos); + CHECK(resp->body.find("\"err\":\"ERR unsupported\"") != std::string::npos); + printf("ALL http_js_redis_test PASSED\n"); + return 0; +} diff --git a/unittest/http_js_ws_test.cpp b/unittest/http_js_ws_test.cpp new file mode 100644 index 000000000..18eb66df9 --- /dev/null +++ b/unittest/http_js_ws_test.cpp @@ -0,0 +1,83 @@ +/* + * http_js_ws_test - HttpJsHandler + hv/ws Promise binding. + */ + +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "WebSocketServer.h" +#include "requests.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_ws_test"); + std::string path = HPath::join("tmp/http_js_ws_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + WebSocketService ws_service; + ws_service.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { channel->send(msg); }; + hv::WebSocketServer ws_server(&ws_service); + ws_server.setPort(0); + ws_server.setThreadNum(1); + CHECK(ws_server.start() == 0); + CHECK(ws_server.port > 0); + + char script_buf[1024]; + snprintf(script_buf, sizeof(script_buf), + "const wsmod = require('hv/ws');\n" + "async function get(ctx) {\n" + " const ws = await wsmod.connect('ws://127.0.0.1:%d/');\n" + " ws.send('hello-js');\n" + " const msg = await ws.recv();\n" + " ws.close();\n" + " return { ok: true, msg };\n" + "}\n", + ws_server.port); + std::string script = write_script("ws.js", script_buf); + + HttpService service; + service.GET("/ws", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/ws", server.port); + auto resp = requests::get(url); + server.stop(); + ws_server.stop(); + hv_msleep(100); + + CHECK(resp != NULL); + CHECK(resp->status_code == 200); + CHECK(resp->body.find("\"ok\":true") != std::string::npos); + CHECK(resp->body.find("\"msg\":\"hello-js\"") != std::string::npos); + printf("ALL http_js_ws_test PASSED\n"); + return 0; +} From 266deee888fd35eb60c529e10e5999d3b55ebf73 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 12:58:46 +0800 Subject: [PATCH 02/13] refactor(js): split quickjs bindings --- .github/workflows/CI.yml | 4 +- CMakeLists.txt | 6 +- Makefile | 16 +- docs/cn/HttpJsHandler.md | 8 + examples/CMakeLists.txt | 10 + examples/hvjs.cpp | 167 ++++ examples/js/http_client.js | 13 + examples/js/mqtt_client.js | 29 + examples/js/redis_client.js | 27 + examples/js/sleep.js | 19 + examples/js/ws_client.js | 19 + http/server/HttpJsHandler.cpp | 1457 +-------------------------------- js/hvjs.cpp | 373 +++++++++ js/hvjs.h | 93 +++ js/hvjs_http.cpp | 402 +++++++++ js/hvjs_mqtt.cpp | 456 +++++++++++ js/hvjs_redis.cpp | 236 ++++++ 17 files changed, 1907 insertions(+), 1428 deletions(-) create mode 100644 examples/hvjs.cpp create mode 100644 examples/js/http_client.js create mode 100644 examples/js/mqtt_client.js create mode 100644 examples/js/redis_client.js create mode 100644 examples/js/sleep.js create mode 100644 examples/js/ws_client.js create mode 100644 js/hvjs.cpp create mode 100644 js/hvjs.h create mode 100644 js/hvjs_http.cpp create mode 100644 js/hvjs_mqtt.cpp create mode 100644 js/hvjs_redis.cpp diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 242cb24c9..10e612d2c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,8 +19,8 @@ jobs: - name: build run: | sudo apt update - sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc + sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc make libhv evpp # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr make libhrpc hrpc PROTOBUF_PREFIX=/usr diff --git a/CMakeLists.txt b/CMakeLists.txt index 35eb2947a..5ed5ee6ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,6 +240,7 @@ if(WITH_JS) /usr/local/opt/quickjs/lib /usr/local/lib/quickjs /usr/local/lib + /usr/lib/quickjs /usr/lib) if(NOT QUICKJS_INCLUDE_DIR OR NOT QUICKJS_LIBRARY) message(FATAL_ERROR "WITH_JS requires QuickJS. Set QUICKJS_ROOT or QUICKJS_INCLUDE_DIR and QUICKJS_LIBRARY.") @@ -287,7 +288,7 @@ if(APPLE) endif() # see Makefile -set(ALL_SRCDIRS . base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt) +set(ALL_SRCDIRS . base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt js) set(CORE_SRCDIRS . base ssl event) if(WIN32 OR MINGW) if(WITH_WEPOLL) @@ -318,6 +319,9 @@ endif() if(WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS} ${EVPP_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil evpp) + if(WITH_JS) + set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} js) + endif() if(WITH_REDIS) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${REDIS_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} redis) diff --git a/Makefile b/Makefile index 60875c196..fa69443fc 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ include config.mk include Makefile.vars MAKEF=$(MAKE) -f Makefile.in -ALL_SRCDIRS=. base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt +ALL_SRCDIRS=. base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt js CORE_SRCDIRS=. base ssl event ifeq ($(WITH_KCP), yes) CORE_SRCDIRS += event/kcp @@ -29,6 +29,12 @@ LIBHV_SRCDIRS += cpputil endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +LIBHV_SRCDIRS += js +endif +endif + ifeq ($(WITH_EVPP), yes) LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) LIBHV_SRCDIRS += cpputil evpp @@ -121,6 +127,11 @@ ifeq ($(WITH_EVPP), yes) EXAMPLES += hvlua endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +EXAMPLES += hvjs +endif +endif examples: $(EXAMPLES) @echo "make examples done." @@ -234,6 +245,9 @@ host: prepare hvlua: prepare libhv $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -o bin/hvlua examples/hvlua.cpp -Llib -lhv -pthread $(LUA_LIBS) +hvjs: prepare libhv + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ijs -o bin/hvjs examples/hvjs.cpp -Llib -lhv -pthread $(JS_LIBS) + multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" diff --git a/docs/cn/HttpJsHandler.md b/docs/cn/HttpJsHandler.md index b7a7c8a8c..1b1cbdfab 100644 --- a/docs/cn/HttpJsHandler.md +++ b/docs/cn/HttpJsHandler.md @@ -224,3 +224,11 @@ make http_server_test WITH_JS=yes WITH_HTTP=yes bin/http_server_test 8080 curl "http://127.0.0.1:8080/script/hello?id=42" ``` + +也可以直接使用 `hvjs` 运行独立脚本示例: + +```bash +make hvjs WITH_JS=yes WITH_HTTP=yes WITH_REDIS=yes WITH_MQTT=yes +bin/hvjs examples/js/sleep.js +bin/hvjs examples/js/http_client.js http://127.0.0.1:8080/ping +``` diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 89eb21fbb..45c02c2a9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -133,6 +133,16 @@ if(WITH_EVPP) list(APPEND EXAMPLES hvlua) endif() + + if(WITH_JS) + include_directories(../js) + + # hvjs: standalone QuickJS runtime on top of libhv's event loop + add_executable(hvjs hvjs.cpp) + target_link_libraries(hvjs ${HV_LIBRARIES}) + + list(APPEND EXAMPLES hvjs) + endif() if(WITH_HTTP) include_directories(../http) diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp new file mode 100644 index 000000000..ce83eb392 --- /dev/null +++ b/examples/hvjs.cpp @@ -0,0 +1,167 @@ +// hvjs: standalone QuickJS runtime on top of libhv's event loop. +// +// Usage: hvjs script.js [args...] +// +// The runtime publishes a shared EventLoop as this thread's loop so async JS +// bindings can reuse libhv clients on the same event loop. Scripts may use +// async/await with the built-in modules exposed through require("hv"), +// require("hv/http"), require("hv/ws"), require("hv/redis") and +// require("hv/mqtt") when the corresponding libhv modules are enabled. + +#include +#include + +#include +#include + +#include + +#include "EventLoop.h" +#include "hfile.h" +#include "hlog.h" +#include "hvjs.h" + +namespace { + +struct HvJsCliTask : public hv::js::HvJsTask { + int exit_code; + + HvJsCliTask() : exit_code(0) {} +}; + +static void usage(const char* prog) { + fprintf(stderr, "Usage: %s script.js [args...]\n", prog); +} + +static bool load_file(const char* filepath, std::string* out) { + HFile file; + if (file.open(filepath, "rb") != 0) { + return false; + } + size_t size = hv_filesize(filepath); + out->resize(size); + if (size == 0) return true; + int nread = file.read(&(*out)[0], (int)size); + return nread >= 0 && (size_t)nread == size; +} + +static JSValue js_print(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + for (int i = 0; i < argc; ++i) { + if (i != 0) fputc(' ', stdout); + std::string s = hv::js::hvjs_to_string(js, argv[i]); + fputs(s.c_str(), stdout); + } + fputc('\n', stdout); + return JS_UNDEFINED; +} + +static void set_args(JSContext* js, int argc, char** argv) { + JSValue arr = JS_NewArray(js); + for (int i = 1; i < argc; ++i) { + JS_SetPropertyUint32(js, arr, i - 1, JS_NewString(js, argv[i])); + } + JSValue global = JS_GetGlobalObject(js); + JS_SetPropertyStr(js, global, "arg", arr); + JS_FreeValue(js, global); +} + +static void finish(hv::js::HvJsTask* base, JSValue result) { + HvJsCliTask* task = static_cast(base); + if (task->finished) return; + task->finished = true; + if (!task->error.empty()) { + fprintf(stderr, "hvjs: %s\n", task->error.c_str()); + task->exit_code = 1; + } + else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + std::string err = hv::js::hvjs_to_string(task->js, result); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + task->exit_code = 1; + } + JS_FreeValue(task->js, result); + if (task->loop_ptr) { + task->loop_ptr->stop(); + } + else if (task->loop) { + hloop_stop(task->loop); + } + hv::js::hvjs_task_unref(task); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 1; + } + const char* script = argv[1]; + std::string code; + if (!load_file(script, &code)) { + fprintf(stderr, "hvjs: failed to read %s\n", script); + return 1; + } + + setvbuf(stdout, NULL, _IOLBF, 0); + hlog_set_handler(stdout_logger); + + hv::EventLoopPtr loop = std::make_shared(); + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, loop.get()); + + HvJsCliTask* task = new HvJsCliTask(); + task->loop_ptr = loop; + task->loop = loop->loop(); + task->finish = finish; + task->rt = JS_NewRuntime(); + task->js = task->rt ? JS_NewContext(task->rt) : NULL; + if (task->rt == NULL || task->js == NULL) { + fprintf(stderr, "hvjs: failed to create quickjs runtime\n"); + hv::js::hvjs_task_unref(task); + return 1; + } + JS_SetContextOpaque(task->js, task); + set_args(task->js, argc, argv); + + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); + JS_SetPropertyStr(task->js, global, "print", JS_NewCFunction(task->js, js_print, "print", 1)); + + std::string wrapped = "(async function(){\n"; + wrapped += code; + wrapped += "\n})()"; + JSValue eval = JS_Eval(task->js, wrapped.c_str(), wrapped.size(), script, JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + std::string err = hv::js::hvjs_exception_string(task->js); + JS_FreeValue(task->js, global); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_unref(task); + return 1; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = eval; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, eval); + if (JS_IsException(task->promise)) { + std::string err = hv::js::hvjs_exception_string(task->js); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_unref(task); + return 1; + } + + hv::js::hvjs_task_ref(task); + hv::js::hvjs_drain_jobs(task); + int exit_code = task->exit_code; + if (!task->finished) { + loop->run(); + exit_code = task->exit_code; + } + hv::js::hvjs_task_unref(task); + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, NULL); + return exit_code; +} diff --git a/examples/js/http_client.js b/examples/js/http_client.js new file mode 100644 index 000000000..458fc922b --- /dev/null +++ b/examples/js/http_client.js @@ -0,0 +1,13 @@ +// hv/http Promise client demo. +// Usage: hvjs examples/js/http_client.js [url] + +const hv = require("hv"); +const http = require("hv/http"); + +const url = arg[1] || "http://127.0.0.1:18090/ping"; + +const resp = await http.get(url); +hv.log("GET", url, "->", resp.status, "body:", resp.body); + +const second = await http.get(url); +hv.log("2nd GET ->", second.status); diff --git a/examples/js/mqtt_client.js b/examples/js/mqtt_client.js new file mode 100644 index 000000000..f609597a0 --- /dev/null +++ b/examples/js/mqtt_client.js @@ -0,0 +1,29 @@ +// hv/mqtt Promise client demo. +// Usage: hvjs examples/js/mqtt_client.js [host] [port] [topic] + +const hv = require("hv"); +const mqtt = require("hv/mqtt"); + +const host = arg[1] || "127.0.0.1"; +const port = Number(arg[2] || 1883); +const topic = arg[3] || "hv/js/test"; + +const client = await mqtt.connect({ + host, + port, + id: "hvjs-demo", + keepalive: 60, + reconnect: { min_delay: 1000, max_delay: 10000, delay_policy: 2 }, +}); + +hv.log("connected to mqtt", host, port); + +client.subscribe(topic, 1); +client.publish(topic, "hello from js", 1); + +for (let i = 1; i <= 3; ++i) { + const msg = await client.recv(); + hv.log("recv ->", msg.topic, msg.payload, "qos", msg.qos); +} + +client.disconnect(); diff --git a/examples/js/redis_client.js b/examples/js/redis_client.js new file mode 100644 index 000000000..42e452315 --- /dev/null +++ b/examples/js/redis_client.js @@ -0,0 +1,27 @@ +// hv/redis Promise client demo. +// Usage: hvjs examples/js/redis_client.js [host] [port] + +const hv = require("hv"); +const redis = require("hv/redis"); + +const host = arg[1] || "127.0.0.1"; +const port = Number(arg[2] || 6379); + +const r = redis.new({ host, port, timeout: 3000 }); + +const ok = await r.set("hv:js:key", "hello"); +hv.log("SET ->", ok); + +const v = await r.get("hv:js:key"); +hv.log("GET ->", v); + +const n = await r.incr("hv:js:counter"); +hv.log("INCR ->", n); + +try { + const res = await r.command(["HSET", "hv:js:hash", "field", "val"]); + hv.log("HSET ->", res); +} +catch (e) { + hv.log("HSET err:", String(e)); +} diff --git a/examples/js/sleep.js b/examples/js/sleep.js new file mode 100644 index 000000000..dfdb466fc --- /dev/null +++ b/examples/js/sleep.js @@ -0,0 +1,19 @@ +// hvjs event-loop sleep example. +// Run: bin/hvjs examples/js/sleep.js + +const hv = require("hv"); + +async function worker(name, ms) { + for (let i = 1; i <= 3; ++i) { + hv.log(name, "step", i); + await hv.sleep(ms); + } + hv.log(name, "done"); +} + +await Promise.all([ + worker("A", 300), + worker("B", 500), +]); + +print("sleep example done"); diff --git a/examples/js/ws_client.js b/examples/js/ws_client.js new file mode 100644 index 000000000..0c8376ce3 --- /dev/null +++ b/examples/js/ws_client.js @@ -0,0 +1,19 @@ +// hv/ws Promise WebSocket client demo. +// Usage: hvjs examples/js/ws_client.js [url] + +const hv = require("hv"); +const wsmod = require("hv/ws"); + +const url = arg[1] || "ws://127.0.0.1:8888/"; +const ws = await wsmod.connect(url); + +hv.log("connected to", url); +ws.send("hello from js"); + +for (let i = 1; i <= 3; ++i) { + const msg = await ws.recv(); + hv.log("recv ->", msg); + ws.send("echo " + i); +} + +ws.close(); diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp index 7a16dc1e0..7187cf0bd 100644 --- a/http/server/HttpJsHandler.cpp +++ b/http/server/HttpJsHandler.cpp @@ -3,305 +3,33 @@ #include "HttpJsHandler.h" #include -#include -#include -#include #include #include -#include #include #include #include -#include -#include - -#include #include "EventLoop.h" #include "hfile.h" #include "hlog.h" #include "hpath.h" #include "hstring.h" -#include "htime.h" -#include "hversion.h" -#ifdef HVJS_WITH_HTTP -#include "AsyncHttpClient.h" -#include "WebSocketClient.h" -#endif -#ifdef HVJS_WITH_REDIS -#include "AsyncRedisClient.h" -#endif -#ifdef HVJS_WITH_MQTT -#include "mqtt_client.h" -#endif +#include "hvjs.h" namespace hv { namespace { -struct JsHttpTask; - -static const int JS_HTTP_METHOD_REQUEST = -1; - -struct JsHttpTask { - JSRuntime* rt; - JSContext* js; - hloop_t* loop; - EventLoopPtr loop_ptr; +struct JsHttpTask : public hv::js::HvJsTask { HttpContextPtr ctx; - JSValue promise; bool async; - bool finished; - bool in_call; - bool closing; - int refcount; - std::string error; - - JsHttpTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), async(false), finished(false), in_call(false), closing(false), refcount(1) {} -}; - -struct JsPromiseOp { - JsHttpTask* task; - JSValue resolve; - JSValue reject; - bool completed; - bool defer_delete; - JsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} - - virtual ~JsPromiseOp() {} -}; - -struct JsSleep : public JsPromiseOp { - htimer_t* timer; - TimerID timer_id; - - JsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} + JsHttpTask() : async(false) {} }; -struct JsImmediatePromise : public JsPromiseOp {}; - -static std::mutex& js_class_id_mutex() { - static std::mutex mutex; - return mutex; -} - -static void js_new_class_id(JSClassID* class_id) { - std::lock_guard lock(js_class_id_mutex()); - JS_NewClassID(class_id); -} - -static void task_ref(JsHttpTask* task) { - ++task->refcount; -} - -static void task_unref(JsHttpTask* task) { - if (--task->refcount != 0) return; - task->closing = true; - if (!JS_IsUndefined(task->promise)) { - JS_FreeValue(task->js, task->promise); - task->promise = JS_UNDEFINED; - } - if (task->js) { - if (task->rt) { - JS_RunGC(task->rt); - } - JS_FreeContext(task->js); - task->js = NULL; - } - if (task->rt) { - JS_RunGC(task->rt); - JS_FreeRuntime(task->rt); - task->rt = NULL; - } - delete task; -} - -static void drain_jobs(JsHttpTask* task); -static std::string js_to_string(JSContext* ctx, JSValueConst value); -static std::string js_exception_string(JSContext* ctx); -static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok); - -static void drain_event_cb(hevent_t* ev) { - JsHttpTask* task = (JsHttpTask*)hevent_userdata(ev); - drain_jobs(task); - task_unref(task); -} - -static void schedule_drain(JsHttpTask* task) { - if (task == NULL || task->closing) return; - task_ref(task); - if (task->loop_ptr) { - task->loop_ptr->queueInLoop([task]() { - drain_jobs(task); - task_unref(task); - }); - } - else if (task->loop) { - hevent_t ev; - memset(&ev, 0, sizeof(ev)); - ev.cb = drain_event_cb; - ev.userdata = task; - hloop_post_event(task->loop, &ev); - } - else { - task_unref(task); - } -} - -template static JSValue js_new_promise(JSContext* js, JsHttpTask* task, T** out) { - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; - T* op = new T(); - op->task = task; - op->resolve = funcs[0]; - op->reject = funcs[1]; - task_ref(task); - *out = op; - return promise; -} - -static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok) { - JsHttpTask* task = op->task; - if (op->completed) { - JS_FreeValue(task->js, value); - return; - } - op->completed = true; - if (!task->closing) { - JSValue func = ok ? op->resolve : op->reject; - JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); - if (JS_IsException(ret) && task->error.empty()) { - task->error = js_exception_string(task->js); - } - JS_FreeValue(task->js, ret); - JS_FreeValue(task->js, value); - JS_FreeValue(task->js, op->resolve); - JS_FreeValue(task->js, op->reject); - op->resolve = JS_UNDEFINED; - op->reject = JS_UNDEFINED; - if (task->in_call) { - op->defer_delete = true; - schedule_drain(task); - return; - } - schedule_drain(task); - } - else { - JS_FreeValue(task->js, value); - JS_FreeValue(task->js, op->resolve); - JS_FreeValue(task->js, op->reject); - op->resolve = JS_UNDEFINED; - op->reject = JS_UNDEFINED; - } - delete op; - task_unref(task); -} - -static void js_promise_resolve(JsPromiseOp* op, JSValue value) { - js_promise_complete(op, value, true); -} - -static void js_promise_reject(JsPromiseOp* op, const char* message) { - js_promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); -} - -static JSValue js_rejected_promise(JSContext* js, const char* message) { - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; - JSValue reason = JS_NewString(js, message ? message : "error"); - JSValue ret = JS_Call(js, funcs[1], JS_UNDEFINED, 1, &reason); - JS_FreeValue(js, ret); - JS_FreeValue(js, reason); - JS_FreeValue(js, funcs[0]); - JS_FreeValue(js, funcs[1]); - return promise; -} - -static JSValue js_async_resolved_promise(JSContext* js, JsHttpTask* task, JSValue value) { - if (task == NULL) { - JS_FreeValue(js, value); - return JS_ThrowInternalError(js, "invalid HttpJsHandler task"); - } - JsImmediatePromise* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) { - JS_FreeValue(js, value); - return promise; - } - js_promise_resolve(op, value); - return promise; -} - -static void js_finish_deferred_op(JsPromiseOp* op) { - if (op == NULL || !op->completed || !op->defer_delete) return; - JsHttpTask* task = op->task; - delete op; - task_unref(task); -} - -static std::string js_to_string(JSContext* ctx, JSValueConst value) { - size_t len = 0; - const char* str = JS_ToCStringLen(ctx, &len, value); - if (str == NULL) return std::string(); - std::string out(str, len); - JS_FreeCString(ctx, str); - return out; -} - -static bool js_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out) { - *out = JS_UNDEFINED; - if (!JS_IsObject(obj)) return false; - *out = JS_GetPropertyStr(js, obj, name); - return !JS_IsUndefined(*out) && !JS_IsException(*out); -} - -static std::string js_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue = "") { - JSValue value; - if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { - if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); - return defvalue; - } - std::string out = js_to_string(js, value); - JS_FreeValue(js, value); - return out; -} - -static int js_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue = 0) { - JSValue value; - if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { - if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); - return defvalue; - } - int32_t out = defvalue; - JS_ToInt32(js, &out, value); - JS_FreeValue(js, value); - return out; -} - -static bool js_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue = false) { - JSValue value; - if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { - if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); - return defvalue; - } - bool out = JS_ToBool(js, value) != 0; - JS_FreeValue(js, value); - return out; -} - -static std::string js_exception_string(JSContext* ctx) { - JSValue exception = JS_GetException(ctx); - std::string msg = js_to_string(ctx, exception); - JS_FreeValue(ctx, exception); - return msg.empty() ? "javascript exception" : msg; -} - static JsHttpTask* js_get_task(JSContext* js) { - return (JsHttpTask*)JS_GetContextOpaque(js); + return static_cast(hv::js::hvjs_get_task(js)); } static JSValue js_ctx_method(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { @@ -333,8 +61,8 @@ static JSValue js_ctx_query(JSContext* js, JSValueConst this_val, int argc, JSVa if (task == NULL || !task->ctx) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); - std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string key = argc > 0 ? hv::js::hvjs_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? hv::js::hvjs_to_string(js, argv[1]) : std::string(); std::string value = task->ctx->param(key.c_str(), defvalue); return JS_NewStringLen(js, value.data(), value.size()); } @@ -345,8 +73,8 @@ static JSValue js_ctx_header(JSContext* js, JSValueConst this_val, int argc, JSV if (task == NULL || !task->ctx) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); - std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string key = argc > 0 ? hv::js::hvjs_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? hv::js::hvjs_to_string(js, argv[1]) : std::string(); std::string value = task->ctx->header(key.c_str(), defvalue); return JS_NewStringLen(js, value.data(), value.size()); } @@ -381,8 +109,8 @@ static JSValue js_ctx_set_header(JSContext* js, JSValueConst this_val, int argc, if (task == NULL || !task->ctx || argc < 2) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string key = js_to_string(js, argv[0]); - std::string value = js_to_string(js, argv[1]); + std::string key = hv::js::hvjs_to_string(js, argv[0]); + std::string value = hv::js::hvjs_to_string(js, argv[1]); task->ctx->setHeader(key.c_str(), value); return JS_UNDEFINED; } @@ -393,7 +121,7 @@ static JSValue js_ctx_text(JSContext* js, JSValueConst this_val, int argc, JSVal if (task == NULL || !task->ctx || argc < 1) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string text = js_to_string(js, argv[0]); + std::string text = hv::js::hvjs_to_string(js, argv[0]); task->ctx->response->String(text); return JS_NewInt32(js, task->ctx->response->status_code); } @@ -406,7 +134,7 @@ static JSValue js_ctx_json(JSContext* js, JSValueConst this_val, int argc, JSVal } JSValue json = JS_JSONStringify(js, argv[0], JS_UNDEFINED, JS_UNDEFINED); if (JS_IsException(json)) return json; - std::string body = js_to_string(js, json); + std::string body = hv::js::hvjs_to_string(js, json); JS_FreeValue(js, json); task->ctx->response->SetContentType(APPLICATION_JSON); task->ctx->response->body = body; @@ -432,1128 +160,8 @@ static JSValue js_new_ctx(JSContext* js, const HttpContextPtr& ctx) { static void task_finish(JsHttpTask* task, JSValue result); -static void drain_jobs(JsHttpTask* task) { - JSContext* job_ctx = NULL; - while (JS_IsJobPending(task->rt)) { - int rc = JS_ExecutePendingJob(task->rt, &job_ctx); - if (rc < 0) { - task->error = js_exception_string(job_ctx ? job_ctx : task->js); - break; - } - } - if (!task->finished && !JS_IsUndefined(task->promise)) { - JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); - if (state != JS_PROMISE_PENDING) { - JSValue value = JS_PromiseResult(task->js, task->promise); - task_finish(task, value); - return; - } - } - if (!task->error.empty()) { - task_finish(task, JS_UNDEFINED); - } -} - -static void sleep_timer_cb(htimer_t* timer) { - JsSleep* sleep = (JsSleep*)hevent_userdata(timer); - js_promise_resolve(sleep, JS_UNDEFINED); -} - -static JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = (JsHttpTask*)JS_GetContextOpaque(js); - if (task == NULL || argc < 1) return JS_EXCEPTION; - int32_t ms = 0; - if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; - - JsSleep* sleep = new JsSleep(); - sleep->task = task; - sleep->resolve = funcs[0]; - JS_FreeValue(js, funcs[1]); - task_ref(task); - if (task->loop_ptr) { - sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { js_promise_resolve(sleep, JS_UNDEFINED); }); - } - else { - sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); - if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); - } - if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { - task_unref(task); - JS_FreeValue(js, sleep->resolve); - delete sleep; - JS_FreeValue(js, promise); - return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); - } - return promise; -} - -static JSValue js_hv_version(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - (void)argc; - (void)argv; - return JS_NewString(js, HV_VERSION_STRING); -} - -static JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - std::string line; - for (int i = 0; i < argc; ++i) { - if (i != 0) line += "\\t"; - line += js_to_string(js, argv[i]); - } - hlogi("%s", line.c_str()); - return JS_UNDEFINED; -} -#ifdef HVJS_WITH_HTTP -struct JsHttpRequest : public JsPromiseOp { - std::shared_ptr client; -}; - -static JSValue js_push_headers(JSContext* js, const http_headers& headers) { - JSValue obj = JS_NewObject(js); - for (auto& kv : headers) { - JS_SetPropertyStr(js, obj, kv.first.c_str(), JS_NewStringLen(js, kv.second.data(), kv.second.size())); - } - return obj; -} - -static JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { - JSValue obj = JS_NewObject(js); - JS_SetPropertyStr(js, obj, "status", JS_NewInt32(js, resp ? resp->status_code : 0)); - if (resp) { - JS_SetPropertyStr(js, obj, "body", JS_NewStringLen(js, resp->body.data(), resp->body.size())); - JS_SetPropertyStr(js, obj, "headers", js_push_headers(js, resp->headers)); - } - else { - JS_SetPropertyStr(js, obj, "body", JS_NewString(js, "")); - JS_SetPropertyStr(js, obj, "headers", JS_NewObject(js)); - } - return obj; -} - -static int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { - if (argc <= url_index) { - JS_ThrowTypeError(js, "missing url"); - return -1; - } - std::string url = js_to_string(js, argv[url_index]); - auto req = std::make_shared(); - req->method = method; - req->url = url; - if (argc > url_index + 1 && !JS_IsUndefined(argv[url_index + 1]) && !JS_IsNull(argv[url_index + 1])) { - std::string body = js_to_string(js, argv[url_index + 1]); - req->body = body; - } - if (argc > url_index + 2 && JS_IsObject(argv[url_index + 2])) { - JSPropertyEnum* tab = NULL; - uint32_t len = 0; - if (JS_GetOwnPropertyNames(js, &tab, &len, argv[url_index + 2], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { - for (uint32_t i = 0; i < len; ++i) { - JSValue key = JS_AtomToString(js, tab[i].atom); - JSValue value = JS_GetProperty(js, argv[url_index + 2], tab[i].atom); - std::string k = js_to_string(js, key); - std::string v = js_to_string(js, value); - if (!k.empty()) req->headers[k] = v; - JS_FreeValue(js, value); - JS_FreeValue(js, key); - } - JS_FreePropertyEnum(js, tab, len); - } - } - *out = req; - return 0; -} - -static JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || !task->loop_ptr) { - return js_rejected_promise(js, "hv.http: no shared event loop on this thread"); - } - http_method method = (http_method)magic; - int url_index = 0; - if (magic == JS_HTTP_METHOD_REQUEST) { - if (argc < 2) return js_rejected_promise(js, "hv.http: request needs method and url"); - std::string m = js_to_string(js, argv[0]); - toupper(m); - method = http_method_enum(m.c_str()); - url_index = 1; - } - if (method == HTTP_CUSTOM_METHOD) { - return js_rejected_promise(js, "hv.http: unsupported method"); - } - - HttpRequestPtr req; - if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { - return JS_EXCEPTION; - } - - JsHttpRequest* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->client = std::make_shared(task->loop_ptr); - std::shared_ptr client = op->client; - task->in_call = true; - int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { - if (op->task->loop_ptr) { - op->task->loop_ptr->queueInLoop([client]() {}); - } - JSContext* js = op->task->js; - if (resp) { - js_promise_resolve(op, js_push_http_response(js, resp)); - } - else { - js_promise_reject(op, "hv.http: request failed"); - } - }); - if (ret != 0) { - js_promise_reject(op, "hv.http: request failed"); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_require_http(JSContext* js) { - JSValue http = JS_NewObject(js); - JS_SetPropertyStr(js, http, "request", JS_NewCFunctionMagic(js, js_http_request, "request", 2, JS_CFUNC_generic_magic, JS_HTTP_METHOD_REQUEST)); - JS_SetPropertyStr(js, http, "get", JS_NewCFunctionMagic(js, js_http_request, "get", 1, JS_CFUNC_generic_magic, HTTP_GET)); - JS_SetPropertyStr(js, http, "post", JS_NewCFunctionMagic(js, js_http_request, "post", 2, JS_CFUNC_generic_magic, HTTP_POST)); - JS_SetPropertyStr(js, http, "put", JS_NewCFunctionMagic(js, js_http_request, "put", 2, JS_CFUNC_generic_magic, HTTP_PUT)); - JS_SetPropertyStr(js, http, "delete", JS_NewCFunctionMagic(js, js_http_request, "delete", 1, JS_CFUNC_generic_magic, HTTP_DELETE)); - return http; -} -#endif -#ifdef HVJS_WITH_REDIS -static JSClassID s_redis_class_id; -static std::once_flag s_redis_class_once; - -struct JsRedisState { - std::shared_ptr client; - bool destroyed; - - JsRedisState() : destroyed(false) {} - - ~JsRedisState() { - destroyed = true; - if (client) { - client->stop(true); - client.reset(); - } - } -}; - -struct JsRedisClient { - std::shared_ptr state; -}; - -struct JsRedisCommand : public JsPromiseOp { - std::shared_ptr redis; -}; - -static void js_redis_finalizer(JSRuntime* rt, JSValue val) { - (void)rt; - JsRedisClient* box = (JsRedisClient*)JS_GetOpaque(val, s_redis_class_id); - if (box) { - delete box; - } -} - -static JsRedisClient* js_redis_client(JSContext* js, JSValueConst this_val) { - JsRedisClient* box = (JsRedisClient*)JS_GetOpaque2(js, this_val, s_redis_class_id); - return box; -} - -static void js_redis_register_class(JSContext* js) { - std::call_once(s_redis_class_once, []() { js_new_class_id(&s_redis_class_id); }); - JSRuntime* rt = JS_GetRuntime(js); - if (!JS_IsRegisteredClass(rt, s_redis_class_id)) { - JSClassDef def; - memset(&def, 0, sizeof(def)); - def.class_name = "hv.redis.client"; - def.finalizer = js_redis_finalizer; - JS_NewClass(rt, s_redis_class_id, &def); - } -} - -static JSValue js_push_redis_reply(JSContext* js, const RedisReply& reply) { - switch (reply.type) { - case REDIS_REPLY_STRING: return JS_NewStringLen(js, reply.str.data(), reply.str.size()); - case REDIS_REPLY_INTEGER: return JS_NewInt64(js, reply.integer); - case REDIS_REPLY_ARRAY: { - if (reply.null_array) return JS_NULL; - JSValue arr = JS_NewArray(js); - for (uint32_t i = 0; i < reply.elements.size(); ++i) { - JSValue item = reply.elements[i].isNil() ? JS_NULL : js_push_redis_reply(js, reply.elements[i]); - JS_SetPropertyUint32(js, arr, i, item); - } - return arr; - } - case REDIS_REPLY_NIL: - default: return JS_NULL; - } -} - -static void js_redis_resolve_result(JsRedisCommand* op, const RedisResult& result) { - JSContext* js = op->task->js; - if (!op->redis || op->redis->destroyed) { - js_promise_reject(op, "hv.redis: client closed"); - return; - } - if (result.code != 0) { - char err[64]; - snprintf(err, sizeof(err), "hv.redis: request failed (%d)", result.code); - js_promise_reject(op, err); - return; - } - if (result.reply.isError()) { - js_promise_reject(op, result.reply.error().c_str()); - return; - } - js_promise_resolve(op, js_push_redis_reply(js, result.reply)); -} - -static bool js_build_redis_command(JSContext* js, JSValueConst* argv, int argc, int first, RedisCommand* cmd) { - if (argc <= first) return false; - if (JS_IsArray(js, argv[first]) && argc == first + 1) { - JSValue lenv = JS_GetPropertyStr(js, argv[first], "length"); - uint32_t len = 0; - JS_ToUint32(js, &len, lenv); - JS_FreeValue(js, lenv); - for (uint32_t i = 0; i < len; ++i) { - JSValue item = JS_GetPropertyUint32(js, argv[first], i); - cmd->push_back(js_to_string(js, item)); - JS_FreeValue(js, item); - } - } - else { - for (int i = first; i < argc; ++i) { - cmd->push_back(js_to_string(js, argv[i])); - } - } - return !cmd->empty(); -} - -static const char* js_redis_verb_name(int magic) { - switch (magic) { - case 1: return "GET"; - case 2: return "SET"; - case 3: return "DEL"; - case 4: return "INCR"; - case 5: return "DECR"; - case 6: return "EXPIRE"; - case 7: return "EXISTS"; - default: return NULL; - } -} - -static JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { - JsRedisClient* box = js_redis_client(js, this_val); - JsRedisState* state = box ? box->state.get() : NULL; - if (state == NULL || !state->client || state->destroyed) { - return js_rejected_promise(js, "hv.redis: client closed"); - } - RedisCommand cmd; - if (magic != 0) { - const char* verb = js_redis_verb_name(magic); - if (verb == NULL) { - return js_rejected_promise(js, "hv.redis: unknown command"); - } - cmd.push_back(verb); - for (int i = 0; i < argc; ++i) { - cmd.push_back(js_to_string(js, argv[i])); - } - } - else if (!js_build_redis_command(js, argv, argc, 0, &cmd)) { - return js_rejected_promise(js, "hv.redis: empty or invalid command"); - } - - JsHttpTask* task = js_get_task(js); - JsRedisCommand* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->redis = box->state; - task->in_call = true; - int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); - if (ret != 0) { - js_promise_reject(op, "hv.redis: request failed"); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_redis_new(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || !task->loop_ptr) { - return JS_ThrowTypeError(js, "hv.redis: no shared event loop on this thread"); - } - js_redis_register_class(js); - - std::string host = "127.0.0.1"; - int port = 6379; - std::string auth; - int db = 0; - int timeout = 0; - if (argc > 0 && JS_IsObject(argv[0])) { - host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); - port = js_get_int_property(js, argv[0], "port", 6379); - auth = js_get_string_property(js, argv[0], "auth", ""); - db = js_get_int_property(js, argv[0], "db", 0); - timeout = js_get_int_property(js, argv[0], "timeout", 0); - } - - JSValue obj = JS_NewObjectClass(js, s_redis_class_id); - if (JS_IsException(obj)) return obj; - JsRedisClient* box = new JsRedisClient(); - box->state = std::make_shared(); - box->state->client = std::make_shared(task->loop_ptr); - box->state->client->setHost(host); - box->state->client->setPort(port); - if (!auth.empty()) box->state->client->setAuth(auth); - if (db > 0) box->state->client->setDb(db); - if (timeout > 0) box->state->client->setTimeout(timeout); - box->state->client->start(false); - JS_SetOpaque(obj, box); - - JS_SetPropertyStr(js, obj, "command", JS_NewCFunctionMagic(js, js_redis_command, "command", 1, JS_CFUNC_generic_magic, 0)); - static const char* verbs[] = {"GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL}; - for (int i = 0; verbs[i]; ++i) { - std::string name = verbs[i]; - for (char& c : name) c = (char)::tolower((unsigned char)c); - JS_SetPropertyStr(js, obj, name.c_str(), JS_NewCFunctionMagic(js, js_redis_command, name.c_str(), 1, JS_CFUNC_generic_magic, i + 1)); - } - return obj; -} - -static JSValue js_require_redis(JSContext* js) { - JSValue redis = JS_NewObject(js); - JS_SetPropertyStr(js, redis, "new", JS_NewCFunction(js, js_redis_new, "new", 1)); - return redis; -} -#endif -#ifdef HVJS_WITH_HTTP -static JSClassID s_ws_class_id; -static std::once_flag s_ws_class_once; - -struct JsWsState { - std::shared_ptr client; - std::deque inbox; - JsPromiseOp* connect_op; - JsPromiseOp* recv_op; - bool js_alive; - bool connected; - bool closed; - - JsWsState() : connect_op(NULL), recv_op(NULL), js_alive(false), connected(false), closed(false) {} - - void detach() { - closed = true; - connected = false; - if (client) { - client->onopen = NULL; - client->onmessage = NULL; - client->onclose = NULL; - client->close(); - client.reset(); - } - } - - ~JsWsState() { detach(); } -}; - -struct JsWsClient { - std::shared_ptr state; -}; - -struct JsWsConnect : public JsPromiseOp { - std::shared_ptr state; -}; - -struct JsWsRecv : public JsPromiseOp { - std::shared_ptr state; -}; - -static JsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { - return (JsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); -} - -static void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { - if (!state) return; - if (loop) { - loop->queueInLoop([state]() { state->detach(); }); - } - else { - state->detach(); - } -} - -static void js_ws_finalizer(JSRuntime* rt, JSValue val) { - (void)rt; - JsWsClient* box = (JsWsClient*)JS_GetOpaque(val, s_ws_class_id); - if (box && box->state) { - box->state->js_alive = false; - if (box->state->connect_op == NULL && box->state->recv_op == NULL) { - box->state->detach(); - } - } - delete box; -} - -static void js_ws_register_class(JSContext* js) { - std::call_once(s_ws_class_once, []() { js_new_class_id(&s_ws_class_id); }); - JSRuntime* rt = JS_GetRuntime(js); - if (!JS_IsRegisteredClass(rt, s_ws_class_id)) { - JSClassDef def; - memset(&def, 0, sizeof(def)); - def.class_name = "hv.ws.client"; - def.finalizer = js_ws_finalizer; - JS_NewClass(rt, s_ws_class_id, &def); - } -} - -static void js_ws_try_deliver(const std::shared_ptr& state) { - if (!state || state->recv_op == NULL) return; - JsWsRecv* op = static_cast(state->recv_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - if (!state->inbox.empty()) { - std::string msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - state->recv_op = NULL; - js_promise_resolve(op, JS_NewStringLen(op->task->js, msg.data(), msg.size())); - } - else if (state->closed) { - state->recv_op = NULL; - js_promise_reject(op, "closed"); - } - if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_ws_detach_after_callback(loop, hold); - } -} - -static JSValue js_ws_send(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsWsClient* box = js_ws_client(js, this_val); - JsWsState* state = box ? box->state.get() : NULL; - if (state == NULL || !state->client || !state->connected) { - return JS_ThrowTypeError(js, "hv.ws: closed"); - } - std::string msg = argc > 0 ? js_to_string(js, argv[0]) : std::string(); - enum ws_opcode opcode = WS_OPCODE_TEXT; - if (argc > 1 && js_to_string(js, argv[1]) == "binary") { - opcode = WS_OPCODE_BINARY; - } - int ret = state->client->send(msg.data(), (int)msg.size(), opcode); - if (ret < 0) { - return JS_ThrowInternalError(js, "hv.ws: send failed"); - } - return JS_NewInt32(js, ret); -} - -static JSValue js_ws_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsWsClient* box = js_ws_client(js, this_val); - JsWsState* state = box ? box->state.get() : NULL; - if (state == NULL || !state->client) { - return js_rejected_promise(js, "closed"); - } - if (!state->inbox.empty()) { - std::string msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - return js_async_resolved_promise(js, js_get_task(js), JS_NewStringLen(js, msg.data(), msg.size())); - } - if (state->closed || !state->connected) { - return js_rejected_promise(js, "closed"); - } - if (state->recv_op != NULL) { - return js_rejected_promise(js, "hv.ws: recv already pending"); - } - JsHttpTask* task = js_get_task(js); - JsWsRecv* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->state = box->state; - state->recv_op = op; - return promise; -} - -static JSValue js_ws_close(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsWsClient* box = js_ws_client(js, this_val); - if (box && box->state) { - std::shared_ptr state = box->state; - if (state->connect_op) { - JsPromiseOp* op = state->connect_op; - state->connect_op = NULL; - js_promise_reject(op, "closed"); - } - if (state->recv_op) { - JsPromiseOp* op = state->recv_op; - state->recv_op = NULL; - js_promise_reject(op, "closed"); - } - state->detach(); - } - return JS_UNDEFINED; -} - -static JSValue js_ws_new_client_object(JSContext* js, const std::shared_ptr& state) { - JSValue obj = JS_NewObjectClass(js, s_ws_class_id); - if (JS_IsException(obj)) return obj; - JsWsClient* box = new JsWsClient(); - box->state = state; - state->js_alive = true; - JS_SetOpaque(obj, box); - JS_SetPropertyStr(js, obj, "send", JS_NewCFunction(js, js_ws_send, "send", 1)); - JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_ws_recv, "recv", 0)); - JS_SetPropertyStr(js, obj, "close", JS_NewCFunction(js, js_ws_close, "close", 0)); - return obj; -} - -static JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || !task->loop_ptr) { - return js_rejected_promise(js, "hv.ws: no shared event loop on this thread"); - } - if (argc < 1) { - return js_rejected_promise(js, "hv.ws: connect needs url"); - } - std::string url = js_to_string(js, argv[0]); - js_ws_register_class(js); - std::shared_ptr state = std::make_shared(); - state->client = std::make_shared(task->loop_ptr); - - JsWsConnect* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - state->connect_op = op; - op->state = state; - state->client->onopen = [state]() { - state->connected = true; - state->closed = false; - if (state->connect_op) { - JsWsConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - state->connect_op = NULL; - JSValue obj = js_ws_new_client_object(op->task->js, hold); - if (JS_IsException(obj)) { - js_promise_reject(op, "hv.ws: create client failed"); - js_ws_detach_after_callback(loop, hold); - } - else { - js_promise_resolve(op, obj); - } - } - }; - state->client->onmessage = [state](const std::string& msg) { - state->inbox.push_back(msg); - js_ws_try_deliver(state); - }; - state->client->onclose = [state]() { - state->connected = false; - state->closed = true; - if (state->connect_op) { - JsWsConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - state->connect_op = NULL; - js_promise_reject(op, "closed"); - js_ws_detach_after_callback(loop, hold); - } - js_ws_try_deliver(state); - }; - task->in_call = true; - int ret = state->client->open(url.c_str()); - if (ret != 0) { - state->connect_op = NULL; - js_promise_reject(op, "hv.ws: open failed"); - state->detach(); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_require_ws(JSContext* js) { - JSValue ws = JS_NewObject(js); - JS_SetPropertyStr(js, ws, "connect", JS_NewCFunction(js, js_ws_connect, "connect", 1)); - return ws; -} -#endif -#ifdef HVJS_WITH_MQTT -static JSClassID s_mqtt_class_id; -static std::once_flag s_mqtt_class_once; - -struct JsMqttMessage { - std::string topic; - std::string payload; - int qos; -}; - -struct JsMqttState { - mqtt_client_t* client; - std::deque inbox; - JsPromiseOp* connect_op; - JsPromiseOp* recv_op; - bool js_alive; - bool closed; - bool reconnect; - - JsMqttState() : client(NULL), connect_op(NULL), recv_op(NULL), js_alive(false), closed(false), reconnect(false) {} - - void detach() { - closed = true; - if (client) { - mqtt_client_set_callback(client, NULL); - mqtt_client_set_userdata(client, NULL); - mqtt_client_free(client); - client = NULL; - } - } - - ~JsMqttState() { detach(); } -}; - -struct JsMqttClient { - std::shared_ptr state; -}; - -struct JsMqttConnect : public JsPromiseOp { - std::shared_ptr state; -}; - -struct JsMqttRecv : public JsPromiseOp { - std::shared_ptr state; -}; - -struct JsMqttDetachEvent { - std::shared_ptr state; -}; - -static JsMqttClient* js_mqtt_client(JSContext* js, JSValueConst this_val) { - return (JsMqttClient*)JS_GetOpaque2(js, this_val, s_mqtt_class_id); -} - -static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); - -static void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { - (void)rt; - JsMqttClient* box = (JsMqttClient*)JS_GetOpaque(val, s_mqtt_class_id); - if (box && box->state) { - box->state->js_alive = false; - if (box->state->connect_op == NULL && box->state->recv_op == NULL) { - box->state->detach(); - } - } - delete box; -} - -static void js_mqtt_register_class(JSContext* js) { - std::call_once(s_mqtt_class_once, []() { js_new_class_id(&s_mqtt_class_id); }); - JSRuntime* rt = JS_GetRuntime(js); - if (!JS_IsRegisteredClass(rt, s_mqtt_class_id)) { - JSClassDef def; - memset(&def, 0, sizeof(def)); - def.class_name = "hv.mqtt.client"; - def.finalizer = js_mqtt_finalizer; - JS_NewClass(rt, s_mqtt_class_id, &def); - } -} - -static JSValue js_mqtt_new_client_object(JSContext* js, const std::shared_ptr& state) { - JSValue obj = JS_NewObjectClass(js, s_mqtt_class_id); - if (JS_IsException(obj)) return obj; - JsMqttClient* box = new JsMqttClient(); - box->state = state; - state->js_alive = true; - JS_SetOpaque(obj, box); - JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_mqtt_recv, "recv", 0)); - JS_SetPropertyStr(js, obj, "publish", JS_NewCFunction(js, js_mqtt_publish, "publish", 2)); - JS_SetPropertyStr(js, obj, "subscribe", JS_NewCFunction(js, js_mqtt_subscribe, "subscribe", 1)); - JS_SetPropertyStr(js, obj, "unsubscribe", JS_NewCFunction(js, js_mqtt_unsubscribe, "unsubscribe", 1)); - JS_SetPropertyStr(js, obj, "disconnect", JS_NewCFunction(js, js_mqtt_disconnect, "disconnect", 0)); - return obj; -} - -static JSValue js_push_mqtt_message(JSContext* js, const JsMqttMessage& msg) { - JSValue obj = JS_NewObject(js); - JS_SetPropertyStr(js, obj, "topic", JS_NewStringLen(js, msg.topic.data(), msg.topic.size())); - JS_SetPropertyStr(js, obj, "payload", JS_NewStringLen(js, msg.payload.data(), msg.payload.size())); - JS_SetPropertyStr(js, obj, "qos", JS_NewInt32(js, msg.qos)); - return obj; -} - -static const char* js_mqtt_closed_reason(const JsMqttState* state) { - return state && state->reconnect ? "reconnecting" : "closed"; -} - -static void js_mqtt_try_deliver(JsMqttState* state) { - if (!state || state->recv_op == NULL) return; - JsMqttRecv* op = static_cast(state->recv_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - if (!state->inbox.empty()) { - JsMqttMessage msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - state->recv_op = NULL; - js_promise_resolve(op, js_push_mqtt_message(op->task->js, msg)); - } - else if (state->closed) { - state->recv_op = NULL; - js_promise_reject(op, js_mqtt_closed_reason(state)); - } - if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } -} - -static void js_mqtt_detach_event_cb(hevent_t* ev) { - JsMqttDetachEvent* detach = (JsMqttDetachEvent*)hevent_userdata(ev); - if (detach) { - detach->state->detach(); - delete detach; - } -} - -static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { - if (!state) return; - state->reconnect = false; - if (state->client) { - mqtt_client_set_reconnect(state->client, NULL); - } - if (loop) { - loop->queueInLoop([state]() { state->detach(); }); - } - else if (raw_loop) { - JsMqttDetachEvent* detach = new JsMqttDetachEvent(); - detach->state = state; - hevent_t ev; - memset(&ev, 0, sizeof(ev)); - ev.cb = js_mqtt_detach_event_cb; - ev.userdata = detach; - hloop_post_event(raw_loop, &ev); - } - else { - state->detach(); - } -} - -static void js_mqtt_on_event(mqtt_client_t* client, int type) { - JsMqttState* state = (JsMqttState*)mqtt_client_get_userdata(client); - if (state == NULL) return; - switch (type) { - case MQTT_TYPE_CONNACK: - state->closed = false; - if (state->connect_op) { - JsMqttConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - state->connect_op = NULL; - JSValue obj = js_mqtt_new_client_object(op->task->js, hold); - if (JS_IsException(obj)) { - js_promise_reject(op, "hv.mqtt: create client failed"); - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } - else { - js_promise_resolve(op, obj); - } - } - break; - case MQTT_TYPE_PUBLISH: { - JsMqttMessage msg; - if (client->message.topic && client->message.topic_len > 0) { - msg.topic.assign(client->message.topic, client->message.topic_len); - } - if (client->message.payload && client->message.payload_len > 0) { - msg.payload.assign(client->message.payload, client->message.payload_len); - } - msg.qos = client->message.qos; - state->inbox.push_back(std::move(msg)); - js_mqtt_try_deliver(state); - break; - } - case MQTT_TYPE_DISCONNECT: - state->closed = true; - if (state->connect_op) { - JsMqttConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - state->connect_op = NULL; - js_promise_reject(op, "connect failed"); - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } - if (state->recv_op) { - JsMqttRecv* op = static_cast(state->recv_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - state->recv_op = NULL; - js_promise_reject(op, js_mqtt_closed_reason(state)); - if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } - } - break; - default: break; - } -} - -static bool js_parse_reconnect(JSContext* js, JSValueConst obj, reconn_setting_t* out) { - JSValue reconnect; - if (!js_get_property(js, obj, "reconnect", &reconnect) || !JS_IsObject(reconnect)) { - if (!JS_IsUndefined(reconnect) && !JS_IsException(reconnect)) JS_FreeValue(js, reconnect); - return false; - } - reconn_setting_init(out); - out->min_delay = (uint32_t)js_get_int_property(js, reconnect, "min_delay", (int)out->min_delay); - out->max_delay = (uint32_t)js_get_int_property(js, reconnect, "max_delay", (int)out->max_delay); - out->delay_policy = (uint32_t)js_get_int_property(js, reconnect, "delay_policy", (int)out->delay_policy); - out->max_retry_cnt = (uint32_t)js_get_int_property(js, reconnect, "max_retry", (int)out->max_retry_cnt); - if (out->max_retry_cnt == 0) out->max_retry_cnt = INFINITE; - if (out->min_delay == 0) out->min_delay = 1; - if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; - if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { - out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; - } - JS_FreeValue(js, reconnect); - return true; -} - -static JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || task->loop == NULL) { - return js_rejected_promise(js, "hv.mqtt: no event loop on this thread"); - } - if (argc < 1 || !JS_IsObject(argv[0])) { - return js_rejected_promise(js, "hv.mqtt: connect needs options"); - } - - std::string host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); - int port = js_get_int_property(js, argv[0], "port", DEFAULT_MQTT_PORT); - int ssl = js_get_bool_property(js, argv[0], "ssl", false) ? 1 : 0; - std::string id = js_get_string_property(js, argv[0], "id", ""); - std::string username = js_get_string_property(js, argv[0], "username", ""); - std::string password = js_get_string_property(js, argv[0], "password", ""); - int keepalive = js_get_int_property(js, argv[0], "keepalive", 0); - int timeout = js_get_int_property(js, argv[0], "connect_timeout", 0); - if (timeout <= 0) timeout = js_get_int_property(js, argv[0], "timeout", 0); - bool clean_session = js_get_bool_property(js, argv[0], "clean_session", true); - - js_mqtt_register_class(js); - std::shared_ptr state = std::make_shared(); - state->client = mqtt_client_new(task->loop); - if (state->client == NULL) { - return js_rejected_promise(js, "hv.mqtt: create client failed"); - } - mqtt_client_set_userdata(state->client, state.get()); - mqtt_client_set_callback(state->client, js_mqtt_on_event); - if (!id.empty()) mqtt_client_set_id(state->client, id.c_str()); - if (!username.empty() || !password.empty()) { - mqtt_client_set_auth(state->client, username.c_str(), password.c_str()); - } - if (keepalive > 0) state->client->keepalive = (unsigned short)keepalive; - state->client->clean_session = clean_session ? 1 : 0; - if (timeout > 0) mqtt_client_set_connect_timeout(state->client, timeout); - reconn_setting_t reconn; - if (js_parse_reconnect(js, argv[0], &reconn)) { - mqtt_client_set_reconnect(state->client, &reconn); - state->reconnect = true; - } - - JsMqttConnect* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) { - state->detach(); - return promise; - } - state->connect_op = op; - op->state = state; - task->in_call = true; - int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); - if (ret != 0) { - state->connect_op = NULL; - js_promise_reject(op, "hv.mqtt: connect failed"); - state->detach(); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsMqttClient* box = js_mqtt_client(js, this_val); - std::shared_ptr state = box ? box->state : std::shared_ptr(); - if (!state || state->client == NULL) { - return js_rejected_promise(js, "closed"); - } - if (!state->inbox.empty()) { - JsMqttMessage msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - return js_async_resolved_promise(js, js_get_task(js), js_push_mqtt_message(js, msg)); - } - if (state->closed) { - return js_rejected_promise(js, js_mqtt_closed_reason(state.get())); - } - if (state->recv_op != NULL) { - return js_rejected_promise(js, "hv.mqtt: recv already pending"); - } - JsHttpTask* task = js_get_task(js); - JsMqttRecv* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->state = state; - state->recv_op = op; - return promise; -} - -static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsMqttClient* box = js_mqtt_client(js, this_val); - JsMqttState* state = box ? box->state.get() : NULL; - if (state == NULL || state->client == NULL || state->closed) { - return JS_ThrowTypeError(js, "hv.mqtt: closed"); - } - if (argc < 2) { - return JS_ThrowTypeError(js, "hv.mqtt: publish needs topic and payload"); - } - std::string topic = js_to_string(js, argv[0]); - std::string payload = js_to_string(js, argv[1]); - int32_t qos = 0; - if (argc > 2 && JS_ToInt32(js, &qos, argv[2]) != 0) return JS_EXCEPTION; - int retain = argc > 3 ? JS_ToBool(js, argv[3]) : 0; - mqtt_message_t msg; - memset(&msg, 0, sizeof(msg)); - msg.topic = topic.c_str(); - msg.topic_len = (unsigned int)topic.size(); - msg.payload = payload.c_str(); - msg.payload_len = (unsigned int)payload.size(); - msg.qos = (unsigned char)qos; - msg.retain = (unsigned char)retain; - int mid = mqtt_client_publish(state->client, &msg); - if (mid < 0) { - return JS_ThrowInternalError(js, "hv.mqtt: publish failed"); - } - return JS_NewInt32(js, mid); -} - -static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsMqttClient* box = js_mqtt_client(js, this_val); - JsMqttState* state = box ? box->state.get() : NULL; - if (state == NULL || state->client == NULL || state->closed) { - return JS_ThrowTypeError(js, "hv.mqtt: closed"); - } - if (argc < 1) { - return JS_ThrowTypeError(js, "hv.mqtt: subscribe needs topic"); - } - std::string topic = js_to_string(js, argv[0]); - int32_t qos = 0; - if (argc > 1 && JS_ToInt32(js, &qos, argv[1]) != 0) return JS_EXCEPTION; - int mid = mqtt_client_subscribe(state->client, topic.c_str(), qos); - if (mid < 0) { - return JS_ThrowInternalError(js, "hv.mqtt: subscribe failed"); - } - return JS_NewInt32(js, mid); -} - -static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsMqttClient* box = js_mqtt_client(js, this_val); - JsMqttState* state = box ? box->state.get() : NULL; - if (state == NULL || state->client == NULL || state->closed) { - return JS_ThrowTypeError(js, "hv.mqtt: closed"); - } - if (argc < 1) { - return JS_ThrowTypeError(js, "hv.mqtt: unsubscribe needs topic"); - } - std::string topic = js_to_string(js, argv[0]); - int mid = mqtt_client_unsubscribe(state->client, topic.c_str()); - if (mid < 0) { - return JS_ThrowInternalError(js, "hv.mqtt: unsubscribe failed"); - } - return JS_NewInt32(js, mid); -} - -static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsMqttClient* box = js_mqtt_client(js, this_val); - if (box && box->state) { - std::shared_ptr state = box->state; - state->reconnect = false; - if (state->connect_op) { - JsPromiseOp* op = state->connect_op; - state->connect_op = NULL; - js_promise_reject(op, "closed"); - } - if (state->recv_op) { - JsPromiseOp* op = state->recv_op; - state->recv_op = NULL; - js_promise_reject(op, "closed"); - } - state->detach(); - } - return JS_UNDEFINED; -} - -static JSValue js_require_mqtt(JSContext* js) { - JSValue mqtt = JS_NewObject(js); - JS_SetPropertyStr(js, mqtt, "connect", JS_NewCFunction(js, js_mqtt_connect, "connect", 1)); - return mqtt; -} -#endif - -static JSValue js_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - if (argc < 1) { - return JS_ThrowTypeError(js, "require needs a module name"); - } - std::string name = js_to_string(js, argv[0]); - if (name == "hv") { - JSValue hv = JS_NewObject(js); - JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); - JS_SetPropertyStr(js, hv, "log", JS_NewCFunction(js, js_hv_log, "log", 1)); - JS_SetPropertyStr(js, hv, "sleep", JS_NewCFunction(js, js_hv_sleep, "sleep", 1)); - return hv; - } -#ifdef HVJS_WITH_HTTP - if (name == "hv/http") { - return js_require_http(js); - } -#endif -#ifdef HVJS_WITH_REDIS - if (name == "hv/redis") { - return js_require_redis(js); - } -#endif -#ifdef HVJS_WITH_HTTP - if (name == "hv/ws") { - return js_require_ws(js); - } -#endif -#ifdef HVJS_WITH_MQTT - if (name == "hv/mqtt") { - return js_require_mqtt(js); - } -#endif - return JS_ThrowReferenceError(js, "module '%s' is not available", name.c_str()); +static void http_js_task_finish(hv::js::HvJsTask* task, JSValue result) { + task_finish(static_cast(task), result); } static bool load_file(const std::string& filepath, std::string* out, std::string* err) { @@ -1607,19 +215,19 @@ static bool apply_result(JSContext* js, JSValueConst value, const HttpContextPtr return true; } if (JS_IsString(value)) { - std::string body = js_to_string(js, value); + std::string body = hv::js::hvjs_to_string(js, value); ctx->response->String(body); return true; } JSValue json = JS_JSONStringify(js, value, JS_UNDEFINED, JS_UNDEFINED); if (!JS_IsException(json)) { - std::string body = js_to_string(js, json); + std::string body = hv::js::hvjs_to_string(js, json); ctx->response->SetContentType(APPLICATION_JSON); ctx->response->body = body; JS_FreeValue(js, json); return true; } - if (err) *err = js_exception_string(js); + if (err) *err = hv::js::hvjs_exception_string(js); return false; } @@ -1632,7 +240,7 @@ static void task_finish(JsHttpTask* task, JSValue result) { task->ctx->response->String(task->error); } else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { - std::string err = js_to_string(task->js, result); + std::string err = hv::js::hvjs_to_string(task->js, result); hloge("[js] http handler rejected: %s", err.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; task->ctx->response->String(err); @@ -1649,7 +257,7 @@ static void task_finish(JsHttpTask* task, JSValue result) { if (task->async) { task->ctx->send(); } - task_unref(task); + hv::js::hvjs_task_unref(task); } } // namespace @@ -1712,12 +320,13 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JsHttpTask* task = new JsHttpTask(); task->ctx = ctx; + task->finish = http_js_task_finish; task->rt = JS_NewRuntime(); task->js = task->rt ? JS_NewContext(task->rt) : NULL; if (task->rt == NULL || task->js == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String("js handler: failed to create quickjs runtime"); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } JS_SetContextOpaque(task->js, task); @@ -1737,20 +346,20 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { if (task->loop == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String("js handler: no event loop on this thread"); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } JSValue global = JS_GetGlobalObject(task->js); - JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, js_require, "require", 1)); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); JSValue eval = JS_Eval(task->js, code.c_str(), code.size(), filepath_.c_str(), JS_EVAL_TYPE_GLOBAL); if (JS_IsException(eval)) { - std::string msg = js_exception_string(task->js); + std::string msg = hv::js::hvjs_exception_string(task->js); JS_FreeValue(task->js, global); ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String(msg); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } JS_FreeValue(task->js, eval); @@ -1760,7 +369,7 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JS_FreeValue(task->js, global); ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; ctx->response->String("no js handler function"); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_NOT_IMPLEMENTED; } @@ -1769,12 +378,12 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JS_FreeValue(task->js, js_ctx); JS_FreeValue(task->js, fn); if (JS_IsException(ret)) { - std::string msg = js_exception_string(task->js); + std::string msg = hv::js::hvjs_exception_string(task->js); JS_FreeValue(task->js, global); JS_FreeValue(task->js, ret); ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String(msg); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } @@ -1787,21 +396,21 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JS_FreeValue(task->js, promise_ctor); JS_FreeValue(task->js, ret); if (JS_IsException(task->promise)) { - std::string msg = js_exception_string(task->js); + std::string msg = hv::js::hvjs_exception_string(task->js); ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String(msg); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - task_ref(task); - drain_jobs(task); + hv::js::hvjs_task_ref(task); + hv::js::hvjs_drain_jobs(task); bool finished = task->finished; int status = ctx->response->status_code; if (!finished) { task->async = true; } - task_unref(task); + hv::js::hvjs_task_unref(task); if (finished) { return status; } diff --git a/js/hvjs.cpp b/js/hvjs.cpp new file mode 100644 index 000000000..7d5546251 --- /dev/null +++ b/js/hvjs.cpp @@ -0,0 +1,373 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#include + +#include + +#include "hlog.h" +#include "hversion.h" + +namespace hv { +namespace js { + +namespace { + +struct HvJsSleep : public HvJsPromiseOp { + htimer_t* timer; + TimerID timer_id; + + HvJsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} +}; + +struct HvJsImmediatePromise : public HvJsPromiseOp {}; + +std::mutex& js_class_id_mutex() { + static std::mutex mutex; + return mutex; +} + +void drain_event_cb(hevent_t* ev) { + HvJsTask* task = (HvJsTask*)hevent_userdata(ev); + hvjs_drain_jobs(task); + hvjs_task_unref(task); +} + +void promise_complete(HvJsPromiseOp* op, JSValue value, bool ok) { + HvJsTask* task = op->task; + if (op->completed) { + JS_FreeValue(task->js, value); + return; + } + op->completed = true; + if (!task->closing) { + JSValue func = ok ? op->resolve : op->reject; + JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); + if (JS_IsException(ret) && task->error.empty()) { + task->error = hvjs_exception_string(task->js); + } + JS_FreeValue(task->js, ret); + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + if (task->in_call) { + op->defer_delete = true; + hvjs_schedule_drain(task); + return; + } + hvjs_schedule_drain(task); + } + else { + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + } + delete op; + hvjs_task_unref(task); +} + +void sleep_timer_cb(htimer_t* timer) { + HvJsSleep* sleep = (HvJsSleep*)hevent_userdata(timer); + hvjs_promise_resolve(sleep, JS_UNDEFINED); +} + +JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || argc < 1) return JS_EXCEPTION; + int32_t ms = 0; + if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + + HvJsSleep* sleep = new HvJsSleep(); + sleep->task = task; + sleep->resolve = funcs[0]; + JS_FreeValue(js, funcs[1]); + hvjs_task_ref(task); + if (task->loop_ptr) { + sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { hvjs_promise_resolve(sleep, JS_UNDEFINED); }); + } + else { + sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); + if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); + } + if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { + hvjs_task_unref(task); + JS_FreeValue(js, sleep->resolve); + delete sleep; + JS_FreeValue(js, promise); + return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); + } + return promise; +} + +JSValue js_hv_version(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + return JS_NewString(js, HV_VERSION_STRING); +} + +JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + std::string line; + for (int i = 0; i < argc; ++i) { + if (i != 0) line += "\t"; + line += hvjs_to_string(js, argv[i]); + } + hlogi("%s", line.c_str()); + return JS_UNDEFINED; +} + +JSValue require_hv(JSContext* js) { + JSValue hv = JS_NewObject(js); + JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); + JS_SetPropertyStr(js, hv, "log", JS_NewCFunction(js, js_hv_log, "log", 1)); + JS_SetPropertyStr(js, hv, "sleep", JS_NewCFunction(js, js_hv_sleep, "sleep", 1)); + return hv; +} + +} // namespace + +HvJsTask::HvJsTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), finished(false), in_call(false), closing(false), refcount(1), finish(NULL) {} + +HvJsTask::~HvJsTask() {} + +HvJsPromiseOp::HvJsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} + +HvJsPromiseOp::~HvJsPromiseOp() {} + +void hvjs_task_ref(HvJsTask* task) { + ++task->refcount; +} + +void hvjs_task_unref(HvJsTask* task) { + if (--task->refcount != 0) return; + task->closing = true; + if (!JS_IsUndefined(task->promise)) { + JS_FreeValue(task->js, task->promise); + task->promise = JS_UNDEFINED; + } + if (task->js) { + if (task->rt) { + JS_RunGC(task->rt); + } + JS_FreeContext(task->js); + task->js = NULL; + } + if (task->rt) { + JS_RunGC(task->rt); + JS_FreeRuntime(task->rt); + task->rt = NULL; + } + delete task; +} + +void hvjs_schedule_drain(HvJsTask* task) { + if (task == NULL || task->closing) return; + hvjs_task_ref(task); + if (task->loop_ptr) { + task->loop_ptr->queueInLoop([task]() { + hvjs_drain_jobs(task); + hvjs_task_unref(task); + }); + } + else if (task->loop) { + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = drain_event_cb; + ev.userdata = task; + hloop_post_event(task->loop, &ev); + } + else { + hvjs_task_unref(task); + } +} + +void hvjs_drain_jobs(HvJsTask* task) { + JSContext* job_ctx = NULL; + while (JS_IsJobPending(task->rt)) { + int rc = JS_ExecutePendingJob(task->rt, &job_ctx); + if (rc < 0) { + task->error = hvjs_exception_string(job_ctx ? job_ctx : task->js); + break; + } + } + if (!task->finished && !JS_IsUndefined(task->promise)) { + JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); + if (state != JS_PROMISE_PENDING) { + JSValue value = JS_PromiseResult(task->js, task->promise); + if (task->finish) { + task->finish(task, value); + } + else { + JS_FreeValue(task->js, value); + task->finished = true; + hvjs_task_unref(task); + } + return; + } + } + if (!task->error.empty()) { + if (task->finish) { + task->finish(task, JS_UNDEFINED); + } + else { + task->finished = true; + hvjs_task_unref(task); + } + } +} + +void hvjs_promise_resolve(HvJsPromiseOp* op, JSValue value) { + promise_complete(op, value, true); +} + +void hvjs_promise_reject(HvJsPromiseOp* op, const char* message) { + promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); +} + +JSValue hvjs_rejected_promise(JSContext* js, const char* message) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + JSValue reason = JS_NewString(js, message ? message : "error"); + JSValue ret = JS_Call(js, funcs[1], JS_UNDEFINED, 1, &reason); + JS_FreeValue(js, ret); + JS_FreeValue(js, reason); + JS_FreeValue(js, funcs[0]); + JS_FreeValue(js, funcs[1]); + return promise; +} + +JSValue hvjs_async_resolved_promise(JSContext* js, HvJsTask* task, JSValue value) { + if (task == NULL) { + JS_FreeValue(js, value); + return JS_ThrowInternalError(js, "invalid hvjs task"); + } + HvJsImmediatePromise* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) { + JS_FreeValue(js, value); + return promise; + } + hvjs_promise_resolve(op, value); + return promise; +} + +void hvjs_finish_deferred_op(HvJsPromiseOp* op) { + if (op == NULL || !op->completed || !op->defer_delete) return; + HvJsTask* task = op->task; + delete op; + hvjs_task_unref(task); +} + +std::string hvjs_to_string(JSContext* ctx, JSValueConst value) { + size_t len = 0; + const char* str = JS_ToCStringLen(ctx, &len, value); + if (str == NULL) return std::string(); + std::string out(str, len); + JS_FreeCString(ctx, str); + return out; +} + +std::string hvjs_exception_string(JSContext* ctx) { + JSValue exception = JS_GetException(ctx); + std::string msg = hvjs_to_string(ctx, exception); + JS_FreeValue(ctx, exception); + return msg.empty() ? "javascript exception" : msg; +} + +bool hvjs_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out) { + *out = JS_UNDEFINED; + if (!JS_IsObject(obj)) return false; + *out = JS_GetPropertyStr(js, obj, name); + return !JS_IsUndefined(*out) && !JS_IsException(*out); +} + +std::string hvjs_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue) { + JSValue value; + if (!hvjs_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + std::string out = hvjs_to_string(js, value); + JS_FreeValue(js, value); + return out; +} + +int hvjs_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue) { + JSValue value; + if (!hvjs_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + int32_t out = defvalue; + JS_ToInt32(js, &out, value); + JS_FreeValue(js, value); + return out; +} + +bool hvjs_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue) { + JSValue value; + if (!hvjs_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + bool out = JS_ToBool(js, value) != 0; + JS_FreeValue(js, value); + return out; +} + +HvJsTask* hvjs_get_task(JSContext* js) { + return (HvJsTask*)JS_GetContextOpaque(js); +} + +void hvjs_new_class_id(JSClassID* class_id) { + std::lock_guard lock(js_class_id_mutex()); + JS_NewClassID(class_id); +} + +JSValue hvjs_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + if (argc < 1) { + return JS_ThrowTypeError(js, "require needs a module name"); + } + std::string name = hvjs_to_string(js, argv[0]); + if (name == "hv") { + return require_hv(js); + } +#ifdef HVJS_WITH_HTTP + if (name == "hv/http") { + return hvjs_require_http(js); + } +#endif +#ifdef HVJS_WITH_REDIS + if (name == "hv/redis") { + return hvjs_require_redis(js); + } +#endif +#ifdef HVJS_WITH_HTTP + if (name == "hv/ws") { + return hvjs_require_ws(js); + } +#endif +#ifdef HVJS_WITH_MQTT + if (name == "hv/mqtt") { + return hvjs_require_mqtt(js); + } +#endif + return JS_ThrowReferenceError(js, "module '%s' is not available", name.c_str()); +} + +} // namespace js +} // namespace hv + +#endif // WITH_JS diff --git a/js/hvjs.h b/js/hvjs.h new file mode 100644 index 000000000..baf6a7173 --- /dev/null +++ b/js/hvjs.h @@ -0,0 +1,93 @@ +#ifndef HV_JS_H_ +#define HV_JS_H_ + +#include + +#include + +#include "EventLoop.h" +#include "hexport.h" + +namespace hv { +namespace js { + +struct HV_EXPORT HvJsTask { + typedef void (*FinishCallback)(HvJsTask* task, JSValue result); + + JSRuntime* rt; + JSContext* js; + hloop_t* loop; + EventLoopPtr loop_ptr; + JSValue promise; + bool finished; + bool in_call; + bool closing; + int refcount; + std::string error; + FinishCallback finish; + + HvJsTask(); + virtual ~HvJsTask(); +}; + +struct HV_EXPORT HvJsPromiseOp { + HvJsTask* task; + JSValue resolve; + JSValue reject; + bool completed; + bool defer_delete; + + HvJsPromiseOp(); + virtual ~HvJsPromiseOp(); +}; + +HV_EXPORT void hvjs_task_ref(HvJsTask* task); +HV_EXPORT void hvjs_task_unref(HvJsTask* task); +HV_EXPORT void hvjs_schedule_drain(HvJsTask* task); +HV_EXPORT void hvjs_drain_jobs(HvJsTask* task); + +template JSValue hvjs_new_promise(JSContext* js, HvJsTask* task, T** out) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + T* op = new T(); + op->task = task; + op->resolve = funcs[0]; + op->reject = funcs[1]; + hvjs_task_ref(task); + *out = op; + return promise; +} + +HV_EXPORT void hvjs_promise_resolve(HvJsPromiseOp* op, JSValue value); +HV_EXPORT void hvjs_promise_reject(HvJsPromiseOp* op, const char* message); +HV_EXPORT JSValue hvjs_rejected_promise(JSContext* js, const char* message); +HV_EXPORT JSValue hvjs_async_resolved_promise(JSContext* js, HvJsTask* task, JSValue value); +HV_EXPORT void hvjs_finish_deferred_op(HvJsPromiseOp* op); + +HV_EXPORT std::string hvjs_to_string(JSContext* ctx, JSValueConst value); +HV_EXPORT std::string hvjs_exception_string(JSContext* ctx); +HV_EXPORT bool hvjs_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out); +HV_EXPORT std::string hvjs_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue = ""); +HV_EXPORT int hvjs_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue = 0); +HV_EXPORT bool hvjs_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue = false); +HV_EXPORT HvJsTask* hvjs_get_task(JSContext* js); +HV_EXPORT void hvjs_new_class_id(JSClassID* class_id); + +HV_EXPORT JSValue hvjs_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); + +#ifdef HVJS_WITH_HTTP +HV_EXPORT JSValue hvjs_require_http(JSContext* js); +HV_EXPORT JSValue hvjs_require_ws(JSContext* js); +#endif +#ifdef HVJS_WITH_REDIS +HV_EXPORT JSValue hvjs_require_redis(JSContext* js); +#endif +#ifdef HVJS_WITH_MQTT +HV_EXPORT JSValue hvjs_require_mqtt(JSContext* js); +#endif + +} // namespace js +} // namespace hv + +#endif // HV_JS_H_ diff --git a/js/hvjs_http.cpp b/js/hvjs_http.cpp new file mode 100644 index 000000000..2bfd25736 --- /dev/null +++ b/js/hvjs_http.cpp @@ -0,0 +1,402 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#ifdef HVJS_WITH_HTTP + +#include +#include + +#include +#include +#include +#include +#include + +#include "AsyncHttpClient.h" +#include "WebSocketClient.h" +#include "hstring.h" + +namespace hv { +namespace js { +namespace { + +static const int JS_HTTP_METHOD_REQUEST = -1; + +struct HvJsHttpRequest : public HvJsPromiseOp { + std::shared_ptr client; +}; + +JSValue js_push_headers(JSContext* js, const http_headers& headers) { + JSValue obj = JS_NewObject(js); + for (auto& kv : headers) { + JS_SetPropertyStr(js, obj, kv.first.c_str(), JS_NewStringLen(js, kv.second.data(), kv.second.size())); + } + return obj; +} + +JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "status", JS_NewInt32(js, resp ? resp->status_code : 0)); + if (resp) { + JS_SetPropertyStr(js, obj, "body", JS_NewStringLen(js, resp->body.data(), resp->body.size())); + JS_SetPropertyStr(js, obj, "headers", js_push_headers(js, resp->headers)); + } + else { + JS_SetPropertyStr(js, obj, "body", JS_NewString(js, "")); + JS_SetPropertyStr(js, obj, "headers", JS_NewObject(js)); + } + return obj; +} + +int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { + if (argc <= url_index) { + JS_ThrowTypeError(js, "missing url"); + return -1; + } + std::string url = hvjs_to_string(js, argv[url_index]); + auto req = std::make_shared(); + req->method = method; + req->url = url; + if (argc > url_index + 1 && !JS_IsUndefined(argv[url_index + 1]) && !JS_IsNull(argv[url_index + 1])) { + std::string body = hvjs_to_string(js, argv[url_index + 1]); + req->body = body; + } + if (argc > url_index + 2 && JS_IsObject(argv[url_index + 2])) { + JSPropertyEnum* tab = NULL; + uint32_t len = 0; + if (JS_GetOwnPropertyNames(js, &tab, &len, argv[url_index + 2], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { + for (uint32_t i = 0; i < len; ++i) { + JSValue key = JS_AtomToString(js, tab[i].atom); + JSValue value = JS_GetProperty(js, argv[url_index + 2], tab[i].atom); + std::string k = hvjs_to_string(js, key); + std::string v = hvjs_to_string(js, value); + if (!k.empty()) req->headers[k] = v; + JS_FreeValue(js, value); + JS_FreeValue(js, key); + } + JS_FreePropertyEnum(js, tab, len); + } + } + *out = req; + return 0; +} + +JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || !task->loop_ptr) { + return hvjs_rejected_promise(js, "hv.http: no shared event loop on this thread"); + } + http_method method = (http_method)magic; + int url_index = 0; + if (magic == JS_HTTP_METHOD_REQUEST) { + if (argc < 2) return hvjs_rejected_promise(js, "hv.http: request needs method and url"); + std::string m = hvjs_to_string(js, argv[0]); + toupper(m); + method = http_method_enum(m.c_str()); + url_index = 1; + } + if (method == HTTP_CUSTOM_METHOD) { + return hvjs_rejected_promise(js, "hv.http: unsupported method"); + } + + HttpRequestPtr req; + if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { + return JS_EXCEPTION; + } + + HvJsHttpRequest* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->client = std::make_shared(task->loop_ptr); + std::shared_ptr client = op->client; + task->in_call = true; + int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { + if (op->task->loop_ptr) { + op->task->loop_ptr->queueInLoop([client]() {}); + } + JSContext* js = op->task->js; + if (resp) { + hvjs_promise_resolve(op, js_push_http_response(js, resp)); + } + else { + hvjs_promise_reject(op, "hv.http: request failed"); + } + }); + if (ret != 0) { + hvjs_promise_reject(op, "hv.http: request failed"); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +static JSClassID s_ws_class_id; +static std::once_flag s_ws_class_once; + +struct HvJsWsState { + std::shared_ptr client; + std::deque inbox; + HvJsPromiseOp* connect_op; + HvJsPromiseOp* recv_op; + bool js_alive; + bool connected; + bool closed; + + HvJsWsState() : connect_op(NULL), recv_op(NULL), js_alive(false), connected(false), closed(false) {} + + void detach() { + closed = true; + connected = false; + if (client) { + client->onopen = NULL; + client->onmessage = NULL; + client->onclose = NULL; + client->close(); + client.reset(); + } + } + + ~HvJsWsState() { detach(); } +}; + +struct HvJsWsClient { + std::shared_ptr state; +}; + +struct HvJsWsConnect : public HvJsPromiseOp { + std::shared_ptr state; +}; + +struct HvJsWsRecv : public HvJsPromiseOp { + std::shared_ptr state; +}; + +HvJsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { + return (HvJsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); +} + +void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { + if (!state) return; + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else { + state->detach(); + } +} + +void js_ws_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + HvJsWsClient* box = (HvJsWsClient*)JS_GetOpaque(val, s_ws_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +void js_ws_register_class(JSContext* js) { + std::call_once(s_ws_class_once, []() { hvjs_new_class_id(&s_ws_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_ws_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.ws.client"; + def.finalizer = js_ws_finalizer; + JS_NewClass(rt, s_ws_class_id, &def); + } +} + +void js_ws_try_deliver(const std::shared_ptr& state) { + if (!state || state->recv_op == NULL) return; + HvJsWsRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + hvjs_promise_resolve(op, JS_NewStringLen(op->task->js, msg.data(), msg.size())); + } + else if (state->closed) { + state->recv_op = NULL; + hvjs_promise_reject(op, "closed"); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_ws_detach_after_callback(loop, hold); + } +} + +JSValue js_ws_send(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsWsClient* box = js_ws_client(js, this_val); + HvJsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || !state->connected) { + return JS_ThrowTypeError(js, "hv.ws: closed"); + } + std::string msg = argc > 0 ? hvjs_to_string(js, argv[0]) : std::string(); + enum ws_opcode opcode = WS_OPCODE_TEXT; + if (argc > 1 && hvjs_to_string(js, argv[1]) == "binary") { + opcode = WS_OPCODE_BINARY; + } + int ret = state->client->send(msg.data(), (int)msg.size(), opcode); + if (ret < 0) { + return JS_ThrowInternalError(js, "hv.ws: send failed"); + } + return JS_NewInt32(js, ret); +} + +JSValue js_ws_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsWsClient* box = js_ws_client(js, this_val); + HvJsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client) { + return hvjs_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return hvjs_async_resolved_promise(js, hvjs_get_task(js), JS_NewStringLen(js, msg.data(), msg.size())); + } + if (state->closed || !state->connected) { + return hvjs_rejected_promise(js, "closed"); + } + if (state->recv_op != NULL) { + return hvjs_rejected_promise(js, "hv.ws: recv already pending"); + } + HvJsTask* task = hvjs_get_task(js); + HvJsWsRecv* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = box->state; + state->recv_op = op; + return promise; +} + +JSValue js_ws_close(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsWsClient* box = js_ws_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + if (state->connect_op) { + HvJsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + hvjs_promise_reject(op, "closed"); + } + if (state->recv_op) { + HvJsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + hvjs_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +JSValue js_ws_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_ws_class_id); + if (JS_IsException(obj)) return obj; + HvJsWsClient* box = new HvJsWsClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "send", JS_NewCFunction(js, js_ws_send, "send", 1)); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_ws_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "close", JS_NewCFunction(js, js_ws_close, "close", 0)); + return obj; +} + +JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || !task->loop_ptr) { + return hvjs_rejected_promise(js, "hv.ws: no shared event loop on this thread"); + } + if (argc < 1) { + return hvjs_rejected_promise(js, "hv.ws: connect needs url"); + } + std::string url = hvjs_to_string(js, argv[0]); + js_ws_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = std::make_shared(task->loop_ptr); + + HvJsWsConnect* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + state->connect_op = op; + op->state = state; + state->client->onopen = [state]() { + state->connected = true; + state->closed = false; + if (state->connect_op) { + HvJsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + JSValue obj = js_ws_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + hvjs_promise_reject(op, "hv.ws: create client failed"); + js_ws_detach_after_callback(loop, hold); + } + else { + hvjs_promise_resolve(op, obj); + } + } + }; + state->client->onmessage = [state](const std::string& msg) { + state->inbox.push_back(msg); + js_ws_try_deliver(state); + }; + state->client->onclose = [state]() { + state->connected = false; + state->closed = true; + if (state->connect_op) { + HvJsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + hvjs_promise_reject(op, "closed"); + js_ws_detach_after_callback(loop, hold); + } + js_ws_try_deliver(state); + }; + task->in_call = true; + int ret = state->client->open(url.c_str()); + if (ret != 0) { + state->connect_op = NULL; + hvjs_promise_reject(op, "hv.ws: open failed"); + state->detach(); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +} // namespace + +JSValue hvjs_require_http(JSContext* js) { + JSValue http = JS_NewObject(js); + JS_SetPropertyStr(js, http, "request", JS_NewCFunctionMagic(js, js_http_request, "request", 2, JS_CFUNC_generic_magic, JS_HTTP_METHOD_REQUEST)); + JS_SetPropertyStr(js, http, "get", JS_NewCFunctionMagic(js, js_http_request, "get", 1, JS_CFUNC_generic_magic, HTTP_GET)); + JS_SetPropertyStr(js, http, "post", JS_NewCFunctionMagic(js, js_http_request, "post", 2, JS_CFUNC_generic_magic, HTTP_POST)); + JS_SetPropertyStr(js, http, "put", JS_NewCFunctionMagic(js, js_http_request, "put", 2, JS_CFUNC_generic_magic, HTTP_PUT)); + JS_SetPropertyStr(js, http, "delete", JS_NewCFunctionMagic(js, js_http_request, "delete", 1, JS_CFUNC_generic_magic, HTTP_DELETE)); + return http; +} + +JSValue hvjs_require_ws(JSContext* js) { + JSValue ws = JS_NewObject(js); + JS_SetPropertyStr(js, ws, "connect", JS_NewCFunction(js, js_ws_connect, "connect", 1)); + return ws; +} + +} // namespace js +} // namespace hv + +#endif // HVJS_WITH_HTTP +#endif // WITH_JS diff --git a/js/hvjs_mqtt.cpp b/js/hvjs_mqtt.cpp new file mode 100644 index 000000000..bf3409537 --- /dev/null +++ b/js/hvjs_mqtt.cpp @@ -0,0 +1,456 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#ifdef HVJS_WITH_MQTT + +#include +#include + +#include +#include +#include +#include + +#include "mqtt_client.h" + +namespace hv { +namespace js { +namespace { + +static JSClassID s_mqtt_class_id; +static std::once_flag s_mqtt_class_once; + +struct HvJsMqttMessage { + std::string topic; + std::string payload; + int qos; +}; + +struct HvJsMqttState { + mqtt_client_t* client; + std::deque inbox; + HvJsPromiseOp* connect_op; + HvJsPromiseOp* recv_op; + bool js_alive; + bool closed; + bool reconnect; + + HvJsMqttState() : client(NULL), connect_op(NULL), recv_op(NULL), js_alive(false), closed(false), reconnect(false) {} + + void detach() { + closed = true; + if (client) { + mqtt_client_set_callback(client, NULL); + mqtt_client_set_userdata(client, NULL); + mqtt_client_free(client); + client = NULL; + } + } + + ~HvJsMqttState() { detach(); } +}; + +struct HvJsMqttClient { + std::shared_ptr state; +}; + +struct HvJsMqttConnect : public HvJsPromiseOp { + std::shared_ptr state; +}; + +struct HvJsMqttRecv : public HvJsPromiseOp { + std::shared_ptr state; +}; + +struct HvJsMqttDetachEvent { + std::shared_ptr state; +}; + +HvJsMqttClient* js_mqtt_client(JSContext* js, JSValueConst this_val) { + return (HvJsMqttClient*)JS_GetOpaque2(js, this_val, s_mqtt_class_id); +} + +JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); + +void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + HvJsMqttClient* box = (HvJsMqttClient*)JS_GetOpaque(val, s_mqtt_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +void js_mqtt_register_class(JSContext* js) { + std::call_once(s_mqtt_class_once, []() { hvjs_new_class_id(&s_mqtt_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_mqtt_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.mqtt.client"; + def.finalizer = js_mqtt_finalizer; + JS_NewClass(rt, s_mqtt_class_id, &def); + } +} + +JSValue js_mqtt_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_mqtt_class_id); + if (JS_IsException(obj)) return obj; + HvJsMqttClient* box = new HvJsMqttClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_mqtt_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "publish", JS_NewCFunction(js, js_mqtt_publish, "publish", 2)); + JS_SetPropertyStr(js, obj, "subscribe", JS_NewCFunction(js, js_mqtt_subscribe, "subscribe", 1)); + JS_SetPropertyStr(js, obj, "unsubscribe", JS_NewCFunction(js, js_mqtt_unsubscribe, "unsubscribe", 1)); + JS_SetPropertyStr(js, obj, "disconnect", JS_NewCFunction(js, js_mqtt_disconnect, "disconnect", 0)); + return obj; +} + +JSValue js_push_mqtt_message(JSContext* js, const HvJsMqttMessage& msg) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "topic", JS_NewStringLen(js, msg.topic.data(), msg.topic.size())); + JS_SetPropertyStr(js, obj, "payload", JS_NewStringLen(js, msg.payload.data(), msg.payload.size())); + JS_SetPropertyStr(js, obj, "qos", JS_NewInt32(js, msg.qos)); + return obj; +} + +const char* js_mqtt_closed_reason(const HvJsMqttState* state) { + return state && state->reconnect ? "reconnecting" : "closed"; +} + +void js_mqtt_try_deliver(HvJsMqttState* state) { + if (!state || state->recv_op == NULL) return; + HvJsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + if (!state->inbox.empty()) { + HvJsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + hvjs_promise_resolve(op, js_push_mqtt_message(op->task->js, msg)); + } + else if (state->closed) { + state->recv_op = NULL; + hvjs_promise_reject(op, js_mqtt_closed_reason(state)); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } +} + +void js_mqtt_detach_event_cb(hevent_t* ev) { + HvJsMqttDetachEvent* detach = (HvJsMqttDetachEvent*)hevent_userdata(ev); + if (detach) { + detach->state->detach(); + delete detach; + } +} + +void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { + if (!state) return; + state->reconnect = false; + if (state->client) { + mqtt_client_set_reconnect(state->client, NULL); + } + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else if (raw_loop) { + HvJsMqttDetachEvent* detach = new HvJsMqttDetachEvent(); + detach->state = state; + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = js_mqtt_detach_event_cb; + ev.userdata = detach; + hloop_post_event(raw_loop, &ev); + } + else { + state->detach(); + } +} + +void js_mqtt_on_event(mqtt_client_t* client, int type) { + HvJsMqttState* state = (HvJsMqttState*)mqtt_client_get_userdata(client); + if (state == NULL) return; + switch (type) { + case MQTT_TYPE_CONNACK: + state->closed = false; + if (state->connect_op) { + HvJsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + JSValue obj = js_mqtt_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + hvjs_promise_reject(op, "hv.mqtt: create client failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + else { + hvjs_promise_resolve(op, obj); + } + } + break; + case MQTT_TYPE_PUBLISH: { + HvJsMqttMessage msg; + if (client->message.topic && client->message.topic_len > 0) { + msg.topic.assign(client->message.topic, client->message.topic_len); + } + if (client->message.payload && client->message.payload_len > 0) { + msg.payload.assign(client->message.payload, client->message.payload_len); + } + msg.qos = client->message.qos; + state->inbox.push_back(std::move(msg)); + js_mqtt_try_deliver(state); + break; + } + case MQTT_TYPE_DISCONNECT: + state->closed = true; + if (state->connect_op) { + HvJsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + hvjs_promise_reject(op, "connect failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + if (state->recv_op) { + HvJsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->recv_op = NULL; + hvjs_promise_reject(op, js_mqtt_closed_reason(state)); + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + } + break; + default: break; + } +} + +bool js_parse_reconnect(JSContext* js, JSValueConst obj, reconn_setting_t* out) { + JSValue reconnect; + if (!hvjs_get_property(js, obj, "reconnect", &reconnect) || !JS_IsObject(reconnect)) { + if (!JS_IsUndefined(reconnect) && !JS_IsException(reconnect)) JS_FreeValue(js, reconnect); + return false; + } + reconn_setting_init(out); + out->min_delay = (uint32_t)hvjs_get_int_property(js, reconnect, "min_delay", (int)out->min_delay); + out->max_delay = (uint32_t)hvjs_get_int_property(js, reconnect, "max_delay", (int)out->max_delay); + out->delay_policy = (uint32_t)hvjs_get_int_property(js, reconnect, "delay_policy", (int)out->delay_policy); + out->max_retry_cnt = (uint32_t)hvjs_get_int_property(js, reconnect, "max_retry", (int)out->max_retry_cnt); + if (out->max_retry_cnt == 0) out->max_retry_cnt = INFINITE; + if (out->min_delay == 0) out->min_delay = 1; + if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; + if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { + out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; + } + JS_FreeValue(js, reconnect); + return true; +} + +JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || task->loop == NULL) { + return hvjs_rejected_promise(js, "hv.mqtt: no event loop on this thread"); + } + if (argc < 1 || !JS_IsObject(argv[0])) { + return hvjs_rejected_promise(js, "hv.mqtt: connect needs options"); + } + + std::string host = hvjs_get_string_property(js, argv[0], "host", "127.0.0.1"); + int port = hvjs_get_int_property(js, argv[0], "port", DEFAULT_MQTT_PORT); + int ssl = hvjs_get_bool_property(js, argv[0], "ssl", false) ? 1 : 0; + std::string id = hvjs_get_string_property(js, argv[0], "id", ""); + std::string username = hvjs_get_string_property(js, argv[0], "username", ""); + std::string password = hvjs_get_string_property(js, argv[0], "password", ""); + int keepalive = hvjs_get_int_property(js, argv[0], "keepalive", 0); + int timeout = hvjs_get_int_property(js, argv[0], "connect_timeout", 0); + if (timeout <= 0) timeout = hvjs_get_int_property(js, argv[0], "timeout", 0); + bool clean_session = hvjs_get_bool_property(js, argv[0], "clean_session", true); + + js_mqtt_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = mqtt_client_new(task->loop); + if (state->client == NULL) { + return hvjs_rejected_promise(js, "hv.mqtt: create client failed"); + } + mqtt_client_set_userdata(state->client, state.get()); + mqtt_client_set_callback(state->client, js_mqtt_on_event); + if (!id.empty()) mqtt_client_set_id(state->client, id.c_str()); + if (!username.empty() || !password.empty()) { + mqtt_client_set_auth(state->client, username.c_str(), password.c_str()); + } + if (keepalive > 0) state->client->keepalive = (unsigned short)keepalive; + state->client->clean_session = clean_session ? 1 : 0; + if (timeout > 0) mqtt_client_set_connect_timeout(state->client, timeout); + reconn_setting_t reconn; + if (js_parse_reconnect(js, argv[0], &reconn)) { + mqtt_client_set_reconnect(state->client, &reconn); + state->reconnect = true; + } + + HvJsMqttConnect* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) { + state->detach(); + return promise; + } + state->connect_op = op; + op->state = state; + task->in_call = true; + int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); + if (ret != 0) { + state->connect_op = NULL; + hvjs_promise_reject(op, "hv.mqtt: connect failed"); + state->detach(); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsMqttClient* box = js_mqtt_client(js, this_val); + std::shared_ptr state = box ? box->state : std::shared_ptr(); + if (!state || state->client == NULL) { + return hvjs_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + HvJsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return hvjs_async_resolved_promise(js, hvjs_get_task(js), js_push_mqtt_message(js, msg)); + } + if (state->closed) { + return hvjs_rejected_promise(js, js_mqtt_closed_reason(state.get())); + } + if (state->recv_op != NULL) { + return hvjs_rejected_promise(js, "hv.mqtt: recv already pending"); + } + HvJsTask* task = hvjs_get_task(js); + HvJsMqttRecv* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = state; + state->recv_op = op; + return promise; +} + +JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsMqttClient* box = js_mqtt_client(js, this_val); + HvJsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 2) { + return JS_ThrowTypeError(js, "hv.mqtt: publish needs topic and payload"); + } + std::string topic = hvjs_to_string(js, argv[0]); + std::string payload = hvjs_to_string(js, argv[1]); + int32_t qos = 0; + if (argc > 2 && JS_ToInt32(js, &qos, argv[2]) != 0) return JS_EXCEPTION; + int retain = argc > 3 ? JS_ToBool(js, argv[3]) : 0; + mqtt_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.topic = topic.c_str(); + msg.topic_len = (unsigned int)topic.size(); + msg.payload = payload.c_str(); + msg.payload_len = (unsigned int)payload.size(); + msg.qos = (unsigned char)qos; + msg.retain = (unsigned char)retain; + int mid = mqtt_client_publish(state->client, &msg); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: publish failed"); + } + return JS_NewInt32(js, mid); +} + +JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsMqttClient* box = js_mqtt_client(js, this_val); + HvJsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: subscribe needs topic"); + } + std::string topic = hvjs_to_string(js, argv[0]); + int32_t qos = 0; + if (argc > 1 && JS_ToInt32(js, &qos, argv[1]) != 0) return JS_EXCEPTION; + int mid = mqtt_client_subscribe(state->client, topic.c_str(), qos); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: subscribe failed"); + } + return JS_NewInt32(js, mid); +} + +JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsMqttClient* box = js_mqtt_client(js, this_val); + HvJsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: unsubscribe needs topic"); + } + std::string topic = hvjs_to_string(js, argv[0]); + int mid = mqtt_client_unsubscribe(state->client, topic.c_str()); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: unsubscribe failed"); + } + return JS_NewInt32(js, mid); +} + +JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsMqttClient* box = js_mqtt_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + state->reconnect = false; + if (state->connect_op) { + HvJsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + hvjs_promise_reject(op, "closed"); + } + if (state->recv_op) { + HvJsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + hvjs_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +} // namespace + +JSValue hvjs_require_mqtt(JSContext* js) { + JSValue mqtt = JS_NewObject(js); + JS_SetPropertyStr(js, mqtt, "connect", JS_NewCFunction(js, js_mqtt_connect, "connect", 1)); + return mqtt; +} + +} // namespace js +} // namespace hv + +#endif // HVJS_WITH_MQTT +#endif // WITH_JS diff --git a/js/hvjs_redis.cpp b/js/hvjs_redis.cpp new file mode 100644 index 000000000..0a3dfaa5b --- /dev/null +++ b/js/hvjs_redis.cpp @@ -0,0 +1,236 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#ifdef HVJS_WITH_REDIS + +#include +#include +#include +#include + +#include +#include +#include + +#include "AsyncRedisClient.h" + +namespace hv { +namespace js { +namespace { + +static JSClassID s_redis_class_id; +static std::once_flag s_redis_class_once; + +struct HvJsRedisState { + std::shared_ptr client; + bool destroyed; + + HvJsRedisState() : destroyed(false) {} + + ~HvJsRedisState() { + destroyed = true; + if (client) { + client->stop(true); + client.reset(); + } + } +}; + +struct HvJsRedisClient { + std::shared_ptr state; +}; + +struct HvJsRedisCommand : public HvJsPromiseOp { + std::shared_ptr redis; +}; + +void js_redis_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + HvJsRedisClient* box = (HvJsRedisClient*)JS_GetOpaque(val, s_redis_class_id); + if (box) { + delete box; + } +} + +HvJsRedisClient* js_redis_client(JSContext* js, JSValueConst this_val) { + HvJsRedisClient* box = (HvJsRedisClient*)JS_GetOpaque2(js, this_val, s_redis_class_id); + return box; +} + +void js_redis_register_class(JSContext* js) { + std::call_once(s_redis_class_once, []() { hvjs_new_class_id(&s_redis_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_redis_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.redis.client"; + def.finalizer = js_redis_finalizer; + JS_NewClass(rt, s_redis_class_id, &def); + } +} + +JSValue js_push_redis_reply(JSContext* js, const RedisReply& reply) { + switch (reply.type) { + case REDIS_REPLY_STRING: return JS_NewStringLen(js, reply.str.data(), reply.str.size()); + case REDIS_REPLY_INTEGER: return JS_NewInt64(js, reply.integer); + case REDIS_REPLY_ARRAY: { + if (reply.null_array) return JS_NULL; + JSValue arr = JS_NewArray(js); + for (uint32_t i = 0; i < reply.elements.size(); ++i) { + JSValue item = reply.elements[i].isNil() ? JS_NULL : js_push_redis_reply(js, reply.elements[i]); + JS_SetPropertyUint32(js, arr, i, item); + } + return arr; + } + case REDIS_REPLY_NIL: + default: return JS_NULL; + } +} + +void js_redis_resolve_result(HvJsRedisCommand* op, const RedisResult& result) { + JSContext* js = op->task->js; + if (!op->redis || op->redis->destroyed) { + hvjs_promise_reject(op, "hv.redis: client closed"); + return; + } + if (result.code != 0) { + char err[64]; + snprintf(err, sizeof(err), "hv.redis: request failed (%d)", result.code); + hvjs_promise_reject(op, err); + return; + } + if (result.reply.isError()) { + hvjs_promise_reject(op, result.reply.error().c_str()); + return; + } + hvjs_promise_resolve(op, js_push_redis_reply(js, result.reply)); +} + +bool js_build_redis_command(JSContext* js, JSValueConst* argv, int argc, int first, RedisCommand* cmd) { + if (argc <= first) return false; + if (JS_IsArray(js, argv[first]) && argc == first + 1) { + JSValue lenv = JS_GetPropertyStr(js, argv[first], "length"); + uint32_t len = 0; + JS_ToUint32(js, &len, lenv); + JS_FreeValue(js, lenv); + for (uint32_t i = 0; i < len; ++i) { + JSValue item = JS_GetPropertyUint32(js, argv[first], i); + cmd->push_back(hvjs_to_string(js, item)); + JS_FreeValue(js, item); + } + } + else { + for (int i = first; i < argc; ++i) { + cmd->push_back(hvjs_to_string(js, argv[i])); + } + } + return !cmd->empty(); +} + +const char* js_redis_verb_name(int magic) { + switch (magic) { + case 1: return "GET"; + case 2: return "SET"; + case 3: return "DEL"; + case 4: return "INCR"; + case 5: return "DECR"; + case 6: return "EXPIRE"; + case 7: return "EXISTS"; + default: return NULL; + } +} + +JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + HvJsRedisClient* box = js_redis_client(js, this_val); + HvJsRedisState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || state->destroyed) { + return hvjs_rejected_promise(js, "hv.redis: client closed"); + } + RedisCommand cmd; + if (magic != 0) { + const char* verb = js_redis_verb_name(magic); + if (verb == NULL) { + return hvjs_rejected_promise(js, "hv.redis: unknown command"); + } + cmd.push_back(verb); + for (int i = 0; i < argc; ++i) { + cmd.push_back(hvjs_to_string(js, argv[i])); + } + } + else if (!js_build_redis_command(js, argv, argc, 0, &cmd)) { + return hvjs_rejected_promise(js, "hv.redis: empty or invalid command"); + } + + HvJsTask* task = hvjs_get_task(js); + HvJsRedisCommand* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->redis = box->state; + task->in_call = true; + int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); + if (ret != 0) { + hvjs_promise_reject(op, "hv.redis: request failed"); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +JSValue js_redis_new(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || !task->loop_ptr) { + return JS_ThrowTypeError(js, "hv.redis: no shared event loop on this thread"); + } + js_redis_register_class(js); + + std::string host = "127.0.0.1"; + int port = 6379; + std::string auth; + int db = 0; + int timeout = 0; + if (argc > 0 && JS_IsObject(argv[0])) { + host = hvjs_get_string_property(js, argv[0], "host", "127.0.0.1"); + port = hvjs_get_int_property(js, argv[0], "port", 6379); + auth = hvjs_get_string_property(js, argv[0], "auth", ""); + db = hvjs_get_int_property(js, argv[0], "db", 0); + timeout = hvjs_get_int_property(js, argv[0], "timeout", 0); + } + + JSValue obj = JS_NewObjectClass(js, s_redis_class_id); + if (JS_IsException(obj)) return obj; + HvJsRedisClient* box = new HvJsRedisClient(); + box->state = std::make_shared(); + box->state->client = std::make_shared(task->loop_ptr); + box->state->client->setHost(host); + box->state->client->setPort(port); + if (!auth.empty()) box->state->client->setAuth(auth); + if (db > 0) box->state->client->setDb(db); + if (timeout > 0) box->state->client->setTimeout(timeout); + box->state->client->start(false); + JS_SetOpaque(obj, box); + + JS_SetPropertyStr(js, obj, "command", JS_NewCFunctionMagic(js, js_redis_command, "command", 1, JS_CFUNC_generic_magic, 0)); + static const char* verbs[] = {"GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL}; + for (int i = 0; verbs[i]; ++i) { + std::string name = verbs[i]; + for (char& c : name) c = (char)::tolower((unsigned char)c); + JS_SetPropertyStr(js, obj, name.c_str(), JS_NewCFunctionMagic(js, js_redis_command, name.c_str(), 1, JS_CFUNC_generic_magic, i + 1)); + } + return obj; +} + +} // namespace + +JSValue hvjs_require_redis(JSContext* js) { + JSValue redis = JS_NewObject(js); + JS_SetPropertyStr(js, redis, "new", JS_NewCFunction(js, js_redis_new, "new", 1)); + return redis; +} + +} // namespace js +} // namespace hv + +#endif // HVJS_WITH_REDIS +#endif // WITH_JS From c76caf5dae4909f9f38e4d20c5891cd02cc7c516 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 13:27:42 +0800 Subject: [PATCH 03/13] fix(js): support older quickjs promise api --- examples/hvjs.cpp | 8 ++- http/server/HttpJsHandler.cpp | 8 ++- js/hvjs.cpp | 112 +++++++++++++++++++++++++++++----- js/hvjs.h | 4 ++ 4 files changed, 116 insertions(+), 16 deletions(-) diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp index ce83eb392..db5467e03 100644 --- a/examples/hvjs.cpp +++ b/examples/hvjs.cpp @@ -74,7 +74,7 @@ static void finish(hv::js::HvJsTask* base, JSValue result) { fprintf(stderr, "hvjs: %s\n", task->error.c_str()); task->exit_code = 1; } - else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + else if (task->promise_rejected) { std::string err = hv::js::hvjs_to_string(task->js, result); fprintf(stderr, "hvjs: %s\n", err.c_str()); task->exit_code = 1; @@ -153,6 +153,12 @@ int main(int argc, char** argv) { hv::js::hvjs_task_unref(task); return 1; } + std::string err; + if (!hv::js::hvjs_watch_promise(task, &err)) { + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_unref(task); + return 1; + } hv::js::hvjs_task_ref(task); hv::js::hvjs_drain_jobs(task); diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp index 7187cf0bd..d17255d06 100644 --- a/http/server/HttpJsHandler.cpp +++ b/http/server/HttpJsHandler.cpp @@ -239,7 +239,7 @@ static void task_finish(JsHttpTask* task, JSValue result) { task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; task->ctx->response->String(task->error); } - else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + else if (task->promise_rejected) { std::string err = hv::js::hvjs_to_string(task->js, result); hloge("[js] http handler rejected: %s", err.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; @@ -402,6 +402,12 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } + if (!hv::js::hvjs_watch_promise(task, &err)) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(err); + hv::js::hvjs_task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } hv::js::hvjs_task_ref(task); hv::js::hvjs_drain_jobs(task); diff --git a/js/hvjs.cpp b/js/hvjs.cpp index 7d5546251..284fed12f 100644 --- a/js/hvjs.cpp +++ b/js/hvjs.cpp @@ -23,6 +23,9 @@ struct HvJsSleep : public HvJsPromiseOp { struct HvJsImmediatePromise : public HvJsPromiseOp {}; +static JSClassID s_task_ref_class_id; +static std::once_flag s_task_ref_class_once; + std::mutex& js_class_id_mutex() { static std::mutex mutex; return mutex; @@ -76,6 +79,38 @@ void sleep_timer_cb(htimer_t* timer) { hvjs_promise_resolve(sleep, JS_UNDEFINED); } +void register_task_ref_class(JSContext* js) { + std::call_once(s_task_ref_class_once, []() { hvjs_new_class_id(&s_task_ref_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_task_ref_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.js.task"; + JS_NewClass(rt, s_task_ref_class_id, &def); + } +} + +JSValue new_task_ref_value(JSContext* js, HvJsTask* task) { + register_task_ref_class(js); + JSValue obj = JS_NewObjectClass(js, s_task_ref_class_id); + if (JS_IsException(obj)) return obj; + JS_SetOpaque(obj, task); + return obj; +} + +JSValue promise_settle_cb(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data) { + (void)this_val; + if (argc < 1) return JS_UNDEFINED; + HvJsTask* task = (HvJsTask*)JS_GetOpaque(func_data[0], s_task_ref_class_id); + if (task == NULL || task->closing || task->promise_settled) { + return JS_UNDEFINED; + } + task->promise_result = JS_DupValue(js, argv[0]); + task->promise_rejected = magic != 0; + task->promise_settled = true; + return JS_UNDEFINED; +} + JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { (void)this_val; HvJsTask* task = hvjs_get_task(js); @@ -136,7 +171,9 @@ JSValue require_hv(JSContext* js) { } // namespace -HvJsTask::HvJsTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), finished(false), in_call(false), closing(false), refcount(1), finish(NULL) {} +HvJsTask::HvJsTask() + : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), promise_result(JS_UNDEFINED), promise_settled(false), promise_rejected(false), finished(false), + in_call(false), closing(false), refcount(1), finish(NULL) {} HvJsTask::~HvJsTask() {} @@ -151,6 +188,10 @@ void hvjs_task_ref(HvJsTask* task) { void hvjs_task_unref(HvJsTask* task) { if (--task->refcount != 0) return; task->closing = true; + if (!JS_IsUndefined(task->promise_result)) { + JS_FreeValue(task->js, task->promise_result); + task->promise_result = JS_UNDEFINED; + } if (!JS_IsUndefined(task->promise)) { JS_FreeValue(task->js, task->promise); task->promise = JS_UNDEFINED; @@ -191,6 +232,51 @@ void hvjs_schedule_drain(HvJsTask* task) { } } +bool hvjs_watch_promise(HvJsTask* task, std::string* err) { + if (task == NULL || task->js == NULL || JS_IsUndefined(task->promise)) return false; + JSContext* js = task->js; + JSValue then = JS_GetPropertyStr(js, task->promise, "then"); + if (JS_IsException(then)) { + if (err) *err = hvjs_exception_string(js); + return false; + } + if (!JS_IsFunction(js, then)) { + JS_FreeValue(js, then); + if (err) *err = "javascript result is not thenable"; + return false; + } + + JSValue task_ref = new_task_ref_value(js, task); + if (JS_IsException(task_ref)) { + JS_FreeValue(js, then); + if (err) *err = hvjs_exception_string(js); + return false; + } + JSValue on_fulfilled = JS_NewCFunctionData(js, promise_settle_cb, 1, 0, 1, &task_ref); + JSValue on_rejected = JS_NewCFunctionData(js, promise_settle_cb, 1, 1, 1, &task_ref); + if (JS_IsException(on_fulfilled) || JS_IsException(on_rejected)) { + if (err) *err = hvjs_exception_string(js); + JS_FreeValue(js, on_fulfilled); + JS_FreeValue(js, on_rejected); + JS_FreeValue(js, task_ref); + JS_FreeValue(js, then); + return false; + } + JSValue args[2] = {on_fulfilled, on_rejected}; + JSValue ret = JS_Call(js, then, task->promise, 2, args); + JS_FreeValue(js, on_fulfilled); + JS_FreeValue(js, on_rejected); + JS_FreeValue(js, task_ref); + JS_FreeValue(js, then); + if (JS_IsException(ret)) { + if (err) *err = hvjs_exception_string(js); + JS_FreeValue(js, ret); + return false; + } + JS_FreeValue(js, ret); + return true; +} + void hvjs_drain_jobs(HvJsTask* task) { JSContext* job_ctx = NULL; while (JS_IsJobPending(task->rt)) { @@ -200,20 +286,18 @@ void hvjs_drain_jobs(HvJsTask* task) { break; } } - if (!task->finished && !JS_IsUndefined(task->promise)) { - JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); - if (state != JS_PROMISE_PENDING) { - JSValue value = JS_PromiseResult(task->js, task->promise); - if (task->finish) { - task->finish(task, value); - } - else { - JS_FreeValue(task->js, value); - task->finished = true; - hvjs_task_unref(task); - } - return; + if (!task->finished && task->promise_settled) { + JSValue value = task->promise_result; + task->promise_result = JS_UNDEFINED; + if (task->finish) { + task->finish(task, value); } + else { + JS_FreeValue(task->js, value); + task->finished = true; + hvjs_task_unref(task); + } + return; } if (!task->error.empty()) { if (task->finish) { diff --git a/js/hvjs.h b/js/hvjs.h index baf6a7173..5c1f1f0dc 100644 --- a/js/hvjs.h +++ b/js/hvjs.h @@ -19,6 +19,9 @@ struct HV_EXPORT HvJsTask { hloop_t* loop; EventLoopPtr loop_ptr; JSValue promise; + JSValue promise_result; + bool promise_settled; + bool promise_rejected; bool finished; bool in_call; bool closing; @@ -44,6 +47,7 @@ struct HV_EXPORT HvJsPromiseOp { HV_EXPORT void hvjs_task_ref(HvJsTask* task); HV_EXPORT void hvjs_task_unref(HvJsTask* task); HV_EXPORT void hvjs_schedule_drain(HvJsTask* task); +HV_EXPORT bool hvjs_watch_promise(HvJsTask* task, std::string* err = NULL); HV_EXPORT void hvjs_drain_jobs(HvJsTask* task); template JSValue hvjs_new_promise(JSContext* js, HvJsTask* task, T** out) { From 42a4aadd985372305a5bf9a780e91f1db2cf58e7 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 13:37:09 +0800 Subject: [PATCH 04/13] fix(js): support older quickjs property api --- js/hvjs_http.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/hvjs_http.cpp b/js/hvjs_http.cpp index 2bfd25736..747ecd875 100644 --- a/js/hvjs_http.cpp +++ b/js/hvjs_http.cpp @@ -75,7 +75,7 @@ int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_metho JS_FreeValue(js, value); JS_FreeValue(js, key); } - JS_FreePropertyEnum(js, tab, len); + js_free(js, tab); } } *out = req; From 87afc6429cb811480b463e7c5708d42e6e88230d Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 13:52:39 +0800 Subject: [PATCH 05/13] ci: test quickjs with static build --- .github/workflows/CI.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 10e612d2c..9eb454521 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,10 +20,16 @@ jobs: run: | sudo apt update sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc make libhv evpp # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr make libhrpc hrpc PROTOBUF_PREFIX=/usr + # Ubuntu packages libquickjs as a non-PIC static library, so JS is + # covered in a static libhv build instead of linking it into libhv.so. + make clean + rm -f lib/libhv.so lib/libhv.so.* + ./configure --disable-shared --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc + make libhv evpp - name: test run: | From 7fde185c9764c30d157285eb379c4d5b544c47d3 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 14:17:26 +0800 Subject: [PATCH 06/13] ci: isolate quickjs static coverage --- .github/workflows/CI.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9eb454521..343909e90 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,16 +20,19 @@ jobs: run: | sudo apt update sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc - make libhv evpp - # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr - make libhrpc hrpc PROTOBUF_PREFIX=/usr # Ubuntu packages libquickjs as a non-PIC static library, so JS is # covered in a static libhv build instead of linking it into libhv.so. make clean - rm -f lib/libhv.so lib/libhv.so.* - ./configure --disable-shared --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc + ./configure --disable-shared --with-http --with-mqtt --with-redis --with-js + make libhv hvjs unittest + bin/hvjs examples/js/sleep.js + make run-unittest + make clean + rm -f bin/hvjs bin/http_js_handler_test bin/http_js_redis_test bin/http_js_ws_test bin/http_js_mqtt_test + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc make libhv evpp + # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr + make libhrpc hrpc PROTOBUF_PREFIX=/usr - name: test run: | From 9c2a704e4b0e7b0f0b7b201e37591569284c4591 Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 04:12:06 +0800 Subject: [PATCH 07/13] fix(js): harden quickjs handler lifecycle --- CMakeLists.txt | 17 +- Makefile | 11 +- Makefile.in | 2 +- Makefile.vars | 4 +- cmake/libhvConfig.cmake.in | 52 ++++ cmake/vars.cmake | 4 + docs/PLAN.md | 2 +- docs/cn/HttpJsHandler.md | 37 ++- docs/cn/hloop.md | 8 + event/hevent.h | 4 + event/hloop.c | 17 ++ event/hloop.h | 7 + examples/http_server_test.cpp | 23 +- examples/hvjs.cpp | 113 ++++---- examples/js/ws_client.js | 2 +- http/server/HttpJsHandler.cpp | 161 +++++++----- http/server/HttpJsHandler.h | 23 +- js/hvjs.cpp | 412 +++++++++++++++++++++++++----- js/hvjs.h | 64 ++++- js/hvjs_http.cpp | 107 ++++++-- js/hvjs_mqtt.cpp | 31 ++- js/hvjs_redis.cpp | 11 +- scripts/unittest.sh | 20 +- unittest/CMakeLists.txt | 4 +- unittest/http_js_handler_test.cpp | 42 ++- unittest/http_js_ws_test.cpp | 47 +++- 26 files changed, 983 insertions(+), 242 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ed5ee6ae..073117dbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) set(LIBHV_FIND_DEPENDENCY_OPENSSL FALSE) +set(LIBHV_FIND_DEPENDENCY_QUICKJS FALSE) option(BUILD_SHARED "build shared library" ON) option(BUILD_STATIC "build static library" ON) @@ -245,8 +246,15 @@ if(WITH_JS) if(NOT QUICKJS_INCLUDE_DIR OR NOT QUICKJS_LIBRARY) message(FATAL_ERROR "WITH_JS requires QuickJS. Set QUICKJS_ROOT or QUICKJS_INCLUDE_DIR and QUICKJS_LIBRARY.") endif() + if(NOT TARGET QuickJS::QuickJS) + add_library(QuickJS::QuickJS UNKNOWN IMPORTED) + set_target_properties(QuickJS::QuickJS PROPERTIES + IMPORTED_LOCATION "${QUICKJS_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${QUICKJS_INCLUDE_DIR}") + endif() include_directories(${QUICKJS_INCLUDE_DIR}) - set(LIBS ${LIBS} ${QUICKJS_LIBRARY}) + set(LIBS ${LIBS} QuickJS::QuickJS) + set(LIBHV_FIND_DEPENDENCY_QUICKJS TRUE) if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT) add_definitions(-DHVJS_WITH_HTTP) endif() @@ -320,6 +328,7 @@ if(WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS} ${EVPP_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil evpp) if(WITH_JS) + set(LIBHV_HEADERS ${LIBHV_HEADERS} ${JS_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} js) endif() if(WITH_REDIS) @@ -385,6 +394,9 @@ if(BUILD_SHARED) target_compile_definitions(hv PRIVATE HV_DYNAMICLIB) target_include_directories(hv PRIVATE ${LIBHV_SRCDIRS} INTERFACE $ $) + if(WITH_JS) + target_include_directories(hv INTERFACE $) + endif() target_link_libraries(hv PUBLIC ${LIBS}) install(TARGETS hv EXPORT libhvTargets @@ -399,6 +411,9 @@ if(BUILD_STATIC) target_compile_definitions(hv_static PUBLIC HV_STATICLIB) target_include_directories(hv_static PRIVATE ${LIBHV_SRCDIRS} INTERFACE $ $) + if(WITH_JS) + target_include_directories(hv_static INTERFACE $) + endif() target_link_libraries(hv_static PUBLIC ${LIBS}) if(NOT (WIN32 AND BUILD_SHARED)) set_target_properties(hv_static PROPERTIES OUTPUT_NAME hv) diff --git a/Makefile b/Makefile index fa69443fc..5470331f3 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ endif ifeq ($(WITH_JS), yes) ifeq ($(WITH_EVPP), yes) +LIBHV_HEADERS += $(JS_HEADERS) LIBHV_SRCDIRS += js endif endif @@ -246,7 +247,7 @@ hvlua: prepare libhv $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -o bin/hvlua examples/hvlua.cpp -Llib -lhv -pthread $(LUA_LIBS) hvjs: prepare libhv - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ijs -o bin/hvjs examples/hvjs.cpp -Llib -lhv -pthread $(JS_LIBS) + $(MAKEF) TARGET=$@ SRCDIRS="$(LIBHV_SRCDIRS)" SRCS="examples/hvjs.cpp" multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" @@ -449,13 +450,13 @@ ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_HTTP), yes) ifeq ($(WITH_HTTP_SERVER), yes) ifeq ($(WITH_HTTP_CLIENT), yes) - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_handler_test unittest/http_js_handler_test.cpp -Llib -lhv -pthread $(JS_LIBS) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ijs -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_handler_test unittest/http_js_handler_test.cpp -Llib -lhv -pthread $(LDFLAGS) $(JS_LIBS) ifeq ($(WITH_REDIS), yes) - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_REDIS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Iredis -o bin/http_js_redis_test unittest/http_js_redis_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(JS_LIBS) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_REDIS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Iredis -o bin/http_js_redis_test unittest/http_js_redis_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(LDFLAGS) $(JS_LIBS) endif - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_ws_test unittest/http_js_ws_test.cpp -Llib -lhv -pthread $(JS_LIBS) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_ws_test unittest/http_js_ws_test.cpp -Llib -lhv -pthread $(LDFLAGS) $(JS_LIBS) ifeq ($(WITH_MQTT), yes) - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_MQTT $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Imqtt -o bin/http_js_mqtt_test unittest/http_js_mqtt_test.cpp -Llib -lhv -pthread $(JS_LIBS) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_MQTT $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Imqtt -o bin/http_js_mqtt_test unittest/http_js_mqtt_test.cpp -Llib -lhv -pthread $(LDFLAGS) $(JS_LIBS) endif endif endif diff --git a/Makefile.in b/Makefile.in index bcb5fce5a..ca33d327a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -129,7 +129,7 @@ ifeq ($(ALL_SRCS), ) ALL_SRCS = $(wildcard *.c *.cc *.cpp) endif override SRCS += $(filter-out %_test.c %_test.cc %_test.cpp, $(ALL_SRCS)) -ifeq ($(filter clean,$(MAKECMDGOALS)),) +ifneq ($(MAKECMDGOALS),clean) ifneq ($(WITH_LUA), yes) override SRCS := $(filter-out %/HttpLuaHandler.cpp HttpLuaHandler.cpp, $(SRCS)) endif diff --git a/Makefile.vars b/Makefile.vars index 89f3fda04..39dc84a52 100644 --- a/Makefile.vars +++ b/Makefile.vars @@ -124,4 +124,6 @@ HTTP_SERVER_HEADERS = http/server/HttpServer.h\ http/server/WebSocketServer.h\ MQTT_HEADERS = mqtt/mqtt_protocol.h\ - mqtt/mqtt_client.h\ + mqtt/mqtt_client.h + +JS_HEADERS = js/hvjs.h diff --git a/cmake/libhvConfig.cmake.in b/cmake/libhvConfig.cmake.in index 9cd94fa01..719e08eba 100644 --- a/cmake/libhvConfig.cmake.in +++ b/cmake/libhvConfig.cmake.in @@ -6,6 +6,46 @@ if(@LIBHV_FIND_DEPENDENCY_OPENSSL@) find_dependency(OpenSSL) endif() +if(@LIBHV_FIND_DEPENDENCY_QUICKJS@ AND NOT TARGET QuickJS::QuickJS) + find_path(QUICKJS_INCLUDE_DIR + NAMES quickjs.h + HINTS + ${QUICKJS_ROOT}/include/quickjs + ${QUICKJS_ROOT}/include + /opt/homebrew/opt/quickjs/include/quickjs + /opt/homebrew/opt/quickjs/include + /usr/local/opt/quickjs/include/quickjs + /usr/local/opt/quickjs/include + /usr/local/include/quickjs + /usr/local/include + /usr/include/quickjs + /usr/include) + find_library(QUICKJS_LIBRARY + NAMES quickjs libquickjs + HINTS + ${QUICKJS_ROOT}/lib/quickjs + ${QUICKJS_ROOT}/lib + /opt/homebrew/opt/quickjs/lib/quickjs + /opt/homebrew/opt/quickjs/lib + /usr/local/opt/quickjs/lib/quickjs + /usr/local/opt/quickjs/lib + /usr/local/lib/quickjs + /usr/local/lib + /usr/lib/quickjs + /usr/lib) + if(NOT QUICKJS_INCLUDE_DIR OR NOT QUICKJS_LIBRARY) + set(libhv_FOUND FALSE) + set(libhv_NOT_FOUND_MESSAGE + "QuickJS dependency not found. Set QUICKJS_ROOT or QUICKJS_INCLUDE_DIR and QUICKJS_LIBRARY.") + return() + else() + add_library(QuickJS::QuickJS UNKNOWN IMPORTED) + set_target_properties(QuickJS::QuickJS PROPERTIES + IMPORTED_LOCATION "${QUICKJS_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${QUICKJS_INCLUDE_DIR}") + endif() +endif() + include("${CMAKE_CURRENT_LIST_DIR}/libhvTargets.cmake") if(TARGET libhv::hv) @@ -34,6 +74,18 @@ if(NOT TARGET hv_static AND TARGET libhv::hv_static) endif() set_and_check(libhv_INCLUDE_DIRS "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") +if(@LIBHV_FIND_DEPENDENCY_QUICKJS@) + if(QUICKJS_INCLUDE_DIR) + set(libhv_INCLUDE_DIRS "${libhv_INCLUDE_DIRS}" "${QUICKJS_INCLUDE_DIR}") + elseif(TARGET QuickJS::QuickJS) + get_target_property(QUICKJS_INCLUDE_DIR QuickJS::QuickJS INTERFACE_INCLUDE_DIRECTORIES) + if(QUICKJS_INCLUDE_DIR) + set(libhv_INCLUDE_DIRS "${libhv_INCLUDE_DIRS}" "${QUICKJS_INCLUDE_DIR}") + endif() + endif() +else() + set(libhv_INCLUDE_DIRS "${libhv_INCLUDE_DIRS}") +endif() set(LIBHV_INCLUDE_DIRS "${libhv_INCLUDE_DIRS}") set(LIBHV_LIBRARY "${libhv_LIBRARY}") set(LIBHV_STATIC_LIBRARY "${libhv_STATIC_LIBRARY}") diff --git a/cmake/vars.cmake b/cmake/vars.cmake index ac58c3e7c..e929e07eb 100644 --- a/cmake/vars.cmake +++ b/cmake/vars.cmake @@ -123,3 +123,7 @@ set(MQTT_HEADERS mqtt/mqtt_protocol.h mqtt/mqtt_client.h ) + +set(JS_HEADERS + js/hvjs.h +) diff --git a/docs/PLAN.md b/docs/PLAN.md index ce5743ed8..bac1d420c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,12 +12,12 @@ - redis client - async DNS - lua binding +- js binding - http js script handler - hrpc = libhv + protobuf ## Plan -- js binding - rudp: FEC, ARQ, UDT, QUIC - coroutine - cppsocket.io diff --git a/docs/cn/HttpJsHandler.md b/docs/cn/HttpJsHandler.md index 1b1cbdfab..ba7fa1982 100644 --- a/docs/cn/HttpJsHandler.md +++ b/docs/cn/HttpJsHandler.md @@ -50,6 +50,7 @@ C++: ```cpp #include "HttpServer.h" +#include "HttpJsHandler.h" #include "HttpScriptHandler.h" using namespace hv; @@ -82,6 +83,19 @@ async function get(ctx) { 如果需要明确指定 JS 引擎,也可以直接使用 `HttpJsHandler("scripts/hello.js")`。推荐用户代码优先使用 `HttpScriptHandler`,这样同一个路由入口可以按脚本后缀分发到不同脚本引擎。 +可以通过 `HttpJsHandlerOptions` 调整脚本热加载和运行限制: + +```cpp +HttpJsHandlerOptions options; +options.reload_on_change = true; +options.timeout_ms = 30000; // 单个 HTTP 请求的墙钟预算,0 表示不限制 +options.memory_limit = 64 * 1024 * 1024; // 每个 event loop 复用的 QuickJS runtime 内存上限,0 表示不限制 +options.stack_size = 1024 * 1024; // QuickJS 栈上限,0 表示不限制 +router.GET("/hello", HttpJsHandler("scripts/hello.js", options)); +``` + +`memory_limit` 和 `stack_size` 作用在每个 event loop 复用的 QuickJS runtime 上;同一个 loop 上第一次创建 JS runtime 时生效。 + ## 目录映射 `HttpService::Script(path, script_dir)` 可以把 URL 前缀映射到脚本目录,内部同样使用 `HttpScriptHandler`: @@ -99,8 +113,8 @@ router.Script("/script/", "scripts"); ```js ctx.method() // GET/POST/... ctx.path() // URL path -ctx.param(name, defaultValue) -ctx.query(name, defaultValue) +ctx.param(name, defaultValue) // path/query 参数 +ctx.query(name, defaultValue) // ctx.param 的别名 ctx.header(name, defaultValue) ctx.body() @@ -156,13 +170,16 @@ await http.request("GET", "http://127.0.0.1:8080/ping"); ```js const wsmod = require("hv/ws"); -const ws = await wsmod.connect("ws://127.0.0.1:8888/"); +const ws = await wsmod.connect("ws://127.0.0.1:8888/", { + timeout: 3000, + ping_interval: 3000 +}); ws.send("hello"); const msg = await ws.recv(); ws.close(); ``` -`recv()` 在收到消息前保持 pending;连接关闭时会 reject。 +`ws.connect()` 使用底层 `TcpClient` 的连接超时;`recv()` 在收到应用消息前保持 pending,连接关闭时会 reject。WebSocket ping/pong 只用于连接健康检查,不会让一个连接健康但没有业务消息的 `recv()` 自动返回。HTTP JS handler 的 `timeout_ms` 是请求级兜底,会结束整个请求并清理仍未完成的 `recv()` 等异步操作。 ### hv/redis @@ -180,6 +197,8 @@ const pong = await r.command(["PING"]); Redis 回复映射:string -> string,integer -> number,nil -> null,array -> array,error reply -> rejected Promise。 +`redis.new()` 会创建一个 `AsyncRedisClient`。如果脚本在每个 HTTP 请求里调用它,就会产生按请求创建/释放连接的开销;高频路径建议在 C++ 层封装连接池,或后续扩展 JS 绑定提供复用能力。 + ### hv/mqtt 需启用 `WITH_MQTT`。 @@ -211,11 +230,17 @@ const msg = await client.recv(); // { topic, payload, qos } client.disconnect(); ``` +`mqtt.connect()` 的 `timeout` / `connect_timeout` 会设置底层 MQTT client 的连接超时;`recv()` 在收到 `PUBLISH` 前保持 pending,连接关闭时会 reject。MQTT keepalive 用于发现断链,正常 PING/PONG 不会让一个没有业务消息的 `recv()` 自动返回。`reconnect.max_retry = 0` 按 libhv reconnect 语义表示无限重试。 + ## 异步模型 -每次 HTTP 请求会创建独立 QuickJS runtime/context。脚本可以返回普通值,也可以返回 Promise;`HttpJsHandler` 会等待 Promise fulfilled/rejected 后再发送 HTTP 响应。`await hv.sleep()`、`await http.get()`、`await ws.recv()`、`await redis.command()`、`await mqtt.connect()` 都在当前 IO 线程的 event loop 上推进,不会阻塞 loop。 +每个 event loop 会复用一个 QuickJS runtime;每次 HTTP 请求会创建独立 QuickJS context,用于隔离请求级全局对象和 `ctx`。脚本可以返回普通值,也可以返回 Promise;`HttpJsHandler` 会等待 Promise fulfilled/rejected 后再发送 HTTP 响应。`await hv.sleep()`、`await http.get()`、`await ws.recv()`、`await redis.command()`、`await mqtt.connect()` 都在当前 IO 线程的 event loop 上推进,不会阻塞 loop。 + +`HttpJsHandler` 会缓存脚本文本,并在 `reload_on_change=true` 时根据文件 `mtime` 自动重新读取;每个请求仍使用独立 QuickJS context,因此脚本里的全局变量不会跨请求共享。QuickJS runtime 按 event loop 复用,可以减少每请求初始化 runtime 的开销,但仍会按请求重新执行脚本文本。 + +默认启用 30 秒请求级 timeout:如果脚本 CPU 循环太久,QuickJS interrupt handler 会中断执行;如果返回的 Promise 长时间不 settle,event loop timer 会结束该 HTTP 请求并清理仍未完成的 libhv 异步操作。错误细节写入日志,HTTP 500 响应体固定为 `javascript handler error`。 -`HttpJsHandler` 会缓存脚本文本,并在 `reload_on_change=true` 时根据文件 `mtime` 自动重新读取;每个请求仍使用独立 QuickJS runtime/context,因此脚本里的全局变量不会跨请求共享。 +当前 JS 字符串绑定通过 `JS_ToCStringLen` 表达数据;`ctx.body()`、`http` response body、`ws.send(..., "binary")`、`mqtt.publish()` 的 payload 仍按字符串处理。需要二进制无损传输时,应等后续版本接入 `ArrayBuffer` / `Uint8Array`。 ## 示例 diff --git a/docs/cn/hloop.md b/docs/cn/hloop.md index dec260c58..b7f9fc1ec 100644 --- a/docs/cn/hloop.md +++ b/docs/cn/hloop.md @@ -104,6 +104,14 @@ void hloop_set_userdata(hloop_t* loop, void* userdata); // 获取事件循环的用户数据 void* hloop_userdata(hloop_t* loop); +// 设置/获取事件循环关联的 lua_State(由 lua 绑定使用) +void hloop_set_lua_state(hloop_t* loop, void* lua_state, void (*dtor)(void* lua_state)); +void* hloop_lua_state(hloop_t* loop); + +// 设置/获取事件循环关联的 JS runtime(由 js 绑定使用) +void hloop_set_js_runtime(hloop_t* loop, void* js_runtime, void (*dtor)(void* js_runtime)); +void* hloop_js_runtime(hloop_t* loop); + // 投递事件 void hloop_post_event(hloop_t* loop, hevent_t* ev); diff --git a/event/hevent.h b/event/hevent.h index 84a8fa66f..7f3289132 100644 --- a/event/hevent.h +++ b/event/hevent.h @@ -72,6 +72,10 @@ struct hloop_s { // lua-free. Set via hloop_set_lua_state with a destructor; freed in hloop_cleanup. void* lua_state; void (*lua_state_dtor)(void* lua_state); + // per-loop JS runtime (js/), stored as opaque void* so the C core stays + // quickjs-free. Set via hloop_set_js_runtime with a destructor; freed in hloop_cleanup. + void* js_runtime; + void (*js_runtime_dtor)(void* js_runtime); }; uint64_t hloop_next_event_id(); diff --git a/event/hloop.c b/event/hloop.c index 603b5ef5a..133df4fd2 100644 --- a/event/hloop.c +++ b/event/hloop.c @@ -372,6 +372,14 @@ static void hloop_cleanup(hloop_t* loop) { loop->lua_state = NULL; loop->lua_state_dtor = NULL; + // per-loop JS runtime (opaque; destructor supplied by js/ layer) + if (loop->js_runtime && loop->js_runtime_dtor) { + printd("cleanup js_runtime...\n"); + loop->js_runtime_dtor(loop->js_runtime); + } + loop->js_runtime = NULL; + loop->js_runtime_dtor = NULL; + // ios printd("cleanup ios...\n"); for (int i = 0; i < loop->ios.maxsize; ++i) { @@ -619,6 +627,15 @@ void* hloop_lua_state(hloop_t* loop) { return loop->lua_state; } +void hloop_set_js_runtime(hloop_t* loop, void* js_runtime, void (*dtor)(void* js_runtime)) { + loop->js_runtime = js_runtime; + loop->js_runtime_dtor = dtor; +} + +void* hloop_js_runtime(hloop_t* loop) { + return loop->js_runtime; +} + static hloop_t* s_signal_loop = NULL; static void signal_handler(int signo) { if (!s_signal_loop) return; diff --git a/event/hloop.h b/event/hloop.h index a94e1edab..a1c89e6a3 100644 --- a/event/hloop.h +++ b/event/hloop.h @@ -183,6 +183,13 @@ HV_EXPORT void* hloop_userdata(hloop_t* loop); HV_EXPORT void hloop_set_lua_state(hloop_t* loop, void* lua_state, void (*dtor)(void* lua_state)); HV_EXPORT void* hloop_lua_state(hloop_t* loop); +// per-loop JS runtime (used by the js/ binding layer). +// The C core treats it as an opaque pointer and never depends on QuickJS. +// @dtor: optional destructor invoked on this pointer in hloop_cleanup. +// Replacing an existing js_runtime does NOT call the previous dtor; the caller manages that. +HV_EXPORT void hloop_set_js_runtime(hloop_t* loop, void* js_runtime, void (*dtor)(void* js_runtime)); +HV_EXPORT void* hloop_js_runtime(hloop_t* loop); + // custom_event /* * hevent_t ev; diff --git a/examples/http_server_test.cpp b/examples/http_server_test.cpp index 402afe77e..7bc97f644 100644 --- a/examples/http_server_test.cpp +++ b/examples/http_server_test.cpp @@ -5,8 +5,8 @@ */ #include "HttpServer.h" -#include "hthread.h" // import hv_gettid -#include "hasync.h" // import hv::async +#include "hthread.h" // import hv_gettid +#include "hasync.h" // import hv::async #if defined(WITH_LUA) || defined(WITH_JS) #include "HttpScriptHandler.h" @@ -55,7 +55,9 @@ int main(int argc, char** argv) { /* API handlers */ // curl -v http://ip:port/ping - router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { return resp->String("pong"); }); + router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { + return resp->String("pong"); + }); // curl -v http://ip:port/data router.GET("/data", [](HttpRequest* req, HttpResponse* resp) { @@ -64,7 +66,9 @@ int main(int argc, char** argv) { }); // curl -v http://ip:port/paths - router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) { return resp->Json(router.Paths()); }); + router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) { + return resp->Json(router.Paths()); + }); // curl -v http://ip:port/get?env=1 router.GET("/get", [](const HttpContextPtr& ctx) { @@ -77,7 +81,9 @@ int main(int argc, char** argv) { }); // curl -v http://ip:port/echo -d "hello,world!" - router.POST("/echo", [](const HttpContextPtr& ctx) { return ctx->send(ctx->body(), ctx->type()); }); + router.POST("/echo", [](const HttpContextPtr& ctx) { + return ctx->send(ctx->body(), ctx->type()); + }); // curl -v http://ip:port/user/123 router.GET("/user/{id}", [](const HttpContextPtr& ctx) { @@ -111,7 +117,9 @@ int main(int argc, char** argv) { // curl -v http://ip:port/close // Test HTTP_STATUS_CLOSE: closes connection without sending any response - router.GET("/close", [](HttpRequest* req, HttpResponse* resp) { return HTTP_STATUS_CLOSE; }); + router.GET("/close", [](HttpRequest* req, HttpResponse* resp) { + return HTTP_STATUS_CLOSE; + }); // middleware router.AllowCORS(); @@ -144,8 +152,7 @@ int main(int argc, char** argv) { server.start(); // press Enter to stop - while (getchar() != '\n') - ; + while (getchar() != '\n'); hv::async::cleanup(); return 0; } diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp index db5467e03..2c3bf0698 100644 --- a/examples/hvjs.cpp +++ b/examples/hvjs.cpp @@ -19,14 +19,15 @@ #include "EventLoop.h" #include "hfile.h" #include "hlog.h" +#include "htime.h" #include "hvjs.h" namespace { struct HvJsCliTask : public hv::js::HvJsTask { - int exit_code; + int* exit_code; - HvJsCliTask() : exit_code(0) {} + HvJsCliTask() : exit_code(NULL) {} }; static void usage(const char* prog) { @@ -72,18 +73,19 @@ static void finish(hv::js::HvJsTask* base, JSValue result) { task->finished = true; if (!task->error.empty()) { fprintf(stderr, "hvjs: %s\n", task->error.c_str()); - task->exit_code = 1; + if (task->exit_code) *task->exit_code = 1; } else if (task->promise_rejected) { std::string err = hv::js::hvjs_to_string(task->js, result); fprintf(stderr, "hvjs: %s\n", err.c_str()); - task->exit_code = 1; + if (task->exit_code) *task->exit_code = 1; } JS_FreeValue(task->js, result); - if (task->loop_ptr) { + hv::js::hvjs_task_cancel_timeout(task); + if (task->loop_ptr && task->loop_ptr->isRunning()) { task->loop_ptr->stop(); } - else if (task->loop) { + else if (task->loop && hloop_status(task->loop) == HLOOP_STATUS_RUNNING) { hloop_stop(task->loop); } hv::js::hvjs_task_unref(task); @@ -110,64 +112,83 @@ int main(int argc, char** argv) { hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, loop.get()); HvJsCliTask* task = new HvJsCliTask(); + int exit_code = 0; + task->exit_code = &exit_code; task->loop_ptr = loop; task->loop = loop->loop(); task->finish = finish; - task->rt = JS_NewRuntime(); - task->js = task->rt ? JS_NewContext(task->rt) : NULL; - if (task->rt == NULL || task->js == NULL) { + hv::js::HvJsRuntimeOptions runtime_options; + hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop, runtime_options)); + task->js = task->runtime ? JS_NewContext(task->runtime->rt) : NULL; + if (task->runtime == NULL || task->js == NULL) { fprintf(stderr, "hvjs: failed to create quickjs runtime\n"); hv::js::hvjs_task_unref(task); return 1; } JS_SetContextOpaque(task->js, task); - set_args(task->js, argc, argv); - - JSValue global = JS_GetGlobalObject(task->js); - JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); - JS_SetPropertyStr(task->js, global, "print", JS_NewCFunction(task->js, js_print, "print", 1)); - - std::string wrapped = "(async function(){\n"; - wrapped += code; - wrapped += "\n})()"; - JSValue eval = JS_Eval(task->js, wrapped.c_str(), wrapped.size(), script, JS_EVAL_TYPE_GLOBAL); - if (JS_IsException(eval)) { - std::string err = hv::js::hvjs_exception_string(task->js); - JS_FreeValue(task->js, global); - fprintf(stderr, "hvjs: %s\n", err.c_str()); + task->timeout_ms = 30000; + task->start_hrtime = gethrtime_us(); + if (!hv::js::hvjs_task_start_timeout(task, task->timeout_ms)) { + fprintf(stderr, "hvjs: failed to create timeout timer\n"); hv::js::hvjs_task_unref(task); return 1; } + set_args(task->js, argc, argv); - JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); - JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); - JS_FreeValue(task->js, global); - JSValue promise_arg = eval; - task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); - JS_FreeValue(task->js, promise_resolve); - JS_FreeValue(task->js, promise_ctor); - JS_FreeValue(task->js, eval); - if (JS_IsException(task->promise)) { - std::string err = hv::js::hvjs_exception_string(task->js); - fprintf(stderr, "hvjs: %s\n", err.c_str()); - hv::js::hvjs_task_unref(task); - return 1; - } - std::string err; - if (!hv::js::hvjs_watch_promise(task, &err)) { - fprintf(stderr, "hvjs: %s\n", err.c_str()); - hv::js::hvjs_task_unref(task); - return 1; + { + hv::js::HvJsTaskScope scope(task); + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); + JS_SetPropertyStr(task->js, global, "print", JS_NewCFunction(task->js, js_print, "print", 1)); + + std::string wrapped = "(async function(){\n"; + wrapped += code; + wrapped += "\n})()"; + JSValue eval = JS_Eval(task->js, wrapped.c_str(), wrapped.size(), script, JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + std::string err = hv::js::hvjs_exception_string(task->js); + JS_FreeValue(task->js, global); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_cancel_timeout(task); + hv::js::hvjs_task_unref(task); + return 1; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = eval; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, eval); + if (JS_IsException(task->promise)) { + std::string err = hv::js::hvjs_exception_string(task->js); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + task->closing = true; + hv::js::hvjs_task_cancel_ops(task, "javascript handler error"); + hv::js::hvjs_task_cancel_timeout(task); + hv::js::hvjs_task_unref(task); + return 1; + } + std::string err; + if (!hv::js::hvjs_watch_promise(task, &err)) { + fprintf(stderr, "hvjs: %s\n", err.c_str()); + task->closing = true; + hv::js::hvjs_task_cancel_ops(task, "javascript handler error"); + hv::js::hvjs_task_cancel_timeout(task); + hv::js::hvjs_task_unref(task); + return 1; + } } hv::js::hvjs_task_ref(task); hv::js::hvjs_drain_jobs(task); - int exit_code = task->exit_code; - if (!task->finished) { + bool finished = task->finished; + hv::js::hvjs_task_unref(task); + if (!finished) { loop->run(); - exit_code = task->exit_code; } - hv::js::hvjs_task_unref(task); hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, NULL); return exit_code; } diff --git a/examples/js/ws_client.js b/examples/js/ws_client.js index 0c8376ce3..805aba278 100644 --- a/examples/js/ws_client.js +++ b/examples/js/ws_client.js @@ -5,7 +5,7 @@ const hv = require("hv"); const wsmod = require("hv/ws"); const url = arg[1] || "ws://127.0.0.1:8888/"; -const ws = await wsmod.connect(url); +const ws = await wsmod.connect(url, { timeout: 3000, ping_interval: 3000 }); hv.log("connected to", url); ws.send("hello from js"); diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp index d17255d06..879c22eee 100644 --- a/http/server/HttpJsHandler.cpp +++ b/http/server/HttpJsHandler.cpp @@ -15,6 +15,7 @@ #include "hlog.h" #include "hpath.h" #include "hstring.h" +#include "htime.h" #include "hvjs.h" namespace hv { @@ -164,6 +165,13 @@ static void http_js_task_finish(hv::js::HvJsTask* task, JSValue result) { task_finish(static_cast(task), result); } +static void close_task(JsHttpTask* task, const char* reason) { + task->closing = true; + hv::js::hvjs_task_cancel_ops(task, reason); + hv::js::hvjs_task_cancel_timeout(task); + hv::js::hvjs_task_unref(task); +} + static bool load_file(const std::string& filepath, std::string* out, std::string* err) { HFile file; if (file.open(filepath.c_str(), "rb") != 0) { @@ -234,25 +242,28 @@ static bool apply_result(JSContext* js, JSValueConst value, const HttpContextPtr static void task_finish(JsHttpTask* task, JSValue result) { if (task->finished) return; task->finished = true; + hv::js::hvjs_task_cancel_timeout(task); if (!task->error.empty()) { hloge("[js] http handler error: %s", task->error.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - task->ctx->response->String(task->error); + task->ctx->response->String("javascript handler error"); } else if (task->promise_rejected) { std::string err = hv::js::hvjs_to_string(task->js, result); hloge("[js] http handler rejected: %s", err.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - task->ctx->response->String(err); + task->ctx->response->String("javascript handler error"); } else { std::string err; if (!apply_result(task->js, result, task->ctx, &err)) { hloge("[js] http handler error: %s", err.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - task->ctx->response->String(err); + task->ctx->response->String("javascript handler error"); } } + task->closing = true; + hv::js::hvjs_task_cancel_ops(task, "javascript task finished"); JS_FreeValue(task->js, result); if (task->async) { task->ctx->send(); @@ -321,15 +332,6 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JsHttpTask* task = new JsHttpTask(); task->ctx = ctx; task->finish = http_js_task_finish; - task->rt = JS_NewRuntime(); - task->js = task->rt ? JS_NewContext(task->rt) : NULL; - if (task->rt == NULL || task->js == NULL) { - ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String("js handler: failed to create quickjs runtime"); - hv::js::hvjs_task_unref(task); - return HTTP_STATUS_INTERNAL_SERVER_ERROR; - } - JS_SetContextOpaque(task->js, task); if (ctx->writer && ctx->writer->io()) { task->loop = hevent_loop(ctx->writer->io()); } @@ -350,63 +352,91 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - JSValue global = JS_GetGlobalObject(task->js); - JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); - - JSValue eval = JS_Eval(task->js, code.c_str(), code.size(), filepath_.c_str(), JS_EVAL_TYPE_GLOBAL); - if (JS_IsException(eval)) { - std::string msg = hv::js::hvjs_exception_string(task->js); - JS_FreeValue(task->js, global); + hv::js::HvJsRuntimeOptions runtime_options; + runtime_options.memory_limit = options_.memory_limit; + runtime_options.stack_size = options_.stack_size; + hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop, runtime_options)); + task->js = task->runtime ? JS_NewContext(task->runtime->rt) : NULL; + if (task->runtime == NULL || task->js == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String(msg); - hv::js::hvjs_task_unref(task); + ctx->response->String("js handler: failed to create quickjs runtime"); + close_task(task, "javascript handler error"); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - JS_FreeValue(task->js, eval); - - JSValue fn; - if (!push_handler_fn(task->js, global, ctx->request->method, &fn)) { - JS_FreeValue(task->js, global); - ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; - ctx->response->String("no js handler function"); - hv::js::hvjs_task_unref(task); - return HTTP_STATUS_NOT_IMPLEMENTED; - } - - JSValue js_ctx = js_new_ctx(task->js, ctx); - JSValue ret = JS_Call(task->js, fn, JS_UNDEFINED, 1, &js_ctx); - JS_FreeValue(task->js, js_ctx); - JS_FreeValue(task->js, fn); - if (JS_IsException(ret)) { - std::string msg = hv::js::hvjs_exception_string(task->js); - JS_FreeValue(task->js, global); - JS_FreeValue(task->js, ret); + JS_SetContextOpaque(task->js, task); + task->timeout_ms = options_.timeout_ms; + task->start_hrtime = gethrtime_us(); + if (!hv::js::hvjs_task_start_timeout(task, options_.timeout_ms)) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String(msg); - hv::js::hvjs_task_unref(task); + ctx->response->String("js handler: failed to create timeout timer"); + close_task(task, "javascript handler error"); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); - JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); - JS_FreeValue(task->js, global); - JSValue promise_arg = ret; - task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); - JS_FreeValue(task->js, promise_resolve); - JS_FreeValue(task->js, promise_ctor); - JS_FreeValue(task->js, ret); - if (JS_IsException(task->promise)) { - std::string msg = hv::js::hvjs_exception_string(task->js); - ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String(msg); - hv::js::hvjs_task_unref(task); - return HTTP_STATUS_INTERNAL_SERVER_ERROR; - } - if (!hv::js::hvjs_watch_promise(task, &err)) { - ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String(err); - hv::js::hvjs_task_unref(task); - return HTTP_STATUS_INTERNAL_SERVER_ERROR; + { + hv::js::HvJsTaskScope scope(task); + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); + + JSValue eval = JS_Eval(task->js, code.c_str(), code.size(), filepath_.c_str(), JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + std::string msg = hv::js::hvjs_exception_string(task->js); + hloge("[js] eval %s failed: %s", filepath_.c_str(), msg.c_str()); + JS_FreeValue(task->js, global); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("javascript handler error"); + close_task(task, "javascript handler error"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + JS_FreeValue(task->js, eval); + + JSValue fn; + if (!push_handler_fn(task->js, global, ctx->request->method, &fn)) { + JS_FreeValue(task->js, global); + ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; + ctx->response->String("no js handler function"); + close_task(task, "javascript handler error"); + return HTTP_STATUS_NOT_IMPLEMENTED; + } + + JSValue js_ctx = js_new_ctx(task->js, ctx); + JSValue ret = JS_Call(task->js, fn, JS_UNDEFINED, 1, &js_ctx); + JS_FreeValue(task->js, js_ctx); + JS_FreeValue(task->js, fn); + if (JS_IsException(ret)) { + std::string msg = hv::js::hvjs_exception_string(task->js); + hloge("[js] handler %s failed: %s", filepath_.c_str(), msg.c_str()); + JS_FreeValue(task->js, global); + JS_FreeValue(task->js, ret); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("javascript handler error"); + close_task(task, "javascript handler error"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = ret; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, ret); + if (JS_IsException(task->promise)) { + std::string msg = hv::js::hvjs_exception_string(task->js); + hloge("[js] Promise.resolve %s failed: %s", filepath_.c_str(), msg.c_str()); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("javascript handler error"); + close_task(task, "javascript handler error"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + if (!hv::js::hvjs_watch_promise(task, &err)) { + hloge("[js] watch promise %s failed: %s", filepath_.c_str(), err.c_str()); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("javascript handler error"); + close_task(task, "javascript handler error"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } } hv::js::hvjs_task_ref(task); @@ -415,12 +445,11 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { int status = ctx->response->status_code; if (!finished) { task->async = true; + hv::js::hvjs_task_unref(task); + return HTTP_STATUS_NEXT; } hv::js::hvjs_task_unref(task); - if (finished) { - return status; - } - return HTTP_STATUS_NEXT; + return status; } } // namespace hv diff --git a/http/server/HttpJsHandler.h b/http/server/HttpJsHandler.h index 8c9a5f7b3..7e1c78b42 100644 --- a/http/server/HttpJsHandler.h +++ b/http/server/HttpJsHandler.h @@ -1,6 +1,8 @@ #ifndef HV_HTTP_JS_HANDLER_H_ #define HV_HTTP_JS_HANDLER_H_ +#include + #include #include @@ -11,17 +13,24 @@ namespace hv { struct HV_EXPORT HttpJsHandlerOptions { bool reload_on_change; - - HttpJsHandlerOptions() { reload_on_change = true; } + int timeout_ms; // request wall-clock timeout; 0 disables + size_t memory_limit; // QuickJS per-loop runtime memory limit; 0 disables + size_t stack_size; // QuickJS max stack size; 0 disables + + HttpJsHandlerOptions() + : reload_on_change(true) + , timeout_ms(30000) + , memory_limit(64 * 1024 * 1024) + , stack_size(1024 * 1024) {} }; // HttpJsHandler runs a QuickJS script to handle an HTTP request. // -// The first implementation uses one QuickJS runtime/context per request. This -// keeps request lifetime, Promise continuations and loop-thread affinity simple; -// scripts can use async functions and await hv.sleep() without blocking the -// server IO loop. The public route surface mirrors HttpLuaHandler: a per-method -// function (get/post/...) takes precedence over handle(ctx). +// One QuickJS runtime is cached on each hloop_t, and each request gets its own +// JSContext for request globals and lifecycle. Scripts can use async functions +// and await hv.sleep() without blocking the server IO loop. The public route +// surface mirrors HttpLuaHandler: a per-method function (get/post/...) takes +// precedence over handle(ctx). class HV_EXPORT HttpJsHandler { public: HttpJsHandler(const char* filepath, const HttpJsHandlerOptions& options = HttpJsHandlerOptions()); diff --git a/js/hvjs.cpp b/js/hvjs.cpp index 284fed12f..6e7de180d 100644 --- a/js/hvjs.cpp +++ b/js/hvjs.cpp @@ -2,11 +2,13 @@ #include "hvjs.h" +#include #include #include #include "hlog.h" +#include "htime.h" #include "hversion.h" namespace hv { @@ -19,6 +21,7 @@ struct HvJsSleep : public HvJsPromiseOp { TimerID timer_id; HvJsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} + void cancel(const char* reason) override; }; struct HvJsImmediatePromise : public HvJsPromiseOp {}; @@ -26,6 +29,52 @@ struct HvJsImmediatePromise : public HvJsPromiseOp {}; static JSClassID s_task_ref_class_id; static std::once_flag s_task_ref_class_once; +void delete_op(HvJsPromiseOp* op); + +void runtime_dtor(void* userdata) { + HvJsRuntime* runtime = (HvJsRuntime*)userdata; + if (runtime == NULL) return; + std::vector tasks; + tasks.swap(runtime->tasks); + for (size_t i = 0; i < tasks.size(); ++i) { + HvJsTask* task = tasks[i]; + if (task == NULL) continue; + hvjs_task_ref(task); + bool release_request_ref = !task->finished; + task->error = "javascript runtime closed"; + task->closing = true; + task->finished = true; + task->in_call = 0; + hvjs_task_cancel_timeout(task); + hvjs_task_cancel_ops(task, task->error.c_str()); + if (task->drain_scheduled) { + task->drain_scheduled = false; + hvjs_task_unref(task); + } + if (release_request_ref) { + hvjs_task_unref(task); + } + hvjs_task_unref(task); + } + if (runtime->rt) { + JS_RunGC(runtime->rt); + JS_FreeRuntime(runtime->rt); + runtime->rt = NULL; + } + delete runtime; +} + +int interrupt_handler(JSRuntime* rt, void* opaque) { + (void)opaque; + HvJsRuntime* runtime = (HvJsRuntime*)JS_GetRuntimeOpaque(rt); + HvJsTask* task = runtime ? runtime->current_task : NULL; + if (task == NULL || task->timeout_ms <= 0 || task->start_hrtime == 0) { + return 0; + } + uint64_t elapsed_us = gethrtime_us() - task->start_hrtime; + return elapsed_us >= (uint64_t)task->timeout_ms * 1000; +} + std::mutex& js_class_id_mutex() { static std::mutex mutex; return mutex; @@ -33,18 +82,95 @@ std::mutex& js_class_id_mutex() { void drain_event_cb(hevent_t* ev) { HvJsTask* task = (HvJsTask*)hevent_userdata(ev); + if (task) { + task->drain_scheduled = false; + } hvjs_drain_jobs(task); hvjs_task_unref(task); } +void finish_deferred_ops(HvJsTask* task) { + if (task == NULL || task->in_call > 0 || task->deferred_ops.empty()) return; + std::vector ops; + ops.swap(task->deferred_ops); + for (size_t i = 0; i < ops.size(); ++i) { + HvJsPromiseOp* op = ops[i]; + if (op == NULL || !op->completed || !op->defer_delete) continue; + delete_op(op); + } +} + +void finish_ready_task(HvJsTask* task) { + if (task == NULL) return; + finish_deferred_ops(task); + if (!task->finished && task->promise_settled) { + JSValue value = task->promise_result; + task->promise_result = JS_UNDEFINED; + if (task->finish) { + HvJsTaskScope scope(task); + task->finish(task, value); + } + else { + JS_FreeValue(task->js, value); + task->finished = true; + hvjs_task_unref(task); + } + } + else if (!task->finished && !task->error.empty()) { + if (task->finish) { + HvJsTaskScope scope(task); + task->finish(task, JS_UNDEFINED); + } + else { + task->finished = true; + hvjs_task_unref(task); + } + } +} + +void delete_op(HvJsPromiseOp* op) { + if (op == NULL) return; + HvJsTask* task = op->task; + if (op->handle) { + *op->handle = NULL; + } + delete op; + hvjs_task_unref(task); +} + +void cancel_op(HvJsPromiseOp* op, const char* reason) { + if (op == NULL) return; + HvJsTask* task = op->task; + JSContext* js = task ? task->js : NULL; + op->completed = true; + if (op->handle) { + *op->handle = NULL; + } + op->cancel(reason); + if (js) { + JS_FreeValue(js, op->resolve); + JS_FreeValue(js, op->reject); + } + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + delete op; + hvjs_task_unref(task); +} + void promise_complete(HvJsPromiseOp* op, JSValue value, bool ok) { + if (op == NULL) return; HvJsTask* task = op->task; + if (task == NULL || task->js == NULL) { + return; + } if (op->completed) { JS_FreeValue(task->js, value); return; } op->completed = true; + hvjs_task_remove_op(task, op); if (!task->closing) { + HvJsTaskScope scope(task); JSValue func = ok ? op->resolve : op->reject; JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); if (JS_IsException(ret) && task->error.empty()) { @@ -56,8 +182,9 @@ void promise_complete(HvJsPromiseOp* op, JSValue value, bool ok) { JS_FreeValue(task->js, op->reject); op->resolve = JS_UNDEFINED; op->reject = JS_UNDEFINED; - if (task->in_call) { + if (task->in_call > 0) { op->defer_delete = true; + task->deferred_ops.push_back(op); hvjs_schedule_drain(task); return; } @@ -70,15 +197,45 @@ void promise_complete(HvJsPromiseOp* op, JSValue value, bool ok) { op->resolve = JS_UNDEFINED; op->reject = JS_UNDEFINED; } - delete op; + delete_op(op); +} + +void task_timeout_timer_cb(htimer_t* timer) { + HvJsTask* task = (HvJsTask*)hevent_userdata(timer); + if (task == NULL) return; + hevent_set_userdata(timer, NULL); + task->timeout_timer = NULL; + if (!task->finished && !task->closing) { + task->error = "javascript request timeout"; + task->closing = true; + hvjs_task_cancel_ops(task, task->error.c_str()); + if (task->finish) { + task->finish(task, JS_UNDEFINED); + } + } hvjs_task_unref(task); } void sleep_timer_cb(htimer_t* timer) { HvJsSleep* sleep = (HvJsSleep*)hevent_userdata(timer); + if (sleep == NULL) return; + sleep->timer = NULL; hvjs_promise_resolve(sleep, JS_UNDEFINED); } +void HvJsSleep::cancel(const char* reason) { + if (timer_id != INVALID_TIMER_ID && task && task->loop_ptr) { + task->loop_ptr->killTimer(timer_id); + timer_id = INVALID_TIMER_ID; + } + if (timer) { + hevent_set_userdata(timer, NULL); + htimer_del(timer); + timer = NULL; + } + HvJsPromiseOp::cancel(reason); +} + void register_task_ref_class(JSContext* js) { std::call_once(s_task_ref_class_once, []() { hvjs_new_class_id(&s_task_ref_class_id); }); JSRuntime* rt = JS_GetRuntime(js); @@ -114,31 +271,30 @@ JSValue promise_settle_cb(JSContext* js, JSValueConst this_val, int argc, JSValu JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { (void)this_val; HvJsTask* task = hvjs_get_task(js); - if (task == NULL || argc < 1) return JS_EXCEPTION; + if (task == NULL) return hvjs_rejected_promise(js, "hv.sleep: no js task"); + if (argc < 1) return hvjs_rejected_promise(js, "hv.sleep: missing timeout"); int32_t ms = 0; - if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; + if (JS_ToInt32(js, &ms, argv[0]) != 0) return hvjs_rejected_promise(js, "hv.sleep: invalid timeout"); - HvJsSleep* sleep = new HvJsSleep(); - sleep->task = task; - sleep->resolve = funcs[0]; - JS_FreeValue(js, funcs[1]); - hvjs_task_ref(task); - if (task->loop_ptr) { - sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { hvjs_promise_resolve(sleep, JS_UNDEFINED); }); + HvJsSleep* sleep = NULL; + JSValue promise = hvjs_new_promise(js, task, &sleep); + if (JS_IsException(promise)) return promise; + std::shared_ptr handle = sleep->handle; + int delay = ms > 0 ? ms : 1; + if (task->loop_ptr && task->loop_ptr->isRunning()) { + sleep->timer_id = task->loop_ptr->setTimeout(delay, [handle](TimerID) { + HvJsPromiseOp* op = handle ? *handle : NULL; + if (op == NULL) return; + static_cast(op)->timer_id = INVALID_TIMER_ID; + hvjs_promise_resolve(op, JS_UNDEFINED); + }); } else { - sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); + sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)delay, 1); if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); } if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { - hvjs_task_unref(task); - JS_FreeValue(js, sleep->resolve); - delete sleep; - JS_FreeValue(js, promise); - return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); + hvjs_promise_reject(sleep, "hv.sleep: failed to create timer"); } return promise; } @@ -171,23 +327,81 @@ JSValue require_hv(JSContext* js) { } // namespace +HvJsRuntimeOptions::HvJsRuntimeOptions() : memory_limit(64 * 1024 * 1024), stack_size(1024 * 1024) {} + +HvJsRuntime::HvJsRuntime() : rt(NULL), current_task(NULL), tasks() {} + +HvJsTaskScope::HvJsTaskScope(HvJsTask* task) : runtime(task ? task->runtime : NULL), current(task), previous(runtime ? runtime->current_task : NULL) { + if (runtime) { + runtime->current_task = task; + } +} + +HvJsTaskScope::~HvJsTaskScope() { + if (runtime && runtime->current_task == current) { + runtime->current_task = previous; + } +} + HvJsTask::HvJsTask() - : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), promise_result(JS_UNDEFINED), promise_settled(false), promise_rejected(false), finished(false), - in_call(false), closing(false), refcount(1), finish(NULL) {} + : runtime(NULL), js(NULL), loop(NULL), loop_ptr(), promise(JS_UNDEFINED), promise_result(JS_UNDEFINED), promise_settled(false), promise_rejected(false), + finished(false), drain_scheduled(false), in_call(0), closing(false), refcount(1), start_hrtime(0), timeout_ms(0), timeout_timer_id(INVALID_TIMER_ID), + timeout_timer(NULL), finish(NULL) {} HvJsTask::~HvJsTask() {} -HvJsPromiseOp::HvJsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} +HvJsPromiseOp::HvJsPromiseOp() + : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false), handle(std::make_shared(this)) {} HvJsPromiseOp::~HvJsPromiseOp() {} +void HvJsPromiseOp::cancel(const char* reason) { + (void)reason; +} + +HvJsRuntime* hvjs_runtime(hloop_t* loop, const HvJsRuntimeOptions& options) { + if (loop == NULL) return NULL; + HvJsRuntime* runtime = (HvJsRuntime*)hloop_js_runtime(loop); + if (runtime) return runtime; + + runtime = new HvJsRuntime(); + runtime->options = options; + runtime->rt = JS_NewRuntime(); + if (runtime->rt == NULL) { + delete runtime; + return NULL; + } + if (runtime->options.memory_limit > 0) { + JS_SetMemoryLimit(runtime->rt, runtime->options.memory_limit); + } + if (runtime->options.stack_size > 0) { + JS_SetMaxStackSize(runtime->rt, runtime->options.stack_size); + } + JS_SetRuntimeOpaque(runtime->rt, runtime); + JS_SetInterruptHandler(runtime->rt, interrupt_handler, NULL); + hloop_set_js_runtime(loop, runtime, runtime_dtor); + return runtime; +} + void hvjs_task_ref(HvJsTask* task) { + if (task == NULL) return; ++task->refcount; } void hvjs_task_unref(HvJsTask* task) { + if (task == NULL) return; if (--task->refcount != 0) return; task->closing = true; + if (task->runtime && task->runtime->current_task == task) { + task->runtime->current_task = NULL; + } + if (task->runtime) { + auto iter = std::find(task->runtime->tasks.begin(), task->runtime->tasks.end(), task); + if (iter != task->runtime->tasks.end()) { + task->runtime->tasks.erase(iter); + } + } + finish_deferred_ops(task); if (!JS_IsUndefined(task->promise_result)) { JS_FreeValue(task->js, task->promise_result); task->promise_result = JS_UNDEFINED; @@ -197,30 +411,117 @@ void hvjs_task_unref(HvJsTask* task) { task->promise = JS_UNDEFINED; } if (task->js) { - if (task->rt) { - JS_RunGC(task->rt); + if (task->runtime && task->runtime->rt) { + JS_RunGC(task->runtime->rt); } JS_FreeContext(task->js); task->js = NULL; } - if (task->rt) { - JS_RunGC(task->rt); - JS_FreeRuntime(task->rt); - task->rt = NULL; - } delete task; } +void hvjs_task_set_runtime(HvJsTask* task, HvJsRuntime* runtime) { + if (task == NULL) return; + task->runtime = runtime; + if (runtime) { + runtime->tasks.push_back(task); + } +} + +bool hvjs_task_start_timeout(HvJsTask* task, int timeout_ms) { + if (task == NULL || timeout_ms <= 0) return true; + task->timeout_ms = timeout_ms; + if (task->start_hrtime == 0) { + task->start_hrtime = gethrtime_us(); + } + hvjs_task_ref(task); + if (task->loop_ptr && task->loop_ptr->isRunning()) { + task->timeout_timer_id = task->loop_ptr->setTimeout(timeout_ms, [task](TimerID timerID) { + if (task->timeout_timer_id != timerID) return; + task->timeout_timer_id = INVALID_TIMER_ID; + if (!task->finished && !task->closing) { + task->error = "javascript request timeout"; + task->closing = true; + hvjs_task_cancel_ops(task, task->error.c_str()); + if (task->finish) { + task->finish(task, JS_UNDEFINED); + } + } + hvjs_task_unref(task); + }); + if (task->timeout_timer_id == INVALID_TIMER_ID) { + hvjs_task_unref(task); + return false; + } + return true; + } + if (task->loop) { + task->timeout_timer = htimer_add(task->loop, task_timeout_timer_cb, (uint32_t)timeout_ms, 1); + if (task->timeout_timer) { + hevent_set_userdata(task->timeout_timer, task); + return true; + } + } + hvjs_task_unref(task); + return false; +} + +void hvjs_task_cancel_timeout(HvJsTask* task) { + if (task == NULL) return; + if (task->timeout_timer_id != INVALID_TIMER_ID && task->loop_ptr) { + if (task->loop_ptr->isRunning()) { + task->loop_ptr->killTimer(task->timeout_timer_id); + } + task->timeout_timer_id = INVALID_TIMER_ID; + hvjs_task_unref(task); + } + if (task->timeout_timer) { + htimer_t* timer = task->timeout_timer; + hevent_set_userdata(timer, NULL); + htimer_del(timer); + task->timeout_timer = NULL; + hvjs_task_unref(task); + } +} + +void hvjs_task_add_op(HvJsTask* task, HvJsPromiseOp* op) { + if (task == NULL || op == NULL) return; + task->ops.push_back(op); +} + +void hvjs_task_remove_op(HvJsTask* task, HvJsPromiseOp* op) { + if (task == NULL || op == NULL) return; + auto iter = std::find(task->ops.begin(), task->ops.end(), op); + if (iter != task->ops.end()) { + task->ops.erase(iter); + } +} + +void hvjs_task_cancel_ops(HvJsTask* task, const char* message) { + if (task == NULL) return; + std::vector ops; + ops.swap(task->ops); + for (size_t i = 0; i < ops.size(); ++i) { + HvJsPromiseOp* op = ops[i]; + if (op == NULL || op->completed) continue; + cancel_op(op, message); + } + finish_deferred_ops(task); +} + void hvjs_schedule_drain(HvJsTask* task) { if (task == NULL || task->closing) return; + if (task->drain_scheduled) return; + task->drain_scheduled = true; hvjs_task_ref(task); - if (task->loop_ptr) { + if (task->loop_ptr && task->loop_ptr->loop() && hloop_status(task->loop_ptr->loop()) == HLOOP_STATUS_RUNNING) { task->loop_ptr->queueInLoop([task]() { + task->drain_scheduled = false; hvjs_drain_jobs(task); hvjs_task_unref(task); }); } - else if (task->loop) { + else if (task->loop && hloop_status(task->loop) == HLOOP_STATUS_RUNNING) { hevent_t ev; memset(&ev, 0, sizeof(ev)); ev.cb = drain_event_cb; @@ -228,6 +529,7 @@ void hvjs_schedule_drain(HvJsTask* task) { hloop_post_event(task->loop, &ev); } else { + task->drain_scheduled = false; hvjs_task_unref(task); } } @@ -235,6 +537,7 @@ void hvjs_schedule_drain(HvJsTask* task) { bool hvjs_watch_promise(HvJsTask* task, std::string* err) { if (task == NULL || task->js == NULL || JS_IsUndefined(task->promise)) return false; JSContext* js = task->js; + HvJsTaskScope scope(task); JSValue then = JS_GetPropertyStr(js, task->promise, "then"); if (JS_IsException(then)) { if (err) *err = hvjs_exception_string(js); @@ -278,35 +581,24 @@ bool hvjs_watch_promise(HvJsTask* task, std::string* err) { } void hvjs_drain_jobs(HvJsTask* task) { + if (task == NULL || task->finished || task->js == NULL || task->runtime == NULL || task->runtime->rt == NULL) return; + HvJsTaskScope scope(task); JSContext* job_ctx = NULL; - while (JS_IsJobPending(task->rt)) { - int rc = JS_ExecutePendingJob(task->rt, &job_ctx); + JSRuntime* rt = task->runtime->rt; + while (JS_IsJobPending(rt)) { + int rc = JS_ExecutePendingJob(rt, &job_ctx); if (rc < 0) { - task->error = hvjs_exception_string(job_ctx ? job_ctx : task->js); + HvJsTask* job_task = job_ctx ? hvjs_get_task(job_ctx) : task; + if (job_task == NULL) job_task = task; + job_task->error = hvjs_exception_string(job_ctx ? job_ctx : task->js); break; } } - if (!task->finished && task->promise_settled) { - JSValue value = task->promise_result; - task->promise_result = JS_UNDEFINED; - if (task->finish) { - task->finish(task, value); - } - else { - JS_FreeValue(task->js, value); - task->finished = true; - hvjs_task_unref(task); - } - return; - } - if (!task->error.empty()) { - if (task->finish) { - task->finish(task, JS_UNDEFINED); - } - else { - task->finished = true; - hvjs_task_unref(task); - } + + HvJsRuntime* runtime = task->runtime; + std::vector tasks = runtime->tasks; + for (size_t i = 0; i < tasks.size(); ++i) { + finish_ready_task(tasks[i]); } } @@ -315,6 +607,7 @@ void hvjs_promise_resolve(HvJsPromiseOp* op, JSValue value) { } void hvjs_promise_reject(HvJsPromiseOp* op, const char* message) { + if (op == NULL || op->task == NULL || op->task->js == NULL) return; promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); } @@ -347,10 +640,11 @@ JSValue hvjs_async_resolved_promise(JSContext* js, HvJsTask* task, JSValue value } void hvjs_finish_deferred_op(HvJsPromiseOp* op) { - if (op == NULL || !op->completed || !op->defer_delete) return; + if (op == NULL) return; HvJsTask* task = op->task; - delete op; - hvjs_task_unref(task); + if (task && task->in_call == 0) { + finish_deferred_ops(task); + } } std::string hvjs_to_string(JSContext* ctx, JSValueConst value) { diff --git a/js/hvjs.h b/js/hvjs.h index 5c1f1f0dc..ae7c25e1f 100644 --- a/js/hvjs.h +++ b/js/hvjs.h @@ -1,7 +1,12 @@ #ifndef HV_JS_H_ #define HV_JS_H_ +#include +#include + +#include #include +#include #include @@ -11,10 +16,38 @@ namespace hv { namespace js { +struct HvJsTask; +struct HvJsPromiseOp; + +struct HV_EXPORT HvJsRuntimeOptions { + size_t memory_limit; + size_t stack_size; + + HvJsRuntimeOptions(); +}; + +struct HV_EXPORT HvJsRuntime { + JSRuntime* rt; + HvJsRuntimeOptions options; + HvJsTask* current_task; + std::vector tasks; + + HvJsRuntime(); +}; + +struct HV_EXPORT HvJsTaskScope { + HvJsRuntime* runtime; + HvJsTask* current; + HvJsTask* previous; + + explicit HvJsTaskScope(HvJsTask* task); + ~HvJsTaskScope(); +}; + struct HV_EXPORT HvJsTask { typedef void (*FinishCallback)(HvJsTask* task, JSValue result); - JSRuntime* rt; + HvJsRuntime* runtime; JSContext* js; hloop_t* loop; EventLoopPtr loop_ptr; @@ -23,11 +56,18 @@ struct HV_EXPORT HvJsTask { bool promise_settled; bool promise_rejected; bool finished; - bool in_call; + bool drain_scheduled; + int in_call; bool closing; int refcount; + uint64_t start_hrtime; + int timeout_ms; + TimerID timeout_timer_id; + htimer_t* timeout_timer; std::string error; FinishCallback finish; + std::vector ops; + std::vector deferred_ops; HvJsTask(); virtual ~HvJsTask(); @@ -39,13 +79,23 @@ struct HV_EXPORT HvJsPromiseOp { JSValue reject; bool completed; bool defer_delete; + std::shared_ptr handle; HvJsPromiseOp(); virtual ~HvJsPromiseOp(); + virtual void cancel(const char* reason); }; +HV_EXPORT HvJsRuntime* hvjs_runtime(hloop_t* loop, const HvJsRuntimeOptions& options); + +HV_EXPORT void hvjs_task_set_runtime(HvJsTask* task, HvJsRuntime* runtime); HV_EXPORT void hvjs_task_ref(HvJsTask* task); HV_EXPORT void hvjs_task_unref(HvJsTask* task); +HV_EXPORT bool hvjs_task_start_timeout(HvJsTask* task, int timeout_ms); +HV_EXPORT void hvjs_task_cancel_timeout(HvJsTask* task); +HV_EXPORT void hvjs_task_add_op(HvJsTask* task, HvJsPromiseOp* op); +HV_EXPORT void hvjs_task_remove_op(HvJsTask* task, HvJsPromiseOp* op); +HV_EXPORT void hvjs_task_cancel_ops(HvJsTask* task, const char* reason); HV_EXPORT void hvjs_schedule_drain(HvJsTask* task); HV_EXPORT bool hvjs_watch_promise(HvJsTask* task, std::string* err = NULL); HV_EXPORT void hvjs_drain_jobs(HvJsTask* task); @@ -54,11 +104,21 @@ template JSValue hvjs_new_promise(JSContext* js, HvJsTask* task, T* JSValue funcs[2]; JSValue promise = JS_NewPromiseCapability(js, funcs); if (JS_IsException(promise)) return promise; + if (task == NULL) { + JS_FreeValue(js, funcs[0]); + JS_FreeValue(js, funcs[1]); + JS_FreeValue(js, promise); + return JS_ThrowInternalError(js, "invalid hvjs task"); + } T* op = new T(); op->task = task; op->resolve = funcs[0]; op->reject = funcs[1]; + if (op->handle) { + *op->handle = op; + } hvjs_task_ref(task); + hvjs_task_add_op(task, op); *out = op; return promise; } diff --git a/js/hvjs_http.cpp b/js/hvjs_http.cpp index 747ecd875..59d598abb 100644 --- a/js/hvjs_http.cpp +++ b/js/hvjs_http.cpp @@ -23,8 +23,26 @@ namespace { static const int JS_HTTP_METHOD_REQUEST = -1; +void js_http_release_client_after_callback(const EventLoopPtr& loop, const std::shared_ptr& client) { + if (!client) return; + if (loop && loop->loop() && hloop_status(loop->loop()) == HLOOP_STATUS_RUNNING) { + loop->queueInLoop([client]() {}); + } +} + struct HvJsHttpRequest : public HvJsPromiseOp { + HttpRequestPtr req; std::shared_ptr client; + + void cancel(const char* reason) override { + if (req) { + req->Cancel(); + } + std::shared_ptr hold = client; + client.reset(); + js_http_release_client_after_callback(task ? task->loop_ptr : EventLoopPtr(), hold); + HvJsPromiseOp::cancel(reason); + } }; JSValue js_push_headers(JSContext* js, const http_headers& headers) { @@ -51,7 +69,6 @@ JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { if (argc <= url_index) { - JS_ThrowTypeError(js, "missing url"); return -1; } std::string url = hvjs_to_string(js, argv[url_index]); @@ -103,20 +120,24 @@ JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueC HttpRequestPtr req; if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { - return JS_EXCEPTION; + return hvjs_rejected_promise(js, "hv.http: missing url"); } HvJsHttpRequest* op = NULL; JSValue promise = hvjs_new_promise(js, task, &op); if (JS_IsException(promise)) return promise; + op->req = req; op->client = std::make_shared(task->loop_ptr); std::shared_ptr client = op->client; - task->in_call = true; - int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { - if (op->task->loop_ptr) { - op->task->loop_ptr->queueInLoop([client]() {}); - } - JSContext* js = op->task->js; + std::shared_ptr handle = op->handle; + ++task->in_call; + int ret = client->send(req, [handle, client](const HttpResponsePtr& resp) { + HvJsPromiseOp* base = handle ? *handle : NULL; + if (base == NULL || base->task == NULL) return; + HvJsHttpRequest* op = static_cast(base); + op->client.reset(); + js_http_release_client_after_callback(base->task->loop_ptr, client); + JSContext* js = base->task->js; if (resp) { hvjs_promise_resolve(op, js_push_http_response(js, resp)); } @@ -127,7 +148,7 @@ JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueC if (ret != 0) { hvjs_promise_reject(op, "hv.http: request failed"); } - task->in_call = false; + --task->in_call; hvjs_finish_deferred_op(op); return promise; } @@ -161,27 +182,71 @@ struct HvJsWsState { ~HvJsWsState() { detach(); } }; +void js_ws_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); + struct HvJsWsClient { std::shared_ptr state; }; +struct HvJsWsDetachEvent { + std::shared_ptr state; +}; + struct HvJsWsConnect : public HvJsPromiseOp { std::shared_ptr state; + + void cancel(const char* reason) override { + std::shared_ptr hold = state; + if (hold) { + hold->connect_op = NULL; + js_ws_detach_after_callback(task ? task->loop_ptr : EventLoopPtr(), task ? task->loop : NULL, hold); + } + HvJsPromiseOp::cancel(reason); + } }; struct HvJsWsRecv : public HvJsPromiseOp { std::shared_ptr state; + + void cancel(const char* reason) override { + std::shared_ptr hold = state; + if (hold) { + hold->recv_op = NULL; + if (!hold->js_alive && hold->connect_op == NULL) { + js_ws_detach_after_callback(task ? task->loop_ptr : EventLoopPtr(), task ? task->loop : NULL, hold); + } + } + HvJsPromiseOp::cancel(reason); + } }; HvJsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { return (HvJsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); } -void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { +void js_ws_detach_event_cb(hevent_t* ev) { + HvJsWsDetachEvent* detach = (HvJsWsDetachEvent*)hevent_userdata(ev); + if (detach) { + detach->state->detach(); + delete detach; + } +} + +void js_ws_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { if (!state) return; - if (loop) { + hloop_t* event_loop = loop ? loop->loop() : NULL; + if (loop && event_loop && hloop_status(event_loop) == HLOOP_STATUS_RUNNING) { loop->queueInLoop([state]() { state->detach(); }); } + else if (raw_loop && hloop_status(raw_loop) == HLOOP_STATUS_RUNNING) { + HvJsWsDetachEvent* detach = new HvJsWsDetachEvent(); + detach->state = state; + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = js_ws_detach_event_cb; + ev.userdata = detach; + hloop_post_event(raw_loop, &ev); + } else { state->detach(); } @@ -216,6 +281,7 @@ void js_ws_try_deliver(const std::shared_ptr& state) { HvJsWsRecv* op = static_cast(state->recv_op); std::shared_ptr hold = op->state; EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; if (!state->inbox.empty()) { std::string msg = std::move(state->inbox.front()); state->inbox.pop_front(); @@ -227,7 +293,7 @@ void js_ws_try_deliver(const std::shared_ptr& state) { hvjs_promise_reject(op, "closed"); } if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_ws_detach_after_callback(loop, hold); + js_ws_detach_after_callback(loop, raw_loop, hold); } } @@ -324,6 +390,13 @@ JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueCon js_ws_register_class(js); std::shared_ptr state = std::make_shared(); state->client = std::make_shared(task->loop_ptr); + if (argc > 1 && JS_IsObject(argv[1])) { + int timeout = hvjs_get_int_property(js, argv[1], "connect_timeout", 0); + if (timeout <= 0) timeout = hvjs_get_int_property(js, argv[1], "timeout", 0); + int ping_interval = hvjs_get_int_property(js, argv[1], "ping_interval", 0); + if (timeout > 0) state->client->setConnectTimeout(timeout); + if (ping_interval > 0) state->client->setPingInterval(ping_interval); + } HvJsWsConnect* op = NULL; JSValue promise = hvjs_new_promise(js, task, &op); @@ -337,11 +410,12 @@ JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueCon HvJsWsConnect* op = static_cast(state->connect_op); std::shared_ptr hold = op->state; EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; state->connect_op = NULL; JSValue obj = js_ws_new_client_object(op->task->js, hold); if (JS_IsException(obj)) { hvjs_promise_reject(op, "hv.ws: create client failed"); - js_ws_detach_after_callback(loop, hold); + js_ws_detach_after_callback(loop, raw_loop, hold); } else { hvjs_promise_resolve(op, obj); @@ -359,20 +433,21 @@ JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueCon HvJsWsConnect* op = static_cast(state->connect_op); std::shared_ptr hold = op->state; EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; state->connect_op = NULL; hvjs_promise_reject(op, "closed"); - js_ws_detach_after_callback(loop, hold); + js_ws_detach_after_callback(loop, raw_loop, hold); } js_ws_try_deliver(state); }; - task->in_call = true; + ++task->in_call; int ret = state->client->open(url.c_str()); if (ret != 0) { state->connect_op = NULL; hvjs_promise_reject(op, "hv.ws: open failed"); state->detach(); } - task->in_call = false; + --task->in_call; hvjs_finish_deferred_op(op); return promise; } diff --git a/js/hvjs_mqtt.cpp b/js/hvjs_mqtt.cpp index bf3409537..f654415c7 100644 --- a/js/hvjs_mqtt.cpp +++ b/js/hvjs_mqtt.cpp @@ -51,16 +51,38 @@ struct HvJsMqttState { ~HvJsMqttState() { detach(); } }; +void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); + struct HvJsMqttClient { std::shared_ptr state; }; struct HvJsMqttConnect : public HvJsPromiseOp { std::shared_ptr state; + + void cancel(const char* reason) override { + std::shared_ptr hold = state; + if (hold) { + hold->connect_op = NULL; + js_mqtt_detach_after_callback(task ? task->loop_ptr : EventLoopPtr(), task ? task->loop : NULL, hold); + } + HvJsPromiseOp::cancel(reason); + } }; struct HvJsMqttRecv : public HvJsPromiseOp { std::shared_ptr state; + + void cancel(const char* reason) override { + std::shared_ptr hold = state; + if (hold) { + hold->recv_op = NULL; + if (!hold->js_alive && hold->connect_op == NULL) { + js_mqtt_detach_after_callback(task ? task->loop_ptr : EventLoopPtr(), task ? task->loop : NULL, hold); + } + } + HvJsPromiseOp::cancel(reason); + } }; struct HvJsMqttDetachEvent { @@ -76,7 +98,6 @@ JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueC JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { (void)rt; @@ -164,10 +185,10 @@ void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, if (state->client) { mqtt_client_set_reconnect(state->client, NULL); } - if (loop) { + if (loop && loop->loop() && hloop_status(loop->loop()) == HLOOP_STATUS_RUNNING) { loop->queueInLoop([state]() { state->detach(); }); } - else if (raw_loop) { + else if (raw_loop && hloop_status(raw_loop) == HLOOP_STATUS_RUNNING) { HvJsMqttDetachEvent* detach = new HvJsMqttDetachEvent(); detach->state = state; hevent_t ev; @@ -314,14 +335,14 @@ JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueC } state->connect_op = op; op->state = state; - task->in_call = true; + ++task->in_call; int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); if (ret != 0) { state->connect_op = NULL; hvjs_promise_reject(op, "hv.mqtt: connect failed"); state->detach(); } - task->in_call = false; + --task->in_call; hvjs_finish_deferred_op(op); return promise; } diff --git a/js/hvjs_redis.cpp b/js/hvjs_redis.cpp index 0a3dfaa5b..59f7afd38 100644 --- a/js/hvjs_redis.cpp +++ b/js/hvjs_redis.cpp @@ -167,12 +167,17 @@ JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValue JSValue promise = hvjs_new_promise(js, task, &op); if (JS_IsException(promise)) return promise; op->redis = box->state; - task->in_call = true; - int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); + std::shared_ptr handle = op->handle; + ++task->in_call; + int ret = state->client->command(cmd, [handle](const RedisResult& result) { + HvJsPromiseOp* op = handle ? *handle : NULL; + if (op == NULL) return; + js_redis_resolve_result(static_cast(op), result); + }); if (ret != 0) { hvjs_promise_reject(op, "hv.redis: request failed"); } - task->in_call = false; + --task->in_call; hvjs_finish_deferred_op(op); return promise; } diff --git a/scripts/unittest.sh b/scripts/unittest.sh index ebffce7cd..5a4d05668 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -36,16 +36,16 @@ if [ -x bin/tlv_test ]; then bin/tlv_test fi if [ -x bin/lua_binding_test ]; then - bin/lua_binding_test + bin/lua_binding_test || exit $? fi if [ -x bin/lua_io_test ]; then - bin/lua_io_test + bin/lua_io_test || exit $? fi if [ -x bin/http_script_handler_test ]; then - bin/http_script_handler_test + bin/http_script_handler_test || exit $? fi if [ -x bin/http_lua_handler_test ]; then - bin/http_lua_handler_test + bin/http_lua_handler_test || exit $? fi if [ -x bin/http_js_handler_test ]; then bin/http_js_handler_test || exit $? @@ -60,13 +60,13 @@ if [ -x bin/http_js_mqtt_test ]; then bin/http_js_mqtt_test || exit $? fi if [ -x bin/lua_http_test ]; then - bin/lua_http_test + bin/lua_http_test || exit $? fi if [ -x bin/lua_ws_test ]; then - bin/lua_ws_test + bin/lua_ws_test || exit $? fi if [ -x bin/lua_mqtt_test ]; then - bin/lua_mqtt_test + bin/lua_mqtt_test || exit $? fi if [ -x bin/hdns_test ]; then bin/hdns_test @@ -76,7 +76,11 @@ if [ -x bin/tcpclient_dns_test ]; then fi for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test lua_redis_test; do if [ -x bin/${redis_test} ]; then - bin/${redis_test} + if [ "${redis_test}" = "lua_redis_test" ]; then + bin/${redis_test} || exit $? + else + bin/${redis_test} + fi fi done if [ -x bin/redis_protocol_test ]; then diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 47b5b4bdd..d0a1327fc 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -135,7 +135,7 @@ endif() if(WITH_JS AND WITH_EVPP AND WITH_HTTP AND WITH_HTTP_SERVER AND WITH_HTTP_CLIENT) add_executable(http_js_handler_test http_js_handler_test.cpp) -target_include_directories(http_js_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) +target_include_directories(http_js_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../js ../http ../http/server ../http/client) target_link_libraries(http_js_handler_test ${HV_LIBRARIES}) set(HTTP_JS_UNITTEST_TARGETS http_js_handler_test) if(WITH_REDIS) @@ -144,12 +144,10 @@ target_include_directories(http_js_redis_test PRIVATE .. ../base ../ssl ../event target_link_libraries(http_js_redis_test ${HV_LIBRARIES}) set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_redis_test) endif() -if(WITH_HTTP_CLIENT) add_executable(http_js_ws_test http_js_ws_test.cpp) target_include_directories(http_js_ws_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) target_link_libraries(http_js_ws_test ${HV_LIBRARIES}) set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_ws_test) -endif() if(WITH_MQTT) add_executable(http_js_mqtt_test http_js_mqtt_test.cpp) target_include_directories(http_js_mqtt_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client ../mqtt) diff --git a/unittest/http_js_handler_test.cpp b/unittest/http_js_handler_test.cpp index e5b28dc29..3049decc3 100644 --- a/unittest/http_js_handler_test.cpp +++ b/unittest/http_js_handler_test.cpp @@ -23,6 +23,7 @@ #include "HttpServer.h" #include "HttpService.h" #include "HttpScriptHandler.h" +#include "hvjs.h" #include "requests.h" #define CHECK(expr) \ @@ -45,6 +46,14 @@ static std::string write_script(const char* name, const char* content) { } int main() { + hloop_t* loop = hloop_new(0); + hv::js::HvJsRuntimeOptions runtime_options; + hv::js::HvJsRuntime* runtime1 = hv::js::hvjs_runtime(loop, runtime_options); + hv::js::HvJsRuntime* runtime2 = hv::js::hvjs_runtime(loop, runtime_options); + CHECK(runtime1 != NULL); + CHECK(runtime1 == runtime2); + hloop_free(&loop); + std::string script = write_script("sleep.js", "const hv = require('hv');\n" "const http = require('hv/http');\n" "async function get(ctx) {\n" @@ -61,6 +70,12 @@ int main() { " data.self = data;\n" " return data;\n" "}\n"); + std::string pending_script = write_script("pending.js", "function get(ctx) {\n" + " return new Promise(function() {});\n" + "}\n"); + std::string spin_script = write_script("spin.js", "function get(ctx) {\n" + " while (true) {}\n" + "}\n"); HttpService service; service.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { @@ -71,6 +86,10 @@ int main() { service.GET("/sleep", hv::HttpScriptHandler(script.c_str())); service.GET("/direct", hv::HttpJsHandler(direct_script.c_str())); service.GET("/circular", hv::HttpJsHandler(circular_script.c_str())); + hv::HttpJsHandlerOptions timeout_options; + timeout_options.timeout_ms = 100; + service.GET("/pending", hv::HttpJsHandler(pending_script.c_str(), timeout_options)); + service.GET("/spin", hv::HttpJsHandler(spin_script.c_str(), timeout_options)); hv::HttpServer server(&service); server.setThreadNum(1); @@ -118,11 +137,22 @@ int main() { char circular_url[128]; snprintf(circular_url, sizeof(circular_url), "http://127.0.0.1:%d/circular", server_port); auto circular_resp = requests::get(circular_url); + char pending_url[128]; + snprintf(pending_url, sizeof(pending_url), "http://127.0.0.1:%d/pending", server_port); + uint64_t pending_start = gettimeofday_ms(); + auto pending_resp = requests::get(pending_url); + uint64_t pending_elapsed = gettimeofday_ms() - pending_start; + char spin_url[128]; + snprintf(spin_url, sizeof(spin_url), "http://127.0.0.1:%d/spin", server_port); + uint64_t spin_start = gettimeofday_ms(); + auto spin_resp = requests::get(spin_url); + uint64_t spin_elapsed = gettimeofday_ms() - spin_start; server.stop(); hv_msleep(100); - printf("ok_count=%d/%d elapsed=%llums (each handler awaits 300ms)\n", ok_count.load(), N, (unsigned long long)elapsed); + printf("ok_count=%d/%d elapsed=%llums pending=%llums spin=%llums\n", + ok_count.load(), N, (unsigned long long)elapsed, (unsigned long long)pending_elapsed, (unsigned long long)spin_elapsed); CHECK(ok_count.load() == N); CHECK(elapsed < 1200); CHECK(direct_resp != NULL); @@ -131,7 +161,15 @@ int main() { CHECK(direct_resp->GetHeader("X-From") == "js"); CHECK(circular_resp != NULL); CHECK(circular_resp->status_code == 500); - CHECK(circular_resp->body.find("circular") != std::string::npos); + CHECK(circular_resp->body == "javascript handler error"); + CHECK(pending_resp != NULL); + CHECK(pending_resp->status_code == 500); + CHECK(pending_resp->body == "javascript handler error"); + CHECK(pending_elapsed < 1000); + CHECK(spin_resp != NULL); + CHECK(spin_resp->status_code == 500); + CHECK(spin_resp->body == "javascript handler error"); + CHECK(spin_elapsed < 1000); printf("ALL http_js_handler_test PASSED\n"); return 0; } diff --git a/unittest/http_js_ws_test.cpp b/unittest/http_js_ws_test.cpp index 18eb66df9..1f12aaecc 100644 --- a/unittest/http_js_ws_test.cpp +++ b/unittest/http_js_ws_test.cpp @@ -10,8 +10,10 @@ #include "hbase.h" #include "hfile.h" #include "hpath.h" +#include "htime.h" #include "HttpServer.h" #include "HttpService.h" +#include "HttpJsHandler.h" #include "HttpScriptHandler.h" #include "WebSocketServer.h" #include "requests.h" @@ -38,17 +40,23 @@ static std::string write_script(const char* name, const char* content) { int main() { WebSocketService ws_service; ws_service.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { channel->send(msg); }; + WebSocketService idle_ws_service; hv::WebSocketServer ws_server(&ws_service); ws_server.setPort(0); ws_server.setThreadNum(1); CHECK(ws_server.start() == 0); CHECK(ws_server.port > 0); + hv::WebSocketServer idle_ws_server(&idle_ws_service); + idle_ws_server.setPort(0); + idle_ws_server.setThreadNum(1); + CHECK(idle_ws_server.start() == 0); + CHECK(idle_ws_server.port > 0); char script_buf[1024]; snprintf(script_buf, sizeof(script_buf), "const wsmod = require('hv/ws');\n" "async function get(ctx) {\n" - " const ws = await wsmod.connect('ws://127.0.0.1:%d/');\n" + " const ws = await wsmod.connect('ws://127.0.0.1:%d/', { timeout: 500, ping_interval: 100 });\n" " ws.send('hello-js');\n" " const msg = await ws.recv();\n" " ws.close();\n" @@ -56,9 +64,32 @@ int main() { "}\n", ws_server.port); std::string script = write_script("ws.js", script_buf); + snprintf(script_buf, sizeof(script_buf), + "const wsmod = require('hv/ws');\n" + "async function get(ctx) {\n" + " const ws = await wsmod.connect('ws://127.0.0.1:%d/', { timeout: 500, ping_interval: 100 });\n" + " const msg = await ws.recv();\n" + " ws.close();\n" + " return { ok: true, msg };\n" + "}\n", + idle_ws_server.port); + std::string idle_script = write_script("ws_idle.js", script_buf); + snprintf(script_buf, sizeof(script_buf), + "const wsmod = require('hv/ws');\n" + "async function get(ctx) {\n" + " const ws = await wsmod.connect('ws://127.0.0.1:%d/', { timeout: 500, ping_interval: 100 });\n" + " ws.recv();\n" + " return { ok: true };\n" + "}\n", + idle_ws_server.port); + std::string fireforget_script = write_script("ws_fireforget.js", script_buf); HttpService service; service.GET("/ws", hv::HttpScriptHandler(script.c_str())); + hv::HttpJsHandlerOptions timeout_options; + timeout_options.timeout_ms = 100; + service.GET("/ws_idle", hv::HttpJsHandler(idle_script.c_str(), timeout_options)); + service.GET("/ws_fireforget", hv::HttpJsHandler(fireforget_script.c_str(), timeout_options)); hv::HttpServer server(&service); server.setThreadNum(1); @@ -70,14 +101,28 @@ int main() { char url[128]; snprintf(url, sizeof(url), "http://127.0.0.1:%d/ws", server.port); auto resp = requests::get(url); + snprintf(url, sizeof(url), "http://127.0.0.1:%d/ws_idle", server.port); + uint64_t idle_start = gettimeofday_ms(); + auto idle_resp = requests::get(url); + uint64_t idle_elapsed = gettimeofday_ms() - idle_start; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/ws_fireforget", server.port); + auto fireforget_resp = requests::get(url); server.stop(); ws_server.stop(); + idle_ws_server.stop(); hv_msleep(100); CHECK(resp != NULL); CHECK(resp->status_code == 200); CHECK(resp->body.find("\"ok\":true") != std::string::npos); CHECK(resp->body.find("\"msg\":\"hello-js\"") != std::string::npos); + CHECK(idle_resp != NULL); + CHECK(idle_resp->status_code == 500); + CHECK(idle_resp->body == "javascript handler error"); + CHECK(idle_elapsed < 1000); + CHECK(fireforget_resp != NULL); + CHECK(fireforget_resp->status_code == 200); + CHECK(fireforget_resp->body.find("\"ok\":true") != std::string::npos); printf("ALL http_js_ws_test PASSED\n"); return 0; } From 02abd36ffd03782d97b13a5a0e8c20dbfd5d9e7d Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 12:00:04 +0800 Subject: [PATCH 08/13] ci: build quickjs with pic --- .github/workflows/CI.yml | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 343909e90..37a96d910 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,18 +19,24 @@ jobs: - name: build run: | sudo apt update - sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs - # Ubuntu packages libquickjs as a non-PIC static library, so JS is - # covered in a static libhv build instead of linking it into libhv.so. - make clean - ./configure --disable-shared --with-http --with-mqtt --with-redis --with-js - make libhv hvjs unittest + sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler + QUICKJS_COMMIT=04be246001599f5995fa2f2d8c91a0f198d3f34c + rm -rf /tmp/quickjs + git init -q /tmp/quickjs + git -C /tmp/quickjs remote add origin https://github.com/bellard/quickjs.git + git -C /tmp/quickjs fetch -q --depth 1 origin ${QUICKJS_COMMIT} + git -C /tmp/quickjs checkout -q FETCH_HEAD + printf '\nCFLAGS_OPT+=-fPIC\n' >> /tmp/quickjs/Makefile + make -C /tmp/quickjs libquickjs.a -j$(nproc) + sudo mkdir -p /usr/local/include/quickjs /usr/local/lib/quickjs + sudo install -m644 /tmp/quickjs/quickjs.h /tmp/quickjs/quickjs-libc.h /usr/local/include/quickjs/ + sudo install -m644 /tmp/quickjs/libquickjs.a /usr/local/lib/quickjs/ + export QUICKJS_ROOT=/usr/local + echo "QUICKJS_ROOT=/usr/local" >> $GITHUB_ENV + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc --with-js + make libhv hvjs evpp unittest bin/hvjs examples/js/sleep.js make run-unittest - make clean - rm -f bin/hvjs bin/http_js_handler_test bin/http_js_redis_test bin/http_js_ws_test bin/http_js_mqtt_test - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc - make libhv evpp # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr make libhrpc hrpc PROTOBUF_PREFIX=/usr From 72c596de57f075b5ca215ab394e31cb69e7c1726 Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 12:24:12 +0800 Subject: [PATCH 09/13] style: unify code style --- .github/workflows/CI.yml | 22 ++++++++++++++-------- CMakeLists.txt | 2 +- Makefile | 2 +- Makefile.vars | 8 ++++++++ cmake/vars.cmake | 6 ++++++ docs/PLAN.md | 1 - 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 37a96d910..4aac5fcad 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,10 +16,8 @@ jobs: steps: - uses: actions/checkout@v3 - - name: build + - name: build-quickjs run: | - sudo apt update - sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler QUICKJS_COMMIT=04be246001599f5995fa2f2d8c91a0f198d3f34c rm -rf /tmp/quickjs git init -q /tmp/quickjs @@ -31,14 +29,22 @@ jobs: sudo mkdir -p /usr/local/include/quickjs /usr/local/lib/quickjs sudo install -m644 /tmp/quickjs/quickjs.h /tmp/quickjs/quickjs-libc.h /usr/local/include/quickjs/ sudo install -m644 /tmp/quickjs/libquickjs.a /usr/local/lib/quickjs/ + + - name: build + run: | + sudo apt update + sudo apt install libssl-dev libnghttp2-dev libprotobuf-dev libprotoc-dev protobuf-compiler liblua5.4-dev export QUICKJS_ROOT=/usr/local - echo "QUICKJS_ROOT=/usr/local" >> $GITHUB_ENV - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc --with-js - make libhv hvjs evpp unittest - bin/hvjs examples/js/sleep.js - make run-unittest + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-rpc --with-lua --with-js + make libhv evpp unittest # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr make libhrpc hrpc PROTOBUF_PREFIX=/usr + # hvlua = libhv + lua binding + make hvlua + bin/hvlua examples/lua/sleep.lua + # hvjs = libhv + js binding + make hvjs + bin/hvjs examples/js/sleep.js - name: test run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 073117dbe..899b01629 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,7 +316,7 @@ if(WITH_PROTOCOL) endif() if(WITH_LUA) - set(LIBHV_HEADERS ${LIBHV_HEADERS} lua/hvlua.h lua/hvlua_json.h lua/hvlua_util.h) + set(LIBHV_HEADERS ${LIBHV_HEADERS} ${LUA_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} lua) if(NOT WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS}) diff --git a/Makefile b/Makefile index 5470331f3..9e8cd0b9f 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ LIBHV_SRCDIRS += protocol endif ifeq ($(WITH_LUA), yes) -LIBHV_HEADERS += lua/hvlua.h lua/hvlua_json.h lua/hvlua_util.h +LIBHV_HEADERS += $(LUA_HEADERS) LIBHV_SRCDIRS += lua ifneq ($(WITH_EVPP), yes) LIBHV_HEADERS += $(CPPUTIL_HEADERS) diff --git a/Makefile.vars b/Makefile.vars index 39dc84a52..f51418a97 100644 --- a/Makefile.vars +++ b/Makefile.vars @@ -8,11 +8,15 @@ INSTALL_INCDIR ?= $(PREFIX)/include/hv INSTALL_LIBDIR ?= $(PREFIX)/lib PKG_CONFIG ?= pkg-config + +# lua LUA_PKG_CONFIG ?= $(shell if command -v $(PKG_CONFIG) >/dev/null 2>&1 && $(PKG_CONFIG) --exists lua; then echo lua; fi) LUA_PREFIX ?= $(shell for dir in /opt/homebrew/opt/lua /usr/local/opt/lua /usr; do if [ -f "$$dir/include/lua/lua.h" ] || [ -f "$$dir/include/lua.h" ]; then echo $$dir; break; fi; done) LUA_INCLUDE_DIR ?= $(shell if [ -n "$(LUA_PREFIX)" ]; then for dir in "$(LUA_PREFIX)/include/lua" "$(LUA_PREFIX)/include/lua5.5" "$(LUA_PREFIX)/include/lua5.4" "$(LUA_PREFIX)/include/lua5.3" "$(LUA_PREFIX)/include"; do if [ -f "$$dir/lua.h" ]; then echo $$dir; break; fi; done; fi) LUA_CFLAGS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --cflags $(LUA_PKG_CONFIG); elif [ -n "$(LUA_INCLUDE_DIR)" ]; then echo -I$(LUA_INCLUDE_DIR); fi) LUA_LIBS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --libs $(LUA_PKG_CONFIG); elif [ -n "$(LUA_PREFIX)" ]; then echo -L$(LUA_PREFIX)/lib -llua; else echo -llua; fi) + +# quickjs QUICKJS_ROOT ?= $(shell for dir in /opt/homebrew/opt/quickjs /usr/local/opt/quickjs /usr; do if [ -f "$$dir/include/quickjs/quickjs.h" ] || [ -f "$$dir/include/quickjs.h" ]; then echo $$dir; break; fi; done) QUICKJS_INCLUDE_DIR ?= $(shell if [ -n "$(QUICKJS_ROOT)" ]; then for dir in "$(QUICKJS_ROOT)/include/quickjs" "$(QUICKJS_ROOT)/include"; do if [ -f "$$dir/quickjs.h" ]; then echo $$dir; break; fi; done; fi) QUICKJS_LIB_DIR ?= $(shell if [ -n "$(QUICKJS_ROOT)" ]; then for dir in "$(QUICKJS_ROOT)/lib/quickjs" "$(QUICKJS_ROOT)/lib"; do if [ -f "$$dir/libquickjs.a" ] || [ -f "$$dir/libquickjs.dylib" ] || [ -f "$$dir/libquickjs.so" ]; then echo $$dir; break; fi; done; fi) @@ -126,4 +130,8 @@ HTTP_SERVER_HEADERS = http/server/HttpServer.h\ MQTT_HEADERS = mqtt/mqtt_protocol.h\ mqtt/mqtt_client.h +LUA_HEADERS = lua/hvlua.h\ + lua/hvlua_util.h\ + lua/hvlua_json.h + JS_HEADERS = js/hvjs.h diff --git a/cmake/vars.cmake b/cmake/vars.cmake index e929e07eb..83e7afd7f 100644 --- a/cmake/vars.cmake +++ b/cmake/vars.cmake @@ -124,6 +124,12 @@ set(MQTT_HEADERS mqtt/mqtt_client.h ) +set(LUA_HEADERS + lua/hvlua.h + lua/hvlua_util.h + lua/hvlua_json.h +) + set(JS_HEADERS js/hvjs.h ) diff --git a/docs/PLAN.md b/docs/PLAN.md index bac1d420c..9b4295118 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -13,7 +13,6 @@ - async DNS - lua binding - js binding -- http js script handler - hrpc = libhv + protobuf ## Plan From aa047ff66acff80cc00ce41e03dcb2842870ce64 Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 12:31:51 +0800 Subject: [PATCH 10/13] ci: use pic quickjs in linux build --- .github/workflows/CI.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4aac5fcad..25dffd267 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,12 +29,13 @@ jobs: sudo mkdir -p /usr/local/include/quickjs /usr/local/lib/quickjs sudo install -m644 /tmp/quickjs/quickjs.h /tmp/quickjs/quickjs-libc.h /usr/local/include/quickjs/ sudo install -m644 /tmp/quickjs/libquickjs.a /usr/local/lib/quickjs/ + echo "QUICKJS_ROOT=/usr/local" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/lib:${LD_LIBRARY_PATH}" >> $GITHUB_ENV - name: build run: | sudo apt update sudo apt install libssl-dev libnghttp2-dev libprotobuf-dev libprotoc-dev protobuf-compiler liblua5.4-dev - export QUICKJS_ROOT=/usr/local ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-rpc --with-lua --with-js make libhv evpp unittest # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr From 65bb368b5b96c01b77342c46db8db8c8923e5d9a Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 14:40:35 +0800 Subject: [PATCH 11/13] fix(js): tighten task cleanup guards --- js/hvjs.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/js/hvjs.cpp b/js/hvjs.cpp index 6e7de180d..19175bee7 100644 --- a/js/hvjs.cpp +++ b/js/hvjs.cpp @@ -402,11 +402,11 @@ void hvjs_task_unref(HvJsTask* task) { } } finish_deferred_ops(task); - if (!JS_IsUndefined(task->promise_result)) { + if (task->js && !JS_IsUndefined(task->promise_result)) { JS_FreeValue(task->js, task->promise_result); task->promise_result = JS_UNDEFINED; } - if (!JS_IsUndefined(task->promise)) { + if (task->js && !JS_IsUndefined(task->promise)) { JS_FreeValue(task->js, task->promise); task->promise = JS_UNDEFINED; } @@ -469,9 +469,7 @@ bool hvjs_task_start_timeout(HvJsTask* task, int timeout_ms) { void hvjs_task_cancel_timeout(HvJsTask* task) { if (task == NULL) return; if (task->timeout_timer_id != INVALID_TIMER_ID && task->loop_ptr) { - if (task->loop_ptr->isRunning()) { - task->loop_ptr->killTimer(task->timeout_timer_id); - } + task->loop_ptr->killTimer(task->timeout_timer_id); task->timeout_timer_id = INVALID_TIMER_ID; hvjs_task_unref(task); } From 5b172cba594033765c4e8742b8de58fea9783bfd Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 15:03:57 +0800 Subject: [PATCH 12/13] refactor(js): keep runtime options internal --- docs/cn/HttpJsHandler.md | 4 +--- examples/hvjs.cpp | 3 +-- http/server/HttpJsHandler.cpp | 5 +---- http/server/HttpJsHandler.h | 10 ++-------- js/hvjs.cpp | 16 ++++++---------- js/hvjs.h | 10 +--------- unittest/http_js_handler_test.cpp | 5 ++--- 7 files changed, 14 insertions(+), 39 deletions(-) diff --git a/docs/cn/HttpJsHandler.md b/docs/cn/HttpJsHandler.md index ba7fa1982..f2e26a4d4 100644 --- a/docs/cn/HttpJsHandler.md +++ b/docs/cn/HttpJsHandler.md @@ -89,12 +89,10 @@ async function get(ctx) { HttpJsHandlerOptions options; options.reload_on_change = true; options.timeout_ms = 30000; // 单个 HTTP 请求的墙钟预算,0 表示不限制 -options.memory_limit = 64 * 1024 * 1024; // 每个 event loop 复用的 QuickJS runtime 内存上限,0 表示不限制 -options.stack_size = 1024 * 1024; // QuickJS 栈上限,0 表示不限制 router.GET("/hello", HttpJsHandler("scripts/hello.js", options)); ``` -`memory_limit` 和 `stack_size` 作用在每个 event loop 复用的 QuickJS runtime 上;同一个 loop 上第一次创建 JS runtime 时生效。 +JS runtime 使用内置的内存和栈限制;v1 暂不暴露运行时限制配置。 ## 目录映射 diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp index 2c3bf0698..c5af1e2a5 100644 --- a/examples/hvjs.cpp +++ b/examples/hvjs.cpp @@ -117,8 +117,7 @@ int main(int argc, char** argv) { task->loop_ptr = loop; task->loop = loop->loop(); task->finish = finish; - hv::js::HvJsRuntimeOptions runtime_options; - hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop, runtime_options)); + hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop)); task->js = task->runtime ? JS_NewContext(task->runtime->rt) : NULL; if (task->runtime == NULL || task->js == NULL) { fprintf(stderr, "hvjs: failed to create quickjs runtime\n"); diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp index 879c22eee..005b4f4e0 100644 --- a/http/server/HttpJsHandler.cpp +++ b/http/server/HttpJsHandler.cpp @@ -352,10 +352,7 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - hv::js::HvJsRuntimeOptions runtime_options; - runtime_options.memory_limit = options_.memory_limit; - runtime_options.stack_size = options_.stack_size; - hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop, runtime_options)); + hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop)); task->js = task->runtime ? JS_NewContext(task->runtime->rt) : NULL; if (task->runtime == NULL || task->js == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; diff --git a/http/server/HttpJsHandler.h b/http/server/HttpJsHandler.h index 7e1c78b42..109e44928 100644 --- a/http/server/HttpJsHandler.h +++ b/http/server/HttpJsHandler.h @@ -1,8 +1,6 @@ #ifndef HV_HTTP_JS_HANDLER_H_ #define HV_HTTP_JS_HANDLER_H_ -#include - #include #include @@ -13,15 +11,11 @@ namespace hv { struct HV_EXPORT HttpJsHandlerOptions { bool reload_on_change; - int timeout_ms; // request wall-clock timeout; 0 disables - size_t memory_limit; // QuickJS per-loop runtime memory limit; 0 disables - size_t stack_size; // QuickJS max stack size; 0 disables + int timeout_ms; // request wall-clock timeout; 0 disables HttpJsHandlerOptions() : reload_on_change(true) - , timeout_ms(30000) - , memory_limit(64 * 1024 * 1024) - , stack_size(1024 * 1024) {} + , timeout_ms(30000) {} }; // HttpJsHandler runs a QuickJS script to handle an HTTP request. diff --git a/js/hvjs.cpp b/js/hvjs.cpp index 19175bee7..bbcf6e468 100644 --- a/js/hvjs.cpp +++ b/js/hvjs.cpp @@ -29,6 +29,9 @@ struct HvJsImmediatePromise : public HvJsPromiseOp {}; static JSClassID s_task_ref_class_id; static std::once_flag s_task_ref_class_once; +const size_t DEFAULT_JS_MEMORY_LIMIT = 64 * 1024 * 1024; +const size_t DEFAULT_JS_STACK_SIZE = 1024 * 1024; + void delete_op(HvJsPromiseOp* op); void runtime_dtor(void* userdata) { @@ -327,8 +330,6 @@ JSValue require_hv(JSContext* js) { } // namespace -HvJsRuntimeOptions::HvJsRuntimeOptions() : memory_limit(64 * 1024 * 1024), stack_size(1024 * 1024) {} - HvJsRuntime::HvJsRuntime() : rt(NULL), current_task(NULL), tasks() {} HvJsTaskScope::HvJsTaskScope(HvJsTask* task) : runtime(task ? task->runtime : NULL), current(task), previous(runtime ? runtime->current_task : NULL) { @@ -359,24 +360,19 @@ void HvJsPromiseOp::cancel(const char* reason) { (void)reason; } -HvJsRuntime* hvjs_runtime(hloop_t* loop, const HvJsRuntimeOptions& options) { +HvJsRuntime* hvjs_runtime(hloop_t* loop) { if (loop == NULL) return NULL; HvJsRuntime* runtime = (HvJsRuntime*)hloop_js_runtime(loop); if (runtime) return runtime; runtime = new HvJsRuntime(); - runtime->options = options; runtime->rt = JS_NewRuntime(); if (runtime->rt == NULL) { delete runtime; return NULL; } - if (runtime->options.memory_limit > 0) { - JS_SetMemoryLimit(runtime->rt, runtime->options.memory_limit); - } - if (runtime->options.stack_size > 0) { - JS_SetMaxStackSize(runtime->rt, runtime->options.stack_size); - } + JS_SetMemoryLimit(runtime->rt, DEFAULT_JS_MEMORY_LIMIT); + JS_SetMaxStackSize(runtime->rt, DEFAULT_JS_STACK_SIZE); JS_SetRuntimeOpaque(runtime->rt, runtime); JS_SetInterruptHandler(runtime->rt, interrupt_handler, NULL); hloop_set_js_runtime(loop, runtime, runtime_dtor); diff --git a/js/hvjs.h b/js/hvjs.h index ae7c25e1f..9661af302 100644 --- a/js/hvjs.h +++ b/js/hvjs.h @@ -19,16 +19,8 @@ namespace js { struct HvJsTask; struct HvJsPromiseOp; -struct HV_EXPORT HvJsRuntimeOptions { - size_t memory_limit; - size_t stack_size; - - HvJsRuntimeOptions(); -}; - struct HV_EXPORT HvJsRuntime { JSRuntime* rt; - HvJsRuntimeOptions options; HvJsTask* current_task; std::vector tasks; @@ -86,7 +78,7 @@ struct HV_EXPORT HvJsPromiseOp { virtual void cancel(const char* reason); }; -HV_EXPORT HvJsRuntime* hvjs_runtime(hloop_t* loop, const HvJsRuntimeOptions& options); +HV_EXPORT HvJsRuntime* hvjs_runtime(hloop_t* loop); HV_EXPORT void hvjs_task_set_runtime(HvJsTask* task, HvJsRuntime* runtime); HV_EXPORT void hvjs_task_ref(HvJsTask* task); diff --git a/unittest/http_js_handler_test.cpp b/unittest/http_js_handler_test.cpp index 3049decc3..b82e2cd0e 100644 --- a/unittest/http_js_handler_test.cpp +++ b/unittest/http_js_handler_test.cpp @@ -47,9 +47,8 @@ static std::string write_script(const char* name, const char* content) { int main() { hloop_t* loop = hloop_new(0); - hv::js::HvJsRuntimeOptions runtime_options; - hv::js::HvJsRuntime* runtime1 = hv::js::hvjs_runtime(loop, runtime_options); - hv::js::HvJsRuntime* runtime2 = hv::js::hvjs_runtime(loop, runtime_options); + hv::js::HvJsRuntime* runtime1 = hv::js::hvjs_runtime(loop); + hv::js::HvJsRuntime* runtime2 = hv::js::hvjs_runtime(loop); CHECK(runtime1 != NULL); CHECK(runtime1 == runtime2); hloop_free(&loop); From c8824e3dd8ff50198dfcaa6321b10f92bdfefe5a Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 21 Aug 2026 17:11:27 +0800 Subject: [PATCH 13/13] feat(js): add hvjs_dofile/hvjs_dostring entry points Expose stable JS-layer run helpers mirroring hvlua_dofile/hvlua_dostring so callers can run a script on a loop's per-loop QuickJS runtime without touching the task/runtime plumbing. The script body is wrapped in an async function, so top-level await works; global require/print/arg are installed. Returns 1 when finished synchronously, 0 when pending on async work (caller runs the loop), <0 on setup/load/runtime error, with an optional exit_code set on reject/timeout. Thin examples/hvjs.cpp down to a hvlua.cpp-style runner that just creates the loop, publishes TLS, and calls hvjs_dofile. --- examples/hvjs.cpp | 183 ++++++---------------------------------------- js/hvjs.cpp | 165 +++++++++++++++++++++++++++++++++++++++++ js/hvjs.h | 10 +++ 3 files changed, 199 insertions(+), 159 deletions(-) diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp index c5af1e2a5..0d58cd96d 100644 --- a/examples/hvjs.cpp +++ b/examples/hvjs.cpp @@ -2,192 +2,57 @@ // // Usage: hvjs script.js [args...] // -// The runtime publishes a shared EventLoop as this thread's loop so async JS -// bindings can reuse libhv clients on the same event loop. Scripts may use -// async/await with the built-in modules exposed through require("hv"), -// require("hv/http"), require("hv/ws"), require("hv/redis") and +// Holds the loop as a shared_ptr (make_shared) and publishes it as +// this thread's loop via EventLoop::run(). Using a shared_ptr (not a stack +// object) is required so the hv.* client bindings can obtain an EventLoopPtr +// for the current thread via currentThreadEventLoopPtr (EventLoop:: +// shared_from_this()) and share this one loop/thread with AsyncHttpClient / +// AsyncRedisClient etc. The script body is wrapped in an async function, so it +// may use top-level await with the built-in modules exposed through +// require("hv"), require("hv/http"), require("hv/ws"), require("hv/redis") and // require("hv/mqtt") when the corresponding libhv modules are enabled. - #include -#include #include -#include - -#include #include "EventLoop.h" -#include "hfile.h" #include "hlog.h" -#include "htime.h" #include "hvjs.h" -namespace { - -struct HvJsCliTask : public hv::js::HvJsTask { - int* exit_code; - - HvJsCliTask() : exit_code(NULL) {} -}; - static void usage(const char* prog) { fprintf(stderr, "Usage: %s script.js [args...]\n", prog); } -static bool load_file(const char* filepath, std::string* out) { - HFile file; - if (file.open(filepath, "rb") != 0) { - return false; - } - size_t size = hv_filesize(filepath); - out->resize(size); - if (size == 0) return true; - int nread = file.read(&(*out)[0], (int)size); - return nread >= 0 && (size_t)nread == size; -} - -static JSValue js_print(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - for (int i = 0; i < argc; ++i) { - if (i != 0) fputc(' ', stdout); - std::string s = hv::js::hvjs_to_string(js, argv[i]); - fputs(s.c_str(), stdout); - } - fputc('\n', stdout); - return JS_UNDEFINED; -} - -static void set_args(JSContext* js, int argc, char** argv) { - JSValue arr = JS_NewArray(js); - for (int i = 1; i < argc; ++i) { - JS_SetPropertyUint32(js, arr, i - 1, JS_NewString(js, argv[i])); - } - JSValue global = JS_GetGlobalObject(js); - JS_SetPropertyStr(js, global, "arg", arr); - JS_FreeValue(js, global); -} - -static void finish(hv::js::HvJsTask* base, JSValue result) { - HvJsCliTask* task = static_cast(base); - if (task->finished) return; - task->finished = true; - if (!task->error.empty()) { - fprintf(stderr, "hvjs: %s\n", task->error.c_str()); - if (task->exit_code) *task->exit_code = 1; - } - else if (task->promise_rejected) { - std::string err = hv::js::hvjs_to_string(task->js, result); - fprintf(stderr, "hvjs: %s\n", err.c_str()); - if (task->exit_code) *task->exit_code = 1; - } - JS_FreeValue(task->js, result); - hv::js::hvjs_task_cancel_timeout(task); - if (task->loop_ptr && task->loop_ptr->isRunning()) { - task->loop_ptr->stop(); - } - else if (task->loop && hloop_status(task->loop) == HLOOP_STATUS_RUNNING) { - hloop_stop(task->loop); - } - hv::js::hvjs_task_unref(task); -} - -} // namespace - int main(int argc, char** argv) { if (argc < 2) { usage(argv[0]); return 1; } const char* script = argv[1]; - std::string code; - if (!load_file(script, &code)) { - fprintf(stderr, "hvjs: failed to read %s\n", script); - return 1; - } + // Route logs to stdout for a CLI runtime (default logger writes a file). + // Line-buffer stdout so logs from long-running scripts appear promptly even + // when stdout is redirected to a file/pipe (not a TTY). setvbuf(stdout, NULL, _IOLBF, 0); hlog_set_handler(stdout_logger); + // A default-constructed EventLoop owns its own hloop_t (auto-freed on run + // exit). Held via shared_ptr so currentThreadEventLoopPtr / shared_from_this + // work — that is how the hv.* client bindings share this one loop/thread. hv::EventLoopPtr loop = std::make_shared(); + + // Publish this thread's loop (TLS) before running the script, so the js + // task and bindings can resolve currentThreadEventLoopPtr during load. hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, loop.get()); - HvJsCliTask* task = new HvJsCliTask(); + // Run the script (may await async ops). hvjs_dofile returns 0 when the + // script is pending on async work and the caller should run the loop, 1 when + // it finished synchronously, and <0 on setup/load/runtime error. The wrapped + // async function settling stops the loop, so we do NOT need hv.stop() here. int exit_code = 0; - task->exit_code = &exit_code; - task->loop_ptr = loop; - task->loop = loop->loop(); - task->finish = finish; - hv::js::hvjs_task_set_runtime(task, hv::js::hvjs_runtime(task->loop)); - task->js = task->runtime ? JS_NewContext(task->runtime->rt) : NULL; - if (task->runtime == NULL || task->js == NULL) { - fprintf(stderr, "hvjs: failed to create quickjs runtime\n"); - hv::js::hvjs_task_unref(task); - return 1; - } - JS_SetContextOpaque(task->js, task); - task->timeout_ms = 30000; - task->start_hrtime = gethrtime_us(); - if (!hv::js::hvjs_task_start_timeout(task, task->timeout_ms)) { - fprintf(stderr, "hvjs: failed to create timeout timer\n"); - hv::js::hvjs_task_unref(task); - return 1; - } - set_args(task->js, argc, argv); - - { - hv::js::HvJsTaskScope scope(task); - JSValue global = JS_GetGlobalObject(task->js); - JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); - JS_SetPropertyStr(task->js, global, "print", JS_NewCFunction(task->js, js_print, "print", 1)); - - std::string wrapped = "(async function(){\n"; - wrapped += code; - wrapped += "\n})()"; - JSValue eval = JS_Eval(task->js, wrapped.c_str(), wrapped.size(), script, JS_EVAL_TYPE_GLOBAL); - if (JS_IsException(eval)) { - std::string err = hv::js::hvjs_exception_string(task->js); - JS_FreeValue(task->js, global); - fprintf(stderr, "hvjs: %s\n", err.c_str()); - hv::js::hvjs_task_cancel_timeout(task); - hv::js::hvjs_task_unref(task); - return 1; - } - - JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); - JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); - JS_FreeValue(task->js, global); - JSValue promise_arg = eval; - task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); - JS_FreeValue(task->js, promise_resolve); - JS_FreeValue(task->js, promise_ctor); - JS_FreeValue(task->js, eval); - if (JS_IsException(task->promise)) { - std::string err = hv::js::hvjs_exception_string(task->js); - fprintf(stderr, "hvjs: %s\n", err.c_str()); - task->closing = true; - hv::js::hvjs_task_cancel_ops(task, "javascript handler error"); - hv::js::hvjs_task_cancel_timeout(task); - hv::js::hvjs_task_unref(task); - return 1; - } - std::string err; - if (!hv::js::hvjs_watch_promise(task, &err)) { - fprintf(stderr, "hvjs: %s\n", err.c_str()); - task->closing = true; - hv::js::hvjs_task_cancel_ops(task, "javascript handler error"); - hv::js::hvjs_task_cancel_timeout(task); - hv::js::hvjs_task_unref(task); - return 1; - } - } - - hv::js::hvjs_task_ref(task); - hv::js::hvjs_drain_jobs(task); - bool finished = task->finished; - hv::js::hvjs_task_unref(task); - if (!finished) { + int rc = hv::js::hvjs_dofile(loop->loop(), script, argc, argv, &exit_code); + if (rc == 0) { loop->run(); } - hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, NULL); - return exit_code; + return rc < 0 ? 1 : exit_code; } diff --git a/js/hvjs.cpp b/js/hvjs.cpp index bbcf6e468..2ae4383e4 100644 --- a/js/hvjs.cpp +++ b/js/hvjs.cpp @@ -5,8 +5,10 @@ #include #include +#include #include +#include "hfile.h" #include "hlog.h" #include "htime.h" #include "hversion.h" @@ -26,11 +28,18 @@ struct HvJsSleep : public HvJsPromiseOp { struct HvJsImmediatePromise : public HvJsPromiseOp {}; +struct HvJsScriptTask : public HvJsTask { + int* exit_code; + + HvJsScriptTask() : exit_code(NULL) {} +}; + static JSClassID s_task_ref_class_id; static std::once_flag s_task_ref_class_once; const size_t DEFAULT_JS_MEMORY_LIMIT = 64 * 1024 * 1024; const size_t DEFAULT_JS_STACK_SIZE = 1024 * 1024; +const int DEFAULT_JS_TASK_TIMEOUT = 30000; void delete_op(HvJsPromiseOp* op); @@ -320,6 +329,64 @@ JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* return JS_UNDEFINED; } +JSValue js_print(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + for (int i = 0; i < argc; ++i) { + if (i != 0) fputc(' ', stdout); + std::string s = hvjs_to_string(js, argv[i]); + fputs(s.c_str(), stdout); + } + fputc('\n', stdout); + return JS_UNDEFINED; +} + +void set_script_args(JSContext* js, int argc, char** argv) { + JSValue arr = JS_NewArray(js); + for (int i = 1; i < argc; ++i) { + JS_SetPropertyUint32(js, arr, i - 1, JS_NewString(js, argv[i])); + } + JSValue global = JS_GetGlobalObject(js); + JS_SetPropertyStr(js, global, "arg", arr); + JS_FreeValue(js, global); +} + +void script_finish(HvJsTask* base, JSValue result) { + HvJsScriptTask* task = static_cast(base); + if (task->finished) return; + task->finished = true; + if (!task->error.empty()) { + hloge("[js] script error: %s", task->error.c_str()); + if (task->exit_code) *task->exit_code = 1; + } + else if (task->promise_rejected) { + std::string err = hvjs_to_string(task->js, result); + hloge("[js] script rejected: %s", err.c_str()); + task->error = err.empty() ? "javascript rejection" : err; + if (task->exit_code) *task->exit_code = 1; + } + JS_FreeValue(task->js, result); + hvjs_task_cancel_timeout(task); + if (task->loop_ptr && task->loop_ptr->isRunning()) { + task->loop_ptr->stop(); + } + else if (task->loop && hloop_status(task->loop) == HLOOP_STATUS_RUNNING) { + hloop_stop(task->loop); + } + hvjs_task_unref(task); +} + +bool load_file(const char* filepath, std::string* out) { + HFile file; + if (file.open(filepath, "rb") != 0) { + return false; + } + size_t size = hv_filesize(filepath); + out->resize(size); + if (size == 0) return true; + int nread = file.read(&(*out)[0], (int)size); + return nread >= 0 && (size_t)nread == size; +} + JSValue require_hv(JSContext* js) { JSValue hv = JS_NewObject(js); JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); @@ -330,6 +397,104 @@ JSValue require_hv(JSContext* js) { } // namespace +int hvjs_dostring(hloop_t* loop, const char* code, const char* filename, int argc, char** argv, int* exit_code) { + if (loop == NULL || code == NULL) return -1; + + HvJsScriptTask* task = new HvJsScriptTask(); + task->exit_code = exit_code; + task->loop = loop; + task->loop_ptr = currentThreadEventLoopPtr; + if (task->loop_ptr == NULL) { + EventLoop* current_loop = currentThreadEventLoop; + if (current_loop && current_loop->loop() == loop) { + task->loop_ptr = current_loop->shared_from_this(); + } + } + task->finish = script_finish; + hvjs_task_set_runtime(task, hvjs_runtime(loop)); + task->js = task->runtime ? JS_NewContext(task->runtime->rt) : NULL; + if (task->runtime == NULL || task->js == NULL) { + hloge("[js] failed to create quickjs runtime"); + hvjs_task_unref(task); + return -1; + } + JS_SetContextOpaque(task->js, task); + task->timeout_ms = DEFAULT_JS_TASK_TIMEOUT; + task->start_hrtime = gethrtime_us(); + if (!hvjs_task_start_timeout(task, task->timeout_ms)) { + hloge("[js] failed to create timeout timer"); + hvjs_task_unref(task); + return -1; + } + set_script_args(task->js, argc, argv); + + std::string err; + { + HvJsTaskScope scope(task); + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hvjs_require, "require", 1)); + JS_SetPropertyStr(task->js, global, "print", JS_NewCFunction(task->js, js_print, "print", 1)); + + std::string wrapped = "(async function(){\n"; + wrapped += code; + wrapped += "\n})()"; + JSValue eval = JS_Eval(task->js, wrapped.c_str(), wrapped.size(), filename ? filename : "", JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + err = hvjs_exception_string(task->js); + JS_FreeValue(task->js, global); + hloge("[js] eval %s failed: %s", filename ? filename : "", err.c_str()); + task->closing = true; + hvjs_task_cancel_ops(task, "javascript script error"); + hvjs_task_cancel_timeout(task); + hvjs_task_unref(task); + return -1; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = eval; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, eval); + if (JS_IsException(task->promise)) { + err = hvjs_exception_string(task->js); + hloge("[js] Promise.resolve %s failed: %s", filename ? filename : "", err.c_str()); + task->closing = true; + hvjs_task_cancel_ops(task, "javascript script error"); + hvjs_task_cancel_timeout(task); + hvjs_task_unref(task); + return -1; + } + if (!hvjs_watch_promise(task, &err)) { + hloge("[js] watch promise %s failed: %s", filename ? filename : "", err.c_str()); + task->closing = true; + hvjs_task_cancel_ops(task, "javascript script error"); + hvjs_task_cancel_timeout(task); + hvjs_task_unref(task); + return -1; + } + } + + hvjs_task_ref(task); + hvjs_drain_jobs(task); + bool finished = task->finished; + bool ok = task->error.empty() && !task->promise_rejected; + hvjs_task_unref(task); + return finished ? (ok ? 1 : -1) : 0; +} + +int hvjs_dofile(hloop_t* loop, const char* filepath, int argc, char** argv, int* exit_code) { + if (filepath == NULL) return -1; + std::string code; + if (!load_file(filepath, &code)) { + hloge("[js] failed to read %s", filepath); + return -1; + } + return hvjs_dostring(loop, code.c_str(), filepath, argc, argv, exit_code); +} + HvJsRuntime::HvJsRuntime() : rt(NULL), current_task(NULL), tasks() {} HvJsTaskScope::HvJsTaskScope(HvJsTask* task) : runtime(task ? task->runtime : NULL), current(task), previous(runtime ? runtime->current_task : NULL) { diff --git a/js/hvjs.h b/js/hvjs.h index 9661af302..09d25f998 100644 --- a/js/hvjs.h +++ b/js/hvjs.h @@ -80,6 +80,16 @@ struct HV_EXPORT HvJsPromiseOp { HV_EXPORT HvJsRuntime* hvjs_runtime(hloop_t* loop); +// Run a script on `loop` using the per-loop QuickJS runtime and a fresh +// JSContext. The script body is wrapped in an async function, so top-level +// await is supported. Global `require`, `print`, and `arg` are installed. +// @return 1 if the script finished synchronously, 0 if it is pending on async +// work and the caller should run the loop, <0 on setup/load/runtime error. +// `exit_code`, if provided, is set to 1 when the script rejects or times out. +HV_EXPORT int hvjs_dofile(hloop_t* loop, const char* filepath, int argc = 0, char** argv = NULL, int* exit_code = NULL); +HV_EXPORT int hvjs_dostring(hloop_t* loop, const char* code, const char* filename = "", int argc = 0, char** argv = NULL, + int* exit_code = NULL); + HV_EXPORT void hvjs_task_set_runtime(HvJsTask* task, HvJsRuntime* runtime); HV_EXPORT void hvjs_task_ref(HvJsTask* task); HV_EXPORT void hvjs_task_unref(HvJsTask* task);