diff --git a/include/inspector.h b/include/inspector.h new file mode 100644 index 0000000..411202a --- /dev/null +++ b/include/inspector.h @@ -0,0 +1,119 @@ +#ifndef ANT_INSPECTOR_H +#define ANT_INSPECTOR_H + +#include "types.h" +#include "modules/http.h" +#include +#include +#include + +typedef struct { + bool enabled; + bool wait_for_session; + char host[64]; + int port; +} ant_inspector_options_t; + +bool ant_inspector_start( + ant_t *js, + const ant_inspector_options_t *options +); + +void ant_inspector_register_script_source( + const char *path, + const char *source, + size_t len, + bool is_module +); + +void ant_inspector_register_script_file( + const char *path, + bool is_module +); + +void ant_inspector_console_api_called( + ant_t *js, const char *level, + ant_value_t *args, int nargs +); + +uint64_t ant_inspector_network_request( + const char *method, + const char *url, + const char *type, + const char *initiator, + bool has_post_data, + const ant_http_header_t *headers +); + +void ant_inspector_network_response( + uint64_t request_id, + const char *url, + int status, + const char *status_text, + const char *mime_type, + const char *type, + const ant_http_header_t *headers +); + +void ant_inspector_network_finish( + uint64_t request_id, + size_t encoded_data_length +); + +void ant_inspector_network_fail( + uint64_t request_id, + const char *error_text, + bool canceled, + const char *type +); + +void ant_inspector_network_set_request_body( + uint64_t request_id, + const uint8_t *data, + size_t len +); + +void ant_inspector_network_append_response_body( + uint64_t request_id, + const uint8_t *data, + size_t len +); + +void ant_inspector_websocket_request( + uint64_t request_id, + const ant_http_header_t *headers +); + +void ant_inspector_websocket_response( + uint64_t request_id, + int status, + const char *status_text, + const ant_http_header_t *headers +); + +void ant_inspector_websocket_frame_sent( + uint64_t request_id, + const uint8_t *data, + size_t len, + bool binary +); + +void ant_inspector_websocket_frame_received( + uint64_t request_id, + const uint8_t *data, + size_t len, + bool binary +); + +void ant_inspector_websocket_error( + uint64_t request_id, + const char *message +); + +void ant_inspector_stop(void); +void ant_inspector_wait_for_session(void); + +uint64_t ant_inspector_websocket_created(const char *url); +void ant_inspector_websocket_closed(uint64_t request_id); + +#endif diff --git a/include/modules/crypto.h b/include/modules/crypto.h index 25fd7f0..7a911c6 100644 --- a/include/modules/crypto.h +++ b/include/modules/crypto.h @@ -5,8 +5,9 @@ #include "types.h" void init_crypto_module(void); -int crypto_fill_random(void *buf, size_t len); - ant_value_t crypto_library(ant_t *js); +int crypto_fill_random(void *buf, size_t len); +int crypto_random_uuid(char out[37]); + #endif diff --git a/sources.json b/sources.json index 67e1b2b..871cc8c 100644 --- a/sources.json +++ b/sources.json @@ -7,6 +7,7 @@ "src/highlight/*.c", "src/esm/*.c", "src/cli/*.c", + "src/inspector/*.c", "src/modules/*.c", "src/streams/*.c", "src/silver/*.c", @@ -21,6 +22,7 @@ "src/gc/*.c", "src/highlight/*.c", "src/esm/*.c", + "src/inspector/*.c", "src/modules/*.c", "src/streams/*.c", "src/silver/*.c", diff --git a/src/inspector/bind.h b/src/inspector/bind.h new file mode 100644 index 0000000..2fa37c2 --- /dev/null +++ b/src/inspector/bind.h @@ -0,0 +1,147 @@ +#ifndef ANT_INSPECTOR_BIND_H +#define ANT_INSPECTOR_BIND_H + +#include "types.h" +#include +#include +#include +#include +#include + +typedef struct inspector_client inspector_client_t; +typedef struct inspector_script inspector_script_t; + +typedef struct inspector_object_handle inspector_object_handle_t; +typedef struct inspector_network_entry inspector_network_entry_t; +typedef struct inspector_console_event inspector_console_event_t; + +typedef struct { + char *data; + size_t len; + size_t cap; +} sbuf_t; + +struct inspector_client { + uv_tcp_t handle; + ant_t *js; + bool websocket; + bool runtime_enabled; + bool console_enabled; + bool console_replayed; + bool debugger_enabled; + bool network_enabled; + char *read_buf; + size_t read_len; + size_t read_cap; + inspector_client_t *next; +}; + +struct inspector_object_handle { + uint32_t id; + ant_value_t value; + inspector_object_handle_t *next; +}; + +struct inspector_network_entry { + uint64_t id; + uint8_t *request_body; + size_t request_body_len; + bool request_body_truncated; + uint8_t *response_body; + size_t response_body_len; + bool response_body_truncated; + inspector_network_entry_t *next; +}; + +struct inspector_console_event { + char *json; + inspector_console_event_t *next; +}; + +struct inspector_script { + int id; + char *url; + char *source; + size_t source_len; + bool is_module; + inspector_script_t *next; +}; + +typedef struct { + ant_t *js; + uv_tcp_t server; + bool started; + bool attached; + bool waiting_for_debugger; + char host[64]; + int port; + char uuid[37]; + inspector_client_t *clients; + inspector_object_handle_t *object_handles; + inspector_network_entry_t *network_entries; + inspector_console_event_t *console_events_head; + inspector_console_event_t *console_events_tail; + inspector_script_t *scripts; + size_t console_event_count; + uint32_t next_object_id; + uint64_t next_network_request_id; + int next_script_id; + int entry_script_id; +} inspector_state_t; + +typedef struct { + uv_write_t req; + uv_buf_t buf; +} inspector_write_t; + +extern inspector_state_t g_inspector; +extern const size_t k_inspector_network_body_limit; +extern const size_t k_inspector_console_event_limit; + +bool sbuf_append_len(sbuf_t *b, const char *s, size_t len); +bool sbuf_append(sbuf_t *b, const char *s); +bool sbuf_appendf(sbuf_t *b, const char *fmt, ...) __attribute__((format(printf, 2, 3))); + +bool sbuf_json_string_len(sbuf_t *b, const char *s, size_t len); +bool sbuf_json_string(sbuf_t *b, const char *s); + +void inspector_send_raw(inspector_client_t *client, const char *data, size_t len); +void inspector_send_ws(inspector_client_t *client, const char *json); +void inspector_send_response_obj(inspector_client_t *client, int id, const char *result_obj); +void inspector_send_empty_result(inspector_client_t *client, int id); +void inspector_send_error(inspector_client_t *client, int id, int code, const char *message); +bool inspector_param_bool(yyjson_val *params, const char *name); + +double inspector_timestamp_seconds(void); +double inspector_wall_time_seconds(void); + +bool inspector_is_remote_handle_value(ant_value_t value); +bool inspector_value_to_remote_object(ant_t *js, ant_value_t value, sbuf_t *out); +void inspector_send_execution_context(inspector_client_t *client); +void inspector_eval(inspector_client_t *client, int id, yyjson_val *params); +void inspector_get_heap_usage(inspector_client_t *client, int id); +void inspector_get_properties(inspector_client_t *client, int id, yyjson_val *params); +void inspector_global_lexical_scope_names(inspector_client_t *client, int id); +void inspector_clear_console_events(void); +void inspector_replay_console_events(inspector_client_t *client); +void inspector_clear_exception_state(ant_t *js); +void inspector_send_eval_result(inspector_client_t *client, int id, ant_value_t result); + +bool inspector_is_url_like(const char *path); +char *inspector_make_script_url(const char *path); + +inspector_script_t *inspector_script_for_id(int id); +inspector_script_t *inspector_script_for_url(const char *url); +inspector_script_t *inspector_entry_script(void); + +void inspector_send_registered_scripts(inspector_client_t *client); +bool inspector_append_call_location(ant_t *js, sbuf_t *b); +void inspector_compile_script(inspector_client_t *client, int id, yyjson_val *params); +void inspector_run_script(inspector_client_t *client, int id, yyjson_val *params); +void inspector_get_script_source(inspector_client_t *client, int id, yyjson_val *params); + +void inspector_get_response_body(inspector_client_t *client, int id, yyjson_val *params); +void inspector_get_request_post_data(inspector_client_t *client, int id, yyjson_val *params); +void inspector_handle_message(inspector_client_t *client, const char *payload, size_t len); + +#endif diff --git a/src/inspector/core.c b/src/inspector/core.c new file mode 100644 index 0000000..002ecf8 --- /dev/null +++ b/src/inspector/core.c @@ -0,0 +1,184 @@ +#include "bind.h" +#include "http/websocket.h" + +#include +#include +#include +#include +#include + +inspector_state_t g_inspector = {0}; + +const size_t k_inspector_network_body_limit = 10 * 1024 * 1024; +const size_t k_inspector_console_event_limit = 200; + +static bool sbuf_reserve(sbuf_t *b, size_t extra) { + if (extra > SIZE_MAX - b->len - 1) return false; + size_t need = b->len + extra + 1; + + if (need <= b->cap) return true; + size_t cap = b->cap ? b->cap * 2 : 256; + + while (cap < need) { + if (cap > SIZE_MAX / 2) return false; + cap *= 2; + } + + char *next = realloc(b->data, cap); + if (!next) return false; + + b->data = next; + b->cap = cap; + + return true; +} + +bool sbuf_append_len(sbuf_t *b, const char *s, size_t len) { + if (!s) return true; + if (!sbuf_reserve(b, len)) return false; + + memcpy(b->data + b->len, s, len); + b->len += len; + b->data[b->len] = '\0'; + + return true; +} + +bool sbuf_append(sbuf_t *b, const char *s) { + return sbuf_append_len(b, s, s ? strlen(s) : 0); +} + +static bool sbuf_vappendf(sbuf_t *b, const char *fmt, va_list ap) { + va_list cp; + va_copy(cp, ap); +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-nonliteral" +#endif + int n = vsnprintf(NULL, 0, fmt, cp); +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + va_end(cp); + if (n < 0) return false; + if (!sbuf_reserve(b, (size_t)n)) { + return false; + } +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-nonliteral" +#endif + vsnprintf(b->data + b->len, b->cap - b->len, fmt, ap); +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + b->len += (size_t)n; + return true; +} + +#if defined(__GNUC__) || defined(__clang__) +bool sbuf_appendf(sbuf_t *b, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +#endif +bool sbuf_appendf(sbuf_t *b, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + bool ok = sbuf_vappendf(b, fmt, ap); + va_end(ap); + return ok; +} + +bool sbuf_json_string_len(sbuf_t *b, const char *s, size_t len) { + if (!sbuf_append(b, "\"")) return false; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '"': if (!sbuf_append(b, "\\\"")) return false; break; + case '\\': if (!sbuf_append(b, "\\\\")) return false; break; + case '\b': if (!sbuf_append(b, "\\b")) return false; break; + case '\f': if (!sbuf_append(b, "\\f")) return false; break; + case '\n': if (!sbuf_append(b, "\\n")) return false; break; + case '\r': if (!sbuf_append(b, "\\r")) return false; break; + case '\t': if (!sbuf_append(b, "\\t")) return false; break; + default: + if (c < 0x20) { + if (!sbuf_appendf(b, "\\u%04x", (unsigned)c)) return false; + } else if (!sbuf_append_len(b, (const char *)&c, 1)) return false; + break; + } + } + return sbuf_append(b, "\""); +} + +bool sbuf_json_string(sbuf_t *b, const char *s) { + return sbuf_json_string_len(b, s ? s : "", s ? strlen(s) : 0); +} + +static void inspector_write_free(uv_write_t *req, int status) { + inspector_write_t *wr = (inspector_write_t *)req; + free(wr->buf.base); + free(wr); +} + +void inspector_send_raw(inspector_client_t *client, const char *data, size_t len) { + if (!client || !data) return; + inspector_write_t *wr = calloc(1, sizeof(*wr)); + + if (!wr) return; + wr->buf.base = malloc(len); + + if (!wr->buf.base) { + free(wr); + return; + } + + memcpy(wr->buf.base, data, len); + wr->buf.len = len; + + if (uv_write(&wr->req, (uv_stream_t *)&client->handle, &wr->buf, 1, inspector_write_free) != 0) { + free(wr->buf.base); + free(wr); + } +} + +void inspector_send_ws(inspector_client_t *client, const char *json) { + if (!client || !client->websocket || !json) return; + size_t frame_len = 0; + uint8_t *frame = ant_ws_encode_frame(ANT_WS_OPCODE_TEXT, (const uint8_t *)json, strlen(json), false, &frame_len); + if (!frame) return; + inspector_send_raw(client, (const char *)frame, frame_len); + free(frame); +} + +void inspector_send_response_obj(inspector_client_t *client, int id, const char *result_obj) { + sbuf_t b = {0}; + if (sbuf_appendf(&b, "{\"id\":%d,\"result\":%s}", id, result_obj ? result_obj : "{}")) + inspector_send_ws(client, b.data); + free(b.data); +} + +void inspector_send_empty_result(inspector_client_t *client, int id) { + inspector_send_response_obj(client, id, "{}"); +} + +void inspector_send_error(inspector_client_t *client, int id, int code, const char *message) { + sbuf_t b = {0}; + if ( + sbuf_appendf(&b, "{\"id\":%d,\"error\":{\"code\":%d,\"message\":", id, code) && + sbuf_json_string(&b, message ? message : "Inspector error") && + sbuf_append(&b, "}}") + ) inspector_send_ws(client, b.data); + free(b.data); +} + +bool inspector_param_bool(yyjson_val *params, const char *name) { + yyjson_val *value = params ? yyjson_obj_get(params, name) : NULL; + return value && yyjson_is_bool(value) && yyjson_get_bool(value); +} + +double inspector_timestamp_seconds(void) { + return (double)uv_hrtime() / 1000000000.0; +} + +double inspector_wall_time_seconds(void) { + return (double)time(NULL); +} diff --git a/src/inspector/debugger.c b/src/inspector/debugger.c new file mode 100644 index 0000000..75deefe --- /dev/null +++ b/src/inspector/debugger.c @@ -0,0 +1,291 @@ +#include "ant.h" +#include "bind.h" +#include "reactor.h" +#include "internal.h" + +#include +#include +#include + +static bool inspector_source_end_position(const char *source, size_t len, int *line, int *column) { + int cur_line = 0; + int cur_column = 0; + + for (size_t i = 0; i < len; i++) { + if (source[i] == '\n') { + cur_line++; + cur_column = 0; + } else cur_column++; + } + + if (line) *line = cur_line; + if (column) *column = cur_column; + + return true; +} + +bool inspector_is_url_like(const char *path) { + if (!path) return false; + for (const char *p = path; *p; p++) { + if (*p == ':') return p[1] == '/' && p[2] == '/'; + if (*p == '/' || *p == '[') return false; + } + return false; +} + +char *inspector_make_script_url(const char *path) { + if (!path || !*path) return strdup("ant://inspector/script"); + if (strcmp(path, "[eval]") == 0) return strdup("ant://inspector/eval"); + if (strcmp(path, "[stdin]") == 0) return strdup("ant://inspector/stdin"); + if (strcmp(path, "[inspector]") == 0) return strdup("ant://inspector/console"); + if (inspector_is_url_like(path)) return strdup(path); + + sbuf_t b = {0}; + if (path[0] == '/') { + if (!sbuf_append(&b, "file://") || !sbuf_append(&b, path)) { + free(b.data); + return NULL; + } + return b.data; + } + + char *real = realpath(path, NULL); + if (real) { + bool ok = sbuf_append(&b, "file://") && sbuf_append(&b, real); + free(real); + if (!ok) { + free(b.data); + return NULL; + } + return b.data; + } + + if (!sbuf_append(&b, "file:///") || !sbuf_append(&b, path)) { + free(b.data); + return NULL; + } + return b.data; +} + +inspector_script_t *inspector_script_for_id(int id) { + if (id <= 0) return NULL; + for (inspector_script_t *script = g_inspector.scripts; script; script = script->next) { + if (script->id == id) return script; + } + return NULL; +} + +inspector_script_t *inspector_script_for_url(const char *url) { + if (!url) return NULL; + for (inspector_script_t *script = g_inspector.scripts; script; script = script->next) { + if (script->url && strcmp(script->url, url) == 0) return script; + } + return NULL; +} + +inspector_script_t *inspector_entry_script(void) { + inspector_script_t *script = inspector_script_for_id(g_inspector.entry_script_id); + if (script) return script; + return g_inspector.scripts; +} + +static bool inspector_send_script_parsed(inspector_client_t *client, const inspector_script_t *script) { + if (!client || !script) return false; + int end_line = 0; + int end_column = 0; + inspector_source_end_position(script->source, script->source_len, &end_line, &end_column); + + sbuf_t b = {0}; + bool ok = + sbuf_append(&b, "{\"method\":\"Debugger.scriptParsed\",\"params\":{\"scriptId\":\"") && + sbuf_appendf(&b, "%d", script->id) && + sbuf_append(&b, "\",\"url\":") && + sbuf_json_string(&b, script->url ? script->url : "") && + sbuf_append(&b, ",\"startLine\":0,\"startColumn\":0,\"endLine\":") && + sbuf_appendf(&b, "%d", end_line) && + sbuf_append(&b, ",\"endColumn\":") && + sbuf_appendf(&b, "%d", end_column) && + sbuf_append(&b, ",\"executionContextId\":1,\"hash\":\"\",\"isLiveEdit\":false,\"sourceMapURL\":\"\",\"hasSourceURL\":false,\"isModule\":") && + sbuf_append(&b, script->is_module ? "true" : "false") && + sbuf_append(&b, ",\"length\":") && + sbuf_appendf(&b, "%zu", script->source_len) && + sbuf_append(&b, "}}"); + if (ok) inspector_send_ws(client, b.data); + free(b.data); + return ok; +} + +static void inspector_notify_script_parsed(const inspector_script_t *script) { + for (inspector_client_t *client = g_inspector.clients; client; client = client->next) + if (client->debugger_enabled) inspector_send_script_parsed(client, script); +} + +static inspector_script_t *inspector_register_script_source( + const char *url, + const char *source, + size_t len, + bool is_module, + bool notify +) { + if (!url) return NULL; + + char *source_copy = malloc(len + 1); + if (!source_copy) return NULL; + if (source && len > 0) memcpy(source_copy, source, len); + source_copy[len] = '\0'; + + inspector_script_t *script = inspector_script_for_url(url); + if (script) { + free(script->source); + script->source = source_copy; + script->source_len = len; + script->is_module = is_module; + } else { + script = calloc(1, sizeof(*script)); + if (!script) { + free(source_copy); + return NULL; + } + script->url = strdup(url); + if (!script->url) { + free(source_copy); + free(script); + return NULL; + } + script->source = source_copy; + script->source_len = len; + script->is_module = is_module; + script->id = ++g_inspector.next_script_id; + if (script->id <= 0) script->id = ++g_inspector.next_script_id; + if (g_inspector.entry_script_id == 0) g_inspector.entry_script_id = script->id; + script->next = g_inspector.scripts; + g_inspector.scripts = script; + } + + if (notify) inspector_notify_script_parsed(script); + return script; +} + +void inspector_send_registered_scripts(inspector_client_t *client) { + for (inspector_script_t *script = g_inspector.scripts; script; script = script->next) + inspector_send_script_parsed(client, script); +} + +static void inspector_eval_source(inspector_client_t *client, int id, const char *source, size_t source_len) { + const char *prev_filename = client->js->filename; + inspector_clear_exception_state(client->js); + js_set_filename(client->js, "[inspector]"); + ant_value_t result = js_eval_bytecode_repl(client->js, source ? source : "", source_len); + js_reactor_pump_repl_nowait(client->js); + js_set_filename(client->js, prev_filename); + inspector_send_eval_result(client, id, result); +} + +void inspector_compile_script(inspector_client_t *client, int id, yyjson_val *params) { + yyjson_val *expr_val = params ? yyjson_obj_get(params, "expression") : NULL; + if (!expr_val || !yyjson_is_str(expr_val)) { + inspector_send_error(client, id, -32602, "Runtime.compileScript requires expression"); + return; + } + + const char *expr = yyjson_get_str(expr_val); + size_t expr_len = yyjson_get_len(expr_val); + yyjson_val *source_url_val = params ? yyjson_obj_get(params, "sourceURL") : NULL; + const char *source_url = source_url_val && yyjson_is_str(source_url_val) ? yyjson_get_str(source_url_val) : NULL; + + char fallback_url[96]; + snprintf(fallback_url, sizeof(fallback_url), "ant://inspector/compiled/%d", g_inspector.next_script_id + 1); + inspector_script_t *script = inspector_register_script_source( + source_url && *source_url ? source_url : fallback_url, + expr, expr_len, + false, true + ); + + if (!script) { + inspector_send_error(client, id, -32000, "Out of memory"); + return; + } + + sbuf_t b = {0}; + if (sbuf_appendf(&b, "{\"scriptId\":\"%d\"}", script->id)) + inspector_send_response_obj(client, id, b.data); + else inspector_send_error(client, id, -32000, "Out of memory"); + free(b.data); +} + +void inspector_run_script(inspector_client_t *client, int id, yyjson_val *params) { + yyjson_val *script_id_val = params ? yyjson_obj_get(params, "scriptId") : NULL; + const char *script_id = script_id_val && yyjson_is_str(script_id_val) ? yyjson_get_str(script_id_val) : NULL; + + inspector_script_t *script = script_id ? inspector_script_for_id(atoi(script_id)) : NULL; + if (!script) { + inspector_send_error(client, id, -32000, "No such script"); + return; + } + + inspector_eval_source(client, id, script->source, script->source_len); +} + +void inspector_get_script_source(inspector_client_t *client, int id, yyjson_val *params) { + yyjson_val *script_id_val = params ? yyjson_obj_get(params, "scriptId") : NULL; + const char *script_id = script_id_val && yyjson_is_str(script_id_val) ? yyjson_get_str(script_id_val) : NULL; + inspector_script_t *script = script_id ? inspector_script_for_id(atoi(script_id)) : NULL; + if (!script) { + inspector_send_error(client, id, -32000, "No such script"); + return; + } + + sbuf_t b = {0}; + if ( + sbuf_append(&b, "{\"scriptSource\":") && + sbuf_json_string_len(&b, script->source ? script->source : "", script->source ? script->source_len : 0) && + sbuf_append(&b, "}") + ) inspector_send_response_obj(client, id, b.data); + else inspector_send_error(client, id, -32000, "Out of memory"); + free(b.data); +} +void ant_inspector_register_script_source( + const char *path, + const char *source, + size_t len, + bool is_module +) { + if (!g_inspector.started) return; + char *url = inspector_make_script_url(path); + if (!url) return; + inspector_register_script_source(url, source ? source : "", source ? len : 0, is_module, true); + free(url); +} + +void ant_inspector_register_script_file(const char *path, bool is_module) { + if (!g_inspector.started || !path || !*path) return; + + FILE *fp = fopen(path, "rb"); + if (!fp) return; + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return; + } + long size = ftell(fp); + if (size < 0) { + fclose(fp); + return; + } + if (fseek(fp, 0, SEEK_SET) != 0) { + fclose(fp); + return; + } + + char *source = malloc((size_t)size + 1); + if (!source) { + fclose(fp); + return; + } + + size_t len = fread(source, 1, (size_t)size, fp); + fclose(fp); + source[len] = '\0'; + + ant_inspector_register_script_source(path, source, len, is_module); + free(source); +} diff --git a/src/inspector/network.c b/src/inspector/network.c new file mode 100644 index 0000000..0ff7bf8 --- /dev/null +++ b/src/inspector/network.c @@ -0,0 +1,492 @@ +#include "bind.h" +#include "base64.h" +#include "inspector.h" + +#include +#include +#include + +static inspector_network_entry_t *inspector_network_entry_for_id(uint64_t id, bool create) { + if (id == 0) return NULL; + for (inspector_network_entry_t *entry = g_inspector.network_entries; entry; entry = entry->next) { + if (entry->id == id) return entry; + } + if (!create) return NULL; + + inspector_network_entry_t *entry = calloc(1, sizeof(*entry)); + if (!entry) return NULL; + entry->id = id; + entry->next = g_inspector.network_entries; + g_inspector.network_entries = entry; + return entry; +} + +static void inspector_network_store_body( + uint8_t **body, + size_t *body_len, + bool *truncated, + const uint8_t *data, + size_t len, + bool append +) { + if (!body || !body_len || !truncated || !data || len == 0 || *truncated) return; + + size_t keep = len; + if (append) { + if (*body_len >= k_inspector_network_body_limit) { + *truncated = true; + return; + } + if (keep > k_inspector_network_body_limit - *body_len) { + keep = k_inspector_network_body_limit - *body_len; + *truncated = true; + } + } else if (keep > k_inspector_network_body_limit) { + keep = k_inspector_network_body_limit; + *truncated = true; + } + + if (append) { + uint8_t *next = realloc(*body, *body_len + keep); + if (!next) { + *truncated = true; + return; + } + memcpy(next + *body_len, data, keep); + *body = next; + *body_len += keep; + } else { + uint8_t *next = malloc(keep); + if (!next) { + *truncated = true; + return; + } + memcpy(next, data, keep); + free(*body); + *body = next; + *body_len = keep; + } +} + +static void inspector_notify_network(const char *json) { + for (inspector_client_t *c = g_inspector.clients; c; c = c->next) { + if (c->network_enabled) inspector_send_ws(c, json); + } +} + +static bool inspector_append_headers_object(sbuf_t *b, const ant_http_header_t *headers) { + if (!sbuf_append(b, "{")) return false; + bool first = true; + for (const ant_http_header_t *header = headers; header; header = header->next) { + if (!header->name || !header->value) continue; + if (!first && !sbuf_append(b, ",")) return false; + first = false; + if (!sbuf_json_string(b, header->name)) return false; + if (!sbuf_append(b, ":")) return false; + if (!sbuf_json_string(b, header->value)) return false; + } + return sbuf_append(b, "}"); +} + +static void inspector_notify_request_extra(uint64_t request_id, const ant_http_header_t *headers) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.requestWillBeSentExtraInfo\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"associatedCookies\":[],\"headers\":")) goto done; + if (!inspector_append_headers_object(&b, headers)) goto done; + if (!sbuf_append(&b, ",\"connectTiming\":{\"requestTime\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, "},\"siteHasCookieInOtherPartition\":false}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +static void inspector_notify_response_extra(uint64_t request_id, int status, const ant_http_header_t *headers) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.responseReceivedExtraInfo\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"blockedCookies\":[],\"headers\":")) goto done; + if (!inspector_append_headers_object(&b, headers)) goto done; + if (!sbuf_append(&b, ",\"resourceIPAddressSpace\":\"Unknown\",\"statusCode\":")) goto done; + if (!sbuf_appendf(&b, "%d", status)) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +static bool inspector_append_network_initiator(sbuf_t *b, const char *label) { + const char *name = label && *label ? label : "Fetch"; + const char *url = "Fetch"; + if (strcmp(name, "Server") == 0) url = "Server"; + else if (strcmp(name, "EventSource") == 0) url = "EventSource"; + + if (!sbuf_append(b, "{\"type\":\"parser\",\"url\":")) return false; + if (!sbuf_json_string(b, url)) return false; + return sbuf_append(b, "}"); +} + +static uint64_t inspector_request_id_param(yyjson_val *params) { + yyjson_val *request_id = params ? yyjson_obj_get(params, "requestId") : NULL; + if (!request_id || !yyjson_is_str(request_id)) return 0; + const char *s = yyjson_get_str(request_id); + if (!s || !*s) return 0; + return strtoull(s, NULL, 10); +} + +void inspector_get_response_body(inspector_client_t *client, int id, yyjson_val *params) { + uint64_t request_id = inspector_request_id_param(params); + inspector_network_entry_t *entry = inspector_network_entry_for_id(request_id, false); + sbuf_t b = {0}; + size_t out_len = 0; + char *encoded = NULL; + + if (!entry || !entry->response_body || entry->response_body_len == 0) { + inspector_send_response_obj(client, id, "{\"body\":\"\",\"base64Encoded\":false}"); + return; + } + + encoded = ant_base64_encode(entry->response_body, entry->response_body_len, &out_len); + if (!encoded) { + inspector_send_error(client, id, -32000, "Out of memory"); + return; + } + + if (!sbuf_append(&b, "{\"body\":") || + !sbuf_json_string_len(&b, encoded, out_len) || + !sbuf_append(&b, ",\"base64Encoded\":true}")) { + free(encoded); + free(b.data); + inspector_send_error(client, id, -32000, "Out of memory"); + return; + } + + free(encoded); + inspector_send_response_obj(client, id, b.data); + free(b.data); +} + +void inspector_get_request_post_data(inspector_client_t *client, int id, yyjson_val *params) { + uint64_t request_id = inspector_request_id_param(params); + inspector_network_entry_t *entry = inspector_network_entry_for_id(request_id, false); + sbuf_t b = {0}; + + if (!entry || !entry->request_body || entry->request_body_len == 0) { + inspector_send_response_obj(client, id, "{\"postData\":\"\"}"); + return; + } + + if (!sbuf_append(&b, "{\"postData\":") || + !sbuf_json_string_len(&b, (const char *)entry->request_body, entry->request_body_len) || + !sbuf_append(&b, "}")) { + free(b.data); + inspector_send_error(client, id, -32000, "Out of memory"); + return; + } + + inspector_send_response_obj(client, id, b.data); + free(b.data); +} +uint64_t ant_inspector_network_request( + const char *method, + const char *url, + const char *type, + const char *initiator, + bool has_post_data, + const ant_http_header_t *headers +) { + if (!g_inspector.started || !g_inspector.attached) return 0; + uint64_t request_id = ++g_inspector.next_network_request_id; + if (request_id == 0) request_id = ++g_inspector.next_network_request_id; + inspector_network_entry_for_id(request_id, true); + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.requestWillBeSent\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"loaderId\":\"ant\",\"documentURL\":")) goto done; + if (!sbuf_json_string(&b, url ? url : "")) goto done; + if (!sbuf_append(&b, ",\"request\":{\"url\":")) goto done; + if (!sbuf_json_string(&b, url ? url : "")) goto done; + if (!sbuf_append(&b, ",\"method\":")) goto done; + if (!sbuf_json_string(&b, method ? method : "GET")) goto done; + if (!sbuf_append(&b, ",\"headers\":")) goto done; + if (!inspector_append_headers_object(&b, headers)) goto done; + if (has_post_data) { + if (!sbuf_append(&b, ",\"hasPostData\":true")) goto done; + } + if (!sbuf_append(&b, ",\"initialPriority\":\"High\",\"referrerPolicy\":\"no-referrer\"},\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"wallTime\":")) goto done; + if (!sbuf_appendf(&b, "%.3f", inspector_wall_time_seconds())) goto done; + if (!sbuf_append(&b, ",\"initiator\":")) goto done; + if (!inspector_append_network_initiator(&b, initiator)) goto done; + if (!sbuf_append(&b, ",\"type\":")) goto done; + if (!sbuf_json_string(&b, type ? type : "Fetch")) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_notify_network(b.data); + inspector_notify_request_extra(request_id, headers); + +done: + free(b.data); + return request_id; +} + +void ant_inspector_network_response( + uint64_t request_id, + const char *url, + int status, + const char *status_text, + const char *mime_type, + const char *type, + const ant_http_header_t *headers +) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.responseReceived\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"loaderId\":\"ant\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"type\":")) goto done; + if (!sbuf_json_string(&b, type ? type : "Fetch")) goto done; + if (!sbuf_append(&b, ",\"response\":{\"url\":")) goto done; + if (!sbuf_json_string(&b, url ? url : "")) goto done; + if (!sbuf_append(&b, ",\"status\":")) goto done; + if (!sbuf_appendf(&b, "%d", status)) goto done; + if (!sbuf_append(&b, ",\"statusText\":")) goto done; + if (!sbuf_json_string(&b, status_text ? status_text : "")) goto done; + if (!sbuf_append(&b, ",\"headers\":")) goto done; + if (!inspector_append_headers_object(&b, headers)) goto done; + if (!sbuf_append(&b, ",\"mimeType\":")) goto done; + if (!sbuf_json_string(&b, mime_type ? mime_type : "")) goto done; + if (!sbuf_append(&b, ",\"connectionReused\":false,\"connectionId\":0,\"encodedDataLength\":0,\"protocol\":\"http/1.1\",\"securityState\":\"unknown\"}}}")) goto done; + inspector_notify_network(b.data); + inspector_notify_response_extra(request_id, status, headers); + +done: + free(b.data); +} + +void ant_inspector_network_finish(uint64_t request_id, size_t encoded_data_length) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.loadingFinished\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"encodedDataLength\":")) goto done; + if (!sbuf_appendf(&b, "%zu", encoded_data_length)) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +void ant_inspector_network_fail( + uint64_t request_id, + const char *error_text, + bool canceled, + const char *type +) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.loadingFailed\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"type\":")) goto done; + if (!sbuf_json_string(&b, type ? type : "Fetch")) goto done; + if (!sbuf_append(&b, ",\"errorText\":")) goto done; + if (!sbuf_json_string(&b, error_text ? error_text : "Failed")) goto done; + if (!sbuf_append(&b, ",\"canceled\":")) goto done; + if (!sbuf_append(&b, canceled ? "true" : "false")) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +void ant_inspector_network_set_request_body(uint64_t request_id, const uint8_t *data, size_t len) { + inspector_network_entry_t *entry = inspector_network_entry_for_id(request_id, true); + if (!entry) return; + inspector_network_store_body( + &entry->request_body, + &entry->request_body_len, + &entry->request_body_truncated, + data, + len, + false + ); +} + +void ant_inspector_network_append_response_body(uint64_t request_id, const uint8_t *data, size_t len) { + inspector_network_entry_t *entry = inspector_network_entry_for_id(request_id, true); + if (!entry) return; + inspector_network_store_body( + &entry->response_body, + &entry->response_body_len, + &entry->response_body_truncated, + data, + len, + true + ); + if (request_id && len > 0 && g_inspector.started && g_inspector.attached) { + sbuf_t b = {0}; + if (sbuf_append(&b, "{\"method\":\"Network.dataReceived\",\"params\":{\"requestId\":\"") && + sbuf_appendf(&b, "%" PRIu64, request_id) && + sbuf_append(&b, "\",\"timestamp\":") && + sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds()) && + sbuf_append(&b, ",\"dataLength\":") && + sbuf_appendf(&b, "%zu", len) && + sbuf_append(&b, ",\"encodedDataLength\":") && + sbuf_appendf(&b, "%zu", len) && + sbuf_append(&b, "}}")) { + inspector_notify_network(b.data); + } + free(b.data); + } +} + +uint64_t ant_inspector_websocket_created(const char *url) { + if (!g_inspector.started || !g_inspector.attached) return 0; + uint64_t request_id = ++g_inspector.next_network_request_id; + if (request_id == 0) request_id = ++g_inspector.next_network_request_id; + inspector_network_entry_for_id(request_id, true); + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.webSocketCreated\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"url\":")) goto done; + if (!sbuf_json_string(&b, url ? url : "")) goto done; + if (!sbuf_append(&b, ",\"initiator\":{\"type\":\"WebSocket\"}}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); + return request_id; +} + +void ant_inspector_websocket_request(uint64_t request_id, const ant_http_header_t *headers) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.webSocketWillSendHandshakeRequest\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"wallTime\":")) goto done; + if (!sbuf_appendf(&b, "%.3f", inspector_wall_time_seconds())) goto done; + if (!sbuf_append(&b, ",\"request\":{\"headers\":")) goto done; + if (!inspector_append_headers_object(&b, headers)) goto done; + if (!sbuf_append(&b, "}}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +void ant_inspector_websocket_response(uint64_t request_id, int status, const char *status_text, const ant_http_header_t *headers) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.webSocketHandshakeResponseReceived\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"response\":{\"status\":")) goto done; + if (!sbuf_appendf(&b, "%d", status)) goto done; + if (!sbuf_append(&b, ",\"statusText\":")) goto done; + if (!sbuf_json_string(&b, status_text ? status_text : "")) goto done; + if (!sbuf_append(&b, ",\"headers\":")) goto done; + if (!inspector_append_headers_object(&b, headers)) goto done; + if (!sbuf_append(&b, "}}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +static void inspector_websocket_frame(uint64_t request_id, const char *method, const uint8_t *data, size_t len, bool binary) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + char *encoded = NULL; + size_t encoded_len = 0; + const char *payload = (const char *)data; + size_t payload_len = len; + + if (binary) { + encoded = ant_base64_encode(data, len, &encoded_len); + if (!encoded) return; + payload = encoded; + payload_len = encoded_len; + } + + if (!sbuf_append(&b, "{\"method\":\"")) goto done; + if (!sbuf_append(&b, method)) goto done; + if (!sbuf_append(&b, "\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"response\":{\"opcode\":")) goto done; + if (!sbuf_append(&b, binary ? "2" : "1")) goto done; + if (!sbuf_append(&b, ",\"mask\":false,\"payloadData\":")) goto done; + if (!sbuf_json_string_len(&b, payload ? payload : "", payload ? payload_len : 0)) goto done; + if (!sbuf_append(&b, "}}}")) goto done; + inspector_notify_network(b.data); + +done: + free(encoded); + free(b.data); +} + +void ant_inspector_websocket_frame_sent(uint64_t request_id, const uint8_t *data, size_t len, bool binary) { + inspector_websocket_frame(request_id, "Network.webSocketFrameSent", data, len, binary); +} + +void ant_inspector_websocket_frame_received(uint64_t request_id, const uint8_t *data, size_t len, bool binary) { + inspector_websocket_frame(request_id, "Network.webSocketFrameReceived", data, len, binary); +} + +void ant_inspector_websocket_error(uint64_t request_id, const char *message) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.webSocketFrameError\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, ",\"errorMessage\":")) goto done; + if (!sbuf_json_string(&b, message ? message : "WebSocket error")) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} + +void ant_inspector_websocket_closed(uint64_t request_id) { + if (!request_id || !g_inspector.started || !g_inspector.attached) return; + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Network.webSocketClosed\",\"params\":{\"requestId\":\"")) goto done; + if (!sbuf_appendf(&b, "%" PRIu64, request_id)) goto done; + if (!sbuf_append(&b, "\",\"timestamp\":")) goto done; + if (!sbuf_appendf(&b, "%.6f", inspector_timestamp_seconds())) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_notify_network(b.data); + +done: + free(b.data); +} diff --git a/src/inspector/router.c b/src/inspector/router.c new file mode 100644 index 0000000..833a835 --- /dev/null +++ b/src/inspector/router.c @@ -0,0 +1,173 @@ +#include "bind.h" +#include "gc.h" + +#include + +typedef void (*inspector_route_fn)( + inspector_client_t *client, + int id, yyjson_val *params +); + +typedef struct { + const char *method; + inspector_route_fn fn; +} inspector_route_t; + +static void route_empty(inspector_client_t *client, int id, yyjson_val *params) { + inspector_send_empty_result(client, id); +} + +static void route_runtime_enable(inspector_client_t *client, int id, yyjson_val *params) { + client->runtime_enabled = true; + inspector_send_empty_result(client, id); + inspector_send_execution_context(client); + inspector_replay_console_events(client); +} + +static void route_console_enable(inspector_client_t *client, int id, yyjson_val *params) { + client->console_enabled = true; + inspector_send_empty_result(client, id); + inspector_replay_console_events(client); +} + +static void route_network_enable(inspector_client_t *client, int id, yyjson_val *params) { + client->network_enabled = true; + inspector_send_empty_result(client, id); +} + +static void route_network_disable(inspector_client_t *client, int id, yyjson_val *params) { + client->network_enabled = false; + inspector_send_empty_result(client, id); +} + +static void route_debugger_enable(inspector_client_t *client, int id, yyjson_val *params) { + client->debugger_enabled = true; + inspector_send_response_obj(client, id, "{\"debuggerId\":\"ant-debugger\"}"); + inspector_send_registered_scripts(client); +} + +static void route_collect_garbage(inspector_client_t *client, int id, yyjson_val *params) { + gc_run(client->js); + inspector_send_empty_result(client, id); +} + +static void route_run_if_waiting(inspector_client_t *client, int id, yyjson_val *params) { + g_inspector.waiting_for_debugger = false; + inspector_send_empty_result(client, id); +} + +static void route_discard_console(inspector_client_t *client, int id, yyjson_val *params) { + inspector_clear_console_events(); + inspector_send_empty_result(client, id); +} + +static void route_get_isolate_id(inspector_client_t *client, int id, yyjson_val *params) { + inspector_send_response_obj(client, id, "{\"id\":\"ant-isolate\"}"); +} + +static void route_schema_domains(inspector_client_t *client, int id, yyjson_val *params) { + inspector_send_response_obj( + client, id, + "{\"domains\":[{\"name\":\"Runtime\",\"version\":\"1.3\"},{\"name\":\"Console\",\"version\":\"1.3\"},{\"name\":\"Debugger\",\"version\":\"1.3\"},{\"name\":\"Network\",\"version\":\"1.3\"},{\"name\":\"HeapProfiler\",\"version\":\"1.3\"},{\"name\":\"Profiler\",\"version\":\"1.3\"}]}" + ); +} + +static void route_network_get_response_body(inspector_client_t *client, int id, yyjson_val *params) { + inspector_get_response_body(client, id, params); +} + +static void route_network_get_request_post_data(inspector_client_t *client, int id, yyjson_val *params) { + inspector_get_request_post_data(client, id, params); +} + +static void route_runtime_evaluate(inspector_client_t *client, int id, yyjson_val *params) { + inspector_eval(client, id, params); +} + +static void route_runtime_compile_script(inspector_client_t *client, int id, yyjson_val *params) { + inspector_compile_script(client, id, params); +} + +static void route_runtime_run_script(inspector_client_t *client, int id, yyjson_val *params) { + inspector_run_script(client, id, params); +} + +static void route_debugger_get_script_source(inspector_client_t *client, int id, yyjson_val *params) { + inspector_get_script_source(client, id, params); +} + +static void route_runtime_get_heap_usage(inspector_client_t *client, int id, yyjson_val *params) { + inspector_get_heap_usage(client, id); +} + +static void route_runtime_get_properties(inspector_client_t *client, int id, yyjson_val *params) { + inspector_get_properties(client, id, params); +} + +static void route_runtime_global_lexical_scope_names(inspector_client_t *client, int id, yyjson_val *params) { + inspector_global_lexical_scope_names(client, id); +} + +static const inspector_route_t k_routes[] = { + {"Console.enable", route_console_enable}, + {"Debugger.enable", route_debugger_enable}, + {"Debugger.getScriptSource", route_debugger_get_script_source}, + {"Debugger.setAsyncCallStackDepth", route_empty}, + {"Debugger.setBlackboxPatterns", route_empty}, + {"Debugger.setBlackboxedRanges", route_empty}, + {"Debugger.setPauseOnExceptions", route_empty}, + {"HeapProfiler.collectGarbage", route_collect_garbage}, + {"HeapProfiler.enable", route_empty}, + {"Inspector.enable", route_empty}, + {"Log.enable", route_empty}, + {"Log.startViolationsReport", route_empty}, + {"Network.clearAcceptedEncodingsOverride", route_empty}, + {"Network.disable", route_network_disable}, + {"Network.emulateNetworkConditionsByRule", route_empty}, + {"Network.enable", route_network_enable}, + {"Network.getRequestPostData", route_network_get_request_post_data}, + {"Network.getResponseBody", route_network_get_response_body}, + {"Network.overrideNetworkState", route_empty}, + {"Network.setAttachDebugStack", route_empty}, + {"Network.setBlockedURLs", route_empty}, + {"Profiler.enable", route_empty}, + {"Runtime.compileScript", route_runtime_compile_script}, + {"Runtime.discardConsoleEntries", route_discard_console}, + {"Runtime.enable", route_runtime_enable}, + {"Runtime.evaluate", route_runtime_evaluate}, + {"Runtime.getHeapUsage", route_runtime_get_heap_usage}, + {"Runtime.getIsolateId", route_get_isolate_id}, + {"Runtime.getProperties", route_runtime_get_properties}, + {"Runtime.globalLexicalScopeNames", route_runtime_global_lexical_scope_names}, + {"Runtime.releaseObject", route_empty}, + {"Runtime.releaseObjectGroup", route_empty}, + {"Runtime.runIfWaitingForDebugger", route_run_if_waiting}, + {"Runtime.runScript", route_runtime_run_script}, + {"Runtime.setAsyncCallStackDepth", route_empty}, + {"Runtime.setCustomObjectFormatterEnabled", route_empty}, + {"Schema.getDomains", route_schema_domains}, +}; + +void inspector_handle_message(inspector_client_t *client, const char *payload, size_t len) { + yyjson_doc *doc = yyjson_read(payload, len, 0); + if (!doc) return; + + yyjson_val *root = yyjson_doc_get_root(doc); + yyjson_val *id_val = yyjson_obj_get(root, "id"); + yyjson_val *method_val = yyjson_obj_get(root, "method"); + yyjson_val *params = yyjson_obj_get(root, "params"); + + int id = id_val && yyjson_is_int(id_val) ? (int)yyjson_get_int(id_val) : 0; + const char *method = method_val && yyjson_is_str(method_val) ? yyjson_get_str(method_val) : ""; + + for ( + size_t i = 0; i < sizeof(k_routes) / sizeof(k_routes[0]); i++) if ( + strcmp(method, k_routes[i].method) == 0 + ) { + k_routes[i].fn(client, id, params); + yyjson_doc_free(doc); return; + } + + if (id != 0) inspector_send_empty_result(client, id); + yyjson_doc_free(doc); +} diff --git a/src/inspector/runtime.c b/src/inspector/runtime.c new file mode 100644 index 0000000..616f31e --- /dev/null +++ b/src/inspector/runtime.c @@ -0,0 +1,812 @@ +#include "ant.h" +#include "bind.h" +#include "pool.h" +#include "errors.h" +#include "reactor.h" +#include "runtime.h" +#include "internal.h" + +#include "gc/roots.h" +#include "modules/symbol.h" +#include "silver/engine.h" + +#include +#include +#include +#include + +bool inspector_is_remote_handle_value(ant_value_t value) { + return is_object_type(value); +} + +static uint32_t inspector_object_handle_id(ant_t *js, ant_value_t value) { + if (!js || !inspector_is_remote_handle_value(value)) return 0; + + for (inspector_object_handle_t *h = g_inspector.object_handles; h; h = h->next) { + if (h->value == value) return h->id; + } + + inspector_object_handle_t *h = calloc(1, sizeof(*h)); + if (!h) return 0; + h->id = ++g_inspector.next_object_id; + if (h->id == 0) h->id = ++g_inspector.next_object_id; + h->value = value; + if (!gc_push_root(js, &h->value)) { + free(h); + return 0; + } + h->next = g_inspector.object_handles; + g_inspector.object_handles = h; + return h->id; +} + +static bool inspector_parse_object_id(const char *object_id, uint32_t *id) { + if (!object_id || strncmp(object_id, "ant:", 4) != 0) return false; + char *end = NULL; + unsigned long parsed = strtoul(object_id + 4, &end, 10); + if (!end || *end != '\0' || parsed == 0 || parsed > UINT32_MAX) return false; + if (id) *id = (uint32_t)parsed; + return true; +} + +static bool inspector_object_for_id(const char *object_id, ant_value_t *out) { + uint32_t id = 0; + if (!inspector_parse_object_id(object_id, &id)) return false; + for (inspector_object_handle_t *h = g_inspector.object_handles; h; h = h->next) { + if (h->id == id) { + if (out) *out = h->value; + return true; + } + } + return false; +} + +static bool inspector_append_object_id(sbuf_t *out, uint32_t id) { + if (id == 0) return true; + return sbuf_append(out, ",\"objectId\":\"ant:") && + sbuf_appendf(out, "%u", id) && + sbuf_append(out, "\""); +} + +static const char *inspector_function_name(ant_t *js, ant_value_t value, size_t *len) { + if (len) *len = 0; + if (vtype(value) == T_CFUNC) { + const ant_cfunc_meta_t *meta = js_as_cfunc_meta(value); + if (!meta || !meta->name) return NULL; + if (len) *len = strlen(meta->name); + return meta->name; + } + + if (vtype(value) != T_FUNC) return NULL; + ant_value_t name = js_get(js, value, "name"); + if (vtype(name) != T_STR) { + ant_value_t cfunc = js_get_slot(js_func_obj(value), SLOT_CFUNC); + return inspector_function_name(js, cfunc, len); + } + return js_getstr(js, name, len); +} + +static bool inspector_append_function_description(ant_t *js, ant_value_t value, sbuf_t *out) { + size_t name_len = 0; + const char *name = inspector_function_name(js, value, &name_len); + if (!sbuf_append(out, "\"function ")) return false; + if (name && name_len > 0) { + if (!sbuf_append_len(out, name, name_len)) return false; + } + return sbuf_append(out, "() { [native code] }\""); +} + +static const char *inspector_object_tag(ant_t *js, ant_value_t value, size_t *len) { + if (len) *len = 0; + if (!js || !inspector_is_remote_handle_value(value)) return NULL; + ant_value_t tag = js_get_sym(js, value, get_toStringTag_sym()); + if (vtype(tag) != T_STR) return NULL; + return js_getstr(js, tag, len); +} + +static bool inspector_append_preview_value(ant_t *js, ant_value_t value, sbuf_t *out) { + uint8_t type = vtype(value); + switch (type) { + case T_UNDEF: + return sbuf_append(out, "\"type\":\"undefined\",\"value\":\"undefined\""); + case T_NULL: + return sbuf_append(out, "\"type\":\"object\",\"subtype\":\"null\",\"value\":\"null\""); + case T_BOOL: + return sbuf_appendf(out, "\"type\":\"boolean\",\"value\":\"%s\"", value == js_true ? "true" : "false"); + case T_NUM: { + double n = js_getnum(value); + if (isnan(n)) return sbuf_append(out, "\"type\":\"number\",\"value\":\"NaN\""); + if (isinf(n)) return sbuf_appendf(out, "\"type\":\"number\",\"value\":\"%sInfinity\"", n < 0 ? "-" : ""); + return sbuf_appendf(out, "\"type\":\"number\",\"value\":\"%.17g\"", n); + } + case T_STR: { + size_t len = 0; + const char *s = js_getstr(js, value, &len); + if (!sbuf_append(out, "\"type\":\"string\",\"value\":")) return false; + return sbuf_json_string_len(out, s, len); + } + case T_FUNC: { + if (!sbuf_append(out, "\"type\":\"function\",\"value\":")) return false; + return inspector_append_function_description(js, value, out); + } + case T_CFUNC: { + if (!sbuf_append(out, "\"type\":\"function\",\"value\":")) return false; + return inspector_append_function_description(js, value, out); + } + default: { + if (inspector_is_remote_handle_value(value)) { + const char *class_name = "Object"; + const char *desc = "Object"; + size_t tag_len = 0; + const char *tag = inspector_object_tag(js, value, &tag_len); + const char *subtype = NULL; + if (value == js_glob(js)) { + class_name = "global"; + desc = "global"; + } else if (type == T_ARR) { + class_name = "Array"; + desc = "Array"; + subtype = "array"; + } else if (type == T_PROMISE) { + class_name = "Promise"; + desc = "Promise"; + subtype = "promise"; + } else if (type == T_GENERATOR) { + class_name = "Generator"; + desc = "Generator"; + subtype = "generator"; + } else if (tag && tag_len > 0) { + if (!sbuf_append(out, "\"type\":\"object\",\"className\":")) return false; + if (!sbuf_json_string_len(out, tag, tag_len)) return false; + if (!sbuf_append(out, ",\"value\":")) return false; + return sbuf_json_string_len(out, tag, tag_len); + } + + if (!sbuf_append(out, "\"type\":\"object\"")) return false; + if (subtype) { + if (!sbuf_append(out, ",\"subtype\":")) return false; + if (!sbuf_json_string(out, subtype)) return false; + } + if (!sbuf_append(out, ",\"className\":")) return false; + if (!sbuf_json_string(out, class_name)) return false; + if (!sbuf_append(out, ",\"value\":")) return false; + return sbuf_json_string(out, desc); + } + + char buf[128]; + js_cstr_t desc = js_to_cstr(js, value, buf, sizeof(buf)); + bool ok = sbuf_append(out, "\"type\":\"object\",\"value\":") && sbuf_json_string(out, desc.ptr); + if (desc.needs_free) free((void *)desc.ptr); + return ok; + } + } +} + +static bool inspector_append_object_preview( + ant_t *js, + ant_value_t value, + const char *type, + const char *subtype, + const char *description, + sbuf_t *out +) { + if (!sbuf_append(out, ",\"preview\":{\"type\":")) return false; + if (!sbuf_json_string(out, type ? type : "object")) return false; + if (subtype) { + if (!sbuf_append(out, ",\"subtype\":")) return false; + if (!sbuf_json_string(out, subtype)) return false; + } + if (!sbuf_append(out, ",\"description\":")) return false; + if (!sbuf_json_string(out, description ? description : "Object")) return false; + if (!sbuf_append(out, ",\"overflow\":")) return false; + + ant_iter_t iter = js_prop_iter_begin(js, value); + const char *key = NULL; + size_t key_len = 0; + ant_value_t prop = js_mkundef(); + size_t count = 0; + const size_t limit = 8; + bool overflow = false; + sbuf_t props = {0}; + if (!sbuf_append(&props, "[")) { + js_prop_iter_end(&iter); + return false; + } + + while (js_prop_iter_next(&iter, &key, &key_len, &prop)) { + if (count >= limit) { + overflow = true; + break; + } + if (count > 0 && !sbuf_append(&props, ",")) { + js_prop_iter_end(&iter); + free(props.data); + return false; + } + if (!sbuf_append(&props, "{\"name\":")) { + js_prop_iter_end(&iter); + free(props.data); + return false; + } + if (!sbuf_json_string_len(&props, key, key_len)) { + js_prop_iter_end(&iter); + free(props.data); + return false; + } + if (!sbuf_append(&props, ",")) { + js_prop_iter_end(&iter); + free(props.data); + return false; + } + if (!inspector_append_preview_value(js, prop, &props)) { + js_prop_iter_end(&iter); + free(props.data); + return false; + } + if (!sbuf_append(&props, "}")) { + js_prop_iter_end(&iter); + free(props.data); + return false; + } + count++; + } + js_prop_iter_end(&iter); + + if (!sbuf_append(&props, "]")) { + free(props.data); + return false; + } + + bool ok = sbuf_append(out, overflow ? "true" : "false") && + sbuf_append(out, ",\"properties\":") && + sbuf_append_len(out, props.data, props.len) && + sbuf_append(out, "}"); + free(props.data); + return ok; +} + +bool inspector_value_to_remote_object(ant_t *js, ant_value_t value, sbuf_t *out) { + uint8_t type = vtype(value); + switch (type) { + case T_UNDEF: + return sbuf_append(out, "{\"type\":\"undefined\"}"); + case T_NULL: + return sbuf_append(out, "{\"type\":\"object\",\"subtype\":\"null\",\"value\":null}"); + case T_BOOL: + return sbuf_appendf(out, "{\"type\":\"boolean\",\"value\":%s}", value == js_true ? "true" : "false"); + case T_NUM: { + double n = js_getnum(value); + if (isnan(n)) return sbuf_append(out, "{\"type\":\"number\",\"unserializableValue\":\"NaN\",\"description\":\"NaN\"}"); + if (isinf(n)) return sbuf_appendf( + out, "{\"type\":\"number\",\"unserializableValue\":\"%sInfinity\",\"description\":\"%sInfinity\"}", + n < 0 ? "-" : "", n < 0 ? "-" : "" + ); + return sbuf_appendf(out, "{\"type\":\"number\",\"value\":%.17g,\"description\":\"%.17g\"}", n, n); + } + case T_STR: { + size_t len = 0; + const char *s = js_getstr(js, value, &len); + if (!sbuf_append(out, "{\"type\":\"string\",\"value\":")) return false; + if (!sbuf_json_string_len(out, s, len)) return false; + if (!sbuf_append(out, ",\"description\":")) return false; + if (!sbuf_json_string_len(out, s, len)) return false; + return sbuf_append(out, "}"); + } + case T_CFUNC: { + ant_value_t promoted = js_cfunc_promote(js, value); + if (vtype(promoted) == T_FUNC) return inspector_value_to_remote_object(js, promoted, out); + + if (!sbuf_append(out, "{\"type\":\"function\",\"className\":\"Function\",\"description\":")) return false; + if (!inspector_append_function_description(js, value, out)) return false; + return sbuf_append(out, "}"); + } + case T_FUNC: { + uint32_t object_id = inspector_object_handle_id(js, value); + sbuf_t desc = {0}; + if (!inspector_append_function_description(js, value, &desc)) { + free(desc.data); + return false; + } + if (!sbuf_append(out, "{\"type\":\"function\",\"className\":\"Function\",\"description\":")) { + free(desc.data); + return false; + } + if (!sbuf_append_len(out, desc.data, desc.len)) { + free(desc.data); + return false; + } + if (!inspector_append_object_id(out, object_id)) { + free(desc.data); + return false; + } + if (desc.len >= 2) desc.data[desc.len - 1] = '\0'; + const char *preview_desc = desc.data ? desc.data + 1 : "function () { [native code] }"; + if (!inspector_append_object_preview(js, value, "function", NULL, preview_desc, out)) { + free(desc.data); + return false; + } + free(desc.data); + return sbuf_append(out, "}"); + } + default: { + if (inspector_is_remote_handle_value(value)) { + const char *class_name = "Object"; + const char *desc = "Object"; + size_t tag_len = 0; + const char *tag = inspector_object_tag(js, value, &tag_len); + const char *subtype = NULL; + if (value == js_glob(js)) { + class_name = "global"; + desc = "global"; + } else if (type == T_ARR) { + class_name = "Array"; + desc = "Array"; + subtype = "array"; + } else if (type == T_PROMISE) { + class_name = "Promise"; + desc = "Promise"; + subtype = "promise"; + } else if (type == T_GENERATOR) { + class_name = "Generator"; + desc = "Generator"; + subtype = "generator"; + } else if (tag && tag_len > 0) { + class_name = NULL; + desc = NULL; + } + + uint32_t object_id = inspector_object_handle_id(js, value); + if (!sbuf_append(out, "{\"type\":\"object\"")) return false; + if (subtype) { + if (!sbuf_append(out, ",\"subtype\":")) return false; + if (!sbuf_json_string(out, subtype)) return false; + } + if (!sbuf_append(out, ",\"className\":")) return false; + if (class_name) { + if (!sbuf_json_string(out, class_name)) return false; + } else if (!sbuf_json_string_len(out, tag, tag_len)) return false; + if (!sbuf_append(out, ",\"description\":")) return false; + if (desc) { + if (!sbuf_json_string(out, desc)) return false; + } else if (!sbuf_json_string_len(out, tag, tag_len)) return false; + if (!inspector_append_object_id(out, object_id)) return false; + const char *preview_desc = desc; + char tag_buf[64]; + if (!preview_desc && tag && tag_len > 0) { + size_t copy_len = tag_len < sizeof(tag_buf) - 1 ? tag_len : sizeof(tag_buf) - 1; + memcpy(tag_buf, tag, copy_len); + tag_buf[copy_len] = '\0'; + preview_desc = tag_buf; + } + if (!inspector_append_object_preview(js, value, "object", subtype, preview_desc, out)) return false; + return sbuf_append(out, "}"); + } + + char buf[512]; + js_cstr_t desc = js_to_cstr(js, value, buf, sizeof(buf)); + bool ok = sbuf_append(out, "{\"type\":\"object\",\"description\":") && + sbuf_json_string(out, desc.ptr) && + sbuf_append(out, "}"); + if (desc.needs_free) free((void *)desc.ptr); + return ok; + } + } +} + +bool inspector_append_call_location(ant_t *js, sbuf_t *b) { + if (!js || !b) return true; + + js_error_site_t saved = js->errsite; + js_clear_error_site(js); + + const char *filename = NULL; + int line = 1; + int column = 1; + js_get_call_location(js, &filename, &line, &column); + js->errsite = saved; + + if (!filename || !*filename) return true; + char *url = inspector_make_script_url(filename); + if (!url) return false; + + inspector_script_t *script = inspector_script_for_url(url); + bool ok = + sbuf_append(b, ",\"stackTrace\":{\"callFrames\":[{\"functionName\":\"\",\"scriptId\":\"") && + (script ? sbuf_appendf(b, "%d", script->id) : sbuf_append(b, "")) && + sbuf_append(b, "\",\"url\":") && + sbuf_json_string(b, url) && + sbuf_append(b, ",\"lineNumber\":") && + sbuf_appendf(b, "%d", line > 0 ? line - 1 : 0) && + sbuf_append(b, ",\"columnNumber\":") && + sbuf_appendf(b, "%d", column > 0 ? column - 1 : 0) && + sbuf_append(b, "}]},\"lineNumber\":") && + sbuf_appendf(b, "%d", line > 0 ? line - 1 : 0) && + sbuf_append(b, ",\"columnNumber\":") && + sbuf_appendf(b, "%d", column > 0 ? column - 1 : 0); + free(url); + return ok; +} + +static bool inspector_ident_char(char c, bool first) { + unsigned char uc = (unsigned char)c; + return c == '_' || c == '$' || isalpha(uc) || (!first && isdigit(uc)); +} + +static bool inspector_eval_safe_member_expr(ant_t *js, const char *expr, size_t expr_len, ant_value_t *out) { + if (!js || !expr || !out) return false; + while (expr_len > 0 && isspace((unsigned char)*expr)) { + expr++; + expr_len--; + } + while (expr_len > 0 && isspace((unsigned char)expr[expr_len - 1])) expr_len--; + while (expr_len > 0 && expr[expr_len - 1] == '.') expr_len--; + if (expr_len == 0) return false; + + ant_value_t cur = js_glob(js); + size_t pos = 0; + bool first_part = true; + while (pos < expr_len) { + if (!inspector_ident_char(expr[pos], true)) return false; + size_t start = pos++; + while (pos < expr_len && inspector_ident_char(expr[pos], false)) pos++; + size_t len = pos - start; + if (len == 0 || len >= 128) return false; + + char key[128]; + memcpy(key, expr + start, len); + key[len] = '\0'; + + if (first_part && (strcmp(key, "globalThis") == 0 || strcmp(key, "global") == 0 || strcmp(key, "this") == 0)) { + cur = js_glob(js); + } else { + if (first_part) cur = js_get(js, js_glob(js), key); + else cur = is_object_type(cur) || vtype(cur) == T_CFUNC ? js_get(js, cur, key) : js_mkundef(); + } + + first_part = false; + if (pos == expr_len) { + *out = cur; + return true; + } + if (expr[pos] != '.') return false; + pos++; + if (pos == expr_len) { + *out = cur; + return true; + } + } + + *out = cur; + return true; +} + +static void inspector_send_side_effect_blocked(inspector_client_t *client, int id) { + inspector_send_response_obj( + client, + id, + "{\"result\":{\"type\":\"undefined\"},\"exceptionDetails\":{\"exceptionId\":1,\"text\":\"EvalError: Possible side-effect in debug-evaluate\",\"lineNumber\":0,\"columnNumber\":0,\"exception\":{\"type\":\"object\",\"subtype\":\"error\",\"className\":\"EvalError\",\"description\":\"EvalError: Possible side-effect in debug-evaluate\"}}}" + ); +} + +void inspector_clear_console_events(void) { + while (g_inspector.console_events_head) { + inspector_console_event_t *event = g_inspector.console_events_head; + g_inspector.console_events_head = event->next; + free(event->json); + free(event); + } + g_inspector.console_events_tail = NULL; + g_inspector.console_event_count = 0; +} + +static void inspector_store_console_event(const char *json) { + if (!json) return; + + inspector_console_event_t *event = calloc(1, sizeof(*event)); + if (!event) return; + event->json = strdup(json); + if (!event->json) { + free(event); + return; + } + + if (g_inspector.console_events_tail) g_inspector.console_events_tail->next = event; + else g_inspector.console_events_head = event; + g_inspector.console_events_tail = event; + g_inspector.console_event_count++; + + while (g_inspector.console_event_count > k_inspector_console_event_limit && g_inspector.console_events_head) { + inspector_console_event_t *old = g_inspector.console_events_head; + g_inspector.console_events_head = old->next; + if (g_inspector.console_events_tail == old) g_inspector.console_events_tail = NULL; + free(old->json); + free(old); + g_inspector.console_event_count--; + } +} + +static void inspector_notify_console(const char *json) { + for (inspector_client_t *c = g_inspector.clients; c; c = c->next) { + if (c->runtime_enabled || c->console_enabled) inspector_send_ws(c, json); + } +} + +void inspector_replay_console_events(inspector_client_t *client) { + if (!client || client->console_replayed) return; + client->console_replayed = true; + for (inspector_console_event_t *event = g_inspector.console_events_head; event; event = event->next) + inspector_send_ws(client, event->json); +} + +void inspector_clear_exception_state(ant_t *js) { + if (!js) return; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); +} + +static ant_value_t inspector_exception_value(ant_t *js, ant_value_t result) { + if (js && js->thrown_exists) return js->thrown_value; + if (vtype(result) == T_ERR && vdata(result) != 0) return mkval(T_OBJ, vdata(result)); + return result; +} + +static bool inspector_exception_description(ant_t *js, ant_value_t err, sbuf_t *out) { + if (!js || !out) return false; + if (vtype(err) == T_OBJ) { + ant_value_t stack = js_get(js, err, "stack"); + if (vtype(stack) == T_STR) { + size_t len = 0; + const char *s = js_getstr(js, stack, &len); + return sbuf_json_string_len(out, s, len); + } + + ant_value_t message = js_get(js, err, "message"); + if (vtype(message) == T_STR) { + size_t len = 0; + const char *s = js_getstr(js, message, &len); + return sbuf_json_string_len(out, s, len); + } + } + + char buf[512]; + js_cstr_t desc = js_to_cstr(js, err, buf, sizeof(buf)); + bool ok = sbuf_json_string(out, desc.ptr); + if (desc.needs_free) free((void *)desc.ptr); + return ok; +} + +static size_t inspector_heap_used(ant_t *js) { + if (!js) return 0; + ant_string_pool_stats_t strings = js_string_pool_stats(&js->pool.string); + ant_pool_stats_t ropes = js_pool_stats(&js->pool.rope); + ant_pool_stats_t symbols = js_pool_stats(&js->pool.symbol); + ant_pool_stats_t permanent = js_pool_stats(&js->pool.permanent); + ant_pool_stats_t bigints = js_class_pool_stats(&js->pool.bigint); + return + js->obj_arena.live_count * js->obj_arena.elem_size + + js->closure_arena.live_count * js->closure_arena.elem_size + + js->upvalue_arena.live_count * js->upvalue_arena.elem_size + + strings.total.used + ropes.used + symbols.used + permanent.used + bigints.used + + code_arena_get_memory() + parse_arena_get_memory(); +} + +static size_t inspector_heap_total(ant_t *js) { + if (!js) return 0; + ant_string_pool_stats_t strings = js_string_pool_stats(&js->pool.string); + ant_pool_stats_t ropes = js_pool_stats(&js->pool.rope); + ant_pool_stats_t symbols = js_pool_stats(&js->pool.symbol); + ant_pool_stats_t permanent = js_pool_stats(&js->pool.permanent); + ant_pool_stats_t bigints = js_class_pool_stats(&js->pool.bigint); + return + js->obj_arena.committed + + js->closure_arena.committed + + js->upvalue_arena.committed + + strings.total.capacity + ropes.capacity + symbols.capacity + permanent.capacity + bigints.capacity + + code_arena_get_memory() + parse_arena_get_memory(); +} + +void inspector_send_execution_context(inspector_client_t *client) { + inspector_send_ws( + client, + "{\"method\":\"Runtime.executionContextCreated\",\"params\":{\"context\":{\"id\":1,\"origin\":\"\",\"name\":\"Ant\",\"uniqueId\":\"ant-main\",\"auxData\":{\"isDefault\":true}}}}" + ); +} + +void inspector_send_eval_result(inspector_client_t *client, int id, ant_value_t result) { + ant_t *js = client->js; + bool exception = is_err(result) || js->thrown_exists; + ant_value_t exception_value = exception ? inspector_exception_value(js, result) : js_mkundef(); + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"result\":")) goto oom; + if (!inspector_value_to_remote_object(js, exception ? exception_value : result, &b)) goto oom; + if (exception) { + if (!sbuf_append(&b, ",\"exceptionDetails\":{\"exceptionId\":1,\"text\":")) goto oom; + if (!inspector_exception_description(js, exception_value, &b)) goto oom; + if (!sbuf_append(&b, ",\"lineNumber\":0,\"columnNumber\":0,\"exception\":")) goto oom; + if (!inspector_value_to_remote_object(js, exception_value, &b)) goto oom; + if (!sbuf_append(&b, "}")) goto oom; + } + if (!sbuf_append(&b, "}")) goto oom; + inspector_send_response_obj(client, id, b.data); + free(b.data); + inspector_clear_exception_state(js); + return; + +oom: + free(b.data); + inspector_clear_exception_state(js); + inspector_send_error(client, id, -32000, "Out of memory"); +} + +void inspector_eval(inspector_client_t *client, int id, yyjson_val *params) { + yyjson_val *expr_val = params ? yyjson_obj_get(params, "expression") : NULL; + if (!expr_val || !yyjson_is_str(expr_val)) { + inspector_send_error(client, id, -32602, "Runtime.evaluate requires expression"); + return; + } + + const char *expr = yyjson_get_str(expr_val); + size_t expr_len = yyjson_get_len(expr_val); + + const char *prev_filename = client->js->filename; + inspector_clear_exception_state(client->js); + + if (inspector_param_bool(params, "throwOnSideEffect")) { + ant_value_t safe_result = js_mkundef(); + if (inspector_eval_safe_member_expr(client->js, expr, expr_len, &safe_result)) { + inspector_send_eval_result(client, id, safe_result); + } else { + inspector_send_side_effect_blocked(client, id); + } + return; + } + + js_set_filename(client->js, "[inspector]"); + ant_value_t result = js_eval_bytecode_repl(client->js, expr, expr_len); + js_reactor_pump_repl_nowait(client->js); + js_set_filename(client->js, prev_filename); + inspector_send_eval_result(client, id, result); +} + +void inspector_get_heap_usage(inspector_client_t *client, int id) { + sbuf_t b = {0}; + if (sbuf_appendf( + &b, "{\"usedSize\":%zu,\"totalSize\":%zu}", + inspector_heap_used(client->js), + inspector_heap_total(client->js) + )) inspector_send_response_obj(client, id, b.data); + else inspector_send_error(client, id, -32000, "Out of memory"); + free(b.data); +} + +void inspector_get_properties(inspector_client_t *client, int id, yyjson_val *params) { + yyjson_val *object_id_val = params ? yyjson_obj_get(params, "objectId") : NULL; + const char *object_id = object_id_val && yyjson_is_str(object_id_val) ? yyjson_get_str(object_id_val) : NULL; + bool own_only = inspector_param_bool(params, "ownProperties"); + ant_value_t object = js_mkundef(); + if (!inspector_object_for_id(object_id, &object)) { + inspector_send_error(client, id, -32000, "Could not find object with given id"); + return; + } + + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"result\":[")) goto oom; + + bool first = true; + for (ant_value_t cur = object, depth = 0; inspector_is_remote_handle_value(cur) && depth < 64; depth++) { + ant_iter_t iter = js_prop_iter_begin(client->js, cur); + const char *key = NULL; + size_t key_len = 0; + ant_value_t value = js_mkundef(); + while (js_prop_iter_next(&iter, &key, &key_len, &value)) { + if (!first && !sbuf_append(&b, ",")) { + js_prop_iter_end(&iter); + goto oom; + } + first = false; + if (!sbuf_append(&b, "{\"name\":")) { + js_prop_iter_end(&iter); + goto oom; + } + if (!sbuf_json_string_len(&b, key, key_len)) { + js_prop_iter_end(&iter); + goto oom; + } + if (!sbuf_append(&b, ",\"enumerable\":true,\"configurable\":true,\"writable\":true,\"isOwn\":")) { + js_prop_iter_end(&iter); + goto oom; + } + if (!sbuf_append(&b, depth == 0 ? "true" : "false")) { + js_prop_iter_end(&iter); + goto oom; + } + if (!sbuf_append(&b, ",\"value\":")) { + js_prop_iter_end(&iter); + goto oom; + } + if (!inspector_value_to_remote_object(client->js, value, &b)) { + js_prop_iter_end(&iter); + goto oom; + } + if (!sbuf_append(&b, "}")) { + js_prop_iter_end(&iter); + goto oom; + } + } + js_prop_iter_end(&iter); + if (own_only) break; + ant_value_t proto = js_get_proto(client->js, cur); + if (!inspector_is_remote_handle_value(proto)) break; + cur = proto; + } + + if (!sbuf_append(&b, "],\"internalProperties\":[")) goto oom; + ant_value_t proto = js_get_proto(client->js, object); + if (inspector_is_remote_handle_value(proto) || vtype(proto) == T_NULL) { + if (!sbuf_append(&b, "{\"name\":\"[[Prototype]]\",\"value\":")) goto oom; + if (!inspector_value_to_remote_object(client->js, proto, &b)) goto oom; + if (!sbuf_append(&b, "}")) goto oom; + } + if (!sbuf_append(&b, "]}")) goto oom; + inspector_send_response_obj(client, id, b.data); + free(b.data); + return; + +oom: + free(b.data); + inspector_send_error(client, id, -32000, "Out of memory"); +} + +void inspector_global_lexical_scope_names(inspector_client_t *client, int id) { + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"names\":[")) goto oom; + + bool first = true; + ant_iter_t iter = js_prop_iter_begin(client->js, js_glob(client->js)); + const char *key = NULL; + size_t key_len = 0; + ant_value_t value = js_mkundef(); + while (js_prop_iter_next(&iter, &key, &key_len, &value)) { + (void)value; + if (!first && !sbuf_append(&b, ",")) { + js_prop_iter_end(&iter); + goto oom; + } + first = false; + if (!sbuf_json_string_len(&b, key, key_len)) { + js_prop_iter_end(&iter); + goto oom; + } + } + js_prop_iter_end(&iter); + + if (!sbuf_append(&b, "]}")) goto oom; + inspector_send_response_obj(client, id, b.data); + free(b.data); + return; + +oom: + free(b.data); + inspector_send_error(client, id, -32000, "Out of memory"); +} + +void ant_inspector_console_api_called(ant_t *js, const char *level, ant_value_t *args, int nargs) { + if (!g_inspector.started) return; + sbuf_t b = {0}; + if (!sbuf_append(&b, "{\"method\":\"Runtime.consoleAPICalled\",\"params\":{\"type\":")) goto done; + if (!sbuf_json_string(&b, level ? level : "log")) goto done; + if (!sbuf_append(&b, ",\"args\":[")) goto done; + for (int i = 0; i < nargs; i++) { + if (i && !sbuf_append(&b, ",")) goto done; + if (!inspector_value_to_remote_object(js, args[i], &b)) goto done; + } + if (!sbuf_appendf(&b, "],\"executionContextId\":1,\"timestamp\":%.3f", (double)uv_hrtime() / 1000000.0)) goto done; + if (!inspector_append_call_location(js, &b)) goto done; + if (!sbuf_append(&b, "}}")) goto done; + inspector_store_console_event(b.data); + inspector_notify_console(b.data); +done: + free(b.data); +} diff --git a/src/inspector/server.c b/src/inspector/server.c new file mode 100644 index 0000000..4588653 --- /dev/null +++ b/src/inspector/server.c @@ -0,0 +1,370 @@ +#include "bind.h" +#include "internal.h" +#include "inspector.h" +#include "http/websocket.h" +#include "modules/crypto.h" + +#include +#include +#include +#include + +static void inspector_process_ws(inspector_client_t *client) { + for (;;) { + ant_ws_frame_t frame = {0}; + ant_ws_frame_result_t r = ant_ws_parse_frame( + (const uint8_t *)client->read_buf, + client->read_len, + true, + &frame + ); + if (r == ANT_WS_FRAME_INCOMPLETE) return; + if (r == ANT_WS_FRAME_PROTOCOL_ERROR) { + uv_close((uv_handle_t *)&client->handle, NULL); + return; + } + if (frame.opcode == ANT_WS_OPCODE_TEXT) { + inspector_handle_message(client, (const char *)frame.payload, frame.payload_len); + } else if (frame.opcode == ANT_WS_OPCODE_CLOSE) { + ant_ws_frame_clear(&frame); + uv_close((uv_handle_t *)&client->handle, NULL); + return; + } else if (frame.opcode == ANT_WS_OPCODE_PING) { + size_t len = 0; + uint8_t *pong = ant_ws_encode_frame(ANT_WS_OPCODE_PONG, frame.payload, frame.payload_len, false, &len); + if (pong) { + inspector_send_raw(client, (const char *)pong, len); + free(pong); + } + } + size_t consumed = frame.consumed_len; + ant_ws_frame_clear(&frame); + if (consumed >= client->read_len) { + client->read_len = 0; + return; + } + memmove(client->read_buf, client->read_buf + consumed, client->read_len - consumed); + client->read_len -= consumed; + } +} + +static const char *header_value(const char *headers, const char *name, char *buf, size_t bufsz) { + size_t name_len = strlen(name); + const char *p = headers; + while ((p = strstr(p, "\n")) != NULL) { + p++; + while (*p == '\r' || *p == '\n') p++; + if (strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') { + p += name_len + 1; + while (*p == ' ' || *p == '\t') p++; + const char *end = p; + while (*end && *end != '\r' && *end != '\n') end++; + size_t len = (size_t)(end - p); + if (len >= bufsz) len = bufsz - 1; + memcpy(buf, p, len); + buf[len] = '\0'; + return buf; + } + } + return NULL; +} + +static bool host_allowed(const char *headers) { + char host[256]; + const char *value = header_value(headers, "Host", host, sizeof(host)); + if (!value) return true; + if (strncmp(value, "127.0.0.1", 9) == 0) return true; + if (strncmp(value, "localhost", 9) == 0) return true; + if (strncmp(value, "[::1]", 5) == 0) return true; + return false; +} + +static bool header_token_eq(const char *headers, const char *name, const char *expected) { + char value[256]; + const char *p = header_value(headers, name, value, sizeof(value)); + if (!p) return false; + size_t expected_len = strlen(expected); + while (*p) { + while (*p == ' ' || *p == '\t' || *p == ',') p++; + const char *start = p; + while (*p && *p != ',') p++; + const char *end = p; + while (end > start && (end[-1] == ' ' || end[-1] == '\t')) end--; + if ((size_t)(end - start) == expected_len && strncasecmp(start, expected, expected_len) == 0) + return true; + } + return false; +} + +static bool request_path_matches_uuid(const char *req) { + char expected[80]; + snprintf(expected, sizeof(expected), "GET /%s ", g_inspector.uuid); + return strncmp(req, expected, strlen(expected)) == 0; +} + +static bool inspector_append_target_url(sbuf_t *b, const char *file) { + if (!file || !*file || strcmp(file, "[repl]") == 0 || strcmp(file, "ant") == 0) + return sbuf_json_string(b, "file:///"); + if (inspector_is_url_like(file)) return sbuf_json_string(b, file); + if (file[0] == '/') { + if (!sbuf_append(b, "\"file://")) return false; + if (!sbuf_append(b, file)) return false; + return sbuf_append(b, "\""); + } + if (!sbuf_append(b, "\"file:///")) return false; + if (!sbuf_append(b, file)) return false; + return sbuf_append(b, "\""); +} + +static bool inspector_append_devtools_url(sbuf_t *b) { + return + sbuf_append(b, "devtools://devtools/bundled/js_app.html?v8only=true&ws=") && + sbuf_append(b, g_inspector.host) && + sbuf_appendf(b, ":%d/%s", g_inspector.port, g_inspector.uuid); +} + +static bool inspector_append_browser_devtools_url(sbuf_t *b) { + return + sbuf_append(b, "http://") && + sbuf_append(b, g_inspector.host) && + sbuf_appendf(b, ":%d/devtools", g_inspector.port); +} + +static char *json_list_response(void) { + sbuf_t b = {0}; + inspector_script_t *entry = inspector_entry_script(); + const char *file = entry && entry->url + ? entry->url + : (g_inspector.js && g_inspector.js->filename ? g_inspector.js->filename : "ant"); + uv_pid_t pid = uv_os_getpid(); + if (!sbuf_append(&b, "[")) goto fail; + if (!sbuf_append(&b, "{\"description\":\"ant[")) goto fail; + if (!sbuf_appendf(&b, "%d", (int)pid)) goto fail; + if (!sbuf_append(&b, "]\",\"devtoolsFrontendUrl\":\"")) goto fail; + if (!inspector_append_devtools_url(&b)) goto fail; + if (!sbuf_append(&b, "\",\"id\":\"")) goto fail; + if (!sbuf_append(&b, g_inspector.uuid)) goto fail; + if (!sbuf_append(&b, "\",\"title\":\"ant[")) goto fail; + if (!sbuf_appendf(&b, "%d", (int)pid)) goto fail; + if (!sbuf_append(&b, "]\"")) goto fail; + if (!sbuf_append(&b, ",\"type\":\"node\",\"url\":")) goto fail; + if (!inspector_append_target_url(&b, file)) goto fail; + if (!sbuf_append(&b, ",\"webSocketDebuggerUrl\":\"ws://")) goto fail; + if (!sbuf_append(&b, g_inspector.host)) goto fail; + if (!sbuf_appendf(&b, ":%d/%s\"}]", g_inspector.port, g_inspector.uuid)) goto fail; + return b.data; +fail: + free(b.data); + return NULL; +} + +static void inspector_http_response(inspector_client_t *client, const char *status, const char *type, const char *body) { + sbuf_t b = {0}; + size_t body_len = body ? strlen(body) : 0; + if (sbuf_appendf( + &b, + "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: close\r\n\r\n", + status, type ? type : "text/plain", body_len + ) && sbuf_append_len(&b, body, body_len)) { + inspector_send_raw(client, b.data, b.len); + } + free(b.data); + uv_close((uv_handle_t *)&client->handle, NULL); +} + +static void inspector_http_devtools_page(inspector_client_t *client) { + sbuf_t devtools = {0}; + sbuf_t body = {0}; + if ( + inspector_append_devtools_url(&devtools) && + sbuf_append(&body, "Ant DevTools") && + sbuf_append(&body, "") && + sbuf_append(&body, "

