Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions OPTIONS.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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)",
Expand Down
16 changes: 16 additions & 0 deletions src/build/generate-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,11 @@ const chromiumBaseConfigurationAttributes: ConfigurationAttributes<IChromiumBase
enum: ['yes', 'no', 'auto'],
description: refString('browser.perScriptSourcemaps.description'),
},
extensionPath: {
type: ['string', 'null'],
description: refString('browser.extensionPath.description'),
default: null,
},
};

/**
Expand Down Expand Up @@ -854,6 +859,17 @@ const chromeLaunchConfig: IDebugger<IChromeLaunchConfiguration> = {
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,
Expand Down
17 changes: 16 additions & 1 deletion src/cdp/connection.ts
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm...on second thought, is this too lax now? initial testing showed that a real error wasnt allowed to slip through but perhaps some more aggressive timing checks are warranted?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refined in commit: e326e5d

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -959,6 +967,7 @@ export const chromeAttachConfigDefaults: IChromeAttachConfiguration = {
request: 'attach',
address: 'localhost',
port: 0,
extensionPath: null,
disableNetworkCache: true,
pathMapping: {},
url: null,
Expand Down Expand Up @@ -996,6 +1005,7 @@ export const chromeLaunchConfigDefaults: IChromeLaunchConfiguration = {
profileStartup: false,
cleanUp: 'wholeBrowser',
killBehavior: KillBehavior.Forceful,
extensionPath: null,
};

export const edgeLaunchConfigDefaults: IEdgeLaunchConfiguration = {
Expand All @@ -1006,6 +1016,7 @@ export const edgeLaunchConfigDefaults: IEdgeLaunchConfiguration = {

const editorBrowserBaseDefaults: IChromiumBaseConfiguration = {
...baseDefaults,
extensionPath: null,
disableNetworkCache: true,
pathMapping: {},
url: null,
Expand Down
6 changes: 6 additions & 0 deletions src/targets/browser/browserAttacher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export class BrowserAttacher<
manager: BrowserTargetManager,
params: AnyChromiumAttachConfiguration,
): Promise<TargetFilter> {
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') {
Expand Down
23 changes: 22 additions & 1 deletion src/targets/browser/browserLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -88,6 +89,7 @@ export abstract class BrowserLauncher<T extends AnyChromiumLaunchConfiguration>
cleanUp,
launchUnelevated: launchUnelevated,
killBehavior,
extensionPath,
}: T,
dap: Dap.Api,
cancellationToken: CancellationToken,
Expand Down Expand Up @@ -117,6 +119,10 @@ export abstract class BrowserLauncher<T extends AnyChromiumLaunchConfiguration>
resolvedDataDir = fs.realpathSync(resolvedDataDir);
}

const effectiveRuntimeArgs = extensionPath
? [...(runtimeArgs || []), `--load-extension=${extensionPath}`]
: runtimeArgs || [];

return await launcher.launch(
dap,
executablePath,
Expand All @@ -132,7 +138,7 @@ export abstract class BrowserLauncher<T extends AnyChromiumLaunchConfiguration>
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,
Expand All @@ -147,6 +153,11 @@ export abstract class BrowserLauncher<T extends AnyChromiumLaunchConfiguration>
}

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']));
}

Expand Down Expand Up @@ -294,6 +305,16 @@ export abstract class BrowserLauncher<T extends AnyChromiumLaunchConfiguration>
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;
}

Expand Down
23 changes: 23 additions & 0 deletions src/targets/browser/browserPathResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -51,6 +54,11 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase<IOptions>
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);
Expand Down Expand Up @@ -108,6 +116,21 @@ export class BrowserSourcePathResolver extends SourcePathResolverBase<IOptions>
// URIs (vscode-dwarf-debugging-ext#7)
url = this.sourceMapOverrides.apply(url);

// Map chrome-extension://<id>/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)) {
Expand Down
3 changes: 3 additions & 0 deletions src/targets/sourcePathResolverFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down