Skip to content

Commit efaaf0f

Browse files
committed
[plugin] Cache command arguments to safely pass the command over JSON-RPC
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
1 parent c1d367f commit efaaf0f

File tree

5 files changed

+80
-16
lines changed

5 files changed

+80
-16
lines changed

packages/plugin-ext/src/common/plugin-api-rpc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export interface CommandRegistryMain {
184184
}
185185

186186
export interface CommandRegistryExt {
187-
$executeCommand<T>(id: string, ...ars: any[]): PromiseLike<T>;
187+
$executeCommand<T>(id: string, ...ars: any[]): PromiseLike<T | undefined>;
188188
registerArgumentProcessor(processor: ArgumentProcessor): void;
189189
}
190190

packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class PluginTree extends TreeImpl {
127127
contextValue: item.contextValue
128128
};
129129
const node = this.getNode(item.id);
130-
if (item.collapsibleState !== TreeViewItemCollapsibleState.None) {
130+
if (item.collapsibleState !== undefined && item.collapsibleState !== TreeViewItemCollapsibleState.None) {
131131
if (CompositeTreeViewNode.is(node)) {
132132
return Object.assign(node, update);
133133
}
@@ -141,13 +141,14 @@ export class PluginTree extends TreeImpl {
141141
}, update);
142142
}
143143
if (TreeViewNode.is(node)) {
144-
return Object.assign(node, update);
144+
return Object.assign(node, update, { command: item.command });
145145
}
146146
return Object.assign({
147147
id: item.id,
148148
parent,
149149
visible: true,
150-
selected: false
150+
selected: false,
151+
command: item.command
151152
}, update);
152153
}
153154

packages/plugin-ext/src/plugin/command-registry.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@
1313
*
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
16+
/*---------------------------------------------------------------------------------------------
17+
* Copyright (c) Microsoft Corporation. All rights reserved.
18+
* Licensed under the MIT License. See License.txt in the project root for license information.
19+
*--------------------------------------------------------------------------------------------*/
1620

1721
import * as theia from '@theia/plugin';
1822
import { CommandRegistryExt, PLUGIN_RPC_CONTEXT as Ext, CommandRegistryMain } from '../common/plugin-api-rpc';
1923
import { RPCProtocol } from '../common/rpc-protocol';
2024
import { Disposable } from './types-impl';
2125
import { KnownCommands } from './type-converters';
26+
import { DisposableCollection } from '@theia/core';
2227

2328
// tslint:disable-next-line:no-any
24-
export type Handler = <T>(...args: any[]) => T | PromiseLike<T>;
29+
export type Handler = <T>(...args: any[]) => T | PromiseLike<T | undefined>;
2530

