Konubinix' opinionated web of thoughts

How to Debug a Typescript Program Running on K8s Using Dap in Emacs?

Fleeting

how to debug a typescript program running on k8s, using ms-vscode.js-debug and dap-mode?

Build the code with support for sourcemaps. Run the container with a debug image that runs node --inspect-brk

port forwarding

Create port forward on 9229 to be able to communicate with the remote node over the node debugging protocol.

Sometime, this error Error: Could not connect to debug target at http://localhost:9229: Could not find any debuggable target indicates that the communication has been broken.

http http://127.0.0.1:9229/json/version 2>&1
http: error: ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) while doing a POST request to URL: http://127.0.0.1:9229/json/version

You should try to fix it before going further until you get the following:

curl http://127.0.0.1:9229/json/version
{
  "Browser": "node.js/v18.15.0",
  "Protocol-Version": "1.1"
}

Sometimes, you simply need to restart the pod. Refreshing the code while a debugger is running may cause this issue.

Also, in my experience, nestjs often fails to keep the debugger open and I see a bunch of “Starting inspector on 127.0.0.1:9229 failed: address already in use”.

use the appropriate code to use dap on emacs

Use the branch vscode-js-debug-feature of this fork of this fork of dap-mode1.

running with default dap-node (spoiler, it won’t work)

setup the configuration

Ensure the launch.json contains a suitable entry to connect to it.

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach to app",
            "address": "localhost",
            "localRoot": "${workspaceFolder}/app",
            "remoteRoot": "/app",
            "port": 9229,
            "sourceMaps": true,
            "trace": true,
            "request": "attach",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "type": "pwa-node"
        }
}

Note that the type is “pwa-node” here. This is a legacy code and dap-mode must eventually use “node”, but so far, “node” is already reserved by the implementation of the legacy ms-vscode.node-debug2. When the fork will (hopefully) be merged upstream, we most likely will be able to use “node”.

get the source and source map support

Get the remote source AND associated source map to get the code AND the source mapping support.

You want at least the sources, the source map files and the node_modules (to step into the dependencies).

kubectl exec myapp -- tar -C /app -cf - . | tar -C . -xf -

You cannot simply pass kubectl cp myapp:/app app because it does not deal with symbolic links and at least the node_modules is full of them.

killing the debugger

Note that you may have to kill the debugger for a fresh start from time to time

pkill -f ms-vscode

breakpoints not matched, what went wrong?

Also, sometimes the breakpoints are not matched remotely. You may have to run the debugger again.

You can try running this

(setq dap-print-io t)
(setq dap-inhibit-io nil)
(shell-command "pkill -f ms-vscode")
(with-current-buffer "*Messages*"
  (read-only-mode -1)
  (erase-buffer))
(when (dap--session-running (dap--cur-session))
  (dap-disconnect (dap--cur-session-or-die)))
(konix/dap-delete-all-sessions)

Then running dap-debug again and see if there is a match for "verified": true,. That means that one breakpoint was matched remotely. All "verified": null, is a good hint that either your config is buggy, or that you need to run it again.

Also, you can Add trace: true into the launch.json entry. Then in ${TMPDIR-/tmp}, you should find a file named vscode-debugadapter-xxxx.json.gz that gives a lot of data telling what was send and received between the local dap bridge and the remote node.

You should see logs like

{
  "tag": "cdp.receive",
  "timestamp": 1704726925553,
  "metadata": {
    "connectionId": 0,
    "message": {
      "method": "Debugger.scriptParsed",
      "params": {
        "scriptId": "3825",
        "url": "file://<remote-path>.js",
        "startLine": 0,
        "startColumn": 0,
        "endLine": 140,
        "endColumn": 42,
        "executionContextId": 1,
        "hash": "132c43512fb1cf792ed7af674bf2c2df2dcc962d",
        "executionContextAuxData": {
          "isDefault": true
        },
        "isLiveEdit": false,
        "sourceMapURL": "<name>.map",
        "hasSourceURL": false,
        "isModule": false,
        "length": 6466,
        "scriptLanguage": "JavaScript",
        "embedderName": "file://<remote-path>.js"
      }
    }
  },
  "level": 0
}


[...]

{
  "tag": "runtime.sourcemap",
  "timestamp": 1704726926476,
  "message": "Mapped remoteToLocal: <remote-path>.js-> <local-path>.js",
  "level": 0
}

[...]

{
  "tag": "runtime.sourcecreate",
  "timestamp": 1704726926891,
  "message": "Creating source from url",
  "metadata": {
    "inputUrl": "file:///<remote-path>.js",
    "absolutePath": "/<local-path>.js"
  },
  "level": 0
}

[...]

{
  "tag": "dap.send",
  "timestamp": 1704726927149,
  "metadata": {
    "connectionId": 1,
    "message": {
      "seq": 4239,
      "type": "event",
      "event": "loadedSource",
      "body": {
        "reason": "new",
        "source": {
          "name": "<remote-path>.js",
          "path": "<remote-path>.js",
          "sourceReference": 1411237085
        }
      }
    }
  },
  "level": 0
}

[...]

{
  "tag": "sourcemap.parsing",
  "timestamp": 1704726927198,
  "message": "Creating sources from source map",
  "metadata": {
    "sourceMapId": 48,
    "metadata": {
      "sourceMapUrl": "file:///<remote-path>.map",
      "compiledPath": "/<local-path>.js"
    }
  },
  "level": 0
}

beware the location of node_modules

Make sure you looked closely at the local path. It might be out of the localRoot folder. It has happened to me when working in a yarn workspace. In that case, using my local build, most of the dependencies node_modules where installed in the root workspace while they were installed in the app in the remote. Therefore my local build pointed towards ./root/node_modules/dep while they were in ./root/app/node_modules/dep remotely.

Actually, this would not have happened if I had followed my own advice and copied the whole /app folder.

Notes linking here


  1. Yeah… This is all still work in progress

     ↩︎