@theia/process
Version:
Theia process support.
349 lines (294 loc) • 10.6 kB
text/typescript
// *****************************************************************************
// 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 }
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(
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 };
}
}