deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/process/.eslintrc.js
Normal file
10
packages/process/.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/process/README.md
Normal file
31
packages/process/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 - PROCESS EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/process` extension allows the management of processes started with or without a `terminal`.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/process`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_process.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>
|
||||
54
packages/process/package.json
Normal file
54
packages/process/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@theia/process",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia process support.",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"node-pty": "1.1.0-beta27",
|
||||
"string-argv": "^0.1.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/common/process-common-module",
|
||||
"frontend": "lib/common/process-common-module"
|
||||
},
|
||||
{
|
||||
"backend": "lib/node/process-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
22
packages/process/src/common/process-common-module.ts
Normal file
22
packages/process/src/common/process-common-module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { ShellCommandBuilder } from './shell-command-builder';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ShellCommandBuilder).toSelf().inSingletonScope();
|
||||
});
|
||||
58
packages/process/src/common/process-manager-types.ts
Normal file
58
packages/process/src/common/process-manager-types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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 { Event } from '@theia/core';
|
||||
|
||||
export interface ManagedProcessManager {
|
||||
register(process: ManagedProcess): number;
|
||||
unregister(process: ManagedProcess): void;
|
||||
get(id: number): ManagedProcess | undefined;
|
||||
}
|
||||
|
||||
export interface ManagedProcess {
|
||||
readonly id: number;
|
||||
readonly onStart: Event<IProcessStartEvent>;
|
||||
readonly onExit: Event<IProcessExitEvent>;
|
||||
readonly onClose: Event<IProcessExitEvent>;
|
||||
readonly onError: Event<ProcessErrorEvent>;
|
||||
readonly killed: boolean;
|
||||
kill(): void;
|
||||
}
|
||||
|
||||
export interface IProcessExitEvent {
|
||||
// Exactly one of code and signal will be set.
|
||||
readonly code?: number,
|
||||
readonly signal?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Data emitted when a process has been successfully started.
|
||||
*/
|
||||
export interface IProcessStartEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Data emitted when a process has failed to start.
|
||||
*/
|
||||
export interface ProcessErrorEvent extends Error {
|
||||
/** An errno-like error string (e.g. ENOENT). */
|
||||
code: string;
|
||||
}
|
||||
|
||||
export enum ProcessType {
|
||||
'Raw',
|
||||
'Terminal'
|
||||
}
|
||||
486
packages/process/src/common/shell-command-builder.slow-spec.ts
Normal file
486
packages/process/src/common/shell-command-builder.slow-spec.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* This test suite assumes that we run in a NodeJS environment!
|
||||
*/
|
||||
|
||||
import { spawn, execSync, SpawnOptions, ChildProcess, spawnSync } from 'child_process';
|
||||
import { Readable } from 'stream';
|
||||
import { join } from 'path';
|
||||
import { ShellCommandBuilder, CommandLineOptions, ProcessInfo } from './shell-command-builder';
|
||||
import * as chalk from 'chalk'; // tslint:disable-line:no-implicit-dependencies
|
||||
|
||||
export interface TestProcessInfo extends ProcessInfo {
|
||||
shell: ChildProcess
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
/**
|
||||
* Extra debugging info (very verbose).
|
||||
*/
|
||||
const _debug: boolean = Boolean(process.env['THEIA_PROCESS_TEST_DEBUG']);
|
||||
/**
|
||||
* On Windows, some shells simply mess up the terminal's output.
|
||||
* Enable if you still want to test those.
|
||||
*/
|
||||
const _runWeirdShell: true | undefined = Boolean(process.env['THEIA_PROCESS_TEST_WEIRD_SHELL']) || undefined;
|
||||
/**
|
||||
* You might only have issues with a specific shell (`cmd.exe` I am looking at you).
|
||||
*/
|
||||
const _onlyTestShell: string | undefined = process.env['THEIA_PROCESS_TEST_ONLY'] || undefined;
|
||||
/**
|
||||
* Only log if environment variable is set.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function debug(...parts: any[]): void {
|
||||
if (_debug) {
|
||||
console.debug(...parts);
|
||||
}
|
||||
}
|
||||
|
||||
const testResources = join(__dirname, '../../src/common/tests');
|
||||
const spawnOptions: SpawnOptions = {
|
||||
// We do our own quoting, don't rely on the one done by NodeJS:
|
||||
windowsVerbatimArguments: true,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
};
|
||||
|
||||
// Formatting options, used with `scanLines` for debugging.
|
||||
const stdoutFormat = (prefix: string) => (data: string) =>
|
||||
`${chalk.bold(chalk.yellow(`${prefix} STDOUT:`))} ${chalk.bgYellow(chalk.black(data))}`;
|
||||
const stderrFormat = (prefix: string) => (data: string) =>
|
||||
`${chalk.bold(chalk.red(`${prefix} STDERR:`))} ${chalk.bgRed(chalk.white(data))}`;
|
||||
|
||||
// Default error scanner
|
||||
const errorScanner = (handle: ScanLineHandle<void>) => {
|
||||
if (
|
||||
/^\s*\w+Error:/.test(handle.line) ||
|
||||
/^\s*Cannot find /.test(handle.line)
|
||||
) {
|
||||
throw new Error(handle.text);
|
||||
}
|
||||
};
|
||||
|
||||
// Yarn mangles the PATH and creates some proxy script around node(.exe),
|
||||
// which messes up our environment, failing the tests.
|
||||
const hostNodePath =
|
||||
process.env['npm_node_execpath'] ||
|
||||
process.env['NODE'];
|
||||
if (!hostNodePath) {
|
||||
throw new Error('Could not determine the real node path.');
|
||||
}
|
||||
|
||||
const shellCommandBuilder = new ShellCommandBuilder();
|
||||
const shellConfigs = [{
|
||||
name: 'bash',
|
||||
path: isWindows
|
||||
? _runWeirdShell && execShellCommand('where bash.exe')
|
||||
: execShellCommand('command -v bash'),
|
||||
nodePath:
|
||||
isWindows && 'node' // Good enough
|
||||
}, {
|
||||
name: 'wsl',
|
||||
path: isWindows
|
||||
? _runWeirdShell && execShellCommand('where wsl.exe')
|
||||
: undefined,
|
||||
nodePath:
|
||||
isWindows && 'node' // Good enough
|
||||
}, {
|
||||
name: 'cmd',
|
||||
path: isWindows
|
||||
? execShellCommand('where cmd.exe')
|
||||
: undefined,
|
||||
}, {
|
||||
name: 'powershell',
|
||||
path: execShellCommand(isWindows
|
||||
? 'where powershell'
|
||||
: 'command -v pwsh'),
|
||||
}];
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
// 18d/12m/19y - Ubuntu 16.04:
|
||||
// Powershell sometimes fails when running as part of an npm lifecycle script.
|
||||
// See following error:
|
||||
//
|
||||
//
|
||||
// FailFast:
|
||||
// The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception.
|
||||
//
|
||||
// at System.Environment.FailFast(System.String, System.Exception)
|
||||
// at System.Environment.FailFast(System.String, System.Exception)
|
||||
// at Microsoft.PowerShell.UnmanagedPSEntry.Start(System.String, System.String[], Int32)
|
||||
// at Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])
|
||||
//
|
||||
// Exception details:
|
||||
// System.TypeInitializationException: The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception. ---> System.ArgumentException: Item has already been added. Key in dictionary: 'SPAWN_WRAP_SHIM_ROOT' Key being added: 'SPAWN_WRAP_SHIM_ROOT'
|
||||
// at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
|
||||
// at System.Environment.ToHashtable(IEnumerable`1 pairs)
|
||||
// at System.Environment.GetEnvironmentVariables()
|
||||
// at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformImplementation..ctor()
|
||||
// at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformSingleton.get_Current()
|
||||
// at Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryConfigurationFactory.Initialize(TelemetryConfiguration configuration, TelemetryModules modules)
|
||||
// at Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.get_Active()
|
||||
// at Microsoft.PowerShell.ApplicationInsightsTelemetry..cctor()
|
||||
// --- End of inner exception stack trace ---
|
||||
// at Microsoft.PowerShell.ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry()
|
||||
// at Microsoft.PowerShell.ConsoleHost.Start(String bannerText, String helpText, String[] args)
|
||||
// at Microsoft.PowerShell.ConsoleShell.Start(String bannerText, String helpText, String[] args)
|
||||
// at Microsoft.PowerShell.UnmanagedPSEntry.Start(String consoleFilePath, String[] args, Int32 argc)
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
let id = 0;
|
||||
for (const shellConfig of shellConfigs) {
|
||||
|
||||
let skipMessage: string | undefined;
|
||||
|
||||
if (typeof _onlyTestShell === 'string' && shellConfig.name !== _onlyTestShell) {
|
||||
skipMessage = `only testing ${_onlyTestShell}`;
|
||||
|
||||
} else if (!shellConfig.path) {
|
||||
// For each shell, skip if we could not find the executable path.
|
||||
skipMessage = 'cannot find shell';
|
||||
|
||||
} else {
|
||||
// Run a test in the shell to catch runtime issues.
|
||||
// CI seems to have issues with some shells depending on the environment...
|
||||
try {
|
||||
const debugName = `${shellConfig.name}/test`;
|
||||
const shellTest = spawnSync(shellConfig.path, {
|
||||
input: 'echo abcdefghijkl\n\n',
|
||||
timeout: 5_000,
|
||||
});
|
||||
debug(stdoutFormat(debugName)(shellTest.stdout.toString()));
|
||||
debug(stderrFormat(debugName)(shellTest.stderr.toString()));
|
||||
if (!/abcdefghijkl/m.test(shellTest.output.toString())) {
|
||||
skipMessage = 'wrong test output';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
skipMessage = 'error occurred';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If skipMessage is set, we should skip the test and explain why.
|
||||
*/
|
||||
const describeOrSkip = (callback: (this: Mocha.Suite) => void) => {
|
||||
const describeMessage = `test ${shellConfig.name} commands`;
|
||||
if (typeof skipMessage === 'undefined') {
|
||||
describe(describeMessage, callback);
|
||||
} else {
|
||||
describe.skip(`${describeMessage} - skip: ${skipMessage}`, callback);
|
||||
}
|
||||
};
|
||||
|
||||
describeOrSkip(function (): void {
|
||||
this.timeout(10_000);
|
||||
|
||||
let nodePath: string;
|
||||
let cwd: string;
|
||||
let submit: string | undefined;
|
||||
let processInfo: TestProcessInfo;
|
||||
let context: TestCaseContext;
|
||||
|
||||
beforeEach(() => {
|
||||
// In WSL, the node path is different than the host one (Windows vs Linux).
|
||||
nodePath = shellConfig.nodePath || hostNodePath;
|
||||
|
||||
// On windows, when running bash we need to convert paths from Windows
|
||||
// to their mounting point, assuming bash is running within WSL.
|
||||
if (isWindows && /bash|wsl/.test(shellConfig.name)) {
|
||||
cwd = convertWindowsPath(testResources);
|
||||
} else {
|
||||
cwd = testResources;
|
||||
}
|
||||
|
||||
// When running powershell, it seems like good measure to send `\n` twice...
|
||||
if (shellConfig.name === 'powershell') {
|
||||
submit = '\n\n';
|
||||
}
|
||||
|
||||
// TestContext holds all state for a given test.
|
||||
const testContextName = `${shellConfig.name}/${++id}`;
|
||||
context = new TestCaseContext(testContextName, submit);
|
||||
processInfo = createShell(context, shellConfig.path!);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processInfo.shell.kill();
|
||||
context.finalize();
|
||||
});
|
||||
|
||||
it('use simple environment variables', async () => {
|
||||
const envName = 'SIMPLE_NAME';
|
||||
const envValue = 'SIMPLE_VALUE';
|
||||
await testCommandLine(
|
||||
context, processInfo,
|
||||
{
|
||||
cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
|
||||
env: {
|
||||
[envName]: envValue,
|
||||
}
|
||||
}, [
|
||||
// stderr
|
||||
scanLines<void>(context, processInfo.shell.stderr!, errorScanner, stderrFormat(context.name)),
|
||||
// stdout
|
||||
scanLines<void>(context, processInfo.shell.stdout!, handle => {
|
||||
errorScanner(handle);
|
||||
if (handle.line.includes(`[${envValue}]`)) {
|
||||
handle.resolve();
|
||||
}
|
||||
}, stdoutFormat(context.name)),
|
||||
]);
|
||||
});
|
||||
|
||||
it('use problematic environment variables', async () => {
|
||||
const envName = 'A?B_C | D $PATH';
|
||||
const envValue = 'SUCCESS';
|
||||
await testCommandLine(
|
||||
context, processInfo,
|
||||
{
|
||||
cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
|
||||
env: {
|
||||
[envName]: envValue,
|
||||
}
|
||||
}, [
|
||||
// stderr
|
||||
scanLines<void>(context, processInfo.shell.stderr!, errorScanner, stderrFormat(context.name)),
|
||||
// stdout
|
||||
scanLines<void>(context, processInfo.shell.stdout!, handle => {
|
||||
errorScanner(handle);
|
||||
if (handle.line.includes(`[${envValue}]`)) {
|
||||
handle.resolve();
|
||||
}
|
||||
if (handle.line.includes('[undefined]')) {
|
||||
handle.reject(new Error(handle.text));
|
||||
}
|
||||
}, stdoutFormat(context.name)),
|
||||
]);
|
||||
});
|
||||
|
||||
it('command with complex arguments', async () => {
|
||||
const left = 'ABC';
|
||||
const right = 'DEF';
|
||||
await testCommandLine(
|
||||
context, processInfo,
|
||||
{
|
||||
cwd, args: [nodePath, '-e', `{
|
||||
const left = '${left}';
|
||||
const right = '${right}';
|
||||
console.log(\`[\${left}|\${right}]\`);
|
||||
}`],
|
||||
}, [
|
||||
// stderr
|
||||
scanLines<void>(context, processInfo.shell.stderr!, errorScanner, stderrFormat(context.name)),
|
||||
// stdout
|
||||
scanLines<void>(context, processInfo.shell.stdout!, handle => {
|
||||
errorScanner(handle);
|
||||
if (handle.line.includes(`[${left}|${right}]`)) {
|
||||
handle.resolve();
|
||||
}
|
||||
}, stdoutFormat(context.name)),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow `command` to fail and return undefined instead.
|
||||
*/
|
||||
function execShellCommand(command: string): string | undefined {
|
||||
try {
|
||||
// If trimmed output is an empty string, return `undefined` instead:
|
||||
return execSync(command).toString().trim() || undefined;
|
||||
} catch (error) {
|
||||
console.error(command, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When executing `bash.exe` on Windows, the `C:`, `D:`, etc drives are mounted under `/mnt/<drive>/...`
|
||||
*/
|
||||
function convertWindowsPath(windowsPath: string): string {
|
||||
return windowsPath
|
||||
// Convert back-slashes to forward-slashes
|
||||
.replace(/\\/g, '/')
|
||||
// Convert drive-letter to usual mounting point in WSL
|
||||
.replace(/^[A-Za-z]:\//, s => `/mnt/${s[0].toLowerCase()}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display trailing whitespace in a string, such as \r and \n.
|
||||
*/
|
||||
function displayWhitespaces(line: string): string {
|
||||
return line
|
||||
.replace(/\r?\n/, s => s.length === 2 ? '<\\r\\n>\r\n' : '<\\n>\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually run `prepareCommandLine`.
|
||||
*/
|
||||
async function testCommandLine(
|
||||
context: TestCaseContext,
|
||||
processInfo: TestProcessInfo,
|
||||
options: CommandLineOptions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
firstOf: Array<Promise<any>>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Promise<any> {
|
||||
const commandLine = shellCommandBuilder.buildCommand(processInfo, options);
|
||||
debug(`${chalk.bold(chalk.white(`${context.name} STDIN:`))} ${chalk.bgWhite(chalk.black(displayWhitespaces(commandLine)))}`);
|
||||
processInfo.shell.stdin!.write(commandLine + context.submit);
|
||||
return Promise.race(firstOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `(Test)ProcessInfo` object by spawning the specified shell.
|
||||
*/
|
||||
function createShell(
|
||||
context: TestCaseContext,
|
||||
shellExecutable: string,
|
||||
shellArguments: string[] = []
|
||||
): TestProcessInfo {
|
||||
const shell = spawn(shellExecutable, shellArguments, spawnOptions);
|
||||
debug(chalk.magenta(`${chalk.bold(`${context.name} SPAWN:`)} ${shellExecutable} ${shellArguments.join(' ')}`));
|
||||
shell.on('close', (code, signal) => debug(chalk.magenta(
|
||||
`${chalk.bold(`${context.name} CLOSE:`)} ${shellExecutable} code(${code}) signal(${signal})`
|
||||
)));
|
||||
return {
|
||||
executable: shellExecutable,
|
||||
arguments: [],
|
||||
shell,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire `callback` once per new detected line.
|
||||
*/
|
||||
async function scanLines<T = void>(
|
||||
context: TestCaseContext,
|
||||
stream: Readable,
|
||||
callback: (handle: ScanLineHandle<T>) => void,
|
||||
debugFormat = (s: string) => s,
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let line = '';
|
||||
let text = '';
|
||||
stream.on('close', () => {
|
||||
debug(debugFormat('<CLOSED>'));
|
||||
});
|
||||
// The `data` listener will be collected on 'close', which will happen
|
||||
// once we kill the process.
|
||||
stream.on('data', data => {
|
||||
if (context.resolved) {
|
||||
return;
|
||||
}
|
||||
const split = data.toString().split('\n');
|
||||
while (!context.resolved && split.length > 1) {
|
||||
line += split.shift()! + '\n';
|
||||
text += line;
|
||||
debug(debugFormat(displayWhitespaces(line)));
|
||||
try {
|
||||
callback({
|
||||
resolve: (value: T) => {
|
||||
if (!context.resolved) {
|
||||
context.resolve();
|
||||
resolve(value);
|
||||
debug(chalk.bold(chalk.green(`${context.name} SCANLINES RESOLVED`)));
|
||||
}
|
||||
},
|
||||
reject: (reason?: Error) => {
|
||||
if (!context.resolved) {
|
||||
context.resolve();
|
||||
reject(reason);
|
||||
debug(chalk.bold(chalk.red(`${context.name} SCANLINES REJECTED`)));
|
||||
}
|
||||
},
|
||||
line,
|
||||
text,
|
||||
});
|
||||
} catch (error) {
|
||||
debug(chalk.bold(chalk.red(`${context.name} SCANLINES THROWED`)));
|
||||
context.resolve();
|
||||
reject(error);
|
||||
break;
|
||||
}
|
||||
line = '';
|
||||
}
|
||||
line += split[0];
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
interface ScanLineHandle<T> {
|
||||
|
||||
/**
|
||||
* Finish listening to new events with a return value.
|
||||
*/
|
||||
resolve: (value: T) => void
|
||||
/**
|
||||
* Finish listening to new events with an error.
|
||||
*/
|
||||
reject: (reason?: Error) => void
|
||||
/**
|
||||
* Currently parsed line.
|
||||
*/
|
||||
line: string
|
||||
/**
|
||||
* The whole output buffer, containing all lines.
|
||||
*/
|
||||
text: string
|
||||
|
||||
}
|
||||
/**
|
||||
* We need a test case context to help with catching listeners that timed-out,
|
||||
* and synchronize multiple listeners so that when one resolves the test case,
|
||||
* the others can be put in "sleep mode" until destruction.
|
||||
*/
|
||||
class TestCaseContext {
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* A name associated with this context, to help with debugging.
|
||||
*/
|
||||
readonly name: string,
|
||||
/**
|
||||
* The characters to send in order to submit a command (mostly
|
||||
* powershell is causing issues).
|
||||
*/
|
||||
public submit = '\n',
|
||||
/**
|
||||
* @internal Current state of the test case, if it is finished or not.
|
||||
*/
|
||||
public resolved = false
|
||||
) { }
|
||||
|
||||
resolve(): void {
|
||||
this.resolved = true;
|
||||
}
|
||||
|
||||
finalize(): void {
|
||||
if (!this.resolved) {
|
||||
this.resolve();
|
||||
debug(chalk.red(`${chalk.bold(`${this.name} CONTEXT:`)} context wasn't resolved when finalizing, resolving!`));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
187
packages/process/src/common/shell-command-builder.ts
Normal file
187
packages/process/src/common/shell-command-builder.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* eslint-disable no-null/no-null */
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
createShellCommandLine, BashQuotingFunctions, PowershellQuotingFunctions, CmdQuotingFunctions, ShellQuoting, ShellQuotedString, escapeForShell, ShellQuotingFunctions
|
||||
} from '../common/shell-quoting';
|
||||
|
||||
export interface ProcessInfo {
|
||||
executable: string
|
||||
arguments: string[]
|
||||
}
|
||||
|
||||
export interface CommandLineOptions {
|
||||
cwd: string
|
||||
args: string[]
|
||||
env?: {
|
||||
[key: string]: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create command lines ready to be sent to a shell's stdin for evaluation.
|
||||
*/
|
||||
@injectable()
|
||||
export class ShellCommandBuilder {
|
||||
|
||||
/**
|
||||
* Constructs a command line to run in a shell. The shell could be
|
||||
* re-used/long-lived, this means we cannot spawn a new process with a nice
|
||||
* and fresh environment, we need to encode environment modifications into
|
||||
* the returned command.
|
||||
*
|
||||
* Inspired by VS Code implementation, see:
|
||||
* https://github.com/microsoft/vscode/blob/f395cac4fff0721a8099126172c01411812bcb4a/src/vs/workbench/contrib/debug/node/terminals.ts#L79
|
||||
*
|
||||
* @param hostProcessInfo the host terminal process infos
|
||||
* @param commandOptions program to execute in the host terminal
|
||||
*/
|
||||
buildCommand(hostProcessInfo: ProcessInfo | undefined, commandOptions: CommandLineOptions): string {
|
||||
|
||||
const host = hostProcessInfo && hostProcessInfo.executable;
|
||||
const cwd = commandOptions.cwd;
|
||||
|
||||
const args = commandOptions.args.map(value => ({
|
||||
value, quoting: ShellQuoting.Strong,
|
||||
} as ShellQuotedString));
|
||||
|
||||
const env: Array<[string, string | null]> = [];
|
||||
if (commandOptions.env) {
|
||||
for (const key of Object.keys(commandOptions.env)) {
|
||||
env.push([key, commandOptions.env[key]]);
|
||||
}
|
||||
}
|
||||
if (host) {
|
||||
if (/(bash|wsl)(.exe)?$/.test(host)) {
|
||||
return this.buildForBash(args, cwd, env);
|
||||
} else if (/(ps|pwsh|powershell)(.exe)?$/i.test(host)) {
|
||||
return this.buildForPowershell(args, cwd, env);
|
||||
} else if (/cmd(.exe)?$/i.test(host)) {
|
||||
return this.buildForCmd(args, cwd, env);
|
||||
}
|
||||
}
|
||||
return this.buildForDefault(args, cwd, env);
|
||||
}
|
||||
|
||||
protected buildForBash(args: Array<string | ShellQuotedString>, cwd?: string, env?: Array<[string, string | null]>): string {
|
||||
let command = '';
|
||||
if (cwd) {
|
||||
command += `cd ${BashQuotingFunctions.strong(cwd)} && `;
|
||||
}
|
||||
if (env?.length) {
|
||||
command += 'env';
|
||||
for (const [key, value] of env) {
|
||||
if (value === null) {
|
||||
command += ` -u ${BashQuotingFunctions.strong(key)}`;
|
||||
} else {
|
||||
command += ` ${BashQuotingFunctions.strong(`${key}=${value}`)}`;
|
||||
}
|
||||
}
|
||||
command += ' ';
|
||||
}
|
||||
command += this.createShellCommandLine(args, BashQuotingFunctions);
|
||||
return command;
|
||||
}
|
||||
|
||||
protected buildForPowershell(args: Array<string | ShellQuotedString>, cwd?: string, env?: Array<[string, string | null]>): string {
|
||||
let command = '';
|
||||
if (cwd) {
|
||||
command += `cd ${PowershellQuotingFunctions.strong(cwd)}; `;
|
||||
}
|
||||
if (env?.length) {
|
||||
for (const [key, value] of env) {
|
||||
// Powershell requires special quoting when dealing with
|
||||
// environment variable names.
|
||||
const quotedKey = key
|
||||
.replace(/`/g, '````')
|
||||
.replace(/\?/g, '``?');
|
||||
if (value === null) {
|
||||
command += `Remove-Item \${env:${quotedKey}}; `;
|
||||
} else {
|
||||
command += `\${env:${quotedKey}}=${PowershellQuotingFunctions.strong(value)}; `;
|
||||
}
|
||||
}
|
||||
}
|
||||
command += '& ' + this.createShellCommandLine(args, PowershellQuotingFunctions);
|
||||
return command;
|
||||
}
|
||||
|
||||
protected buildForCmd(args: Array<string | ShellQuotedString>, cwd?: string, env?: Array<[string, string | null]>): string {
|
||||
let command = '';
|
||||
if (cwd) {
|
||||
command += `cd ${CmdQuotingFunctions.strong(cwd)} && `;
|
||||
}
|
||||
// Current quoting mechanism only works within a nested `cmd` call:
|
||||
command += 'cmd /C "';
|
||||
if (env?.length) {
|
||||
for (const [key, value] of env) {
|
||||
if (value === null) {
|
||||
command += `set ${CmdQuotingFunctions.strong(key)}="" && `;
|
||||
} else {
|
||||
command += `set ${CmdQuotingFunctions.strong(`${key}=${value}`)} && `;
|
||||
}
|
||||
}
|
||||
}
|
||||
command += this.createShellCommandLine(args, CmdQuotingFunctions);
|
||||
command += '"';
|
||||
return command;
|
||||
}
|
||||
|
||||
protected buildForDefault(args: Array<string | ShellQuotedString>, cwd?: string, env?: Array<[string, string | null]>): string {
|
||||
return this.buildForBash(args, cwd, env);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will try to leave `arg[0]` unescaped if possible. The reason
|
||||
* is that shells like `cmd` expect their own commands like `dir` to be
|
||||
* unescaped.
|
||||
*
|
||||
* @returns empty string if `args` is empty, otherwise an escaped command.
|
||||
*/
|
||||
protected createShellCommandLine(args: (string | ShellQuotedString)[], quotingFunctions: ShellQuotingFunctions): string {
|
||||
let command = '';
|
||||
if (args.length > 0) {
|
||||
const [exec, ...execArgs] = args;
|
||||
// Some commands like `dir` should not be quoted for `cmd` to understand:
|
||||
command += this.quoteExecutableIfNecessary(exec, quotingFunctions);
|
||||
if (execArgs.length > 0) {
|
||||
command += ' ' + createShellCommandLine(execArgs, quotingFunctions);
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
protected quoteExecutableIfNecessary(exec: string | ShellQuotedString, quotingFunctions: ShellQuotingFunctions): string {
|
||||
return typeof exec === 'string' && !this.needsQuoting(exec) ? exec : escapeForShell(exec, quotingFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this method returns `false` then we definitely need quoting.
|
||||
*
|
||||
* May return false positives.
|
||||
*/
|
||||
protected needsQuoting(arg: string): boolean {
|
||||
return /\W/.test(arg);
|
||||
}
|
||||
}
|
||||
176
packages/process/src/common/shell-quoting.spec.ts
Normal file
176
packages/process/src/common/shell-quoting.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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 { expect } from 'chai';
|
||||
|
||||
import { escapeForShell, BashQuotingFunctions, ShellQuoting, CmdQuotingFunctions, PowershellQuotingFunctions } from './shell-quoting';
|
||||
|
||||
describe('Shell arguments escaping:', () => {
|
||||
|
||||
// Procedurally execute tests from this list of data.
|
||||
const testData = {
|
||||
bash: {
|
||||
// https://www.gnu.org/software/bash/manual/html_node/Quoting.html
|
||||
quotingFunctions: BashQuotingFunctions,
|
||||
data: {
|
||||
[ShellQuoting.Escape]: [
|
||||
{ input: 'abc', expected: 'abc' },
|
||||
{ input: 'ab c', expected: 'ab\\ c' },
|
||||
{ input: 'ab"c', expected: 'ab\\"c' },
|
||||
{ input: 'ab\'c', expected: 'ab\\\'c' },
|
||||
{ input: 'ab\\ c\\', expected: 'ab\\\\\\ c\\\\' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: 'setTimeout\\(\\(\\)\\ =\\>\\ \\{\\ console.log\\(1,\\ \\"2\\\'3\\"\\)\\;\\ \\},\\ 100\\)',
|
||||
},
|
||||
],
|
||||
[ShellQuoting.Strong]: [
|
||||
{ input: 'abc', expected: '\'abc\'' },
|
||||
{ input: 'ab c', expected: '\'ab c\'' },
|
||||
{ input: 'ab"c', expected: '\'ab"c\'' },
|
||||
{ input: 'ab\'c', expected: '\'ab\'"\'"\'c\'' },
|
||||
{ input: 'ab\\ c\\', expected: '\'ab\\ c\\\'' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: '\'setTimeout(() => { console.log(1, "2\'"\'"\'3"); }, 100)\'',
|
||||
},
|
||||
],
|
||||
[ShellQuoting.Weak]: [
|
||||
{ input: 'abc', expected: '"abc"' },
|
||||
{ input: 'ab c', expected: '"ab c"' },
|
||||
{ input: 'ab"c', expected: '"ab\\"c"' },
|
||||
{ input: 'ab\'c', expected: '"ab\'c"' },
|
||||
{ input: 'ab\\ c\\', expected: '"ab\\ c\\\\"' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: '"setTimeout(() => { console.log(1, \\"2\'3\\"); }, 100)"',
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
cmd: {
|
||||
// https://ss64.com/nt/syntax-esc.html
|
||||
quotingFunctions: CmdQuotingFunctions,
|
||||
data: {
|
||||
[ShellQuoting.Escape]: [
|
||||
{ input: 'abc', expected: 'abc' },
|
||||
{ input: 'ab c', expected: 'ab" "c' },
|
||||
{ input: 'ab"c', expected: 'ab\\"c' },
|
||||
{ input: 'ab\'c', expected: 'ab\'c' },
|
||||
{ input: 'ab^ c^', expected: 'ab^^" "c^^' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: 'setTimeout^(^(^)" "=^>" "{" "console.log^(1," "\\"2\'3\\"^);" "}," "100^)',
|
||||
},
|
||||
{
|
||||
input: 'console.log("%PATH%")',
|
||||
expected: 'console.log^(\\"^%PATH^%\\"^)',
|
||||
},
|
||||
],
|
||||
[ShellQuoting.Strong]: [
|
||||
{ input: 'abc', expected: '"abc"' },
|
||||
{ input: 'ab c', expected: '"ab c"' },
|
||||
{ input: 'ab"c', expected: '"ab\\"c"' },
|
||||
{ input: 'ab\'c', expected: '"ab\'c"' },
|
||||
{ input: 'ab^ c^', expected: '"ab^^ c^^"' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: '"setTimeout^(^(^) =^> { console.log^(1, \\"2\'3\\"^); }, 100^)"',
|
||||
},
|
||||
{
|
||||
input: 'console.log("%PATH%")',
|
||||
expected: '"console.log^(\\""%"PATH"%"\\"^)"',
|
||||
},
|
||||
],
|
||||
[ShellQuoting.Weak]: [
|
||||
{ input: 'abc', expected: '"abc"' },
|
||||
{ input: 'ab c', expected: '"ab c"' },
|
||||
{ input: 'ab"c', expected: '"ab\\"c"' },
|
||||
{ input: 'ab\'c', expected: '"ab\'c"' },
|
||||
{ input: 'ab^ c^', expected: '"ab^^ c^^"' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: '"setTimeout^(^(^) =^> { console.log^(1, \\"2\'3\\"^); }, 100^)"',
|
||||
},
|
||||
{
|
||||
input: 'console.log("%PATH%")',
|
||||
expected: '"console.log^(\\"%PATH%\\"^)"',
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
powershell: {
|
||||
// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-6
|
||||
quotingFunctions: PowershellQuotingFunctions,
|
||||
data: {
|
||||
[ShellQuoting.Escape]: [
|
||||
{ input: 'abc', expected: 'abc' },
|
||||
{ input: 'ab c', expected: 'ab` c' },
|
||||
{ input: 'ab"c', expected: 'ab`"c' },
|
||||
{ input: 'ab\'c', expected: 'ab`\'c' },
|
||||
{ input: 'ab` c`', expected: 'ab``` c``' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: 'setTimeout`(`(`)` =`>` `{` console.log`(1,` `"2`\'3`"`)`;` `},` 100`)',
|
||||
},
|
||||
],
|
||||
[ShellQuoting.Strong]: [
|
||||
{ input: 'abc', expected: '\'abc\'' },
|
||||
{ input: 'ab c', expected: '\'ab c\'' },
|
||||
{ input: 'ab"c', expected: '\'ab"c\'' },
|
||||
{ input: 'ab\'c', expected: '\'ab\'\'c\'' },
|
||||
{ input: 'ab` c`', expected: '\'ab` c`\'' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: '\'setTimeout(() => { console.log(1, "2\'\'3"); }, 100)\'',
|
||||
},
|
||||
],
|
||||
[ShellQuoting.Weak]: [
|
||||
{ input: 'abc', expected: '"abc"' },
|
||||
{ input: 'ab c', expected: '"ab c"' },
|
||||
{ input: 'ab"c', expected: '"ab`"c"' },
|
||||
{ input: 'ab\'c', expected: '"ab\'c"' },
|
||||
{ input: 'ab` c`', expected: '"ab` c``"' },
|
||||
{
|
||||
input: 'setTimeout(() => { console.log(1, "2\'3"); }, 100)',
|
||||
expected: '"setTimeout(() => { console.log(1, `"2\'3`"); }, 100)"',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Iter through each runtime (bash/cmd/powershell):
|
||||
for (const runtime of Object.keys(testData)) {
|
||||
const testInfo = testData[runtime as keyof typeof testData];
|
||||
|
||||
// Get all quoting types (escape/strong/weak):
|
||||
for (const quotingType of Object.keys(testInfo.data)) {
|
||||
const testInput = testInfo.data[quotingType as keyof typeof testInfo['data']];
|
||||
|
||||
// Run the test for each input:
|
||||
it(`${runtime}/${quotingType}`, () => {
|
||||
for (const test of testInput) {
|
||||
expect(escapeForShell({
|
||||
quoting: quotingType as ShellQuoting,
|
||||
value: test.input,
|
||||
}, testInfo.quotingFunctions)).equal(test.expected);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
236
packages/process/src/common/shell-quoting.ts
Normal file
236
packages/process/src/common/shell-quoting.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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
|
||||
// *****************************************************************************
|
||||
|
||||
// #region vscode
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// See: https://github.com/microsoft/vscode/blob/9ebb7c43bc99fd6e1a295040674d1f8e5831b9be/src/vs/vscode.d.ts#L5326-L5370
|
||||
|
||||
/**
|
||||
* Defines how an argument should be quoted if it contains
|
||||
* spaces or unsupported characters.
|
||||
*/
|
||||
export const enum ShellQuoting {
|
||||
|
||||
/**
|
||||
* Character escaping should be used. This for example
|
||||
* uses \ on bash and ` on PowerShell.
|
||||
*/
|
||||
Escape = 'escape',
|
||||
|
||||
/**
|
||||
* Strong string quoting should be used. This for example
|
||||
* uses " for Windows cmd and ' for bash and PowerShell.
|
||||
* Strong quoting treats arguments as literal strings.
|
||||
* Under PowerShell echo 'The value is $(2 * 3)' will
|
||||
* print `The value is $(2 * 3)`
|
||||
*/
|
||||
Strong = 'strong',
|
||||
|
||||
/**
|
||||
* Weak string quoting should be used. This for example
|
||||
* uses " for Windows cmd, bash and PowerShell. Weak quoting
|
||||
* still performs some kind of evaluation inside the quoted
|
||||
* string. Under PowerShell echo "The value is $(2 * 3)"
|
||||
* will print `The value is 6`
|
||||
*/
|
||||
Weak = 'weak'
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that will be quoted depending on the used shell.
|
||||
*/
|
||||
export interface ShellQuotedString {
|
||||
/**
|
||||
* The actual string value.
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The quoting style to use.
|
||||
*/
|
||||
quoting: ShellQuoting;
|
||||
}
|
||||
|
||||
// #endregion vscode
|
||||
|
||||
/**
|
||||
* Functions that provide shell quoting capabilities.
|
||||
*/
|
||||
export interface ShellQuotingFunctions {
|
||||
|
||||
characters: {
|
||||
/** Characters that require quotes, white space is always implied. */
|
||||
needQuotes?: string
|
||||
/** The character used to escape sequences. */
|
||||
escape?: string
|
||||
/** The character used to quote sequences, preventing variable expansion. */
|
||||
strong?: string
|
||||
/** The character used to quote sequences, allowing variable expansion. */
|
||||
weak?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Should add escape-characters in front of forbidden characters.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
escape?(this: any, arg: string): string
|
||||
|
||||
/**
|
||||
* Should quote the argument in such a way that variables CANNOT be expanded.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
strong?(this: any, arg: string): string;
|
||||
|
||||
/**
|
||||
* Should quote the argument in such a way that variables CAN be expanded.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
weak?(this: any, arg: string): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of args into an escaped shell command.
|
||||
*
|
||||
* There are two main use cases when handling command/arguments for a shell:
|
||||
* 1. User already wrote the escaped commandline, then just use that.
|
||||
* 2. User wants a specific process to be invoked with some arguments.
|
||||
*
|
||||
* The `createShellCommandLine` function is useful for the latter.
|
||||
*
|
||||
* @param args Standard list of spawn/exec arguments, first item is the command.
|
||||
* @param quotingFunctions Collection of functions to process arguments.
|
||||
*/
|
||||
export function createShellCommandLine(args: Array<string | ShellQuotedString>, quotingFunctions?: ShellQuotingFunctions): string {
|
||||
return args.map(arg => escapeForShell(arg, quotingFunctions)).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape (or quote) a given input.
|
||||
*
|
||||
* @param arg Input to escape.
|
||||
* @param quotingFunctions Collection of functions to process the given `arg`.
|
||||
* @param quotingType Override the quoting type specified by the given `arg`.
|
||||
*/
|
||||
export function escapeForShell(arg: string | ShellQuotedString, quotingFunctions?: ShellQuotingFunctions, quotingType?: ShellQuoting): string {
|
||||
let value: string;
|
||||
let quoting: ShellQuoting | undefined = quotingType;
|
||||
if (typeof arg === 'string') {
|
||||
if (!quoting) {
|
||||
return arg;
|
||||
}
|
||||
value = arg;
|
||||
} else {
|
||||
if (!quoting) {
|
||||
quoting = arg.quoting;
|
||||
}
|
||||
value = arg.value;
|
||||
}
|
||||
if (quotingFunctions && typeof quotingFunctions[quoting] === 'function') {
|
||||
return quotingFunctions[quoting]!(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const BashQuotingFunctions: Required<ShellQuotingFunctions> = {
|
||||
characters: {
|
||||
needQuotes: '()',
|
||||
escape: '\\',
|
||||
strong: '\'',
|
||||
weak: '"',
|
||||
},
|
||||
escape(arg): string {
|
||||
return arg
|
||||
.replace(/[\s\\|(){}<>$&;"']/g, '\\$&');
|
||||
},
|
||||
strong(arg): string {
|
||||
// ('+) becomes ('"'+"')
|
||||
return `'${arg
|
||||
.replace(/'+/g, '\'"$&"\'')}'`;
|
||||
},
|
||||
weak(arg): string {
|
||||
return `"${arg
|
||||
// Escape escape-characters.
|
||||
.replace(/\\"/g, '\\\\"')
|
||||
// Escape user-specified double-quotes.
|
||||
.replace(/"/g, '\\"')
|
||||
// Escape trailing (\), we don't want the user to escape our last quote.
|
||||
.replace(/\\$/g, '\\\\')}"`;
|
||||
},
|
||||
};
|
||||
|
||||
export const CmdQuotingFunctions: Required<ShellQuotingFunctions> = {
|
||||
characters: {
|
||||
weak: '"',
|
||||
},
|
||||
escape(arg): string {
|
||||
return arg
|
||||
// Escape forbidden characters (see: cmd /?).
|
||||
.replace(/[%&<>()@^|]/g, '^$&')
|
||||
// Some characters must be escaped using `\`.
|
||||
.replace(/[\\"]/g, '\\$&')
|
||||
// Double-quote whitespaces, else we cannot escape it.
|
||||
.replace(/\s+/g, '"$&"');
|
||||
},
|
||||
strong(arg): string {
|
||||
return this.weak(arg)
|
||||
// Try to prevent variable expansion.
|
||||
.replace(/%/g, '"%"');
|
||||
},
|
||||
weak(arg): string {
|
||||
return `"${arg
|
||||
// Escape double quotes.
|
||||
.replace(/\\"/g, '\\\\"')
|
||||
.replace(/"/g, '\\"')
|
||||
// Escape forbidden characters (see: cmd /?)
|
||||
.replace(/[&<>()@^|]/g, '^$&')
|
||||
// Escape trailing backslash, we don't want the user to escape our last quote.
|
||||
.replace(/\\$/g, '\\\\')
|
||||
// Escape line returns
|
||||
.replace(/\r?\n/g, '^$&')}"`;
|
||||
},
|
||||
};
|
||||
|
||||
export const PowershellQuotingFunctions: Required<ShellQuotingFunctions> = {
|
||||
characters: {
|
||||
needQuotes: '()',
|
||||
escape: '`',
|
||||
strong: '\'',
|
||||
weak: '"',
|
||||
},
|
||||
escape(arg): string {
|
||||
return arg.replace(/[`|{}()<>;"' ]/g, '`$&');
|
||||
},
|
||||
strong(arg): string {
|
||||
// In powershell, one must write ('') for a single quote to be displayed
|
||||
// within a single quoted string.
|
||||
return `'${arg
|
||||
.replace(/'/g, '\'\'')}'`;
|
||||
},
|
||||
weak(arg): string {
|
||||
return `"${arg
|
||||
// Escape escape-characters.
|
||||
.replace(/`"/g, '``"')
|
||||
// Escape user-specified backticks.
|
||||
.replace(/"/g, '`"')
|
||||
// Escape trailing (`), we don't want the user to escape our last quote.
|
||||
.replace(/`$/g, '``')}"`;
|
||||
},
|
||||
};
|
||||
1
packages/process/src/common/tests/$weird(),file=name.js
Normal file
1
packages/process/src/common/tests/$weird(),file=name.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log('FORBIDDEN_OK')
|
||||
1
packages/process/src/common/tests/white space.js
Normal file
1
packages/process/src/common/tests/white space.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log('WHITESPACE_OK')
|
||||
47
packages/process/src/node/dev-null-stream.ts
Normal file
47
packages/process/src/node/dev-null-stream.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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 stream = require('stream');
|
||||
|
||||
/**
|
||||
* A Node stream like `/dev/null`.
|
||||
*
|
||||
* Writing goes to a black hole, reading returns `EOF`.
|
||||
*/
|
||||
export class DevNullStream extends stream.Duplex {
|
||||
|
||||
constructor(options: {
|
||||
/**
|
||||
* Makes this stream call `destroy` on itself, emitting the `close` event.
|
||||
*/
|
||||
autoDestroy?: boolean,
|
||||
} = {}) {
|
||||
super();
|
||||
if (options.autoDestroy) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override _write(chunk: any, encoding: string, callback: (err?: Error) => void): void {
|
||||
callback();
|
||||
}
|
||||
|
||||
override _read(size: number): void {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
22
packages/process/src/node/index.ts
Normal file
22
packages/process/src/node/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './process-manager';
|
||||
export * from './process';
|
||||
export * from './raw-process';
|
||||
export * from './terminal-process';
|
||||
export * from './task-terminal-process';
|
||||
export * from './multi-ring-buffer';
|
||||
486
packages/process/src/node/multi-ring-buffer.spec.ts
Normal file
486
packages/process/src/node/multi-ring-buffer.spec.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as chai from 'chai';
|
||||
import { MultiRingBuffer } from './multi-ring-buffer';
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('MultiRingBuffer', function (): void {
|
||||
|
||||
it('expect buffer to be empty initialized', function (): void {
|
||||
const size = 2;
|
||||
const compareTo = Buffer.from('0000', 'hex');
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
expect(ringBuffer['buffer'].equals(compareTo)).to.be.true;
|
||||
});
|
||||
|
||||
it('expect enq and deq a string with unicode characters > 1 byte and no wrap around', function (): void {
|
||||
const ringBufferSize = 15;
|
||||
const ringBuffer = new MultiRingBuffer({ size: ringBufferSize });
|
||||
const buffer = '\u00bd + \u00bc = \u00be';
|
||||
const bufferByteLength = Buffer.byteLength(buffer, 'utf8');
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(bufferByteLength);
|
||||
});
|
||||
|
||||
it('expect enq and deq a string with unicode characters > 1 byte and wrap around', function (): void {
|
||||
const buffer = '\u00bd + \u00bc = \u00be';
|
||||
const ringBufferSize = Buffer.byteLength(buffer[buffer.length - 1]);
|
||||
const ringBuffer = new MultiRingBuffer({ size: ringBufferSize });
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(ringBufferSize);
|
||||
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
|
||||
expect(ringBuffer).to.not.be.equal(undefined);
|
||||
if (readBuffer !== undefined) {
|
||||
expect(readBuffer).to.be.equal(buffer[buffer.length - 1].toString());
|
||||
}
|
||||
});
|
||||
|
||||
it('expect enq a string < ring buffer size ', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffer = 'test';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(buffer.length);
|
||||
expect(ringBuffer.empty()).to.be.equal(false);
|
||||
const reader = ringBuffer.getReader();
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(buffer.length);
|
||||
expect(ringBuffer.emptyForReader(reader)).to.be.equal(false);
|
||||
|
||||
});
|
||||
|
||||
it('expect deq a string < ring buffer size ', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
const buffer = 'test';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
expect(readBuffer).to.equal(buffer);
|
||||
|
||||
});
|
||||
|
||||
it('expect deq a string > ring buffer size ', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffer = 'testabcd';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
expect(readBuffer).to.equal(buffer.substring(buffer.length - size));
|
||||
|
||||
});
|
||||
|
||||
it('expect enq deq enq deq a string > ring buffer size ', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffer = '12345678';
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
expect(readBuffer).to.equal(buffer.substring(buffer.length - size));
|
||||
}
|
||||
});
|
||||
|
||||
it('expect enq a string == ring buffer size then one > ring buffer size and dequeue them ', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['12345', '12345678'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
}
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal(buffers[buffers.length - 1].substring(buffers[buffers.length - 1].length - size));
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq a string == ring buffer size then one < ring buffer size and dequeue them ', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['12345', '123'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
}
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('45123');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq a string == ring buffer size then one < ring buffer then one < buffer size and dequeue ', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['12345', '123', '678'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
}
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('23678');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq buffer size then enq 1 to dequeue the right value', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['12345', '1'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
}
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('23451');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq buffer size then enq 1 twice to dequeue the right value', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['12345', '1', '12345', '1'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
}
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('23451');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq buffer size of various sizes dequeue the right value', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['12345', '123', '678', '12345', '1', '12345', '123', '12', '12', '1', '12', '123', '1234', '12345', '1', '12'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
}
|
||||
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('45112');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq buffer sizes < buffer size to dequeue normally', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['1', '1'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
}
|
||||
expect(ringBuffer.size()).to.be.equal(2);
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('11');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq buffer size of various sizes dequeue the right value', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffers = ['1', '1', '12', '12'];
|
||||
|
||||
for (const buffer of buffers) {
|
||||
ringBuffer.enq(buffer);
|
||||
}
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
const reader = ringBuffer.getReader();
|
||||
let i = 0;
|
||||
for (const _ of buffers) {
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
if (i === 0) {
|
||||
expect(readBuffer).to.equal('11212');
|
||||
} else {
|
||||
expect(readBuffer).to.equal(undefined);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect multiple enq and deq to deq the right values', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
ringBuffer.enq('12345');
|
||||
|
||||
expect(ringBuffer.size()).to.be.equal(size);
|
||||
const reader = ringBuffer.getReader();
|
||||
let readBuffer = ringBuffer.deq(reader);
|
||||
expect(readBuffer).to.equal('12345');
|
||||
|
||||
ringBuffer.enq('123');
|
||||
readBuffer = ringBuffer.deq(reader);
|
||||
expect(readBuffer).to.equal('123');
|
||||
|
||||
ringBuffer.enq('12345');
|
||||
readBuffer = ringBuffer.deq(reader);
|
||||
expect(readBuffer).to.equal('12345');
|
||||
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect data from stream on enq', async function (): Promise<void> {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffer = 'abc';
|
||||
|
||||
const astream = ringBuffer.getStream();
|
||||
const p = new Promise<void>(resolve => {
|
||||
astream.on('data', (chunk: string) => {
|
||||
expect(chunk).to.be.equal(buffer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
ringBuffer.enq(buffer);
|
||||
|
||||
await p;
|
||||
});
|
||||
|
||||
it('expect data from stream when data is already ended', async function (): Promise<void> {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffer = 'abc';
|
||||
ringBuffer.enq(buffer);
|
||||
|
||||
const astream = ringBuffer.getStream();
|
||||
const p = new Promise<void>(resolve => {
|
||||
astream.on('data', (chunk: string) => {
|
||||
expect(chunk).to.be.equal(buffer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await p;
|
||||
});
|
||||
|
||||
it('expect disposing of a stream to delete it from the ringbuffer', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const astream = ringBuffer.getStream();
|
||||
astream.dispose();
|
||||
expect(ringBuffer.streamsSize()).to.be.equal(0);
|
||||
expect(ringBuffer.readersSize()).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect disposing of a reader to delete it from the ringbuffer', function (): void {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const reader = ringBuffer.getReader();
|
||||
ringBuffer.closeReader(reader);
|
||||
expect(ringBuffer.readersSize()).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect enq a string in utf8 and get it in hex', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
const buffer = 'test';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader, -1, 'hex');
|
||||
expect(readBuffer).to.equal('74657374');
|
||||
});
|
||||
|
||||
it('expect enq a string in hex and get it in utf8', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
const buffer = '74657374';
|
||||
|
||||
ringBuffer.enq(buffer, 'hex');
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
expect(readBuffer).to.equal('test');
|
||||
});
|
||||
|
||||
it('expect data from stream in hex when enq in uf8', async function (): Promise<void> {
|
||||
const size = 5;
|
||||
const ringBuffer = new MultiRingBuffer({ size });
|
||||
const buffer = 'test';
|
||||
ringBuffer.enq(buffer);
|
||||
|
||||
const astream = ringBuffer.getStream('hex');
|
||||
const p = new Promise<void>(resolve => {
|
||||
astream.on('data', (chunk: string) => {
|
||||
expect(chunk).to.be.equal('74657374');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await p;
|
||||
});
|
||||
|
||||
it('expect deq a string < ring buffer size with the internal encoding in hex ', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5, encoding: 'hex' });
|
||||
const buffer = 'test';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
const reader = ringBuffer.getReader();
|
||||
const readBuffer = ringBuffer.deq(reader);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
expect(readBuffer).to.equal(buffer);
|
||||
|
||||
});
|
||||
|
||||
it('expect the ringbuffer to be empty if we enq an empty string', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
const buffer = '';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(0);
|
||||
expect(ringBuffer.empty()).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('expect an invalid reader count to be zero', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
expect(ringBuffer.sizeForReader(1)).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('expect an invalid reader to be empty', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
expect(ringBuffer.emptyForReader(1)).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('expect partially deq a string < ring buffer size ', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
const buffer = 'test';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
const reader = ringBuffer.getReader();
|
||||
let readBuffer = ringBuffer.deq(reader, 2);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(2);
|
||||
expect(readBuffer).to.equal('te');
|
||||
|
||||
readBuffer = ringBuffer.deq(reader, 2);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(0);
|
||||
expect(readBuffer).to.equal('st');
|
||||
});
|
||||
|
||||
it('expect partially deq a string < ring buffer size then enq and deq again ', function (): void {
|
||||
const ringBuffer = new MultiRingBuffer({ size: 5 });
|
||||
const buffer = 'test';
|
||||
const secondBuffer = 'abcd';
|
||||
|
||||
ringBuffer.enq(buffer);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
const reader = ringBuffer.getReader();
|
||||
let readBuffer = ringBuffer.deq(reader, 2);
|
||||
expect(ringBuffer.size()).to.be.equal(4);
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(2);
|
||||
expect(readBuffer).to.equal('te');
|
||||
|
||||
ringBuffer.enq(secondBuffer);
|
||||
readBuffer = ringBuffer.deq(reader, 4);
|
||||
expect(ringBuffer.size()).to.be.equal(5);
|
||||
expect(readBuffer).to.equal('tabc');
|
||||
expect(ringBuffer.sizeForReader(reader)).to.be.equal(1);
|
||||
|
||||
});
|
||||
});
|
||||
348
packages/process/src/node/multi-ring-buffer.ts
Normal file
348
packages/process/src/node/multi-ring-buffer.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as stream from 'stream';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Disposable } from '@theia/core/lib/common';
|
||||
|
||||
/**
|
||||
* The MultiRingBuffer is a ring buffer implementation that allows
|
||||
* multiple independent readers.
|
||||
*
|
||||
* These readers are created using the getReader or getStream functions
|
||||
* to create a reader that can be read using deq() or one that is a readable stream.
|
||||
*/
|
||||
|
||||
export class MultiRingBufferReadableStream extends stream.Readable implements Disposable {
|
||||
|
||||
protected more = false;
|
||||
protected disposed = false;
|
||||
|
||||
constructor(protected readonly ringBuffer: MultiRingBuffer,
|
||||
protected readonly reader: number,
|
||||
protected readonly encoding: BufferEncoding = 'utf8'
|
||||
) {
|
||||
super();
|
||||
this.setEncoding(encoding);
|
||||
}
|
||||
|
||||
override _read(size: number): void {
|
||||
this.more = true;
|
||||
this.deq(size);
|
||||
}
|
||||
|
||||
override _destroy(err: Error | null, callback: (err: Error | null) => void): void {
|
||||
this.ringBuffer.closeStream(this);
|
||||
this.ringBuffer.closeReader(this.reader);
|
||||
this.disposed = true;
|
||||
this.removeAllListeners();
|
||||
callback(err);
|
||||
}
|
||||
|
||||
onData(): void {
|
||||
if (this.more === true) {
|
||||
this.deq(-1);
|
||||
}
|
||||
}
|
||||
|
||||
deq(size: number): void {
|
||||
if (this.disposed === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = undefined;
|
||||
do {
|
||||
buffer = this.ringBuffer.deq(this.reader, size, this.encoding);
|
||||
if (buffer !== undefined) {
|
||||
this.more = this.push(buffer, this.encoding);
|
||||
}
|
||||
}
|
||||
while (buffer !== undefined && this.more === true && this.disposed === false);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export const MultiRingBufferOptions = Symbol('MultiRingBufferOptions');
|
||||
export interface MultiRingBufferOptions {
|
||||
readonly size: number,
|
||||
readonly encoding?: BufferEncoding,
|
||||
}
|
||||
|
||||
export interface WrappedPosition { newPos: number, wrap: boolean }
|
||||
|
||||
@injectable()
|
||||
export class MultiRingBuffer implements Disposable {
|
||||
|
||||
protected readonly buffer: Buffer;
|
||||
protected head: number = -1;
|
||||
protected tail: number = -1;
|
||||
protected readonly maxSize: number;
|
||||
protected readonly encoding: BufferEncoding;
|
||||
|
||||
/* <id, position> */
|
||||
protected readonly readers: Map<number, number>;
|
||||
/* <stream : id> */
|
||||
protected readonly streams: Map<MultiRingBufferReadableStream, number>;
|
||||
protected readerId = 0;
|
||||
|
||||
constructor(
|
||||
@inject(MultiRingBufferOptions) protected readonly options: MultiRingBufferOptions
|
||||
) {
|
||||
this.maxSize = options.size;
|
||||
if (options.encoding !== undefined) {
|
||||
this.encoding = options.encoding;
|
||||
} else {
|
||||
this.encoding = 'utf8';
|
||||
}
|
||||
this.buffer = Buffer.alloc(this.maxSize);
|
||||
this.readers = new Map();
|
||||
this.streams = new Map();
|
||||
}
|
||||
|
||||
enq(str: string, encoding = 'utf8'): void {
|
||||
let buffer: Buffer = Buffer.from(str, encoding as BufferEncoding);
|
||||
|
||||
// Take the last elements of string if it's too big, drop the rest
|
||||
if (buffer.length > this.maxSize) {
|
||||
buffer = buffer.slice(buffer.length - this.maxSize);
|
||||
}
|
||||
|
||||
if (buffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// empty
|
||||
if (this.head === -1 && this.tail === -1) {
|
||||
this.head = 0;
|
||||
this.tail = 0;
|
||||
buffer.copy(this.buffer, this.head, 0, buffer.length);
|
||||
this.head = buffer.length - 1;
|
||||
this.onData(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const startHead = this.inc(this.head, 1).newPos;
|
||||
|
||||
if (this.inc(startHead, buffer.length).wrap === true) {
|
||||
buffer.copy(this.buffer, startHead, 0, this.maxSize - startHead);
|
||||
buffer.copy(this.buffer, 0, this.maxSize - startHead);
|
||||
} else {
|
||||
buffer.copy(this.buffer, startHead);
|
||||
}
|
||||
|
||||
this.incTails(buffer.length);
|
||||
this.head = this.inc(this.head, buffer.length).newPos;
|
||||
this.onData(startHead);
|
||||
}
|
||||
|
||||
getReader(): number {
|
||||
this.readers.set(this.readerId, this.tail);
|
||||
return this.readerId++;
|
||||
}
|
||||
|
||||
closeReader(id: number): void {
|
||||
this.readers.delete(id);
|
||||
}
|
||||
|
||||
getStream(encoding?: BufferEncoding): MultiRingBufferReadableStream {
|
||||
const reader = this.getReader();
|
||||
const readableStream = new MultiRingBufferReadableStream(this, reader, encoding);
|
||||
this.streams.set(readableStream, reader);
|
||||
return readableStream;
|
||||
}
|
||||
|
||||
closeStream(readableStream: MultiRingBufferReadableStream): void {
|
||||
this.streams.delete(<MultiRingBufferReadableStream>readableStream);
|
||||
}
|
||||
|
||||
protected onData(start: number): void {
|
||||
/* Any stream that has read everything already
|
||||
* Should go back to the last buffer in start offset */
|
||||
for (const [id, pos] of this.readers) {
|
||||
if (pos === -1) {
|
||||
this.readers.set(id, start);
|
||||
}
|
||||
}
|
||||
/* Notify the streams there's new data. */
|
||||
for (const [readableStream] of this.streams) {
|
||||
readableStream.onData();
|
||||
}
|
||||
}
|
||||
|
||||
deq(id: number, size = -1, encoding: BufferEncoding = 'utf8'): string | undefined {
|
||||
const pos = this.readers.get(id);
|
||||
if (pos === undefined || pos === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let buffer = '';
|
||||
const maxDeqSize = this.sizeForReader(id);
|
||||
const wrapped = this.isWrapped(pos, this.head);
|
||||
|
||||
let deqSize;
|
||||
if (size === -1) {
|
||||
deqSize = maxDeqSize;
|
||||
} else {
|
||||
deqSize = Math.min(size, maxDeqSize);
|
||||
}
|
||||
|
||||
if (wrapped === false) { // no wrap
|
||||
buffer = this.buffer.toString(encoding, pos, pos + deqSize);
|
||||
} else { // wrap
|
||||
buffer = buffer.concat(this.buffer.toString(encoding, pos, this.maxSize),
|
||||
this.buffer.toString(encoding, 0, deqSize - (this.maxSize - pos)));
|
||||
}
|
||||
|
||||
const lastIndex = this.inc(pos, deqSize - 1).newPos;
|
||||
// everything is read
|
||||
if (lastIndex === this.head) {
|
||||
this.readers.set(id, -1);
|
||||
} else {
|
||||
this.readers.set(id, this.inc(pos, deqSize).newPos);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
sizeForReader(id: number): number {
|
||||
const pos = this.readers.get(id);
|
||||
if (pos === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.sizeFrom(pos, this.head, this.isWrapped(pos, this.head));
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.sizeFrom(this.tail, this.head, this.isWrapped(this.tail, this.head));
|
||||
}
|
||||
|
||||
protected isWrapped(from: number, to: number): boolean {
|
||||
if (to < from) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
protected sizeFrom(from: number, to: number, wrap: boolean): number {
|
||||
if (from === -1 || to === -1) {
|
||||
return 0;
|
||||
} else {
|
||||
if (wrap === false) {
|
||||
return to - from + 1;
|
||||
} else {
|
||||
return to + 1 + this.maxSize - from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emptyForReader(id: number): boolean {
|
||||
const pos = this.readers.get(id);
|
||||
if (pos === undefined || pos === -1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
empty(): boolean {
|
||||
if (this.head === -1 && this.tail === -1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
streamsSize(): number {
|
||||
return this.streams.size;
|
||||
}
|
||||
|
||||
readersSize(): number {
|
||||
return this.readers.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all the attached readers/streams.
|
||||
*/
|
||||
dispose(): void {
|
||||
for (const readableStream of this.streams.keys()) {
|
||||
readableStream.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/* Position should be incremented if it goes pass end. */
|
||||
protected shouldIncPos(pos: number, end: number, size: number): boolean {
|
||||
const { newPos: newHead, wrap } = this.inc(end, size);
|
||||
|
||||
/* Tail Head */
|
||||
if (this.isWrapped(pos, end) === false) {
|
||||
// Head needs to wrap to push the tail
|
||||
if (wrap === true && newHead >= pos) {
|
||||
return true;
|
||||
}
|
||||
} else { /* Head Tail */
|
||||
// If we wrap head is pushing tail, or if it goes over pos
|
||||
if (wrap === true || newHead >= pos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected incTailSize(pos: number, head: number, size: number): WrappedPosition {
|
||||
const { newPos: newHead } = this.inc(head, size);
|
||||
/* New tail is 1 past newHead. */
|
||||
return this.inc(newHead, 1);
|
||||
}
|
||||
|
||||
protected incTail(pos: number, size: number): WrappedPosition {
|
||||
|
||||
if (this.shouldIncPos(pos, this.head, size) === false) {
|
||||
return { newPos: pos, wrap: false };
|
||||
}
|
||||
|
||||
return this.incTailSize(pos, this.head, size);
|
||||
}
|
||||
|
||||
/* Increment the main tail and all the reader positions. */
|
||||
protected incTails(size: number): void {
|
||||
this.tail = this.incTail(this.tail, size).newPos;
|
||||
|
||||
for (const [id, pos] of this.readers) {
|
||||
if (pos !== -1) {
|
||||
if (this.shouldIncPos(pos, this.tail, size) === true) {
|
||||
this.readers.set(id, this.tail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected inc(pos: number, size: number): WrappedPosition {
|
||||
if (size === 0) {
|
||||
return { newPos: pos, wrap: false };
|
||||
}
|
||||
const newPos = (pos + size) % this.maxSize;
|
||||
const wrap = newPos <= pos;
|
||||
return { newPos, wrap };
|
||||
}
|
||||
}
|
||||
62
packages/process/src/node/process-backend-module.ts
Normal file
62
packages/process/src/node/process-backend-module.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule, Container } from '@theia/core/shared/inversify';
|
||||
import { RawProcess, RawProcessOptions, RawProcessFactory, RawForkOptions } from './raw-process';
|
||||
import { TerminalProcess, TerminalProcessOptions, TerminalProcessFactory } from './terminal-process';
|
||||
import { TaskTerminalProcess, TaskTerminalProcessFactory } from './task-terminal-process';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { ProcessManager } from './process-manager';
|
||||
import { MultiRingBuffer, MultiRingBufferOptions } from './multi-ring-buffer';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(RawProcess).toSelf().inTransientScope();
|
||||
bind(ProcessManager).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(ProcessManager);
|
||||
bind(RawProcessFactory).toFactory(ctx =>
|
||||
(options: RawProcessOptions | RawForkOptions) => {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = ctx.container;
|
||||
|
||||
child.bind(RawProcessOptions).toConstantValue(options);
|
||||
return child.get(RawProcess);
|
||||
}
|
||||
);
|
||||
|
||||
bind(TerminalProcess).toSelf().inTransientScope();
|
||||
bind(TerminalProcessFactory).toFactory(ctx =>
|
||||
(options: TerminalProcessOptions) => {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = ctx.container;
|
||||
|
||||
child.bind(TerminalProcessOptions).toConstantValue(options);
|
||||
return child.get(TerminalProcess);
|
||||
}
|
||||
);
|
||||
|
||||
bind(TaskTerminalProcess).toSelf().inTransientScope();
|
||||
bind(TaskTerminalProcessFactory).toFactory(ctx =>
|
||||
(options: TerminalProcessOptions) => {
|
||||
const child = ctx.container.createChild();
|
||||
child.bind(TerminalProcessOptions).toConstantValue(options);
|
||||
return child.get(TaskTerminalProcess);
|
||||
}
|
||||
);
|
||||
|
||||
bind(MultiRingBuffer).toSelf().inTransientScope();
|
||||
/* 1MB size, TODO should be a user preference. */
|
||||
bind(MultiRingBufferOptions).toConstantValue({ size: 1048576 });
|
||||
});
|
||||
107
packages/process/src/node/process-manager.ts
Normal file
107
packages/process/src/node/process-manager.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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, named } from '@theia/core/shared/inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { ManagedProcessManager, ManagedProcess } from '../common/process-manager-types';
|
||||
import { MAX_SAFE_INTEGER } from '@theia/core/lib/common/numbers';
|
||||
import { Process } from './process';
|
||||
|
||||
@injectable()
|
||||
export class ProcessManager implements ManagedProcessManager, BackendApplicationContribution {
|
||||
|
||||
protected readonly processes: Map<number, Process>;
|
||||
protected readonly deleteEmitter: Emitter<number>;
|
||||
|
||||
constructor(
|
||||
@inject(ILogger) @named('process') protected logger: ILogger
|
||||
) {
|
||||
this.processes = new Map();
|
||||
this.deleteEmitter = new Emitter<number>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given process into this manager. Both on process termination and on error,
|
||||
* the process will be automatically removed from the manager.
|
||||
*
|
||||
* @param process the process to register.
|
||||
*/
|
||||
register(process: Process): number {
|
||||
const id = this.generateId();
|
||||
this.processes.set(id, process);
|
||||
process.onError(() => this.unregister(process));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a random id for a process that is not assigned to a different process yet.
|
||||
*/
|
||||
protected generateId(): number {
|
||||
let id = undefined;
|
||||
while (id === undefined) {
|
||||
const candidate = Math.floor(Math.random() * MAX_SAFE_INTEGER);
|
||||
if (!this.processes.has(candidate)) {
|
||||
id = candidate;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the process from this process manager. Invoking this method, will make
|
||||
* sure that the process is terminated before eliminating it from the manager's cache.
|
||||
*
|
||||
* @param process the process to unregister from this process manager.
|
||||
*/
|
||||
unregister(process: ManagedProcess): void {
|
||||
const processLabel = this.getProcessLabel(process);
|
||||
this.logger.debug(`Unregistering process. ${processLabel}`);
|
||||
if (!process.killed) {
|
||||
this.logger.debug(`Ensuring process termination. ${processLabel}`);
|
||||
process.kill();
|
||||
}
|
||||
if (this.processes.delete(process.id)) {
|
||||
this.deleteEmitter.fire(process.id);
|
||||
this.logger.debug(`The process was successfully unregistered. ${processLabel}`);
|
||||
} else {
|
||||
this.logger.warn(`This process was not registered or was already unregistered. ${processLabel}`);
|
||||
}
|
||||
}
|
||||
|
||||
get(id: number): ManagedProcess | undefined {
|
||||
return this.processes.get(id);
|
||||
}
|
||||
|
||||
get onDelete(): Event<number> {
|
||||
return this.deleteEmitter.event;
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
for (const process of this.processes.values()) {
|
||||
try {
|
||||
this.unregister(process);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error occurred when unregistering process. ${this.getProcessLabel(process)}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getProcessLabel(process: ManagedProcess): string {
|
||||
return `[ID: ${process.id}]`;
|
||||
}
|
||||
|
||||
}
|
||||
207
packages/process/src/node/process.ts
Normal file
207
packages/process/src/node/process.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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, unmanaged } from '@theia/core/shared/inversify';
|
||||
import { ILogger, Emitter, Event, isObject } from '@theia/core/lib/common';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { isOSX, isWindows } from '@theia/core';
|
||||
import { Readable, Writable } from 'stream';
|
||||
import { exec } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import { IProcessStartEvent, IProcessExitEvent, ProcessErrorEvent, ProcessType, ManagedProcessManager, ManagedProcess } from '../common/process-manager-types';
|
||||
export { IProcessStartEvent, IProcessExitEvent, ProcessErrorEvent, ProcessType };
|
||||
|
||||
/**
|
||||
* Options to spawn a new process (`spawn`).
|
||||
*
|
||||
* For more information please refer to the spawn function of Node's
|
||||
* child_process module:
|
||||
*
|
||||
* https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
|
||||
*/
|
||||
export interface ProcessOptions {
|
||||
readonly command: string,
|
||||
args?: string[],
|
||||
options?: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to fork a new process using the current Node interpreter (`fork`).
|
||||
*
|
||||
* For more information please refer to the fork function of Node's
|
||||
* child_process module:
|
||||
*
|
||||
* https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
|
||||
*/
|
||||
export interface ForkOptions {
|
||||
readonly modulePath: string,
|
||||
args?: string[],
|
||||
options?: object
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class Process implements ManagedProcess {
|
||||
|
||||
readonly id: number;
|
||||
protected readonly startEmitter: Emitter<IProcessStartEvent> = new Emitter<IProcessStartEvent>();
|
||||
protected readonly exitEmitter: Emitter<IProcessExitEvent> = new Emitter<IProcessExitEvent>();
|
||||
protected readonly closeEmitter: Emitter<IProcessExitEvent> = new Emitter<IProcessExitEvent>();
|
||||
protected readonly errorEmitter: Emitter<ProcessErrorEvent> = new Emitter<ProcessErrorEvent>();
|
||||
protected _killed = false;
|
||||
|
||||
/**
|
||||
* The OS process id.
|
||||
*/
|
||||
abstract readonly pid: number;
|
||||
|
||||
/**
|
||||
* The stdout stream.
|
||||
*/
|
||||
abstract readonly outputStream: Readable;
|
||||
|
||||
/**
|
||||
* The stderr stream.
|
||||
*/
|
||||
abstract readonly errorStream: Readable;
|
||||
|
||||
/**
|
||||
* The stdin stream.
|
||||
*/
|
||||
abstract readonly inputStream: Writable;
|
||||
|
||||
constructor(
|
||||
@unmanaged() protected readonly processManager: ManagedProcessManager,
|
||||
@unmanaged() protected readonly logger: ILogger,
|
||||
@unmanaged() protected readonly type: ProcessType,
|
||||
@unmanaged() protected readonly options: ProcessOptions | ForkOptions
|
||||
) {
|
||||
this.id = this.processManager.register(this);
|
||||
this.initialCwd = options && options.options && 'cwd' in options.options && options.options['cwd'].toString() || __dirname;
|
||||
}
|
||||
|
||||
abstract kill(signal?: string): void;
|
||||
|
||||
get killed(): boolean {
|
||||
return this._killed;
|
||||
}
|
||||
|
||||
get onStart(): Event<IProcessStartEvent> {
|
||||
return this.startEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the process to exit, streams can still emit data.
|
||||
*/
|
||||
get onExit(): Event<IProcessExitEvent> {
|
||||
return this.exitEmitter.event;
|
||||
}
|
||||
|
||||
get onError(): Event<ProcessErrorEvent> {
|
||||
return this.errorEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for both process exit and for all the streams to be closed.
|
||||
*/
|
||||
get onClose(): Event<IProcessExitEvent> {
|
||||
return this.closeEmitter.event;
|
||||
}
|
||||
|
||||
protected emitOnStarted(): void {
|
||||
this.startEmitter.fire({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the onExit event for this process. Only one of code and signal
|
||||
* should be defined.
|
||||
*/
|
||||
protected emitOnExit(code?: number, signal?: string): void {
|
||||
const exitEvent: IProcessExitEvent = { code, signal };
|
||||
this.handleOnExit(exitEvent);
|
||||
this.exitEmitter.fire(exitEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the onClose event for this process. Only one of code and signal
|
||||
* should be defined.
|
||||
*/
|
||||
protected emitOnClose(code?: number, signal?: string): void {
|
||||
this.closeEmitter.fire({ code, signal });
|
||||
}
|
||||
|
||||
protected handleOnExit(event: IProcessExitEvent): void {
|
||||
this._killed = true;
|
||||
const signalSuffix = event.signal ? `, signal: ${event.signal}` : '';
|
||||
const executable = this.isForkOptions(this.options) ? this.options.modulePath : this.options.command;
|
||||
|
||||
this.logger.debug(`Process ${this.pid} has exited with code ${event.code}${signalSuffix}.`,
|
||||
executable, this.options.args);
|
||||
}
|
||||
|
||||
protected emitOnError(err: ProcessErrorEvent): void {
|
||||
this.handleOnError(err);
|
||||
this.errorEmitter.fire(err);
|
||||
}
|
||||
|
||||
protected async emitOnErrorAsync(error: ProcessErrorEvent): Promise<void> {
|
||||
process.nextTick(this.emitOnError.bind(this), error);
|
||||
}
|
||||
|
||||
protected handleOnError(error: ProcessErrorEvent): void {
|
||||
this._killed = true;
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
protected isForkOptions(options: unknown): options is ForkOptions {
|
||||
return isObject<ForkOptions>(options) && !!options.modulePath;
|
||||
}
|
||||
|
||||
protected readonly initialCwd: string;
|
||||
|
||||
/**
|
||||
* @returns the current working directory as a URI (usually file:// URI)
|
||||
*/
|
||||
public getCwdURI(): Promise<string> {
|
||||
if (isOSX) {
|
||||
return new Promise<string>(resolve => {
|
||||
exec('lsof -OPln -p ' + this.pid + ' | grep cwd', (error, stdout, stderr) => {
|
||||
if (stdout !== '') {
|
||||
resolve(FileUri.create(stdout.substring(stdout.indexOf('/'), stdout.length - 1)).toString());
|
||||
} else {
|
||||
resolve(FileUri.create(this.initialCwd).toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (!isWindows) {
|
||||
return new Promise<string>(resolve => {
|
||||
fs.readlink('/proc/' + this.pid + '/cwd', (err, linkedstr) => {
|
||||
if (err || !linkedstr) {
|
||||
resolve(FileUri.create(this.initialCwd).toString());
|
||||
} else {
|
||||
resolve(FileUri.create(linkedstr).toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new Promise<string>(resolve => {
|
||||
resolve(FileUri.create(this.initialCwd).toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
56
packages/process/src/node/pseudo-pty.ts
Normal file
56
packages/process/src/node/pseudo-pty.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Alibaba 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 { IPty } from 'node-pty';
|
||||
import { Event } from '@theia/core';
|
||||
|
||||
export class PseudoPty implements IPty {
|
||||
|
||||
readonly pid: number = -1;
|
||||
|
||||
readonly cols: number = -1;
|
||||
|
||||
readonly rows: number = -1;
|
||||
|
||||
readonly process: string = '';
|
||||
|
||||
handleFlowControl = false;
|
||||
|
||||
readonly onData: Event<string> = Event.None;
|
||||
|
||||
readonly onExit: Event<{ exitCode: number, signal?: number }> = Event.None;
|
||||
|
||||
on(event: string, listener: (data: string) => void): void;
|
||||
|
||||
on(event: string, listener: (exitCode: number, signal?: number) => void): void;
|
||||
|
||||
on(event: string, listener: (error?: string) => void): void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
on(event: string, listener: (...args: any[]) => void): void { }
|
||||
|
||||
resize(columns: number, rows: number): void { }
|
||||
|
||||
write(data: string): void { }
|
||||
|
||||
kill(signal?: string): void { }
|
||||
|
||||
pause(): void { }
|
||||
|
||||
resume(): void { }
|
||||
|
||||
clear(): void { }
|
||||
}
|
||||
197
packages/process/src/node/raw-process.spec.ts
Normal file
197
packages/process/src/node/raw-process.spec.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import * as chai from 'chai';
|
||||
import * as process from 'process';
|
||||
import * as stream from 'stream';
|
||||
import { createProcessTestContainer } from './test/process-test-container';
|
||||
import { RawProcessFactory } from './raw-process';
|
||||
import * as temp from 'temp';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { IProcessStartEvent, ProcessErrorEvent } from './process';
|
||||
|
||||
/* Allow to create temporary files, but delete them when we're done. */
|
||||
const track = temp.track();
|
||||
|
||||
/**
|
||||
* Globals
|
||||
*/
|
||||
|
||||
const expect = chai.expect;
|
||||
const FORK_TEST_FILE = path.join(__dirname, '../../src/node/test/process-fork-test.js');
|
||||
|
||||
describe('RawProcess', function (): void {
|
||||
|
||||
this.timeout(20_000);
|
||||
|
||||
let rawProcessFactory: RawProcessFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
rawProcessFactory = createProcessTestContainer().get<RawProcessFactory>(RawProcessFactory);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
track.cleanupSync();
|
||||
});
|
||||
|
||||
it('test error on non-existent path', async function (): Promise<void> {
|
||||
const error = await new Promise<ProcessErrorEvent>((resolve, reject) => {
|
||||
const proc = rawProcessFactory({ command: '/non-existent' });
|
||||
proc.onStart(reject);
|
||||
proc.onError(resolve);
|
||||
proc.onExit(reject);
|
||||
});
|
||||
|
||||
expect(error.code).eq('ENOENT');
|
||||
});
|
||||
|
||||
it('test error on non-executable path', async function (): Promise<void> {
|
||||
// Create a non-executable file.
|
||||
const f = track.openSync('non-executable');
|
||||
fs.writeSync(f.fd, 'echo bob');
|
||||
|
||||
// Make really sure it's non-executable.
|
||||
let mode = fs.fstatSync(f.fd).mode;
|
||||
mode &= ~fs.constants.S_IXUSR;
|
||||
mode &= ~fs.constants.S_IXGRP;
|
||||
mode &= ~fs.constants.S_IXOTH;
|
||||
fs.fchmodSync(f.fd, mode);
|
||||
|
||||
fs.closeSync(f.fd);
|
||||
|
||||
const error = await new Promise<ProcessErrorEvent>((resolve, reject) => {
|
||||
const proc = rawProcessFactory({ command: f.path });
|
||||
proc.onStart(reject);
|
||||
proc.onError(resolve);
|
||||
proc.onExit(reject);
|
||||
});
|
||||
|
||||
// do not check the exact error code as this seems to change between nodejs version
|
||||
|
||||
expect(error).to.exist;
|
||||
});
|
||||
|
||||
it('test start event', function (): Promise<IProcessStartEvent> {
|
||||
return new Promise<IProcessStartEvent>(async (resolve, reject) => {
|
||||
const args = ['-e', 'process.exit(3)'];
|
||||
const rawProcess = rawProcessFactory({ command: process.execPath, 'args': args });
|
||||
rawProcess.onStart(resolve);
|
||||
rawProcess.onError(reject);
|
||||
rawProcess.onExit(reject);
|
||||
});
|
||||
});
|
||||
|
||||
it('test exit', async function (): Promise<void> {
|
||||
const args = ['--version'];
|
||||
const rawProcess = rawProcessFactory({ command: process.execPath, 'args': args });
|
||||
const p = new Promise<number>((resolve, reject) => {
|
||||
rawProcess.onError(reject);
|
||||
rawProcess.onExit(event => {
|
||||
if (event.code === undefined) {
|
||||
reject(new Error('event.code is undefined'));
|
||||
} else {
|
||||
resolve(event.code);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const exitCode = await p;
|
||||
expect(exitCode).equal(0);
|
||||
});
|
||||
|
||||
it('test pipe stdout stream', async function (): Promise<void> {
|
||||
const output = await new Promise<string>(async (resolve, reject) => {
|
||||
const args = ['-e', 'console.log("text to stdout")'];
|
||||
const outStream = new stream.PassThrough();
|
||||
const rawProcess = rawProcessFactory({ command: process.execPath, 'args': args });
|
||||
rawProcess.onError(reject);
|
||||
|
||||
rawProcess.outputStream.pipe(outStream);
|
||||
|
||||
let buf = '';
|
||||
outStream.on('data', data => {
|
||||
buf += data.toString();
|
||||
});
|
||||
outStream.on('end', () => {
|
||||
resolve(buf.trim());
|
||||
});
|
||||
});
|
||||
|
||||
expect(output).to.be.equal('text to stdout');
|
||||
});
|
||||
|
||||
it('test pipe stderr stream', async function (): Promise<void> {
|
||||
const output = await new Promise<string>(async (resolve, reject) => {
|
||||
const args = ['-e', 'console.error("text to stderr")'];
|
||||
const outStream = new stream.PassThrough();
|
||||
const rawProcess = rawProcessFactory({ command: process.execPath, 'args': args });
|
||||
rawProcess.onError(reject);
|
||||
|
||||
rawProcess.errorStream.pipe(outStream);
|
||||
|
||||
let buf = '';
|
||||
outStream.on('data', data => {
|
||||
buf += data.toString();
|
||||
});
|
||||
outStream.on('end', () => {
|
||||
resolve(buf.trim());
|
||||
});
|
||||
});
|
||||
|
||||
expect(output).to.be.equal('text to stderr');
|
||||
});
|
||||
|
||||
it('test forked pipe stdout stream', async function (): Promise<void> {
|
||||
const args = ['version'];
|
||||
const rawProcess = rawProcessFactory({ modulePath: FORK_TEST_FILE, args, options: { stdio: 'pipe' } });
|
||||
|
||||
const outStream = new stream.PassThrough();
|
||||
|
||||
const p = new Promise<string>((resolve, reject) => {
|
||||
let version = '';
|
||||
outStream.on('data', data => {
|
||||
version += data.toString();
|
||||
});
|
||||
outStream.on('end', () => {
|
||||
resolve(version.trim());
|
||||
});
|
||||
});
|
||||
|
||||
rawProcess.outputStream.pipe(outStream);
|
||||
|
||||
expect(await p).to.be.equal('1.0.0');
|
||||
});
|
||||
|
||||
it('test forked pipe stderr stream', async function (): Promise<void> {
|
||||
const rawProcess = rawProcessFactory({ modulePath: FORK_TEST_FILE, args: [], options: { stdio: 'pipe' } });
|
||||
|
||||
const outStream = new stream.PassThrough();
|
||||
|
||||
const p = new Promise<string>((resolve, reject) => {
|
||||
let version = '';
|
||||
outStream.on('data', data => {
|
||||
version += data.toString();
|
||||
});
|
||||
outStream.on('end', () => {
|
||||
resolve(version.trim());
|
||||
});
|
||||
});
|
||||
|
||||
rawProcess.errorStream.pipe(outStream);
|
||||
|
||||
expect(await p).to.have.string('Error');
|
||||
});
|
||||
});
|
||||
156
packages/process/src/node/raw-process.ts
Normal file
156
packages/process/src/node/raw-process.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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, named } from '@theia/core/shared/inversify';
|
||||
import { ProcessManager } from './process-manager';
|
||||
import { ILogger } from '@theia/core/lib/common';
|
||||
import { Process, ProcessType, ProcessOptions, ForkOptions, ProcessErrorEvent } from './process';
|
||||
import { ChildProcess, spawn, fork } from 'child_process';
|
||||
import * as stream from 'stream';
|
||||
|
||||
// The class was here before, exporting to not break anything.
|
||||
export { DevNullStream } from './dev-null-stream';
|
||||
import { DevNullStream } from './dev-null-stream';
|
||||
|
||||
export const RawProcessOptions = Symbol('RawProcessOptions');
|
||||
|
||||
/**
|
||||
* Options to spawn a new process (`spawn`).
|
||||
*
|
||||
* For more information please refer to the spawn function of Node's
|
||||
* child_process module:
|
||||
*
|
||||
* https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
|
||||
*/
|
||||
export interface RawProcessOptions extends ProcessOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to fork a new process using the current Node interpreter (`fork`).
|
||||
*
|
||||
* For more information please refer to the fork function of Node's
|
||||
* `child_process` module:
|
||||
*
|
||||
* https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
|
||||
*/
|
||||
export interface RawForkOptions extends ForkOptions {
|
||||
}
|
||||
|
||||
export const RawProcessFactory = Symbol('RawProcessFactory');
|
||||
export interface RawProcessFactory {
|
||||
(options: RawProcessOptions | RawForkOptions): RawProcess;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class RawProcess extends Process {
|
||||
|
||||
/**
|
||||
* If the process fails to launch, it will be undefined.
|
||||
*/
|
||||
readonly process: ChildProcess | undefined;
|
||||
|
||||
readonly outputStream: stream.Readable;
|
||||
readonly errorStream: stream.Readable;
|
||||
readonly inputStream: stream.Writable;
|
||||
|
||||
constructor( // eslint-disable-next-line @typescript-eslint/indent
|
||||
@inject(RawProcessOptions) options: RawProcessOptions | RawForkOptions,
|
||||
@inject(ProcessManager) processManager: ProcessManager,
|
||||
@inject(ILogger) @named('process') logger: ILogger
|
||||
) {
|
||||
super(processManager, logger, ProcessType.Raw, options);
|
||||
const executable = this.isForkOptions(options) ? options.modulePath : options.command;
|
||||
|
||||
this.logger.debug(`Starting raw process: ${executable},`
|
||||
+ ` with args: ${options.args ? options.args.join(' ') : ''}, `
|
||||
+ ` with options: ${JSON.stringify(options.options)}`);
|
||||
|
||||
// About catching errors: spawn will sometimes throw directly
|
||||
// (EACCES on Linux), sometimes return a Process object with the pid
|
||||
// property undefined (ENOENT on Linux) and then emit an 'error' event.
|
||||
// For now, we try to normalize that into always emitting an 'error'
|
||||
// event.
|
||||
try {
|
||||
if (this.isForkOptions(options)) {
|
||||
this.process = fork(
|
||||
options.modulePath,
|
||||
options.args || [],
|
||||
options.options || {});
|
||||
} else {
|
||||
this.process = spawn(
|
||||
options.command,
|
||||
options.args || [],
|
||||
options.options || {});
|
||||
}
|
||||
|
||||
this.process.on('error', (error: NodeJS.ErrnoException) => {
|
||||
error.code = error.code || 'Unknown error';
|
||||
this.emitOnError(error as ProcessErrorEvent);
|
||||
});
|
||||
|
||||
// When no stdio option is passed, it is null by default.
|
||||
this.outputStream = this.process.stdout || new DevNullStream({ autoDestroy: true });
|
||||
this.inputStream = this.process.stdin || new DevNullStream({ autoDestroy: true });
|
||||
this.errorStream = this.process.stderr || new DevNullStream({ autoDestroy: true });
|
||||
|
||||
this.process.on('exit', (exitCode, signal) => {
|
||||
// node's child_process exit sets the unused parameter to null,
|
||||
// but we want it to be undefined instead.
|
||||
this.emitOnExit(
|
||||
typeof exitCode === 'number' ? exitCode : undefined,
|
||||
typeof signal === 'string' ? signal : undefined,
|
||||
);
|
||||
this.processManager.unregister(this);
|
||||
});
|
||||
|
||||
this.process.on('close', (exitCode, signal) => {
|
||||
// node's child_process exit sets the unused parameter to null,
|
||||
// but we want it to be undefined instead.
|
||||
this.emitOnClose(
|
||||
typeof exitCode === 'number' ? exitCode : undefined,
|
||||
typeof signal === 'string' ? signal : undefined,
|
||||
);
|
||||
});
|
||||
|
||||
if (this.process.pid !== undefined) {
|
||||
process.nextTick(this.emitOnStarted.bind(this));
|
||||
}
|
||||
} catch (error) {
|
||||
/* When an error is thrown, set up some fake streams, so the client
|
||||
code doesn't break because these field are undefined. */
|
||||
this.outputStream = new DevNullStream({ autoDestroy: true });
|
||||
this.inputStream = new DevNullStream({ autoDestroy: true });
|
||||
this.errorStream = new DevNullStream({ autoDestroy: true });
|
||||
|
||||
/* Call the client error handler, but first give them a chance to register it. */
|
||||
this.emitOnErrorAsync(error);
|
||||
}
|
||||
}
|
||||
|
||||
get pid(): number {
|
||||
if (!this.process || !this.process.pid) {
|
||||
throw new Error('process did not start correctly');
|
||||
}
|
||||
return this.process.pid;
|
||||
}
|
||||
|
||||
kill(signal?: string): void {
|
||||
if (this.process && this.killed === false) {
|
||||
this.process.kill(signal as NodeJS.Signals);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
packages/process/src/node/string-argv.d.ts
vendored
Normal file
21
packages/process/src/node/string-argv.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 TypeFox 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
|
||||
// *****************************************************************************
|
||||
|
||||
declare module 'string-argv' {
|
||||
function stringArgv(...args: string[]): string[];
|
||||
export = stringArgv;
|
||||
}
|
||||
|
||||
41
packages/process/src/node/task-terminal-process.ts
Normal file
41
packages/process/src/node/task-terminal-process.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company 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 } from '@theia/core/shared/inversify';
|
||||
import { TerminalProcess, TerminalProcessOptions } from './terminal-process';
|
||||
|
||||
export const TaskTerminalProcessFactory = Symbol('TaskTerminalProcessFactory');
|
||||
export interface TaskTerminalProcessFactory {
|
||||
(options: TerminalProcessOptions): TaskTerminalProcess;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class TaskTerminalProcess extends TerminalProcess {
|
||||
|
||||
public exited = false;
|
||||
public attachmentAttempted = false;
|
||||
|
||||
protected override onTerminalExit(code: number | undefined, signal: string | undefined): void {
|
||||
this.emitOnExit(code, signal);
|
||||
this.exited = true;
|
||||
// Unregister process only if task terminal already attached (or failed attach),
|
||||
// Fixes https://github.com/eclipse-theia/theia/issues/2961
|
||||
if (this.attachmentAttempted) {
|
||||
this.unregisterProcess();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
119
packages/process/src/node/terminal-process.spec.ts
Normal file
119
packages/process/src/node/terminal-process.spec.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import * as chai from 'chai';
|
||||
import * as process from 'process';
|
||||
import * as stream from 'stream';
|
||||
import { createProcessTestContainer } from './test/process-test-container';
|
||||
import { TerminalProcessFactory } from './terminal-process';
|
||||
import { IProcessExitEvent, ProcessErrorEvent } from './process';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
|
||||
/**
|
||||
* Globals
|
||||
*/
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
let terminalProcessFactory: TerminalProcessFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
terminalProcessFactory = createProcessTestContainer().get<TerminalProcessFactory>(TerminalProcessFactory);
|
||||
});
|
||||
|
||||
describe('TerminalProcess', function (): void {
|
||||
|
||||
this.timeout(20_000);
|
||||
|
||||
it('test error on non existent path', async function (): Promise<void> {
|
||||
const error = await new Promise<ProcessErrorEvent | IProcessExitEvent>((resolve, reject) => {
|
||||
const proc = terminalProcessFactory({ command: '/non-existent' });
|
||||
proc.onError(resolve);
|
||||
proc.onExit(resolve);
|
||||
});
|
||||
|
||||
if (isWindows) {
|
||||
expect(error.code).eq('ENOENT');
|
||||
} else {
|
||||
expect(error.code).eq(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('test implicit .exe (Windows only)', async function (): Promise<void> {
|
||||
const match = /^(.+)\.exe$/.exec(process.execPath);
|
||||
if (!isWindows || !match) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
const command = match[1];
|
||||
const args = ['--version'];
|
||||
const terminal = await new Promise<IProcessExitEvent>((resolve, reject) => {
|
||||
const proc = terminalProcessFactory({ command, args });
|
||||
proc.onExit(resolve);
|
||||
proc.onError(reject);
|
||||
});
|
||||
|
||||
expect(terminal.code).to.exist;
|
||||
});
|
||||
|
||||
it('test error on trying to execute a directory', async function (): Promise<void> {
|
||||
const error = await new Promise<ProcessErrorEvent | IProcessExitEvent>((resolve, reject) => {
|
||||
const proc = terminalProcessFactory({ command: __dirname });
|
||||
proc.onError(resolve);
|
||||
proc.onExit(resolve);
|
||||
});
|
||||
if (isWindows) {
|
||||
expect(error.code).eq('ENOENT');
|
||||
} else {
|
||||
expect(error.code).eq(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('test exit', async function (): Promise<void> {
|
||||
const args = ['--version'];
|
||||
const exit = await new Promise<IProcessExitEvent>((resolve, reject) => {
|
||||
const proc = terminalProcessFactory({ command: process.execPath, args });
|
||||
proc.onExit(resolve);
|
||||
proc.onError(reject);
|
||||
});
|
||||
|
||||
expect(exit.code).eq(0);
|
||||
});
|
||||
|
||||
it('test pipe stream', async function (): Promise<void> {
|
||||
const v = await new Promise<string>((resolve, reject) => {
|
||||
const args = ['--version'];
|
||||
const terminalProcess = terminalProcessFactory({ command: process.execPath, args });
|
||||
terminalProcess.onError(reject);
|
||||
const outStream = new stream.PassThrough();
|
||||
|
||||
terminalProcess.createOutputStream().pipe(outStream);
|
||||
|
||||
let version = '';
|
||||
outStream.on('data', data => {
|
||||
version += data.toString();
|
||||
});
|
||||
/* node-pty is not sending 'end' on the stream as it quits
|
||||
only 'exit' is sent on the terminal process. */
|
||||
terminalProcess.onExit(() => {
|
||||
resolve(version.trim());
|
||||
});
|
||||
});
|
||||
|
||||
/* Avoid using equal since terminal characters can be inserted at the end. */
|
||||
expect(v).to.have.string(process.version);
|
||||
});
|
||||
|
||||
});
|
||||
294
packages/process/src/node/terminal-process.ts
Normal file
294
packages/process/src/node/terminal-process.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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, named } from '@theia/core/shared/inversify';
|
||||
import { Disposable, DisposableCollection, Emitter, Event, isWindows } from '@theia/core';
|
||||
import { ILogger } from '@theia/core/lib/common';
|
||||
import { Process, ProcessType, ProcessOptions, /* ProcessErrorEvent */ } from './process';
|
||||
import { ProcessManager } from './process-manager';
|
||||
import { IPty, spawn } from 'node-pty';
|
||||
import { MultiRingBuffer, MultiRingBufferReadableStream } from './multi-ring-buffer';
|
||||
import { DevNullStream } from './dev-null-stream';
|
||||
import { signame } from './utils';
|
||||
import { PseudoPty } from './pseudo-pty';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
export const TerminalProcessOptions = Symbol('TerminalProcessOptions');
|
||||
export interface TerminalProcessOptions extends ProcessOptions {
|
||||
/**
|
||||
* Windows only. Allow passing complex command lines already escaped for CommandLineToArgvW.
|
||||
*/
|
||||
commandLine?: string;
|
||||
isPseudo?: boolean;
|
||||
}
|
||||
|
||||
export const TerminalProcessFactory = Symbol('TerminalProcessFactory');
|
||||
export interface TerminalProcessFactory {
|
||||
(options: TerminalProcessOptions): TerminalProcess;
|
||||
}
|
||||
|
||||
export enum NodePtyErrors {
|
||||
EACCES = 'Permission denied',
|
||||
ENOENT = 'No such file or directory'
|
||||
}
|
||||
|
||||
/**
|
||||
* Run arbitrary processes inside pseudo-terminals (PTY).
|
||||
*
|
||||
* Note: a PTY is not a shell process (bash/pwsh/cmd...)
|
||||
*/
|
||||
@injectable()
|
||||
export class TerminalProcess extends Process {
|
||||
|
||||
protected readonly terminal: IPty | undefined;
|
||||
private _delayedResizer: DelayedResizer | undefined;
|
||||
private _exitCode: number | undefined;
|
||||
|
||||
readonly outputStream = this.createOutputStream();
|
||||
readonly errorStream = new DevNullStream({ autoDestroy: true });
|
||||
readonly inputStream: Writable;
|
||||
|
||||
constructor( // eslint-disable-next-line @typescript-eslint/indent
|
||||
@inject(TerminalProcessOptions) protected override readonly options: TerminalProcessOptions,
|
||||
@inject(ProcessManager) processManager: ProcessManager,
|
||||
@inject(MultiRingBuffer) protected readonly ringBuffer: MultiRingBuffer,
|
||||
@inject(ILogger) @named('process') logger: ILogger
|
||||
) {
|
||||
super(processManager, logger, ProcessType.Terminal, options);
|
||||
|
||||
if (options.isPseudo) {
|
||||
// do not need to spawn a process, new a pseudo pty instead
|
||||
this.terminal = new PseudoPty();
|
||||
this.inputStream = new DevNullStream({ autoDestroy: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isForkOptions(this.options)) {
|
||||
throw new Error('terminal processes cannot be forked as of today');
|
||||
}
|
||||
this.logger.debug('Starting terminal process', JSON.stringify(options, undefined, 2));
|
||||
|
||||
// Delay resizes to avoid conpty not respecting very early resize calls
|
||||
// see https://github.com/microsoft/vscode/blob/a1c783c/src/vs/platform/terminal/node/terminalProcess.ts#L177
|
||||
if (isWindows) {
|
||||
this._delayedResizer = new DelayedResizer();
|
||||
this._delayedResizer.onTrigger(dimensions => {
|
||||
this._delayedResizer?.dispose();
|
||||
this._delayedResizer = undefined;
|
||||
if (dimensions.cols && dimensions.rows) {
|
||||
this.resize(dimensions.cols, dimensions.rows);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const startTerminal = (command: string): { terminal: IPty | undefined, inputStream: Writable } => {
|
||||
try {
|
||||
return this.createPseudoTerminal(command, options, ringBuffer);
|
||||
} catch (error) {
|
||||
// Normalize the error to make it as close as possible as what
|
||||
// node's child_process.spawn would generate in the same
|
||||
// situation.
|
||||
const message: string = error.message;
|
||||
|
||||
if (message.startsWith('File not found: ') || message.endsWith(NodePtyErrors.ENOENT)) {
|
||||
if (isWindows && command && !command.toLowerCase().endsWith('.exe')) {
|
||||
const commandExe = command + '.exe';
|
||||
this.logger.debug(`Trying terminal command '${commandExe}' because '${command}' was not found.`);
|
||||
return startTerminal(commandExe);
|
||||
}
|
||||
|
||||
// Proceed with failure, reporting the original command because it was
|
||||
// the intended command and it was not found
|
||||
error.errno = 'ENOENT';
|
||||
error.code = 'ENOENT';
|
||||
error.path = options.command;
|
||||
} else if (message.endsWith(NodePtyErrors.EACCES)) {
|
||||
// The shell program exists but was not accessible, so just fail
|
||||
error.errno = 'EACCES';
|
||||
error.code = 'EACCES';
|
||||
error.path = options.command;
|
||||
}
|
||||
|
||||
// node-pty throws exceptions on Windows.
|
||||
// Call the client error handler, but first give them a chance to register it.
|
||||
this.emitOnErrorAsync(error);
|
||||
|
||||
return { terminal: undefined, inputStream: new DevNullStream({ autoDestroy: true }) };
|
||||
}
|
||||
};
|
||||
|
||||
const { terminal, inputStream } = startTerminal(options.command);
|
||||
this.terminal = terminal;
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for the constructor to attempt to create the pseudo-terminal encapsulating the shell process.
|
||||
*
|
||||
* @param command the shell command to launch
|
||||
* @param options options for the shell process
|
||||
* @param ringBuffer a ring buffer in which to collect terminal output
|
||||
* @returns the terminal PTY and a stream by which it may be sent input
|
||||
*/
|
||||
private createPseudoTerminal(command: string, options: TerminalProcessOptions, ringBuffer: MultiRingBuffer): { terminal: IPty | undefined, inputStream: Writable } {
|
||||
const terminal = spawn(
|
||||
command,
|
||||
(isWindows && options.commandLine) || options.args || [],
|
||||
options.options || {}
|
||||
);
|
||||
|
||||
process.nextTick(() => this.emitOnStarted());
|
||||
|
||||
// node-pty actually wait for the underlying streams to be closed before emitting exit.
|
||||
// We should emulate the `exit` and `close` sequence.
|
||||
terminal.onExit(({ exitCode, signal }) => {
|
||||
// see https://github.com/microsoft/node-pty/issues/751
|
||||
if (exitCode === undefined) {
|
||||
exitCode = 0;
|
||||
}
|
||||
// Make sure to only pass either code or signal as !undefined, not
|
||||
// both.
|
||||
//
|
||||
// node-pty quirk: On Linux/macOS, if the process exited through the
|
||||
// exit syscall (with an exit code), signal will be 0 (an invalid
|
||||
// signal value). If it was terminated because of a signal, the
|
||||
// signal parameter will hold the signal number and code should
|
||||
// be ignored.
|
||||
this._exitCode = exitCode;
|
||||
if (signal === undefined || signal === 0) {
|
||||
this.onTerminalExit(exitCode, undefined);
|
||||
} else {
|
||||
this.onTerminalExit(undefined, signame(signal));
|
||||
}
|
||||
process.nextTick(() => {
|
||||
if (signal === undefined || signal === 0) {
|
||||
this.emitOnClose(exitCode, undefined);
|
||||
} else {
|
||||
this.emitOnClose(undefined, signame(signal));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
terminal.onData((data: string) => {
|
||||
ringBuffer.enq(data);
|
||||
});
|
||||
|
||||
const inputStream = new Writable({
|
||||
write: (chunk: string) => {
|
||||
this.write(chunk);
|
||||
},
|
||||
});
|
||||
|
||||
return { terminal, inputStream };
|
||||
}
|
||||
|
||||
createOutputStream(): MultiRingBufferReadableStream {
|
||||
return this.ringBuffer.getStream();
|
||||
}
|
||||
|
||||
get pid(): number {
|
||||
this.checkTerminal();
|
||||
return this.terminal!.pid;
|
||||
}
|
||||
|
||||
get executable(): string {
|
||||
return (this.options as ProcessOptions).command;
|
||||
}
|
||||
|
||||
get arguments(): string[] {
|
||||
return this.options.args || [];
|
||||
}
|
||||
|
||||
protected onTerminalExit(code: number | undefined, signal: string | undefined): void {
|
||||
this.emitOnExit(code, signal);
|
||||
this.unregisterProcess();
|
||||
}
|
||||
|
||||
unregisterProcess(): void {
|
||||
this.processManager.unregister(this);
|
||||
}
|
||||
|
||||
kill(signal?: string): void {
|
||||
if (this.terminal && this.killed === false) {
|
||||
this.terminal.kill(signal);
|
||||
}
|
||||
}
|
||||
|
||||
resize(cols: number, rows: number): void {
|
||||
if (typeof cols !== 'number' || typeof rows !== 'number' || isNaN(cols) || isNaN(rows)) {
|
||||
return;
|
||||
}
|
||||
this.checkTerminal();
|
||||
try {
|
||||
// Ensure that cols and rows are always >= 1, this prevents a native exception in winpty.
|
||||
cols = Math.max(cols, 1);
|
||||
rows = Math.max(rows, 1);
|
||||
|
||||
// Delay resize if needed
|
||||
if (this._delayedResizer) {
|
||||
this._delayedResizer.cols = cols;
|
||||
this._delayedResizer.rows = rows;
|
||||
return;
|
||||
}
|
||||
|
||||
this.terminal!.resize(cols, rows);
|
||||
} catch (error) {
|
||||
// swallow error if the pty has already exited
|
||||
// see also https://github.com/microsoft/vscode/blob/a1c783c/src/vs/platform/terminal/node/terminalProcess.ts#L549
|
||||
if (this._exitCode !== undefined &&
|
||||
error.message !== 'ioctl(2) failed, EBADF' &&
|
||||
error.message !== 'Cannot resize a pty that has already exited') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write(data: string): void {
|
||||
this.checkTerminal();
|
||||
this.terminal!.write(data);
|
||||
}
|
||||
|
||||
protected checkTerminal(): void | never {
|
||||
if (!this.terminal) {
|
||||
throw new Error('pty process did not start correctly');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the latest resize event to be trigger at a later point.
|
||||
*/
|
||||
class DelayedResizer extends DisposableCollection {
|
||||
rows: number | undefined;
|
||||
cols: number | undefined;
|
||||
private _timeout: NodeJS.Timeout;
|
||||
|
||||
private readonly _onTrigger = new Emitter<{ rows?: number; cols?: number }>();
|
||||
get onTrigger(): Event<{ rows?: number; cols?: number }> { return this._onTrigger.event; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.push(this._onTrigger);
|
||||
this._timeout = setTimeout(() => this._onTrigger.fire({ rows: this.rows, cols: this.cols }), 1000);
|
||||
this.push(Disposable.create(() => clearTimeout(this._timeout)));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
}
|
||||
22
packages/process/src/node/test/process-fork-test.js
Normal file
22
packages/process/src/node/test/process-fork-test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Arm 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
|
||||
// *****************************************************************************
|
||||
|
||||
if (process.argv[2] === 'version') {
|
||||
console.log('1.0.0');
|
||||
} else {
|
||||
process.stderr.write('Error: Argument expected')
|
||||
process.exit(1)
|
||||
}
|
||||
27
packages/process/src/node/test/process-test-container.ts
Normal file
27
packages/process/src/node/test/process-test-container.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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 { Container } from '@theia/core/shared/inversify';
|
||||
import { bindLogger } from '@theia/core/lib/node/logger-backend-module';
|
||||
import processBackendModule from '../process-backend-module';
|
||||
|
||||
export function createProcessTestContainer(): Container {
|
||||
const testContainer = new Container();
|
||||
|
||||
bindLogger(testContainer.bind.bind(testContainer));
|
||||
testContainer.load(processBackendModule);
|
||||
|
||||
return testContainer;
|
||||
}
|
||||
79
packages/process/src/node/utils.ts
Normal file
79
packages/process/src/node/utils.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 TypeFox 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 { isWindows } from '@theia/core';
|
||||
import * as os from 'os';
|
||||
const stringArgv = require('string-argv');
|
||||
|
||||
/**
|
||||
* Parses the given line into an array of args respecting escapes and string literals.
|
||||
* @param line the given line to parse
|
||||
*/
|
||||
export function parseArgs(line: string | undefined): string[] {
|
||||
if (line) {
|
||||
return stringArgv(line);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Polyfill for Object.entries, until we upgrade to ES2017.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function objectEntries(obj: any): any[] {
|
||||
const props = Object.keys(obj);
|
||||
const result = new Array(props.length);
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
result[i] = [props[i], obj[props[i]]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a signal number to its short name (using the signal definitions of
|
||||
* the current host). Should never be called on Windows. For Linux, this is
|
||||
* only valid for the x86 and ARM architectures, since other architectures may
|
||||
* use different numbers, see signal(7).
|
||||
*/
|
||||
export function signame(sig: number): string {
|
||||
// We should never reach this on Windows, since signals are not a thing
|
||||
// there.
|
||||
if (isWindows) {
|
||||
throw new Error('Trying to get a signal name on Windows.');
|
||||
}
|
||||
|
||||
for (const entry of objectEntries(os.constants.signals)) {
|
||||
if (entry[1] === sig) {
|
||||
return entry[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Don't know this signal? Return the number as a string.
|
||||
return sig.toString(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a code number to its short name
|
||||
*/
|
||||
export function codename(code: number): string {
|
||||
for (const entry of objectEntries(os.constants.errno)) {
|
||||
if (entry[1] === code) {
|
||||
return entry[0];
|
||||
}
|
||||
}
|
||||
// Return the number as string if we did not find a name for it.
|
||||
return code.toString(10);
|
||||
}
|
||||
17
packages/process/tsconfig.json
Normal file
17
packages/process/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib",
|
||||
"strictFunctionTypes": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user