eventmachine-tail 0.1.2801 → 0.1.20100505022629
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/lib/em/filetail.rb +48 -6
- data/lib/em/globwatcher.rb +78 -12
- data/samples/glob-tail.rb +4 -2
- data/samples/globwatch.rb +23 -0
- data/test/test_filetail.rb +77 -0
- metadata +21 -10
data/lib/em/filetail.rb
CHANGED
@@ -6,26 +6,62 @@ require "logger"
|
|
6
6
|
|
7
7
|
EventMachine.epoll if EventMachine.epoll?
|
8
8
|
|
9
|
+
# Tail a file.
|
10
|
+
#
|
11
|
+
# Example
|
12
|
+
# class Tailer < EventMachine::Tail
|
13
|
+
# def receive_data(data)
|
14
|
+
# puts "Got #{data.length} bytes"
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # Now add it to EM
|
19
|
+
# EM.run do
|
20
|
+
# EM.file_tail("/var/log/messages", Tailer)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # Or this way:
|
24
|
+
# EM.run do
|
25
|
+
# Tailer.new("/var/log/messages")
|
26
|
+
# end
|
9
27
|
class EventMachine::FileTail
|
10
28
|
CHUNKSIZE = 65536
|
11
29
|
MAXSLEEP = 2
|
12
30
|
|
13
31
|
attr_reader :path
|
14
|
-
|
32
|
+
|
33
|
+
# Tail a file
|
34
|
+
#
|
35
|
+
# path is a string file path
|
36
|
+
# startpos is an offset to start tailing the file at. If -1, start at end of
|
37
|
+
# file.
|
38
|
+
#
|
15
39
|
public
|
16
|
-
def initialize(path, startpos
|
40
|
+
def initialize(path, startpos=-1)
|
17
41
|
@path = path
|
18
42
|
@logger = Logger.new(STDOUT)
|
19
43
|
@logger.level = Logger::WARN
|
20
44
|
|
21
|
-
#@need_scheduling = true
|
22
|
-
open
|
23
|
-
|
24
45
|
@fstat = File.stat(@path)
|
25
|
-
|
46
|
+
|
47
|
+
if @fstat.directory?
|
48
|
+
raise Errno::EISDIR.new(@path)
|
49
|
+
end
|
50
|
+
|
51
|
+
open
|
26
52
|
watch
|
53
|
+
if (startpos == -1)
|
54
|
+
@file.sysseek(0, IO::SEEK_END)
|
55
|
+
else
|
56
|
+
@file.sysseek(startpos, IO::SEEK_SET)
|
57
|
+
schedule_next_read
|
58
|
+
end
|
27
59
|
end # def initialize
|
28
60
|
|
61
|
+
# notify is invoked by EventMachine when the file you are tailing
|
62
|
+
# has been modified or otherwise needs to be acted on.
|
63
|
+
#
|
64
|
+
# You won't normally call this method.
|
29
65
|
public
|
30
66
|
def notify(status)
|
31
67
|
@logger.debug("#{status} on #{path}")
|
@@ -136,6 +172,12 @@ end # class EventMachine::FileTail::FileWatch < EventMachine::FileWatch
|
|
136
172
|
|
137
173
|
# Add EventMachine::file_tail
|
138
174
|
module EventMachine
|
175
|
+
|
176
|
+
# Tail a file.
|
177
|
+
#
|
178
|
+
# path is the path to the file to tail.
|
179
|
+
# handler should be a module implementing 'receive_data' or
|
180
|
+
# must be a subclasses of EventMachine::FileTail
|
139
181
|
def self.file_tail(path, handler=nil, *args)
|
140
182
|
args.unshift(path)
|
141
183
|
klass = klass_from_handler(EventMachine::FileTail, handler, *args);
|
data/lib/em/globwatcher.rb
CHANGED
@@ -5,14 +5,33 @@ require "eventmachine"
|
|
5
5
|
require "logger"
|
6
6
|
require "em/filetail"
|
7
7
|
|
8
|
+
# A file glob pattern watcher for EventMachine.
|
9
|
+
#
|
10
|
+
# If you are unfamiliar with globs, see Wikipedia:
|
11
|
+
# http://en.wikipedia.org/wiki/Glob_(programming)
|
12
|
+
#
|
13
|
+
# Any glob supported by Dir#glob will work with
|
14
|
+
# this class.
|
15
|
+
#
|
16
|
+
# This class will allow you to get notified whenever a file
|
17
|
+
# is created or deleted that matches your glob.
|
18
|
+
#
|
19
|
+
#
|
8
20
|
class EventMachine::FileGlobWatch
|
9
|
-
def initialize(pathglob,
|
21
|
+
def initialize(pathglob, interval=60)
|
10
22
|
@pathglob = pathglob
|
11
|
-
|
23
|
+
#@handler = handler
|
12
24
|
@files = Set.new
|
25
|
+
@watches = Hash.new
|
13
26
|
@logger = Logger.new(STDOUT)
|
14
|
-
@logger.level = Logger::
|
27
|
+
@logger.level = Logger::INFO
|
15
28
|
|
29
|
+
# We periodically check here because it is easier than writing our own glob
|
30
|
+
# parser (so we can smartly watch globs like /foo/*/bar*/*.log)
|
31
|
+
#
|
32
|
+
# Reasons to fix this -
|
33
|
+
# This will likely perform badly on globs that result in a large number of
|
34
|
+
# files.
|
16
35
|
EM.next_tick do
|
17
36
|
find_files
|
18
37
|
EM.add_periodic_timer(interval) do
|
@@ -21,44 +40,91 @@ class EventMachine::FileGlobWatch
|
|
21
40
|
end
|
22
41
|
end # def initialize
|
23
42
|
|
43
|
+
private
|
24
44
|
def find_files
|
45
|
+
@logger.info("Searching for files in #{@pathglob}")
|
25
46
|
list = Set.new(Dir.glob(@pathglob))
|
26
47
|
list.each do |path|
|
27
48
|
next if @files.include?(path)
|
28
|
-
|
49
|
+
add(path)
|
29
50
|
end
|
30
51
|
|
31
52
|
(@files - list).each do |missing|
|
32
|
-
|
33
|
-
@handler.file_removed(missing)
|
53
|
+
remove(missing)
|
34
54
|
end
|
35
55
|
end # def find_files
|
36
56
|
|
37
|
-
|
57
|
+
public
|
58
|
+
def remove(path)
|
59
|
+
@files.delete(path)
|
60
|
+
@watches.delete(path)
|
61
|
+
file_removed(path)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def add(path)
|
38
66
|
@logger.info "Watching #{path}"
|
39
67
|
@files.add(path)
|
40
|
-
|
68
|
+
|
69
|
+
# If EventMachine::watch_file fails, that's ok, I guess.
|
70
|
+
# We'll still find the file 'missing' from the next glob attempt.
|
71
|
+
begin
|
72
|
+
@watches[path] = EventMachine::watch_file(path, GlobFileWatch, self)
|
73
|
+
rescue Errno::EACCES => e
|
74
|
+
@logger.warn(e)
|
75
|
+
end
|
76
|
+
file_found(path)
|
41
77
|
end # def watch
|
78
|
+
|
79
|
+
private
|
80
|
+
class GlobFileWatch < EventMachine::FileWatch
|
81
|
+
def initialize(globwatch)
|
82
|
+
@globwatch = globwatch
|
83
|
+
end
|
84
|
+
|
85
|
+
def file_moved
|
86
|
+
stop_watching
|
87
|
+
@globwatch.remove(path)
|
88
|
+
end
|
89
|
+
|
90
|
+
def file_deleted
|
91
|
+
@globwatch.remove(path)
|
92
|
+
end
|
93
|
+
end # class GlobFileWatch < EventMachine::FileWatch
|
42
94
|
end # class EventMachine::FileGlobWatch
|
43
95
|
|
44
|
-
class EventMachine::FileGlobWatchTail
|
45
|
-
def initialize(handler=nil, *args)
|
96
|
+
class EventMachine::FileGlobWatchTail < EventMachine::FileGlobWatch
|
97
|
+
def initialize(path, handler=nil, *args)
|
98
|
+
super(path, *args)
|
46
99
|
@handler = handler
|
47
100
|
@args = args
|
48
101
|
end
|
49
102
|
|
50
103
|
def file_found(path)
|
51
|
-
|
104
|
+
begin
|
105
|
+
EventMachine::file_tail(path, @handler, *@args)
|
106
|
+
rescue Errno::EACCES => e
|
107
|
+
file_error(path, e)
|
108
|
+
rescue Errno::EISDIR => e
|
109
|
+
file_error(path, e)
|
110
|
+
end
|
52
111
|
end
|
53
112
|
|
54
113
|
def file_removed(path)
|
55
114
|
# Nothing to do
|
56
115
|
end
|
116
|
+
|
117
|
+
def file_error(path, e)
|
118
|
+
$stderr.puts "#{e.class} while trying to tail #{path}"
|
119
|
+
# Ignore by default
|
120
|
+
end
|
57
121
|
end # class EventMachine::FileGlobWatchHandler
|
58
122
|
|
123
|
+
# Add EventMachine::glob_tail
|
59
124
|
module EventMachine
|
60
125
|
def self.glob_tail(glob, handler=nil, *args)
|
61
|
-
handler = EventMachine::
|
126
|
+
handler = EventMachine::FileGlobWatch if handler == nil
|
127
|
+
args.unshift(glob)
|
62
128
|
klass = klass_from_handler(EventMachine::FileGlobWatchTail, handler, *args)
|
63
129
|
c = klass.new(*args)
|
64
130
|
yield c if block_given?
|
data/samples/glob-tail.rb
CHANGED
@@ -38,9 +38,11 @@ def main(args)
|
|
38
38
|
end
|
39
39
|
|
40
40
|
EventMachine.run do
|
41
|
-
handler = EventMachine::FileGlobWatchTail.new(Reader)
|
41
|
+
#handler = EventMachine::FileGlobWatchTail.new(Reader)
|
42
42
|
args.each do |path|
|
43
|
-
EventMachine::
|
43
|
+
EventMachine::FileGlobWatchTail.new(path, Reader)
|
44
|
+
#EventMachine::FileGlobWatch.new(path, handler)
|
45
|
+
#EventMachine::file_tail(path, handler)
|
44
46
|
end
|
45
47
|
end
|
46
48
|
end # def main
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "eventmachine"
|
5
|
+
require "eventmachine-tail"
|
6
|
+
|
7
|
+
class Watcher < EventMachine::FileGlobWatch
|
8
|
+
def initialize(pathglob, interval=5)
|
9
|
+
super(pathglob, interval)
|
10
|
+
end
|
11
|
+
|
12
|
+
def file_removed(path)
|
13
|
+
puts "Removed: #{path}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def file_found(path)
|
17
|
+
puts "Found: #{path}"
|
18
|
+
end
|
19
|
+
end # class Watcher
|
20
|
+
|
21
|
+
EM.run do
|
22
|
+
Watcher.new("/var/log/*")
|
23
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
$:.unshift "#{File.dirname(__FILE__)}/../lib"
|
5
|
+
require 'eventmachine-tail'
|
6
|
+
require 'tempfile'
|
7
|
+
require 'test/unit'
|
8
|
+
require 'timeout'
|
9
|
+
|
10
|
+
|
11
|
+
# Generate some data
|
12
|
+
DATA = (1..10).collect { |i| rand.to_s }
|
13
|
+
SLEEPMAX = 2
|
14
|
+
|
15
|
+
class Reader < EventMachine::FileTail
|
16
|
+
def initialize(path, startpos=-1, testobj=nil)
|
17
|
+
super(path, startpos)
|
18
|
+
@data = DATA.clone
|
19
|
+
@buffer = BufferedTokenizer.new
|
20
|
+
@testobj = testobj
|
21
|
+
@lineno = 0
|
22
|
+
end # def initialize
|
23
|
+
|
24
|
+
def receive_data(data)
|
25
|
+
@buffer.extract(data).each do |line|
|
26
|
+
@lineno += 1
|
27
|
+
expected = @data.shift
|
28
|
+
@testobj.assert_equal(expected, line,
|
29
|
+
"Expected '#{expected}' on line #{@lineno}, but got '#{line}'")
|
30
|
+
if @data.length == 0
|
31
|
+
EM.stop_event_loop
|
32
|
+
end
|
33
|
+
end # @buffer.extract
|
34
|
+
end # def receive_data
|
35
|
+
end # class Reader
|
36
|
+
|
37
|
+
class TestFileTail < Test::Unit::TestCase
|
38
|
+
def abort_after_timeout(seconds)
|
39
|
+
EM::Timer.new(seconds) do
|
40
|
+
EM.stop_event_loop
|
41
|
+
flunk("Timeout (#{seconds} seconds) while running tests. Failing.")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# This test should run slow. We are trying to ensure that
|
46
|
+
# our file_tail correctly reads data slowly fed into the file
|
47
|
+
# as 'tail -f' would.
|
48
|
+
def test_filetail
|
49
|
+
tmp = Tempfile.new("testfiletail")
|
50
|
+
data = DATA.clone
|
51
|
+
EM.run do
|
52
|
+
abort_after_timeout(DATA.length * SLEEPMAX + 10)
|
53
|
+
|
54
|
+
EM::file_tail(tmp.path, Reader, -1, self)
|
55
|
+
timer = EM::PeriodicTimer.new(0.2) do
|
56
|
+
tmp.puts data.shift
|
57
|
+
tmp.flush
|
58
|
+
sleep(rand * SLEEPMAX)
|
59
|
+
timer.cancel if data.length == 0
|
60
|
+
end
|
61
|
+
end # EM.run
|
62
|
+
end # def test_filetail
|
63
|
+
|
64
|
+
def test_filetail_with_seek
|
65
|
+
tmp = Tempfile.new("testfiletail")
|
66
|
+
data = DATA.clone
|
67
|
+
data.each { |i| tmp.puts i }
|
68
|
+
tmp.flush
|
69
|
+
EM.run do
|
70
|
+
abort_after_timeout(2)
|
71
|
+
|
72
|
+
# Set startpos of 0 (beginning of file)
|
73
|
+
EM::file_tail(tmp.path, Reader, 0, self)
|
74
|
+
end # EM.run
|
75
|
+
end # def test_filetail
|
76
|
+
end # class TestFileTail
|
77
|
+
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eventmachine-tail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 20100505022629
|
9
|
+
version: 0.1.20100505022629
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Jordan Sissel
|
@@ -9,20 +14,22 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-
|
17
|
+
date: 2010-05-05 00:00:00 -07:00
|
13
18
|
default_executable:
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
21
|
name: eventmachine
|
17
|
-
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
24
|
requirements:
|
21
25
|
- - ">="
|
22
26
|
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
23
29
|
version: "0"
|
24
|
-
|
25
|
-
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
description: Add file 'tail' implemented with EventMachine. Also includes a 'glob watch' class for watching a directory pattern for new matches, like /var/log/*.log
|
26
33
|
email: jls@semicomplete.com
|
27
34
|
executables: []
|
28
35
|
|
@@ -36,6 +43,8 @@ files:
|
|
36
43
|
- lib/em/globwatcher.rb
|
37
44
|
- samples/tail.rb
|
38
45
|
- samples/glob-tail.rb
|
46
|
+
- samples/globwatch.rb
|
47
|
+
- test/test_filetail.rb
|
39
48
|
has_rdoc: true
|
40
49
|
homepage: http://code.google.com/p/semicomplete/wiki/EventMachineTail
|
41
50
|
licenses: []
|
@@ -50,18 +59,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
59
|
requirements:
|
51
60
|
- - ">="
|
52
61
|
- !ruby/object:Gem::Version
|
62
|
+
segments:
|
63
|
+
- 0
|
53
64
|
version: "0"
|
54
|
-
version:
|
55
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
66
|
requirements:
|
57
67
|
- - ">="
|
58
68
|
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
59
71
|
version: "0"
|
60
|
-
version:
|
61
72
|
requirements: []
|
62
73
|
|
63
74
|
rubyforge_project:
|
64
|
-
rubygems_version: 1.3.
|
75
|
+
rubygems_version: 1.3.6
|
65
76
|
signing_key:
|
66
77
|
specification_version: 3
|
67
78
|
summary: eventmachine tail - a file tail implementation
|