Compare commits

...

200 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
Maciej Winnicki 70ef84434f
release v3.1.1 2016-09-04 17:46:54 +02:00
Maciej Winnicki a304ac098b
fix lint 2016-09-04 17:44:25 +02:00
Maciej Winnicki ba362a4100
version bump 2016-09-04 17:31:53 +02:00
Maciej Winnicki 5c582d4b2d
update socket.io lib 2016-09-04 17:31:35 +02:00
Maciej Winnicki 4949ca416d Migration to Node v4. Closes #69 2016-05-05 09:58:03 +02:00
Maciej Winnicki 837901debb replace jshint jscs with eslint 2016-05-03 19:12:15 +02:00
Maciej Winnicki 658ccb7a23 Lint; v2.3.0 2016-03-30 19:23:15 +02:00
Maciej Winnicki a8f6942b5c Merge pull request #65 from MAD-GooZe/master
Displaying terminal ansi color characters
2016-03-30 19:10:37 +02:00
Alexey Gusev 0c6068b30e enable html escaping && load ansi_up.js in tests 2016-03-30 19:29:29 +03:00
Alexey Gusev 7c8f63d6c1 ansi_up.js && version bump 2016-03-30 19:06:10 +03:00
Maciej Winnicki c38de1be51 fix preset loading. v2.2.1 2016-01-29 13:36:21 +01:00
Maciej Winnicki b5865d5423 v bump 2016-01-25 12:57:05 +01:00
Maciej Winnicki 3e1c620b24 Typo 2016-01-25 12:55:51 +01:00
Maciej Winnicki b025ff6dbd Merge pull request #61 from andresol/master
Fix for tail'ing over network
2016-01-25 12:54:52 +01:00
andresol 5e96521809 Fix for tail'ing over network
Do not kill tail on error. Just log. File trunkcated is common on
tailing over windows.
2016-01-25 12:24:52 +01:00
Maciej Winnicki 2ac11eb0ac Stylezz 2016-01-11 10:58:52 +01:00
Maciej Winnicki a7cd6897ad Properly handle signals. Closes #57 2016-01-11 10:53:52 +01:00
Maciej Winnicki 2bff6b68de Update README.md 2015-10-07 10:08:44 +02:00
Maciej Winnicki 3df000920c Info about tailing multiple files 2015-09-17 10:05:53 +02:00
Maciej Winnicki 054eb70c46 Test escaping HTML fix #50 2015-09-16 23:54:14 +02:00
Maciej Winnicki 3879a6eb79 Update README.md
[ci skip]
2015-07-07 23:13:34 +02:00
Maciej Winnicki 2bfb862258 Version bump [ci skip] 2015-07-06 19:36:28 +02:00
Maciej Winnicki a1eef74c73 Merge pull request #49 from matteocontrini/master
Escape HTML tags
2015-07-06 19:31:44 +02:00
Matteo Contrini a10c35018a Code style 2015-07-05 13:07:48 +02:00
Matteo Contrini 99c37d6d99 Escape HTML tags 2015-07-05 13:00:22 +02:00
Maciej Winnicki 95671f0164 Info about docker 2015-04-23 20:05:06 +02:00
Maciej Winnicki d50b6d4177 Version bump [ci skip] 2015-03-27 12:14:15 +01:00
Maciej Winnicki a321a09bf3 Test against 0.12 2015-03-27 12:07:07 +01:00
Maciej Winnicki 511a33e0d3 Handle multiple files 2015-03-27 12:06:06 +01:00
Maciej Winnicki a65f5f4104 Doc update 2015-03-03 23:49:31 +01:00
Maciej Winnicki 019ce4acd9 2.0.0 release 2015-03-03 23:45:09 +01:00
Maciej Winnicki 654a62c522 Fixed highlighting in deamon mode (fix #44) 2015-03-03 23:44:09 +01:00
Maciej Winnicki 80607e1237 New option --ui-higlight-preset 2015-03-03 23:43:36 +01:00
Maciej Winnicki b85c8284bc Code refactor + test for ssh 2015-02-23 21:27:09 +01:00
Maciej Winnicki 4711e72e48 Simpler notation 2015-02-23 20:56:10 +01:00
Maciej Winnicki 15f1c39d15 Merge pull request #42 from mdelapenya/ssh-daemonize
[#41] Pass SSH parameters to daemon
2015-02-23 20:45:04 +01:00
Manuel de la Peña c0b796c5b0 [#42] Add another test: without SSH configuration 2015-02-23 20:04:35 +01:00
Manuel de la Peña d7ec19d5a4 [#42] Update test with doSSH approach 2015-02-23 20:04:00 +01:00
Manuel de la Peña 1d8fb184c3 [#42] Follow doSecure approach for SSH connections 2015-02-23 20:03:25 +01:00
Manuel de la Peña 3909011c1c [#41] Source formatting (Travis-ci complained about line length) 2015-02-20 14:55:03 +01:00
Manuel de la Peña bcfbd599bc [#41] Add a test to verify the issue 2015-02-20 14:49:45 +01:00
Manuel de la Peña d45568a11d [#41] Pass SSH parameters to daemon 2015-02-20 14:12:51 +01:00
Maciej Winnicki 77bddbe17e README update with info about ssh. Version bump. 2015-02-10 00:30:16 +01:00
Maciej Winnicki 3b1b976c87 Merge pull request #39 from RoCat/master
add remote tail over ssh
2015-02-10 00:23:46 +01:00
rcatoio 2bfb6b1c51 simplify error catching 2015-02-04 17:13:34 +01:00
rcatoio 82618c8cc9 fix codestyle 2015-02-04 16:45:03 +01:00
rcatoio d8f9b491ea add remote tail over ssh 2015-02-04 14:22:35 +01:00
Maciej Winnicki a5d7b9e797 Update README.md 2014-11-28 11:25:45 +01:00
Maciej Winnicki ebe8d0abba New NPM badge 2014-11-14 20:48:34 +01:00
Maciej Winnicki 70b5c7c61f New version 2014-10-08 00:12:29 +02:00
Maciej Winnicki 9fc775997f Updated readme with info about highlight 2014-10-08 00:11:38 +02:00
Maciej Winnicki c4caed9a58 Redundant code moved to before each 2014-10-07 23:58:30 +02:00
Maciej Winnicki 79f63d39fb Some parameters moved from jshint to jscs 2014-10-07 23:40:55 +02:00
Maciej Winnicki 6223033bb0 Merge pull request #36 from mthenw/highlights
--ui-highlight
2014-10-06 22:59:45 +02:00
Maciej Winnicki 3435c986aa Tests and README update 2014-10-06 22:55:51 +02:00
Maciej Winnicki 38c256a9f3 Merge branch 'master' into highlights 2014-10-05 15:02:06 +02:00
Maciej Winnicki 14bbc135db temp package update because of travis build failure 2014-10-05 14:57:07 +02:00
Maciej Winnicki a74a7b75fe jscs check 2014-10-05 14:33:25 +02:00
Maciej Winnicki d0b1cdcc98 npm scripts instead of Makefile 2014-10-05 13:11:02 +02:00
Patrik Johnson 4d658ed940 Apply css stylings to either lines or individual words, if matching strings are found in a supplied configuration file 2014-10-02 00:38:16 +03:00
Maciej Winnicki b57fa79588 Missing ; 2014-07-13 02:45:15 +02:00
Maciej Winnicki 78076c1b8e Buffer lines on start (fixes #33) 2014-07-13 02:42:21 +02:00
Maciej Winnicki bd21fe14c7 Travis don't suppor 0.9 2014-05-29 01:29:16 +02:00
Maciej Winnicki b439f5ff90 Updated travis worker to stable version 2014-05-29 01:26:41 +02:00
Maciej Winnicki cbde520735 Updated travis worker's node version 2014-05-29 01:23:19 +02:00
Maciej Winnicki 9ace721324 daemonize with all cli parameters 2014-05-29 01:14:05 +02:00
Maciej Winnicki c3fbf35568 New description 2014-04-26 12:45:09 +02:00
Maciej Winnicki f573abe064 Not needed info 2014-03-29 21:47:26 +01:00
Maciej Winnicki bd6abfd8ef Forgot about lint 2014-03-29 21:44:40 +01:00
44 changed files with 6282 additions and 970 deletions

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

3
.gitignore vendored
View File

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

View File

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

View File

@ -1,32 +0,0 @@
{
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"forin": true,
"indent": 4,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"plusplus": true,
"quotmark": "single",
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"maxlen": 120,
"white": true,
"node": true,
"browser": true,
"globals": {
"io": true,
"describe": false,
"it": false,
"beforeEach": false,
"afterEach": false
}
}

View File

@ -1,7 +1,16 @@
language: node_js
node_js:
- 0.8
- 0.10
- 6
- 8
- 10
script:
- make lint
- make 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

View File

@ -1,10 +0,0 @@
lint:
@./node_modules/.bin/jshint .
test:
@./node_modules/.bin/mocha --reporter spec test/*.js
test-watch:
@./node_modules/.bin/mocha --reporter spec test/*.js --watch
.PHONY: lint test test-watch

119
README.md
View File

@ -1,23 +1,36 @@
# frontail(1) tail -F output in 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://travis-ci.org/mthenw/frontail.svg?branch=master)](https://travis-ci.org/mthenw/frontail)
[![NPM version](https://badge.fury.io/js/frontail.png)](http://badge.fury.io/js/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)
* user basic authentication
* log rotation
* auto-scrolling
* marking logs
* themes (default, dark)
* number of unread logs in favicon
- 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
npm install frontail -g
- 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
@ -25,7 +38,6 @@
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
@ -39,11 +51,82 @@
-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 (every line after first one)
--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)
--path <path> prefix path for the running application, default /
--disable-usage-stats disable gathering usage statistics
--help output usage information
Web interface is on http://localhost:[port]
Web interface runs on **http://[host]:[port]**.
## Screenshot
### Tailing multiple files
![screenshot1](https://dl.dropboxusercontent.com/u/3101412/frontail1.0.png)
`[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/default.json` is used:
```
{
"words": {
"err": "color: red;"
},
"lines": {
"err": "font-weight: bold;"
}
}
```
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 yours, please create PR with json file._
Available presets:
- 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 $@

275
index.js
View File

@ -1,144 +1,171 @@
'use strict';
var connect = require('connect');
var cookieParser = require('cookie');
var crypto = require('crypto');
var daemon = require('daemon');
var fs = require('fs');
var program = require('commander');
var sanitizer = require('validator').sanitize;
var socketio = require('socket.io');
var tail = require('./lib/tail');
var connectBuilder = require('./lib/connect_builder');
var serverBuilder = require('./lib/server_builder');
program
.version(require('./package.json').version)
.usage('[options] [file ...]')
.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 (every line after first one)')
.parse(process.argv);
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 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 process
*/
var logFile = fs.openSync(program.logPath, 'a');
var args = ['-p', program.port, '-n', program.number, '-l', program.lines, '-t', program.theme];
if (doAuthorization) {
args = args.concat(['-U', program.user, '-P', program.password]);
}
args = args.concat(program.args);
var proc = daemon.daemon(
__filename,
args,
{
stdout: logFile,
stderr: logFile
}
);
fs.writeFileSync(program.pidPath, proc.pid);
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.set('authorization', function (handshakeData, accept) {
if (handshakeData.headers.cookie) {
var cookie = cookieParser.parse(handshakeData.headers.cookie);
var sessionId = connect.utils.parseSignedCookie(cookie[sessionKey], sessionSecret);
if (sessionId) {
return accept(null, true);
}
return accept('Invalid cookie', false);
} else {
return accept('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);
}
/**
* When connected send starting data
*/
var tailer = tail(program.args, {buffer: program.number});
var filesSocket = io.of('/' + filesNamespace).on('connection', function (socket) {
socket.emit('options:lines', program.lines);
program.uiHideTopbar && socket.emit('options:hide-topbar');
!program.uiIndent && socket.emit('options:no-indent');
tailer.getBuffer().forEach(function (line) {
socket.emit('line', line);
});
return next(new Error('No cookie in header'), false);
});
}
/**
* Send incoming data
*/
tailer.on('line', function (line) {
filesSocket.emit('line', sanitizer(line).xss());
/**
* Setup UI highlights
*/
let highlightConfig;
if (program.uiHighlight) {
let presetPath;
if (!program.uiHighlightPreset) {
presetPath = path.join(__dirname, 'preset', 'default.json');
} else {
presetPath = path.resolve(untildify(program.uiHighlightPreset));
}
if (fs.existsSync(presetPath)) {
highlightConfig = JSON.parse(fs.readFileSync(presetPath));
} else {
throw new Error(`Preset file ${presetPath} doesn't exists`);
}
}
/**
* When connected send starting data
*/
const tailer = tail(program.args, {
buffer: program.number,
});
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', (line) => {
filesSocket.emit('line', line);
});
stats.track('runtime', 'started');
/**
* Handle signals
*/
const cleanExit = () => {
stats.timeEnd('runtime', 'runtime', () => {
process.exit();
});
};
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);

69
lib/daemonize.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const daemon = require('daemon-fix41');
const fs = require('fs');
const defaultOptions = {
doAuthorization: false,
doSecure: false,
};
module.exports = (script, params, opts) => {
const options = opts || defaultOptions;
const logFile = fs.openSync(params.logPath, 'a');
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.doSecure) {
args.push('-k', params.key, '-c', params.certificate);
}
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.uiHighlight) {
args.push('--ui-highlight');
}
if (params.uiHighlightPreset) {
args.push('--ui-highlight-preset', params.uiHighlightPreset);
}
if (params.disableUsageStats) {
args.push('--disable-usage-stats', params.disableUsageStats);
}
args = args.concat(params.args);
const proc = daemon.daemon(script, args, {
stdout: logFile,
stderr: logFile,
});
fs.writeFileSync(params.pidPath, proc.pid);
};

87
lib/options_parser.js Normal file
View File

@ -0,0 +1,87 @@
const program = require('commander');
program
.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,43 +1,72 @@
/* eslint no-underscore-dangle: off */
'use strict';
var EventEmitter = require('events').EventEmitter;
var spawn = require('child_process').spawn;
var util = require('util');
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);
this.buffer = [];
this.options = options || {buffer: 0};
const options = opts || {
buffer: 0,
};
this._buffer = new CBuffer(options.buffer);
var self = this;
var tail = spawn('tail', ['-F'].concat(path));
tail.stdout.on('data', function (data) {
let stream;
var lines = data.toString('utf-8');
lines = lines.split('\n');
lines.pop();
lines.forEach(function (line) {
if (self.options.buffer) {
if (self.buffer.length === self.options.buffer) {
self.buffer.shift();
}
self.buffer.push(line);
}
self.emit('line', line);
});
});
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';
}
process.on('exit', function () {
tail.kill();
});
};
util.inherits(Tail, EventEmitter);
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());
}
});
stream = cp.stdout;
Tail.prototype.getBuffer = function () {
return this.buffer;
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,
});
}
}
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 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,223 +0,0 @@
/*global Tinycon: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;
/**
* 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 = _newLinesCount + 1;
if (_newLinesCount > 99) {
Tinycon.setBubble(99);
} else {
Tinycon.setBubble(_newLinesCount);
}
};
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) {
if (e.keyCode === 27) { //esc
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('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';
p.innerHTML = data;
div.className = 'line';
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,38 +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="/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,27 +1,82 @@
{
"name": "frontail",
"version": "1.1.0",
"description": "tail -F output in 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": "0.9.16",
"connect": "2.11.0",
"validator": "1.5.0",
"daemon": "1.1.0",
"cookie": "0.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",
"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": {
"jshint": "~2.3.0",
"should": "~2.0.2",
"mocha": "~1.13.0",
"temp": "~0.6.0",
"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",
"jsdom": "~0.10.1"
"supertest": "^3.3.0",
"temp": "~0.8.1"
},
"scripts": {
"lint": "eslint .",
"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",
@ -29,11 +84,11 @@
},
"keywords": [
"tail",
"syslog"
"syslog",
"realtime",
"log",
"devops"
],
"engine": {
"node": ">= 0.8.0"
},
"main": "index",
"preferGlobal": true
}

8
preset/default.json Normal file
View File

@ -0,0 +1,8 @@
{
"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,105 +1,242 @@
'use strict';
require('should');
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 window, io;
describe('browser application', () => {
let io;
let window;
beforeEach(function (done) {
io = new EventEmitter();
jsdom.env(
'<title></title><body><div class="topbar"></div><div class="log"></div><input type="test" id="filter"/></body>',
['../lib/web/assets/app.js', './lib/jquery.js'],
function (errors, domWindow) {
window = domWindow;
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'),
});
}
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);
}
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;
it('should show lines from socket.io', function () {
initApp();
io.emit('line', 'test');
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>');
done();
},
});
});
it('should select line when clicked', function () {
initApp();
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 () {
initApp();
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 () {
initApp();
const line = window.document.querySelector('.line');
line.parentNode.innerHTML.should.equal(
'<div class="line" style="background: black"><p class="inner-line">line1</p></div>'
);
});
io.emit('options:lines', 2);
io.emit('line', 'line1');
io.emit('line', 'line2');
io.emit('line', 'line3');
it('should escape HTML', () => {
io.emit('line', '<a/>');
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');
});
const line = window.document.querySelector('.line');
line.innerHTML.should.equal('<p class="inner-line">&lt;a/&gt;</p>');
});
it('should hide topbar', function () {
initApp();
it('should work filter from URL', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
io.emit('options:hide-topbar');
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 topbar = window.document.querySelector('.topbar');
topbar.className.should.match(/hide/);
var body = window.document.querySelector('body');
body.className.should.match(/no-topbar/);
});
it('should clean filter', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
it('should not indent log lines', function () {
initApp();
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/');
});
io.emit('options:no-indent');
it('should change filter', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
var log = window.document.querySelector('.log');
log.className.should.match(/no-indent/);
});
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');
});
function initApp() {
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')
});
}
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');
function clickOnElement(line) {
var click = window.document.createEvent('MouseEvents');
click.initMouseEvent('click', true, true);
line.dispatchEvent(click);
}
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 <==');
});
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');
btn.className.should.not.containEql('play');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('line2');
});
});

View File

@ -1,108 +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();
request(app)
.get('/test')
.expect(200)
.expect('Content-Type', 'text/html', done);
});
it('should build app that serve index file', function (done) {
var app = connectBuilder().index(__dirname + '/fixtures/index', '/testfile').build();
it('should build app that replace index title', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
.build();
request(app)
.get('/')
.expect(200)
.expect('Content-Type', 'text/html', done);
});
request(app).get('/').expect('<head><title>/testfile</title></head>', done);
});
it('should build app that replace index title', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_title', '/testfile')
.build();
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();
request(app)
.get('/')
.expect('<head><title>/testfile</title></head>', done);
});
request(app).get('/').expect('ns', done);
});
it('builds app that sets socket.io namespace based on files', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_ns', '/testfile', 'ns', 'dark')
.build();
it('should build app that sets theme', (done) => {
const app = connectBuilder('/')
.index(
path.join(__dirname, '/fixtures/index_with_theme'),
'/testfile',
'ns',
'dark'
)
.build();
request(app)
.get('/')
.expect('ns', 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 theme', function (done) {
var app = connectBuilder()
.index(__dirname + '/fixtures/index_with_theme', '/testfile', 'ns', 'dark')
.build();
it('should build app that sets default theme', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile')
.build();
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', 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
);
});
});

267
test/daemonize.js Normal file
View File

@ -0,0 +1,267 @@
'use strict';
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', () => {
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');
});
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']);
});
it('with port', () => {
optionsParser.parse(['node', '/path/to/frontail', '-p', '80']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-p', 80]);
});
it('with lines number', () => {
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', () => {
optionsParser.parse(['node', '/path/to/frontail', '-l', '1']);
daemonize('script', optionsParser);
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,41 +1,60 @@
'use strict';
require('should');
var fs = require('fs');
var tail = require('../lib/tail');
var temp = require('temp');
const fs = require('fs');
const temp = require('temp');
const tail = require('../lib/tail');
describe('tail', function () {
temp.track();
const TEMP_FILE_PROFIX = '';
const SPAWN_DELAY = 10;
it('calls event line if new line appear in file', function (done) {
temp.open('', function (err, info) {
tail(info.path).on('line', function (line) {
line.should.equal('testline');
done();
});
function writeLines(fd, count) {
for (let i = 0; i < count; i += 1) {
fs.writeSync(
fd,
`line${i}
`
);
}
fs.closeSync(fd);
}
fs.writeSync(info.fd, 'testline\n');
fs.closeSync(info.fd);
});
describe('tail', () => {
temp.track();
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('', function (err, info) {
fs.writeSync(info.fd, 'testline1\n');
fs.writeSync(info.fd, 'testline2\n');
fs.writeSync(info.fd, 'testline3\n');
fs.closeSync(info.fd);
it('buffers lines on start', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
writeLines(info.fd, 20);
var calls = 0;
var tailer = tail(info.path, {buffer: 2}).on('line', function () {
calls += 1;
if (calls === 3) {
tailer.getBuffer().should.eql(['testline2', 'testline3']);
done();
}
});
});
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', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
writeLines(info.fd, 3);
const tailer = tail(info.path);
setTimeout(() => {
tailer.getBuffer().should.be.empty;
done();
}, SPAWN_DELAY);
});
});
});

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
}
}

327
web/assets/ansi_up.js Normal file
View File

@ -0,0 +1,327 @@
// ansi_up.js
// version : 1.3.0
// author : Dru Nelson
// license : MIT
// http://github.com/drudru/ansi_up
(function (Date, undefined) {
var ansi_up,
VERSION = "1.3.0",
// check for nodeJS
hasModule = (typeof module !== 'undefined'),
// Normal and then Bright
ANSI_COLORS = [
[
{ color: "0, 0, 0", 'class': "ansi-black" },
{ color: "187, 0, 0", 'class': "ansi-red" },
{ color: "0, 187, 0", 'class': "ansi-green" },
{ color: "187, 187, 0", 'class': "ansi-yellow" },
{ color: "0, 0, 187", 'class': "ansi-blue" },
{ color: "187, 0, 187", 'class': "ansi-magenta" },
{ color: "0, 187, 187", 'class': "ansi-cyan" },
{ color: "255,255,255", 'class': "ansi-white" }
],
[
{ color: "85, 85, 85", 'class': "ansi-bright-black" },
{ color: "255, 85, 85", 'class': "ansi-bright-red" },
{ color: "0, 255, 0", 'class': "ansi-bright-green" },
{ color: "255, 255, 85", 'class': "ansi-bright-yellow" },
{ color: "85, 85, 255", 'class': "ansi-bright-blue" },
{ color: "255, 85, 255", 'class': "ansi-bright-magenta" },
{ color: "85, 255, 255", 'class': "ansi-bright-cyan" },
{ color: "255, 255, 255", 'class': "ansi-bright-white" }
]
],
// 256 Colors Palette
PALETTE_COLORS;
function Ansi_Up() {
this.fg = this.bg = this.fg_truecolor = this.bg_truecolor = null;
this.bright = 0;
}
Ansi_Up.prototype.setup_palette = function() {
PALETTE_COLORS = [];
// Index 0..15 : System color
(function() {
var i, j;
for (i = 0; i < 2; ++i) {
for (j = 0; j < 8; ++j) {
PALETTE_COLORS.push(ANSI_COLORS[i][j]['color']);
}
}
})();
// Index 16..231 : RGB 6x6x6
// https://gist.github.com/jasonm23/2868981#file-xterm-256color-yaml
(function() {
var levels = [0, 95, 135, 175, 215, 255];
var format = function (r, g, b) { return levels[r] + ', ' + levels[g] + ', ' + levels[b] };
var r, g, b;
for (r = 0; r < 6; ++r) {
for (g = 0; g < 6; ++g) {
for (b = 0; b < 6; ++b) {
PALETTE_COLORS.push(format.call(this, r, g, b));
}
}
}
})();
// Index 232..255 : Grayscale
(function() {
var level = 8;
var format = function(level) { return level + ', ' + level + ', ' + level };
var i;
for (i = 0; i < 24; ++i, level += 10) {
PALETTE_COLORS.push(format.call(this, level));
}
})();
};
Ansi_Up.prototype.escape_for_html = function (txt) {
return txt.replace(/[&<>]/gm, function(str) {
if (str == "&") return "&amp;";
if (str == "<") return "&lt;";
if (str == ">") return "&gt;";
});
};
Ansi_Up.prototype.linkify = function (txt) {
return txt.replace(/(https?:\/\/[^\s]+)/gm, function(str) {
return "<a href=\"" + str + "\">" + str + "</a>";
});
};
Ansi_Up.prototype.ansi_to_html = function (txt, options) {
return this.process(txt, options, true);
};
Ansi_Up.prototype.ansi_to_text = function (txt) {
var options = {};
return this.process(txt, options, false);
};
Ansi_Up.prototype.process = function (txt, options, markup) {
var self = this;
var raw_text_chunks = txt.split(/\033\[/);
var first_chunk = raw_text_chunks.shift(); // the first chunk is not the result of the split
var color_chunks = raw_text_chunks.map(function (chunk) {
return self.process_chunk(chunk, options, markup);
});
color_chunks.unshift(first_chunk);
return color_chunks.join('');
};
Ansi_Up.prototype.process_chunk = function (text, options, markup) {
// Are we using classes or styles?
options = typeof options == 'undefined' ? {} : options;
var use_classes = typeof options.use_classes != 'undefined' && options.use_classes;
var key = use_classes ? 'class' : 'color';
// Each 'chunk' is the text after the CSI (ESC + '[') and before the next CSI/EOF.
//
// This regex matches four groups within a chunk.
//
// The first and third groups match code type.
// We supported only SGR command. It has empty first group and 'm' in third.
//
// The second group matches all of the number+semicolon command sequences
// before the 'm' (or other trailing) character.
// These are the graphics or SGR commands.
//
// The last group is the text (including newlines) that is colored by
// the other group's commands.
var matches = text.match(/^([!\x3c-\x3f]*)([\d;]*)([\x20-\x2c]*[\x40-\x7e])([\s\S]*)/m);
if (!matches) return text;
var orig_txt = matches[4];
var nums = matches[2].split(';');
// We currently support only "SGR" (Select Graphic Rendition)
// Simply ignore if not a SGR command.
if (matches[1] !== '' || matches[3] !== 'm') {
return orig_txt;
}
if (!markup) {
return orig_txt;
}
var self = this;
while (nums.length > 0) {
var num_str = nums.shift();
var num = parseInt(num_str);
if (isNaN(num) || num === 0) {
self.fg = self.bg = null;
self.bright = 0;
} else if (num === 1) {
self.bright = 1;
} else if (num == 39) {
self.fg = null;
} else if (num == 49) {
self.bg = null;
} else if ((num >= 30) && (num < 38)) {
self.fg = ANSI_COLORS[self.bright][(num % 10)][key];
} else if ((num >= 90) && (num < 98)) {
self.fg = ANSI_COLORS[1][(num % 10)][key];
} else if ((num >= 40) && (num < 48)) {
self.bg = ANSI_COLORS[0][(num % 10)][key];
} else if ((num >= 100) && (num < 108)) {
self.bg = ANSI_COLORS[1][(num % 10)][key];
} else if (num === 38 || num === 48) { // extend color (38=fg, 48=bg)
(function() {
var is_foreground = (num === 38);
if (nums.length >= 1) {
var mode = nums.shift();
if (mode === '5' && nums.length >= 1) { // palette color
var palette_index = parseInt(nums.shift());
if (palette_index >= 0 && palette_index <= 255) {
if (!use_classes) {
if (!PALETTE_COLORS) {
self.setup_palette.call(self);
}
if (is_foreground) {
self.fg = PALETTE_COLORS[palette_index];
} else {
self.bg = PALETTE_COLORS[palette_index];
}
} else {
var klass = (palette_index >= 16)
? ('ansi-palette-' + palette_index)
: ANSI_COLORS[palette_index > 7 ? 1 : 0][palette_index % 8]['class'];
if (is_foreground) {
self.fg = klass;
} else {
self.bg = klass;
}
}
}
} else if(mode === '2' && nums.length >= 3) { // true color
var r = parseInt(nums.shift());
var g = parseInt(nums.shift());
var b = parseInt(nums.shift());
if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
var color = r + ', ' + g + ', ' + b;
if (!use_classes) {
if (is_foreground) {
self.fg = color;
} else {
self.bg = color;
}
} else {
if (is_foreground) {
self.fg = 'ansi-truecolor';
self.fg_truecolor = color;
} else {
self.bg = 'ansi-truecolor';
self.bg_truecolor = color;
}
}
}
}
}
})();
}
}
if ((self.fg === null) && (self.bg === null)) {
return orig_txt;
} else {
var styles = [];
var classes = [];
var data = {};
var render_data = function (data) {
var fragments = [];
var key;
for (key in data) {
if (data.hasOwnProperty(key)) {
fragments.push('data-' + key + '="' + this.escape_for_html(data[key]) + '"');
}
}
return fragments.length > 0 ? ' ' + fragments.join(' ') : '';
};
if (self.fg) {
if (use_classes) {
classes.push(self.fg + "-fg");
if (self.fg_truecolor !== null) {
data['ansi-truecolor-fg'] = self.fg_truecolor;
self.fg_truecolor = null;
}
} else {
styles.push("color:rgb(" + self.fg + ")");
}
}
if (self.bg) {
if (use_classes) {
classes.push(self.bg + "-bg");
if (self.bg_truecolor !== null) {
data['ansi-truecolor-bg'] = self.bg_truecolor;
self.bg_truecolor = null;
}
} else {
styles.push("background-color:rgb(" + self.bg + ")");
}
}
if (use_classes) {
return '<span class="' + classes.join(' ') + '"' + render_data.call(self, data) + '>' + orig_txt + '</span>';
} else {
return '<span style="' + styles.join(';') + '"' + render_data.call(self, data) + '>' + orig_txt + '</span>';
}
}
};
// Module exports
ansi_up = {
escape_for_html: function (txt) {
var a2h = new Ansi_Up();
return a2h.escape_for_html(txt);
},
linkify: function (txt) {
var a2h = new Ansi_Up();
return a2h.linkify(txt);
},
ansi_to_html: function (txt, options) {
var a2h = new Ansi_Up();
return a2h.ansi_to_html(txt, options);
},
ansi_to_text: function (txt) {
var a2h = new Ansi_Up();
return a2h.ansi_to_text(txt);
},
ansi_to_html_obj: function () {
return new Ansi_Up();
}
};
// CommonJS module is defined
if (hasModule) {
module.exports = ansi_up;
}
/*global ender:false */
if (typeof window !== 'undefined' && typeof ender === 'undefined') {
window.ansi_up = ansi_up;
}
/*global define:false */
if (typeof define === "function" && define.amd) {
define("ansi_up", [], function () {
return ansi_up;
});
}
})(Date);

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>