Skip to content

Commit 08ed2b6

Browse files
connor4312weinand
andauthored
debug: allow debugging the renderer process (microsoft#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://<guid>.vscode-webview-test.com/<random>/index.html?id=<guid>&extensionId=&purpose=notebookRenderer ``` And a renderer is: ``` https://<guid>.vscode-webview-test.com/<random>/index.html?id=<guid>&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 <aweinand@microsoft.com>
1 parent 5caf5ae commit 08ed2b6

9 files changed

Lines changed: 118 additions & 25 deletions

File tree

src/vs/code/electron-main/app.ts

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm
8080
import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels';
8181
import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService';
8282
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
83+
import { createServer, AddressInfo } from 'net';
84+
import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
8385

8486
export class CodeApplication extends Disposable {
8587
private windowsMainService: IWindowsMainService | undefined;
@@ -831,20 +833,93 @@ class ElectronExtensionHostDebugBroadcastChannel<TContext> extends ExtensionHost
831833
super();
832834
}
833835

834-
async call(ctx: TContext, command: string, arg?: any): Promise<any> {
836+
call(ctx: TContext, command: string, arg?: any): Promise<any> {
835837
if (command === 'openExtensionDevelopmentHostWindow') {
836-
const env = arg[1];
837-
const pargs = parseArgs(arg[0], OPTIONS);
838-
const extDevPaths = pargs.extensionDevelopmentPath;
839-
if (extDevPaths) {
840-
this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, {
841-
context: OpenContext.API,
842-
cli: pargs,
843-
userEnv: Object.keys(env).length > 0 ? env : undefined
844-
});
845-
}
838+
return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]);
846839
} else {
847840
return super.call(ctx, command, arg);
848841
}
849842
}
843+
844+
private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
845+
const pargs = parseArgs(args, OPTIONS);
846+
const extDevPaths = pargs.extensionDevelopmentPath;
847+
if (!extDevPaths) {
848+
return {};
849+
}
850+
851+
const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, {
852+
context: OpenContext.API,
853+
cli: pargs,
854+
userEnv: Object.keys(env).length > 0 ? env : undefined
855+
});
856+
857+
if (!debugRenderer) {
858+
return {};
859+
}
860+
861+
const debug = codeWindow.win.webContents.debugger;
862+
863+
let listeners = debug.isAttached() ? Infinity : 0;
864+
const server = createServer(listener => {
865+
if (listeners++ === 0) {
866+
debug.attach();
867+
}
868+
869+
let closed = false;
870+
const writeMessage = (message: object) => {
871+
if (!closed) { // in case sendCommand promises settle after closed
872+
listener.write(JSON.stringify(message) + '\0'); // null-delimited, CDP-compatible
873+
}
874+
};
875+
876+
const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) =>
877+
writeMessage(({ method, params, sessionId }));
878+
879+
codeWindow.win.on('close', () => {
880+
debug.removeListener('message', onMessage);
881+
listener.end();
882+
closed = true;
883+
});
884+
885+
debug.addListener('message', onMessage);
886+
887+
let buf = Buffer.alloc(0);
888+
listener.on('data', data => {
889+
buf = Buffer.concat([buf, data]);
890+
for (let delimiter = buf.indexOf(0); delimiter !== -1; delimiter = buf.indexOf(0)) {
891+
let data: { id: number; sessionId: string; params: {} };
892+
try {
893+
const contents = buf.slice(0, delimiter).toString('utf8');
894+
buf = buf.slice(delimiter + 1);
895+
data = JSON.parse(contents);
896+
} catch (e) {
897+
console.error('error reading cdp line', e);
898+
}
899+
900+
// depends on a new API for which electron.d.ts has not been updated:
901+
// @ts-ignore
902+
debug.sendCommand(data.method, data.params, data.sessionId)
903+
.then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result }))
904+
.catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } }));
905+
}
906+
});
907+
908+
listener.on('error', err => {
909+
console.error('error on cdp pipe:', err);
910+
});
911+
912+
listener.on('close', () => {
913+
closed = true;
914+
if (--listeners === 0) {
915+
debug.detach();
916+
}
917+
});
918+
});
919+
920+
await new Promise(r => server.listen(0, r));
921+
codeWindow.win.on('close', () => server.close());
922+
923+
return { rendererDebugPort: (server.address() as AddressInfo).port };
924+
}
850925
}

src/vs/platform/debug/common/extensionHostDebug.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export interface ICloseSessionEvent {
3434
sessionId: string;
3535
}
3636

37+
export interface IOpenExtensionWindowResult {
38+
rendererDebugPort?: number;
39+
}
40+
3741
export interface IExtensionHostDebugService {
3842
readonly _serviceBrand: undefined;
3943

@@ -52,5 +56,5 @@ export interface IExtensionHostDebugService {
5256
terminateSession(sessionId: string, subId?: string): void;
5357
readonly onTerminateSession: Event<ITerminateSessionEvent>;
5458

55-
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise<void>;
59+
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult>;
5660
}

src/vs/platform/debug/common/extensionHostDebugIpc.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
7-
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
7+
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
88
import { Event, Emitter } from 'vs/base/common/event';
99
import { IRemoteConsoleLog } from 'vs/base/common/console';
1010
import { Disposable } from 'vs/base/common/lifecycle';
@@ -101,7 +101,7 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte
101101
return this.channel.listen('terminate');
102102
}
103103

104-
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise<void> {
105-
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env]);
104+
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
105+
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env, debugRenderer]);
106106
}
107107
}

