github.com/suraciii/debug-skill
Advanced tools
+95
-204
@@ -6,2 +6,3 @@ package dap | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
@@ -15,2 +16,43 @@ "os/exec" | ||
| // waitForReady scans pipe lines for readyString, extracts an address via parseAddr, | ||
| // and kills cmd on timeout (10s) or early exit. Caller must have already started cmd. | ||
| func waitForReady(cmd *exec.Cmd, pipe io.ReadCloser, readyString string, parseAddr func(line string) string) (string, error) { | ||
| scanner := bufio.NewScanner(pipe) | ||
| addrCh := make(chan string, 1) | ||
| go func() { | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| if strings.Contains(line, readyString) { | ||
| addrCh <- parseAddr(line) | ||
| for scanner.Scan() { | ||
| } | ||
| return | ||
| } | ||
| } | ||
| close(addrCh) | ||
| }() | ||
| select { | ||
| case addr, ok := <-addrCh: | ||
| if !ok || addr == "" { | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return "", fmt.Errorf("process exited without reporting listen address") | ||
| } | ||
| return addr, nil | ||
| case <-time.After(10 * time.Second): | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return "", fmt.Errorf("process did not start within 10s") | ||
| } | ||
| } | ||
| // normalizePort ensures port has a ":" prefix and returns both forms. | ||
| func normalizePort(port string) (withColon, bare string) { | ||
| if !strings.HasPrefix(port, ":") { | ||
| port = ":" + port | ||
| } | ||
| return port, strings.TrimPrefix(port, ":") | ||
| } | ||
| // Backend abstracts the debugger-specific logic for spawning a DAP server | ||
@@ -23,3 +65,2 @@ // and building launch/attach argument maps. | ||
| LaunchArgs(program string, stopOnEntry bool, args []string) (launchArgs map[string]any, cleanup func(), err error) | ||
| AttachArgs(pid int) (map[string]any, error) | ||
| RemoteAttachArgs(host string, port int) (map[string]any, error) | ||
@@ -68,13 +109,7 @@ // StopOnEntryBreakpoint returns a function name to use as a breakpoint | ||
| func (b *debugpyBackend) Spawn(port string) (*exec.Cmd, string, error) { | ||
| if !strings.HasPrefix(port, ":") { | ||
| port = ":" + port | ||
| } | ||
| actualPort := strings.TrimPrefix(port, ":") | ||
| _, actualPort := normalizePort(port) | ||
| // Use debugpy.adapter in debugServer mode (standalone DAP server). | ||
| // This is how VS Code launches debugpy - it listens for DAP connections on TCP. | ||
| cmd := exec.Command("python3", "-m", "debugpy.adapter", "--host", "127.0.0.1", "--port", actualPort, "--log-stderr") | ||
| cmd.Stdout = nil | ||
| // Capture stderr to detect readiness ("Listening for incoming Client connections") | ||
| stderrPipe, err := cmd.StderrPipe() | ||
@@ -84,38 +119,13 @@ if err != nil { | ||
| } | ||
| if err := cmd.Start(); err != nil { | ||
| return nil, "", fmt.Errorf("starting debugpy adapter: %w", err) | ||
| return nil, "", fmt.Errorf("starting debugpy: %w", err) | ||
| } | ||
| // Wait for "Listening" message on stderr | ||
| scanner := bufio.NewScanner(stderrPipe) | ||
| ready := make(chan struct{}) | ||
| go func() { | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| if strings.Contains(line, "Listening") { | ||
| close(ready) | ||
| // Keep draining to avoid blocking the adapter | ||
| for scanner.Scan() { | ||
| } | ||
| return | ||
| } | ||
| } | ||
| // If scanner ends without finding "Listening", close ready anyway | ||
| select { | ||
| case <-ready: | ||
| default: | ||
| close(ready) | ||
| } | ||
| }() | ||
| select { | ||
| case <-ready: | ||
| case <-time.After(10 * time.Second): | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("debugpy adapter did not start within 10s") | ||
| addr, err := waitForReady(cmd, stderrPipe, "Listening", func(string) string { | ||
| return "127.0.0.1:" + actualPort | ||
| }) | ||
| if err != nil { | ||
| return nil, "", fmt.Errorf("starting debugpy: %w", err) | ||
| } | ||
| return cmd, "127.0.0.1:" + actualPort, nil | ||
| return cmd, addr, nil | ||
| } | ||
@@ -147,9 +157,2 @@ | ||
| func (b *debugpyBackend) AttachArgs(pid int) (map[string]any, error) { | ||
| return map[string]any{ | ||
| "request": "attach", | ||
| "processId": pid, | ||
| }, nil | ||
| } | ||
| func (b *debugpyBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) { | ||
@@ -173,5 +176,3 @@ return map[string]any{ | ||
| if !strings.HasPrefix(port, ":") { | ||
| port = ":" + port | ||
| } | ||
| port, _ = normalizePort(port) | ||
@@ -181,3 +182,2 @@ cmd := exec.Command("dlv", "dap", "--listen", port) | ||
| // Capture stdout to detect "DAP server listening at:" | ||
| stdoutPipe, err := cmd.StdoutPipe() | ||
@@ -187,3 +187,2 @@ if err != nil { | ||
| } | ||
| if err := cmd.Start(); err != nil { | ||
@@ -193,38 +192,14 @@ return nil, "", fmt.Errorf("starting dlv: %w", err) | ||
| // Parse listen address from stdout | ||
| scanner := bufio.NewScanner(stdoutPipe) | ||
| addrCh := make(chan string, 1) | ||
| go func() { | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| if strings.Contains(line, "DAP server listening at:") { | ||
| // Format: "DAP server listening at: [::]:PORT" or "DAP server listening at: 127.0.0.1:PORT" | ||
| idx := strings.Index(line, "DAP server listening at:") | ||
| addr := strings.TrimSpace(line[idx+len("DAP server listening at:"):]) | ||
| // Normalize [::]:PORT to 127.0.0.1:PORT | ||
| if strings.HasPrefix(addr, "[::]") { | ||
| addr = "127.0.0.1" + addr[4:] | ||
| } | ||
| addrCh <- addr | ||
| for scanner.Scan() { | ||
| } | ||
| return | ||
| } | ||
| addr, err := waitForReady(cmd, stdoutPipe, "DAP server listening at:", func(line string) string { | ||
| idx := strings.Index(line, "DAP server listening at:") | ||
| a := strings.TrimSpace(line[idx+len("DAP server listening at:"):]) | ||
| if strings.HasPrefix(a, "[::]") { | ||
| a = "127.0.0.1" + a[4:] | ||
| } | ||
| close(addrCh) | ||
| }() | ||
| select { | ||
| case addr, ok := <-addrCh: | ||
| if !ok || addr == "" { | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("dlv exited without reporting listen address") | ||
| } | ||
| return cmd, addr, nil | ||
| case <-time.After(10 * time.Second): | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("dlv did not start within 10s") | ||
| return a | ||
| }) | ||
| if err != nil { | ||
| return nil, "", fmt.Errorf("starting dlv: %w", err) | ||
| } | ||
| return cmd, addr, nil | ||
| } | ||
@@ -285,10 +260,2 @@ | ||
| func (b *delveBackend) AttachArgs(pid int) (map[string]any, error) { | ||
| return map[string]any{ | ||
| "request": "attach", | ||
| "mode": "local", | ||
| "processId": pid, | ||
| }, nil | ||
| } | ||
| func (b *delveBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) { | ||
@@ -382,10 +349,7 @@ return map[string]any{ | ||
| if !strings.HasPrefix(port, ":") { | ||
| port = ":" + port | ||
| } | ||
| actualPort := strings.TrimPrefix(port, ":") | ||
| _, actualPort := normalizePort(port) | ||
| cmd := exec.Command(binary, "--connection", fmt.Sprintf("listen://127.0.0.1:%s", actualPort)) | ||
| cmd.Stderr = nil | ||
| // Capture stdout to detect "Listening for: connection://..." | ||
| stdoutPipe, err := cmd.StdoutPipe() | ||
@@ -395,4 +359,2 @@ if err != nil { | ||
| } | ||
| cmd.Stderr = nil | ||
| if err := cmd.Start(); err != nil { | ||
@@ -402,41 +364,15 @@ return nil, "", fmt.Errorf("starting lldb-dap: %w", err) | ||
| // Parse listen address from stdout | ||
| scanner := bufio.NewScanner(stdoutPipe) | ||
| addrCh := make(chan string, 1) | ||
| go func() { | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| // Format: "Listening for: connection://[127.0.0.1]:PORT" | ||
| if strings.Contains(line, "Listening") { | ||
| // Extract host:port from the connection URL | ||
| if idx := strings.Index(line, "connection://"); idx >= 0 { | ||
| raw := line[idx+len("connection://"):] | ||
| // Handle bracketed addresses like [127.0.0.1]:PORT | ||
| raw = strings.ReplaceAll(raw, "[", "") | ||
| raw = strings.ReplaceAll(raw, "]", "") | ||
| addrCh <- raw | ||
| } else { | ||
| addrCh <- "127.0.0.1:" + actualPort | ||
| } | ||
| for scanner.Scan() { | ||
| } | ||
| return | ||
| } | ||
| addr, err := waitForReady(cmd, stdoutPipe, "Listening", func(line string) string { | ||
| if idx := strings.Index(line, "connection://"); idx >= 0 { | ||
| raw := line[idx+len("connection://"):] | ||
| raw = strings.ReplaceAll(raw, "[", "") | ||
| raw = strings.ReplaceAll(raw, "]", "") | ||
| return raw | ||
| } | ||
| close(addrCh) | ||
| }() | ||
| select { | ||
| case addr, ok := <-addrCh: | ||
| if !ok || addr == "" { | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("lldb-dap exited without reporting listen address") | ||
| } | ||
| return cmd, addr, nil | ||
| case <-time.After(10 * time.Second): | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("lldb-dap did not start within 10s") | ||
| return "127.0.0.1:" + actualPort | ||
| }) | ||
| if err != nil { | ||
| return nil, "", fmt.Errorf("starting lldb-dap: %w", err) | ||
| } | ||
| return cmd, addr, nil | ||
| } | ||
@@ -502,8 +438,2 @@ | ||
| func (b *lldbBackend) AttachArgs(pid int) (map[string]any, error) { | ||
| return map[string]any{ | ||
| "pid": pid, | ||
| }, nil | ||
| } | ||
| func (b *lldbBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) { | ||
@@ -523,10 +453,7 @@ return nil, fmt.Errorf("lldb-dap does not support remote attach") | ||
| if !strings.HasPrefix(port, ":") { | ||
| port = ":" + port | ||
| } | ||
| actualPort := strings.TrimPrefix(port, ":") | ||
| _, actualPort := normalizePort(port) | ||
| cmd := exec.Command("node", serverPath, actualPort) | ||
| cmd.Stderr = nil | ||
| // Capture stdout to detect "Listening at HOST:PORT" | ||
| stdoutPipe, err := cmd.StdoutPipe() | ||
@@ -536,4 +463,2 @@ if err != nil { | ||
| } | ||
| cmd.Stderr = nil | ||
| if err := cmd.Start(); err != nil { | ||
@@ -543,50 +468,24 @@ return nil, "", fmt.Errorf("starting js-debug: %w", err) | ||
| scanner := bufio.NewScanner(stdoutPipe) | ||
| addrCh := make(chan string, 1) | ||
| go func() { | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| // js-debug prints: "Debug server listening at HOST:PORT" | ||
| // e.g. "Debug server listening at ::1:12345" | ||
| if strings.Contains(line, "Debug server listening at") { | ||
| parts := strings.Fields(line) | ||
| addr := "127.0.0.1:" + actualPort | ||
| if len(parts) > 0 { | ||
| raw := parts[len(parts)-1] | ||
| // Extract port from the last colon (handles IPv6 like "::1:PORT") | ||
| if idx := strings.LastIndex(raw, ":"); idx >= 0 { | ||
| port := raw[idx+1:] | ||
| host := raw[:idx] | ||
| // Normalize: use the original host but bracket IPv6 | ||
| if strings.Contains(host, ":") { | ||
| addr = "[" + host + "]:" + port | ||
| } else if host == "" { | ||
| addr = "127.0.0.1:" + port | ||
| } else { | ||
| addr = host + ":" + port | ||
| } | ||
| } | ||
| } | ||
| addrCh <- addr | ||
| for scanner.Scan() { | ||
| } | ||
| return | ||
| addr, err := waitForReady(cmd, stdoutPipe, "Debug server listening at", func(line string) string { | ||
| parts := strings.Fields(line) | ||
| if len(parts) == 0 { | ||
| return "127.0.0.1:" + actualPort | ||
| } | ||
| raw := parts[len(parts)-1] | ||
| if idx := strings.LastIndex(raw, ":"); idx >= 0 { | ||
| p := raw[idx+1:] | ||
| host := raw[:idx] | ||
| if strings.Contains(host, ":") { | ||
| return "[" + host + "]:" + p | ||
| } else if host == "" { | ||
| return "127.0.0.1:" + p | ||
| } | ||
| return host + ":" + p | ||
| } | ||
| close(addrCh) | ||
| }() | ||
| select { | ||
| case addr, ok := <-addrCh: | ||
| if !ok || addr == "" { | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("js-debug exited without reporting listen address") | ||
| } | ||
| return cmd, addr, nil | ||
| case <-time.After(10 * time.Second): | ||
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
| return nil, "", fmt.Errorf("js-debug did not start within 10s") | ||
| return "127.0.0.1:" + actualPort | ||
| }) | ||
| if err != nil { | ||
| return nil, "", fmt.Errorf("starting js-debug: %w", err) | ||
| } | ||
| return cmd, addr, nil | ||
| } | ||
@@ -617,10 +516,2 @@ | ||
| func (b *jsDebugBackend) AttachArgs(pid int) (map[string]any, error) { | ||
| return map[string]any{ | ||
| "type": "pwa-node", | ||
| "request": "attach", | ||
| "processId": pid, | ||
| }, nil | ||
| } | ||
| func (b *jsDebugBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) { | ||
@@ -627,0 +518,0 @@ return map[string]any{ |
@@ -157,3 +157,3 @@ # CLI API Reference | ||
| **Flags:** | ||
| - `--break-on-exception <filter>` — Set exception breakpoint filters (repeatable, replaces current filters) | ||
| - `--break-on-exception <filter>` — Add exception breakpoint filters (repeatable, merged with existing) | ||
@@ -160,0 +160,0 @@ ```bash |
+34
-102
@@ -125,2 +125,19 @@ package dap | ||
| // runDaemonCommand marshals args, sends command to daemon, prints response, exits on error. | ||
| func runDaemonCommand(command string, args any) error { | ||
| var rawArgs json.RawMessage | ||
| if args != nil { | ||
| rawArgs, _ = json.Marshal(args) | ||
| } | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: command, Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| } | ||
| // addBreakpointFlags registers --break, --remove-break, --break-on-exception flags on a command. | ||
@@ -131,3 +148,3 @@ func addBreakpointFlags(cmd *cobra.Command, breaks, removeBreaks *breakpointFlag, exceptionFilters *[]string) { | ||
| cmd.Flags().StringArrayVar(exceptionFilters, "break-on-exception", nil, | ||
| "Set exception breakpoints (repeatable, replaces current).\n"+ | ||
| "Add exception breakpoint filters (repeatable, merged with existing).\n"+ | ||
| "Filter IDs are backend-specific (see 'dap debug --help').") | ||
@@ -288,17 +305,6 @@ } | ||
| } | ||
| stepArgs := StepArgs{ | ||
| return runDaemonCommand("step", StepArgs{ | ||
| Mode: mode, | ||
| BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters), | ||
| } | ||
| rawArgs, _ := json.Marshal(stepArgs) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "step", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }) | ||
| }, | ||
@@ -327,3 +333,3 @@ } | ||
| --remove-break removes specific breakpoints | ||
| --break-on-exception replaces exception breakpoint filters`, | ||
| --break-on-exception adds exception breakpoint filters (merged with existing)`, | ||
| Example: ` dap continue | ||
@@ -337,15 +343,5 @@ dap continue --break app.py:42 # add a breakpoint and continue | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| contArgs := ContinueArgs{ | ||
| return runDaemonCommand("continue", ContinueArgs{ | ||
| BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters), | ||
| } | ||
| rawArgs, _ := json.Marshal(contArgs) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "continue", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }) | ||
| }, | ||
@@ -377,16 +373,6 @@ } | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctxArgs := ContextArgs{ | ||
| return runDaemonCommand("context", ContextArgs{ | ||
| Frame: frame, | ||
| BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters), | ||
| } | ||
| rawArgs, _ := json.Marshal(ctxArgs) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "context", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }) | ||
| }, | ||
@@ -419,17 +405,7 @@ } | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| evalArgs := EvalArgs{ | ||
| return runDaemonCommand("eval", EvalArgs{ | ||
| Expression: args[0], | ||
| Frame: frame, | ||
| BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters), | ||
| } | ||
| rawArgs, _ := json.Marshal(evalArgs) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "eval", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }) | ||
| }, | ||
@@ -461,15 +437,5 @@ } | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| outArgs := OutputArgs{ | ||
| return runDaemonCommand("output", OutputArgs{ | ||
| BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters), | ||
| } | ||
| rawArgs, _ := json.Marshal(outArgs) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "output", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }) | ||
| }, | ||
@@ -496,11 +462,3 @@ } | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_list"}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| return runDaemonCommand("break_list", nil) | ||
| }, | ||
@@ -537,15 +495,6 @@ } | ||
| } | ||
| rawArgs, _ := json.Marshal(BreakAddArgs{ | ||
| return runDaemonCommand("break_add", BreakAddArgs{ | ||
| Breaks: allBreaks, | ||
| ExceptionFilters: addExceptionFilters, | ||
| }) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_add", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }, | ||
@@ -555,3 +504,3 @@ } | ||
| addCmd.Flags().StringArrayVar(&addExceptionFilters, "break-on-exception", nil, | ||
| "Set exception breakpoint filters (repeatable, replaces current filters). Filter IDs are backend-specific (see 'dap debug --help').") | ||
| "Add exception breakpoint filters (repeatable, merged with existing). Filter IDs are backend-specific (see 'dap debug --help').") | ||
@@ -586,15 +535,6 @@ // break remove | ||
| } | ||
| rawArgs, _ := json.Marshal(BreakRemoveArgs{ | ||
| return runDaemonCommand("break_remove", BreakRemoveArgs{ | ||
| Breaks: allBreaks, | ||
| ExceptionFilters: rmExceptionFilters, | ||
| }) | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_remove", Args: rawArgs}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| }, | ||
@@ -612,11 +552,3 @@ } | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_clear"}) | ||
| if err != nil { | ||
| return noDaemonError(err) | ||
| } | ||
| fmt.Print(FormatResponse(resp, globalFlags.jsonOutput)) | ||
| if resp.Status == "error" { | ||
| os.Exit(1) | ||
| } | ||
| return nil | ||
| return runDaemonCommand("break_clear", nil) | ||
| }, | ||
@@ -623,0 +555,0 @@ } |
+8
-0
@@ -46,2 +46,10 @@ package dap | ||
| } | ||
| // Multi-byte: emoji runes should not be split | ||
| emoji := "hello🌍🌎🌏world" | ||
| got := truncateString(emoji, 8) // "hello🌍🌎🌏" (8 runes) | ||
| want := "hello🌍🌎🌏..." | ||
| if got != want { | ||
| t.Errorf("multi-byte truncated = %q, want %q", got, want) | ||
| } | ||
| } |
+4
-3
@@ -209,8 +209,9 @@ package dap | ||
| // truncateString truncates a string to maxLen characters. | ||
| // truncateString truncates a string to maxLen runes. | ||
| func truncateString(s string, maxLen int) string { | ||
| if len(s) <= maxLen { | ||
| runes := []rune(s) | ||
| if len(runes) <= maxLen { | ||
| return s | ||
| } | ||
| return s[:maxLen] + "..." | ||
| return string(runes[:maxLen]) + "..." | ||
| } | ||
@@ -217,0 +218,0 @@ |
+46
-0
| package dap | ||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
@@ -250,2 +251,47 @@ "strings" | ||
| func TestRequireSession(t *testing.T) { | ||
| d := &Daemon{} | ||
| // No client — should return error response | ||
| resp := d.requireSession() | ||
| if resp == nil { | ||
| t.Fatal("expected error response when client is nil") | ||
| } | ||
| if resp.Status != "error" { | ||
| t.Errorf("expected status error, got %q", resp.Status) | ||
| } | ||
| if !strings.Contains(resp.Error, "no active debug session") { | ||
| t.Errorf("expected 'no active debug session' in error, got %q", resp.Error) | ||
| } | ||
| } | ||
| func TestMalformedJSONArgs(t *testing.T) { | ||
| d := &Daemon{} | ||
| // Set a fake client so requireSession passes | ||
| d.client = &DAPClient{} | ||
| malformed := json.RawMessage(`{invalid`) | ||
| tests := []struct { | ||
| name string | ||
| handler func(json.RawMessage) *Response | ||
| }{ | ||
| {"handleStep", d.handleStep}, | ||
| {"handleContinue", d.handleContinue}, | ||
| {"handleContext", d.handleContext}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| resp := tt.handler(malformed) | ||
| if resp.Status != "error" { | ||
| t.Errorf("expected error status, got %q", resp.Status) | ||
| } | ||
| if !strings.Contains(resp.Error, "invalid args") { | ||
| t.Errorf("expected 'invalid args' in error, got %q", resp.Error) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestMergeBreakpoints(t *testing.T) { | ||
@@ -252,0 +298,0 @@ tests := []struct { |
+166
-94
@@ -23,8 +23,11 @@ package dap | ||
| // DefaultSocketDir is the directory for the daemon socket. | ||
| var DefaultSocketDir = filepath.Join(os.Getenv("HOME"), ".dap-cli") | ||
| // defaultSocketDir returns the directory for the daemon socket. | ||
| func defaultSocketDir() string { | ||
| home, _ := os.UserHomeDir() | ||
| return filepath.Join(home, ".dap-cli") | ||
| } | ||
| // SessionSocketPath returns the socket path for a named session. | ||
| func SessionSocketPath(name string) string { | ||
| return filepath.Join(DefaultSocketDir, name+".sock") | ||
| return filepath.Join(defaultSocketDir(), name+".sock") | ||
| } | ||
@@ -39,4 +42,6 @@ | ||
| type Daemon struct { | ||
| client *DAPClient | ||
| backend Backend | ||
| clientMu sync.Mutex // guards d.client pointer; never held during I/O | ||
| client *DAPClient | ||
| backend Backend | ||
| adapterCmd *exec.Cmd | ||
@@ -71,2 +76,4 @@ | ||
| // (single-threaded dispatch via Serve) — no mutex needed. | ||
| // d.client is guarded by clientMu for pointer swaps (readLoop↔handlers); once | ||
| // requireSession passes, handlers use d.client directly (sequential dispatch). | ||
| sessionBreaks []Breakpoint // stored breakpoints for child session re-init | ||
@@ -86,2 +93,31 @@ sessionExceptionFilters []string // stored exception filter IDs for child session re-init | ||
| const errNoSession = "no active debug session (program may have terminated) — run 'dap debug' to start a new session" | ||
| func errResponse(msg string) *Response { | ||
| return &Response{Status: "error", Error: msg} | ||
| } | ||
| func errResponsef(format string, args ...any) *Response { | ||
| return &Response{Status: "error", Error: fmt.Sprintf(format, args...)} | ||
| } | ||
| func (d *Daemon) requireSession() *Response { | ||
| if d.getClient() == nil { | ||
| return errResponse(errNoSession) | ||
| } | ||
| return nil | ||
| } | ||
| func (d *Daemon) getClient() *DAPClient { | ||
| d.clientMu.Lock() | ||
| defer d.clientMu.Unlock() | ||
| return d.client | ||
| } | ||
| func (d *Daemon) setClient(c *DAPClient) { | ||
| d.clientMu.Lock() | ||
| d.client = c | ||
| d.clientMu.Unlock() | ||
| } | ||
| func idleTimeout() time.Duration { | ||
@@ -417,3 +453,3 @@ if v := os.Getenv("DAP_IDLE_TIMEOUT"); v != "" { | ||
| default: | ||
| return &Response{Status: "error", Error: fmt.Sprintf("unknown command: %s", req.Command)} | ||
| return errResponsef("unknown command: %s", req.Command) | ||
| } | ||
@@ -425,3 +461,3 @@ } | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)} | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
@@ -435,3 +471,3 @@ | ||
| if err != nil { | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
@@ -441,7 +477,8 @@ } else if args.Script != "" { | ||
| } else if args.Attach != "" { | ||
| return &Response{Status: "error", Error: "backend required for remote attach (e.g. --backend debugpy)"} | ||
| return errResponse("backend required for remote attach (e.g. --backend debugpy)") | ||
| } else { | ||
| return &Response{Status: "error", Error: "script path or --attach required"} | ||
| return errResponse("script path or --attach required") | ||
| } | ||
| d.backend = backend | ||
| d.stopSession() // clean up any previous session | ||
@@ -454,5 +491,5 @@ isRemote := args.Attach != "" | ||
| if err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("connecting to %s: %v", args.Attach, err)} | ||
| return errResponsef("connecting to %s: %v", args.Attach, err) | ||
| } | ||
| d.client = client | ||
| d.setClient(client) | ||
| d.expectCh = make(chan godap.Message, 64) | ||
@@ -462,3 +499,3 @@ go d.readLoop() | ||
| if err := d.initializeDAP(backend); err != nil { | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
@@ -469,3 +506,3 @@ | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("invalid attach address %q: %v", args.Attach, splitErr)} | ||
| return errResponsef("invalid attach address %q: %v", args.Attach, splitErr) | ||
| } | ||
@@ -476,7 +513,7 @@ remotePort, _ := strconv.Atoi(portStr) | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("preparing attach: %v", err)} | ||
| return errResponsef("preparing attach: %v", err) | ||
| } | ||
| if err := d.client.AttachRequestWithArgs(attachArgs); err != nil { | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("attach: %v", err)} | ||
| return errResponsef("attach: %v", err) | ||
| } | ||
@@ -486,6 +523,6 @@ } else { | ||
| if err := d.startAdapter(backend); err != nil { | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
| if err := d.initializeDAP(backend); err != nil { | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
@@ -496,3 +533,3 @@ | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("preparing launch: %v", err)} | ||
| return errResponsef("preparing launch: %v", err) | ||
| } | ||
@@ -502,3 +539,3 @@ d.cleanupFn = cleanupFn | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("launch: %v", err)} | ||
| return errResponsef("launch: %v", err) | ||
| } | ||
@@ -514,3 +551,3 @@ } | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("waiting for initialized: %v", err)} | ||
| return errResponsef("waiting for initialized: %v", err) | ||
| } | ||
@@ -524,10 +561,10 @@ switch m := msg.(type) { | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("request failed: %s", errMsg)} | ||
| return errResponsef("request failed: %s", errMsg) | ||
| case *godap.ExitedEvent, *godap.TerminatedEvent: | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: "debug adapter exited during initialization — check that the program path is valid and the debugger is installed"} | ||
| return errResponse("debug adapter exited during initialization — check that the program path is valid and the debugger is installed") | ||
| case godap.ResponseMessage: | ||
| if !m.GetResponse().Success { | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("request failed: %s", m.GetResponse().Message)} | ||
| return errResponsef("request failed: %s", m.GetResponse().Message) | ||
| } | ||
@@ -546,3 +583,3 @@ case *godap.InitializedEvent: | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set entry breakpoint: %v", err)} | ||
| return errResponsef("set entry breakpoint: %v", err) | ||
| } | ||
@@ -557,3 +594,3 @@ } | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set breakpoints: %v", err)} | ||
| return errResponsef("set breakpoints: %v", err) | ||
| } | ||
@@ -567,7 +604,7 @@ } | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)} | ||
| return errResponsef("set exception breakpoints: %v", err) | ||
| } | ||
| if err := d.client.ConfigurationDoneRequest(); err != nil { | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("finalizing debug setup: %v", err)} | ||
| return errResponsef("finalizing debug setup: %v", err) | ||
| } | ||
@@ -580,3 +617,3 @@ | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
@@ -594,3 +631,3 @@ if ctx.Location == nil { | ||
| d.stopSession() | ||
| return &Response{Status: "error", Error: fmt.Sprintf("continue past entry: %v", err)} | ||
| return errResponsef("continue past entry: %v", err) | ||
| } | ||
@@ -610,13 +647,18 @@ continue | ||
| if err := d.updateBreakpoints(bu.Breaks, bu.RemoveBreaks); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set breakpoints: %v", err)} | ||
| return errResponsef("set breakpoints: %v", err) | ||
| } | ||
| } | ||
| // ExceptionFilters on inline commands (--break-on-exception) replaces all current filters, | ||
| // unlike handleBreakAdd which merges additively. This matches the CLI flag semantics | ||
| // documented in the API: "replaces current filters". | ||
| if bu.ExceptionFilters != nil { | ||
| d.sessionExceptionFilters = bu.ExceptionFilters | ||
| if err := d.client.SetExceptionBreakpointsRequest(bu.ExceptionFilters); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)} | ||
| if len(bu.ExceptionFilters) > 0 { | ||
| existing := make(map[string]bool) | ||
| for _, f := range d.sessionExceptionFilters { | ||
| existing[f] = true | ||
| } | ||
| for _, f := range bu.ExceptionFilters { | ||
| if !existing[f] { | ||
| d.sessionExceptionFilters = append(d.sessionExceptionFilters, f) | ||
| } | ||
| } | ||
| if err := d.client.SetExceptionBreakpointsRequest(d.sessionExceptionFilters); err != nil { | ||
| return errResponsef("set exception breakpoints: %v", err) | ||
| } | ||
| } | ||
@@ -627,4 +669,4 @@ return nil | ||
| func (d *Daemon) handleStep(rawArgs json.RawMessage) *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -634,3 +676,5 @@ | ||
| if rawArgs != nil { | ||
| _ = json.Unmarshal(rawArgs, &args) | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
| } | ||
@@ -650,14 +694,14 @@ if args.Mode == "" { | ||
| if err := d.client.NextRequest(threadID); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("step over: %v", err)} | ||
| return errResponsef("step over: %v", err) | ||
| } | ||
| case "in": | ||
| if err := d.client.StepInRequest(threadID); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("step in: %v", err)} | ||
| return errResponsef("step in: %v", err) | ||
| } | ||
| case "out": | ||
| if err := d.client.StepOutRequest(threadID); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("step out: %v", err)} | ||
| return errResponsef("step out: %v", err) | ||
| } | ||
| default: | ||
| return &Response{Status: "error", Error: fmt.Sprintf("invalid step mode %q — use: in, out, over", args.Mode)} | ||
| return errResponsef("invalid step mode %q — use: in, out, over", args.Mode) | ||
| } | ||
@@ -669,4 +713,4 @@ | ||
| func (d *Daemon) handleContinue(rawArgs json.RawMessage) *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -676,3 +720,5 @@ | ||
| if rawArgs != nil { | ||
| _ = json.Unmarshal(rawArgs, &args) | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
| } | ||
@@ -687,3 +733,3 @@ | ||
| if err := d.client.ContinueRequest(threadID); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("continue: %v", err)} | ||
| return errResponsef("continue: %v", err) | ||
| } | ||
@@ -695,4 +741,4 @@ | ||
| func (d *Daemon) handleContext(rawArgs json.RawMessage) *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -702,3 +748,5 @@ | ||
| if rawArgs != nil { | ||
| _ = json.Unmarshal(rawArgs, &args) | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
| } | ||
@@ -714,3 +762,3 @@ | ||
| if err != nil { | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
@@ -721,4 +769,4 @@ return &Response{Status: "stopped", Data: ctx} | ||
| func (d *Daemon) handleEval(rawArgs json.RawMessage) *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -728,3 +776,3 @@ | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)} | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
@@ -739,3 +787,3 @@ | ||
| if args.Frame >= len(d.frameIDs) { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("frame %d out of range (stack has %d frames)", args.Frame, len(d.frameIDs))} | ||
| return errResponsef("frame %d out of range (stack has %d frames)", args.Frame, len(d.frameIDs)) | ||
| } | ||
@@ -746,3 +794,3 @@ frameID = d.frameIDs[args.Frame] | ||
| if err := d.client.EvaluateRequest(args.Expression, frameID, "repl"); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("evaluate: %v", err)} | ||
| return errResponsef("evaluate: %v", err) | ||
| } | ||
@@ -753,3 +801,3 @@ | ||
| if err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("reading eval response: %v", err)} | ||
| return errResponsef("reading eval response: %v", err) | ||
| } | ||
@@ -759,3 +807,3 @@ switch resp := msg.(type) { | ||
| if !resp.Success { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("eval failed: %s", resp.Message)} | ||
| return errResponsef("eval failed: %s", resp.Message) | ||
| } | ||
@@ -772,6 +820,6 @@ return &Response{ | ||
| case *godap.ExitedEvent, *godap.TerminatedEvent: | ||
| return &Response{Status: "error", Error: "program terminated during evaluation"} | ||
| return errResponse("program terminated during evaluation") | ||
| case godap.ResponseMessage: | ||
| if !resp.GetResponse().Success { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("eval failed: %s", resp.GetResponse().Message)} | ||
| return errResponsef("eval failed: %s", resp.GetResponse().Message) | ||
| } | ||
@@ -784,6 +832,8 @@ return &Response{Status: "ok", Data: &ContextResult{EvalResult: &EvalResult{Value: "(no result)"}}} | ||
| func (d *Daemon) handleOutput(rawArgs json.RawMessage) *Response { | ||
| if d.client != nil { | ||
| if d.getClient() != nil { | ||
| var args OutputArgs | ||
| if rawArgs != nil { | ||
| _ = json.Unmarshal(rawArgs, &args) | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
| } | ||
@@ -802,4 +852,4 @@ if errResp := d.applyBreakpointUpdates(args.BreakpointUpdates); errResp != nil { | ||
| func (d *Daemon) handleBreakList() *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -818,4 +868,4 @@ breaks := make([]Breakpoint, len(d.sessionBreaks)) | ||
| func (d *Daemon) handleBreakAdd(rawArgs json.RawMessage) *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -825,3 +875,3 @@ | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)} | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
@@ -831,3 +881,3 @@ | ||
| if err := d.updateBreakpoints(args.Breaks, nil); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set breakpoints: %v", err)} | ||
| return errResponsef("set breakpoints: %v", err) | ||
| } | ||
@@ -837,6 +887,14 @@ } | ||
| if len(args.ExceptionFilters) > 0 { | ||
| d.sessionExceptionFilters = args.ExceptionFilters | ||
| if err := d.client.SetExceptionBreakpointsRequest(args.ExceptionFilters); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)} | ||
| existing := make(map[string]bool) | ||
| for _, f := range d.sessionExceptionFilters { | ||
| existing[f] = true | ||
| } | ||
| for _, f := range args.ExceptionFilters { | ||
| if !existing[f] { | ||
| d.sessionExceptionFilters = append(d.sessionExceptionFilters, f) | ||
| } | ||
| } | ||
| if err := d.client.SetExceptionBreakpointsRequest(d.sessionExceptionFilters); err != nil { | ||
| return errResponsef("set exception breakpoints: %v", err) | ||
| } | ||
| } | ||
@@ -848,4 +906,4 @@ | ||
| func (d *Daemon) handleBreakRemove(rawArgs json.RawMessage) *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -855,3 +913,3 @@ | ||
| if err := json.Unmarshal(rawArgs, &args); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)} | ||
| return errResponsef("invalid args: %v", err) | ||
| } | ||
@@ -861,3 +919,3 @@ | ||
| if err := d.updateBreakpoints(nil, args.Breaks); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("remove breakpoints: %v", err)} | ||
| return errResponsef("remove breakpoints: %v", err) | ||
| } | ||
@@ -883,3 +941,3 @@ } | ||
| if err := d.client.SetExceptionBreakpointsRequest(filters); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)} | ||
| return errResponsef("set exception breakpoints: %v", err) | ||
| } | ||
@@ -892,4 +950,4 @@ } | ||
| func (d *Daemon) handleBreakClear() *Response { | ||
| if d.client == nil { | ||
| return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"} | ||
| if resp := d.requireSession(); resp != nil { | ||
| return resp | ||
| } | ||
@@ -904,3 +962,3 @@ | ||
| if err := d.client.SetBreakpointsRequest(file, nil); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("clear breakpoints: %v", err)} | ||
| return errResponsef("clear breakpoints: %v", err) | ||
| } | ||
@@ -913,3 +971,3 @@ } | ||
| if err := d.client.SetExceptionBreakpointsRequest([]string{}); err != nil { | ||
| return &Response{Status: "error", Error: fmt.Sprintf("clear exception breakpoints: %v", err)} | ||
| return errResponsef("clear exception breakpoints: %v", err) | ||
| } | ||
@@ -932,6 +990,9 @@ | ||
| func (d *Daemon) stopSession() { | ||
| if d.client != nil { | ||
| _ = d.client.DisconnectRequest(true) | ||
| d.client.Close() | ||
| d.client = nil | ||
| d.clientMu.Lock() | ||
| c := d.client | ||
| d.client = nil | ||
| d.clientMu.Unlock() | ||
| if c != nil { | ||
| _ = c.DisconnectRequest(true) | ||
| c.Close() | ||
| } | ||
@@ -974,3 +1035,3 @@ if d.adapterCmd != nil && d.adapterCmd.Process != nil { | ||
| } | ||
| d.client = client | ||
| d.setClient(client) | ||
| d.expectCh = make(chan godap.Message, 64) | ||
@@ -1017,3 +1078,6 @@ go d.readLoop() | ||
| // Initialize child | ||
| _ = childClient.InitializeRequest(d.backend.AdapterID()) | ||
| if err := childClient.InitializeRequest(d.backend.AdapterID()); err != nil { | ||
| childClient.Close() | ||
| return fmt.Errorf("child initialize: %w", err) | ||
| } | ||
| for { | ||
@@ -1031,3 +1095,5 @@ cmsg, cerr := childClient.ReadMessage() | ||
| // Launch child with config from startDebugging | ||
| _ = childClient.LaunchRequestWithArgs(config) | ||
| if err := childClient.LaunchRequestWithArgs(config); err != nil { | ||
| log.Printf("child launch: %v", err) | ||
| } | ||
@@ -1051,3 +1117,5 @@ // Read until InitializedEvent (child may send it immediately) | ||
| d.recordRequestedBreaks(file, bps) | ||
| _ = childClient.SetBreakpointsRequest(file, bps) | ||
| if err := childClient.SetBreakpointsRequest(file, bps); err != nil { | ||
| log.Printf("child set breakpoints: %v", err) | ||
| } | ||
| } | ||
@@ -1058,7 +1126,11 @@ childExceptionFilters := d.sessionExceptionFilters | ||
| } | ||
| _ = childClient.SetExceptionBreakpointsRequest(childExceptionFilters) | ||
| _ = childClient.ConfigurationDoneRequest() | ||
| if err := childClient.SetExceptionBreakpointsRequest(childExceptionFilters); err != nil { | ||
| log.Printf("child set exception breakpoints: %v", err) | ||
| } | ||
| if err := childClient.ConfigurationDoneRequest(); err != nil { | ||
| log.Printf("child configuration done: %v", err) | ||
| } | ||
| // Swap to child session — readLoop continues reading from child | ||
| d.client = childClient | ||
| d.setClient(childClient) | ||
| return nil | ||
@@ -1080,3 +1152,3 @@ } | ||
| if err != nil { | ||
| return &Response{Status: "error", Error: err.Error()} | ||
| return errResponse(err.Error()) | ||
| } | ||
@@ -1083,0 +1155,0 @@ if ctx.Location == nil { |
+1
-1
@@ -105,3 +105,3 @@ package dap | ||
| RemoveBreaks []Breakpoint `json:"remove_breaks,omitempty"` // breakpoints to remove | ||
| ExceptionFilters []string `json:"exception_filters,omitempty"` // backend-specific filter IDs (replaces current) | ||
| ExceptionFilters []string `json:"exception_filters,omitempty"` // backend-specific filter IDs (additive, merged with existing) | ||
| } | ||
@@ -108,0 +108,0 @@ |