diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc1d9deef91f..18b9302cf7ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## v0.12.0 + +Breaking changes: + +- [plugin] don't block web socket with many plugins [6252](https://github.com/eclipse-theia/theia/pull/6252) + - `PluginModel` does not have anymore `contributes` and `dependencies` to avoid sending unnecessary data. + - Use `PluginReader.readContribution` to load contributes. + - Use `PluginReader.readDependencies` to load dependencies. + - `PluginMetadata` does not have anymore raw package.json model to avoid sending excessive data to the frontend. + - `theia.Plugin.packageJSON` throws an unsupported error for frontend plugins as a consequence. Please convert to a backend plugin if you need access to it. + - `PluginManagerExt.$init` does not start plugins anymore, but only initialize the manager RPC services to avoid sending excessive initialization data, as all preferences, on each deployment. + - Please call `$start` to start plugins. + - `PluginDeployerHandler.getPluginMetadata` is replaced with `PluginDeployerHandler.getPluginDependencies` to access plugin dependencies. + - `HostedPluginServer.getDeployedMetadata` is replaced with `HostedPluginServer.getDeployedPluginIds` and `HostedPluginServer.getDeployedPlugins` + to fetch first only ids of deployed plugins and then deployed metadata for only yet not loaded plugins. + - `HostedPluginDeployerHandler.getDeployedFrontendMetadata` and `HostedPluginDeployerHandler.getDeployedBackendMetadata` are replaced with + `HostedPluginDeployerHandler.getDeployedFrontendPluginIds`, `HostedPluginDeployerHandlergetDeployedBackendPluginIds` and `HostedPluginDeployerHandler.getDeployedPlugin` to featch first only ids and then deplyoed metadata fro only yet not loaded plugins. + - `PluginHost.init` can initialize plugins asynchronous, synchronous initialization is still supported. + - `HostedPluginReader.doGetPluginMetadata` is renamed to `HostedPluginReader.getPluginMetadata`. + - `PluginDebugAdapterContribution.languages`, `PluginDebugAdapterContribution.getSchemaAttributes` and `PluginDebugAdapterContribution.getConfigurationSnippets` are removed to prevent sending the contributions second time to the frontend. Debug contributions are loaded statically from the deployed plugin metadata instead. The same for corresponding methods in `DebugExtImpl`. + ## v0.11.0 - [core] added ENTER event handler to the open button in explorer [#6158](https://github.com/eclipse-theia/theia/pull/6158) diff --git a/packages/core/package.json b/packages/core/package.json index 95ff7a05d4734..9eff029440be1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -43,7 +43,7 @@ "vscode-languageserver-types": "^3.15.0-next", "vscode-uri": "^1.0.8", "vscode-ws-jsonrpc": "^0.1.1", - "ws": "^5.2.2", + "ws": "^7.1.2", "yargs": "^11.1.0" }, "publishConfig": { diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index 90a149dc5e3a9..1ab7cbe683f0d 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -84,7 +84,10 @@ export class MessagingContribution implements BackendApplicationContribution, Me onStart(server: http.Server | https.Server): void { const wss = new ws.Server({ server, - perMessageDeflate: false + perMessageDeflate: { + // don't compress if a message is less than 256kb + threshold: 256 * 1024 + } }); interface CheckAliveWS extends ws { alive: boolean; diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts index 168d097510fa6..a9404af8371c8 100644 --- a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts +++ b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts @@ -23,8 +23,6 @@ import { LanguageGrammarDefinitionContribution, getEncodedLanguageId } from './t import { createTextmateTokenizer, TokenizerOption } from './textmate-tokenizer'; import { TextmateRegistry } from './textmate-registry'; import { MonacoThemeRegistry } from './monaco-theme-registry'; -import { MonacoEditor } from '../monaco-editor'; -import { EditorManager } from '@theia/editor/lib/browser'; export const OnigasmPromise = Symbol('OnigasmPromise'); export type OnigasmPromise = Promise; @@ -60,9 +58,6 @@ export class MonacoTextmateService implements FrontendApplicationContribution { @inject(MonacoThemeRegistry) protected readonly monacoThemeRegistry: MonacoThemeRegistry; - @inject(EditorManager) - private readonly editorManager: EditorManager; - initialize(): void { if (!isBasicWasmSupported) { console.log('Textmate support deactivated because WebAssembly is not detected.'); @@ -109,7 +104,6 @@ export class MonacoTextmateService implements FrontendApplicationContribution { for (const { id } of monaco.languages.getLanguages()) { monaco.languages.onLanguage(id, () => this.activateLanguage(id)); } - this.detectLanguages(); } protected readonly toDisposeOnUpdateTheme = new DisposableCollection(); @@ -166,11 +160,4 @@ export class MonacoTextmateService implements FrontendApplicationContribution { } } - detectLanguages(): void { - for (const editor of MonacoEditor.getAll(this.editorManager)) { - if (editor.languageAutoDetected) { - editor.detectLanguage(); - } - } - } } diff --git a/packages/plugin-dev/src/node/hosted-plugin-reader.ts b/packages/plugin-dev/src/node/hosted-plugin-reader.ts index a866d3bbcb49f..9de24675a5bb7 100644 --- a/packages/plugin-dev/src/node/hosted-plugin-reader.ts +++ b/packages/plugin-dev/src/node/hosted-plugin-reader.ts @@ -34,7 +34,7 @@ export class HostedPluginReader implements BackendApplicationContribution { protected deployerHandler: HostedPluginDeployerHandler; async initialize(): Promise { - this.pluginReader.doGetPluginMetadata(process.env.HOSTED_PLUGIN) + this.pluginReader.getPluginMetadata(process.env.HOSTED_PLUGIN) .then(this.hostedPlugin.resolve.bind(this.hostedPlugin)); const pluginPath = process.env.HOSTED_PLUGIN; diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index 7f02a8f511d77..4a049bce4de63 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -44,7 +44,8 @@ export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIF vscode.commands.registerCommand = function (command: theia.CommandDescription | string, handler?: (...args: any[]) => T | Thenable, thisArg?: any): any { // use of the ID when registering commands if (typeof command === 'string') { - const commands = plugin.model.contributes && plugin.model.contributes.commands; + const rawCommands = plugin.rawModel.contributes && plugin.rawModel.contributes.commands; + const commands = rawCommands ? Array.isArray(rawCommands) ? rawCommands : [rawCommands] : undefined; if (handler && commands && commands.some(item => item.command === command)) { return vscode.commands.registerHandler(command, handler, thisArg); } diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts index 204f287852230..82407d75674ee 100644 --- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts +++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts @@ -34,6 +34,7 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca plugin.name = plugin.name.substr(built_prefix.length); } const result: PluginModel = { + packagePath: plugin.packagePath, // see id definition: https://github.com/microsoft/vscode/blob/15916055fe0cb9411a5f36119b3b012458fe0a1d/src/vs/platform/extensions/common/extensions.ts#L167-L169 id: `${plugin.publisher.toLowerCase()}.${plugin.name.toLowerCase()}`, name: plugin.name, @@ -47,13 +48,26 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca }, entryPoint: { backend: plugin.main - }, - extensionDependencies: this.getDeployableDependencies(plugin.extensionDependencies || []) + } }; - result.contributes = this.readContributions(plugin); return result; } + /** + * Maps extension dependencies to deployable extension dependencies. + */ + getDependencies(plugin: PluginPackage): Map | undefined { + if (!plugin.extensionDependencies || !plugin.extensionDependencies.length) { + return undefined; + } + const dependencies = new Map(); + for (const dependency of plugin.extensionDependencies) { + const dependencyId = dependency.toLowerCase(); + dependencies.set(dependencyId, this.VSCODE_PREFIX + dependencyId); + } + return dependencies; + } + getLifecycle(plugin: PluginPackage): PluginLifecycle { return { startMethod: 'activate', @@ -63,11 +77,4 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca }; } - /** - * Converts an array of extension dependencies - * to an array of deployable extension dependencies - */ - private getDeployableDependencies(dependencies: string[]): string[] { - return dependencies.map(dep => this.VSCODE_PREFIX + dep.toLowerCase()); - } } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index c85c047d05801..d063df23d43e8 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -66,7 +66,6 @@ import { import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin'; -import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; import { DebuggerDescription } from '@theia/debug/lib/common/debug-service'; import { DebugProtocol } from 'vscode-debugprotocol'; import { SymbolInformation } from 'vscode-languageserver-types'; @@ -75,16 +74,6 @@ import { MaybePromise } from '@theia/core/lib/common/types'; import { QuickOpenItem, QuickOpenItemOptions } from '@theia/core/lib/common/quick-open-model'; import { QuickTitleButton } from '@theia/core/lib/common/quick-open-model'; -export interface PluginInitData { - plugins: PluginMetadata[]; - preferences: PreferenceData; - globalState: KeysToKeysToAnyValue; - workspaceState: KeysToKeysToAnyValue; - env: EnvInit; - extApi?: ExtPluginApi[]; - activationEvents: string[] -} - export interface PreferenceData { [scope: number]: any; } @@ -99,7 +88,7 @@ export interface Plugin { export interface ConfigStorage { hostLogPath: string; - hostStoragePath: string, + hostStoragePath?: string, } export interface EnvInit { @@ -141,6 +130,7 @@ export const emptyPlugin: Plugin = { type: 'empty', version: 'empty' }, + packagePath: 'empty', entryPoint: { } @@ -161,12 +151,35 @@ export const emptyPlugin: Plugin = { } }; +export interface PluginManagerInitializeParams { + preferences: PreferenceData + globalState: KeysToKeysToAnyValue + workspaceState: KeysToKeysToAnyValue + env: EnvInit + extApi?: ExtPluginApi[] +} + +export interface PluginManagerStartParams { + plugins: PluginMetadata[] + configStorage: ConfigStorage + activationEvents: string[] +} + export interface PluginManagerExt { - $stop(pluginId?: string): PromiseLike; - $init(pluginInit: PluginInitData, configStorage: ConfigStorage): PromiseLike; + /** initialize the manager, should be called only once */ + $init(params: PluginManagerInitializeParams): Promise; + + /** load and activate plugins */ + $start(params: PluginManagerStartParams): Promise; + + /** deactivate the plugin */ + $stop(pluginId: string): Promise; + + /** deactivate all plugins */ + $stop(): Promise; - $updateStoragePath(path: string | undefined): PromiseLike; + $updateStoragePath(path: string | undefined): Promise; $activateByEvent(event: string): Promise; } @@ -1241,9 +1254,6 @@ export interface DebugExt { $sessionDidChange(sessionId: string | undefined): void; $provideDebugConfigurations(debugType: string, workspaceFolder: string | undefined): Promise; $resolveDebugConfigurations(debugConfiguration: theia.DebugConfiguration, workspaceFolder: string | undefined): Promise; - $getSupportedLanguages(debugType: string): Promise; - $getSchemaAttributes(debugType: string): Promise; - $getConfigurationSnippets(debugType: string): Promise; $createDebugSession(debugConfiguration: theia.DebugConfiguration): Promise; $terminateDebugSession(sessionId: string): Promise; $getTerminalCreationOptions(debugType: string): Promise; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 0572e9a94f51f..01528100e7358 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -228,6 +228,14 @@ export interface PluginScanner { * @returns {PluginLifecycle} */ getLifecycle(plugin: PluginPackage): PluginLifecycle; + + getContribution(plugin: PluginPackage): PluginContribution | undefined; + + /** + * A mapping between a dependency as its defined in package.json + * and its deployable form, e.g. `publisher.name` -> `vscode:extension/publisher.name` + */ + getDependencies(plugin: PluginPackage): Map | undefined; } export const PluginDeployer = Symbol('PluginDeployer'); @@ -374,22 +382,20 @@ export interface PluginModel { type: PluginEngine; version: string; }; - entryPoint: { - frontend?: string; - backend?: string; - }; - contributes?: PluginContribution; - /** - * The deployable form of extensionDependencies from package.json, - * i.e. not `publisher.name`, but `vscode:extension/publisher.name`. - */ - extensionDependencies?: string[]; + entryPoint: PluginEntryPoint; + packagePath: string; +} + +export interface PluginEntryPoint { + frontend?: string; + backend?: string; } /** * This interface describes some static plugin contributions. */ export interface PluginContribution { + activationEvents?: string[]; configuration?: PreferenceSchema[]; configurationDefaults?: PreferenceSchemaProperties; languages?: LanguageContribution[]; @@ -583,7 +589,6 @@ export interface ExtensionContext { export interface PluginMetadata { host: string; - source: PluginPackage; model: PluginModel; lifecycle: PluginLifecycle; } @@ -610,20 +615,34 @@ export interface HostedPluginClient { onDidDeploy(): void; } +export interface PluginDependencies { + metadata: PluginMetadata + mapping?: Map +} + export const PluginDeployerHandler = Symbol('PluginDeployerHandler'); export interface PluginDeployerHandler { deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise; deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise; - getPluginMetadata(pluginToBeInstalled: PluginDeployerEntry): Promise + getPluginDependencies(pluginToBeInstalled: PluginDeployerEntry): Promise +} + +export interface GetDeployedPluginsParams { + pluginIds: string[] +} + +export interface DeployedPlugin { + metadata: PluginMetadata; + contributes?: PluginContribution; } export const HostedPluginServer = Symbol('HostedPluginServer'); export interface HostedPluginServer extends JsonRpcServer { - getDeployedMetadata(): Promise; - getDeployedFrontendMetadata(): Promise; - getDeployedBackendMetadata(): Promise; + getDeployedPluginIds(): Promise; + + getDeployedPlugins(params: GetDeployedPluginsParams): Promise; getExtPluginAPI(): Promise; diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 6fb03f665fa5c..e9c554f23cc88 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -21,9 +21,11 @@ // tslint:disable:no-any +import debounce = require('lodash.debounce'); +import { UUID } from '@phosphor/coreutils'; import { injectable, inject, interfaces, named, postConstruct } from 'inversify'; import { PluginWorker } from '../../main/browser/plugin-worker'; -import { PluginMetadata, getPluginId, HostedPluginServer } from '../../common/plugin-protocol'; +import { PluginMetadata, getPluginId, HostedPluginServer, DeployedPlugin } from '../../common/plugin-protocol'; import { HostedPluginWatcher } from './hosted-plugin-watcher'; import { MAIN_RPC_CONTEXT, PluginManagerExt } from '../../common/plugin-api-rpc'; import { setUpPluginApi } from '../../main/browser/main-context'; @@ -33,16 +35,14 @@ import { ILogger, ContributionProvider, CommandRegistry, WillExecuteCommandEvent, CancellationTokenSource, JsonRpcProxy, ProgressService } from '@theia/core'; -import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser'; +import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser/preferences'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler'; import { getQueryParameters } from '../../main/browser/env-main'; -import { ExtPluginApi, MainPluginApiProvider } from '../../common/plugin-ext-api-contribution'; +import { MainPluginApiProvider } from '../../common/plugin-ext-api-contribution'; import { PluginPathsService } from '../../main/common/plugin-paths-protocol'; import { getPreferences } from '../../main/browser/preference-registry-main'; import { PluginServer } from '../../common/plugin-protocol'; -import { KeysToKeysToAnyValue } from '../../common/types'; -import { FileStat } from '@theia/filesystem/lib/common/filesystem'; import { MonacoTextmateService } from '@theia/monaco/lib/browser/textmate'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; @@ -61,7 +61,10 @@ export const PluginProgressLocation = 'plugin'; @injectable() export class HostedPluginSupport { - container: interfaces.Container; + + protected readonly clientId = UUID.uuid4(); + + protected container: interfaces.Container; @inject(ILogger) protected readonly logger: ILogger; @@ -155,7 +158,7 @@ export class HostedPluginSupport { get plugins(): PluginMetadata[] { const plugins: PluginMetadata[] = []; - this.contributions.forEach(contributions => plugins.push(contributions.plugin)); + this.contributions.forEach(contributions => plugins.push(contributions.plugin.metadata)); return plugins; } @@ -167,38 +170,32 @@ export class HostedPluginSupport { this.server.onDidOpenConnection(() => this.load()); } - async load(): Promise { + protected loadQueue: Promise = Promise.resolve(undefined); + load = debounce(() => this.loadQueue = this.loadQueue.then(async () => { try { - await this.progressService.withProgress('', PluginProgressLocation, async () => { - const roots = this.workspaceService.tryGetRoots(); - const [plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates] = await Promise.all([ - this.server.getDeployedMetadata(), - this.pluginPathsService.getHostLogPath(), - this.getStoragePath(), - this.server.getExtPluginAPI(), - this.pluginServer.getAllStorageValues(undefined), - this.pluginServer.getAllStorageValues({ workspace: this.workspaceService.workspace, roots }) - ]); - await this.doLoad({ plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates, roots }, this.container); - }); + await this.progressService.withProgress('', PluginProgressLocation, () => this.doLoad()); } catch (e) { console.error('Failed to load plugins:', e); } - } + }), 50, { leading: true }); - protected async doLoad(initData: PluginsInitializationData, container: interfaces.Container): Promise { + protected async doLoad(): Promise { const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ })); this.server.onDidCloseConnection(() => toDisconnect.dispose()); + // process empty plugins as well in order to properly remove stale plugin widgets + await this.syncPlugins(); + // make sure that the previous state, including plugin widgets, is restored // and core layout is initialized, i.e. explorer, scm, debug views are already added to the shell // but shell is not yet revealed await this.appState.reachedState('initialized_layout'); + if (toDisconnect.disposed) { // if disconnected then don't try to load plugin contributions return; } - const contributionsByHost = this.loadContributions(initData.plugins, toDisconnect); + const contributionsByHost = this.loadContributions(toDisconnect); await this.viewRegistry.initWidgets(); // remove restored plugin widgets which were not registered by contributions @@ -209,35 +206,73 @@ export class HostedPluginSupport { // if disconnected then don't try to init plugin code and dynamic contributions return; } - toDisconnect.push(this.startPlugins(contributionsByHost, initData, container)); + await this.startPlugins(contributionsByHost, toDisconnect); + } + + /** + * Sync loaded and deployed plugins: + * - undeployed plugins are unloaded + * - newly deployed plugins are initialized + */ + protected async syncPlugins(): Promise { + let initialized = 0; + const syncPluginsMeasurement = this.createMeasurement('syncPlugins'); + + const toUnload = new Set(this.contributions.keys()); + try { + const pluginIds: string[] = []; + const deployedPluginIds = await this.server.getDeployedPluginIds(); + for (const pluginId of deployedPluginIds) { + toUnload.delete(pluginId); + if (!this.contributions.has(pluginId)) { + pluginIds.push(pluginId); + } + } + for (const pluginId of toUnload) { + const contribution = this.contributions.get(pluginId); + if (contribution) { + contribution.dispose(); + } + } + if (pluginIds.length) { + const plugins = await this.server.getDeployedPlugins({ pluginIds }); + for (const plugin of plugins) { + const pluginId = plugin.metadata.model.id; + const contributions = new PluginContributions(plugin); + this.contributions.set(pluginId, contributions); + contributions.push(Disposable.create(() => this.contributions.delete(pluginId))); + initialized++; + } + } + } finally { + if (initialized || toUnload.size) { + this.onDidChangePluginsEmitter.fire(undefined); + } + } + + this.logMeasurement('Sync', initialized, syncPluginsMeasurement); } /** * Always synchronous in order to simplify handling disconnections. * @throws never */ - protected loadContributions(plugins: PluginMetadata[], toDisconnect: DisposableCollection): Map { + protected loadContributions(toDisconnect: DisposableCollection): Map { + let loaded = 0; + const loadPluginsMeasurement = this.createMeasurement('loadPlugins'); + const hostContributions = new Map(); - const toUnload = new Set(this.contributions.keys()); - let loaded = false; - for (const plugin of plugins) { + for (const contributions of this.contributions.values()) { + const plugin = contributions.plugin.metadata; const pluginId = plugin.model.id; - toUnload.delete(pluginId); - - let contributions = this.contributions.get(pluginId); - if (!contributions) { - contributions = new PluginContributions(plugin); - this.contributions.set(pluginId, contributions); - contributions.push(Disposable.create(() => this.contributions.delete(pluginId))); - loaded = true; - } if (contributions.state === PluginContributions.State.INITIALIZING) { contributions.state = PluginContributions.State.LOADING; - contributions.push(Disposable.create(() => console.log(`[${plugin.model.id}]: Unloaded plugin.`))); - contributions.push(this.contributionHandler.handleContributions(plugin)); + contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`))); + contributions.push(this.contributionHandler.handleContributions(this.clientId, contributions.plugin)); contributions.state = PluginContributions.State.LOADED; - console.log(`[${plugin.model.id}]: Loaded contributions.`); + console.log(`[${this.clientId}][${pluginId}]: Loaded contributions.`); + loaded++; } if (contributions.state === PluginContributions.State.LOADED) { @@ -248,89 +283,111 @@ export class HostedPluginSupport { hostContributions.set(host, dynamicContributions); toDisconnect.push(Disposable.create(() => { contributions!.state = PluginContributions.State.LOADED; - console.log(`[${plugin.model.id}]: Disconnected.`); + console.log(`[${this.clientId}][${pluginId}]: Disconnected.`); })); } } - for (const pluginId of toUnload) { - const contribution = this.contributions.get(pluginId); - if (contribution) { - contribution.dispose(); - } - } - if (loaded || toUnload.size) { - this.onDidChangePluginsEmitter.fire(undefined); - } + + this.logMeasurement('Load contributions', loaded, loadPluginsMeasurement); + return hostContributions; } - protected startPlugins( - contributionsByHost: Map, - initData: PluginsInitializationData, - container: interfaces.Container - ): Disposable { - const toDisconnect = new DisposableCollection(); + protected async startPlugins(contributionsByHost: Map, toDisconnect: DisposableCollection): Promise { + let started = 0; + const startPluginsMeasurement = this.createMeasurement('startPlugins'); + + const [hostLogPath, hostStoragePath] = await Promise.all([ + this.pluginPathsService.getHostLogPath(), + this.getStoragePath() + ]); + if (toDisconnect.disposed) { + return; + } + const thenable: Promise[] = []; + const configStorage = { hostLogPath, hostStoragePath }; for (const [host, hostContributions] of contributionsByHost) { - const manager = this.obtainManager(host, hostContributions, container, toDisconnect); - this.initPlugins(manager, { - ...initData, - plugins: hostContributions.map(contributions => contributions.plugin) - }).then(() => { - if (toDisconnect.disposed) { - return; - } - for (const contributions of hostContributions) { - const plugin = contributions.plugin; - const id = plugin.model.id; - contributions.state = PluginContributions.State.STARTED; - console.log(`[${id}]: Started plugin.`); - toDisconnect.push(contributions.push(Disposable.create(() => { - console.log(`[${id}]: Stopped plugin.`); - manager.$stop(id); - }))); - - this.activateByWorkspaceContains(manager, plugin); + const manager = await this.obtainManager(host, hostContributions, toDisconnect); + if (!manager) { + return; + } + const plugins = hostContributions.map(contributions => contributions.plugin.metadata); + thenable.push((async () => { + try { + const activationEvents = [...this.activationEvents]; + await manager.$start({ plugins, configStorage, activationEvents }); + if (toDisconnect.disposed) { + return; + } + for (const contributions of hostContributions) { + started++; + const plugin = contributions.plugin; + const id = plugin.metadata.model.id; + contributions.state = PluginContributions.State.STARTED; + console.log(`[${this.clientId}][${id}]: Started plugin.`); + toDisconnect.push(contributions.push(Disposable.create(() => { + console.log(`[${this.clientId}][${id}]: Stopped plugin.`); + manager.$stop(id); + }))); + + this.activateByWorkspaceContains(manager, plugin); + } + } catch (e) { + console.error(`Failed to start plugins for '${host}' host`, e); } - }); + })()); } - return toDisconnect; + await Promise.all(thenable); + if (toDisconnect.disposed) { + return; + } + this.logMeasurement('Start', started, startPluginsMeasurement); } - protected obtainManager(host: string, hostContributions: PluginContributions[], container: interfaces.Container, toDispose: DisposableCollection): PluginManagerExt { + protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise { let manager = this.managers.get(host); if (!manager) { - const pluginId = getPluginId(hostContributions[0].plugin.model); - const rpc = this.initRpc(host, pluginId, container); - toDispose.push(rpc); + const pluginId = getPluginId(hostContributions[0].plugin.metadata.model); + const rpc = this.initRpc(host, pluginId); + toDisconnect.push(rpc); + manager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); this.managers.set(host, manager); - toDispose.push(Disposable.create(() => this.managers.delete(host))); + toDisconnect.push(Disposable.create(() => this.managers.delete(host))); + + const [extApi, globalState, workspaceState] = await Promise.all([ + this.server.getExtPluginAPI(), + this.pluginServer.getAllStorageValues(undefined), + this.pluginServer.getAllStorageValues({ + workspace: this.workspaceService.workspace, + roots: this.workspaceService.tryGetRoots() + }) + ]); + if (toDisconnect.disposed) { + return undefined; + } + + await manager.$init({ + preferences: getPreferences(this.preferenceProviderProvider, this.workspaceService.tryGetRoots()), + globalState, + workspaceState, + env: { queryParams: getQueryParameters(), language: navigator.language }, + extApi + }); + if (toDisconnect.disposed) { + return undefined; + } } return manager; } - protected initRpc(host: PluginHost, pluginId: string, container: interfaces.Container): RPCProtocol { + protected initRpc(host: PluginHost, pluginId: string): RPCProtocol { const rpc = host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(pluginId, host); - setUpPluginApi(rpc, container); - this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, container)); + setUpPluginApi(rpc, this.container); + this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, this.container)); return rpc; } - protected async initPlugins(manager: PluginManagerExt, data: PluginsInitializationData): Promise { - await manager.$init({ - plugins: data.plugins, - preferences: getPreferences(this.preferenceProviderProvider, data.roots), - globalState: data.globalStates, - workspaceState: data.workspaceStates, - env: { queryParams: getQueryParameters(), language: navigator.language }, - extApi: data.pluginAPIs, - activationEvents: [...this.activationEvents] - }, { - hostLogPath: data.logPath, - hostStoragePath: data.storagePath || '' - }); - } - private createServerRpc(pluginID: string, hostID: string): RPCProtocol { return new RPCProtocolImpl({ onMessage: this.watcher.onPostMessageEvent, @@ -420,14 +477,15 @@ export class HostedPluginSupport { await Promise.all(promises); } - protected async activateByWorkspaceContains(manager: PluginManagerExt, plugin: PluginMetadata): Promise { - if (!plugin.source.activationEvents) { + protected async activateByWorkspaceContains(manager: PluginManagerExt, plugin: DeployedPlugin): Promise { + const activationEvents = plugin.contributes && plugin.contributes.activationEvents; + if (!activationEvents) { return; } const paths: string[] = []; const includePatterns: string[] = []; // should be aligned with https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts#L460-L469 - for (const activationEvent of plugin.source.activationEvents) { + for (const activationEvent of activationEvents) { if (/^workspaceContains:/.test(activationEvent)) { const fileNameOrGlob = activationEvent.substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { @@ -437,7 +495,7 @@ export class HostedPluginSupport { } } } - const activatePlugin = () => manager.$activateByEvent(`onPlugin:${plugin.model.id}`); + const activatePlugin = () => manager.$activateByEvent(`onPlugin:${plugin.metadata.model.id}`); const promises: Promise[] = []; if (paths.length) { promises.push(this.workspaceService.containsSome(paths)); @@ -472,21 +530,35 @@ export class HostedPluginSupport { } } -} + protected createMeasurement(name: string): () => number { + const startMarker = `${name}-start`; + const endMarker = `${name}-end`; + performance.clearMeasures(name); + performance.clearMarks(startMarker); + performance.clearMarks(endMarker); + + performance.mark(startMarker); + return () => { + performance.mark(endMarker); + performance.measure(name, startMarker, endMarker); + const duration = performance.getEntriesByName(name)[0].duration; + performance.clearMeasures(name); + performance.clearMarks(startMarker); + performance.clearMarks(endMarker); + return duration; + }; + } + + protected logMeasurement(prefix: string, count: number, measurement: () => number): void { + const pluginCount = `${count} plugin${count === 1 ? '' : 's'}`; + console.log(`[${this.clientId}] ${prefix} of ${pluginCount} took: ${measurement()} ms`); + } -interface PluginsInitializationData { - plugins: PluginMetadata[], - logPath: string, - storagePath: string | undefined, - pluginAPIs: ExtPluginApi[], - globalStates: KeysToKeysToAnyValue, - workspaceStates: KeysToKeysToAnyValue, - roots: FileStat[], } export class PluginContributions extends DisposableCollection { constructor( - readonly plugin: PluginMetadata + readonly plugin: DeployedPlugin ) { super(); } diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index d0a04cf39e70f..64927e2a78789 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -19,7 +19,7 @@ import { RPCProtocolImpl } from '../../../common/rpc-protocol'; import { PluginManagerExtImpl } from '../../../plugin/plugin-manager'; import { MAIN_RPC_CONTEXT, Plugin, emptyPlugin } from '../../../common/plugin-api-rpc'; import { createAPIFactory } from '../../../plugin/plugin-context'; -import { getPluginId, PluginMetadata } from '../../../common/plugin-protocol'; +import { getPluginId, PluginMetadata, PluginPackage } from '../../../common/plugin-protocol'; import * as theia from '@theia/plugin'; import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry'; import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution'; @@ -90,10 +90,12 @@ const pluginManager = new PluginManagerExtImpl({ } const plugin: Plugin = { pluginPath: pluginModel.entryPoint.frontend!, - pluginFolder: plg.source.packagePath, + pluginFolder: pluginModel.packagePath, model: pluginModel, lifecycle: pluginLifecycle, - rawModel: plg.source + get rawModel(): PluginPackage { + throw new Error('not supported'); + } }; result.push(plugin); const apiImpl = apiFactory(plugin); @@ -102,10 +104,12 @@ const pluginManager = new PluginManagerExtImpl({ } else { foreign.push({ pluginPath: pluginModel.entryPoint.backend!, - pluginFolder: plg.source.packagePath, + pluginFolder: pluginModel.packagePath, model: pluginModel, lifecycle: pluginLifecycle, - rawModel: plg.source + get rawModel(): PluginPackage { + throw new Error('not supported'); + } }); } } diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts index 216443f25f3e9..28f9726eaa59b 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts @@ -16,7 +16,7 @@ import { injectable, inject } from 'inversify'; import { ILogger } from '@theia/core'; -import { PluginDeployerHandler, PluginDeployerEntry, PluginMetadata } from '../../common/plugin-protocol'; +import { PluginDeployerHandler, PluginDeployerEntry, PluginEntryPoint, DeployedPlugin, PluginDependencies } from '../../common/plugin-protocol'; import { HostedPluginReader } from './plugin-reader'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -32,67 +32,98 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { /** * Managed plugin metadata backend entries. */ - private currentBackendPluginsMetadata: PluginMetadata[] = []; + private readonly deployedBackendPlugins = new Map(); /** * Managed plugin metadata frontend entries. */ - private currentFrontendPluginsMetadata: PluginMetadata[] = []; + private readonly deployedFrontendPlugins = new Map(); private backendPluginsMetadataDeferred = new Deferred(); private frontendPluginsMetadataDeferred = new Deferred(); - async getDeployedFrontendMetadata(): Promise { + async getDeployedFrontendPluginIds(): Promise { // await first deploy await this.frontendPluginsMetadataDeferred.promise; // fetch the last deployed state - return this.currentFrontendPluginsMetadata; + return [...this.deployedFrontendPlugins.keys()]; } - async getDeployedBackendMetadata(): Promise { + async getDeployedBackendPluginIds(): Promise { // await first deploy await this.backendPluginsMetadataDeferred.promise; // fetch the last deployed state - return this.currentBackendPluginsMetadata; + return [...this.deployedBackendPlugins.keys()]; } - getPluginMetadata(plugin: PluginDeployerEntry): Promise { - return this.reader.getPluginMetadata(plugin.path()); + getDeployedPlugin(pluginId: string): DeployedPlugin | undefined { + const metadata = this.deployedBackendPlugins.get(pluginId); + if (metadata) { + return metadata; + } + return this.deployedFrontendPlugins.get(pluginId); } - async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise { - for (const plugin of frontendPlugins) { - const metadata = await this.reader.getPluginMetadata(plugin.path()); - if (metadata) { - if (this.currentFrontendPluginsMetadata.some(value => value.model.id === metadata.model.id)) { - continue; - } - - this.currentFrontendPluginsMetadata.push(metadata); - this.logger.info(`Deploying frontend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.frontend || plugin.path()}"`); + /** + * @throws never! in order to isolate plugin deployment + */ + async getPluginDependencies(entry: PluginDeployerEntry): Promise { + const pluginPath = entry.path(); + try { + const manifest = await this.reader.readPackage(pluginPath); + if (!manifest) { + return undefined; } + const metadata = this.reader.readMetadata(manifest); + const dependencies: PluginDependencies = { metadata }; + dependencies.mapping = this.reader.readDependencies(manifest); + return dependencies; + } catch (e) { + console.error(`Failed to load plugin dependencies from '${pluginPath}' path`, e); + return undefined; } + } + async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise { + for (const plugin of frontendPlugins) { + await this.deployPlugin(plugin, 'frontend'); + } // resolve on first deploy this.frontendPluginsMetadataDeferred.resolve(undefined); } async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise { for (const plugin of backendPlugins) { - const metadata = await this.reader.getPluginMetadata(plugin.path()); - if (metadata) { - if (this.currentBackendPluginsMetadata.some(value => value.model.id === metadata.model.id)) { - continue; - } - - this.currentBackendPluginsMetadata.push(metadata); - this.logger.info(`Deploying backend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.backend || plugin.path()}"`); - } + await this.deployPlugin(plugin, 'backend'); } - // resolve on first deploy this.backendPluginsMetadataDeferred.resolve(undefined); } + /** + * @throws never! in order to isolate plugin deployment + */ + protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise { + const pluginPath = entry.path(); + try { + const manifest = await this.reader.readPackage(pluginPath); + if (!manifest) { + return; + } + + const metadata = this.reader.readMetadata(manifest); + if (this.deployedBackendPlugins.has(metadata.model.id)) { + return; + } + + const deployed: DeployedPlugin = { metadata }; + deployed.contributes = this.reader.readContribution(manifest); + this.deployedBackendPlugins.set(metadata.model.id, deployed); + this.logger.info(`Deploying ${entryPoint} plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`); + } catch (e) { + console.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e); + } + } + } diff --git a/packages/plugin-ext/src/hosted/node/metadata-scanner.ts b/packages/plugin-ext/src/hosted/node/metadata-scanner.ts index 040f4fa111846..61b8c94bf068e 100644 --- a/packages/plugin-ext/src/hosted/node/metadata-scanner.ts +++ b/packages/plugin-ext/src/hosted/node/metadata-scanner.ts @@ -33,7 +33,6 @@ export class MetadataScanner { const scanner = this.getScanner(plugin); return { host: 'main', - source: plugin, model: scanner.getModel(plugin), lifecycle: scanner.getLifecycle(plugin) }; diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index 9decabc2591f6..c0b2192ce8432 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -27,6 +27,7 @@ import { WorkspaceExtImpl } from '../../plugin/workspace'; import { MessageRegistryExt } from '../../plugin/message-registry'; import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext'; import { ClipboardExt } from '../../plugin/clipboard-ext'; +import { loadManifest } from './plugin-manifest-loader'; /** * Handle the RPC calls. @@ -128,40 +129,46 @@ export class PluginHostRPC { console.error(e); } }, - init(raw: PluginMetadata[]): [Plugin[], Plugin[]] { + async init(raw: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> { console.log('PLUGIN_HOST(' + process.pid + '): PluginManagerExtImpl/init()'); const result: Plugin[] = []; const foreign: Plugin[] = []; for (const plg of raw) { - const pluginModel = plg.model; - const pluginLifecycle = plg.lifecycle; - - if (pluginModel.entryPoint!.frontend) { - foreign.push({ - pluginPath: pluginModel.entryPoint.frontend!, - pluginFolder: plg.source.packagePath, - model: pluginModel, - lifecycle: pluginLifecycle, - rawModel: plg.source - }); - } else { - let backendInitPath = pluginLifecycle.backendInitPath; - // if no init path, try to init as regular Theia plugin - if (!backendInitPath) { - backendInitPath = __dirname + '/scanners/backend-init-theia.js'; - } + try { + const pluginModel = plg.model; + const pluginLifecycle = plg.lifecycle; + + const rawModel = await loadManifest(pluginModel.packagePath); + rawModel.packagePath = pluginModel.packagePath; + if (pluginModel.entryPoint!.frontend) { + foreign.push({ + pluginPath: pluginModel.entryPoint.frontend!, + pluginFolder: pluginModel.packagePath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel + }); + } else { + let backendInitPath = pluginLifecycle.backendInitPath; + // if no init path, try to init as regular Theia plugin + if (!backendInitPath) { + backendInitPath = __dirname + '/scanners/backend-init-theia.js'; + } - const plugin: Plugin = { - pluginPath: pluginModel.entryPoint.backend!, - pluginFolder: plg.source.packagePath, - model: pluginModel, - lifecycle: pluginLifecycle, - rawModel: plg.source - }; + const plugin: Plugin = { + pluginPath: pluginModel.entryPoint.backend!, + pluginFolder: pluginModel.packagePath, + model: pluginModel, + lifecycle: pluginLifecycle, + rawModel + }; - self.initContext(backendInitPath, plugin); + self.initContext(backendInitPath, plugin); - result.push(plugin); + result.push(plugin); + } + } catch (e) { + console.error(`Failed to initialize ${plg.model.id} plugin.`, e); } } return [result, foreign]; diff --git a/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts b/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts new file mode 100644 index 0000000000000..6b64f6f8b0ae2 --- /dev/null +++ b/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts @@ -0,0 +1,71 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// tslint:disable:no-any + +import * as path from 'path'; +import * as fs from 'fs-extra'; + +const NLS_REGEX = /^%([\w\d.-]+)%$/i; + +export async function loadManifest(pluginPath: string): Promise { + const [manifest, translations] = await Promise.all([ + fs.readJson(path.join(pluginPath, 'package.json')), + loadTranslations(pluginPath) + ]); + return manifest && translations && Object.keys(translations).length ? + localize(manifest, translations) : + manifest; +} + +async function loadTranslations(pluginPath: string): Promise { + try { + return await fs.readJson(path.join(pluginPath, 'package.nls.json')); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + return {}; + } +} + +function localize(value: any, translations: { + [key: string]: string +}): any { + if (typeof value === 'string') { + const match = NLS_REGEX.exec(value); + return match && translations[match[1]] || value; + } + if (Array.isArray(value)) { + const result = []; + for (const item of value) { + result.push(localize(item, translations)); + } + return result; + } + if (value === null) { + return value; + } + if (typeof value === 'object') { + const result: { [key: string]: any } = {}; + // tslint:disable-next-line:forin + for (const propertyName in value) { + result[propertyName] = localize(value[propertyName], translations); + } + return result; + } + return value; +} diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index d1611818605da..6792a35507ff2 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -14,17 +14,15 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -// tslint:disable:no-any - import * as path from 'path'; -import * as fs from 'fs-extra'; import * as express from 'express'; import * as escape_html from 'escape-html'; import { ILogger } from '@theia/core'; import { inject, injectable, optional, multiInject } from 'inversify'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { PluginMetadata, getPluginId, MetadataProcessor } from '../../common/plugin-protocol'; +import { PluginMetadata, getPluginId, MetadataProcessor, PluginPackage, PluginContribution } from '../../common/plugin-protocol'; import { MetadataScanner } from './metadata-scanner'; +import { loadManifest } from './plugin-manifest-loader'; @injectable() export class HostedPluginReader implements BackendApplicationContribution { @@ -77,35 +75,36 @@ export class HostedPluginReader implements BackendApplicationContribution { res.status(404).send(`The plugin with id '${escape_html(pluginId)}' does not exist.`); } - async getPluginMetadata(pluginPath: string): Promise { - return this.doGetPluginMetadata(pluginPath); - } - /** - * MUST never throw to isolate plugin deployment + * @throws never */ - async doGetPluginMetadata(pluginPath: string | undefined): Promise { + async getPluginMetadata(pluginPath: string | undefined): Promise { try { - if (!pluginPath) { - return undefined; - } - pluginPath = path.normalize(pluginPath + '/'); - return await this.loadPluginMetadata(pluginPath); + const manifest = await this.readPackage(pluginPath); + return manifest && this.readMetadata(manifest); } catch (e) { this.logger.error(`Failed to load plugin metadata from "${pluginPath}"`, e); return undefined; } } - protected async loadPluginMetadata(pluginPath: string): Promise { - const manifest = await this.loadManifest(pluginPath); + async readPackage(pluginPath: string | undefined): Promise { + if (!pluginPath) { + return undefined; + } + pluginPath = path.normalize(pluginPath + '/'); + const manifest = await loadManifest(pluginPath); if (!manifest) { return undefined; } manifest.packagePath = pluginPath; - const pluginMetadata = this.scanner.getPluginMetadata(manifest); + return manifest; + } + + readMetadata(plugin: PluginPackage): PluginMetadata { + const pluginMetadata = this.scanner.getPluginMetadata(plugin); if (pluginMetadata.model.entryPoint.backend) { - pluginMetadata.model.entryPoint.backend = path.resolve(pluginPath, pluginMetadata.model.entryPoint.backend); + pluginMetadata.model.entryPoint.backend = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.backend); } if (pluginMetadata) { // Add post processor @@ -114,60 +113,19 @@ export class HostedPluginReader implements BackendApplicationContribution { metadataProcessor.process(pluginMetadata); }); } - this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), pluginPath); + this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), plugin.packagePath); } return pluginMetadata; } - protected async loadManifest(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; + readContribution(plugin: PluginPackage): PluginContribution | undefined { + const scanner = this.scanner.getScanner(plugin); + return scanner.getContribution(plugin); } - 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 {}; - } - } - - 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 (value === null) { - return value; - } - 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; + readDependencies(plugin: PluginPackage): Map | undefined { + const scanner = this.scanner.getScanner(plugin); + return scanner.getDependencies(plugin); } - 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 e39cdfac9f9c0..e385c55acfea0 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-service.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-service.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { injectable, inject, named, postConstruct } from 'inversify'; -import { HostedPluginServer, HostedPluginClient, PluginMetadata, PluginDeployer } from '../../common/plugin-protocol'; +import { HostedPluginServer, HostedPluginClient, PluginMetadata, PluginDeployer, GetDeployedPluginsParams, DeployedPlugin } from '../../common/plugin-protocol'; import { HostedPluginSupport } from './hosted-plugin'; import { ILogger, Disposable } from '@theia/core'; import { ContributionProvider } from '@theia/core'; @@ -63,28 +63,50 @@ export class HostedPluginServerImpl implements HostedPluginServer { this.hostedPlugin.setClient(client); } - getDeployedFrontendMetadata(): Promise { - return this.deployerHandler.getDeployedFrontendMetadata(); - } - - async getDeployedMetadata(): Promise { - const backendMetadata = await this.deployerHandler.getDeployedBackendMetadata(); + async getDeployedPluginIds(): Promise { + const backendMetadata = await this.deployerHandler.getDeployedBackendPluginIds(); if (backendMetadata.length > 0) { this.hostedPlugin.runPluginServer(); } - const allMetadata: PluginMetadata[] = []; - allMetadata.push(...await this.deployerHandler.getDeployedFrontendMetadata()); - allMetadata.push(...backendMetadata); - - // ask remote as well - const extraBackendPluginsMetadata = await this.hostedPlugin.getExtraPluginMetadata(); - allMetadata.push(...extraBackendPluginsMetadata); - - return allMetadata; + const plugins = new Set(); + for (const pluginId of await this.deployerHandler.getDeployedFrontendPluginIds()) { + plugins.add(pluginId); + } + for (const pluginId of backendMetadata) { + plugins.add(pluginId); + } + const extraPluginMetadata = await this.hostedPlugin.getExtraPluginMetadata(); + for (const plugin of extraPluginMetadata) { + plugins.add(plugin.model.id); + } + return [...plugins.values()]; } - getDeployedBackendMetadata(): Promise { - return Promise.resolve(this.deployerHandler.getDeployedBackendMetadata()); + async getDeployedPlugins({ pluginIds }: GetDeployedPluginsParams): Promise { + if (!pluginIds.length) { + return []; + } + const plugins = []; + let extraPluginMetadata: Map | undefined; + for (const pluginId of pluginIds) { + let plugin = this.deployerHandler.getDeployedPlugin(pluginId); + if (!plugin) { + if (!extraPluginMetadata) { + extraPluginMetadata = new Map(); + for (const extraMetadata of await this.hostedPlugin.getExtraPluginMetadata()) { + extraPluginMetadata.set(extraMetadata.model.id, extraMetadata); + } + } + const metadata = extraPluginMetadata.get(pluginId); + if (metadata) { + plugin = { metadata }; + } + } + if (plugin) { + plugins.push(plugin); + } + } + return plugins; } onMessage(message: string): Promise { diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index cde6c5ccbeb39..e8a81f6cec7f7 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -84,6 +84,7 @@ export class TheiaPluginScanner implements PluginScanner { getModel(plugin: PluginPackage): PluginModel { const result: PluginModel = { + packagePath: plugin.packagePath, // see id definition: https://github.com/microsoft/vscode/blob/15916055fe0cb9411a5f36119b3b012458fe0a1d/src/vs/platform/extensions/common/extensions.ts#L167-L169 id: `${plugin.publisher.toLowerCase()}.${plugin.name.toLowerCase()}`, name: plugin.name, @@ -98,10 +99,8 @@ export class TheiaPluginScanner implements PluginScanner { entryPoint: { frontend: plugin.theiaPlugin!.frontend, backend: plugin.theiaPlugin!.backend - }, - extensionDependencies: plugin.extensionDependencies || [] + } }; - result.contributes = this.readContributions(plugin); return result; } @@ -115,12 +114,23 @@ export class TheiaPluginScanner implements PluginScanner { }; } - protected readContributions(rawPlugin: PluginPackage): PluginContribution | undefined { - if (!rawPlugin.contributes) { + getDependencies(rawPlugin: PluginPackage): Map | undefined { + // skip it since there is no way to load transitive dependencies for Theia plugins yet + return undefined; + } + + getContribution(rawPlugin: PluginPackage): PluginContribution | undefined { + if (!rawPlugin.contributes && !rawPlugin.activationEvents) { return undefined; } - const contributions: PluginContribution = {}; + const contributions: PluginContribution = { + activationEvents: rawPlugin.activationEvents + }; + + if (!rawPlugin.contributes) { + return contributions; + } try { if (rawPlugin.contributes.configuration) { diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts index 8c33989e75272..4967d88c9b9de 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -47,7 +47,6 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa import { PluginDebugSessionFactory } from './plugin-debug-session-factory'; import { PluginWebSocketChannel } from '../../../common/connection'; import { PluginDebugAdapterContributionRegistrator, PluginDebugService } from './plugin-debug-service'; -import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater'; import { FileSystem } from '@theia/filesystem/lib/common'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; @@ -66,7 +65,6 @@ export class DebugMainImpl implements DebugMain, Disposable { private readonly debugPreferences: DebugPreferences; private readonly sessionContributionRegistrator: PluginDebugSessionContributionRegistrator; private readonly adapterContributionRegistrator: PluginDebugAdapterContributionRegistrator; - private readonly debugSchemaUpdater: DebugSchemaUpdater; private readonly fileSystem: FileSystem; private readonly pluginService: HostedPluginSupport; @@ -87,7 +85,6 @@ export class DebugMainImpl implements DebugMain, Disposable { this.debugPreferences = container.get(DebugPreferences); this.adapterContributionRegistrator = container.get(PluginDebugService); this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry); - this.debugSchemaUpdater = container.get(DebugSchemaUpdater); this.fileSystem = container.get(FileSystem); this.pluginService = container.get(HostedPluginSupport); @@ -146,7 +143,6 @@ export class DebugMainImpl implements DebugMain, Disposable { ); const toDispose = new DisposableCollection( - Disposable.create(() => this.debugSchemaUpdater.update()), Disposable.create(() => this.debuggerContributions.delete(debugType)) ); this.debuggerContributions.set(debugType, toDispose); @@ -160,8 +156,6 @@ export class DebugMainImpl implements DebugMain, Disposable { }) ]); this.toDispose.push(Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType))); - - this.debugSchemaUpdater.update(); } async $unregisterDebuggerConfiguration(debugType: string): Promise { diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts index d37d41893db80..98fbe0ce8c2ea 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts @@ -16,7 +16,6 @@ import { DebugExt, } from '../../../common/plugin-api-rpc'; import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; -import { IJSONSchemaSnippet, IJSONSchema } from '@theia/core/lib/common/json-schema'; import { MaybePromise } from '@theia/core/lib/common/types'; import { DebuggerDescription } from '@theia/debug/lib/common/debug-service'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; @@ -38,18 +37,6 @@ export class PluginDebugAdapterContribution { return this.description.label; } - get languages(): MaybePromise { - return this.debugExt.$getSupportedLanguages(this.type); - } - - async getSchemaAttributes(): Promise { - return this.debugExt.$getSchemaAttributes(this.type); - } - - async getConfigurationSnippets(): Promise { - return this.debugExt.$getConfigurationSnippets(this.type); - } - async provideDebugConfigurations(workspaceFolderUri: string | undefined): Promise { return this.debugExt.$provideDebugConfigurations(this.type, workspaceFolderUri); } diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts index 243723cb842d1..6cabe2a706d6c 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts @@ -22,6 +22,7 @@ import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribut import { injectable, inject, postConstruct } from 'inversify'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { DebuggerContribution } from '../../../common/plugin-protocol'; /** * Debug adapter contribution registrator. @@ -45,6 +46,8 @@ export interface PluginDebugAdapterContributionRegistrator { */ @injectable() export class PluginDebugService implements DebugService, PluginDebugAdapterContributionRegistrator { + + protected readonly debuggers: DebuggerContribution[] = []; protected readonly contributors = new Map(); protected readonly toDispose = new DisposableCollection(); @@ -88,8 +91,14 @@ export class PluginDebugService implements DebugService, PluginDebugAdapterContr } async debugTypes(): Promise { - const debugTypes = await this.delegated.debugTypes(); - return debugTypes.concat(Array.from(this.contributors.keys())); + const debugTypes = new Set(await this.delegated.debugTypes()); + for (const contribution of this.debuggers) { + debugTypes.add(contribution.type); + } + for (const debugType of this.contributors.keys()) { + debugTypes.add(debugType); + } + return [...debugTypes]; } async provideDebugConfigurations(debugType: string, workspaceFolderUri: string | undefined): Promise { @@ -123,14 +132,24 @@ export class PluginDebugService implements DebugService, PluginDebugAdapterContr return this.delegated.resolveDebugConfiguration(resolved, workspaceFolderUri); } + registerDebugger(contribution: DebuggerContribution): Disposable { + this.debuggers.push(contribution); + return Disposable.create(() => { + const index = this.debuggers.indexOf(contribution); + if (index !== -1) { + this.debuggers.splice(index, 1); + } + }); + } + async getDebuggersForLanguage(language: string): Promise { const debuggers = await this.delegated.getDebuggersForLanguage(language); - for (const contributor of this.contributors.values()) { - const languages = await contributor.languages; + for (const contributor of this.debuggers) { + const languages = contributor.languages; if (languages && languages.indexOf(language) !== -1) { - const { type } = contributor; - debuggers.push({ type, label: await contributor.label || type }); + const { label, type } = contributor; + debuggers.push({ type, label: label || type }); } } @@ -138,20 +157,22 @@ export class PluginDebugService implements DebugService, PluginDebugAdapterContr } async getSchemaAttributes(debugType: string): Promise { - const contributor = this.contributors.get(debugType); - if (contributor) { - return contributor.getSchemaAttributes && contributor.getSchemaAttributes() || []; - } else { - return this.delegated.getSchemaAttributes(debugType); + let schemas = await this.delegated.getSchemaAttributes(debugType); + for (const contribution of this.debuggers) { + if (contribution.configurationAttributes && + (contribution.type === debugType || contribution.type === '*' || debugType === '*')) { + schemas = schemas.concat(contribution.configurationAttributes); + } } + return schemas; } async getConfigurationSnippets(): Promise { let snippets = await this.delegated.getConfigurationSnippets(); - for (const contributor of this.contributors.values()) { - if (contributor.getConfigurationSnippets) { - snippets = snippets.concat(await contributor.getConfigurationSnippets()); + for (const contribution of this.debuggers) { + if (contribution.configurationSnippets) { + snippets = snippets.concat(contribution.configurationSnippets); } } diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 4b63aae6a2510..ba8a0b8af8353 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -19,7 +19,7 @@ import { ITokenTypeMap, IEmbeddedLanguagesMap, StandardTokenType } from 'vscode- import { TextmateRegistry, getEncodedLanguageId, MonacoTextmateService, GrammarDefinition } from '@theia/monaco/lib/browser/textmate'; import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginViewRegistry } from './view/plugin-view-registry'; -import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, PluginMetadata } from '../../common'; +import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, DeployedPlugin } from '../../common'; import { PreferenceSchemaProvider } from '@theia/core/lib/browser'; import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/browser/preferences'; import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler'; @@ -29,6 +29,8 @@ import { CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter } from '@theia/core/lib/common/event'; import { TaskDefinitionRegistry, ProblemMatcherRegistry, ProblemPatternRegistry } from '@theia/task/lib/browser'; +import { PluginDebugService } from './debug/plugin-debug-service'; +import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater'; @injectable() export class PluginContributionHandler { @@ -71,27 +73,35 @@ export class PluginContributionHandler { @inject(ProblemPatternRegistry) protected readonly problemPatternRegistry: ProblemPatternRegistry; + @inject(PluginDebugService) + protected readonly debugService: PluginDebugService; + + @inject(DebugSchemaUpdater) + protected readonly debugSchema: DebugSchemaUpdater; + protected readonly commandHandlers = new Map(); protected readonly onDidRegisterCommandHandlerEmitter = new Emitter(); readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event; + protected readonly activatedLanguages = new Set(); + /** * Always synchronous in order to simplify handling disconnections. * @throws never, loading of each contribution should handle errors * in order to avoid preventing loading of other contibutions or extensions */ - handleContributions(plugin: PluginMetadata): Disposable { - const contributions = plugin.model.contributes; + handleContributions(clientId: string, plugin: DeployedPlugin): Disposable { + const contributions = plugin.contributes; if (!contributions) { return Disposable.NULL; } - const toDispose = new DisposableCollection; + const toDispose = new DisposableCollection(); const pushContribution = (id: string, contribute: () => Disposable) => { try { toDispose.push(contribute()); } catch (e) { - console.error(`[${plugin.model.id}]: Failed to load '${id}' contribution.`, e); + console.error(`[${clientId}][${plugin.metadata.model.id}]: Failed to load '${id}' contribution.`, e); } }; @@ -107,8 +117,15 @@ export class PluginContributionHandler { pushContribution('configurationDefaults', () => this.updateDefaultOverridesSchema(configurationDefaults)); } - if (contributions.languages) { - for (const lang of contributions.languages) { + const languages = contributions.languages; + if (languages && languages.length) { + for (const lang of languages) { + /* + * Monaco guesses a language for opened plain text models on `monaco.languages.register`. + * It can trigger language activation before grammars are registered. + * Install onLanguage listener earlier in order to catch such activations and activate grammars as well. + */ + monaco.languages.onLanguage(lang.id, () => this.activatedLanguages.add(lang.id)); // it is not possible to unregister a language monaco.languages.register({ id: lang.id, @@ -136,7 +153,6 @@ export class PluginContributionHandler { const grammars = contributions.grammars; if (grammars && grammars.length) { - toDispose.push(Disposable.create(() => this.monacoTextmateService.detectLanguages())); for (const grammar of grammars) { if (grammar.injectTo) { for (const injectScope of grammar.injectTo) { @@ -173,11 +189,10 @@ export class PluginContributionHandler { tokenTypes: this.convertTokenTypes(grammar.tokenTypes) })); pushContribution(`grammar.language.${language}.activation`, - () => monaco.languages.onLanguage(language, () => this.monacoTextmateService.activateLanguage(language)) + () => this.onDidActivateLanguage(language, () => this.monacoTextmateService.activateLanguage(language)) ); } } - this.monacoTextmateService.detectLanguages(); } pushContribution('commands', () => this.registerCommands(contributions)); @@ -239,6 +254,16 @@ export class PluginContributionHandler { } } + if (contributions.debuggers && contributions.debuggers.length) { + toDispose.push(Disposable.create(() => this.debugSchema.update())); + for (const contribution of contributions.debuggers) { + pushContribution(`debuggers.${contribution.type}`, + () => this.debugService.registerDebugger(contribution) + ); + } + this.debugSchema.update(); + } + return toDispose; } @@ -293,6 +318,14 @@ export class PluginContributionHandler { return !!this.commandHandlers.get(id); } + protected onDidActivateLanguage(language: string, cb: () => {}): Disposable { + if (this.activatedLanguages.has(language)) { + cb(); + return Disposable.NULL; + } + return monaco.languages.onLanguage(language, cb); + } + private updateConfigurationSchema(schema: PreferenceSchema): Disposable { this.validateConfigurationSchema(schema); return this.preferenceSchemaProvider.setSchema(schema); 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 8f3613eed317c..d4bb7b49e0ebf 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -126,28 +126,22 @@ export class PluginDeployerImpl implements PluginDeployer { await this.applyFileHandlers(pluginDeployerEntries); await this.applyDirectoryFileHandlers(pluginDeployerEntries); for (const deployerEntry of pluginDeployerEntries) { - const metadata = await this.pluginDeployerHandler.getPluginMetadata(deployerEntry); - if (metadata && !pluginsToDeploy.has(metadata.model.id)) { - pluginsToDeploy.set(metadata.model.id, deployerEntry); - chunk.push(metadata); + const dependencies = await this.pluginDeployerHandler.getPluginDependencies(deployerEntry); + if (dependencies && !pluginsToDeploy.has(dependencies.metadata.model.id)) { + pluginsToDeploy.set(dependencies.metadata.model.id, deployerEntry); + if (dependencies.mapping) { + chunk.push(dependencies.mapping); + } } } } catch (e) { console.error(`Failed to resolve plugins from '${current}'`, e); } } - for (const metadata of chunk) { - const extensionDependencies = metadata.source.extensionDependencies; - const deployableExtensionDependencies = metadata.model.extensionDependencies; - if (extensionDependencies && deployableExtensionDependencies) { - for (let dependencyIndex = 0; dependencyIndex < extensionDependencies.length; dependencyIndex++) { - const dependencyId = extensionDependencies[dependencyIndex].toLowerCase(); - if (!pluginsToDeploy.has(dependencyId)) { - const deployableDependency = deployableExtensionDependencies[dependencyIndex]; - if (deployableDependency) { - queue.push(deployableDependency); - } - } + for (const dependencies of chunk) { + for (const [dependency, deployableDependency] of dependencies) { + if (!pluginsToDeploy.has(dependency)) { + queue.push(deployableDependency); } } } diff --git a/packages/plugin-ext/src/plugin/node/debug/debug.ts b/packages/plugin-ext/src/plugin/node/debug/debug.ts index 821958875bf44..e6787709b6ca1 100644 --- a/packages/plugin-ext/src/plugin/node/debug/debug.ts +++ b/packages/plugin-ext/src/plugin/node/debug/debug.ts @@ -14,15 +14,14 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { Emitter } from '@theia/core/lib/common/event'; -import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; import { Path } from '@theia/core/lib/common/path'; import { CommunicationProvider } from '@theia/debug/lib/common/debug-model'; import * as theia from '@theia/plugin'; import URI from 'vscode-uri'; import { Breakpoint } from '../../../common/plugin-api-rpc-model'; import { DebugExt, DebugMain, PLUGIN_RPC_CONTEXT as Ext, TerminalOptionsExt } from '../../../common/plugin-api-rpc'; +import { PluginPackageDebuggersContribution } from '../../../common/plugin-protocol'; import { RPCProtocol } from '../../../common/rpc-protocol'; -import { DebuggerContribution } from '../../../common'; import { PluginWebSocketChannel } from '../../../common/connection'; import { CommandRegistryImpl } from '../../command-registry'; import { ConnectionExtImpl } from '../../connection-ext'; @@ -46,7 +45,11 @@ export class DebugExtImpl implements DebugExt { // providers by type private configurationProviders = new Map>(); - private debuggersContributions = new Map(); + /** + * Only use internally, don't send it to the frontend. It's expensive! + * It's already there as a part of the plugin metadata. + */ + private debuggersContributions = new Map(); private descriptorFactories = new Map(); private trackerFactories: [string, theia.DebugAdapterTrackerFactory][] = []; private contributionPaths = new Map(); @@ -87,8 +90,8 @@ export class DebugExtImpl implements DebugExt { * @param pluginFolder plugin folder path * @param contributions available debuggers contributions */ - registerDebuggersContributions(pluginFolder: string, contributions: DebuggerContribution[]): void { - contributions.forEach((contribution: DebuggerContribution) => { + registerDebuggersContributions(pluginFolder: string, contributions: PluginPackageDebuggersContribution[]): void { + contributions.forEach(contribution => { this.contributionPaths.set(contribution.type, pluginFolder); this.debuggersContributions.set(contribution.type, contribution); this.proxy.$registerDebuggerContribution({ @@ -229,21 +232,6 @@ export class DebugExtImpl implements DebugExt { } } - async $getSupportedLanguages(debugType: string): Promise { - const contribution = this.debuggersContributions.get(debugType); - return contribution && contribution.languages || []; - } - - async $getSchemaAttributes(debugType: string): Promise { - const contribution = this.debuggersContributions.get(debugType); - return contribution && contribution.configurationAttributes || []; - } - - async $getConfigurationSnippets(debugType: string): Promise { - const contribution = this.debuggersContributions.get(debugType); - return contribution && contribution.configurationSnippets || []; - } - async $getTerminalCreationOptions(debugType: string): Promise { return this.doGetTerminalCreationOptions(debugType); } diff --git a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts index b3ae5312466e8..a03cc830aaebb 100644 --- a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts +++ b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts @@ -16,13 +16,14 @@ import * as path from 'path'; import * as theia from '@theia/plugin'; -import { PlatformSpecificAdapterContribution, DebuggerContribution } from '../../../common'; +import { PlatformSpecificAdapterContribution, PluginPackageDebuggersContribution } from '../../../common'; import { isWindows, isOSX } from '@theia/core/lib/common/os'; /** * Resolves [DebugAdapterExecutable](#DebugAdapterExecutable) based on contribution. */ -export async function resolveDebugAdapterExecutable(pluginPath: string, debuggerContribution: DebuggerContribution): Promise { +export async function resolveDebugAdapterExecutable( + pluginPath: string, debuggerContribution: PluginPackageDebuggersContribution): Promise { const info = toPlatformInfo(debuggerContribution); let program = (info && info.program || debuggerContribution.program); if (!program) { @@ -43,7 +44,7 @@ export async function resolveDebugAdapterExecutable(pluginPath: string, debugger }; } -function toPlatformInfo(executable: DebuggerContribution): PlatformSpecificAdapterContribution | undefined { +function toPlatformInfo(executable: PluginPackageDebuggersContribution): PlatformSpecificAdapterContribution | undefined { if (isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { return executable.winx86 || executable.win || executable.windows; } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 32310db739808..b4bea604d8cbe 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -641,7 +641,7 @@ export function createAPIFactory( } }; - const debuggersContributions = plugin.model.contributes && plugin.model.contributes.debuggers || []; + const debuggersContributions = plugin.rawModel.contributes && plugin.rawModel.contributes.debuggers || []; debugExt.assistedInject(connectionExt, commandRegistry); debugExt.registerDebuggersContributions(plugin.pluginFolder, debuggersContributions); const debug: typeof theia.debug = { diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 2f4b432282235..e0fab347d7bc1 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -20,11 +20,12 @@ import { MainMessageType, MessageRegistryMain, PluginManagerExt, - PluginInitData, PluginManager, Plugin, PluginAPI, - ConfigStorage + ConfigStorage, + PluginManagerInitializeParams, + PluginManagerStartParams } from '../common/plugin-api-rpc'; import { PluginMetadata } from '../common/plugin-protocol'; import * as theia from '@theia/plugin'; @@ -43,7 +44,7 @@ export interface PluginHost { // tslint:disable-next-line:no-any loadPlugin(plugin: Plugin): any; - init(data: PluginMetadata[]): [Plugin[], Plugin[]]; + init(data: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> | [Plugin[], Plugin[]]; initExtApi(extApi: ExtPluginApi[]): void; @@ -136,46 +137,46 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } } - async $init(pluginInit: PluginInitData, configStorage: ConfigStorage): Promise { + async $init(params: PluginManagerInitializeParams): Promise { this.storageProxy = this.rpc.set( MAIN_RPC_CONTEXT.STORAGE_EXT, new KeyValueStorageProxy(this.rpc.getProxy(PLUGIN_RPC_CONTEXT.STORAGE_MAIN), - pluginInit.globalState, - pluginInit.workspaceState) + params.globalState, + params.workspaceState) ); - // init query parameters - this.envExt.setQueryParameters(pluginInit.env.queryParams); - this.envExt.setLanguage(pluginInit.env.language); + this.envExt.setQueryParameters(params.env.queryParams); + this.envExt.setLanguage(params.env.language); - this.preferencesManager.init(pluginInit.preferences); + this.preferencesManager.init(params.preferences); - if (pluginInit.extApi) { - this.host.initExtApi(pluginInit.extApi); + if (params.extApi) { + this.host.initExtApi(params.extApi); } + } - const [plugins, foreignPlugins] = this.host.init(pluginInit.plugins); + async $start(params: PluginManagerStartParams): Promise { + const [plugins, foreignPlugins] = await this.host.init(params.plugins); // add foreign plugins for (const plugin of foreignPlugins) { - this.registerPlugin(plugin, configStorage); + this.registerPlugin(plugin, params.configStorage); } // add own plugins, before initialization for (const plugin of plugins) { - this.registerPlugin(plugin, configStorage); + this.registerPlugin(plugin, params.configStorage); } // run eager plugins await this.$activateByEvent('*'); - for (const activationEvent of pluginInit.activationEvents) { + for (const activationEvent of params.activationEvents) { await this.$activateByEvent(activationEvent); } if (this.host.loadTests) { return this.host.loadTests(); } - this.fireOnDidChange(); - return Promise.resolve(); + this.fireOnDidChange(); } protected registerPlugin(plugin: Plugin, configStorage: ConfigStorage): void { @@ -248,11 +249,10 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { return loading; } - $updateStoragePath(path: string | undefined): PromiseLike { + async $updateStoragePath(path: string | undefined): Promise { this.pluginContextsMap.forEach((pluginContext: theia.PluginContext, pluginId: string) => { pluginContext.storagePath = path ? join(path, pluginId) : undefined; }); - return Promise.resolve(); } async $activateByEvent(activationEvent: string): Promise { @@ -271,7 +271,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { const subscriptions: theia.Disposable[] = []; const asAbsolutePath = (relativePath: string): string => join(plugin.pluginFolder, relativePath); const logPath = join(configStorage.hostLogPath, plugin.model.id); // todo check format - const storagePath = join(configStorage.hostStoragePath, plugin.model.id); + const storagePath = join(configStorage.hostStoragePath || '', plugin.model.id); const pluginContext: theia.PluginContext = { extensionPath: plugin.pluginFolder, globalState: new Memento(plugin.model.id, true, this.storageProxy), diff --git a/yarn.lock b/yarn.lock index 8e090dde90702..7a8e33f9ec63f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,6 +1584,11 @@ async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" +async-limiter@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" @@ -11406,12 +11411,20 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -ws@^5.2.0, ws@^5.2.2: +ws@^5.2.0: version "5.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== dependencies: async-limiter "~1.0.0" +ws@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73" + integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg== + dependencies: + async-limiter "^1.0.0" + xdg-basedir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"