158 lines
3.8 KiB
Ruby
158 lines
3.8 KiB
Ruby
# -*- encoding: utf-8 -*-
|
|
|
|
require 'otnetstring'
|
|
|
|
class FSEvent
|
|
class << self
|
|
class_eval <<-END
|
|
def root_path
|
|
"#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))}"
|
|
end
|
|
END
|
|
class_eval <<-END
|
|
def watcher_path
|
|
"#{File.join(FSEvent.root_path, 'bin', 'fsevent_watch')}"
|
|
end
|
|
END
|
|
end
|
|
|
|
attr_reader :paths, :callback
|
|
|
|
def initialize args = nil, &block
|
|
watch(args, &block) unless args.nil?
|
|
end
|
|
|
|
def watch(watch_paths, options=nil, &block)
|
|
@paths = watch_paths.kind_of?(Array) ? watch_paths : [watch_paths]
|
|
@callback = block
|
|
|
|
if options.kind_of?(Hash)
|
|
@options = parse_options(options)
|
|
elsif options.kind_of?(Array)
|
|
@options = options
|
|
else
|
|
@options = []
|
|
end
|
|
end
|
|
|
|
def run
|
|
@pipe = open_pipe
|
|
@running = true
|
|
|
|
# please note the use of IO::select() here, as it is used specifically to
|
|
# preserve correct signal handling behavior in ruby 1.8.
|
|
while @running && IO::select([@pipe], nil, nil, nil)
|
|
# managing the IO ourselves allows us to be careful and never pass an
|
|
# incomplete message to OTNetstring.parse()
|
|
message = ""
|
|
length = ""
|
|
byte = nil
|
|
|
|
reading_length = true
|
|
found_length = false
|
|
|
|
while reading_length
|
|
byte = @pipe.read_nonblock(1)
|
|
if "#{byte}" =~ /\d/
|
|
length << byte
|
|
found_length = true
|
|
elsif found_length == false
|
|
next
|
|
else
|
|
reading_length = false
|
|
end
|
|
end
|
|
length = Integer(length, 10)
|
|
type = byte
|
|
|
|
message << "#{length}#{type}"
|
|
message << @pipe.read(length)
|
|
|
|
decoded = OTNetstring.parse(message)
|
|
modified_paths = decoded["events"].map {|event| event["path"]}
|
|
# passing the full info as a second block param feels icky, but such is
|
|
# the trap of backward compatibility.
|
|
case callback.arity
|
|
when 1
|
|
callback.call(modified_paths)
|
|
when 2
|
|
callback.call(modified_paths, decoded)
|
|
end
|
|
end
|
|
rescue Interrupt, IOError, Errno::EBADF
|
|
ensure
|
|
stop
|
|
end
|
|
|
|
def stop
|
|
unless @pipe.nil?
|
|
Process.kill('KILL', @pipe.pid) if process_running?(@pipe.pid)
|
|
@pipe.close
|
|
end
|
|
rescue IOError
|
|
ensure
|
|
@running = false
|
|
end
|
|
|
|
def process_running?(pid)
|
|
begin
|
|
Process.kill(0, pid)
|
|
true
|
|
rescue Errno::ESRCH
|
|
false
|
|
end
|
|
end
|
|
|
|
if RUBY_VERSION < '1.9'
|
|
def open_pipe
|
|
IO.popen("'#{self.class.watcher_path}' #{options_string} #{shellescaped_paths}")
|
|
end
|
|
|
|
private
|
|
|
|
def options_string
|
|
@options.join(' ')
|
|
end
|
|
|
|
def shellescaped_paths
|
|
@paths.map {|path| shellescape(path)}.join(' ')
|
|
end
|
|
|
|
# for Ruby 1.8.6 support
|
|
def shellescape(str)
|
|
# An empty argument will be skipped, so return empty quotes.
|
|
return "''" if str.empty?
|
|
|
|
str = str.dup
|
|
|
|
# Process as a single byte sequence because not all shell
|
|
# implementations are multibyte aware.
|
|
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
|
|
|
|
# A LF cannot be escaped with a backslash because a backslash + LF
|
|
# combo is regarded as line continuation and simply ignored.
|
|
str.gsub!(/\n/, "'\n'")
|
|
|
|
return str
|
|
end
|
|
else
|
|
def open_pipe
|
|
IO.popen([self.class.watcher_path] + @options + @paths)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def parse_options(options={})
|
|
opts = ['--format=otnetstring']
|
|
opts.concat(['--since-when', options[:since_when]]) if options[:since_when]
|
|
opts.concat(['--latency', options[:latency]]) if options[:latency]
|
|
opts.push('--no-defer') if options[:no_defer]
|
|
opts.push('--watch-root') if options[:watch_root]
|
|
opts.push('--file-events') if options[:file_events]
|
|
# ruby 1.9's IO.popen(array-of-stuff) syntax requires all items to be strings
|
|
opts.map {|opt| "#{opt}"}
|
|
end
|
|
|
|
end
|