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