diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index 9dc623f6662bd..dc1fec70e41e1 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -13,23 +13,25 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { inject, injectable, optional, multiInject } from 'inversify'; + + // tslint:disable:no-any + +import * as path from 'path'; +import * as fs from 'fs-extra'; import * as express from 'express'; -import * as fs from 'fs'; -import { resolve } from 'path'; +import { inject, injectable, optional, multiInject } from 'inversify'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; +import { PluginMetadata, getPluginId, MetadataProcessor } from '../../common/plugin-protocol'; import { MetadataScanner } from './metadata-scanner'; -import { PluginMetadata, PluginPackage, getPluginId, MetadataProcessor } from '../../common/plugin-protocol'; -import { ILogger } from '@theia/core'; @injectable() export class HostedPluginReader implements BackendApplicationContribution { - @inject(ILogger) - protected readonly logger: ILogger; + @inject(MetadataScanner) + private readonly scanner: MetadataScanner; - @inject(MetadataScanner) private readonly scanner: MetadataScanner; - private plugin: PluginMetadata | undefined; + private readonly hostedPlugin = new Deferred(); @optional() @multiInject(MetadataProcessor) private readonly metadataProcessors: MetadataProcessor[]; @@ -40,15 +42,8 @@ export class HostedPluginReader implements BackendApplicationContribution { private pluginsIdsFiles: Map = new Map(); initialize(): void { - if (process.env.HOSTED_PLUGIN) { - let pluginPath = process.env.HOSTED_PLUGIN; - if (pluginPath) { - if (!pluginPath.endsWith('/')) { - pluginPath += '/'; - } - this.plugin = this.getPluginMetadata(pluginPath); - } - } + this.doGetPluginMetadata(process.env.HOSTED_PLUGIN) + .then(this.hostedPlugin.resolve.bind(this.hostedPlugin)); } configure(app: express.Application): void { @@ -66,25 +61,46 @@ export class HostedPluginReader implements BackendApplicationContribution { }); } - public getPluginMetadata(path: string): PluginMetadata | undefined { - if (!path.endsWith('/')) { - path += '/'; + async getPluginMetadata(pluginPath: string | undefined): Promise { + const plugin = await this.doGetPluginMetadata(pluginPath); + if (plugin) { + const hostedPlugin = await this.getPlugin(); + if (hostedPlugin && hostedPlugin.model.name === plugin.model.name) { + // prefer hosted plugin + return undefined; + } } - const packageJsonPath = path + 'package.json'; - if (!fs.existsSync(packageJsonPath)) { + return plugin; + } + + /** + * MUST never throw to isolate plugin deployment + */ + protected async doGetPluginMetadata(pluginPath: string | undefined) { + try { + if (!pluginPath) { + return undefined; + } + if (!pluginPath.endsWith('/')) { + pluginPath += '/'; + } + return await this.loadPluginMetadata(pluginPath); + } catch (e) { + console.error(`Failed to load plugin metadata from "${pluginPath}"`, e); return undefined; } + } - let rawData = fs.readFileSync(packageJsonPath).toString(); - rawData = this.localize(rawData, path); - - const plugin: PluginPackage = JSON.parse(rawData); - plugin.packagePath = path; - const pluginMetadata = this.scanner.getPluginMetadata(plugin); + protected async loadPluginMetadata(pluginPath: string): Promise { + const manifest = await this.loadnManifest(pluginPath); + if (!manifest) { + return undefined; + } + manifest.packagePath = pluginPath; + const pluginMetadata = this.scanner.getPluginMetadata(manifest); if (pluginMetadata.model.entryPoint.backend) { - pluginMetadata.model.entryPoint.backend = resolve(path, pluginMetadata.model.entryPoint.backend); + pluginMetadata.model.entryPoint.backend = path.resolve(pluginPath, pluginMetadata.model.entryPoint.backend); } - if (pluginMetadata) { // Add post processor if (this.metadataProcessors) { @@ -92,29 +108,61 @@ export class HostedPluginReader implements BackendApplicationContribution { metadataProcessor.process(pluginMetadata); }); } - this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), path); + this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), pluginPath); } - return pluginMetadata; } - private localize(rawData: string, pluginPath: string): string { - const nlsPath = pluginPath + 'package.nls.json'; - if (fs.existsSync(nlsPath)) { - const nlsMap: { - [key: string]: string - } = require(nlsPath); - for (const key of Object.keys(nlsMap)) { - const value = nlsMap[key].replace(/\"/g, '\\"'); - rawData = rawData.split('%' + key + '%').join(value); + async getPlugin(): Promise { + return this.hostedPlugin.promise; + } + + protected async loadnManifest(pluginPath: string): Promise { + const [manifest, translations] = await Promise.all([ + fs.readJson(path.join(pluginPath, 'package.json')), + this.loadTranslations(pluginPath) + ]); + return manifest && translations && Object.keys(translations).length ? + this.localize(manifest, translations) : + manifest; + } + + protected async loadTranslations(pluginPath: string): Promise { + try { + return await fs.readJson(path.join(pluginPath, 'package.nls.json')); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; } + return {}; } - - return rawData; } - getPlugin(): PluginMetadata | undefined { - return this.plugin; + protected localize(value: any, translations: { + [key: string]: string + }): any { + if (typeof value === 'string') { + const match = HostedPluginReader.NLS_REGEX.exec(value); + return match && translations[match[1]] || value; + } + if (Array.isArray(value)) { + const result = []; + for (const item of value) { + result.push(this.localize(item, translations)); + } + return result; + } + if (typeof value === 'object') { + const result: { [key: string]: any } = {}; + // tslint:disable-next-line:forin + for (const propertyName in value) { + result[propertyName] = this.localize(value[propertyName], translations); + } + return result; + } + return value; } + static NLS_REGEX = /^%([\w\d.-]+)%$/i; + } diff --git a/packages/plugin-ext/src/hosted/node/plugin-service.ts b/packages/plugin-ext/src/hosted/node/plugin-service.ts index b3ae7da8163ee..8de8d4fc76008 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-service.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-service.ts @@ -20,15 +20,12 @@ import { HostedInstanceManager } from './hosted-instance-manager'; import { HostedPluginSupport } from './hosted-plugin'; import { HostedPluginsManager } from './hosted-plugins-manager'; import URI from '@theia/core/lib/common/uri'; -import { ILogger } from '@theia/core'; import { ContributionProvider } from '@theia/core'; import { ExtPluginApiProvider, ExtPluginApi } from '../../common/plugin-ext-api-contribution'; @injectable() export class HostedPluginServerImpl implements HostedPluginServer { - @inject(ILogger) - protected readonly logger: ILogger; @inject(HostedPluginsManager) protected readonly hostedPluginsManager: HostedPluginsManager; @@ -58,8 +55,8 @@ export class HostedPluginServerImpl implements HostedPluginServer { setClient(client: HostedPluginClient): void { this.hostedPlugin.setClient(client); } - getHostedPlugin(): Promise { - const pluginMetadata = this.reader.getPlugin(); + async getHostedPlugin(): Promise { + const pluginMetadata = await this.reader.getPlugin(); if (pluginMetadata) { this.hostedPlugin.runPlugin(pluginMetadata.model); } @@ -78,16 +75,14 @@ export class HostedPluginServerImpl implements HostedPluginServer { } // need to run a new node instance with plugin-host for all plugins - deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise { - // get metadata - frontendPlugins.forEach(frontendPluginDeployerEntry => { - const pluginMetadata = this.reader.getPluginMetadata(frontendPluginDeployerEntry.path()); - if (pluginMetadata) { - this.currentFrontendPluginsMetadata.push(pluginMetadata); - this.logger.info('HostedPluginServerImpl/ asking to deploy the frontend Plugin', frontendPluginDeployerEntry.path(), 'and model is', pluginMetadata.model); + async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise { + for (const plugin of frontendPlugins) { + const metadata = await this.reader.getPluginMetadata(plugin.path()); + if (metadata) { + this.currentFrontendPluginsMetadata.push(metadata); + console.info(`Deploying frontend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.frontend || plugin.path()}"`); } - }); - return Promise.resolve(); + } } getDeployedBackendMetadata(): Promise { @@ -95,20 +90,17 @@ export class HostedPluginServerImpl implements HostedPluginServer { } // need to run a new node instance with plugin-host for all plugins - deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise { + async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise { if (backendPlugins.length > 0) { this.hostedPlugin.runPluginServer(); } - - // get metadata - backendPlugins.forEach(backendPluginDeployerEntry => { - const pluginMetadata = this.reader.getPluginMetadata(backendPluginDeployerEntry.path()); - if (pluginMetadata) { - this.currentBackendPluginsMetadata.push(pluginMetadata); - this.logger.info('HostedPluginServerImpl/ asking to deploy the backend Plugin', backendPluginDeployerEntry.path(), 'and model is', pluginMetadata.model); + for (const plugin of backendPlugins) { + const metadata = await this.reader.getPluginMetadata(plugin.path()); + if (metadata) { + this.currentBackendPluginsMetadata.push(metadata); + console.info(`Deploying backend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.backend || plugin.path()}"`); } - }); - return Promise.resolve(); + } } onMessage(message: string): Promise { diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts index 6da0ab37b6aca..2a182583b6e35 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -130,7 +130,7 @@ export class PluginDeployerImpl implements PluginDeployer { /** * deploy all plugins that have been accepted */ - public async deployPlugins(): Promise { + async deployPlugins(): Promise { const acceptedPlugins = this.pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted()); const acceptedFrontendPlugins = this.pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.FRONTEND)); const acceptedBackendPlugins = this.pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.BACKEND)); @@ -147,11 +147,11 @@ export class PluginDeployerImpl implements PluginDeployer { const pluginPaths = acceptedBackendPlugins.map(pluginEntry => pluginEntry.path()); this.logger.debug('local path to deploy on remote instance', pluginPaths); - // start the backend plugins - this.hostedPluginServer.deployBackendPlugins(acceptedBackendPlugins); - this.hostedPluginServer.deployFrontendPlugins(acceptedFrontendPlugins); - return Promise.resolve(); - + await Promise.all([ + // start the backend plugins + this.hostedPluginServer.deployBackendPlugins(acceptedBackendPlugins), + this.hostedPluginServer.deployFrontendPlugins(acceptedFrontendPlugins) + ]); } /**