filewatch 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/globtail ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "filewatch/tail"
4
+ require "optparse"
5
+
6
+ progname = File.basename($0)
7
+
8
+ config = {
9
+ :verbose => false,
10
+ }
11
+
12
+ opts = OptionParser.new do |opts|
13
+ opts.banner = "#{progname} [-v] [-s path] [-i interval] [-x glob] path/glob ..."
14
+
15
+ opts.on("-v", "--verbose", "Enable verbose/debug output") do
16
+ config[:verbose] = true
17
+ end
18
+
19
+ opts.on("-x", "--exclude PATH", String, "path to exclude from watching") do |path|
20
+ config[:exclude] ||= []
21
+ config[:exclude] << path
22
+ end
23
+
24
+ opts.on("-s", "--sincedb PATH", String, "Sincedb path") do |path|
25
+ config[:sincedb_path] = path
26
+ end
27
+
28
+ opts.on("-i", "--interval SECONDS", Integer,
29
+ "Sincedb write interval") do |path|
30
+ config[:sincedb_write_interval] = path
31
+ end
32
+ end
33
+
34
+ begin
35
+ opts.order!
36
+ rescue OptionParser::InvalidOption
37
+ $stderr.puts "#{progname}: #{$!}"
38
+ $stderr.puts opts.usage
39
+ end
40
+
41
+ logger = Logger.new(STDERR)
42
+ logger.progname = progname
43
+ logger.level = config[:verbose] ? Logger::DEBUG : Logger::INFO
44
+ config[:logger] = logger
45
+
46
+ tail = FileWatch::Tail.new(config)
47
+
48
+ ARGV.each { |path| tail.tail(path) }
49
+
50
+ Signal.trap("EXIT") { tail.sincedb_write("globtail exiting") }
51
+
52
+ $stdout.sync = true
53
+ tail.subscribe do |path, line|
54
+ puts "#{path}: #{line}"
55
+ end
@@ -29,7 +29,7 @@
29
29
  # end
30
30
  # end
31
31
 
32
- class BufferedTokenizer
32
+ module FileWatch; class BufferedTokenizer
33
33
  # New BufferedTokenizers will operate on lines delimited by "\n" by default
34
34
  # or allow you to specify any delimiter token you so choose, which will then
35
35
  # be used by String#split to tokenize the input data
@@ -136,4 +136,4 @@ cter token support.
136
136
  def empty?
137
137
  @input.empty?
138
138
  end
139
- end
139
+ end; end
@@ -1,58 +1,181 @@
1
+ require "filewatch/buftok"
1
2
  require "filewatch/watch"
