Files
flipper/desktop/flipper-server-core/src/utils/tail.tsx
Andrey Goncharov 976399b950 Move Tail to flipper-server-core
Summary: Tail is a server-side logging utility. We cannot add it to flipper-frontend-core as it is going to be included into the browser bundle then and break the build.

Reviewed By: antonk52

Differential Revision: D36771864

fbshipit-source-id: 169cdb88543f1e47f39e8c0976c3a3f45c0439d5
2022-05-31 02:58:21 -07:00

145 lines
3.6 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import EventEmitter from 'events';
import fs, {FSWatcher} from 'fs-extra';
import path from 'path';
export type TailOptions = {
separator: RegExp;
encoding: BufferEncoding;
fromBeginning: boolean;
};
type TailBlock = {
start: number;
end: number;
};
export class Tail extends EventEmitter {
absPath: string;
dispatcher = new EventEmitter();
buffer = '';
queue: TailBlock[] = [];
isWatching = false;
currentCursorPosition = 0;
watcher: FSWatcher | undefined = undefined;
constructor(
private readonly filename: string,
private readonly options: TailOptions = {
separator: /[\r]{0,1}\n/,
fromBeginning: true,
encoding: 'utf8',
},
) {
super();
this.absPath = path.dirname(this.filename);
this.dispatcher.on('next', () => {
this.read_();
});
}
async watch() {
if (this.isWatching) {
return;
}
this.isWatching = true;
let startingCursor = 0;
if (!this.options.fromBeginning) {
startingCursor = await this.latestPosition_();
}
this.watch_(startingCursor, this.options.fromBeginning);
}
unwatch() {
if (this.watcher) {
this.watcher.close();
}
this.isWatching = false;
}
private async latestPosition_(): Promise<number> {
return (await fs.stat(this.filename)).size;
}
private read_() {
if (this.queue.length >= 1) {
const block = this.queue[0];
if (block.end > block.start) {
const stream = fs.createReadStream(this.filename, {
start: block.start,
end: block.end - 1,
encoding: this.options.encoding,
});
stream.on('error', (error) => {
this.emit('error', error);
});
stream.on('end', () => {
this.queue.shift();
if (this.queue.length > 0) {
this.dispatcher.emit('next');
}
if (this.buffer.length > 0) {
this.emit('line', this.buffer);
this.buffer = '';
}
});
stream.on('data', (d) => {
this.buffer += d;
const parts = this.buffer.split(this.options.separator);
// The last part may be an incomplete chunk, so
// push that back into the buffer. Otherwise, reset buffer.
this.buffer = parts.pop() || '';
for (const chunk of parts) {
this.emit('line', chunk);
}
});
}
}
}
private async fileUpdated_() {
const latestPosition = await this.latestPosition_();
// Case where text is not appended but it's actually a w+
if (latestPosition < this.currentCursorPosition) {
this.currentCursorPosition = latestPosition;
} else if (latestPosition > this.currentCursorPosition) {
this.queue.push({start: this.currentCursorPosition, end: latestPosition});
this.currentCursorPosition = latestPosition;
// Only emit if the queue was empty and now is not.
if (this.queue.length == 1) {
this.dispatcher.emit('next');
}
}
}
private watch_(cursor: number, readPrevious: boolean) {
this.currentCursorPosition = cursor;
if (readPrevious) {
this.fileUpdated_();
}
this.watcher = fs.watch(this.filename, {}, (eventType, filename) => {
this.watchEvent_(eventType, filename);
});
}
private watchEvent_(eventType: string, _filename: string) {
try {
if (eventType === 'change') {
this.fileUpdated_();
}
} catch (err) {}
}
}