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 +46 -16
- data/lib/em/globwatcher.rb +31 -13
- data/test/test_filetail.rb +48 -0
- data/test/test_glob.rb +51 -1
- metadata +4 -4
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
177
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
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
|
data/lib/em/globwatcher.rb
CHANGED
@@ -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 =
|
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 =
|
86
|
+
list = Dir.glob(@glob)
|
87
|
+
|
88
|
+
known_files = @files.clone
|
87
89
|
list.each do |path|
|
88
|
-
|
89
|
-
|
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
|
-
|
93
|
-
|
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(
|
100
|
-
@files.delete(
|
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
|
108
|
-
@files.
|
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
|
data/test/test_filetail.rb
CHANGED
@@ -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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
17
|
+
date: 2010-08-29 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|