2
- require "filewatch/namespace"
3
-
4
- class FileWatch::Tail
5
- # This class exists to wrap inotify, kqueue, periodic polling, etc,
6
- # to provide you with a way to watch files and directories.
7
- #
8
- # For now, it only supports inotify.
9
- def initialize
10
- @watch = FileWatch::Watch.new
11
- @files = {}
12
- end
13
-
14
- public
15
- def watch(path, *what_to_watch)
16
- @watch.watch(path, *what_to_watch)
17
-
18
- if File.file?(path)
19
- @files[path] = File.new(path, "r")
20
-
21
- # TODO(sissel): Support 'since'-like support.
22
- # Always start at the end of the file, this may change in the future.
23
- @files[path].sysseek(0, IO::SEEK_END)
24
- end
25
- end # def watch
26
-
27
- def subscribe(handler=nil, &block)
28
- @watch.subscribe(nil) do |event|
29
- path = event.name
30
- if @files.include?(path)
31
- file = @files[path]
32
- event.actions.each do |action|
33
- # call method 'file_action_<action>' like 'file_action_modify'
34
- method = "file_action_#{action}".to_sym
35
- if respond_to?(method)
36
- send(method, file, event, &block)
37
- else
38
- $stderr.puts "Unsupported method #{self.class.name}##{method}"
3
+ require "logger"
4
+
5
+ module FileWatch
6
+ class Tail
7
+ attr_accessor :logger
8
+
9
+ public
10
+ def initialize(opts={})
11
+ if opts[:logger]
12
+ @logger = opts[:logger]
13
+ else
14
+ @logger = Logger.new(STDERR)
15
+ @logger.level = Logger::INFO
16
+ end
17
+ @files = {}
18
+ @buffers = {}
19
+ @watch = FileWatch::Watch.new
20
+ @watch.logger = @logger
21
+ @sincedb = {}
22
+ @sincedb_last_write = 0
23
+ @statcache = {}
24
+ @opts = {
25
+ :sincedb_write_interval => 10,
26
+ :sincedb_path => "#{ENV["HOME"]}/.sincedb",
27
+ :exclude => [],
28
+ }.merge(opts)
29
+ @watch.exclude(@opts[:exclude])
30
+
31
+ _sincedb_open
32
+ end # def initialize
33
+
34
+ public
35
+ def logger=(logger)
36
+ @logger = logger
37
+ @watch.logger = logger
38
+ end # def logger=
39
+
40
+ public
41
+ def tail(path)
42
+ @watch.watch(path)
43
+ end # def tail
44
+
45
+ public
46
+ def subscribe(&block)
47
+ # subscribe(stat_interval = 1, discover_interval = 5, &block)
48
+ @watch.subscribe do |event, path|
49
+ case event
50
+ when :create, :create_initial
51
+ if @files.member?(path)
52
+ @logger.debug("#{event} for #{path}: already exists in @files")
53
+ next
39
54
  end
55
+ _open_file(path, event)
56
+ _read_file(path, &block)
57
+ when :modify
58
+ if !@files.member?(path)
59
+ @logger.debug(":modify for #{path}, does not exist in @files")
60
+ _open_file(path)
61
+ end
62
+ _read_file(path, &block)
63
+ when :delete
64
+ @logger.debug(":delete for #{path}, deleted from @files")
65
+ _read_file(path, &block)
66
+ @files[path].close
67
+ @files.delete(path)
68
+ @statcache.delete(path)
69
+ else
70
+ @logger.warn("unknown event type #{event} for #{path}")
71
+ end
72
+ end # @watch.subscribe
73
+ end # def each
74
+
75
+ private
76
+ def _open_file(path, event)
77
+ @logger.debug("_open_file: #{path}: opening")
78
+ # TODO(petef): handle File.open failing
79
+ begin
80
+ @files[path] = File.open(path)
81
+ rescue Errno::ENOENT
82
+ @logger.warn("#{path}: open: #{$!}")
83
+ @files.delete(path)
84
+ return
85
+ end
86
+
87
+ stat = File::Stat.new(path)
88
+ inode = [stat.ino, stat.dev_major, stat.dev_minor]
89
+ @statcache[path] = inode
90
+
91
+ if @sincedb.member?(inode)
92
+ last_size = @sincedb[inode]
93
+ @logger.debug("#{path}: sincedb last value #{@sincedb[inode]}, cur size #{stat.size}")
94
+ if last_size <= stat.size
95
+ @logger.debug("#{path}: sincedb: seeking to #{last_size}")
96
+ @files[path].sysseek(last_size, IO::SEEK_SET)
97
+ else
98
+ @logger.debug("#{path}: last value size is greater than current value, starting over")
99
+ @sincedb[inode] = 0
40
100
  end
101
+ elsif event == :create_initial && @files[path]
102
+ @logger.debug("#{path}: initial create, no sincedb, seeking to end #{stat.size}")
103
+ @files[path].sysseek(stat.size, IO::SEEK_SET)
104
+ @sincedb[inode] = stat.size
41
105
  else
42
- $stderr.puts "Event on unwatched file: #{event}"
106
+ @logger.debug("#{path}: staying at position 0, no sincedb")
43
107
  end
