Compare commits

...

131 Commits

Author SHA1 Message Date
dependabot[bot] 52014199f1
Bump glob-parent from 5.1.0 to 5.1.2 (#237)
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.0 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.0...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-03 23:17:46 +02:00
dependabot[bot] 18de86b061
Bump lodash from 4.17.19 to 4.17.21 (#235)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 22:56:20 +02:00
Maciej Winnicki 44ff51a618
feat: produce Windows exe 2021-03-17 16:17:42 +01:00
Ethan Dye 514c16e488
chore: update socket.io (#232)
Signed-off-by: Ethan Dye <mrtops03@gmail.com>
2021-03-17 15:49:33 +01:00
EvertEt 6610661b26
chore: fix typo (#229)
Related to #163
2021-03-09 10:49:09 +01:00
Maciej Winnicki 9d14ac51a8
Release 4.9.2 2021-02-17 23:03:05 +01:00
dependabot[bot] ab66d83efc
Bump dot-prop from 4.2.0 to 4.2.1 (#224)
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-17 22:59:38 +01:00
Aaron Dewes a5582dc34f
feat: use Debian buster (#221) 2021-02-17 22:59:19 +01:00
Maciej Winnicki cd871ad361
chore: use explicit modules versions (#225) 2021-02-17 22:57:35 +01:00
dependabot[bot] 38022f7a0a
Bump socket.io from 2.2.0 to 2.4.0 (#223)
Bumps [socket.io](https://github.com/socketio/socket.io) from 2.2.0 to 2.4.0.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/2.4.0/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/2.2.0...2.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-04 08:54:43 +01:00
dependabot[bot] ea330b8ef9
Bump lodash from 4.17.15 to 4.17.19 (#213)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-20 13:37:15 +02:00
Maciej Winnicki fb1da7bd18
chore: update pkg 2020-04-14 09:37:00 +02:00
Maciej Winnicki 5dad99106e
fix: disable usage stats param not passed when in daemon mode. Closes #188 2020-04-13 21:24:54 +02:00
Maciej Winnicki 94e2c46ee6
chore: prettier standard 2020-04-13 21:21:19 +02:00
Maciej Winnicki 0ceb864693
fix: ellipsis in title 2020-04-13 21:03:18 +02:00
Maciej Winnicki 39f8410a2a
chore: fix lint 2020-03-31 20:25:47 +02:00
DomExpire 6c4561a8d7
fix of the approximate value of window.pageYOffset on chrome android (#202) 2020-03-31 20:19:08 +02:00
Maciej Winnicki d43976f3a5
Release 4.9.1 2020-03-26 15:26:52 +01:00
Maciej Winnicki 7f506ae5bf
fix: use different base image to support rpi 2020-03-26 15:24:39 +01:00
dependabot[bot] 0fcd6278fd
Bump acorn from 5.7.3 to 5.7.4 (#199)
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-03-14 21:40:11 +01:00
donnie 0aad9dd2dc
Fix websocket error during handshake (#195)
See [this SO issue](https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respo)
2020-02-19 19:55:30 +01:00
Maciej Winnicki c12a1878de
Update Release guide 2020-02-03 13:26:32 +01:00
Maciej Winnicki 25f3b23c86
Release 4.9.0 2020-02-03 13:19:55 +01:00
Maciej Winnicki 2b3a64dca2
Fix publish step 2020-02-03 13:19:23 +01:00
Maciej Winnicki 9e6e7ee5dc
Fix publish command 2020-02-03 13:02:09 +01:00
Maciej Winnicki 1a9ce68c4a
Fix publish step 2020-02-03 12:56:45 +01:00
Maciej Winnicki 06fd619a40
Fix GH action 2020-02-03 12:51:20 +01:00
Maciej Winnicki b80fc8fd0c
Fix GH action 2020-02-03 12:39:12 +01:00
Maciej Winnicki 34d5893694
Fix GH action 2020-02-03 12:37:48 +01:00
Maciej Winnicki 18c4603016
Fix GH actions config 2020-02-03 12:35:50 +01:00
Maciej Winnicki a63e6dbd87
Lint fixes 2020-02-03 12:23:47 +01:00
Alexander Wunschik 0eac303527
Add windows support (#194)
* fix: replace native tail with fs-tail-stream
fixes #111

* fix: use options.buffer as start parameter

* feat: add additional windows support

* feat: add additional windows support
2020-02-03 12:02:21 +01:00
Maciej Winnicki 1357793a28
Make CI step conditional 2019-09-13 21:34:33 +02:00
Maciej Winnicki 7ff68040c3
Typo 2019-09-13 21:21:44 +02:00
Maciej Winnicki b31de76d33
converted main.workflow to Actions V2 yml files (#183) 2019-09-13 21:19:15 +02:00
Alexander Wunschik 09acdc62af update commander to 3.0.1 (#182)
see also #176
2019-08-30 13:20:11 +02:00
Maciej Winnicki 3b390a0d54
Release 4.8.0 2019-08-29 16:42:53 +02:00
Pavlo Bashynskyi 01afdc8cdd Add pause button (#163) Closes #113. Closes #137. 2019-08-27 11:24:25 +02:00
Maciej Winnicki 4d5f29b209
Track --number param 2019-08-26 22:03:27 +02:00
Maciej Winnicki 29d660623f
Release 4.7.0 2019-08-26 20:17:38 +02:00
Maciej Winnicki 2150761fa2
Fix auto-scrolling (#180) 2019-08-26 20:14:30 +02:00
dependabot[bot] 1cacc260f4 Bump eslint-utils from 1.3.1 to 1.4.2 (#181)
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-26 20:10:31 +02:00
Maciej Winnicki f8de308820
Update pkg 2019-08-08 21:29:44 +02:00
Maciej Winnicki 7c8c9cab98
Fix CI issues (#179) 2019-08-08 21:22:14 +02:00
dependabot[bot] 62c0d67370 Bump extend from 3.0.1 to 3.0.2 (#178)
Bumps [extend](https://github.com/justmoon/node-extend) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/justmoon/node-extend/releases)
- [Changelog](https://github.com/justmoon/node-extend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/justmoon/node-extend/compare/v3.0.1...v3.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-06 19:41:48 +02:00
Alexander Wunschik 364f68b9c1 chore(docs): fix help option (#176)
fixes #53
2019-08-06 19:39:40 +02:00
Alexander Wunschik bc8c1f8d00 chore(docs): fix web interface url (#177) 2019-08-06 16:25:33 +02:00
Ryan Hunt e6cc5f4dd1 keep empty lines (#174) 2019-07-22 21:03:10 +02:00
dependabot[bot] 95d8cd5dcc Bump lodash from 4.17.4 to 4.17.15 (#173)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.4 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.4...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-21 22:14:59 +02:00
Maciej Winnicki 54101c8fad
Release 4.6.0 2019-07-03 22:58:25 +02:00
Maciej Winnicki 5bf80592cc
Support running frontial Docker on ARM by switching base image to the official one. Closes #168 2019-07-03 22:56:00 +02:00
dependabot[bot] f5c86cd3d7 Bump stringstream from 0.0.5 to 0.0.6 (#167)
Bumps [stringstream](https://github.com/mhart/StringStream) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/mhart/StringStream/releases)
- [Commits](https://github.com/mhart/StringStream/compare/v0.0.5...v0.0.6)

Signed-off-by: dependabot[bot] <support@github.com>
2019-06-30 19:02:31 +02:00
dependabot[bot] 4f4693b623 Bump js-yaml from 3.12.0 to 3.13.1 (#166)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)
2019-06-05 10:03:23 +02:00
Maciej Winnicki e9e16fa578
Release 4.5.4 2019-02-21 23:17:30 +01:00
Maciej Winnicki a869307ecc
Fix proper signal handling. Closes #158 #160 2019-02-21 23:15:17 +01:00
Maciej Winnicki 205cdbf696
Cleanup unused files (#157) 2019-01-18 22:43:51 +01:00
Maciej Winnicki 658a0914bf
Revert "Temporary remove GH workflow because of https://github.com/actions/bin/issues/13"
This reverts commit 26c3957457.
2019-01-18 22:24:18 +01:00
Maciej Winnicki b2a4f4b4af
Release 4.5.3 2019-01-09 22:04:34 +01:00
Maciej Winnicki da920b4c04
Update npm token 2019-01-09 22:03:53 +01:00
Maciej Winnicki 955bf9d436
Release 4.5.2 2019-01-09 22:00:28 +01:00
Maciej Winnicki 57367a2b0d
Fix highligthing multiple word. Closes #151 (#155) 2019-01-09 21:58:01 +01:00
Maciej Winnicki 26c3957457
Temporary remove GH workflow because of https://github.com/actions/bin/issues/13 2019-01-09 21:53:10 +01:00
Maciej Winnicki 953c6b7dbe
Add GH actions workflow 2019-01-09 21:45:21 +01:00
Maciej Winnicki 8ea0f65760
test2 2019-01-07 21:53:05 +01:00
Maciej Winnicki 6c77313a6a
test 2019-01-07 21:50:38 +01:00
Maciej Winnicki 2cfff5caf7
Update deps (#154) 2018-12-18 12:14:26 +01:00
Maciej Winnicki ef8c14bb9c
Release 4.5.1 2018-11-28 10:17:37 +01:00
Maciej Winnicki c35287ce7a
Fix runtime stats sending 2018-11-28 10:13:32 +01:00
Maciej Winnicki 28de6d18dc
Update mocha dependency 2018-11-28 09:50:19 +01:00
Maciej Winnicki 274e48731f
Release 4.5.0 2018-11-22 12:11:15 +01:00
Maciej Winnicki 4a87fa6bdf
Add usage stats gathering 2018-11-22 12:09:51 +01:00
Maciej Winnicki 90d1428449
Release 4.4.0 2018-10-30 08:52:40 +01:00
watanabe takanobu 75d31aafd6 Support OpenBSD (#146) 2018-10-30 08:39:21 +01:00
Maciej Winnicki a99286fa0d
Update Docker image link 2018-10-24 14:07:09 +02:00
Maciej Winnicki 4687fa0698
Release 4.3.3 2018-10-24 13:51:57 +02:00
Maciej Winnicki ab97b1bc36
Add Dockerfile. Closes #125 (#145) 2018-10-24 13:42:49 +02:00
Maciej Winnicki 71f2114127
Add title attribute with full path. Closes #85 2018-10-23 12:12:45 +02:00
Maciej Winnicki 6faeac0e06
Fix text overflow in navbar. Closes #128 (#144) 2018-10-23 12:07:41 +02:00
Maciej Winnicki 8547d3d241
Update presets lit 2018-10-12 10:31:08 +02:00
Maciej Winnicki 1edcfac40e
Fix filename size (#142) 2018-10-12 10:23:12 +02:00
novahawk13 3d8b93b2d2 Fixes issue where navbar was overlaying text (#139)
* Edited webpage and css to prevent navbar from ever overlaying over text
2018-10-12 10:15:33 +02:00
Maciej Winnicki 751a2b9db7
Release 4.3.2 2018-10-11 13:40:50 +02:00
Maciej Winnicki b1eada0b0e
Update eslint (#141) 2018-10-11 12:53:53 +02:00
Krishna 6e758d8e34 Basic Python logging preset (#140) 2018-10-11 11:57:40 +02:00
Pavlo Bashynskyi 7e7f8e59e8 Set filter in URL (#138)
* Set filter in URL

* fix: jsdom > v11 broke with node4

* Updating URL from filter input
2018-10-01 22:04:14 +02:00
Maciej Winnicki fd735d87c0
Update connect dependencies. Closes #133 2018-08-14 11:10:10 +02:00
Maciej Winnicki 446d144b21
Update deps 2018-08-14 10:15:06 +02:00
Maciej Winnicki 54d293e9f5
Add infor about not running on Windows 2018-08-14 10:09:10 +02:00
Krzysztof Starzyk 1fb8d5be97 add npmlog style preset (#135) 2018-08-14 09:43:45 +02:00
Maciej Winnicki 710551da7a
Release 4.2.2 2018-07-25 10:03:29 +02:00
Maciej Winnicki a6c98e46d8
Update deps 2018-07-25 10:00:00 +02:00
Maciej Winnicki 500c505137
Update TravisCI node versions 2018-07-25 09:48:24 +02:00
Maciej Winnicki c8b02c6246
Add relesing guide 2018-07-25 09:46:08 +02:00
Maciej Winnicki c2f98d5554
Release v4.2.1 2018-07-25 09:37:23 +02:00
Maciej Winnicki b19ea8ae1d
Publish to NPM via Travis 2018-07-25 09:33:33 +02:00
Andrew Wright 1e54a7786c Add viewport width to support responsive design (#121)
The UI is built to respond to the width of the screen it is being
displayed on and provide the best experience across device. When I tried
to view logs streaming on my phone (Apple iPhone 6s), site was
attempting to render the "bigger-screen" version.

This is because, by default, mobile browsers advertise a bigger "view
port" than screen size, to allow compability with non-responsive sites.
In order to benefit from the responsiveness provided the viewport meta
tag needs to be overwritten. More on that tag can be found at
(mdn)[https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag].
2018-04-11 23:24:08 +02:00
Maciej Winnicki 0623cd91aa
4.2.0 2018-04-07 19:39:59 +02:00
Maciej Winnicki 30e8eb04c3
add pkg to package.json 2018-04-07 19:38:39 +02:00
Maciej Winnicki 35f7e0e80a
add --url-path option. Closes #108 (#120)
* Added configuration to fix #108

* Conflicting option

* Updated README

* Fix for web sockets behind nginx

* make url path working without nginx

* update readme
2018-04-07 14:16:20 +02:00
Maciej Winnicki 9be6486c92
release 4.1.1 2018-02-07 22:57:40 +01:00
Maciej Winnicki 6ad045f9e0
replace git repo dependency with forked package. Closes #110 2018-02-07 22:57:04 +01:00
Maciej Winnicki 0f0e8d5044
update readme with info about binary files 2018-01-03 14:10:18 +01:00
Maciej Winnicki baa9e3de7b
release 4.1.0 2018-01-03 14:00:16 +01:00
Maciej Winnicki 19b15685b9
update socket.io dep. Closes #103 2018-01-03 13:56:52 +01:00
Maciej Winnicki 6cf8a3391f
prepare to be used as a standalone binary package with pkg 2018-01-03 13:49:37 +01:00
Maciej Winnicki 87bfcff493
remove codesponso 2017-11-27 19:38:52 +01:00
Todsaporn Banjerdkit 6d3f7c2439 Update README.md (#102) 2017-10-25 22:16:21 +02:00
Maciej Winnicki 98a3003902 add codesponsor banner 2017-09-29 14:46:24 +02:00
Maciej Winnicki 439194393f
add docker download badge 2017-09-26 16:22:49 +02:00
Maciej Winnicki 200f8f8fbf
release 4.0.4 2017-08-22 16:36:26 +02:00
Maciej Winnicki 756cbee2a5
use byline also for stdin 2017-08-22 16:28:21 +02:00
Maciej Winnicki bc8eb6cada add gif to the README file 2017-08-22 16:26:40 +02:00
Maciej Winnicki de39dc6ca1
release 4.0.3 2017-08-21 12:24:23 +02:00
Maciej Winnicki 71dc8ceb83
use byline for supporting large files 2017-08-21 12:22:15 +02:00
Maciej Winnicki be1eefcd1e
fix deamon error. Closes #100 2017-08-21 11:48:48 +02:00
Maciej Winnicki 2b6923a9b1 Merge pull request #96 from yagop/patch-1
Check if cookies[sessionKey] exists
2017-07-03 11:41:22 +02:00
Yago fcd8ee906e Check if cookies[sessionKey] exists 2017-06-29 16:16:33 +02:00
Maciej Winnicki 16b064c545 update screenshot 2017-04-10 11:35:20 +02:00
Maciej Winnicki 9994060700
fix updating favico every line 2017-02-27 15:30:13 +01:00
Maciej Winnicki ce7aadcd4e
fix typo 2017-01-31 10:49:23 +01:00
Maciej Winnicki d9748a8133 Merge pull request #87 from mindflowers/form-enter
Disable enter in filter form
2017-01-31 10:49:15 +01:00
mindflowers 8d78f2e7f5 Disable enter in filter form 2017-01-31 11:50:23 +05:30
Maciej Winnicki ece7dc9c2a update README 2016-10-06 10:35:25 +02:00
Maciej Winnicki 73a667c0d6
fix showing empty lines. Closes #78. Closes #79 2016-10-05 16:16:43 +02:00
Maciej Winnicki 31e25acce6
update coding standard 2016-10-05 15:57:53 +02:00
Maciej Winnicki 432716822d
update readme 2016-10-05 09:54:58 +02:00
Maciej Winnicki 9dbe9b2f51
support stdin 2016-10-05 09:51:57 +02:00
Maciej Winnicki fb6560e0e1
remove SSH feature 2016-10-04 19:49:18 +02:00
Maciej Winnicki 7007f92715
release 3.1.2 2016-09-04 18:55:10 +02:00
Maciej Winnicki fc6128160c
remove server side sanitization (it's done by ansi_up in frontend). Closes #75 2016-09-04 18:30:57 +02:00
Maciej Winnicki b74dfc805e
update readme 2016-09-04 18:01:43 +02:00
43 changed files with 5857 additions and 1526 deletions

View File

@ -1,2 +0,0 @@
lib/web/assets/tinycon.min.js
lib/web/assets/ansi_up.js

View File

@ -1,238 +0,0 @@
module.exports = {
"env": {
"browser": true,
"node": true,
"mocha": true
},
"extends": "eslint:recommended",
"rules": {
"accessor-pairs": "error",
"array-bracket-spacing": "error",
"array-callback-return": "error",
"arrow-body-style": "error",
"arrow-parens": "error",
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": [
"error",
"always"
],
"brace-style": [
"error",
"1tbs",
{
"allowSingleLine": true
}
],
"callback-return": "error",
"camelcase": "error",
"comma-spacing": [
"error",
{
"after": true,
"before": false
}
],
"comma-style": [
"error",
"last"
],
"complexity": "error",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "error",
"consistent-this": "off",
"curly": "error",
"default-case": "error",
"dot-location": [
"error",
"property"
],
"dot-notation": [
"error",
{
"allowKeywords": true
}
],
"eol-last": "error",
"eqeqeq": "error",
"func-names": "off",
"func-style": [
"error",
"expression"
],
"generator-star-spacing": "error",
"global-require": "off",
"guard-for-in": "error",
"handle-callback-err": "off",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"indent": "error",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "error",
"keyword-spacing": [
"error",
{
"after": true,
"before": true
}
],
"linebreak-style": [
"error",
"unix"
],
"lines-around-comment": "off",
"max-depth": "error",
"max-len": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "error",
"new-cap": "error",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-continue": "error",
"no-console": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "off",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "error",
"no-floating-decimal": "error",
"no-implicit-globals": "off",
"no-implied-eval": "error",
"no-inline-comments": "error",
"no-inner-declarations": [
"error",
"functions"
],
"no-invalid-this": "off",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-mixed-requires": "error",
"no-multi-spaces": "off",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "off",
"no-plusplus": "error",
"no-process-env": "error",
"no-process-exit": "off",
"no-proto": "error",
"no-redeclare": [
"error",
{
"builtinGlobals": false
}
],
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-sync": "off",
"no-ternary": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-undefined": "error",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unsafe-finally": "error",
"no-use-before-define": "off",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-escape": "error",
"no-var": "off",
"no-void": "error",
"no-warning-comments": "error",
"no-whitespace-before-property": "error",
"no-with": "error",
"object-curly-spacing": [
"error",
"never"
],
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": "error",
"operator-linebreak": "error",
"padded-blocks": "off",
"prefer-arrow-callback": "off",
"prefer-const": "error",
"prefer-reflect": "off",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "off",
"quote-props": "off",
"quotes": [
"error",
"single"
],
"radix": "error",
"require-jsdoc": "off",
"require-yield": "error",
"semi": "error",
"semi-spacing": "error",
"sort-imports": "error",
"sort-vars": "error",
"space-before-blocks": "error",
"space-before-function-paren": "error",
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"template-curly-spacing": "error",
"vars-on-top": "off",
"wrap-iife": ["error", "inside"],
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
}
};

20
.github/workflows/push.yml vendored Normal file
View File

@ -0,0 +1,20 @@
on: push
name: Build, Lint, Test, and Publish
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm install
- run: npm test
- run: npm run lint
- uses: primer/publish@master
if: github.ref == 'refs/heads/master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
NPM_REGISTRY_URL: registry.npmjs.org

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
node_modules
test/fixtures/*.pem
npm-debug.log
dist
.DS_Store

View File

@ -1,8 +1,16 @@
language: node_js
node_js:
- 4
- 5
- 6
- 6
- 8
- 10
script:
- npm run lint
- npm test
- npm run lint
- npm test
deploy:
provider: npm
email: maciej.winnicki@gmail.com
api_key:
secure: OuE8bOpsq/XN2AL6z0SWqHtpfKB5Qg3wug+hZVka+AdWaU6Gm0uvs4yA7B/uw9o5gH6KKgM+CP50Lq1xhC12CZbKLjkAUuFJQb9FrU/7jEAp+t+DKpoVBcru8bBjTxxx4wO1zXFcbJZ9zb4c4h76j2nfaNVuPTBB+mDF/HDd914=
on:
tags: true
repo: mthenw/frontail

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:12-buster-slim
WORKDIR /frontail
ADD . .
RUN npm install --production
ENTRYPOINT ["/frontail/docker-entrypoint.sh"]
EXPOSE 9001
CMD ["--help"]

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 Maciej Winnicki
Copyright (c) 2017 Maciej Winnicki
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

112
README.md
View File

@ -1,32 +1,36 @@
# frontail realtime log stream in the browser
# frontail streaming logs to the browser
```frontail``` is node.js application for serving `tail -F` output to browser.
`frontail` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI.
[![Build Status](https://img.shields.io/travis/mthenw/frontail.svg?style=flat)](https://travis-ci.org/mthenw/frontail)
[![Version](http://img.shields.io/npm/v/frontail.svg?style=flat)](https://www.npmjs.org/package/frontail)
![frontial](https://user-images.githubusercontent.com/455261/29570317-660c8122-8756-11e7-9d2f-8fea19e05211.gif)
[![Docker Pulls](https://img.shields.io/docker/pulls/mthenw/frontail.svg)](https://hub.docker.com/r/mthenw/frontail/)
## Quick start
- `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
- `frontail /var/log/syslog`
- visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
## Features
* search (```Tab``` to focus, ```Esc``` to clear)
* basic authentication
* working over ssh
* log rotation
* auto-scrolling
* marking logs
* themes (default, dark)
* number of unread logs in favicon
* [tailing multiple files](#tailing-multiple-files)
* [highlighting](#highlighting)
- log rotation (not on Windows)
- auto-scrolling
- marking logs
- pausing logs
- number of unread logs in favicon
- themes (default, dark)
- [highlighting](#highlighting)
- search (`Tab` to focus, `Esc` to clear)
- set filter from url parameter `filter`
- tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
- basic authentication
## Installation
## Installation options
Note that as of 3.0.0 release, frontail requires Node.js 4 or newer. Earlier versions (2.x) work on legacy Node.js versions like 0.10 or 0.12.
npm install frontail -g
or use [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/)
docker run -d -v /var/log/syslog:/syslog -P mthenw/frontail /syslog
- download a binary file from [Releases](https://github.com/mthenw/frontail/releases) pagegit st
- using [npm package](https://www.npmjs.com/package/frontail): `npm i frontail -g`
- using [Docker image](https://cloud.docker.com/repository/docker/mthenw/frontail): `docker run -d -P -v /var/log:/log mthenw/frontail /log/syslog`
## Usage
@ -34,7 +38,6 @@ or use [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/)
Options:
-h, --help output usage information
-V, --version output the version number
-h, --host <host> listening host, default 0.0.0.0
-p, --port <port> listening port, default 9001
@ -48,23 +51,30 @@ or use [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/)
-c, --certificate <cert.pem> Certificate for HTTPS, option works only along with -k option
--pid-path <path> if run as daemon file that will store the process id, default /var/run/frontail.pid
--log-path <path> if run as daemon file that will be used as a log, default /dev/null
--url-path <path> URL path for the browser application, default /
--ui-hide-topbar hide topbar (log file name and search box)
--ui-no-indent don't indent log lines
--ui-highlight highlight words or lines if defined string found in logs, default preset
--ui-highlight-preset <path> custom preset for highlighting (see ./preset/default.json)
--remote-host <remote_host> setting the remote host for a tail over ssh (e.g: 192.168.0.2)
--remote-user <remote_user> setting the remote user for a tail over ssh, default root
--remote-port <remote_port> setting the remote port for a tail over ssh, default 22
--path <path> prefix path for the running application, default /
--disable-usage-stats disable gathering usage statistics
--help output usage information
Web interface runs on **http://localhost:[port]**.
Web interface runs on **http://[host]:[port]**.
### Tailing multiple files
`[file ...]` argument is passed directly to `tail` command which means that `*`, `?` and other shell special characters can be used (I recommend reading [Wildcards, Quotes, Back Quotes and Apostrophes in shell commands](http://www.codecoffee.com/tipsforlinux/articles/26-1.html)).
`[file ...]` accepts multiple paths, `*`, `?` and other shell special characters([Wildcards, Quotes, Back Quotes and Apostrophes in shell commands](http://www.codecoffee.com/tipsforlinux/articles/26-1.html)).
### stdin
Use `-` for streaming stdin:
./server | frontail -
### Highlighting
```--ui-highlight``` option turns on highlighting in UI. By default preset from ```./preset/defatult.json``` is used:
`--ui-highlight` option turns on highlighting in UI. By default preset from `./preset/default.json` is used:
```
{
@ -77,10 +87,46 @@ Web interface runs on **http://localhost:[port]**.
}
```
which means that every "err" string will be in red and every line with "err" will be bolded. Custom preset can be provided by
which means that every "err" string will be in red and every line containing "err" will be bolded.
*New presets are very welcome. If you don't like default or you would like to share your, please create PR with json file.*
_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._
## Screenshot
Available presets:
![screenshot1](https://dl.dropboxusercontent.com/u/3101412/frontail1.0.png)
- default
- npmlog
- python
### Running behind nginx
Using the `--url-path` option `frontail` can run behind nginx with the example configuration
Using `frontail` with `--url-path /frontail`
```
events {
worker_connections 1024;
}
http {
server {
listen 8080;
server_name localhost;
location /frontail {
proxy_pass http://127.0.0.1:9001/frontail;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
```
### Usage statistics
`frontail` by default (from `v4.5.0`) gathers **anonymous** usage statistics in Google Analytics. It can be disabled with
`--disable-usage-stats`.
The data is used to help me understand how `frontail` is used and I can make it better.

40
RELEASING.md Normal file
View File

@ -0,0 +1,40 @@
# Releasing Frontail
After all [pull requests](https://github.com/mthenw/frontail/pulls) for a release have been merged and all [Travis CI builds](https://travis-ci.org/mthenw/frontail) are green, you may create a release as follows:
1. If you haven't already, switch to the master branch, ensure that you have no changes, and pull from origin.
```sh
$ git checkout master
$ git status
$ git pull <remote> master --rebase
```
1. Edit the `package.json` file changing `version` field to your new release version and run `npm i`.
1. Commit your changes.
```sh
$ git commit -am "Release <version>"
```
1. Push the commit.
```sh
$ git push origin head
```
1. GitHub action will publish new version to NPM and push new tag.
1. Publish new release on GitHub with [`release`](https://github.com/zeit/release) package.
```sh
$ git pull
$ npx release -P
```
1. Upload binaries
```sh
$ npm run pkg
```

5
docker-entrypoint.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
exec ./bin/frontail $@

245
index.js
View File

@ -1,136 +1,171 @@
'use strict';
var connect = require('connect');
var cookieParser = require('cookie');
var crypto = require('crypto');
var path = require('path');
var sanitizer = require('validator').sanitize;
var socketio = require('socket.io');
var tail = require('./lib/tail');
var connectBuilder = require('./lib/connect_builder');
var program = require('./lib/options_parser');
var serverBuilder = require('./lib/server_builder');
var daemonize = require('./lib/daemonize');
const cookie = require('cookie');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const path = require('path');
const { Server } = require('socket.io');
const fs = require('fs');
const untildify = require('untildify');
const tail = require('./lib/tail');
const connectBuilder = require('./lib/connect_builder');
const program = require('./lib/options_parser');
const serverBuilder = require('./lib/server_builder');
const daemonize = require('./lib/daemonize');
const usageStats = require('./lib/stats');
/**
* Parse args
*/
program.parse(process.argv);
if (program.args.length === 0) {
console.error('Arguments needed, use --help');
process.exit();
console.error('Arguments needed, use --help');
process.exit();
}
/**
* Init usage statistics
*/
const stats = usageStats(!program.disableUsageStats, program);
stats.track('runtime', 'init');
stats.time('runtime', 'runtime');
/**
* Validate params
*/
var doAuthorization = !!(program.user && program.password);
var doSecure = !!(program.key && program.certificate);
var doSSH = !!program.remoteHost;
var sessionSecret = String(+new Date()) + Math.random();
var sessionKey = 'sid';
var files = program.args.join(' ');
var filesNamespace = crypto.createHash('md5').update(files).digest('hex');
const doAuthorization = !!(program.user && program.password);
const doSecure = !!(program.key && program.certificate);
const sessionSecret = String(+new Date()) + Math.random();
const files = program.args.join(' ');
const filesNamespace = crypto.createHash('md5').update(files).digest('hex');
const urlPath = program.urlPath.replace(/\/$/, ''); // remove trailing slash
if (program.daemonize) {
daemonize(__filename, program, {
doAuthorization: doAuthorization,
doSecure: doSecure,
doSSH: doSSH
});
daemonize(__filename, program, {
doAuthorization,
doSecure,
});
} else {
/**
* HTTP(s) server setup
*/
var appBuilder = connectBuilder();
if (doAuthorization) {
appBuilder.session(sessionSecret, sessionKey);
appBuilder.authorize(program.user, program.password);
}
appBuilder
.static(__dirname + '/lib/web/assets')
.index(__dirname + '/lib/web/index.html', files, filesNamespace, program.theme);
/**
* HTTP(s) server setup
*/
const appBuilder = connectBuilder(urlPath);
if (doAuthorization) {
appBuilder.session(sessionSecret);
appBuilder.authorize(program.user, program.password);
}
appBuilder
.static(path.join(__dirname, 'web', 'assets'))
.index(
path.join(__dirname, 'web', 'index.html'),
files,
filesNamespace,
program.theme
);
var builder = serverBuilder();
if (doSecure) {
builder.secure(program.key, program.certificate);
}
var server = builder
.use(appBuilder.build())
.port(program.port)
.host(program.host)
.build();
const builder = serverBuilder();
if (doSecure) {
builder.secure(program.key, program.certificate);
}
const server = builder
.use(appBuilder.build())
.port(program.port)
.host(program.host)
.build();
/**
* socket.io setup
*/
var io = socketio.listen(server, {log: false});
/**
* socket.io setup
*/
const io = new Server({ path: `${urlPath}/socket.io` });
io.attach(server);
if (doAuthorization) {
io.use(function (socket, next) {
var handshakeData = socket.request;
if (handshakeData.headers.cookie) {
var cookie = cookieParser.parse(handshakeData.headers.cookie);
var sessionId = connect.utils.parseSignedCookie(cookie[sessionKey], sessionSecret);
if (sessionId) {
return next(null);
}
return next(new Error('Invalid cookie'), false);
} else {
return next(new Error('No cookie in header'), false);
}
});
}
if (doAuthorization) {
io.use((socket, next) => {
const handshakeData = socket.request;
if (handshakeData.headers.cookie) {
const cookies = cookie.parse(handshakeData.headers.cookie);
const sessionIdEncoded = cookies['connect.sid'];
if (!sessionIdEncoded) {
return next(new Error('Session cookie not provided'), false);
}
const sessionId = cookieParser.signedCookie(
sessionIdEncoded,
sessionSecret
);
if (sessionId) {
return next(null);
}
return next(new Error('Invalid cookie'), false);
}
/**
* Setup UI highlights
*/
var highlightConfig;
if (program.uiHighlight) {
highlightConfig = require(path.resolve(__dirname, program.uiHighlightPreset));
}
return next(new Error('No cookie in header'), false);
});
}
/**
* When connected send starting data
*/
var tailer;
if (doSSH) {
var sshOptions = {
remoteHost: program.remoteHost,
remoteUser: program.remoteUser,
remotePort: program.remotePort
};
/**
* Setup UI highlights
*/
let highlightConfig;
if (program.uiHighlight) {
let presetPath;
tailer = tail(program.args, {buffer: program.number, ssh: sshOptions});
if (!program.uiHighlightPreset) {
presetPath = path.join(__dirname, 'preset', 'default.json');
} else {
tailer = tail(program.args, {buffer: program.number});
presetPath = path.resolve(untildify(program.uiHighlightPreset));
}
var filesSocket = io.of('/' + filesNamespace).on('connection', function (socket) {
socket.emit('options:lines', program.lines);
if (fs.existsSync(presetPath)) {
highlightConfig = JSON.parse(fs.readFileSync(presetPath));
} else {
throw new Error(`Preset file ${presetPath} doesn't exists`);
}
}
program.uiHideTopbar && socket.emit('options:hide-topbar');
!program.uiIndent && socket.emit('options:no-indent');
program.uiHighlight && socket.emit('options:highlightConfig', highlightConfig);
/**
* When connected send starting data
*/
const tailer = tail(program.args, {
buffer: program.number,
});
tailer.getBuffer().forEach(function (line) {
socket.emit('line', line);
});
const filesSocket = io.of(`/${filesNamespace}`).on('connection', (socket) => {
socket.emit('options:lines', program.lines);
if (program.uiHideTopbar) {
socket.emit('options:hide-topbar');
}
if (!program.uiIndent) {
socket.emit('options:no-indent');
}
if (program.uiHighlight) {
socket.emit('options:highlightConfig', highlightConfig);
}
tailer.getBuffer().forEach((line) => {
socket.emit('line', line);
});
});
/**
* Send incoming data
*/
tailer.on('line', function (line) {
filesSocket.emit('line', sanitizer(line).xss());
/**
* Send incoming data
*/
tailer.on('line', (line) => {
filesSocket.emit('line', line);
});
stats.track('runtime', 'started');
/**
* Handle signals
*/
const cleanExit = () => {
stats.timeEnd('runtime', 'runtime', () => {
process.exit();
});
/**
* Handle signals
*/
var cleanExit = function () {
process.exit();
};
process.on('SIGINT', cleanExit);
process.on('SIGTERM', cleanExit);
};
process.on('SIGINT', cleanExit);
process.on('SIGTERM', cleanExit);
}

View File

@ -1,53 +1,75 @@
'use strict';
var connect = require('connect');
var fs = require('fs');
const connect = require('connect');
const fs = require('fs');
const serveStatic = require('serve-static');
const expressSession = require('express-session');
const basicAuth = require('basic-auth-connect');
var ConnectBuilder = function () {
this.app = connect();
function ConnectBuilder(urlPath) {
this.app = connect();
this.urlPath = urlPath;
}
ConnectBuilder.prototype.authorize = function authorize(user, pass) {
this.app.use(
this.urlPath,
basicAuth(
(incomingUser, incomingPass) =>
user === incomingUser && pass === incomingPass
)
);
return this;
};
ConnectBuilder.prototype.authorize = function (user, pass) {
this.app.use(connect.basicAuth(function (incomingUser, incomingPass) {
return user === incomingUser && pass === incomingPass;
}));
return this;
ConnectBuilder.prototype.build = function build() {
return this.app;
};
ConnectBuilder.prototype.build = function () {
return this.app;
};
ConnectBuilder.prototype.index = function index(
path,
files,
filesNamespace,
themeOpt
) {
const theme = themeOpt || 'default';
ConnectBuilder.prototype.index = function (path, files, filesNamespace, theme) {
theme = theme || 'default';
this.app.use(function (req, res) {
fs.readFile(path, function (err, data) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(data.toString('utf-8')
.replace(/__TITLE__/g, files)
.replace(/__THEME__/g, theme)
.replace(/__NAMESPACE__/g, filesNamespace),
'utf-8'
);
});
this.app.use(this.urlPath, (req, res) => {
fs.readFile(path, (err, data) => {
res.writeHead(200, {
'Content-Type': 'text/html',
});
res.end(
data
.toString('utf-8')
.replace(/__TITLE__/g, files)
.replace(/__THEME__/g, theme)
.replace(/__NAMESPACE__/g, filesNamespace)
.replace(/__PATH__/g, this.urlPath),
'utf-8'
);
});
});
return this;
return this;
};
ConnectBuilder.prototype.session = function (secret, key) {
this.app.use(connect.cookieParser());
this.app.use(connect.session({secret: secret, key: key}));
return this;
ConnectBuilder.prototype.session = function sessionf(secret) {
this.app.use(
this.urlPath,
expressSession({
secret,
resave: false,
saveUninitialized: true,
})
);
return this;
};
ConnectBuilder.prototype.static = function (path) {
this.app.use(connect.static(path));
return this;
ConnectBuilder.prototype.static = function staticf(path) {
this.app.use(this.urlPath, serveStatic(path));
return this;
};
module.exports = function () {
return new ConnectBuilder();
};
module.exports = (urlPath) => new ConnectBuilder(urlPath);

View File

@ -1,66 +1,69 @@
'use strict';
var daemon = require('daemon');
var fs = require('fs');
const daemon = require('daemon-fix41');
const fs = require('fs');
var defaultOptions = {
doAuthorization: false,
doSecure: false
const defaultOptions = {
doAuthorization: false,
doSecure: false,
};
module.exports = function (script, params, options) {
options = options || defaultOptions;
module.exports = (script, params, opts) => {
const options = opts || defaultOptions;
var logFile = fs.openSync(params.logPath, 'a');
const logFile = fs.openSync(params.logPath, 'a');
var args = [
'-h', params.host,
'-p', params.port,
'-n', params.number,
'-l', params.lines,
'-t', params.theme
];
let args = [
'-h',
params.host,
'-p',
params.port,
'-n',
params.number,
'-l',
params.lines,
'-t',
params.theme,
];
if (options.doAuthorization) {
args.push(
'-U', params.user,
'-P', params.password
);
}
if (options.doAuthorization) {
args.push('-U', params.user, '-P', params.password);
}
if (options.doSecure) {
args.push(
'-k', params.key,
'-c', params.certificate
);
}
if (options.doSecure) {
args.push('-k', params.key, '-c', params.certificate);
}
if (options.doSSH) {
args.push(
'--remote-host', params.remoteHost,
'--remote-user', params.remoteUser,
'--remote-port', params.remotePort
);
}
if (params.uiHideTopbar) {
args.push('--ui-hide-topbar');
}
if (params.uiHideTopbar) {
args.push('--ui-hide-topbar');
}
if (params.urlPath) {
args.push('--url-path', params.urlPath);
}
if (!params.uiIndent) {
args.push('--ui-no-indent');
}
if (!params.uiIndent) {
args.push('--ui-no-indent');
}
if (params.uiHighlight) {
args.push('--ui-highlight', '--ui-highlight-preset', params.uiHighlightPreset);
}
if (params.uiHighlight) {
args.push('--ui-highlight');
}
args = args.concat(params.args);
if (params.uiHighlightPreset) {
args.push('--ui-highlight-preset', params.uiHighlightPreset);
}
var proc = daemon.daemon(script, args, {
stdout: logFile,
stderr: logFile
});
if (params.disableUsageStats) {
args.push('--disable-usage-stats', params.disableUsageStats);
}
fs.writeFileSync(params.pidPath, proc.pid);
args = args.concat(params.args);
const proc = daemon.daemon(script, args, {
stdout: logFile,
stderr: logFile,
});
fs.writeFileSync(params.pidPath, proc.pid);
};

View File

@ -1,36 +1,87 @@
'use strict';
var program = require('commander');
const program = require('commander');
program
.version(require('../package.json').version)
.usage('[options] [file ...]')
.option('--remote-host <remote_host>', 'setting the remote host for a tail over ssh (e.g: 192.168.0.2)', String,
null)
.option('--remote-user <remote_user>', 'setting the remote user for a tail over ssh, default root', String, 'root')
.option('--remote-port <remote_port>', 'setting the remote port for a tail over ssh, default 22', Number, 22)
.option('-h, --host <host>', 'listening host, default 0.0.0.0', String, '0.0.0.0')
.option('-p, --port <port>', 'listening port, default 9001', Number, 9001)
.option('-n, --number <number>', 'starting lines number, default 10', Number, 10)
.option('-l, --lines <lines>', 'number on lines stored in browser, default 2000', Number, 2000)
.option('-t, --theme <theme>', 'name of the theme (default, dark)', String, 'default')
.option('-d, --daemonize', 'run as daemon')
.option('-U, --user <username>', 'Basic Authentication username, option works only along with -P option',
String, false)
.option('-P, --password <password>', 'Basic Authentication password, option works only along with -U option',
String, false)
.option('-k, --key <key.pem>', 'Private Key for HTTPS, option works only along with -c option',
String, false)
.option('-c, --certificate <cert.pem>', 'Certificate for HTTPS, option works only along with -k option',
String, false)
.option('--pid-path <path>', 'if run as daemon file that will store the process id, default /var/run/frontail.pid',
String, '/var/run/frontail.pid')
.option('--log-path <path>', 'if run as daemon file that will be used as a log, default /dev/null',
String, '/dev/null')
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
.option('--ui-no-indent', 'don\'t indent log lines')
.option('--ui-highlight', 'highlight words or lines if defined string found in logs, default preset')
.option('--ui-highlight-preset <path>', 'custom preset for highlighting (see ./preset/default.json)', String,
'./preset/default.json');
.version(require('../package.json').version)
.usage('[options] [file ...]')
.helpOption('--help')
.option(
'-h, --host <host>',
'listening host, default 0.0.0.0',
String,
'0.0.0.0'
)
.option('-p, --port <port>', 'listening port, default 9001', Number, 9001)
.option(
'-n, --number <number>',
'starting lines number, default 10',
Number,
10
)
.option(
'-l, --lines <lines>',
'number on lines stored in browser, default 2000',
Number,
2000
)
.option(
'-t, --theme <theme>',
'name of the theme (default, dark)',
String,
'default'
)
.option('-d, --daemonize', 'run as daemon')
.option(
'-U, --user <username>',
'Basic Authentication username, option works only along with -P option',
String,
false
)
.option(
'-P, --password <password>',
'Basic Authentication password, option works only along with -U option',
String,
false
)
.option(
'-k, --key <key.pem>',
'Private Key for HTTPS, option works only along with -c option',
String,
false
)
.option(
'-c, --certificate <cert.pem>',
'Certificate for HTTPS, option works only along with -k option',
String,
false
)
.option(
'--pid-path <path>',
'if run as daemon file that will store the process id, default /var/run/frontail.pid',
String,
'/var/run/frontail.pid'
)
.option(
'--log-path <path>',
'if run as daemon file that will be used as a log, default /dev/null',
String,
'/dev/null'
)
.option(
'--url-path <path>',
'URL path for the browser application, default /',
String,
'/'
)
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
.option('--ui-no-indent', "don't indent log lines")
.option(
'--ui-highlight',
'highlight words or lines if defined string found in logs, default preset'
)
.option(
'--ui-highlight-preset <path>',
'custom preset for highlighting (see ./preset/default.json)'
)
.option('--disable-usage-stats', 'disable gathering usage statistics');
module.exports = program;

View File

@ -1,52 +1,54 @@
/* eslint no-underscore-dangle: off */
'use strict';
var fs = require('fs');
var http = require('http');
var https = require('https');
const fs = require('fs');
const http = require('http');
const https = require('https');
var ServerBuilder = function () {
this._host = null;
this._port = 9001;
function ServerBuilder() {
this._host = null;
this._port = 9001;
}
ServerBuilder.prototype.build = function build() {
if (this._key && this._cert) {
const options = {
key: this._key,
cert: this._cert,
};
return https
.createServer(options, this._callback)
.listen(this._port, this._host);
}
return http.createServer(this._callback).listen(this._port, this._host);
};
ServerBuilder.prototype.build = function () {
if (this._key && this._cert) {
var options = {
key: this._key,
cert: this._cert
};
return https.createServer(options, this._callback).listen(this._port, this._host);
} else {
return http.createServer(this._callback).listen(this._port, this._host);
}
ServerBuilder.prototype.host = function hostf(host) {
this._host = host;
return this;
};
ServerBuilder.prototype.host = function (host) {
this._host = host;
return this;
ServerBuilder.prototype.port = function portf(port) {
this._port = port;
return this;
};
ServerBuilder.prototype.port = function (port) {
this._port = port;
return this;
ServerBuilder.prototype.secure = function secure(keyPath, certPath) {
try {
this._key = fs.readFileSync(keyPath);
this._cert = fs.readFileSync(certPath);
} catch (e) {
throw new Error('No key or certificate file found');
}
return this;
};
ServerBuilder.prototype.secure = function (keyPath, certPath) {
try {
this._key = fs.readFileSync(keyPath);
this._cert = fs.readFileSync(certPath);
} catch (e) {
throw new Error('No key or certificate file found');
}
return this;
ServerBuilder.prototype.use = function use(callback) {
this._callback = callback;
return this;
};
ServerBuilder.prototype.use = function (callback) {
this._callback = callback;
return this;
};
module.exports = function () {
return new ServerBuilder();
};
module.exports = () => new ServerBuilder();

69
lib/stats.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const ua = require('universal-analytics');
const isDocker = require('is-docker');
const Configstore = require('configstore');
const uuidv4 = require('uuid/v4');
const pkg = require('../package.json');
const trackingID = 'UA-129582046-1';
// Usage stats
function Stats(enabled, opts) {
this.timer = {};
if (enabled === true) {
const config = new Configstore(pkg.name);
let clientID = uuidv4();
if (config.has('clientID')) {
clientID = config.get('clientID');
} else {
config.set('clientID', clientID);
}
const tracker = ua(trackingID, clientID);
tracker.set('aip', 1); // Anonymize IP
tracker.set('an', 'frontail'); // Application Name
tracker.set('av', pkg.version); // Application Version
tracker.set('ds', 'app'); // Data Source
tracker.set('cd1', process.platform); // platform
tracker.set('cd2', process.arch); // arch
tracker.set('cd3', process.version.match(/^v(\d+\.\d+)/)[1]); // Node version
tracker.set('cd4', isDocker()); // is Docker
tracker.set('cd5', opts.number !== 10); // is --number parameter set
this.tracker = tracker;
}
return this;
}
Stats.prototype.track = function track(category, action) {
if (!this.tracker) {
return;
}
this.tracker.event(category, action).send();
};
Stats.prototype.time = function time(category, action) {
if (!this.tracker) {
return;
}
if (!this.timer[category]) {
this.timer[category] = {};
}
this.timer[category][action] = Date.now();
};
Stats.prototype.timeEnd = function timeEnd(category, action, cb) {
if (!this.tracker) {
cb();
return;
}
this.tracker
.timing(category, action, Date.now() - this.timer[category][action])
.send(cb);
};
module.exports = (enabled, opts) => new Stats(enabled, opts);

View File

@ -1,56 +1,72 @@
/* eslint no-underscore-dangle: off */
'use strict';
var EventEmitter = require('events').EventEmitter;
var childProcess = require('child_process');
var util = require('util');
var CBuffer = require('CBuffer');
const events = require('events');
const childProcess = require('child_process');
const tailStream = require('fs-tail-stream');
const util = require('util');
const CBuffer = require('CBuffer');
const byline = require('byline');
const commandExistsSync = require('command-exists').sync;
var Tail = function (path, options) {
EventEmitter.call(this);
function Tail(path, opts) {
events.EventEmitter.call(this);
options = options || {buffer: 0};
this._buffer = new CBuffer(options.buffer);
var tail;
const options = opts || {
buffer: 0,
};
this._buffer = new CBuffer(options.buffer);
if (options.ssh) {
var args = [
options.ssh.remoteUser + '@' + options.ssh.remoteHost,
'-p', options.ssh.remotePort,
'tail -f'
].concat(path);
let stream;
tail = childProcess.spawn('ssh', args);
} else {
tail = childProcess.spawn('tail', ['-n', options.buffer, '-F'].concat(path));
}
if (path[0] === '-') {
stream = process.stdin;
} else {
/* Check if this os provides the `tail` command. */
const hasTailCommand = commandExistsSync('tail');
if (hasTailCommand) {
let followOpt = '-F';
if (process.platform === 'openbsd') {
followOpt = '-f';
}
tail.stderr.on('data', function (data) {
const cp = childProcess.spawn(
'tail',
['-n', options.buffer, followOpt].concat(path)
);
cp.stderr.on('data', (data) => {
// If there is any important error then display it in the console. Tail will keep running.
// File can be truncated over network.
if (data.toString().indexOf('file truncated') === -1) {
console.error(data.toString());
console.error(data.toString());
}
});
});
stream = cp.stdout;
tail.stdout.on('data', function (data) {
var lines = data.toString('utf-8').split('\n');
lines.pop();
lines.forEach(function (line) {
this._buffer.push(line);
this.emit('line', line);
}.bind(this));
}.bind(this));
process.on('exit', () => {
cp.kill();
});
} else {
/* This is used if the os does not support the `tail`command. */
stream = tailStream.createReadStream(path.join(), {
encoding: 'utf8',
start: options.buffer,
tail: true,
});
}
}
process.on('exit', function () {
tail.kill();
});
};
util.inherits(Tail, EventEmitter);
byline(stream, { keepEmptyLines: true }).on('data', (line) => {
const str = line.toString();
this._buffer.push(str);
this.emit('line', str);
});
}
util.inherits(Tail, events.EventEmitter);
Tail.prototype.getBuffer = function () {
return this._buffer.toArray();
Tail.prototype.getBuffer = function getBuffer() {
return this._buffer.toArray();
};
module.exports = function (path, options) {
return new Tail(path, options);
};
module.exports = (path, options) => new Tail(path, options);

View File

@ -1,278 +0,0 @@
/* global Tinycon:false, ansi_up:false */
window.App = (function (window, document) {
'use strict';
/**
* @type {Object}
* @private
*/
var _socket;
/**
* @type {HTMLElement}
* @private
*/
var _logContainer;
/**
* @type {HTMLElement}
* @private
*/
var _filterInput;
/**
* @type {String}
* @private
*/
var _filterValue = '';
/**
* @type {HTMLElement}
* @private
*/
var _topbar;
/**
* @type {HTMLElement}
* @private
*/
var _body;
/**
* @type {number}
* @private
*/
var _linesLimit = Math.Infinity;
/**
* @type {number}
* @private
*/
var _newLinesCount = 0;
/**
* @type {boolean}
* @private
*/
var _isWindowFocused = true;
/**
* @type {object}
* @private
*/
var _highlightConfig;
/**
* Hide element if doesn't contain filter value
*
* @param {Object} element
* @private
*/
var _filterElement = function (element) {
var pattern = new RegExp(_filterValue, 'i');
if (pattern.test(element.textContent)) {
element.style.display = '';
} else {
element.style.display = 'none';
}
};
/**
* Filter logs based on _filterValue
*
* @function
* @private
*/
var _filterLogs = function () {
var collection = _logContainer.childNodes;
var i = collection.length;
if (i === 0) {
return;
}
while (i) {
_filterElement(collection[i - 1]);
i -= 1;
}
window.scrollTo(0, document.body.scrollHeight);
};
/**
* @return {Boolean}
* @private
*/
var _isScrolledBottom = function () {
var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
var totalHeight = document.body.offsetHeight;
var clientHeight = document.documentElement.clientHeight;
return totalHeight <= currentScroll + clientHeight;
};
/**
* @return void
* @private
*/
var _faviconReset = function () {
_newLinesCount = 0;
Tinycon.setBubble(0);
};
/**
* @return void
* @private
*/
var _updateFaviconCounter = function () {
if (_isWindowFocused) {
return;
}
_newLinesCount += 1;
if (_newLinesCount > 99) {
Tinycon.setBubble(99);
} else {
Tinycon.setBubble(_newLinesCount);
}
};
/**
* @return String
* @private
*/
var _highlightWord = function (line) {
if (_highlightConfig) {
if (_highlightConfig.words) {
for (var wordCheck in _highlightConfig.words) {
if (_highlightConfig.words.hasOwnProperty(wordCheck)) {
line = line.replace(
wordCheck,
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>'
);
}
}
}
}
return line;
};
/**
* @return HTMLElement
* @private
*/
var _highlightLine = function (line, container) {
if (_highlightConfig) {
if (_highlightConfig.lines) {
for (var lineCheck in _highlightConfig.lines) {
if (line.indexOf(lineCheck) !== -1) {
container.setAttribute('style', _highlightConfig.lines[lineCheck]);
}
}
}
}
return container;
};
return {
/**
* Init socket.io communication and log container
*
* @param {Object} opts options
*/
init: function (opts) {
var self = this;
// Elements
_logContainer = opts.container;
_filterInput = opts.filterInput;
_filterInput.focus();
_topbar = opts.topbar;
_body = opts.body;
// Filter input bind
_filterInput.addEventListener('keyup', function (e) {
// ESC
if (e.keyCode === 27) {
this.value = '';
_filterValue = '';
} else {
_filterValue = this.value;
}
_filterLogs();
});
// Favicon counter bind
window.addEventListener('blur', function () {
_isWindowFocused = false;
}, true);
window.addEventListener('focus', function () {
_isWindowFocused = true;
_faviconReset();
}, true);
// socket.io init
_socket = opts.socket;
_socket
.on('options:lines', function (limit) {
_linesLimit = limit;
})
.on('options:hide-topbar', function () {
_topbar.className += ' hide';
_body.className = 'no-topbar';
})
.on('options:no-indent', function () {
_logContainer.className += ' no-indent';
})
.on('options:highlightConfig', function (highlightConfig) {
_highlightConfig = highlightConfig;
})
.on('line', function (line) {
self.log(line);
});
},
/**
* Log data
*
* @param {string} data data to log
*/
log: function (data) {
var wasScrolledBottom = _isScrolledBottom();
var div = document.createElement('div');
var p = document.createElement('p');
p.className = 'inner-line';
// convert ansi color codes to html && escape HTML tags
data = ansi_up.escape_for_html(data); // eslint-disable-line
data = ansi_up.ansi_to_html(data); // eslint-disable-line
p.innerHTML = _highlightWord(data);
div.className = 'line';
div = _highlightLine(data, div);
div.addEventListener('click', function () {
if (this.className.indexOf('selected') === -1) {
this.className = 'line-selected';
} else {
this.className = 'line';
}
});
div.appendChild(p);
_filterElement(div);
_logContainer.appendChild(div);
if (_logContainer.children.length > _linesLimit) {
_logContainer.removeChild(_logContainer.children[0]);
}
if (wasScrolledBottom) {
window.scrollTo(0, document.body.scrollHeight);
}
_updateFaviconCounter();
}
};
})(window, document);

File diff suppressed because one or more lines are too long

View File

@ -1,46 +0,0 @@
@import "bootstrap.min.css";
body {
padding-top: 4em;
background-color: #2F3238;
}
.no-topbar {
padding-top: 10px;
}
.navbar {
background-color: #26292E;
border: 0;
}
.form-control {
border: 0;
color: #7F8289;
background-color: #2F3238;
}
.log {
white-space: pre-wrap;
color: #7F8289;
font-size: .85em;
background: inherit;
border: 0;
padding: 0;
}
.log .inner-line {
padding: 0 15px;
margin-left: 84pt;
text-indent: -84pt;
margin-bottom: 0;
}
.log.no-indent .inner-line {
margin-left: 0;
text-indent: 0;
}
.log .line-selected {
background-color: #302436;
}

View File

@ -1,38 +0,0 @@
@import "bootstrap.min.css";
body {
padding-top: 4em;
}
.no-topbar {
padding-top: 10px;
}
.navbar-inverse .navbar-brand {
color: white;
}
.log {
white-space: pre-wrap;
color: black;
font-size: .85em;
background: inherit;
border: 0;
padding: 0;
}
.log .inner-line {
padding: 0 15px;
margin-left: 84pt;
text-indent: -84pt;
margin-bottom: 0;
}
.log.no-indent .inner-line {
margin-left: 0;
text-indent: 0;
}
.log .line-selected {
background-color: #ffb2b0;
}

View File

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>tail -F __TITLE__</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="/styles/__THEME__.css">
<link rel="icon" href="/favicon.ico">
</head>
<body>
<nav class="topbar navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<span class="navbar-brand" href="#">tail -F __TITLE__</span>
<form class="navbar-form navbar-right" role="search">
<div class="form-group">
<input type="text" class="form-control query" placeholder="Filter" tabindex="1">
</div>
</form>
</div>
</nav>
<pre class="log"></pre>
<script src="/socket.io/socket.io.js"></script>
<script src="/tinycon.min.js"></script>
<script src="/ansi_up.js"></script>
<script src="/app.js"></script>
<script type="text/javascript">
var socket = new io.connect('/' + '__NAMESPACE__');
window.load = App.init({
socket: socket,
container: document.getElementsByClassName('log')[0],
filterInput: document.getElementsByClassName('query')[0],
topbar: document.getElementsByClassName('topbar')[0],
body: document.getElementsByTagName('body')[0]
});
</script>
</body>
</html>

3785
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,82 @@
{
"name": "frontail",
"version": "3.1.1",
"description": "realtime log stream in the browser",
"version": "4.9.2",
"description": "streaming logs to the browser",
"homepage": "https://github.com/mthenw/frontail",
"author": "Maciej Winnicki <maciej.winnicki@gmail.com>",
"license": "MIT",
"bin": "./bin/frontail",
"dependencies": {
"commander": "1.3.2",
"socket.io": "1.4.8",
"connect": "2.11.0",
"validator": "1.5.0",
"daemon": "1.1.0",
"CBuffer": "0.1.4",
"basic-auth-connect": "1.0.0",
"byline": "5.0.0",
"command-exists": "1.2.8",
"commander": "3.0.1",
"configstore": "4.0.0",
"connect": "3.7.0",
"cookie": "0.1.0",
"CBuffer": "0.1.4"
"cookie-parser": "1.4.5",
"daemon-fix41": "1.1.2",
"express-session": "1.15.6",
"fs-tail-stream": "1.1.0",
"is-docker": "1.1.0",
"serve-static": "1.14.1",
"socket.io": "3.1.2",
"universal-analytics": "0.4.23",
"untildify": "3.0.2",
"uuid": "3.3.2"
},
"devDependencies": {
"eslint": "^3.4.0",
"jsdom": "^8.5.0",
"mocha": "~2.3.2",
"eslint": "~6.8.0",
"eslint-config-airbnb-base": "~14.0.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "~2.20.0",
"jsdom": "~11.12.0",
"mocha": "~5.2.0",
"pkg": "~4.4.7",
"should": "~3.3.2",
"sinon": "~1.7.3",
"supertest": "~0.8.1",
"supertest": "^3.3.0",
"temp": "~0.8.1"
},
"scripts": {
"lint": "eslint .",
"test": "mocha --reporter spec test/*.js"
"test": "mocha -r should --exit test/*.js",
"pkg": "pkg --out-path=dist ."
},
"pkg": {
"assets": [
"preset/*.json",
"web/**/*"
],
"targets": [
"node12-alpine-x64",
"node12-linux-x64",
"node12-macos-x64",
"node12-windows-x64"
]
},
"eslintConfig": {
"extends": [
"airbnb-base",
"prettier"
],
"rules": {
"no-console": "off",
"strict": "off",
"implicit-arrow-linebreak": "off"
},
"env": {
"node": true
},
"ignorePatterns": [
"web/assets/tinycon.min.js",
"web/assets/ansi_up.js"
]
},
"prettier": {
"singleQuote": true,
"arrowParens": "always"
},
"repository": {
"type": "git",

View File

@ -1,8 +1,8 @@
{
"words": {
"err": "color: red;"
},
"lines": {
"err": "font-weight: bold;"
}
"words": {
"err": "color: red;"
},
"lines": {
"err": "font-weight: bold;"
}
}

15
preset/npmlog.json Normal file
View File

@ -0,0 +1,15 @@
{
"words": {
"verb": "color: blue; background-color: black;",
"info": "color: green;",
"http": "color: green; background-color: black;",
"WARN": "color: black; background-color: yellow; font-style: normal;",
"error": "color: red;",
"ERR!": "color: red; background-color: black;"
},
"lines": {
"WARN": "font-style: italic;",
"ERR!": "font-weight: bold;"
}
}

12
preset/python.json Normal file
View File

@ -0,0 +1,12 @@
{
"words": {
"TODO": "color: purple"
},
"lines": {
"CRITICAL": "color: Red; font-weight: bold",
"ERROR": "color: Crimson; font-weight: bold",
"WARN": "color: Orange; font-style: italic",
"INFO": "color: DeepSkyBlue;",
"DEBUG": "color: LightGrey;"
}
}

9
test/.eslintrc.json Normal file
View File

@ -0,0 +1,9 @@
{
"rules": {
"import/no-extraneous-dependencies": "off",
"no-unused-expressions": "off"
},
"env": {
"mocha": true
}
}

View File

@ -1,125 +1,242 @@
'use strict';
require('should');
var fs = require('fs');
var jsdom = require('jsdom');
var EventEmitter = require('events').EventEmitter;
const fs = require('fs');
const jsdom = require('jsdom/lib/old-api.js');
const events = require('events');
describe('browser application', function () {
var io, window;
describe('browser application', () => {
let io;
let window;
beforeEach(function (done) {
io = new EventEmitter();
var html = '<title></title><body><div class="topbar"></div>' +
'<div class="log"></div><input type="test" id="filter"/></body>';
var ansiup = fs.readFileSync('./lib/web/assets/ansi_up.js', 'utf-8');
var src = fs.readFileSync('./lib/web/assets/app.js', 'utf-8');
jsdom.env({html: html, src: [ansiup, src], onload: function (domWindow) {
window = domWindow;
initApp();
done();
}});
function initApp() {
window.App.init({
socket: io,
container: window.document.querySelector('.log'),
filterInput: window.document.querySelector('#filter'),
pauseBtn: window.document.querySelector('#pauseBtn'),
topbar: window.document.querySelector('.topbar'),
body: window.document.querySelector('body'),
});
}
it('should show lines from socket.io', function () {
io.emit('line', 'test');
function clickOnElement(line) {
const click = window.document.createEvent('MouseEvents');
click.initMouseEvent(
'click',
true,
true,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
line.dispatchEvent(click);
}
var log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(1);
log.childNodes[0].textContent.should.be.equal('test');
log.childNodes[0].className.should.be.equal('line');
log.childNodes[0].tagName.should.be.equal('DIV');
log.childNodes[0].innerHTML.should.be.equal('<p class="inner-line">test</p>');
beforeEach((done) => {
io = new events.EventEmitter();
const html =
'<title></title><body><div class="topbar"></div>' +
'<div class="log"></div><button type="button" id="pauseBtn"></button>' +
'<input type="test" id="filter"/></body>';
const ansiup = fs.readFileSync('./web/assets/ansi_up.js', 'utf-8');
const src = fs.readFileSync('./web/assets/app.js', 'utf-8');
jsdom.env({
html,
url: 'http://localhost?filter=line.*',
src: [ansiup, src],
onload: (domWindow) => {
window = domWindow;
initApp();
done();
},
});
});
it('should select line when clicked', function () {
io.emit('line', 'test');
it('should show lines from socket.io', () => {
io.emit('line', 'test');
var line = window.document.querySelector('.line');
clickOnElement(line);
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(1);
log.childNodes[0].textContent.should.be.equal('test');
log.childNodes[0].className.should.be.equal('line');
log.childNodes[0].tagName.should.be.equal('DIV');
log.childNodes[0].innerHTML.should.be.equal(
'<p class="inner-line">test</p>'
);
});
line.className.should.be.equal('line-selected');
it('should select line when clicked', () => {
io.emit('line', 'test');
const line = window.document.querySelector('.line');
clickOnElement(line);
line.className.should.be.equal('line-selected');
});
it('should deselect line when selected line clicked', () => {
io.emit('line', 'test');
const line = window.document.querySelector('.line');
clickOnElement(line);
clickOnElement(line);
line.className.should.be.equal('line');
});
it('should limit number of lines in browser', () => {
io.emit('options:lines', 2);
io.emit('line', 'line1');
io.emit('line', 'line2');
io.emit('line', 'line3');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(2);
log.childNodes[0].textContent.should.be.equal('line2');
log.childNodes[1].textContent.should.be.equal('line3');
});
it('should hide topbar', () => {
io.emit('options:hide-topbar');
const topbar = window.document.querySelector('.topbar');
topbar.className.should.match(/hide/);
const body = window.document.querySelector('body');
body.className.should.match(/no-topbar/);
});
it('should not indent log lines', () => {
io.emit('options:no-indent');
const log = window.document.querySelector('.log');
log.className.should.match(/no-indent/);
});
it('should highlight word', () => {
io.emit('options:highlightConfig', {
words: {
foo: 'background: black',
bar: 'background: black',
},
});
io.emit('line', 'foo bar');
it('should deselect line when selected line clicked', function () {
io.emit('line', 'test');
const line = window.document.querySelector('.line');
line.innerHTML.should.containEql(
'<span style="background: black">foo</span> <span style="background: black">bar</span>'
);
});
var line = window.document.querySelector('.line');
clickOnElement(line);
clickOnElement(line);
line.className.should.be.equal('line');
it('should highlight line', () => {
io.emit('options:highlightConfig', {
lines: {
line: 'background: black',
},
});
io.emit('line', 'line1');
it('should limit number of lines in browser', function () {
io.emit('options:lines', 2);
io.emit('line', 'line1');
io.emit('line', 'line2');
io.emit('line', 'line3');
const line = window.document.querySelector('.line');
line.parentNode.innerHTML.should.equal(
'<div class="line" style="background: black"><p class="inner-line">line1</p></div>'
);
});
var log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(2);
log.childNodes[0].textContent.should.be.equal('line2');
log.childNodes[1].textContent.should.be.equal('line3');
});
it('should escape HTML', () => {
io.emit('line', '<a/>');
it('should hide topbar', function () {
io.emit('options:hide-topbar');
const line = window.document.querySelector('.line');
line.innerHTML.should.equal('<p class="inner-line">&lt;a/&gt;</p>');
});
var topbar = window.document.querySelector('.topbar');
topbar.className.should.match(/hide/);
var body = window.document.querySelector('body');
body.className.should.match(/no-topbar/);
});
it('should work filter from URL', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
it('should not indent log lines', function () {
io.emit('options:no-indent');
const filterInput = window.document.querySelector('#filter');
filterInput.value.should.be.equal('line.*');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(3);
log.childNodes[0].style.display.should.be.equal('');
log.childNodes[1].style.display.should.be.equal('none');
log.childNodes[2].style.display.should.be.equal('');
window.location.href.should.containEql('filter=line.*');
});
var log = window.document.querySelector('.log');
log.className.should.match(/no-indent/);
});
it('should clean filter', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
it('should highlight word', function () {
io.emit('options:highlightConfig', {words: {line: 'background: black'}});
io.emit('line', 'line1');
const filterInput = window.document.querySelector('#filter');
const event = new window.KeyboardEvent('keyup', { keyCode: 27 });
filterInput.dispatchEvent(event);
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(3);
log.childNodes[0].style.display.should.be.equal('');
log.childNodes[1].style.display.should.be.equal('');
log.childNodes[2].style.display.should.be.equal('');
window.location.href.should.be.equal('http://localhost/');
});
var line = window.document.querySelector('.line');
line.innerHTML.should.containEql('<span style="background: black">line</span>');
});
it('should change filter', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
it('should highlight line', function () {
io.emit('options:highlightConfig', {lines: {line: 'background: black'}});
io.emit('line', 'line1');
const log = window.document.querySelector('.log');
const filterInput = window.document.querySelector('#filter');
filterInput.value = 'other';
const event = new window.KeyboardEvent('keyup', { keyCode: 13 });
filterInput.dispatchEvent(event);
log.childNodes.length.should.be.equal(3);
log.childNodes[0].style.display.should.be.equal('none');
log.childNodes[1].style.display.should.be.equal('');
log.childNodes[2].style.display.should.be.equal('none');
window.location.href.should.containEql('filter=other');
});
var line = window.document.querySelector('.line');
line.parentNode.innerHTML.should.equal(
'<div class="line" style="background: black"><p class="inner-line">line1</p></div>'
);
});
it('should pause', () => {
io.emit('line', 'line1');
const btn = window.document.querySelector('#pauseBtn');
const event = window.document.createEvent('Event');
event.initEvent('mouseup', true, true);
btn.dispatchEvent(event);
io.emit('line', 'line2');
io.emit('line', 'line3');
it('should escape HTML', function () {
io.emit('line', '<a/>');
btn.className.should.containEql('play');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('==> SKIPPED: 2 <==');
});
var line = window.document.querySelector('.line');
line.innerHTML.should.equal('<p class="inner-line">&lt;a/&gt;</p>');
});
it('should play', () => {
const btn = window.document.querySelector('#pauseBtn');
const event = window.document.createEvent('Event');
event.initEvent('mouseup', true, true);
btn.dispatchEvent(event);
io.emit('line', 'line1');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(1);
log.lastChild.textContent.should.be.equal('==> SKIPPED: 1 <==');
btn.className.should.containEql('play');
btn.dispatchEvent(event);
io.emit('line', 'line2');
var initApp = function () {
window.App.init({
socket: io,
container: window.document.querySelector('.log'),
filterInput: window.document.querySelector('#filter'),
topbar: window.document.querySelector('.topbar'),
body: window.document.querySelector('body')
});
};
var clickOnElement = function (line) {
var click = window.document.createEvent('MouseEvents');
click.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
line.dispatchEvent(click);
};
btn.className.should.not.containEql('play');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('line2');
});
});

View File

@ -1,107 +1,123 @@
'use strict';
require('should');
var connectBuilder = require('../lib/connect_builder');
var request = require('supertest');
const request = require('supertest');
const path = require('path');
const connectBuilder = require('../lib/connect_builder');
describe('connectBuilder', function () {
it('should build connect app', function () {
connectBuilder().build().should.have.property('use');
connectBuilder().build().should.have.property('listen');
describe('connectBuilder', () => {
it('should build connect app', () => {
connectBuilder('/').build().should.have.property('use');
connectBuilder().build().should.have.property('listen');
});
it('should build app requiring authorized user', (done) => {
const app = connectBuilder('/').authorize('user', 'pass').build();
request(app)
.get('/')
.expect('www-authenticate', 'Basic realm="Authorization Required"')
.expect(401, done);
});
it('should build app allowing user to login', (done) => {
const app = connectBuilder('/').authorize('user', 'pass').build();
app.use((req, res) => {
res.end('secret!');
});
it('should build app requiring authorized user', function (done) {
var app = connectBuilder().authorize('user', 'pass').build();
request(app)
.get('/')
.set('Authorization', 'Basic dXNlcjpwYXNz')
.expect(200, 'secret!', done);
});
request(app)
.get('/')
.expect('www-authenticate', 'Basic realm="Authorization Required"')
.expect(401, done);
it('should build app that setup session', (done) => {
const app = connectBuilder('/').session('secret').build();
app.use((req, res) => {
res.end();
});
it('should build app allowing user to login', function (done) {
var app = connectBuilder().authorize('user', 'pass').build();
app.use(function (req, res) {
res.end('secret!');
});
request(app)
.get('/')
.expect('set-cookie', /^connect.sid/, done);
});
request(app)
.get('/')
.set('Authorization', 'Basic dXNlcjpwYXNz')
.expect(200, 'secret!', done);
});
it('should build app that serve static files', (done) => {
const app = connectBuilder('/')
.static(path.join(__dirname, 'fixtures'))
.build();
it('should build app that setup session', function (done) {
var app = connectBuilder().session('secret', 'sessionkey').build();
app.use(function (req, res) {
res.end();
});
request(app).get('/foo.txt').expect('bar', done);
});
request(app)
.get('/')
.expect('set-cookie', /^sessionkey/, done);
});
it('should build app that serve index file', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
.build();
it('should build app that serve static files', function (done) {
var app = connectBuilder().static(__dirname + '/fixtures').build();
request(app).get('/').expect(200).expect('Content-Type', 'text/html', done);
});
request(app)
.get('/foo')
.expect('bar', done);
});
it('should build app that serve index file on specified path', (done) => {
const app = connectBuilder('/test')
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
.build();
it('should build app that serve index file', function (done) {
var app = connectBuilder().index(__dirname + '/fixtures/index', '/testfile').build();
request(app)
.get('/test')
.expect(200)
.expect('Content-Type', 'text/html', done);
});
request(app)
.get('/')
.expect(200)
.expect('Content-Type', 'text/html', done);
});
it('should build app that replace index title', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
.build();
it('should build app that replace index title', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_title', '/testfile')
.build();
request(app).get('/').expect('<head><title>/testfile</title></head>', done);
});
request(app)
.get('/')
.expect('<head><title>/testfile</title></head>', done);
});
it('should build app that sets socket.io namespace based on files', (done) => {
const app = connectBuilder('/')
.index(
path.join(__dirname, 'fixtures/index_with_ns'),
'/testfile',
'ns',
'dark'
)
.build();
it('should build app that sets socket.io namespace based on files', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_ns', '/testfile', 'ns', 'dark')
.build();
request(app).get('/').expect('ns', done);
});
request(app)
.get('/')
.expect('ns', done);
});
it('should build app that sets theme', (done) => {
const app = connectBuilder('/')
.index(
path.join(__dirname, '/fixtures/index_with_theme'),
'/testfile',
'ns',
'dark'
)
.build();
it('should build app that sets theme', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_theme', '/testfile', 'ns', 'dark')
.build();
request(app)
.get('/')
.expect(
'<head><title>/testfile</title><link href="dark.css" rel="stylesheet" type="text/css"/></head>',
done
);
});
request(app)
.get('/')
.expect(
'<head><title>/testfile</title><link href="dark.css" rel="stylesheet" type="text/css"/></head>',
done
);
});
it('should build app that sets default theme', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile')
.build();
it('should build app that sets default theme', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_theme', '/testfile')
.build();
request(app)
.get('/')
.expect(
'<head><title>/testfile</title><link href="default.css" rel="stylesheet" type="text/css"/></head>',
done
);
});
request(app)
.get('/')
.expect(
'<head><title>/testfile</title><link href="default.css" rel="stylesheet" type="text/css"/></head>',
done
);
});
});

View File

@ -1,192 +1,267 @@
'use strict';
require('should');
var daemon = require('daemon');
var optionsParser = require('../lib/options_parser');
var daemonize = require('../lib/daemonize');
var sinon = require('sinon');
var fs = require('fs');
const daemon = require('daemon-fix41');
const sinon = require('sinon');
const fs = require('fs');
const optionsParser = require('../lib/options_parser');
const daemonize = require('../lib/daemonize');
describe('daemonize', function () {
beforeEach(function () {
sinon.stub(daemon, 'daemon');
daemon.daemon.returns({pid: 1000});
sinon.stub(fs, 'writeFileSync');
sinon.stub(fs, 'openSync');
describe('daemonize', () => {
beforeEach(() => {
sinon.stub(daemon, 'daemon');
daemon.daemon.returns({
pid: 1000,
});
sinon.stub(fs, 'writeFileSync');
sinon.stub(fs, 'openSync');
});
afterEach(() => {
daemon.daemon.restore();
fs.writeFileSync.restore();
fs.openSync.restore();
});
describe('should daemon', () => {
it('current script', () => {
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[0].should.match('script');
});
afterEach(function () {
daemon.daemon.restore();
fs.writeFileSync.restore();
fs.openSync.restore();
it('with hostname', () => {
optionsParser.parse(['node', '/path/to/frontail', '-h', '127.0.0.1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-h', '127.0.0.1']);
});
describe('should daemon', function () {
it('current script', function () {
daemonize('script', optionsParser);
it('with port', () => {
optionsParser.parse(['node', '/path/to/frontail', '-p', '80']);
daemon.daemon.lastCall.args[0].should.match('script');
});
daemonize('script', optionsParser);
it('with hostname', function () {
optionsParser.parse(['node', '/path/to/frontail', '-h', '127.0.0.1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-h', '127.0.0.1']);
});
it('with port', function () {
optionsParser.parse(['node', '/path/to/frontail', '-p', '80']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-p', 80]);
});
it('with lines number', function () {
optionsParser.parse(['node', '/path/to/frontail', '-n', '1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-n', 1]);
});
it('with lines stored in browser', function () {
optionsParser.parse(['node', '/path/to/frontail', '-l', '1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-l', 1]);
});
it('with theme', function () {
optionsParser.parse(['node', '/path/to/frontail', '-t', 'dark']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-t', 'dark']);
});
it('with authorization', function () {
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
daemonize('script', optionsParser, {doAuthorization: true});
daemon.daemon.lastCall.args[1].should.containDeep(['-U', 'user', '-P', 'passw0rd']);
});
it('without authorization if option doAuthorization not passed', function () {
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.not.containDeep(['-U', 'user', '-P', 'passw0rd']);
});
it('with secure connection', function () {
optionsParser.parse(['node', '/path/to/frontail', '-k', 'key.file', '-c', 'cert.file']);
daemonize('script', optionsParser, {doSecure: true});
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
});
it('with ssh configuration', function () {
optionsParser.parse([
'node', '/path/to/frontail',
'--remote-host', 'remoteHost',
'--remote-user', 'remoteUser',
'--remote-port', '23'
]);
daemonize('script', optionsParser, {doSSH: true});
daemon.daemon.lastCall.args[1].should.containDeep([
'--remote-host', 'remoteHost',
'--remote-user', 'remoteUser',
'--remote-port', '23'
]);
});
it('without ssh configuration', function () {
optionsParser.parse([
'node', '/path/to/frontail',
'--remote-user', 'remoteUser',
'--remote-port', '23'
]);
daemonize('script', optionsParser, {doSSH: false});
daemon.daemon.lastCall.args[1].should.not.containDeep([
'--remote-host', 'remoteHost',
'--remote-user', 'remoteUser',
'--remote-port', '23'
]);
});
it('without secure connection if option doSecure not passed', function () {
optionsParser.parse(['node', '/path/to/frontail', '-k', 'key.file', '-c', 'cert.file']);
daemonize('script', optionsParser, {doSecure: true});
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
});
it('with hide-topbar option', function () {
optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-hide-topbar']);
});
it('with no-indent option', function () {
optionsParser.parse(['node', '/path/to/frontail', '--ui-no-indent']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-no-indent']);
});
it('with highlight option', function () {
optionsParser.parse(['node', '/path/to/frontail', '--ui-highlight']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight']);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight-preset', './preset/default.json']);
});
it('with file to tail', function () {
optionsParser.parse(['node', '/path/to/frontail', '/path/to/file']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['/path/to/file']);
});
daemon.daemon.lastCall.args[1].should.containDeep(['-p', 80]);
});
it('should write pid to pidfile', function () {
optionsParser.parse(['node', '/path/to/frontail', '--pid-path', '/path/to/pid']);
it('with lines number', () => {
optionsParser.parse(['node', '/path/to/frontail', '-n', '1']);
daemonize('script', optionsParser);
daemonize('script', optionsParser);
fs.writeFileSync.lastCall.args[0].should.be.equal('/path/to/pid');
fs.writeFileSync.lastCall.args[1].should.be.equal(1000);
daemon.daemon.lastCall.args[1].should.containDeep(['-n', 1]);
});
it('should log to file', function () {
optionsParser.parse(['node', '/path/to/frontail', '--log-path', '/path/to/log']);
fs.openSync.returns('file');
it('with lines stored in browser', () => {
optionsParser.parse(['node', '/path/to/frontail', '-l', '1']);
daemonize('script', optionsParser);
daemonize('script', optionsParser);
fs.openSync.lastCall.args[0].should.equal('/path/to/log');
fs.openSync.lastCall.args[1].should.equal('a');
daemon.daemon.lastCall.args[2].should.eql({
stdout: 'file',
stderr: 'file'
});
daemon.daemon.lastCall.args[1].should.containDeep(['-l', 1]);
});
it('with theme', () => {
optionsParser.parse(['node', '/path/to/frontail', '-t', 'dark']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-t', 'dark']);
});
it('with authorization', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-U',
'user',
'-P',
'passw0rd',
]);
daemonize('script', optionsParser, {
doAuthorization: true,
});
daemon.daemon.lastCall.args[1].should.containDeep([
'-U',
'user',
'-P',
'passw0rd',
]);
});
it('without authorization if option doAuthorization not passed', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-U',
'user',
'-P',
'passw0rd',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.not.containDeep([
'-U',
'user',
'-P',
'passw0rd',
]);
});
it('with secure connection', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-k',
'key.file',
'-c',
'cert.file',
]);
daemonize('script', optionsParser, {
doSecure: true,
});
daemon.daemon.lastCall.args[1].should.containDeep([
'-k',
'key.file',
'-c',
'cert.file',
]);
});
it('without secure connection if option doSecure not passed', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-k',
'key.file',
'-c',
'cert.file',
]);
daemonize('script', optionsParser, {
doSecure: true,
});
daemon.daemon.lastCall.args[1].should.containDeep([
'-k',
'key.file',
'-c',
'cert.file',
]);
});
it('with url-path option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--url-path', '/test']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--url-path',
'/test',
]);
});
it('with hide-topbar option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-hide-topbar']);
});
it('with no-indent option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--ui-no-indent']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-no-indent']);
});
it('with highlight option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--ui-highlight']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight']);
});
it('with highlight preset option', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--ui-highlight',
'--ui-highlight-preset',
'test.json',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--ui-highlight-preset',
'test.json',
]);
});
it('with disable usage stats', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--disable-usage-stats',
'test.json',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--disable-usage-stats',
'test.json',
]);
});
it('with file to tail', () => {
optionsParser.parse(['node', '/path/to/frontail', '/path/to/file']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['/path/to/file']);
});
});
it('should write pid to pidfile', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--pid-path',
'/path/to/pid',
]);
daemonize('script', optionsParser);
fs.writeFileSync.lastCall.args[0].should.be.equal('/path/to/pid');
fs.writeFileSync.lastCall.args[1].should.be.equal(1000);
});
it('should log to file', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--log-path',
'/path/to/log',
]);
fs.openSync.returns('file');
daemonize('script', optionsParser);
fs.openSync.lastCall.args[0].should.equal('/path/to/log');
fs.openSync.lastCall.args[1].should.equal('a');
daemon.daemon.lastCall.args[2].should.eql({
stdout: 'file',
stderr: 'file',
});
});
});

View File

@ -1,114 +1,127 @@
'use strict';
require('should');
var fs = require('fs');
var http = require('http');
var https = require('https');
var serverBuilder = require('../lib/server_builder');
var sinon = require('sinon');
const fs = require('fs');
const http = require('http');
const https = require('https');
const sinon = require('sinon');
const serverBuilder = require('../lib/server_builder');
describe('serverBuilder', function () {
describe('serverBuilder', () => {
describe('http server', () => {
let httpServer;
let createServer;
describe('http server', function () {
var httpServer;
var createServer;
beforeEach(function () {
httpServer = sinon.createStubInstance(http.Server);
httpServer.listen.returns(httpServer);
createServer = sinon.stub(http, 'createServer').returns(httpServer);
});
afterEach(function () {
createServer.restore();
});
it('should build server', function () {
var server = serverBuilder().build();
createServer.calledOnce.should.equal(true);
server.should.be.an.instanceof(http.Server);
});
it('should build server accepting requests', function () {
var callback = function () {};
serverBuilder().use(callback).build();
createServer.calledWith(callback).should.equal(true);
});
it('should build listening server', function () {
serverBuilder().build();
httpServer.listen.calledOnce.should.equal(true);
});
it('should build server listening on specified port', function () {
serverBuilder().port(6666).build();
httpServer.listen.calledWith(6666).should.equal(true);
});
it('should build server listening on default port', function () {
serverBuilder().build();
httpServer.listen.calledWith(9001).should.equal(true);
});
it('should build server listening on specified host', function () {
serverBuilder().host('127.0.0.1').build();
httpServer.listen.calledWith(9001, '127.0.0.1').should.equal(true);
});
it('should build server listening on default host', function () {
serverBuilder().build();
httpServer.listen.calledWith(9001, null).should.equal(true);
});
beforeEach(() => {
httpServer = sinon.createStubInstance(http.Server);
httpServer.listen.returns(httpServer);
createServer = sinon.stub(http, 'createServer').returns(httpServer);
});
describe('https server', function () {
var httpsServer;
var createHttpsServer;
var readFileSyncStub;
beforeEach(function () {
httpsServer = sinon.createStubInstance(https.Server);
httpsServer.listen.returns(httpsServer);
createHttpsServer = sinon.stub(https, 'createServer').returns(httpsServer);
readFileSyncStub = sinon.stub(fs, 'readFileSync');
readFileSyncStub.withArgs('key.pem').returns('testkey');
readFileSyncStub.withArgs('cert.pem').returns('testcert');
});
afterEach(function () {
createHttpsServer.restore();
readFileSyncStub.restore();
});
it('should build server', function () {
var server = serverBuilder().secure('key.pem', 'cert.pem').build();
server.should.be.an.instanceof(https.Server);
createHttpsServer.calledWith({key: 'testkey', cert: 'testcert'}).should.equal(true);
});
it('should build server accepting requests', function () {
var callback = function () {};
serverBuilder().use(callback).secure('key.pem', 'cert.pem').build();
createHttpsServer.calledWith({key: 'testkey', cert: 'testcert'}, callback).should.equal(true);
});
it('should throw error if key or cert not provided', function () {
readFileSyncStub.restore();
(function () {
serverBuilder().secure('nofile', 'nofile');
}).should.throw('No key or certificate file found');
});
afterEach(() => {
createServer.restore();
});
it('should build server', () => {
const server = serverBuilder().build();
createServer.calledOnce.should.equal(true);
server.should.be.an.instanceof(http.Server);
});
it('should build server accepting requests', () => {
const callback = () => {};
serverBuilder().use(callback).build();
createServer.calledWith(callback).should.equal(true);
});
it('should build listening server', () => {
serverBuilder().build();
httpServer.listen.calledOnce.should.equal(true);
});
it('should build server listening on specified port', () => {
serverBuilder().port(6666).build();
httpServer.listen.calledWith(6666).should.equal(true);
});
it('should build server listening on default port', () => {
serverBuilder().build();
httpServer.listen.calledWith(9001).should.equal(true);
});
it('should build server listening on specified host', () => {
serverBuilder().host('127.0.0.1').build();
httpServer.listen.calledWith(9001, '127.0.0.1').should.equal(true);
});
it('should build server listening on default host', () => {
serverBuilder().build();
httpServer.listen.calledWith(9001, null).should.equal(true);
});
});
describe('https server', () => {
let httpsServer;
let createHttpsServer;
let readFileSyncStub;
beforeEach(() => {
httpsServer = sinon.createStubInstance(https.Server);
httpsServer.listen.returns(httpsServer);
createHttpsServer = sinon
.stub(https, 'createServer')
.returns(httpsServer);
readFileSyncStub = sinon.stub(fs, 'readFileSync');
readFileSyncStub.withArgs('key.pem').returns('testkey');
readFileSyncStub.withArgs('cert.pem').returns('testcert');
});
afterEach(() => {
createHttpsServer.restore();
readFileSyncStub.restore();
});
it('should build server', () => {
const server = serverBuilder().secure('key.pem', 'cert.pem').build();
server.should.be.an.instanceof(https.Server);
createHttpsServer
.calledWith({
key: 'testkey',
cert: 'testcert',
})
.should.equal(true);
});
it('should build server accepting requests', () => {
const callback = () => {};
serverBuilder().use(callback).secure('key.pem', 'cert.pem').build();
createHttpsServer
.calledWith(
{
key: 'testkey',
cert: 'testcert',
},
callback
)
.should.equal(true);
});
it('should throw error if key or cert not provided', () => {
readFileSyncStub.restore();
(() => {
serverBuilder().secure('nofile', 'nofile');
}).should.throw('No key or certificate file found');
});
});
});

View File

@ -1,73 +1,60 @@
'use strict';
require('should');
var fs = require('fs');
var tail = require('../lib/tail');
var temp = require('temp');
var sinon = require('sinon');
var childProcess = require('child_process');
const fs = require('fs');
const temp = require('temp');
const tail = require('../lib/tail');
var TEMP_FILE_PROFIX = '';
var SPAWN_DELAY = 10;
const TEMP_FILE_PROFIX = '';
const SPAWN_DELAY = 10;
describe('tail', function () {
temp.track();
function writeLines(fd, count) {
for (let i = 0; i < count; i += 1) {
fs.writeSync(
fd,
`line${i}
`
);
}
fs.closeSync(fd);
}
it('calls event line if new line appear in file', function (done) {
temp.open(TEMP_FILE_PROFIX, function (err, info) {
tail(info.path).on('line', function (line) {
line.should.equal('line0');
done();
});
describe('tail', () => {
temp.track();
setTimeout(writeLines, SPAWN_DELAY, info.fd, 1);
});
it('calls event line if new line appear in file', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
tail(info.path).on('line', (line) => {
line.should.equal('line0');
done();
});
setTimeout(writeLines, SPAWN_DELAY, info.fd, 1);
});
});
it('buffers lines on start', function (done) {
temp.open(TEMP_FILE_PROFIX, function (err, info) {
writeLines(info.fd, 20);
it('buffers lines on start', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
writeLines(info.fd, 20);
var tailer = tail(info.path, {buffer: 2});
setTimeout(function () {
tailer.getBuffer().should.be.eql(['line18', 'line19']);
done();
}, SPAWN_DELAY);
});
const tailer = tail(info.path, {
buffer: 2,
});
setTimeout(() => {
tailer.getBuffer().should.be.eql(['line18', 'line19']);
done();
}, SPAWN_DELAY);
});
});
it('buffers no lines on start by default', function (done) {
temp.open(TEMP_FILE_PROFIX, function (err, info) {
writeLines(info.fd, 3);
it('buffers no lines on start by default', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
writeLines(info.fd, 3);
var tailer = tail(info.path);
setTimeout(function () {
tailer.getBuffer().should.be.empty;
done();
}, SPAWN_DELAY);
});
const tailer = tail(info.path);
setTimeout(() => {
tailer.getBuffer().should.be.empty;
done();
}, SPAWN_DELAY);
});
describe('with ssh options', function () {
it('should call ssh command', function () {
sinon.spy(childProcess, 'spawn');
var sshOptions = {
remoteUser: 'testUser',
remoteHost: 'host',
remotePort: 1234
};
tail('test/path', {ssh: sshOptions});
childProcess.spawn.calledWith('ssh', ['testUser@host', '-p', 1234, 'tail -f', 'test/path']).should.be.true;
childProcess.spawn.restore();
});
});
var writeLines = function (fd, count) {
for (var i = 0; i < count; i += 1) {
fs.writeSync(fd, 'line' + i + '\n');
}
fs.closeSync(fd);
};
});
});

13
web/assets/.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"rules": {
"no-var": "off",
"no-underscore-dangle": "off",
"prefer-arrow-callback": "off",
"func-names": "off",
"space-before-function-paren": "off",
"prefer-template": "off"
},
"env": {
"browser": true
}
}

345
web/assets/app.js Normal file
View File

@ -0,0 +1,345 @@
/* global Tinycon:false, ansi_up:false */
window.App = (function app(window, document) {
'use strict';
/**
* @type {Object}
* @private
*/
var _socket;
/**
* @type {HTMLElement}
* @private
*/
var _logContainer;
/**
* @type {HTMLElement}
* @private
*/
var _filterInput;
/**
* @type {String}
* @private
*/
var _filterValue = '';
/**
* @type {HTMLElement}
* @private
*/
var _pauseBtn;
/**
* @type {boolean}
* @private
*/
var _isPaused = false;
/**
* @type {number}
* @private
*/
var _skipCounter = 0;
/**
* @type {HTMLElement}
* @private
*/
var _topbar;
/**
* @type {HTMLElement}
* @private
*/
var _body;
/**
* @type {number}
* @private
*/
var _linesLimit = Math.Infinity;
/**
* @type {number}
* @private
*/
var _newLinesCount = 0;
/**
* @type {boolean}
* @private
*/
var _isWindowFocused = true;
/**
* @type {object}
* @private
*/
var _highlightConfig;
/**
* Hide element if doesn't contain filter value
*
* @param {Object} element
* @private
*/
var _filterElement = function(elem) {
var pattern = new RegExp(_filterValue, 'i');
var element = elem;
if (pattern.test(element.textContent)) {
element.style.display = '';
} else {
element.style.display = 'none';
}
};
/**
* Filter logs based on _filterValue
*
* @function
* @private
*/
var _filterLogs = function() {
var collection = _logContainer.childNodes;
var i = collection.length;
if (i === 0) {
return;
}
while (i) {
_filterElement(collection[i - 1]);
i -= 1;
}
window.scrollTo(0, document.body.scrollHeight);
};
/**
* Set _filterValue from URL parameter `filter`
*
* @function
* @private
*/
var _setFilterValueFromURL = function(filterInput, uri) {
var _url = new URL(uri);
var _filterValueFromURL = _url.searchParams.get('filter');
if (typeof _filterValueFromURL !== 'undefined' && _filterValueFromURL !== null) {
_filterValue = _filterValueFromURL;
filterInput.value = _filterValue; // eslint-disable-line
}
};
/**
* Set parameter `filter` in URL
*
* @function
* @private
*/
var _setFilterParam = function(value, uri) {
var _url = new URL(uri);
var _params = new URLSearchParams(_url.search.slice(1));
if (value === '') {
_params.delete('filter');
} else {
_params.set('filter', value);
}
_url.search = _params.toString();
window.history.replaceState(null, document.title, _url.toString());
};
/**
* @return void
* @private
*/
var _faviconReset = function() {
_newLinesCount = 0;
Tinycon.setBubble(0);
};
/**
* @return void
* @private
*/
var _updateFaviconCounter = function() {
if (_isWindowFocused || _isPaused) {
return;
}
if (_newLinesCount < 99) {
_newLinesCount += 1;
Tinycon.setBubble(_newLinesCount);
}
};
/**
* @return String
* @private
*/
var _highlightWord = function(line) {
var output = line;
if (_highlightConfig && _highlightConfig.words) {
Object.keys(_highlightConfig.words).forEach((wordCheck) => {
output = output.replace(
wordCheck,
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>',
);
});
}
return output;
};
/**
* @return HTMLElement
* @private
*/
var _highlightLine = function(line, container) {
if (_highlightConfig && _highlightConfig.lines) {
Object.keys(_highlightConfig.lines).forEach((lineCheck) => {
if (line.indexOf(lineCheck) !== -1) {
container.setAttribute('style', _highlightConfig.lines[lineCheck]);
}
});
}
return container;
};
return {
/**
* Init socket.io communication and log container
*
* @param {Object} opts options
*/
init: function init(opts) {
var self = this;
// Elements
_logContainer = opts.container;
_filterInput = opts.filterInput;
_filterInput.focus();
_pauseBtn = opts.pauseBtn;
_topbar = opts.topbar;
_body = opts.body;
_setFilterValueFromURL(_filterInput, window.location.toString());
// Filter input bind
_filterInput.addEventListener('keyup', function(e) {
// ESC
if (e.keyCode === 27) {
this.value = '';
_filterValue = '';
} else {
_filterValue = this.value;
}
_setFilterParam(_filterValue, window.location.toString());
_filterLogs();
});
// Pause button bind
_pauseBtn.addEventListener('mouseup', function() {
_isPaused = !_isPaused;
if (_isPaused) {
this.className += ' play';
} else {
_skipCounter = 0;
this.classList.remove('play');
}
});
// Favicon counter bind
window.addEventListener(
'blur',
function() {
_isWindowFocused = false;
},
true,
);
window.addEventListener(
'focus',
function() {
_isWindowFocused = true;
_faviconReset();
},
true,
);
// socket.io init
_socket = opts.socket;
_socket
.on('options:lines', function(limit) {
_linesLimit = limit;
})
.on('options:hide-topbar', function() {
_topbar.className += ' hide';
_body.className = 'no-topbar';
})
.on('options:no-indent', function() {
_logContainer.className += ' no-indent';
})
.on('options:highlightConfig', function(highlightConfig) {
_highlightConfig = highlightConfig;
})
.on('line', function(line) {
if (_isPaused) {
_skipCounter += 1;
self.log('==> SKIPPED: ' + _skipCounter + ' <==', (_skipCounter > 1));
} else {
self.log(line);
}
});
},
/**
* Log data
*
* @param {string} data data to log
*/
log: function log(data, replace = false) {
var wasScrolledBottom = window.innerHeight + Math.ceil(window.pageYOffset + 1)
>= document.body.offsetHeight;
var div = document.createElement('div');
var p = document.createElement('p');
p.className = 'inner-line';
// convert ansi color codes to html && escape HTML tags
data = ansi_up.escape_for_html(data); // eslint-disable-line
data = ansi_up.ansi_to_html(data); // eslint-disable-line
p.innerHTML = _highlightWord(data);
div.className = 'line';
div = _highlightLine(data, div);
div.addEventListener('click', function click() {
if (this.className.indexOf('selected') === -1) {
this.className = 'line-selected';
} else {
this.className = 'line';
}
});
div.appendChild(p);
_filterElement(div);
if (replace) {
_logContainer.replaceChild(div, _logContainer.lastChild);
} else {
_logContainer.appendChild(div);
}
if (_logContainer.children.length > _linesLimit) {
_logContainer.removeChild(_logContainer.children[0]);
}
if (wasScrolledBottom) {
window.scrollTo(0, document.body.scrollHeight);
}
_updateFaviconCounter();
},
};
}(window, document));

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

6
web/assets/styles/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,67 @@
@import 'default.css';
body {
padding-top: 4em;
background-color: #2f3238;
}
.no-topbar {
padding-top: 10px;
}
.navbar {
background-color: #26292e;
border: 0;
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-brand:hover {
color: #999;
}
.btn-pause {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%237f8289' viewBox='0 0 8 8'><path d='M1 1v6h2v-6h-2zm4 0v6h2v-6h-2z'></path></svg>") no-repeat center center;
background-color: #2f3238;
border: 1px solid transparent;
}
.btn-pause.play {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%237f8289' viewBox='0 0 8 8'><path d='M1 1v6l6-3-6-3z'></path></svg>") no-repeat center center;
background-color: #2f3238;
}
.form-control {
border: 0;
color: #7f8289;
background-color: #2f3238;
}
.log {
white-space: pre-wrap;
color: #7f8289;
font-size: 0.85em;
background: inherit;
border: 0;
padding: 0;
}
.log .inner-line {
padding: 0 15px;
margin-left: 84pt;
text-indent: -84pt;
margin-bottom: 0;
}
.log .inner-line:empty::after {
content: '.';
visibility: hidden;
}
.log.no-indent .inner-line {
margin-left: 0;
text-indent: 0;
}
.log .line-selected {
background-color: #302436;
}

View File

@ -0,0 +1,84 @@
@import 'bootstrap.min.css';
body {
padding-top: 60px;
}
@media screen and (max-width: 768px) {
body {
padding-top: 120px;
}
}
.navbar-brand {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: inline-block;
width: 100%;
}
.btn-pause {
margin: 8px 0 8px;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 8 8'><path d='M1 1v6h2v-6h-2zm4 0v6h2v-6h-2z'></path></svg>")
no-repeat center center;
background-color: #e2e6ea;
background-size: contain;
cursor: pointer;
display: inline-block;
height: 34px;
width: 34px;
border: 1px solid #ccc;
}
.btn-pause.play {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 8 8'><path d='M1 1v6l6-3-6-3z'></path></svg>")
no-repeat center center;
background-color: #e2e6ea;
}
.navbar-form-custom {
padding: 8px 0;
}
.no-horiz-padding {
padding-left: 0px;
padding-right: 0px;
}
.no-topbar {
padding-top: 10px;
}
.navbar-inverse .navbar-brand {
color: white;
}
.log {
white-space: pre-wrap;
color: black;
font-size: 0.85em;
background: inherit;
border: 0;
padding: 0;
}
.log .inner-line {
padding: 0 15px;
margin-left: 84pt;
text-indent: -84pt;
margin-bottom: 0;
}
.log .inner-line:empty::after {
content: '.';
visibility: hidden;
}
.log.no-indent .inner-line {
margin-left: 0;
text-indent: 0;
}
.log .line-selected {
background-color: #ffb2b0;
}

57
web/index.html Normal file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>tail -f __TITLE__</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="__PATH__/styles/__THEME__.css">
<link rel="icon" href="__PATH__/favicon.ico">
</head>
<body>
<nav class="topbar navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8">
<span class="navbar-brand text-overflow" title="__TITLE__">tail -f __TITLE__</span>
</div>
<div class="col-sm-1 text-right">
<button type="button" class="btn btn-light btn-pause" data-toggle="button" aria-pressed="false" autocomplete="off"></button>
</div>
<div class="col-sm-3">
<form class="navbar-form-custom" role="search" onkeypress="return event.keyCode != 13;">
<input type="text" class="form-control query" placeholder="Filter" tabindex="1">
</form>
</div>
</div>
</div>
</nav>
<div class="container-fluid no-horiz-padding">
<pre class="log"></pre>
</div>
<script src="__PATH__/socket.io/socket.io.js"></script>
<script src="__PATH__/tinycon.min.js"></script>
<script src="__PATH__/ansi_up.js"></script>
<script src="__PATH__/app.js"></script>
<script type="text/javascript">
var socket = new io.connect('/__NAMESPACE__', {
path: '__PATH__/socket.io',
transports: ['websocket']
});
window.load = App.init({
socket: socket,
container: document.getElementsByClassName('log')[0],
filterInput: document.getElementsByClassName('query')[0],
pauseBtn: document.getElementsByClassName('btn-pause')[0],
topbar: document.getElementsByClassName('topbar')[0],
body: document.getElementsByTagName('body')[0]
});
</script>
</body>
</html>