eventmachine-tail 0.2.20100525165012 → 0.3.20100829013522

Sign up to get free protection for your applications and to get access to all the features.
data/lib/em/filetail.rb CHANGED
@@ -63,14 +63,16 @@ class EventMachine::FileTail
63
63
  raise Errno::EISDIR.new(@path)
64
64
  end
65
65
 
66
- open
67
- watch { |what| notify(what) }
68
- if (startpos == -1)
69
- @file.sysseek(0, IO::SEEK_END)
70
- else
71
- @file.sysseek(startpos, IO::SEEK_SET)
72
- schedule_next_read
73
- end
66
+ EventMachine::next_tick do
67
+ open
68
+ watch { |what| notify(what) }
69
+ if (startpos == -1)
70
+ @file.sysseek(0, IO::SEEK_END)
71
+ else
72
+ @file.sysseek(startpos, IO::SEEK_SET)
73
+ schedule_next_read
74
+ end
75
+ end # EventMachine::next_tick
74
76
  end # def initialize
75
77
 
76
78
  # This method is called when a tailed file has data read.
@@ -112,7 +114,8 @@ class EventMachine::FileTail
112
114
  schedule_next_read
113
115
  elsif status == :moved
114
116
  # TODO(sissel): read to EOF, then reopen.
115
- open
117
+ # If the file was moved, treat it like EOF.
118
+ eof
116
119
  end
117
120
  end
118
121
 
@@ -121,6 +124,7 @@ class EventMachine::FileTail
121
124
  def open
122
125
  @file.close if @file
123
126
  begin
127
+ @logger.debug "Opening file #{@path}"
124
128
  @file = File.open(@path, "r")
125
129
  rescue Errno::ENOENT
126
130
  # no file found
@@ -135,7 +139,8 @@ class EventMachine::FileTail
135
139
  # Watch our file.
136
140
  private
137
141
  def watch(&block)
138
- EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, block)
142
+ @logger.debug "Starting watch on #{@path}"
143
+ @watch = EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, block)
139
144
  end
140
145
 
141
146
  # Schedule a read.
@@ -149,6 +154,7 @@ class EventMachine::FileTail
149
154
  # Read CHUNKSIZE from our file and pass it to .receive_data()
150
155
  private
151
156
  def read
157
+ @logger.debug "#{self}: Reading..."
152
158
  begin
153
159
  data = @file.sysread(CHUNKSIZE)
154
160
  # Won't get here if sysread throws EOF
@@ -173,25 +179,49 @@ class EventMachine::FileTail
173
179
  #end
174
180
 
175
181
  # TODO(sissel): should we schedule an fstat instead of doing it now?
176
- fstat = File.stat(@path)
177
- handle_fstat(fstat)
182
+ begin
183
+ fstat = File.stat(@path)
184
+ handle_fstat(fstat)
185
+ rescue Errno::ENOENT
186
+ # The file disappeared. Wait for it to reappear.
187
+ # This can happen if it was deleted or moved during log rotation.
188
+ @logger.debug "File not found, waiting for it to reappear. (#{@path})"
189
+ timer = EM::PeriodicTimer.new(0.250) do
190
+ begin
191
+ fstat = File.stat(@path)
192
+ handle_fstat(fstat)
193
+ timer.cancel
194
+ rescue Errno::ENOENT
195
+ # ignore
196
+ end # begin/rescue ENOENT
197
+ end # EM::PeriodicTimer
198
+ end # begin/rescue ENOENT
178
199
  end # def eof
179
200
 
180
201
  # Handle fstat changes appropriately.
181
202
  private
182
203
  def handle_fstat(fstat)
183
- if (fstat.ino != @fstat.ino)
184
- open # Reopen if the inode has changed
185
- elsif (fstat.rdev != @fstat.rdev)
186
- open # Reopen if the filesystem device changed
204
+ if (fstat.ino != @fstat.ino or fstat.rdev != @fstat.rdev)
205
+ EventMachine::next_tick do
206
+ @logger.debug "Inode or device changed. Reopening..."
207
+ @watch.stop_watching
208
+ open # Reopen if the inode has changed
209
+ watch { |what| notify(what) }
210
+ end
187
211
  elsif (fstat.size < @fstat.size)