src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
88
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
99
import { IEditorInput } from 'vs/workbench/common/editor';
1010
import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
11-
import { IWebviewService, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
11+
import { IWebviewService, WebviewExtensionDescription, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview';
1212
import { reviveWebviewExtensionDescription, SerializedWebview, WebviewEditorInputFactory, DeserializedWebview } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory';
1313
import { IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
1414
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
@@ -96,6 +96,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory {
9696
private static reviveWebview(data: { id: string, state: any, options: WebviewInputOptions, extension?: WebviewExtensionDescription, }, webviewService: IWebviewService) {
9797
return new Lazy(() => {
9898
const webview = webviewService.createWebviewOverlay(data.id, {
99+
purpose: WebviewContentPurpose.CustomEditor,
99100
enableFindWidget: data.options.enableFindWidget,
100101
retainContextWhenHidden: data.options.retainContextWhenHidden
101102
}, data.options, data.extension);

src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { ExtensionHostDebugChannelClient, ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
77
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
88
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
9-
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
9+
import { IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
1010
import { IDebugHelperService } from 'vs/workbench/contrib/debug/common/debug';
1111
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1212
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
@@ -62,7 +62,7 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i
6262
}));
6363
}
6464

65-
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise<void> {
65+
async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise<IOpenExtensionWindowResult> {
6666

6767
// Find out which workspace to open debug window on
6868
let debugWorkspace: IWorkspace = undefined;
@@ -110,10 +110,12 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i
110110
}
111111

112112
// Open debug window as new window. Pass ParsedArgs over.
113-
return this.workspaceProvider.open(debugWorkspace, {
113+
await this.workspaceProvider.open(debugWorkspace, {
114114
reuse: false, // debugging always requires a new window
115115
payload: Array.from(environment.entries()) // mandatory properties to enable debugging
116116
});
117+
118+
return {};
117119
}
118120

119121
private findArgument(key: string, args: string[]): string | undefined {

src/vs/workbench/contrib/debug/browser/rawDebugSession.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
1212
import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils';
1313
import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from 'vs/workbench/contrib/debug/common/debug';
1414
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
15-
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
15+
import { IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
1616
import { URI } from 'vs/base/common/uri';
1717
import { IProcessEnvironment } from 'vs/base/common/platform';
1818
import { env as processEnv } from 'vs/base/common/process';
@@ -33,6 +33,7 @@ interface ILaunchVSCodeArgument {
3333

3434
interface ILaunchVSCodeArguments {
3535
args: ILaunchVSCodeArgument[];
36+
debugRenderer?: boolean;
3637
env?: { [key: string]: string | null; };
3738
}
3839

@@ -550,8 +551,9 @@ export class RawDebugSession implements IDisposable {
550551

551552
switch (request.command) {
552553
case 'launchVSCode':
553-
this.launchVsCode(<ILaunchVSCodeArguments>request.arguments).then(_ => {
554+
this.launchVsCode(<ILaunchVSCodeArguments>request.arguments).then(result => {
554555
response.body = {
556+
rendererDebugPort: result.rendererDebugPort,
555557
//processId: pid
556558
};
557559
safeSendResponse(response);
@@ -584,7 +586,7 @@ export class RawDebugSession implements IDisposable {
584586
}
585587
}
586588

587-
private launchVsCode(vscodeArgs: ILaunchVSCodeArguments): Promise<void> {
589+
private launchVsCode(vscodeArgs: ILaunchVSCodeArguments): Promise<IOpenExtensionWindowResult> {
588590

589591
const args: string[] = [];
590592

@@ -612,7 +614,7 @@ export class RawDebugSession implements IDisposable {
612614
Object.keys(env).filter(k => env[k] === null).forEach(key => delete env[key]);
613615
}
614616

615-
return this.extensionHostDebugService.openExtensionDevelopmentHostWindow(args, env);
617+
return this.extensionHostDebugService.openExtensionDevelopmentHostWindow(args, env, !!vscodeArgs.debugRenderer);
616618
}
617619

618620
private send<R extends DebugProtocol.Response>(command: string, args: any, token?: CancellationToken, timeout?: number): Promise<R> {

src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB
1818
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
1919
import { CellOutputKind, IProcessedOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
2020
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
21-
import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
21+
import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview';
2222
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri';
2323
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
2424
import { dirname, joinPath } from 'vs/base/common/resources';
@@ -471,6 +471,7 @@ ${loaderJs}
471471
this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), ...workspaceFolders, rootPath];
472472

473473
const webview = webviewService.createWebviewElement(this.id, {
474+
purpose: WebviewContentPurpose.NotebookRenderer,
474475
enableFindWidget: false,
475476
}, {
476477
allowMultipleAPIAcquire: true,

src/vs/workbench/contrib/webview/browser/webview.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ export interface IWebviewService {
5353
setIcons(id: string, value: WebviewIcons | undefined): void;
5454
}
5555

56+
export const enum WebviewContentPurpose {
57+
NotebookRenderer = 'notebookRenderer',
58+
CustomEditor = 'customEditor',
59+
}
60+
5661
export interface WebviewOptions {
62+
// The purpose of the webview; this is (currently) only used for filtering in js-debug
63+
readonly purpose?: WebviewContentPurpose;
5764
readonly customClasses?: string;
5865
readonly enableFindWidget?: boolean;
5966
readonly tryRestoreScrollPosition?: boolean;

src/vs/workbench/contrib/webview/browser/webviewElement.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
6767
this.localLocalhost(entry.origin);
6868
}));
6969

70-
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}`);
70+
// The extensionId and purpose in the URL are used for filtering in js-debug:
71+
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
7172
}
7273

7374
protected createElement(options: WebviewOptions, _contentOptions: WebviewContentOptions) {

0 commit comments

Comments
 (0)