Skip to content

Commit

Permalink
fix #6228: pull/push plugin metadata one by one
Browse files Browse the repository at this point in the history
in order to avoid blocking web socket and depriving other ws channels

Signed-off-by: Anton Kosiakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Sep 20, 2019
1 parent 657bda8 commit 13212b0
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 131 deletions.
38 changes: 24 additions & 14 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,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;
}
Expand All @@ -100,7 +90,7 @@ export interface Plugin {

export interface ConfigStorage {
hostLogPath: string;
hostStoragePath: string,
hostStoragePath?: string,
}

export interface EnvInit {
Expand Down Expand Up @@ -162,12 +152,32 @@ export const emptyPlugin: Plugin = {
}
};

export interface PluginManagerInitializeParams {
preferences: PreferenceData
globalState: KeysToKeysToAnyValue
workspaceState: KeysToKeysToAnyValue
env: EnvInit
extApi?: ExtPluginApi[]
}

export interface PluginManagerExt {
$stop(pluginId?: string): PromiseLike<void>;

$init(pluginInit: PluginInitData, configStorage: ConfigStorage): PromiseLike<void>;
/** initialize the manager, should be called only once */
initiliaze(params: PluginManagerInitializeParams): Promise<void>;

/** load the plugin, does not activate it */
$load(plugin: PluginMetadata, configStorage: ConfigStorage): Promise<void>;

/** activated loaded plugins */
$start(activationEvents: string[]): Promise<void>;

/** deactivate the plugin */
$stop(pluginId: string): Promise<void>;

/** deactivate all plugins */
$stop(): Promise<void>;

$updateStoragePath(path: string | undefined): PromiseLike<void>;
$updateStoragePath(path: string | undefined): Promise<void>;

$activateByEvent(event: string): Promise<void>;
}
Expand Down
8 changes: 5 additions & 3 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,13 @@ export interface PluginDeployerHandler {
}

export const HostedPluginServer = Symbol('HostedPluginServer');
/**
* Plugin metadata is very expensive to load for many plugins, avoid exposing it.
*/
export interface HostedPluginServer extends JsonRpcServer<HostedPluginClient> {

getDeployedMetadata(): Promise<PluginMetadata[]>;
getDeployedFrontendMetadata(): Promise<PluginMetadata[]>;
getDeployedBackendMetadata(): Promise<PluginMetadata[]>;
getDeployedPlugins(): Promise<string[]>;
getDeployedPlugin(id: string): Promise<PluginMetadata | undefined>;

getExtPluginAPI(): Promise<ExtPluginApi[]>;

Expand Down
174 changes: 103 additions & 71 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,36 +169,33 @@ export class HostedPluginSupport {

async load(): Promise<void> {
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);
}
}

protected async doLoad(initData: PluginsInitializationData, container: interfaces.Container): Promise<void> {
protected async doLoad(): Promise<void> {
const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ }));
this.server.onDidCloseConnection(() => toDisconnect.dispose());

const pluginContributions = await this.initializeContributions(toDisconnect);
if (!pluginContributions) {
return;
}

// 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(pluginContributions, toDisconnect);

await this.viewRegistry.initWidgets();
// remove restored plugin widgets which were not registered by contributions
Expand All @@ -209,29 +206,55 @@ export class HostedPluginSupport {
// if disconnected then don't try to init plugin code and dynamic contributions
return;
}
toDisconnect.push(this.startPlugins(contributionsByHost, initData, container));
this.startPlugins(contributionsByHost, toDisconnect);
}

/**
* Always synchronous in order to simplify handling disconnections.
* @throws never
*/
protected loadContributions(plugins: PluginMetadata[], toDisconnect: DisposableCollection): Map<PluginHost, PluginContributions[]> {
const hostContributions = new Map<PluginHost, PluginContributions[]>();
protected async initializeContributions(toDisconnect: DisposableCollection): Promise<PluginContributions[] | undefined> {
const pluginContributions: PluginContributions[] = [];
const pluginIds = await this.server.getDeployedPlugins();
if (toDisconnect.disposed) {
return undefined;
}
const toUnload = new Set(this.contributions.keys());
let loaded = false;
for (const plugin of plugins) {
const pluginId = plugin.model.id;
for (const pluginId of pluginIds) {
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;
// pull one by one to avoid blocking web socket
const plugin = await this.server.getDeployedPlugin(pluginId);
if (toDisconnect.disposed) {
return undefined;
}
if (plugin) {
contributions = new PluginContributions(plugin);
this.contributions.set(pluginId, contributions);
contributions.push(Disposable.create(() => this.contributions.delete(pluginId)));
loaded = true;
}
}
}
for (const pluginId of toUnload) {
const contribution = this.contributions.get(pluginId);
if (contribution) {
contribution.dispose();
}
}
if (loaded || toUnload.size) {
this.onDidChangePluginsEmitter.fire(undefined);
}
return pluginContributions;
}

/**
* Always synchronous in order to simplify handling disconnections.
* @throws never
*/
protected loadContributions(pluginContributions: PluginContributions[], toDisconnect: DisposableCollection): Map<PluginHost, PluginContributions[]> {
const hostContributions = new Map<PluginHost, PluginContributions[]>();
for (const contributions of pluginContributions) {
const plugin = contributions.plugin;
if (contributions.state === PluginContributions.State.INITIALIZING) {
contributions.state = PluginContributions.State.LOADING;
contributions.push(Disposable.create(() => console.log(`[${plugin.model.id}]: Unloaded plugin.`)));
Expand All @@ -252,30 +275,31 @@ export class HostedPluginSupport {
}));
}
}
for (const pluginId of toUnload) {
const contribution = this.contributions.get(pluginId);
if (contribution) {
contribution.dispose();
}
}
if (loaded || toUnload.size) {
this.onDidChangePluginsEmitter.fire(undefined);
}
return hostContributions;
}

protected startPlugins(
contributionsByHost: Map<PluginHost, PluginContributions[]>,
initData: PluginsInitializationData,
container: interfaces.Container
): Disposable {
const toDisconnect = new DisposableCollection();
protected async startPlugins(contributionsByHost: Map<PluginHost, PluginContributions[]>, toDisconnect: DisposableCollection): Promise<void> {
const [hostLogPath, hostStoragePath] = await Promise.all([
this.pluginPathsService.getHostLogPath(),
this.getStoragePath()
]);
if (toDisconnect.disposed) {
return;
}
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(() => {
const manager = await this.obtainManager(host, hostContributions, toDisconnect);
if (!manager) {
return;
}
for (const contributions of hostContributions) {
// push one by one to avoid blocking websocket
await manager.$load(contributions.plugin, configStorage);
if (toDisconnect.disposed) {
return;
}
}
manager.$start([...this.activationEvents]).then(() => {
if (toDisconnect.disposed) {
return;
}
Expand All @@ -293,44 +317,52 @@ export class HostedPluginSupport {
}
});
}
return toDisconnect;
}

protected obtainManager(host: string, hostContributions: PluginContributions[], container: interfaces.Container, toDispose: DisposableCollection): PluginManagerExt {
protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise<PluginManagerExt | undefined> {
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 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.initiliaze({
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<void> {
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,
Expand Down Expand Up @@ -473,7 +505,7 @@ export class HostedPluginSupport {

}

interface PluginsInitializationData {
export interface PluginsInitializationData {
plugins: PluginMetadata[],
logPath: string,
storagePath: string | undefined,
Expand Down
Loading

0 comments on commit 13212b0

Please sign in to comment.