diff --git a/content/blog/dape/Emacs Debugging with Dape.md b/content/blog/dape/Emacs Debugging with Dape.md new file mode 100755 index 0000000..45adafc --- /dev/null +++ b/content/blog/dape/Emacs Debugging with Dape.md @@ -0,0 +1,371 @@ ++++ +title: Emacs Debugging with Dape +date: 2023-10-20 +[taxonomies] +tags = ["emacs"] +categories = ["programming"] ++++ +For reasons I keep a set of machines where I regularly work on in sync so they offer a familiar environment. This includes a dual boot desktop (Ubuntu/Windows+WSL), laptop (Arch/Windows+WSL), a VPS(Arch) doing stuff, a virtual Ubuntu thing which is always running to monitor stuff on my NAS, and a Steam deck (Arch kinda). I expect these to be familiar environments so I can do some light development and scripting on them at least. +# Test plan + +In order to test the debugging support we need some minimum set of requirements to avoid descending in the abyss of incremental details. + +To evaluate the configuration for a language we need + +- a simple project in order not to be debugging the build system or the code itself taking more time than setting up the debugging. DAP is plenty complicated on its own. +- The DAP adapter installed with minimal manual work because otherwise it wont get done and there will be too much variance/rework between systems +- A set of familiar keybindings + +My requirements are: +- I can run the program in the debugger +- I can pass command line arguments to the script +- I can break the program on a line in the editor +- I can examine variables in scope when the breakpoint is hit +- I can see the output of the program +- I can quit/restart and there should not be leaking of processes/network ports/buffers. I.e. I can get back to editing and debugging without rebooting my PC. +- (I would like to pass environment variables too, but since apparently direnv works fine for my purposes I hardly ever use it anymore, so keep that on the backburner for now.) + +I do not really use debugging for something else. I actually seldom use the debugger. Mostly for learning new programming languages or playing with libraries concepts. If things get hairy I return back to logging/println debugging anyway (or adding more tests to herd the bugs in a small area). + +My test procedure is as follows. +- open the source code +- place a breakpoint early in the program after some variables are set +- `dd` to start the debugger +- curse on the dape prompt to select the program and pass arguments, set the *cwd* and more of that fun. +- check if the breakpoint is hit +- examine the variables +- continue to see the program continue with `dc` +- quit the program with `dq` +- verify we are back in our familiar editing session +- do a quick `ps ax` to see if we do not have unwanted critters still crawling around the process space. + +It is far from a comprehensive test, but we are testing here if the DAP adapter is properly installed and behaving as expected in my environment, not a validation test of the adapter itself. + + +## Javascript/Typescript example + +I assume that if I can debug typescript, I can debug javascript. So I created a little typescript program to calculate the answer to the meaning of life, the universe and everything. + +But first installing the DAP adapter. It is a bit fiddly and I do not want to repeat that on all my systems so I let emacs to the heavy lifting. It are just the instructions from the *dape* repo: + +``` +(setq snam-vscode-js-debug-dir (file-name-concat user-emacs-directory "dape/vscode-js-debug")) +(defun snam-install-vscode-js-debug () + "Run installation procedure to install JS debugging support" + (interactive) + (mkdir snam-vscode-js-debug-dir t) + (let ((default-directory (expand-file-name snam-vscode-js-debug-dir))) + + (vc-git-clone "https://github.com/microsoft/vscode-js-debug.git" "." nil) + (message "git repository created") + (call-process "npm" nil "*snam-install*" t "install") + (message "npm dependencies installed") + (call-process "npx" nil "*snam-install*" t "gulp" "dapDebugServer") + (message "vscode-js-debug installed"))) + + +``` + +Then add the dap config for the javascript and typescript modes: + +``` lisp + (add-to-list 'dape-configs + `(vscode-js-node + modes (js-mode js-ts-mode typescript-mode typescript-ts-mode) + host "localhost" + port 8123 + command "node" + command-cwd ,(file-name-concat snam-vscode-js-debug-dir "dist") + command-args ("src/dapDebugServer.js" "8123") + :type "pwa-node" + :request "launch" + :cwd dape-cwd-fn + :program dape-find-file-buffer-default + :outputCapture "console" + :sourceMapRenames t + :pauseForSourceMap nil + :enableContentValidation t + :autoAttachChildProcesses t + :console "internalConsole" + :killBehavior "forceful")) +``` + +The debugging our little app. Answer at the prompt: + +``` +vscode-js-node :program "src/main.ts" +``` + +and the debug view opens: + +![Typescript Debug Example](typescript_example.png) +## Python example + +As indicated on the dape repo install the python DAP adapter + +``` sh +$ pip install debugpy +``` + +Add the default settings to `dape-configs` + +``` lisp + (add-to-list 'dape-configs + `(debugpy + modes (python-ts-mode python-mode) + command "c:/Python312/python.exe" + command-args ("-m" "debugpy.adapter") + :type "executable" + :request "launch" + :cwd dape-cwd-fn + :program dape-find-file-buffer-default)) +``` + +and update the command to wherever python is installed. This is from my windows machine which succeeded again to lose its default python config so I have to coerce it a bit to find it. + +Open a python file, and start the debugger. On the `Dape config:` prompt enter: + +``` +debugpy :args ["6" "7"] +``` + +There are many more parameters, but *dape* will ask the missing ones with the functions in the configuration, here `dapy-cwd-fn` to fill in the current working directory (usually the project root) in the `:cwd` field and `dape-find-file-buffer-default` will propose the open file to run as the program to debug. + +It cannot meaningfully guess which parameters I want to pass so I add manually the `:attr` field with the values `"6"` and `"7"` in a vector. Do not use a list for that as that is not properly encoded by the json converter. + +It asks to confirm the program and opens the debugging views: + +![Python debugging views](python_example.png) + +## Go Example + +Installing the go adapter is just installing *delve* with your favorite package manager. If your package manager does not help (looking at you, chocolatey): + +```sh +go install github.com/go-delve/delve/cmd/dlv@latest +``` + +I did not bother to emacsify this as it is completely different for all my systems and I probably revisit that some time if I have a good idea to somehow abstract the differences away. + +Adding the config to `dape-configs`: + +``` + (add-to-list 'dape-configs + `(delve + modes (go-mode go-ts-mode) + command "dlv" + command-args ("dap" "--listen" "127.0.0.1:55878") + command-cwd dape-cwd-fn + host "127.0.0.1" + port 55878 + :type "debug" ;; needed to set the adapterID correctly as a string type + :request "launch" + :cwd dape-cwd-fn + :program dape-cwd-fn)) + +``` + +see debugging tips for an experience report (I made some typo while copy pasting... ). I used it to illustrate how I got around installation hickups. +## Rust Example + +Similar to Typescript I assume that if I can debug Rust, I should be able to debug C/C++ (which I try to avoid as much as possible). + +I automated the installation of the DAP adapter for LLD + +``` lisp +(setq snam-codelldb-dir (file-name-concat user-emacs-directory "dape/codelldb")) +(defun snam-install-codelldb () + "Install Vadimcn.Vscode-Lldb DAP server for C/C++/RUST" + (interactive) + (let* ((default-directory snam-codelldb-dir) + (arch (car (split-string system-configuration "-" nil nil))) + (os (pcase system-type + (='windows-nt "windows") + (='gnu/linux "linux") + (='darwin "darwin") + ('_ "unknown"))) + (version "1.10.0") + (release-url (concat "https://github.com/vadimcn/codelldb/releases/download/v" version "/codelldb-" arch "-" os ".vsix"))) + (mkdir default-directory t) + (url-copy-file release-url "codelldb.zip" t) + (message "codelldb archive downloaded") + (call-process "unzip" nil "*snam-install*" t "codelldb.zip") + (message "codelldb installed") + )) +``` + +Again this is just the installation instructions from the *dape* repo in emacs lisp. + +adding the default config: + +``` lisp + (add-to-list 'dape-configs + `(codelldb + modes (c-mode c-ts-mode + c++-mode c++-ts-mode + rust-ts-mode rust-mode) + ;; Replace vadimcn.vscode-lldb with the vsix directory you just extracted + command ,(expand-file-name + (file-name-concat + snam-codelldb-dir + (concat "extension/adapter/codelldb" + (if (eq system-type 'windows-nt) + ".exe" + "")))) + host "localhost" + port 5818 + command-args ("--port" "5818") + :type "lldb" + :request "launch" + :cwd dape-cwd-fn + :program dape-find-file)) +``` + +Since here the DAP is a binary I have to finagle the Windows file extension to make it work. + +Then opening a rust binary, here `src/main.rs`, setting a breakpoint and launching the debugger. + +I answered on the prompt + +``` +codelldb :args ["on"] +``` +to specify the arguments for the program to be debugged. + +Dape sees that the program is not specified so it asks for it and with tab completion `tardebbil` this is quickly selected and I am greeted with: + +![Rust debugging example](rust_example.png) + +## Debugging Tips + +When trying the Go example I hit a snag : the debugger started but no active thread was present. + +First stop is to open `*dape_debug*` buffer : + +``` +[info] Starting new multi session +[info] Server process started ("dlv" "dap" "--listen" "127.0.0.1:55878") +[info] +Process ("dlv" "dap" "--listen" "127.0.0.1:55878") exited with 1 +[info] Connection to server established 127.0.0.1:55878 +[io] Sending: +(:arguments + (:clientID "dape" :adapterID nil :pathFormat "path" :linesStartAt1 t :columnsStartAt1 t :supportsRunInTerminalRequest t :supportsProgressReporting t :supportsStartDebuggingRequest t) + :type "request" :command "initialize" :seq 1) + +[info] +Process nil exited with 256 +[error] Timeout for reached for seq 1 +``` + +So the process immediately exits, but we see no output. It would be nice to get that in the buffer, but it isn't... + +Let's steal the command and run it in a terminal : + +``` +Zeus :: ~ % dlv dap --listen 127.0.0.1:55878 >>> +DAP server listening at: 127.0.0.1:55878 +``` + +Now restart the debugger by quitting the debug session and starting a fresh one (we cannot use the restart command as the adapter is not running, we need to restart the adapter not the debugged program). + +``` +Zeus :: ~ % dlv dap --listen 127.0.0.1:55878 >>> +DAP server listening at: 127.0.0.1:55878 +2023-10-20T18:13:02+02:00 error layer=dap DAP error: json: cannot unmarshal bool into Go struct field InitializeRequestArguments.arguments.adapterID of type string +Zeus :: ~ % +``` + +Ok, the typechecker got a bool where it expected a string. Opening *dape.el* and searching for `json-serialize` we find the location. + +``` +(defun dape-send-object (process seq object) + "Helper for `dape-request' to send SEQ request with OBJECT to PROCESS." + (let* ((object (plist-put object :seq seq)) + (json (json-serialize object :false-object nil)) + (string (format "Content-Length: %d\r\n\r\n%s" (length json) json))) + (dape--debug 'io "Sending:\n%s" (pp-to-string object)) + (process-send-string process string))) +``` + +Let's print out the JSON we're sending : + +``` +(defun dape-send-object (process seq object) + "Helper for `dape-request' to send SEQ request with OBJECT to PROCESS." + (let* ((object (plist-put object :seq seq)) + (json (json-serialize object :false-object nil)) + (string (format "Content-Length: %d\r\n\r\n%s" (length json) json))) + (dape--debug 'io "Sending:\n%s" (pp-to-string object)) + (dape--debug 'io "Sending JSON:\n%s" json) + (process-send-string process string))) +``` + +this gives + +``` +(:arguments + (:clientID "dape" :adapterID nil :pathFormat "path" :linesStartAt1 t :columnsStartAt1 t :supportsRunInTerminalRequest t :supportsProgressReporting t :supportsStartDebuggingRequest t) + :type "request" :command "initialize" :seq 1) + +[io] Sending JSON: +{"arguments":{"clientID":"dape","adapterID":false,"pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsRunInTerminalRequest":true,"supportsProgressReporting":true,"supportsStartDebuggingRequest":true},"type":"request","command":"initialize","seq":1} +[info] +``` + +The supportsXXX things look to be bools, but the *adapterID* feels funny to be a bool... Where does this come from? + +``` +(defun dape--initialize (process) + "Send initialize request to PROCESS." + (dape-request process + "initialize" + (list :clientID "dape" + :adapterID (plist-get dape--config + :type) + :pathFormat "path" + :linesStartAt1 t + :columnsStartAt1 t + ;;:locale "en-US" + ;;:supportsVariableType t + ;;:supportsVariablePaging t + :supportsRunInTerminalRequest t + ;;:supportsMemoryReferences t + ;;:supportsInvalidatedEvent t + ;;:supportsMemoryEvent t + ;;:supportsArgsCanBeInterpretedByShell t + :supportsProgressReporting t + :supportsStartDebuggingRequest t + ;;:supportsVariableType t + ) +... +``` + +Ok, it comes from the config `:type` attribute... What do I have in my config??? + +``` + (add-to-list 'dape-configs + `(delve + modes (go-mode go-ts-mode) + command "dlv" + command-args ("dap" "--listen" "127.0.0.1:55878") + command-cwd dape-cwd-fn + host "127.0.0.1" + port 55878 + dape "debug" + :request "launch" + :cwd dape-cwd-fn + :program dape-cwd-fn)) + +``` + + +Hmm... what is that `dape` attribute. In all other configs there is a `:type` in that place. That seems a slip of the finger triggering an unlucky evil command. (I checked on the repo and it was correct there so it was my fault). Let's change it to `:type` and `C-x C-e` the `add-to-list` expression to update the default config. + +Let's try again : + +![go debug example](go_example.png) + +Ok, now we're cookin' with fire!!! + + diff --git a/content/blog/dape/go_example.png b/content/blog/dape/go_example.png new file mode 100755 index 0000000..862f0d0 Binary files /dev/null and b/content/blog/dape/go_example.png differ diff --git a/content/blog/dape/python_example.png b/content/blog/dape/python_example.png new file mode 100755 index 0000000..a41f6ba Binary files /dev/null and b/content/blog/dape/python_example.png differ diff --git a/content/blog/dape/rust_example.png b/content/blog/dape/rust_example.png new file mode 100755 index 0000000..549a269 Binary files /dev/null and b/content/blog/dape/rust_example.png differ diff --git a/content/blog/dape/typescript_example.png b/content/blog/dape/typescript_example.png new file mode 100755 index 0000000..92f2f36 Binary files /dev/null and b/content/blog/dape/typescript_example.png differ