mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-24 03:35:38 +00:00
428 lines
13 KiB
TypeScript
428 lines
13 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import {disposeAll} from 'vs/base/common/lifecycle';
|
|
import {IDisposable} from 'vs/base/common/lifecycle';
|
|
import * as paths from 'vs/base/common/paths';
|
|
import Severity from 'vs/base/common/severity';
|
|
import {TPromise} from 'vs/base/common/winjs.base';
|
|
import {AbstractPluginService, ActivatedPlugin, IPluginContext, IPluginMemento} from 'vs/platform/extensions/common/abstractPluginService';
|
|
import {IMessage, IPluginDescription, IPluginStatus} from 'vs/platform/extensions/common/plugins';
|
|
import {PluginsRegistry} from 'vs/platform/extensions/common/pluginsRegistry';
|
|
import {IMessageService} from 'vs/platform/message/common/message';
|
|
import {PluginHostStorage} from 'vs/platform/storage/common/remotable.storage';
|
|
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
|
|
import {IThreadService, Remotable} from 'vs/platform/thread/common/thread';
|
|
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
|
|
|
|
const hasOwnProperty = Object.hasOwnProperty;
|
|
|
|
class PluginMemento implements IPluginMemento {
|
|
|
|
private _id: string;
|
|
private _shared: boolean;
|
|
private _storage: PluginHostStorage;
|
|
|
|
private _init: TPromise<PluginMemento>;
|
|
private _value: { [n: string]: any; };
|
|
|
|
constructor(id: string, global: boolean, storage: PluginHostStorage) {
|
|
this._id = id;
|
|
this._shared = global;
|
|
this._storage = storage;
|
|
|
|
this._init = this._storage.getValue(this._shared, this._id, Object.create(null)).then(value => {
|
|
this._value = value;
|
|
return this;
|
|
});
|
|
}
|
|
|
|
get whenReady(): TPromise<PluginMemento> {
|
|
return this._init;
|
|
}
|
|
|
|
get<T>(key: string, defaultValue: T): T {
|
|
let value = this._value[key];
|
|
if (typeof value === 'undefined') {
|
|
value = defaultValue;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
update(key: string, value: any): Thenable<boolean> {
|
|
this._value[key] = value;
|
|
return this._storage
|
|
.setValue(this._shared, this._id, this._value)
|
|
.then(() => true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a failed extension in the ext host.
|
|
*/
|
|
export class MainProcessFailedPlugin extends ActivatedPlugin {
|
|
constructor() {
|
|
super(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an extension that was successfully loaded or an
|
|
* empty extension in the ext host.
|
|
*/
|
|
export class MainProcessSuccessPlugin extends ActivatedPlugin {
|
|
constructor() {
|
|
super(false);
|
|
}
|
|
}
|
|
|
|
|
|
@Remotable.MainContext('MainProcessPluginService')
|
|
export class MainProcessPluginService extends AbstractPluginService<ActivatedPlugin> {
|
|
|
|
private _threadService: IThreadService;
|
|
private _messageService: IMessageService;
|
|
private _telemetryService: ITelemetryService;
|
|
private _proxy: PluginHostPluginService;
|
|
private _isDev: boolean;
|
|
private _pluginsStatus: { [id: string]: IPluginStatus };
|
|
|
|
/**
|
|
* This class is constructed manually because it is a service, so it doesn't use any ctor injection
|
|
*/
|
|
constructor(
|
|
contextService: IWorkspaceContextService,
|
|
threadService: IThreadService,
|
|
messageService: IMessageService,
|
|
telemetryService: ITelemetryService
|
|
) {
|
|
super(false);
|
|
let config = contextService.getConfiguration();
|
|
this._isDev = !config.env.isBuilt || !!config.env.pluginDevelopmentPath;
|
|
|
|
this._messageService = messageService;
|
|
threadService.registerRemotableInstance(MainProcessPluginService, this);
|
|
this._threadService = threadService;
|
|
this._telemetryService = telemetryService;
|
|
this._proxy = this._threadService.getRemotable(PluginHostPluginService);
|
|
this._pluginsStatus = {};
|
|
|
|
PluginsRegistry.handleExtensionPoints((severity, source, message) => {
|
|
this.showMessage(severity, source, message);
|
|
});
|
|
}
|
|
|
|
protected _createFailedPlugin() {
|
|
return new MainProcessFailedPlugin();
|
|
}
|
|
|
|
private getTelemetryActivationEvent(pluginDescription: IPluginDescription): any {
|
|
let event = {
|
|
id: pluginDescription.id,
|
|
name: pluginDescription.name,
|
|
publisherDisplayName: pluginDescription.publisher,
|
|
activationEvents: pluginDescription.activationEvents ? pluginDescription.activationEvents.join(',') : null
|
|
};
|
|
|
|
for (let contribution in pluginDescription.contributes) {
|
|
let contributionDetails = pluginDescription.contributes[contribution];
|
|
|
|
if (!contributionDetails) {
|
|
continue;
|
|
}
|
|
|
|
switch (contribution) {
|
|
case 'debuggers':
|
|
let types = contributionDetails.reduce((p, c) => p ? p + ',' + c['type'] : c['type'], '');
|
|
event['contribution.debuggers'] = types;
|
|
break;
|
|
case 'grammars':
|
|
let grammers = contributionDetails.reduce((p, c) => p ? p + ',' + c['language'] : c['language'], '');
|
|
event['contribution.grammars'] = grammers;
|
|
break;
|
|
case 'languages':
|
|
let languages = contributionDetails.reduce((p, c) => p ? p + ',' + c['id'] : c['id'], '');
|
|
event['contribution.languages'] = languages;
|
|
break;
|
|
case 'tmSnippets':
|
|
let tmSnippets = contributionDetails.reduce((p, c) => p ? p + ',' + c['languageId'] : c['languageId'], '');
|
|
event['contribution.tmSnippets'] = tmSnippets;
|
|
break;
|
|
default:
|
|
event[`contribution.${contribution}`] = true;
|
|
}
|
|
}
|
|
|
|
return event;
|
|
}
|
|
|
|
protected _showMessage(severity: Severity, msg: string): void {
|
|
this._proxy.$doShowMessage(severity, msg);
|
|
this.$doShowMessage(severity, msg);
|
|
}
|
|
|
|
public showMessage(severity: Severity, source: string, message: string) {
|
|
super.showMessage(severity, source, message);
|
|
if (!this._pluginsStatus[source]) {
|
|
this._pluginsStatus[source] = { messages: [] };
|
|
}
|
|
this._pluginsStatus[source].messages.push({ type: severity, source, message });
|
|
}
|
|
|
|
public $doShowMessage(severity: Severity, msg: string): void {
|
|
let messageShown = false;
|
|
if (severity === Severity.Error || severity === Severity.Warning) {
|
|
if (this._isDev) {
|
|
// Only show nasty intrusive messages if doing extension development.
|
|
this._messageService.show(severity, msg);
|
|
messageShown = true;
|
|
}
|
|
}
|
|
|
|
if (!messageShown) {
|
|
switch (severity) {
|
|
case Severity.Error:
|
|
console.error(msg);
|
|
break;
|
|
case Severity.Warning:
|
|
console.warn(msg);
|
|
break;
|
|
default:
|
|
console.log(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
public getPluginsStatus(): { [id: string]: IPluginStatus } {
|
|
return this._pluginsStatus;
|
|
}
|
|
|
|
public deactivate(pluginId: string): void {
|
|
this._proxy.deactivate(pluginId);
|
|
}
|
|
|
|
// -- overwriting AbstractPluginService
|
|
|
|
protected _actualActivatePlugin(pluginDescription: IPluginDescription): TPromise<ActivatedPlugin> {
|
|
let event = this.getTelemetryActivationEvent(pluginDescription);
|
|
this._telemetryService.publicLog('activatePlugin', event);
|
|
// redirect plugin activation to the plugin host
|
|
return this._proxy.$activatePluginInPluginHost(pluginDescription).then(_ => {
|
|
// the plugin host calls $onPluginActivatedInPluginHost, where we write to `activatedPlugins`
|
|
return this.activatedPlugins[pluginDescription.id];
|
|
});
|
|
}
|
|
|
|
// -- called by plugin host
|
|
|
|
public $onPluginHostReady(pluginDescriptions: IPluginDescription[], messages: IMessage[]): void {
|
|
PluginsRegistry.registerPlugins(pluginDescriptions);
|
|
this.registrationDone(messages);
|
|
}
|
|
|
|
public $onPluginActivatedInPluginHost(pluginId: string): void {
|
|
this.activatedPlugins[pluginId] = new MainProcessSuccessPlugin();
|
|
}
|
|
|
|
public $onPluginActivationFailedInPluginHost(pluginId: string): void {
|
|
this.activatedPlugins[pluginId] = new MainProcessFailedPlugin();
|
|
}
|
|
}
|
|
|
|
export interface IPluginModule {
|
|
activate(ctx: IPluginContext): TPromise<IPluginExports>;
|
|
deactivate(): void;
|
|
}
|
|
|
|
export interface IPluginExports {
|
|
// _pluginExportsBrand: any;
|
|
}
|
|
|
|
export class ExtHostPlugin extends ActivatedPlugin {
|
|
|
|
module: IPluginModule;
|
|
exports: IPluginExports;
|
|
subscriptions: IDisposable[];
|
|
|
|
constructor(activationFailed: boolean, module: IPluginModule, exports: IPluginExports, subscriptions: IDisposable[]) {
|
|
super(activationFailed);
|
|
this.module = module;
|
|
this.exports = exports;
|
|
this.subscriptions = subscriptions;
|
|
}
|
|
}
|
|
|
|
export class EmptyPlugin extends ExtHostPlugin {
|
|
constructor() {
|
|
super(false, { activate: undefined, deactivate: undefined }, undefined, []);
|
|
}
|
|
}
|
|
|
|
@Remotable.PluginHostContext('PluginHostPluginService')
|
|
export class PluginHostPluginService extends AbstractPluginService<ExtHostPlugin> {
|
|
|
|
private _threadService: IThreadService;
|
|
private _storage: PluginHostStorage;
|
|
private _proxy: MainProcessPluginService;
|
|
|
|
/**
|
|
* This class is constructed manually because it is a service, so it doesn't use any ctor injection
|
|
*/
|
|
constructor(threadService: IThreadService) {
|
|
super(true);
|
|
threadService.registerRemotableInstance(PluginHostPluginService, this);
|
|
this._threadService = threadService;
|
|
this._storage = new PluginHostStorage(threadService);
|
|
this._proxy = this._threadService.getRemotable(MainProcessPluginService);
|
|
}
|
|
|
|
protected _showMessage(severity: Severity, msg: string): void {
|
|
this._proxy.$doShowMessage(severity, msg);
|
|
this.$doShowMessage(severity, msg);
|
|
}
|
|
|
|
public $doShowMessage(severity: Severity, msg: string): void {
|
|
switch (severity) {
|
|
case Severity.Error:
|
|
console.error(msg);
|
|
break;
|
|
case Severity.Warning:
|
|
console.warn(msg);
|
|
break;
|
|
default:
|
|
console.log(msg);
|
|
}
|
|
}
|
|
|
|
public get(pluginId: string): IPluginExports {
|
|
if (!hasOwnProperty.call(this.activatedPlugins, pluginId)) {
|
|
throw new Error('Plugin `' + pluginId + '` is not known or not activated');
|
|
}
|
|
return this.activatedPlugins[pluginId].exports;
|
|
}
|
|
|
|
public deactivate(pluginId: string): void {
|
|
let plugin = this.activatedPlugins[pluginId];
|
|
if (!plugin) {
|
|
return;
|
|
}
|
|
|
|
// call deactivate if available
|
|
try {
|
|
if (typeof plugin.module.deactivate === 'function') {
|
|
plugin.module.deactivate();
|
|
}
|
|
} catch (err) {
|
|
// TODO: Do something with err if this is not the shutdown case
|
|
}
|
|
|
|
// clean up subscriptions
|
|
try {
|
|
disposeAll(plugin.subscriptions);
|
|
} catch (err) {
|
|
// TODO: Do something with err if this is not the shutdown case
|
|
}
|
|
}
|
|
|
|
protected _createFailedPlugin() {
|
|
return new ExtHostPlugin(true, { activate: undefined, deactivate: undefined }, undefined, []);
|
|
}
|
|
|
|
// -- overwriting AbstractPluginService
|
|
|
|
public registrationDone(messages: IMessage[]): void {
|
|
super.registrationDone([]);
|
|
this._proxy.$onPluginHostReady(PluginsRegistry.getAllPluginDescriptions(), messages);
|
|
}
|
|
|
|
protected _loadPluginModule(pluginDescription: IPluginDescription): TPromise<IPluginModule> {
|
|
return loadCommonJSModule(pluginDescription.main);
|
|
}
|
|
|
|
protected _loadPluginContext(pluginDescription: IPluginDescription): TPromise<IPluginContext> {
|
|
|
|
let globalState = new PluginMemento(pluginDescription.id, true, this._storage);
|
|
let workspaceState = new PluginMemento(pluginDescription.id, false, this._storage);
|
|
|
|
return TPromise.join([globalState.whenReady, workspaceState.whenReady]).then(() => {
|
|
return Object.freeze(<IPluginContext>{
|
|
globalState,
|
|
workspaceState,
|
|
subscriptions: [],
|
|
get extensionPath() { return pluginDescription.extensionFolderPath; },
|
|
asAbsolutePath: (relativePath: string) => { return paths.normalize(paths.join(pluginDescription.extensionFolderPath, relativePath), true); }
|
|
});
|
|
});
|
|
}
|
|
|
|
protected _actualActivatePlugin(pluginDescription: IPluginDescription): TPromise<ActivatedPlugin> {
|
|
|
|
return this._superActualActivatePlugin(pluginDescription).then((activatedPlugin) => {
|
|
this._proxy.$onPluginActivatedInPluginHost(pluginDescription.id);
|
|
return activatedPlugin;
|
|
}, (err) => {
|
|
this._proxy.$onPluginActivationFailedInPluginHost(pluginDescription.id);
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
private _superActualActivatePlugin(pluginDescription: IPluginDescription): TPromise<ExtHostPlugin> {
|
|
|
|
if (!pluginDescription.main) {
|
|
// Treat the plugin as being empty => NOT AN ERROR CASE
|
|
return TPromise.as(new EmptyPlugin());
|
|
}
|
|
return this._loadPluginModule(pluginDescription).then((pluginModule) => {
|
|
return this._loadPluginContext(pluginDescription).then(context => {
|
|
return PluginHostPluginService._callActivate(pluginModule, context);
|
|
});
|
|
});
|
|
}
|
|
|
|
private static _callActivate(pluginModule: IPluginModule, context: IPluginContext): TPromise<ExtHostPlugin> {
|
|
// Make sure the plugin's surface is not undefined
|
|
pluginModule = pluginModule || {
|
|
activate: undefined,
|
|
deactivate: undefined
|
|
};
|
|
|
|
// let subscriptions:IDisposable[] = [];
|
|
return this._callActivateOptional(pluginModule, context).then((pluginExports) => {
|
|
return new ExtHostPlugin(false, pluginModule, pluginExports, context.subscriptions);
|
|
});
|
|
}
|
|
|
|
private static _callActivateOptional(pluginModule: IPluginModule, context: IPluginContext): TPromise<IPluginExports> {
|
|
if (typeof pluginModule.activate === 'function') {
|
|
try {
|
|
return TPromise.as(pluginModule.activate.apply(global, [context]));
|
|
} catch (err) {
|
|
return TPromise.wrapError(err);
|
|
}
|
|
} else {
|
|
// No activate found => the module is the plugin's exports
|
|
return TPromise.as<IPluginExports>(pluginModule);
|
|
}
|
|
}
|
|
|
|
// -- called by main thread
|
|
|
|
public $activatePluginInPluginHost(pluginDescription: IPluginDescription): TPromise<void> {
|
|
return this._activatePlugin(pluginDescription);
|
|
}
|
|
|
|
}
|
|
|
|
function loadCommonJSModule<T>(modulePath: string): TPromise<T> {
|
|
let r: T = null;
|
|
try {
|
|
r = require.__$__nodeRequire<T>(modulePath);
|
|
} catch (e) {
|
|
return TPromise.wrapError(e);
|
|
}
|
|
return TPromise.as(r);
|
|
}
|