tail-forever
Version:
a node.js library to implement tail -F
348 lines (321 loc) • 10.7 kB
JavaScript
// Generated by CoffeeScript 2.4.0
(function() {
var SeriesQueue, Tail, assert, async, environment, events, fs, iconv, jschardet, split, us,
boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
events = require("events");
fs = require('fs');
async = require('uclogs-async');
jschardet = require('jschardet');
iconv = require('iconv-lite');
assert = require('assert');
us = require('underscore');
environment = process.env['NODE_ENV'] || 'development';
split = function(size, chunk_size) {
var result;
result = [];
while (size > 0) {
if (size >= chunk_size) {
result.push(chunk_size);
size -= chunk_size;
} else {
result.push(size);
size = 0;
}
}
return result;
};
SeriesQueue = class SeriesQueue {
next() {
var element;
if (this.queue.length >= 1 && !this.lock) {
element = this.queue.shift();
this.lock = true; // acqure lock
return this.task(element, () => {
this.lock = false; //# release lock
if (this.queue.length >= 1) {
return setImmediate(() => {
return this.next();
});
}
});
}
}
constructor(task) {
this.task = task;
this.queue = [];
this.lock = false;
}
push(element) {
this.queue.push(element);
return setImmediate(() => {
return this.next();
});
}
clean() {
return this.queue = [];
}
length() {
return this.queue.length;
}
};
Tail = class Tail extends events.EventEmitter {
_readBlock(block, callback) {
boundMethodCheck(this, Tail);
return fs.fstat(block.fd, (err, stat) => {
var end, size, split_size, start;
if (err) {
return callback();
}
start = this.bookmarks[block.fd];
end = stat.size;
if (start > end) {
start = 0;
}
size = end - start;
if (this.maxSize > 0 && size > this.maxSize) {
start = end - this.maxSize;
size = this.maxSize;
}
if (start < 0) {
start = 0;
}
split_size = this.bufferSize > 0 ? this.bufferSize : size;
return async.reduce(split(size, split_size), start, (start, size, callback) => {
var buff;
buff = Buffer.alloc(size);
return fs.read(block.fd, buff, 0, size, start, (err, bytesRead, buff) => {
var chunk, data, detected_enc, encoding, i, len, parts;
if (err) {
this.emit('error', err);
return callback(err);
}
if (this.encoding !== 'auto') {
encoding = this.encoding;
} else {
detected_enc = jschardet.detect(buff);
if (!(detected_enc != null ? detected_enc.encoding : void 0) || detected_enc.confidence < 0.98) {
encoding = "utf-8";
} else if (!iconv.encodingExists(detected_enc.encoding)) {
console.error(`auto detected ${detected_enc.encoding} is not supported, use UTF-8 as alternative`);
encoding = 'utf-8';
} else {
encoding = detected_enc.encoding;
}
}
data = iconv.decode(buff, encoding);
this.buffer += data;
parts = this.buffer.split(this.separator);
this.buffer = parts.pop();
for (i = 0, len = parts.length; i < len; i++) {
chunk = parts[i];
this.emit("line", chunk);
}
if (this.buffer.length > this.maxLineSize) {
this.buffer = '';
}
this.bookmarks[block.fd] = start + bytesRead;
return callback(null, this.bookmarks[block.fd]);
});
}, (err) => {
if (err) {
console.log(err);
if (err) {
return callback(err);
}
} else {
return callback();
}
});
});
}
_close(fd) {
var err;
try {
fs.closeSync(fd);
if (process.env.DEBUG === 'tail-forever') {
return console.log("\t\tfile closed " + fd);
}
} catch (error) {
err = error;
return console.log(err);
} finally {
this.fileOpen = false;
this.current.fd = null;
delete this.bookmarks[fd];
}
}
_checkOpen(start, inode) {
var e, fd, stat;
try {
/*
try to open file
start: the postion to read file start from. default is file's tail position
inode: if this parameters present, the start take effect if only file has same inode
*/
if (this.fileOpen) {
console.log('file already open');
return;
}
stat = fs.statSync(this.filename);
if (!stat.isFile()) {
throw new Error(`${this.filename} is not a regular file`);
}
fd = fs.openSync(this.filename, 'r');
this.fileOpen = true;
if (process.env.DEBUG === 'tail-forever') {
console.log("\t\t## FD open = " + fd);
}
stat = fs.fstatSync(fd);
this.current = {
fd: fd,
inode: stat.ino
};
if ((start != null) && start >= 0 && (!inode || inode === stat.ino)) {
this.bookmarks[fd] = start;
} else {
this.bookmarks[fd] = stat.size;
}
return this.queue.push({
type: 'read',
fd: this.current.fd,
inode: this.current.inode
});
} catch (error) {
e = error;
if (e.code === 'ENOENT') { // file not exists
return this.current = {
fd: null,
inode: 0
};
} else {
throw new Error(`failed to read file ${this.filename}: ${e.message}`);
}
}
}
/*
options:
- separator: default is '\n'
- start: where start from, default is the tail of file
- inode: the tail file's inode, if file's inode not equal this will treat a new file
- interval: the interval millseconds to polling file state. default is 1 seconds
- maxSize: the maximum byte size to read one time. 0 or nagative is unlimit.
- maxLineSize: the maximum byte of one line
- bufferSize: the memory buffer size. default is 1M. Tail read file content into buffer first. nagative value is no buffer
- encoding: the file encoding. defalut value is "utf-8", if "auto" encoding will be auto detected by jschardet
*/
constructor(filename, options = {}) {
var ref, ref1, ref2, ref3, ref4, ref5;
super();
this._readBlock = this._readBlock.bind(this);
this.filename = filename;
this.options = options;
if (this.options.start != null) {
assert.ok(us.isNumber(this.options.start), "start should be number");
}
if (this.options.inode != null) {
assert.ok(us.isNumber(this.options.inode), "inode should be number");
}
if (this.options.interval != null) {
assert.ok(us.isNumber(this.options.interval), "interval should be number");
}
if (this.options.maxSize != null) {
assert.ok(us.isNumber(this.options.maxSize), "maxSize should be number");
}
if (this.options.maxLineSize != null) {
assert.ok(us.isNumber(this.options.maxLineSize), "start maxLineSize should be number");
}
if (this.options.bufferSize != null) {
assert.ok(us.isNumber(this.options.bufferSize), "bufferSize should be number");
}
this.fileOpen = false;
this.separator = (((ref = this.options) != null ? ref.separator : void 0) != null) || '\n';
this.buffer = '';
this.queue = new SeriesQueue(this._readBlock);
this.isWatching = false;
this.bookmarks = {};
this._checkOpen(this.options.start, this.options.inode);
this.interval = (ref1 = this.options.interval) != null ? ref1 : 1000;
this.maxSize = (ref2 = this.options.maxSize) != null ? ref2 : -1;
this.maxLineSize = (ref3 = this.options.maxLineSize) != null ? ref3 : 1024 * 1024; // 1M
this.bufferSize = (ref4 = this.options.bufferSize) != null ? ref4 : 1024 * 1024; // 1M
this.encoding = (ref5 = this.options.encoding) != null ? ref5 : 'utf-8';
if (this.encoding !== 'auto' && !iconv.encodingExists(this.encoding)) {
throw new Error(`${this.encoding} is not supported, check encoding supported list in https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings`);
}
this.watch();
}
watch() {
if (this.isWatching) {
return;
}
this.isWatching = true;
return fs.watchFile(this.filename, {
interval: this.interval
}, (curr, prev) => {
return this._watchFileEvent(curr, prev);
});
}
_watchFileEvent(curr, prev) {
var oldFd;
if ((curr.ino !== this.current.inode) || curr.ino === 0) {
//# file was rotate or relink
//# need to close old file descriptor and open new one
if (this.current && this.current.fd) {
// _checkOpen closes old file descriptor
if (process.env.DEBUG === 'tail-forever') {
console.log("\t\tinode changed: @current.fd=" + this.current.fd + " -> closing FD " + this.current.fd);
}
oldFd = this.current.fd;
this._close(oldFd);
}
}
if (!this.fileOpen) {
return this._checkOpen(0);
} else {
return this.queue.push({
type: 'read',
fd: this.current.fd,
inode: this.current.inode
});
}
}
where() {
if (!this.current.fd) {
return null;
}
return {
inode: this.current.inode,
pos: this.bookmarks[this.current.fd]
};
}
unwatch() {
var fd, memory, pos, ref;
this.queue.clean();
fs.unwatchFile(this.filename);
this.isWatching = false;
if (this.current.fd) {
memory = {
inode: this.current.inode,
pos: this.bookmarks[this.current.fd]
};
} else {
memory = {
inode: 0,
pos: 0
};
}
ref = this.bookmarks;
for (fd in ref) {
pos = ref[fd];
this._close(parseInt(fd));
}
this.bookmarks = {};
this.current = {
fd: null,
inode: 0
};
return memory;
}
};
module.exports = Tail;
}).call(this);