-
Notifications
You must be signed in to change notification settings - Fork 37
/
fsevent.rb
157 lines (131 loc) · 3.81 KB
/
fsevent.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# -*- 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