mirror of https://github.com/mthenw/frontail.git
Compare commits
157 Commits
Author | SHA1 | Date |
---|---|---|
|
52014199f1 | |
|
18de86b061 | |
|
44ff51a618 | |
|
514c16e488 | |
|
6610661b26 | |
|
9d14ac51a8 | |
|
ab66d83efc | |
|
a5582dc34f | |
|
cd871ad361 | |
|
38022f7a0a | |
|
ea330b8ef9 | |
|
fb1da7bd18 | |
|
5dad99106e | |
|
94e2c46ee6 | |
|
0ceb864693 | |
|
39f8410a2a | |
|
6c4561a8d7 | |
|
d43976f3a5 | |
|
7f506ae5bf | |
|
0fcd6278fd | |
|
0aad9dd2dc | |
|
c12a1878de | |
|
25f3b23c86 | |
|
2b3a64dca2 | |
|
9e6e7ee5dc | |
|
1a9ce68c4a | |
|
06fd619a40 | |
|
b80fc8fd0c | |
|
34d5893694 | |
|
18c4603016 | |
|
a63e6dbd87 | |
|
0eac303527 | |
|
1357793a28 | |
|
7ff68040c3 | |
|
b31de76d33 | |
|
09acdc62af | |
|
3b390a0d54 | |
|
01afdc8cdd | |
|
4d5f29b209 | |
|
29d660623f | |
|
2150761fa2 | |
|
1cacc260f4 | |
|
f8de308820 | |
|
7c8c9cab98 | |
|
62c0d67370 | |
|
364f68b9c1 | |
|
bc8c1f8d00 | |
|
e6cc5f4dd1 | |
|
95d8cd5dcc | |
|
54101c8fad | |
|
5bf80592cc | |
|
f5c86cd3d7 | |
|
4f4693b623 | |
|
e9e16fa578 | |
|
a869307ecc | |
|
205cdbf696 | |
|
658a0914bf | |
|
b2a4f4b4af | |
|
da920b4c04 | |
|
955bf9d436 | |
|
57367a2b0d | |
|
26c3957457 | |
|
953c6b7dbe | |
|
8ea0f65760 | |
|
6c77313a6a | |
|
2cfff5caf7 | |
|
ef8c14bb9c | |
|
c35287ce7a | |
|
28de6d18dc | |
|
274e48731f | |
|
4a87fa6bdf | |
|
90d1428449 | |
|
75d31aafd6 | |
|
a99286fa0d | |
|
4687fa0698 | |
|
ab97b1bc36 | |
|
71f2114127 | |
|
6faeac0e06 | |
|
8547d3d241 | |
|
1edcfac40e | |
|
3d8b93b2d2 | |
|
751a2b9db7 | |
|
b1eada0b0e | |
|
6e758d8e34 | |
|
7e7f8e59e8 | |
|
fd735d87c0 | |
|
446d144b21 | |
|
54d293e9f5 | |
|
1fb8d5be97 | |
|
710551da7a | |
|
a6c98e46d8 | |
|
500c505137 | |
|
c8b02c6246 | |
|
c2f98d5554 | |
|
b19ea8ae1d | |
|
1e54a7786c | |
|
0623cd91aa | |
|
30e8eb04c3 | |
|
35f7e0e80a | |
|
9be6486c92 | |
|
6ad045f9e0 | |
|
0f0e8d5044 | |
|
baa9e3de7b | |
|
19b15685b9 | |
|
6cf8a3391f | |
|
87bfcff493 | |
|
6d3f7c2439 | |
|
98a3003902 | |
|
439194393f | |
|
200f8f8fbf | |
|
756cbee2a5 | |
|
bc8eb6cada | |
|
de39dc6ca1 | |
|
71dc8ceb83 | |
|
be1eefcd1e | |
|
2b6923a9b1 | |
|
fcd8ee906e | |
|
16b064c545 | |
|
9994060700 | |
|
ce7aadcd4e | |
|
d9748a8133 | |
|
8d78f2e7f5 | |
|
ece7dc9c2a | |
|
73a667c0d6 | |
|
31e25acce6 | |
|
432716822d | |
|
9dbe9b2f51 | |
|
fb6560e0e1 | |
|
7007f92715 | |
|
fc6128160c | |
|
b74dfc805e | |
|
70ef84434f | |
|
a304ac098b | |
|
ba362a4100 | |
|
5c582d4b2d | |
|
4949ca416d | |
|
837901debb | |
|
658ccb7a23 | |
|
a8f6942b5c | |
|
0c6068b30e | |
|
7c8f63d6c1 | |
|
c38de1be51 | |
|
b5865d5423 | |
|
3e1c620b24 | |
|
b025ff6dbd | |
|
5e96521809 | |
|
2ac11eb0ac | |
|
a7cd6897ad | |
|
2bff6b68de | |
|
3df000920c | |
|
054eb70c46 | |
|
3879a6eb79 | |
|
2bfb862258 | |
|
a1eef74c73 | |
|
a10c35018a | |
|
99c37d6d99 | |
|
95671f0164 |
|
@ -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
|
|
@ -1,3 +1,5 @@
|
|||
node_modules
|
||||
test/fixtures/*.pem
|
||||
npm-debug.log
|
||||
dist
|
||||
.DS_Store
|
||||
|
|
65
.jscsrc
65
.jscsrc
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"excludeFiles": ["node_modules/**", "lib/web/assets/tinycon.min.js"],
|
||||
|
||||
"requireCurlyBraces": [
|
||||
"if",
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"do",
|
||||
"try",
|
||||
"catch"
|
||||
],
|
||||
"requireSpaceAfterKeywords": [
|
||||
"if",
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"do",
|
||||
"switch",
|
||||
"case",
|
||||
"return",
|
||||
"try",
|
||||
"catch",
|
||||
"function",
|
||||
"typeof"
|
||||
],
|
||||
"requireSpaceBeforeBlockStatements": true,
|
||||
"requireParenthesesAroundIIFE": true,
|
||||
"requireSpacesInConditionalExpression": true,
|
||||
"disallowSpacesInNamedFunctionExpression": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"disallowSpacesInFunctionDeclaration": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"requireBlocksOnNewline": 1,
|
||||
"disallowEmptyBlocks": true,
|
||||
"disallowSpacesInsideArrayBrackets": true,
|
||||
"disallowSpacesInsideParentheses": true,
|
||||
"disallowQuotedKeysInObjects": true,
|
||||
"disallowSpaceAfterObjectKeys": true,
|
||||
"requireCommaBeforeLineBreak": true,
|
||||
"disallowSpaceAfterPrefixUnaryOperators": true,
|
||||
"disallowSpaceBeforePostfixUnaryOperators": true,
|
||||
"disallowSpaceBeforeBinaryOperators": [
|
||||
","
|
||||
],
|
||||
"requireSpaceBeforeBinaryOperators": true,
|
||||
"requireSpaceAfterBinaryOperators": true,
|
||||
"requireCamelCaseOrUpperCaseIdentifiers": true,
|
||||
"disallowKeywords": [ "with" ],
|
||||
"disallowMultipleLineBreaks": true,
|
||||
"validateQuoteMarks": "'",
|
||||
"validateIndentation": 4,
|
||||
"disallowMixedSpacesAndTabs": true,
|
||||
"disallowTrailingWhitespace": true,
|
||||
"disallowTrailingComma": true,
|
||||
"disallowKeywordsOnNewLine": [ "else" ],
|
||||
"requireLineFeedAtFileEnd": true,
|
||||
"requireCapitalizedConstructors": true,
|
||||
"requireDotNotation": true,
|
||||
"disallowYodaConditions": true,
|
||||
"disallowNewlineBeforeBlockStatements": true,
|
||||
"maximumLineLength": 120
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
node_modules
|
||||
lib/web/assets/tinycon.min.js
|
29
.jshintrc
29
.jshintrc
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"forin": true,
|
||||
"latedef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": true,
|
||||
"plusplus": true,
|
||||
"quotmark": "single",
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"expr": true,
|
||||
|
||||
"node": true,
|
||||
"browser": true,
|
||||
|
||||
"globals": {
|
||||
"io": true,
|
||||
"describe": false,
|
||||
"it": false,
|
||||
"beforeEach": false,
|
||||
"afterEach": false
|
||||
}
|
||||
}
|
18
.travis.yml
18
.travis.yml
|
@ -1,8 +1,16 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 0.10
|
||||
- 0.12
|
||||
- 6
|
||||
- 8
|
||||
- 10
|
||||
script:
|
||||
- npm run lint
|
||||
- npm run cs
|
||||
- npm test
|
||||
- npm run lint
|
||||
- npm test
|
||||
deploy:
|
||||
provider: npm
|
||||
email: maciej.winnicki@gmail.com
|
||||
api_key:
|
||||
secure: OuE8bOpsq/XN2AL6z0SWqHtpfKB5Qg3wug+hZVka+AdWaU6Gm0uvs4yA7B/uw9o5gH6KKgM+CP50Lq1xhC12CZbKLjkAUuFJQb9FrU/7jEAp+t+DKpoVBcru8bBjTxxx4wO1zXFcbJZ9zb4c4h76j2nfaNVuPTBB+mDF/HDd914=
|
||||
on:
|
||||
tags: true
|
||||
repo: mthenw/frontail
|
||||
|
|
|
@ -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"]
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
109
README.md
109
README.md
|
@ -1,25 +1,36 @@
|
|||
# frontail(1) – realtime log stream in the browser
|
||||
# frontail – streaming logs to the browser
|
||||
|
||||
```frontail``` is node.js application for serving `tail -F` output to browser.
|
||||
`frontail` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI.
|
||||
|
||||
[](https://travis-ci.org/mthenw/frontail)
|
||||
[](https://www.npmjs.org/package/frontail)
|
||||

|
||||
|
||||
[](https://hub.docker.com/r/mthenw/frontail/)
|
||||
|
||||
## Quick start
|
||||
|
||||
- `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
|
||||
- `frontail /var/log/syslog`
|
||||
- visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
|
||||
|
||||
## Features
|
||||
|
||||
* search (```Tab``` to focus, ```Esc``` to clear)
|
||||
* basic authentication
|
||||
* working over ssh
|
||||
* log rotation
|
||||
* auto-scrolling
|
||||
* marking logs
|
||||
* themes (default, dark)
|
||||
* number of unread logs in favicon
|
||||
* [highlighting](https://github.com/mthenw/frontail#highlighting)
|
||||
- log rotation (not on Windows)
|
||||
- auto-scrolling
|
||||
- marking logs
|
||||
- pausing logs
|
||||
- number of unread logs in favicon
|
||||
- themes (default, dark)
|
||||
- [highlighting](#highlighting)
|
||||
- search (`Tab` to focus, `Esc` to clear)
|
||||
- set filter from url parameter `filter`
|
||||
- tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
|
||||
- basic authentication
|
||||
|
||||
## Installation
|
||||
## Installation options
|
||||
|
||||
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
|
||||
|
||||
|
@ -27,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
|
||||
|
@ -41,19 +51,30 @@
|
|||
-c, --certificate <cert.pem> Certificate for HTTPS, option works only along with -k option
|
||||
--pid-path <path> if run as daemon file that will store the process id, default /var/run/frontail.pid
|
||||
--log-path <path> if run as daemon file that will be used as a log, default /dev/null
|
||||
--url-path <path> URL path for the browser application, default /
|
||||
--ui-hide-topbar hide topbar (log file name and search box)
|
||||
--ui-no-indent don't indent log lines
|
||||
--ui-highlight highlight words or lines if defined string found in logs, default preset ./preset/default.json
|
||||
--ui-highlight highlight words or lines if defined string found in logs, default preset
|
||||
--ui-highlight-preset <path> custom preset for highlighting (see ./preset/default.json)
|
||||
--remote-host <remote_host> setting the remote host for a tail over ssh (e.g: 192.168.0.2)
|
||||
--remote-user <remote_user> setting the remote user for a tail over ssh, default root
|
||||
--remote-port <remote_port> setting the remote port for a tail over ssh, default 22
|
||||
--path <path> prefix path for the running application, default /
|
||||
--disable-usage-stats disable gathering usage statistics
|
||||
--help output usage information
|
||||
|
||||
Web interface is on **http://localhost:[port]**.
|
||||
Web interface runs on **http://[host]:[port]**.
|
||||
|
||||
### Tailing multiple files
|
||||
|
||||
`[file ...]` accepts multiple paths, `*`, `?` and other shell special characters([Wildcards, Quotes, Back Quotes and Apostrophes in shell commands](http://www.codecoffee.com/tipsforlinux/articles/26-1.html)).
|
||||
|
||||
### stdin
|
||||
|
||||
Use `-` for streaming stdin:
|
||||
|
||||
./server | frontail -
|
||||
|
||||
### Highlighting
|
||||
|
||||
```--ui-highlight``` option turns on highlighting in UI. By default preset from ```./preset/defatult.json``` is used:
|
||||
`--ui-highlight` option turns on highlighting in UI. By default preset from `./preset/default.json` is used:
|
||||
|
||||
```
|
||||
{
|
||||
|
@ -66,10 +87,46 @@ Web interface is on **http://localhost:[port]**.
|
|||
}
|
||||
```
|
||||
|
||||
which means that every "err" string will be in red and every line with "err" will be bolded. Custom preset can be provided by
|
||||
which means that every "err" string will be in red and every line containing "err" will be bolded.
|
||||
|
||||
*New presets are very welcome. If you don't like default or you would like to share your, please create PR with json file.*
|
||||
_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._
|
||||
|
||||
## Screenshot
|
||||
Available presets:
|
||||
|
||||

|
||||
- 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.
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
exec ./bin/frontail $@
|
235
index.js
235
index.js
|
@ -1,126 +1,171 @@
|
|||
'use strict';
|
||||
|
||||
var connect = require('connect');
|
||||
var cookieParser = require('cookie');
|
||||
var crypto = require('crypto');
|
||||
var path = require('path');
|
||||
var sanitizer = require('validator').sanitize;
|
||||
var socketio = require('socket.io');
|
||||
var tail = require('./lib/tail');
|
||||
var connectBuilder = require('./lib/connect_builder');
|
||||
var program = require('./lib/options_parser');
|
||||
var serverBuilder = require('./lib/server_builder');
|
||||
var daemonize = require('./lib/daemonize');
|
||||
const cookie = require('cookie');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const { Server } = require('socket.io');
|
||||
const fs = require('fs');
|
||||
const untildify = require('untildify');
|
||||
const tail = require('./lib/tail');
|
||||
const connectBuilder = require('./lib/connect_builder');
|
||||
const program = require('./lib/options_parser');
|
||||
const serverBuilder = require('./lib/server_builder');
|
||||
const daemonize = require('./lib/daemonize');
|
||||
const usageStats = require('./lib/stats');
|
||||
|
||||
/**
|
||||
* Parse args
|
||||
*/
|
||||
program.parse(process.argv);
|
||||
if (program.args.length === 0) {
|
||||
console.error('Arguments needed, use --help');
|
||||
process.exit();
|
||||
console.error('Arguments needed, use --help');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init usage statistics
|
||||
*/
|
||||
const stats = usageStats(!program.disableUsageStats, program);
|
||||
stats.track('runtime', 'init');
|
||||
stats.time('runtime', 'runtime');
|
||||
|
||||
/**
|
||||
* Validate params
|
||||
*/
|
||||
var doAuthorization = !!(program.user && program.password);
|
||||
var doSecure = !!(program.key && program.certificate);
|
||||
var doSSH = !!program.remoteHost;
|
||||
var sessionSecret = String(+new Date()) + Math.random();
|
||||
var sessionKey = 'sid';
|
||||
var files = program.args.join(' ');
|
||||
var filesNamespace = crypto.createHash('md5').update(files).digest('hex');
|
||||
const doAuthorization = !!(program.user && program.password);
|
||||
const doSecure = !!(program.key && program.certificate);
|
||||
const sessionSecret = String(+new Date()) + Math.random();
|
||||
const files = program.args.join(' ');
|
||||
const filesNamespace = crypto.createHash('md5').update(files).digest('hex');
|
||||
const urlPath = program.urlPath.replace(/\/$/, ''); // remove trailing slash
|
||||
|
||||
if (program.daemonize) {
|
||||
daemonize(__filename, program, {
|
||||
doAuthorization: doAuthorization,
|
||||
doSecure: doSecure,
|
||||
doSSH: doSSH
|
||||
});
|
||||
daemonize(__filename, program, {
|
||||
doAuthorization,
|
||||
doSecure,
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* HTTP(s) server setup
|
||||
*/
|
||||
var appBuilder = connectBuilder();
|
||||
if (doAuthorization) {
|
||||
appBuilder.session(sessionSecret, sessionKey);
|
||||
appBuilder.authorize(program.user, program.password);
|
||||
}
|
||||
appBuilder
|
||||
.static(__dirname + '/lib/web/assets')
|
||||
.index(__dirname + '/lib/web/index.html', files, filesNamespace, program.theme);
|
||||
/**
|
||||
* HTTP(s) server setup
|
||||
*/
|
||||
const appBuilder = connectBuilder(urlPath);
|
||||
if (doAuthorization) {
|
||||
appBuilder.session(sessionSecret);
|
||||
appBuilder.authorize(program.user, program.password);
|
||||
}
|
||||
appBuilder
|
||||
.static(path.join(__dirname, 'web', 'assets'))
|
||||
.index(
|
||||
path.join(__dirname, 'web', 'index.html'),
|
||||
files,
|
||||
filesNamespace,
|
||||
program.theme
|
||||
);
|
||||
|
||||
var builder = serverBuilder();
|
||||
if (doSecure) {
|
||||
builder.secure(program.key, program.certificate);
|
||||
}
|
||||
var server = builder
|
||||
.use(appBuilder.build())
|
||||
.port(program.port)
|
||||
.host(program.host)
|
||||
.build();
|
||||
const builder = serverBuilder();
|
||||
if (doSecure) {
|
||||
builder.secure(program.key, program.certificate);
|
||||
}
|
||||
const server = builder
|
||||
.use(appBuilder.build())
|
||||
.port(program.port)
|
||||
.host(program.host)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* socket.io setup
|
||||
*/
|
||||
var io = socketio.listen(server, {log: false});
|
||||
/**
|
||||
* socket.io setup
|
||||
*/
|
||||
const io = new Server({ path: `${urlPath}/socket.io` });
|
||||
io.attach(server);
|
||||
|
||||
if (doAuthorization) {
|
||||
io.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup UI highlights
|
||||
*/
|
||||
var highlightConfig;
|
||||
if (program.uiHighlight) {
|
||||
highlightConfig = require(path.resolve(program.uiHighlightPreset));
|
||||
}
|
||||
return next(new Error('No cookie in header'), false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When connected send starting data
|
||||
*/
|
||||
var tailer;
|
||||
if (doSSH) {
|
||||
var sshOptions = {
|
||||
remoteHost: program.remoteHost,
|
||||
remoteUser: program.remoteUser,
|
||||
remotePort: program.remotePort
|
||||
};
|
||||
/**
|
||||
* Setup UI highlights
|
||||
*/
|
||||
let highlightConfig;
|
||||
if (program.uiHighlight) {
|
||||
let presetPath;
|
||||
|
||||
tailer = tail(program.args, {buffer: program.number, ssh: sshOptions});
|
||||
if (!program.uiHighlightPreset) {
|
||||
presetPath = path.join(__dirname, 'preset', 'default.json');
|
||||
} else {
|
||||
tailer = tail(program.args, {buffer: program.number});
|
||||
presetPath = path.resolve(untildify(program.uiHighlightPreset));
|
||||
}
|
||||
|
||||
var filesSocket = io.of('/' + filesNamespace).on('connection', function (socket) {
|
||||
socket.emit('options:lines', program.lines);
|
||||
if (fs.existsSync(presetPath)) {
|
||||
highlightConfig = JSON.parse(fs.readFileSync(presetPath));
|
||||
} else {
|
||||
throw new Error(`Preset file ${presetPath} doesn't exists`);
|
||||
}
|
||||
}
|
||||
|
||||
program.uiHideTopbar && socket.emit('options:hide-topbar');
|
||||
!program.uiIndent && socket.emit('options:no-indent');
|
||||
program.uiHighlight && socket.emit('options:highlightConfig', highlightConfig);
|
||||
/**
|
||||
* When connected send starting data
|
||||
*/
|
||||
const tailer = tail(program.args, {
|
||||
buffer: program.number,
|
||||
});
|
||||
|
||||
tailer.getBuffer().forEach(function (line) {
|
||||
socket.emit('line', line);
|
||||
});
|
||||
const filesSocket = io.of(`/${filesNamespace}`).on('connection', (socket) => {
|
||||
socket.emit('options:lines', program.lines);
|
||||
|
||||
if (program.uiHideTopbar) {
|
||||
socket.emit('options:hide-topbar');
|
||||
}
|
||||
|
||||
if (!program.uiIndent) {
|
||||
socket.emit('options:no-indent');
|
||||
}
|
||||
|
||||
if (program.uiHighlight) {
|
||||
socket.emit('options:highlightConfig', highlightConfig);
|
||||
}
|
||||
|
||||
tailer.getBuffer().forEach((line) => {
|
||||
socket.emit('line', line);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Send incoming data
|
||||
*/
|
||||
tailer.on('line', function (line) {
|
||||
filesSocket.emit('line', sanitizer(line).xss());
|
||||
/**
|
||||
* Send incoming data
|
||||
*/
|
||||
tailer.on('line', (line) => {
|
||||
filesSocket.emit('line', line);
|
||||
});
|
||||
|
||||
stats.track('runtime', 'started');
|
||||
|
||||
/**
|
||||
* Handle signals
|
||||
*/
|
||||
const cleanExit = () => {
|
||||
stats.timeEnd('runtime', 'runtime', () => {
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
process.on('SIGINT', cleanExit);
|
||||
process.on('SIGTERM', cleanExit);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
101
lib/daemonize.js
101
lib/daemonize.js
|
@ -1,66 +1,69 @@
|
|||
'use strict';
|
||||
|
||||
var daemon = require('daemon');
|
||||
var fs = require('fs');
|
||||
const daemon = require('daemon-fix41');
|
||||
const fs = require('fs');
|
||||
|
||||
var defaultOptions = {
|
||||
doAuthorization: false,
|
||||
doSecure: false
|
||||
const defaultOptions = {
|
||||
doAuthorization: false,
|
||||
doSecure: false,
|
||||
};
|
||||
|
||||
module.exports = function (script, params, options) {
|
||||
options = options || defaultOptions;
|
||||
module.exports = (script, params, opts) => {
|
||||
const options = opts || defaultOptions;
|
||||
|
||||
var logFile = fs.openSync(params.logPath, 'a');
|
||||
const logFile = fs.openSync(params.logPath, 'a');
|
||||
|
||||
var args = [
|
||||
'-h', params.host,
|
||||
'-p', params.port,
|
||||
'-n', params.number,
|
||||
'-l', params.lines,
|
||||
'-t', params.theme
|
||||
];
|
||||
let args = [
|
||||
'-h',
|
||||
params.host,
|
||||
'-p',
|
||||
params.port,
|
||||
'-n',
|
||||
params.number,
|
||||
'-l',
|
||||
params.lines,
|
||||
'-t',
|
||||
params.theme,
|
||||
];
|
||||
|
||||
if (options.doAuthorization) {
|
||||
args.push(
|
||||
'-U', params.user,
|
||||
'-P', params.password
|
||||
);
|
||||
}
|
||||
if (options.doAuthorization) {
|
||||
args.push('-U', params.user, '-P', params.password);
|
||||
}
|
||||
|
||||
if (options.doSecure) {
|
||||
args.push(
|
||||
'-k', params.key,
|
||||
'-c', params.certificate
|
||||
);
|
||||
}
|
||||
if (options.doSecure) {
|
||||
args.push('-k', params.key, '-c', params.certificate);
|
||||
}
|
||||
|
||||
if (options.doSSH) {
|
||||
args.push(
|
||||
'--remote-host', params.remoteHost,
|
||||
'--remote-user', params.remoteUser,
|
||||
'--remote-port', params.remotePort
|
||||
);
|
||||
}
|
||||
if (params.uiHideTopbar) {
|
||||
args.push('--ui-hide-topbar');
|
||||
}
|
||||
|
||||
if (params.uiHideTopbar) {
|
||||
args.push('--ui-hide-topbar');
|
||||
}
|
||||
if (params.urlPath) {
|
||||
args.push('--url-path', params.urlPath);
|
||||
}
|
||||
|
||||
if (!params.uiIndent) {
|
||||
args.push('--ui-no-indent');
|
||||
}
|
||||
if (!params.uiIndent) {
|
||||
args.push('--ui-no-indent');
|
||||
}
|
||||
|
||||
if (params.uiHighlight) {
|
||||
args.push('--ui-highlight', '--ui-highlight-preset', params.uiHighlightPreset);
|
||||
}
|
||||
if (params.uiHighlight) {
|
||||
args.push('--ui-highlight');
|
||||
}
|
||||
|
||||
args = args.concat(params.args);
|
||||
if (params.uiHighlightPreset) {
|
||||
args.push('--ui-highlight-preset', params.uiHighlightPreset);
|
||||
}
|
||||
|
||||
var proc = daemon.daemon(script, args, {
|
||||
stdout: logFile,
|
||||
stderr: logFile
|
||||
});
|
||||
if (params.disableUsageStats) {
|
||||
args.push('--disable-usage-stats', params.disableUsageStats);
|
||||
}
|
||||
|
||||
fs.writeFileSync(params.pidPath, proc.pid);
|
||||
args = args.concat(params.args);
|
||||
|
||||
const proc = daemon.daemon(script, args, {
|
||||
stdout: logFile,
|
||||
stderr: logFile,
|
||||
});
|
||||
|
||||
fs.writeFileSync(params.pidPath, proc.pid);
|
||||
};
|
||||
|
|
|
@ -1,37 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
var program = require('commander');
|
||||
const program = require('commander');
|
||||
|
||||
program
|
||||
.version(require('../package.json').version)
|
||||
.usage('[options] [file ...]')
|
||||
.option('--remote-host <remote_host>', 'setting the remote host for a tail over ssh (e.g: 192.168.0.2)', String,
|
||||
null)
|
||||
.option('--remote-user <remote_user>', 'setting the remote user for a tail over ssh, default root', String, 'root')
|
||||
.option('--remote-port <remote_port>', 'setting the remote port for a tail over ssh, default 22', Number, 22)
|
||||
.option('-h, --host <host>', 'listening host, default 0.0.0.0', String, '0.0.0.0')
|
||||
.option('-p, --port <port>', 'listening port, default 9001', Number, 9001)
|
||||
.option('-n, --number <number>', 'starting lines number, default 10', Number, 10)
|
||||
.option('-l, --lines <lines>', 'number on lines stored in browser, default 2000', Number, 2000)
|
||||
.option('-t, --theme <theme>', 'name of the theme (default, dark)', String, 'default')
|
||||
.option('-d, --daemonize', 'run as daemon')
|
||||
.option('-U, --user <username>', 'Basic Authentication username, option works only along with -P option',
|
||||
String, false)
|
||||
.option('-P, --password <password>', 'Basic Authentication password, option works only along with -U option',
|
||||
String, false)
|
||||
.option('-k, --key <key.pem>', 'Private Key for HTTPS, option works only along with -c option',
|
||||
String, false)
|
||||
.option('-c, --certificate <cert.pem>', 'Certificate for HTTPS, option works only along with -k option',
|
||||
String, false)
|
||||
.option('--pid-path <path>', 'if run as daemon file that will store the process id, default /var/run/frontail.pid',
|
||||
String, '/var/run/frontail.pid')
|
||||
.option('--log-path <path>', 'if run as daemon file that will be used as a log, default /dev/null',
|
||||
String, '/dev/null')
|
||||
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
|
||||
.option('--ui-no-indent', 'don\'t indent log lines')
|
||||
.option('--ui-highlight',
|
||||
'highlight words or lines if defined string found in logs, default preset ./preset/default.json')
|
||||
.option('--ui-highlight-preset <path>', 'custom preset for highlighting (see ./preset/default.json)', String,
|
||||
'./preset/default.json');
|
||||
.version(require('../package.json').version)
|
||||
.usage('[options] [file ...]')
|
||||
.helpOption('--help')
|
||||
.option(
|
||||
'-h, --host <host>',
|
||||
'listening host, default 0.0.0.0',
|
||||
String,
|
||||
'0.0.0.0'
|
||||
)
|
||||
.option('-p, --port <port>', 'listening port, default 9001', Number, 9001)
|
||||
.option(
|
||||
'-n, --number <number>',
|
||||
'starting lines number, default 10',
|
||||
Number,
|
||||
10
|
||||
)
|
||||
.option(
|
||||
'-l, --lines <lines>',
|
||||
'number on lines stored in browser, default 2000',
|
||||
Number,
|
||||
2000
|
||||
)
|
||||
.option(
|
||||
'-t, --theme <theme>',
|
||||
'name of the theme (default, dark)',
|
||||
String,
|
||||
'default'
|
||||
)
|
||||
.option('-d, --daemonize', 'run as daemon')
|
||||
.option(
|
||||
'-U, --user <username>',
|
||||
'Basic Authentication username, option works only along with -P option',
|
||||
String,
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'-P, --password <password>',
|
||||
'Basic Authentication password, option works only along with -U option',
|
||||
String,
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'-k, --key <key.pem>',
|
||||
'Private Key for HTTPS, option works only along with -c option',
|
||||
String,
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'-c, --certificate <cert.pem>',
|
||||
'Certificate for HTTPS, option works only along with -k option',
|
||||
String,
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'--pid-path <path>',
|
||||
'if run as daemon file that will store the process id, default /var/run/frontail.pid',
|
||||
String,
|
||||
'/var/run/frontail.pid'
|
||||
)
|
||||
.option(
|
||||
'--log-path <path>',
|
||||
'if run as daemon file that will be used as a log, default /dev/null',
|
||||
String,
|
||||
'/dev/null'
|
||||
)
|
||||
.option(
|
||||
'--url-path <path>',
|
||||
'URL path for the browser application, default /',
|
||||
String,
|
||||
'/'
|
||||
)
|
||||
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
|
||||
.option('--ui-no-indent', "don't indent log lines")
|
||||
.option(
|
||||
'--ui-highlight',
|
||||
'highlight words or lines if defined string found in logs, default preset'
|
||||
)
|
||||
.option(
|
||||
'--ui-highlight-preset <path>',
|
||||
'custom preset for highlighting (see ./preset/default.json)'
|
||||
)
|
||||
.option('--disable-usage-stats', 'disable gathering usage statistics');
|
||||
|
||||
module.exports = program;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
102
lib/tail.js
102
lib/tail.js
|
@ -1,54 +1,72 @@
|
|||
/* eslint no-underscore-dangle: off */
|
||||
|
||||
'use strict';
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var childProcess = require('child_process');
|
||||
var util = require('util');
|
||||
var CBuffer = require('CBuffer');
|
||||
const events = require('events');
|
||||
const childProcess = require('child_process');
|
||||
const tailStream = require('fs-tail-stream');
|
||||
const util = require('util');
|
||||
const CBuffer = require('CBuffer');
|
||||
const byline = require('byline');
|
||||
const commandExistsSync = require('command-exists').sync;
|
||||
|
||||
var Tail = function (path, options) {
|
||||
EventEmitter.call(this);
|
||||
function Tail(path, opts) {
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
options = options || {buffer: 0};
|
||||
this._buffer = new CBuffer(options.buffer);
|
||||
var tail;
|
||||
const options = opts || {
|
||||
buffer: 0,
|
||||
};
|
||||
this._buffer = new CBuffer(options.buffer);
|
||||
|
||||
if (options.ssh) {
|
||||
var args = [
|
||||
options.ssh.remoteUser + '@' + options.ssh.remoteHost,
|
||||
'-p', options.ssh.remotePort,
|
||||
'tail -f'
|
||||
].concat(path);
|
||||
let stream;
|
||||
|
||||
tail = childProcess.spawn('ssh', args);
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
process.on('exit', () => {
|
||||
cp.kill();
|
||||
});
|
||||
} else {
|
||||
tail = childProcess.spawn('tail', ['-n', options.buffer, '-F'].concat(path));
|
||||
/* This is used if the os does not support the `tail`command. */
|
||||
stream = tailStream.createReadStream(path.join(), {
|
||||
encoding: 'utf8',
|
||||
start: options.buffer,
|
||||
tail: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tail.stderr.on('data', function (data) {
|
||||
//if there is any error then display it in the console and then kill the tail.
|
||||
console.error(data.toString());
|
||||
process.exit();
|
||||
});
|
||||
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.stdout.on('data', function (data) {
|
||||
var lines = data.toString('utf-8').split('\n');
|
||||
lines.pop();
|
||||
lines.forEach(function (line) {
|
||||
this._buffer.push(line);
|
||||
this.emit('line', line);
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
|
||||
process.on('exit', function () {
|
||||
tail.kill();
|
||||
});
|
||||
};
|
||||
util.inherits(Tail, EventEmitter);
|
||||
|
||||
Tail.prototype.getBuffer = function () {
|
||||
return this._buffer.toArray();
|
||||
Tail.prototype.getBuffer = function getBuffer() {
|
||||
return this._buffer.toArray();
|
||||
};
|
||||
|
||||
module.exports = function (path, options) {
|
||||
return new Tail(path, options);
|
||||
};
|
||||
module.exports = (path, options) => new Tail(path, options);
|
||||
|
|
|
@ -1,273 +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;
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
* @private
|
||||
*/
|
||||
var _highlightConfig;
|
||||
|
||||
/**
|
||||
* Hide element if doesn't contain filter value
|
||||
*
|
||||
* @param {Object} element
|
||||
* @private
|
||||
*/
|
||||
var _filterElement = function (element) {
|
||||
var pattern = new RegExp(_filterValue, 'i');
|
||||
if (pattern.test(element.textContent)) {
|
||||
element.style.display = '';
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter logs based on _filterValue
|
||||
*
|
||||
* @function
|
||||
* @private
|
||||
*/
|
||||
var _filterLogs = function () {
|
||||
var collection = _logContainer.childNodes;
|
||||
var i = collection.length;
|
||||
|
||||
if (i === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (i) {
|
||||
_filterElement(collection[i - 1]);
|
||||
i -= 1;
|
||||
}
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {Boolean}
|
||||
* @private
|
||||
*/
|
||||
var _isScrolledBottom = function () {
|
||||
var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
var totalHeight = document.body.offsetHeight;
|
||||
var clientHeight = document.documentElement.clientHeight;
|
||||
return totalHeight <= currentScroll + clientHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @private
|
||||
*/
|
||||
var _faviconReset = function () {
|
||||
_newLinesCount = 0;
|
||||
Tinycon.setBubble(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @private
|
||||
*/
|
||||
var _updateFaviconCounter = function () {
|
||||
if (_isWindowFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
_newLinesCount = _newLinesCount + 1;
|
||||
|
||||
if (_newLinesCount > 99) {
|
||||
Tinycon.setBubble(99);
|
||||
} else {
|
||||
Tinycon.setBubble(_newLinesCount);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return String
|
||||
* @private
|
||||
*/
|
||||
var _highlightWord = function (line) {
|
||||
if (_highlightConfig) {
|
||||
if (_highlightConfig.words) {
|
||||
for (var wordCheck in _highlightConfig.words) {
|
||||
if (_highlightConfig.words.hasOwnProperty(wordCheck)) {
|
||||
line = line.replace(
|
||||
wordCheck,
|
||||
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return HTMLElement
|
||||
* @private
|
||||
*/
|
||||
var _highlightLine = function (line, container) {
|
||||
if (_highlightConfig) {
|
||||
if (_highlightConfig.lines) {
|
||||
for (var lineCheck in _highlightConfig.lines) {
|
||||
if (line.indexOf(lineCheck) !== -1) {
|
||||
container.setAttribute('style', _highlightConfig.lines[lineCheck]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Init socket.io communication and log container
|
||||
*
|
||||
* @param {Object} opts options
|
||||
*/
|
||||
init: function (opts) {
|
||||
var self = this;
|
||||
|
||||
// Elements
|
||||
_logContainer = opts.container;
|
||||
_filterInput = opts.filterInput;
|
||||
_filterInput.focus();
|
||||
_topbar = opts.topbar;
|
||||
_body = opts.body;
|
||||
|
||||
// Filter input bind
|
||||
_filterInput.addEventListener('keyup', function (e) {
|
||||
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('options:highlightConfig', function (highlightConfig) {
|
||||
_highlightConfig = highlightConfig;
|
||||
})
|
||||
.on('line', function (line) {
|
||||
self.log(line);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Log data
|
||||
*
|
||||
* @param {string} data data to log
|
||||
*/
|
||||
log: function (data) {
|
||||
var wasScrolledBottom = _isScrolledBottom();
|
||||
var div = document.createElement('div');
|
||||
var p = document.createElement('p');
|
||||
p.className = 'inner-line';
|
||||
|
||||
p.innerHTML = _highlightWord(data);
|
||||
|
||||
div.className = 'line';
|
||||
div = _highlightLine(data, div);
|
||||
div.addEventListener('click', function () {
|
||||
if (this.className.indexOf('selected') === -1) {
|
||||
this.className = 'line-selected';
|
||||
} else {
|
||||
this.className = 'line';
|
||||
}
|
||||
});
|
||||
|
||||
div.appendChild(p);
|
||||
_filterElement(div);
|
||||
_logContainer.appendChild(div);
|
||||
|
||||
if (_logContainer.children.length > _linesLimit) {
|
||||
_logContainer.removeChild(_logContainer.children[0]);
|
||||
}
|
||||
|
||||
if (wasScrolledBottom) {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
_updateFaviconCounter();
|
||||
}
|
||||
};
|
||||
})(window, document);
|
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
83
package.json
83
package.json
|
@ -1,34 +1,82 @@
|
|||
{
|
||||
"name": "frontail",
|
||||
"version": "2.1.0",
|
||||
"description": "realtime log stream in the browser",
|
||||
"version": "4.9.2",
|
||||
"description": "streaming logs to the browser",
|
||||
"homepage": "https://github.com/mthenw/frontail",
|
||||
"author": "Maciej Winnicki <maciej.winnicki@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bin": "./bin/frontail",
|
||||
"dependencies": {
|
||||
"commander": "1.3.2",
|
||||
"socket.io": "0.9.16",
|
||||
"connect": "2.11.0",
|
||||
"validator": "1.5.0",
|
||||
"daemon": "1.1.0",
|
||||
"CBuffer": "0.1.4",
|
||||
"basic-auth-connect": "1.0.0",
|
||||
"byline": "5.0.0",
|
||||
"command-exists": "1.2.8",
|
||||
"commander": "3.0.1",
|
||||
"configstore": "4.0.0",
|
||||
"connect": "3.7.0",
|
||||
"cookie": "0.1.0",
|
||||
"CBuffer": "0.1.4"
|
||||
"cookie-parser": "1.4.5",
|
||||
"daemon-fix41": "1.1.2",
|
||||
"express-session": "1.15.6",
|
||||
"fs-tail-stream": "1.1.0",
|
||||
"is-docker": "1.1.0",
|
||||
"serve-static": "1.14.1",
|
||||
"socket.io": "3.1.2",
|
||||
"universal-analytics": "0.4.23",
|
||||
"untildify": "3.0.2",
|
||||
"uuid": "3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jscs": "^1.6.2",
|
||||
"jsdom": "^3.1.0",
|
||||
"jshint": "~2.3.0",
|
||||
"mocha": "~2.2.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",
|
||||
"supertest": "^3.3.0",
|
||||
"temp": "~0.8.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "./node_modules/.bin/jshint .",
|
||||
"cs": "./node_modules/.bin/jscs .",
|
||||
"test": "./node_modules/.bin/mocha --reporter spec test/*.js"
|
||||
"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",
|
||||
|
@ -41,9 +89,6 @@
|
|||
"log",
|
||||
"devops"
|
||||
],
|
||||
"engine": {
|
||||
"node": ">= 0.8.0"
|
||||
},
|
||||
"main": "index",
|
||||
"preferGlobal": true
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"words": {
|
||||
"err": "color: red;"
|
||||
},
|
||||
"lines": {
|
||||
"err": "font-weight: bold;"
|
||||
}
|
||||
"words": {
|
||||
"err": "color: red;"
|
||||
},
|
||||
"lines": {
|
||||
"err": "font-weight: bold;"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"no-unused-expressions": "off"
|
||||
},
|
||||
"env": {
|
||||
"mocha": true
|
||||
}
|
||||
}
|
302
test/app.js
302
test/app.js
|
@ -1,116 +1,242 @@
|
|||
'use strict';
|
||||
|
||||
require('should');
|
||||
var fs = require('fs');
|
||||
var jsdom = require('jsdom');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
const fs = require('fs');
|
||||
const jsdom = require('jsdom/lib/old-api.js');
|
||||
const events = require('events');
|
||||
|
||||
describe('browser application', function () {
|
||||
var window, io;
|
||||
describe('browser application', () => {
|
||||
let io;
|
||||
let window;
|
||||
|
||||
beforeEach(function (done) {
|
||||
io = new EventEmitter();
|
||||
var html = '<title></title><body><div class="topbar"></div>' +
|
||||
'<div class="log"></div><input type="test" id="filter"/></body>';
|
||||
var src = fs.readFileSync('./lib/web/assets/app.js', 'utf-8');
|
||||
|
||||
jsdom.env({ html: html, src: src, loaded: function (errors, domWindow) {
|
||||
window = domWindow;
|
||||
|
||||
initApp();
|
||||
done();
|
||||
}});
|
||||
function initApp() {
|
||||
window.App.init({
|
||||
socket: io,
|
||||
container: window.document.querySelector('.log'),
|
||||
filterInput: window.document.querySelector('#filter'),
|
||||
pauseBtn: window.document.querySelector('#pauseBtn'),
|
||||
topbar: window.document.querySelector('.topbar'),
|
||||
body: window.document.querySelector('body'),
|
||||
});
|
||||
}
|
||||
|
||||
it('should show lines from socket.io', function () {
|
||||
io.emit('line', 'test');
|
||||
function clickOnElement(line) {
|
||||
const click = window.document.createEvent('MouseEvents');
|
||||
click.initMouseEvent(
|
||||
'click',
|
||||
true,
|
||||
true,
|
||||
window,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
null
|
||||
);
|
||||
line.dispatchEvent(click);
|
||||
}
|
||||
|
||||
var log = window.document.querySelector('.log');
|
||||
log.childNodes.length.should.be.equal(1);
|
||||
log.childNodes[0].textContent.should.be.equal('test');
|
||||
log.childNodes[0].className.should.be.equal('line');
|
||||
log.childNodes[0].tagName.should.be.equal('DIV');
|
||||
log.childNodes[0].innerHTML.should.be.equal('<p class="inner-line">test</p>');
|
||||
beforeEach((done) => {
|
||||
io = new events.EventEmitter();
|
||||
const html =
|
||||
'<title></title><body><div class="topbar"></div>' +
|
||||
'<div class="log"></div><button type="button" id="pauseBtn"></button>' +
|
||||
'<input type="test" id="filter"/></body>';
|
||||
const ansiup = fs.readFileSync('./web/assets/ansi_up.js', 'utf-8');
|
||||
const src = fs.readFileSync('./web/assets/app.js', 'utf-8');
|
||||
|
||||
jsdom.env({
|
||||
html,
|
||||
url: 'http://localhost?filter=line.*',
|
||||
src: [ansiup, src],
|
||||
onload: (domWindow) => {
|
||||
window = domWindow;
|
||||
|
||||
initApp();
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should select line when clicked', function () {
|
||||
io.emit('line', 'test');
|
||||
it('should show lines from socket.io', () => {
|
||||
io.emit('line', 'test');
|
||||
|
||||
var line = window.document.querySelector('.line');
|
||||
clickOnElement(line);
|
||||
const log = window.document.querySelector('.log');
|
||||
log.childNodes.length.should.be.equal(1);
|
||||
log.childNodes[0].textContent.should.be.equal('test');
|
||||
log.childNodes[0].className.should.be.equal('line');
|
||||
log.childNodes[0].tagName.should.be.equal('DIV');
|
||||
log.childNodes[0].innerHTML.should.be.equal(
|
||||
'<p class="inner-line">test</p>'
|
||||
);
|
||||
});
|
||||
|
||||
line.className.should.be.equal('line-selected');
|
||||
it('should select line when clicked', () => {
|
||||
io.emit('line', 'test');
|
||||
|
||||
const line = window.document.querySelector('.line');
|
||||
clickOnElement(line);
|
||||
|
||||
line.className.should.be.equal('line-selected');
|
||||
});
|
||||
|
||||
it('should deselect line when selected line clicked', () => {
|
||||
io.emit('line', 'test');
|
||||
|
||||
const line = window.document.querySelector('.line');
|
||||
clickOnElement(line);
|
||||
clickOnElement(line);
|
||||
|
||||
line.className.should.be.equal('line');
|
||||
});
|
||||
|
||||
it('should limit number of lines in browser', () => {
|
||||
io.emit('options:lines', 2);
|
||||
io.emit('line', 'line1');
|
||||
io.emit('line', 'line2');
|
||||
io.emit('line', 'line3');
|
||||
|
||||
const log = window.document.querySelector('.log');
|
||||
log.childNodes.length.should.be.equal(2);
|
||||
log.childNodes[0].textContent.should.be.equal('line2');
|
||||
log.childNodes[1].textContent.should.be.equal('line3');
|
||||
});
|
||||
|
||||
it('should hide topbar', () => {
|
||||
io.emit('options:hide-topbar');
|
||||
|
||||
const topbar = window.document.querySelector('.topbar');
|
||||
topbar.className.should.match(/hide/);
|
||||
const body = window.document.querySelector('body');
|
||||
body.className.should.match(/no-topbar/);
|
||||
});
|
||||
|
||||
it('should not indent log lines', () => {
|
||||
io.emit('options:no-indent');
|
||||
|
||||
const log = window.document.querySelector('.log');
|
||||
log.className.should.match(/no-indent/);
|
||||
});
|
||||
|
||||
it('should highlight word', () => {
|
||||
io.emit('options:highlightConfig', {
|
||||
words: {
|
||||
foo: 'background: black',
|
||||
bar: 'background: black',
|
||||
},
|
||||
});
|
||||
io.emit('line', 'foo bar');
|
||||
|
||||
it('should deselect line when selected line clicked', function () {
|
||||
io.emit('line', 'test');
|
||||
const line = window.document.querySelector('.line');
|
||||
line.innerHTML.should.containEql(
|
||||
'<span style="background: black">foo</span> <span style="background: black">bar</span>'
|
||||
);
|
||||
});
|
||||
|
||||
var line = window.document.querySelector('.line');
|
||||
clickOnElement(line);
|
||||
clickOnElement(line);
|
||||
|
||||
line.className.should.be.equal('line');
|
||||
it('should highlight line', () => {
|
||||
io.emit('options:highlightConfig', {
|
||||
lines: {
|
||||
line: 'background: black',
|
||||
},
|
||||
});
|
||||
io.emit('line', 'line1');
|
||||
|
||||
it('should limit number of lines in browser', function () {
|
||||
io.emit('options:lines', 2);
|
||||
io.emit('line', 'line1');
|
||||
io.emit('line', 'line2');
|
||||
io.emit('line', 'line3');
|
||||
const line = window.document.querySelector('.line');
|
||||
line.parentNode.innerHTML.should.equal(
|
||||
'<div class="line" style="background: black"><p class="inner-line">line1</p></div>'
|
||||
);
|
||||
});
|
||||
|
||||
var log = window.document.querySelector('.log');
|
||||
log.childNodes.length.should.be.equal(2);
|
||||
log.childNodes[0].textContent.should.be.equal('line2');
|
||||
log.childNodes[1].textContent.should.be.equal('line3');
|
||||
});
|
||||
it('should escape HTML', () => {
|
||||
io.emit('line', '<a/>');
|
||||
|
||||
it('should hide topbar', function () {
|
||||
io.emit('options:hide-topbar');
|
||||
const line = window.document.querySelector('.line');
|
||||
line.innerHTML.should.equal('<p class="inner-line"><a/></p>');
|
||||
});
|
||||
|
||||
var topbar = window.document.querySelector('.topbar');
|
||||
topbar.className.should.match(/hide/);
|
||||
var body = window.document.querySelector('body');
|
||||
body.className.should.match(/no-topbar/);
|
||||
});
|
||||
it('should work filter from URL', () => {
|
||||
io.emit('line', 'line1');
|
||||
io.emit('line', 'another');
|
||||
io.emit('line', 'line2');
|
||||
|
||||
it('should not indent log lines', function () {
|
||||
io.emit('options:no-indent');
|
||||
const filterInput = window.document.querySelector('#filter');
|
||||
filterInput.value.should.be.equal('line.*');
|
||||
const log = window.document.querySelector('.log');
|
||||
log.childNodes.length.should.be.equal(3);
|
||||
log.childNodes[0].style.display.should.be.equal('');
|
||||
log.childNodes[1].style.display.should.be.equal('none');
|
||||
log.childNodes[2].style.display.should.be.equal('');
|
||||
window.location.href.should.containEql('filter=line.*');
|
||||
});
|
||||
|
||||
var log = window.document.querySelector('.log');
|
||||
log.className.should.match(/no-indent/);
|
||||
});
|
||||
it('should clean filter', () => {
|
||||
io.emit('line', 'line1');
|
||||
io.emit('line', 'another');
|
||||
io.emit('line', 'line2');
|
||||
|
||||
it('should highlight word', function () {
|
||||
io.emit('options:highlightConfig', {words: {line: 'background: black'}});
|
||||
io.emit('line', 'line1');
|
||||
const filterInput = window.document.querySelector('#filter');
|
||||
const event = new window.KeyboardEvent('keyup', { keyCode: 27 });
|
||||
filterInput.dispatchEvent(event);
|
||||
const log = window.document.querySelector('.log');
|
||||
log.childNodes.length.should.be.equal(3);
|
||||
log.childNodes[0].style.display.should.be.equal('');
|
||||
log.childNodes[1].style.display.should.be.equal('');
|
||||
log.childNodes[2].style.display.should.be.equal('');
|
||||
window.location.href.should.be.equal('http://localhost/');
|
||||
});
|
||||
|
||||
var line = window.document.querySelector('.line');
|
||||
line.innerHTML.should.containEql('<span style="background: black">line</span>');
|
||||
});
|
||||
it('should change filter', () => {
|
||||
io.emit('line', 'line1');
|
||||
io.emit('line', 'another');
|
||||
io.emit('line', 'line2');
|
||||
|
||||
it('should highlight line', function () {
|
||||
io.emit('options:highlightConfig', {lines: {line: 'background: black'}});
|
||||
io.emit('line', 'line1');
|
||||
const log = window.document.querySelector('.log');
|
||||
const filterInput = window.document.querySelector('#filter');
|
||||
filterInput.value = 'other';
|
||||
const event = new window.KeyboardEvent('keyup', { keyCode: 13 });
|
||||
filterInput.dispatchEvent(event);
|
||||
log.childNodes.length.should.be.equal(3);
|
||||
log.childNodes[0].style.display.should.be.equal('none');
|
||||
log.childNodes[1].style.display.should.be.equal('');
|
||||
log.childNodes[2].style.display.should.be.equal('none');
|
||||
window.location.href.should.containEql('filter=other');
|
||||
});
|
||||
|
||||
var line = window.document.querySelector('.line');
|
||||
line.parentNode.innerHTML.should.equal(
|
||||
'<div class="line" style="background: black;"><p class="inner-line">line1</p></div>'
|
||||
);
|
||||
});
|
||||
it('should pause', () => {
|
||||
io.emit('line', 'line1');
|
||||
const btn = window.document.querySelector('#pauseBtn');
|
||||
const event = window.document.createEvent('Event');
|
||||
event.initEvent('mouseup', true, true);
|
||||
btn.dispatchEvent(event);
|
||||
io.emit('line', 'line2');
|
||||
io.emit('line', 'line3');
|
||||
|
||||
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')
|
||||
});
|
||||
}
|
||||
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 <==');
|
||||
});
|
||||
|
||||
function clickOnElement(line) {
|
||||
var click = window.document.createEvent('MouseEvents');
|
||||
click.initMouseEvent('click', true, true);
|
||||
line.dispatchEvent(click);
|
||||
}
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,107 +1,123 @@
|
|||
'use strict';
|
||||
|
||||
require('should');
|
||||
var connectBuilder = require('../lib/connect_builder');
|
||||
var request = require('supertest');
|
||||
const request = require('supertest');
|
||||
const path = require('path');
|
||||
const connectBuilder = require('../lib/connect_builder');
|
||||
|
||||
describe('connectBuilder', function () {
|
||||
it('should build connect app', function () {
|
||||
connectBuilder().build().should.have.property('use');
|
||||
connectBuilder().build().should.have.property('listen');
|
||||
describe('connectBuilder', () => {
|
||||
it('should build connect app', () => {
|
||||
connectBuilder('/').build().should.have.property('use');
|
||||
connectBuilder().build().should.have.property('listen');
|
||||
});
|
||||
|
||||
it('should build app requiring authorized user', (done) => {
|
||||
const app = connectBuilder('/').authorize('user', 'pass').build();
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect('www-authenticate', 'Basic realm="Authorization Required"')
|
||||
.expect(401, done);
|
||||
});
|
||||
|
||||
it('should build app allowing user to login', (done) => {
|
||||
const app = connectBuilder('/').authorize('user', 'pass').build();
|
||||
app.use((req, res) => {
|
||||
res.end('secret!');
|
||||
});
|
||||
|
||||
it('should build app requiring authorized user', function (done) {
|
||||
var app = connectBuilder().authorize('user', 'pass').build();
|
||||
request(app)
|
||||
.get('/')
|
||||
.set('Authorization', 'Basic dXNlcjpwYXNz')
|
||||
.expect(200, 'secret!', done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect('www-authenticate', 'Basic realm="Authorization Required"')
|
||||
.expect(401, done);
|
||||
it('should build app that setup session', (done) => {
|
||||
const app = connectBuilder('/').session('secret').build();
|
||||
app.use((req, res) => {
|
||||
res.end();
|
||||
});
|
||||
|
||||
it('should build app allowing user to login', function (done) {
|
||||
var app = connectBuilder().authorize('user', 'pass').build();
|
||||
app.use(function (req, res) {
|
||||
res.end('secret!');
|
||||
});
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect('set-cookie', /^connect.sid/, done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.set('Authorization', 'Basic dXNlcjpwYXNz')
|
||||
.expect(200, 'secret!', done);
|
||||
});
|
||||
it('should build app that serve static files', (done) => {
|
||||
const app = connectBuilder('/')
|
||||
.static(path.join(__dirname, 'fixtures'))
|
||||
.build();
|
||||
|
||||
it('should build app that setup session', function (done) {
|
||||
var app = connectBuilder().session('secret', 'sessionkey').build();
|
||||
app.use(function (req, res) {
|
||||
res.end();
|
||||
});
|
||||
request(app).get('/foo.txt').expect('bar', done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect('set-cookie', /^sessionkey/, done);
|
||||
});
|
||||
it('should build app that serve index file', (done) => {
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
|
||||
.build();
|
||||
|
||||
it('should build app that serve static files', function (done) {
|
||||
var app = connectBuilder().static(__dirname + '/fixtures').build();
|
||||
request(app).get('/').expect(200).expect('Content-Type', 'text/html', done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/foo')
|
||||
.expect('bar', done);
|
||||
});
|
||||
it('should build app that serve index file on specified path', (done) => {
|
||||
const app = connectBuilder('/test')
|
||||
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
|
||||
.build();
|
||||
|
||||
it('should build app that serve index file', function (done) {
|
||||
var app = connectBuilder().index(__dirname + '/fixtures/index', '/testfile').build();
|
||||
request(app)
|
||||
.get('/test')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'text/html', done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'text/html', done);
|
||||
});
|
||||
it('should build app that replace index title', (done) => {
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
|
||||
.build();
|
||||
|
||||
it('should build app that replace index title', function (done) {
|
||||
var app = connectBuilder()
|
||||
.index(__dirname + '/fixtures/index_with_title', '/testfile')
|
||||
.build();
|
||||
request(app).get('/').expect('<head><title>/testfile</title></head>', done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect('<head><title>/testfile</title></head>', done);
|
||||
});
|
||||
it('should build app that sets socket.io namespace based on files', (done) => {
|
||||
const app = connectBuilder('/')
|
||||
.index(
|
||||
path.join(__dirname, 'fixtures/index_with_ns'),
|
||||
'/testfile',
|
||||
'ns',
|
||||
'dark'
|
||||
)
|
||||
.build();
|
||||
|
||||
it('should build app that sets socket.io namespace based on files', function (done) {
|
||||
var app = connectBuilder()
|
||||
.index(__dirname + '/fixtures/index_with_ns', '/testfile', 'ns', 'dark')
|
||||
.build();
|
||||
request(app).get('/').expect('ns', done);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect('ns', done);
|
||||
});
|
||||
it('should build app that sets theme', (done) => {
|
||||
const app = connectBuilder('/')
|
||||
.index(
|
||||
path.join(__dirname, '/fixtures/index_with_theme'),
|
||||
'/testfile',
|
||||
'ns',
|
||||
'dark'
|
||||
)
|
||||
.build();
|
||||
|
||||
it('should build app that sets theme', function (done) {
|
||||
var app = connectBuilder()
|
||||
.index(__dirname + '/fixtures/index_with_theme', '/testfile', 'ns', 'dark')
|
||||
.build();
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect(
|
||||
'<head><title>/testfile</title><link href="dark.css" rel="stylesheet" type="text/css"/></head>',
|
||||
done
|
||||
);
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect(
|
||||
'<head><title>/testfile</title><link href="dark.css" rel="stylesheet" type="text/css"/></head>',
|
||||
done
|
||||
);
|
||||
});
|
||||
it('should build app that sets default theme', (done) => {
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile')
|
||||
.build();
|
||||
|
||||
it('should build app that sets default theme', function (done) {
|
||||
var app = connectBuilder()
|
||||
.index(__dirname + '/fixtures/index_with_theme', '/testfile')
|
||||
.build();
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect(
|
||||
'<head><title>/testfile</title><link href="default.css" rel="stylesheet" type="text/css"/></head>',
|
||||
done
|
||||
);
|
||||
});
|
||||
request(app)
|
||||
.get('/')
|
||||
.expect(
|
||||
'<head><title>/testfile</title><link href="default.css" rel="stylesheet" type="text/css"/></head>',
|
||||
done
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,192 +1,267 @@
|
|||
'use strict';
|
||||
|
||||
require('should');
|
||||
var daemon = require('daemon');
|
||||
var optionsParser = require('../lib/options_parser');
|
||||
var daemonize = require('../lib/daemonize');
|
||||
var sinon = require('sinon');
|
||||
var fs = require('fs');
|
||||
const daemon = require('daemon-fix41');
|
||||
const sinon = require('sinon');
|
||||
const fs = require('fs');
|
||||
const optionsParser = require('../lib/options_parser');
|
||||
const daemonize = require('../lib/daemonize');
|
||||
|
||||
describe('daemonize', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(daemon, 'daemon');
|
||||
daemon.daemon.returns({pid: 1000});
|
||||
sinon.stub(fs, 'writeFileSync');
|
||||
sinon.stub(fs, 'openSync');
|
||||
describe('daemonize', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(daemon, 'daemon');
|
||||
daemon.daemon.returns({
|
||||
pid: 1000,
|
||||
});
|
||||
sinon.stub(fs, 'writeFileSync');
|
||||
sinon.stub(fs, 'openSync');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
daemon.daemon.restore();
|
||||
fs.writeFileSync.restore();
|
||||
fs.openSync.restore();
|
||||
});
|
||||
|
||||
describe('should daemon', () => {
|
||||
it('current script', () => {
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[0].should.match('script');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
daemon.daemon.restore();
|
||||
fs.writeFileSync.restore();
|
||||
fs.openSync.restore();
|
||||
it('with hostname', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-h', '127.0.0.1']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-h', '127.0.0.1']);
|
||||
});
|
||||
|
||||
describe('should daemon', function () {
|
||||
it('current script', function () {
|
||||
daemonize('script', optionsParser);
|
||||
it('with port', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-p', '80']);
|
||||
|
||||
daemon.daemon.lastCall.args[0].should.match('script');
|
||||
});
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
it('with hostname', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-h', '127.0.0.1']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-h', '127.0.0.1']);
|
||||
});
|
||||
|
||||
it('with port', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-p', '80']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-p', 80]);
|
||||
});
|
||||
|
||||
it('with lines number', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-n', '1']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-n', 1]);
|
||||
});
|
||||
|
||||
it('with lines stored in browser', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-l', '1']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-l', 1]);
|
||||
});
|
||||
|
||||
it('with theme', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-t', 'dark']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-t', 'dark']);
|
||||
});
|
||||
|
||||
it('with authorization', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
|
||||
|
||||
daemonize('script', optionsParser, {doAuthorization: true});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-U', 'user', '-P', 'passw0rd']);
|
||||
});
|
||||
|
||||
it('without authorization if option doAuthorization not passed', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.not.containDeep(['-U', 'user', '-P', 'passw0rd']);
|
||||
});
|
||||
|
||||
it('with secure connection', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-k', 'key.file', '-c', 'cert.file']);
|
||||
|
||||
daemonize('script', optionsParser, {doSecure: true});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
|
||||
});
|
||||
|
||||
it('with ssh configuration', function () {
|
||||
optionsParser.parse([
|
||||
'node', '/path/to/frontail',
|
||||
'--remote-host', 'remoteHost',
|
||||
'--remote-user', 'remoteUser',
|
||||
'--remote-port', '23'
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser, {doSSH: true});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'--remote-host', 'remoteHost',
|
||||
'--remote-user', 'remoteUser',
|
||||
'--remote-port', '23'
|
||||
]);
|
||||
});
|
||||
|
||||
it('without ssh configuration', function () {
|
||||
optionsParser.parse([
|
||||
'node', '/path/to/frontail',
|
||||
'--remote-user', 'remoteUser',
|
||||
'--remote-port', '23'
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser, {doSSH: false});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.not.containDeep([
|
||||
'--remote-host', 'remoteHost',
|
||||
'--remote-user', 'remoteUser',
|
||||
'--remote-port', '23'
|
||||
]);
|
||||
});
|
||||
|
||||
it('without secure connection if option doSecure not passed', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-k', 'key.file', '-c', 'cert.file']);
|
||||
|
||||
daemonize('script', optionsParser, {doSecure: true});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
|
||||
});
|
||||
|
||||
it('with hide-topbar option', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-hide-topbar']);
|
||||
});
|
||||
|
||||
it('with no-indent option', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-no-indent']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-no-indent']);
|
||||
});
|
||||
|
||||
it('with highlight option', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-highlight']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight']);
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight-preset', './preset/default.json']);
|
||||
});
|
||||
|
||||
it('with file to tail', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '/path/to/file']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['/path/to/file']);
|
||||
});
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-p', 80]);
|
||||
});
|
||||
|
||||
it('should write pid to pidfile', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--pid-path', '/path/to/pid']);
|
||||
it('with lines number', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-n', '1']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
fs.writeFileSync.lastCall.args[0].should.be.equal('/path/to/pid');
|
||||
fs.writeFileSync.lastCall.args[1].should.be.equal(1000);
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-n', 1]);
|
||||
});
|
||||
|
||||
it('should log to file', function () {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--log-path', '/path/to/log']);
|
||||
fs.openSync.returns('file');
|
||||
it('with lines stored in browser', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-l', '1']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
fs.openSync.lastCall.args[0].should.equal('/path/to/log');
|
||||
fs.openSync.lastCall.args[1].should.equal('a');
|
||||
daemon.daemon.lastCall.args[2].should.eql({
|
||||
stdout: 'file',
|
||||
stderr: 'file'
|
||||
});
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-l', 1]);
|
||||
});
|
||||
|
||||
it('with theme', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '-t', 'dark']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['-t', 'dark']);
|
||||
});
|
||||
|
||||
it('with authorization', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'-U',
|
||||
'user',
|
||||
'-P',
|
||||
'passw0rd',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser, {
|
||||
doAuthorization: true,
|
||||
});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'-U',
|
||||
'user',
|
||||
'-P',
|
||||
'passw0rd',
|
||||
]);
|
||||
});
|
||||
|
||||
it('without authorization if option doAuthorization not passed', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'-U',
|
||||
'user',
|
||||
'-P',
|
||||
'passw0rd',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.not.containDeep([
|
||||
'-U',
|
||||
'user',
|
||||
'-P',
|
||||
'passw0rd',
|
||||
]);
|
||||
});
|
||||
|
||||
it('with secure connection', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'-k',
|
||||
'key.file',
|
||||
'-c',
|
||||
'cert.file',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser, {
|
||||
doSecure: true,
|
||||
});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'-k',
|
||||
'key.file',
|
||||
'-c',
|
||||
'cert.file',
|
||||
]);
|
||||
});
|
||||
|
||||
it('without secure connection if option doSecure not passed', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'-k',
|
||||
'key.file',
|
||||
'-c',
|
||||
'cert.file',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser, {
|
||||
doSecure: true,
|
||||
});
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'-k',
|
||||
'key.file',
|
||||
'-c',
|
||||
'cert.file',
|
||||
]);
|
||||
});
|
||||
|
||||
it('with url-path option', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--url-path', '/test']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'--url-path',
|
||||
'/test',
|
||||
]);
|
||||
});
|
||||
|
||||
it('with hide-topbar option', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-hide-topbar']);
|
||||
});
|
||||
|
||||
it('with no-indent option', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-no-indent']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-no-indent']);
|
||||
});
|
||||
|
||||
it('with highlight option', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-highlight']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight']);
|
||||
});
|
||||
|
||||
it('with highlight preset option', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'--ui-highlight',
|
||||
'--ui-highlight-preset',
|
||||
'test.json',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'--ui-highlight-preset',
|
||||
'test.json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('with disable usage stats', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'--disable-usage-stats',
|
||||
'test.json',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep([
|
||||
'--disable-usage-stats',
|
||||
'test.json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('with file to tail', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '/path/to/file']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['/path/to/file']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should write pid to pidfile', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'--pid-path',
|
||||
'/path/to/pid',
|
||||
]);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
fs.writeFileSync.lastCall.args[0].should.be.equal('/path/to/pid');
|
||||
fs.writeFileSync.lastCall.args[1].should.be.equal(1000);
|
||||
});
|
||||
|
||||
it('should log to file', () => {
|
||||
optionsParser.parse([
|
||||
'node',
|
||||
'/path/to/frontail',
|
||||
'--log-path',
|
||||
'/path/to/log',
|
||||
]);
|
||||
fs.openSync.returns('file');
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
fs.openSync.lastCall.args[0].should.equal('/path/to/log');
|
||||
fs.openSync.lastCall.args[1].should.equal('a');
|
||||
daemon.daemon.lastCall.args[2].should.eql({
|
||||
stdout: 'file',
|
||||
stderr: 'file',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
105
test/tail.js
105
test/tail.js
|
@ -1,73 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
require('should');
|
||||
var fs = require('fs');
|
||||
var tail = require('../lib/tail');
|
||||
var temp = require('temp');
|
||||
var sinon = require('sinon');
|
||||
var childProcess = require('child_process');
|
||||
const fs = require('fs');
|
||||
const temp = require('temp');
|
||||
const tail = require('../lib/tail');
|
||||
|
||||
var TEMP_FILE_PROFIX = '';
|
||||
var SPAWN_DELAY = 10;
|
||||
const TEMP_FILE_PROFIX = '';
|
||||
const SPAWN_DELAY = 10;
|
||||
|
||||
describe('tail', function () {
|
||||
temp.track();
|
||||
function writeLines(fd, count) {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
fs.writeSync(
|
||||
fd,
|
||||
`line${i}
|
||||
`
|
||||
);
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
|
||||
it('calls event line if new line appear in file', function (done) {
|
||||
temp.open(TEMP_FILE_PROFIX, function (err, info) {
|
||||
tail(info.path).on('line', function (line) {
|
||||
line.should.equal('line0');
|
||||
done();
|
||||
});
|
||||
describe('tail', () => {
|
||||
temp.track();
|
||||
|
||||
setTimeout(writeLines, SPAWN_DELAY, info.fd, 1);
|
||||
});
|
||||
it('calls event line if new line appear in file', (done) => {
|
||||
temp.open(TEMP_FILE_PROFIX, (err, info) => {
|
||||
tail(info.path).on('line', (line) => {
|
||||
line.should.equal('line0');
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(writeLines, SPAWN_DELAY, info.fd, 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('buffers lines on start', function (done) {
|
||||
temp.open(TEMP_FILE_PROFIX, function (err, info) {
|
||||
writeLines(info.fd, 20);
|
||||
it('buffers lines on start', (done) => {
|
||||
temp.open(TEMP_FILE_PROFIX, (err, info) => {
|
||||
writeLines(info.fd, 20);
|
||||
|
||||
var tailer = tail(info.path, {buffer: 2});
|
||||
setTimeout(function () {
|
||||
tailer.getBuffer().should.be.eql(['line18', 'line19']);
|
||||
done();
|
||||
}, SPAWN_DELAY);
|
||||
});
|
||||
const tailer = tail(info.path, {
|
||||
buffer: 2,
|
||||
});
|
||||
setTimeout(() => {
|
||||
tailer.getBuffer().should.be.eql(['line18', 'line19']);
|
||||
done();
|
||||
}, SPAWN_DELAY);
|
||||
});
|
||||
});
|
||||
|
||||
it('buffers no lines on start by default', function (done) {
|
||||
temp.open(TEMP_FILE_PROFIX, function (err, info) {
|
||||
writeLines(info.fd, 3);
|
||||
it('buffers no lines on start by default', (done) => {
|
||||
temp.open(TEMP_FILE_PROFIX, (err, info) => {
|
||||
writeLines(info.fd, 3);
|
||||
|
||||
var tailer = tail(info.path);
|
||||
setTimeout(function () {
|
||||
tailer.getBuffer().should.be.empty;
|
||||
done();
|
||||
}, SPAWN_DELAY);
|
||||
});
|
||||
const tailer = tail(info.path);
|
||||
setTimeout(() => {
|
||||
tailer.getBuffer().should.be.empty;
|
||||
done();
|
||||
}, SPAWN_DELAY);
|
||||
});
|
||||
|
||||
describe('with ssh options', function () {
|
||||
it('should call ssh command', function () {
|
||||
sinon.spy(childProcess, 'spawn');
|
||||
var sshOptions = {
|
||||
remoteUser: 'testUser',
|
||||
remoteHost: 'host',
|
||||
remotePort: 1234
|
||||
};
|
||||
|
||||
tail('test/path', {ssh: sshOptions});
|
||||
|
||||
childProcess.spawn.calledWith('ssh', ['testUser@host', '-p', 1234, 'tail -f', 'test/path']).should.be.true;
|
||||
childProcess.spawn.restore();
|
||||
});
|
||||
});
|
||||
|
||||
function writeLines(fd, count) {
|
||||
for (var i = 0; i < count; i += 1) {
|
||||
fs.writeSync(fd, 'line' + i + '\n');
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 "&";
|
||||
if (str == "<") return "<";
|
||||
if (str == ">") return ">";
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
|
@ -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));
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
Loading…
Reference in New Issue