helix icon indicating copy to clipboard operation
helix copied to clipboard

Debugging support with DAP connection

Open dsseng opened this issue 3 years ago • 12 comments

Describe your feature request

Add support for setting breakpoints and running debug session, backed by DAP (debug adapter protocol). Maybe this needs to be done after WASM plugins.

DAP spec is here. The only relevant thing found on crates.io is DAP types crate. DAP should be rather simple to parse with Serde.

Neovim has nvim-dap plugin implementing this feature, VS Code has DAP built-in.


Current state: an initial implementation of DAP with some types, editor connection. To test DAP client:

  1. Create a debuggee program:
package main

import "fmt"

func main() {
	a := "Hello"
	b := "World"
	fmt.Println(a, b)
	for {
	}
}
#include <stdio.h>

void main() {
	char *a = "Hello";
	char *b = "World";
	printf("%s %s\n", a, b);
	while (1) {
	}
}
  1. go build -gcflags '-N -l' main.go to build binary with readable variables or use :dbg source main.go for Go. 2.1. gcc main.c -o main -O0 -g for C program ~~3. Start up Delve (chosen as a monolithic DAP+debugger in one binary) on port 7777: dlv dap -l 127.0.0.1:7777 3.1. Or lldb: lldb-vscode -p 7777~~
  2. Don't care about that, tcp_process transport will find free port and start debug adapter itself.
  3. Run DAP example cargo run --example dap-dlv or dap-lldb
  4. Press enter when you want to continue communicating to debugger.

Editor integration works with Go, C/C++ and Rust programs at the moment. ~~Just enter directory containing main.go/main (build output)/target/debug/rustdebug for Rust and work with it.~~ Now you need to specify target manually, see languages.toml In editor I currently use example attached as a zip. godebug.zip

TODO:

  • [x] DAP runs over TCP/IP. ~~Not stdio~~. So, Transport must be able to use TCP sockets and the lifecycle of debug adapters
  • [x] More types, requests, events implemented. I'll do that in some time.
    • [x] attach
    • [x] parse stopped event
    • [x] Pass unused events to application or stream all the events in some way via channel
    • [x] pause, step~~, restart, eval~~
    • [ ] restart
      • [ ] supportsRestartRequest
    • [x] eval
    • [x] conditional breakpoints
    • [x] logpoints
    • [x] conditional logpoints
    • [x] Some internal state tracking
    • [x] breakpoint event
    • [ ] progress* events
    • [ ] output event's data can be recorded as well
    • [x] Better state handling (replace is_running)
    • [ ] BreakpointLocations (not supported by lldb and Delve)
    • [ ] Goto (no supported debugger)
    • [ ] Completions Request for eval autocompletion (no supported debugger)
      • [ ] supportsCompletionsRequest
    • [x] setExceptionBreakpoints
    • [ ] SetVariable Request
    • [ ] Source Request
    • [x] Check support for features in debugger
      • [x] Breakpoints
      • [ ] supportsTerminateRequest
  • [x] Editor UI connection
    • [x] Breakpoints setting
      • [x] Show unverified breakpoints and breakpoints moved by debugger
    • [x] Debugger state management
    • [x] Variable introspection
      • [x] Scrollable window for variables
    • [x] Highlight stack pointer
      • [x] Scroll to pos
    • [ ] REPL for eval (after commpletions for eval are done)
    • [x] Attach support
    • [x] Pretty-print errors instead of crashing when something goes wrong with debugger
    • [x] multi-thread support for stack pointers
    • [x] Preview of stack location
    • [x] Go to previous stack frame
    • [ ] Column-precision for breakpoints
    • [x] Set breakpoints by mouse
      • [x] Right click opens the prompt for editing condition
      • [x] ~~Middle click~~ Right click + Alt opens the prompt for editing condition
      • [x] those (:top:) ideas, but with keybindings
  • [x] Fix Tried sending event into a closed channel for events
  • [x] Split out interface for adapter-specific launch and attach args
  • [x] Configuration for languages to specify how to start and configure debuggers for them ~~, maybe some debug adapters' quirks~~.
    • [x] Names for launch configs
    • [x] Templates
    • [x] Pickers
      • [ ] Show defaults and allow editing args
    • [x] Non-string arguments
    • [ ] Multiple debuggers for language (needs a use case, gdb will be the first one)
  • [ ] Consider adding workspace configs for debug targets or parsing existing ones.
  • [ ] Test debuggers, add them to editor.
    • [x] Delve's dlv dap
    • [x] lldb with lldb-vscode
    • [x] Node.js
    • [ ] gdb integration, which means rr could be supported, test it as well
      • [x] Initial connection to gdbserver with lldb, :warning: Is not compatible enough, OpenOCD and rr did not work
    • [ ] https://github.com/microsoft/debugpy/
    • [ ] Chromium
    • [ ] Firefox
  • [ ] Docs for everything

