diff --git a/package-lock.json b/package-lock.json index 65d494b2d..c128d7eaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10745,9 +10745,10 @@ "dev": true }, "typescript": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz", - "integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==" + "version": "3.8.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.1-rc.tgz", + "integrity": "sha512-aOIe066DyZn2uYIiND6fXMUUJ70nxwu/lKhA92QuQzXyC86fr0ywo1qvO8l2m0EnDcfjprYPuFRgNgDj7U2GlQ==", + "dev": true }, "uc.micro": { "version": "1.0.6", diff --git a/package.json b/package.json index bc2db890d..1dc1ec552 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "reflect-metadata": "^0.1.13", "source-map": "^0.7.3", "split2": "^3.1.1", - "typescript": "^3.7.4", + "typescript": "^3.8.1-rc", "vscode-nls": "^4.1.1", "ws": "^7.0.1" }, diff --git a/src/binder.ts b/src/binder.ts index 242b41eb6..0b1508719 100644 --- a/src/binder.ts +++ b/src/binder.ts @@ -166,7 +166,11 @@ export class Binder implements IDisposable { provideLaunchParams(this._rootServices, params); this._rootServices.get(ILogger).setup(resolveLoggerOptions(dap, params.trace)); - const cts = CancellationTokenSource.withTimeout(params.timeout); + const cts = + params.timeout > 0 + ? CancellationTokenSource.withTimeout(params.timeout) + : new CancellationTokenSource(); + if (params.rootPath) params.rootPath = urlUtils.platformPathToPreferredCase(params.rootPath); this._launchParams = params; let results = await Promise.all( diff --git a/src/build/generate-contributions.ts b/src/build/generate-contributions.ts index 87f6f691d..aee76738e 100644 --- a/src/build/generate-contributions.ts +++ b/src/build/generate-contributions.ts @@ -1,7 +1,12 @@ /*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import { Contributions, IConfigurationTypes, Configuration } from '../common/contributionUtils'; +import { + Contributions, + DebugType, + IConfigurationTypes, + Configuration, +} from '../common/contributionUtils'; import { IMandatedConfiguration, AnyLaunchConfiguration, @@ -12,11 +17,13 @@ import { OutputSource, INodeLaunchConfiguration, IExtensionHostConfiguration, - IChromeBaseConfiguration, + IChromiumBaseConfiguration, IChromeLaunchConfiguration, IChromeAttachConfiguration, ITerminalLaunchConfiguration, baseDefaults, + IEdgeLaunchConfiguration, + IEdgeAttachConfiguration, } from '../configuration'; import { JSONSchema6 } from 'json-schema'; import strings from './strings'; @@ -135,7 +142,7 @@ const baseConfigurationAttributes: ConfigurationAttributes = }, skipFiles: { type: 'array', - description: refString('chrome.skipFiles.description'), + description: refString('browser.skipFiles.description'), default: ['/**'], }, smartStep: { @@ -145,7 +152,7 @@ const baseConfigurationAttributes: ConfigurationAttributes = }, sourceMaps: { type: 'boolean', - description: refString('chrome.sourceMaps.description'), + description: refString('browser.sourceMaps.description'), default: true, }, sourceMapPathOverrides: { @@ -248,7 +255,7 @@ const nodeBaseConfigurationAttributes: ConfigurationAttributes = { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'attach', label: refString('node.label'), variables: { @@ -259,7 +266,7 @@ const nodeAttachConfig: IDebugger = { label: refString('node.snippet.attach.label'), description: refString('node.snippet.attach.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'attach', name: '${1:Attach}', port: 9229, @@ -270,7 +277,7 @@ const nodeAttachConfig: IDebugger = { label: refString('node.snippet.remoteattach.label'), description: refString('node.snippet.remoteattach.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'attach', name: '${1:Attach to Remote}', address: '${2:TCP/IP address of process to be debugged}', @@ -284,7 +291,7 @@ const nodeAttachConfig: IDebugger = { label: refString('node.snippet.attachProcess.label'), description: refString('node.snippet.attachProcess.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'attach', name: '${1:Attach by Process ID}', processId: '^"\\${command:PickProcess}"', @@ -353,7 +360,7 @@ const nodeAttachConfig: IDebugger = { * Node attach configuration. */ const nodeLaunchConfig: IDebugger = { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', label: refString('node.label'), variables: { @@ -364,7 +371,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.launch.label'), description: refString('node.snippet.launch.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: '${2:Launch Program}', program: '^"\\${workspaceFolder}/${1:app.js}"', @@ -375,7 +382,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.npm.label'), markdownDescription: refString('node.snippet.npm.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: '${1:Launch via NPM}', runtimeExecutable: 'npm', @@ -388,7 +395,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.nodemon.label'), description: refString('node.snippet.nodemon.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: 'nodemon', runtimeExecutable: 'nodemon', @@ -403,7 +410,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.mocha.label'), description: refString('node.snippet.mocha.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: 'Mocha Tests', program: '^"\\${workspaceFolder}/node_modules/mocha/bin/_mocha"', @@ -416,7 +423,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.yo.label'), markdownDescription: refString('node.snippet.yo.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: 'Yeoman ${1:generator}', program: '^"\\${workspaceFolder}/node_modules/yo/lib/cli.js"', @@ -430,7 +437,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.gulp.label'), description: refString('node.snippet.gulp.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: 'Gulp ${1:task}', program: '^"\\${workspaceFolder}/node_modules/gulp/bin/gulp.js"', @@ -442,7 +449,7 @@ const nodeLaunchConfig: IDebugger = { label: refString('node.snippet.electron.label'), description: refString('node.snippet.electron.description'), body: { - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', name: 'Electron Main', runtimeExecutable: '^"\\${workspaceFolder}/node_modules/.bin/electron"', @@ -508,7 +515,7 @@ const nodeLaunchConfig: IDebugger = { }; const nodeTerminalConfiguration: IDebugger = { - type: Contributions.TerminalDebugType, + type: DebugType.Terminal, request: 'launch', label: refString('debug.terminal.label'), configurationSnippets: [ @@ -516,7 +523,7 @@ const nodeTerminalConfiguration: IDebugger = { label: refString('debug.terminal.snippet.label'), description: refString('debug.terminal.snippet.label'), body: { - type: Contributions.TerminalDebugType, + type: DebugType.Terminal, request: 'launch', name: 'Run npm start', command: 'npm start', @@ -536,53 +543,48 @@ const nodeTerminalConfiguration: IDebugger = { /** * Shared Chrome configuration. */ -const chromeBaseConfigurationAttributes: ConfigurationAttributes = { +const chromiumBaseConfigurationAttributes: ConfigurationAttributes = { ...baseConfigurationAttributes, port: { type: 'number', - description: refString('chrome.port.description'), + description: refString('browser.port.description'), default: 9222, }, address: { type: 'string', - description: refString('chrome.address.description'), + description: refString('browser.address.description'), default: '127.0.0.1', }, disableNetworkCache: { type: 'boolean', - description: refString('chrome.disableNetworkCache.description'), + description: refString('browser.disableNetworkCache.description'), default: true, }, pathMapping: { type: 'object', - description: refString('chrome.pathMapping.description'), + description: refString('browser.pathMapping.description'), default: {}, }, webRoot: { type: 'string', - description: refString('chrome.webRoot.description'), + description: refString('browser.webRoot.description'), default: '${workspaceFolder}', }, urlFilter: { type: 'string', - description: refString('chrome.urlFilter.description'), + description: refString('browser.urlFilter.description'), default: '', }, url: { type: 'string', - description: refString('chrome.url.description'), + description: refString('browser.url.description'), default: 'http://localhost:8080', }, - useWebView: { - type: 'boolean', - description: refString('edge.useWebView.description'), - default: false, - }, server: { oneOf: [ { type: 'object', - description: refString('chrome.server.description'), + description: refString('browser.server.description'), additionalProperties: false, default: { program: 'node my-server.js' }, properties: nodeLaunchConfig.configurationAttributes, @@ -600,7 +602,7 @@ const chromeBaseConfigurationAttributes: ConfigurationAttributes = { - type: Contributions.ExtensionHostDebugType, + type: DebugType.ExtensionHost, request: 'launch', label: refString('extensionHost.label'), required: ['args'], @@ -609,7 +611,7 @@ const extensionHostConfig: IDebugger = { label: refString('extensionHost.snippet.launch.label'), description: refString('extensionHost.snippet.launch.description'), body: { - type: Contributions.ExtensionHostDebugType, + type: DebugType.ExtensionHost, request: 'launch', name: refString('extensionHost.launch.config.name'), runtimeExecutable: '^"\\${execPath}"', @@ -638,7 +640,7 @@ const extensionHostConfig: IDebugger = { }; const chromeLaunchConfig: IDebugger = { - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, request: 'launch', label: refString('chrome.label'), configurationSnippets: [ @@ -646,7 +648,7 @@ const chromeLaunchConfig: IDebugger = { label: refString('chrome.launch.label'), description: refString('chrome.launch.description'), body: { - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, request: 'launch', name: 'Launch Chrome', url: 'http://localhost:8080', @@ -655,25 +657,25 @@ const chromeLaunchConfig: IDebugger = { }, ], configurationAttributes: { - ...chromeBaseConfigurationAttributes, + ...chromiumBaseConfigurationAttributes, file: { type: 'string', - description: refString('chrome.file.description'), + description: refString('browser.file.description'), default: '${workspaceFolder}/index.html', }, userDataDir: { type: ['string', 'boolean'], - description: refString('chrome.userDataDir.description'), + description: refString('browser.userDataDir.description'), default: true, }, runtimeExecutable: { type: ['string', 'null'], - description: refString('chrome.runtimeExecutable.description'), + description: refString('browser.runtimeExecutable.description'), default: 'stable', }, runtimeArgs: { type: 'array', - description: refString('chrome.runtimeArgs.description'), + description: refString('browser.runtimeArgs.description'), items: { type: 'string', }, @@ -681,19 +683,19 @@ const chromeLaunchConfig: IDebugger = { }, env: { type: 'object', - description: refString('chrome.env.description'), + description: refString('browser.env.description'), default: {}, }, cwd: { type: 'string', - description: refString('chrome.cwd.description'), + description: refString('browser.cwd.description'), default: null, }, }, }; const chromeAttachConfig: IDebugger = { - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, request: 'attach', label: refString('chrome.label'), configurationSnippets: [ @@ -701,7 +703,7 @@ const chromeAttachConfig: IDebugger = { label: refString('chrome.attach.label'), description: refString('chrome.attach.description'), body: { - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, request: 'attach', name: 'Attach to Chrome', port: 9222, @@ -710,13 +712,62 @@ const chromeAttachConfig: IDebugger = { }, ], configurationAttributes: { - ...chromeBaseConfigurationAttributes, + ...chromiumBaseConfigurationAttributes, }, }; -const edgeLaunchConfig: IDebugger = { - ...chromeLaunchConfig, - type: Contributions.EdgeDebugType, +const edgeLaunchConfig: IDebugger = { + type: DebugType.Edge, + request: 'launch', + label: refString('edge.launch.label'), + configurationSnippets: [ + { + label: refString('edge.launch.label'), + description: refString('edge.launch.description'), + body: { + type: DebugType.Edge, + request: 'launch', + name: 'Launch Edge', + url: 'http://localhost:8080', + webRoot: '^"${2:\\${workspaceFolder\\}}"', + }, + }, + ], + configurationAttributes: { + ...chromeLaunchConfig.configurationAttributes, + useWebView: { + type: 'boolean', + description: refString('edge.useWebView.description'), + default: false, + }, + }, +}; + +const edgeAttachConfig: IDebugger = { + type: DebugType.Edge, + request: 'attach', + label: refString('edge.label'), + configurationSnippets: [ + { + label: refString('edge.attach.label'), + description: refString('edge.attach.description'), + body: { + type: DebugType.Edge, + request: 'attach', + name: 'Attach to Chrome', + port: 9222, + webRoot: '^"${2:\\${workspaceFolder\\}}"', + }, + }, + ], + configurationAttributes: { + ...chromiumBaseConfigurationAttributes, + useWebView: { + type: 'boolean', + description: refString('edge.useWebView.description'), + default: false, + }, + }, }; function buildDebuggers() { @@ -728,6 +779,7 @@ function buildDebuggers() { chromeLaunchConfig, chromeAttachConfig, edgeLaunchConfig, + edgeAttachConfig, ]; // eslint-disable-next-line diff --git a/src/build/strings.ts b/src/build/strings.ts index db4cb3c0f..b8123bdaf 100644 --- a/src/build/strings.ts +++ b/src/build/strings.ts @@ -30,45 +30,50 @@ const strings = { 'edge.useWebView.description': "(Edge (Chromium) only) When 'true', the debugger will treat the runtime executable as a host application that contains a WebView allowing you to debug the WebView script content.", - 'chrome.address.description': 'TCP/IP address of debug port', - 'chrome.baseUrl.description': + 'chrome.label': 'Chrome (preview)', + 'edge.label': 'Edge (preview)', + 'edge.launch.label': 'Edge: Launch', + 'edge.attach.label': 'Edge: Attach', + 'edge.launch.description': 'Launch Edge to debug a URL', + 'edge.attach.description': 'Attach to an instance of Edge already in debug mode', + 'chrome.launch.label': 'Chrome: Launch', + 'chrome.launch.description': 'Launch Chrome to debug a URL', + 'chrome.attach.label': 'Chrome: Attach', + 'chrome.attach.description': 'Attach to an instance of Chrome already in debug mode', + + 'browser.address.description': 'TCP/IP address of debug port', + 'browser.baseUrl.description': 'Base URL to resolve paths baseUrl. baseURL is trimmed when mapping URLs to the files on disk. Defaults to the launch URL domain.', - 'chrome.cwd.description': 'Optional working directory for the runtime executable.', - 'chrome.disableNetworkCache.description': + 'browser.cwd.description': 'Optional working directory for the runtime executable.', + 'browser.disableNetworkCache.description': 'Controls whether to skip the network cache for each request', - 'chrome.env.description': 'Optional dictionary of environment key/value pairs for the browser.', - 'chrome.file.description': 'A local html file to open in the browser', - 'chrome.pathMapping.description': - 'A mapping of URLs/paths to local folders, to resolve scripts in Chrome to scripts on disk', - 'chrome.port.description': 'Port to use for Chrome remote debugging.', - 'chrome.runtimeExecutable.description': + 'browser.env.description': 'Optional dictionary of environment key/value pairs for the browser.', + 'browser.file.description': 'A local html file to open in the browser', + 'browser.pathMapping.description': + 'A mapping of URLs/paths to local folders, to resolve scripts in the Browser to scripts on disk', + 'browser.port.description': 'Port to use for remote debugging the browser.', + 'browser.runtimeExecutable.description': "Either 'canary', 'stable', 'custom' or path to the browser executable. Custom means a custom wrapper, custom build or CHROME_PATH environment variable.", - 'chrome.showAsyncStacks.description': 'Show the async calls that led to the current call stack', - 'chrome.skipFiles.description': + 'browser.skipFiles.description': 'An array of file or folder names, or path globs, to skip when debugging.', - 'chrome.smartStep.description': + 'browser.smartStep.description': 'Automatically step through unmapped lines in sourcemapped files. For example, code that TypeScript produces automatically when downcompiling async/await or other features.', - 'chrome.sourceMapPathOverrides.description': + 'browser.sourceMapPathOverrides.description': 'A set of mappings for rewriting the locations of source files from what the sourcemap says, to their locations on disk. See README for details.', - 'chrome.sourceMaps.description': 'Use JavaScript source maps (if they exist).', - 'chrome.timeout.description': - 'Retry for this number of milliseconds to connect to Chrome. Default is 10000 ms.', - 'chrome.url.description': 'Will search for a tab with this exact url and attach to it, if found', - 'chrome.urlFilter.description': + 'browser.sourceMaps.description': 'Use JavaScript source maps (if they exist).', + 'browser.timeout.description': + 'Retry for this number of milliseconds to connect to the browser. Default is 10000 ms.', + 'browser.url.description': 'Will search for a tab with this exact url and attach to it, if found', + 'browser.urlFilter.description': 'Will search for a page with this url and attach to it, if found. Can have * wildcards.', - 'chrome.webRoot.description': + 'browser.webRoot.description': 'This specifies the workspace absolute path to the webserver root. Used to resolve paths like `/app.js` to files on disk. Shorthand for a pathMapping for "/"', 'node.launch.args.description': 'Command line arguments passed to the program.', - 'chrome.runtimeArgs.description': 'Optional arguments passed to the runtime executable.', - 'chrome.server.description': + 'browser.runtimeArgs.description': 'Optional arguments passed to the runtime executable.', + 'browser.server.description': "Configures a web server to start up. Takes the same configuration as the 'node' launch task.", - 'chrome.userDataDir.description': - 'By default, Chrome is launched with a separate user profile in a temp folder. Use this option to override it. Set to false to launch with your default user profile.', - 'chrome.label': 'Chrome (preview)', - 'chrome.launch.label': 'Chrome: Launch', - 'chrome.launch.description': 'Launch Chrome to debug a URL', - 'chrome.attach.label': 'Chrome: Attach', - 'chrome.attach.description': 'Attach to an instance of Chrome already in debug mode', + 'browser.userDataDir.description': + 'By default, the browser is launched with a separate user profile in a temp folder. Use this option to override it. Set to false to launch with your default user profile.', 'debug.npm.script': 'Debug NPM Script', 'debug.npm.noWorkspaceFolder': 'You need to open a workspace folder to debug npm scripts.', diff --git a/src/chromeDebugConfigurationProvider.ts b/src/chromeDebugConfigurationProvider.ts deleted file mode 100644 index 8f9c16a5a..000000000 --- a/src/chromeDebugConfigurationProvider.ts +++ /dev/null @@ -1,109 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import { - ResolvingChromeConfiguration, - AnyChromeConfiguration, - INodeLaunchConfiguration, - chromeAttachConfigDefaults, - chromeLaunchConfigDefaults, -} from './configuration'; -import { NodeDebugConfigurationProvider } from './nodeDebugConfigurationProvider'; -import { Contributions } from './common/contributionUtils'; -import { BaseConfigurationProvider } from './baseConfigurationProvider'; -import { basename } from 'path'; -import { TerminalDebugConfigurationProvider } from './terminalDebugConfigurationProvider'; - -const localize = nls.loadMessageBundle(); - -/** - * Configuration provider for Chrome debugging. - */ -export class ChromeDebugConfigurationProvider - extends BaseConfigurationProvider - implements vscode.DebugConfigurationProvider { - constructor( - context: vscode.ExtensionContext, - private readonly nodeProvider: NodeDebugConfigurationProvider, - private readonly terminalProvider: TerminalDebugConfigurationProvider, - ) { - super(context); - - this.setProvideDefaultConfiguration( - () => - createLaunchConfigFromContext() || { - type: Contributions.ChromeDebugType, - request: 'launch', - name: localize('chrome.launch.name', 'Launch Chrome against localhost'), - url: 'http://localhost:8080', - webRoot: '${workspaceFolder}', - }, - ); - } - - /** - * @override - */ - protected async resolveDebugConfigurationAsync( - folder: vscode.WorkspaceFolder | undefined, - config: ResolvingChromeConfiguration, - ): Promise { - if (!config.name && !config.type && !config.request) { - const fromContext = createLaunchConfigFromContext(); - if (!fromContext) { - // Return null so it will create a launch.json and fall back on - // provideDebugConfigurations - better to point the user towards - // the config than try to work automagically for complex scenarios. - return; - } - - config = fromContext; - } - - if (config.request === 'attach') { - // todo https://github.com/microsoft/vscode-chrome-debug/blob/ee5ae7ac7734f369dba58ba57bb910aac467c97a/src/extension.ts#L48 - } - - if (config.server && 'program' in config.server) { - const serverOpts = { - ...config.server, - type: Contributions.NodeDebugType, - request: 'launch', - name: `${config.name}: Server`, - }; - - config.server = (await this.nodeProvider.resolveDebugConfiguration( - folder, - serverOpts, - )) as INodeLaunchConfiguration; - } else if (config.server && 'command' in config.server) { - config.server = await this.terminalProvider.resolveDebugConfiguration(folder, { - ...config.server, - type: Contributions.TerminalDebugType, - request: 'launch', - name: `${config.name}: Server`, - }); - } - - return config.request === 'attach' - ? { ...chromeAttachConfigDefaults, ...config } - : { ...chromeLaunchConfigDefaults, ...config }; - } -} - -function createLaunchConfigFromContext(): ResolvingChromeConfiguration | void { - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === 'html') { - return { - type: Contributions.ChromeDebugType, - request: 'launch', - name: `Open ${basename(editor.document.uri.fsPath)}`, - file: editor.document.uri.fsPath, - }; - } - - return undefined; -} diff --git a/src/common/contributionUtils.ts b/src/common/contributionUtils.ts index bd8675795..ca266f433 100644 --- a/src/common/contributionUtils.ts +++ b/src/common/contributionUtils.ts @@ -17,15 +17,31 @@ export const enum Contributions { RemoveCustomBreakpointCommand = 'extension.NAMESPACE(chrome-debug).removeCustomBreakpoint', RemoveAllCustomBreakpointsCommand = 'extension.NAMESPACE(chrome-debug).removeAllCustomBreakpoints', - ExtensionHostDebugType = 'NAMESPACE(extensionHost)', - TerminalDebugType = 'NAMESPACE(node-terminal)', - NodeDebugType = 'NAMESPACE(node)', - ChromeDebugType = 'NAMESPACE(chrome)', - EdgeDebugType = 'NAMESPACE(msedge)', - BrowserBreakpointsView = 'jsBrowserBreakpoints', } +export const enum DebugType { + ExtensionHost = 'NAMESPACE(extensionHost)', + Terminal = 'NAMESPACE(node-terminal)', + Node = 'NAMESPACE(node)', + Chrome = 'NAMESPACE(chrome)', + Edge = 'NAMESPACE(msedge)', +} + +// constructing it this way makes sure we can't forget to add a type: +const debugTypes: { [K in DebugType]: null } = { + [DebugType.ExtensionHost]: null, + [DebugType.Terminal]: null, + [DebugType.Node]: null, + [DebugType.Chrome]: null, + [DebugType.Edge]: null, +}; + +/** + * Set of all known debug types. + */ +export const allDebugTypes: ReadonlySet = new Set(Object.keys(debugTypes)); + export const enum Configuration { UsePreviewDebugger = 'debug.javascript.usePreview', NpmScriptLens = 'debug.javascript.codelens.npmScripts', diff --git a/src/common/urlUtils.ts b/src/common/urlUtils.ts index 7850c6b58..a0abd8677 100644 --- a/src/common/urlUtils.ts +++ b/src/common/urlUtils.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as http from 'http'; import * as https from 'https'; import { fixDriveLetterAndSlashes } from './pathUtils'; -import { AnyChromeConfiguration } from '../configuration'; +import { AnyChromiumConfiguration } from '../configuration'; import { escapeRegexSpecialChars } from './stringUtils'; let isCaseSensitive = process.platform !== 'win32'; @@ -318,7 +318,7 @@ export const isLoopback = (address: string) => { * Creates a target filter function for the given Chrome configuration. */ export const createTargetFilterForConfig = ( - config: AnyChromeConfiguration, + config: AnyChromiumConfiguration, ): ((t: { url: string }) => boolean) => { const filter = config.urlFilter || config.url || ('file' in config && config.file); if (!filter) { diff --git a/src/configuration.ts b/src/configuration.ts index 1f9814652..0509ca9cb 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import Dap from './dap/api'; -import { Contributions } from './common/contributionUtils'; +import { DebugType } from './common/contributionUtils'; import { assertNever } from './common/objUtils'; import { AnyRestartOptions } from './targets/node/restartPolicy'; @@ -167,7 +167,7 @@ export interface IBaseConfiguration extends IMandatedConfiguration { } export interface IExtensionHostConfiguration extends INodeBaseConfiguration { - type: Contributions.ExtensionHostDebugType; + type: DebugType.ExtensionHost; request: 'attach' | 'launch'; /** @@ -243,7 +243,7 @@ export interface IConfigurationWithEnv { * Configuration for a launch request. */ export interface INodeLaunchConfiguration extends INodeBaseConfiguration, IConfigurationWithEnv { - type: Contributions.NodeDebugType; + type: DebugType.Node; request: 'launch'; /** @@ -297,9 +297,7 @@ export interface INodeLaunchConfiguration extends INodeBaseConfiguration, IConfi */ export type PathMapping = Readonly<{ [key: string]: string }>; -export interface IChromeBaseConfiguration extends IBaseConfiguration { - type: Contributions.ChromeDebugType | Contributions.EdgeDebugType; - +export interface IChromiumBaseConfiguration extends IBaseConfiguration { /** * Controls whether to skip the network cache for each request. */ @@ -333,18 +331,13 @@ export interface IChromeBaseConfiguration extends IBaseConfiguration { * Launch options to boot a server. */ server: INodeLaunchConfiguration | ITerminalLaunchConfiguration | null; - - /** - * (Edge only) Enable web view debugging. - */ - useWebView: boolean; } /** * Opens a debugger-enabled terminal. */ export interface ITerminalLaunchConfiguration extends INodeBaseConfiguration { - type: Contributions.TerminalDebugType; + type: DebugType.Terminal; request: 'launch'; /** @@ -357,7 +350,7 @@ export interface ITerminalLaunchConfiguration extends INodeBaseConfiguration { * Configuration for an attach request. */ export interface INodeAttachConfiguration extends INodeBaseConfiguration { - type: Contributions.NodeDebugType; + type: DebugType.Node; request: 'attach'; /** @@ -382,7 +375,7 @@ export interface INodeAttachConfiguration extends INodeBaseConfiguration { attachExistingChildren: boolean; } -export interface IChromeLaunchConfiguration extends IChromeBaseConfiguration { +interface IChromiumLaunchConfiguration extends IChromiumBaseConfiguration { request: 'launch'; /** @@ -438,18 +431,47 @@ export interface IChromeLaunchConfiguration extends IChromeBaseConfiguration { inspectUri?: string; } +/** + * Configuration to launch to a Chrome instance. + */ +export interface IChromeLaunchConfiguration extends IChromiumLaunchConfiguration { + type: DebugType.Chrome; +} + /** * Configuration to attach to a Chrome instance. */ -export interface IChromeAttachConfiguration extends IChromeBaseConfiguration { +export interface IChromeAttachConfiguration extends IChromiumBaseConfiguration { + type: DebugType.Chrome; request: 'attach'; } +/** + * Configuration to launch to a Edge instance. + */ +export interface IEdgeLaunchConfiguration extends IChromiumLaunchConfiguration { + type: DebugType.Edge; + + /** + * Enable web view debugging. + */ + useWebView: boolean; +} + +/** + * Configuration to attach to a Edge instance. + */ +export interface IEdgeAttachConfiguration extends IChromiumBaseConfiguration { + type: DebugType.Edge; + request: 'attach'; + useWebView: boolean; +} + /** * Attach request used internally to inject a pre-built target into the lifecycle. */ export interface ITerminalDelegateConfiguration extends INodeBaseConfiguration { - type: Contributions.TerminalDebugType; + type: DebugType.Terminal; request: 'attach'; delegateId: number; } @@ -461,7 +483,11 @@ export type AnyNodeConfiguration = | IExtensionHostConfiguration | ITerminalDelegateConfiguration; export type AnyChromeConfiguration = IChromeAttachConfiguration | IChromeLaunchConfiguration; -export type AnyLaunchConfiguration = AnyChromeConfiguration | AnyNodeConfiguration; +export type AnyEdgeConfiguration = IEdgeAttachConfiguration | IEdgeLaunchConfiguration; +export type AnyChromiumLaunchConfiguration = IEdgeLaunchConfiguration | IChromeLaunchConfiguration; +export type AnyChromiumAttachConfiguration = IEdgeAttachConfiguration | IChromeAttachConfiguration; +export type AnyChromiumConfiguration = AnyEdgeConfiguration | AnyChromeConfiguration; +export type AnyLaunchConfiguration = AnyChromiumConfiguration | AnyNodeConfiguration; export type AnyTerminalConfiguration = | ITerminalDelegateConfiguration | ITerminalLaunchConfiguration; @@ -489,12 +515,14 @@ export type ResolvingNodeConfiguration = | ResolvingNodeAttachConfiguration | ResolvingNodeLaunchConfiguration; export type ResolvingChromeConfiguration = ResolvingConfiguration; +export type ResolvingEdgeConfiguration = ResolvingConfiguration; export type AnyResolvingConfiguration = | ResolvingExtensionHostConfiguration | ResolvingChromeConfiguration | ResolvingNodeAttachConfiguration | ResolvingNodeLaunchConfiguration - | ResolvingTerminalConfiguration; + | ResolvingTerminalConfiguration + | ResolvingEdgeConfiguration; export const AnyLaunchConfiguration = Symbol('AnyLaunchConfiguration'); @@ -550,14 +578,14 @@ const nodeBaseDefaults: INodeBaseConfiguration = { export const terminalBaseDefaults: ITerminalLaunchConfiguration = { ...nodeBaseDefaults, showAsyncStacks: { onceBreakpointResolved: 16 }, - type: Contributions.TerminalDebugType, + type: DebugType.Terminal, request: 'launch', name: 'Debugger Terminal', }; export const delegateDefaults: ITerminalDelegateConfiguration = { ...nodeBaseDefaults, - type: Contributions.TerminalDebugType, + type: DebugType.Terminal, request: 'attach', name: 'Debugger Terminal', showAsyncStacks: { onceBreakpointResolved: 16 }, @@ -566,7 +594,7 @@ export const delegateDefaults: ITerminalDelegateConfiguration = { export const extensionHostConfigDefaults: IExtensionHostConfiguration = { ...nodeBaseDefaults, - type: Contributions.ExtensionHostDebugType, + type: DebugType.ExtensionHost, name: 'Debug Extension', request: 'launch', args: ['--extensionDevelopmentPath=${workspaceFolder}'], @@ -578,7 +606,7 @@ export const extensionHostConfigDefaults: IExtensionHostConfiguration = { export const nodeLaunchConfigDefaults: INodeLaunchConfiguration = { ...nodeBaseDefaults, - type: Contributions.NodeDebugType, + type: DebugType.Node, request: 'launch', program: '', stopOnEntry: false, @@ -592,7 +620,7 @@ export const nodeLaunchConfigDefaults: INodeLaunchConfiguration = { export const chromeAttachConfigDefaults: IChromeAttachConfiguration = { ...baseDefaults, - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, request: 'attach', port: 0, disableNetworkCache: true, @@ -602,13 +630,17 @@ export const chromeAttachConfigDefaults: IChromeAttachConfiguration = { sourceMapPathOverrides: defaultSourceMapPathOverrides('${webRoot}'), webRoot: '${workspaceFolder}', server: null, - // Edge only +}; + +export const edgeAttachConfigDefaults: IEdgeAttachConfiguration = { + ...chromeAttachConfigDefaults, + type: DebugType.Edge, useWebView: false, }; export const chromeLaunchConfigDefaults: IChromeLaunchConfiguration = { ...chromeAttachConfigDefaults, - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, request: 'launch', cwd: null, file: null, @@ -618,9 +650,15 @@ export const chromeLaunchConfigDefaults: IChromeLaunchConfiguration = { userDataDir: false, }; +export const edgeLaunchConfigDefaults: IEdgeLaunchConfiguration = { + ...chromeLaunchConfigDefaults, + type: DebugType.Edge, + useWebView: false, +}; + export const nodeAttachConfigDefaults: INodeAttachConfiguration = { ...nodeBaseDefaults, - type: Contributions.NodeDebugType, + type: DebugType.Node, attachSpawnedProcesses: true, attachExistingChildren: true, restart: false, @@ -648,6 +686,12 @@ export function applyChromeDefaults(config: ResolvingChromeConfiguration): AnyCh : { ...chromeLaunchConfigDefaults, ...config }; } +export function applyEdgeDefaults(config: ResolvingEdgeConfiguration): AnyEdgeConfiguration { + return config.request === 'attach' + ? { ...edgeAttachConfigDefaults, ...config } + : { ...edgeLaunchConfigDefaults, ...config }; +} + export function applyExtensionHostDefaults( config: ResolvingExtensionHostConfiguration, ): IExtensionHostConfiguration { @@ -668,19 +712,19 @@ export const isConfigurationWithEnv = (config: unknown): config is IConfiguratio export function applyDefaults(config: AnyResolvingConfiguration): AnyLaunchConfiguration { let configWithDefaults: AnyLaunchConfiguration; switch (config.type) { - case Contributions.NodeDebugType: + case DebugType.Node: configWithDefaults = applyNodeDefaults(config); break; - case Contributions.EdgeDebugType: - case Contributions.ChromeDebugType: + case DebugType.Edge: + configWithDefaults = applyEdgeDefaults(config); + break; + case DebugType.Chrome: configWithDefaults = applyChromeDefaults(config); - // Reset type to ChromeDebugType incase EdgeDebugType was used. - configWithDefaults.type = Contributions.ChromeDebugType; break; - case Contributions.ExtensionHostDebugType: + case DebugType.ExtensionHost: configWithDefaults = applyExtensionHostDefaults(config); break; - case Contributions.TerminalDebugType: + case DebugType.Terminal: configWithDefaults = applyTerminalDefaults(config); break; default: @@ -695,7 +739,7 @@ function resolveWorkspaceRoot(config: AnyLaunchConfiguration): AnyLaunchConfigur config = resolveVariableInConfig( config, 'webRoot', - config.type === Contributions.ChromeDebugType ? config.webRoot : config.__workspaceFolder, + config.type === DebugType.Chrome ? config.webRoot : config.__workspaceFolder, ); return config; diff --git a/src/extension.ts b/src/extension.ts index cabcc6267..c80cfef52 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,23 +9,21 @@ import { registerDebugTerminalUI } from './ui/debugTerminalUI'; import { registerPrettyPrintActions } from './ui/prettyPrintUI'; import { SessionManager } from './ui/sessionManager'; import { DebugSessionTracker } from './ui/debugSessionTracker'; -import { NodeDebugConfigurationProvider } from './nodeDebugConfigurationProvider'; -import { ChromeDebugConfigurationProvider } from './chromeDebugConfigurationProvider'; -import { Contributions, registerCommand } from './common/contributionUtils'; +import { Contributions, registerCommand, allDebugTypes } from './common/contributionUtils'; import { pickProcess, attachProcess } from './ui/processPicker'; -import { ExtensionHostConfigurationProvider } from './extensionHostConfigurationProvider'; -import { TerminalDebugConfigurationProvider } from './terminalDebugConfigurationProvider'; import { debugNpmScript } from './ui/debugNpmScript'; import { registerCustomBreakpointsUI } from './ui/customBreakpointsUI'; import { registerLongBreakpointUI } from './ui/longPredictionUI'; import { toggleSkippingFile } from './ui/toggleSkippingFile'; import { registerNpmScriptLens } from './ui/npmScriptLens'; import { DelegateLauncherFactory } from './targets/delegate/delegateLauncherFactory'; +import { IDebugConfigurationProvider } from './ui/configuration'; export function activate(context: vscode.ExtensionContext) { const services = createGlobalContainer({ storagePath: context.storagePath || context.extensionPath, isVsCode: true, + context, }); context.subscriptions.push( @@ -35,50 +33,17 @@ export function activate(context: vscode.ExtensionContext) { registerCommand(vscode.commands, Contributions.ToggleSkippingCommand, toggleSkippingFile), ); - const extensionConfigProvider = new ExtensionHostConfigurationProvider(context); - const nodeConfigProvider = new NodeDebugConfigurationProvider(context); - const terminalConfigProvider = new TerminalDebugConfigurationProvider(context); - const chromeConfigProvider = new ChromeDebugConfigurationProvider( - context, - nodeConfigProvider, - terminalConfigProvider, - ); - context.subscriptions.push( - vscode.debug.registerDebugConfigurationProvider( - Contributions.NodeDebugType, - nodeConfigProvider, - ), - vscode.debug.registerDebugConfigurationProvider( - Contributions.ChromeDebugType, - chromeConfigProvider, - ), - vscode.debug.registerDebugConfigurationProvider( - Contributions.ExtensionHostDebugType, - extensionConfigProvider, - ), - vscode.debug.registerDebugConfigurationProvider( - Contributions.TerminalDebugType, - terminalConfigProvider, - ), + ...services + .getAll(IDebugConfigurationProvider) + .map(provider => vscode.debug.registerDebugConfigurationProvider(provider.type, provider)), ); const sessionManager = new SessionManager(services); context.subscriptions.push( - vscode.debug.registerDebugAdapterDescriptorFactory(Contributions.NodeDebugType, sessionManager), - vscode.debug.registerDebugAdapterDescriptorFactory( - Contributions.TerminalDebugType, - sessionManager, - ), - vscode.debug.registerDebugAdapterDescriptorFactory( - Contributions.ExtensionHostDebugType, - sessionManager, - ), - vscode.debug.registerDebugAdapterDescriptorFactory( - Contributions.ChromeDebugType, - sessionManager, + ...[...allDebugTypes].map(type => + vscode.debug.registerDebugAdapterDescriptorFactory(type, sessionManager), ), - vscode.debug.registerDebugAdapterDescriptorFactory(Contributions.EdgeDebugType, sessionManager), ); context.subscriptions.push( vscode.debug.onDidTerminateDebugSession(s => sessionManager.terminate(s)), diff --git a/src/flatSessionLauncher.ts b/src/flatSessionLauncher.ts index 6f446b9be..29c3bcf3a 100644 --- a/src/flatSessionLauncher.ts +++ b/src/flatSessionLauncher.ts @@ -20,7 +20,7 @@ import { ITarget } from './targets/targets'; import * as crypto from 'crypto'; import { MessageEmitterConnection, ChildConnection } from './dap/flatSessionConnection'; import { IDisposable } from './common/events'; -import { Contributions } from './common/contributionUtils'; +import { DebugType } from './common/contributionUtils'; import { TargetOrigin } from './targets/targetOrigin'; import { TelemetryReporter } from './telemetry/telemetryReporter'; import { ILogger } from './common/logging'; @@ -62,7 +62,7 @@ function main(inputStream: NodeJS.ReadableStream, outputStream: NodeJS.WritableS async acquireDap(target: ITarget): Promise { const sessionId = crypto.randomBytes(20).toString('hex'); const config = { - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, name: target.name(), request: 'attach', __pendingTargetId: target.id(), diff --git a/src/ioc-extras.ts b/src/ioc-extras.ts index 93b12333c..dd8e912a0 100644 --- a/src/ioc-extras.ts +++ b/src/ioc-extras.ts @@ -15,6 +15,11 @@ export const StoragePath = Symbol('StoragePath'); */ export const IsVSCode = Symbol('IsVSCode'); +/** + * Key for the vscode.ExtensionContext. Only available in the extension. + */ +export const ExtensionContext = Symbol('ExtensionContext'); + const toDispose = new WeakMap(); /** diff --git a/src/ioc.ts b/src/ioc.ts index 23d4bf299..1f7298c55 100644 --- a/src/ioc.ts +++ b/src/ioc.ts @@ -6,6 +6,7 @@ import 'reflect-metadata'; import { Container } from 'inversify'; +import * as vscode from 'vscode'; import { BreakpointsPredictor, IBreakpointsPredictor } from './adapter/breakpointPredictor'; import { IScriptSkipper, ScriptSkipper } from './adapter/scriptSkipper'; import Cdp from './cdp/api'; @@ -19,21 +20,24 @@ import { ISourcePathResolver } from './common/sourcePathResolver'; import { AnyLaunchConfiguration } from './configuration'; import Dap from './dap/api'; import { IDapApi } from './dap/connection'; -import { IsVSCode, StoragePath, trackDispose } from './ioc-extras'; +import { ExtensionContext, IsVSCode, StoragePath, trackDispose } from './ioc-extras'; import { BrowserAttacher } from './targets/browser/browserAttacher'; -import { BrowserLauncher } from './targets/browser/browserLauncher'; +import { ChromeLauncher } from './targets/browser/chromeLauncher'; +import { EdgeLauncher } from './targets/browser/edgeLauncher'; import { DelegateLauncherFactory } from './targets/delegate/delegateLauncherFactory'; import { ExtensionHostAttacher } from './targets/node/extensionHostAttacher'; import { ExtensionHostLauncher } from './targets/node/extensionHostLauncher'; import { NodeAttacher } from './targets/node/nodeAttacher'; import { NodeLauncher } from './targets/node/nodeLauncher'; import { INodePathProvider, NodePathProvider } from './targets/node/nodePathProvider'; +import { INvmResolver, NvmResolver } from './targets/node/nvmResolver'; import { IProgramLauncher } from './targets/node/processLauncher'; import { RestartPolicyFactory } from './targets/node/restartPolicy'; import { SubprocessProgramLauncher } from './targets/node/subprocessProgramLauncher'; import { TerminalProgramLauncher } from './targets/node/terminalProgramLauncher'; import { ITargetOrigin } from './targets/targetOrigin'; import { ILauncher, ITarget } from './targets/targets'; +import { IDebugConfigurationProvider } from './ui/configuration/configurationProvider'; /** * Contains IOC container factories for the extension. We use Inverisfy, which @@ -149,11 +153,16 @@ export const createTopLevelSessionContainer = (parent: Container) => { .to(NodeAttacher) .onActivation(trackDispose); container - .bind(BrowserLauncher) + .bind(ChromeLauncher) .toSelf() .inSingletonScope() .onActivation(trackDispose); - container.bind(ILauncher).toService(BrowserLauncher); + container.bind(ILauncher).toService(ChromeLauncher); + container + .bind(ILauncher) + .to(EdgeLauncher) + .inSingletonScope() + .onActivation(trackDispose); container .bind(ILauncher) .to(BrowserAttacher) @@ -166,7 +175,11 @@ export const createTopLevelSessionContainer = (parent: Container) => { return container; }; -export const createGlobalContainer = (options: { storagePath: string; isVsCode: boolean }) => { +export const createGlobalContainer = (options: { + storagePath: string; + isVsCode: boolean; + context?: vscode.ExtensionContext; +}) => { const container = new Container(); container .bind(DelegateLauncherFactory) @@ -175,6 +188,38 @@ export const createGlobalContainer = (options: { storagePath: string; isVsCode: container.bind(StoragePath).toConstantValue(options.storagePath); container.bind(IsVSCode).toConstantValue(options.isVsCode); + container.bind(INvmResolver).to(NvmResolver); + + if (options.context) { + container.bind(ExtensionContext).toConstantValue(options.context); + } + + // Dependency that pull from the vscode global--aren't safe to require at + // a top level (e.g. in the debug server) + if (options.isVsCode) { + const { + ChromeDebugConfigurationProvider, + EdgeDebugConfigurationProvider, + ExtensionHostConfigurationProvider, + NodeConfigurationProvider, + TerminalDebugConfigurationProvider, + // eslint-disable-next-line @typescript-eslint/no-var-requires + } = require('./ui/configuration'); + + [ + ChromeDebugConfigurationProvider, + EdgeDebugConfigurationProvider, + ExtensionHostConfigurationProvider, + NodeConfigurationProvider, + TerminalDebugConfigurationProvider, + ].forEach(cls => { + container + .bind(cls) + .toSelf() + .inSingletonScope(); + container.bind(IDebugConfigurationProvider).to(cls); + }); + } return container; }; diff --git a/src/targets/browser/browserAttacher.ts b/src/targets/browser/browserAttacher.ts index 847ac9e6d..214b3e637 100644 --- a/src/targets/browser/browserAttacher.ts +++ b/src/targets/browser/browserAttacher.ts @@ -11,7 +11,7 @@ import { ITarget, ILauncher, ILaunchResult, ILaunchContext, IStopMetadata } from import { BrowserSourcePathResolver } from './browserPathResolver'; import { baseURL } from './browserLaunchParams'; import { AnyLaunchConfiguration, IChromeAttachConfiguration } from '../../configuration'; -import { Contributions } from '../../common/contributionUtils'; +import { DebugType } from '../../common/contributionUtils'; import { TelemetryReporter } from '../../telemetry/telemetryReporter'; import { createTargetFilterForConfig } from '../../common/urlUtils'; import { delay } from '../../common/promiseUtil'; @@ -39,10 +39,6 @@ export class BrowserAttacher implements ILauncher { constructor(@inject(ILogger) private readonly logger: ILogger) {} - targetManager(): BrowserTargetManager | undefined { - return this._targetManager; - } - dispose() { for (const disposable of this._disposables) disposable.dispose(); this._disposables = []; @@ -55,7 +51,7 @@ export class BrowserAttacher implements ILauncher { { targetOrigin, cancellationToken, telemetryReporter }: ILaunchContext, clientCapabilities: Dap.InitializeParams, ): Promise { - if (params.type !== Contributions.ChromeDebugType || params.request !== 'attach') { + if (params.type !== DebugType.Chrome || params.request !== 'attach') { return { blockSessionTermination: false }; } @@ -211,7 +207,7 @@ export class BrowserAttacher implements ILauncher { } targetList(): ITarget[] { - const manager = this.targetManager(); + const manager = this._targetManager; return manager ? manager.targetList() : []; } } diff --git a/src/targets/browser/browserLaunchParams.ts b/src/targets/browser/browserLaunchParams.ts index 5349b4365..5071fa508 100644 --- a/src/targets/browser/browserLaunchParams.ts +++ b/src/targets/browser/browserLaunchParams.ts @@ -3,9 +3,9 @@ *--------------------------------------------------------*/ import { URL } from 'url'; -import { AnyChromeConfiguration } from '../../configuration'; +import { AnyChromiumConfiguration } from '../../configuration'; -export function baseURL(params: AnyChromeConfiguration): string | undefined { +export function baseURL(params: AnyChromiumConfiguration): string | undefined { if (params.url) { try { const baseUrl = new URL(params.url); diff --git a/src/targets/browser/browserLauncher.ts b/src/targets/browser/browserLauncher.ts index 5f8cbfc33..5f407d59e 100644 --- a/src/targets/browser/browserLauncher.ts +++ b/src/targets/browser/browserLauncher.ts @@ -2,37 +2,24 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import { createServer } from 'net'; -import { randomBytes } from 'crypto'; -import { tmpdir } from 'os'; import * as fs from 'fs'; import * as path from 'path'; import { CancellationToken } from 'vscode'; import * as nls from 'vscode-nls'; import CdpConnection from '../../cdp/connection'; -import { timeoutPromise, NeverCancelled } from '../../common/cancellation'; -import { Contributions } from '../../common/contributionUtils'; +import { timeoutPromise } from '../../common/cancellation'; import { EnvironmentVars } from '../../common/environmentVars'; import { EventEmitter, IDisposable } from '../../common/events'; -import { absolutePathToFileUrl, createTargetFilterForConfig } from '../../common/urlUtils'; -import { AnyChromeConfiguration, IChromeLaunchConfiguration } from '../../configuration'; +import { absolutePathToFileUrl } from '../../common/urlUtils'; +import { AnyChromiumLaunchConfiguration, AnyLaunchConfiguration } from '../../configuration'; import Dap from '../../dap/api'; -import { - ILaunchContext, - ILauncher, - ILaunchResult, - IStopMetadata, - ITarget, - IWebViewConnectionInfo, -} from '../../targets/targets'; +import { ILaunchContext, ILauncher, ILaunchResult, IStopMetadata, ITarget } from '../targets'; import { TelemetryReporter } from '../../telemetry/telemetryReporter'; import { baseURL } from './browserLaunchParams'; import { BrowserSourcePathResolver } from './browserPathResolver'; import { BrowserTarget, BrowserTargetManager } from './browserTargets'; import findBrowser from './findBrowser'; import * as launcher from './launcher'; -import { WebSocketTransport } from '../../cdp/transport'; -import { getDeferred } from '../../common/promiseUtil'; import { ILogger } from '../../common/logging'; import { injectable, inject } from 'inversify'; import { StoragePath } from '../../ioc-extras'; @@ -49,12 +36,12 @@ export interface IDapInitializeParamsWithExtensions extends Dap.InitializeParams } @injectable() -export class BrowserLauncher implements ILauncher { +export abstract class BrowserLauncher + implements ILauncher { private _connectionForTest: CdpConnection | undefined; - private _storagePath: string; private _targetManager: BrowserTargetManager | undefined; - private _launchParams: IChromeLaunchConfiguration | undefined; - private _mainTarget?: BrowserTarget; + private _launchParams: T | undefined; + protected _mainTarget?: BrowserTarget; private _disposables: IDisposable[] = []; private _onTerminatedEmitter = new EventEmitter(); readonly onTerminated = this._onTerminatedEmitter.event; @@ -62,22 +49,16 @@ export class BrowserLauncher implements ILauncher { readonly onTargetListChanged = this._onTargetListChangedEmitter.event; constructor( - @inject(StoragePath) storagePath: string, - @inject(ILogger) private readonly logger: ILogger, - ) { - this._storagePath = storagePath; - } - - targetManager(): BrowserTargetManager | undefined { - return this._targetManager; - } + @inject(StoragePath) private readonly storagePath: string, + @inject(ILogger) protected readonly logger: ILogger, + ) {} dispose() { for (const disposable of this._disposables) disposable.dispose(); this._disposables = []; } - async _launchBrowser( + protected async launchBrowser( { runtimeExecutable: executable, runtimeArgs, @@ -89,7 +70,7 @@ export class BrowserLauncher implements ILauncher { inspectUri, webRoot, launchUnelevated: launchUnelevated, - }: IChromeLaunchConfiguration, + }: T, dap: Dap.Api, cancellationToken: CancellationToken, telemetryReporter: TelemetryReporter, @@ -128,7 +109,7 @@ export class BrowserLauncher implements ILauncher { let resolvedDataDir: string | undefined; if (!executable || chromeVersions.has(executable) || userDataDir === true) { resolvedDataDir = path.join( - this._storagePath, + this.storagePath, runtimeArgs?.includes('--headless') ? '.headless-profile' : '.profile', ); } else if (typeof userDataDir === 'string') { @@ -136,7 +117,7 @@ export class BrowserLauncher implements ILauncher { } try { - fs.mkdirSync(this._storagePath); + fs.mkdirSync(this.storagePath); } catch (e) {} return await launcher.launch( @@ -163,85 +144,23 @@ export class BrowserLauncher implements ILauncher { ); } - async prepareWebViewLaunch( - params: IChromeLaunchConfiguration, - filter: (info: IWebViewConnectionInfo) => boolean, - telemetryReporter: TelemetryReporter, - ): Promise { - const promisedPort = getDeferred(); - - if (!params.runtimeExecutable) { - // runtimeExecutable is required for web view debugging. - promisedPort.resolve(params.port); - return promisedPort.promise; - } - - const exeName = params.runtimeExecutable.split(/\\|\//).pop(); - const pipeName = `VSCode_${randomBytes(12).toString('base64')}`; - // This is a known pipe name scheme described in the web view documentation - // https://docs.microsoft.com/microsoft-edge/hosting/webview2/reference/webview2.idl - const serverName = `\\\\.\\pipe\\WebView2\\Debugger\\${exeName}\\${pipeName}`; - - const server = createServer(stream => { - stream.on('data', async data => { - const info: IWebViewConnectionInfo = JSON.parse(data.toString()); - - // devtoolsActivePort will always start with the port number - // and look something like '92202\n ...' - const dtString = info.devtoolsActivePort || ''; - const dtPort = parseInt(dtString.split('\n').shift() || ''); - const port = params.port || dtPort || params.port; - - if (!this._mainTarget && filter(info)) { - promisedPort.resolve(port); - } - - // All web views started under our debugger are waiting to to be resumed. - const wsURL = `ws://${params.address}:${port}/devtools/${info.type}/${info.id}`; - const ws = await WebSocketTransport.create(wsURL, NeverCancelled); - const connection = new CdpConnection(ws, this.logger, telemetryReporter); - await connection.rootSession().Runtime.runIfWaitingForDebugger({}); - connection.close(); - }); - }); - server.on('close', () => promisedPort.resolve(params.port)); - server.listen(serverName); - - // We must set a user data directory so the DevToolsActivePort file will be written. - // See: https://crrev.com//21e1940/content/public/browser/devtools_agent_host.h#99 - params.userDataDir = - params.userDataDir || path.join(tmpdir(), `vscode-js-debug-userdatadir_${params.port}`); - - // Web views are indirectly configured for debugging with environment variables. - // See the WebView2 documentation for more details. - params.env = params.env || {}; - params.env['WEBVIEW2_USER_DATA_FOLDER'] = params.userDataDir.toString(); - params.env['WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS'] = `--remote-debugging-port=${params.port}`; - params.env['WEBVIEW2_WAIT_FOR_SCRIPT_DEBUGGER'] = 'true'; - params.env['WEBVIEW2_PIPE_FOR_SCRIPT_DEBUGGER'] = pipeName; - - return promisedPort.promise; - } - - async prepareLaunch( - params: IChromeLaunchConfiguration, + /** + * Starts the launch process. It boots the browser and waits until the target + * page is available, and then returns the newly-created target. + */ + private async prepareLaunch( + params: T, { dap, targetOrigin, cancellationToken, telemetryReporter }: ILaunchContext, clientCapabilities: IDapInitializeParamsWithExtensions, ): Promise { - const targetFilter = createTargetFilterForConfig(params); - const promisedPort = params.useWebView - ? this.prepareWebViewLaunch(params, targetFilter, telemetryReporter) - : undefined; - let launched: launcher.ILaunchResult; try { - launched = await this._launchBrowser( + launched = await this.launchBrowser( params, dap, cancellationToken, telemetryReporter, clientCapabilities, - promisedPort, ); } catch (e) { return localize('error.browserLaunchError', 'Unable to launch browser: "{0}"', e.message); @@ -294,12 +213,10 @@ export class BrowserLauncher implements ILauncher { this._onTargetListChangedEmitter.fire(); }); - const filter = params.useWebView ? targetFilter : undefined; - // Note: assuming first page is our main target breaks multiple debugging sessions // sharing the browser instance. This can be fixed. this._mainTarget = await timeoutPromise( - this._targetManager.waitForMainTarget(filter), + this._targetManager.waitForMainTarget(), cancellationToken, 'Could not attach to main target', ); @@ -311,10 +228,11 @@ export class BrowserLauncher implements ILauncher { return this._mainTarget; } - private async finishLaunch( - mainTarget: BrowserTarget, - params: AnyChromeConfiguration, - ): Promise { + /** + * Finalizes the launch after a page is available, navigating to the + * requested URL. + */ + private async finishLaunch(mainTarget: BrowserTarget, params: T): Promise { if ('skipNavigateForTest' in params) { return; } @@ -329,21 +247,34 @@ export class BrowserLauncher implements ILauncher { } } - async launch( - params: AnyChromeConfiguration, + /** + * @inheritdoc + */ + public async launch( + params: AnyLaunchConfiguration, context: ILaunchContext, clientCapabilities: Dap.InitializeParams, ): Promise { - if (params.type !== Contributions.ChromeDebugType || params.request !== 'launch') { + const resolved = this.resolveParams(params); + if (!resolved) { return { blockSessionTermination: false }; } - const targetOrError = await this.prepareLaunch(params, context, clientCapabilities); - if (typeof targetOrError === 'string') return { error: targetOrError }; - await this.finishLaunch(targetOrError, params); + const targetOrError = await this.prepareLaunch(resolved, context, clientCapabilities); + if (typeof targetOrError === 'string') { + return { error: targetOrError }; + } + + await this.finishLaunch(targetOrError, resolved); return { blockSessionTermination: true }; } + /** + * Returns the params type if they can be launched by this launcher, + * or undefined if they cannot. + */ + protected abstract resolveParams(params: AnyLaunchConfiguration): T | undefined; + async terminate(): Promise { if (this._mainTarget) { await this._mainTarget.cdp().Page.navigate({ url: 'about:blank' }); @@ -362,7 +293,7 @@ export class BrowserLauncher implements ILauncher { } targetList(): ITarget[] { - const manager = this.targetManager(); + const manager = this._targetManager; return manager ? manager.targetList() : []; } diff --git a/src/targets/browser/browserTargets.ts b/src/targets/browser/browserTargets.ts index ebf40b402..83fe8c9fd 100644 --- a/src/targets/browser/browserTargets.ts +++ b/src/targets/browser/browserTargets.ts @@ -13,7 +13,7 @@ import { FrameModel } from './frames'; import { ServiceWorkerModel } from './serviceWorkers'; import { ISourcePathResolver } from '../../common/sourcePathResolver'; import { ScriptSkipper } from '../../adapter/scriptSkipper'; -import { AnyChromeConfiguration } from '../../configuration'; +import { AnyChromiumConfiguration } from '../../configuration'; import { LogTag, ILogger } from '../../common/logging'; import { TelemetryReporter } from '../../telemetry/telemetryReporter'; import { killTree } from '../node/killTree'; @@ -46,7 +46,7 @@ export class BrowserTargetManager implements IDisposable { connection: CdpConnection, process: undefined | IBrowserProcess, sourcePathResolver: ISourcePathResolver, - launchParams: AnyChromeConfiguration, + launchParams: AnyChromiumConfiguration, logger: ILogger, telemetry: TelemetryReporter, targetOrigin: ITargetOrigin, @@ -74,7 +74,7 @@ export class BrowserTargetManager implements IDisposable { sourcePathResolver: ISourcePathResolver, private readonly logger: ILogger, private readonly telemetry: TelemetryReporter, - private readonly launchParams: AnyChromeConfiguration, + private readonly launchParams: AnyChromiumConfiguration, targetOrigin: ITargetOrigin, ) { this._connection = connection; diff --git a/src/targets/browser/chromeLauncher.ts b/src/targets/browser/chromeLauncher.ts new file mode 100644 index 000000000..1e561ce28 --- /dev/null +++ b/src/targets/browser/chromeLauncher.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { DebugType } from '../../common/contributionUtils'; +import { IChromeLaunchConfiguration, AnyLaunchConfiguration } from '../../configuration'; +import { injectable } from 'inversify'; +import { BrowserLauncher } from './browserLauncher'; + +@injectable() +export class ChromeLauncher extends BrowserLauncher { + /** + * @inheritdoc + */ + protected resolveParams(params: AnyLaunchConfiguration) { + return params.type === DebugType.Chrome && params.request === 'launch' ? params : undefined; + } +} diff --git a/src/targets/browser/edgeLauncher.ts b/src/targets/browser/edgeLauncher.ts new file mode 100644 index 000000000..97d80cbaa --- /dev/null +++ b/src/targets/browser/edgeLauncher.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { createServer } from 'net'; +import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; +import CdpConnection from '../../cdp/connection'; +import { IEdgeLaunchConfiguration, AnyLaunchConfiguration } from '../../configuration'; +import { IWebViewConnectionInfo } from '../targets'; +import { TelemetryReporter } from '../../telemetry/telemetryReporter'; +import { getDeferred } from '../../common/promiseUtil'; +import { WebSocketTransport } from '../../cdp/transport'; +import { NeverCancelled } from '../../common/cancellation'; +import { join } from 'path'; +import Dap from '../../dap/api'; +import { CancellationToken } from 'vscode'; +import { createTargetFilterForConfig } from '../../common/urlUtils'; +import { BrowserLauncher, IDapInitializeParamsWithExtensions } from './browserLauncher'; +import { DebugType } from '../../common/contributionUtils'; + +export class EdgeLauncher extends BrowserLauncher { + /** + * @inheritdoc + */ + protected resolveParams(params: AnyLaunchConfiguration) { + return params.type === DebugType.Edge && params.request === 'launch' ? params : undefined; + } + + /** + * @inheritdoc + */ + protected launchBrowser( + params: IEdgeLaunchConfiguration, + dap: Dap.Api, + cancellationToken: CancellationToken, + telemetryReporter: TelemetryReporter, + clientCapabilities: IDapInitializeParamsWithExtensions, + ) { + return super.launchBrowser( + params, + dap, + cancellationToken, + telemetryReporter, + clientCapabilities, + params.useWebView + ? this.getWebviewPort(params, createTargetFilterForConfig(params), telemetryReporter) + : undefined, + ); + } + + /** + * Gets the port number we should connect to to debug webviews in the target. + */ + private async getWebviewPort( + params: IEdgeLaunchConfiguration, + filter: (info: IWebViewConnectionInfo) => boolean, + telemetryReporter: TelemetryReporter, + ): Promise { + const promisedPort = getDeferred(); + + if (!params.runtimeExecutable) { + // runtimeExecutable is required for web view debugging. + promisedPort.resolve(params.port); + return promisedPort.promise; + } + + const exeName = params.runtimeExecutable.split(/\\|\//).pop(); + const pipeName = `VSCode_${randomBytes(12).toString('base64')}`; + // This is a known pipe name scheme described in the web view documentation + // https://docs.microsoft.com/microsoft-edge/hosting/webview2/reference/webview2.idl + const serverName = `\\\\.\\pipe\\WebView2\\Debugger\\${exeName}\\${pipeName}`; + + const server = createServer(stream => { + stream.on('data', async data => { + const info: IWebViewConnectionInfo = JSON.parse(data.toString()); + + // devtoolsActivePort will always start with the port number + // and look something like '92202\n ...' + const dtString = info.devtoolsActivePort || ''; + const dtPort = parseInt(dtString.split('\n').shift() || ''); + const port = params.port || dtPort || params.port; + + if (!this._mainTarget && filter(info)) { + promisedPort.resolve(port); + } + + // All web views started under our debugger are waiting to to be resumed. + const wsURL = `ws://${params.address}:${port}/devtools/${info.type}/${info.id}`; + const ws = await WebSocketTransport.create(wsURL, NeverCancelled); + const connection = new CdpConnection(ws, this.logger, telemetryReporter); + await connection.rootSession().Runtime.runIfWaitingForDebugger({}); + connection.close(); + }); + }); + server.on('error', promisedPort.reject); + server.on('close', () => promisedPort.resolve(params.port)); + server.listen(serverName); + + // We must set a user data directory so the DevToolsActivePort file will be written. + // See: https://crrev.com//21e1940/content/public/browser/devtools_agent_host.h#99 + params.userDataDir = + params.userDataDir || join(tmpdir(), `vscode-js-debug-userdatadir_${params.port}`); + + // Web views are indirectly configured for debugging with environment variables. + // See the WebView2 documentation for more details. + params.env = params.env || {}; + params.env['WEBVIEW2_USER_DATA_FOLDER'] = params.userDataDir.toString(); + params.env['WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS'] = `--remote-debugging-port=${params.port}`; + params.env['WEBVIEW2_WAIT_FOR_SCRIPT_DEBUGGER'] = 'true'; + params.env['WEBVIEW2_PIPE_FOR_SCRIPT_DEBUGGER'] = pipeName; + + return promisedPort.promise; + } +} diff --git a/src/targets/browser/launcher.ts b/src/targets/browser/launcher.ts index 9c74fe2fd..21f17da84 100644 --- a/src/targets/browser/launcher.ts +++ b/src/targets/browser/launcher.ts @@ -20,9 +20,9 @@ import { killTree } from '../node/killTree'; import { launchUnelevatedChrome } from './unelevatedChome'; import { IBrowserProcess, NonTrackedBrowserProcess } from './browserProcess'; import Dap from '../../dap/api'; -import { IDapInitializeParamsWithExtensions } from './browserLauncher'; import { constructInspectorWSUri } from './constructInspectorWSUri'; import { ILogger } from '../../common/logging'; +import { IDapInitializeParamsWithExtensions } from './browserLauncher'; const DEFAULT_ARGS = [ '--disable-background-networking', diff --git a/src/targets/delegate/delegateLauncher.ts b/src/targets/delegate/delegateLauncher.ts index 5f68b950a..986adac26 100644 --- a/src/targets/delegate/delegateLauncher.ts +++ b/src/targets/delegate/delegateLauncher.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import { ILauncher, ILaunchResult, ITarget, IStopMetadata, ILaunchContext } from '../targets'; import { AnyLaunchConfiguration } from '../../configuration'; -import { Contributions } from '../../common/contributionUtils'; +import { DebugType } from '../../common/contributionUtils'; import { ObservableMap } from '../targetList'; import { EventEmitter } from '../../common/events'; import { IPendingDapApi } from '../../dap/pending-api'; @@ -78,7 +78,7 @@ export class DelegateLauncher implements ILauncher { params: AnyLaunchConfiguration, context: ILaunchContext, ): Promise { - if (params.type !== Contributions.TerminalDebugType || params.request !== 'attach') { + if (params.type !== DebugType.Terminal || params.request !== 'attach') { return { blockSessionTermination: false }; } diff --git a/src/targets/node/extensionHostAttacher.ts b/src/targets/node/extensionHostAttacher.ts index 6293209f1..363b3dd06 100644 --- a/src/targets/node/extensionHostAttacher.ts +++ b/src/targets/node/extensionHostAttacher.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import { AnyLaunchConfiguration, IExtensionHostConfiguration } from '../../configuration'; -import { Contributions } from '../../common/contributionUtils'; +import { DebugType } from '../../common/contributionUtils'; import { IRunData } from './nodeLauncherBase'; import { SubprocessProgram, TerminalProcess } from './program'; import { retryGetWSEndpoint } from '../browser/launcher'; @@ -36,7 +36,7 @@ export class ExtensionHostAttacher extends NodeAttacherBase { * @inheritdoc */ protected resolveParams(params: AnyLaunchConfiguration): INodeAttachConfiguration | undefined { - return params.type === Contributions.NodeDebugType && params.request === 'attach' - ? params - : undefined; + return params.type === DebugType.Node && params.request === 'attach' ? params : undefined; } /** diff --git a/src/targets/node/nodeLauncher.ts b/src/targets/node/nodeLauncher.ts index 5d1f55816..bba61c549 100644 --- a/src/targets/node/nodeLauncher.ts +++ b/src/targets/node/nodeLauncher.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import { INodeLaunchConfiguration, AnyLaunchConfiguration } from '../../configuration'; -import { Contributions } from '../../common/contributionUtils'; +import { DebugType } from '../../common/contributionUtils'; import { IProgramLauncher } from './processLauncher'; import { CallbackFile } from './callback-file'; import { RestartPolicyFactory, IRestartPolicy } from './restartPolicy'; @@ -64,13 +64,9 @@ export class NodeLauncher extends NodeLauncherBase { */ protected resolveParams(params: AnyLaunchConfiguration): INodeLaunchConfiguration | undefined { let config: INodeLaunchConfiguration | undefined; - if (params.type === Contributions.NodeDebugType && params.request === 'launch') { + if (params.type === DebugType.Node && params.request === 'launch') { config = { ...params }; - } else if ( - params.type === Contributions.ChromeDebugType && - params.server && - 'program' in params.server - ) { + } else if (params.type === DebugType.Chrome && params.server && 'program' in params.server) { config = { ...params.server }; } diff --git a/src/targets/node/nvmResolver.ts b/src/targets/node/nvmResolver.ts index 26264060f..793a569ae 100644 --- a/src/targets/node/nvmResolver.ts +++ b/src/targets/node/nvmResolver.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { nvmHomeNotFound, nvmNotFound, nvmVersionNotFound } from '../../dap/errors'; +import { injectable } from 'inversify'; /** * Resolves the location of Node installation querying an nvm installation. @@ -17,7 +18,10 @@ export interface INvmResolver { resolveNvmVersionPath(version: string): Promise; } -export class NvmResolver { +export const INvmResolver = Symbol('INvmResolver'); + +@injectable() +export class NvmResolver implements INvmResolver { constructor( private readonly env = process.env, private readonly arch = process.arch, diff --git a/src/targets/node/terminalNodeLauncher.ts b/src/targets/node/terminalNodeLauncher.ts index 9ad9e42a6..53795e09e 100644 --- a/src/targets/node/terminalNodeLauncher.ts +++ b/src/targets/node/terminalNodeLauncher.ts @@ -4,7 +4,7 @@ import { AnyLaunchConfiguration, ITerminalLaunchConfiguration } from '../../configuration'; import * as vscode from 'vscode'; -import { Contributions } from '../../common/contributionUtils'; +import { DebugType } from '../../common/contributionUtils'; import { NodeLauncherBase, IRunData } from './nodeLauncherBase'; import { IProgram } from './program'; import { IStopMetadata } from '../targets'; @@ -45,15 +45,11 @@ export class TerminalNodeLauncher extends NodeLauncherBase { - let provider: NodeDebugConfigurationProvider; + let provider: NodeConfigurationProvider; let nvmResolver: { resolveNvmVersionPath: SinonStub }; const folder: vscode.WorkspaceFolder = { uri: vscode.Uri.file(testFixturesDir), @@ -23,7 +23,7 @@ describe('NodeDebugConfigurationProvider', () => { beforeEach(() => { nvmResolver = { resolveNvmVersionPath: stub() }; - provider = new NodeDebugConfigurationProvider({ logPath: testFixturesDir } as any, nvmResolver); + provider = new NodeConfigurationProvider({ logPath: testFixturesDir } as any, nvmResolver); EnvironmentVars.platform = 'linux'; }); @@ -190,7 +190,7 @@ describe('NodeDebugConfigurationProvider', () => { nvmResolver.resolveNvmVersionPath.resolves('/my/node/location'); const result = await provider.resolveDebugConfiguration(folder, { - type: Contributions.NodeDebugType, + type: DebugType.Node, name: '', request: 'launch', program: 'hello.js', @@ -209,7 +209,7 @@ describe('NodeDebugConfigurationProvider', () => { describe('inspect flags', () => { it('demaps', async () => { const result = (await provider.resolveDebugConfiguration(folder, { - type: Contributions.NodeDebugType, + type: DebugType.Node, name: '', request: 'launch', program: 'hello.js', @@ -222,7 +222,7 @@ describe('NodeDebugConfigurationProvider', () => { it('does not overwrite existing stop on entry', async () => { const result = (await provider.resolveDebugConfiguration(folder, { - type: Contributions.NodeDebugType, + type: DebugType.Node, name: '', request: 'launch', program: 'hello.js', diff --git a/src/test/test.ts b/src/test/test.ts index f050eb228..e3a2ab282 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -22,7 +22,7 @@ import { } from '../configuration'; import Dap from '../dap/api'; import DapConnection from '../dap/connection'; -import { BrowserLauncher } from '../targets/browser/browserLauncher'; +import { ChromeLauncher } from '../targets/browser/chromeLauncher'; import { ITarget } from '../targets/targets'; import { GoldenText } from './goldenText'; import { Logger } from './logger'; @@ -306,7 +306,7 @@ export class TestRoot { private _workerCallback: (session: ITestHandle) => void; private _launchCallback: (session: ITestHandle) => void; - _browserLauncher: BrowserLauncher; + _browserLauncher: ChromeLauncher; readonly binder: Binder; private _onSessionCreatedEmitter = new EventEmitter(); @@ -337,7 +337,7 @@ export class TestRoot { const services = createTopLevelSessionContainer( createGlobalContainer({ storagePath, isVsCode: true }), ); - this._browserLauncher = services.get(BrowserLauncher); + this._browserLauncher = services.get(ChromeLauncher); this.binder = new Binder( this, this._root.adapterConnection, diff --git a/src/baseConfigurationProvider.ts b/src/ui/configuration/baseConfigurationProvider.ts similarity index 83% rename from src/baseConfigurationProvider.ts rename to src/ui/configuration/baseConfigurationProvider.ts index 3f97bbc36..928d94cc8 100644 --- a/src/baseConfigurationProvider.ts +++ b/src/ui/configuration/baseConfigurationProvider.ts @@ -3,16 +3,29 @@ *--------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ResolvingConfiguration, AnyLaunchConfiguration } from './configuration'; -import { fulfillLoggerOptions } from './common/logging'; +import { ResolvingConfiguration, AnyLaunchConfiguration } from '../../configuration'; +import { fulfillLoggerOptions } from '../../common/logging'; +import { injectable, inject } from 'inversify'; +import { ExtensionContext } from '../../ioc-extras'; +import { IDebugConfigurationProvider } from './configurationProvider'; /** * Base configuration provider that handles some resolution around common * options and handles errors. */ +@injectable() export abstract class BaseConfigurationProvider - implements vscode.DebugConfigurationProvider { - constructor(protected readonly extensionContext: vscode.ExtensionContext) {} + implements IDebugConfigurationProvider { + /** + * @inheritdoc + */ + public get type() { + return this.getType(); + } + + constructor( + @inject(ExtensionContext) protected readonly extensionContext: vscode.ExtensionContext, + ) {} /** * @inheritdoc @@ -96,4 +109,9 @@ export abstract class BaseConfigurationProvider + implements vscode.DebugConfigurationProvider { + /** + * @override + */ + protected async resolveDebugConfigurationAsync( + folder: vscode.WorkspaceFolder | undefined, + config: ResolvingChromeConfiguration, + ): Promise { + if (!config.name && !config.type && !config.request) { + const fromContext = this.createLaunchConfigFromContext(); + if (!fromContext) { + // Return null so it will create a launch.json and fall back on + // provideDebugConfigurations - better to point the user towards + // the config than try to work automagically for complex scenarios. + return; + } + + config = fromContext; + } + + await this.resolveBrowserCommon(folder, config); + + return config.request === 'attach' + ? { ...chromeAttachConfigDefaults, ...config } + : { ...chromeLaunchConfigDefaults, ...config }; + } + + protected createLaunchConfigFromContext(): ResolvingChromeConfiguration | void { + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.languageId === 'html') { + return { + type: DebugType.Chrome, + request: 'launch', + name: `Open ${basename(editor.document.uri.fsPath)}`, + file: editor.document.uri.fsPath, + }; + } + + return undefined; + } + + protected getDefaultAttachment(): ResolvingChromeConfiguration { + return { + type: DebugType.Chrome, + request: 'launch', + name: localize('chrome.launch.name', 'Launch Chrome against localhost'), + url: 'http://localhost:8080', + webRoot: '${workspaceFolder}', + }; + } + + protected getType() { + return DebugType.Chrome as const; + } +} diff --git a/src/ui/configuration/chromiumDebugConfigurationProvider.ts b/src/ui/configuration/chromiumDebugConfigurationProvider.ts new file mode 100644 index 000000000..19416d4a1 --- /dev/null +++ b/src/ui/configuration/chromiumDebugConfigurationProvider.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { + INodeLaunchConfiguration, + AnyChromiumConfiguration, + ResolvingConfiguration, +} from '../../configuration'; +import { NodeConfigurationProvider } from './nodeDebugConfigurationProvider'; +import { DebugType } from '../../common/contributionUtils'; +import { BaseConfigurationProvider } from './baseConfigurationProvider'; +import { TerminalDebugConfigurationProvider } from './terminalDebugConfigurationProvider'; +import { injectable, inject } from 'inversify'; +import { ExtensionContext } from '../../ioc-extras'; + +/** + * Configuration provider for Chrome debugging. + */ +@injectable() +export abstract class ChromiumDebugConfigurationProvider + extends BaseConfigurationProvider + implements vscode.DebugConfigurationProvider { + constructor( + @inject(ExtensionContext) context: vscode.ExtensionContext, + @inject(NodeConfigurationProvider) + private readonly nodeProvider: NodeConfigurationProvider, + @inject(TerminalDebugConfigurationProvider) + private readonly terminalProvider: TerminalDebugConfigurationProvider, + ) { + super(context); + + this.setProvideDefaultConfiguration( + () => this.createLaunchConfigFromContext() || this.getDefaultAttachment(), + ); + } + + protected async resolveBrowserCommon( + folder: vscode.WorkspaceFolder | undefined, + config: ResolvingConfiguration, + ) { + if (config.request === 'attach') { + // todo https://github.com/microsoft/vscode-chrome-debug/blob/ee5ae7ac7734f369dba58ba57bb910aac467c97a/src/extension.ts#L48 + } + + if (config.server && 'program' in config.server) { + const serverOpts = { + ...config.server, + type: DebugType.Node, + request: 'launch', + name: `${config.name}: Server`, + }; + + config.server = (await this.nodeProvider.resolveDebugConfiguration( + folder, + serverOpts, + )) as INodeLaunchConfiguration; + } else if (config.server && 'command' in config.server) { + config.server = await this.terminalProvider.resolveDebugConfiguration(folder, { + ...config.server, + type: DebugType.Terminal, + request: 'launch', + name: `${config.name}: Server`, + }); + } + } + + protected abstract getDefaultAttachment(): ResolvingConfiguration; + + protected abstract createLaunchConfigFromContext(): ResolvingConfiguration | void; +} diff --git a/src/ui/configuration/configurationProvider.ts b/src/ui/configuration/configurationProvider.ts new file mode 100644 index 000000000..f15364a8a --- /dev/null +++ b/src/ui/configuration/configurationProvider.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export interface IDebugConfigurationProvider extends vscode.DebugConfigurationProvider { + readonly type: string; +} + +export const IDebugConfigurationProvider = Symbol('IDebugConfigurationProvider'); diff --git a/src/ui/configuration/edgeDebugConfigurationProvider.ts b/src/ui/configuration/edgeDebugConfigurationProvider.ts new file mode 100644 index 000000000..f3f6ef7d3 --- /dev/null +++ b/src/ui/configuration/edgeDebugConfigurationProvider.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { + edgeAttachConfigDefaults, + edgeLaunchConfigDefaults, + ResolvingEdgeConfiguration, + AnyEdgeConfiguration, +} from '../../configuration'; +import { DebugType } from '../../common/contributionUtils'; +import { basename } from 'path'; +import { ChromiumDebugConfigurationProvider } from './chromiumDebugConfigurationProvider'; +import { injectable } from 'inversify'; + +const localize = nls.loadMessageBundle(); + +/** + * Configuration provider for Chrome debugging. + */ +@injectable() +export class EdgeDebugConfigurationProvider + extends ChromiumDebugConfigurationProvider + implements vscode.DebugConfigurationProvider { + /** + * @override + */ + protected async resolveDebugConfigurationAsync( + folder: vscode.WorkspaceFolder | undefined, + config: ResolvingEdgeConfiguration, + ): Promise { + if (!config.name && !config.type && !config.request) { + const fromContext = this.createLaunchConfigFromContext(); + if (!fromContext) { + // Return null so it will create a launch.json and fall back on + // provideDebugConfigurations - better to point the user towards + // the config than try to work automagically for complex scenarios. + return; + } + + config = fromContext; + } + + await this.resolveBrowserCommon(folder, config); + + // Disable attachment timeouts for webview apps. We aren't opening a + // browser immediately, and it may take an arbitrary amount of time within + // the app until a debuggable webview appears. + if (config.useWebView) { + config.timeout = config.timeout ?? 0; + } + + return config.request === 'attach' + ? { ...edgeAttachConfigDefaults, ...config } + : { ...edgeLaunchConfigDefaults, ...config }; + } + + protected createLaunchConfigFromContext(): ResolvingEdgeConfiguration | void { + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.languageId === 'html') { + return { + type: DebugType.Edge, + request: 'launch', + name: `Open ${basename(editor.document.uri.fsPath)}`, + file: editor.document.uri.fsPath, + }; + } + + return undefined; + } + + protected getDefaultAttachment(): ResolvingEdgeConfiguration { + return { + type: DebugType.Edge, + request: 'launch', + name: localize('edge.launch.name', 'Launch Chrome against localhost'), + url: 'http://localhost:8080', + webRoot: '${workspaceFolder}', + }; + } + + protected getType() { + return DebugType.Edge as const; + } +} diff --git a/src/extensionHostConfigurationProvider.ts b/src/ui/configuration/extensionHostConfigurationProvider.ts similarity index 80% rename from src/extensionHostConfigurationProvider.ts rename to src/ui/configuration/extensionHostConfigurationProvider.ts index 3da06126f..65688c8d5 100644 --- a/src/extensionHostConfigurationProvider.ts +++ b/src/ui/configuration/extensionHostConfigurationProvider.ts @@ -7,12 +7,15 @@ import { extensionHostConfigDefaults, IExtensionHostConfiguration, ResolvingExtensionHostConfiguration, -} from './configuration'; +} from '../../configuration'; import { BaseConfigurationProvider } from './baseConfigurationProvider'; +import { injectable } from 'inversify'; +import { DebugType } from '../../common/contributionUtils'; /** * Configuration provider for Extension host debugging. */ +@injectable() export class ExtensionHostConfigurationProvider extends BaseConfigurationProvider implements vscode.DebugConfigurationProvider { @@ -25,4 +28,8 @@ export class ExtensionHostConfigurationProvider ...config, }); } + + protected getType() { + return DebugType.ExtensionHost as const; + } } diff --git a/src/ui/configuration/index.ts b/src/ui/configuration/index.ts new file mode 100644 index 000000000..541455a7d --- /dev/null +++ b/src/ui/configuration/index.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +export * from './configurationProvider'; +export { ChromeDebugConfigurationProvider } from './chromeDebugConfigurationProvider'; +export { EdgeDebugConfigurationProvider } from './edgeDebugConfigurationProvider'; +export { ExtensionHostConfigurationProvider } from './extensionHostConfigurationProvider'; +export { NodeConfigurationProvider } from './nodeDebugConfigurationProvider'; +export { TerminalDebugConfigurationProvider } from './terminalDebugConfigurationProvider'; diff --git a/src/nodeDebugConfigurationProvider.ts b/src/ui/configuration/nodeDebugConfigurationProvider.ts similarity index 91% rename from src/nodeDebugConfigurationProvider.ts rename to src/ui/configuration/nodeDebugConfigurationProvider.ts index 55bd0e2d3..40b530d0e 100644 --- a/src/nodeDebugConfigurationProvider.ts +++ b/src/ui/configuration/nodeDebugConfigurationProvider.ts @@ -6,23 +6,25 @@ import * as nls from 'vscode-nls'; import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; -import { writeToConsole } from './common/console'; +import { writeToConsole } from '../../common/console'; import { nodeAttachConfigDefaults, nodeLaunchConfigDefaults, ResolvingNodeAttachConfiguration, ResolvingNodeLaunchConfiguration, AnyNodeConfiguration, -} from './configuration'; -import { Contributions } from './common/contributionUtils'; -import { NvmResolver, INvmResolver } from './targets/node/nvmResolver'; -import { EnvironmentVars } from './common/environmentVars'; -import { resolveProcessId } from './ui/processPicker'; +} from '../../configuration'; +import { DebugType } from '../../common/contributionUtils'; +import { INvmResolver } from '../../targets/node/nvmResolver'; +import { EnvironmentVars } from '../../common/environmentVars'; +import { resolveProcessId } from '../processPicker'; import { BaseConfigurationProvider } from './baseConfigurationProvider'; -import { fixInspectFlags } from './ui/configurationUtils'; +import { fixInspectFlags } from '../configurationUtils'; +import { injectable, inject } from 'inversify'; +import { ExtensionContext } from '../../ioc-extras'; // eslint-disable-next-line -const config = require('../package.json'); +const config = require('../../../package.json'); const localize = nls.loadMessageBundle(); @@ -39,14 +41,13 @@ type ResolvingNodeConfiguration = * close to 1:1 drop-in, this is nearly identical to the original vscode- * node-debug, with support for some legacy options (mern, useWSL) removed. */ -export class NodeDebugConfigurationProvider extends BaseConfigurationProvider - implements vscode.DebugConfigurationProvider { +@injectable() +export class NodeConfigurationProvider extends BaseConfigurationProvider { constructor( - context: vscode.ExtensionContext, - private readonly nvmResolver: INvmResolver = new NvmResolver(), + @inject(ExtensionContext) context: vscode.ExtensionContext, + @inject(INvmResolver) private readonly nvmResolver: INvmResolver, ) { super(context); - this.setProvideDefaultConfiguration(folder => createLaunchConfigFromContext(folder, true)); } @@ -110,6 +111,10 @@ export class NodeDebugConfigurationProvider extends BaseConfigurationProvider/**'], diff --git a/src/terminalDebugConfigurationProvider.ts b/src/ui/configuration/terminalDebugConfigurationProvider.ts similarity index 90% rename from src/terminalDebugConfigurationProvider.ts rename to src/ui/configuration/terminalDebugConfigurationProvider.ts index 0409eef25..e4a310934 100644 --- a/src/terminalDebugConfigurationProvider.ts +++ b/src/ui/configuration/terminalDebugConfigurationProvider.ts @@ -7,9 +7,10 @@ import { ResolvedConfiguration, terminalBaseDefaults, ITerminalLaunchConfiguration, -} from './configuration'; +} from '../../configuration'; import { BaseConfigurationProvider } from './baseConfigurationProvider'; import { guessWorkingDirectory } from './nodeDebugConfigurationProvider'; +import { DebugType } from '../../common/contributionUtils'; /** * Configuration provider for node debugging. In order to allow for a @@ -35,4 +36,8 @@ export class TerminalDebugConfigurationProvider return { ...terminalBaseDefaults, ...config }; } + + protected getType() { + return DebugType.Terminal as const; + } } diff --git a/src/ui/debugSessionTracker.ts b/src/ui/debugSessionTracker.ts index ea0efc64d..983dba52f 100644 --- a/src/ui/debugSessionTracker.ts +++ b/src/ui/debugSessionTracker.ts @@ -4,7 +4,7 @@ import * as vscode from 'vscode'; import Dap from '../dap/api'; -import { Contributions } from '../common/contributionUtils'; +import { DebugType } from '../common/contributionUtils'; export class DebugSessionTracker implements vscode.Disposable { private _onSessionAddedEmitter = new vscode.EventEmitter(); @@ -16,10 +16,7 @@ export class DebugSessionTracker implements vscode.Disposable { public attach() { vscode.debug.onDidStartDebugSession( session => { - if ( - session.type === Contributions.ChromeDebugType && - session.configuration.request === 'attach' - ) { + if (session.type === DebugType.Chrome && session.configuration.request === 'attach') { this.sessions.set(session.id, session); this._onSessionAddedEmitter.fire(session); } @@ -38,10 +35,7 @@ export class DebugSessionTracker implements vscode.Disposable { vscode.debug.onDidReceiveDebugSessionCustomEvent( event => { - if ( - event.session.type !== Contributions.ChromeDebugType && - event.session.type !== Contributions.NodeDebugType - ) { + if (event.session.type !== DebugType.Chrome && event.session.type !== DebugType.Node) { return; } diff --git a/src/ui/debugTerminalUI.ts b/src/ui/debugTerminalUI.ts index 30494aab8..9e2b4a8c5 100644 --- a/src/ui/debugTerminalUI.ts +++ b/src/ui/debugTerminalUI.ts @@ -8,6 +8,7 @@ import { registerCommand, readConfig, Configuration, + DebugType, } from '../common/contributionUtils'; import { TerminalNodeLauncher } from '../targets/node/terminalNodeLauncher'; import { NodePathProvider } from '../targets/node/nodePathProvider'; @@ -68,7 +69,7 @@ function launchTerminal( workspaceFolder, applyDefaults({ ...baseDebugOptions, - type: Contributions.TerminalDebugType, + type: DebugType.Terminal, name: 'Node.js Process', request: 'attach', delegateId, @@ -88,7 +89,7 @@ function launchTerminal( launcher.launch( applyDefaults({ ...baseDebugOptions, - type: Contributions.TerminalDebugType, + type: DebugType.Terminal, name: 'Debugger Terminal', request: 'launch', command, diff --git a/src/ui/sessionManager.ts b/src/ui/sessionManager.ts index f77f179de..62e81a502 100644 --- a/src/ui/sessionManager.ts +++ b/src/ui/sessionManager.ts @@ -8,7 +8,7 @@ import { Binder, IBinderDelegate } from '../binder'; import DapConnection from '../dap/connection'; import { ITarget } from '../targets/targets'; import { IDisposable } from '../common/events'; -import { Contributions } from '../common/contributionUtils'; +import { DebugType } from '../common/contributionUtils'; import { TargetOrigin } from '../targets/targetOrigin'; import { TelemetryReporter } from '../telemetry/telemetryReporter'; import { ILogger } from '../common/logging'; @@ -159,7 +159,7 @@ export class SessionManager this._sessionForTargetCallbacks.set(target, { fulfill, reject }); const config = { - type: Contributions.ChromeDebugType, + type: DebugType.Chrome, name: target.name(), request: 'attach', __pendingTargetId: target.id(),