eventmachine-tail 0.0.1 → 0.6.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.
@@ -1,87 +1,280 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "rubygems" if __FILE__ == $0
4
- require "set"
3
+ require "em/filetail"
5
4
  require "eventmachine"
6
- require "ap"
7
5
  require "logger"
6
+ require "set"
8
7
 
9
- require "filetail"
8
+ EventMachine.epoll if EventMachine.epoll?
9
+ EventMachine.kqueue = true if EventMachine.kqueue?
10
10
 
11
+ # A file glob pattern watcher for EventMachine.
12
+ #
13
+ # If you are unfamiliar with globs, see Wikipedia:
14
+ # http://en.wikipedia.org/wiki/Glob_(programming)
15
+ #
16
+ # Any glob supported by Dir#glob will work with
17
+ # this class.
18
+ #
19
+ # This class will allow you to get notified whenever a file
20
+ # is created or deleted that matches your glob.
21
+ #
22
+ # If you are subclassing, here are the methods you should implement:
23
+ # file_found(path)
24
+ # file_deleted(path)
25
+ #
26
+ # See alsoe
27
+ # * EventMachine::watch_glob
28
+ # * EventMachine::FileGlobWatch#file_found
29
+ # * EventMachine::FileGlobWatch#file_deleted
30
+ #
11
31
  class EventMachine::FileGlobWatch
12
- def initialize(pathglob, handler, interval=60)
13
- @pathglob = pathglob
14
- @handler = handler
15
- @files = Set.new
32
+ # Watch a glob
33
+ #
34
+ # * glob - a string path or glob, such as "/var/log/*.log"
35
+ # * interval - number of seconds between scanning the glob for changes
36
+ def initialize(glob, interval=60)
37
+ @glob = glob
38
+ @files = Hash.new
39
+ @watches = Hash.new
40
+ @logger = Logger.new(STDOUT)
41
+ @logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
16
42
 
43
+ # We periodically check here because it is easier than writing our own glob
44
+ # parser (so we can smartly watch globs like /foo/*/bar*/*.log)
45
+ #
46
+ # Reasons to fix this -
47
+ # This will likely perform badly on globs that result in a large number of
48
+ # files.
17
49
  EM.next_tick do
18
50
  find_files
19
51
  EM.add_periodic_timer(interval) do
20
52
  find_files
21
53
  end
22
- end
54
+ end # EM.next_tick
23
55
  end # def initialize
24
56
 
57
+ # This method is called when a new file is found
58
+ #
59
+ # * path - the string path of the file found
60
+ #
61
+ # You must implement this in your subclass or module for it
62
+ # to work with EventMachine::watch_glob
63
+ public
64
+ def file_found(path)
65
+ raise NotImplementedError.new("#{self.class.name}#file_found is not "\
66
+ "implemented. Did you forget to implement this in your subclass or "\
67
+ "module?")
68
+ end # def file_found
69
+
70
+ # This method is called when a file is deleted.
71
+ #
72
+ # * path - the string path of the file deleted
73
+ #
74
+ # You must implement this in your subclass or module for it
75
+ # to work with EventMachine::watch_glob
76
+ public
77
+ def file_deleted(path)
78
+ raise NotImplementedError.new("#{self.class.name}#file_deleted is not "\
79
+ "implemented. Did you forget to implement this in your subclass or "\
80
+ "module?")
81
+ end # def file_found
82
+
83
+ private
25
84
  def find_files
26
- list = Set.new(Dir.glob(@pathglob))
85
+ @logger.info("Searching for files in #{@glob}")
86
+ list = Dir.glob(@glob)
87
+
88
+ known_files = @files.clone
27
89
  list.each do |path|
28
- next if @files.include?(path)
29
- watch(path)
90
+ fileinfo = FileInfo.new(path) rescue next
91
+ # Skip files that have the same inode (renamed or hardlinked)
92
+ known_files.delete(fileinfo.stat.ino)
93
+ next if @files.include?(fileinfo.stat.ino)
94
+
95
+ track(fileinfo)
96
+ file_found(path)
30
97
  end
31
98
 
