Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement "headless plugins" in a new plugin host outside of frontend connections #13138

Merged
merged 3 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

@jcortell68 jcortell68 Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the separate plugin-host Node process

I wonder if the use of the will mislead the reader into thinking there is only one plugin-host Node process. Using a may equally mislead the reader in thinking that the code will run in a Node process dedicated to that code. Of course, neither is true.

  • there is a single plugin-host process for all headless plugins running in an app
  • there is a plugin-host process for each frontend that attaches to the app's backend. Each one houses all traditional VS Code plugins

How to succinctly express that here...oof. I leave that up to you. 😅

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');
jfaltermeier marked this conversation as resolved.
Show resolved Hide resolved
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();
Copy link
Contributor

@jcortell68 jcortell68 Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we specify here that this Ext API provider should be providing an API object only in the headless plugin-host and not the traditional plugin-host? In other words, if this api-provider-sample Theia Extension wanted to contribute namespace ABC to traditional plugins (VS Code Extensions) and namespace DEF to headless plugins, is that not possible?

Copy link
Contributor

@jcortell68 jcortell68 Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I dove further into having a single Theia Extension provide different API namespaces for traditional plugins vs headless ones, I see that it's not really feasible. So, I think the moral of the story here is: in that situation, the Theia application developer should have two Theia Extensions--one that provides the traditional API, one that provides the headless API. This really isn't a burden and it keeps things simple (better organized). But I guess the question I posed above is relevant even in that approach. How do I specify that a ExtPluginApiProvider should be used in one plugin-host vs the other (traditional vs headless)

Copy link
Contributor

@jcortell68 jcortell68 Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve experimented with this PR and I believe I've confirmed my suspicions. There is no scoping of API providers to a specific plugin-host (normal vs headless). A contributed API provider is instantiated and exercised in both types of plugin-hosts. This seems problematic to me. It means a headless plugin can do an import of a normal (non-headless) custom API module and the corresponding API provider running in the headless plugin-host will give it to him--even if it makes no sense. The headless plugin will end up with an API object whose calls will never complete because the Ext object has a proxy to something that has no Main on the other end.

But “wait”, you say. “Why is a headless plugin trying to interact with the UI? That’s not what headless plugins are for.” Absolutely. The current implementation in this PR is not responsible for that nonsense. It is, however, responsible for allowing the nonsense to get too far. The import of the normal API object from a headless plugin should fail but it doesn’t.

There is a case where this actually would be desirable, though. Technically speaking, an API object need not interact with anything on the mainland. An app developer could theoretically provide an API object whose implementation can be carried out entirely in the plugin-host–i.e., one that does not make use of RPC. E.g., a Math API object, as silly as that would be. In that case, there would be an argument for an API provider to allow itself to be available for both types of plugins: normal and headless. Should we support that? If so…sure; that’s fine. But we still need the scoping capability to prevent it in the 99% of situations, where it’s not going to make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The distinction of which plugin host an API provider may publish its API should now be possible with commit f100932.

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
Loading