108
+ end # def _open_file
109
+
110
+ private
111
+ def _read_file(path, &block)
112
+ @buffers[path] ||= FileWatch::BufferedTokenizer.new
113
+
114
+ changed = false
115
+ loop do
116
+ begin
117
+ data = @files[path].read_nonblock(4096)
118
+ changed = true
119
+ @buffers[path].extract(data).each do |line|
120
+ yield(path, line)
121
+ end
122
+
123
+ @sincedb[@statcache[path]] = @files[path].pos
124
+ rescue Errno::EWOULDBLOCK, Errno::EINTR, EOFError
125
+ break
126
+ end
127
+ end
128
+
129
+ if changed
130
+ now = Time.now.to_i
131
+ delta = now - @sincedb_last_write
132
+ if delta >= @opts[:sincedb_write_interval]
133
+ @logger.debug("writing sincedb (delta since last write = #{delta})")
134
+ _sincedb_write
135
+ @sincedb_last_write = now
136
+ end
137
+ end
138
+ end # def _read_file
139
+
140
+ public
141
+ def sincedb_write(reason=nil)
142
+ @logger.debug("caller requested sincedb write (#{reason})")
143
+ _sincedb_write
44
144
  end
45
- end # def subscribe
46
145
 
47
- def file_action_modify(file, event)
48
- loop do
146
+ private
147
+ def _sincedb_open
148
+ path = @opts[:sincedb_path]
49
149
  begin
50
- data = file.sysread(4096)
51
- yield event.name, data
52
- rescue EOFError
53
- break
150
+ db = File.open(path)
151
+ rescue
152
+ @logger.debug("_sincedb_open: #{path}: #{$!}")
153
+ return
54
154
  end
55
- end
56
- end
57
155
 
