diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 242cb24c9..343909e90 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,7 +19,16 @@ jobs: - name: build run: | sudo apt update - sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler + 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 + 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e1ab1cfd..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) @@ -36,6 +37,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 +215,57 @@ 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/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.") + 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::QuickJS) + set(LIBHV_FIND_DEPENDENCY_QUICKJS TRUE) + 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) @@ -243,7 +296,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) @@ -274,6 +327,10 @@ 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_HEADERS ${LIBHV_HEADERS} ${JS_HEADERS}) + set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} js) + endif() if(WITH_REDIS) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${REDIS_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} redis) @@ -286,8 +343,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 +371,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) @@ -322,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 @@ -336,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 4f573da45..5470331f3 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,13 @@ LIBHV_SRCDIRS += cpputil endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +LIBHV_HEADERS += $(JS_HEADERS) +LIBHV_SRCDIRS += js +endif +endif + ifeq ($(WITH_EVPP), yes) LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) LIBHV_SRCDIRS += cpputil evpp @@ -49,8 +56,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 @@ -115,6 +128,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." @@ -228,6 +246,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 + $(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" @@ -424,6 +445,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 -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 $(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 $(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 $(LDFLAGS) $(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..ca33d327a 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)) +ifneq ($(MAKECMDGOALS),clean) +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..39dc84a52 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\ \ @@ -119,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/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..bac1d420c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,11 +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 new file mode 100644 index 000000000..ba7fa1982 --- /dev/null +++ b/docs/cn/HttpJsHandler.md @@ -0,0 +1,259 @@ +# 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 "HttpJsHandler.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`,这样同一个路由入口可以按脚本后缀分发到不同脚本引擎。 + +可以通过 `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`: + +```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) // path/query 参数 +ctx.query(name, defaultValue) // ctx.param 的别名 +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/", { + timeout: 3000, + ping_interval: 3000 +}); +ws.send("hello"); +const msg = await ws.recv(); +ws.close(); +``` + +`ws.connect()` 使用底层 `TcpClient` 的连接超时;`recv()` 在收到应用消息前保持 pending,连接关闭时会 reject。WebSocket ping/pong 只用于连接健康检查,不会让一个连接健康但没有业务消息的 `recv()` 自动返回。HTTP JS handler 的 `timeout_ms` 是请求级兜底,会结束整个请求并清理仍未完成的 `recv()` 等异步操作。 + +### 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。 + +`redis.new()` 会创建一个 `AsyncRedisClient`。如果脚本在每个 HTTP 请求里调用它,就会产生按请求创建/释放连接的开销;高频路径建议在 C++ 层封装连接池,或后续扩展 JS 绑定提供复用能力。 + +### 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(); +``` + +`mqtt.connect()` 的 `timeout` / `connect_timeout` 会设置底层 MQTT client 的连接超时;`recv()` 在收到 `PUBLISH` 前保持 pending,连接关闭时会 reject。MQTT keepalive 用于发现断链,正常 PING/PONG 不会让一个没有业务消息的 `recv()` 自动返回。`reconnect.max_retry = 0` 按 libhv reconnect 语义表示无限重试。 + +## 异步模型 + +每个 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`。 + +当前 JS 字符串绑定通过 `JS_ToCStringLen` 表达数据;`ctx.body()`、`http` response body、`ws.send(..., "binary")`、`mqtt.publish()` 的 payload 仍按字符串处理。需要二进制无损传输时,应等后续版本接入 `ArrayBuffer` / `Uint8Array`。 + +## 示例 + +```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" +``` + +也可以直接使用 `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/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/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/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/http_server_test.cpp b/examples/http_server_test.cpp index b6854008d..7bc97f644 100644 --- a/examples/http_server_test.cpp +++ b/examples/http_server_test.cpp @@ -8,7 +8,7 @@ #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 @@ -95,8 +95,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 diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp new file mode 100644 index 000000000..2c3bf0698 --- /dev/null +++ b/examples/hvjs.cpp @@ -0,0 +1,194 @@ +// 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 "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; + } + + 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(); + int exit_code = 0; + task->exit_code = &exit_code; + 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)); + 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) { + loop->run(); + } + 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..805aba278 --- /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, { timeout: 3000, ping_interval: 3000 }); + +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/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..879c22eee --- /dev/null +++ b/http/server/HttpJsHandler.cpp @@ -0,0 +1,456 @@ +#ifdef WITH_JS + +#include "HttpJsHandler.h" + +#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 "hvjs.h" + +namespace hv { + +namespace { + +struct JsHttpTask : public hv::js::HvJsTask { + HttpContextPtr ctx; + bool async; + + JsHttpTask() : async(false) {} +}; + +static JsHttpTask* js_get_task(JSContext* js) { + return static_cast(hv::js::hvjs_get_task(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 ? 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()); +} + +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 ? 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()); +} + +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 = 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; +} + +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 = hv::js::hvjs_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 = hv::js::hvjs_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 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) { + 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 = 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 = 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 = hv::js::hvjs_exception_string(js); + return false; +} + +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("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("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("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(); + } + hv::js::hvjs_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->finish = http_js_task_finish; + 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"); + hv::js::hvjs_task_unref(task); + 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)); + 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("js handler: failed to create quickjs runtime"); + close_task(task, "javascript handler error"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + 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("js handler: failed to create timeout timer"); + close_task(task, "javascript handler error"); + 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); + hv::js::hvjs_drain_jobs(task); + bool finished = task->finished; + 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); + return status; +} + +} // namespace hv +#endif // WITH_JS diff --git a/http/server/HttpJsHandler.h b/http/server/HttpJsHandler.h new file mode 100644 index 000000000..7e1c78b42 --- /dev/null +++ b/http/server/HttpJsHandler.h @@ -0,0 +1,56 @@ +#ifndef HV_HTTP_JS_HANDLER_H_ +#define HV_HTTP_JS_HANDLER_H_ + +#include + +#include +#include + +#include "hexport.h" +#include "HttpService.h" + +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 + + 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. +// +// 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()); + + 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/js/hvjs.cpp b/js/hvjs.cpp new file mode 100644 index 000000000..6e7de180d --- /dev/null +++ b/js/hvjs.cpp @@ -0,0 +1,751 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#include +#include + +#include + +#include "hlog.h" +#include "htime.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) {} + void cancel(const char* reason) override; +}; + +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; +} + +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()) { + 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 > 0) { + op->defer_delete = true; + task->deferred_ops.push_back(op); + 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(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); + 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); + 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 hvjs_rejected_promise(js, "hv.sleep: invalid timeout"); + + 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)delay, 1); + if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); + } + if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { + hvjs_promise_reject(sleep, "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 + +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() + : 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), 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; + } + if (!JS_IsUndefined(task->promise)) { + JS_FreeValue(task->js, task->promise); + task->promise = JS_UNDEFINED; + } + if (task->js) { + if (task->runtime && task->runtime->rt) { + JS_RunGC(task->runtime->rt); + } + JS_FreeContext(task->js); + task->js = 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 && 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 && hloop_status(task->loop) == HLOOP_STATUS_RUNNING) { + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = drain_event_cb; + ev.userdata = task; + hloop_post_event(task->loop, &ev); + } + else { + task->drain_scheduled = false; + hvjs_task_unref(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); + 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) { + if (task == NULL || task->finished || task->js == NULL || task->runtime == NULL || task->runtime->rt == NULL) return; + HvJsTaskScope scope(task); + JSContext* job_ctx = NULL; + JSRuntime* rt = task->runtime->rt; + while (JS_IsJobPending(rt)) { + int rc = JS_ExecutePendingJob(rt, &job_ctx); + if (rc < 0) { + 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; + } + } + + HvJsRuntime* runtime = task->runtime; + std::vector tasks = runtime->tasks; + for (size_t i = 0; i < tasks.size(); ++i) { + finish_ready_task(tasks[i]); + } +} + +void hvjs_promise_resolve(HvJsPromiseOp* op, JSValue value) { + promise_complete(op, value, true); +} + +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); +} + +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) return; + HvJsTask* task = op->task; + if (task && task->in_call == 0) { + finish_deferred_ops(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..ae7c25e1f --- /dev/null +++ b/js/hvjs.h @@ -0,0 +1,157 @@ +#ifndef HV_JS_H_ +#define HV_JS_H_ + +#include +#include + +#include +#include +#include + +#include + +#include "EventLoop.h" +#include "hexport.h" + +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); + + HvJsRuntime* runtime; + JSContext* js; + hloop_t* loop; + EventLoopPtr loop_ptr; + JSValue promise; + JSValue promise_result; + bool promise_settled; + bool promise_rejected; + bool finished; + 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(); +}; + +struct HV_EXPORT HvJsPromiseOp { + HvJsTask* task; + JSValue resolve; + 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); + +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; + 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; +} + +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..59d598abb --- /dev/null +++ b/js/hvjs_http.cpp @@ -0,0 +1,477 @@ +#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; + +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) { + 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) { + 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_free(js, tab); + } + } + *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 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; + 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)); + } + else { + hvjs_promise_reject(op, "hv.http: request failed"); + } + }); + if (ret != 0) { + hvjs_promise_reject(op, "hv.http: request failed"); + } + --task->in_call; + 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(); } +}; + +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_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; + 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(); + } +} + +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(); + 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(); + 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, raw_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); + 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); + 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(); + 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, raw_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(); + 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, raw_loop, hold); + } + js_ws_try_deliver(state); + }; + ++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; + 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..f654415c7 --- /dev/null +++ b/js/hvjs_mqtt.cpp @@ -0,0 +1,477 @@ +#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(); } +}; + +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 { + 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_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->loop() && hloop_status(loop->loop()) == HLOOP_STATUS_RUNNING) { + loop->queueInLoop([state]() { state->detach(); }); + } + else if (raw_loop && hloop_status(raw_loop) == HLOOP_STATUS_RUNNING) { + 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; + 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; + 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..59f7afd38 --- /dev/null +++ b/js/hvjs_redis.cpp @@ -0,0 +1,241 @@ +#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; + 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; + 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 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..5a4d05668 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -36,25 +36,37 @@ 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 $? +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 + 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 @@ -64,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 2b62d081d..d0a1327fc 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -133,6 +133,29 @@ 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 ../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) +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() +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) +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 +222,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..3049decc3 --- /dev/null +++ b/unittest/http_js_handler_test.cpp @@ -0,0 +1,175 @@ +/* + * 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 "hvjs.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() { + 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" + " 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"); + 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) { + (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::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); + 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); + 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 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); + 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 == "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_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..1f12aaecc --- /dev/null +++ b/unittest/http_js_ws_test.cpp @@ -0,0 +1,128 @@ +/* + * http_js_ws_test - HttpJsHandler + hv/ws Promise binding. + */ + +#include +#include +#include +#include + +#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" + +#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); }; + 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/', { timeout: 500, ping_interval: 100 });\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); + 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); + 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); + 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; +}