UNPKG

@theia/process

Version:
349 lines (294 loc) • 10.6 kB
// ***************************************************************************** // 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 }; } }