diff --git a/src/vs/base/parts/ipc/browser/ipc.mp.ts b/src/vs/base/parts/ipc/browser/ipc.mp.ts new file mode 100644 index 00000000000..a5871ddbf52 --- /dev/null +++ b/src/vs/base/parts/ipc/browser/ipc.mp.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; + +/** + * An implementation of a `IPCClient` on top of DOM `MessagePort`. + */ +export class Client extends MessagePortClient implements IDisposable { + + /** + * @param clientId a way to uniquely identify this client among + * other clients. this is important for routing because every + * client can also be a server + */ + constructor(port: MessagePort, clientId: string) { + super(port, clientId); + } +} diff --git a/src/vs/base/parts/ipc/common/ipc.electron.ts b/src/vs/base/parts/ipc/common/ipc.electron.ts index 52d8c99575d..ae5a30a8af7 100644 --- a/src/vs/base/parts/ipc/common/ipc.electron.ts +++ b/src/vs/base/parts/ipc/common/ipc.electron.ts @@ -28,7 +28,7 @@ export class Protocol implements IMessagePassingProtocol { } } - dispose(): void { + disconnect(): void { this.sender.send('vscode:disconnect', null); } } diff --git a/src/vs/base/parts/ipc/common/ipc.mp.ts b/src/vs/base/parts/ipc/common/ipc.mp.ts new file mode 100644 index 00000000000..8e59b5db9bf --- /dev/null +++ b/src/vs/base/parts/ipc/common/ipc.mp.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; + +/** + * Declare minimal `MessageEvent` and `MessagePort` interfaces here + * so that this utility can be used both from `browser` and + * `electron-main` namespace where message ports are available. + */ + +export interface MessageEvent { + + /** + * For our use we only consider `Uint8Array` and `disconnect` + * a valid data transfer via message ports. Our protocol + * implementation is buffer based and we only need the explicit + * `disconnect` because message ports currently have no way of + * indicating when their connection is closed. + */ + data: Uint8Array | 'disconnect'; +} + +export interface MessagePort { + + addEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void; + removeEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void; + + postMessage(message: Uint8Array | 'disconnect'): void; + + start(): void; + close(): void; +} + +/** + * The MessagePort `Protocol` leverages MessagePort style IPC communication + * for the implementation of the `IMessagePassingProtocol`. That style of API + * is a simple `onmessage` / `postMessage` pattern. + */ +export class Protocol implements IMessagePassingProtocol { + + private readonly onRawData = Event.fromDOMEventEmitter(this.port, 'message', (e: MessageEvent) => e.data === 'disconnect' ? e.data : VSBuffer.wrap(e.data)); + + readonly onMessage = Event.filter(this.onRawData, data => data !== 'disconnect') as Event; + readonly onDisconnect = Event.signal(Event.filter(this.onRawData, data => data === 'disconnect')); + + constructor(private port: MessagePort) { + + // we must call start() to ensure messages are flowing + port.start(); + + // when the other end disconnects, ensure that we close + // our end as well to stay in sync + Event.once(this.onDisconnect)(() => port.close()); + } + + send(message: VSBuffer): void { + this.port.postMessage(message.buffer); + } + + disconnect(): void { + this.port.postMessage('disconnect'); + this.port.close(); + } +} + +/** + * An implementation of a `IPCClient` on top of MessagePort style IPC communication. + */ +export class Client extends IPCClient implements IDisposable { + + private protocol: Protocol; + + constructor(port: MessagePort, clientId: string) { + const protocol = new Protocol(port); + super(protocol, clientId); + + this.protocol = protocol; + } + + dispose(): void { + this.protocol.disconnect(); + } +} diff --git a/src/vs/base/parts/ipc/electron-main/ipc.mp.ts b/src/vs/base/parts/ipc/electron-main/ipc.mp.ts new file mode 100644 index 00000000000..34e159cecd1 --- /dev/null +++ b/src/vs/base/parts/ipc/electron-main/ipc.mp.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BrowserWindow, ipcMain, IpcMainEvent, MessagePortMain } from 'electron'; +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; + +/** + * An implementation of a `IPCClient` on top of Electron `MessagePortMain`. + */ +export class Client extends MessagePortClient implements IDisposable { + + /** + * @param clientId a way to uniquely identify this client among + * other clients. this is important for routing because every + * client can also be a server + */ + constructor(port: MessagePortMain, clientId: string) { + super({ + addEventListener: (type, listener) => port.addListener(type, listener), + removeEventListener: (type, listener) => port.removeListener(type, listener), + postMessage: message => port.postMessage(message), + start: () => port.start(), + close: () => port.close() + }, clientId); + } +} + +/** + * This method opens a message channel connection + * in the target window. The target window needs + * to use the `Server` from `electron-sandbox/ipc.mp`. + */ +export async function connect(window: BrowserWindow): Promise { + + // Ask to create message channel inside the window + // and send over a UUID to correlate the response + const nonce = generateUuid(); + window.webContents.send('vscode:createMessageChannel', nonce); + + // Wait until the window has returned the `MessagePort` + // We need to filter by the `nonce` to ensure we listen + // to the right response. + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePortMain }>(ipcMain, 'vscode:createMessageChannelResult', (e: IpcMainEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); + + return port; +} diff --git a/src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts b/src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts index e83b9c02b2b..f2eaa813112 100644 --- a/src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts +++ b/src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts @@ -33,6 +33,6 @@ export class Client extends IPCClient implements IDisposable { } dispose(): void { - this.protocol.dispose(); + this.protocol.disconnect(); } } diff --git a/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts b/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts new file mode 100644 index 00000000000..7df6de38d4c --- /dev/null +++ b/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { Event } from 'vs/base/common/event'; +import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc'; +import { Protocol as MessagePortProtocol } from 'vs/base/parts/ipc/common/ipc.mp'; + +/** + * An implementation of a `IPCServer` on top of MessagePort style IPC communication. + * The clients register themselves via Electron IPC transfer. + */ +export class Server extends IPCServer { + + private static getOnDidClientConnect(): Event { + + // Clients connect via `vscode:createMessageChannel` to get a + // `MessagePort` that is ready to be used. For every connection + // we create a pair of message ports and send it back. + // + // The `nonce` is included so that the main side has a chance to + // correlate the response back to the sender. + const onCreateMessageChannel = Event.fromNodeEventEmitter(ipcRenderer, 'vscode:createMessageChannel', (_, nonce: string) => nonce); + + return Event.map(onCreateMessageChannel, nonce => { + + // Create a new pair of ports and protocol for this connection + const { port1: incomingPort, port2: outgoingPort } = new MessageChannel(); + const protocol = new MessagePortProtocol(incomingPort); + + const result: ClientConnectionEvent = { protocol, onDidClientDisconnect: protocol.onDisconnect }; + + // Send one port back to the requestor + ipcRenderer.postMessage('vscode:createMessageChannelResult', nonce, [outgoingPort]); + + return result; + }); + } + + constructor() { + super(Server.getOnDidClientConnect()); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 663b67bfab4..702c64434b5 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -6,9 +6,8 @@ import product from 'vs/platform/product/common/product'; import * as fs from 'fs'; import { gracefulify } from 'graceful-fs'; -import { isWindows } from 'vs/base/common/platform'; -import { IChannel, IServerChannel, StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; -import { serve as nodeIPCServe, Server as NodeIPCServer, connect as nodeIPCConnect } from 'vs/base/parts/ipc/node/ipc.net'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; +import { StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; @@ -40,7 +39,7 @@ import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/co import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -79,34 +78,16 @@ import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProc import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -class MainProcessService implements IMainProcessService { - - declare readonly _serviceBrand: undefined; - - constructor( - private server: NodeIPCServer, - private mainRouter: StaticRouter - ) { } - - getChannel(channelName: string): IChannel { - return this.server.getChannel(channelName, this.mainRouter); - } - - registerChannel(channelName: string, channel: IServerChannel): void { - this.server.registerChannel(channelName, channel); - } -} - class SharedProcessMain extends Disposable { - constructor(private server: NodeIPCServer, private configuration: ISharedProcessConfiguration) { + private server = this._register(new MessagePortServer()); + + constructor(private configuration: ISharedProcessConfiguration) { super(); // Enable gracefulFs gracefulify(fs); - this._register(this.server); - this.registerListeners(); } @@ -160,7 +141,7 @@ class SharedProcessMain extends Disposable { // Log const mainRouter = new StaticRouter(ctx => ctx === 'main'); - const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); + const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); // we only use this for log levels const multiplexLogger = this._register(new MultiplexLogService([ this._register(new ConsoleLogService(this.configuration.logLevel)), this._register(new SpdLogService('sharedprocess', environmentService.logsPath, this.configuration.logLevel)) @@ -170,7 +151,7 @@ class SharedProcessMain extends Disposable { services.set(ILogService, logService); // Main Process - const mainProcessService = new MainProcessService(this.server, mainRouter); + const mainProcessService = new MessagePortMainProcessService(this.server, mainRouter); services.set(IMainProcessService, mainProcessService); // Files @@ -345,48 +326,14 @@ class SharedProcessMain extends Disposable { } } -function setupNodeIPC(hook: string): Promise { - function setup(retry: boolean): Promise { - return nodeIPCServe(hook).then(null, err => { - if (!retry || isWindows || err.code !== 'EADDRINUSE') { - return Promise.reject(err); - } - - // should retry, not windows and eaddrinuse - - return nodeIPCConnect(hook, '').then( - client => { - // we could connect to a running instance. this is not good, abort - client.dispose(); - return Promise.reject(new Error('There is an instance already running.')); - }, - err => { - // it happens on Linux and OS X that the pipe is left behind - // let's delete it, since we can't connect to it - // and the retry the whole thing - try { - fs.unlinkSync(hook); - } catch (e) { - return Promise.reject(new Error('Error deleting the shared ipc hook.')); - } - - return setup(false); - } - ); - }); - } - - return setup(true); -} - export async function main(configuration: ISharedProcessConfiguration): Promise { - // await IPC connection and signal this back to electron-main - const server = await setupNodeIPC(configuration.sharedIPCHandle); + // create shared process and signal back to main that we are + // ready to accept message ports as client connections + const sharedProcess = new SharedProcessMain(configuration); ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready'); // await initialization and signal this back to electron-main - const sharedProcess = new SharedProcessMain(server, configuration); await sharedProcess.open(); ipcRenderer.send('vscode:shared-process->electron-main=init-done'); } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 07b56c050aa..254c26ac98d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -13,8 +13,8 @@ import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc'; import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron'; -import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net'; -import { Server as NodeIPCServer, connect as nodeIPCConnect } from 'vs/base/parts/ipc/node/ipc.net'; +import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -426,16 +426,20 @@ export class CodeApplication extends Disposable { // Spawn shared process after the first window has opened and 3s have passed const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); - const sharedProcessClient = sharedProcess.whenIpcReady().then(() => { - this.logService.trace('Shared process: IPC ready'); + const sharedProcessClient = (async () => { + this.logService.trace('Main->SharedProcess#connect'); - return nodeIPCConnect(this.environmentService.sharedIPCHandle, 'main'); - }); - const sharedProcessReady = sharedProcess.whenReady().then(() => { - this.logService.trace('Shared process: init ready'); + const port = await sharedProcess.connect(); + + this.logService.trace('Main->SharedProcess#connect: connection established'); + + return new MessagePortClient(port, 'main'); + })(); + const sharedProcessReady = (async () => { + await sharedProcess.whenReady(); return sharedProcessClient; - }); + })(); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentService.args, process.env)); @@ -482,7 +486,7 @@ export class CodeApplication extends Disposable { return machineId; } - private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise>): Promise { + private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); switch (process.platform) { @@ -584,7 +588,7 @@ export class CodeApplication extends Disposable { }); } - private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { + private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { // Register more Main IPC services const launchMainService = accessor.get(ILaunchMainService); diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index d196ef0e872..fbf5263a3f0 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { memoize } from 'vs/base/common/decorators'; +import { BrowserWindow, ipcMain, Event, MessagePortMain } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { BrowserWindow, ipcMain, Event } from 'electron'; import { ISharedProcess } from 'vs/platform/sharedProcess/electron-main/sharedProcessMainService'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; @@ -15,14 +14,13 @@ import { FileAccess } from 'vs/base/common/network'; import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform'; import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; import { Disposable } from 'vs/base/common/lifecycle'; +import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp'; +import { assertIsDefined } from 'vs/base/common/types'; export class SharedProcess extends Disposable implements ISharedProcess { private readonly whenSpawnedBarrier = new Barrier(); - // overall ready promise when shared process signals initialization is done - private readonly _whenReady = new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => resolve())); - private window: BrowserWindow | undefined = undefined; private windowCloseListener: ((event: Event) => void) | undefined = undefined; @@ -40,7 +38,16 @@ export class SharedProcess extends Disposable implements ISharedProcess { } private registerListeners(): void { + + // Lifecycle this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown())); + + // Shared process connections + ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => { + const port = await this.connect(); + + e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]); + }); } private onWillShutdown(): void { @@ -50,7 +57,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { } // Signal exit to shared process when shutting down - window.webContents.send('vscode:electron-main->shared-process=exit'); // TODO verify call + window.webContents.send('vscode:electron-main->shared-process=exit'); // Shut the shared process down when we are quitting // @@ -75,17 +82,45 @@ export class SharedProcess extends Disposable implements ISharedProcess { }, 0); } - @memoize - private get _whenIpcReady(): Promise { + private _whenReady: Promise | undefined = undefined; + whenReady(): Promise { + if (!this._whenReady) { + // Overall signal that the shared process window was loaded and + // all services within have been created. + this._whenReady = new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => { + this.logService.trace('SharedProcess: Overall ready'); - // Create window for shared process - this.createWindow(); + resolve(); + })); + } - // Listeners - this.registerWindowListeners(); + return this._whenReady; + } - // complete IPC-ready promise when shared process signals this to us - return new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => resolve(undefined))); + private _whenIpcReady: Promise | undefined = undefined; + private get whenIpcReady() { + if (!this._whenIpcReady) { + this._whenIpcReady = (async () => { + + // Always wait for `spawn()` + await this.whenSpawnedBarrier.wait(); + + // Create window for shared process + this.createWindow(); + + // Listeners + this.registerWindowListeners(); + + // Wait for window indicating that IPC connections are accepted + await new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { + this.logService.trace('SharedProcess: IPC ready'); + + resolve(); + })); + })(); + } + + return this._whenIpcReady; } private createWindow(): void { @@ -151,7 +186,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { // Crashes & Unrsponsive & Failed to load this.window.webContents.on('render-process-gone', (event, details) => this.logService.error(`[VS Code]: sharedProcess crashed (detail: ${details?.reason})`)); this.window.on('unresponsive', () => this.logService.error('[VS Code]: detected unresponsive sharedProcess window')); - this.window.webContents.on('did-fail-load', (event: Event, errorCode: number, errorDescription: string) => this.logService.warn('[VS Code]: fail to load sharedProcess window, ', errorDescription)); + this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('[VS Code]: fail to load sharedProcess window, ', errorDescription)); } spawn(userEnv: NodeJS.ProcessEnv): void { @@ -161,20 +196,14 @@ export class SharedProcess extends Disposable implements ISharedProcess { this.whenSpawnedBarrier.open(); } - async whenReady(): Promise { + async connect(): Promise { - // Always wait for `spawn()` - await this.whenSpawnedBarrier.wait(); + // Wait for shared process being ready to accept connection + await this.whenIpcReady; - await this._whenReady; - } - - async whenIpcReady(): Promise { - - // Always wait for `spawn()` - await this.whenSpawnedBarrier.wait(); - - await this._whenIpcReady; + // Connect and return message port + const window = assertIsDefined(this.window); + return connectMessagePort(window); } toggle(): void { diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index c4de35da47b..357229c46a5 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -415,7 +415,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Crashes & Unrsponsive & Failed to load this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details)); this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); - this._win.webContents.on('did-fail-load', (event: Event, errorCode: number, errorDescription: string) => this.logService.warn('[VS Code]: fail to load workbench window, ', errorDescription)); + this._win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('[VS Code]: fail to load workbench window, ', errorDescription)); // Window close this._win.on('closed', () => { diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 5725a9f7780..fafe76a3e1e 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -22,7 +22,7 @@ import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { Codicon } from 'vs/base/common/codicons'; @@ -266,7 +266,7 @@ export class IssueReporter extends Disposable { private initServices(configuration: IssueReporterConfiguration): void { const serviceCollection = new ServiceCollection(); - const mainProcessService = new MainProcessService(configuration.windowId); + const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService; diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index eb8c234cb5a..e4e7be122e7 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -17,7 +17,7 @@ import { ProcessItem } from 'vs/base/common/processes'; import * as dom from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ByteSize } from 'vs/platform/files/common/files'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; @@ -233,7 +233,7 @@ class ProcessExplorer { private tree: DataTree | undefined; constructor(windowId: number, private data: ProcessExplorerData) { - const mainProcessService = new MainProcessService(windowId); + const mainProcessService = new ElectronIPCMainProcessService(windowId); this.nativeHostService = new NativeHostService(windowId, mainProcessService) as INativeHostService; this.applyStyles(data.styles); diff --git a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts index 0fc2e8cad56..ef17fe1cfe6 100644 --- a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts +++ b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Server } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; export const IMainProcessService = createDecorator('mainProcessService'); @@ -19,7 +20,10 @@ export interface IMainProcessService { registerChannel(channelName: string, channel: IServerChannel): void; } -export class MainProcessService extends Disposable implements IMainProcessService { +/** + * An implementation of `IMainProcessService` that leverages Electron's IPC. + */ +export class ElectronIPCMainProcessService extends Disposable implements IMainProcessService { declare readonly _serviceBrand: undefined; @@ -41,3 +45,24 @@ export class MainProcessService extends Disposable implements IMainProcessServic this.mainProcessConnection.registerChannel(channelName, channel); } } + +/** + * An implementation of `IMainProcessService` that leverages MessagePorts. + */ +export class MessagePortMainProcessService implements IMainProcessService { + + declare readonly _serviceBrand: undefined; + + constructor( + private server: Server, + private router: StaticRouter + ) { } + + getChannel(channelName: string): IChannel { + return this.server.getChannel(channelName, this.router); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.server.registerChannel(channelName, channel); + } +} diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 23ee89c3631..56fd77afd1f 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -32,7 +32,7 @@ import { IWorkbenchConfigurationService } from 'vs/workbench/services/configurat import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { registerWindowDriver } from 'vs/platform/driver/electron-browser/driver'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; @@ -181,7 +181,7 @@ class DesktopMain extends Disposable { // Main Process - const mainProcessService = this._register(new MainProcessService(this.configuration.windowId)); + const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId)); serviceCollection.set(IMainProcessService, mainProcessService); // Environment diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 4e309ea232f..bd84d333419 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -19,7 +19,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -153,7 +153,7 @@ class DesktopMain extends Disposable { // Main Process - const mainProcessService = this._register(new MainProcessService(this.configuration.windowId)); + const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId)); serviceCollection.set(IMainProcessService, mainProcessService); // Environment diff --git a/src/vs/workbench/services/sharedProcess/electron-browser/sharedProcessService.ts b/src/vs/workbench/services/sharedProcess/electron-browser/sharedProcessService.ts index 95be518ea2d..23a398a164f 100644 --- a/src/vs/workbench/services/sharedProcess/electron-browser/sharedProcessService.ts +++ b/src/vs/workbench/services/sharedProcess/electron-browser/sharedProcessService.ts @@ -3,38 +3,66 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net'; -import { connect as nodeIPCConnect } from 'vs/base/parts/ipc/node/ipc.net'; +import { Event } from 'vs/base/common/event'; +import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ISharedProcessService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class SharedProcessService implements ISharedProcessService { +export class SharedProcessService extends Disposable implements ISharedProcessService { declare readonly _serviceBrand: undefined; - private withSharedProcessConnection: Promise>; - private sharedProcessMainChannel: IChannel; + private readonly sharedProcessMainChannel = this.mainProcessService.getChannel('sharedProcess'); + private readonly withSharedProcessConnection: Promise; constructor( - @IMainProcessService mainProcessService: IMainProcessService, - @INativeHostService nativeHostService: INativeHostService, - @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService + @IMainProcessService private readonly mainProcessService: IMainProcessService, + @INativeHostService private readonly nativeHostService: INativeHostService, + @ILogService private readonly logService: ILogService, + @ILifecycleService private readonly lifecycleService: ILifecycleService ) { - this.sharedProcessMainChannel = mainProcessService.getChannel('sharedProcess'); + super(); - this.withSharedProcessConnection = (async () => { - await this.whenReady(); + this.withSharedProcessConnection = this.connect(); - return nodeIPCConnect(environmentService.sharedIPCHandle, `window:${nativeHostService.windowId}`); - })(); + this.registerListeners(); } - private whenReady(): Promise { - return this.sharedProcessMainChannel.call('whenReady'); + private registerListeners(): void { + + // Lifecycle + this.lifecycleService.onWillShutdown(() => this.dispose()); + } + + private async connect(): Promise { + this.logService.trace('Workbench->SharedProcess#connect'); + + // await the shared process to be ready + await this.sharedProcessMainChannel.call('whenReady'); + + // Ask to create message channel inside the window + // and send over a UUID to correlate the response + const nonce = generateUuid(); + ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce); + + // Wait until the main side has returned the `MessagePort` + // We need to filter by the `nonce` to ensure we listen + // to the right response. + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); + + this.logService.trace('Workbench->SharedProcess#connect: connection established'); + + return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`)); } getChannel(channelName: string): IChannel {