58
- end # class FileWatch::Tail
156
+ @logger.debug("_sincedb_open: reading from #{path}")
157
+ db.each do |line|
158
+ ino, dev_major, dev_minor, pos = line.split(" ", 4)
159
+ inode = [ino.to_i, dev_major.to_i, dev_minor.to_i]
160
+ @logger.debug("_sincedb_open: setting #{inode.inspect} to #{pos.to_i}")
161
+ @sincedb[inode] = pos.to_i
162
+ end
163
+ end # def _sincedb_open
164
+
165
+ private
166
+ def _sincedb_write
167
+ path = @opts[:sincedb_path]
168
+ begin
169
+ db = File.open(path, "w")
170
+ rescue
171
+ @logger.debug("_sincedb_write: #{path}: #{$!}")
172
+ return
173
+ end
174
+
175
+ @sincedb.each do |inode, pos|
176
+ db.puts([inode, pos].flatten.join(" "))
177
+ end
178
+ db.close
179
+ end # def _sincedb_write
180
+ end # class Watch
181
+ end # module FileWatch
@@ -1,26 +1,145 @@
1
- require "filewatch/namespace"
2
- require "filewatch/inotify/fd"
3
- require "filewatch/exception"
4
-
5
- class FileWatch::Watch
6
- # This class exists to wrap inotify, kqueue, periodic polling, etc,
7
- # to provide you with a way to watch files and directories.
8
- #
9
- # For now, it only supports inotify.
10
- def initialize
11
- @inotify = FileWatch::Inotify::FD.new
12
- end
13
-
14
- public
15
- def watch(path, *what_to_watch)
16
- return @inotify.watch(path, *what_to_watch)
17
- end # def watch
18
-
19
- def subscribe(handler=nil, &block)
20
- @inotify.subscribe(handler, &block)
21
- end
22
-
23
- def each(&block)
24
- @inotify.each(&block)
25
- end # def each
26
- end # class FileWatch::Watch
1
+ require "logger"
2
+
3
+ module FileWatch
4
+ class Watch
5
+ attr_accessor :logger
6
+
7
+ public
8
+ def initialize(opts={})
9
+ if opts[:logger]
10
+ @logger = opts[:logger]
11
+ else
12
+ @logger = Logger.new(STDERR)
13
+ @logger.level = Logger::INFO
14
+ end
15
+ @watching = []
16
+ @exclude = []
17
+ @files = Hash.new { |h, k| h[k] = Hash.new }
18
+ end # def initialize
19
+
20
+ public
21
+ def logger=(logger)
22
+ @logger = logger
23
+ end
24
+
25
+ public
26
+ def exclude(path)
27
+ path.to_a.each { |p| @exclude << p }
28
+ end
29
+
30
+ public
31
+ def watch(path)
32
+ if ! @watching.member?(path)
33
+ @watching << path
34
+ _discover_file(path, true)
35
+ end
36
+
37
+ return true
38
+ end # def tail
39
+
40
+ # Calls &block with params [event_type, path]
41
+ # event_type can be one of:
42
+ # :create_initial - initially present file (so start at end for tail)
43
+ # :create - file is created (new file after initial globs, start at 0)
44
+ # :modify - file is modified (size increases)
45
+ # :delete - file is deleted
46
+ public
47
+ def each(&block)
48
+ # Send any creates.
49
+ @files.keys.each do |path|
50
+ if ! @files[path][:create_sent]
51
+ if @files[path][:initial]
52
+ yield(:create_initial, path)
53
+ else
54
+ yield(:create, path)
55
+ end
56
+ @files[path][:create_sent] = true
57
+ end
58
+ end
59
+
60
+ @files.keys.each do |path|
61
+ begin
62
+ stat = File::Stat.new(path)
63
+ rescue Errno::ENOENT
64
+ # file has gone away or we can't read it anymore.
65
+ @files.delete(path)
66
+ @logger.debug("#{path}: stat failed (#{$!}), deleting from @files")
67
+ yield(:delete, path)
68
+ next
69
+ end
70
+
71
+ inode = [stat.ino, stat.dev_major, stat.dev_minor]
72
+ if inode != @files[path][:inode]
73
+ @logger.debug("#{path}: old inode was #{@files[path][:inode].inspect}, new is #{inode.inspect}")
74
+ yield(:delete, path)
75
+ yield(:create, path)
76
+ elsif stat.size < @files[path][:size]
77
+ @logger.debug("#{path}: file rolled, new size is #{stat.size}, old size #{@files[path][:size]}")
78
+ yield(:delete, path)
79
+ yield(:create, path)
80
+ elsif stat.size > @files[path][:size]
81
+ @logger.debug("#{path}: file grew, old size #{@files[path][:size]}, new size #{stat.size}")
82
+ yield(:modify, path)
83
+ end
84
+
85
+ @files[path][:size] = stat.size
86
+ @files[path][:inode] = inode
87
+ end # @files.keys.each
88
+ end # def each
89
+
90
+ public
91
+ def discover
92
+ @watching.each do |path|
93
+ _discover_file(path)
94
+ end
95
+ end
96
+
97
+ public
98
+ def subscribe(stat_interval = 1, discover_interval = 5, &block)
99
+ glob = 0
100
+ loop do
101
+ each(&block)
102
+
103
+ glob += 1
104
+ if glob == discover_interval
105
+ discover
106
+ glob = 0
107
+ end
108
+
109
+ sleep(stat_interval)
110
+ end
111
+ end # def subscribe
112
+
113
+ private
114
+ def _discover_file(path, initial=false)
115
+ Dir.glob(path).each do |file|
116
+ next if @files.member?(file)
117
+ next unless File.file?(file)
118
+
119
+ @logger.debug("_discover_file: #{path}: new: #{file} (exclude is #{@exclude.inspect})")
120
+
121
+ skip = false
122
+ @exclude.each do |pattern|
123
+ if File.fnmatch?(pattern, File.basename(file))
124
+ @logger.debug("_discover_file: #{file}: skipping because it " +
125
+ "matches exclude #{pattern}")
126
+ skip = true
127
+ break
128
+ end
129
+ end
130
+ next if skip
131
+
132
+ stat = File::Stat.new(file)
133
+ @files[file] = {
134
+ :size => 0,
135
+ :inode => [stat.ino, stat.dev_major, stat.dev_minor],
136
+ :create_sent => false,
137
+ }
138
+ if initial
139
+ @files[file][:initial] = true
140
+ end
141
+ end
142
+ end # def _discover_file
143
+
144
+ end # class Watch
145
+ end # module FileWatch