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) {