32
- (@files - list).each do |missing|
33
- @files.delete(missing)
34
- @handler.file_removed(missing)
99
+ # Report missing files.
100
+ known_files.each do |inode, fileinfo|
101
+ remove(fileinfo)
35
102
  end
36
103
  end # def find_files
37
104
 
38
- def watch(path)
39
- puts "Watching #{path}"
40
- @files.add(path)
41
- @handler.file_found(path)
105
+ # Remove a file from being watched and notify file_deleted()
106
+ private
107
+ def remove(fileinfo)
108
+ @files.delete(fileinfo.stat.ino)
109
+ @watches.delete(fileinfo.path)
110
+ file_deleted(fileinfo.path)
111
+ end # def remove
112
+
113
+ # Add a file to watch and notify file_found()
114
+ private
115
+ def track(fileinfo)
116
+ @files[fileinfo.stat.ino] = fileinfo
117
+
118
+ # If EventMachine::watch_file fails, that's ok, I guess.
119
+ # We'll still find the file 'missing' from the next glob attempt.
120
+ #begin
121
+ # EM currently has a bug that only the first handler for a watch_file
122
+ # on each file gets events. This causes globtails to never get data
123
+ # since the glob is watching the file already.
124
+ # Until we fix that, let's skip file watching here.
125
+ #@watches[path] = EventMachine::watch_file(path, FileWatcher, self) do |path|
126
+ # remove(path)
127
+ #end
128
+ #rescue Errno::EACCES => e
129
+ #@logger.warn(e)
130
+ #end
42
131
  end # def watch
132
+
133
+ private
134
+ class FileWatcher < EventMachine::FileWatch
135
+ def initialize(globwatch, &block)
136
+ @globwatch = globwatch
137
+ @block = block
138
+ end
139
+
140
+ def file_moved
141
+ stop_watching
142
+ block.call path
143
+ end
144
+
145
+ def file_deleted
146
+ block.call path
147
+ end
148
+ end # class EventMachine::FileGlobWatch::FileWatcher < EventMachine::FileWatch
149
+
150
+ private
151
+ class FileInfo
152
+ attr_reader :path
153
+ attr_reader :stat
154
+
155
+ def initialize(path)
156
+ @path = path
157
+ @stat = File.stat(path)
158
+ end
159
+ end # class FileInfo
43
160
  end # class EventMachine::FileGlobWatch
44
161
 
45
- class EventMachine::FileGlobWatchHandler
46
- LOGGER = Logger.new(STDOUT)
47
- def initialize(handler=nil)
162
+ # A glob tailer for EventMachine
163
+ #
164
+ # This class combines features of EventMachine::file_tail and
165
+ # EventMachine::watch_glob.
166
+ #
167
+ # You won't generally subclass this class (See EventMachine::FileGlobWatch)
168
+ #
169
+ # See also: EventMachine::glob_tail
170
+ #
171
+ class EventMachine::FileGlobWatchTail < EventMachine::FileGlobWatch
172
+ # Initialize a new file glob tail.
173
+ #
174
+ # * path - glob or file path (string)
175
+ # * handler - a module or subclass of EventMachine::FileTail
176
+ # See also EventMachine::file_tail
177
+ # * interval - how often (seconds) the glob path should be scanned
178
+ # * exclude - an array of Regexp (or anything with .match) for
179
+ # excluding from things to tail
180
+ #
181
+ # The remainder of arguments are passed to EventMachine::file_tail as
182
+ # EventMachine::file_tail(path_found, handler, *args, &block)
183
+ public
184
+ def initialize(path, handler=nil, interval=60, exclude=[], *args, &block)
185
+ super(path, interval)
48
186
  @handler = handler
49
- end
187
+ @args = args
188
+ @exclude = exclude
189
+
190
+ if block_given?
191
+ @handler = block
192
+ end
193
+ end # def initialize
50
194
 
195
+ public
51
196
  def file_found(path)
