Skip to content

Commit 13c37ce

Browse files
committed
ui: add API for command filtering for embedders
In some applications that embed the Perfetto UI, there are a number of commands that are not needed. For example, in an application that manages the opening of traces into instances of the Perfetto UI, there is no need for the commands that open and close traces. And other commands may overlap or conflict with other capabilities provided by the host application, which therefore may wish to exclude those commands. Add an API to the Registry class to permit an embedding application to install a filter that quietly prevents registration of unwanted services. Employ this capability in the CommandManager. Signed-off-by: Christian W. Damus <cdamus.ext@eclipsesource.com>
1 parent 541e23c commit 13c37ce

File tree

4 files changed

+153
-1
lines changed

4 files changed

+153
-1
lines changed

ui/src/base/registry.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class RegistryError extends Error {
2626
export class Registry<T> {
2727
private key: (t: T) => string;
2828
protected registry: Map<string, T>;
29+
private keyFilter: (key: string) => boolean = () => true;
2930

3031
static kindRegistry<T extends HasKind>(): Registry<T> {
3132
return new Registry<T>((t) => t.kind);
@@ -36,8 +37,23 @@ export class Registry<T> {
3637
this.key = key;
3738
}
3839

40+
set filter(filter: ((key: string) => boolean) | undefined) {
41+
this.keyFilter = filter ?? (() => true);
42+
43+
// Run the filter to knock out anything already registered that does not pass it
44+
[...this.registry.keys()]
45+
.filter((key) => !this.keyFilter(key))
46+
.forEach((key) => this.registry.delete(key));
47+
}
48+
3949
register(registrant: T): Disposable {
4050
const kind = this.key(registrant);
51+
if (!this.keyFilter(kind)) {
52+
// Simply refuse to register the entry
53+
return {
54+
[Symbol.dispose]: () => undefined,
55+
};
56+
}
4157
if (this.registry.has(kind)) {
4258
throw new RegistryError(
4359
`Registrant ${kind} already exists in the registry`,
@@ -85,8 +101,14 @@ export class Registry<T> {
85101
// A proxy is not sufficient because we need non-overridden
86102
// methods to delegate to overridden methods.
87103
const result = new (class ChildRegistry extends Registry<T> {
88-
constructor (private readonly parent: Registry<T>) {
104+
constructor(private readonly parent: Registry<T>) {
89105
super(parent.key);
106+
this.keyFilter = parent.keyFilter;
107+
}
108+
109+
override set filter(filter: ((key: string) => boolean) | undefined) {
110+
this.parent.filter = filter;
111+
super.filter = filter;
90112
}
91113

92114
override has(kind: string): boolean {

ui/src/base/registry_unittest.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,37 @@ test('registry allows iteration', () => {
5959
expect(values.includes(b)).toBe(true);
6060
});
6161

62+
describe('registry filter', () => {
63+
test('prevents registration of non-matching kinds', () => {
64+
const registry = Registry.kindRegistry<Registrant>();
65+
registry.filter = (key) => key.startsWith('a');
66+
67+
const alpha: Registrant = {kind: 'alpha', n: 1};
68+
const beta: Registrant = {kind: 'beta', n: 2};
69+
registry.register(alpha);
70+
registry.register(beta);
71+
72+
expect(registry.has('alpha')).toBe(true);
73+
expect(registry.has('beta')).toBe(false);
74+
expect(() => registry.get('beta')).toThrow();
75+
expect(registry.get('alpha')).toBe(alpha);
76+
});
77+
78+
test('removes extant non-matching registrations', () => {
79+
const registry = Registry.kindRegistry<Registrant>();
80+
const a: Registrant = {kind: 'alpha', n: 1};
81+
const b: Registrant = {kind: 'beta', n: 2};
82+
registry.register(a);
83+
registry.register(b);
84+
85+
registry.filter = (key) => key.startsWith('a');
86+
87+
expect(registry.has('alpha')).toBe(true);
88+
expect(registry.has('beta')).toBe(false);
89+
expect(() => registry.get('beta')).toThrow();
90+
});
91+
});
92+
6293
describe('Hierarchical (child) registries', () => {
6394
test('inheritance of registrations', () => {
6495
const parent = Registry.kindRegistry<Registrant>();
@@ -176,4 +207,60 @@ describe('Hierarchical (child) registries', () => {
176207

177208
expect(child).not.toHaveProperty('id');
178209
});
210+
211+
describe('registry filter', () => {
212+
test('child inherits parent filter', () => {
213+
const parent = Registry.kindRegistry<Registrant>();
214+
parent.filter = (key) => key.startsWith('x');
215+
216+
const child = parent.createChild();
217+
218+
const childOk: Registrant = {kind: 'xyz', n: 1};
219+
const childNok: Registrant = {kind: 'abc', n: 2};
220+
child.register(childOk);
221+
child.register(childNok);
222+
223+
const parentOk: Registrant = {kind: 'xenon', n: 3};
224+
const parentNok: Registrant = {kind: 'beta', n: 4};
225+
parent.register(parentOk);
226+
parent.register(parentNok);
227+
228+
expect(child.has('xyz')).toBe(true);
229+
expect(child.has('abc')).toBe(false);
230+
expect(child.get('xyz')).toBe(childOk);
231+
232+
// Other registrations still accessible (or not) as usual
233+
expect(child.get('xenon')).toBe(parentOk);
234+
expect(parent.has('beta')).toBe(false);
235+
expect(() => child.get('beta')).toThrow();
236+
});
237+
238+
test('setting filter on child cascades to parent and prunes both registries', () => {
239+
const parent = Registry.kindRegistry<Registrant>();
240+
const a: Registrant = {kind: 'a', n: 1};
241+
const b: Registrant = {kind: 'b', n: 2};
242+
parent.register(a);
243+
parent.register(b);
244+
245+
const child = parent.createChild();
246+
const c: Registrant = {kind: 'c', n: 3};
247+
const d: Registrant = {kind: 'd', n: 4};
248+
child.register(c);
249+
child.register(d);
250+
251+
child.filter = (key) => key === 'b' || key === 'd';
252+
253+
// Parent pruned
254+
expect(parent.has('a')).toBe(false);
255+
expect(parent.has('b')).toBe(true);
256+
257+
// Child pruned
258+
expect(child.has('c')).toBe(false);
259+
expect(child.has('d')).toBe(true);
260+
261+
// Other registrations still accessible (or not) as usual
262+
expect(child.get('b')).toBe(b);
263+
expect(() => parent.get('d')).toThrow();
264+
});
265+
});
179266
});

ui/src/core/command_manager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ export class CommandManagerImpl implements CommandManager {
8282
return new CommandManagerImpl(this.registry);
8383
}
8484

85+
/**
86+
* Set a filter to screen command registrations. Command IDs that do not
87+
* pass the filter are not registered. This is distinct from the
88+
* `allowlistCheck` function, which screens out start-up commands that
89+
* should be skipped.
90+
*/
91+
set filter(filter: ((commandId: string) => boolean) | undefined) {
92+
this.registry.filter = filter;
93+
}
94+
8595
getCommand(commandId: string): Command {
8696
return this.registry.get(commandId);
8797
}

ui/src/core/command_manager_unittest.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,36 @@ describe('CommandManagerImpl child manager', () => {
3838
expect(parent.hasCommand('child')).toBe(false);
3939
});
4040
});
41+
42+
describe('CommandManagerImpl filtering', () => {
43+
test('filters existing and future registrations', () => {
44+
const parent = new CommandManagerImpl();
45+
46+
const allowCmd = TestCommand('allow');
47+
const denyCmd = TestCommand('deny');
48+
49+
parent.registerCommand(allowCmd);
50+
parent.registerCommand(denyCmd);
51+
52+
const mgr = parent.createChild();
53+
expect(mgr.hasCommand('allow')).toBe(true);
54+
expect(mgr.hasCommand('deny')).toBe(true);
55+
56+
mgr.filter = (id) => id.startsWith('allow');
57+
58+
// Extant non-matching commands are purged
59+
expect(mgr.hasCommand('allow')).toBe(true);
60+
expect(mgr.hasCommand('deny')).toBe(false);
61+
62+
// New registrations are screened by the filter.
63+
mgr.registerCommand(TestCommand('deny'));
64+
expect(mgr.hasCommand('deny')).toBe(false);
65+
mgr.registerCommand(TestCommand('allowed'));
66+
expect(mgr.hasCommand('allowed')).toBe(true);
67+
68+
// Clearing the filter allows registering the previously denied command.
69+
mgr.filter = undefined;
70+
mgr.registerCommand(TestCommand('deny'));
71+
expect(mgr.hasCommand('deny')).toBe(true);
72+
});
73+
});

0 commit comments

Comments
 (0)