Skip to content

Commit

Permalink
fix #3965: isolate plugin deployment and fix localization
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Jan 14, 2019
1 parent d1410f7 commit 0c7fb6a
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 77 deletions.
142 changes: 95 additions & 47 deletions packages/plugin-ext/src/hosted/node/plugin-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PluginMetadata | undefined>();

@optional()
@multiInject(MetadataProcessor) private readonly metadataProcessors: MetadataProcessor[];
Expand All @@ -40,15 +42,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 @@ -66,55 +61,108 @@ export class HostedPluginReader implements BackendApplicationContribution {
});
}

public getPluginMetadata(path: string): PluginMetadata | undefined {
if (!path.endsWith('/')) {
path += '/';
async getPluginMetadata(pluginPath: string | undefined): 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) {
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<PluginMetadata | undefined> {
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) {
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 loadnManifest(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;

}
40 changes: 16 additions & 24 deletions packages/plugin-ext/src/hosted/node/plugin-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -58,8 +55,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 +75,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);
console.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);
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<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

0 comments on commit 0c7fb6a

Please sign in to comment.