deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,31 @@
<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 - PLUGIN-DEVELOPMENT EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/plugin-dev` extension contributes functionality for the `plugin host`.
## Additional Information
- [API documentation for `@theia/plugin-dev`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_plugin-dev.html)
- [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>

View File

@@ -0,0 +1,57 @@
{
"name": "@theia/plugin-dev",
"version": "1.68.0",
"description": "Theia - Plugin Development Extension",
"main": "lib/common/index.js",
"typings": "lib/common/index.d.ts",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/debug": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/output": "1.68.0",
"@theia/plugin-ext": "1.68.0",
"@theia/workspace": "1.68.0",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"backend": "lib/node/plugin-dev-backend-module",
"backendElectron": "lib/node-electron/plugin-dev-electron-backend-module",
"frontend": "lib/browser/plugin-dev-frontend-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"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,356 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from '@theia/core/shared/inversify';
import { StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
import { StatusBarAlignment, StatusBarEntry, FrontendApplicationContribution, codicon } from '@theia/core/lib/browser';
import { MessageService, PreferenceChange, PreferenceServiceImpl } from '@theia/core/lib/common';
import { CommandRegistry } from '@theia/core/shared/@lumino/commands';
import { Menu } from '@theia/core/shared/@lumino/widgets';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ConnectionStatusService, ConnectionStatus } from '@theia/core/lib/browser/connection-status-service';
import { PluginDevServer } from '../common/plugin-dev-protocol';
import { HostedPluginManagerClient, HostedInstanceState, HostedPluginCommands, HostedInstanceData } from './hosted-plugin-manager-client';
import { HostedPluginLogViewer } from './hosted-plugin-log-viewer';
import { HostedPluginPreferences } from '../common/hosted-plugin-preferences';
import { nls } from '@theia/core/lib/common/nls';
/**
* Adds a status bar element displaying the state of secondary Theia instance with hosted plugin and
* allows controlling the instance by simple clicking on the status bar element.
*/
@injectable()
export class HostedPluginController implements FrontendApplicationContribution {
public static readonly HOSTED_PLUGIN = 'hosted-plugin';
public static readonly HOSTED_PLUGIN_OFFLINE = 'hosted-plugin-offline';
public static readonly HOSTED_PLUGIN_FAILED = 'hosted-plugin-failed';
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(FrontendApplicationStateService)
protected readonly frontendApplicationStateService: FrontendApplicationStateService;
@inject(PluginDevServer)
protected readonly hostedPluginServer: PluginDevServer;
@inject(HostedPluginManagerClient)
protected readonly hostedPluginManagerClient: HostedPluginManagerClient;
@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;
@inject(HostedPluginLogViewer)
protected readonly hostedPluginLogViewer: HostedPluginLogViewer;
@inject(HostedPluginPreferences)
protected readonly hostedPluginPreferences: HostedPluginPreferences;
@inject(PreferenceServiceImpl)
protected readonly preferenceService: PreferenceServiceImpl;
@inject(MessageService)
protected readonly messageService: MessageService;
private pluginState: HostedInstanceState = HostedInstanceState.STOPPED;
// used only for displaying Running instead of Watching in status bar if run of watcher fails
private watcherSuccess: boolean;
private entry: StatusBarEntry | undefined;
public initialize(): void {
this.hostedPluginServer.getHostedPlugin().then(pluginMetadata => {
if (!pluginMetadata) {
this.frontendApplicationStateService.reachedState('ready').then(() => {
// handles status bar item
this.hostedPluginManagerClient.onStateChanged(e => {
if (e.state === HostedInstanceState.STARTING) {
this.onHostedPluginStarting();
} else if (e.state === HostedInstanceState.RUNNING) {
this.onHostedPluginRunning();
} else if (e.state === HostedInstanceState.STOPPED) {
this.onHostedPluginStopped();
} else if (e.state === HostedInstanceState.FAILED) {
this.onHostedPluginFailed();
}
});
// handles watch compilation
this.hostedPluginManagerClient.onStateChanged(e => this.handleWatchers(e));
// updates status bar if page is loading when hosted instance is already running
this.hostedPluginServer.isHostedPluginInstanceRunning().then(running => {
if (running) {
this.onHostedPluginRunning();
}
});
});
this.connectionStatusService.onStatusChange(() => this.onConnectionStatusChanged());
this.preferenceService.onPreferenceChanged(preference => this.onPreferencesChanged(preference));
} else {
console.error(`Need to load plugin ${pluginMetadata.model.id}`);
}
});
}
/**
* Display status bar element for stopped plugin.
*/
protected async onHostedPluginStopped(): Promise<void> {
this.pluginState = HostedInstanceState.STOPPED;
this.entry = {
text: `${nls.localize('theia/plugin-dev/hostedPluginStopped', 'Hosted Plugin: Stopped')} $(angle-up)`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
onclick: e => {
this.showMenu(e.clientX, e.clientY);
}
};
this.entry.className = HostedPluginController.HOSTED_PLUGIN;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}
/**
* Display status bar element for starting plugin.
*/
protected async onHostedPluginStarting(): Promise<void> {
this.pluginState = HostedInstanceState.STARTING;
this.hostedPluginLogViewer.showLogConsole();
this.entry = {
text: `$(cog~spin) ${nls.localize('theia/plugin-dev/hostedPluginStarting', 'Hosted Plugin: Starting')}`,
alignment: StatusBarAlignment.LEFT,
priority: 100
};
this.entry.className = HostedPluginController.HOSTED_PLUGIN;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}
/**
* Display status bar element for running plugin.
*/
protected async onHostedPluginRunning(): Promise<void> {
this.pluginState = HostedInstanceState.RUNNING;
let entryText: string;
if (this.hostedPluginPreferences['hosted-plugin.watchMode'] && this.watcherSuccess) {
entryText = `$(cog~spin) ${nls.localize('theia/plugin-dev/hostedPluginWatching', 'Hosted Plugin: Watching')}$(angle-up)`;
} else {
entryText = `$(cog~spin) ${nls.localize('theia/plugin-dev/hostedPluginRunning', 'Hosted Plugin: Running')} $(angle-up)`;
}
this.entry = {
text: entryText,
alignment: StatusBarAlignment.LEFT,
priority: 100,
onclick: e => {
this.showMenu(e.clientX, e.clientY);
}
};
this.entry.className = HostedPluginController.HOSTED_PLUGIN;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}
/**
* Display status bar element for failed plugin.
*/
protected async onHostedPluginFailed(): Promise<void> {
this.pluginState = HostedInstanceState.FAILED;
this.entry = {
text: `${nls.localize('theia/plugin-dev/hostedPluginStopped', 'Hosted Plugin: Stopped')} $(angle-up)`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
onclick: e => {
this.showMenu(e.clientX, e.clientY);
}
};
this.entry.className = HostedPluginController.HOSTED_PLUGIN_FAILED;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}
protected async onPreferencesChanged(preference: PreferenceChange): Promise<void> {
if (preference.preferenceName === 'hosted-plugin.watchMode') {
if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
const pluginLocation = await this.hostedPluginServer.getHostedPluginURI();
const isWatchCompilationRunning = await this.hostedPluginServer.isWatchCompilationRunning(pluginLocation);
if (this.hostedPluginPreferences['hosted-plugin.watchMode']) {
if (!isWatchCompilationRunning) {
await this.runWatchCompilation(pluginLocation.toString());
}
} else {
if (isWatchCompilationRunning) {
await this.hostedPluginServer.stopWatchCompilation(pluginLocation.toString());
}
}
// update status bar
this.onHostedPluginRunning();
}
}
}
/**
* Starts / stops watchers on hosted instance state change.
*
* @param event hosted instance state change event
*/
protected async handleWatchers(event: HostedInstanceData): Promise<void> {
if (event.state === HostedInstanceState.RUNNING) {
if (this.hostedPluginPreferences['hosted-plugin.watchMode']) {
await this.runWatchCompilation(event.pluginLocation.toString());
// update status bar
this.onHostedPluginRunning();
}
} else if (event.state === HostedInstanceState.STOPPING) {
if (this.hostedPluginPreferences['hosted-plugin.watchMode']) {
const isRunning = await this.hostedPluginServer.isWatchCompilationRunning(event.pluginLocation.toString());
if (isRunning) {
try {
await this.hostedPluginServer.stopWatchCompilation(event.pluginLocation.toString());
} catch (error) {
this.messageService.error(this.getErrorMessage(error));
}
}
}
}
}
private async runWatchCompilation(pluginLocation: string): Promise<void> {
try {
await this.hostedPluginServer.runWatchCompilation(pluginLocation);
this.watcherSuccess = true;
} catch (error) {
this.messageService.error(this.getErrorMessage(error));
this.watcherSuccess = false;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private getErrorMessage(error: any): string {
return error?.message?.substring(error.message.indexOf(':') + 1) || '';
}
/**
* Updating status bar element when changing connection status.
*/
private onConnectionStatusChanged(): void {
if (this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE) {
// Re-set the element only if it's visible on status bar
if (this.entry) {
const offlineElement = {
text: nls.localize('theia/plugin-dev/hostedPluginStopped', 'Hosted Plugin: Stopped'),
alignment: StatusBarAlignment.LEFT,
priority: 100
};
this.entry.className = HostedPluginController.HOSTED_PLUGIN_OFFLINE;
this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, offlineElement);
}
} else {
// ask state of hosted plugin when switching to Online
if (this.entry) {
this.hostedPluginServer.isHostedPluginInstanceRunning().then(running => {
if (running) {
this.onHostedPluginRunning();
} else {
this.onHostedPluginStopped();
}
});
}
}
}
/**
* Show menu containing actions to start/stop/restart hosted plugin.
*/
protected showMenu(x: number, y: number): void {
const commands = new CommandRegistry();
const menu = new Menu({
commands
});
if (this.pluginState === HostedInstanceState.RUNNING) {
this.addCommandsForRunningPlugin(commands, menu);
} else if (this.pluginState === HostedInstanceState.STOPPED || this.pluginState === HostedInstanceState.FAILED) {
this.addCommandsForStoppedPlugin(commands, menu);
}
menu.open(x, y);
}
/**
* Adds commands to the menu for running plugin.
*/
protected addCommandsForRunningPlugin(commands: CommandRegistry, menu: Menu): void {
commands.addCommand(HostedPluginCommands.STOP.id, {
label: nls.localize('theia/plugin-dev/stopInstance', 'Stop Instance'),
iconClass: codicon('debug-stop'),
execute: () => setTimeout(() => this.hostedPluginManagerClient.stop(), 100)
});
menu.addItem({
type: 'command',
command: HostedPluginCommands.STOP.id
});
commands.addCommand(HostedPluginCommands.RESTART.id, {
label: nls.localize('theia/plugin-dev/restartInstance', 'Restart Instance'),
iconClass: codicon('debug-restart'),
execute: () => setTimeout(() => this.hostedPluginManagerClient.restart(), 100)
});
menu.addItem({
type: 'command',
command: HostedPluginCommands.RESTART.id
});
}
/**
* Adds command to the menu for stopped plugin.
*/
protected addCommandsForStoppedPlugin(commands: CommandRegistry, menu: Menu): void {
commands.addCommand(HostedPluginCommands.START.id, {
label: nls.localize('theia/plugin-dev/startInstance', 'Start Instance'),
iconClass: codicon('play'),
execute: () => setTimeout(() => this.hostedPluginManagerClient.start(), 100)
});
menu.addItem({
type: 'command',
command: HostedPluginCommands.START.id
});
commands.addCommand(HostedPluginCommands.DEBUG.id, {
label: nls.localize('theia/plugin-dev/debugInstance', 'Debug Instance'),
iconClass: codicon('debug'),
execute: () => setTimeout(() => this.hostedPluginManagerClient.debug(), 100)
});
menu.addItem({
type: 'command',
command: HostedPluginCommands.DEBUG.id
});
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { injectable, inject } from '@theia/core/shared/inversify';
import { CommandRegistry, CommandContribution } from '@theia/core/lib/common';
import { HostedPluginManagerClient, HostedPluginCommands } from './hosted-plugin-manager-client';
@injectable()
export class HostedPluginFrontendContribution implements CommandContribution {
@inject(HostedPluginManagerClient)
protected readonly hostedPluginManagerClient: HostedPluginManagerClient;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(HostedPluginCommands.START, {
execute: () => this.hostedPluginManagerClient.start()
});
commands.registerCommand(HostedPluginCommands.DEBUG, {
execute: () => this.hostedPluginManagerClient.debug()
});
commands.registerCommand(HostedPluginCommands.STOP, {
execute: () => this.hostedPluginManagerClient.stop()
});
commands.registerCommand(HostedPluginCommands.RESTART, {
execute: () => this.hostedPluginManagerClient.restart()
});
commands.registerCommand(HostedPluginCommands.SELECT_PATH, {
execute: () => this.hostedPluginManagerClient.selectPluginPath()
});
}
}

View File

@@ -0,0 +1,93 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from '@theia/core/shared/inversify';
import { StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
import { StatusBarAlignment, StatusBarEntry, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginDevServer } from '../common/plugin-dev-protocol';
import { ConnectionStatusService, ConnectionStatus } from '@theia/core/lib/browser/connection-status-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { nls } from '@theia/core/lib/common/nls';
import { WindowTitleService } from '@theia/core/lib/browser/window/window-title-service';
/**
* Informs the user whether Theia is running with hosted plugin.
* Adds 'Development Host' status bar element and appends the same prefix to window title.
*/
@injectable()
export class HostedPluginInformer implements FrontendApplicationContribution {
public static readonly DEVELOPMENT_HOST_TITLE = nls.localize('theia/plugin-dev/devHost', 'Development Host');
public static readonly DEVELOPMENT_HOST = 'development-host';
public static readonly DEVELOPMENT_HOST_OFFLINE = 'development-host-offline';
private entry: StatusBarEntry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(PluginDevServer)
protected readonly hostedPluginServer: PluginDevServer;
@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;
@inject(FrontendApplicationStateService)
protected readonly frontendApplicationStateService: FrontendApplicationStateService;
@inject(WindowTitleService)
protected readonly windowTitleService: WindowTitleService;
public initialize(): void {
this.hostedPluginServer.getHostedPlugin().then(pluginMetadata => {
if (pluginMetadata) {
this.windowTitleService.update({
developmentHost: HostedPluginInformer.DEVELOPMENT_HOST_TITLE
});
this.entry = {
text: `$(cube) ${HostedPluginInformer.DEVELOPMENT_HOST_TITLE}`,
tooltip: `${nls.localize('theia/plugin-dev/hostedPlugin', 'Hosted Plugin')} '${pluginMetadata.model.name}'`,
alignment: StatusBarAlignment.LEFT,
priority: 100
};
this.frontendApplicationStateService.reachedState('ready').then(() => {
this.updateStatusBarElement();
});
this.connectionStatusService.onStatusChange(() => this.updateStatusBarElement());
}
});
}
private updateStatusBarElement(): void {
if (this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE) {
this.entry.className = HostedPluginInformer.DEVELOPMENT_HOST_OFFLINE;
} else {
this.entry.className = HostedPluginInformer.DEVELOPMENT_HOST;
}
this.statusBar.setElement(HostedPluginInformer.DEVELOPMENT_HOST, this.entry);
}
}

View File

@@ -0,0 +1,52 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { OutputChannel, OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { LogPart } from '@theia/plugin-ext/lib/common/types';
import { HostedPluginWatcher } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin-watcher';
@injectable()
export class HostedPluginLogViewer {
public static OUTPUT_CHANNEL_NAME = 'hosted-instance-log';
@inject(HostedPluginWatcher)
protected readonly watcher: HostedPluginWatcher;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
protected channel: OutputChannel;
showLogConsole(): void {
this.outputContribution.openView({ reveal: true }).then(view => {
view.activate();
});
}
@postConstruct()
protected init(): void {
this.channel = this.outputChannelManager.getChannel(HostedPluginLogViewer.OUTPUT_CHANNEL_NAME);
this.watcher.onLogMessageEvent(event => this.logMessageEventHandler(event));
}
protected logMessageEventHandler(event: LogPart): void {
this.channel.appendLine(event.data);
}
}

View File

@@ -0,0 +1,434 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Path } from '@theia/core/lib/common/path';
import { MessageService, Command, Emitter, Event } from '@theia/core/lib/common';
import { LabelProvider, isNative, AbstractDialog } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { PluginDebugConfiguration, PluginDevServer } from '../common/plugin-dev-protocol';
import { LaunchVSCodeArgument, LaunchVSCodeRequest, LaunchVSCodeResult } from '@theia/debug/lib/browser/debug-contribution';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { HostedPluginPreferences } from '../common/hosted-plugin-preferences';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
import { nls } from '@theia/core/lib/common/nls';
/**
* Commands to control Hosted plugin instances.
*/
export namespace HostedPluginCommands {
const HOSTED_PLUGIN_CATEGORY_KEY = 'theia/plugin-dev/hostedPlugin';
const HOSTED_PLUGIN_CATEGORY = 'Hosted Plugin';
export const START = Command.toLocalizedCommand({
id: 'hosted-plugin:start',
category: HOSTED_PLUGIN_CATEGORY,
label: 'Start Instance'
}, 'theia/plugin-dev/startInstance', HOSTED_PLUGIN_CATEGORY_KEY);
export const DEBUG = Command.toLocalizedCommand({
id: 'hosted-plugin:debug',
category: HOSTED_PLUGIN_CATEGORY,
label: 'Debug Instance'
}, 'theia/plugin-dev/debugInstance', HOSTED_PLUGIN_CATEGORY_KEY);
export const STOP = Command.toLocalizedCommand({
id: 'hosted-plugin:stop',
category: HOSTED_PLUGIN_CATEGORY,
label: 'Stop Instance'
}, 'theia/plugin-dev/stopInstance', HOSTED_PLUGIN_CATEGORY_KEY);
export const RESTART = Command.toLocalizedCommand({
id: 'hosted-plugin:restart',
category: HOSTED_PLUGIN_CATEGORY,
label: 'Restart Instance'
}, 'theia/plugin-dev/restartInstance', HOSTED_PLUGIN_CATEGORY_KEY);
export const SELECT_PATH = Command.toLocalizedCommand({
id: 'hosted-plugin:select-path',
category: HOSTED_PLUGIN_CATEGORY,
label: 'Select Path'
}, 'theia/plugin-dev/selectPath', HOSTED_PLUGIN_CATEGORY_KEY);
}
/**
* Available states of hosted plugin instance.
*/
export enum HostedInstanceState {
STOPPED = 'stopped',
STARTING = 'starting',
RUNNING = 'running',
STOPPING = 'stopping',
FAILED = 'failed'
}
export interface HostedInstanceData {
state: HostedInstanceState;
pluginLocation: URI;
}
/**
* Responsible for UI to set up and control Hosted Plugin Instance.
*/
@injectable()
export class HostedPluginManagerClient {
private openNewTabAskDialog: OpenHostedInstanceLinkDialog;
private connection: DebugSessionConnection;
// path to the plugin on the file system
protected pluginLocation: URI | undefined;
// URL to the running plugin instance
protected pluginInstanceURL: string | undefined;
protected isDebug = false;
protected readonly stateChanged = new Emitter<HostedInstanceData>();
get onStateChanged(): Event<HostedInstanceData> {
return this.stateChanged.event;
}
@inject(PluginDevServer)
protected readonly hostedPluginServer: PluginDevServer;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(EnvVariablesServer)
protected readonly environments: EnvVariablesServer;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(DebugSessionManager)
protected readonly debugSessionManager: DebugSessionManager;
@inject(HostedPluginPreferences)
protected readonly hostedPluginPreferences: HostedPluginPreferences;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
this.openNewTabAskDialog = new OpenHostedInstanceLinkDialog(this.windowService);
// is needed for case when page is loaded when hosted instance is already running.
if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
this.pluginLocation = new URI(await this.hostedPluginServer.getHostedPluginURI());
}
}
get lastPluginLocation(): string | undefined {
if (this.pluginLocation) {
return this.pluginLocation.toString();
}
return undefined;
}
async start(debugConfig?: PluginDebugConfiguration): Promise<void> {
if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
this.messageService.warn(nls.localize('theia/plugin-dev/alreadyRunning', 'Hosted instance is already running.'));
return;
}
if (!this.pluginLocation) {
await this.selectPluginPath();
if (!this.pluginLocation) {
// selection was cancelled
return;
}
}
try {
this.stateChanged.fire({ state: HostedInstanceState.STARTING, pluginLocation: this.pluginLocation });
this.messageService.info(nls.localize('theia/plugin-dev/starting', 'Starting hosted instance server ...'));
if (debugConfig) {
this.isDebug = true;
this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation.toString(), debugConfig);
} else {
this.isDebug = false;
this.pluginInstanceURL = await this.hostedPluginServer.runHostedPluginInstance(this.pluginLocation.toString());
}
await this.openPluginWindow();
this.messageService.info(`${nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:')} ${this.pluginInstanceURL}`);
this.stateChanged.fire({ state: HostedInstanceState.RUNNING, pluginLocation: this.pluginLocation });
} catch (error) {
this.messageService.error(nls.localize('theia/plugin-dev/failed', 'Failed to run hosted plugin instance: {0}', this.getErrorMessage(error)));
this.stateChanged.fire({ state: HostedInstanceState.FAILED, pluginLocation: this.pluginLocation });
this.stop();
}
}
async debug(config?: PluginDebugConfiguration): Promise<string | undefined> {
await this.start(this.setDebugConfig(config));
await this.startDebugSessionManager();
return this.pluginInstanceURL;
}
async startDebugSessionManager(): Promise<void> {
let outFiles: string[] | undefined = undefined;
if (this.pluginLocation && this.hostedPluginPreferences['hosted-plugin.launchOutFiles'].length > 0) {
const fsPath = await this.fileService.fsPath(this.pluginLocation);
if (fsPath) {
outFiles = this.hostedPluginPreferences['hosted-plugin.launchOutFiles'].map(outFile =>
outFile.replace('${pluginPath}', new Path(fsPath).toString())
);
}
}
const name = nls.localize('theia/plugin-dev/hostedPlugin', 'Hosted Plugin');
await this.debugSessionManager.start({
name,
configuration: {
type: 'node',
request: 'attach',
timeout: 30000,
name,
smartStep: true,
sourceMaps: !!outFiles,
outFiles
}
});
}
async stop(checkRunning: boolean = true): Promise<void> {
if (checkRunning && !await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
this.messageService.warn(nls.localize('theia/plugin-dev/notRunning', 'Hosted instance is not running.'));
return;
}
try {
this.stateChanged.fire({ state: HostedInstanceState.STOPPING, pluginLocation: this.pluginLocation! });
await this.hostedPluginServer.terminateHostedPluginInstance();
this.messageService.info((this.pluginInstanceURL
? nls.localize('theia/plugin-dev/instanceTerminated', '{0} has been terminated', this.pluginInstanceURL)
: nls.localize('theia/plugin-dev/unknownTerminated', 'The instance has been terminated')));
this.stateChanged.fire({ state: HostedInstanceState.STOPPED, pluginLocation: this.pluginLocation! });
} catch (error) {
this.messageService.error(this.getErrorMessage(error));
}
}
async restart(): Promise<void> {
if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
await this.stop(false);
this.messageService.info(nls.localize('theia/plugin-dev/starting', 'Starting hosted instance server ...'));
// It takes some time before OS released all resources e.g. port.
// Keep trying to run hosted instance with delay.
this.stateChanged.fire({ state: HostedInstanceState.STARTING, pluginLocation: this.pluginLocation! });
let lastError;
for (let tries = 0; tries < 15; tries++) {
try {
if (this.isDebug) {
this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation!.toString(), {
debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode'],
debugPort: [...this.hostedPluginPreferences['hosted-plugin.debugPorts']]
});
await this.startDebugSessionManager();
} else {
this.pluginInstanceURL = await this.hostedPluginServer.runHostedPluginInstance(this.pluginLocation!.toString());
}
await this.openPluginWindow();
this.messageService.info(`${nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:')} ${this.pluginInstanceURL}`);
this.stateChanged.fire({
state: HostedInstanceState.RUNNING,
pluginLocation: this.pluginLocation!
});
return;
} catch (error) {
lastError = error;
await new Promise(resolve => setTimeout(resolve, 500));
}
}
this.messageService.error(nls.localize('theia/plugin-dev/failed', 'Failed to run hosted plugin instance: {0}', this.getErrorMessage(lastError)));
this.stateChanged.fire({ state: HostedInstanceState.FAILED, pluginLocation: this.pluginLocation! });
this.stop();
} else {
this.messageService.warn(nls.localize('theia/plugin-dev/notRunning', 'Hosted instance is not running.'));
this.start();
}
}
/**
* Creates directory choose dialog and set selected folder into pluginLocation field.
*/
async selectPluginPath(): Promise<void> {
const workspaceFolder = (await this.workspaceService.roots)[0] || await this.fileService.resolve(new URI(await this.environments.getHomeDirUri()));
if (!workspaceFolder) {
throw new Error('Unable to find the root');
}
const result = await this.fileDialogService.showOpenDialog({
title: HostedPluginCommands.SELECT_PATH.label!,
openLabel: nls.localizeByDefault('Select'),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false
}, workspaceFolder);
if (result) {
if (await this.hostedPluginServer.isPluginValid(result.toString())) {
this.pluginLocation = result;
this.messageService.info(nls.localize('theia/plugin-dev/pluginFolder', 'Plugin folder is set to: {0}', this.labelProvider.getLongName(result)));
} else {
this.messageService.error(nls.localize('theia/plugin-dev/noValidPlugin', 'Specified folder does not contain valid plugin.'));
}
}
}
register(configType: string, connection: DebugSessionConnection): void {
if (configType === 'pwa-extensionHost') {
this.connection = connection;
this.connection.onRequest('launchVSCode', (request: LaunchVSCodeRequest) => this.launchVSCode(request));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.connection.on('exited', async (args: any) => {
await this.stop();
});
}
}
/**
* Opens window with URL to the running plugin instance.
*/
protected async openPluginWindow(): Promise<void> {
// do nothing for electron browser
if (isNative) {
return;
}
if (this.pluginInstanceURL) {
try {
this.windowService.openNewWindow(this.pluginInstanceURL);
} catch (err) {
// browser blocked opening of a new tab
this.openNewTabAskDialog.showOpenNewTabAskDialog(this.pluginInstanceURL);
}
}
}
protected async launchVSCode({ arguments: { args } }: LaunchVSCodeRequest): Promise<LaunchVSCodeResult> {
let result = {};
let instanceURI;
const sessions = this.debugSessionManager.sessions.filter(session => session.id !== this.connection.sessionId);
/* if `launchVSCode` is invoked and sessions do not exist - it means that `start` debug was invoked.
if `launchVSCode` is invoked and sessions do exist - it means that `restartSessions()` was invoked,
which invoked `this.sendRequest('restart', {})`, which restarted `vscode-builtin-js-debug` plugin which is
connected to first session (sessions[0]), which means that other existing (child) sessions need to be terminated
and new ones will be created by running `startDebugSessionManager()`
*/
if (sessions.length > 0) {
sessions.forEach(session => this.debugSessionManager.terminateSession(session));
await this.startDebugSessionManager();
instanceURI = this.pluginInstanceURL;
} else {
instanceURI = await this.debug(this.getDebugPluginConfig(args));
}
if (instanceURI) {
const instanceURL = new URL(instanceURI);
if (instanceURL.port) {
result = Object.assign(result, { rendererDebugPort: instanceURL.port });
}
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected getErrorMessage(error: any): string {
return error?.message?.substring(error.message.indexOf(':') + 1) || '';
}
private setDebugConfig(config?: PluginDebugConfiguration): PluginDebugConfiguration {
config = Object.assign(config || {}, { debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode'] });
if (config.pluginLocation) {
this.pluginLocation = new URI((!config.pluginLocation.startsWith('/') ? '/' : '') + config.pluginLocation.replace(/\\/g, '/')).withScheme('file');
}
if (config.debugPort === undefined) {
config.debugPort = [...this.hostedPluginPreferences['hosted-plugin.debugPorts']];
}
return config;
}
private getDebugPluginConfig(args: LaunchVSCodeArgument[]): PluginDebugConfiguration {
let pluginLocation;
for (const arg of args) {
if (arg?.prefix === '--extensionDevelopmentPath=') {
pluginLocation = arg.path;
}
}
return {
pluginLocation
};
}
}
class OpenHostedInstanceLinkDialog extends AbstractDialog<string> {
protected readonly windowService: WindowService;
protected readonly openButton: HTMLButtonElement;
protected readonly messageNode: HTMLDivElement;
protected readonly linkNode: HTMLAnchorElement;
value: string;
constructor(windowService: WindowService) {
super({
title: nls.localize('theia/plugin-dev/preventedNewTab', 'Your browser prevented opening of a new tab')
});
this.windowService = windowService;
this.linkNode = document.createElement('a');
this.linkNode.target = '_blank';
this.linkNode.setAttribute('style', 'color: var(--theia-editorWidget-foreground);');
this.contentNode.appendChild(this.linkNode);
const messageNode = document.createElement('div');
messageNode.innerText = nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:') + ' ';
messageNode.appendChild(this.linkNode);
this.contentNode.appendChild(messageNode);
this.appendCloseButton();
this.openButton = this.appendAcceptButton(nls.localizeByDefault('Open'));
}
showOpenNewTabAskDialog(uri: string): void {
this.value = uri;
this.linkNode.textContent = uri;
this.linkNode.href = uri;
this.openButton.onclick = () => {
this.windowService.openNewWindow(uri);
};
this.open();
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { HostedPluginLogViewer } from './hosted-plugin-log-viewer';
import { HostedPluginManagerClient } from './hosted-plugin-manager-client';
import { HostedPluginInformer } from './hosted-plugin-informer';
import { bindHostedPluginPreferences } from '../common/hosted-plugin-preferences';
import { HostedPluginController } from './hosted-plugin-controller';
import { ContainerModule } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser';
import { HostedPluginFrontendContribution } from './hosted-plugin-frontend-contribution';
import { CommandContribution } from '@theia/core/lib/common/command';
import { PluginDevServer, pluginDevServicePath } from '../common/plugin-dev-protocol';
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bindHostedPluginPreferences(bind);
bind(HostedPluginLogViewer).toSelf().inSingletonScope();
bind(HostedPluginManagerClient).toSelf().inSingletonScope();
bind(DebugContribution).toService(HostedPluginManagerClient);
bind(FrontendApplicationContribution).to(HostedPluginInformer).inSingletonScope();
bind(FrontendApplicationContribution).to(HostedPluginController).inSingletonScope();
bind(HostedPluginFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(HostedPluginFrontendContribution);
bind(PluginDevServer).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
return connection.createProxy<PluginDevServer>(pluginDevServicePath);
}).inSingletonScope();
});

View File

@@ -0,0 +1,94 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { interfaces } from '@theia/core/shared/inversify';
import { createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService } from '@theia/core/lib/common';
import { nls } from '@theia/core/lib/common/nls';
import { PluginDebugPort } from './plugin-dev-protocol';
export const HostedPluginConfigSchema: PreferenceSchema = {
properties: {
'hosted-plugin.watchMode': {
type: 'boolean',
description: nls.localize('theia/plugin-dev/watchMode', 'Run watcher on plugin under development'),
default: true
},
'hosted-plugin.debugMode': {
type: 'string',
description: nls.localize('theia/plugin-dev/debugMode', 'Using inspect or inspect-brk for Node.js debug'),
default: 'inspect',
enum: ['inspect', 'inspect-brk']
},
'hosted-plugin.launchOutFiles': {
type: 'array',
items: {
type: 'string'
},
markdownDescription: nls.localize(
'theia/plugin-dev/launchOutFiles',
'Array of glob patterns for locating generated JavaScript files (`${pluginPath}` will be replaced by plugin actual path).'
),
default: ['${pluginPath}/out/**/*.js']
},
'hosted-plugin.debugPorts': {
type: 'array',
items: {
type: 'object',
properties: {
'serverName': {
type: 'string',
description: nls.localize('theia/plugin-dev/debugPorts/serverName',
'The plugin host server name, e.g. "hosted-plugin" as in "--hosted-plugin-inspect=" ' +
'or "headless-hosted-plugin" as in "--headless-hosted-plugin-inspect="'),
},
'debugPort': {
type: 'number',
minimum: 0,
maximum: 65535,
description: nls.localize('theia/plugin-dev/debugPorts/debugPort', 'Port to use for this server\'s Node.js debug'),
}
},
},
default: undefined,
description: nls.localize('theia/plugin-dev/debugPorts', 'Port configuration per server for Node.js debug'),
}
}
};
export interface HostedPluginConfiguration {
'hosted-plugin.watchMode': boolean;
'hosted-plugin.debugMode': string;
'hosted-plugin.launchOutFiles': string[];
'hosted-plugin.debugPorts': PluginDebugPort[];
}
export const HostedPluginPreferenceContribution = Symbol('HostedPluginPreferenceContribution');
export const HostedPluginPreferences = Symbol('HostedPluginPreferences');
export type HostedPluginPreferences = PreferenceProxy<HostedPluginConfiguration>;
export function createNavigatorPreferences(preferences: PreferenceService, schema: PreferenceSchema = HostedPluginConfigSchema): HostedPluginPreferences {
return createPreferenceProxy(preferences, schema);
}
export function bindHostedPluginPreferences(bind: interfaces.Bind): void {
bind(HostedPluginPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(HostedPluginPreferenceContribution);
return createNavigatorPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(HostedPluginPreferenceContribution).toConstantValue({ schema: HostedPluginConfigSchema });
bind(PreferenceContribution).toService(HostedPluginPreferenceContribution);
}

View File

@@ -0,0 +1,21 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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
// *****************************************************************************
// Exports contribution point for uri postprocessor of hosted plugin manager.
// This could be used to alter hosted instance uri, for example, change port.
export * from '../node/hosted-plugin-uri-postprocessor';
export * from './plugin-dev-protocol';

View File

@@ -0,0 +1,50 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { PluginMetadata } from '@theia/plugin-ext/lib/common/plugin-protocol';
export const pluginDevServicePath = '/services/plugin-dev';
export const PluginDevServer = Symbol('PluginDevServer');
export interface PluginDevServer extends RpcServer<PluginDevClient> {
getHostedPlugin(): Promise<PluginMetadata | undefined>;
runHostedPluginInstance(uri: string): Promise<string>;
runDebugHostedPluginInstance(uri: string, debugConfig: PluginDebugConfiguration): Promise<string>;
terminateHostedPluginInstance(): Promise<void>;
isHostedPluginInstanceRunning(): Promise<boolean>;
getHostedPluginInstanceURI(): Promise<string>;
getHostedPluginURI(): Promise<string>;
runWatchCompilation(uri: string): Promise<void>;
stopWatchCompilation(uri: string): Promise<void>;
isWatchCompilationRunning(uri: string): Promise<boolean>;
isPluginValid(uri: string): Promise<boolean>;
}
export interface PluginDevClient {
}
export interface PluginDebugPort {
serverName: string,
debugPort: number,
}
export interface PluginDebugConfiguration {
debugMode?: string;
pluginLocation?: string;
debugPort?: string | PluginDebugPort[]
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { HostedInstanceManager, ElectronNodeHostedPluginRunner } from '../node/hosted-instance-manager';
import { ContainerModule } from '@theia/core/shared/inversify';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { bindCommonHostedBackend } from '../node/plugin-dev-backend-module';
const hostedBackendConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bind(HostedInstanceManager).to(ElectronNodeHostedPluginRunner);
});
export default new ContainerModule(bind => {
bindCommonHostedBackend(bind);
bind(ConnectionContainerModule).toConstantValue(hostedBackendConnectionModule);
});

View File

@@ -0,0 +1,395 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { RequestOptions, RequestService } from '@theia/core/shared/@theia/request';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import * as cp from 'child_process';
import * as fs from '@theia/core/shared/fs-extra';
import * as net from 'net';
import * as path from 'path';
import URI from '@theia/core/lib/common/uri';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { HostedPluginUriPostProcessor, HostedPluginUriPostProcessorSymbolName } from './hosted-plugin-uri-postprocessor';
import { environment, isWindows } from '@theia/core';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { LogType } from '@theia/plugin-ext/lib/common/types';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin';
import { MetadataScanner } from '@theia/plugin-ext/lib/hosted/node/metadata-scanner';
import { PluginDebugConfiguration } from '../common/plugin-dev-protocol';
import { HostedPluginProcess } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-process';
import { isENOENT } from '@theia/plugin-ext/lib/common/errors';
const DEFAULT_HOSTED_PLUGIN_PORT = 3030;
export const HostedInstanceManager = Symbol('HostedInstanceManager');
/**
* Is responsible for running and handling separate Theia instance with given plugin.
*/
export interface HostedInstanceManager {
/**
* Checks whether hosted instance is run.
*/
isRunning(): boolean;
/**
* Runs specified by the given uri plugin in separate Theia instance.
*
* @param pluginUri uri to the plugin source location
* @param port port on which new instance of Theia should be run. Optional.
* @returns uri where new Theia instance is run
*/
run(pluginUri: URI, port?: number): Promise<URI>;
/**
* Runs specified by the given uri plugin with debug in separate Theia instance.
* @param pluginUri uri to the plugin source location
* @param debugConfig debug configuration
* @returns uri where new Theia instance is run
*/
debug(pluginUri: URI, debugConfig: PluginDebugConfiguration): Promise<URI>;
/**
* Terminates hosted plugin instance.
* Throws error if instance is not running.
*/
terminate(): void;
/**
* Returns uri where hosted instance is run.
* Throws error if instance is not running.
*/
getInstanceURI(): URI;
/**
* Returns uri where plugin loaded into hosted instance is located.
* Throws error if instance is not running.
*/
getPluginURI(): URI;
/**
* Checks whether given uri points to a valid plugin.
*
* @param uri uri to the plugin source location
*/
isPluginValid(uri: URI): Promise<boolean>;
}
const HOSTED_INSTANCE_START_TIMEOUT_MS = 30000;
const THEIA_INSTANCE_REGEX = /.*Theia app listening on (.*).*\./;
const PROCESS_OPTIONS = {
cwd: process.cwd(),
env: { ...process.env }
};
@injectable()
export abstract class AbstractHostedInstanceManager implements HostedInstanceManager {
protected hostedInstanceProcess: cp.ChildProcess;
protected isPluginRunning: boolean = false;
protected instanceUri: URI;
protected pluginUri: URI;
protected instanceOptions: Omit<RequestOptions, 'url'>;
@inject(HostedPluginSupport)
protected readonly hostedPluginSupport: HostedPluginSupport;
@inject(MetadataScanner)
protected readonly metadata: MetadataScanner;
@inject(HostedPluginProcess)
protected readonly hostedPluginProcess: HostedPluginProcess;
@inject(RequestService)
protected readonly request: RequestService;
isRunning(): boolean {
return this.isPluginRunning;
}
async run(pluginUri: URI, port?: number): Promise<URI> {
return this.doRun(pluginUri, port);
}
async debug(pluginUri: URI, debugConfig: PluginDebugConfiguration): Promise<URI> {
return this.doRun(pluginUri, undefined, debugConfig);
}
private async doRun(pluginUri: URI, port?: number, debugConfig?: PluginDebugConfiguration): Promise<URI> {
if (this.isPluginRunning) {
this.hostedPluginSupport.sendLog({ data: 'Hosted plugin instance is already running.', type: LogType.Info });
throw new Error('Hosted instance is already running.');
}
let command: string[];
let processOptions: cp.SpawnOptions;
if (pluginUri.scheme === 'file') {
processOptions = { ...PROCESS_OPTIONS };
// get filesystem path that work cross operating systems
processOptions.env!.HOSTED_PLUGIN = FileUri.fsPath(pluginUri.toString());
// Disable all the other plugins on this instance
processOptions.env!.THEIA_PLUGINS = '';
command = await this.getStartCommand(port, debugConfig);
} else {
throw new Error('Not supported plugin location: ' + pluginUri.toString());
}
this.instanceUri = await this.postProcessInstanceUri(await this.runHostedPluginTheiaInstance(command, processOptions));
this.pluginUri = pluginUri;
// disable redirect to grab the release
this.instanceOptions = {
followRedirects: 0
};
this.instanceOptions = await this.postProcessInstanceOptions(this.instanceOptions);
await this.checkInstanceUriReady();
return this.instanceUri;
}
terminate(): void {
if (this.isPluginRunning && !!this.hostedInstanceProcess.pid) {
this.hostedPluginProcess.killProcessTree(this.hostedInstanceProcess.pid);
this.hostedPluginSupport.sendLog({ data: 'Hosted instance has been terminated', type: LogType.Info });
this.isPluginRunning = false;
} else {
throw new Error('Hosted plugin instance is not running.');
}
}
getInstanceURI(): URI {
if (this.isPluginRunning) {
return this.instanceUri;
}
throw new Error('Hosted plugin instance is not running.');
}
getPluginURI(): URI {
if (this.isPluginRunning) {
return this.pluginUri;
}
throw new Error('Hosted plugin instance is not running.');
}
/**
* Checks that the `instanceUri` is responding before exiting method
*/
public async checkInstanceUriReady(): Promise<void> {
return new Promise<void>((resolve, reject) => this.pingLoop(60, resolve, reject));
}
/**
* Start a loop to ping, if ping is OK return immediately, else start a new ping after 1second. We iterate for the given amount of loops provided in remainingCount
* @param remainingCount the number of occurrence to check
* @param resolve resolve function if ok
* @param reject reject function if error
*/
private async pingLoop(remainingCount: number,
resolve: (value?: void | PromiseLike<void> | undefined | Error) => void,
reject: (value?: void | PromiseLike<void> | undefined | Error) => void): Promise<void> {
const isOK = await this.ping();
if (isOK) {
resolve();
} else {
if (remainingCount > 0) {
setTimeout(() => this.pingLoop(--remainingCount, resolve, reject), 1000);
} else {
reject(new Error('Unable to ping the remote server'));
}
}
}
/**
* Ping the plugin URI (checking status of the head)
*/
private async ping(): Promise<boolean> {
try {
const url = this.instanceUri.toString();
// Wait that the status is OK
const response = await this.request.request({ url, type: 'HEAD', ...this.instanceOptions });
return response.res.statusCode === 200;
} catch {
return false;
}
}
async isPluginValid(uri: URI): Promise<boolean> {
const pckPath = path.join(FileUri.fsPath(uri), 'package.json');
try {
const pck = await fs.readJSON(pckPath);
this.metadata.getScanner(pck);
return true;
} catch (err) {
if (!isENOENT(err)) {
console.error(err);
}
return false;
}
}
protected async getStartCommand(port?: number, debugConfig?: PluginDebugConfiguration): Promise<string[]> {
const processArguments = process.argv;
let command: string[];
if (environment.electron.is()) {
command = ['npm', 'run', 'theia', 'start'];
} else {
command = processArguments.filter((arg, index, args) => {
// remove --port=X and --port X arguments if set
// remove --plugins arguments
if (arg.startsWith('--port') || args[index - 1] === '--port') {
return;
} else {
return arg;
}
});
}
if (process.env.HOSTED_PLUGIN_HOSTNAME) {
command.push('--hostname=' + process.env.HOSTED_PLUGIN_HOSTNAME);
}
if (port) {
await this.validatePort(port);
command.push('--port=' + port);
}
if (debugConfig) {
if (debugConfig.debugPort === undefined) {
command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0`);
} else if (typeof debugConfig.debugPort === 'string') {
command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0:${debugConfig.debugPort}`);
} else if (Array.isArray(debugConfig.debugPort)) {
if (debugConfig.debugPort.length === 0) {
// treat empty array just like undefined
command.push(`--hosted-plugin-${debugConfig.debugMode || 'inspect'}=0.0.0.0`);
} else {
for (const serverToPort of debugConfig.debugPort) {
command.push(`--${serverToPort.serverName}-${debugConfig.debugMode || 'inspect'}=0.0.0.0:${serverToPort.debugPort}`);
}
}
}
}
return command;
}
protected async postProcessInstanceUri(uri: URI): Promise<URI> {
return uri;
}
protected async postProcessInstanceOptions(options: Omit<RequestOptions, 'url'>): Promise<Omit<RequestOptions, 'url'>> {
return options;
}
protected runHostedPluginTheiaInstance(command: string[], options: cp.SpawnOptions): Promise<URI> {
this.isPluginRunning = true;
return new Promise((resolve, reject) => {
let started = false;
const outputListener = (data: string | Buffer) => {
const line = data.toString();
const match = THEIA_INSTANCE_REGEX.exec(line);
if (match) {
this.hostedInstanceProcess.stdout!.removeListener('data', outputListener);
started = true;
resolve(new URI(match[1]));
}
};
if (isWindows) {
// Has to be set for running on windows (electron).
// See also: https://github.com/nodejs/node/issues/3675
options.shell = true;
}
this.hostedInstanceProcess = cp.spawn(command.shift()!, command, options);
this.hostedInstanceProcess.on('error', () => { this.isPluginRunning = false; });
this.hostedInstanceProcess.on('exit', () => { this.isPluginRunning = false; });
this.hostedInstanceProcess.stdout!.addListener('data', outputListener);
this.hostedInstanceProcess.stdout!.addListener('data', data => {
this.hostedPluginSupport.sendLog({ data: data.toString(), type: LogType.Info });
});
this.hostedInstanceProcess.stderr!.addListener('data', data => {
this.hostedPluginSupport.sendLog({ data: data.toString(), type: LogType.Error });
});
setTimeout(() => {
if (!started) {
this.terminate();
this.isPluginRunning = false;
reject(new Error('Timeout.'));
}
}, HOSTED_INSTANCE_START_TIMEOUT_MS);
});
}
protected async validatePort(port: number): Promise<void> {
if (port < 1 || port > 65535) {
throw new Error('Port value is incorrect.');
}
if (! await this.isPortFree(port)) {
throw new Error('Port ' + port + ' is already in use.');
}
}
protected isPortFree(port: number): Promise<boolean> {
return new Promise(resolve => {
const server = net.createServer();
server.listen(port, '0.0.0.0');
server.on('error', () => {
resolve(false);
});
server.on('listening', () => {
server.close();
resolve(true);
});
});
}
}
@injectable()
export class NodeHostedPluginRunner extends AbstractHostedInstanceManager {
@inject(ContributionProvider) @named(Symbol.for(HostedPluginUriPostProcessorSymbolName))
protected readonly uriPostProcessors: ContributionProvider<HostedPluginUriPostProcessor>;
protected override async postProcessInstanceUri(uri: URI): Promise<URI> {
for (const uriPostProcessor of this.uriPostProcessors.getContributions()) {
uri = await uriPostProcessor.processUri(uri);
}
return uri;
}
protected override async postProcessInstanceOptions(options: object): Promise<object> {
for (const uriPostProcessor of this.uriPostProcessors.getContributions()) {
options = await uriPostProcessor.processOptions(options);
}
return options;
}
protected override async getStartCommand(port?: number, debugConfig?: PluginDebugConfiguration): Promise<string[]> {
if (!port) {
port = process.env.HOSTED_PLUGIN_PORT ?
Number(process.env.HOSTED_PLUGIN_PORT) :
(debugConfig?.debugPort ? Number(debugConfig.debugPort) : DEFAULT_HOSTED_PLUGIN_PORT);
}
return super.getStartCommand(port, debugConfig);
}
}
@injectable()
export class ElectronNodeHostedPluginRunner extends AbstractHostedInstanceManager {
}

View File

@@ -0,0 +1,57 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { inject, injectable } from '@theia/core/shared/inversify';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { HostedPluginReader as PluginReaderHosted } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { PluginDeployerHandler, PluginMetadata } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { PluginDeployerEntryImpl } from '@theia/plugin-ext/lib/main/node/plugin-deployer-entry-impl';
@injectable()
export class HostedPluginReader implements BackendApplicationContribution {
@inject(PluginReaderHosted)
protected pluginReader: PluginReaderHosted;
private readonly hostedPlugin = new Deferred<PluginMetadata | undefined>();
@inject(PluginDeployerHandler)
protected deployerHandler: PluginDeployerHandler;
async initialize(): Promise<void> {
this.pluginReader.getPluginMetadata(process.env.HOSTED_PLUGIN)
.then(this.hostedPlugin.resolve.bind(this.hostedPlugin));
const pluginPath = process.env.HOSTED_PLUGIN;
if (pluginPath) {
const hostedPlugin = new PluginDeployerEntryImpl('Hosted Plugin', pluginPath!, pluginPath);
hostedPlugin.storeValue('isUnderDevelopment', true);
const hostedMetadata = await this.hostedPlugin.promise;
if (hostedMetadata!.model.entryPoint && (hostedMetadata!.model.entryPoint.backend || hostedMetadata!.model.entryPoint.headless)) {
this.deployerHandler.deployBackendPlugins([hostedPlugin]);
}
if (hostedMetadata!.model.entryPoint && hostedMetadata!.model.entryPoint.frontend) {
this.deployerHandler.deployFrontendPlugins([hostedPlugin]);
}
}
}
async getPlugin(): Promise<PluginMetadata | undefined> {
return this.hostedPlugin.promise;
}
}

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 URI from '@theia/core/lib/common/uri';
// We export symbol name instead of symbol itself here because we need to provide
// a contribution point to which any extensions could contribute.
// In case of just symbols, symbol inside an extension won't be the same as here
// even if the extension imports this module.
// To solve this problem we should provide global symbol. So right way to use the contribution point is:
// ...
// bind(Symbol.for(HostedPluginUriPostProcessorSymbolName)).to(AContribution);
// ...
export const HostedPluginUriPostProcessorSymbolName = 'HostedPluginUriPostProcessor';
export interface HostedPluginUriPostProcessor {
processUri(uri: URI): Promise<URI>;
processOptions(options: object): Promise<object>;
}

View File

@@ -0,0 +1,144 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { inject, injectable } from '@theia/core/shared/inversify';
import * as cp from 'child_process';
import * as fs from '@theia/core/shared/fs-extra';
import * as path from 'path';
import { FileUri } from '@theia/core/lib/node';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin';
import { LogType } from '@theia/plugin-ext/lib/common/types';
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
export const HostedPluginsManager = Symbol('HostedPluginsManager');
export interface HostedPluginsManager {
/**
* Runs watcher script to recompile plugin on any changes along given path.
*
* @param uri uri to plugin root folder.
*/
runWatchCompilation(uri: string): Promise<void>;
/**
* Stops watcher script.
*
* @param uri uri to plugin root folder.
*/
stopWatchCompilation(uri: string): Promise<void>;
/**
* Checks if watcher script to recompile plugin is running.
*
* @param uri uri to plugin root folder.
*/
isWatchCompilationRunning(uri: string): Promise<boolean>;
}
@injectable()
export class HostedPluginsManagerImpl implements HostedPluginsManager {
@inject(HostedPluginSupport)
protected readonly hostedPluginSupport: HostedPluginSupport;
@inject(ProcessUtils)
protected readonly processUtils: ProcessUtils;
protected watchCompilationRegistry: Map<string, cp.ChildProcess>;
constructor() {
this.watchCompilationRegistry = new Map();
}
runWatchCompilation(uri: string): Promise<void> {
const pluginRootPath = FileUri.fsPath(uri);
if (this.watchCompilationRegistry.has(pluginRootPath)) {
throw new Error('Watcher is already running in ' + pluginRootPath);
}
if (!this.checkWatchScript(pluginRootPath)) {
this.hostedPluginSupport.sendLog({
data: 'Plugin in ' + uri + ' doesn\'t have watch script',
type: LogType.Error
});
throw new Error('Watch script doesn\'t exist in ' + pluginRootPath + 'package.json');
}
return this.runWatchScript(pluginRootPath);
}
private killProcessTree(parentPid: number): void {
this.processUtils.terminateProcessTree(parentPid);
}
stopWatchCompilation(uri: string): Promise<void> {
const pluginPath = FileUri.fsPath(uri);
const watchProcess = this.watchCompilationRegistry.get(pluginPath);
if (!watchProcess) {
throw new Error('Watcher is not running in ' + pluginPath);
}
this.killProcessTree(watchProcess.pid!);
return Promise.resolve();
}
isWatchCompilationRunning(uri: string): Promise<boolean> {
const pluginPath = FileUri.fsPath(uri);
return new Promise(resolve => resolve(this.watchCompilationRegistry.has(pluginPath)));
}
protected runWatchScript(pluginRootPath: string): Promise<void> {
const watchProcess = cp.spawn('npm', ['run', 'watch'], { cwd: pluginRootPath, shell: true });
watchProcess.on('exit', () => this.unregisterWatchScript(pluginRootPath));
this.watchCompilationRegistry.set(pluginRootPath, watchProcess);
this.hostedPluginSupport.sendLog({
data: 'Compilation watcher has been started in ' + pluginRootPath,
type: LogType.Info
});
return Promise.resolve();
}
protected unregisterWatchScript(pluginRootPath: string): void {
this.watchCompilationRegistry.delete(pluginRootPath);
this.hostedPluginSupport.sendLog({
data: 'Compilation watcher has been stopped in ' + pluginRootPath,
type: LogType.Info
});
}
/**
* Checks whether watch script is present into package.json by given parent folder.
*
* @param pluginPath path to plugin's root directory
*/
protected async checkWatchScript(pluginPath: string): Promise<boolean> {
const pluginPackageJsonPath = path.join(pluginPath, 'package.json');
try {
const packageJson = await fs.readJSON(pluginPackageJsonPath);
const scripts = packageJson['scripts'];
if (scripts && scripts['watch']) {
return true;
}
} catch { }
return false;
}
}

View File

@@ -0,0 +1,56 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { HostedInstanceManager, NodeHostedPluginRunner } from './hosted-instance-manager';
import { HostedPluginUriPostProcessorSymbolName } from './hosted-plugin-uri-postprocessor';
import { HostedPluginsManager, HostedPluginsManagerImpl } from './hosted-plugins-manager';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { PluginDevServerImpl } from './plugin-dev-service';
import { PluginDevServer, PluginDevClient, pluginDevServicePath } from '../common/plugin-dev-protocol';
import { HostedPluginReader } from './hosted-plugin-reader';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { bindHostedPluginPreferences } from '../common/hosted-plugin-preferences';
const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(HostedPluginsManagerImpl).toSelf().inSingletonScope();
bind(HostedPluginsManager).toService(HostedPluginsManagerImpl);
bind(PluginDevServerImpl).toSelf().inSingletonScope();
bind(PluginDevServer).toService(PluginDevServerImpl);
bindBackendService<PluginDevServer, PluginDevClient>(pluginDevServicePath, PluginDevServer, (server, client) => {
server.setClient(client);
client.onDidCloseConnection(() => server.dispose());
return server;
});
});
export function bindCommonHostedBackend(bind: interfaces.Bind): void {
bind(HostedPluginReader).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(HostedPluginReader);
bind(ConnectionContainerModule).toConstantValue(commonHostedConnectionModule);
}
const hostedBackendConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bindContributionProvider(bind, Symbol.for(HostedPluginUriPostProcessorSymbolName));
bind(HostedInstanceManager).to(NodeHostedPluginRunner).inSingletonScope();
});
export default new ContainerModule(bind => {
bindHostedPluginPreferences(bind);
bindCommonHostedBackend(bind);
bind(ConnectionContainerModule).toConstantValue(hostedBackendConnectionModule);
});