188
212
  # Schedule a read if the file size has changed
189
213
  @logger.info("File likely truncated... #{path}")
190
214
  @file.sysseek(0, IO::SEEK_SET)
191
215
  schedule_next_read
216
+ else
217
+ #@logger.debug "Nothing to run for fstat..."
192
218
  end
193
219
  @fstat = fstat
194
220
  end # def eof
221
+
222
+ def to_s
223
+ return "#{self.class.name}(#{@path}) @ pos:#{@file.sysseek(0, IO::SEEK_CUR)}"
224
+ end # def to_s
195
225
  end # class EventMachine::FileTail
196
226
 
197
227
  # Internal usage only. This class is used by EventMachine::FileTail
@@ -35,7 +35,7 @@ class EventMachine::FileGlobWatch
35
35
  # * interval - number of seconds between scanning the glob for changes
36
36
  def initialize(glob, interval=60)
37
37
  @glob = glob
38
- @files = Set.new
38
+ @files = Hash.new
39
39
  @watches = Hash.new
40
40
  @logger = Logger.new(STDOUT)
41
41
  @logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
@@ -83,29 +83,37 @@ class EventMachine::FileGlobWatch
83
83
  private
84
84
  def find_files
85
85
  @logger.info("Searching for files in #{@glob}")
86
- list = Set.new(Dir.glob(@glob))
86
+ list = Dir.glob(@glob)
87
+
88
+ known_files = @files.clone
87
89
  list.each do |path|
88
- next if @files.include?(path)
89
- add(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)
90
97
  end
91
98
 
92
- (@files - list).each do |missing|
93
- remove(missing)
99
+ # Report missing files.
100
+ known_files.each do |inode, fileinfo|
101
+ remove(fileinfo)
94
102
  end
95
103
  end # def find_files
96
104
 
97
105
  # Remove a file from being watched and notify file_deleted()
98
106
  private
99
- def remove(path)
100
- @files.delete(path)
101
- @watches.delete(path)
102
- file_deleted(path)
107
+ def remove(fileinfo)
108
+ @files.delete(fileinfo.stat.ino)
109
+ @watches.delete(fileinfo.path)
110
+ file_deleted(fileinfo.path)
103
111
  end # def remove
104
112
 
105
113
  # Add a file to watch and notify file_found()
106
114
  private
107
- def add(path)
108
- @files.add(path)
115
+ def track(fileinfo)
116
+ @files[fileinfo.stat.ino] = fileinfo
109
117
 
110
118
  # If EventMachine::watch_file fails, that's ok, I guess.
111
119
  # We'll still find the file 'missing' from the next glob attempt.
@@ -120,7 +128,6 @@ class EventMachine::FileGlobWatch
120
128
  #rescue Errno::EACCES => e
121
129
  #@logger.warn(e)
122
130
  #end
123
- file_found(path)
124
131
  end # def watch
125
132
 
126
133
  private
@@ -139,6 +146,17 @@ class EventMachine::FileGlobWatch
139
146
  block.call path
140
147
  end
141
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
142
160
  end # class EventMachine::FileGlobWatch
143
161
 
144
162
  # A glob tailer for EventMachine
@@ -92,5 +92,53 @@ class TestFileTail < Test::Unit::TestCase
92
92
  end
93
93
  end # EM.run
94
94
  end # def test_filetail_with_block
