├── .gitignore ├── package.json ├── README.md ├── LICENSE ├── nodejs-tail.js ├── test └── nodejs-tail.spec.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-tail", 3 | "version": "1.1.1", 4 | "description": "Simple NodeJs implementation of tail command", 5 | "main": "nodejs-tail.js", 6 | "scripts": { 7 | "test": "node test/nodejs-tail.spec.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vladimir-kazan/nodejs-tail.git" 12 | }, 13 | "keywords": [ 14 | "nodejs", 15 | "tail", 16 | "follow", 17 | "logs" 18 | ], 19 | "author": "Vladimir Kuznetsov ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/vladimir-kazan/nodejs-tail/issues" 23 | }, 24 | "homepage": "https://github.com/vladimir-kazan/nodejs-tail#readme", 25 | "dependencies": { 26 | "chokidar": "3.4.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-tail 2 | 3 | Simple NodeJs implementation of tail command. 4 | ## Install 5 | 6 | ```js 7 | yarn add nodejs-tail 8 | ``` 9 | 10 | ## Syntax 11 | 12 | `new Tail(filename, options)` 13 | 14 | - _filename_ - file to watch 15 | - _options_ - [chokidar](https://github.com/paulmillr/chokidar) watcher options, with next values **always everwritten**: `{ 16 | alwaysStat: true, 17 | ignoreInitial: false, 18 | persistent: true, 19 | }`. That is required to work similar to `tail` command. 20 | 21 | ## Example 22 | 23 | ```js 24 | const Tail = require('nodejs-tail'); 25 | 26 | const filename = 'some.log'; 27 | const tail = new Tail(filename); 28 | 29 | tail.on('line', (line) => { 30 | process.stdout.write(line); 31 | }) 32 | 33 | tail.on('close', () => { 34 | console.log('watching stopped'); 35 | }) 36 | 37 | tail.watch(); 38 | 39 | setTimeout(() => { 40 | tail.close(); 41 | }, 3000); 42 | ``` 43 | 44 | 45 | MIT License. Copyright (c) 2017-2020 Vladimir Kuznetsov 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vladimir Kuznetsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nodejs-tail.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar'); 2 | const EventEmitter = require('events'); 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | 6 | const watcher = Symbol('watcher'); 7 | const fd = Symbol('fd'); 8 | 9 | function closeFile() { 10 | if (this[td]) { 11 | fs.close(this[fd], (err) => { 12 | if (err) { 13 | return; 14 | } 15 | this[td] = undefined; 16 | }); 17 | } 18 | } 19 | 20 | class Tail extends EventEmitter { 21 | 22 | constructor(filename, options) { 23 | super(); 24 | this.filename = filename; 25 | this.options = Object.assign(options || {}, { 26 | alwaysStat: true, 27 | ignoreInitial: false, 28 | persistent: true, 29 | }); 30 | this[watcher] = undefined; 31 | this[fd] = undefined; 32 | } 33 | 34 | watch() { 35 | let lastSize = 0; 36 | 37 | this[watcher] = chokidar.watch(this.filename, this.options) 38 | .on('add', (path, stats) => { 39 | lastSize = stats.size; 40 | }) 41 | .on('change', (path, stats) => { 42 | const diff = stats.size - lastSize; 43 | if (diff <= 0) { 44 | lastSize = stats.size; 45 | return; 46 | } 47 | const buffer = Buffer.alloc(diff); 48 | this[fd] = fs.openSync(path, 'r'); 49 | fs.read(this[fd], buffer, 0, diff, lastSize, (err) => { 50 | if (err) { 51 | return; 52 | } 53 | fs.closeSync(this[fd]); 54 | buffer.toString().split(os.EOL).forEach((line, idx, ar) => { 55 | if (idx < ar.length && line) { 56 | this.emit('line', line); 57 | } 58 | }); 59 | 60 | }); 61 | lastSize = stats.size; 62 | }) 63 | .on('unlink', () => { 64 | lastSize = 0; 65 | closeFile.bind(this); 66 | }); 67 | } 68 | 69 | close() { 70 | if (this[watcher]) { 71 | this[watcher].unwatch(this.filename); 72 | this[watcher].close(); 73 | this[watcher] = undefined; 74 | } 75 | this.emit('close'); 76 | } 77 | } 78 | 79 | module.exports = Tail; 80 | -------------------------------------------------------------------------------- /test/nodejs-tail.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const cluster = require('cluster'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Tail = require('..'); 6 | 7 | const filename = path.join(__dirname, 'test.log'); 8 | 9 | function before() { 10 | if (fs.existsSync(filename)) { 11 | fs.unlinkSync(filename); 12 | } 13 | fs.writeFileSync(filename, ''); 14 | } 15 | 16 | function after() { 17 | if (fs.existsSync(filename)) { 18 | fs.unlinkSync(filename); 19 | } 20 | } 21 | 22 | function startTail(tail) { 23 | 24 | const assertData = [ 25 | 'Line #1', 26 | 'Line #2', 27 | 'Line #3', 28 | 'Line #4', 29 | 'Line #5' 30 | ]; 31 | return new Promise((resolve, reject) => { 32 | let counter = 0; 33 | let closeCalled = false; 34 | tail.on('line', (line) => { 35 | assert.equal(assertData[counter], line); 36 | process.stdout.write(`\nLine recieved:\n${line}\n`); 37 | counter += 1; 38 | }); 39 | tail.on('close', () => { 40 | assert.equal(5, counter); 41 | closeCalled = true; 42 | resolve({ counter, closeCalled }); 43 | }); 44 | tail.watch(); 45 | }); 46 | } 47 | 48 | before(); 49 | 50 | if (cluster.isMaster) { 51 | const tail = new Tail(filename); 52 | startTail(tail).then((data) => { 53 | // assert 54 | assert.ok(data.closeCalled); 55 | }, () => { console.log('er'); }); 56 | 57 | // start log emulation in separate process 58 | cluster.fork(); 59 | cluster.on('exit', (worker, code, signal) => { 60 | assert.equal(0, code); 61 | tail.close(); 62 | }); 63 | 64 | } else { 65 | // emulate logger 66 | const TIMEOUT = 1000; 67 | const testData = [ 68 | 'Line #1\n', 69 | 'Line #2\n', 70 | 'Line #3\n', 71 | 'Line #4\nLine #5\n' 72 | ]; 73 | 74 | testData.forEach((item, idx, ar) => { 75 | setTimeout(() => { 76 | fs.appendFileSync(filename, item); 77 | process.stdout.write(`\nWrite to log:\n${item}`); 78 | if (idx + 1 === ar.length) { 79 | setTimeout(() => { 80 | cluster.worker.kill() 81 | }, TIMEOUT); 82 | } 83 | }, TIMEOUT * (idx + 1)); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | anymatch@~3.1.1: 6 | version "3.1.1" 7 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" 8 | integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== 9 | dependencies: 10 | normalize-path "^3.0.0" 11 | picomatch "^2.0.4" 12 | 13 | binary-extensions@^2.0.0: 14 | version "2.1.0" 15 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" 16 | integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== 17 | 18 | braces@~3.0.2: 19 | version "3.0.2" 20 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 21 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 22 | dependencies: 23 | fill-range "^7.0.1" 24 | 25 | chokidar@3.4.3: 26 | version "3.4.3" 27 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" 28 | integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== 29 | dependencies: 30 | anymatch "~3.1.1" 31 | braces "~3.0.2" 32 | glob-parent "~5.1.0" 33 | is-binary-path "~2.1.0" 34 | is-glob "~4.0.1" 35 | normalize-path "~3.0.0" 36 | readdirp "~3.5.0" 37 | optionalDependencies: 38 | fsevents "~2.1.2" 39 | 40 | fill-range@^7.0.1: 41 | version "7.0.1" 42 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 43 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 44 | dependencies: 45 | to-regex-range "^5.0.1" 46 | 47 | fsevents@~2.1.2: 48 | version "2.1.3" 49 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" 50 | integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== 51 | 52 | glob-parent@~5.1.0: 53 | version "5.1.1" 54 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" 55 | integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== 56 | dependencies: 57 | is-glob "^4.0.1" 58 | 59 | is-binary-path@~2.1.0: 60 | version "2.1.0" 61 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 62 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 63 | dependencies: 64 | binary-extensions "^2.0.0" 65 | 66 | is-extglob@^2.1.1: 67 | version "2.1.1" 68 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 69 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 70 | 71 | is-glob@^4.0.1, is-glob@~4.0.1: 72 | version "4.0.1" 73 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 74 | integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 75 | dependencies: 76 | is-extglob "^2.1.1" 77 | 78 | is-number@^7.0.0: 79 | version "7.0.0" 80 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 81 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 82 | 83 | normalize-path@^3.0.0, normalize-path@~3.0.0: 84 | version "3.0.0" 85 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 86 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 87 | 88 | picomatch@^2.0.4, picomatch@^2.2.1: 89 | version "2.2.2" 90 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" 91 | integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== 92 | 93 | readdirp@~3.5.0: 94 | version "3.5.0" 95 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" 96 | integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== 97 | dependencies: 98 | picomatch "^2.2.1" 99 | 100 | to-regex-range@^5.0.1: 101 | version "5.0.1" 102 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 103 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 104 | dependencies: 105 | is-number "^7.0.0" 106 | --------------------------------------------------------------------------------