From 08ed2b692a7638179a6bfed65e0551dc77d2d7d8 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 25 Jun 2020 11:54:36 -0700 Subject: [PATCH] debug: allow debugging the renderer process (#100242) * debug: allow debugging the renderer process This adds a new parameter to the `launchVSCode` request -- `debugRenderer`. If set to true and we're in Electron, the main process will attempt to stand up a CDP-speaking server and respond with the port the debugger can connect to in order to handle debug events. Note: this only works when webviews are iframe-based. It's _possible_ to do the same treatment to webviews with greater complexity. I didn't implement that today, and maybe we say since iframes are coming eventually (and they can be toggled on with a user setting that we could have in the starter...) we don't need do add handling for that. This does not work when debugging in web because there's no way to force the browser to enter debug mode. Code in this PR is still rough, I will keep this in PR until I have js-debug working end to end and iron out any kinks. * fixup! adopt new api * webviews: add 'purpose' flag and extension ID to iframe webview uri The `purpose` can be used for notebooks instead of the extension ID, since there's no extension associated with the renderer view. The renderer URL now looks like: ``` https://.vscode-webview-test.com//index.html?id=&extensionId=&purpose=notebookRenderer ``` And a renderer is: ``` https://.vscode-webview-test.com//index.html?id=&extensionId=connor4312.vsix-viewer&purpose=undefined ``` I wanted to put this in the page title, but unfortunately this is hard due to https://bugs.chromium.org/p/chromium/issues/detail?id=1058108. Instead, add it to the url which _is_ available and given in the first targetInfoChanged event. Co-authored-by: Andre Weinand --- src/vs/code/electron-main/app.ts | 97 ++++++++++++++++--- .../debug/common/extensionHostDebug.ts | 6 +- .../debug/common/extensionHostDebugIpc.ts | 6 +- .../browser/customEditorInputFactory.ts | 3 +- .../browser/extensionHostDebugService.ts | 8 +- .../contrib/debug/browser/rawDebugSession.ts | 10 +- .../view/renderers/backLayerWebView.ts | 3 +- .../contrib/webview/browser/webview.ts | 7 ++ .../contrib/webview/browser/webviewElement.ts | 3 +- 9 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index aa4fa2727d4..5a463e2cc24 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -80,6 +80,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; +import { createServer, AddressInfo } from 'net'; +import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -831,20 +833,93 @@ class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHost super(); } - async call(ctx: TContext, command: string, arg?: any): Promise { + call(ctx: TContext, command: string, arg?: any): Promise { if (command === 'openExtensionDevelopmentHostWindow') { - const env = arg[1]; - const pargs = parseArgs(arg[0], OPTIONS); - const extDevPaths = pargs.extensionDevelopmentPath; - if (extDevPaths) { - this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { - context: OpenContext.API, - cli: pargs, - userEnv: Object.keys(env).length > 0 ? env : undefined - }); - } + return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]); } else { return super.call(ctx, command, arg); } } + + private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise { + const pargs = parseArgs(args, OPTIONS); + const extDevPaths = pargs.extensionDevelopmentPath; + if (!extDevPaths) { + return {}; + } + + const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { + context: OpenContext.API, + cli: pargs, + userEnv: Object.keys(env).length > 0 ? env : undefined + }); + + if (!debugRenderer) { + return {}; + } + + const debug = codeWindow.win.webContents.debugger; + + let listeners = debug.isAttached() ? Infinity : 0; + const server = createServer(listener => { + if (listeners++ === 0) { + debug.attach(); + } + + let closed = false; + const writeMessage = (message: object) => { + if (!closed) { // in case sendCommand promises settle after closed + listener.write(JSON.stringify(message) + '\0'); // null-delimited, CDP-compatible + } + }; + + const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) => + writeMessage(({ method, params, sessionId })); + + codeWindow.win.on('close', () => { + debug.removeListener('message', onMessage); + listener.end(); + closed = true; + }); + + debug.addListener('message', onMessage); + + let buf = Buffer.alloc(0); + listener.on('data', data => { + buf = Buffer.concat([buf, data]); + for (let delimiter = buf.indexOf(0); delimiter !== -1; delimiter = buf.indexOf(0)) { + let data: { id: number; sessionId: string; params: {} }; + try { + const contents = buf.slice(0, delimiter).toString('utf8'); + buf = buf.slice(delimiter + 1); + data = JSON.parse(contents); + } catch (e) { + console.error('error reading cdp line', e); + } + + // depends on a new API for which electron.d.ts has not been updated: + // @ts-ignore + debug.sendCommand(data.method, data.params, data.sessionId) + .then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result })) + .catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } })); + } + }); + + listener.on('error', err => { + console.error('error on cdp pipe:', err); + }); + + listener.on('close', () => { + closed = true; + if (--listeners === 0) { + debug.detach(); + } + }); + }); + + await new Promise(r => server.listen(0, r)); + codeWindow.win.on('close', () => server.close()); + + return { rendererDebugPort: (server.address() as AddressInfo).port }; + } } diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index 8dc4e4b2ada..b263bdd9c1c 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -34,6 +34,10 @@ export interface ICloseSessionEvent { sessionId: string; } +export interface IOpenExtensionWindowResult { + rendererDebugPort?: number; +} + export interface IExtensionHostDebugService { readonly _serviceBrand: undefined; @@ -52,5 +56,5 @@ export interface IExtensionHostDebugService { terminateSession(sessionId: string, subId?: string): void; readonly onTerminateSession: Event; - openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise; + openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise; } diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index 84c034b8391..60011be13e3 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; +import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { Event, Emitter } from 'vs/base/common/event'; import { IRemoteConsoleLog } from 'vs/base/common/console'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -101,7 +101,7 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte return this.channel.listen('terminate'); } - openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise { - return this.channel.call('openExtensionDevelopmentHostWindow', [args, env]); + openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise { + return this.channel.call('openExtensionDevelopmentHostWindow', [args, env, debugRenderer]); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index dcf62e4c3bb..cf1b74effb8 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorInput } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { IWebviewService, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, WebviewExtensionDescription, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { reviveWebviewExtensionDescription, SerializedWebview, WebviewEditorInputFactory, DeserializedWebview } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; import { IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -96,6 +96,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { private static reviveWebview(data: { id: string, state: any, options: WebviewInputOptions, extension?: WebviewExtensionDescription, }, webviewService: IWebviewService) { return new Lazy(() => { const webview = webviewService.createWebviewOverlay(data.id, { + purpose: WebviewContentPurpose.CustomEditor, enableFindWidget: data.options.enableFindWidget, retainContextWhenHidden: data.options.retainContextWhenHidden }, data.options, data.extension); diff --git a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts index 3d297be710d..64c59a53fd4 100644 --- a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts @@ -6,7 +6,7 @@ import { ExtensionHostDebugChannelClient, ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; +import { IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { IDebugHelperService } from 'vs/workbench/contrib/debug/common/debug'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; @@ -62,7 +62,7 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i })); } - openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise { + async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise { // Find out which workspace to open debug window on let debugWorkspace: IWorkspace = undefined; @@ -110,10 +110,12 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i } // Open debug window as new window. Pass ParsedArgs over. - return this.workspaceProvider.open(debugWorkspace, { + await this.workspaceProvider.open(debugWorkspace, { reuse: false, // debugging always requires a new window payload: Array.from(environment.entries()) // mandatory properties to enable debugging }); + + return {}; } private findArgument(key: string, args: string[]): string | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 5fa487c2a76..a400dde3bd5 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -12,7 +12,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from 'vs/workbench/contrib/debug/common/debug'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; +import { IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { URI } from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { env as processEnv } from 'vs/base/common/process'; @@ -33,6 +33,7 @@ interface ILaunchVSCodeArgument { interface ILaunchVSCodeArguments { args: ILaunchVSCodeArgument[]; + debugRenderer?: boolean; env?: { [key: string]: string | null; }; } @@ -550,8 +551,9 @@ export class RawDebugSession implements IDisposable { switch (request.command) { case 'launchVSCode': - this.launchVsCode(request.arguments).then(_ => { + this.launchVsCode(request.arguments).then(result => { response.body = { + rendererDebugPort: result.rendererDebugPort, //processId: pid }; safeSendResponse(response); @@ -584,7 +586,7 @@ export class RawDebugSession implements IDisposable { } } - private launchVsCode(vscodeArgs: ILaunchVSCodeArguments): Promise { + private launchVsCode(vscodeArgs: ILaunchVSCodeArguments): Promise { const args: string[] = []; @@ -612,7 +614,7 @@ export class RawDebugSession implements IDisposable { Object.keys(env).filter(k => env[k] === null).forEach(key => delete env[key]); } - return this.extensionHostDebugService.openExtensionDevelopmentHostWindow(args, env); + return this.extensionHostDebugService.openExtensionDevelopmentHostWindow(args, env, !!vscodeArgs.debugRenderer); } private send(command: string, args: any, token?: CancellationToken, timeout?: number): Promise { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index a11a5599d9f..220e3d823f3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -18,7 +18,7 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CellOutputKind, IProcessedOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { dirname, joinPath } from 'vs/base/common/resources'; @@ -471,6 +471,7 @@ ${loaderJs} this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), ...workspaceFolders, rootPath]; const webview = webviewService.createWebviewElement(this.id, { + purpose: WebviewContentPurpose.NotebookRenderer, enableFindWidget: false, }, { allowMultipleAPIAcquire: true, diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 1d359d78f52..fc6fa9ac0c9 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -53,7 +53,14 @@ export interface IWebviewService { setIcons(id: string, value: WebviewIcons | undefined): void; } +export const enum WebviewContentPurpose { + NotebookRenderer = 'notebookRenderer', + CustomEditor = 'customEditor', +} + export interface WebviewOptions { + // The purpose of the webview; this is (currently) only used for filtering in js-debug + readonly purpose?: WebviewContentPurpose; readonly customClasses?: string; readonly enableFindWidget?: boolean; readonly tryRestoreScrollPosition?: boolean; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 86f12969b72..8f0e450d8c5 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -67,7 +67,8 @@ export class IFrameWebview extends BaseWebview implements Web this.localLocalhost(entry.origin); })); - this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}`); + // The extensionId and purpose in the URL are used for filtering in js-debug: + this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`); } protected createElement(options: WebviewOptions, _contentOptions: WebviewContentOptions) {