95
+
96
+ def test_filetail_tracks_renames
97
+ tmp = Tempfile.new("testfiletail")
98
+ data = DATA.clone
99
+ filename = tmp.path
100
+
101
+ data_copy = data.clone
102
+
103
+ # Write first so the first read happens immediately
104
+ tmp.puts data_copy.shift
105
+ tmp.flush
106
+ EM.run do
107
+ abort_after_timeout(DATA.length * SLEEPMAX + 10)
108
+
109
+ lineno = 0
110
+ # Start at file position 0.
111
+ EM::file_tail(tmp.path, nil, 0) do |filetail, line|
112
+ lineno += 1
113
+ expected = data.shift
114
+ #puts "Got #{lineno}: #{line}"
115
+ assert_equal(expected, line,
116
+ "Expected '#{expected}' on line #{lineno}, but got '#{line}'")
117
+ finish if data.length == 0
118
+
119
+ # Start a timer on the first read.
120
+ # This is to ensure we have the file tailing before
121
+ # we try to rename.
122
+ if lineno == 1
123
+ timer = EM::PeriodicTimer.new(0.2) do
124
+ value = data_copy.shift
125
+ tmp.puts value
126
+ tmp.flush
127
+ sleep(rand * SLEEPMAX)
128
+
129
+ # Rename the file, create a new one in it's place.
130
+ # This is to simulate log rotation, etc.
131
+ path_newname = "#{filename}_#{value}"
132
+ File.rename(filename, path_newname)
133
+ File.delete(path_newname)
134
+ tmp = File.open(filename, "w")
135
+ timer.cancel if data_copy.length == 0
136
+ end # timer
137
+ end # if lineno == 1
138
+ end # EM::filetail(...)
139
+ end # EM.run
140
+
141
+ File.delete(filename)
142
+ end # def test_filetail_tracks_renames
95
143
  end # class TestFileTail
96
144
 
data/test/test_glob.rb CHANGED
@@ -86,11 +86,61 @@ class TestGlobWatcher < Test::Unit::TestCase
86
86
  datacopy = @data.clone
87
87
  timer = EM::PeriodicTimer.new(0.2) do
88
88
  #puts "Creating: #{datacopy.first}"
89
- File.new(datacopy.shift, "w")
89
+ File.new(datacopy.shift, "w").close
90
90
  sleep(rand * SLEEPMAX)
91
91
  timer.cancel if datacopy.length == 0
92
92
  end
93
93
  end # EM.run
94
94
  end # def test_glob_finds_newly_created_files_at_runtime
95
+
96
+ def test_glob_ignores_file_renames
97
+ EM.run do
98
+ abort_after_timeout(SLEEPMAX * @data.length + 10)
99
+
100
+ EM::watch_glob("#{@dir}/*", Watcher, @watchinterval, @data.clone, self)
101
+
102
+ datacopy = @data.clone
103
+ timer = EM::PeriodicTimer.new(0.2) do
104
+ filename = datacopy.shift
105
+ File.new(filename, "w").close
106
+ sleep(rand * SLEEPMAX)
107
+
108
+ # This file rename should be ignored.
109
+ EM::Timer.new(2) do
110
+ newname = "#{filename}.renamed"
111
+ File.rename(filename, newname)
112
+
113
+ # Track the new filename so teardown removes it.
114
+ @data << newname
115
+ end
116
+ timer.cancel if datacopy.length == 0
117
+ end
118
+ end
119
+ end # def test_glob_ignores_file_renames
120
+
121
+ def test_glob_ignores_duplicate_hardlinks
122
+ EM.run do
123
+ abort_after_timeout(SLEEPMAX * @data.length + 10)
124
+
125
+ EM::watch_glob("#{@dir}/*", Watcher, @watchinterval, @data.clone, self)
126
+
127
+ datacopy = @data.clone
128
+ timer = EM::PeriodicTimer.new(0.2) do
129
+ filename = datacopy.shift
130
+ File.new(filename, "w").close
131
+ sleep(rand * SLEEPMAX)
132
+
133
+ # This file rename should be ignored.
134
+ EM::Timer.new(2) do
135
+ newname = "#{filename}.renamed"
136
+ File.link(filename, newname)
137
+
138
+ # Track the new filename so teardown removes it.
139
+ @data << newname
140
+ end
141
+ timer.cancel if datacopy.length == 0
142
+ end
143
+ end
144
+ end # def test_glob_ignores_file_renames
95
145
  end # class TestGlobWatcher
96
146
 
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
8
- - 20100525165012
9
- version: 0.2.20100525165012
7
+ - 3
8
+ - 20100829013522
9
+ version: 0.3.20100829013522
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-25 00:00:00 -07:00
17
+ date: 2010-08-29 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency