Path: blob/main/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts
5237 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BrowserWindow } from 'electron';6import { Server } from 'http';7import { Socket } from 'net';8import { VSBuffer } from '../../../base/common/buffer.js';9import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';10import { generateUuid } from '../../../base/common/uuid.js';11import { ISocket } from '../../../base/parts/ipc/common/ipc.net.js';12import { upgradeToISocket } from '../../../base/parts/ipc/node/ipc.net.js';13import { OPTIONS, parseArgs } from '../../environment/node/argv.js';14import { IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js';15import { IOpenExtensionWindowResult } from '../common/extensionHostDebug.js';16import { ExtensionHostDebugBroadcastChannel } from '../common/extensionHostDebugIpc.js';1718export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends ExtensionHostDebugBroadcastChannel<TContext> {1920constructor(21private windowsMainService: IWindowsMainService22) {23super();24}2526override call(ctx: TContext, command: string, arg?: any): Promise<any> {27if (command === 'openExtensionDevelopmentHostWindow') {28return this.openExtensionDevelopmentHostWindow(arg[0], arg[1]);29} else if (command === 'attachToCurrentWindowRenderer') {30return this.attachToCurrentWindowRenderer(arg[0]);31} else {32return super.call(ctx, command, arg);33}34}3536private async attachToCurrentWindowRenderer(windowId: number): Promise<IOpenExtensionWindowResult> {37const codeWindow = this.windowsMainService.getWindowById(windowId);38if (!codeWindow?.win) {39return { success: false };40}4142return this.openCdp(codeWindow.win, true);43}4445private async openExtensionDevelopmentHostWindow(args: string[], debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {46const pargs = parseArgs(args, OPTIONS);47pargs.debugRenderer = debugRenderer;4849const extDevPaths = pargs.extensionDevelopmentPath;50if (!extDevPaths) {51return { success: false };52}5354const [codeWindow] = await this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, {55context: OpenContext.API,56cli: pargs,57forceProfile: pargs.profile,58forceTempProfile: pargs['profile-temp']59});6061if (!debugRenderer) {62return { success: true };63}6465const win = codeWindow.win;66if (!win) {67return { success: true };68}6970return this.openCdp(win, false);71}7273private async openCdpServer(ident: string, onSocket: (socket: ISocket) => void): Promise<{ server: Server; wsUrl: string; port: number }> {74const { createServer } = await import('http'); // Lazy due to https://github.com/nodejs/node/issues/5968675const server = createServer((req, res) => {76if (req.url === '/json/list' || req.url === '/json') {77res.setHeader('Content-Type', 'application/json');78res.end(JSON.stringify([{79description: 'VS Code Renderer',80devtoolsFrontendUrl: '',81id: ident,82title: 'VS Code Renderer',83type: 'page',84url: 'vscode://renderer',85webSocketDebuggerUrl: wsUrl86}]));87return;88} else if (req.url === '/json/version') {89res.setHeader('Content-Type', 'application/json');90res.end(JSON.stringify({91'Browser': 'VS Code Renderer',92'Protocol-Version': '1.3',93'webSocketDebuggerUrl': wsUrl94}));95return;96}9798res.statusCode = 404;99res.end();100});101102await new Promise<void>(r => server.listen(0, '127.0.0.1', r));103const serverAddr = server.address();104const port = typeof serverAddr === 'object' && serverAddr ? serverAddr.port : 0;105const serverAddrBase = typeof serverAddr === 'string' ? serverAddr : `ws://127.0.0.1:${serverAddr?.port}`;106const wsUrl = `${serverAddrBase}/${ident}`;107108server.on('upgrade', (req, socket) => {109if (!req.url?.includes(ident)) {110socket.end();111return;112}113const upgraded = upgradeToISocket(req, socket as Socket, {114debugLabel: 'extension-host-cdp-' + generateUuid(),115enableMessageSplitting: false,116});117118if (upgraded) {119onSocket(upgraded);120}121});122123return { server, wsUrl, port };124}125126private async openCdp(win: BrowserWindow, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {127const debug = win.webContents.debugger;128129let listeners = debug.isAttached() ? Infinity : 0;130const ident = generateUuid();131const pageSessionId = debugRenderer ? `page-${ident}` : undefined;132const { server, wsUrl, port } = await this.openCdpServer(ident, listener => {133if (listeners++ === 0) {134debug.attach();135}136137const store = new DisposableStore();138store.add(listener);139140const writeMessage = (message: object) => {141if (!store.isDisposed) { // in case sendCommand promises settle after closed142listener.write(VSBuffer.fromString(JSON.stringify(message))); // null-delimited, CDP-compatible143}144};145146const onMessage = (_event: Electron.Event, method: string, params: unknown, sessionId?: string) =>147writeMessage({ method, params, sessionId: sessionId || pageSessionId });148149const onWindowClose = () => {150listener.end();151store.dispose();152};153154win.addListener('close', onWindowClose);155store.add(toDisposable(() => win.removeListener('close', onWindowClose)));156157debug.addListener('message', onMessage);158store.add(toDisposable(() => debug.removeListener('message', onMessage)));159160store.add(listener.onData(rawData => {161let data: { id: number; sessionId?: string; method: string; params: Record<string, unknown> };162try {163data = JSON.parse(rawData.toString());164} catch (e) {165console.error('error reading cdp line', e);166return;167}168169if (debugRenderer) {170// Emulate Target.* methods that js-debug expects but Electron's debugger doesn't support171const targetInfo = { targetId: ident, type: 'page', title: 'VS Code Renderer', url: 'vscode://renderer' };172if (data.method === 'Target.setDiscoverTargets') {173writeMessage({ id: data.id, sessionId: data.sessionId, result: {} });174writeMessage({ method: 'Target.targetCreated', sessionId: data.sessionId, params: { targetInfo: { ...targetInfo, attached: false, canAccessOpener: false } } });175return;176}177if (data.method === 'Target.attachToTarget') {178writeMessage({ id: data.id, sessionId: data.sessionId, result: { sessionId: pageSessionId } });179writeMessage({ method: 'Target.attachedToTarget', params: { sessionId: pageSessionId, targetInfo: { ...targetInfo, attached: true, canAccessOpener: false }, waitingForDebugger: false } });180return;181}182if (data.method === 'Target.setAutoAttach' || data.method === 'Target.attachToBrowserTarget') {183writeMessage({ id: data.id, sessionId: data.sessionId, result: data.method === 'Target.attachToBrowserTarget' ? { sessionId: 'browser' } : {} });184return;185}186if (data.method === 'Target.getTargets') {187writeMessage({ id: data.id, sessionId: data.sessionId, result: { targetInfos: [{ ...targetInfo, attached: true }] } });188return;189}190}191192// Forward to Electron's debugger, stripping our synthetic page sessionId193const forwardSessionId = data.sessionId === pageSessionId ? undefined : data.sessionId;194195debug.sendCommand(data.method, data.params, forwardSessionId)196.then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result }))197.catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } }));198}));199200store.add(listener.onClose(() => {201if (--listeners === 0) {202debug.detach();203}204}));205});206207win.on('close', () => server.close());208209return { rendererDebugAddr: wsUrl, success: true, port: port };210}211}212213214