wtails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![single view](https://raw.github.com/jonigata/wtails/master/doc/img/single_view.png)
|
23
|
+
|
24
|
+
dual (upper/lower) view
|
25
|
+
-----------------------
|
26
|
+
![dual view](https://raw.github.com/jonigata/wtails/master/doc/img/dual_view.png)
|
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
|
+
|