diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 56d365091e9..2ce9e0eb47c 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -207,3 +207,18 @@ export function withNullAsUndefined(x: T | null): T | undefined { export function withUndefinedAsNull(x: T | undefined): T | null { return typeof x === 'undefined' ? null : x; } + +/** + * Allows to add a first parameter to functions of a type. + */ +export type AddFirstParameterToFunctions = { + + // For every property + [K in keyof Target]: + + // Function: add param to function + Target[K] extends (...args: any) => TargetFunctionsReturnType ? (firstArg: FirstParameter, ...args: Parameters) => ReturnType : + + // Else: just leave as is + Target[K] +}; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 9e88ccd42c7..5e65b09c74f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -34,7 +34,7 @@ import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; -import { SimpleServiceProxyChannel } from 'vs/platform/ipc/node/simpleIpcProxy'; +import { createChannelReceiver } from 'vs/platform/ipc/node/ipcChannelCreator'; import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -541,15 +541,15 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('update', updateChannel); const issueService = accessor.get(IIssueService); - const issueChannel = new SimpleServiceProxyChannel(issueService); + const issueChannel = createChannelReceiver(issueService); electronIpcServer.registerChannel('issue', issueChannel); const electronService = accessor.get(IElectronService); - const electronChannel = new SimpleServiceProxyChannel(electronService); + const electronChannel = createChannelReceiver(electronService); electronIpcServer.registerChannel('electron', electronChannel); const sharedProcessMainService = accessor.get(ISharedProcessMainService); - const sharedProcessChannel = new SimpleServiceProxyChannel(sharedProcessMainService); + const sharedProcessChannel = createChannelReceiver(sharedProcessMainService); electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel); const workspacesMainService = accessor.get(IWorkspacesMainService); @@ -562,7 +562,7 @@ export class CodeApplication extends Disposable { sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); const menubarService = accessor.get(IMenubarService); - const menubarChannel = new SimpleServiceProxyChannel(menubarService); + const menubarChannel = createChannelReceiver(menubarService); electronIpcServer.registerChannel('menubar', menubarChannel); const urlService = accessor.get(IURLService); diff --git a/src/vs/platform/electron/electron-browser/electronService.ts b/src/vs/platform/electron/electron-browser/electronService.ts index b1c6db59d4f..690925f54f0 100644 --- a/src/vs/platform/electron/electron-browser/electronService.ts +++ b/src/vs/platform/electron/electron-browser/electronService.ts @@ -5,7 +5,7 @@ import { IElectronService } from 'vs/platform/electron/node/electron'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; -import { createSimpleChannelProxy } from 'vs/platform/ipc/node/simpleIpcProxy'; +import { createChannelSender } from 'vs/platform/ipc/node/ipcChannelCreator'; import { IWindowService } from 'vs/platform/windows/common/windows'; export class ElectronService { @@ -16,6 +16,6 @@ export class ElectronService { @IMainProcessService mainProcessService: IMainProcessService, @IWindowService windowService: IWindowService ) { - return createSimpleChannelProxy(mainProcessService.getChannel('electron'), windowService.windowId); + return createChannelSender(mainProcessService.getChannel('electron'), { context: windowService.windowId }); } } diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 48ba9ca7f34..493cbac43ad 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu } from 'electron'; +import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app } from 'electron'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { OpenContext, INativeOpenDialogOptions, IWindowOpenable, IOpenInWindowOptions, IOpenedWindow, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { isMacintosh } from 'vs/base/common/platform'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; -import { AddContextToFunctions } from 'vs/platform/ipc/node/simpleIpcProxy'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -export class ElectronMainService implements AddContextToFunctions { +export class ElectronMainService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { _serviceBrand: undefined; diff --git a/src/vs/platform/ipc/node/ipcChannelCreator.ts b/src/vs/platform/ipc/node/ipcChannelCreator.ts new file mode 100644 index 00000000000..748f52b4b9c --- /dev/null +++ b/src/vs/platform/ipc/node/ipcChannelCreator.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { revive } from 'vs/base/common/marshalling'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { isUpperAsciiLetter } from 'vs/base/common/strings'; + +// +// Use both `createChannelReceiver` and `createChannelSender` +// for automated process <=> process communication over methods. +// + +export interface IBaseChannelOptions { + + /** + * Disables automatic marshalling of `URI`. + * If marshalling is disabled, `UriComponents` + * must be used instead. + */ + disableMarshalling?: boolean; +} + +export interface IChannelReceiverOptions extends IBaseChannelOptions { } + +export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel { + const handler = service as { [key: string]: unknown }; + const disableMarshalling = options && options.disableMarshalling; + + // Buffer any event that should be supported by + // iterating over all property keys and finding them + const mapEventNameToEvent = new Map>(); + for (const key in handler) { + if (propertyIsEvent(key)) { + mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event, true)); + } + } + + return new class implements IServerChannel { + + listen(_: unknown, event: string): Event { + const eventImpl = mapEventNameToEvent.get(event); + if (eventImpl) { + return eventImpl as Event; + } + + throw new Error(`Event not found: ${event}`); + } + + call(_: unknown, command: string, args?: any[]): Promise { + const target = handler[command]; + if (typeof target === 'function') { + + // Revive unless marshalling disabled + if (!disableMarshalling && Array.isArray(args)) { + for (let i = 0; i < args.length; i++) { + args[i] = revive(args[i]); + } + } + + return target.apply(handler, args); + } + + throw new Error(`Method not found: ${command}`); + } + }; +} + +export interface IChannelSenderOptions extends IBaseChannelOptions { + + /** + * If provided, will add the value of `context` + * to each method call to the target. + */ + context?: unknown; +} + +export function createChannelSender(channel: IChannel, options?: IChannelSenderOptions): T { + const disableMarshalling = options && options.disableMarshalling; + + return new Proxy({}, { + get(_target, propKey, _receiver) { + if (typeof propKey === 'string') { + + // Event + if (propertyIsEvent(propKey)) { + return channel.listen(propKey); + } + + // Function + return async function (...args: any[]) { + + // Add context if any + let methodArgs: any[]; + if (options && !isUndefinedOrNull(options.context)) { + methodArgs = [options.context, ...args]; + } else { + methodArgs = args; + } + + const result = await channel.call(propKey, methodArgs); + + // Revive unless marshalling disabled + if (!disableMarshalling) { + return revive(result); + } + + return result; + }; + } + + throw new Error(`Property not found: ${String(propKey)}`); + } + }) as T; +} + +function propertyIsEvent(name: string): boolean { + // Assume a property is an event if it has a form of "onSomething" + return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2)); +} diff --git a/src/vs/platform/ipc/node/simpleIpcProxy.ts b/src/vs/platform/ipc/node/simpleIpcProxy.ts deleted file mode 100644 index 84e9beee2a8..00000000000 --- a/src/vs/platform/ipc/node/simpleIpcProxy.ts +++ /dev/null @@ -1,101 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { revive } from 'vs/base/common/marshalling'; - -// -// Use both `SimpleServiceProxyChannel` and `createSimpleChannelProxy` -// for a very basic process <=> process communication over methods. -// - -export type AddContextToFunctions = { - // For every property: IF property is a FUNCTION ADD context as first parameter and original parameters afterwards with same return type, otherwise preserve as is - [K in keyof Target]: Target[K] extends (...args: any) => any ? (context: Context, ...args: Parameters) => ReturnType : Target[K] -}; - -interface ISimpleChannelProxyContext { - __$simpleIPCContextMarker: boolean; - proxyContext: unknown; -} - -function serializeContext(proxyContext?: unknown): ISimpleChannelProxyContext | undefined { - if (proxyContext) { - return { __$simpleIPCContextMarker: true, proxyContext }; - } - - return undefined; -} - -function deserializeContext(candidate?: ISimpleChannelProxyContext | undefined): unknown | undefined { - if (candidate && candidate.__$simpleIPCContextMarker === true) { - return candidate.proxyContext; - } - - return undefined; -} - -export class SimpleServiceProxyChannel implements IServerChannel { - - private service: { [key: string]: unknown }; - - constructor(service: unknown) { - this.service = service as { [key: string]: unknown }; - } - - listen(_: unknown, event: string): Event { - throw new Error(`Events are currently unsupported by SimpleServiceProxyChannel: ${event}`); - } - - call(_: unknown, command: string, args?: any[]): Promise { - const target = this.service[command]; - if (typeof target === 'function') { - if (Array.isArray(args)) { - - // Deserialize context if any - const context = deserializeContext(args[0]); - if (context) { - args[0] = context; - } - - // Revive: handles URIs and RegExp - for (let i = 0; i < args.length; i++) { - args[i] = revive(args[i]); - } - } - - return target.apply(this.service, args); - } - - throw new Error(`Method not found: ${command}`); - } -} - -export function createSimpleChannelProxy(channel: IChannel, context?: unknown): T { - const serializedContext = serializeContext(context); - - return new Proxy({}, { - get(_target, propKey, _receiver) { - if (typeof propKey === 'string') { - return async function (...args: any[]) { - - // Serialize context if any - let methodArgs: any[]; - if (serializedContext) { - methodArgs = [context, ...args]; - } else { - methodArgs = args; - } - - // Revive: handles URIs and RegExp - return revive(await channel.call(propKey, methodArgs)); - }; - } - - throw new Error(`Unable to provide main channel proxy implementation for: ${String(propKey)}`); - } - }) as T; -} diff --git a/src/vs/platform/issue/electron-browser/issueService.ts b/src/vs/platform/issue/electron-browser/issueService.ts index 72502019070..46fed585d13 100644 --- a/src/vs/platform/issue/electron-browser/issueService.ts +++ b/src/vs/platform/issue/electron-browser/issueService.ts @@ -5,13 +5,13 @@ import { IIssueService } from 'vs/platform/issue/node/issue'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; -import { createSimpleChannelProxy } from 'vs/platform/ipc/node/simpleIpcProxy'; +import { createChannelSender } from 'vs/platform/ipc/node/ipcChannelCreator'; export class IssueService { _serviceBrand: undefined; constructor(@IMainProcessService mainProcessService: IMainProcessService) { - return createSimpleChannelProxy(mainProcessService.getChannel('issue')); + return createChannelSender(mainProcessService.getChannel('issue')); } } diff --git a/src/vs/platform/menubar/electron-browser/menubarService.ts b/src/vs/platform/menubar/electron-browser/menubarService.ts index 4a537c4e6c4..8eb8e4fbb69 100644 --- a/src/vs/platform/menubar/electron-browser/menubarService.ts +++ b/src/vs/platform/menubar/electron-browser/menubarService.ts @@ -5,13 +5,13 @@ import { IMenubarService } from 'vs/platform/menubar/node/menubar'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; -import { createSimpleChannelProxy } from 'vs/platform/ipc/node/simpleIpcProxy'; +import { createChannelSender } from 'vs/platform/ipc/node/ipcChannelCreator'; export class MenubarService { _serviceBrand: undefined; constructor(@IMainProcessService mainProcessService: IMainProcessService) { - return createSimpleChannelProxy(mainProcessService.getChannel('menubar')); + return createChannelSender(mainProcessService.getChannel('menubar')); } }