diff --git a/Cargo.lock b/Cargo.lock index f6aa2494..3ab0eeff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,7 +150,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -180,6 +180,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + [[package]] name = "better_scoped_tls" version = "0.1.1" @@ -295,7 +301,7 @@ checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -304,6 +310,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "calloop" version = "0.10.5" @@ -521,7 +533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "577ec3850834c2578eb44afa9250f9a807f8497664e6e2aaae19cea0aac2fe3b" dependencies = [ "anyhow", - "base64", + "base64 0.13.1", "deno_media_type", "dprint-swc-ext", "serde", @@ -716,7 +728,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -749,7 +761,7 @@ dependencies = [ "pmutil", "proc-macro2", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -1034,7 +1046,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -1536,7 +1548,9 @@ name = "oden" version = "0.1.0" dependencies = [ "anyhow", + "base64 0.21.4", "bytemuck", + "bytes", "deno_ast", "env_logger", "fontdue", @@ -1546,7 +1560,9 @@ dependencies = [ "notify", "oden-js", "pollster", + "sha-1", "sourcemap 7.0.0", + "thiserror", "tracy-client", "wgpu", "winit", @@ -1712,7 +1728,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -1764,9 +1780,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1788,9 +1804,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2006,7 +2022,7 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2211,7 +2227,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2277,7 +2293,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2326,7 +2342,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2409,7 +2425,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2438,7 +2454,7 @@ version = "0.178.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "675b5c755b0448268830e85e59429095d3423c0ce4a850b209c6f0eeab069f63" dependencies = [ - "base64", + "base64 0.13.1", "dashmap", "indexmap", "once_cell", @@ -2513,7 +2529,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2525,7 +2541,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2549,7 +2565,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2565,9 +2581,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -2594,22 +2610,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2699,7 +2715,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2907,7 +2923,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -2941,7 +2957,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index aba24a55..0ebe9f63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ tracing = ["tracy-client/enable"] [dependencies] anyhow = "1.0" +base64 = "0.21.4" bytemuck = { version = "1.13", features = ["derive"] } +bytes = "1.5.0" deno_ast = { version = "0.29.3", features = ["transpiling", "typescript"] } env_logger = "0.10" fontdue = "0.7.3" @@ -20,7 +22,9 @@ lru = "0.11.0" notify = "6" oden-js = { path = "oden-js" } pollster = "0.3" +sha-1 = "0.10.0" sourcemap = "7.0.0" +thiserror = "1.0.49" tracy-client = { version = "0.15.2", default-features = false } wgpu = "0.17" winit = "0.28" diff --git a/oden-js-sys/quickjs/Makefile b/oden-js-sys/quickjs/Makefile index 49b1f6fa..0c9202a5 100644 --- a/oden-js-sys/quickjs/Makefile +++ b/oden-js-sys/quickjs/Makefile @@ -1,6 +1,6 @@ # # QuickJS Javascript Engine -# +# # Copyright (c) 2017-2021 Fabrice Bellard # Copyright (c) 2017-2021 Charlie Gordon # @@ -54,9 +54,9 @@ OBJDIR=.obj ifdef CONFIG_WIN32 ifdef CONFIG_M32 - CROSS_PREFIX=i686-w64-mingw32- + CROSS_PREFIX=i686-w64-mingw32- else - CROSS_PREFIX=x86_64-w64-mingw32- + CROSS_PREFIX=x86_64-w64-mingw32- endif EXE=.exe else @@ -76,13 +76,13 @@ ifdef CONFIG_CLANG CFLAGS += -Wchar-subscripts -funsigned-char CFLAGS += -MMD -MF $(OBJDIR)/$(@F).d ifdef CONFIG_DEFAULT_AR - AR=$(CROSS_PREFIX)ar + AR=$(CROSS_PREFIX)ar else - ifdef CONFIG_LTO - AR=$(CROSS_PREFIX)llvm-ar - else - AR=$(CROSS_PREFIX)ar - endif + ifdef CONFIG_LTO + AR=$(CROSS_PREFIX)llvm-ar + else + AR=$(CROSS_PREFIX)ar + endif endif else HOST_CC=gcc @@ -90,9 +90,9 @@ else CFLAGS=-g -Wall -MMD -MF $(OBJDIR)/$(@F).d CFLAGS += -Wno-array-bounds -Wno-format-truncation ifdef CONFIG_LTO - AR=$(CROSS_PREFIX)gcc-ar + AR=$(CROSS_PREFIX)gcc-ar else - AR=$(CROSS_PREFIX)ar + AR=$(CROSS_PREFIX)ar endif endif STRIP=$(CROSS_PREFIX)strip @@ -132,7 +132,7 @@ else LDEXPORT=-rdynamic endif -PROGS=qjs$(EXE) qjsc$(EXE) run-test262 +PROGS=qjs$(EXE) qjsc$(EXE) qjsd$(EXE) run-test262 ifneq ($(CROSS_PREFIX),) QJSC_CC=gcc QJSC=./host-qjsc @@ -170,7 +170,7 @@ QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $( QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) ifdef CONFIG_BIGNUM -QJS_LIB_OBJS+=$(OBJDIR)/libbf.o +QJS_LIB_OBJS+=$(OBJDIR)/libbf.o QJS_OBJS+=$(OBJDIR)/qjscalc.o endif @@ -193,10 +193,19 @@ qjs-debug$(EXE): $(patsubst %.o, %.debug.o, $(QJS_OBJS)) qjsc$(EXE): $(OBJDIR)/qjsc.o $(QJS_LIB_OBJS) $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) +qjsc-debug$(EXE): $(OBJDIR)/qjsc.debug.o $(patsubst %.o, %.debug.o, $(QJS_LIB_OBJS)) + $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) + +qjsd$(EXE): $(OBJDIR)/qjsd.o $(QJS_LIB_OBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) + +qjsd-debug$(EXE): $(OBJDIR)/qjsd.debug.o $(patsubst %.o, %.debug.o, $(QJS_LIB_OBJS)) + $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) + ifneq ($(CROSS_PREFIX),) $(QJSC): $(OBJDIR)/qjsc.host.o \ - $(patsubst %.o, %.host.o, $(QJS_LIB_OBJS)) + $(patsubst %.o, %.host.o, $(QJS_LIB_OBJS)) $(HOST_CC) $(LDFLAGS) -o $@ $^ $(HOST_LIBS) endif #CROSS_PREFIX @@ -242,7 +251,7 @@ qjscalc.c: $(QJSC) qjscalc.js ifneq ($(wildcard unicode/UnicodeData.txt),) $(OBJDIR)/libunicode.o $(OBJDIR)/libunicode.m32.o $(OBJDIR)/libunicode.m32s.o \ - $(OBJDIR)/libunicode.nolto.o: libunicode-table.h + $(OBJDIR)/libunicode.nolto.o: libunicode-table.h libunicode-table.h: unicode_gen ./unicode_gen unicode $@ @@ -294,7 +303,7 @@ clean: rm -f *.a *.o *.d *~ unicode_gen regexp_test $(PROGS) rm -f hello.c test_fib.c rm -f examples/*.so tests/*.so - rm -rf $(OBJDIR)/ *.dSYM/ qjs-debug + rm -rf $(OBJDIR)/ *.dSYM/ qjs-debug qjsc-debug rm -rf run-test262-debug run-test262-32 install: all @@ -316,8 +325,8 @@ endif # example of static JS compilation HELLO_SRCS=examples/hello.js HELLO_OPTS=-fno-string-normalize -fno-map -fno-promise -fno-typedarray \ - -fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \ - -fno-date -fno-module-loader + -fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \ + -fno-date -fno-module-loader ifdef CONFIG_BIGNUM HELLO_OPTS+=-fno-bigint endif @@ -336,8 +345,8 @@ endif # example of static JS compilation with modules HELLO_MODULE_SRCS=examples/hello_module.js HELLO_MODULE_OPTS=-fno-string-normalize -fno-map -fno-promise -fno-typedarray \ - -fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \ - -fno-date -m + -fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \ + -fno-date -m examples/hello_module: $(QJSC) libquickjs$(LTOEXT).a $(HELLO_MODULE_SRCS) $(QJSC) $(HELLO_MODULE_OPTS) -o $@ $(HELLO_MODULE_SRCS) @@ -358,11 +367,11 @@ examples/point.so: $(OBJDIR)/examples/point.pic.o ############################################################################### # documentation -DOCS=doc/quickjs.pdf doc/quickjs.html doc/jsbignum.pdf doc/jsbignum.html +DOCS=doc/quickjs.pdf doc/quickjs.html doc/jsbignum.pdf doc/jsbignum.html build_doc: $(DOCS) -clean_doc: +clean_doc: rm -f $(DOCS) doc/%.pdf: doc/%.texi diff --git a/oden-js-sys/quickjs/quickjs-opcode.h b/oden-js-sys/quickjs/quickjs-opcode.h index c731a14a..877ea45d 100644 --- a/oden-js-sys/quickjs/quickjs-opcode.h +++ b/oden-js-sys/quickjs/quickjs-opcode.h @@ -260,6 +260,7 @@ DEF(is_undefined_or_null, 1, 1, 1, none) DEF( mul_pow10, 1, 2, 1, none) DEF( math_mod, 1, 2, 1, none) #endif +DEF( breakpoint, 1, 0, 1, none) /* must be the last non short and non temporary opcode */ DEF( nop, 1, 0, 0, none) diff --git a/oden-js-sys/quickjs/quickjs.c b/oden-js-sys/quickjs/quickjs.c index 4d62fd6c..505f27dd 100644 --- a/oden-js-sys/quickjs/quickjs.c +++ b/oden-js-sys/quickjs/quickjs.c @@ -289,6 +289,9 @@ struct JSRuntime { JSModuleLoaderFunc *module_loader_func; void *module_loader_opaque; + JSMapSourceFunc *map_source_func; + void *map_source_opaque; + BOOL can_block : 8; /* TRUE if Atomics.wait can block */ /* used to allocate, free and clone SharedArrayBuffers */ JSSharedArrayBufferFunctions sab_funcs; @@ -306,8 +309,12 @@ struct JSRuntime { uint32_t operator_count; #endif void *user_opaque; - JSMapSourceFunc *map_source_func; - void *map_source_opaque; + + struct list_head breakpoint_list; + JSDebugCallbackFunc *debug_callback; + void *debug_opaque; + int stepping_pc; + uint8_t stepping_byte; }; struct JSClass { @@ -317,7 +324,7 @@ struct JSClass { JSClassGCMark *gc_mark; JSClassCall *call; /* pointers for exotic behavior, can be NULL if none are present */ - const JSClassExoticMethods *exotic; + const JSClassExoticMethods *exotic; }; #define JS_MODE_STRICT (1 << 0) @@ -997,6 +1004,25 @@ enum OPCodeEnum { OP_TEMP_END, }; +typedef struct JSBreakpoint { + JSRuntime *rt; + JSAtom file; + int line; + + struct list_head link; + + int ref_count; + JSFunctionBytecode *function; + int code_offset; + uint8_t replaced_byte; + JS_BOOL enabled; +} JSBreakpoint; + +typedef struct JSDebugContext { + JSContext *ctx; + JSStackFrame *sf; +} JSDebugContext; + static int JS_InitAtoms(JSRuntime *rt); static JSAtom __JS_NewAtomInit(JSRuntime *rt, const char *str, int len, int atom_type); @@ -1253,6 +1279,9 @@ static JSValue JS_InstantiateFunctionListItem2(JSContext *ctx, JSObject *p, JSAtom atom, void *opaque); void JS_SetUncatchableError(JSContext *ctx, JSValueConst val, BOOL flag); +static JSBreakpoint *find_existing_breakpoint(JSContext *ctx, JSFunctionBytecode *func, int pc); +static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_offset); + static const JSClassExoticMethods js_arguments_exotic_methods; static const JSClassExoticMethods js_string_exotic_methods; static const JSClassExoticMethods js_proxy_exotic_methods; @@ -1636,6 +1665,8 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque) #endif init_list_head(&rt->job_list); + init_list_head(&rt->breakpoint_list); + if (JS_InitAtoms(rt)) goto fail; @@ -6469,6 +6500,50 @@ static const char *get_func_name(JSContext *ctx, JSValueConst func) /* only taken into account if filename is provided */ #define JS_BACKTRACE_FLAG_SINGLE_LEVEL (1 << 1) +static void get_frame_debug_position(JSContext *ctx, JSStackFrame *sf, + const char **function, const char **file, + int *line, BOOL *backtrace_barrier) +{ + JSObject *p; + + *backtrace_barrier = FALSE; + *function = get_func_name(ctx, sf->cur_func); + + p = JS_VALUE_GET_OBJ(sf->cur_func); + if (js_class_has_bytecode(p->class_id)) { + JSFunctionBytecode *b; + int line_num1; + + b = p->u.func.function_bytecode; + *backtrace_barrier = b->backtrace_barrier; + if (b->has_debug) { + line_num1 = find_line_num(ctx, b, sf->cur_pc - b->byte_code_buf - 1); + + JSAtom fname = b->debug.filename; + if (ctx->rt->map_source_func) { + ctx->rt->map_source_func( + ctx, + ctx->rt->map_source_opaque, + fname, line_num1, + &fname, &line_num1); + } + + *file = JS_AtomToCString(ctx, fname); + *line = line_num1; + + if (ctx->rt->map_source_func) { + JS_FreeAtomRT(ctx->rt, fname); + } + } else { + *file = NULL; + *line = -1; + } + } else { + *file = js_strdup(ctx, "native"); + *line = -1; + } +} + /* if filename != NULL, an additional level is added with the filename and line number information (used for parse error). */ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, @@ -6479,8 +6554,9 @@ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, JSValue str; DynBuf dbuf; const char *func_name_str; + const char *file_name_str; + int line_num1; const char *str1; - JSObject *p; BOOL backtrace_barrier; js_dbuf_init(ctx, &dbuf); @@ -6502,7 +6578,9 @@ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, backtrace_flags &= ~JS_BACKTRACE_FLAG_SKIP_FIRST_LEVEL; continue; } - func_name_str = get_func_name(ctx, sf->cur_func); + + get_frame_debug_position(ctx, sf, &func_name_str, &file_name_str, + &line_num1, &backtrace_barrier); if (!func_name_str || func_name_str[0] == '\0') str1 = ""; else @@ -6510,43 +6588,15 @@ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, dbuf_printf(&dbuf, " at %s", str1); JS_FreeCString(ctx, func_name_str); - p = JS_VALUE_GET_OBJ(sf->cur_func); - backtrace_barrier = FALSE; - if (js_class_has_bytecode(p->class_id)) { - JSFunctionBytecode *b; - const char *atom_str; - int line_num1; - - b = p->u.func.function_bytecode; - backtrace_barrier = b->backtrace_barrier; - if (b->has_debug) { - line_num1 = find_line_num(ctx, b, - sf->cur_pc - b->byte_code_buf - 1); - - JSAtom file = b->debug.filename; - if (ctx->rt->map_source_func) { - ctx->rt->map_source_func( - ctx, - ctx->rt->map_source_opaque, - file, line_num1, - &file, &line_num1); - } - - atom_str = JS_AtomToCString(ctx, file); - dbuf_printf(&dbuf, " (%s", - atom_str ? atom_str : ""); - JS_FreeCString(ctx, atom_str); - if (line_num1 != -1) - dbuf_printf(&dbuf, ":%d", line_num1); - dbuf_putc(&dbuf, ')'); - - if (ctx->rt->map_source_func) { - JS_FreeAtomRT(ctx->rt, file); - } + if (file_name_str && file_name_str[0]) { + dbuf_printf(&dbuf, " (%s", file_name_str); + if (line_num1 >= 0) { + dbuf_printf(&dbuf, ":%d", line_num1); } - } else { - dbuf_printf(&dbuf, " (native)"); + dbuf_putc(&dbuf, ')'); } + JS_FreeCString(ctx, file_name_str); + dbuf_putc(&dbuf, '\n'); /* stop backtrace if JS_EVAL_FLAG_BACKTRACE_BARRIER was used */ if (backtrace_barrier) @@ -16227,10 +16277,14 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, size_t alloca_size; #if !DIRECT_DISPATCH -#define SWITCH(pc) switch (opcode = *pc++) -#define CASE(op) case op -#define DEFAULT default -#define BREAK break +#define SWITCH(pc) \ + opcode = *pc++; \ + resume_breakpoint: \ + switch (opcode) +#define RESUME_BREAKPOINT(op) opcode = op; goto resume_breakpoint; +#define CASE(op) case op +#define DEFAULT default +#define BREAK break #else static const void * const dispatch_table[256] = { #define DEF(id, size, n_pop, n_push, f) && case_OP_ ## id, @@ -16242,10 +16296,11 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, #include "quickjs-opcode.h" [ OP_COUNT ... 255 ] = &&case_default }; -#define SWITCH(pc) goto *dispatch_table[opcode = *pc++]; -#define CASE(op) case_ ## op -#define DEFAULT case_default -#define BREAK SWITCH(pc) +#define SWITCH(pc) goto *dispatch_table[opcode = *pc++]; +#define RESUME_BREAKPOINT(op) opcode = op; goto *dispatch_table[opcode]; +#define CASE(op) case_ ## op +#define DEFAULT case_default +#define BREAK SWITCH(pc) #endif if (js_poll_interrupts(caller_ctx)) @@ -18667,6 +18722,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JS_FreeValue(ctx, sp[-1]); sp[-1] = JS_FALSE; BREAK; + CASE(OP_breakpoint): + { + int code_offset = (int)(pc - b->byte_code_buf - 1); + int next_op = js_handle_breakpoint(ctx, b, code_offset); + RESUME_BREAKPOINT(next_op); + } + BREAK; CASE(OP_invalid): DEFAULT: JS_ThrowInternalError(ctx, "invalid opcode: pc=%u opcode=0x%02x", @@ -31142,6 +31204,7 @@ static __exception int resolve_variables(JSContext *ctx, JSFunctionDef *s) case OP_nop: /* remove erased code */ break; + case OP_set_class_name: /* only used during parsing */ break; @@ -33717,6 +33780,24 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, goto fail1; fun_obj = JS_DupValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)); } + + if (ctx->rt->debug_callback) { + JSDebugContext dctx; + dctx.ctx = ctx; + dctx.sf = sf; + + JSDebugEvent evt; + evt.reason = JS_BREAK_CODE_LOADED; + evt.u.loaded.filename = filename; + + /* TODO: Handle the step/resume/what-have-you. Step from here will + be complicated because the function we need to step into will be + the one we just parsed, kinda- unless it's a module? Even then + it's not clear where the break will go if we're loading a + module... yikes. */ + /* resume = */ctx->rt->debug_callback(&dctx, &evt, ctx->rt->debug_opaque); + } + if (flags & JS_EVAL_FLAG_COMPILE_ONLY) { ret_val = fun_obj; } else { @@ -54086,3 +54167,220 @@ void JS_SetSourceMapFunc(JSRuntime *rt, JSMapSourceFunc *map_func, void *opaque) rt->map_source_func = map_func; rt->map_source_opaque = opaque; } + +/* Debugger */ + +void JS_SetDebugCallbackFunc(JSRuntime *rt, JSDebugCallbackFunc *func, void *opaque) +{ + rt->debug_callback = func; + rt->debug_opaque = opaque; +} + +static BOOL find_pc_offset(JSFunctionBytecode *b, int target_line, int *break_pc, int *actual_line) +{ + const uint8_t *p_end, *p; + int new_line_num, line_num, new_pc, pc, v, ret; + unsigned int op; + + if (!b->has_debug || !b->debug.pc2line_buf) { + /* function was stripped */ + return FALSE; + } + if (target_line < b->debug.line_num) { + /* this line is before this function even starts */ + return FALSE; + } + + p = b->debug.pc2line_buf; + p_end = p + b->debug.pc2line_len; + pc = 0; + line_num = b->debug.line_num; + while (p < p_end) { + op = *p++; + if (op == 0) { + uint32_t val; + ret = get_leb128(&val, p, p_end); + if (ret < 0) + goto fail; + new_pc = pc + val; + p += ret; + ret = get_sleb128(&v, p, p_end); + if (ret < 0) { + fail: + /* should never happen */ + return FALSE; + } + p += ret; + new_line_num = line_num + v; + } else { + op -= PC2LINE_OP_FIRST; + new_pc = pc + (op / PC2LINE_RANGE); + new_line_num = line_num + (op % PC2LINE_RANGE) + PC2LINE_BASE; + } + if (new_line_num > target_line) { + *break_pc = pc; + *actual_line = line_num; + return TRUE; + } + line_num = new_line_num; + pc = new_pc; + } + return FALSE; +} + +static JSBreakpoint *find_existing_breakpoint(JSContext *ctx, JSFunctionBytecode *func, int pc) +{ + struct list_head *el; + list_for_each(el, &ctx->rt->breakpoint_list) { + JSBreakpoint *bp = list_entry(el, JSBreakpoint, link); + if (bp->function == func && bp->code_offset == pc) { + return JS_DupBreakpoint(bp); + } + } + return NULL; +} + +static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_offset) +{ + int next_op; + /* JSResumeMode resume = JS_RESUME_MODE_CONTINUE; */ + JSRuntime *rt = ctx->rt; + JSBreakpoint *bp = find_existing_breakpoint(ctx, b, code_offset); + if (bp) { + if (bp->enabled) { + if (rt->debug_callback) { + JSDebugContext dctx; + dctx.ctx = ctx; + dctx.sf = ctx->rt->current_stack_frame; + + JSDebugEvent evt; + evt.reason = JS_BREAK_BREAKPOINT; + evt.u.breakpoint = bp; + + /* resume = */rt->debug_callback(&dctx, &evt, rt->debug_opaque); + } + } + next_op = bp->replaced_byte; + } else { + assert(code_offset == rt->stepping_pc); + rt->stepping_pc = -1; + next_op = rt->stepping_byte; + b->byte_code_buf[code_offset] = rt->stepping_byte; + + if (rt->debug_callback) { + JSDebugContext dctx; + dctx.ctx = ctx; + dctx.sf = ctx->rt->current_stack_frame; + + JSDebugEvent evt; + evt.reason = JS_BREAK_STEP; + + /* resume = */rt->debug_callback(&dctx, &evt, rt->debug_opaque); + } + } + + return next_op; +} + +JSContext *JS_DebugGetContext(JSDebugContext *ctx) +{ + return ctx->ctx; +} + +JSDebugFrame *JS_DebugGetFrame(JSDebugContext *ctx) { + return (JSDebugFrame *)(ctx->sf); +} + +JSDebugFrame *JS_DebugGetPreviousFrame(JSDebugContext *ctx, JSDebugFrame *frame) { + JSStackFrame *sf = (JSStackFrame *)frame; + if (!sf) + return NULL; + return (JSDebugFrame *)(sf->prev_frame); +} + +void JS_DebugGetFrameSourcePosition(JSDebugContext *dctx, JSDebugFrame *frame, + const char **function, const char **file, + int *line) { + JSStackFrame *sf = (JSStackFrame *)frame; + JSContext *ctx = dctx->ctx; + BOOL backtrace_barrier; + + get_frame_debug_position(ctx, sf, function, file, line, &backtrace_barrier); +} + + +JSBreakpoint *JS_SetBreakpoint(JSContext *ctx, JSAtom file, int line) +{ + struct list_head *el; + list_for_each(el, &ctx->rt->gc_obj_list) { + JSGCObjectHeader *gp = list_entry(el, JSGCObjectHeader, link); + if (gp->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE) { + JSFunctionBytecode *func = (JSFunctionBytecode *)gp; + if (func->debug.filename == file) { + int break_pc, actual_line; + if (find_pc_offset(func, line, &break_pc, &actual_line)) { + uint8_t code = func->byte_code_buf[break_pc]; + if (code == OP_breakpoint) { + return find_existing_breakpoint(ctx, func, break_pc); + } + + JSBreakpoint *breakpoint = js_mallocz(ctx, sizeof(JSBreakpoint)); + if (!breakpoint) { + return NULL; + } + breakpoint->ref_count = 1; + + breakpoint->rt = ctx->rt; + breakpoint->file = JS_DupAtom(ctx, file); + breakpoint->line = actual_line; + + list_add_tail(&breakpoint->link, &ctx->rt->breakpoint_list); + breakpoint->function = func; + func->header.ref_count++; + + breakpoint->code_offset = break_pc; + breakpoint->enabled = TRUE; + breakpoint->replaced_byte = code; + func->byte_code_buf[break_pc] = OP_breakpoint; + + return breakpoint; + } + } + } + } + return NULL; +} + +JSBreakpoint *JS_DupBreakpoint(JSBreakpoint *breakpoint) +{ + breakpoint->ref_count += 1; + return breakpoint; +} + +void JS_DeleteBreakpoint(JSBreakpoint *breakpoint) +{ + breakpoint->ref_count -= 1; + if (breakpoint->ref_count == 0) { + breakpoint->function->byte_code_buf[breakpoint->code_offset] = breakpoint->replaced_byte; + list_del(&breakpoint->link); + JS_FreeValueRT(breakpoint->rt, JS_MKPTR(JS_TAG_FUNCTION_BYTECODE, breakpoint->function)); + JS_FreeAtomRT(breakpoint->rt, breakpoint->file); + } +} + +JS_BOOL JS_GetBreakpointEnabled(JSBreakpoint *breakpoint) +{ + return breakpoint->enabled; +} + +void JS_SetBreakpointEnabled(JSBreakpoint *breakpoint, JS_BOOL enabled) +{ + breakpoint->enabled = enabled; +} + +void JS_GetBreakpointLocation(JSBreakpoint *breakpoint, JSAtom *file, int *line) +{ + *file = JS_DupAtomRT(breakpoint->rt, breakpoint->file); + *line = breakpoint->line; +} + diff --git a/oden-js-sys/quickjs/quickjs.h b/oden-js-sys/quickjs/quickjs.h index 90c578ae..7d9faf31 100644 --- a/oden-js-sys/quickjs/quickjs.h +++ b/oden-js-sys/quickjs/quickjs.h @@ -1050,6 +1050,56 @@ typedef void JSMapSourceFunc(JSContext *ctx, void *opaque, JSAtom file, int line void JS_SetSourceMapFunc(JSRuntime *rt, JSMapSourceFunc *map_func, void *opaque); +/* Debugger support */ + +typedef struct JSDebugContext JSDebugContext; +typedef struct JSDebugFrame JSDebugFrame; + +JSContext *JS_DebugGetContext(JSDebugContext *ctx); + +JSDebugFrame *JS_DebugGetFrame(JSDebugContext *ctx); +JSDebugFrame *JS_DebugGetPreviousFrame(JSDebugContext *ctx, JSDebugFrame *frame); +void JS_DebugGetFrameSourcePosition(JSDebugContext *ctx, JSDebugFrame *frame, + const char **function, const char **file, + int *line); + +typedef enum JSResumeMode { + JS_RESUME_MODE_CONTINUE, + JS_RESUME_MODE_STEP_OVER, + JS_RESUME_MODE_STEP_INTO, +} JSResumeMode; + +typedef enum JSBreakReason { + JS_BREAK_CODE_LOADED, + JS_BREAK_BREAKPOINT, + JS_BREAK_STEP, +} JSBreakReason; + +typedef struct JSBreakpoint JSBreakpoint; + +typedef struct JSDebugEvent { + JSBreakReason reason; + union { + struct { + const char *filename; + } loaded; + JSBreakpoint *breakpoint; + } u; +} JSDebugEvent; + +typedef JSResumeMode JSDebugCallbackFunc(JSDebugContext *ctx, + JSDebugEvent *event, + void *opaque); +void JS_SetDebugCallbackFunc(JSRuntime *rt, JSDebugCallbackFunc *bp_func, + void *opqaue); + +JSBreakpoint *JS_SetBreakpoint(JSContext *ctx, JSAtom file, int line); +JSBreakpoint *JS_DupBreakpoint(JSBreakpoint *breakpoint); +void JS_DeleteBreakpoint(JSBreakpoint *breakpoint); +JS_BOOL JS_GetBreakpointEnabled(JSBreakpoint *breakpoint); +void JS_SetBreakpointEnabled(JSBreakpoint *breakpoint, JS_BOOL enabled); +void JS_GetBreakpointLocation(JSBreakpoint *breakpoint, JSAtom *file, int *line); + #undef js_unlikely #undef js_force_inline diff --git a/oden-js/src/context.rs b/oden-js/src/context.rs index 5eb6901a..a25eef51 100644 --- a/oden-js/src/context.rs +++ b/oden-js/src/context.rs @@ -1,6 +1,6 @@ use crate::{ callback::new_fn, conversion::RustFunction, module::Module, Atom, ClassID, Error, Promise, - Result, Runtime, Value, ValueRef, ValueResult, + Result, Runtime, TryIntoValue, Value, ValueRef, ValueResult, }; use bitflags::bitflags; use oden_js_sys as sys; @@ -177,6 +177,21 @@ impl ContextRef { self.check_exception(unsafe { sys::JS_NewObject(self.ctx) }) } + /// Construct a new value of type object with the specified properties. + pub fn new_object_props(&self, props: [(K, V); N]) -> ValueResult + where + K: AsRef, + V: TryIntoValue, + { + let mut obj = self.new_object()?; + for (k, v) in props.into_iter() { + let k: &str = k.as_ref(); + let v = v.try_into_value(self)?; + obj.set_property(self, k, &v)?; + } + Ok(obj) + } + /// Construct a new value from a boolean. pub fn new_bool(&self, value: T) -> ValueResult where @@ -249,6 +264,19 @@ impl ContextRef { self.check_exception(unsafe { sys::JS_NewArray(self.ctx) }) } + /// Construct a new array value with the given contents + pub fn new_array_values(&self, values: [V; N]) -> ValueResult + where + V: TryIntoValue, + { + let mut arr = self.new_array()?; + for (i, v) in values.into_iter().enumerate() { + let v = v.try_into_value(self)?; + arr.set_index(self, i.try_into().unwrap(), &v)?; + } + Ok(arr) + } + /// Construct a new value from a string. pub fn new_string(&self, value: &str) -> ValueResult { let c_value = CString::new(value)?; @@ -413,6 +441,31 @@ impl ContextRef { ) }) } + + /// Parse the specified JSON as data. `filename` is used for reporting + /// errors. + pub fn parse_json(&self, data: &str, filename: &str) -> Result { + let c_data = CString::new(data)?; + let c_filename = CString::new(filename)?; + + self.check_exception(unsafe { + sys::JS_ParseJSON(self.ctx, c_data.as_ptr(), data.len(), c_filename.as_ptr()) + }) + } + + /// Convert the value to a JSON string. + pub fn json_stringify(&self, value: &ValueRef) -> Result { + self.check_exception(unsafe { + let undef = sys::JSValue { + u: sys::JSValueUnion { + ptr: std::ptr::null_mut(), + }, + tag: sys::JS_TAG_UNDEFINED as i64, + }; + + sys::JS_JSONStringify(self.ctx, value.val, undef, undef) + }) + } } #[derive(Debug)] diff --git a/oden-js/src/value.rs b/oden-js/src/value.rs index 55530347..0ef8c8d3 100644 --- a/oden-js/src/value.rs +++ b/oden-js/src/value.rs @@ -481,6 +481,12 @@ impl Clone for Value { } } +impl AsRef for Value { + fn as_ref(&self) -> &ValueRef { + self + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 72e6c69f..d0d86d4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1376,6 +1376,8 @@ fn main_thread(event_loop: EventLoopProxy, state: State, reciever: Re let mut script = script::ScriptContext::new(None, script_reload_send.clone()) .expect("Unable to create initial script context"); + let _debugger = script::debugger::start_debugger(); + const SPF: f64 = 1.0 / 60.0; loop { frame_mark(); diff --git a/src/script.rs b/src/script.rs index ac9905b4..81b9faf9 100644 --- a/src/script.rs +++ b/src/script.rs @@ -11,6 +11,7 @@ use std::time::Instant; use tracy_client::span; use winit::event::*; +pub mod debugger; pub mod graphics; mod input; mod io; diff --git a/src/script/debugger.rs b/src/script/debugger.rs new file mode 100644 index 00000000..389b0cb8 --- /dev/null +++ b/src/script/debugger.rs @@ -0,0 +1,701 @@ +use base64::Engine; +use bytes::buf::BufMut; +use oden_js::{Context, ContextRef, Runtime, ValueRef}; +use sha1::{Digest, Sha1}; +use std::collections::HashMap; +use std::io; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, +}; +use std::thread; +use thiserror::Error; + +pub enum DebuggerMethod { + UnknownMethod, + DebuggerEnable, + DebuggerSetAsyncCallStackDepth, + DebuggerSetBlackboxPatterns, + DebuggerSetPauseOnExceptions, + ProfilerEnable, + RuntimeEvaluate, + RuntimeEnable, + RuntimeRunIfWaitingForDebugger, +} + +impl DebuggerMethod { + pub fn from_request(method: &str, _params: &ValueRef) -> Result { + let evt = match method { + "Profiler.enable" => DebuggerMethod::ProfilerEnable, + "Runtime.enable" => DebuggerMethod::RuntimeEnable, + "Runtime.evaluate" => DebuggerMethod::RuntimeEvaluate, + "Runtime.runIfWaitingForDebugger" => DebuggerMethod::RuntimeRunIfWaitingForDebugger, + "Debugger.enable" => DebuggerMethod::DebuggerEnable, + "Debugger.setAsyncCallStackDepth" => DebuggerMethod::DebuggerSetAsyncCallStackDepth, + "Debugger.setBlackboxPatterns" => DebuggerMethod::DebuggerSetBlackboxPatterns, + "Debugger.setPauseOnExceptions" => DebuggerMethod::DebuggerSetPauseOnExceptions, + _ => { + eprintln!("Unsupported method: {method}"); + DebuggerMethod::UnknownMethod + } + }; + + Ok(evt) + } +} + +struct DebuggerMessage { + pub id: String, + pub session_id: Option, + pub method: DebuggerMethod, + pub response: Websocket, +} + +impl DebuggerMessage { + pub fn from_message( + context: &ContextRef, + message: &ValueRef, + response: Websocket, + ) -> Result { + let session_id = { + let session_id = message.get_property(&context, "sessionId")?; + if session_id.is_undefined() { + None + } else { + Some(session_id.to_string(&context)?) + } + }; + let id = { + let id = message.get_property(context, "id")?; + id.to_string(&context)? + }; + let method = { + let method = message.get_property(&context, "method")?; + method.to_string(&context)? + }; + let params = message.get_property(&context, "params")?; + + let method = DebuggerMethod::from_request(&method, ¶ms)?; + Ok(DebuggerMessage { + id, + session_id, + method, + response, + }) + } +} + +pub enum DebuggerEvent { + Error { + id: String, + code: i32, + message: String, + }, +} + +pub struct Debugger { + _thread: thread::JoinHandle<()>, + messages: Receiver, +} + +#[allow(dead_code)] +pub fn start_debugger() -> Debugger { + let (send_message, receive_message) = channel(); + let thread = thread::spawn(move || { + if let Err(e) = debugger_listener(send_message) { + eprintln!("Error in debug listener: {:?}", e); + } + }); + + Debugger { + _thread: thread, + messages: receive_message, + } +} + +#[allow(dead_code)] +impl Debugger { + pub fn handle_debugger_messages(&self, _context: &Context) { + while let Ok(msg) = self.messages.try_recv() { + let event = match msg.method { + // TODO + _ => DebuggerEvent::Error { + id: msg.id.clone(), + code: -32601, + message: "Unsupported method".to_string(), + }, + }; + + // msg.response.send_json_response(context, context.new_string(msg.id)? + // let _ = msg.response.send(event); + } + } + + pub fn send_debug_event(&self) {} +} + +pub fn debugger_listener(send: Sender) -> Result<()> { + let listener = TcpListener::bind("127.0.0.1:9229")?; + let port = listener.local_addr()?.port(); + println!("Debugger listening on http://127.0.0.1:{port}"); + + for stream in listener.incoming() { + let stream = match stream { + Ok(s) => s, + Err(e) => { + eprintln!("Error accepting incoming connection: {:?}", e); + continue; + } + }; + + DebuggerConnection::start(stream, send.clone()); + } + Ok(()) +} + +struct DebuggerConnection { + stream: TcpStream, + send: Sender, + event_send: Sender, + events: Receiver, + context: Context, +} + +impl DebuggerConnection { + fn start(stream: TcpStream, send: Sender) -> Sender { + let (event_send, event_receive) = channel(); + let result = event_send.clone(); + thread::spawn(move || { + let mut connection = DebuggerConnection { + stream, + send, + event_send, + events: event_receive, + context: Context::new(Runtime::new()), + }; + loop { + match connection.handle_connection() { + Ok(keep_open) => { + if !keep_open { + break; + } + } + Err(e) => { + eprintln!("Closing connection: {e}"); + break; + } + } + } + }); + result + } + + // ============================================================================ + // Dumb HTTP stuff + // ============================================================================ + fn handle_connection(&mut self) -> Result { + let port = self.stream.local_addr()?.port(); + let id = "dd5cfe85-f0b1-4241-a643-8ed81e436188"; + + let mut buffer = String::new(); + let mut buf_reader = BufReader::new(&mut self.stream); + while buffer.trim().is_empty() { + buf_reader.read_line(&mut buffer)?; + } + + let parts: Vec<_> = buffer.trim().split(" ").collect(); + if parts.len() != 3 { + eprintln!("Invalid request line: {buffer}"); + self.write_http_error(400, "Invalid request")?; + return Ok(false); + } + + let method = parts[0]; + let path = parts[1]; + eprintln!("Debugger: {method} {path}"); + + let mut headers = HashMap::new(); + let mut header_line_buffer = String::new(); + loop { + header_line_buffer.clear(); + buf_reader.read_line(&mut header_line_buffer)?; + let header_line = header_line_buffer.trim(); + if header_line.is_empty() { + break; + } + + let sep_idx = match header_line.find(":") { + Some(idx) => idx, + None => { + self.write_http_error(400, "Invalid request")?; + return Ok(false); + } + }; + let header = &header_line[0..sep_idx]; + let value = &header_line[sep_idx + 1..]; + // eprintln!("HEADER: {header} {value}"); + headers.insert(header.trim().to_lowercase(), value.trim().to_string()); + } + + if method == "GET" { + if path == "/json/version" { + // NOTE: Deno spits out a V8 version here but we're not running v8. + self.write_http_ok( + "application/json", + r#" +{ + "Browser": "Oden/0.0.1", + "Protocol-Version": "1.3" +}"# + .as_bytes(), + )?; + } else if path == "/json" || path == "/json/list" { + // TODO: title should reflect the game being loaded + // TODO: url should go to the main .ts file + // TODO: faciconUrl? + let result = format!( + r#" +[{{ + "description": "oden", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/ws/01b9bd23-8810-43ed-86f7-5feef5d120fa&experiments=true&v8only=true", + "id": "{id}", + "title": "oden game", + "url": "oden://game", + "webSocketDebuggerUrl": "ws://127.0.0.1:{port}/ws/{id}" +}}] +"# + ); + self.write_http_ok("application/json", result.as_bytes())?; + } else if path == "/ws/dd5cfe85-f0b1-4241-a643-8ed81e436188" { + // I don't feel like making a new guid for each thing. + // Websocket upgrade and then debugger messages. + + let mut key = match headers.get("sec-websocket-key") { + Some(v) => v.clone(), + None => { + self.write_http_error(400, "Invalid request")?; + return Ok(false); + } + }; + key.push_str("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + let mut hasher = Sha1::new(); + hasher.update(key.as_bytes()); + let result = base64::engine::general_purpose::STANDARD.encode(hasher.finalize()); + let response = format!( + "HTTP/1.1 101 Switching Protocols\r\n\ + Upgrade: websocket\r\n\ + Connection: Upgrade\r\n\ + Sec-Websocket-Accept: {result}\r\n\ + \r\n" + ); + self.stream.write_all(response.as_bytes())?; + + self.handle_websocket_connection()?; + return Ok(false); + } else { + self.write_http_error(404, "Not Found")?; + } + } else { + self.write_http_error(404, "Not Found")?; + } + Ok(true) + } + + fn write_http_response( + &mut self, + code: u16, + phrase: &str, + content_type: &str, + data: &[u8], + ) -> Result<()> { + let length = data.len(); + let buffer = format!( + "HTTP/1.1 {code} {phrase}\r\n\ + content-length: {length}\r\n\ + content-type: {content_type}\r\n\ + \r\n" + ); + self.stream.write_all(buffer.as_bytes())?; + self.stream.write_all(data)?; + Ok(()) + } + + fn write_http_error(&mut self, code: u16, phrase: &str) -> Result<()> { + self.write_http_response(code, phrase, "text/plain", phrase.as_bytes()) + } + + fn write_http_ok(&mut self, content_type: &str, data: &[u8]) -> Result<()> { + self.write_http_response(200, "OK", content_type, data) + } + + fn handle_websocket_connection(&mut self) -> Result<()> { + let websocket = Websocket::new(&self.stream)?; + let mut payload: Vec = Vec::new(); + loop { + let op = websocket.read_websocket_message(&mut payload)?; + if let WebsocketOp::Close = op { + return Ok(()); + } + let text = String::from_utf8_lossy(&payload); + eprintln!("Received: {text}"); + + let message = match self.context.parse_json(&text, "") { + Ok(v) => v, + Err(e) => { + eprintln!("error parsing json {e}"); + websocket.send_websocket_close(1007, "invalid json")?; + return Err("invalid json".into()); + } + }; + + let message = + DebuggerMessage::from_message(&self.context, &message, websocket.clone())?; + let _ = self.send.send(message); + } + } +} + +// ============================================================================ +// Dumb Websocket stuff +// ============================================================================ +#[derive(Clone)] +struct Websocket { + // TODO: Atomic close + send: Arc>, + receive: Arc>, +} + +impl Websocket { + pub fn new(stream: &TcpStream) -> Result { + let send = stream.try_clone()?; + let receive = stream.try_clone()?; + Ok(Websocket { + send: Arc::new(Mutex::new(send)), + receive: Arc::new(Mutex::new(receive)), + }) + } + + pub fn read_websocket_message(&self, payload: &mut Vec) -> Result { + // BLARG: https://datatracker.ietf.org/doc/html/rfc6455 + payload.clear(); + + let mut stream = self.receive.lock().unwrap(); + let mut opcode: WebsocketOp = WebsocketOp::Continue; + loop { + let frame = WebsocketFrameHeader::read(&mut stream)?; + eprintln!("FRAME: {:?}", frame); + let mask = match frame.mask { + Some(m) => m, + None => { + eprintln!("Client sent unmasked frame"); + self.send_websocket_close(1002, "client sent unmasked frame")?; + return Err(DebuggerError::WebsocketError); + } + }; + + // OK what are we dealing with here? Check the opcode before we decide + // where to read the data. + if frame.opcode.is_control() { + // This is a control frame, handle it on its own. + if frame.length > 125 { + eprintln!("Control frame too big ({})", frame.length); + self.send_websocket_close(1002, "control frame too big")?; + return Err(DebuggerError::WebsocketError); + } + let mut buffer: [u8; 125] = [0; 125]; + stream.read_exact(&mut buffer[0..frame.length])?; + + if frame.opcode == WebsocketOp::Close { + self.send_websocket_message(frame.opcode, &buffer[..frame.length])?; + return Ok(WebsocketOp::Close); + } else if frame.opcode == WebsocketOp::Ping { + self.send_websocket_message(WebsocketOp::Pong, &buffer[..frame.length])?; + } else if frame.opcode == WebsocketOp::Pong { + // got a pong + } + } else { + // This is a user data frame, it goes into `payload`. + let last_tail = payload.len(); + if last_tail + frame.length > 1 * 1024 * 1024 { + eprintln!("Message too big ({})", frame.length); + self.send_websocket_close(1009, "message too big")?; + return Err(DebuggerError::WebsocketError); + } + payload.resize(last_tail + frame.length, 0); + let slice = payload.as_mut_slice(); + let mut slice = &mut slice[last_tail..]; + // eprintln!("Reading {} bytes", slice.len()); + stream.read_exact(&mut slice)?; + for i in 0..frame.length { + slice[i] = slice[i] ^ mask[i % 4]; + } + // eprintln!("Done"); + + if opcode == WebsocketOp::Continue { + opcode = frame.opcode; + } + + if frame.fin { + // Dispatch. + // eprintln!("Dispatching: {:?} {}", opcode, payload.len()); + return Ok(opcode); + } + } + } + } + + pub fn send_websocket_message(&self, opcode: WebsocketOp, payload: &[u8]) -> Result<()> { + let mut stream = self.send.lock().unwrap(); + WebsocketFrameHeader { + fin: true, + opcode, + length: payload.len(), + mask: None, + } + .write(&mut stream)?; + stream.write_all(payload)?; + Ok(()) + } + + pub fn send_websocket_close(&self, close_code: u16, reason: &str) -> Result<()> { + if reason.len() > 123 { + return Err("reason doesn't fit in close packet".into()); + } + + let mut body: [u8; 125] = [0; 125]; + let mut dst = &mut body[..]; + dst.put_u16(close_code); + dst.put_slice(reason.as_bytes()); + let remaining = dst.remaining_mut(); + + let body_len = body.len() - remaining; + self.send_websocket_message(WebsocketOp::Close, &body[0..body_len]) + } + + pub fn send_json_event( + &self, + context: &ContextRef, + event: &str, + params: &ValueRef, + ) -> Result<()> { + self.send_json_message( + &context, + context + .new_object_props([ + ("method", context.new_string(event)?.as_ref()), + ("params", params), + ])? + .as_ref(), + ) + } + + pub fn send_json_error( + &self, + context: &ContextRef, + id: &ValueRef, + code: i32, + msg: &str, + ) -> Result<()> { + self.send_json_message( + &context, + context + .new_object_props([ + ("id", id), + ( + "error", + context + .new_object_props([ + ("code", context.new_i32(code)?), + ("message", context.new_string(msg)?), + ])? + .as_ref(), + ), + ])? + .as_ref(), + ) + } + + pub fn send_json_response( + &self, + context: &ContextRef, + id: &ValueRef, + result: &ValueRef, + ) -> Result<()> { + let response = context.new_object_props([("id", id), ("result", result)])?; + self.send_json_message(&context, &response) + } + + pub fn send_json_message(&self, context: &ContextRef, value: &ValueRef) -> Result<()> { + let txt = context.json_stringify(&value)?.to_string(&context)?; + eprintln!("Send: {txt}"); + self.send_websocket_message(WebsocketOp::Text, txt.as_bytes()) + } +} + +#[derive(Debug)] +struct WebsocketFrameHeader { + pub fin: bool, + pub opcode: WebsocketOp, + pub length: usize, + pub mask: Option<[u8; 4]>, +} + +impl WebsocketFrameHeader { + fn read(stream: &mut TcpStream) -> Result { + let mut buffer: [u8; 2] = [0, 0]; + stream.read_exact(&mut buffer)?; + if (buffer[0] & 0x70) != 0 { + eprintln!("Frame has non-zero RSV* ({:x})", buffer[0]); + return Err("unknown extensions".into()); + } + let has_mask = (buffer[1] & 0x80) != 0; + + let fin = (buffer[0] & 0x80) != 0; + let opcode = buffer[0] & 0x0F; + let mut length: u64 = (buffer[1] & !0x80).into(); + // eprintln!("Decoded length {} from {}", length, buffer[1]); + if length == 126 { + let mut len_buffer: [u8; 2] = [0, 0]; + stream.read_exact(&mut len_buffer)?; + length = u16::from_be_bytes(len_buffer).into(); + } else if length == 127 { + let mut len_buffer: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; + stream.read_exact(&mut len_buffer)?; + length = u64::from_be_bytes(len_buffer); + } + if length > 1 * 1024 * 1024 { + eprintln!("Frame too big ({length})"); + return Err(io::Error::new(io::ErrorKind::InvalidData, "frame too big").into()); + } + + let mask = if has_mask { + let mut mask: [u8; 4] = [0, 0, 0, 0]; + stream.read_exact(&mut mask)?; + Some(mask) + } else { + None + }; + + let length: usize = length.try_into().unwrap(); + Ok(WebsocketFrameHeader { + fin, + opcode: opcode.try_into()?, + length, + mask, + }) + } + + fn write(&self, stream: &mut TcpStream) -> Result<()> { + let mut header: [u8; 16] = [0; 16]; + let mut dst = &mut header[..]; + + let byte: u8 = self.opcode.into(); + dst.put_u8(byte | if self.fin { 0x80 } else { 0x00 }); + + let mask = if self.mask.is_some() { 0x80 } else { 0x00 }; + if self.length < 126 { + let len: u8 = self.length.try_into().unwrap(); + dst.put_u8(len | mask); + } else if self.length <= u16::max_value().into() { + dst.put_u8(126 | mask); + dst.put_u16(self.length.try_into().unwrap()); + } else { + dst.put_u8(127 | mask); + dst.put_u64(self.length.try_into().unwrap()); + } + if let Some(bytes) = self.mask { + dst.put_slice(&bytes); + } + let remaining = dst.remaining_mut(); + + let header_len = header.len() - remaining; + stream.write_all(&header[0..header_len])?; + Ok(()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +enum WebsocketOp { + Continue, + Text, + Binary, + Close, + Ping, + Pong, +} + +impl WebsocketOp { + fn is_control(&self) -> bool { + let byte: u8 = (*self).into(); + byte & 0x08 != 0 + } +} + +impl TryFrom for WebsocketOp { + type Error = DebuggerError; + + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(WebsocketOp::Continue), + 0x1 => Ok(WebsocketOp::Text), + 0x2 => Ok(WebsocketOp::Binary), + 0x8 => Ok(WebsocketOp::Close), + 0x9 => Ok(WebsocketOp::Ping), + 0xA => Ok(WebsocketOp::Pong), + _ => Err(format!("unknown opcode {value}").into()), + } + } +} + +impl From for u8 { + fn from(value: WebsocketOp) -> Self { + match value { + WebsocketOp::Continue => 0x0, + WebsocketOp::Text => 0x1, + WebsocketOp::Binary => 0x2, + WebsocketOp::Close => 0x8, + WebsocketOp::Ping => 0x9, + WebsocketOp::Pong => 0xA, + } + } +} + +#[derive(Debug, Error)] +pub enum DebuggerError { + #[error("an unknown error has occurred: {0}")] + MiscError(String), + #[error("a javascript error has occurred: {0}")] + JsError(oden_js::Error), + #[error("an io error has occurred: {0}")] + IoError(io::Error), + #[error("the websocket encountered a protocol error")] + WebsocketError, +} + +type Result = core::result::Result; + +impl From<&str> for DebuggerError { + fn from(value: &str) -> Self { + DebuggerError::MiscError(value.to_string()) + } +} + +impl From for DebuggerError { + fn from(value: String) -> Self { + DebuggerError::MiscError(value) + } +} + +impl From for DebuggerError { + fn from(value: oden_js::Error) -> Self { + DebuggerError::JsError(value) + } +} + +impl From for DebuggerError { + fn from(value: io::Error) -> Self { + DebuggerError::IoError(value) + } +}