diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index c42da191b4fc5..52fa82c0e520f 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -14,12 +14,12 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { BackendInitializationFn, createAPI, PluginMetadata } from '@theia/plugin-ext'; +import { BackendInitializationFn, createAPI, PluginMetadata, PluginManager } from '@theia/plugin-ext'; -export const doInitialization: BackendInitializationFn = (rpc: any, pluginMetadata: PluginMetadata) => { +export const doInitialization: BackendInitializationFn = (rpc: any, manager: PluginManager, pluginMetadata: PluginMetadata) => { const module = require('module'); const vscodeModuleName = 'vscode'; - const vscode = createAPI(rpc); + const vscode = createAPI(rpc, manager); // register the commands that are in the package.json file const contributes: any = pluginMetadata.source.contributes; @@ -37,6 +37,16 @@ export const doInitialization: BackendInitializationFn = (rpc: any, pluginMetada } }; + // use Theia plugin api instead vscode extensions + (vscode).extensions = { + get all(): any[] { + return vscode.plugins.all; + }, + getExtension(pluginId: string): any | undefined { + return vscode.plugins.getPlugin(pluginId); + } + }; + // add theia into global goal as 'vscode' const g = global as any; g[vscodeModuleName] = vscode; diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts index 9d9ef842d392b..fa37c36df1996 100644 --- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts +++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts @@ -27,6 +27,7 @@ export class VsCodePluginScanner implements PluginScanner { getModel(plugin: PluginPackage): PluginModel { return { + id: `${plugin.publisher}.${plugin.name}`, name: plugin.name, publisher: plugin.publisher, version: plugin.version, diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index bcbeafff9d629..a1e3f1cfd646b 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -16,7 +16,7 @@ import { createProxyIdentifier, ProxyIdentifier } from './rpc-protocol'; import * as theia from '@theia/plugin'; -import { PluginLifecycle, PluginModel, PluginMetadata } from '../common/plugin-protocol'; +import { PluginLifecycle, PluginModel, PluginMetadata, PluginPackage } from '../common/plugin-protocol'; import { QueryParameters } from '../common/env'; import { TextEditorCursorStyle } from '../common/editor-options'; import { TextEditorLineNumbersStyle, EndOfLine, OverviewRulerLane, IndentAction } from '../plugin/types-impl'; @@ -33,18 +33,36 @@ import { MarkerData } from './model'; -export interface HostedPluginManagerExt { - $initialize(contextPath: string, pluginMetadata: PluginMetadata): void; - $loadPlugin(contextPath: string, plugin: Plugin): void; - $stopPlugin(contextPath: string): PromiseLike; +export interface PluginInitData { + plugins: PluginMetadata[]; } export interface Plugin { pluginPath: string; + initPath: string; model: PluginModel; + rawModel: PluginPackage; lifecycle: PluginLifecycle; } +export interface PluginAPI { + +} + +export interface PluginManager { + getAllPlugins(): Plugin[]; + getPluginById(pluginId: string): Plugin | undefined; + getPluginExport(pluginId: string): PluginAPI | undefined; + isRunning(pluginId: string): boolean; + activatePlugin(pluginId: string): PromiseLike; +} + +export interface PluginManagerExt { + $stopPlugin(contextPath: string): PromiseLike; + + $init(pluginInit: PluginInitData): PromiseLike; +} + export interface CommandRegistryMain { $registerCommand(command: theia.Command): void; @@ -609,7 +627,7 @@ export const PLUGIN_RPC_CONTEXT = { }; export const MAIN_RPC_CONTEXT = { - HOSTED_PLUGIN_MANAGER_EXT: createProxyIdentifier('HostedPluginManagerExt'), + HOSTED_PLUGIN_MANAGER_EXT: createProxyIdentifier('PluginManagerExt'), COMMAND_REGISTRY_EXT: createProxyIdentifier('CommandRegistryExt'), QUICK_OPEN_EXT: createProxyIdentifier('QuickOpenExt'), WINDOW_STATE_EXT: createProxyIdentifier('WindowStateExt'), diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 5bf17ccf7c001..1ea47ad9c90ff 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -17,7 +17,7 @@ import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { RPCProtocol } from '../api/rpc-protocol'; import { Disposable } from '@theia/core/lib/common/disposable'; import { LogPart } from './types'; -import { CharacterPair, CommentRule } from '../api/plugin-api'; +import { CharacterPair, CommentRule, PluginManager } from '../api/plugin-api'; export const hostedServicePath = '/services/hostedPlugin'; @@ -253,6 +253,7 @@ export interface PluginDeployerDirectoryHandler { * This interface describes a plugin model object, which is populated from package.json. */ export interface PluginModel { + id: string; name: string; publisher: string; version: string; @@ -360,7 +361,7 @@ export interface PluginLifecycle { * The export function of initialization module of backend plugin. */ export interface BackendInitializationFn { - (rpc: RPCProtocol, pluginMetadata: PluginMetadata): void; + (rpc: RPCProtocol, manager: PluginManager, pluginMetadata: PluginMetadata): void; } export interface BackendLoadingFn { diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 9c146eff4f61a..8847dea6c6a6a 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -17,7 +17,7 @@ import { injectable, inject, interfaces } from 'inversify'; import { PluginWorker } from '../../main/browser/plugin-worker'; import { HostedPluginServer, PluginMetadata } from '../../common/plugin-protocol'; import { HostedPluginWatcher } from './hosted-plugin-watcher'; -import { MAIN_RPC_CONTEXT, Plugin } from '../../api/plugin-api'; +import { MAIN_RPC_CONTEXT } from '../../api/plugin-api'; import { setUpPluginApi } from '../../main/browser/main-context'; import { RPCProtocol, RPCProtocolImpl } from '../../api/rpc-protocol'; import { ILogger } from '@theia/core'; @@ -43,9 +43,6 @@ export class HostedPluginSupport { private theiaReadyPromise: Promise; - private backendApiInitialized = false; - private frontendApiInitialized = false; - constructor( @inject(PreferenceServiceImpl) private readonly preferenceServiceImpl: PreferenceServiceImpl ) { @@ -58,81 +55,55 @@ export class HostedPluginSupport { } public initPlugins(): void { - this.server.getHostedPlugin().then((pluginMetadata: any) => { - if (pluginMetadata) { - this.loadPlugin(pluginMetadata, this.container); + const backendMetadata = this.server.getDeployedBackendMetadata(); + const frontendMetadata = this.server.getDeployedFrontendMetadata(); + Promise.all([backendMetadata, frontendMetadata, this.server.getHostedPlugin()]).then(metadata => { + const plugins = [...metadata['0'], ...metadata['1']]; + if (metadata['2']) { + plugins.push(metadata['2']!); } + this.loadPlugins(plugins, this.container); }); - const backendMetadata = this.server.getDeployedBackendMetadata(); + } - backendMetadata.then((pluginMetadata: PluginMetadata[]) => { - pluginMetadata.forEach(metadata => this.loadPlugin(metadata, this.container)); - }); + loadPlugins(pluginsMetadata: PluginMetadata[], container: interfaces.Container): void { + const [frontend, backend] = this.initContributions(pluginsMetadata); + this.theiaReadyPromise.then(() => { + if (frontend) { + this.worker = new PluginWorker(); + const hostedExtManager = this.worker.rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); + hostedExtManager.$init({ plugins: pluginsMetadata }); + setUpPluginApi(this.worker.rpc, container); + } - this.server.getDeployedFrontendMetadata().then((pluginMetadata: PluginMetadata[]) => { - pluginMetadata.forEach(metadata => this.loadPlugin(metadata, this.container)); + if (backend) { + const rpc = this.createServerRpc(); + const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); + hostedExtManager.$init({ plugins: pluginsMetadata }); + setUpPluginApi(rpc, container); + } }); } - public loadPlugin(pluginMetadata: PluginMetadata, container: interfaces.Container): void { - const pluginModel = pluginMetadata.model; - const pluginLifecycle = pluginMetadata.lifecycle; - this.logger.info('Ask to load the plugin with model ', pluginModel, ' and lifecycle', pluginLifecycle); - if (pluginMetadata.model.contributes) { - this.contributionHandler.handleContributions(pluginMetadata.model.contributes); - } - if (pluginModel.entryPoint!.frontend) { - this.logger.info(`Loading frontend hosted plugin: ${pluginModel.name}`); - this.worker = new PluginWorker(); + private initContributions(pluginsMetadata: PluginMetadata[]): [boolean, boolean] { + const result: [boolean, boolean] = [false, false]; + for (const plugin of pluginsMetadata) { + if (plugin.model.entryPoint.frontend) { + result[0] = true; + } - this.theiaReadyPromise.then(() => { - const hostedExtManager = this.worker.rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.frontend!, - model: pluginModel, - lifecycle: pluginLifecycle - }; - let frontendInitPath = pluginLifecycle.frontendInitPath; - if (frontendInitPath) { - hostedExtManager.$initialize(frontendInitPath, pluginMetadata); - } else { - frontendInitPath = ''; - } - // we should create only one instance of the plugin api per connection - if (!this.frontendApiInitialized) { - setUpPluginApi(this.worker.rpc, container); - this.frontendApiInitialized = true; - } - hostedExtManager.$loadPlugin(frontendInitPath, plugin); - }); - } - if (pluginModel.entryPoint!.backend) { - this.logger.info(`Loading backend hosted plugin: ${pluginModel.name}`); - const rpc = this.createServerRpc(); + if (plugin.model.entryPoint.backend) { + result[1] = true; + } - this.theiaReadyPromise.then(() => { - const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.backend!, - model: pluginModel, - lifecycle: pluginLifecycle - }; - let backendInitPath = pluginLifecycle.backendInitPath; - if (backendInitPath) { - hostedExtManager.$initialize(backendInitPath, pluginMetadata); - } else { - backendInitPath = ''; - } - // we should create only one instance of the plugin api per connection - if (!this.backendApiInitialized) { - setUpPluginApi(rpc, container); - this.backendApiInitialized = true; - } - hostedExtManager.$loadPlugin(backendInitPath, plugin); - }); + if (plugin.model.contributes) { + this.contributionHandler.handleContributions(plugin.model.contributes); + } } + + return result; } private createServerRpc(): RPCProtocol { diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index cc6f8368e76bd..47c9835865245 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -16,14 +16,13 @@ import { Emitter } from '@theia/core/lib/common/event'; import { RPCProtocolImpl } from '../../../api/rpc-protocol'; -import { HostedPluginManagerExtImpl } from '../../plugin/hosted-plugin-manager'; +import { PluginManagerExtImpl } from '../../../plugin/plugin-manager'; import { MAIN_RPC_CONTEXT, Plugin } from '../../../api/plugin-api'; -import { createAPI, startPlugin } from '../../../plugin/plugin-context'; +import { createAPI } from '../../../plugin/plugin-context'; import { getPluginId, PluginMetadata } from '../../../common/plugin-protocol'; -import { Disposable } from '@theia/core/src/common'; +// tslint:disable-next-line:no-any const ctx = self as any; -const plugins = new Map(); const emitter = new Emitter(); const rpc = new RPCProtocolImpl({ @@ -32,18 +31,17 @@ const rpc = new RPCProtocolImpl({ ctx.postMessage(m); } }); +// tslint:disable-next-line:no-any addEventListener('message', (message: any) => { emitter.fire(message.data); }); +function initialize(contextPath: string, pluginMetadata: PluginMetadata): void { + ctx.importScripts('/context/' + contextPath); +} -const theia = createAPI(rpc); -ctx['theia'] = theia; - -rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtImpl({ - initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - ctx.importScripts('/context/' + contextPath); - }, - loadPlugin(contextPath: string, plugin: Plugin): void { +const pluginManager = new PluginManagerExtImpl({ + // tslint:disable-next-line:no-any + loadPlugin(contextPath: string, plugin: Plugin): any { if (isElectron()) { ctx.importScripts(plugin.pluginPath); } else { @@ -55,32 +53,49 @@ rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtIm console.error(`WebWorker: Cannot start plugin "${plugin.model.name}". Frontend plugin not found: "${plugin.lifecycle.frontendModuleName}"`); return; } - startPlugin(plugin, ctx[plugin.lifecycle.frontendModuleName], plugins); + return ctx[plugin.lifecycle.frontendModuleName]; } }, - stopPlugins(contextPath: string, pluginIds: string[]): void { - pluginIds.forEach(pluginId => { - const pluginData = plugins.get(pluginId); - if (pluginData) { - // call stop method - if (pluginData.stopPluginMethod) { - pluginData.stopPluginMethod(); + init(rawPluginData: PluginMetadata[]): [Plugin[], Plugin[]] { + const result: Plugin[] = []; + const foreign: Plugin[] = []; + for (const plg of rawPluginData) { + const pluginModel = plg.model; + const pluginLifecycle = plg.lifecycle; + if (pluginModel.entryPoint!.frontend) { + let frontendInitPath = pluginLifecycle.frontendInitPath; + if (frontendInitPath) { + initialize(frontendInitPath, plg); + } else { + frontendInitPath = ''; } - - // dispose any objects - const pluginContext = pluginData.pluginContext; - if (pluginContext) { - pluginContext.subscriptions.forEach((element: Disposable) => { - element.dispose(); - }); - } - - // delete entry - plugins.delete(pluginId); + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.frontend!, + initPath: frontendInitPath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }; + result.push(plugin); + } else { + foreign.push({ + pluginPath: pluginModel.entryPoint.backend!, + initPath: pluginLifecycle.backendInitPath!, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }); } - }); + } + + return [result, foreign]; } -})); +}); + +const theia = createAPI(rpc, pluginManager); +ctx['theia'] = theia; + +rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, pluginManager); function isElectron() { if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) { diff --git a/packages/plugin-ext/src/hosted/node/plugin-host.ts b/packages/plugin-ext/src/hosted/node/plugin-host.ts index 3938d045eb811..59608f53f96e0 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host.ts @@ -15,40 +15,39 @@ ********************************************************************************/ import { Emitter } from '@theia/core/lib/common/event'; -import { startPlugin } from '../../plugin/plugin-context'; -import { HostedPluginManagerExtImpl } from '../plugin/hosted-plugin-manager'; +import { PluginManagerExtImpl } from '../../plugin/plugin-manager'; import { RPCProtocolImpl } from '../../api/rpc-protocol'; import { MAIN_RPC_CONTEXT, Plugin } from '../../api/plugin-api'; import { PluginMetadata } from '../../common/plugin-protocol'; console.log('PLUGIN_HOST(' + process.pid + ') starting instance'); -const plugins = new Map(); - -const emmitter = new Emitter(); +const emitter = new Emitter(); const rpc = new RPCProtocolImpl({ - onMessage: emmitter.event, + onMessage: emitter.event, send: (m: {}) => { if (process.send) { process.send(JSON.stringify(m)); } } }); -process.on('message', (message: any) => { + +process.on('message', (message: string) => { try { - emmitter.fire(JSON.parse(message)); + emitter.fire(JSON.parse(message)); } catch (e) { console.error(e); } }); -rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtImpl({ +// tslint:disable-next-line:no-any +function initialize(contextPath: string, pluginMetadata: PluginMetadata): any { + console.log('PLUGIN_HOST(' + process.pid + '): initializing(' + contextPath + ')'); + const backendInit = require(contextPath); + backendInit.doInitialization(rpc, pluginManager, pluginMetadata); +} - initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - console.log('PLUGIN_HOST(' + process.pid + '): initializing(' + contextPath + ')'); - const backendInit = require(contextPath); - backendInit.doInitialization(rpc, pluginMetadata); - }, +const pluginManager = new PluginManagerExtImpl({ loadPlugin(contextPath: string, plugin: Plugin): void { console.log('PLUGIN_HOST(' + process.pid + '): loadPlugin(' + plugin.pluginPath + ')'); const backendInit = require(contextPath); @@ -56,35 +55,46 @@ rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtIm backendInit.doLoad(rpc, plugin); } try { - const pluginMain = require(plugin.pluginPath); - startPlugin(plugin, pluginMain, plugins); - + return require(plugin.pluginPath); } catch (e) { console.error(e); } }, - stopPlugins(contextPath: string, pluginIds: string[]): void { - console.log('PLUGIN_HOST(' + process.pid + '): stopPlugins(' + JSON.stringify(pluginIds) + ')'); - pluginIds.forEach(pluginId => { - const pluginData = plugins.get(pluginId); - - if (pluginData) { - // call stop method - if (pluginData.stopPluginMethod) { - pluginData.stopPluginMethod(); - } + init(raw: PluginMetadata[]): [Plugin[], Plugin[]] { + const result: Plugin[] = []; + const foreign: Plugin[] = []; + for (const plg of raw) { + const pluginModel = plg.model; + const pluginLifecycle = plg.lifecycle; + if (pluginModel.entryPoint!.backend) { - // dispose any objects - const pluginContext = pluginData.pluginContext; - if (pluginContext) { - pluginContext.subscriptions.forEach((element: any) => { - element.dispose(); - }); + let backendInitPath = pluginLifecycle.backendInitPath; + if (backendInitPath) { + initialize(backendInitPath, plg); + } else { + backendInitPath = ''; } - - // delete entry - plugins.delete(pluginId); + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.backend!, + initPath: backendInitPath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }; + result.push(plugin); + } else { + foreign.push({ + pluginPath: pluginModel.entryPoint.frontend!, + initPath: pluginLifecycle.frontendInitPath!, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel: plg.source + }); } - }); + } + + return [result, foreign]; } -})); +}); + +rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, pluginManager); diff --git a/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts index 9a6a314832f7f..ff8ec074566db 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts @@ -16,9 +16,10 @@ import { createAPI } from '../../../plugin/plugin-context'; import { BackendInitializationFn } from '../../../common/plugin-protocol'; +import { PluginManager } from '../../../api/plugin-api'; -export const doInitialization: BackendInitializationFn = (rpc: any) => { - const theia = createAPI(rpc); +export const doInitialization: BackendInitializationFn = (rpc: any, manager: PluginManager) => { + const theia = createAPI(rpc, manager); // add theia into global goal const g = global as any; diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index b61942a9fb1a7..cd672421f762d 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -49,6 +49,7 @@ export class TheiaPluginScanner implements PluginScanner { getModel(plugin: PluginPackage): PluginModel { const result: PluginModel = { + id: `${plugin.publisher}.${plugin.name}`, name: plugin.name, publisher: plugin.publisher, version: plugin.version, diff --git a/packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts b/packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts deleted file mode 100644 index 6cdce9b08b30d..0000000000000 --- a/packages/plugin-ext/src/hosted/plugin/hosted-plugin-manager.ts +++ /dev/null @@ -1,50 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { HostedPluginManagerExt, Plugin } from '../../api/plugin-api'; -import { getPluginId, PluginMetadata } from '../../common/plugin-protocol'; - -export interface PluginHost { - initialize(contextPath: string, pluginMetadata: PluginMetadata): void; - - loadPlugin(contextPath: string, plugin: Plugin): void; - - stopPlugins(contextPath: string, pluginIds: string[]): void; -} - -export class HostedPluginManagerExtImpl implements HostedPluginManagerExt { - - private runningPluginIds: string[]; - - constructor(private readonly host: PluginHost) { - this.runningPluginIds = []; - } - - $initialize(contextPath: string, pluginMetadata: PluginMetadata): void { - this.host.initialize(contextPath, pluginMetadata); - } - - $loadPlugin(contextPath: string, plugin: Plugin): void { - this.runningPluginIds.push(getPluginId(plugin.model)); - this.host.loadPlugin(contextPath, plugin); - } - - $stopPlugin(contextPath: string): PromiseLike { - this.host.stopPlugins(contextPath, this.runningPluginIds); - return Promise.resolve(); - } - -} diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index b6c3cae780ddf..3beabff2848bd 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -20,13 +20,11 @@ import { ContainerModule } from 'inversify'; import { FrontendApplicationContribution, FrontendApplication, WidgetFactory, bindViewContribution } from '@theia/core/lib/browser'; import { MaybePromise, CommandContribution, ResourceResolver } from '@theia/core/lib/common'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; -import { PluginWorker } from './plugin-worker'; import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { HostedPluginWatcher } from '../../hosted/browser/hosted-plugin-watcher'; import { HostedPluginLogViewer } from '../../hosted/browser/hosted-plugin-log-viewer'; import { HostedPluginManagerClient } from '../../hosted/browser/hosted-plugin-manager-client'; import { PluginApiFrontendContribution } from './plugin-frontend-contribution'; -import { setUpPluginApi } from './main-context'; import { HostedPluginServer, hostedServicePath, PluginServer, pluginServerJsonRpcPath } from '../../common/plugin-protocol'; import { ModalNotification } from './dialogs/modal-notification'; import { PluginWidget } from './plugin-ext-widget'; @@ -48,7 +46,6 @@ export default new ContainerModule(bind => { bind(ModalNotification).toSelf().inSingletonScope(); - bind(PluginWorker).toSelf().inSingletonScope(); bind(HostedPluginSupport).toSelf().inSingletonScope(); bind(HostedPluginWatcher).toSelf().inSingletonScope(); bind(HostedPluginLogViewer).toSelf().inSingletonScope(); @@ -68,9 +65,6 @@ export default new ContainerModule(bind => { bind(FrontendApplicationContribution).toDynamicValue(ctx => ({ onStart(app: FrontendApplication): MaybePromise { - const worker = ctx.container.get(PluginWorker); - - setUpPluginApi(worker.rpc, ctx.container); ctx.container.get(HostedPluginSupport).checkAndLoadPlugin(ctx.container); } })); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 1480230305294..ccf766613a0f5 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -18,9 +18,8 @@ import { CommandRegistryImpl } from './command-registry'; import { Emitter } from '@theia/core/lib/common/event'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { QuickOpenExtImpl } from './quick-open'; -import { MAIN_RPC_CONTEXT, Plugin } from '../api/plugin-api'; +import { MAIN_RPC_CONTEXT, Plugin as InternalPlugin, PluginManager } from '../api/plugin-api'; import { RPCProtocol } from '../api/rpc-protocol'; -import { getPluginId } from '../common/plugin-protocol'; import { MessageRegistryExt } from './message-registry'; import { StatusBarMessageRegistryExt } from './status-bar-message-registry'; import { WindowStateExtImpl } from './window-state'; @@ -69,7 +68,7 @@ import { LanguagesExtImpl, score } from './languages'; import { fromDocumentSelector } from './type-converters'; import { DialogsExtImpl } from './dialogs'; -export function createAPI(rpc: RPCProtocol): typeof theia { +export function createAPI(rpc: RPCProtocol, pluginManager: PluginManager): typeof theia { const commandRegistryExt = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); const quickOpenExt = rpc.set(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT, new QuickOpenExtImpl(rpc)); const dialogsExt = new DialogsExtImpl(rpc); @@ -295,6 +294,19 @@ export function createAPI(rpc: RPCProtocol): typeof theia { }, }; + const plugins: typeof theia.plugins = { + get all(): theia.Plugin[] { + return pluginManager.getAllPlugins().map(plg => new Plugin(pluginManager, plg)); + }, + getPlugin(pluginId: string): theia.Plugin | undefined { + const plugin = pluginManager.getPluginById(pluginId); + if (plugin) { + return new Plugin(pluginManager, plugin); + } + return undefined; + } + }; + return { version: require('../../package.json').version, commands, @@ -302,6 +314,7 @@ export function createAPI(rpc: RPCProtocol): typeof theia { workspace, env, languages, + plugins, // Types StatusBarAlignment: StatusBarAlignment, Disposable: Disposable, @@ -338,34 +351,25 @@ export function createAPI(rpc: RPCProtocol): typeof theia { }; } -// tslint:disable-next-line:no-any -export function startPlugin(plugin: Plugin, pluginMain: any, plugins: Map): void { - - const pluginId = getPluginId(plugin.model); - const pluginData: any = {}; - - // Create pluginContext object for this plugin. - const subscriptions: theia.Disposable[] = []; - const pluginContext: theia.PluginContext = { - subscriptions: subscriptions - }; - pluginData.pluginContext = pluginContext; - - if (typeof pluginMain[plugin.lifecycle.startMethod] === 'function') { - pluginMain[plugin.lifecycle.startMethod].apply(getGlobal(), [pluginContext]); - } else { - console.log('There is no start method on plugin'); +class Plugin implements theia.Plugin { + id: string; + pluginPath: string; + isActive: boolean; + packageJSON: any; + pluginType: theia.PluginType; + constructor(private readonly pluginManager: PluginManager, plugin: InternalPlugin) { + this.id = plugin.model.id; + this.pluginPath = plugin.rawModel.packagePath; + this.packageJSON = plugin.rawModel; + this.isActive = true; + this.pluginType = plugin.model.entryPoint.frontend ? 'frontend' : 'backend'; } - if (typeof pluginMain[plugin.lifecycle.stopMethod] === 'function') { - pluginData.stopMethod = pluginMain[plugin.lifecycle.stopMethod]; + get exports(): T { + return this.pluginManager.getPluginExport(this.id); } - plugins.set(pluginId, pluginData); -} - -// for electron -function getGlobal() { - // tslint:disable-next-line:no-null-keyword - return typeof self === 'undefined' ? typeof global === 'undefined' ? null : global : self; + activate(): PromiseLike { + return this.pluginManager.activatePlugin(this.id).then(() => this.exports); + } } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts new file mode 100644 index 0000000000000..17a1611779490 --- /dev/null +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -0,0 +1,148 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { PluginManagerExt, PluginInitData, PluginManager, Plugin, PluginAPI } from '../api/plugin-api'; +import { PluginMetadata } from '../common/plugin-protocol'; +import * as theia from '@theia/plugin'; + +import { dispose } from '../common/disposable-util'; +import { Deferred } from '@theia/core/lib/common/promise-util'; + +export interface PluginHost { + + // tslint:disable-next-line:no-any + loadPlugin(contextPath: string, plugin: Plugin): any; + + init(data: PluginMetadata[]): [Plugin[], Plugin[]]; +} + +interface StopFn { + (): void; +} + +class ActivatedPlugin { + constructor(public readonly pluginContext: theia.PluginContext, + public readonly exports?: PluginAPI, + public readonly stopFn?: StopFn) { + } +} + +export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { + + private registry = new Map(); + private activatedPlugins = new Map(); + private pluginActivationPromises = new Map>(); + + constructor(private readonly host: PluginHost) { + } + + $stopPlugin(contextPath: string): PromiseLike { + this.activatedPlugins.forEach(plugin => { + if (plugin.stopFn) { + plugin.stopFn(); + } + + // dispose any objects + const pluginContext = plugin.pluginContext; + if (pluginContext) { + dispose(pluginContext.subscriptions); + } + }); + return Promise.resolve(); + } + + $init(pluginInit: PluginInitData): PromiseLike { + const [plugins, foreignPlugins] = this.host.init(pluginInit.plugins); + // add foreign plugins + for (const plugin of foreignPlugins) { + this.registry.set(plugin.model.id, plugin); + } + // add own plugins, before initialization + for (const plugin of plugins) { + this.registry.set(plugin.model.id, plugin); + } + // run plugins + for (const plugin of plugins) { + const pluginMain = this.host.loadPlugin(plugin.initPath, plugin); + this.startPlugin(plugin, pluginMain); + } + + return Promise.resolve(); + } + + // tslint:disable-next-line:no-any + private startPlugin(plugin: Plugin, pluginMain: any): void { + + // Create pluginContext object for this plugin. + const subscriptions: theia.Disposable[] = []; + const pluginContext: theia.PluginContext = { + subscriptions: subscriptions + }; + + let stopFn = undefined; + if (typeof pluginMain[plugin.lifecycle.stopMethod] === 'function') { + stopFn = pluginMain[plugin.lifecycle.stopMethod]; + } + if (typeof pluginMain[plugin.lifecycle.startMethod] === 'function') { + const pluginExport = pluginMain[plugin.lifecycle.startMethod].apply(getGlobal(), [pluginContext]); + this.activatedPlugins.set(plugin.model.id, new ActivatedPlugin(pluginContext, pluginExport, stopFn)); + + // resolve activation promise + if (this.pluginActivationPromises.has(plugin.model.id)) { + this.pluginActivationPromises.get(plugin.model.id)!.resolve(); + this.pluginActivationPromises.delete(plugin.model.id); + } + } else { + console.log('there is no doStart method on plugin'); + } + } + + getAllPlugins(): Plugin[] { + return Array.from(this.registry.values()); + } + getPluginExport(pluginId: string): PluginAPI | undefined { + const activePlugin = this.activatedPlugins.get(pluginId); + if (activePlugin) { + return activePlugin.exports; + } + return undefined; + } + + getPluginById(pluginId: string): Plugin | undefined { + return this.registry.get(pluginId); + } + + isRunning(pluginId: string): boolean { + return this.registry.has(pluginId); + } + + activatePlugin(pluginId: string): PromiseLike { + if (this.pluginActivationPromises.has(pluginId)) { + return this.pluginActivationPromises.get(pluginId)!.promise; + } + + const deferred = new Deferred(); + this.pluginActivationPromises.set(pluginId, deferred); + return deferred.promise; + } + +} + +// for electron +function getGlobal() { + // tslint:disable-next-line:no-null-keyword + return typeof self === 'undefined' ? typeof global === 'undefined' ? null : global : self; +} diff --git a/packages/plugin/API.md b/packages/plugin/API.md index 23d6d37d6a3ae..93016ad71e866 100644 --- a/packages/plugin/API.md +++ b/packages/plugin/API.md @@ -2,6 +2,48 @@ ## Theia Plugin system description +### Plugin API + +Namespace for dealing with installed plug-ins. Plug-ins are represented +by an Plugin-interface which enables reflection on them. +Plug-in writers can provide APIs to other plug-ins by returning their API public +surface from the `start`-call. + +For example some plugin exports it's API: + +```javascript +export function start() { + let api = { + sum(a, b) { + return a + b; + }, + mul(a, b) { + return a * b; + } + }; + // 'export' public api-surface + return api; +} +``` + +Another plugin can use that API: + +```javascript +let mathExt = theia.plugins.getPlugin('genius.math'); +let importedApi = mathExt.exports; +console.log(importedApi.mul(42, 1)); +``` + +Also plugin API allows access to plugin `package.json` content. + +Example: + +```javascript +const fooPlugin = plugins.getPlugin('publisher.plugin_name'); +const fooPluginPackageJson = fooPlugin.packageJSON; +console.log(fooPluginPackageJson.someField); +``` + ### Command API A command is a unique identifier of a function which diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index fd84bba7eb58a..490f2c6b9c364 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -40,6 +40,105 @@ declare module '@theia/plugin' { } + export type PluginType = 'frontend' | 'backend'; + + /** + * Represents an extension. + * + * To get an instance of an `Plugin` use [getPlugin](#plugins.getPlugin). + */ + export interface Plugin { + + /** + * The canonical plug-in identifier in the form of: `publisher.name`. + */ + readonly id: string; + + /** + * The absolute file path of the directory containing this plug-in. + */ + readonly pluginPath: string; + + /** + * `true` if the plug-in has been activated. + */ + readonly isActive: boolean; + + /** + * The parsed contents of the plug-in's package.json. + */ + readonly packageJSON: any; + + /** + * + */ + readonly pluginType : PluginType; + + /** + * The public API exported by this plug-in. It is an invalid action + * to access this field before this plug-in has been activated. + */ + readonly exports: T; + + /** + * Activates this plug-in and returns its public API. + * + * @return A promise that will resolve when this plug-in has been activated. + */ + activate(): PromiseLike; + } + + /** + * Namespace for dealing with installed plug-ins. Plug-ins are represented + * by an [plug-in](#Plugin)-interface which enables reflection on them. + * + * Plug-in writers can provide APIs to other plug-ins by returning their API public + * surface from the `start`-call. + * + * ```javascript + * export function start() { + * let api = { + * sum(a, b) { + * return a + b; + * }, + * mul(a, b) { + * return a * b; + * } + * }; + * // 'export' public api-surface + * return api; + * } + * ``` + * ```javascript + * let mathExt = plugins.getPlugin('genius.math'); + * let importedApi = mathExt.exports; + * + * console.log(importedApi.mul(42, 1)); + * ``` + */ + export namespace plugins { + /** + * Get an plug-in by its full identifier in the form of: `publisher.name`. + * + * @param pluginId An plug-in identifier. + * @return An plug-in or `undefined`. + */ + export function getPlugin(pluginId: string): Plugin | undefined; + + /** + * Get an plug-in its full identifier in the form of: `publisher.name`. + * + * @param pluginId An plug-in identifier. + * @return An plug-in or `undefined`. + */ + export function getPlugin(pluginId: string): Plugin | undefined; + + /** + * All plug-ins currently known to the system. + */ + export let all: Plugin[]; + } + /** * A command is a unique identifier of a function * which can be executed by a user via a keyboard shortcut,