52
- EventMachine::file_tail(path, @handler)
53
- end
197
+ begin
198
+ @logger.info "#{self.class}: Trying #{path}"
199
+ @exclude.each do |exclude|
200
+ @logger.info "#{self.class}: Testing #{exclude} =~ #{path} == #{exclude.match(path) != nil}"
201
+ if exclude.match(path) != nil
202
+ file_excluded(path)
203
+ return
204
+ end
205
+ end
206
+ @logger.info "#{self.class}: Watching #{path}"
207
+
208
+ if @handler.is_a? Proc
209
+ EventMachine::file_tail(path, nil, *@args, &@handler)
210
+ else
211
+ EventMachine::file_tail(path, @handler, *@args)
212
+ end
213
+ rescue Errno::EACCES => e
214
+ file_error(path, e)
215
+ rescue Errno::EISDIR => e
216
+ file_error(path, e)
217
+ end
218
+ end # def file_found
219
+
220
+ public
221
+ def file_excluded(path)
222
+ @logger.info "#{self.class}: Skipping path #{path} due to exclude rule"
223
+ end # def file_excluded
54
224
 
55
- def file_removed(path)
225
+ public
226
+ def file_deleted(path)
56
227
  # Nothing to do
57
- end
228
+ end # def file_deleted
229
+
230
+ public
231
+ def file_error(path, e)
232
+ $stderr.puts "#{e.class} while trying to tail #{path}"
233
+ # otherwise, drop the error by default
234
+ end # def file_error
58
235
  end # class EventMachine::FileGlobWatchHandler
59
236
 
60
237
  module EventMachine
61
- def self.glob_tail(glob, handler=nil, *args)
62
- handler = EventMachine::FileGlobHandler if handler == nil
63
- klass = klass_from_handler(EventMachine::FileGlobWatchHandler, handler, *args)
238
+ # Watch a glob and tail any files found.
239
+ #
240
+ # * glob - a string path or glob, such as /var/log/*.log
241
+ # * handler - a module or subclass of EventMachine::FileGlobWatchTail.
242
+ # handler can be omitted if you give a block.
243
+ #
244
+ # If you give a block and omit the handler parameter, then the behavior
245
+ # is that your block is called for every line read from any file the same
246
+ # way EventMachine::file_tail does when called with a block.
247
+ #
248
+ # See EventMachine::FileGlobWatchTail for the callback methods.
249
+ # See EventMachine::file_tail for more information about block behavior.
250
+ def self.glob_tail(glob, handler=nil, *args, &block)
251
+ handler = EventMachine::FileGlobWatchTail if handler == nil
252
+ args.unshift(glob)
253
+ klass = klass_from_handler(EventMachine::FileGlobWatchTail, handler, *args)
254
+ c = klass.new(*args, &block)
255
+ return c
256
+ end
257
+
258
+ # Watch a glob for any files.
259
+ #
260
+ # * glob - a string path or glob, such as "/var/log/*.log"
261
+ # * handler - must be a module or a subclass of EventMachine::FileGlobWatch
262
+ #
263
+ # The remaining (optional) arguments are passed to your handler like this:
264
+ # If you call this:
265
+ # EventMachine.watch_glob("/var/log/*.log", YourHandler, 1, 2, 3, ...)
266
+ # This will be invoked when new matching files are found:
267
+ # YourHandler.new(path_found, 1, 2, 3, ...)
268
+ # ^ path_found is the new path found by the glob
269
+ #
270
+ # See EventMachine::FileGlobWatch for the callback methods.
271
+ def self.watch_glob(glob, handler=nil, *args)
272
+ # This code mostly styled on what EventMachine does in many of it's other
273
+ # methods.
274
+ args = [glob, *args]
275
+ klass = klass_from_handler(EventMachine::FileGlobWatch, handler, *args);
64
276
  c = klass.new(*args)
65
277
  yield c if block_given?
66
278
  return c
