VS Code Extension debugger fails to attach

Viewed 347

I'm writing an extension for VS Code, based on sirmspencer's AutoHide, and >90% of my tries to debug it fail.
It launches and works, but the debugger doesn't seem to attach:

  • Breakpoints are never hit, even though the extension host is running past them
  • Console output is not sent to the Debug Console (very annoying)
  • When I close the test window, terminating the process, the debugger doesn't get notified and keeps running
  • When I stop the debugger, the test window doesn't get notified and stays open

What could be the problem?
[Video]

Here are the files in .\.vscode:
launch.json:

// A launch configuration that compiles the extension and then opens it inside a new window
{
    "version": "0.1.0",
    "configurations": [
        {
            "name": "Launch Extension",
            "type": "extensionHost",
            "request": "launch",
            "runtimeExecutable": "${execPath}",
            "args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
            "stopOnEntry": false,
            "sourceMaps": true,
            "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ],
            "preLaunchTask": "npm"
        },
        {
            "name": "Launch Tests",
            "type": "extensionHost",
            "request": "launch",
            "runtimeExecutable": "${execPath}",
            "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
            "stopOnEntry": false,
            "sourceMaps": true,
            "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ],
            "preLaunchTask": "npm"
        }
    ]
}

settings.json:

// Place your settings in this file to overwrite default and user settings.
{
    "files.exclude": {
        "out": false // set this to true to hide the "out" folder with the compiled JS files
    },
    "search.exclude": {
        "out": true // set this to false to include "out" folder in search results
    }
}

tasks.json:

// A task runner that calls a custom npm script that compiles the extension.
{
    "version": "0.1.0",

    // we want to run npm
    "command": "npm",

    // the command is a shell script
    "isShellCommand": true,

    // show the output window only if unrecognized errors occur.
    "showOutput": "silent",

    // we run the custom script "compile" as defined in package.json
    "args": ["run", "compile", "--loglevel", "silent"],

    // The tsc compiler is started in watching mode
    "isWatching": true,

    // use the standard tsc in watch mode problem matcher to find compile problems in the output.
    "problemMatcher": "$tsc-watch"
}

And my package.json:

{
    "name": "vscode-hideEmptyErrors",
    "displayName": "Hide Empty Problems Panel",
    "description": "Hide the 'Problems' panel when there are no relevant errors",
    "version": "1.0.0",
    "publisher": "Black Platypus",
    "repository": {
        "url": "https://example.com"
    },
    "icon": "Images/Icons/Logo_512.png",
    "engines": {
        "vscode": "^1.43.0"
    },
    "extensionKind": [
        "ui",
        "workspace"
    ],
    "categories": [
        "Other"
    ],
    "keywords": [
        "problems",
        "panel",
        "hide",
        "auto"
    ],
    "activationEvents": [
        "*"
    ],
    "main": "./out/src/extension",
    "contributes": {
        "configuration": {
            "type": "object",
            "title": "hideEmptyErrors",
            "properties": {
                "hideEmptyErrors.autoHidePanel": {
                    "type": "boolean",
                    "default": true,
                    "description": "Hide the 'Problems' panel when there are no relevant errors."
                },
                "hideEmptyErrors.noEditorBehavior": {
                    "type": "string",
                    "description": "What should happen when there is no active editor or the active editor has no document while the Problems panel is active?",
                    "default": "No change",
                    "enum": [
                        "No change",
                        "Hide panel",
                        "Show Panel"
                    ],
                    "enumDescriptions": [
                        "Keeps the panel's current state (visible/hidden)",
                        "Always hides the panel",
                        "Always shows the panel"
                    ]
                },
                "hideEmptyErrors.onlyCountErrorsForCurrentFile": {
                    "type": "boolean",
                    "default": true,
                    "description": "Only count errors for the currently active editor's file."
                }
            }
        },
        "commands": [
            {
                "command": "hideEmptyErrors.toggleHidePanel",
                "category": "Auto Hide",
                "title": "Toggle Auto Hide Panel for Current Workspace"
            }
        ]
    },
    "scripts": {
        "compile": "tsc -watch -p ./",
        "#test": "node ./node_modules/vscode/bin/test",
        "vscode:prepublish": "tsc -p ./",
        "#postinstall": "node ./node_modules/vscode/bin/install",
        "publish": "vsce publish"
    },
    "devDependencies": {
        "@types/mocha": "^2.2.32",
        "@types/node": "7.0.7",
        "mocha": "^7.2.0",
        "typescript": "^2.9.2",
        "vscode": "^1.1.36"
    }
}
1 Answers

I have the same issue with the exact same symptoms, down to the "Could not find a free port for debugging" warning in the Dev Tools Console. I suspect this is due to an extremely restrictive firewall/antivirus. It seems that the Extension Development Host tries to find itself a TCP port to communicate with the debugger client, which fails for some reason. This is the relevant code segment in localProcessExtensionHost.ts:

/**
 * Find a free port if extension host debugging is enabled.
 */
private async _tryFindDebugPort(): Promise<number> {

    if (typeof this._environmentService.debugExtensionHost.port !== 'number') {
        return 0;
    }

    const expected = this._environmentService.debugExtensionHost.port;
    const port = await findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */);

    if (!this._isExtensionDevTestFromCli) {
        if (!port) {
            console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color:');
        } else {
            if (port !== expected) {
                console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:');
            }
            if (this._isExtensionDevDebugBrk) {
                console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:');
            } else {
                console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:');
            }
        }
    }

    return port || 0;
}

It turns out that the port search in findFreePort just tries ten or so TCP ports starting from the one given in this._environmentService.port, with a default value of 5870 that you can override with command-line arguments, if I believe the unit tests for environmentService in environmentService.test.ts:

    assert.deepStrictEqual(parse([]), { port: 5870, break: false, debugId: undefined });
    assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined });
    assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
    assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined });
    assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
    assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });

In conclusion, adding --debugPluginHost=PORT to your launch configuration with a port other than 5870 seems to make debugging work. Here is an example with port 1234, which finally fixed the hello world example for me:

    {
        "name": "Run Extension",
        "type": "extensionHost",
        "request": "launch",
        "args": [
            "--debugPluginHost=1234",
            "--extensionDevelopmentPath=${workspaceFolder}"
        ],
        "outFiles": [
            "${workspaceFolder}/out/**/*.js"
        ],
        "preLaunchTask": "${defaultBuildTask}"
    },

You might have to experiment with port numbers to find one that works for you.

Related