eventmachine-tail 0.2.20100516235116 → 0.2.20100517011408
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/em/filetail.rb +62 -30
- data/lib/em/globwatcher.rb +118 -32
- data/samples/globwatch.rb +1 -1
- data/test/alltests.rb +2 -0
- data/test/test_glob.rb +1 -1
- metadata +4 -3
data/lib/em/filetail.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require "rubygems" if __FILE__ == $0
|
4
3
|
require "eventmachine"
|
5
4
|
require "logger"
|
6
5
|
|
@@ -9,7 +8,7 @@ EventMachine.epoll if EventMachine.epoll?
|
|
9
8
|
# Tail a file.
|
10
9
|
#
|
11
10
|
# Example
|
12
|
-
# class Tailer < EventMachine::
|
11
|
+
# class Tailer < EventMachine::FileTail
|
13
12
|
# def receive_data(data)
|
14
13
|
# puts "Got #{data.length} bytes"
|
15
14
|
# end
|
@@ -24,18 +23,26 @@ EventMachine.epoll if EventMachine.epoll?
|
|
24
23
|
# EM.run do
|
25
24
|
# Tailer.new("/var/log/messages")
|
26
25
|
# end
|
26
|
+
#
|
27
|
+
# See also: EventMachine::FileTail#receive_data
|
27
28
|
class EventMachine::FileTail
|
29
|
+
# Maximum size to read at a time from a single file.
|
28
30
|
CHUNKSIZE = 65536
|
29
|
-
MAXSLEEP = 2
|
30
31
|
|
32
|
+
#
|
33
|
+
#MAXSLEEP = 2
|
34
|
+
|
35
|
+
# The path of the file being tailed
|
31
36
|
attr_reader :path
|
32
37
|
|
33
38
|
# Tail a file
|
34
39
|
#
|
35
|
-
# path is a string file path
|
36
|
-
# startpos is an offset to start tailing the file at. If -1, start at end of
|
40
|
+
# * path is a string file path to tail
|
41
|
+
# * startpos is an offset to start tailing the file at. If -1, start at end of
|
37
42
|
# file.
|
38
43
|
#
|
44
|
+
# See also: EventMachine::file_tail
|
45
|
+
#
|
39
46
|
public
|
40
47
|
def initialize(path, startpos=-1)
|
41
48
|
@path = path
|
@@ -51,7 +58,7 @@ class EventMachine::FileTail
|
|
51
58
|
end
|
52
59
|
|
53
60
|
open
|
54
|
-
watch
|
61
|
+
watch { |what| notify(what) }
|
55
62
|
if (startpos == -1)
|
56
63
|
@file.sysseek(0, IO::SEEK_END)
|
57
64
|
else
|
@@ -60,11 +67,33 @@ class EventMachine::FileTail
|
|
60
67
|
end
|
61
68
|
end # def initialize
|
62
69
|
|
63
|
-
#
|
64
|
-
#
|
70
|
+
# This method is called when a tailed file has data read.
|
71
|
+
#
|
72
|
+
# * data - string data read from the file.
|
73
|
+
#
|
74
|
+
# If you want to read lines from your file, you should use BufferedTokenizer
|
75
|
+
# (which comes with EventMachine):
|
76
|
+
# class Tailer < EventMachine::FileTail
|
77
|
+
# def initialize(*args)
|
78
|
+
# super(*args)
|
79
|
+
# @buffer = BufferedTokenizer.new
|
80
|
+
# end
|
65
81
|
#
|
66
|
-
#
|
82
|
+
# def receive_data(data)
|
83
|
+
# @buffer.extract(data).each do |line|
|
84
|
+
# # do something with 'line'
|
85
|
+
# end
|
86
|
+
# end
|
67
87
|
public
|
88
|
+
def receive_data(data)
|
89
|
+
raise NotImplementedError.new("#{self.class.name}#receive_data is not "\
|
90
|
+
"implemented. Did you forget to implement this in your subclass or "\
|
91
|
+
"module?")
|
92
|
+
end # def receive_data
|
93
|
+
|
94
|
+
# notify is invoked when the file you are tailing has been modified or
|
95
|
+
# otherwise needs to be acted on.
|
96
|
+
private
|
68
97
|
def notify(status)
|
69
98
|
@logger.debug("#{status} on #{path}")
|
70
99
|
if status == :modified
|
@@ -75,11 +104,7 @@ class EventMachine::FileTail
|
|
75
104
|
end
|
76
105
|
end
|
77
106
|
|
78
|
-
|
79
|
-
def receive_data(data)
|
80
|
-
@logger.warn("Got #{data.length} bytes")
|
81
|
-
end
|
82
|
-
|
107
|
+
# Open (or reopen, if necessary) our file and schedule a read.
|
83
108
|
private
|
84
109
|
def open
|
85
110
|
@file.close if @file
|
@@ -95,11 +120,13 @@ class EventMachine::FileTail
|
|
95
120
|
schedule_next_read
|
96
121
|
end
|
97
122
|
|
123
|
+
# Watch our file.
|
98
124
|
private
|
99
|
-
def watch
|
100
|
-
EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher,
|
125
|
+
def watch(&block)
|
126
|
+
EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, block)
|
101
127
|
end
|
102
128
|
|
129
|
+
# Schedule a read.
|
103
130
|
private
|
104
131
|
def schedule_next_read
|
105
132
|
EventMachine::add_timer(@naptime) do
|
@@ -107,6 +134,7 @@ class EventMachine::FileTail
|
|
107
134
|
end
|
108
135
|
end
|
109
136
|
|
137
|
+
# Read CHUNKSIZE from our file and pass it to .receive_data()
|
110
138
|
private
|
111
139
|
def read
|
112
140
|
begin
|
@@ -132,11 +160,12 @@ class EventMachine::FileTail
|
|
132
160
|
#@logger.info("EOF. Naptime: #{@naptime}")
|
133
161
|
#end
|
134
162
|
|
135
|
-
# TODO(sissel): schedule an fstat instead of doing it now
|
163
|
+
# TODO(sissel): should we schedule an fstat instead of doing it now?
|
136
164
|
fstat = File.stat(@path)
|
137
165
|
handle_fstat(fstat)
|
138
166
|
end # def eof
|
139
167
|
|
168
|
+
# Handle fstat changes appropriately.
|
140
169
|
private
|
141
170
|
def handle_fstat(fstat)
|
142
171
|
if (fstat.ino != @fstat.ino)
|
@@ -144,6 +173,7 @@ class EventMachine::FileTail
|
|
144
173
|
elsif (fstat.rdev != @fstat.rdev)
|
145
174
|
open # Reopen if the filesystem device changed
|
146
175
|
elsif (fstat.size < @fstat.size)
|
176
|
+
# Schedule a read if the file size has changed
|
147
177
|
@logger.info("File likely truncated... #{path}")
|
148
178
|
@file.sysseek(0, IO::SEEK_SET)
|
149
179
|
schedule_next_read
|
@@ -152,30 +182,32 @@ class EventMachine::FileTail
|
|
152
182
|
end # def eof
|
153
183
|
end # class EventMachine::FileTail
|
154
184
|
|
155
|
-
# Internal usage only
|
185
|
+
# Internal usage only. This class is used by EventMachine::FileTail
|
186
|
+
# to watch files you are tailing.
|
187
|
+
#
|
188
|
+
# See also: EventMachine::FileTail#watch
|
156
189
|
class EventMachine::FileTail::FileWatcher < EventMachine::FileWatch
|
157
|
-
def initialize(
|
158
|
-
@filewatch = filewatch
|
190
|
+
def initialize(block)
|
159
191
|
@logger = Logger.new(STDOUT)
|
160
192
|
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
161
|
-
@
|
162
|
-
end
|
193
|
+
@callback = block
|
194
|
+
end # def initialize
|
163
195
|
|
164
196
|
def file_modified
|
165
|
-
@
|
166
|
-
end
|
197
|
+
@callback.call(:modified)
|
198
|
+
end # def file_modified
|
167
199
|
|
168
200
|
def file_moved
|
169
|
-
@
|
170
|
-
end
|
201
|
+
@callback.call(:moved)
|
202
|
+
end # def file_moved
|
171
203
|
|
172
204
|
def file_deleted
|
173
|
-
@
|
174
|
-
end
|
205
|
+
@callback.call(:deleted)
|
206
|
+
end # def file_deleted
|
175
207
|
|
176
208
|
def unbind
|
177
|
-
@
|
178
|
-
end
|
209
|
+
@callback.call(:unbind)
|
210
|
+
end # def unbind
|
179
211
|
end # class EventMachine::FileTail::FileWatch < EventMachine::FileWatch
|
180
212
|
|
181
213
|
# Add EventMachine::file_tail
|
data/lib/em/globwatcher.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "em/filetail"
|
4
4
|
require "eventmachine"
|
5
5
|
require "logger"
|
6
|
-
require "
|
6
|
+
require "set"
|
7
|
+
|
8
|
+
EventMachine.epoll if EventMachine.epoll?
|
7
9
|
|
8
10
|
# A file glob pattern watcher for EventMachine.
|
9
11
|
#
|
@@ -16,10 +18,22 @@ require "em/filetail"
|
|
16
18
|
# This class will allow you to get notified whenever a file
|
17
19
|
# is created or deleted that matches your glob.
|
18
20
|
#
|
21
|
+
# If you are subclassing, here are the methods you should implement:
|
22
|
+
# file_found(path)
|
23
|
+
# file_deleted(path)
|
24
|
+
#
|
25
|
+
# See alsoe
|
26
|
+
# * EventMachine::watch_glob
|
27
|
+
# * EventMachine::FileGlobWatch#file_found
|
28
|
+
# * EventMachine::FileGlobWatch#file_deleted
|
19
29
|
#
|
20
30
|
class EventMachine::FileGlobWatch
|
21
|
-
|
22
|
-
|
31
|
+
# Watch a glob
|
32
|
+
#
|
33
|
+
# * glob - a string path or glob, such as "/var/log/*.log"
|
34
|
+
# * interval - number of seconds between scanning the glob for changes
|
35
|
+
def initialize(glob, interval=60)
|
36
|
+
@glob = glob
|
23
37
|
@files = Set.new
|
24
38
|
@watches = Hash.new
|
25
39
|
@logger = Logger.new(STDOUT)
|
@@ -36,13 +50,39 @@ class EventMachine::FileGlobWatch
|
|
36
50
|
EM.add_periodic_timer(interval) do
|
37
51
|
find_files
|
38
52
|
end
|
39
|
-
end
|
53
|
+
end # EM.next_tick
|
40
54
|
end # def initialize
|
41
55
|
|
56
|
+
# This method is called when a new file is found
|
57
|
+
#
|
58
|
+
# * path - the string path of the file found
|
59
|
+
#
|
60
|
+
# You must implement this in your subclass or module for it
|
61
|
+
# to work with EventMachine::watch_glob
|
62
|
+
public
|
63
|
+
def file_found(path)
|
64
|
+
raise NotImplementedError.new("#{self.class.name}#file_found is not "\
|
65
|
+
"implemented. Did you forget to implement this in your subclass or "\
|
66
|
+
"module?")
|
67
|
+
end # def file_found
|
68
|
+
|
69
|
+
# This method is called when a file is deleted.
|
70
|
+
#
|
71
|
+
# * path - the string path of the file deleted
|
72
|
+
#
|
73
|
+
# You must implement this in your subclass or module for it
|
74
|
+
# to work with EventMachine::watch_glob
|
75
|
+
public
|
76
|
+
def file_deleted(path)
|
77
|
+
raise NotImplementedError.new("#{self.class.name}#file_found is not "\
|
78
|
+
"implemented. Did you forget to implement this in your subclass or "\
|
79
|
+
"module?")
|
80
|
+
end # def file_found
|
81
|
+
|
42
82
|
private
|
43
83
|
def find_files
|
44
|
-
@logger.info("Searching for files in #{@
|
45
|
-
list = Set.new(Dir.glob(@
|
84
|
+
@logger.info("Searching for files in #{@glob}")
|
85
|
+
list = Set.new(Dir.glob(@glob))
|
46
86
|
list.each do |path|
|
47
87
|
next if @files.include?(path)
|
48
88
|
add(path)
|
@@ -53,56 +93,82 @@ class EventMachine::FileGlobWatch
|
|
53
93
|
end
|
54
94
|
end # def find_files
|
55
95
|
|
56
|
-
|
96
|
+
# Remove a file from being watched and notify file_deleted()
|
97
|
+
private
|
57
98
|
def remove(path)
|
58
99
|
@files.delete(path)
|
59
100
|
@watches.delete(path)
|
60
|
-
|
61
|
-
end
|
101
|
+
file_deleted(path)
|
102
|
+
end # def remove
|
62
103
|
|
104
|
+
# Add a file to watch and notify file_found()
|
63
105
|
private
|
64
106
|
def add(path)
|
65
107
|
@files.add(path)
|
66
108
|
|
67
109
|
# If EventMachine::watch_file fails, that's ok, I guess.
|
68
110
|
# We'll still find the file 'missing' from the next glob attempt.
|
69
|
-
begin
|
111
|
+
#begin
|
70
112
|
# EM currently has a bug that only the first handler for a watch_file
|
71
113
|
# on each file gets events. This causes globtails to never get data
|
72
114
|
# since the glob is watching the file already.
|
73
115
|
# Until we fix that, let's skip file watching here.
|
74
|
-
#@watches[path] = EventMachine::watch_file(path,
|
75
|
-
|
76
|
-
|
77
|
-
|
116
|
+
#@watches[path] = EventMachine::watch_file(path, FileWatcher, self) do |path|
|
117
|
+
# remove(path)
|
118
|
+
#end
|
119
|
+
#rescue Errno::EACCES => e
|
120
|
+
#@logger.warn(e)
|
121
|
+
#end
|
78
122
|
file_found(path)
|
79
123
|
end # def watch
|
80
124
|
|
81
125
|
private
|
82
|
-
class
|
83
|
-
def initialize(globwatch)
|
126
|
+
class FileWatcher < EventMachine::FileWatch
|
127
|
+
def initialize(globwatch, &block)
|
84
128
|
@globwatch = globwatch
|
129
|
+
@block = block
|
85
130
|
end
|
86
131
|
|
87
132
|
def file_moved
|
88
133
|
stop_watching
|
89
|
-
|
134
|
+
block.call path
|
90
135
|
end
|
91
136
|
|
92
137
|
def file_deleted
|
93
|
-
|
138
|
+
block.call path
|
94
139
|
end
|
95
|
-
end # class
|
140
|
+
end # class EventMachine::FileGlobWatch::FileWatcher < EventMachine::FileWatch
|
96
141
|
end # class EventMachine::FileGlobWatch
|
97
142
|
|
143
|
+
# A glob tailer for EventMachine
|
144
|
+
#
|
145
|
+
# This class combines features of EventMachine::file_tail and
|
146
|
+
# EventMachine::watch_glob.
|
147
|
+
#
|
148
|
+
# You won't generally subclass this class (See EventMachine::FileGlobWatch)
|
149
|
+
#
|
150
|
+
# See also: EventMachine::glob_tail
|
151
|
+
#
|
98
152
|
class EventMachine::FileGlobWatchTail < EventMachine::FileGlobWatch
|
153
|
+
# Initialize a new file glob tail.
|
154
|
+
#
|
155
|
+
# path should be a glob or file path.
|
156
|
+
# handler should be a module or subclass of EventMachine::FileTail
|
157
|
+
# See also EventMachine::file_tail
|
158
|
+
# interval is how often (seconds) the glob path should be scanned
|
159
|
+
# exclude is an array of Regexp (or anything with .match) for
|
160
|
+
# excluding from things to tail
|
161
|
+
# The remainder of arguments are passed to EventMachine::file_tail as
|
162
|
+
# EventMachine::file_tail(path_found, handler, *args)
|
163
|
+
public
|
99
164
|
def initialize(path, handler=nil, interval=60, exclude=[], *args)
|
100
165
|
super(path, interval)
|
101
166
|
@handler = handler
|
102
167
|
@args = args
|
103
168
|
@exclude = exclude
|
104
|
-
end
|
169
|
+
end # def initialize
|
105
170
|
|
171
|
+
public
|
106
172
|
def file_found(path)
|
107
173
|
begin
|
108
174
|
@logger.info "#{self.class}: Trying #{path}"
|
@@ -120,25 +186,32 @@ class EventMachine::FileGlobWatchTail < EventMachine::FileGlobWatch
|
|
120
186
|
file_error(path, e)
|
121
187
|
rescue Errno::EISDIR => e
|
122
188
|
file_error(path, e)
|
123
|
-
end
|
124
|
-
end
|
189
|
+
end
|
190
|
+
end # def file_found
|
125
191
|
|
192
|
+
public
|
126
193
|
def file_excluded(path)
|
127
194
|
@logger.info "#{self.class}: Skipping path #{path} due to exclude rule"
|
128
|
-
end
|
195
|
+
end # def file_excluded
|
129
196
|
|
130
|
-
|
197
|
+
public
|
198
|
+
def file_deleted(path)
|
131
199
|
# Nothing to do
|
132
|
-
end
|
200
|
+
end # def file_deleted
|
133
201
|
|
202
|
+
public
|
134
203
|
def file_error(path, e)
|
135
204
|
$stderr.puts "#{e.class} while trying to tail #{path}"
|
136
205
|
# otherwise, drop the error by default
|
137
|
-
end
|
206
|
+
end # def file_error
|
138
207
|
end # class EventMachine::FileGlobWatchHandler
|
139
208
|
|
140
|
-
# Add EventMachine::glob_tail
|
141
209
|
module EventMachine
|
210
|
+
# Watch a glob and tail any files found.
|
211
|
+
#
|
212
|
+
# 'glob' should be a string path or glob, such as /var/log/*.log
|
213
|
+
# handler must be a module or subclass of EventMachine::FileGlobWatchTail
|
214
|
+
# See EventMachine::FileGlobWatchTail for the callback methods.
|
142
215
|
def self.glob_tail(glob, handler=nil, *args)
|
143
216
|
handler = EventMachine::FileGlobWatch if handler == nil
|
144
217
|
args.unshift(glob)
|
@@ -146,12 +219,25 @@ module EventMachine
|
|
146
219
|
c = klass.new(*args)
|
147
220
|
yield c if block_given?
|
148
221
|
return c
|
149
|
-
end
|
150
|
-
|
151
|
-
|
222
|
+
end
|
223
|
+
|
224
|
+
# Watch a glob for any files.
|
225
|
+
#
|
226
|
+
# * glob - a string path or glob, such as "/var/log/*.log"
|
227
|
+
# * handler - must be a module or a subclass of EventMachine::FileGlobWatch
|
228
|
+
#
|
229
|
+
# The remaining (optional) arguments are passed to your handler like this:
|
230
|
+
# If you call this:
|
231
|
+
# EventMachine.watch_glob("/var/log/*.log", YourHandler, 1, 2, 3, ...)
|
232
|
+
# This will be invoked when new matching files are found:
|
233
|
+
# YourHandler.new(path_found, 1, 2, 3, ...)
|
234
|
+
# ^ path_found is the new path found by the glob
|
235
|
+
#
|
236
|
+
# See EventMachine::FileGlobWatch for the callback methods.
|
237
|
+
def self.watch_glob(glob, handler=nil, *args)
|
152
238
|
# This code mostly styled on what EventMachine does in many of it's other
|
153
239
|
# methods.
|
154
|
-
args = [
|
240
|
+
args = [glob, *args]
|
155
241
|
klass = klass_from_handler(EventMachine::FileGlobWatch, handler, *args);
|
156
242
|
c = klass.new(*args)
|
157
243
|
yield c if block_given?
|
data/samples/globwatch.rb
CHANGED
data/test/alltests.rb
ADDED
data/test/test_glob.rb
CHANGED
@@ -25,7 +25,7 @@ class Watcher < EventMachine::FileGlobWatch
|
|
25
25
|
@testobj.finish if @data.length == 0
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
28
|
+
def file_deleted(path)
|
29
29
|
@testobj.assert(@data.include?(path), "Expected #{path} in \n#{@data.join("\n")}")
|
30
30
|
@data.delete(path)
|
31
31
|
@testobj.finish if @data.length == 0
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
version: 0.2.
|
8
|
+
- 20100517011408
|
9
|
+
version: 0.2.20100517011408
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jordan Sissel
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-05-
|
17
|
+
date: 2010-05-17 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -46,6 +46,7 @@ files:
|
|
46
46
|
- samples/globwatch.rb
|
47
47
|
- test/test_filetail.rb
|
48
48
|
- test/test_glob.rb
|
49
|
+
- test/alltests.rb
|
49
50
|
- test/testcase_helpers.rb
|
50
51
|
- bin/rtail
|
51
52
|
has_rdoc: true
|