import fs from 'fs'
import os from 'os'
import events from 'events'
const INITIAL_READ_LINES = 10
const READ_CHUNK_SIZE = 16
const POLLING_INTERVAL = 10000
export default class TailFile extends events.EventEmitter {
private filename: fs.PathLike
private descriptor: number | null
private eof: number | null
private inode: number | null
private timer?: NodeJS.Timeout
constructor(filename: fs.PathLike) {
super()
this.filename = filename
this.descriptor = null
this.eof = null
this.inode = null
}
start() {
this._openFile()
this._readInitialLines()
this._scheduleTimer()
}
private _openFile() {
this.descriptor = fs.openSync(this.filename, 'r')
const stats = fs.fstatSync(this.descriptor)
this.eof = stats.size
this.inode = stats.ino
}
private _closeFile() {
if (this.descriptor !== null) {
fs.closeSync(this.descriptor)
}
this.eof = this.inode = this.descriptor = null
}
private _readInitialLines() {
if (this.descriptor === null || this.eof === null) {
return
}
let bufferSize = Math.min(READ_CHUNK_SIZE, this.eof)
let buffer = Buffer.alloc(bufferSize)
let stringTail = ''
let position = this.eof
while (position > 0) {
position -= bufferSize
if (position < 0) {
bufferSize += position
buffer = buffer.subarray(0, position)
position = 0
}
fs.readSync(this.descriptor, buffer, 0, bufferSize, position)
stringTail = buffer.toString() + stringTail
const newLineChars = stringTail.match(os.EOL)?.length ?? 0
if (newLineChars >= INITIAL_READ_LINES || position === 0) {
stringTail = stringTail.split(os.EOL).slice(-INITIAL_READ_LINES).join(os.EOL)
this.emit('data', stringTail)
break
}
}
}
private _pollFileForChanges() {
if (this.descriptor === null || this.eof === null || this.inode === null) {
return
}
const stats = fs.statSync(this.filename)
if (stats.ino !== this.inode) {
this._readRemainderFromDescriptor()
this._openFile()
this._readFromDescriptor(0, this.eof)
} else if (stats.size < this.eof) {
this._readFromDescriptor(0, this.eof)
} else if (stats.size > this.eof) {
this._readFromDescriptor(this.eof, stats.size)
}
this.eof = stats.size
this._scheduleTimer()
}
private _readFromDescriptor(start: number, end: number) {
if (this.descriptor === null) {
return
}
const bufferSize = end - start
const buffer = Buffer.alloc(bufferSize)
fs.readSync(this.descriptor, buffer, 0, bufferSize, start)
this.emit('data', buffer.toString())
}
private _readRemainderFromDescriptor() {
if (this.descriptor === null || this.eof === null) {
return
}
const stats = fs.fstatSync(this.descriptor)
const bufferSize = stats.size - this.eof
const buffer = Buffer.alloc(bufferSize)
fs.readSync(this.descriptor, buffer, 0, bufferSize, this.eof)
this.emit('data', buffer.toString())
this._closeFile()
}
private _scheduleTimer() {
clearTimeout(this.timer)
this.timer = setTimeout(this._pollFileForChanges.bind(this), POLLING_INTERVAL)
}
}