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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/adminix.gemspec +1 -4
  4. data/app/assets/images/logo.png +0 -0
  5. data/app/assets/javascripts/application.js +50 -0
  6. data/app/assets/javascripts/bootstrap.min.js +7 -0
  7. data/app/assets/javascripts/dataTables.bootstrap4.js +184 -0
  8. data/app/assets/javascripts/jquery.dataTables.js +15243 -0
  9. data/app/assets/javascripts/jquery.min.js +2 -0
  10. data/app/assets/javascripts/sb-admin-2.min.js +6 -0
  11. data/app/assets/stylesheets/bootstrap.min.css +6 -0
  12. data/app/assets/stylesheets/dataTables.bootstrap.css +314 -0
  13. data/app/assets/stylesheets/dataTables.responsive.css +106 -0
  14. data/app/assets/stylesheets/font-awesome.min.css +4 -0
  15. data/app/assets/stylesheets/sb-admin-2.min.css +5 -0
  16. data/app/views/scripts/restart_watcher.sh.erb +9 -0
  17. data/app/views/scripts/run_script.sh.erb +12 -0
  18. data/app/views/scripts/start_process.sh.erb +7 -0
  19. data/app/views/scripts/stop_process.sh.erb +7 -0
  20. data/app/views/scripts/update_process.sh.erb +24 -0
  21. data/app/views/scripts/update_watcher.sh.erb +3 -0
  22. data/app/views/web/dashboard.html.erb +90 -0
  23. data/app/views/web/job.html.erb +46 -0
  24. data/app/views/web/link.html.erb +12 -0
  25. data/app/views/web/loadstamp.html.erb +57 -0
  26. data/app/views/web/log.html.erb +49 -0
  27. data/app/views/web/partials/footer.html.erb +11 -0
  28. data/app/views/web/partials/header.html.erb +50 -0
  29. data/bin/install_adminix +40 -0
  30. data/bin/push +13 -0
  31. data/development.log +0 -0
  32. data/exe/adminix +91 -28
  33. data/lib/adminix.rb +42 -5
  34. data/lib/adminix/config.rb +170 -96
  35. data/lib/adminix/entities.rb +5 -0
  36. data/lib/adminix/entities/job.rb +54 -0
  37. data/lib/adminix/entities/log.rb +21 -0
  38. data/lib/adminix/entities/service.rb +211 -0
  39. data/lib/adminix/entities/sysload_stamp.rb +37 -0
  40. data/lib/adminix/entities/variable.rb +32 -0
  41. data/lib/adminix/helpers.rb +7 -2
  42. data/lib/adminix/helpers/command.rb +73 -0
  43. data/lib/adminix/helpers/files.rb +82 -0
  44. data/lib/adminix/helpers/log_reader.rb +16 -0
  45. data/lib/adminix/helpers/net_http.rb +63 -0
  46. data/lib/adminix/helpers/output.rb +28 -0
  47. data/lib/adminix/helpers/systemctl.rb +54 -0
  48. data/lib/adminix/services.rb +3 -0
  49. data/lib/adminix/services/app_service.rb +143 -0
  50. data/lib/adminix/services/logs_service.rb +13 -0
  51. data/lib/adminix/services/system_load_service.rb +16 -0
  52. data/lib/adminix/version.rb +1 -1
  53. data/lib/adminix/watcher.rb +76 -144
  54. data/lib/adminix/web.rb +4 -0
  55. data/lib/adminix/web/router.rb +98 -0
  56. data/lib/adminix/web/server.rb +60 -0
  57. data/lib/adminix/web/view_helper.rb +14 -0
  58. data/lib/event_machine.rb +2 -0
  59. data/lib/event_machine/http_server.rb +2 -0
  60. data/lib/event_machine/http_server/response.rb +314 -0
  61. data/lib/event_machine/http_server/server.rb +107 -0
  62. data/lib/event_machine/tail.rb +2 -0
  63. data/lib/event_machine/tail/filetail.rb +470 -0
  64. data/lib/event_machine/tail/globwatcher.rb +294 -0
  65. metadata +60 -45
  66. data/lib/adminix/errors.rb +0 -7
  67. data/lib/adminix/helpers/file.rb +0 -13
  68. data/lib/adminix/helpers/http.rb +0 -19
  69. data/lib/adminix/log_watch_handler.rb +0 -23
  70. data/lib/adminix/server_setup.rb +0 -76
  71. data/lib/adminix/service.rb +0 -179
  72. data/lib/adminix/setup.rb +0 -3
  73. data/lib/adminix/setup/routes.rb +0 -113
  74. data/lib/adminix/setup/services.rb +0 -139
  75. data/lib/adminix/setup/views.rb +0 -183
  76. data/lib/adminix/system.rb +0 -106
  77. 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,2 @@
1
+ require_relative 'tail/filetail'
2
+ require_relative 'tail/globwatcher'
@@ -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