Skip to content

Commit

Permalink
Implement "headless plugins" in a new plugin host outside of frontend…
Browse files Browse the repository at this point in the history
… connections (#13138)

Define a 'theiaHeadlessPlugin' engine to identify headless plugins
that support only the headless plugin host. Ordinary backend plugins
that also run in the headless host can add a 'headless' entry-point
in the 'theiaPlugin' object in package.json

- update the Theia application webpack generator to handle packing the
  headless-plugin API init script
- implement a headless entry-point in the PluginModel as a peer to
  frontend and backend entry-points
- factor out common plugin manager behaviour into an abstract class
  - define distinct plugin manager (vscode/theia) and headless plugin
    manager implementations
  - similarly for the HostedPluginSupport
- define the minimal TerminalExt/Main APIs for access to the default
  shell to support the 'env.shell' API
- implement nested Inversify container for headless plugins to isolate
  them and their plugin host from the connection-scoped plugins
- add examples for headless plugin
  - add a "Greeting of the Day" example custom API provider
  - add a plugin-gotd example headless plugin that uses the custom API
    example to greet the world on activation and also illustrates
    simple vscode API usage

- Support headless entrypoint in VSCode plugins
  - support the headless entrypoint in an otherwise VS Code plugin
  - prefer the VS Code names for start/stop functions
  - update the example plugin to use be a dual VSCode/headless plugin
    using the vscode API in the usual backend entrypoint

- Support distinct and application-specific headless activation events
  - extend the PluginPackage interface to add a "headless" property
    initially defining only an optional "activationEvents" string array
    property for headless mode. The idea being that eventually other
    things like "contributions" might also be defined, here
  - support application injection of activation events that it will
    trigger in its plugins via the HeadlessHostedPluginSupport class's
    activateByEvent(activationEvent: string) API
  - adapt the example GotD plugin's package manifest to use the new
    "headless" property

- Differentiate provision of ext APIs to headless and backend plugin hosts
  - define a distinct headless ext API initialization function protocol
    and headless API initialization script path to target the headless
    plugin host specifically in the ext API provider
  - refactor the initialization of ext APIs in the plugin host to make use
    of this new distinction
  - update the Greeting-of-the-Day example API provider to support both
    headless and backend plugins
  - define the index for the common plugin-ext-headless API exports
  - fix up other minor details

- Inversify injection in the Plugin Host
  - implement an Inversify container in the plugin hosts
  - inject Ext interfaces and the RPC framework
  - get container modules in plugin entrypoints to configure the container
  - the plugin container module can provide for API initialization in a
    simpler, more reusable way than the current initialization function,
    which is still supported for backwards compatibility

Signed-off-by: Christian W. Damus <[email protected]>
Co-authored-by: Martin Fleck <[email protected]>
Co-authored-by: Mark Sujew <[email protected]>
  • Loading branch information
3 people authored Jan 19, 2024
1 parent b4bbecd commit 56cf3db
Show file tree
Hide file tree
Showing 94 changed files with 4,155 additions and 794 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ for (const [entryPointName, entryPointPath] of Object.entries({
${this.ifPackage('@theia/plugin-ext', "'backend-init-theia': '@theia/plugin-ext/lib/hosted/node/scanners/backend-init-theia',")}
${this.ifPackage('@theia/filesystem', "'nsfw-watcher': '@theia/filesystem/lib/node/nsfw-watcher',")}
${this.ifPackage('@theia/plugin-ext-vscode', "'plugin-vscode-init': '@theia/plugin-ext-vscode/lib/node/plugin-vscode-init',")}
${this.ifPackage('@theia/api-provider-sample', "'gotd-api-init': '@theia/api-provider-sample/lib/plugin/gotd-api-init',")}
})) {
commonJsLibraries[entryPointName] = {
import: require.resolve(entryPointPath),
Expand Down Expand Up @@ -429,6 +430,8 @@ const config = {
'ipc-bootstrap': require.resolve('@theia/core/lib/node/messaging/ipc-bootstrap'),
${this.ifPackage('@theia/plugin-ext', () => `// VS Code extension support:
'plugin-host': require.resolve('@theia/plugin-ext/lib/hosted/node/plugin-host'),`)}
${this.ifPackage('@theia/plugin-ext-headless', () => `// Theia Headless Plugin support:
'plugin-host-headless': require.resolve('@theia/plugin-ext-headless/lib/hosted/node/plugin-host-headless'),`)}
${this.ifPackage('@theia/process', () => `// Make sure the node-pty thread worker can be executed:
'worker/conoutSocketWorker': require.resolve('node-pty/lib/worker/conoutSocketWorker'),`)}
${this.ifPackage('@theia/git', () => `// Ensure the git locator process can the started
Expand Down
10 changes: 10 additions & 0 deletions examples/api-provider-sample/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};
48 changes: 48 additions & 0 deletions examples/api-provider-sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<div align='center'>

<br />

<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />

<h2>ECLIPSE THEIA - API PROVIDER SAMPLE</h2>

<hr />

</div>

## Description

The `@theia/api-provider-sample` extension is a programming example showing how to define and provide a custom API object for _plugins_ to use.
The purpose of the extension is to:
- provide developers with realistic coding examples of providing custom API objects
- provide easy-to-use and test examples for features when reviewing pull requests

The extension is for reference and test purposes only and is not published on `npm` (`private: true`).

### Greeting of the Day

The sample defines a `gotd` API that plugins can import and use to obtain tailored messages with which to greet the world, for example in their activation function.

The source code is laid out in the `src/` tree as follows:

- `gotd.d.ts` — the TypeScript definition of the `gotd` API object that plugins import to interact with the "Greeting of the Day" service
- `plugin/` — the API initialization script and the implementation of the API objects (`GreetingExt` and similar interfaces).
All code in this directory runs exclusively in the separate plugin-host Node process, isolated from the main Theia process, together with either headless plugins or the backend of VS Code plugins.
The `GreetingExtImpl` and similar classes communicate with the actual API implementation (`GreetingMainImpl` etc.) classes in the main Theia process via RPC
- `node/` — the API classes implementing `GreetingMain` and similar interfaces and the Inversify bindings that register the API provider.
All code in this directory runs in the main Theia Node process
- `common/` — the RPC API Ext/Main interface definitions corresponding to the backend of the `gotd` plugin API

## Additional Information

- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)

## License

- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)

## Trademark
"Theia" is a trademark of the Eclipse Foundation
https://www.eclipse.org/theia
42 changes: 42 additions & 0 deletions examples/api-provider-sample/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"private": true,
"name": "@theia/api-provider-sample",
"version": "1.45.0",
"description": "Theia - Example code to demonstrate Theia API Provider Extensions",
"dependencies": {
"@theia/core": "1.45.0",
"@theia/plugin-ext-headless": "1.45.0",
"@theia/plugin-ext": "1.45.0"
},
"theiaExtensions": [
{
"backend": "lib/node/gotd-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"types": "src/gotd.d.ts",
"scripts": {
"lint": "theiaext lint",
"build": "theiaext build",
"watch": "theiaext watch",
"clean": "theiaext clean"
},
"devDependencies": {
"@theia/ext-scripts": "1.45.0"
}
}
70 changes: 70 additions & 0 deletions examples/api-provider-sample/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { createProxyIdentifier } from '@theia/plugin-ext/lib/common/rpc-protocol';
import type { greeting } from '../gotd';
import { Event } from '@theia/core';

export enum GreetingKind {
DIRECT = 1,
QUIRKY = 2,
SNARKY = 3,
}

export interface GreeterData {
readonly uuid: string;
greetingKinds: greeting.GreetingKind[];
};

export const GreetingMain = Symbol('GreetingMain');
export interface GreetingMain {
$getMessage(greeterId: string): Promise<string>;

$createGreeter(): Promise<GreeterData>;
$destroyGreeter(greeterId: GreeterData['uuid']): Promise<void>;

$updateGreeter(data: GreeterData): void;
}

export const GreetingExt = Symbol('GreetingExt');
export interface GreetingExt {

//
// External protocol
//

registerGreeter(): Promise<string>;
unregisterGreeter(uuid: string): Promise<void>;

getMessage(greeterId: string): Promise<string>;
getGreetingKinds(greeterId: string): readonly greeting.GreetingKind[];
setGreetingKindEnabled(greeterId: string, greetingKind: greeting.GreetingKind, enable: boolean): void;
onGreetingKindsChanged(greeterId: string): Event<readonly greeting.GreetingKind[]>;

//
// Internal protocol
//

$greeterUpdated(data: GreeterData): void;

}

export const PLUGIN_RPC_CONTEXT = {
GREETING_MAIN: createProxyIdentifier<GreetingMain>('GreetingMain'),
};

export const MAIN_RPC_CONTEXT = {
GREETING_EXT: createProxyIdentifier<GreetingExt>('GreetingExt'),
};
49 changes: 49 additions & 0 deletions examples/api-provider-sample/src/gotd.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************

// Strictly speaking, the 'greeting' namespace is an unnecessary level of organization
// but it serves to illustrate how API namespaces are implemented in the backend.
export namespace greeting {
export function createGreeter(): Promise<greeting.Greeter>;

export enum GreetingKind {
DIRECT = 1,
QUIRKY = 2,
SNARKY = 3,
}

export interface Greeter extends Disposable {
greetingKinds: readonly GreetingKind[];

getMessage(): Promise<string>;

setGreetingKind(kind: GreetingKind, enable = true): void;

onGreetingKindsChanged: Event<readonly GreetingKind[]>;
}
}

export interface Event<T> {
(listener: (e: T) => unknown, thisArg?: unknown): Disposable;
}

export interface Disposable {
dispose(): void;
}

namespace Disposable {
export function create(func: () => void): Disposable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as path from 'path';
import { injectable } from '@theia/core/shared/inversify';
import { ExtPluginApi, ExtPluginApiProvider } from '@theia/plugin-ext-headless';

@injectable()
export class ExtPluginGotdApiProvider implements ExtPluginApiProvider {
provideApi(): ExtPluginApi {
// We can support both backend plugins and headless plugins, so we have only one
// entry-point script. Moreover, the application build packages that script in
// the `../backend/` directory from its source `../plugin/` location, alongside
// the scripts for all other plugin API providers.
const universalInitPath = path.join(__dirname, '../backend/gotd-api-init');
return {
backendInitPath: universalInitPath,
headlessInitPath: universalInitPath
};
}
}
28 changes: 28 additions & 0 deletions examples/api-provider-sample/src/node/gotd-backend-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { ExtPluginApiProvider } from '@theia/plugin-ext';
import { ExtPluginGotdApiProvider } from './ext-plugin-gotd-api-provider';
import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution';
import { GotdMainPluginApiProvider } from './gotd-main-plugin-provider';
import { GreetingMain } from '../common/plugin-api-rpc';
import { GreetingMainImpl } from './greeting-main-impl';

export default new ContainerModule(bind => {
bind(Symbol.for(ExtPluginApiProvider)).to(ExtPluginGotdApiProvider).inSingletonScope();
bind(MainPluginApiProvider).to(GotdMainPluginApiProvider).inSingletonScope();
bind(GreetingMain).to(GreetingMainImpl).inSingletonScope();
});
29 changes: 29 additions & 0 deletions examples/api-provider-sample/src/node/gotd-main-plugin-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { GreetingMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';

@injectable()
export class GotdMainPluginApiProvider implements MainPluginApiProvider {
@inject(GreetingMain)
protected readonly greetingMain: GreetingMain;

initialize(rpc: RPCProtocol): void {
rpc.set(PLUGIN_RPC_CONTEXT.GREETING_MAIN, this.greetingMain);
}
}
Loading

0 comments on commit 56cf3db

Please sign in to comment.