View File

@@ -0,0 +1,107 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { PluginDebugConfiguration, PluginDevServer, PluginDevClient } from '../common/plugin-dev-protocol';
import { injectable, inject } from '@theia/core/shared/inversify';
import { HostedInstanceManager } from './hosted-instance-manager';
import { PluginMetadata } from '@theia/plugin-ext/lib/common/plugin-protocol';
import URI from '@theia/core/lib/common/uri';
import { HostedPluginReader } from './hosted-plugin-reader';
import { HostedPluginsManager } from './hosted-plugins-manager';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin';
@injectable()
export class PluginDevServerImpl implements PluginDevServer {
@inject(HostedPluginsManager)
protected readonly hostedPluginsManager: HostedPluginsManager;
@inject(HostedInstanceManager)
protected readonly hostedInstanceManager: HostedInstanceManager;
@inject(HostedPluginReader)
private readonly reader: HostedPluginReader;
@inject(HostedPluginSupport)
private readonly hostedPlugin: HostedPluginSupport;
dispose(): void {
// Terminate the hosted instance if it is currently running.
if (this.hostedInstanceManager.isRunning()) {
this.hostedInstanceManager.terminate();
}
}
setClient(client: PluginDevClient): void {
}
async getHostedPlugin(): Promise<PluginMetadata | undefined> {
const pluginMetadata = await this.reader.getPlugin();
if (pluginMetadata) {
this.hostedPlugin.runPlugin(pluginMetadata.model);
}
return Promise.resolve(this.reader.getPlugin());
}
isPluginValid(uri: string): Promise<boolean> {
return Promise.resolve(this.hostedInstanceManager.isPluginValid(new URI(uri)));
}
runHostedPluginInstance(uri: string): Promise<string> {
return this.uriToStrPromise(this.hostedInstanceManager.run(new URI(uri)));
}
runDebugHostedPluginInstance(uri: string, debugConfig: PluginDebugConfiguration): Promise<string> {
return this.uriToStrPromise(this.hostedInstanceManager.debug(new URI(uri), debugConfig));
}
terminateHostedPluginInstance(): Promise<void> {
this.hostedInstanceManager.terminate();
return Promise.resolve();
}
isHostedPluginInstanceRunning(): Promise<boolean> {
return Promise.resolve(this.hostedInstanceManager.isRunning());
}
getHostedPluginInstanceURI(): Promise<string> {
return Promise.resolve(this.hostedInstanceManager.getInstanceURI().toString());
}
getHostedPluginURI(): Promise<string> {
return Promise.resolve(this.hostedInstanceManager.getPluginURI().toString());
}
protected uriToStrPromise(promise: Promise<URI>): Promise<string> {
return new Promise((resolve, reject) => {
promise.then((uri: URI) => {
resolve(uri.toString());
}).catch(error => reject(error));
});
}
runWatchCompilation(path: string): Promise<void> {
return this.hostedPluginsManager.runWatchCompilation(path);
}
stopWatchCompilation(path: string): Promise<void> {
return this.hostedPluginsManager.stopWatchCompilation(path);
}
isWatchCompilationRunning(path: string): Promise<boolean> {
return this.hostedPluginsManager.isWatchCompilationRunning(path);
}
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('plugin-dev package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,35 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"lib": [
"es6",
"dom"
]
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../debug"
},
{
"path": "../filesystem"
},
{
"path": "../output"
},
{
"path": "../plugin-ext"
},
{
"path": "../workspace"
}
]
}