diff --git a/packages/plugin/src/api/plugin-api.ts b/packages/plugin/src/api/plugin-api.ts index e1e2f602daeca..7acf087ec76dc 100644 --- a/packages/plugin/src/api/plugin-api.ts +++ b/packages/plugin/src/api/plugin-api.ts @@ -6,17 +6,18 @@ */ import { createProxyIdentifier, ProxyIdentifier } from './rpc-protocol'; import * as theia from '@theia/plugin'; +import { PluginLifecycle, PluginModel } from '../common/plugin-protocol'; export interface HostedPluginManagerExt { - $loadPlugin(ext: Plugin): void; + $initialize(contextPath: string): void; + $loadPlugin(plugin: Plugin): void; $stopPlugin(): PromiseLike; } export interface Plugin { - name: string; - publisher: string; - version: string; pluginPath: string; + model: PluginModel; + lifecycle: PluginLifecycle; } export interface CommandRegistryMain { diff --git a/packages/plugin/src/browser/hosted-plugin.ts b/packages/plugin/src/browser/hosted-plugin.ts index 5cab94d118c58..7df18bb8a1b98 100644 --- a/packages/plugin/src/browser/hosted-plugin.ts +++ b/packages/plugin/src/browser/hosted-plugin.ts @@ -5,10 +5,10 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import { injectable, inject, interfaces } from 'inversify'; -import { HostedPluginServer, Plugin } from '../common/plugin-protocol'; +import { HostedPluginServer, PluginModel, PluginLifecycle } from '../common/plugin-protocol'; import { PluginWorker } from './plugin-worker'; import { setUpPluginApi } from './main-context'; -import { MAIN_RPC_CONTEXT } from '../api/plugin-api'; +import { MAIN_RPC_CONTEXT, Plugin } from '../api/plugin-api'; import { HostedPluginWatcher } from './hosted-plugin-watcher'; import { RPCProtocol, RPCProtocolImpl } from '../api/rpc-protocol'; @injectable() @@ -20,36 +20,42 @@ export class HostedPluginSupport { } checkAndLoadPlugin(container: interfaces.Container): void { - this.server.getHostedPlugin().then(plugin => { - if (plugin) { - this.loadPlugin(plugin, container); + this.server.getHostedPlugin().then((pluginMedata: any) => { + if (pluginMedata) { + this.loadPlugin(pluginMedata.model, pluginMedata.lifecycle, container); } }); } - private loadPlugin(plugin: Plugin, container: interfaces.Container): void { - if (plugin.theiaPlugin!.worker) { - console.log(`Loading hosted plugin: ${plugin.name}`); + private loadPlugin(pluginModel: PluginModel, pluginLifecycle: PluginLifecycle, container: interfaces.Container): void { + if (pluginModel.entryPoint!.frontend) { + console.log(`Loading hosted plugin: ${pluginModel.name}`); this.worker = new PluginWorker(); setUpPluginApi(this.worker.rpc, container); const hostedExtManager = this.worker.rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - hostedExtManager.$loadPlugin({ - pluginPath: plugin.theiaPlugin.worker!, - name: plugin.name, - publisher: plugin.publisher, - version: plugin.version - }); + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.frontend!, + model: pluginModel, + lifecycle: pluginLifecycle + }; + if (pluginLifecycle.frontendInitPath) { + hostedExtManager.$initialize(pluginLifecycle.frontendInitPath); + } + hostedExtManager.$loadPlugin(plugin); } - if (plugin.theiaPlugin!.node) { + if (pluginModel.entryPoint!.backend) { const rpc = this.createServerRpc(); setUpPluginApi(rpc, container); const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - hostedExtManager.$loadPlugin({ - pluginPath: plugin.theiaPlugin.node!, - name: plugin.name, - publisher: plugin.publisher, - version: plugin.version - }); + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.backend!, + model: pluginModel, + lifecycle: pluginLifecycle + }; + if (pluginLifecycle.backendInitPath) { + hostedExtManager.$initialize(pluginLifecycle.backendInitPath); + } + hostedExtManager.$loadPlugin(plugin); } } diff --git a/packages/plugin/src/common/plugin-protocol.ts b/packages/plugin/src/common/plugin-protocol.ts index 2d4a0218346a7..cd14f31b295a9 100644 --- a/packages/plugin/src/common/plugin-protocol.ts +++ b/packages/plugin/src/common/plugin-protocol.ts @@ -5,14 +5,128 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { RPCProtocol } from '../api/rpc-protocol'; +import { Disposable } from '../plugin/types-impl'; export const hostedServicePath = '/services/hostedPlugin'; -export interface Plugin { +/** + * Plugin engine (API) type, i.e. 'theiaPlugin', 'vscode', etc. + */ +export type PluginEngine = string; + +/** + * This interface describes a package.json object. + */ +export interface PluginPackage { + name: string; + publisher: string; + version: string; + engines: { + [type in PluginEngine]: string; + }; + theiaPlugin?: { + frontend?: string; + backend?: string; + }; + main?: string; + displayName: string; + description: string; + contributes: {}; +} + +export const PluginScanner = Symbol('PluginScanner'); + +/** + * This scanner process package.json object and returns plugin metadata objects. + */ +export interface PluginScanner { + /** + * The type of plugin's API (engine name) + */ + apiType: PluginEngine; + + /** + * Creates plugin's model. + * + * @param {PluginPackage} plugin + * @returns {PluginModel} + */ + getModel(plugin: PluginPackage): PluginModel; + + /** + * Creates plugin's lifecycle. + * + * @returns {PluginLifecycle} + */ + getLifecycle(plugin: PluginPackage): PluginLifecycle; +} + +/** + * This interface describes a plugin model object, which is populated from package.json. + */ +export interface PluginModel { name: string; publisher: string; version: string; - theiaPlugin: { worker?: string, node?: string }; + displayName: string; + description: string; + engine: { + type: PluginEngine; + version: string; + }; + entryPoint: { + frontend?: string; + backend?: string; + }; +} + +/** + * This interface describes a plugin lifecycle object. + */ +export interface PluginLifecycle { + startMethod: string; + stopMethod: string; + /** + * Frontend module name, frontend plugin should expose this name. + */ + frontendModuleName?: string; + /** + * Path to the script which should do some initialization before frontend plugin is loaded. + */ + frontendInitPath?: string; + /** + * Path to the script which should do some initialization before backend plugin is loaded. + */ + backendInitPath?: string; +} + +/** + * The export function of initialization module of backend plugin. + */ +export interface BackendInitializationFn { + (rpc: RPCProtocol): void; +} + +export interface PluginContext { + subscriptions: Disposable[]; +} + +export interface ExtensionContext { + subscriptions: Disposable[]; +} + +export interface PluginMetadata { + model: PluginModel; + lifecycle: PluginLifecycle; +} + +export function getPluginId(plugin: PluginPackage | PluginModel): string { + return `${plugin.publisher}_${plugin.name}`; +} + +export function buildFrontendModuleName(plugin: PluginPackage | PluginModel): string { + return `${plugin.publisher}_${plugin.name}`.replace(/\W/g, '_'); } export const HostedPluginClient = Symbol('HostedPluginClient'); @@ -22,6 +136,6 @@ export interface HostedPluginClient { export const HostedPluginServer = Symbol('HostedPluginServer'); export interface HostedPluginServer extends JsonRpcServer { - getHostedPlugin(): Promise; + getHostedPlugin(): Promise; onMessage(message: string): Promise; } diff --git a/packages/plugin/src/node/context/backend-init-theia.ts b/packages/plugin/src/node/context/backend-init-theia.ts new file mode 100644 index 0000000000000..6c777916871fe --- /dev/null +++ b/packages/plugin/src/node/context/backend-init-theia.ts @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { BackendInitializationFn } from '../../common/plugin-protocol'; +import { createAPI } from '../../plugin/plugin-context'; + +export const doInitialization: BackendInitializationFn = (rpc: any) => { + const theia = createAPI(rpc); + + // add theia into global goal + const g = global as any; + g['theia'] = theia; + + const NODE_MODULE_NAMES = ['@theia/plugin', '@wiptheia/plugin']; + const module = require('module'); + + // add theia object as module into npm cache + NODE_MODULE_NAMES.forEach(moduleName => { + require.cache[moduleName] = { + id: moduleName, + filename: moduleName, + loaded: true, + exports: theia + }; + }); + + // save original resolve method + const internalResolve = module._resolveFilename; + + // if we try to resolve theia module, return the filename entry to use cache. + module._resolveFilename = (request: string, parent: {}) => { + if (NODE_MODULE_NAMES.indexOf(request) !== -1) { + return request; + } + const retVal = internalResolve(request, parent); + return retVal; + }; +}; diff --git a/packages/plugin/src/node/context/backend-init-vscode.ts b/packages/plugin/src/node/context/backend-init-vscode.ts new file mode 100644 index 0000000000000..ee7035268e9da --- /dev/null +++ b/packages/plugin/src/node/context/backend-init-vscode.ts @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { BackendInitializationFn } from '../../common/plugin-protocol'; +import { createAPI } from '../../plugin/plugin-context'; + +export const doInitialization: BackendInitializationFn = (rpc: any) => { + const module = require('module'); + const vscodeModuleName = 'vscode'; + const theia = createAPI(rpc); + + // add theia into global goal as 'vscode' + const g = global as any; + g[vscodeModuleName] = theia; + + // add vscode object as module into npm cache + require.cache[vscodeModuleName] = { + id: vscodeModuleName, + filename: vscodeModuleName, + loaded: true, + exports: g[vscodeModuleName] + }; + + // save original resolve method + const internalResolve = module._resolveFilename; + + // if we try to resolve vscode module, return the filename entry to use cache. + module._resolveFilename = (request: string, parent: {}) => { + if (vscodeModuleName === request) { + return request; + } + const retVal = internalResolve(request, parent); + return retVal; + }; +}; diff --git a/packages/plugin/src/node/hosted-plugin.ts b/packages/plugin/src/node/hosted-plugin.ts index ab9c0208d8fca..0b596f936bd5d 100644 --- a/packages/plugin/src/node/hosted-plugin.ts +++ b/packages/plugin/src/node/hosted-plugin.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as cp from "child_process"; import { injectable, inject } from "inversify"; -import { Plugin, HostedPluginClient } from '../common/plugin-protocol'; +import { HostedPluginClient, PluginModel } from '../common/plugin-protocol'; import { ILogger, ConnectionErrorHandler } from "@theia/core/lib/common"; import { Emitter } from '@theia/core/lib/common/event'; import { createIpcEnv } from "@theia/core/lib/node/messaging/ipc-protocol"; @@ -37,8 +37,8 @@ export class HostedPluginSupport { this.client = client; } - runPlugin(plugin: Plugin): void { - if (plugin.theiaPlugin.node) { + runPlugin(plugin: PluginModel): void { + if (plugin.entryPoint.backend) { this.runPluginServer(plugin); } } @@ -75,7 +75,7 @@ export class HostedPluginSupport { }); } - private runPluginServer(plugin: Plugin): void { + private runPluginServer(plugin: PluginModel): void { if (this.cp) { this.terminatePluginServer(this.cp); } diff --git a/packages/plugin/src/node/metadata-scanner.ts b/packages/plugin/src/node/metadata-scanner.ts new file mode 100644 index 0000000000000..6d5c41762b89c --- /dev/null +++ b/packages/plugin/src/node/metadata-scanner.ts @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, multiInject } from 'inversify'; +import { PluginPackage, PluginScanner, PluginMetadata } from '../common/plugin-protocol'; + +@injectable() +export class MetadataScanner { + private scanners: Map = new Map(); + + constructor( + @multiInject(PluginScanner) scanners: PluginScanner[] + ) { + scanners.forEach((scanner: PluginScanner) => { + this.scanners.set(scanner.apiType, scanner); + }); + } + + public getPluginMetadata(plugin: PluginPackage): PluginMetadata { + const scanner = this.getScanner(plugin); + return { + model: scanner.getModel(plugin), + lifecycle: scanner.getLifecycle(plugin) + }; + } + + /** + * Returns the first suitable scanner. + * + * @param {PluginPackage} plugin + * @returns {PluginScanner} + */ + private getScanner(plugin: PluginPackage): PluginScanner { + let scanner; + if (plugin && plugin.engines) { + const scanners = Object.keys(plugin.engines) + .filter((engineName: string) => this.scanners.has(engineName)) + .map((engineName: string) => this.scanners.get(engineName)); + + // get the first suitable scanner from the list + scanner = scanners[0]; + } + + if (!scanner) { + throw new Error('There is no suitable scanner found for ' + plugin.name); + } + + return scanner; + } +} diff --git a/packages/plugin/src/node/plugin-api-backend-module.ts b/packages/plugin/src/node/plugin-api-backend-module.ts index 475f04552189e..baa0609777329 100644 --- a/packages/plugin/src/node/plugin-api-backend-module.ts +++ b/packages/plugin/src/node/plugin-api-backend-module.ts @@ -8,16 +8,22 @@ import { ContainerModule } from "inversify"; import { ConnectionHandler, JsonRpcConnectionHandler } from "@theia/core/lib/common/messaging"; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; +import { MetadataScanner } from './metadata-scanner'; import { PluginApiContribution, HostedPluginServerImpl } from './plugin-service'; import { HostedPluginReader } from './plugin-reader'; -import { HostedPluginClient, HostedPluginServer, hostedServicePath } from '../common/plugin-protocol'; +import { HostedPluginClient, HostedPluginServer, hostedServicePath, PluginScanner } from '../common/plugin-protocol'; import { HostedPluginSupport } from './hosted-plugin'; +import { TheiaPluginScanner } from './scanners/scanner-theia'; +import { VsCodePluginScanner } from './scanners/scanner-vscode'; export default new ContainerModule(bind => { bind(PluginApiContribution).toSelf().inSingletonScope(); bind(HostedPluginReader).toSelf().inSingletonScope(); bind(HostedPluginServer).to(HostedPluginServerImpl).inSingletonScope(); bind(HostedPluginSupport).toSelf().inSingletonScope(); + bind(PluginScanner).to(TheiaPluginScanner).inSingletonScope(); + bind(PluginScanner).to(VsCodePluginScanner).inSingletonScope(); + bind(MetadataScanner).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toDynamicValue(ctx => ctx.container.get(PluginApiContribution)).inSingletonScope(); bind(BackendApplicationContribution).toDynamicValue(ctx => ctx.container.get(HostedPluginReader)).inSingletonScope(); diff --git a/packages/plugin/src/node/plugin-host.ts b/packages/plugin/src/node/plugin-host.ts index 7f8a9ad3df377..3386237798915 100644 --- a/packages/plugin/src/node/plugin-host.ts +++ b/packages/plugin/src/node/plugin-host.ts @@ -5,14 +5,15 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ +import { resolve } from 'path'; import { RPCProtocolImpl } from '../api/rpc-protocol'; import { Emitter } from '@theia/core/lib/common/event'; -import { createAPI, startExtension } from '../plugin/plugin-context'; +import { startPlugin } from '../plugin/plugin-context'; import { MAIN_RPC_CONTEXT } from '../api/plugin-api'; import { HostedPluginManagerExtImpl } from '../plugin/hosted-plugin-manager'; +import { Plugin } from '../api/plugin-api'; -const NODE_MODULE_NAMES = ['@theia/plugin', '@wiptheia/plugin']; -const plugins = new Array<() => void>(); +const plugins = new Map void>(); const emmitter = new Emitter(); const rpc = new RPCProtocolImpl({ @@ -28,51 +29,31 @@ process.on('message', (message: any) => { emmitter.fire(JSON.parse(message)); }); -const theia = createAPI(rpc); - -// add theia into global goal -const g = global as any; -g['theia'] = theia; - rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtImpl({ - loadPlugin(path: string): void { - console.log("Ext: load: " + path); - const module = require('module'); - - // add theia object as module into npm cache - NODE_MODULE_NAMES.forEach((moduleName) => { - require.cache[moduleName] = { - id: moduleName, - filename: moduleName, - loaded: true, - exports: theia - }; - }); - - // save original resolve method - const internalResolve = module._resolveFilename; - - // if we try to resolve theia module, return the filename entry to use cache. - module._resolveFilename = (request: string, parent: {}) => { - if (NODE_MODULE_NAMES.indexOf(request) !== -1) { - return request; - } - const retVal = internalResolve(request, parent); - return retVal; - }; + initialize(contextPath: string): void { + const backendInitPath = resolve(__dirname, 'context', contextPath); + const backendInit = require(backendInitPath); + backendInit.doInitialization(rpc); + }, + loadPlugin(plugin: Plugin): void { + console.log("Ext: load: " + plugin.pluginPath); try { - const plugin = require(path); - startExtension(plugin, plugins); + const pluginMain = require(plugin.pluginPath); + startPlugin(plugin, pluginMain, plugins); } catch (e) { console.error(e); } }, - stopPlugins(): void { - console.log("Plugin: Stopping plugins."); - for (const s of plugins) { - s(); - } + stopPlugins(pluginIds: string[]): void { + console.log("Plugin: Stopping plugin: ", pluginIds); + pluginIds.forEach(pluginId => { + const stopPluginMethod = plugins.get(pluginId); + if (stopPluginMethod) { + stopPluginMethod(); + plugins.delete(pluginId); + } + }); } })); diff --git a/packages/plugin/src/node/plugin-reader.ts b/packages/plugin/src/node/plugin-reader.ts index 7181834ed213b..85e8564376b9a 100644 --- a/packages/plugin/src/node/plugin-reader.ts +++ b/packages/plugin/src/node/plugin-reader.ts @@ -5,15 +5,17 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { injectable } from "inversify"; +import { inject, injectable } from "inversify"; import * as express from 'express'; import * as fs from 'fs'; -import { Plugin } from '../common/plugin-protocol'; import { resolve } from 'path'; +import { PluginPackage, PluginMetadata } from '../common/plugin-protocol'; +import { MetadataScanner } from './metadata-scanner'; @injectable() export class HostedPluginReader implements BackendApplicationContribution { - private plugin: Plugin | undefined; + @inject(MetadataScanner) private readonly scanner: MetadataScanner; + private plugin: PluginMetadata | undefined; private pluginPath: string; initialize(): void { @@ -42,17 +44,18 @@ export class HostedPluginReader implements BackendApplicationContribution { } const packageJsonPath = path + 'package.json'; if (fs.existsSync(packageJsonPath)) { - const plugin: Plugin = require(packageJsonPath); - this.plugin = plugin; - if (plugin.theiaPlugin.node) { - plugin.theiaPlugin.node = resolve(path, plugin.theiaPlugin.node); + const plugin: PluginPackage = require(packageJsonPath); + this.plugin = this.scanner.getPluginMetadata(plugin); + + if (this.plugin.model.entryPoint.backend) { + this.plugin.model.entryPoint.backend = resolve(path, this.plugin.model.entryPoint.backend); } } else { this.plugin = undefined; } } - getPlugin(): Plugin | undefined { + getPlugin(): PluginMetadata | undefined { return this.plugin; } } diff --git a/packages/plugin/src/node/plugin-service.ts b/packages/plugin/src/node/plugin-service.ts index 5b127af9c15bc..aa80894cd80a1 100644 --- a/packages/plugin/src/node/plugin-service.ts +++ b/packages/plugin/src/node/plugin-service.ts @@ -7,7 +7,7 @@ import * as express from 'express'; import { injectable, inject } from "inversify"; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { HostedPluginServer, HostedPluginClient, Plugin } from '../common/plugin-protocol'; +import { HostedPluginServer, HostedPluginClient, PluginMetadata } from '../common/plugin-protocol'; import { HostedPluginReader } from './plugin-reader'; import { HostedPluginSupport } from './hosted-plugin'; @@ -36,10 +36,10 @@ export class HostedPluginServerImpl implements HostedPluginServer { setClient(client: HostedPluginClient): void { this.hostedPlugin.setClient(client); } - getHostedPlugin(): Promise { - const ext = this.reader.getPlugin(); - if (ext) { - this.hostedPlugin.runPlugin(ext); + getHostedPlugin(): Promise { + const pluginMetadata = this.reader.getPlugin(); + if (pluginMetadata) { + this.hostedPlugin.runPlugin(pluginMetadata.model); } return Promise.resolve(this.reader.getPlugin()); } diff --git a/packages/plugin/src/node/scanners/scanner-theia.ts b/packages/plugin/src/node/scanners/scanner-theia.ts new file mode 100644 index 0000000000000..8de2bbc86b5c4 --- /dev/null +++ b/packages/plugin/src/node/scanners/scanner-theia.ts @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable } from 'inversify'; +import { + PluginEngine, + PluginModel, + PluginPackage, + PluginScanner, + PluginLifecycle, + buildFrontendModuleName +} from '../../common/plugin-protocol'; + +@injectable() +export class TheiaPluginScanner implements PluginScanner { + private readonly _apiType: PluginEngine = 'theiaPlugin'; + + get apiType(): PluginEngine { + return this._apiType; + } + + getModel(plugin: PluginPackage): PluginModel { + return { + name: plugin.name, + publisher: plugin.publisher, + version: plugin.version, + displayName: plugin.displayName, + description: plugin.description, + engine: { + type: this._apiType, + version: plugin.engines[this._apiType] + }, + entryPoint: { + frontend: plugin.theiaPlugin!.frontend, + backend: plugin.theiaPlugin!.backend + } + }; + } + + getLifecycle(plugin: PluginPackage): PluginLifecycle { + return { + startMethod: 'start', + stopMethod: 'stop', + frontendModuleName: buildFrontendModuleName(plugin), + + backendInitPath: 'backend-init-theia.js' + }; + } +} diff --git a/packages/plugin/src/node/scanners/scanner-vscode.ts b/packages/plugin/src/node/scanners/scanner-vscode.ts new file mode 100644 index 0000000000000..a641f35d41809 --- /dev/null +++ b/packages/plugin/src/node/scanners/scanner-vscode.ts @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable } from 'inversify'; +import { PluginEngine, PluginModel, PluginPackage, PluginScanner, PluginLifecycle } from '../../common/plugin-protocol'; + +@injectable() +export class VsCodePluginScanner implements PluginScanner { + private readonly _apiType: PluginEngine = 'vscode'; + + get apiType(): PluginEngine { + return this._apiType; + } + + getModel(plugin: PluginPackage): PluginModel { + return { + name: plugin.name, + publisher: plugin.publisher, + version: plugin.version, + displayName: plugin.displayName, + description: plugin.description, + engine: { + type: this._apiType, + version: plugin.engines[this._apiType] + }, + entryPoint: { + backend: plugin.main + } + }; + } + + getLifecycle(plugin: PluginPackage): PluginLifecycle { + return { + startMethod: 'activate', + stopMethod: 'deactivate', + + backendInitPath: 'backend-init-vscode.js' + }; + } +} diff --git a/packages/plugin/src/plugin/hosted-plugin-manager.ts b/packages/plugin/src/plugin/hosted-plugin-manager.ts index 3f543561d95a9..9bebbbc357ec9 100644 --- a/packages/plugin/src/plugin/hosted-plugin-manager.ts +++ b/packages/plugin/src/plugin/hosted-plugin-manager.ts @@ -5,24 +5,35 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import { HostedPluginManagerExt, Plugin } from '../api/plugin-api'; +import { getPluginId } from '../common/plugin-protocol'; export interface PluginHost { - loadPlugin(scriptPath: string): void; + initialize(contextPath: string): void; - stopPlugins(): void; + loadPlugin(plugin: Plugin): void; + + stopPlugins(pluginIds: string[]): void; } export class HostedPluginManagerExtImpl implements HostedPluginManagerExt { + private runningPluginIds: string[]; + constructor(private readonly host: PluginHost) { + this.runningPluginIds = []; + } + + $initialize(contextPath: string): void { + this.host.initialize(contextPath); } - $loadPlugin(ext: Plugin): void { - this.host.loadPlugin(ext.pluginPath); + $loadPlugin(plugin: Plugin): void { + this.runningPluginIds.push(getPluginId(plugin.model)); + this.host.loadPlugin(plugin); } $stopPlugin(): PromiseLike { - this.host.stopPlugins(); + this.host.stopPlugins(this.runningPluginIds); return Promise.resolve(); } diff --git a/packages/plugin/src/plugin/plugin-context.ts b/packages/plugin/src/plugin/plugin-context.ts index abd5b08534a40..e8baf0e9e5e68 100644 --- a/packages/plugin/src/plugin/plugin-context.ts +++ b/packages/plugin/src/plugin/plugin-context.ts @@ -12,6 +12,8 @@ import { Disposable } from './types-impl'; import { Emitter } from '@theia/core/lib/common/event'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { QuickOpenExtImpl } from './quick-open'; +import { Plugin } from '../api/plugin-api'; +import { getPluginId } from '../common/plugin-protocol'; export function createAPI(rpc: RPCProtocol): typeof theia { const commandRegistryExt = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); @@ -49,14 +51,15 @@ export function createAPI(rpc: RPCProtocol): typeof theia { } -export function startExtension(plugin: any, plugins: Array<() => void>): void { - if (typeof plugin.doStartThings === 'function') { - plugin.doStartThings.apply(global, []); +export function startPlugin(plugin: Plugin, pluginMain: any, plugins: Map void>): void { + if (typeof pluginMain[plugin.lifecycle.startMethod] === 'function') { + pluginMain[plugin.lifecycle.startMethod].apply(global, []); } else { console.log('there is no doStart method on plugin'); } - if (typeof plugin.doStopThings === 'function') { - plugins.push(plugin.doStopThings); + if (typeof pluginMain[plugin.lifecycle.stopMethod] === 'function') { + const pluginId = getPluginId(plugin.model); + plugins.set(pluginId, pluginMain[plugin.lifecycle.stopMethod]); } } diff --git a/packages/plugin/src/worker/worker-main.ts b/packages/plugin/src/worker/worker-main.ts index db4a4d6778a56..84b79ff9a4fe2 100644 --- a/packages/plugin/src/worker/worker-main.ts +++ b/packages/plugin/src/worker/worker-main.ts @@ -7,12 +7,13 @@ import { RPCProtocolImpl } from '../api/rpc-protocol'; import { Emitter } from '@theia/core/lib/common/event'; -import { createAPI, startExtension } from '../plugin/plugin-context'; +import { createAPI, startPlugin } from '../plugin/plugin-context'; import { MAIN_RPC_CONTEXT } from '../api/plugin-api'; import { HostedPluginManagerExtImpl } from '../plugin/hosted-plugin-manager'; +import { Plugin } from '../api/plugin-api'; const ctx = self as any; -const plugins = new Array<() => void>(); +const plugins = new Map void>(); const emmitter = new Emitter(); const rpc = new RPCProtocolImpl({ @@ -29,14 +30,26 @@ const theia = createAPI(rpc); ctx['theia'] = theia; rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, new HostedPluginManagerExtImpl({ - loadPlugin(path: string): void { - ctx.importScripts('/hostedPlugin/' + path); - // FIXME: simplePlugin should come from metadata - startExtension(ctx['simplePlugin'], plugins); + initialize(contextPath: string): void { + ctx.importScripts('/context/' + contextPath); }, - stopPlugins(): void { - for (const s of plugins) { - s(); + loadPlugin(plugin: Plugin): void { + ctx.importScripts('/hostedPlugin/' + plugin.pluginPath); + if (plugin.lifecycle.frontendModuleName) { + if (!ctx[plugin.lifecycle.frontendModuleName]) { + console.error(`WebWorker: Cannot start plugin "${plugin.model.name}". Frontend plugin not found: "${plugin.lifecycle.frontendModuleName}"`); + return; + } + startPlugin(plugin, ctx[plugin.lifecycle.frontendModuleName], plugins); } + }, + stopPlugins(pluginIds: string[]): void { + pluginIds.forEach(pluginId => { + const stopPluginMethod = plugins.get(pluginId); + if (stopPluginMethod) { + stopPluginMethod(); + plugins.delete(pluginId); + } + }); } }));