From b108befeaebcbc2880964525b39bfbebac059720 Mon Sep 17 00:00:00 2001 From: Koji AGAWA Date: Wed, 22 May 2024 02:29:28 +0900 Subject: [PATCH] feat(driver): implement subscript and lcurl.safe (#57) fixes #15 * chore(driver): use emscripten's zlib * chore(web): footer * feat(driver): subscript * chore(driver): subscript * chore(driver): subscript * chore(driver): subscript * chore(driver): fix memory handling * chore(driver): fix build sharing * chore(driver): fix content-type --- packages/driver/CMakeLists.txt | 19 +- packages/driver/boot.lua | 31 -- packages/driver/index.html | 2 - packages/driver/src/c/driver.c | 39 ++ packages/driver/src/c/lcurl.c | 386 ++++++++++++++++++++ packages/driver/src/c/lcurl.h | 8 + packages/driver/src/c/sub.c | 377 +++++++++++++++++++ packages/driver/src/c/sub.h | 10 + packages/driver/src/js/driver.ts | 4 +- packages/driver/src/js/logger.ts | 1 + packages/driver/src/js/renderer/renderer.ts | 2 +- packages/driver/src/js/run.ts | 4 +- packages/driver/src/js/sub.ts | 120 ++++++ packages/driver/src/js/worker.ts | 121 ++++-- packages/web/functions/api/fetch.ts | 8 +- packages/web/src/Footer.tsx | 2 +- 16 files changed, 1048 insertions(+), 86 deletions(-) create mode 100644 packages/driver/src/c/lcurl.c create mode 100644 packages/driver/src/c/lcurl.h create mode 100644 packages/driver/src/c/sub.c create mode 100644 packages/driver/src/c/sub.h create mode 100644 packages/driver/src/js/sub.ts diff --git a/packages/driver/CMakeLists.txt b/packages/driver/CMakeLists.txt index a521fe8..887d59f 100644 --- a/packages/driver/CMakeLists.txt +++ b/packages/driver/CMakeLists.txt @@ -18,14 +18,7 @@ file(GLOB_RECURSE LUA_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../../vendor/lua/*.c) list(REMOVE_ITEM LUA_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../../vendor/lua/lua.c) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../vendor/lua) -set(ZLIB_ENABLE_TESTS OFF) -set(ZLIBNG_ENABLE_TESTS OFF) -set(WITH_GTEST OFF) -set(ZLIB_COMPAT ON) -set(BUILD_SHARED_LIBS OFF) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../vendor/zlib-ng ${CMAKE_CURRENT_BINARY_DIR}/vendor/zlib-ng) - -add_compile_options("-flto" "-g" "-gsource-map") +add_compile_options("-flto" "-g" "-gsource-map" "-sUSE_ZLIB") set(CMAKE_EXECUTABLE_SUFFIX ".mjs") add_executable(${PROJECT_NAME} @@ -43,13 +36,15 @@ add_executable(${PROJECT_NAME} src/c/wasmfs/nodefs.cpp src/c/wasmfs/nodefs.h src/c/wasmfs/nodefs_js.cpp + src/c/sub.c + src/c/sub.h + src/c/lcurl.c + src/c/lcurl.h ) -target_link_libraries(${PROJECT_NAME} PRIVATE $) -target_include_directories(${PROJECT_NAME} PRIVATE $) - set(DRIVER_LINK_FLAGS "-flto" + "-sUSE_ZLIB" "-sMODULARIZE" "-sSTACK_SIZE=131072" "-sASYNCIFY" @@ -59,7 +54,7 @@ set(DRIVER_LINK_FLAGS "-sWASMFS" "-sSTRICT" "-sINCOMING_MODULE_JS_API=[print,printErr]" - "-sEXPORTED_FUNCTIONS=[_malloc,_init,_start,_on_frame,_on_key_down,_on_key_up,_on_char,_on_download_page_result]" + "-sEXPORTED_FUNCTIONS=[_malloc,_free,_init,_start,_on_frame,_on_key_down,_on_key_up,_on_char,_on_download_page_result,_on_subscript_finished]" "-sEXPORTED_RUNTIME_METHODS=cwrap,ccall,ERRNO_CODES,setValue,HEAPU8,Asyncify" "-sASYNCIFY_IMPORTS=js_wasmfs_node_read" ) diff --git a/packages/driver/boot.lua b/packages/driver/boot.lua index c5f2f7a..5fdc50f 100644 --- a/packages/driver/boot.lua +++ b/packages/driver/boot.lua @@ -68,13 +68,6 @@ end function GetWorkDir() return "" end -function LaunchSubScript(scriptText, funcList, subList, ...) - error("SubScript is not implemented") -end -function AbortSubScript(ssID) -end -function IsSubScriptRunning(ssID) -end function LoadModule(fileName, ...) if not fileName:match("%.lua") then fileName = fileName .. ".lua" @@ -163,27 +156,3 @@ mainObject["OnInit"] = function(self) return false end end - -local dkjson = require "dkjson" -local downloadHandle = nil -mainObject["DownloadPage"] = function(self, url, callback, params) - params = params or {} - print(string.format("DownloadPage: url=[%s], header=[%s], body=[%s]", url, params.header, params.body)) - if downloadHandle then - error("Download already in progress") - else - DownloadPage(url, params.header, params.body) - downloadHandle = callback - end -end -OnDownloadPageResult = function(resultJson) - print("OnDownloadPageResult") - if downloadHandle then - local callback = downloadHandle - downloadHandle = nil - local result = dkjson.decode(resultJson) - callback({header=result.header, body=result.body}, result.error) - else - error("No download handle") - end -end diff --git a/packages/driver/index.html b/packages/driver/index.html index c09025f..e0689a2 100644 --- a/packages/driver/index.html +++ b/packages/driver/index.html @@ -4,8 +4,6 @@ Path of Building Web - -
diff --git a/packages/driver/src/c/driver.c b/packages/driver/src/c/driver.c index 151b3c2..9e7c578 100644 --- a/packages/driver/src/c/driver.c +++ b/packages/driver/src/c/driver.c @@ -10,6 +10,7 @@ #include "draw.h" #include "image.h" #include "fs.h" +#include "sub.h" extern backend_t wasmfs_create_nodefs_backend(const char* root); @@ -338,6 +339,7 @@ int init() { image_init(L); draw_init(L); fs_init(L); + sub_init(L); // lua_pushcclosure(L, GetTime, 0); @@ -486,3 +488,40 @@ int on_download_page_result(const char *json) { } return 0; } + +EMSCRIPTEN_KEEPALIVE +int on_subscript_finished(int id, const uint8_t *data) { + lua_State *L = GL; + + int extra = push_callback(L, "OnSubFinished"); + if (extra >= 0) { + lua_pushlightuserdata(L, (void *)id); + int count = sub_lua_deserialize(L, data); + if (lua_pcall(L, extra + 1 + count, 0, 0) != LUA_OK) { + const char *msg = lua_tostring(L, -1); + fprintf(stderr, "on_subscript_finished error: %s\n", msg); + return 1; + } + return 0; + } + return 1; +} + +// Call from main worker +EMSCRIPTEN_KEEPALIVE +int on_subscript_error(int id, const char *message) { + lua_State *L = GL; + + int extra = push_callback(L, "OnSubError"); + if (extra >= 0) { + lua_pushlightuserdata(L, (void *)id); + lua_pushstring(L, message); + if (lua_pcall(L, extra + 2, 0, 0) != LUA_OK) { + const char *msg = lua_tostring(L, -1); + fprintf(stderr, "on_subscript_error error: %s\n", msg); + return 1; + } + return 0; + } + return 1; +} \ No newline at end of file diff --git a/packages/driver/src/c/lcurl.c b/packages/driver/src/c/lcurl.c new file mode 100644 index 0000000..efb5381 --- /dev/null +++ b/packages/driver/src/c/lcurl.c @@ -0,0 +1,386 @@ +#include "lcurl.h" + +#include +#include +#include +#include + +typedef struct { + const char *url; + char *headers; + const char *body; + int status_code; + int header_function; + int write_function; +} Easy; + +typedef struct { + char message[1024]; +} Error; + +enum { + OPT_HTTPHEADER = 10023, + OPT_USERAGENT = 10018, + OPT_ACCEPT_ENCODING = 10102, + OPT_POST = 10024, + OPT_POSTFIELDS = 10015, + OPT_IPRESOLVE = 113, + OPT_PROXY = 10004, + INFO_RESPONSE_CODE = 2097154, +}; + +static int lcurl_error_msg(lua_State *L) { + Error *e = luaL_checkudata(L, 1, "lcurl_error"); + lua_pushstring(L, e->message); + return 1; +} + +static const struct luaL_Reg lcurl_error_methods[] = { + {"msg", lcurl_error_msg}, + {NULL, NULL} +}; + +static int lcurl_error_new(lua_State *L, const char *msg) { + Error *e = (Error *)lua_newuserdata(L, sizeof(Error)); + strncpy(e->message, msg, sizeof(e->message) - 1); + e->message[sizeof(e->message) - 1] = '\0'; + + luaL_getmetatable(L, "lcurl_error"); + lua_setmetatable(L, -2); + + return 1; +} + +static int lcurl_easy_setopt(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + int option = luaL_checkinteger(L, 2); + + switch (option) { + case OPT_HTTPHEADER: { + luaL_checktype(L, 3, LUA_TTABLE); + lua_pushnil(L); + while (lua_next(L, 3) != 0) { + const char *header = luaL_checkstring(L, -1); + if (le->headers) { + size_t len = strlen(le->headers); + size_t header_len = strlen(header); + le->headers = realloc(le->headers, len + header_len + 3); + strcat(le->headers, header); + strcat(le->headers, "\r\n"); + } else { + le->headers = strdup(header); + } + lua_pop(L, 1); + } + } + case OPT_USERAGENT: { + const char *user_agent = luaL_checkstring(L, 3); + const char *line = "User-Agent: "; + le->headers = realloc(le->headers, strlen(le->headers) + strlen(user_agent) + strlen(line) + 3); + strcat(le->headers, line); + strcat(le->headers, user_agent); + strcat(le->headers, "\r\n"); + break; + } + case OPT_ACCEPT_ENCODING: { + const char *accept_encoding = luaL_checkstring(L, 3); + if (strlen(accept_encoding) > 0) { + const char *line = "Accept-Encoding: "; + le->headers = realloc(le->headers, strlen(le->headers) + strlen(accept_encoding) + strlen(line) + 3); + strcat(le->headers, line); + strcat(le->headers, accept_encoding); + strcat(le->headers, "\r\n"); + } + break; + } + case OPT_POST: { + // fetch will POST when the body is set + break; + } + case OPT_POSTFIELDS: { + const char *body = luaL_checkstring(L, 3); + le->body = strdup(body); + break; + } + case OPT_IPRESOLVE: { + // no-op + break; + } + case OPT_PROXY: { + // no-op + break; + } + default: + luaL_error(L, "Option not supported"); + break; + } + return 0; +} + +static int lcurl_easy_setopt_headerfunction(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + if (!lua_isfunction(L, 2)) { + luaL_error(L, "Argument must be a function"); + return 0; + } + + lua_pushvalue(L, 2); + le->header_function = luaL_ref(L, LUA_REGISTRYINDEX); + + return 0; +} + +static int lcurl_easy_setopt_writefunction(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + if (!lua_isfunction(L, 2)) { + luaL_error(L, "Argument must be a function"); + return 0; + } + + lua_pushvalue(L, 2); + le->write_function = luaL_ref(L, LUA_REGISTRYINDEX); + + return 0; +} + +static int lcurl_easy_setopt_url(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + const char *url = luaL_checkstring(L, 2); + le->url = strdup(url); + + return 0; +} + +EM_ASYNC_JS(const char *, fetch, (const char *url, const char *headers, const char *body), { + const reqHeaders = headers ? UTF8ToString(headers) : undefined; + const reqBody = body ? UTF8ToString(body) : undefined; + + const res = await Module.bridge.fetch(UTF8ToString(url), reqHeaders, reqBody); + const j = JSON.parse(res); + + const resBody = ""+j.body; + const status = ""+j.status; + const header = ""+j.header; + const error = ""+j.error; + + const buf = Module._malloc(resBody.length + status.length + header.length + error.length + 4); + let p = buf; + stringToUTF8(resBody, p, resBody.length + 1); + p += resBody.length + 1; + stringToUTF8(status, p, status.length + 1); + p += status.length + 1; + stringToUTF8(header, p, header.length + 1); + p += header.length + 1; + stringToUTF8(error, p, error.length + 1); + p += error.length + 1; + return buf; +}) + +static int lcurl_easy_perform(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + if (!le->url) { + lcurl_error_new(L, "URL not set"); + lua_pushnil(L); + lua_pushvalue(L, -2); + return 2; + } + + const char *response = fetch(le->url, le->headers, le->body); + + const char *p = response; + const char *body = p; + p += strlen(p) + 1; + const char *status = p; + p += strlen(p) + 1; + const char *header = p; + p += strlen(p) + 1; + const char *error = p; + +// printf("body: %s\nstatus: %s\nheader: %s\nerror: %s\n", body, status, header, error); + + le->status_code = 0; + + if (strcmp(error, "undefined") != 0) { +// printf("error is defined, returning error\n"); + lcurl_error_new(L, error); + lua_pushnil(L); + lua_pushvalue(L, -2); + free((void *)response); + return 2; + } + + if (strlen(status) > 0) { + le->status_code = strtol(status, NULL, 10); +// printf("status code: %d\n", le->status_code); + } + + if (le->header_function != LUA_REFNIL) { + lua_rawgeti(L, LUA_REGISTRYINDEX, le->header_function); + lua_pushstring(L, header); + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { +// printf("error calling header function\n"); + lcurl_error_new(L, lua_tostring(L, -1)); + lua_pushnil(L); + lua_pushvalue(L, -2); + free((void *)response); + return 2; + } + } + + if (le->write_function != LUA_REFNIL) { + lua_rawgeti(L, LUA_REGISTRYINDEX, le->write_function); + lua_pushstring(L, body); + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { +// printf("error calling write function\n"); + lcurl_error_new(L, lua_tostring(L, -1)); + lua_pushnil(L); + lua_pushvalue(L, -2); + free((void *)response); + return 2; + } + } + + lua_pushnil(L); + lua_pushnil(L); + return 2; +} + +static int lcurl_easy_getinfo(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + int option = luaL_checkinteger(L, 2); + switch (option) { + case INFO_RESPONSE_CODE: + lua_pushinteger(L, le->status_code); + return 1; + default: + return 0; + } +} + +static int lcurl_easy_getinfo_response_code(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + lua_pushinteger(L, le->status_code); + return 1; +} + +static int lcurl_easy_close(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + if (le->url) { + free((void *)le->url); + le->url = NULL; + } + if (le->headers) { + free((void *)le->headers); + le->headers = NULL; + } + if (le->body) { + free((void *)le->body); + le->body = NULL; + } + if (le->header_function != LUA_REFNIL) { + luaL_unref(L, LUA_REGISTRYINDEX, le->header_function); + le->header_function = LUA_REFNIL; + } + if (le->write_function != LUA_REFNIL) { + luaL_unref(L, LUA_REGISTRYINDEX, le->write_function); + le->write_function = LUA_REFNIL; + } + return 0; +} + +static int lcurl_easy_gc(lua_State *L) { + Easy *le = luaL_checkudata(L, 1, "lcurl_easy"); + + lcurl_easy_close(L); + + return 0; +} + +static const struct luaL_Reg lcurl_easy_methods[] = { + {"setopt", lcurl_easy_setopt}, + {"setopt_url", lcurl_easy_setopt_url}, + {"setopt_headerfunction", lcurl_easy_setopt_headerfunction}, + {"setopt_writefunction", lcurl_easy_setopt_writefunction}, + {"perform", lcurl_easy_perform}, + {"getinfo", lcurl_easy_getinfo}, + {"getinfo_response_code", lcurl_easy_getinfo_response_code}, + {"close", lcurl_easy_close}, + {"__gc", lcurl_easy_gc}, + {NULL, NULL} +}; + +static int lcurl_easy_new(lua_State *L) { + Easy *le = (Easy *)lua_newuserdata(L, sizeof(Easy)); + le->url = NULL; + le->headers = strdup(""); + le->body = NULL; + le->status_code = 0; + le->header_function = LUA_REFNIL; + le->write_function = LUA_REFNIL; + + luaL_getmetatable(L, "lcurl_easy"); + lua_setmetatable(L, -2); + + return 1; +} + +static int lcurl_easy(lua_State *L) { + return lcurl_easy_new(L); +} + +// Register functions +static const struct luaL_Reg lcurl_functions[] = { + {"easy", lcurl_easy}, + {NULL, NULL} // Sentinel +}; + +// Module initialization function +int luaopen_lcurl(lua_State *L) { + luaL_newmetatable(L, "lcurl_error"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); + lua_settable(L, -3); + luaL_setfuncs(L, lcurl_error_methods, 0); + + luaL_newmetatable(L, "lcurl_easy"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); + lua_settable(L, -3); + luaL_setfuncs(L, lcurl_easy_methods, 0); + + luaL_newlib(L, lcurl_functions); + + lua_pushinteger(L, OPT_HTTPHEADER); + lua_setfield(L, -2, "OPT_HTTPHEADER"); + lua_pushinteger(L, OPT_USERAGENT); + lua_setfield(L, -2, "OPT_USERAGENT"); + lua_pushinteger(L, OPT_ACCEPT_ENCODING); + lua_setfield(L, -2, "OPT_ACCEPT_ENCODING"); + lua_pushinteger(L, OPT_POST); + lua_setfield(L, -2, "OPT_POST"); + lua_pushinteger(L, OPT_POSTFIELDS); + lua_setfield(L, -2, "OPT_POSTFIELDS"); + lua_pushinteger(L, OPT_IPRESOLVE); + lua_setfield(L, -2, "OPT_IPRESOLVE"); + lua_pushinteger(L, OPT_PROXY); + lua_setfield(L, -2, "OPT_PROXY"); + lua_pushinteger(L, INFO_RESPONSE_CODE); + lua_setfield(L, -2, "INFO_RESPONSE_CODE"); + + return 1; +} + +// Function to preload the module +void lcurl_register(lua_State *L) { + lua_getglobal(L, "package"); + lua_getfield(L, -1, "preload"); + lua_pushcfunction(L, luaopen_lcurl); + lua_setfield(L, -2, "lcurl.safe"); + lua_pop(L, 2); // pop package and preload tables +} diff --git a/packages/driver/src/c/lcurl.h b/packages/driver/src/c/lcurl.h new file mode 100644 index 0000000..c4160e7 --- /dev/null +++ b/packages/driver/src/c/lcurl.h @@ -0,0 +1,8 @@ +#ifndef DRIVER_LCURL_H +#define DRIVER_LCURL_H + +#include "lua.h" + +void lcurl_register(lua_State *L); + +#endif //DRIVER_LCURL_H diff --git a/packages/driver/src/c/sub.c b/packages/driver/src/c/sub.c new file mode 100644 index 0000000..c450e64 --- /dev/null +++ b/packages/driver/src/c/sub.c @@ -0,0 +1,377 @@ +#include "sub.h" +#include "lauxlib.h" +#include "lualib.h" +#include "lcurl.h" +#include +#include + +#include +#include +#include + +typedef enum { + TYPE_DOUBLE, + TYPE_BOOLEAN, + TYPE_STRING +} DataType; + +typedef union { + double doubleValue; + int intValue; + const char *stringValue; // NULL if not present +} DataValue; + +typedef struct { + DataType type; + DataValue value; +} DataItem; + +size_t serialize(DataItem *data, size_t count, unsigned char **buffer) { + size_t totalSize = sizeof(size_t); + for (size_t i = 0; i < count; ++i) { + totalSize += sizeof(DataType); + switch (data[i].type) { + case TYPE_DOUBLE: + totalSize += sizeof(double); + break; + case TYPE_BOOLEAN: + totalSize += sizeof(int); + break; + case TYPE_STRING: + totalSize += sizeof(size_t) + (data[i].value.stringValue ? strlen(data[i].value.stringValue) + 1 : 0); + break; + } + } + + *buffer = (unsigned char *)malloc(totalSize); + unsigned char *ptr = *buffer; + + memcpy(ptr, &count, sizeof(size_t)); + ptr += sizeof(size_t); + + for (size_t i = 0; i < count; ++i) { + memcpy(ptr, &data[i].type, sizeof(DataType)); + ptr += sizeof(DataType); + switch (data[i].type) { + case TYPE_DOUBLE: + memcpy(ptr, &data[i].value.doubleValue, sizeof(double)); + ptr += sizeof(float); + break; + case TYPE_BOOLEAN: + memcpy(ptr, &data[i].value.intValue, sizeof(int)); + ptr += sizeof(int); + break; + case TYPE_STRING: { + size_t stringLen = data[i].value.stringValue ? strlen(data[i].value.stringValue) + 1 : 0; + memcpy(ptr, &stringLen, sizeof(size_t)); + ptr += sizeof(size_t); + if (stringLen > 0) { + memcpy(ptr, data[i].value.stringValue, stringLen); + ptr += stringLen; + } + break; + } + } + } + + return totalSize; +} + +DataItem* deserialize(const unsigned char *buffer, int *count) { + const unsigned char *ptr = buffer; + + memcpy(count, ptr, sizeof(int)); + ptr += sizeof(int); + + DataItem *data = (DataItem *)malloc(*count * sizeof(DataItem)); + for (int i = 0; i < *count; ++i) { + memcpy(&data[i].type, ptr, sizeof(DataType)); + ptr += sizeof(DataType); + switch (data[i].type) { + case TYPE_DOUBLE: + memcpy(&data[i].value.doubleValue, ptr, sizeof(double)); + ptr += sizeof(float); + break; + case TYPE_BOOLEAN: + memcpy(&data[i].value.intValue, ptr, sizeof(int)); + ptr += sizeof(int); + break; + case TYPE_STRING: { + size_t stringLen; + memcpy(&stringLen, ptr, sizeof(size_t)); + ptr += sizeof(size_t); + if (stringLen > 0) { + data[i].value.stringValue = (char *)malloc(stringLen); + memcpy((void *)data[i].value.stringValue, ptr, stringLen); + ptr += stringLen; + } else { + data[i].value.stringValue = NULL; + } + break; + } + } + } + + return data; +} + +EM_ASYNC_JS(int, launch_sub_script, (const char *script, const char *funcs, const char *subs, size_t size, void *data), { + try { + return await Module.launchSubScript(UTF8ToString(script), UTF8ToString(funcs), UTF8ToString(subs), size, data); + } catch (e) { + console.error("launch_sub_script error", e); + return 0; + } +}) + +static size_t lua_serialize(lua_State *L, int offset, uint8_t **serializedData) { + int n = lua_gettop(L); + size_t dataCount = n - offset + 1; + DataItem *data = (DataItem *)malloc(dataCount * sizeof(DataItem)); + for (int i = 0; i < dataCount; ++i) { + switch (lua_type(L, i + offset)) { + case LUA_TNUMBER: + data[i].type = TYPE_DOUBLE; + data[i].value.doubleValue = lua_tonumber(L, i + offset); + break; + case LUA_TBOOLEAN: + data[i].type = TYPE_BOOLEAN; + data[i].value.intValue = lua_toboolean(L, i + offset); + break; + case LUA_TSTRING: + data[i].type = TYPE_STRING; + data[i].value.stringValue = lua_tostring(L, i + offset); + break; + case LUA_TNIL: + data[i].type = TYPE_STRING; + data[i].value.stringValue = NULL; + break; + default: + assert(0); + } + } + size_t dataSize = serialize(data, dataCount, serializedData); + lua_settop(L, offset - 1); + return dataSize; +} + +int sub_lua_deserialize(lua_State *L, const uint8_t *serializedData) { + int dataCount; + DataItem *data = deserialize(serializedData, &dataCount); + for (int i = 0; i < dataCount; ++i) { + lua_checkstack(L, 1); + switch (data[i].type) { + case TYPE_DOUBLE: + lua_pushnumber(L, data[i].value.doubleValue); + break; + case TYPE_BOOLEAN: + lua_pushboolean(L, data[i].value.intValue); + break; + case TYPE_STRING: + if (data[i].value.stringValue) { + lua_pushstring(L, data[i].value.stringValue); + } else { + lua_pushnil(L); + } + break; + } + } + free(data); + return dataCount; +} + +// Call from main worker +static int LaunchSubScript(lua_State *L) { + int n = lua_gettop(L); + assert(n >= 3); + assert(lua_isstring(L, 1)); + assert(lua_isstring(L, 2)); + assert(lua_isstring(L, 3)); + + const char *script = lua_tostring(L, 1); + const char *funcs = lua_tostring(L, 2); + const char *subs = lua_tostring(L, 3); + + uint8_t *serializedData; + size_t dataSize = lua_serialize(L, 4, &serializedData); + + int r = launch_sub_script(script, funcs, subs, dataSize, serializedData); + if (r > 0) { + lua_pushlightuserdata(L, (void *)r); + } else { + lua_pushnil(L); + } + + free(serializedData); + + return 1; +} + +EM_ASYNC_JS(void, abort_sub_script, (int id), { + try { + await Module.abortSubScript(id); + } catch (e) { + console.error("abort_sub_script error", e); + } +}) + +// Call from main worker +static int AbortSubScript(lua_State *L) { + int n = lua_gettop(L); + assert(n >= 1); + assert(lua_islightuserdata(L, 1)); + + int id = (int)lua_touserdata(L, 1); + + abort_sub_script(id); + + return 0; +} + +// Call from main worker +static int IsSubScriptRunning(lua_State *L) { + int n = lua_gettop(L); + assert(n >= 1); + assert(lua_islightuserdata(L, 1)); + + int id = (int)lua_touserdata(L, 1); + + int r = EM_ASM_INT({ + return Module.isSubScriptRunning($0); + }, id); + + lua_pushboolean(L, r); + + return 1; +} + +// Call from main worker +void sub_init(lua_State *L) { + // SubScript + lua_pushcclosure(L, LaunchSubScript, 0); + lua_setglobal(L, "LaunchSubScript"); + + lua_pushcclosure(L, AbortSubScript, 0); + lua_setglobal(L, "AbortSubScript"); + + lua_pushcclosure(L, IsSubScriptRunning, 0); + lua_setglobal(L, "IsSubScriptRunning"); +} + +static int panic_func(lua_State *L) { + const char *msg = lua_tostring(L, -1); + fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n", msg); + return 0; +} + +static int traceback (lua_State *L) { + if (!lua_isstring(L, 1)) /* 'message' not a string? */ + return 1; /* keep it intact */ + lua_getglobal(L, "debug"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return 1; + } + lua_getfield(L, -1, "traceback"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + return 1; + } + lua_pushvalue(L, 1); /* pass error message */ + lua_pushinteger(L, 2); /* skip this function and traceback */ + lua_call(L, 2, 1); /* call debug.traceback */ + return 1; +} + +// TODO: use main worker's ConPrintf +static int ConPrintf(lua_State *L) { + int n = lua_gettop(L); + if (n < 1) { + return luaL_error(L, "ConPrintf needs at least one argument"); + } + + const char *fmt = luaL_checkstring(L, 1); + + luaL_Buffer b; + luaL_buffinit(L, &b); + + for (int i = 2; i <= n; i++) { + lua_pushvalue(L, i); + luaL_addvalue(&b); + } + + luaL_pushresult(&b); + const char *args = lua_tostring(L, -1); + + lua_getglobal(L, "string"); + lua_getfield(L, -1, "format"); + lua_remove(L, -2); // remove the 'string' table from the stack + + lua_pushstring(L, fmt); + lua_pushstring(L, args); + + if (lua_pcall(L, 2, 1, 0) != LUA_OK) { + return luaL_error(L, "error calling 'string.format': %s", lua_tostring(L, -1)); + } + + const char *formatted = lua_tostring(L, -1); + + lua_getglobal(L, "print"); + lua_pushstring(L, formatted); + + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { + return luaL_error(L, "error calling 'print': %s", lua_tostring(L, -1)); + } + + return 0; +} + +// Call from sub worker +EMSCRIPTEN_KEEPALIVE +int sub_start(const char *script, const char *funcs, const char *subs, size_t size, void *data) { + lua_State *L = luaL_newstate(); + if (L == NULL) { + free(data); + return 1; + } + + lua_atpanic(L, panic_func); + lua_pushcfunction(L, traceback); + + luaL_openlibs(L); + // TODO: os.exit() + lua_register(L, "ConPrintf", ConPrintf); + + lcurl_register(L); + + int err = luaL_loadstring(L, script); + if (err != LUA_OK) { + free(data); + return 2; + } + + int count = sub_lua_deserialize(L, data); + free(data); + + if (lua_pcall(L, count, LUA_MULTRET, 1) != LUA_OK) { + const char *msg = lua_tostring(L, -1); + fprintf(stderr, "sub_start error: %s\n", msg); + + EM_ASM({ + Module.bridge.onSubScriptError(UTF8ToString($0)); + }, msg); + + return 3; + } + + uint8_t *result; + size_t result_size = lua_serialize(L, 2, &result); + + EM_ASM({ + Module.bridge.onSubScriptFinished($0, $1); + }, result, result_size); + + free(result); + + return 0; +} diff --git a/packages/driver/src/c/sub.h b/packages/driver/src/c/sub.h new file mode 100644 index 0000000..f03d75f --- /dev/null +++ b/packages/driver/src/c/sub.h @@ -0,0 +1,10 @@ +#ifndef DRIVER_SUB_H +#define DRIVER_SUB_H + +#include +#include "lua.h" + +void sub_init(lua_State *L); +int sub_lua_deserialize(lua_State *L, const uint8_t *serializedData); + +#endif //DRIVER_SUB_H diff --git a/packages/driver/src/js/driver.ts b/packages/driver/src/js/driver.ts index f18c477..fc1407e 100644 --- a/packages/driver/src/js/driver.ts +++ b/packages/driver/src/js/driver.ts @@ -39,7 +39,9 @@ export class Driver { Comlink.proxy(this.hostCallbacks.onFetch), Comlink.proxy((text: string) => this.copy(text)), Comlink.proxy(() => this.paste()), - Comlink.proxy((url) => window.open(url, "_blank")), + Comlink.proxy((url) => { + window.open(url, "_blank"); + }), ); } diff --git a/packages/driver/src/js/logger.ts b/packages/driver/src/js/logger.ts index 0a704fc..e05d8da 100644 --- a/packages/driver/src/js/logger.ts +++ b/packages/driver/src/js/logger.ts @@ -150,6 +150,7 @@ const logger = { export const log = new Log().init( { kvfs: "INFO", + subscript: "DEBUG", }, (level, tag, msg, params) => { logger[level as keyof typeof logger](tag, msg, params); diff --git a/packages/driver/src/js/renderer/renderer.ts b/packages/driver/src/js/renderer/renderer.ts index b1f600a..81bed42 100644 --- a/packages/driver/src/js/renderer/renderer.ts +++ b/packages/driver/src/js/renderer/renderer.ts @@ -214,7 +214,7 @@ export class Renderer { const pos = { x, y }; for (let line of text.split("\n")) { // TODO: Implement multiple draw from a single string - if (line.length > 1024) line = line.substring(0, 1024); + if (line.length > 256) line = line.substring(0, 256); this.drawStringLine(pos, align, height, font, line); } } diff --git a/packages/driver/src/js/run.ts b/packages/driver/src/js/run.ts index 694cfd7..3f6bd53 100644 --- a/packages/driver/src/js/run.ts +++ b/packages/driver/src/js/run.ts @@ -6,8 +6,8 @@ const versionPrefix = `${__ASSET_PREFIX__}/v${version}`; const driver = new Driver("release", versionPrefix, { onError: (message) => console.error(message), onFrame: (_render, _time) => {}, - onFetch: async (url, _headers, _body) => { - throw new Error(`Fetch not implemented in shell: ${url}`); + onFetch: async (_url, _headers, _body) => { + throw new Error("Fetch not implemented in shell"); }, }); await driver.start({ cloudflareKvPrefix: "/api/kv/", cloudflareKvAccessToken: undefined }); diff --git a/packages/driver/src/js/sub.ts b/packages/driver/src/js/sub.ts new file mode 100644 index 0000000..2968bf3 --- /dev/null +++ b/packages/driver/src/js/sub.ts @@ -0,0 +1,120 @@ +import * as Comlink from "comlink"; +import { log, tag } from "./logger.ts"; + +interface DriverModule extends EmscriptenModule { + cwrap: typeof cwrap; + bridge: unknown; +} + +type Imports = { + subStart: (script: string, funcs: string, subs: string, size: number, data: number) => void; +}; + +export class SubScriptWorker { + private onFinished: (data: Uint8Array) => void = () => {}; + private onError: (message: string) => void = () => {}; + private onFetch: ( + url: string, + header: Record, + body: string | undefined, + ) => Promise<{ + body: string | undefined; + headers: Record; + status: number | undefined; + error: string | undefined; + }> = async () => ({ body: undefined, headers: {}, status: undefined, error: undefined }); + + async start( + script: string, + data: Uint8Array, + onFinished: (data: Uint8Array) => void, + onError: (message: string) => void, + onFetch: ( + url: string, + header: Record, + body: string | undefined, + ) => Promise<{ + body: string | undefined; + headers: Record; + status: number | undefined; + error: string | undefined; + }>, + ) { + const build = "release"; // TODO: configurable + this.onFinished = onFinished; + this.onError = onError; + this.onFetch = onFetch; + log.debug(tag.subscript, "start", { script }); + + const driver = (await import(`../../dist/driver-${build}.mjs`)) as { + default: EmscriptenModuleFactory; + }; + const module = await driver.default({ + print: console.log, // TODO: log.info + printErr: console.warn, // TODO: log.info + }); + + module.bridge = this.resolveExports(module); + const imports = this.resolveImports(module); + + const wasmData = module._malloc(data.length); + module.HEAPU8.set(data, wasmData); + + const ret = await imports.subStart(script, "", "", data.length, wasmData); + log.info(tag.subscript, `finished: ret=${ret}`); + } + + private resolveImports(module: DriverModule): Imports { + return { + subStart: module.cwrap("sub_start", "number", ["string", "string", "string", "number", "number"], { + async: true, + }), + }; + } + + private resolveExports(module: DriverModule) { + return { + onSubScriptError: (message: string) => { + log.error(tag.subscript, "onSubScriptError", { message }); + this.onError(message); + }, + onSubScriptFinished: (data: number, size: number) => { + const result = module.HEAPU8.slice(data, data + size); + log.debug(tag.subscript, "onSubScriptFinished", { result }); + this.onFinished(result); + }, + fetch: async (url: string, header: string | undefined, body: string | undefined) => { + try { + log.debug(tag.subscript, "fetch request", { url, header, body }); + const headers: Record = header + ? header + .split("\n") + .map((_) => _.split(":")) + .filter((_) => _.length === 2) + .reduce((acc, [k, v]) => Object.assign(acc, { [k.trim()]: v.trim() }), {}) + : {}; + if (!headers["Content-Type"]) { + headers["Content-Type"] = "application/x-www-form-urlencoded"; + } + const r = await this.onFetch(url, headers, body); + log.debug(tag.subscript, "fetch", r.body, r.status, r.error); + const headerText = Object.entries(r?.headers ?? {}) + .map(([k, v]) => `${k}: ${v}`) + .join("\n"); + return JSON.stringify({ + body: r?.body, + status: r?.status, + header: headerText, + error: r?.error, + }); + } catch (e) { + log.error(tag.subscript, "fetch error", { error: e }); + return JSON.stringify({ error: (e as Error).message }); + } + }, + }; + } +} + +const worker = new SubScriptWorker(); +Comlink.expose(worker); diff --git a/packages/driver/src/js/worker.ts b/packages/driver/src/js/worker.ts index b370685..1526eb1 100644 --- a/packages/driver/src/js/worker.ts +++ b/packages/driver/src/js/worker.ts @@ -5,8 +5,8 @@ import type { FilesystemConfig } from "./driver.ts"; import type { UIState } from "./event.ts"; import { CloudflareKV, WebAccess } from "./fs.ts"; import { ImageRepository } from "./image"; +import { log, tag } from "./logger.ts"; // @ts-ignore -import { createNODEFS } from "./nodefs.js"; import { BinPackingTextRasterizer, Renderer, @@ -15,6 +15,46 @@ import { WebGL1Backend, loadFonts, } from "./renderer"; +import type { SubScriptWorker } from "./sub.ts"; +import WorkerObject from "./sub.ts?worker"; + +export class SubScriptHost { + private worker: Worker | undefined; + private subScriptWorker: Comlink.Remote | undefined; + + constructor( + readonly script: string, + readonly funcs: string, + readonly subs: string, + readonly data: Uint8Array, + readonly onFinished: (data: Uint8Array) => void, + readonly onError: (message: string) => void, + readonly onFetch: HostCallbacks["onFetch"], + ) {} + + async start() { + this.worker = new WorkerObject(); + this.subScriptWorker = Comlink.wrap(this.worker); + this.subScriptWorker + .start( + this.script, + this.data, + Comlink.proxy(this.onFinished), + Comlink.proxy(this.onError), + Comlink.proxy(this.onFetch), + ) + .then(() => {}); + } + + async terminate() { + this.worker?.terminate(); + this.worker = undefined; + } + + isRunning() { + return this.worker !== undefined; + } +} interface DriverModule extends EmscriptenModule { cwrap: typeof cwrap; @@ -51,6 +91,8 @@ type Imports = { onKeyDown: (name: string, doubleClick: number) => void; onChar: (char: string, doubleClick: number) => void; onDownloadPageResult: (result: string) => void; + onSubScriptFinished: (id: number, data: number) => number; + onSubScriptError: (id: number, message: string) => number; }; export class DriverWorker { @@ -72,6 +114,8 @@ export class DriverWorker { private imports: Imports | undefined; private dirtyCount = 0; private isRunning = true; + private subScriptIndex = 1; + private subScripts: SubScriptHost[] = []; async start( build: "debug" | "release", @@ -215,6 +259,8 @@ export class DriverWorker { onKeyDown: module.cwrap("on_key_down", "number", ["string", "number"]), onChar: module.cwrap("on_char", "number", ["string", "number"]), onDownloadPageResult: module.cwrap("on_download_page_result", "number", ["string"]), + onSubScriptFinished: module.cwrap("on_subscript_finished", "number", ["number", "number"]), + onSubScriptError: module.cwrap("on_subscript_error", "number", ["number", "string"]), }; } @@ -244,37 +290,48 @@ export class DriverWorker { copy: (text: string) => this.mainCallbacks?.copy(text), paste: () => this.mainCallbacks?.paste(), openUrl: (url: string) => this.mainCallbacks?.openUrl(url), - fetch: async (url: string, header: string | undefined, body: string | undefined) => { - try { - const headers = header - ? header - .split("\n") - .map((_) => _.split(":")) - .reduce((acc, [k, v]) => ({ ...acc, [k.trim()]: v.trim() }), {}) - : {}; - const r = await this.hostCallbacks?.onFetch(url, headers, body); - const headerText = Object.entries(r?.headers ?? {}) - .map(([k, v]) => `${k}: ${v}`) - .join("\n"); - this.imports?.onDownloadPageResult( - JSON.stringify({ - body: r?.body, - status: r?.status, - header: headerText, - error: r?.error, - }), - ); - } catch (e: unknown) { - console.error(e); - this.imports?.onDownloadPageResult( - JSON.stringify({ - body: undefined, - status: undefined, - header: undefined, - error: e, - }), - ); - } + launchSubScript: async (script: string, funcs: string, subs: string, size: number, data: number) => { + const id = this.subScriptIndex; + const dataArray = new Uint8Array(size); + dataArray.set(new Uint8Array(module.HEAPU8.buffer, data, size)); + const subScript = new SubScriptHost( + script, + funcs, + subs, + dataArray, + (data: Uint8Array) => { + this.subScripts[id]?.terminate(); + delete this.subScripts[id]; + + const wasmData = module._malloc(data.length); + module.HEAPU8.set(data, wasmData); + const ret = this.imports?.onSubScriptFinished(id, wasmData); + module._free(wasmData); + + log.debug(tag.subscript, "onSubScriptFinished callback done", { ret }); + this.invalidate(); + }, + (message: string) => { + this.subScripts[id]?.terminate(); + delete this.subScripts[id]; + + const ret = this.imports?.onSubScriptError(id, message); + log.debug(tag.subscript, "onSubScriptError callback done", { ret }); + this.invalidate(); + }, + this.hostCallbacks?.onFetch ?? + (() => Promise.resolve({ error: "onFetch not implemented", body: "", headers: {}, status: 500 })), + ); + + this.subScripts[id] = subScript; + await subScript.start(); + return this.subScriptIndex++; + }, + abortSubScript: async (id: number) => { + await this.subScripts[id]?.terminate(); + }, + isSubScriptRunning: (id: number) => { + return this.subScripts[id]?.isRunning() ?? false; }, }; } diff --git a/packages/web/functions/api/fetch.ts b/packages/web/functions/api/fetch.ts index 644c48b..2348b29 100644 --- a/packages/web/functions/api/fetch.ts +++ b/packages/web/functions/api/fetch.ts @@ -11,20 +11,20 @@ interface FetchRequest { export const onRequest: PagesFunction = async (context) => { const req: FetchRequest = await context.request.json(); try { - let r; + let r: Request; if (req.body) { r = new Request(req.url, { method: "POST", body: req.body, headers: Object.assign({}, req.headers, { - "User-Agent": "pob.cool", + // "User-Agent": "pob.cool", }), }); } else { r = new Request(req.url, { method: "GET", headers: Object.assign({}, req.headers, { - "User-Agent": "pob.cool", + // "User-Agent": "pob.cool", }), }); } @@ -37,7 +37,7 @@ export const onRequest: PagesFunction = async (context) => { status: rep.status, }), ); - } catch (e: any) { + } catch (e) { return new Response( JSON.stringify({ body: undefined, diff --git a/packages/web/src/Footer.tsx b/packages/web/src/Footer.tsx index c3a43d8..6394b24 100644 --- a/packages/web/src/Footer.tsx +++ b/packages/web/src/Footer.tsx @@ -8,7 +8,7 @@ export default function Footer(p: { frameTime: number }) { @atty303 - ) - This site is not affiliated with Grinding Gear Games or Path of Exile. + ) - This product isn't affiliated with or endorsed by Grinding Gear Games in any way.