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

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

View File

@@ -0,0 +1,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 {
}