deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
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 {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user