Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #3965: isolate plugin deployment and fix localization #4049

Merged
merged 1 commit into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 92 additions & 46 deletions packages/plugin-ext/src/hosted/node/plugin-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@
*
* 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 { MetadataScanner } from './metadata-scanner';
import { PluginMetadata, PluginPackage, getPluginId, MetadataProcessor } from '../../common/plugin-protocol';
import { ILogger } from '@theia/core';
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';

@injectable()
export class HostedPluginReader implements BackendApplicationContribution {
Expand All @@ -31,7 +35,7 @@ export class HostedPluginReader implements BackendApplicationContribution {
@inject(MetadataScanner)
private readonly scanner: MetadataScanner;

private plugin: PluginMetadata | undefined;
private readonly hostedPlugin = new Deferred<PluginMetadata | undefined>();

@optional()
@multiInject(MetadataProcessor) private readonly metadataProcessors: MetadataProcessor[];
Expand All @@ -42,15 +46,8 @@ export class HostedPluginReader implements BackendApplicationContribution {
private pluginsIdsFiles: Map<string, string> = 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 {
Expand All @@ -68,59 +65,108 @@ export class HostedPluginReader implements BackendApplicationContribution {
});
}

getPluginMetadata(path: string): PluginMetadata | undefined {
if (!path.endsWith('/')) {
path += '/';
async getPluginMetadata(pluginPath: string): Promise<PluginMetadata | undefined> {
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) {
this.logger.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);
if (this.plugin && this.plugin.model && this.plugin.model.name === pluginMetadata.model.name) {
// prefer hosted plugin
protected async loadPluginMetadata(pluginPath: string): Promise<PluginMetadata | undefined> {
const manifest = await this.loadManifest(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) {
this.metadataProcessors.forEach(metadataProcessor => {
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<PluginMetadata | undefined> {
return this.hostedPlugin.promise;
}

protected async loadManifest(pluginPath: string): Promise<any> {
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<any> {
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;

}
38 changes: 16 additions & 22 deletions packages/plugin-ext/src/hosted/node/plugin-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { ExtPluginApiProvider, ExtPluginApi } from '../../common/plugin-ext-api-

@injectable()
export class HostedPluginServerImpl implements HostedPluginServer {

@inject(ILogger)
protected readonly logger: ILogger;
@inject(HostedPluginsManager)
Expand Down Expand Up @@ -58,8 +57,8 @@ export class HostedPluginServerImpl implements HostedPluginServer {
setClient(client: HostedPluginClient): void {
this.hostedPlugin.setClient(client);
}
getHostedPlugin(): Promise<PluginMetadata | undefined> {
const pluginMetadata = this.reader.getPlugin();
async getHostedPlugin(): Promise<PluginMetadata | undefined> {
const pluginMetadata = await this.reader.getPlugin();
if (pluginMetadata) {
this.hostedPlugin.runPlugin(pluginMetadata.model);
}
Expand All @@ -78,37 +77,32 @@ export class HostedPluginServerImpl implements HostedPluginServer {
}

// need to run a new node instance with plugin-host for all plugins
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void> {
// 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<void> {
for (const plugin of frontendPlugins) {
const metadata = await this.reader.getPluginMetadata(plugin.path());
if (metadata) {
this.currentFrontendPluginsMetadata.push(metadata);
this.logger.info(`Deploying frontend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.frontend || plugin.path()}"`);
}
});
return Promise.resolve();
}
}

getDeployedBackendMetadata(): Promise<PluginMetadata[]> {
return Promise.resolve(this.currentBackendPluginsMetadata);
}

// need to run a new node instance with plugin-host for all plugins
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void> {
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void> {
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);
this.logger.info(`Deploying backend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.backend || plugin.path()}"`);
}
});
return Promise.resolve();
}
}

onMessage(message: string): Promise<void> {
Expand Down
12 changes: 6 additions & 6 deletions packages/plugin-ext/src/main/node/plugin-deployer-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class PluginDeployerImpl implements PluginDeployer {
/**
* deploy all plugins that have been accepted
*/
public async deployPlugins(): Promise<any> {
async deployPlugins(): Promise<any> {
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));
Expand All @@ -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)
]);
}

/**
Expand Down