deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/plugin-dev/.eslintrc.js
Normal file
10
packages/plugin-dev/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
31
packages/plugin-dev/README.md
Normal file
31
packages/plugin-dev/README.md
Normal 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>
|
||||
57
packages/plugin-dev/package.json
Normal file
57
packages/plugin-dev/package.json
Normal 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"
|
||||
}
|
||||
356
packages/plugin-dev/src/browser/hosted-plugin-controller.ts
Normal file
356
packages/plugin-dev/src/browser/hosted-plugin-controller.ts
Normal 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
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
93
packages/plugin-dev/src/browser/hosted-plugin-informer.ts
Normal file
93
packages/plugin-dev/src/browser/hosted-plugin-informer.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
52
packages/plugin-dev/src/browser/hosted-plugin-log-viewer.ts
Normal file
52
packages/plugin-dev/src/browser/hosted-plugin-log-viewer.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
434
packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts
Normal file
434
packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
94
packages/plugin-dev/src/common/hosted-plugin-preferences.ts
Normal file
94
packages/plugin-dev/src/common/hosted-plugin-preferences.ts
Normal 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);
|
||||
}
|
||||
21
packages/plugin-dev/src/common/index.ts
Normal file
21
packages/plugin-dev/src/common/index.ts
Normal 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';
|
||||
50
packages/plugin-dev/src/common/plugin-dev-protocol.ts
Normal file
50
packages/plugin-dev/src/common/plugin-dev-protocol.ts
Normal 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[]
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
395
packages/plugin-dev/src/node/hosted-instance-manager.ts
Normal file
395
packages/plugin-dev/src/node/hosted-instance-manager.ts
Normal 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 {
|
||||
|
||||
}
|
||||
57
packages/plugin-dev/src/node/hosted-plugin-reader.ts
Normal file
57
packages/plugin-dev/src/node/hosted-plugin-reader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
144
packages/plugin-dev/src/node/hosted-plugins-manager.ts
Normal file
144
packages/plugin-dev/src/node/hosted-plugins-manager.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
56
packages/plugin-dev/src/node/plugin-dev-backend-module.ts
Normal file
56
packages/plugin-dev/src/node/plugin-dev-backend-module.ts
Normal 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);
|
||||
});
|
||||
107
packages/plugin-dev/src/node/plugin-dev-service.ts
Normal file
107
packages/plugin-dev/src/node/plugin-dev-service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
28
packages/plugin-dev/src/package.spec.ts
Normal file
28
packages/plugin-dev/src/package.spec.ts
Normal 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);
|
||||
});
|
||||
35
packages/plugin-dev/tsconfig.json
Normal file
35
packages/plugin-dev/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user