diff --git a/OPTIONS.md b/OPTIONS.md index f59a48a3f..6c54fbbbd 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -231,7 +231,8 @@
trueToggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.
trueToggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the ms-vscode.wasm-dwarf-debugging extension to function.
trueOptional dictionary of environment key/value pairs for the browser.
-{}A local html file to open in the browser
+{}Absolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.
+nullA local html file to open in the browser
nullWhether default browser launch arguments (to disable features that may make debugging harder) will be included in the launch.
trueAdvanced: whether any default launch/debugging arguments are set on the browser. The debugger will assume the browser will use pipe debugging such as that which is provided with --remote-debugging-pipe.
trueFormat to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- browserInspectUriPath is the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.
undefinedControls whether to skip the network cache for each request
trueToggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.
trueToggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the ms-vscode.wasm-dwarf-debugging extension to function.
trueFormat to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- browserInspectUriPath is the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.
trueAbsolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.
+nullFormat to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- browserInspectUriPath is the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.
undefinedIf source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with ! the files are excluded. If not specified, the generated code is expected in the same directory as its source.
[ "${workspaceFolder}/**/*.(m|c|)js", @@ -340,7 +342,8 @@Default value:
trueenableContentValidation
Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.
Default value:
trueenableDWARF
Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the
ms-vscode.wasm-dwarf-debuggingextension to function.Default value:
trueenv
Optional dictionary of environment key/value pairs for the browser.
-Default value:
{}file
A local html file to open in the browser
+Default value:
{}extensionPath
Absolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.
+Default value:
nullfile
A local html file to open in the browser
Default value:
nullincludeDefaultArgs
Whether default browser launch arguments (to disable features that may make debugging harder) will be included in the launch.
Default value:
trueincludeLaunchArgs
Advanced: whether any default launch/debugging arguments are set on the browser. The debugger will assume the browser will use pipe debugging such as that which is provided with
--remote-debugging-pipe.Default value:
trueinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
@@ -397,7 +400,8 @@{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
undefineddisableNetworkCache
Controls whether to skip the network cache for each request
Default value:
trueenableContentValidation
Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.
Default value:
trueenableDWARF
Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the
-ms-vscode.wasm-dwarf-debuggingextension to function.Default value:
trueinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
+{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
trueextensionPath
Absolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.
+Default value:
nullinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
undefinedoutFiles
If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with
!the files are excluded. If not specified, the generated code is expected in the same directory as its source.Default value:
[ "${workspaceFolder}/**/*.(m|c|)js", @@ -446,7 +450,8 @@Default value:
undefineddisableNetworkCache
Controls whether to skip the network cache for each request
Default value:
trueenableContentValidation
Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.
Default value:
trueenableDWARF
Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the
-ms-vscode.wasm-dwarf-debuggingextension to function.Default value:
trueinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
+{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
trueextensionPath
Absolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.
+Default value:
nullinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
undefinedoutFiles
If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with
!the files are excluded. If not specified, the generated code is expected in the same directory as its source.Default value:
[ "${workspaceFolder}/**/*.(m|c|)js", @@ -491,7 +496,8 @@Default value:
undefineddisableNetworkCache
Controls whether to skip the network cache for each request
Default value:
trueenableContentValidation
Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.
Default value:
trueenableDWARF
Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the
-ms-vscode.wasm-dwarf-debuggingextension to function.Default value:
trueinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
+{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
trueextensionPath
Absolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.
+Default value:
nullinspectUri
Format to use to rewrite the inspectUri: It's a template string that interpolates keys in
{curlyBraces}. Available keys are:
-url.*is the parsed address of the running application. For instance,{url.port},{url.hostname}
-portis the debug port that Chrome is listening on.
-browserInspectUriis the inspector URI on the launched browser
-browserInspectUriPathis the path part of the inspector URI on the launched browser (e.g.: "/devtools/browser/e9ec0098-306e-472a-8133-5e42488929c2").
-wsProtocolis the hinted websocket protocol. This is set towssif the original URL ishttps, orwsotherwise.Default value:
undefinedoutFiles
If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with
!the files are excluded. If not specified, the generated code is expected in the same directory as its source.Default value:
[ "${workspaceFolder}/**/*.(m|c|)js", diff --git a/package.nls.json b/package.nls.json index e19e9f450..ebb70b2a0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -26,6 +26,7 @@ "browser.profileStartup.description": "If true, will start profiling soon as the process launches", "browser.restart": "Whether to reconnect if the browser connection is closed", "browser.revealPage": "Focus Tab", + "browser.extensionPath.description": "Absolute path to an unpacked browser extension directory (containing manifest.json) to load and debug. The debugger will attach to the extension's background script or service worker.", "browser.runtimeArgs.description": "Optional arguments passed to the runtime executable.", "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.", "browser.runtimeExecutable.edge.description": "Either 'canary', 'stable', 'dev', 'custom' or path to the browser executable. Custom means a custom wrapper, custom build or EDGE_PATH environment variable.", @@ -47,6 +48,8 @@ "chrome.label": "Web App (Chrome)", "chrome.launch.description": "Launch Chrome to debug a URL", "chrome.launch.label": "Chrome: Launch", + "chrome.launch.extension.description": "Launch Chrome to debug an unpacked browser extension", + "chrome.launch.extension.label": "Chrome: Launch Extension", "editorBrowser.attach.description": "Attach to an open VS Code integrated browser", "editorBrowser.attach.label": "Integrated Browser: Attach", "editorBrowser.label": "Web App (Integrated Browser)", diff --git a/src/build/generate-contributions.ts b/src/build/generate-contributions.ts index e27f9b5b9..a7dea0dcc 100644 --- a/src/build/generate-contributions.ts +++ b/src/build/generate-contributions.ts @@ -793,6 +793,11 @@ const chromiumBaseConfigurationAttributes: ConfigurationAttributes= { webRoot: '^"${2:\\${workspaceFolder\\}}"', }, }, + { + label: refString('chrome.launch.extension.label'), + description: refString('chrome.launch.extension.description'), + body: { + type: DebugType.Chrome, + request: 'launch', + name: 'Launch Chrome Extension', + extensionPath: '^"${1:\\${workspaceFolder\\}}"', + webRoot: '^"${2:\\${workspaceFolder\\}}"', + }, + }, ], configurationAttributes: { ...chromiumBaseConfigurationAttributes, diff --git a/src/cdp/connection.ts b/src/cdp/connection.ts index ade3b967d..febe68881 100644 --- a/src/cdp/connection.ts +++ b/src/cdp/connection.ts @@ -105,8 +105,23 @@ export default class Connection { if (!session) { const disposedDate = this._disposedSessions.get(object.sessionId); if (!disposedDate) { + if (object.method) { + // Event (not a response) on an unregistered session. This can happen + // when Chrome pushes events (e.g. Inspector.workerScriptLoaded) on a + // new session before our createSession() call completes — a race + // between Target.attachToTarget's response and early CDP events. + // Safe to drop: events are fire-and-forget with no waiting caller. + this.logger.warn( + LogTag.Internal, + `Got event for unknown session, ignoring`, + { sessionId: object.sessionId, method: object.method }, + ); + return; + } + // A command response on an unregistered session is a real bug — we + // only send commands after createSession(), so this should never happen. throw new Error( - `Unknown session id: ${object.sessionId} while processing: ${object.method}`, + `Unknown session id: ${object.sessionId} while processing response`, ); } else { const secondsAgo = (Date.now() - disposedDate.getTime()) / 1000.0; diff --git a/src/configuration.ts b/src/configuration.ts index f311d499d..bb83373d4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -454,6 +454,14 @@ export interface INodeLaunchConfiguration extends INodeBaseConfiguration, IConfi export type PathMapping = Readonly<{ [key: string]: string }>; export interface IChromiumBaseConfiguration extends IBaseConfiguration { + /** + * Absolute path to the root directory of an unpacked browser extension to + * load and debug. When set, the debugger automatically passes + * --load-extension to Chrome/Edge (launch only) and attaches to the + * extension's background script or service worker rather than a web page. + */ + extensionPath: string | null; + /** * Controls whether to skip the network cache for each request. */ @@ -959,6 +967,7 @@ export const chromeAttachConfigDefaults: IChromeAttachConfiguration = { request: 'attach', address: 'localhost', port: 0, + extensionPath: null, disableNetworkCache: true, pathMapping: {}, url: null, @@ -996,6 +1005,7 @@ export const chromeLaunchConfigDefaults: IChromeLaunchConfiguration = { profileStartup: false, cleanUp: 'wholeBrowser', killBehavior: KillBehavior.Forceful, + extensionPath: null, }; export const edgeLaunchConfigDefaults: IEdgeLaunchConfiguration = { @@ -1006,6 +1016,7 @@ export const edgeLaunchConfigDefaults: IEdgeLaunchConfiguration = { const editorBrowserBaseDefaults: IChromiumBaseConfiguration = { ...baseDefaults, + extensionPath: null, disableNetworkCache: true, pathMapping: {}, url: null, diff --git a/src/targets/browser/browserAttacher.ts b/src/targets/browser/browserAttacher.ts index 92c9b2f22..b321b670f 100644 --- a/src/targets/browser/browserAttacher.ts +++ b/src/targets/browser/browserAttacher.ts @@ -182,6 +182,12 @@ export class BrowserAttacher< manager: BrowserTargetManager, params: AnyChromiumAttachConfiguration, ): Promise { + if (params.extensionPath) { + return (t: { url: string; type: string }) => + t.url.startsWith('chrome-extension://') + && (t.type === BrowserTargetType.ServiceWorker || t.type === BrowserTargetType.Page); + } + const rawFilter = createTargetFilterForConfig(params); const baseFilter = requirePageTarget(rawFilter); if (params.targetSelection !== 'pick') { diff --git a/src/targets/browser/browserLauncher.ts b/src/targets/browser/browserLauncher.ts index 24fa2dfb4..48c2ad78d 100644 --- a/src/targets/browser/browserLauncher.ts +++ b/src/targets/browser/browserLauncher.ts @@ -29,6 +29,7 @@ import { ProtocolError } from '../../dap/protocolError'; import { FS, FsPromises, IInitializeParams, StoragePath } from '../../ioc-extras'; import { ITelemetryReporter } from '../../telemetry/telemetryReporter'; import { ILaunchContext, ILauncher, ILaunchResult, IStopMetadata, ITarget } from '../targets'; +import { BrowserSourcePathResolver } from './browserPathResolver'; import { BrowserTargetManager } from './browserTargetManager'; import { BrowserTarget, BrowserTargetType } from './browserTargets'; import * as launcher from './launcher'; @@ -88,6 +89,7 @@ export abstract class BrowserLauncher cleanUp, launchUnelevated: launchUnelevated, killBehavior, + extensionPath, }: T, dap: Dap.Api, cancellationToken: CancellationToken, @@ -117,6 +119,10 @@ export abstract class BrowserLauncher resolvedDataDir = fs.realpathSync(resolvedDataDir); } + const effectiveRuntimeArgs = extensionPath + ? [...(runtimeArgs || []), `--load-extension=${extensionPath}`] + : runtimeArgs || []; + return await launcher.launch( dap, executablePath, @@ -132,7 +138,7 @@ export abstract class BrowserLauncher hasUserNavigation: !!(url || file), cwd: cwd || webRoot || undefined, env: EnvironmentVars.merge(EnvironmentVars.processEnv(), env), - args: runtimeArgs || [], + args: effectiveRuntimeArgs, userDataDir: resolvedDataDir, connection: port || (inspectUri ? 0 : 'pipe'), // We don't default to pipe if we are using an inspectUri launchUnelevated: launchUnelevated, @@ -147,6 +153,11 @@ export abstract class BrowserLauncher } protected getFilterForTarget(params: T) { + if (params.extensionPath) { + return (t: { url: string; type: string }) => + t.url.startsWith('chrome-extension://') + && (t.type === BrowserTargetType.ServiceWorker || t.type === BrowserTargetType.Page); + } return requirePageTarget(createTargetFilterForConfig(params, ['about:blank'])); } @@ -294,6 +305,16 @@ export abstract class BrowserLauncher throw new ProtocolError(targetPageNotFound()); } + // Once we know which target we attached to, pin the extension ID in the + // path resolver so subsequent source URL resolutions only accept that exact + // extension and not any other chrome-extension:// origin. + if (params.extensionPath) { + const idMatch = mainTarget.fileName()?.match(/^chrome-extension:\/\/([a-z0-9]{32})\//); + if (idMatch) { + (this.pathResolver as BrowserSourcePathResolver).pinExtensionId(idMatch[1]); + } + } + return mainTarget; } diff --git a/src/targets/browser/browserPathResolver.ts b/src/targets/browser/browserPathResolver.ts index f64a735a9..ee798be47 100644 --- a/src/targets/browser/browserPathResolver.ts +++ b/src/targets/browser/browserPathResolver.ts @@ -31,6 +31,9 @@ export interface IOptions extends ISourcePathResolverOptions { pathMapping: PathMapping; clientID: string | undefined; remoteFilePrefix: string | undefined; + extensionPath?: string; + /** Pinned extension ID once discovered from the attached target's URL. */ + extensionId?: string; } const enum Suffix { @@ -51,6 +54,11 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase super(options, logger); } + /** Pins the resolved extension ID so path resolution only accepts that exact origin. */ + public pinExtensionId(id: string) { + (this.options as IOptions).extensionId = id; + } + /** @override */ private absolutePathToUrlPath(absolutePath: string): { url: string; needsWildcard: boolean } { absolutePath = path.normalize(absolutePath); @@ -108,6 +116,21 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase // URIs (vscode-dwarf-debugging-ext#7) url = this.sourceMapOverrides.apply(url); + // Map chrome-extension:// /path → extensionPath/path. + // If extensionId is known (pinned after attaching to the target) we match + // only that exact ID. Otherwise we accept any 32-char ID so the very first + // resolution works before we know the ID. + if (this.options.extensionPath && url.startsWith('chrome-extension://')) { + const idPattern = this.options.extensionId + ? this.options.extensionId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + : '[a-z0-9]{32}'; + const match = url.match(new RegExp(`^chrome-extension://${idPattern}(/.*)?\$`)); + if (match) { + const relPath = (match[1] ?? '/').replace(/^\//, ''); + return path.join(this.options.extensionPath, relPath || 'index.html'); + } + } + // If we have a file URL, we know it's absolute already and points // to a location on disk. if (utils.isFileUrl(url)) { diff --git a/src/targets/sourcePathResolverFactory.ts b/src/targets/sourcePathResolverFactory.ts index 6f6f636e4..04f61b230 100644 --- a/src/targets/sourcePathResolverFactory.ts +++ b/src/targets/sourcePathResolverFactory.ts @@ -92,6 +92,9 @@ export class SourcePathResolverFactory implements ISourcePathResolverFactory { sourceMapOverrides: c.sourceMapPathOverrides, clientID: this.initializeParams.clientID, remoteFilePrefix: c.__remoteFilePrefix, + extensionPath: 'extensionPath' in c && typeof c.extensionPath === 'string' + ? c.extensionPath + : undefined, }, logger, );