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

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

View File

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

View 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';

View 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);
});
});

View 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 };
}
}

View 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 });
});

View 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}]`;
}
}

View 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());
});
}
}
}

View 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 { }
}

View 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');
});
});

View 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);
}
}
}

View 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;
}

View 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();
}
}
}

View 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);
});
});

View 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);
}
}

View 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)
}

View 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;
}

View 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);
}