tail-forever
Version:
a node.js library to implement tail -F
232 lines (199 loc) • 7.34 kB
text/coffeescript
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 = (size, chunk_size) ->
result = []
while size > 0
if size >= chunk_size
result.push chunk_size
size -= chunk_size
else
result.push size
size = 0
return result
class SeriesQueue
next : () ->
if .length >= 1 && not
element = .shift()
= true # acqure lock
(element, () =>
= false ## release lock
if .length >= 1
setImmediate(() => () )
)
constructor:() ->
= []
= false
push: (element) ->
.push element
setImmediate(() =>
()
)
clean: () ->
= []
length : () ->
.length
class Tail extends events.EventEmitter
_readBlock:(block, callback) =>
fs.fstat block.fd, (err, stat) =>
if err
return callback()
start = [block.fd]
end = stat.size
if start > end
start = 0
size = end - start
if > 0 and size >
start = end -
size =
if start < 0
start = 0
split_size = if > 0 then else size
async.reduce split(size, split_size), start, (start, size, callback) =>
buff = Buffer.alloc(size)
fs.read block.fd, buff, 0, size, start, (err, bytesRead, buff) =>
if err
('error', err)
return callback(err)
if != 'auto'
encoding =
else
detected_enc = jschardet.detect buff
if not detected_enc?.encoding or detected_enc.confidence < 0.98
encoding = "utf-8"
else if not 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
+= data
parts = .split()
= parts.pop()
("line", chunk) for chunk in parts
if .length >
= ''
[block.fd] = start + bytesRead
callback(null, [block.fd])
, (err) =>
if err
console.log err
return callback(err) if err
else
return callback()
_close: (fd) ->
try
fs.closeSync fd
if process.env.DEBUG == 'tail-forever'
console.log "\t\tfile closed " + fd
catch err
console.log err
finally
= false
.fd = null
delete [fd]
_checkOpen : (start, inode) ->
###
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
###
try
if
console.log 'file already open'
return
stat = fs.statSync
if not stat.isFile()
throw new Error("#{@filename} is not a regular file")
fd = fs.openSync(, 'r')
= true
if process.env.DEBUG == 'tail-forever'
console.log "\t\t## FD open = " + fd
stat = fs.fstatSync(fd)
= {fd: fd, inode: stat.ino}
if start? and start >=0 and ( !inode or inode == stat.ino )
[fd] = start
else
[fd] = stat.size
.push({type:'read', fd: .fd, inode: .inode})
catch e
if e.code == 'ENOENT' # file not exists
= {fd: null, inode: 0}
else
throw new Error("failed to read file #{@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:(, = {}) ->
super()
assert.ok us.isNumber(.start), "start should be number" if .start?
assert.ok us.isNumber(.inode), "inode should be number" if .inode?
assert.ok us.isNumber(.interval), "interval should be number" if .interval?
assert.ok us.isNumber(.maxSize), "maxSize should be number" if .maxSize?
assert.ok us.isNumber(.maxLineSize), "start maxLineSize should be number" if .maxLineSize?
assert.ok us.isNumber(.bufferSize), "bufferSize should be number" if .bufferSize?
= false
= ?.separator? || '\n'
= ''
= new SeriesQueue()
= false
= {}
(.start, .inode)
= .interval ? 1000
= .maxSize ? -1
= .maxLineSize ? 1024 * 1024 # 1M
= .bufferSize ? 1024 * 1024 # 1M
= .encoding ? 'utf-8'
if != 'auto' and not iconv.encodingExists
throw new Error("#{@encoding} is not supported, check encoding supported list in https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings")
()
watch: ->
return if
= true
fs.watchFile , {interval: }, (curr, prev) => curr, prev
_watchFileEvent: (curr, prev) ->
if (curr.ino != .inode) || curr.ino == 0
## file was rotate or relink
## need to close old file descriptor and open new one
if && .fd
# _checkOpen closes old file descriptor
if process.env.DEBUG == 'tail-forever'
console.log "\t\tinode changed: @current.fd=" + .fd + " -> closing FD " + .fd
oldFd = .fd
(oldFd)
# (0)
if !
(0)
else
.push({type:'read', fd: .fd, inode: .inode})
where : () ->
if not .fd
return null
return {inode: .inode, pos: [.fd]}
unwatch: ->
.clean()
fs.unwatchFile()
= false
if .fd
memory = {inode: .inode, pos: [.fd]}
else
memory = {inode: 0, pos: 0}
for fd, pos of
(parseInt(fd))
= {}
= {fd:null, inode:0}
return memory
module.exports = Tail