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
145 lines
3.6 KiB
TypeScript
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) {}
|
|
}
|
|
}
|