microsoft / vscode-js-debug

A DAP-compatible JavaScript debugger. Used in VS Code, VS, + more
MIT License
1.67k stars 281 forks source link

Breakpoint is failing on nodemon + jest + docker / compose setup #1617

Closed yuguan91 closed 1 year ago

yuguan91 commented 1 year ago

Describe the bug Breakpoints fails to work on nodemon + jest + docker. Sometimes I get a breakpoint at wrong line numbers in a readonly file with the same name, sometimes no breakpoints at all.

My local files are located in /User/me/some/path/app Inside docker container, the files are located in root '/app'

I have a prelaunch task:

        {
            "label": "Start App",
            "type": "shell",
            "command": "./scripts/start-app.sh",
            "group": "test",
            "problemMatcher": [],
            "isBackground": true,
            "presentation": {
                "reveal": "always",
                "panel": "shared"
            }
        }

where ./scripts/start-app.sh essentially does:

docker-compose exec api \
    nodemon --no-lazy --legacy-watch --watch /app \
        --inspect-brk=app:9229 \
        /app/test/node_modules/jest/bin/jest.js --coverage \
            --config=/service/jest.config.json --runInBand

And an attach configuration:

{
            "type": "node",
            "request": "attach",
            "name": "App",
            "port": 9229,
            "restart": true,
            "preLaunchTask": "Start App",
            "localRoot": "${workspaceFolder}/app/",
            "remoteRoot": "/",
            "outFiles": [
                "${workspaceFolder}/app/**/*.js",
                "!**/node_modules/**"
            ],
            "skipFiles": [
                "<node_internals>/**/*.js",
                "${workspaceFolder}/**/node_modules/**/*.js"
            ],
            "pauseForSourceMap": true,
            "disableOptimisticBPs": true,
            "internalConsoleOptions": "neverOpen",
}

Debugger is able to attach to the process, because break point does hit the first line ofresolve() in path.js. But debugger statement doesn't work, nor does any break points.

I've got the vscode-js-debug nightly extension, and debugged it to a point where I think I found the cause:

When using jest, my code is getting compiled to add instrumentation. The source map somehow already has the original local path, so when _onScriptParsed calls rebaseRemoteToLocal() the following happens:

  public rebaseRemoteToLocal(remotePath: string) {
    // this.options === {
    //   removeRoot: '/',
    //   localRoot: '/User/me/some/path/app'
    // }
    // remotePath === '/User/me/some/path/app/file.js'

    if (!this.options.remoteRoot || !this.options.localRoot || !this.canMapPath(remotePath)) {
      return path.resolve(remotePath);
    }

    // we get here, and relativePath becomes 'User/me/some/path/app/file.js'
    const relativePath = properRelative(this.options.remoteRoot, remotePath);
    if (relativePath.startsWith('..')) {
      return '';
    }

    // we get here, and localPath becomes '/User/me/some/path/app/User/me/some/path/app/file.js'
    let localPath = properJoin(this.options.localRoot, relativePath);

    localPath = fixDriveLetter(localPath);
    this.logger.verbose(
      LogTag.RuntimeSourceMap,
      `Mapped remoteToLocal: ${remotePath} -> ${localPath}`,
    );
    return properResolve(localPath);
  }

I've added a branch statement in this function to check if remotePath already contains localRoot (and removeRoot is a substring of localRoot)

  public rebaseRemoteToLocal(remotePath: string) {
    if (!this.options.remoteRoot || !this.options.localRoot || !this.canMapPath(remotePath)) {
      return path.resolve(remotePath);
    }

    if (remotePath.includes(this.options.localRoot)) {
      // somehow the remote path is already available, don't rebase anymore
      return path.resolve(remotePath);
    }
    ...
  }

And that seemed to fix my issue.

To Reproduce Steps to reproduce the behavior:

  1. You probably will need: jest: 26.4.2
  2. Create docker, compose, some js src and test
  3. Copy my launch and task configs
  4. Try set a break point

Log File Will send logs to your email

VS Code Version: Version: 1.76.2 (Universal) Commit: ee2b180d582a7f601fa6ecfdad8d9fd269ab1884 Date: 2023-03-14T17:54:09.061Z (2 wks ago) Electron: 19.1.11 Chromium: 102.0.5005.196 Node.js: 16.14.2 V8: 10.2.154.26-electron.0 OS: Darwin x64 22.3.0 Sandboxed: No

Additional Context: I upgraded from 1.59 to 1.76, since debugger hasn't worked with our old setup after 1.59, until today I have the time for the upgrade. The regression might have happened anywhere in between.

yuguan91 commented 1 year ago

We are using docker-compose's volumes to mount local files to remote, so when we save to local file, the changes are reflected in the container, and that will trigger nodemon to restart our jest unit tests. The remote path might have been messed up by mounting the volumes.

connor4312 commented 1 year ago

Verification steps:

  1. Make a simple Jest package in a folder called jest-test https://jestjs.io/docs/getting-started

  2. Run in docker, example

    docker run -w /jest-test -v `pwd`:/jest-test -p 19229:19229 node:18-alpine --inspect-brk=0.0.0.0:19229 /jest-test/node_modules/jest/bin/jest.js --runInBand
  3. Create a launch config

    {
      "name": "Attach to Node",
      "port": 19229,
      "request": "attach",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "continueOnAttach": true,
      "localRoot": "${workspaceFolder}/../",
      "remoteRoot": "/",
      "type": "node"
    }
  4. Verify you can set and hit a breakpoint

connor4312 commented 1 year ago

This will be fixed in the nightly build on Monday after 5PM PST. Please let me know if it works for you.

yuguan91 commented 1 year ago

Confirmed on nightly, bp is working.

eleanorjboyd commented 1 year ago

marking as verified based on @yuguan91's comment