Local ``languages.toml` for Node:

[[language]]
name = "javascript"

[language.debugger]
command = "node"
args = [ "/usr/share/code-insiders/resources/app/extensions/ms-vscode.node-debug2/out/src/nodeDebug.js" ]

Adjust path to extension in VSCode (or location where you unpacked the extension manually).

dsseng avatar Jul 25 '21 17:07 dsseng

I feel like this is something that is common enough to be a built-in feature. Though we could also go ahead and package it with Helix as a default plugin.

kirawi avatar Jul 25 '21 18:07 kirawi

Built-in seems the best. Plugins that depend on other plugins looks like not-so-cool idea.

Edit: and, of course, some APIs might be available for plugins. Maybe even some generics for custom, not DAP-compliant debuggers registered by plugins.

dsseng avatar Jul 25 '21 18:07 dsseng

Yes, given that LSP is built in, DAP will be built in too. Note that both GDB and LLDB don't support DAP and implement a custom protocol, though LLDB seems to now offer a wrapper: https://github.com/llvm/llvm-project/tree/main/lldb/tools/lldb-vscode

archseer avatar Jul 26 '21 01:07 archseer

DAP is more common, so built in. Others might be added through wrappers and then using plugin API. Maybe GDB might be shipped as an official plugin?

dsseng avatar Jul 26 '21 08:07 dsseng

I started some development for DAP locally. Currently trying to make a transport layer.

dsseng avatar Aug 11 '21 12:08 dsseng

We should be able to reuse most of the LSP low-level code, it's the same protocol. Just the high level requests are different.

archseer avatar Aug 11 '21 12:08 archseer

It's not json-rpc. Currently copied lsp transport, will later isolate the common code

dsseng avatar Aug 11 '21 12:08 dsseng

Currently I already have an implementation (example for library, not in editor) that does some simple tasks:

  • connect to DAP via netcat (will implement proper TCP later, for now just command-line communication)
  • initialize debugger and get capabilities (using Delve now since it's self-contained adapter+debugger)
  • load binary (launch request)
  • set a breakpoint in code and confirm it's been set
  • configurationDone - let debugger start target
  • wait until target stops on breakpoint
  • continue
  • stop debug adapter

dsseng avatar Aug 12 '21 12:08 dsseng

Interesting, so DAP doesn't run over stdin/stdout?

I think the way forward will be to define a transport and all the request/response protocol types in helix-dap, then construct a session type that will be similar to doc.language_server. I figured attach would be an easy way to get going, along with being able to set breakpoints. We will need to come up with a proposal on how to integrate with the editor, and I'd leave any larger UI changes until later (we'll probably need a custom vars component).

I took a look at emacs dap-mode today: https://emacs-lsp.github.io/dap-mode/page/gallery/

archseer avatar Aug 12 '21 13:08 archseer

E.g. delve doesn't. It's a TCP/IP socket:

dlv dap -h
[EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).

The server is headless and requires a DAP client like vscode to connect and request a binary
to be launched or process to be attached to. The following modes are supported:
- launch + exec (executes precompiled binary, like 'dlv exec')
- launch + debug (builds and launches, like 'dlv debug')
- launch + test (builds and tests, like 'dlv test')
- attach + local (attaches to a running process, like 'dlv attach')
The server does not yet support asynchronous request-response communication, so features
like pausing or setting breakpoints while the program is running are not yet available.
The server does not accept multiple client connections (--accept-multiclient),
a feature that is often relied on to enable --continue with remote debugging.

Usage:
  dlv dap [flags]

Global Flags:
      --accept-multiclient               Allows a headless server to accept multiple client connections.
      --allow-non-terminal-interactive   Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
      --api-version int                  Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
      --backend string                   Backend selection (see 'dlv help backend'). (default "default")
      --build-flags string               Build flags, to be passed to the compiler. For example: --build-flags="-tags=integration -mod=vendor -cover -v"
      --check-go-version                 Checks that the version of Go in use is compatible with Delve. (default true)
      --disable-aslr                     Disables address space randomization
      --headless                         Run debug server only, in headless mode.
      --init string                      Init file, executed by the terminal client.
  -l, --listen string                    Debugging server listen address. (default "127.0.0.1:0")
      --log                              Enable debugging server logging.
      --log-dest string                  Writes logs to the specified file or file descriptor (see 'dlv help log').
      --log-output string                Comma separated list of components that should produce debug output (see 'dlv help log')
      --only-same-user                   Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
  -r, --redirect stringArray             Specifies redirect rules for target process (see 'dlv help redirect')
      --wd string                        Working directory for running the program.

I'm currently tidying up the code and will push it to GitHub for us to comfortably look at it and collaborate. Stack traces work atm :rocket:!

dsseng avatar Aug 12 '21 13:08 dsseng

@archseer It's up https://github.com/helix-editor/helix/pull/574

dsseng avatar Aug 12 '21 13:08 dsseng

Looks like it can be both ways. Either it's an executable we're responsible with starting and then communicating via stdio, or it's a server (that can also be externally running, we're not responsible for) and we attach to via TCP.

archseer avatar Aug 12 '21 14:08 archseer