67
- end
68
- end
69
-
70
- if __FILE__ == $0
71
- class Reader < EventMachine::FileTail
72
- def initialize(*args)
73
- super(*args)
74
- @buffer = BufferedTokenizer.new
75
- end
76
-
77
- def receive_data(data)
78
- @buffer.extract(data).each do |line|
79
- ap [path, line]
80
- end
81
- end
82
- end
83
-
84
- EventMachine.run do
85
- EventMachine::FileGlobWatch.new("/var/log/*.log", EventMachine::FileGlobWatchHandler.new(Reader))
86
- end
87
- end
279
+ end # def EventMachine::watch_glob
280
+ end # module EventMachine
@@ -0,0 +1,2 @@
1
+ require 'em/filetail'
2
+ require 'em/globwatcher'
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Sample that uses eventmachine-tail to watch a file or set of files.
4
+ # Basically, this example implements 'tail -f' but can accept globs
5
+ # that are also watched.
6
+ #
7
+ # For example, '/var/log/*.log' will be periodically watched for new
8
+ # matching files which will additionally be watched.
9
+ #
10
+ # Usage example:
11
+ # glob-tail.rb "/var/log/*.log" "/var/log/httpd/*.log"
12
+ #
13
+ # (Important to use quotes or otherwise escape the '*' chars, otherwise
14
+ # your shell will interpret them)
15
+
16
+ require "rubygems"
17
+ require "eventmachine"
18
+ require "eventmachine-tail"
19
+
20
+ class Reader < EventMachine::FileTail
21
+ def initialize(path, startpos=-1)
22
+ super(path, startpos)
23
+ puts "Tailing #{path}"
24
+ @buffer = BufferedTokenizer.new
25
+ end
26
+
27
+ def receive_data(data)
28
+ @buffer.extract(data).each do |line|
29
+ puts "#{path}: #{line}"
30
+ end
31
+ end
32
+ end
33
+
34
+ def main(args)
35
+ if args.length == 0
36
+ puts "Usage: #{$0} <path_or_glob> [path_or_glob2] [...]"
37
+ return 1
38
+ end
39
+
40
+ EventMachine.run do
41
+ args.each do |path|
42
+ EventMachine::FileGlobWatchTail.new(path, Reader)
43
+ end
44
+ end
45
+ end # def main
46
+
47
+ exit(main(ARGV))
@@ -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_deleted(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,30 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Simple 'tail -f' example. This one uses a block instead of a separate handler
4
+ # class
5
+ # Usage example:
6
+ # tail-with-block.rb /var/log/messages
7
+
8
+ require "rubygems"
9
+ require "eventmachine"
10
+ require "eventmachine-tail"
11
+
12
+ def main(args)
13
+ if args.length == 0
14
+ puts "Usage: #{$0} <path> [path2] [...]"
15
+ return 1
16
+ end
17
+
18
+ EventMachine.run do
19
+ args.each do |path|
20
+ EventMachine::file_tail(path) do |filetail, line|
21
+ # filetail is the 'EventMachine::FileTail' instance for this file.
22
+ # line is the line read from thefile.
23
+ # this block is invoked for every line read.
24
+ puts line
25
+ end
26
+ end
27
+ end
28
+ end # def main
29
+
30
+ exit(main(ARGV))
data/samples/tail.rb ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Simple 'tail -f' example.
4
+ # Usage example:
5
+ # tail.rb /var/log/messages
6
+
7
+ require "rubygems"
8
+ require "eventmachine"
9
+ require "eventmachine-tail"
10
+
11
+ class Reader < EventMachine::FileTail
12
+ def initialize(path, startpos=-1)
13
+ super(path, startpos)
14
+ puts "Tailing #{path}"
15
+ @buffer = BufferedTokenizer.new
16
+ end
17
+
18
+ def receive_data(data)
19
+ @buffer.extract(data).each do |line|
20
+ puts "#{path}: #{line}"
21
+ end
22
+ end
23
+ end
24
+
25
+ def main(args)
26
+ if args.length == 0
27
+ puts "Usage: #{$0} <path> [path2] [...]"
28
+ return 1
29
+ end
30
+
31
+ EventMachine.run do
32
+ args.each do |path|
33
+ EventMachine::file_tail(path, Reader)
34
+ end
35
+ end
36
+ end # def main
37
+
38
+ exit(main(ARGV))
data/test/alltests.rb ADDED
@@ -0,0 +1,5 @@
1
+ $: << File.expand_path('..', __FILE__)
2
+
3
+
4
+ require 'test_filetail'
5
+ require 'test_glob'
@@ -0,0 +1,4 @@
1
+ /tmp/foo/testfile {
2
+ rotate 5
3
+ size 50
4
+ }