Просмотр исходного кода

Gsoc 2025 debugger (#390)

* implement `evaluate` request

* Update main.c

* Update main.c

* change port to 6110

* Update main.c

* simplify the workdir process and minor optimizations

* implement exit event

* fix bp not hit because of path format

* implement output event

* add `ctype.h` to `core.c`

* implement exception breakpoint

* fix assert triggered when called isspace

---------

Co-authored-by: blueloveTH <blueloveth@foxmail.com>
lightovernight 6 месяцев назад
Родитель
Сommit
e40c4af55c
4 измененных файлов с 175 добавлено и 43 удалено
  1. 4 2
      include/pocketpy/debugger/core.h
  2. 1 1
      src/common/sourcedata.c
  3. 86 18
      src/debugger/core.c
  4. 84 22
      src/debugger/dap.c

+ 4 - 2
include/pocketpy/debugger/core.h

@@ -6,7 +6,7 @@
 #include "pocketpy/pocketpy.h"
 
 typedef enum { C11_STEP_IN, C11_STEP_OVER, C11_STEP_OUT, C11_STEP_CONTINUE } C11_STEP_MODE;
-
+typedef enum { C11_DEBUGGER_NOSTOP, C11_DEBUGGER_STEP, C11_DEBUGGER_EXCEPTION, C11_DEBUGGER_BP} C11_STOP_REASON;
 typedef enum {
     C11_DEBUGGER_SUCCESS = 0,
     C11_DEBUGGER_EXIT = 1,
@@ -16,11 +16,13 @@ typedef enum {
 
 void c11_debugger_init(void);
 void c11_debugger_set_step_mode(C11_STEP_MODE mode);
+void c11_debugger_exception_on_trace(py_Ref exc);
 C11_DEBUGGER_STATUS c11_debugger_on_trace(py_Frame* frame, enum py_TraceEvent event);
+const char* c11_debugger_excinfo(const char ** message);
 void c11_debugger_frames(c11_sbuf* buffer);
 void c11_debugger_scopes(int frameid, c11_sbuf* buffer);
 bool c11_debugger_unfold_var(int var_id, c11_sbuf* buffer);
 int c11_debugger_setbreakpoint(const char* filename, int lineno);
 int c11_debugger_reset_breakpoints_by_source(const char* sourcesname);
-int c11_debugger_should_pause(void);
+C11_STOP_REASON c11_debugger_should_pause(void);
 int c11_debugger_should_keep_pause(void);

+ 1 - 1
src/common/sourcedata.c

@@ -26,7 +26,7 @@ static void SourceData__ctor(struct SourceData* self,
     self->source = c11_sbuf__submit(&ss);
     // remove trailing newline
     int last_index = self->source->size - 1;
-    while(last_index >= 0 && isspace(self->source->data[last_index])) {
+    while(last_index >= 0 && isspace((unsigned char)self->source->data[last_index])) {
         last_index--;
     }
     if(last_index >= 0) {

+ 86 - 18
src/debugger/core.c

@@ -1,4 +1,5 @@
 #include "pocketpy/interpreter/frame.h"
+#include "pocketpy/objects/exception.h"
 #include "pocketpy/pocketpy.h"
 #include <ctype.h>
 
@@ -26,6 +27,8 @@ typedef struct c11_debugger_scope_index {
 static struct c11_debugger {
     py_Frame* current_frame;
     const char* current_filename;
+    const char* current_excname;
+    const char* current_excmessage;
     enum py_TraceEvent current_event;
 
     int curr_stack_depth;
@@ -34,7 +37,9 @@ static struct c11_debugger {
     int step_line;
     C11_STEP_MODE step_mode;
     bool keep_suspend;
+    bool isexceptionmode;
 
+    c11_vector* exception_stacktrace;
     c11_vector breakpoints;
     c11_vector py_frames;
     c11_smallmap_d2index scopes_query_cache;
@@ -76,6 +81,7 @@ void c11_debugger_init() {
     debugger.pause_allowed_depth = -1;
     debugger.step_line = -1;
     debugger.keep_suspend = false;
+    debugger.isexceptionmode = false;
     debugger.step_mode = C11_STEP_CONTINUE;
     init_structures();
 }
@@ -96,6 +102,30 @@ C11_DEBUGGER_STATUS c11_debugger_on_trace(py_Frame* frame, enum py_TraceEvent ev
     return C11_DEBUGGER_SUCCESS;
 }
 
+void c11_debugger_exception_on_trace(py_Ref exc) {
+    BaseException* ud = py_touserdata(exc);
+    c11_vector* stacktrace = &ud->stacktrace;
+    const char* name = py_tpname(exc->type);
+    const char* message;
+    bool ok = py_str(exc);
+    if(!ok || !py_isstr(py_retval())) {
+        message = "<exception str() failed>";
+    } else {
+        message = c11_strdup(py_tostr(py_retval()));
+    }
+    debugger.exception_stacktrace = stacktrace;
+    debugger.isexceptionmode = true;
+    debugger.current_excname = name;
+    debugger.current_excmessage = message;
+    clear_structures();
+
+}
+
+const char* c11_debugger_excinfo(const char ** message){
+    *message = debugger.current_excmessage;
+    return debugger.current_excname;
+}
+
 void c11_debugger_set_step_mode(C11_STEP_MODE mode) {
     switch(mode) {
         case C11_STEP_IN: debugger.pause_allowed_depth = INT32_MAX; break;
@@ -105,12 +135,12 @@ void c11_debugger_set_step_mode(C11_STEP_MODE mode) {
             break;
         case C11_STEP_OUT: debugger.pause_allowed_depth = debugger.curr_stack_depth - 1; break;
         case C11_STEP_CONTINUE: debugger.pause_allowed_depth = -1; break;
+        default: break;
     }
     debugger.step_mode = mode;
     debugger.keep_suspend = false;
 }
 
-
 int c11_debugger_setbreakpoint(const char* filename, int lineno) {
     c11_debugger_breakpoint breakpoint = {.sourcename = c11_strdup(filename), .lineno = lineno};
     c11_vector__push(c11_debugger_breakpoint, &debugger.breakpoints, breakpoint);
@@ -137,33 +167,31 @@ int c11_debugger_reset_breakpoints_by_source(const char* sourcesname) {
 }
 
 bool c11_debugger_path_equal(const char* path1, const char* path2) {
-    if (path1 == NULL || path2 == NULL) return false;
-    while (*path1 && *path2) {
+    if(path1 == NULL || path2 == NULL) return false;
+    while(*path1 && *path2) {
         char c1 = (*path1 == '\\') ? '/' : *path1;
         char c2 = (*path2 == '\\') ? '/' : *path2;
         c1 = (char)tolower((unsigned char)c1);
         c2 = (char)tolower((unsigned char)c2);
-        if (c1 != c2) return false;
+        if(c1 != c2) return false;
         path1++;
         path2++;
     }
     return *path1 == *path2;
 }
 
-
-int c11_debugger_should_pause() {
-    if(debugger.current_event == TRACE_EVENT_POP) return false;
-    bool should_pause = false;
+C11_STOP_REASON c11_debugger_should_pause() {
+    if(debugger.current_event == TRACE_EVENT_POP && !debugger.isexceptionmode) return C11_DEBUGGER_NOSTOP;
+    C11_STOP_REASON pause_resaon = C11_DEBUGGER_NOSTOP;
     int is_out = debugger.curr_stack_depth <= debugger.pause_allowed_depth;
     int is_new_line = debugger.current_line != debugger.step_line;
     switch(debugger.step_mode) {
-        case C11_STEP_IN: should_pause = true; break;
-
+        case C11_STEP_IN: pause_resaon = C11_DEBUGGER_STEP; break;
         case C11_STEP_OVER:
-            if(is_new_line && is_out) should_pause = true;
+            if(is_new_line && is_out) pause_resaon = C11_DEBUGGER_STEP;
             break;
         case C11_STEP_OUT:
-            if(is_out) should_pause = true;
+            if(is_out) pause_resaon = C11_DEBUGGER_STEP;
             break;
         case C11_STEP_CONTINUE:
         default: break;
@@ -172,18 +200,18 @@ int c11_debugger_should_pause() {
         c11__foreach(c11_debugger_breakpoint, &debugger.breakpoints, bp) {
             if(c11_debugger_path_equal(debugger.current_filename, bp->sourcename) &&
                debugger.current_line == bp->lineno) {
-                should_pause = true;
+                pause_resaon = C11_DEBUGGER_BP;
                 break;
             }
         }
     }
-    if(should_pause) { debugger.keep_suspend = true; }
-    return should_pause;
+    if(debugger.isexceptionmode) pause_resaon = C11_DEBUGGER_EXCEPTION;
+    if(pause_resaon != C11_DEBUGGER_NOSTOP) { debugger.keep_suspend = true; }
+    return pause_resaon;
 }
 
 int c11_debugger_should_keep_pause(void) { return debugger.keep_suspend; }
 
-
 inline static c11_sv sv_from_cstr(const char* str) {
     c11_sv sv = {.data = str, .size = strlen(str)};
     return sv;
@@ -200,7 +228,7 @@ const inline static char* get_basename(const char* path) {
     return last_slash ? last_slash + 1 : path;
 }
 
-void c11_debugger_frames(c11_sbuf* buffer) {
+void c11_debugger_normal_frames(c11_sbuf* buffer) {
     c11_sbuf__write_cstr(buffer, "{\"stackFrames\": [");
     int idx = 0;
     py_Frame* now_frame = debugger.current_frame;
@@ -226,6 +254,33 @@ void c11_debugger_frames(c11_sbuf* buffer) {
     pk_sprintf(buffer, "], \"totalFrames\": %d}", idx);
 }
 
+void c11_debugger_exception_frames(c11_sbuf* buffer) {
+    c11_sbuf__write_cstr(buffer, "{\"stackFrames\": [");
+    int idx = 0;
+    c11__foreach(BaseExceptionFrame, debugger.exception_stacktrace, it) {
+        if(idx > 0) c11_sbuf__write_char(buffer, ',');
+        int line = it->lineno;
+        const char* filename = it->src->filename->data;
+        const char* basename = get_basename(filename);
+        const char* modname = it->name == NULL ? basename : it->name->data;
+        pk_sprintf(
+            buffer,
+            "{\"id\": %d, \"name\": %Q, \"line\": %d, \"column\": 1, \"source\": {\"name\": %Q, \"path\": %Q}}",
+            idx,
+            sv_from_cstr(modname),
+            line,
+            sv_from_cstr(basename),
+            sv_from_cstr(filename));
+        idx++;
+    }
+    pk_sprintf(buffer, "], \"totalFrames\": %d}", idx);
+}
+
+void c11_debugger_frames(c11_sbuf* buffer) {
+    debugger.isexceptionmode ? c11_debugger_exception_frames(buffer)
+                             : c11_debugger_normal_frames(buffer);
+}
+
 inline static c11_debugger_scope_index append_new_scope(int frameid) {
     assert(frameid < debugger.py_frames.length);
     py_Frame* requested_frame = c11__getitem(py_Frame*, &debugger.py_frames, frameid);
@@ -238,6 +293,17 @@ inline static c11_debugger_scope_index append_new_scope(int frameid) {
     return result;
 }
 
+inline static c11_debugger_scope_index append_new_exception_scope(int frameid) {
+    assert(frameid < debugger.exception_stacktrace->length);
+    BaseExceptionFrame* requested_frame =
+        c11__at(BaseExceptionFrame, debugger.exception_stacktrace, frameid);
+    int base_index = py_list_len(python_vars);
+    py_list_append(python_vars, &requested_frame->locals);
+    py_list_append(python_vars, &requested_frame->globals);
+    c11_debugger_scope_index result = {.locals_ref = base_index, .globals_ref = base_index + 1};
+    return result;
+}
+
 void c11_debugger_scopes(int frameid, c11_sbuf* buffer) {
     // query cache
     c11_debugger_scope_index* result =
@@ -250,7 +316,9 @@ void c11_debugger_scopes(int frameid, c11_sbuf* buffer) {
     if(result != NULL) {
         pk_sprintf(buffer, scopes_fmt, result->locals_ref, result->globals_ref);
     } else {
-        c11_debugger_scope_index new_record = append_new_scope(frameid);
+        c11_debugger_scope_index new_record = debugger.isexceptionmode
+                                                  ? append_new_exception_scope(frameid)
+                                                  : append_new_scope(frameid);
         c11_smallmap_d2index__set(&debugger.scopes_query_cache, frameid, new_record);
         pk_sprintf(buffer, scopes_fmt, new_record.locals_ref, new_record.globals_ref);
     }

+ 84 - 22
src/debugger/dap.c

@@ -21,7 +21,8 @@
     X(threads)                                                                                     \
     X(configurationDone)                                                                           \
     X(ready)                                                                                       \
-    X(evaluate)
+    X(evaluate)                                                                                    \
+    X(exceptionInfo)
 
 #define DECLARE_HANDLE_FN(name) void c11_dap_handle_##name(py_Ref arguments, c11_sbuf*);
 DAP_COMMAND_LIST(DECLARE_HANDLE_FN)
@@ -49,18 +50,23 @@ static struct c11_dap_server {
     c11_socket_handler server;
     c11_socket_handler toclient;
     bool isconfiguredone;
-    bool isatttach;
+    bool isfirstatttach;
+    bool isattach;
     bool isclientready;
 } server;
 
 void c11_dap_handle_initialize(py_Ref arguments, c11_sbuf* buffer) {
-    c11_sbuf__write_cstr(buffer, "\"body\":{\"supportsConfigurationDoneRequest\":true}");
+    c11_sbuf__write_cstr(buffer,
+                         "\"body\":{"
+                         "\"supportsConfigurationDoneRequest\":true,"
+                         "\"supportsExceptionInfoRequest\":true"
+                         "}");
     c11_sbuf__write_char(buffer, ',');
 }
 
-void c11_dap_handle_attach(py_Ref arguments, c11_sbuf* buffer) { server.isatttach = true; }
+void c11_dap_handle_attach(py_Ref arguments, c11_sbuf* buffer) { server.isfirstatttach = true; }
 
-void c11_dap_handle_launch(py_Ref arguments, c11_sbuf* buffer) { server.isatttach = true; }
+void c11_dap_handle_launch(py_Ref arguments, c11_sbuf* buffer) { server.isfirstatttach = true; }
 
 void c11_dap_handle_ready(py_Ref arguments, c11_sbuf* buffer) { server.isclientready = true; }
 
@@ -120,6 +126,38 @@ void c11_dap_handle_setBreakpoints(py_Ref arguments, c11_sbuf* buffer) {
     PK_FREE((void*)sourcename);
 }
 
+inline static void c11_dap_build_ExceptionInfo(const char* exc_type, const char* exc_message, c11_sbuf* buffer) {
+    const char* safe_type = exc_type ? exc_type : "UnknownException";
+    const char* safe_message = exc_message ? exc_message : "No additional details available";
+
+    c11_sv type_sv = {.data = safe_type, .size = strlen(safe_type)};
+    c11_sv message_sv = {.data = safe_message, .size = strlen(safe_message)};
+
+    c11_sbuf combined_buffer;
+    c11_sbuf__ctor(&combined_buffer);
+    pk_sprintf(&combined_buffer, "%s: %s", safe_type, safe_message);
+    c11_string* combined_details = c11_sbuf__submit(&combined_buffer);
+
+    pk_sprintf(buffer, 
+        "{\"exceptionId\":%Q,"
+        "\"description\":%Q,"
+        "\"breakMode\":\"unhandled\"}",
+        type_sv,
+        (c11_sv){.data = combined_details->data, .size = combined_details->size}
+    );
+
+    c11_string__delete(combined_details);
+}
+
+void c11_dap_handle_exceptionInfo(py_Ref arguments, c11_sbuf* buffer) {
+    const char* message;
+    const char* name = c11_debugger_excinfo(&message);
+    c11_sbuf__write_cstr(buffer, "\"body\":");
+    c11_dap_build_ExceptionInfo(name, message, buffer);
+    c11_sbuf__write_char(buffer, ',');
+}
+
+
 void c11_dap_handle_stackTrace(py_Ref arguments, c11_sbuf* buffer) {
     c11_sbuf__write_cstr(buffer, "\"body\":");
     c11_debugger_frames(buffer);
@@ -132,7 +170,6 @@ void c11_dap_handle_evaluate(py_Ref arguments, c11_sbuf* buffer) {
         py_printexc();
         c11__abort("[DEBUGGER ERROR] no expression found in evaluate request");
     }
-
     // [eval, nil, expression,  globals, locals]
     // vectorcall would pop the above 5 items
     // so we don't need to pop them manually
@@ -153,7 +190,7 @@ void c11_dap_handle_evaluate(py_Ref arguments, c11_sbuf* buffer) {
         py_str(py_retval());
         result = c11_strdup(py_tostr(py_retval()));
     }
-    
+
     c11_sv result_sv = {.data = result, .size = strlen(result)};
     pk_sprintf(buffer, "{\"result\":%Q,\"variablesReference\":0}", result_sv);
     PK_FREE((void*)result);
@@ -289,12 +326,24 @@ void c11_dap_send_output_event(const char* category, const char* fmt, ...) {
     c11_string__delete(output_json);
 }
 
-void c11_dap_send_stop_event() {
-    c11_dap_send_event("stopped", "{\"threadId\":1,\"allThreadsStopped\":true}");
+void c11_dap_send_stop_event(C11_STOP_REASON reason) {
+    if(reason == C11_DEBUGGER_NOSTOP) return;
+    const char* reason_str = "unknown";
+    switch(reason) {
+        case C11_DEBUGGER_STEP: reason_str = "step"; break;
+        case C11_DEBUGGER_EXCEPTION: reason_str = "exception"; break;
+        case C11_DEBUGGER_BP: reason_str = "breakpoint"; break;
+        default: break;
+    }
+    c11_sbuf buf;
+    c11_sbuf__ctor(&buf);
+    pk_sprintf(&buf, "{\"reason\":\"%s\",\"threadId\":1,\"allThreadsStopped\":true", reason_str);
+    c11_sbuf__write_char(&buf, '}');
+    c11_string* body_json = c11_sbuf__submit(&buf);
+    c11_dap_send_event("stopped", body_json->data);
+    c11_string__delete(body_json);
 }
 
-
-
 void c11_dap_send_exited_event(int exitCode) {
     char body[64];
     snprintf(body, sizeof(body), "{\"exitCode\":%d}", exitCode);
@@ -382,9 +431,11 @@ const char* c11_dap_read_message() {
 void c11_dap_init_server(const char* hostname, unsigned short port) {
     server.dap_next_seq = 1;
     server.isconfiguredone = false;
+    server.isclientready = false;
+    server.isfirstatttach = false;
+    server.isattach = false;
     server.buffer_begin = server.buffer_data;
     server.server = c11_socket_create(C11_AF_INET, C11_SOCK_STREAM, 0);
-    server.isclientready = false;
     c11_socket_bind(server.server, hostname, port);
     c11_socket_listen(server.server, 0);
     // c11_dap_send_output_event("console", "[DEBUGGER INFO] : listen on %s:%hu\n",hostname,port);
@@ -418,9 +469,10 @@ inline static void c11_dap_handle_message() {
 void c11_dap_configure_debugger() {
     while(server.isconfiguredone == false) {
         c11_dap_handle_message();
-        if(server.isatttach) {
+        if(server.isfirstatttach) {
             c11_dap_send_initialized_event();
-            server.isatttach = false;
+            server.isfirstatttach = false;
+            server.isattach = true;
         } else if(server.isclientready) {
             server.isclientready = false;
             return;
@@ -431,6 +483,7 @@ void c11_dap_configure_debugger() {
 
 void c11_dap_tracefunc(py_Frame* frame, enum py_TraceEvent event) {
     py_sys_settrace(NULL, false);
+    server.isattach = false;
     C11_DEBUGGER_STATUS result = c11_debugger_on_trace(frame, event);
     if(result == C11_DEBUGGER_EXIT) {
         // c11_dap_send_output_event("console", "[DEBUGGER INFO] : program exit\n");
@@ -451,14 +504,16 @@ void c11_dap_tracefunc(py_Frame* frame, enum py_TraceEvent event) {
         exit(1);
     }
     c11_dap_handle_message();
-    if(!c11_debugger_should_pause()) {
+    C11_STOP_REASON reason = c11_debugger_should_pause();
+    if(reason != C11_DEBUGGER_NOSTOP) {
         py_sys_settrace(c11_dap_tracefunc, false);
         return;
     }
-    c11_dap_send_stop_event();
+    c11_dap_send_stop_event(reason);
     while(c11_debugger_should_keep_pause()) {
         c11_dap_handle_message();
     }
+    server.isattach = true;
     py_sys_settrace(c11_dap_tracefunc, false);
 }
 
@@ -479,13 +534,20 @@ void py_debugger_waitforattach(const char* hostname, unsigned short port) {
 
 void py_debugger_exit(int exitCode) { c11_dap_send_exited_event(exitCode); }
 
-bool py_debugger_isattached() {
-    return false;  // TODO: implement this function
-}
+bool py_debugger_isattached() { return server.isattach; }
 
 void py_debugger_exceptionbreakpoint(py_Ref exc) {
     assert(py_isinstance(exc, tp_BaseException));
-    // BaseException* ud = py_touserdata(exc);
-    // c11_vector* stacktrace = &ud->stacktrace;
-    // TODO: implement this function
+
+    py_sys_settrace(NULL, true);
+    server.isattach = false;
+
+    c11_debugger_exception_on_trace(exc);
+    for(;;) {
+        C11_STOP_REASON reason = c11_debugger_should_pause();
+        c11_dap_send_stop_event(reason);
+        while(c11_debugger_should_keep_pause()) {
+            c11_dap_handle_message();
+        }
+    }
 }