From 743a1cc623f696af58e35be671410e6df5e28919 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 24 Sep 2023 11:24:28 -0700 Subject: [PATCH 1/2] [quickjs] More debugger work --- oden-js-sys/quickjs/quickjs.c | 55 ++++++++++++++++++++++++++++------- oden-js-sys/quickjs/quickjs.h | 14 +++++++-- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/oden-js-sys/quickjs/quickjs.c b/oden-js-sys/quickjs/quickjs.c index f7033f31..505f27dd 100644 --- a/oden-js-sys/quickjs/quickjs.c +++ b/oden-js-sys/quickjs/quickjs.c @@ -1018,6 +1018,11 @@ typedef struct JSBreakpoint { 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); @@ -6503,6 +6508,7 @@ static void get_frame_debug_position(JSContext *ctx, JSStackFrame *sf, *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; @@ -6510,7 +6516,6 @@ static void get_frame_debug_position(JSContext *ctx, JSStackFrame *sf, b = p->u.func.function_bytecode; *backtrace_barrier = b->backtrace_barrier; - *function = JS_AtomToCString(ctx, b->func_name); if (b->has_debug) { line_num1 = find_line_num(ctx, b, sf->cur_pc - b->byte_code_buf - 1); @@ -33775,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 { @@ -54147,6 +54170,12 @@ void JS_SetSourceMapFunc(JSRuntime *rt, JSMapSourceFunc *map_func, void *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; @@ -54199,7 +54228,8 @@ static BOOL find_pc_offset(JSFunctionBytecode *b, int target_line, int *break_pc return FALSE; } -static JSBreakpoint *find_existing_breakpoint(JSContext *ctx, JSFunctionBytecode *func, int pc) { +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); @@ -54210,12 +54240,8 @@ static JSBreakpoint *find_existing_breakpoint(JSContext *ctx, JSFunctionBytecode return NULL; } -typedef struct JSDebugContext { - JSContext *ctx; - JSStackFrame *sf; -} JSDebugContext; - -static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_offset) { +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; @@ -54227,7 +54253,11 @@ static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_ dctx.ctx = ctx; dctx.sf = ctx->rt->current_stack_frame; - /* resume = */rt->debug_callback(&dctx, JS_BREAK_BREAKPOINT, bp, rt->debug_opaque); + 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; @@ -54241,8 +54271,11 @@ static int js_handle_breakpoint(JSContext *ctx, JSFunctionBytecode *b, int code_ JSDebugContext dctx; dctx.ctx = ctx; dctx.sf = ctx->rt->current_stack_frame; + + JSDebugEvent evt; + evt.reason = JS_BREAK_STEP; - /* resume = */rt->debug_callback(&dctx, JS_BREAK_STEP, NULL, rt->debug_opaque); + /* resume = */rt->debug_callback(&dctx, &evt, rt->debug_opaque); } } @@ -54271,7 +54304,7 @@ void JS_DebugGetFrameSourcePosition(JSDebugContext *dctx, JSDebugFrame *frame, JSStackFrame *sf = (JSStackFrame *)frame; JSContext *ctx = dctx->ctx; BOOL backtrace_barrier; - + get_frame_debug_position(ctx, sf, function, file, line, &backtrace_barrier); } diff --git a/oden-js-sys/quickjs/quickjs.h b/oden-js-sys/quickjs/quickjs.h index 6ca6cec7..7d9faf31 100644 --- a/oden-js-sys/quickjs/quickjs.h +++ b/oden-js-sys/quickjs/quickjs.h @@ -1070,15 +1070,25 @@ typedef enum JSResumeMode { } 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, - JSBreakReason reason, - JSBreakpoint *breakpoint, + JSDebugEvent *event, void *opaque); void JS_SetDebugCallbackFunc(JSRuntime *rt, JSDebugCallbackFunc *bp_func, void *opqaue); From c990de5ad653d86e57b0057d98490f73572949fc Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 24 Sep 2023 11:24:58 -0700 Subject: [PATCH 2/2] [oden] Start on a debugger interface compatible with chrome This is trying to implement the chrome devtools protocol as documented here: https://chromedevtools.github.io/devtools-protocol/v8/ Doing it in rust and hoping to meet in the middle in quickjs, because I don't like how many decisions I need to make building a stand-alone quickjs debugger. --- src/lib.rs | 2 + src/script.rs | 1 + src/script/debugger.rs | 139 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/script/debugger.rs 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..4f5a5c3a --- /dev/null +++ b/src/script/debugger.rs @@ -0,0 +1,139 @@ +use std::collections::HashMap; +use std::io; +use std::io::{BufRead, BufReader, Write}; +use std::net::{TcpListener, TcpStream}; +use std::thread; + +pub struct Debugger { + _thread: thread::JoinHandle<()>, +} + +fn write_http_response( + stream: &mut TcpStream, + code: u16, + phrase: &str, + content_type: &str, + data: &[u8], +) -> io::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" + ); + stream.write_all(buffer.as_bytes())?; + stream.write_all(data) +} + +fn write_http_error(stream: &mut TcpStream, code: u16, phrase: &str) -> io::Result<()> { + write_http_response(stream, code, phrase, "text/plain", phrase.as_bytes()) +} + +fn write_http_ok(stream: &mut TcpStream, content_type: &str, data: &[u8]) -> io::Result<()> { + write_http_response(stream, 200, "OK", content_type, data) +} + +fn handle_connection(mut stream: TcpStream) -> io::Result<()> { + let mut buf_reader = BufReader::new(&mut stream); + + let mut buffer = String::new(); + buf_reader.read_line(&mut buffer)?; + + let parts: Vec<_> = buffer.trim().split(" ").collect(); + if parts.len() != 3 { + eprintln!("Invalid request line: {buffer}"); + write_http_error(&mut stream, 400, "Invalid request")?; + return Ok(()); + } + + 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 => { + write_http_error(&mut stream, 400, "Invalid request")?; + return Ok(()); + } + }; + let header = &header_line[0..sep_idx]; + let value = &header_line[sep_idx + 1..]; + eprintln!("HEADER: {header} {value}"); + headers.insert(header.trim().to_string(), 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. + write_http_ok( + &mut stream, + "application/json", + "{\"Browser\": \"Oden/0.0.1\", \"Protocol-Version\": \"1.3\"}".as_bytes(), + )?; + } else if path == "/json" || path == "/json/list" { + // [ + // { + // "description": "deno", + // "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/ws/01b9bd23-8810-43ed-86f7-5feef5d120fa&experiments=true&v8only=true", + // "faviconUrl": "https://deno.land/favicon.ico", + // "id": "01b9bd23-8810-43ed-86f7-5feef5d120fa", + // "title": "deno - main [pid: 40483]", + // "type": "node", + // "url": "file:///Users/doty/src/ioniq/status.ts", + // "webSocketDebuggerUrl": "ws://127.0.0.1:9229/ws/01b9bd23-8810-43ed-86f7-5feef5d120fa" + // } + // ] + } 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. + } else { + write_http_error(&mut stream, 404, "Not Found")?; + } + } else { + write_http_error(&mut stream, 404, "Not Found")?; + } + Ok(()) +} + +pub fn debugger_listener() -> io::Result<()> { + let listener = TcpListener::bind("127.0.0.1:0")?; + 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; + } + }; + + // TODO: Thread pool extraction here. + if let Err(e) = handle_connection(stream) { + eprintln!("Error handling incoming connection: {e}"); + } + } + Ok(()) +} + +#[allow(dead_code)] +pub fn start_debugger() -> Debugger { + let thread = thread::spawn(move || { + if let Err(e) = debugger_listener() { + eprintln!("ERROR STARTING DEBUGGER: {:?}", e); + } + }); + Debugger { _thread: thread } +}