eventmachine-tail 0.2.20100525165012 → 0.3.20100829013522

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