filewatch 0.2.5 → 0.3.0

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/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