Ant DevTools

Paste into Chrome's address bar.
DevTools frontend") && + sbuf_append(&body, devtools.data) && + sbuf_append(&body, "
") + ) inspector_http_response(client, "200 OK", "text/html; charset=UTF-8", body.data); + else inspector_http_response(client, "500 Internal Server Error", "text/plain", "Out of memory\n"); + free(devtools.data); + free(body.data); +} + +static void inspector_upgrade(inspector_client_t *client, const char *headers) { + char key[256]; + if (!host_allowed(headers) || !request_path_matches_uuid(headers)) { + inspector_http_response(client, "403 Forbidden", "text/plain", "Forbidden\n"); + return; + } + if (!header_value(headers, "Sec-WebSocket-Key", key, sizeof(key))) { + inspector_http_response(client, "400 Bad Request", "text/plain", "Missing WebSocket key\n"); + return; + } + char *accept = ant_ws_accept_key(key); + if (!accept) { + inspector_http_response(client, "400 Bad Request", "text/plain", "Invalid WebSocket key\n"); + return; + } + sbuf_t b = {0}; + if (sbuf_appendf( + &b, + "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", + accept + )) inspector_send_raw(client, b.data, b.len); + free(accept); + free(b.data); + client->websocket = true; + g_inspector.attached = true; + client->read_len = 0; +} + +static void inspector_process_http(inspector_client_t *client) { + char *end = strstr(client->read_buf, "\r\n\r\n"); + if (!end) return; + if (!host_allowed(client->read_buf)) { + inspector_http_response(client, "403 Forbidden", "text/plain", "Forbidden\n"); + return; + } + if (strncmp(client->read_buf, "GET /json/list ", 15) == 0 || + strncmp(client->read_buf, "GET /json ", 10) == 0) { + char *body = json_list_response(); + inspector_http_response(client, "200 OK", "application/json; charset=UTF-8", body ? body : "[]"); + free(body); + } else if (strncmp(client->read_buf, "GET /devtools ", 14) == 0 || + strncmp(client->read_buf, "GET /devtools/ ", 15) == 0) { + inspector_http_devtools_page(client); + } else if (strncmp(client->read_buf, "GET /json/version ", 18) == 0) { + inspector_http_response( + client, + "200 OK", + "application/json; charset=UTF-8", + "{\"Browser\":\"Ant/v" ANT_VERSION "\",\"Protocol-Version\":\"1.3\",\"V8-Version\":\"ant\",\"WebKit-Version\":\"ant\"}" + ); + } else if ( + strncasecmp(client->read_buf, "GET /", 5) == 0 && + header_token_eq(client->read_buf, "Upgrade", "websocket") && + header_token_eq(client->read_buf, "Connection", "upgrade") + ) inspector_upgrade(client, client->read_buf); + else inspector_http_response(client, "404 Not Found", "text/plain", "Not found\n"); +} + +static void inspector_alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { + buf->base = malloc(suggested_size); + buf->len = suggested_size; +} + +static void inspector_client_closed(uv_handle_t *handle) { + inspector_client_t *client = (inspector_client_t *)handle; + inspector_client_t **p = &g_inspector.clients; + while (*p) { + if (*p == client) { + *p = client->next; + break; + } + p = &(*p)->next; + } + free(client->read_buf); + free(client); +} + +static void inspector_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + inspector_client_t *client = (inspector_client_t *)stream; + if (nread <= 0) { + free(buf->base); + if (nread < 0) uv_close((uv_handle_t *)&client->handle, inspector_client_closed); + return; + } + if (client->read_len + (size_t)nread + 1 > client->read_cap) { + size_t next_cap = client->read_cap ? client->read_cap * 2 : 8192; + while (next_cap < client->read_len + (size_t)nread + 1) next_cap *= 2; + char *next = realloc(client->read_buf, next_cap); + if (!next) { + free(buf->base); + uv_close((uv_handle_t *)&client->handle, inspector_client_closed); + return; + } + client->read_buf = next; + client->read_cap = next_cap; + } + memcpy(client->read_buf + client->read_len, buf->base, (size_t)nread); + client->read_len += (size_t)nread; + client->read_buf[client->read_len] = '\0'; + free(buf->base); + + if (client->websocket) inspector_process_ws(client); + else inspector_process_http(client); +} + +static void inspector_connection_cb(uv_stream_t *server, int status) { + if (status < 0) return; + inspector_client_t *client = calloc(1, sizeof(*client)); + if (!client) return; + client->js = g_inspector.js; + uv_tcp_init(uv_default_loop(), &client->handle); + client->handle.data = client; + if (uv_accept(server, (uv_stream_t *)&client->handle) != 0) { + uv_close((uv_handle_t *)&client->handle, inspector_client_closed); + return; + } + client->next = g_inspector.clients; + g_inspector.clients = client; + uv_read_start((uv_stream_t *)&client->handle, inspector_alloc_cb, inspector_read_cb); +} + +static bool inspector_parse_sockaddr(const char *host, int port, struct sockaddr_in *addr) { + const char *bind_host = host && *host ? host : "127.0.0.1"; + if (uv_ip4_addr(bind_host, port, addr) == 0) return true; + if (strcmp(bind_host, "localhost") == 0 && uv_ip4_addr("127.0.0.1", port, addr) == 0) return true; + return false; +} + +bool ant_inspector_start(ant_t *js, const ant_inspector_options_t *options) { + if (!options || !options->enabled || g_inspector.started) return true; + memset(&g_inspector, 0, sizeof(g_inspector)); + g_inspector.js = js; + g_inspector.port = options->port > 0 ? options->port : 9229; + snprintf(g_inspector.host, sizeof(g_inspector.host), "%s", options->host[0] ? options->host : "127.0.0.1"); + if (crypto_random_uuid(g_inspector.uuid) < 0) return false; + + struct sockaddr_in addr; + if (!inspector_parse_sockaddr(g_inspector.host, g_inspector.port, &addr)) return false; + if (uv_tcp_init(uv_default_loop(), &g_inspector.server) != 0) return false; + if (uv_tcp_bind(&g_inspector.server, (const struct sockaddr *)&addr, 0) != 0) return false; + if (uv_listen((uv_stream_t *)&g_inspector.server, 16, inspector_connection_cb) != 0) return false; + if (!options->wait_for_session) uv_unref((uv_handle_t *)&g_inspector.server); + g_inspector.waiting_for_debugger = options->wait_for_session; + g_inspector.started = true; + + sbuf_t devtools = {0}; + if (inspector_append_browser_devtools_url(&devtools)) + fprintf(stderr, "Debugger listening on %s\n", devtools.data); + free(devtools.data); + return true; +} + +void ant_inspector_stop(void) { + if (!g_inspector.started) return; + for (inspector_client_t *c = g_inspector.clients; c; c = c->next) + uv_close((uv_handle_t *)&c->handle, inspector_client_closed); + uv_close((uv_handle_t *)&g_inspector.server, NULL); + inspector_clear_console_events(); + while (g_inspector.scripts) { + inspector_script_t *script = g_inspector.scripts; + g_inspector.scripts = script->next; + free(script->url); + free(script->source); + free(script); + } + while (g_inspector.network_entries) { + inspector_network_entry_t *entry = g_inspector.network_entries; + g_inspector.network_entries = entry->next; + free(entry->request_body); + free(entry->response_body); + free(entry); + } + g_inspector.started = false; +} + +void ant_inspector_wait_for_session(void) { + while (g_inspector.started && (!g_inspector.attached || g_inspector.waiting_for_debugger)) + uv_run(uv_default_loop(), UV_RUN_ONCE); + if (g_inspector.started) uv_unref((uv_handle_t *)&g_inspector.server); +} diff --git a/src/main.c b/src/main.c index 8bc016c..753953b 100644 --- a/src/main.c +++ b/src/main.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -17,8 +18,8 @@ #include "watch.h" #include "reactor.h" #include "runtime.h" -#include "output.h" #include "snapshot.h" +#include "inspector.h" #include "esm/loader.h" #include "esm/library.h" #include "esm/remote.h" @@ -261,6 +262,38 @@ static argv_split_t build_process_argv(int argc, char **argv, const char *module return (argv_split_t){ total, out }; } +static void parse_inspector_spec(const char *spec, char *host, size_t host_len, int *port) { + if (!spec || !*spec) return; + bool all_digits = true; + + for (const char *p = spec; *p; p++) if (!isdigit((unsigned char)*p)) { + all_digits = false; + break; + } + + if (all_digits) { + int parsed = atoi(spec); + if (parsed > 0) *port = parsed; + return; + } + + const char *colon = strrchr(spec, ':'); + if (colon && colon != spec && colon[1]) { + size_t len = (size_t)(colon - spec); + if (len >= host_len) len = host_len - 1; + + memcpy(host, spec, len); + host[len] = '\0'; + + int parsed = atoi(colon + 1); + if (parsed > 0) *port = parsed; + + return; + } + + snprintf(host, host_len, "%s", spec); +} + static char *read_stdin(size_t *len) { size_t cap = 4096; *len = 0; @@ -394,6 +427,13 @@ int main(int argc, char *argv[]) { setup_console_colors(); parse_ant_debug_flags(); + + ant_inspector_options_t inspector = { + .enabled = false, + .wait_for_session = false, + .host = "127.0.0.1", + .port = 9229, + }; int filtered_argc = 0; int original_argc = argc; char **original_argv = argv; @@ -412,11 +452,30 @@ int main(int argc, char *argv[]) { free(exec_argv); return exitcode; } + // TODO: only parse before script filename char **filtered_argv = try_oom(sizeof(char*) * argc); for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--verbose") == 0) pkg_verbose = true; else if (strcmp(argv[i], "--no-color") == 0) { crprintf_set_color(false); io_no_color = true; } else if (strncmp(argv[i], "--stack-size=", 13) == 0) sv_user_stack_size_kb = atoi(argv[i] + 13); + else if (strcmp(argv[i], "--inspect") == 0) inspector.enabled = true; + + else if (strncmp(argv[i], "--inspect=", 10) == 0) { + inspector.enabled = true; + parse_inspector_spec(argv[i] + 10, inspector.host, sizeof(inspector.host), &inspector.port); + } + + else if (strcmp(argv[i], "--inspect-wait") == 0) { + inspector.enabled = true; + inspector.wait_for_session = true; + } + + else if (strncmp(argv[i], "--inspect-wait=", 15) == 0) { + inspector.enabled = true; + inspector.wait_for_session = true; + parse_inspector_spec(argv[i] + 15, inspector.host, sizeof(inspector.host), &inspector.port); + } + else filtered_argv[filtered_argc++] = argv[i]; } @@ -702,10 +761,28 @@ int main(int argc, char *argv[]) { crfprintf(stderr, msg.snapshot_warn, js_str(js, snapshot_result)); } - if (internal_crash_report_mode) { - js_result = ant_crash_run_internal_report(js); + if (inspector.enabled && !ant_inspector_start(js, &inspector)) { + fprintf(stderr, "Unable to start inspector on %s:%d\n", inspector.host, inspector.port); + js_result = EXIT_FAILURE; + goto cleanup; } + if (inspector.enabled && eval->count > 0) { + const char *script = eval->sval[0]; + ant_inspector_register_script_source("[eval]", script, strlen(script), false); + } + + if (inspector.enabled && eval->count == 0 && !repl_mode && !stdin_mode) for ( + int fi = 0; fi < file->count; fi++) { + char *resolved_file = resolve_js_file(file->filename[fi]); + if (!resolved_file) continue; + ant_inspector_register_script_file(resolved_file, true); + free(resolved_file); + } + + if (inspector.wait_for_session) ant_inspector_wait_for_session(); + if (internal_crash_report_mode) js_result = ant_crash_run_internal_report(js); + else if (eval->count > 0) { const char *script = eval->sval[0]; eval_code(js, script, strlen(script), "[eval]", print->count > 0); @@ -721,6 +798,7 @@ int main(int argc, char *argv[]) { crfprintf(stderr, msg.ant_allocation_fatal); js_result = EXIT_FAILURE; goto cleanup; } + if (inspector.enabled) ant_inspector_register_script_source("[stdin]", buf, len, false); eval_code(js, buf, len, "[stdin]", print->count > 0); free(buf); } diff --git a/src/modules/crypto.c b/src/modules/crypto.c index 396a90a..5f613ef 100644 --- a/src/modules/crypto.c +++ b/src/modules/crypto.c @@ -62,10 +62,9 @@ static inline ant_value_t crypto_random_error(ant_t *js) { return js_mkerr(js, "secure random generation failed"); } -static ant_value_t crypto_format_uuid_v4(ant_t *js, const uint8_t uuid[16]) { +static inline void crypto_format_uuid_v4(const uint8_t uuid[16], char out[37]) { static char lut[256][2]; static bool lut_init = false; - char uuid_str[36]; if (!lut_init) { static const char hex[] = "0123456789abcdef"; @@ -76,32 +75,42 @@ static ant_value_t crypto_format_uuid_v4(ant_t *js, const uint8_t uuid[16]) { lut_init = true; } - memcpy(uuid_str + 0, lut[uuid[0]], 2); - memcpy(uuid_str + 2, lut[uuid[1]], 2); - memcpy(uuid_str + 4, lut[uuid[2]], 2); - memcpy(uuid_str + 6, lut[uuid[3]], 2); + memcpy(out + 0, lut[uuid[0]], 2); + memcpy(out + 2, lut[uuid[1]], 2); + memcpy(out + 4, lut[uuid[2]], 2); + memcpy(out + 6, lut[uuid[3]], 2); - uuid_str[8] = '-'; - memcpy(uuid_str + 9, lut[uuid[4]], 2); - memcpy(uuid_str + 11, lut[uuid[5]], 2); + out[8] = '-'; + memcpy(out + 9, lut[uuid[4]], 2); + memcpy(out + 11, lut[uuid[5]], 2); - uuid_str[13] = '-'; - memcpy(uuid_str + 14, lut[uuid[6]], 2); - memcpy(uuid_str + 16, lut[uuid[7]], 2); + out[13] = '-'; + memcpy(out + 14, lut[uuid[6]], 2); + memcpy(out + 16, lut[uuid[7]], 2); - uuid_str[18] = '-'; - memcpy(uuid_str + 19, lut[uuid[8]], 2); - memcpy(uuid_str + 21, lut[uuid[9]], 2); + out[18] = '-'; + memcpy(out + 19, lut[uuid[8]], 2); + memcpy(out + 21, lut[uuid[9]], 2); - uuid_str[23] = '-'; - memcpy(uuid_str + 24, lut[uuid[10]], 2); - memcpy(uuid_str + 26, lut[uuid[11]], 2); - memcpy(uuid_str + 28, lut[uuid[12]], 2); - memcpy(uuid_str + 30, lut[uuid[13]], 2); - memcpy(uuid_str + 32, lut[uuid[14]], 2); - memcpy(uuid_str + 34, lut[uuid[15]], 2); + out[23] = '-'; + memcpy(out + 24, lut[uuid[10]], 2); + memcpy(out + 26, lut[uuid[11]], 2); + memcpy(out + 28, lut[uuid[12]], 2); + memcpy(out + 30, lut[uuid[13]], 2); + memcpy(out + 32, lut[uuid[14]], 2); + memcpy(out + 34, lut[uuid[15]], 2); + out[36] = '\0'; +} + +int crypto_random_uuid(char out[37]) { + uint8_t uuid[16]; + if (!out || crypto_fill_random(uuid, sizeof(uuid)) < 0) return -1; + + uuid[6] = (uint8_t)((uuid[6] & 0x0f) | 0x40); + uuid[8] = (uint8_t)((uuid[8] & 0x3f) | 0x80); - return js_mkstr(js, uuid_str, sizeof(uuid_str)); + crypto_format_uuid_v4(uuid, out); + return 0; } static ant_value_t crypto_make_buffer(ant_t *js, const uint8_t *data, size_t len) { @@ -351,13 +360,9 @@ static ant_value_t js_crypto_random_bytes(ant_t *js, ant_value_t *args, int narg // crypto.randomUUID() static ant_value_t js_crypto_random_uuid(ant_t *js, ant_value_t *args, int nargs) { - uint8_t uuid[16]; - if (crypto_fill_random(uuid, sizeof(uuid)) < 0) return crypto_random_error(js); - - uuid[6] = (uint8_t)((uuid[6] & 0x0f) | 0x40); - uuid[8] = (uint8_t)((uuid[8] & 0x3f) | 0x80); - - return crypto_format_uuid_v4(js, uuid); + char uuid[37]; + if (crypto_random_uuid(uuid) < 0) return crypto_random_error(js); + return js_mkstr(js, uuid, 36); } // crypto.randomUUIDv7() diff --git a/src/modules/eventsource.c b/src/modules/eventsource.c index 10a204d..45d04af 100644 --- a/src/modules/eventsource.c +++ b/src/modules/eventsource.c @@ -9,11 +9,12 @@ #include #include "ant.h" +#include "ptr.h" #include "common.h" #include "errors.h" -#include "internal.h" -#include "ptr.h" #include "runtime.h" +#include "internal.h" +#include "inspector.h" #include "silver/engine.h" #include "http/eventsource.h" @@ -32,6 +33,8 @@ typedef struct eventsource_state_s { ant_value_t obj; ant_http_request_t *request; eventsource_timer_t *timer; + uint64_t network_request_id; + size_t network_encoded_length; ant_sse_parser_t parser; char *url; char *last_event_id; @@ -237,6 +240,14 @@ static void eventsource_http_on_response(ant_http_request_t *req, const ant_http const char *content_type = eventsource_find_header(resp->headers, "content-type"); if (!es || es->closed) return; + + ant_inspector_network_response( + es->network_request_id, es->url, + resp->status, resp->status_text, + content_type ? content_type : "text/event-stream", + "EventSource", resp->headers + ); + if (resp->status != 200 || !eventsource_content_type_ok(content_type)) { eventsource_emit_error_once(es); ant_http_request_cancel(req); @@ -252,11 +263,16 @@ static void eventsource_http_on_response(ant_http_request_t *req, const ant_http static void eventsource_http_on_body(ant_http_request_t *req, const uint8_t *chunk, size_t len, void *user_data) { eventsource_state_t *es = (eventsource_state_t *)user_data; if (!es || es->closed || !es->opened_request) return; + + es->network_encoded_length += len; + ant_inspector_network_append_response_body(es->network_request_id, chunk, len); + if (!ant_sse_parser_feed(&es->parser, (const char *)chunk, len, eventsource_on_message, es)) { eventsource_emit_error_once(es); ant_http_request_cancel(req); return; } + if (es->parser.has_retry) es->retry_ms = es->parser.retry; } @@ -275,6 +291,19 @@ static void eventsource_http_on_complete( if (!es) return; es->request = NULL; es->opened_request = false; + if (es->network_request_id) { + if (result == ANT_HTTP_RESULT_OK && error_code == 0) ant_inspector_network_finish( + es->network_request_id, + es->network_encoded_length + ); else ant_inspector_network_fail( + es->network_request_id, + error_message ? error_message : (es->closed ? "EventSource closed" : "EventSource connection failed"), + es->closed || result == ANT_HTTP_RESULT_ABORTED, + "EventSource" + ); + es->network_request_id = 0; + es->network_encoded_length = 0; + } if (!es->closed) { if (result != ANT_HTTP_RESULT_ABORTED) eventsource_emit_error_once(es); @@ -303,6 +332,13 @@ static void eventsource_start_request(eventsource_state_t *es) { accept.next = &last_id; } + es->network_encoded_length = 0; + es->network_request_id = ant_inspector_network_request( + "GET", es->url, + "EventSource", "EventSource", + false, &accept + ); + options.method = "GET"; options.url = es->url; options.headers = &accept; @@ -316,6 +352,8 @@ static void eventsource_start_request(eventsource_state_t *es) { ); if (rc != 0) { + ant_inspector_network_fail(es->network_request_id, uv_strerror(rc), false, "EventSource"); + es->network_request_id = 0; es->request = NULL; eventsource_emit_error_once(es); es->error_emitted_for_request = false; diff --git a/src/modules/fetch.c b/src/modules/fetch.c index 1edc69a..728e759 100644 --- a/src/modules/fetch.c +++ b/src/modules/fetch.c @@ -12,6 +12,7 @@ #include "ptr.h" #include "common.h" #include "errors.h" +#include "inspector.h" #include "internal.h" #include "runtime.h" #include "esm/remote.h" @@ -40,10 +41,14 @@ typedef struct fetch_request_s { int refs; int redirect_count; + bool settled; bool aborted; bool restart_pending; bool response_started; + + uint64_t network_request_id; + size_t network_encoded_length; } fetch_request_t; enum { FETCH_REQUEST_NATIVE_TAG = 0x46524551u }; // FREQ @@ -159,6 +164,11 @@ static const char *fetch_find_header_value(const ant_http_header_t *headers, con return NULL; } +static const char *fetch_response_mime_type(const ant_http_response_t *resp) { + const char *content_type = resp ? fetch_find_header_value(resp->headers, "content-type") : NULL; + return content_type ? content_type : ""; +} + static bool fetch_redirect_rewrites_to_get(int status, const char *method) { if (!method) return false; if (status == 303) return strcasecmp(method, "HEAD") != 0; @@ -462,6 +472,17 @@ static void fetch_http_on_response(ant_http_request_t *http_req, const ant_http_ return; } + url = fetch_build_request_url(request); + ant_inspector_network_response( + req->network_request_id, url, + resp->status, resp->status_text, + fetch_response_mime_type(resp), + "Fetch", resp->headers + ); + + free(url); + url = NULL; + if (fetch_is_redirect_status(resp->status)) { const char *location = fetch_find_header_value(resp->headers, "location"); const char *redirect_mode = request->redirect ? request->redirect : "follow"; @@ -521,13 +542,15 @@ static void fetch_http_on_response(ant_http_request_t *http_req, const ant_http_ static void fetch_http_on_body(ant_http_request_t *http_req, const uint8_t *chunk, size_t len, void *user_data) { fetch_request_t *req = (fetch_request_t *)user_data; ant_t *js = req->js; + ant_value_t stream = 0; ant_value_t controller = 0; ant_value_t value = 0; ant_value_t step = 0; - (void)http_req; if (req->aborted || !is_object_type(req->response_obj)) return; + req->network_encoded_length += len; + ant_inspector_network_append_response_body(req->network_request_id, chunk, len); stream = js_get_slot(req->response_obj, SLOT_RESPONSE_BODY_STREAM); if (!rs_is_stream(stream)) return; @@ -570,15 +593,26 @@ static void fetch_http_on_complete( req->http_req = NULL; if (req->restart_pending) { + ant_inspector_network_finish(req->network_request_id, req->network_encoded_length); + req->network_request_id = 0; + req->network_encoded_length = 0; req->restart_pending = false; fetch_start_http(req); return; } if (result != ANT_HTTP_RESULT_OK || error_code != 0) { + ant_inspector_network_fail( + req->network_request_id, + error_message ? error_message : "fetch failed", + result == ANT_HTTP_RESULT_ABORTED, + "Fetch" + ); + reason = fetch_transport_reason(req, result, error_message); if (is_object_type(req->response_obj)) fetch_error_response_body(req, reason); else fetch_reject(req, reason); + fetch_request_release(req); return; } @@ -591,6 +625,7 @@ static void fetch_http_on_complete( } } else fetch_reject(req, fetch_type_error(js, "fetch completed without a response")); + ant_inspector_network_finish(req->network_request_id, req->network_encoded_length); fetch_request_release(req); } @@ -814,6 +849,17 @@ static void fetch_start_http(fetch_request_t *req) { options.body = request->body_data; options.body_len = request->body_size; options.chunked_body = request->body_is_stream; + + req->network_encoded_length = 0; + req->network_request_id = ant_inspector_network_request( + request->method, url, + "Fetch", "Fetch", + request->has_body && !request->body_is_stream && request->body_data && request->body_size > 0, + headers + ); + + if (request->has_body && !request->body_is_stream && request->body_data && request->body_size > 0) + ant_inspector_network_set_request_body(req->network_request_id, request->body_data, request->body_size); rc = ant_http_request_start( uv_default_loop(), &options, @@ -825,6 +871,7 @@ static void fetch_start_http(fetch_request_t *req) { free(url); if (rc != 0) { + ant_inspector_network_fail(req->network_request_id, uv_strerror(rc), false, "Fetch"); fetch_reject(req, fetch_type_error(req->js, uv_strerror(rc))); fetch_request_release(req); return; diff --git a/src/modules/io.c b/src/modules/io.c index d99490c..5ad980b 100644 --- a/src/modules/io.c +++ b/src/modules/io.c @@ -25,6 +25,7 @@ #include "internal.h" #include "runtime.h" #include "utils.h" +#include "inspector.h" #include "gc/roots.h" #include "silver/engine.h" #include "modules/io.h" @@ -642,22 +643,27 @@ for (int i = 0; i < nargs; i++) { }} static ant_value_t js_console_log(ant_t *js, ant_value_t *args, int nargs) { + ant_inspector_console_api_called(js, "log", args, nargs); return console_emit_current(js, false, NULL, args, nargs); } static ant_value_t js_console_error(ant_t *js, ant_value_t *args, int nargs) { + ant_inspector_console_api_called(js, "error", args, nargs); return console_emit_current(js, true, NULL, args, nargs); } static ant_value_t js_console_warn(ant_t *js, ant_value_t *args, int nargs) { + ant_inspector_console_api_called(js, "warning", args, nargs); return console_emit_current(js, true, NULL, args, nargs); } static ant_value_t js_console_info(ant_t *js, ant_value_t *args, int nargs) { + ant_inspector_console_api_called(js, "info", args, nargs); return console_emit_current(js, false, NULL, args, nargs); } static ant_value_t js_console_debug(ant_t *js, ant_value_t *args, int nargs) { + ant_inspector_console_api_called(js, "debug", args, nargs); return console_emit_current(js, false, NULL, args, nargs); } @@ -665,11 +671,13 @@ static ant_value_t js_console_assert(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkundef(); bool is_truthy = js_truthy(js, args[0]); if (is_truthy) return js_mkundef(); + ant_inspector_console_api_called(js, "assert", args + 1, nargs - 1); return console_emit_current(js, true, "Assertion failed:", args + 1, nargs - 1); } static ant_value_t js_console_trace(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = console_get_effective_this(js, js_getthis(js)); + ant_inspector_console_api_called(js, "trace", args, nargs); console_emit_current(js, true, "Trace:", args, nargs); ant_value_t stack = js_capture_raw_stack(js); if (vtype(stack) == T_STR) { @@ -1244,32 +1252,48 @@ static ant_value_t js_console_constructor(ant_t *js, ant_value_t *args, int narg return console_obj; } -ant_value_t console_library(ant_t *js) { - if (!g_console_ctor) { - g_console_proto = js_mkobj(js); - console_apply_methods(js, g_console_proto); - js_set_sym(js, g_console_proto, get_toStringTag_sym(), js_mkstr(js, "console", 7)); - g_console_ctor = js_make_ctor(js, js_console_constructor, g_console_proto, "Console", 7); - gc_register_root(&g_console_proto); - gc_register_root(&g_console_ctor); - } +static void console_ensure_constructor(ant_t *js) { + if (g_console_ctor) return; + + g_console_proto = js_mkobj(js); + console_apply_methods(js, g_console_proto); + + js_set_sym(js, g_console_proto, get_toStringTag_sym(), js_mkstr(js, "console", 7)); + g_console_ctor = js_make_ctor(js, js_console_constructor, g_console_proto, "Console", 7); + + gc_register_root(&g_console_proto); + gc_register_root(&g_console_ctor); +} +static ant_value_t console_create_default(ant_t *js) { + console_ensure_constructor(js); ant_value_t console_obj = js_mkobj(js); + js_set_proto_init(console_obj, g_console_proto); js_set_slot_wb(js, console_obj, SLOT_CONSOLE_COUNTS, js_mkobj(js)); js_set_slot_wb(js, console_obj, SLOT_CONSOLE_TIMERS, js_mkobj(js)); js_set_slot(console_obj, SLOT_CONSOLE_GROUP_INDENT, js_mknum(2)); js_set_slot(console_obj, SLOT_CONSOLE_GROUP_LEVEL, js_mknum(0)); + js_set_sym(js, console_obj, get_toStringTag_sym(), js_mkstr(js, "console", 7)); + + return console_obj; +} + +ant_value_t console_library(ant_t *js) { + ant_value_t console_obj = console_create_default(js); js_set(js, console_obj, "Console", g_console_ctor); js_set(js, console_obj, "default", console_obj); - js_set_sym(js, console_obj, get_toStringTag_sym(), js_mkstr(js, "console", 7)); return console_obj; } void init_console_module() { ant_t *js = rt->js; - ant_value_t console_obj = console_library(js); + + ant_value_t console_obj = console_create_default(js); + console_apply_methods(js, console_obj); + + js_set(js, console_obj, "Console", g_console_ctor); js_set(js, js_glob(js), "console", console_obj); } diff --git a/src/modules/server.c b/src/modules/server.c index a9ca5de..e6612a8 100644 --- a/src/modules/server.c +++ b/src/modules/server.c @@ -10,6 +10,7 @@ #include #include "ant.h" +#include "inspector.h" #include "internal.h" #include "ptr.h" @@ -83,8 +84,12 @@ struct server_request_s { size_t consumed_len; int refs; + uint64_t network_request_id; + size_t network_encoded_length; + bool keep_alive; bool response_started; + bool network_finished; }; struct server_conn_state_s { @@ -230,6 +235,71 @@ static void server_request_release(server_request_t *req) { server_conn_state_maybe_free(req->conn_state); } +static char *server_request_url(server_request_t *req) { + request_data_t *data = req && is_object_type(req->request_obj) ? request_get_data(req->request_obj) : NULL; + return data ? build_href(&data->url) : NULL; +} + +static void server_network_start(server_request_t *req) { + request_data_t *data = req && is_object_type(req->request_obj) ? request_get_data(req->request_obj) : NULL; + char *url = server_request_url(req); + + if (!req || !data || !url) { + free(url); + return; + } + + req->network_request_id = ant_inspector_network_request( + data->method, url, + "Fetch", "Server", + data->has_body && data->body_data && data->body_size > 0, + req->raw_headers + ); + + req->network_encoded_length = 0; + req->network_finished = false; + + if (data->has_body && data->body_data && data->body_size > 0) ant_inspector_network_set_request_body( + req->network_request_id, + data->body_data, data->body_size + ); + + free(url); +} + +static void server_network_response( + server_request_t *req, int status, + const char *status_text, + const char *mime_type, + const ant_http_header_t *headers +) { + char *url = server_request_url(req); + if (!req || !url) { + free(url); + return; + } + + ant_inspector_network_response( + req->network_request_id, url, + status, status_text, + mime_type, "Fetch", headers + ); + + free(url); +} + +static void server_network_finish(server_request_t *req) { + if (!req || req->network_finished) return; + ant_inspector_network_finish(req->network_request_id, req->network_encoded_length); + req->network_finished = true; +} + +static void server_network_fail(server_request_t *req, const char *message) { + if (!req || req->network_finished) return; + ant_inspector_network_fail(req->network_request_id, message ? message : "server request failed", false, "Fetch"); + req->network_finished = true; +} + static server_request_t *server_find_request(server_runtime_t *server, ant_value_t request_obj) { server_request_t *req = NULL; if (!server || !is_object_type(request_obj)) return NULL; @@ -329,6 +399,66 @@ static ant_http_header_t *server_copy_raw_headers(const ant_http_header_t *heade return head; } +typedef struct { + ant_http_header_t *head; + ant_http_header_t **tail; + bool failed; +} server_header_capture_t; + +static void server_capture_header(const char *name, const char *value, void *ctx) { + server_header_capture_t *capture = (server_header_capture_t *)ctx; + ant_http_header_t *header = NULL; + + if (!capture || capture->failed || !name || !value) return; + header = calloc(1, sizeof(*header)); + if (!header) { + capture->failed = true; + return; + } + + header->name = strdup(name); + header->value = strdup(value); + + if (!header->name || !header->value) { + free(header->name); + free(header->value); + free(header); + capture->failed = true; + return; + } + + *capture->tail = header; + capture->tail = &header->next; +} + +static ant_http_header_t *server_capture_response_headers( + ant_value_t headers, bool body_is_stream, + size_t body_size, bool keep_alive +) { + server_header_capture_t capture = {0}; + char content_length[64]; + + capture.tail = &capture.head; + headers_for_each(headers, server_capture_header, &capture); + + if (!capture.failed) { + if (body_is_stream) server_capture_header("transfer-encoding", "chunked", &capture); + else { + snprintf(content_length, sizeof(content_length), "%zu", body_size); + server_capture_header("content-length", content_length, &capture); + }} + + if (!capture.failed) + server_capture_header("connection", keep_alive ? "keep-alive" : "close", &capture); + + if (capture.failed) { + ant_http_headers_free(capture.head); + return NULL; + } + + return capture.head; +} + static ant_value_t server_call_fetch(server_runtime_t *server, ant_value_t request_obj) { ant_t *js = server->js; ant_value_t args[2] = { request_obj, server->server_ctx }; @@ -670,6 +800,7 @@ static bool server_queue_write(ant_conn_t *conn, server_request_t *req, char *da int rc = 0; if (!conn || ant_conn_is_closing(conn)) { + if (req) server_network_fail(req, "connection closed"); free(data); return false; } @@ -682,10 +813,13 @@ static bool server_queue_write(ant_conn_t *conn, server_request_t *req, char *da wr->request = req; wr->conn = conn; wr->action = action; + if (req) server_request_retain(req); + if (req) req->network_encoded_length += len; rc = ant_conn_write(conn, data, len, server_write_cb, wr); if (rc != 0) { + if (req) server_network_fail(req, uv_strerror(rc)); if (req) server_request_release(req); free(wr); ant_conn_close(conn); @@ -803,6 +937,34 @@ static inline void server_send_internal_error(ant_conn_t *conn, const char *body ); } +static void server_send_request_internal_error(server_request_t *req, const char *body) { + ant_http1_buffer_t buf; + char *out = NULL; + size_t out_len = 0; + + const char *text = body ? body : "Internal Server Error"; + if (!req || !req->conn || ant_conn_is_closing(req->conn)) return; + + server_network_response(req, 500, "Internal Server Error", "text/plain;charset=UTF-8", NULL); + ant_inspector_network_append_response_body(req->network_request_id, (const uint8_t *)text, strlen(text)); + ant_http1_buffer_init(&buf); + + if (!ant_http1_write_basic_response( + &buf, 500, + "Internal Server Error", + "text/plain;charset=UTF-8", + (const uint8_t *)text, strlen(text), false + )) { + ant_http1_buffer_free(&buf); + server_network_fail(req, "failed to write internal error response"); + ant_conn_close(req->conn); + return; + } + + out = ant_http1_buffer_take(&buf, &out_len); + server_queue_write(req->conn, req, out, out_len, SERVER_WRITE_CLOSE_CLIENT); +} + static void server_finish_with_response(server_request_t *req, ant_value_t response_obj) { response_data_t *resp = response_get_data(response_obj); ant_value_t headers = response_get_headers(response_obj); @@ -812,6 +974,7 @@ static void server_finish_with_response(server_request_t *req, ant_value_t respo bool body_is_stream = resp && resp->body_is_stream && rs_is_stream(stream); bool head_only = false; + ant_http_header_t *network_headers = NULL; const char *status_text = NULL; ant_http1_buffer_t buf; @@ -820,19 +983,28 @@ static void server_finish_with_response(server_request_t *req, ant_value_t respo if (!req->conn || ant_conn_is_closing(req->conn)) return; if (!resp) { - server_send_internal_error(req->conn, "Invalid Response"); + server_send_request_internal_error(req, "Invalid Response"); return; } if (is_object_type(websocket_obj)) { + server_network_response(req, resp->status, "Switching Protocols", "", NULL); server_finish_websocket_upgrade(req, response_obj, websocket_obj); return; } req->response_obj = response_obj; req->response_started = true; + head_only = strcasecmp(request_get_data(req->request_obj)->method, "HEAD") == 0; status_text = (resp->status_text && resp->status_text[0]) ? resp->status_text : ant_http1_default_status_text(resp->status); + + network_headers = server_capture_response_headers(headers, body_is_stream && !head_only, resp->body_size, req->keep_alive); + server_network_response(req, resp->status, status_text, resp->body_type, network_headers); + ant_http_headers_free(network_headers); + + if (!body_is_stream && !head_only && resp->body_data && resp->body_size > 0) + ant_inspector_network_append_response_body(req->network_request_id, resp->body_data, resp->body_size); ant_http1_buffer_init(&buf); if (!ant_http1_write_response_head(&buf, resp->status, status_text, headers, body_is_stream, resp->body_size, req->keep_alive)) { @@ -876,7 +1048,7 @@ static ant_value_t server_on_response_reject(ant_t *js, ant_value_t *args, int n if (req->conn && !ant_conn_is_closing(req->conn)) { msg = js_str(js, reason); - server_send_internal_error(req->conn, msg); + server_send_request_internal_error(req, msg); } server_request_release(req); @@ -892,10 +1064,8 @@ static ant_value_t server_on_response_fulfill(ant_t *js, ant_value_t *args, int if (!response_get_data(value)) { if (req->conn && !ant_conn_is_closing(req->conn)) - server_send_internal_error(req->conn, "fetch handler must return a Response"); - } else if (req->conn && !ant_conn_is_closing(req->conn)) { - server_finish_with_response(req, value); - } + server_send_request_internal_error(req, "fetch handler must return a Response"); + } else if (req->conn && !ant_conn_is_closing(req->conn)) server_finish_with_response(req, value); server_request_release(req); return js_mkundef(); @@ -907,7 +1077,7 @@ static void server_handle_fetch_result(server_request_t *req, ant_value_t result if (is_err(result)) { ant_value_t reason = server_exception_reason(js, result); const char *msg = js_str(js, reason); - server_send_internal_error(req->conn, msg); + server_send_request_internal_error(req, msg); return; } @@ -924,7 +1094,7 @@ static void server_handle_fetch_result(server_request_t *req, ant_value_t result } if (!response_get_data(result)) { - server_send_internal_error(req->conn, "fetch handler must return a Response"); + server_send_request_internal_error(req, "fetch handler must return a Response"); return; } @@ -985,6 +1155,7 @@ static ant_value_t server_stream_read_fulfill(ant_t *js, ant_value_t *args, int server_request_release(req); return js_mkundef(); } + ant_inspector_network_append_response_body(req->network_request_id, chunk, chunk_len); ant_http1_buffer_init(&buf); if (!ant_http1_write_chunk(&buf, chunk, chunk_len)) { @@ -1049,8 +1220,8 @@ static void server_write_cb(ant_conn_t *conn, int status, void *user_data) { server_request_t *req = wr->request; server_conn_state_t *cs = conn ? (server_conn_state_t *)ant_conn_get_user_data(conn) : NULL; - if (status < 0 && conn && !ant_conn_is_closing(conn)) - ant_conn_close(conn); + if (status < 0 && conn && !ant_conn_is_closing(conn)) ant_conn_close(conn); + if (status < 0 && req) server_network_fail(req, uv_strerror(status)); if (status == 0 && conn && !ant_conn_is_closing(conn)) { switch (wr->action) { @@ -1063,6 +1234,7 @@ static void server_write_cb(ant_conn_t *conn, int status, void *user_data) { break; case SERVER_WRITE_KEEP_ALIVE: + if (req) server_network_finish(req); if (cs && req) { ant_conn_consume(conn, req->consumed_len); ant_http1_conn_parser_reset(&cs->parser); @@ -1075,6 +1247,7 @@ static void server_write_cb(ant_conn_t *conn, int status, void *user_data) { break; case SERVER_WRITE_WEBSOCKET_UPGRADE: + if (req) server_network_finish(req); if (cs && req) { ant_value_t websocket_obj = js_get(req->server->js, req->response_obj, "__antWebSocket"); ant_conn_consume(conn, req->consumed_len); @@ -1091,6 +1264,7 @@ static void server_write_cb(ant_conn_t *conn, int status, void *user_data) { break; case SERVER_WRITE_CLOSE_CLIENT: + if (req) server_network_finish(req); ant_conn_close(conn); break; @@ -1229,6 +1403,7 @@ static void server_process_client_request( req->next = server->requests; server->requests = req; cs->active_req = req; + server_network_start(req); ant_conn_pause_read(conn); ant_conn_set_timeout_ms(conn, server->request_timeout_ms); diff --git a/src/modules/websocket.c b/src/modules/websocket.c index 64aff74..0a099c9 100644 --- a/src/modules/websocket.c +++ b/src/modules/websocket.c @@ -12,6 +12,7 @@ #include "ant.h" #include "common.h" #include "errors.h" +#include "inspector.h" #include "internal.h" #include "ptr.h" #include "runtime.h" @@ -30,6 +31,7 @@ typedef struct websocket_state_s { tlsuv_websocket_t client; uv_connect_t connect_req; ant_conn_t *server_conn; + uint64_t inspector_request_id; struct websocket_state_s *next; uint8_t *fragment_buf; size_t fragment_len; @@ -195,6 +197,8 @@ static void websocket_emit_message(websocket_state_t *ws, const uint8_t *data, s static void websocket_emit_close(websocket_state_t *ws, uint16_t code, const char *reason, bool was_clean) { if (!ws || ws->close_emitted) return; + ant_inspector_websocket_closed(ws->inspector_request_id); + ws->close_emitted = true; ws->ready_state = WS_CLOSED; websocket_fragment_clear(ws); @@ -219,13 +223,21 @@ static void websocket_client_connect_cb(uv_connect_t *req, int status) { if (!ws) return; if (status == 0) { + ant_http_header_t upgrade = { "Upgrade", "websocket", NULL }; + ant_http_header_t connection = { "Connection", "Upgrade", &upgrade }; + + ant_inspector_websocket_response(ws->inspector_request_id, 101, "Switching Protocols", &connection); ws->ready_state = WS_OPEN; + websocket_sync_state(ws); websocket_emit_simple(ws, "open"); + return; } ws->ready_state = WS_CLOSED; + ant_inspector_websocket_error(ws->inspector_request_id, uv_strerror(status)); + websocket_sync_state(ws); websocket_emit_simple(ws, "error"); websocket_emit_close(ws, 1006, "", false); @@ -236,18 +248,21 @@ static void websocket_client_read_cb(uv_stream_t *handle, ssize_t nread, const u if (!ws) return; if (nread > 0) { + ant_inspector_websocket_frame_received(ws->inspector_request_id, (const uint8_t *)buf->base, (size_t)nread, false); websocket_emit_message(ws, (const uint8_t *)buf->base, (size_t)nread, false); return; } if (nread < 0) { - if (nread != UV_EOF) websocket_emit_simple(ws, "error"); + if (nread != UV_EOF) { + ant_inspector_websocket_error(ws->inspector_request_id, uv_strerror((int)nread)); + websocket_emit_simple(ws, "error"); + } websocket_emit_close(ws, nread == UV_EOF ? 1000 : 1006, "", nread == UV_EOF); } } static void websocket_write_cb(uv_write_t *req, int status) { - (void)status; free(req->data); free(req); } @@ -330,16 +345,26 @@ static ant_value_t js_websocket_ctor(ant_t *js, ant_value_t *args, int nargs) { js_set_native(obj, ws, WS_NATIVE_TAG); js_set(js, obj, "url", url_val); websocket_sync_state(ws); + + ws->inspector_request_id = ant_inspector_websocket_created(url); + ant_inspector_websocket_request(ws->inspector_request_id, NULL); if (tlsuv_websocket_init(uv_default_loop(), &ws->client) != 0) { ws->ready_state = WS_CLOSED; + ant_inspector_websocket_error(ws->inspector_request_id, "WebSocket initialization failed"); websocket_sync_state(ws); return obj; } - int rc = tlsuv_websocket_connect(&ws->connect_req, &ws->client, url, websocket_client_connect_cb, websocket_client_read_cb); + int rc = tlsuv_websocket_connect( + &ws->connect_req, &ws->client, url, + websocket_client_connect_cb, websocket_client_read_cb + ); + if (rc != 0) { ws->ready_state = WS_CLOSED; + ant_inspector_websocket_error(ws->inspector_request_id, uv_strerror(rc)); + websocket_sync_state(ws); websocket_emit_simple(ws, "error"); websocket_emit_close(ws, 1006, "", false); @@ -378,6 +403,7 @@ static ant_value_t js_websocket_send(ant_t *js, ant_value_t *args, int nargs) { if (ws->is_client) { int rc = websocket_client_write_frame(ws, binary ? ANT_WS_OPCODE_BINARY : ANT_WS_OPCODE_TEXT, bytes, len); if (rc != 0) return js_mkerr_typed(js, JS_ERR_TYPE, "%s", uv_strerror(rc)); + ant_inspector_websocket_frame_sent(ws->inspector_request_id, bytes, len, binary); return js_mkundef(); }