blob: 808609d7ddb5b5b2ec737df040f993b7dcdd272a [file] [log] [blame]
# -*- 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(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