2631
export interface ArgumentProcessor {
2732
// tslint:disable-next-line:no-any
@@ -34,10 +39,16 @@ export class CommandRegistryImpl implements CommandRegistryExt {
3439
private readonly commands = new Set<string>();
3540
private readonly handlers = new Map<string, Handler>();
3641
private readonly argumentProcessors: ArgumentProcessor[];
42+
private readonly commandsConverter: CommandsConverter;
3743

3844
constructor(rpc: RPCProtocol) {
3945
this.proxy = rpc.getProxy(Ext.COMMAND_REGISTRY_MAIN);
4046
this.argumentProcessors = [];
47+
this.commandsConverter = new CommandsConverter(this);
48+
}
49+
50+
get converter(): CommandsConverter {
51+
return this.commandsConverter;
4152
}
4253

4354
// tslint:disable-next-line:no-any
@@ -78,7 +89,7 @@ export class CommandRegistryImpl implements CommandRegistryExt {
7889
}
7990

8091
// tslint:disable-next-line:no-any
81-
$executeCommand<T>(id: string, ...args: any[]): PromiseLike<T> {
92+
$executeCommand<T>(id: string, ...args: any[]): PromiseLike<T | undefined> {
8293
if (this.handlers.has(id)) {
8394
return this.executeLocalCommand(id, ...args);
8495
} else {
@@ -102,7 +113,7 @@ export class CommandRegistryImpl implements CommandRegistryExt {
102113
}
103114

104115
// tslint:disable-next-line:no-any
105-
private async executeLocalCommand<T>(id: string, ...args: any[]): Promise<T> {
116+
private async executeLocalCommand<T>(id: string, ...args: any[]): Promise<T | undefined> {
106117
const handler = this.handlers.get(id);
107118
if (handler) {
108119
return handler<T>(...args.map(arg => this.argumentProcessors.reduce((r, p) => p.processArgument(r), arg)));
@@ -123,3 +134,52 @@ export class CommandRegistryImpl implements CommandRegistryExt {
123134
this.argumentProcessors.push(processor);
124135
}
125136
}
137+
138+
// copied and modified from https://github.com/microsoft/vscode/blob/1.37.1/src/vs/workbench/api/common/extHostCommands.ts#L217-L259
139+
export class CommandsConverter {
140+
141+
private readonly safeCommandId: string;
142+
private readonly commands: CommandRegistryImpl;
143+
private readonly commandsMap = new Map<number, theia.Command>();
144+
private handle = 0;
145+
private isSafeCommandRegistered: boolean;
146+
147+
constructor(commands: CommandRegistryImpl) {
148+
this.safeCommandId = `theia_safe_cmd_${Date.now().toString()}`;
149+
this.commands = commands;
150+
this.isSafeCommandRegistered = false;
151+
}
152+
153+
/**
154+
* Convert to a command that can be safely passed over JSON-RPC.
155+
*/
156+
toSafeCommand(command: theia.Command, disposables: DisposableCollection): theia.Command {
157+
if (!this.isSafeCommandRegistered) {
158+
this.commands.registerCommand({ id: this.safeCommandId }, this.executeSafeCommand, this);
159+
this.isSafeCommandRegistered = true;
160+
}
161+
162+
const result: theia.Command = {};
163+
Object.assign(result, command);
164+
165+
if (command.command && command.arguments && command.arguments.length > 0) {
166+
const id = this.handle++;
167+
this.commandsMap.set(id, command);
168+
disposables.push(new Disposable(() => this.commandsMap.delete(id)));
169+
result.command = this.safeCommandId;
170+
result.arguments = [id];
171+
}
172+
173+
return result;
174+
}
175+
176+
// tslint:disable-next-line:no-any
177+
private executeSafeCommand<R>(...args: any[]): PromiseLike<R | undefined> {
178+
const command = this.commandsMap.get(args[0]);
179+
if (!command || !command.command) {
180+
return Promise.reject('command NOT FOUND');
181+
}
182+
return this.commands.executeCommand(command.command, ...(command.arguments || []));
183+
}
184+
185+
}

packages/plugin-ext/src/plugin/plugin-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export function createAPIFactory(
174174
return function (plugin: InternalPlugin): typeof theia {
175175
const commands: typeof theia.commands = {
176176
// tslint:disable-next-line:no-any
177-
registerCommand(command: theia.CommandDescription, handler?: <T>(...args: any[]) => T | Thenable<T>, thisArg?: any): Disposable {
177+
registerCommand(command: theia.CommandDescription, handler?: <T>(...args: any[]) => T | Thenable<T | undefined>, thisArg?: any): Disposable {
178178
return commandRegistry.registerCommand(command, handler, thisArg);
179179
},
180180
// tslint:disable-next-line:no-any

packages/plugin-ext/src/plugin/tree/tree-views.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@ import { Emitter } from '@theia/core/lib/common/event';
2323
import { Disposable, ThemeIcon } from '../types-impl';
2424
import { Plugin, PLUGIN_RPC_CONTEXT, TreeViewsExt, TreeViewsMain, TreeViewItem } from '../../common/plugin-api-rpc';
2525
import { RPCProtocol } from '../../common/rpc-protocol';
26-
import { CommandRegistryImpl } from '../command-registry';
26+
import { CommandRegistryImpl, CommandsConverter } from '../command-registry';
2727
import { TreeViewSelection } from '../../common';
2828
import { PluginPackage } from '../../common/plugin-protocol';
29+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
30+
import { toInternalCommand } from '../type-converters';
2931

3032
export class TreeViewsExtImpl implements TreeViewsExt {
3133

3234
private proxy: TreeViewsMain;
3335

3436
private treeViews: Map<string, TreeViewExtImpl<any>> = new Map<string, TreeViewExtImpl<any>>();
3537

36-
constructor(rpc: RPCProtocol, commandRegistry: CommandRegistryImpl) {
38+
constructor(rpc: RPCProtocol, readonly commandRegistry: CommandRegistryImpl) {
3739
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TREE_VIEWS_MAIN);
3840
commandRegistry.registerArgumentProcessor({
3941
processArgument: arg => {
@@ -61,7 +63,7 @@ export class TreeViewsExtImpl implements TreeViewsExt {
6163
throw new Error('Options with treeDataProvider is mandatory');
6264
}
6365

64-
const treeView = new TreeViewExtImpl(plugin, treeViewId, options.treeDataProvider, this.proxy);
66+
const treeView = new TreeViewExtImpl(plugin, treeViewId, options.treeDataProvider, this.proxy, this.commandRegistry.converter);
6567
this.treeViews.set(treeViewId, treeView);
6668

6769
return {
@@ -120,6 +122,8 @@ class TreeViewExtImpl<T> extends Disposable {
120122
private onDidCollapseElementEmitter: Emitter<TreeViewExpansionEvent<T>> = new Emitter<TreeViewExpansionEvent<T>>();
121123
public readonly onDidCollapseElement = this.onDidCollapseElementEmitter.event;
122124

125+
private disposables = new DisposableCollection();
126+
123127
private selection: T[] = [];
124128
get selectedElements(): T[] { return this.selection; }
125129

@@ -129,7 +133,8 @@ class TreeViewExtImpl<T> extends Disposable {
129133
private plugin: Plugin,
130134
private treeViewId: string,
131135
private treeDataProvider: TreeDataProvider<T>,
132-
private proxy: TreeViewsMain) {
136+
private proxy: TreeViewsMain,
137+
readonly commandsConverter: CommandsConverter) {
133138

134139
super(() => {
135140
proxy.$unregisterTreeDataProvider(treeViewId);
@@ -169,6 +174,7 @@ class TreeViewExtImpl<T> extends Disposable {
169174
console.error(`No tree item with id '${parentId}' found.`);
170175
return [];
171176
}
177+
this.disposables.dispose();
172178

173179
// ask data provider for children for cached element
174180
const result = await this.treeDataProvider.getChildren(parent);
@@ -244,9 +250,6 @@ class TreeViewExtImpl<T> extends Disposable {
244250
}
245251
}
246252

247-
if (treeItem.command) {
248-
treeItem.command.arguments = [id];
249-
}
250253
const treeViewItem = {
251254
id,
252255
label,
@@ -257,7 +260,7 @@ class TreeViewExtImpl<T> extends Disposable {
257260
tooltip: treeItem.tooltip,
258261
collapsibleState: treeItem.collapsibleState,
259262
contextValue: treeItem.contextValue,
260-
command: treeItem.command
263+
command: treeItem.command ? toInternalCommand(this.commandsConverter.toSafeCommand(treeItem.command, this.disposables)) : undefined
261264
} as TreeViewItem;
262265

263266
treeItems.push(treeViewItem);

0 commit comments

Comments
 (0)