adminix 0.1.49 → 0.2
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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/adminix.gemspec +1 -4
- data/app/assets/images/logo.png +0 -0
- data/app/assets/javascripts/application.js +50 -0
- data/app/assets/javascripts/bootstrap.min.js +7 -0
- data/app/assets/javascripts/dataTables.bootstrap4.js +184 -0
- data/app/assets/javascripts/jquery.dataTables.js +15243 -0
- data/app/assets/javascripts/jquery.min.js +2 -0
- data/app/assets/javascripts/sb-admin-2.min.js +6 -0
- data/app/assets/stylesheets/bootstrap.min.css +6 -0
- data/app/assets/stylesheets/dataTables.bootstrap.css +314 -0
- data/app/assets/stylesheets/dataTables.responsive.css +106 -0
- data/app/assets/stylesheets/font-awesome.min.css +4 -0
- data/app/assets/stylesheets/sb-admin-2.min.css +5 -0
- data/app/views/scripts/restart_watcher.sh.erb +9 -0
- data/app/views/scripts/run_script.sh.erb +12 -0
- data/app/views/scripts/start_process.sh.erb +7 -0
- data/app/views/scripts/stop_process.sh.erb +7 -0
- data/app/views/scripts/update_process.sh.erb +24 -0
- data/app/views/scripts/update_watcher.sh.erb +3 -0
- data/app/views/web/dashboard.html.erb +90 -0
- data/app/views/web/job.html.erb +46 -0
- data/app/views/web/link.html.erb +12 -0
- data/app/views/web/loadstamp.html.erb +57 -0
- data/app/views/web/log.html.erb +49 -0
- data/app/views/web/partials/footer.html.erb +11 -0
- data/app/views/web/partials/header.html.erb +50 -0
- data/bin/install_adminix +40 -0
- data/bin/push +13 -0
- data/development.log +0 -0
- data/exe/adminix +91 -28
- data/lib/adminix.rb +42 -5
- data/lib/adminix/config.rb +170 -96
- data/lib/adminix/entities.rb +5 -0
- data/lib/adminix/entities/job.rb +54 -0
- data/lib/adminix/entities/log.rb +21 -0
- data/lib/adminix/entities/service.rb +211 -0
- data/lib/adminix/entities/sysload_stamp.rb +37 -0
- data/lib/adminix/entities/variable.rb +32 -0
- data/lib/adminix/helpers.rb +7 -2
- data/lib/adminix/helpers/command.rb +73 -0
- data/lib/adminix/helpers/files.rb +82 -0
- data/lib/adminix/helpers/log_reader.rb +16 -0
- data/lib/adminix/helpers/net_http.rb +63 -0
- data/lib/adminix/helpers/output.rb +28 -0
- data/lib/adminix/helpers/systemctl.rb +54 -0
- data/lib/adminix/services.rb +3 -0
- data/lib/adminix/services/app_service.rb +143 -0
- data/lib/adminix/services/logs_service.rb +13 -0
- data/lib/adminix/services/system_load_service.rb +16 -0
- data/lib/adminix/version.rb +1 -1
- data/lib/adminix/watcher.rb +76 -144
- data/lib/adminix/web.rb +4 -0
- data/lib/adminix/web/router.rb +98 -0
- data/lib/adminix/web/server.rb +60 -0
- data/lib/adminix/web/view_helper.rb +14 -0
- data/lib/event_machine.rb +2 -0
- data/lib/event_machine/http_server.rb +2 -0
- data/lib/event_machine/http_server/response.rb +314 -0
- data/lib/event_machine/http_server/server.rb +107 -0
- data/lib/event_machine/tail.rb +2 -0
- data/lib/event_machine/tail/filetail.rb +470 -0
- data/lib/event_machine/tail/globwatcher.rb +294 -0
- metadata +60 -45
- data/lib/adminix/errors.rb +0 -7
- data/lib/adminix/helpers/file.rb +0 -13
- data/lib/adminix/helpers/http.rb +0 -19
- data/lib/adminix/log_watch_handler.rb +0 -23
- data/lib/adminix/server_setup.rb +0 -76
- data/lib/adminix/service.rb +0 -179
- data/lib/adminix/setup.rb +0 -3
- data/lib/adminix/setup/routes.rb +0 -113
- data/lib/adminix/setup/services.rb +0 -139
- data/lib/adminix/setup/views.rb +0 -183
- data/lib/adminix/system.rb +0 -106
- data/views/daemon_scripts/upstart.conf.erb +0 -23
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module HttpServer
|
6
|
+
class Server < EM::P::HeaderAndContentProtocol
|
7
|
+
|
8
|
+
# everything starts from here.
|
9
|
+
# Protocol::HeaderAndContentProtocol does the dirty job for us
|
10
|
+
# it will pass headers and content, we just need to parse the headers
|
11
|
+
# the fill the right variables
|
12
|
+
def receive_request(headers, content)
|
13
|
+
|
14
|
+
# save the whole headers array, verbatim
|
15
|
+
@http_headers = headers
|
16
|
+
|
17
|
+
# parse the headers into an hash to be able to access them like:
|
18
|
+
# @http[:host]
|
19
|
+
# @http[:content_type]
|
20
|
+
@http = headers_2_hash headers
|
21
|
+
|
22
|
+
# parse the HTTP request
|
23
|
+
parse_first_header headers.first
|
24
|
+
|
25
|
+
# save the binary content
|
26
|
+
@http_content = content
|
27
|
+
|
28
|
+
# invoke the method in the user-provided instance
|
29
|
+
if respond_to?(:process_http_request)
|
30
|
+
process_http_request
|
31
|
+
end
|
32
|
+
rescue Exception => e
|
33
|
+
# invoke the method in the user-provided instance
|
34
|
+
if respond_to?(:http_request_errback)
|
35
|
+
http_request_errback e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# parse the first HTTP header line
|
40
|
+
# get the http METHOD, URI and PROTOCOL
|
41
|
+
def parse_first_header(line)
|
42
|
+
|
43
|
+
# split the line into: METHOD URI PROTOCOL
|
44
|
+
# eg: GET / HTTP/1.1
|
45
|
+
parsed = line.split(' ')
|
46
|
+
|
47
|
+
# a correct request has three parts
|
48
|
+
return bad_parsing unless parsed.size == 3
|
49
|
+
|
50
|
+
@http_request_method, uri, @http_protocol = parsed
|
51
|
+
|
52
|
+
# optional query string
|
53
|
+
@http_request_uri, @http_query_string = uri.split('?')
|
54
|
+
end
|
55
|
+
|
56
|
+
def bad_parsing
|
57
|
+
code = 400
|
58
|
+
desc = "Bad request"
|
59
|
+
string = respond_to?(:http_error_string) ? http_error_string(code, desc) : default_error_string(code, desc)
|
60
|
+
send_error string
|
61
|
+
raise("#{code} #{desc}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def default_error_string(code, desc)
|
65
|
+
string = "HTTP/1.1 #{code} #{desc}\r\n"
|
66
|
+
string << "Connection: close\r\n"
|
67
|
+
string << "Content-type: text/plain\r\n"
|
68
|
+
string << "\r\n"
|
69
|
+
string << "Detected error: HTTP code #{code}"
|
70
|
+
end
|
71
|
+
|
72
|
+
# send back to the client an HTTP error
|
73
|
+
def send_error(string)
|
74
|
+
send_data string
|
75
|
+
close_connection_after_writing
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
if __FILE__ == $0
|
84
|
+
|
85
|
+
class HTTPHandler < EM::HttpServer::Server
|
86
|
+
|
87
|
+
def process_http_request
|
88
|
+
puts @http_request_method
|
89
|
+
puts @http_request_uri
|
90
|
+
puts @http_query_string
|
91
|
+
puts @http_protocol
|
92
|
+
puts @http[:cookie]
|
93
|
+
puts @http[:content_type]
|
94
|
+
puts @http_content
|
95
|
+
puts @http.inspect
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
port = 8080
|
101
|
+
# all the events are handled here
|
102
|
+
EM::run do
|
103
|
+
EM::start_server("0.0.0.0", port, HTTPHandler)
|
104
|
+
puts "Listening on port #{port}..."
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,470 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "eventmachine"
|
4
|
+
require "logger"
|
5
|
+
|
6
|
+
EventMachine.epoll if EventMachine.epoll?
|
7
|
+
EventMachine.kqueue = true if EventMachine.kqueue?
|
8
|
+
|
9
|
+
# Tail a file.
|
10
|
+
#
|
11
|
+
# Example
|
12
|
+
# class Tailer < EventMachine::FileTail
|
13
|
+
# def receive_data(data)
|
14
|
+
# puts "Got #{data.length} bytes"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # Optional
|
18
|
+
# def eof
|
19
|
+
# puts "Got EOF!"
|
20
|
+
# # If you want to stop
|
21
|
+
# stop
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # Now add it to EM
|
26
|
+
# EM.run do
|
27
|
+
# EM.file_tail("/var/log/messages", Tailer)
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# # Or this way:
|
31
|
+
# EM.run do
|
32
|
+
# Tailer.new("/var/log/messages")
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# See also: EventMachine::FileTail#receive_data
|
36
|
+
class EventMachine::FileTail
|
37
|
+
# Maximum size to read at a time from a single file.
|
38
|
+
CHUNKSIZE = 65536
|
39
|
+
|
40
|
+
#MAXSLEEP = 2
|
41
|
+
|
42
|
+
FORCE_ENCODING = !! (defined? Encoding)
|
43
|
+
|
44
|
+
# The path of the file being tailed
|
45
|
+
attr_reader :path
|
46
|
+
|
47
|
+
# The current file read position
|
48
|
+
attr_reader :position
|
49
|
+
|
50
|
+
# If this tail is closed
|
51
|
+
attr_reader :closed
|
52
|
+
|
53
|
+
# Check interval when checking symlinks for changes. This is only useful
|
54
|
+
# when you are actually tailing symlinks.
|
55
|
+
attr_accessor :symlink_check_interval
|
56
|
+
|
57
|
+
# Check interval for looking for a file if we are tailing it and it has
|
58
|
+
# gone missing.
|
59
|
+
attr_accessor :missing_file_check_interval
|
60
|
+
|
61
|
+
# Tail a file
|
62
|
+
#
|
63
|
+
# * path is a string file path to tail
|
64
|
+
# * startpos is an offset to start tailing the file at. If -1, start at end of
|
65
|
+
# file.
|
66
|
+
#
|
67
|
+
# If you want debug messages, run ruby with '-d' or set $DEBUG
|
68
|
+
#
|
69
|
+
# See also: EventMachine::file_tail
|
70
|
+
#
|
71
|
+
public
|
72
|
+
def initialize(path, startpos=-1, &block)
|
73
|
+
@path = path
|
74
|
+
@logger = Logger.new(STDERR)
|
75
|
+
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
76
|
+
@logger.debug("Tailing #{path} starting at position #{startpos}")
|
77
|
+
|
78
|
+
@file = nil
|
79
|
+
@want_eof_handling = false
|
80
|
+
@want_read = false
|
81
|
+
@want_reopen = false
|
82
|
+
@reopen_on_eof = false
|
83
|
+
@symlink_timer = nil
|
84
|
+
@missing_file_check_timer = nil
|
85
|
+
@read_timer = nil
|
86
|
+
@symlink_target = nil
|
87
|
+
@symlink_stat = nil
|
88
|
+
|
89
|
+
@symlink_check_interval = 1
|
90
|
+
@missing_file_check_interval = 1
|
91
|
+
|
92
|
+
read_file_metadata
|
93
|
+
|
94
|
+
if @filestat.directory?
|
95
|
+
on_exception Errno::EISDIR.new(@path)
|
96
|
+
end
|
97
|
+
|
98
|
+
if block_given?
|
99
|
+
@handler = block
|
100
|
+
@buffer = BufferedTokenizer.new
|
101
|
+
end
|
102
|
+
|
103
|
+
EventMachine::next_tick do
|
104
|
+
open
|
105
|
+
next unless @file
|
106
|
+
|
107
|
+
if (startpos == -1)
|
108
|
+
@position = @file.sysseek(0, IO::SEEK_END)
|
109
|
+
# TODO(sissel): if we don't have inotify or kqueue, should we
|
110
|
+
# schedule a next read, here?
|
111
|
+
# Is there a race condition between setting the file position and
|
112
|
+
# watching given the two together are not atomic?
|
113
|
+
else
|
114
|
+
@position = @file.sysseek(startpos, IO::SEEK_SET)
|
115
|
+
schedule_next_read
|
116
|
+
end
|
117
|
+
watch
|
118
|
+
end # EventMachine::next_tick
|
119
|
+
end # def initialize
|
120
|
+
|
121
|
+
# This method is called when a tailed file has data read.
|
122
|
+
#
|
123
|
+
# * data - string data read from the file.
|
124
|
+
#
|
125
|
+
# If you want to read lines from your file, you should use BufferedTokenizer
|
126
|
+
# (which comes with EventMachine):
|
127
|
+
# class Tailer < EventMachine::FileTail
|
128
|
+
# def initialize(*args)
|
129
|
+
# super(*args)
|
130
|
+
# @buffer = BufferedTokenizer.new
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# def receive_data(data)
|
134
|
+
# @buffer.extract(data).each do |line|
|
135
|
+
# # do something with 'line'
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
public
|
139
|
+
def receive_data(data)
|
140
|
+
if @handler # FileTail.new called with a block
|
141
|
+
@buffer.extract(data).each do |line|
|
142
|
+
@handler.call(self, line)
|
143
|
+
end
|
144
|
+
else
|
145
|
+
on_exception NotImplementedError.new("#{self.class.name}#receive_data is not "\
|
146
|
+
"implemented. Did you forget to implement this in your subclass or "\
|
147
|
+
"module?")
|
148
|
+
end
|
149
|
+
end # def receive_data
|
150
|
+
|
151
|
+
def on_exception(exception)
|
152
|
+
@logger.error("Exception raised. Using default handler in #{self.class.name}")
|
153
|
+
raise exception
|
154
|
+
end
|
155
|
+
|
156
|
+
# This method is called when a tailed file reaches EOF.
|
157
|
+
#
|
158
|
+
# If you want to stop reading this file, call close(), otherwise
|
159
|
+
# this eof is handled as normal tailing does. The default
|
160
|
+
# EOF handler is to do nothing.
|
161
|
+
public
|
162
|
+
def eof
|
163
|
+
@logger.debug { 'EOF' }
|
164
|
+
# do nothing, subclassers should implement this.
|
165
|
+
end # def eof
|
166
|
+
|
167
|
+
# notify is invoked by EM::watch_file when the file you are tailing has been
|
168
|
+
# modified or otherwise needs to be acted on.
|
169
|
+
private
|
170
|
+
def notify(status)
|
171
|
+
@logger.debug { "notify: #{status} on #{path}" }
|
172
|
+
if status == :modified
|
173
|
+
schedule_next_read
|
174
|
+
elsif status == :moved
|
175
|
+
# read to EOF, then reopen.
|
176
|
+
schedule_next_read
|
177
|
+
elsif status == :unbind
|
178
|
+
# :unbind is called after the :deleted handler
|
179
|
+
# :deleted happens on FreeBSD's newsyslog instead of :moved
|
180
|
+
# clean up @watch since its reference is wiped in EM's file_deleted callback
|
181
|
+
@watch = nil
|
182
|
+
end
|
183
|
+
end # def notify
|
184
|
+
|
185
|
+
# Open (or reopen, if necessary) our file and schedule a read.
|
186
|
+
private
|
187
|
+
def open
|
188
|
+
return if @closed
|
189
|
+
@file.close if @file && !@file.closed?
|
190
|
+
return unless File.exists?(@path)
|
191
|
+
begin
|
192
|
+
@logger.debug "Opening file #{@path}"
|
193
|
+
@file = File.open(@path, "r")
|
194
|
+
rescue Errno::ENOENT => e
|
195
|
+
@logger.info("File not found: '#{@path}' (#{e})")
|
196
|
+
on_exception(e)
|
197
|
+
end
|
198
|
+
|
199
|
+
@naptime = 0
|
200
|
+
@logger.debug { 'EOF' }
|
201
|
+
@position = 0
|
202
|
+
schedule_next_read
|
203
|
+
end # def open
|
204
|
+
|
205
|
+
# Close this filetail
|
206
|
+
public
|
207
|
+
def close
|
208
|
+
@closed = true
|
209
|
+
@want_read = false
|
210
|
+
EM.schedule do
|
211
|
+
@watch.stop_watching if @watch
|
212
|
+
EventMachine::cancel_timer(@read_timer) if @read_timer
|
213
|
+
@symlink_timer.cancel if @symlink_timer
|
214
|
+
@missing_file_check_timer.cancel if @missing_file_check_timer
|
215
|
+
@file.close if @file
|
216
|
+
end
|
217
|
+
end # def close
|
218
|
+
|
219
|
+
# More rubyesque way of checking if this tail is closed
|
220
|
+
public
|
221
|
+
def closed?
|
222
|
+
@closed
|
223
|
+
end
|
224
|
+
|
225
|
+
# Watch our file.
|
226
|
+
private
|
227
|
+
def watch
|
228
|
+
@watch.stop_watching if @watch
|
229
|
+
@symlink_timer.cancel if @symlink_timer
|
230
|
+
return unless File.exists?(@path)
|
231
|
+
|
232
|
+
@logger.debug "Starting watch on #{@path}"
|
233
|
+
callback = proc { |what| notify(what) }
|
234
|
+
@watch = EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, callback)
|
235
|
+
watch_symlink if @symlink_target
|
236
|
+
end # def watch
|
237
|
+
|
238
|
+
# Watch a symlink
|
239
|
+
# EM doesn't currently support watching symlinks alone (inotify follows
|
240
|
+
# symlinks by default), so let's periodically stat the symlink.
|
241
|
+
private
|
242
|
+
def watch_symlink(&block)
|
243
|
+
@symlink_timer.cancel if @symlink_timer
|
244
|
+
|
245
|
+
@logger.debug "Launching timer to check for symlink changes since EM can't right now: #{@path}"
|
246
|
+
@symlink_timer = EM::PeriodicTimer.new(@symlink_check_interval) do
|
247
|
+
begin
|
248
|
+
@logger.debug("Checking #{@path}")
|
249
|
+
read_file_metadata do |filestat, linkstat, linktarget|
|
250
|
+
handle_fstat(filestat, linkstat, linktarget)
|
251
|
+
end
|
252
|
+
rescue Errno::ENOENT
|
253
|
+
# The file disappeared. Wait for it to reappear.
|
254
|
+
# This can happen if it was deleted or moved during log rotation.
|
255
|
+
@logger.debug "File not found, waiting for it to reappear. (#{@path})"
|
256
|
+
end # begin/rescue ENOENT
|
257
|
+
end # EM::PeriodicTimer
|
258
|
+
end # def watch_symlink
|
259
|
+
|
260
|
+
private
|
261
|
+
def schedule_next_read
|
262
|
+
if !@want_read
|
263
|
+
@want_read = true
|
264
|
+
@read_timer = EventMachine::add_timer(@naptime) do
|
265
|
+
@want_read = false
|
266
|
+
read
|
267
|
+
end
|
268
|
+
end # if !@want_read
|
269
|
+
end # def schedule_next_read
|
270
|
+
|
271
|
+
# Read CHUNKSIZE from our file and pass it to .receive_data()
|
272
|
+
private
|
273
|
+
def read
|
274
|
+
return if @closed
|
275
|
+
|
276
|
+
data = nil
|
277
|
+
@logger.debug "#{self}: Reading..."
|
278
|
+
begin
|
279
|
+
data = @file.sysread(CHUNKSIZE)
|
280
|
+
rescue EOFError, IOError
|
281
|
+
schedule_eof
|
282
|
+
return
|
283
|
+
end
|
284
|
+
|
285
|
+
data.force_encoding(@file.external_encoding) if FORCE_ENCODING
|
286
|
+
|
287
|
+
# Won't get here if sysread throws EOF
|
288
|
+
@position += data.length
|
289
|
+
@naptime = 0
|
290
|
+
|
291
|
+
# Subclasses should implement receive_data
|
292
|
+
receive_data(data)
|
293
|
+
schedule_next_read
|
294
|
+
end # def read
|
295
|
+
|
296
|
+
# Do EOF handling on next EM iteration
|
297
|
+
private
|
298
|
+
def schedule_eof
|
299
|
+
if !@want_eof_handling
|
300
|
+
eof # Call our own eof event
|
301
|
+
@want_eof_handling = true
|
302
|
+
EventMachine::next_tick do
|
303
|
+
handle_eof
|
304
|
+
end # EventMachine::next_tick
|
305
|
+
end # if !@want_eof_handling
|
306
|
+
end # def schedule_eof
|
307
|
+
|
308
|
+
private
|
309
|
+
def schedule_reopen
|
310
|
+
if !@want_reopen
|
311
|
+
EventMachine::next_tick do
|
312
|
+
@want_reopen = false
|
313
|
+
open
|
314
|
+
watch
|
315
|
+
end
|
316
|
+
end # if !@want_reopen
|
317
|
+
end # def schedule_reopen
|
318
|
+
|
319
|
+
private
|
320
|
+
def handle_eof
|
321
|
+
@want_eof_handling = false
|
322
|
+
|
323
|
+
if @reopen_on_eof
|
324
|
+
@reopen_on_eof = false
|
325
|
+
schedule_reopen
|
326
|
+
end
|
327
|
+
|
328
|
+
# EOF actions:
|
329
|
+
# - Check if the file inode/device is changed
|
330
|
+
# - If symlink, check if the symlink has changed
|
331
|
+
# - Otherwise, do nothing
|
332
|
+
begin
|
333
|
+
read_file_metadata do |filestat, linkstat, linktarget|
|
334
|
+
handle_fstat(filestat, linkstat, linktarget)
|
335
|
+
end
|
336
|
+
rescue Errno::ENOENT
|
337
|
+
# The file disappeared. Wait for it to reappear.
|
338
|
+
# This can happen if it was deleted or moved during log rotation.
|
339
|
+
@missing_file_check_timer = EM::PeriodicTimer.new(@missing_file_check_interval) do
|
340
|
+
begin
|
341
|
+
read_file_metadata do |filestat, linkstat, linktarget|
|
342
|
+
handle_fstat(filestat, linkstat, linktarget)
|
343
|
+
end
|
344
|
+
@missing_file_check_timer.cancel
|
345
|
+
rescue Errno::ENOENT
|
346
|
+
# The file disappeared. Wait for it to reappear.
|
347
|
+
# This can happen if it was deleted or moved during log rotation.
|
348
|
+
@logger.debug "File not found, waiting for it to reappear. (#{@path})"
|
349
|
+
end # begin/rescue ENOENT
|
350
|
+
end # EM::PeriodicTimer
|
351
|
+
end # begin/rescue ENOENT
|
352
|
+
end # def handle_eof
|
353
|
+
|
354
|
+
private
|
355
|
+
def read_file_metadata(&block)
|
356
|
+
begin
|
357
|
+
filestat = File.stat(@path)
|
358
|
+
symlink_stat = nil
|
359
|
+
symlink_target = nil
|
360
|
+
|
361
|
+
if filestat.symlink?
|
362
|
+
symlink_stat = File.lstat(@path) rescue nil
|
363
|
+
symlink_target = File.readlink(@path) rescue nil
|
364
|
+
end
|
365
|
+
rescue Errno::ENOENT
|
366
|
+
raise
|
367
|
+
rescue => e
|
368
|
+
@logger.debug("File stat on '#{@path}' failed")
|
369
|
+
on_exception e
|
370
|
+
end
|
371
|
+
|
372
|
+
if block_given?
|
373
|
+
yield filestat, symlink_stat, symlink_target
|
374
|
+
end
|
375
|
+
|
376
|
+
@filestat = filestat
|
377
|
+
@symlink_stat = symlink_stat
|
378
|
+
@symlink_target = symlink_target
|
379
|
+
end # def read_file_metadata
|
380
|
+
|
381
|
+
# Handle fstat changes appropriately.
|
382
|
+
private
|
383
|
+
def handle_fstat(filestat, symlinkstat, symlinktarget)
|
384
|
+
# If the symlink target changes, the filestat.ino is very likely to have
|
385
|
+
# changed since that is the stat on the resolved file (that the link points
|
386
|
+
# to). However, we'll check explicitly for the symlink target changing
|
387
|
+
# for better debuggability.
|
388
|
+
if symlinktarget
|
389
|
+
if symlinkstat.ino != @symlink_stat.ino
|
390
|
+
@logger.debug "Inode or device changed on symlink. Reopening..."
|
391
|
+
@reopen_on_eof = true
|
392
|
+
schedule_next_read
|
393
|
+
elsif symlinktarget != @symlink_target
|
394
|
+
@logger.debug "Symlink target changed. Reopening..."
|
395
|
+
@reopen_on_eof = true
|
396
|
+
schedule_next_read
|
397
|
+
end
|
398
|
+
elsif (filestat.ino != @filestat.ino or filestat.rdev != @filestat.rdev)
|
399
|
+
@logger.debug "Inode or device changed. Reopening..."
|
400
|
+
@logger.debug filestat
|
401
|
+
@reopen_on_eof = true
|
402
|
+
schedule_next_read
|
403
|
+
elsif (filestat.size < @filestat.size)
|
404
|
+
# If the file size shrank, assume truncation and seek to the beginning.
|
405
|
+
@logger.info("File likely truncated... #{path}")
|
406
|
+
@position = @file.sysseek(0, IO::SEEK_SET)
|
407
|
+
schedule_next_read
|
408
|
+
end
|
409
|
+
end # def handle_fstat
|
410
|
+
|
411
|
+
def to_s
|
412
|
+
return "#{self.class.name}(#{@path}) @ pos:#{@position}"
|
413
|
+
end # def to_s
|
414
|
+
end # class EventMachine::FileTail
|
415
|
+
|
416
|
+
# Internal usage only. This class is used by EventMachine::FileTail
|
417
|
+
# to watch files you are tailing.
|
418
|
+
#
|
419
|
+
# See also: EventMachine::FileTail#watch
|
420
|
+
class EventMachine::FileTail::FileWatcher < EventMachine::FileWatch
|
421
|
+
def initialize(block)
|
422
|
+
@logger = Logger.new(STDERR)
|
423
|
+
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
424
|
+
@callback = block
|
425
|
+
end # def initialize
|
426
|
+
|
427
|
+
def file_modified
|
428
|
+
@callback.call(:modified)
|
429
|
+
end # def file_modified
|
430
|
+
|
431
|
+
def file_moved
|
432
|
+
@callback.call(:moved)
|
433
|
+
end # def file_moved
|
434
|
+
|
435
|
+
def file_deleted
|
436
|
+
@callback.call(:deleted)
|
437
|
+
end # def file_deleted
|
438
|
+
|
439
|
+
def unbind
|
440
|
+
@callback.call(:unbind)
|
441
|
+
end # def unbind
|
442
|
+
end # class EventMachine::FileTail::FileWatch < EventMachine::FileWatch
|
443
|
+
|
444
|
+
# Add EventMachine::file_tail
|
445
|
+
module EventMachine
|
446
|
+
# Tail a file.
|
447
|
+
#
|
448
|
+
# path is the path to the file to tail.
|
449
|
+
# handler should be a module implementing 'receive_data' or
|
450
|
+
# must be a subclasses of EventMachine::FileTail
|
451
|
+
#
|
452
|
+
# For example:
|
453
|
+
# EM::file_tail("/var/log/messages", MyHandler)
|
454
|
+
#
|
455
|
+
# If a block is given, and the handler is not specified or does
|
456
|
+
# not implement EventMachine::FileTail#receive_data, then it
|
457
|
+
# will be called as such:
|
458
|
+
# EM::file_tail(...) do |filetail, line|
|
459
|
+
# # filetail is the FileTail instance watching the file
|
460
|
+
# # line is the line read from the file
|
461
|
+
# end
|
462
|
+
def self.file_tail(path, handler=nil, *args, &block)
|
463
|
+
# This code mostly styled on what EventMachine does in many of it's other
|
464
|
+
# methods.
|
465
|
+
args = [path, *args]
|
466
|
+
klass = klass_from_handler(EventMachine::FileTail, handler, *args);
|
467
|
+
c = klass.new(*args, &block)
|
468
|
+
return c
|
469
|
+
end # def self.file_tail
|
470
|
+
end # module EventMachine
|