UNPKG

tail-forever

Version:

a node.js library to implement tail -F

348 lines (321 loc) 10.7 kB
// 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);