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://<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>
This commit is contained in:
Connor Peet
2020-06-25 11:54:36 -07:00
committed by GitHub
parent 5caf5aee6d
commit 08ed2b692a
9 changed files with 118 additions and 25 deletions

View File

@@ -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<TContext> extends ExtensionHost
super();
}
async call(ctx: TContext, command: string, arg?: any): Promise<any> {
call(ctx: TContext, command: string, arg?: any): Promise<any> {
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<IOpenExtensionWindowResult> {
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 };
}
}