wtails 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +52 -0
- data/bin/wtails +29 -0
- data/doc/img/dual_view.png +0 -0
- data/doc/img/single_view.png +0 -0
- data/lib/wtails.rb +54 -0
- data/lib/wtails/browser.rb +21 -0
- data/lib/wtails/stdin.rb +28 -0
- data/lib/wtails/tail.rb +33 -0
- data/lib/wtails/version.rb +3 -0
- data/lib/wtails/web_server.rb +48 -0
- data/lib/wtails/web_socket.rb +74 -0
- data/public/css/style.css +166 -0
- data/public/images/favicon.ico +0 -0
- data/public/js/jquery.min.js +4 -0
- data/public/js/lemmon-slider.js +361 -0
- data/public/js/tinycon.js +228 -0
- data/public/js/wtails.js +30 -0
- data/views/_init.erb +14 -0
- data/views/_pane.erb +33 -0
- data/views/dual.erb +45 -0
- data/views/single.erb +31 -0
- data/views/test.erb +30 -0
- data/wtails.gemspec +24 -0
- metadata +173 -0
data/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
wtails
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
Wtails is a web server acts like 'tails -f', inspired by webtail( https://github.com/r7kamura/webtail ).
|
|
5
|
+
|
|
6
|
+
You can watch multiple files with slider UI.
|
|
7
|
+
|
|
8
|
+
Wtails can serve files without launching browser(--serve), this option is usable for watching remote files.
|
|
9
|
+
|
|
10
|
+
usage
|
|
11
|
+
=====
|
|
12
|
+
|
|
13
|
+
% wtails foo.log bar.log baz.log - --serve
|
|
14
|
+
|
|
15
|
+
then web server started, and you can now browse them at 'http://localhost:9999'.
|
|
16
|
+
|
|
17
|
+
screenshot
|
|
18
|
+
==========
|
|
19
|
+
|
|
20
|
+
single view
|
|
21
|
+
-----------
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
dual (upper/lower) view
|
|
25
|
+
-----------------------
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
.wtailsrc
|
|
29
|
+
=========
|
|
30
|
+
|
|
31
|
+
Semantics of .wtailsrc is different from webtail. You should modify 'line' local variable.
|
|
32
|
+
|
|
33
|
+
example:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
$ cat ~/.webtailrc
|
|
37
|
+
var text = line.text();
|
|
38
|
+
|
|
39
|
+
if (text == '\n') {
|
|
40
|
+
line.css({
|
|
41
|
+
margin: '3em 0',
|
|
42
|
+
height: 1,
|
|
43
|
+
background: 'lime'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (text.match(/require|opt/)) {
|
|
48
|
+
line.css({
|
|
49
|
+
color: '#E1017B'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
```
|
data/bin/wtails
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'trollop'
|
|
5
|
+
|
|
6
|
+
opts = Trollop::options do
|
|
7
|
+
version "wtails 0.1 (c) 2012 Naoyuki Hirayama"
|
|
8
|
+
banner <<-EOS
|
|
9
|
+
Wtails is a web server acts like 'tail -f'.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
wtails [options] <filename>+
|
|
13
|
+
|
|
14
|
+
EOS
|
|
15
|
+
|
|
16
|
+
opt :port, "Port number for http server", :default => 9999
|
|
17
|
+
opt :rc, 'Callback file location', :default => "~/.wtailsrc"
|
|
18
|
+
opt :serve, "Just serve, don't open browser", :default => false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Trollop::die "need at least one filename" if ARGV.empty?
|
|
22
|
+
|
|
23
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
|
24
|
+
require "wtails"
|
|
25
|
+
begin
|
|
26
|
+
Wtails.run(opts.to_hash, ARGV)
|
|
27
|
+
rescue Errno::ENOENT => e
|
|
28
|
+
puts e
|
|
29
|
+
end
|
|
Binary file
|
|
Binary file
|
data/lib/wtails.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require "eventmachine-tail"
|
|
5
|
+
require 'websocket-eventmachine-server'
|
|
6
|
+
|
|
7
|
+
require "sinatra/base"
|
|
8
|
+
require "launchy"
|
|
9
|
+
|
|
10
|
+
require "wtails/version"
|
|
11
|
+
require "wtails/web_server"
|
|
12
|
+
require "wtails/web_socket"
|
|
13
|
+
require "wtails/tail"
|
|
14
|
+
require "wtails/stdin"
|
|
15
|
+
#require "wtails/browser"
|
|
16
|
+
|
|
17
|
+
module Wtails
|
|
18
|
+
extend self
|
|
19
|
+
|
|
20
|
+
def run(opts, files)
|
|
21
|
+
configure(opts)
|
|
22
|
+
|
|
23
|
+
Thread.abort_on_exception = true
|
|
24
|
+
EM.run do
|
|
25
|
+
EM.defer { WebSocket.run(opts, files) }
|
|
26
|
+
EM.defer { WebServer.run(opts, files) }
|
|
27
|
+
#EM.defer { Browser.run }
|
|
28
|
+
Tail.run(opts, files)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def channel(path)
|
|
33
|
+
@channel ||= {}
|
|
34
|
+
@channel[path] ||= EM::Channel.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def config
|
|
38
|
+
@config
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def http_server
|
|
42
|
+
@http_server
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def http_server=(http_server)
|
|
46
|
+
@http_server = http_server
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def configure(args)
|
|
52
|
+
@config = args
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Wtails
|
|
2
|
+
module Browser
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def run
|
|
6
|
+
if Wtails.config[:serve]
|
|
7
|
+
puts "serve on http://localhost:#{Wtails.config[:port]}"
|
|
8
|
+
else
|
|
9
|
+
# ugly, but there is nothing for it but to poll
|
|
10
|
+
# because thin has no start callback
|
|
11
|
+
while true
|
|
12
|
+
if Wtails.http_server && Wtails.http_server.running?
|
|
13
|
+
break
|
|
14
|
+
end
|
|
15
|
+
sleep(0.1)
|
|
16
|
+
end
|
|
17
|
+
::Launchy.open("http://localhost:#{Wtails.config[:port]}") rescue nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/wtails/stdin.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Wtails
|
|
2
|
+
module Stdin
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
ENTITY_MAP = {
|
|
6
|
+
"<" => "<",
|
|
7
|
+
">" => ">",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
STDIN.each do |line|
|
|
12
|
+
line = unescape_entity(line)
|
|
13
|
+
line = strip_ansi_sequence(line)
|
|
14
|
+
Wtails.channel('-') << line
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def strip_ansi_sequence(str)
|
|
21
|
+
str.gsub(/\e\[.*?m/, "")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unescape_entity(str)
|
|
25
|
+
str.gsub(/#{Regexp.union(ENTITY_MAP.keys)}/o) {|key| ENTITY_MAP[key] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/wtails/tail.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Wtails
|
|
2
|
+
module Tail
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
ENTITY_MAP = {
|
|
6
|
+
"<" => "<",
|
|
7
|
+
">" => ">",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
def run(opts, files)
|
|
11
|
+
files.each do |path|
|
|
12
|
+
if path == '-'
|
|
13
|
+
EM.defer { Wtails::Stdin.run }
|
|
14
|
+
else
|
|
15
|
+
EventMachine::file_tail(path, Reader)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Reader < EventMachine::FileTail
|
|
21
|
+
def initialize(path, startpos=-1)
|
|
22
|
+
super(path, startpos)
|
|
23
|
+
@buffer = BufferedTokenizer.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def receive_data(data)
|
|
27
|
+
@buffer.extract(data).each do |line|
|
|
28
|
+
Wtails.channel(path) << line
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Wtails
|
|
2
|
+
module WebServer
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def run(opts, files)
|
|
6
|
+
::Rack::Handler::Thin.run(
|
|
7
|
+
Server.new,
|
|
8
|
+
:Port => opts[:port]
|
|
9
|
+
) do |http_server|
|
|
10
|
+
Wtails.http_server = http_server
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Server < ::Sinatra::Base
|
|
15
|
+
set :wtailsrc do
|
|
16
|
+
path = File.expand_path(Wtails.config[:rc])
|
|
17
|
+
File.exist?(path) && File.read(path)
|
|
18
|
+
end
|
|
19
|
+
set :views, File.expand_path("../../../views/", __FILE__)
|
|
20
|
+
set :public, File.expand_path("../../../public/", __FILE__)
|
|
21
|
+
|
|
22
|
+
get "/" do
|
|
23
|
+
redirect "/single"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
get "/single" do
|
|
27
|
+
erb :single, :locals => make_local_variables
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
get "/dual" do
|
|
31
|
+
erb :dual, :locals => make_local_variables
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
get "/test" do
|
|
35
|
+
erb :test
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def make_local_variables
|
|
39
|
+
{
|
|
40
|
+
:files => WebSocket.servers.map { |s| [s.file, s.port] },
|
|
41
|
+
:wtailsrc => settings.wtailsrc,
|
|
42
|
+
:host => URI.parse(request.url).host,
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Wtails
|
|
2
|
+
module WebSocket
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
LOG_SIZE = 100
|
|
6
|
+
|
|
7
|
+
def run(opts, files)
|
|
8
|
+
port = opts[:port]
|
|
9
|
+
@servers = files.map do |file|
|
|
10
|
+
port += 1
|
|
11
|
+
Server.new(file, port)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def servers
|
|
16
|
+
@servers ||= []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Server
|
|
20
|
+
def initialize(file, port)
|
|
21
|
+
@file = file
|
|
22
|
+
@port = port
|
|
23
|
+
@logs = []
|
|
24
|
+
@channel = Wtails.channel(file)
|
|
25
|
+
@channel.subscribe do |msg|
|
|
26
|
+
@logs << msg
|
|
27
|
+
@logs.shift if @logs.size > LOG_SIZE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts = {:host => "127.0.0.1", :port => port}
|
|
31
|
+
s = ::WebSocket::EventMachine::Server.start(opts) do |socket|
|
|
32
|
+
socket.onopen(&onopen(socket))
|
|
33
|
+
socket.onmessage(&onmessage)
|
|
34
|
+
socket.onerror(&onerror)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def onopen(socket)
|
|
38
|
+
proc do
|
|
39
|
+
send_message = proc do |message|
|
|
40
|
+
next unless message
|
|
41
|
+
str = message.respond_to?(:force_encoding) ?
|
|
42
|
+
message.force_encoding("UTF-8") :
|
|
43
|
+
message
|
|
44
|
+
|
|
45
|
+
socket.send(str)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@logs.each(&send_message)
|
|
49
|
+
id = @channel.subscribe(&send_message)
|
|
50
|
+
|
|
51
|
+
socket.onclose do
|
|
52
|
+
@channel.unsubscribe(id)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def onmessage
|
|
58
|
+
proc do |message|
|
|
59
|
+
@channel << message
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def onerror
|
|
64
|
+
proc do |error|
|
|
65
|
+
puts error
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
attr_reader :port
|
|
71
|
+
attr_reader :file
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
html {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
background: #444;
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0 0 20px 0;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
-moz-box-sizing: border-box;
|
|
15
|
+
-webkit-box-sizing: border-box;
|
|
16
|
+
-o-box-sizing: border-box;
|
|
17
|
+
-ms-box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
header {
|
|
21
|
+
overflow: visible;
|
|
22
|
+
height: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
header div {
|
|
26
|
+
background: #888;
|
|
27
|
+
padding: 2px 8px 4px 8px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
header div a {
|
|
31
|
+
color: #eee;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.main-view {
|
|
35
|
+
height: 100%;
|
|
36
|
+
padding-top: 32px;
|
|
37
|
+
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
-moz-box-sizing: border-box;
|
|
40
|
+
-webkit-box-sizing: border-box;
|
|
41
|
+
-o-box-sizing: border-box;
|
|
42
|
+
-ms-box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.pane-full {
|
|
46
|
+
width: 100%;
|
|
47
|
+
height: 100%;
|
|
48
|
+
position: relative;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.pane-half {
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 50%;
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.slider {
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
position: relative;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
}
|
|
63
|
+
.slider ul {
|
|
64
|
+
margin: 0 auto;
|
|
65
|
+
padding: 0;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.slider li {
|
|
70
|
+
float: left;
|
|
71
|
+
list-style:none;
|
|
72
|
+
margin:0 16px 0 0;
|
|
73
|
+
height: 100%;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.terminal-title {
|
|
77
|
+
color: #ddd;
|
|
78
|
+
height: 0px;
|
|
79
|
+
overflow: visible;
|
|
80
|
+
text-align: center;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.terminal-place {
|
|
84
|
+
height: 100%;
|
|
85
|
+
padding-top: 18px;
|
|
86
|
+
|
|
87
|
+
box-sizing: border-box;
|
|
88
|
+
-moz-box-sizing: border-box;
|
|
89
|
+
-webkit-box-sizing: border-box;
|
|
90
|
+
-o-box-sizing: border-box;
|
|
91
|
+
-ms-box-sizing: border-box;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.terminal-frame {
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
width: 100%;
|
|
97
|
+
height: 100%;
|
|
98
|
+
border: 1px solid #EEE;
|
|
99
|
+
background: #111;
|
|
100
|
+
padding: 12px;
|
|
101
|
+
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
-moz-box-sizing: border-box;
|
|
104
|
+
-webkit-box-sizing: border-box;
|
|
105
|
+
-o-box-sizing: border-box;
|
|
106
|
+
-ms-box-sizing: border-box;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.terminal {
|
|
110
|
+
color: #ddd;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.controls {
|
|
114
|
+
position: absolute;
|
|
115
|
+
width: 100%;
|
|
116
|
+
height: 100%;
|
|
117
|
+
top: 0;
|
|
118
|
+
left: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.floating-button {
|
|
122
|
+
position: absolute;
|
|
123
|
+
border-radius: 5px;
|
|
124
|
+
-webkit-border-radius: 5px;
|
|
125
|
+
-moz-border-radius: 5px;
|
|
126
|
+
background: rgba(255, 255, 255, 0.2);
|
|
127
|
+
color: #fff;
|
|
128
|
+
text-decoration: none;
|
|
129
|
+
width: 48px;
|
|
130
|
+
height: 48px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.floating-button > span {
|
|
134
|
+
width: 48px;
|
|
135
|
+
height: 48px;
|
|
136
|
+
display: table-cell;
|
|
137
|
+
vertical-align: middle;
|
|
138
|
+
text-align: center;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.prev-slide {
|
|
142
|
+
top: 45%;
|
|
143
|
+
left: -8px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.next-slide {
|
|
147
|
+
top: 45%;
|
|
148
|
+
right: -8px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.left-button {
|
|
152
|
+
width: 16px;
|
|
153
|
+
height: 24px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.right-button {
|
|
157
|
+
width: 16px;
|
|
158
|
+
height: 24px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pre {
|
|
162
|
+
margin: 0;
|
|
163
|
+
padding: 0;
|
|
164
|
+
font-family: "Monaco", "Consolas", monospace;
|
|
165
|
+
}
|
|
166
|
+
|