em-dir-watcher 0.1.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +2 -1
- data/README.md +126 -9
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/examples/monitor.rb +11 -3
- data/lib/em-dir-watcher/invokers/subprocess_invoker.rb +100 -0
- data/lib/em-dir-watcher/monitor.rb +47 -0
- data/lib/em-dir-watcher/platform/linux.rb +51 -0
- data/lib/em-dir-watcher/platform/mac/ffi_fsevents_watcher.rb +80 -0
- data/lib/em-dir-watcher/platform/mac/rubycocoa_watcher.rb +51 -0
- data/lib/em-dir-watcher/platform/mac.rb +50 -0
- data/lib/em-dir-watcher/platform/windows/monitor.rb +0 -2
- data/lib/em-dir-watcher/platform/windows/path_to_ruby_exe.rb +0 -3
- data/lib/em-dir-watcher/platform/windows.rb +29 -7
- data/lib/em-dir-watcher/tree.rb +216 -0
- data/lib/em-dir-watcher.rb +4 -9
- data/test/helper.rb +9 -1
- data/test/test_monitor.rb +161 -0
- data/test/test_tree.rb +440 -0
- data/testloop +11 -0
- metadata +16 -8
- data/.document +0 -5
- data/lib/em-dir-watcher/platform/nix.rb +0 -107
- data/lib/em-dir-watcher/platform/windows/monitor-nix.rb +0 -29
- data/test/test_em-dir-watcher.rb +0 -7
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,46 @@
|
|
1
|
-
em-dir-watcher:
|
2
|
-
|
1
|
+
em-dir-watcher: sane cross-platform file system change monitoring for Event Machine
|
2
|
+
===================================================================================
|
3
|
+
|
4
|
+
Compatible with Ruby 1.8+
|
5
|
+
|
6
|
+
Supported platforms:
|
7
|
+
|
8
|
+
* Mac OS X: employs FSEvents API via Ruby Cocoa. To support Ruby 1.8, forks a separate watcher process and communicates with it via pipes. (Ruby 1.9 should use threads instead, but this hasn't been implemented yet.)
|
9
|
+
|
10
|
+
* Linux: employs inotify via rb-inotify gem. This backend is nicest of all because inotify uses file descriptors and thus can be monitored from within Event Machine reactor.
|
11
|
+
|
12
|
+
* Windows: employs Directory Change Notifications API via win32-changenotify. To support Ruby 1.8, forks a separate watcher process and communicates with it via sockets. (We'd love to communicate via pipes, but couldn't figure a way to do it in a Event Machine-friendly way. Also we'd love to use threads on Ruby 1.9, but not there yet.)
|
13
|
+
|
14
|
+
* There is no fallback polling backend at the moment. Please contribute it if you need one.
|
15
|
+
|
16
|
+
|
17
|
+
Why not FSSM or Directory Watcher?
|
18
|
+
--------------------------------------
|
19
|
+
|
20
|
+
FSSM rescans the entire directory tree on each change, so it has about 0.5 sec lag on average-sized projects. Em-dir-watcher only rescans the changed subdirectories (on all systems), and avoids rescanning subtrees on the systems that support non-subtree notifications (Mac, Linux). We'd love to see our `Tree` class used in fssm — this should be an easy change.
|
21
|
+
|
22
|
+
Also, fssm does not know anything about Event Machine, so has to be run in a separate process/thread even on the systems that are reactor-friendly (i.e. Linux). Em-dir-watcher uses `EM.watch` to listen to inotify events on Linux.
|
23
|
+
|
24
|
+
DirectoryWatcher's Event Machine edition uses `EM.watchFile`, which runs out of max open file limit pretty quickly. Also it employs polling to catch added or removed files, and has to walk the entire directory tree every time. Em-dir-watcher uses native backends to watch for file system changes (just like FSSM), and finds added or removed files just as quickly as modified ones.
|
25
|
+
|
26
|
+
Besides, both fssm and directory_watcher do not support exclusions, and thus will walk, update and keep the entire tree in memory including the subfolders you don't need. Em-dir-watcher never walks excluded subfolders, so you can exclude the stuff you don't need to watch to further improve the performance.
|
27
|
+
|
28
|
+
|
29
|
+
Installation
|
30
|
+
------------
|
31
|
+
|
32
|
+
Mac:
|
33
|
+
|
34
|
+
sudo gem install em-dir-watcher
|
35
|
+
|
36
|
+
Linux:
|
37
|
+
|
38
|
+
sudo gem install rb-inotify em-dir-watcher
|
39
|
+
|
40
|
+
Windows:
|
41
|
+
|
42
|
+
gem install win32-changenotify em-dir-watcher
|
3
43
|
|
4
|
-
Employs FSEvents, inotify or Win32 Directory Change Notifications APIs under EventMachine. (Forks a subprocess for blocking watchers.)
|
5
44
|
|
6
45
|
Usage
|
7
46
|
-----
|
@@ -10,11 +49,13 @@ Usage
|
|
10
49
|
require 'em-dir-watcher'
|
11
50
|
|
12
51
|
EM.run {
|
13
|
-
dw = EMDirWatcher.watch '.'
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
52
|
+
dw = EMDirWatcher.watch '.' do |paths|
|
53
|
+
paths.each do |path|
|
54
|
+
if File.exists? path
|
55
|
+
puts "Modified: #{path}"
|
56
|
+
else
|
57
|
+
puts "Deleted: #{path}"
|
58
|
+
end
|
18
59
|
end
|
19
60
|
end
|
20
61
|
puts "EventMachine running..."
|
@@ -22,7 +63,83 @@ Usage
|
|
22
63
|
|
23
64
|
Run `examples/monitor.rb` to see it in action.
|
24
65
|
|
66
|
+
|
67
|
+
EMDirWatcher.watch
|
68
|
+
------------------
|
69
|
+
|
70
|
+
`EMDirWatcher.watch` accepts a path and an options hash:
|
71
|
+
|
72
|
+
EMDirWatcher.watch File.expand_path('~/my_project'),
|
73
|
+
:include_only => ['*.html', '*.css', '*.js'],
|
74
|
+
:exclude => ['~*', 'vendor/plugins'],
|
75
|
+
:grace_period => 0.2
|
76
|
+
|
77
|
+
It returns an object that has a single `stop` method:
|
78
|
+
|
79
|
+
dw = EMDirWatcher.watch File.expand_path('~/my_project')
|
80
|
+
...
|
81
|
+
dw.stop
|
82
|
+
|
83
|
+
|
84
|
+
Inclusions and exclusions
|
85
|
+
-------------------------
|
86
|
+
|
87
|
+
If the list of inclusions is `nil` (the default), all files are included. Otherwise it has to be an array, and specifies a list of patterns to monitor. Each pattern is either a name glob, a path glob or a path regexp (more on this later).
|
88
|
+
|
89
|
+
The list of exclusions defaults to an empty array, and specifies the list of patterns to exclude. Each pattern is either a name glob, a path glob or a path regexp. If a path matches both an inclusion and an exclusion filter, it is excluded.
|
90
|
+
|
91
|
+
Patterns are inspired by Git's `.gitignore` conventions. Each pattern can be one of the following:
|
92
|
+
|
93
|
+
* A string that does not contain a slash is treated as a shell-style glob and is matched against file **base names.** For example, you can use `*.html` to match HTML files in any directory, or `~*` to match temporary files in any directory.
|
94
|
+
|
95
|
+
* A string that contains a slash is treated as a shell-style glob and is matched against file **paths** (relative to the monitored directory). For example, you can use `lib/rake` to match all files in `lib/rake` directory, or `/vendor` to match a `vendor` directory.
|
96
|
+
|
97
|
+
* A regexp is matched against file relative paths. For example, you can use `/[A-Z]/` to match all files and directories that contain upper-case characters in their name.
|
98
|
+
|
99
|
+
|
100
|
+
Grace period
|
101
|
+
------------
|
102
|
+
|
103
|
+
Use `:grace_period => 1.0` to combine the changes together if they occur in quick succession. This, for example, may be used to avoid starting a build while some files are still being updated from a repository.
|
104
|
+
|
105
|
+
The default grace period is `0`, which means that changes are reported immediately when they occur.
|
106
|
+
|
107
|
+
|
108
|
+
Hacking
|
109
|
+
-------
|
110
|
+
|
111
|
+
Required software:
|
112
|
+
|
113
|
+
sudo gem install jeweler shoulda
|
114
|
+
|
115
|
+
To run the tests, use:
|
116
|
+
|
117
|
+
rake test
|
118
|
+
|
119
|
+
This is expected to work on all platforms and shouldn't give any failures, EXCEPT that on a Mac spurious test failures occur in about 1–3 of 20 test runs, and we are unable to get rid of them.
|
120
|
+
|
121
|
+
You can use `./testloop` script to run the tests multiple times in a row to check for unreliable behaviors.
|
122
|
+
|
123
|
+
To give a more context on Mac test failures, two constants that have an effect on them are `STARTUP_DELAY` in `lib/em-dir-watcher/platform/mac.rb` and `UNIT_DELAY` in `tests/test_monitor.rb`. We've settled on a sweet spot of `0.5`/`0.5`, which gives a 5%–15% failure rate. Increasing them to `1.0`/`1.0` still results in the same failure rate. Decreasing them to `0.2`/`0.2` results in 30% failed test runs.
|
124
|
+
|
125
|
+
You can use `rake rcov` to check code coverage info. Currently `tree.rb` and `monitor.rb` have 100% test coverage, `mac.rb` has 80% coverage, `linux.rb` should have about 100% coverage and `windows.rb` should have about 80% coverage (the last two were not measured).
|
126
|
+
|
127
|
+
|
128
|
+
Help Wanted aka TODO
|
129
|
+
--------------------
|
130
|
+
|
131
|
+
If you find yourself using this gem, consider implementing one of the following functions:
|
132
|
+
|
133
|
+
* Ruby 1.9-friendly thread-based Mac backend
|
134
|
+
* Ruby 1.9-friendly thread-based Windows backend
|
135
|
+
* FFI-based Mac backend — there is no need to employ Ruby Cocoa monster just to invoke a handful of functions; a nearly-working version is in `lib/em-dir-watcher/platform/mac/ffi_fsevents_watcher.rb`, just needs some final touches
|
136
|
+
* polling-based fallback backend for the folks on funny operating systems (from FreeBSD to Solaris to HP UX :)
|
137
|
+
* use Mac OS X's FSEvents native grace period implementation
|
138
|
+
|
139
|
+
|
25
140
|
License
|
26
141
|
-------
|
27
142
|
|
28
|
-
Copyright
|
143
|
+
Copyright © 2010 Andrey Tarantsov, Mikhail Gusarov.
|
144
|
+
|
145
|
+
Distributed under the MIT license. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -9,7 +9,7 @@ begin
|
|
9
9
|
gem.description = gem.summary
|
10
10
|
gem.email = "andreyvit@gmail.com"
|
11
11
|
gem.homepage = "http://github.com/mockko/em-dir-watcher"
|
12
|
-
gem.authors = ["Andrey Tarantsov"]
|
12
|
+
gem.authors = ["Andrey Tarantsov", "Mikhail Gusarov"]
|
13
13
|
# gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
14
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
15
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1
|
1
|
+
0.9.1
|
data/examples/monitor.rb
CHANGED
@@ -4,11 +4,19 @@ require 'rubygems'
|
|
4
4
|
require 'em-dir-watcher'
|
5
5
|
|
6
6
|
dir = (ARGV.empty? ? '.' : ARGV.shift)
|
7
|
-
|
7
|
+
inclusions = ARGV.reject { |arg| arg =~ /^!/ }
|
8
|
+
inclusions = nil if inclusions == []
|
9
|
+
exclusions = ARGV.select { |arg| arg =~ /^!/ }.collect { |arg| arg[1..-1] }
|
10
|
+
|
11
|
+
EM.error_handler{ |e|
|
12
|
+
puts "Error raised during event loop: #{e.class.name} #{e.message}"
|
13
|
+
puts e.backtrace
|
14
|
+
}
|
8
15
|
|
9
16
|
EM.run {
|
10
|
-
dw = EMDirWatcher.watch dir,
|
11
|
-
|
17
|
+
dw = EMDirWatcher.watch dir, inclusions, exclusions do |path|
|
18
|
+
full_path = File.join(dir, path)
|
19
|
+
if File.exists? full_path
|
12
20
|
puts "Modified: #{path}"
|
13
21
|
else
|
14
22
|
puts "Deleted: #{path}"
|
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
require 'io/nonblock'
|
3
|
+
|
4
|
+
module EMDirWatcher
|
5
|
+
module Invokers
|
6
|
+
|
7
|
+
class SubprocessInvoker
|
8
|
+
|
9
|
+
attr_reader :active
|
10
|
+
attr_reader :input_handler
|
11
|
+
attr_accessor :additional_delay
|
12
|
+
|
13
|
+
def initialize subprocess, &input_handler
|
14
|
+
@subprocess = subprocess
|
15
|
+
@input_handler = input_handler
|
16
|
+
@active = true
|
17
|
+
@ready_to_use = false
|
18
|
+
@ready_to_use_handlers = []
|
19
|
+
@additional_delay = 0.1
|
20
|
+
start_subprocess
|
21
|
+
end
|
22
|
+
|
23
|
+
def when_ready_to_use &ready_to_use_handler
|
24
|
+
if @ready_to_use_handlers.nil?
|
25
|
+
ready_to_use_handler.call()
|
26
|
+
else
|
27
|
+
@ready_to_use_handlers << ready_to_use_handler
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop
|
32
|
+
@active = false
|
33
|
+
kill
|
34
|
+
end
|
35
|
+
|
36
|
+
# private methods
|
37
|
+
|
38
|
+
def ready_to_use!
|
39
|
+
return if @ready_to_use
|
40
|
+
@ready_to_use = true
|
41
|
+
EM.add_timer @additional_delay do
|
42
|
+
@ready_to_use_handlers.each { |handler| handler.call() }
|
43
|
+
@ready_to_use_handlers = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def kill
|
48
|
+
if @io
|
49
|
+
Process.kill 'TERM', @io.pid
|
50
|
+
Process.waitpid @io.pid
|
51
|
+
@io = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def start_subprocess
|
56
|
+
io = open('|-', 'r')
|
57
|
+
if io.nil?
|
58
|
+
$stdout.sync = true
|
59
|
+
ready = lambda { puts }
|
60
|
+
output = lambda { |single_line_string| puts single_line_string.strip }
|
61
|
+
@subprocess.call ready, output
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
@io = io
|
65
|
+
@io.nonblock = true
|
66
|
+
|
67
|
+
@connection = EM.watch io do |conn|
|
68
|
+
class << conn
|
69
|
+
attr_accessor :invoker
|
70
|
+
|
71
|
+
def notify_readable
|
72
|
+
@invoker.ready_to_use!
|
73
|
+
@data_received ||= ""
|
74
|
+
@data_received << @io.read
|
75
|
+
while line = @data_received.slice!(/^[^\n]*[\n]/m)
|
76
|
+
@invoker.input_handler.call line.strip
|
77
|
+
end
|
78
|
+
rescue EOFError
|
79
|
+
detach
|
80
|
+
@invoker.kill # waitpid to cleanup zombie
|
81
|
+
if @invoker.active
|
82
|
+
EM.next_tick do
|
83
|
+
@invoker.start_subprocess
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def unbind
|
89
|
+
@invoker.kill
|
90
|
+
end
|
91
|
+
end
|
92
|
+
conn.invoker = self
|
93
|
+
conn.notify_readable = true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module EMDirWatcher
|
4
|
+
Watcher = Platform.const_get(PLATFORM)::Watcher
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
:exclude => [],
|
8
|
+
:include_only => nil,
|
9
|
+
:grace_period => 0.0,
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
INFINITELY_SMALL_PERIOD = 0.001
|
13
|
+
|
14
|
+
def self.watch path, options={}
|
15
|
+
unless (invalid_keys = options.keys - DEFAULT_OPTIONS.keys).empty?
|
16
|
+
raise StandardError, "Unsupported options given to EMDirWatcher.watch: " + invalid_keys.join(", ")
|
17
|
+
end
|
18
|
+
options = DEFAULT_OPTIONS.merge(options)
|
19
|
+
grace_period = options[:grace_period]
|
20
|
+
|
21
|
+
tree = Tree.new path, options[:include_only], options[:exclude]
|
22
|
+
|
23
|
+
pending_refresh_requests = Set.new
|
24
|
+
process_pending_refresh_requests_scheduled = false
|
25
|
+
|
26
|
+
process_pending_refresh_requests = lambda do
|
27
|
+
process_pending_refresh_requests_scheduled = false
|
28
|
+
changed_paths = Set.new
|
29
|
+
pending_refresh_requests.each do |change_scope, refresh_subtree|
|
30
|
+
changed_paths += tree.refresh! change_scope, refresh_subtree
|
31
|
+
end
|
32
|
+
yield changed_paths.to_a unless changed_paths.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
Watcher.new path, options[:include_only], options[:exclude] do |change_scope, refresh_subtree|
|
36
|
+
pending_refresh_requests << [change_scope, refresh_subtree]
|
37
|
+
if grace_period <= INFINITELY_SMALL_PERIOD
|
38
|
+
process_pending_refresh_requests.call
|
39
|
+
else
|
40
|
+
unless process_pending_refresh_requests_scheduled
|
41
|
+
EM.add_timer grace_period, &process_pending_refresh_requests
|
42
|
+
process_pending_refresh_requests_scheduled = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rb-inotify'
|
2
|
+
require 'io/nonblock'
|
3
|
+
|
4
|
+
module EMDirWatcher
|
5
|
+
module Platform
|
6
|
+
module Linux
|
7
|
+
|
8
|
+
module Native
|
9
|
+
extend FFI::Library
|
10
|
+
ffi_lib "c"
|
11
|
+
attach_function :close, [:int], :int
|
12
|
+
end
|
13
|
+
|
14
|
+
class Watcher
|
15
|
+
|
16
|
+
def initialize path, inclusions, exclusions
|
17
|
+
@notifier = INotify::Notifier.new
|
18
|
+
|
19
|
+
@notifier.watch(path, :recursive, :attrib, :modify, :create,
|
20
|
+
:delete, :delete_self, :moved_from, :moved_to,
|
21
|
+
:move_self) do |event|
|
22
|
+
yield event.absolute_name
|
23
|
+
end
|
24
|
+
|
25
|
+
@conn = EM.watch @notifier.to_io do |conn|
|
26
|
+
class << conn
|
27
|
+
attr_accessor :notifier
|
28
|
+
|
29
|
+
def notify_readable
|
30
|
+
@notifier.process
|
31
|
+
end
|
32
|
+
end
|
33
|
+
conn.notifier = @notifier
|
34
|
+
conn.notify_readable = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def when_ready_to_use
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
|
42
|
+
def ready_to_use?; true; end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
Native.close @notifier.fd
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
require 'ffi'
|
3
|
+
|
4
|
+
module EMDirWatcher
|
5
|
+
module Platform
|
6
|
+
module Mac
|
7
|
+
|
8
|
+
module CarbonCore
|
9
|
+
extend FFI::Library
|
10
|
+
ffi_lib '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework/Versions/Current/CarbonCore'
|
11
|
+
|
12
|
+
attach_function :CFStringCreateWithCString, [:pointer, :string, :int], :pointer
|
13
|
+
KCFStringEncodingUTF8 = 0x08000100
|
14
|
+
attach_function :CFStringGetLength, [:pointer, :pointer, :int, :pointer, :pointer, :pointer], :int
|
15
|
+
|
16
|
+
attach_function :CFArrayCreate, [:pointer, :pointer, :int, :pointer], :pointer
|
17
|
+
|
18
|
+
attach_function :CFRunLoopRun, :CFRunLoopRun, [], :void
|
19
|
+
attach_function :CFRunLoopGetCurrent, [], :pointer
|
20
|
+
attach_variable :kCFRunLoopDefaultMode, :pointer
|
21
|
+
|
22
|
+
callback :FSEventStreamCallback, [:int], :void
|
23
|
+
|
24
|
+
KFSEventStreamEventIdSinceNow = -1
|
25
|
+
attach_function :FSEventStreamCreate, [:pointer, :FSEventStreamCallback, :pointer, :pointer, :long, :double, :int], :pointer
|
26
|
+
attach_function :FSEventStreamScheduleWithRunLoop, [:pointer, :pointer, :pointer], :void
|
27
|
+
attach_function :FSEventStreamStart, [:pointer], :void
|
28
|
+
attach_function :FSEventStreamStop, [:pointer], :void
|
29
|
+
end
|
30
|
+
|
31
|
+
class FSEventStream
|
32
|
+
|
33
|
+
KFSEventStreamEventFlagMustScanSubDirs = 0x1
|
34
|
+
|
35
|
+
class StreamError < StandardError;
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(paths, &block)
|
39
|
+
raise ArgumentError, 'No callback block was specified.' unless block_given?
|
40
|
+
paths.each { |path| raise ArgumentError, "The specified path (#{path}) does not exist." unless File.exist?(path) }
|
41
|
+
|
42
|
+
handler = lambda do |stream, client_callback_info, number_of_events, paths_pointer, event_flags, event_ids|
|
43
|
+
$stderr.puts "CHANGED!"
|
44
|
+
block.call(['/'])
|
45
|
+
end
|
46
|
+
latency = 0.0
|
47
|
+
flags = 0
|
48
|
+
|
49
|
+
path_cfstring = CarbonCore.CFStringCreateWithCString nil, paths[0], CarbonCore::KCFStringEncodingUTF8
|
50
|
+
# puts "path_cfstring = #{path_cfstring}"
|
51
|
+
# puts "len = #{CarbonCore.CFStringGetLength(path_cfstring)}"
|
52
|
+
|
53
|
+
paths_ptr = FFI::MemoryPointer.new(:pointer)
|
54
|
+
paths_ptr.write_pointer path_cfstring
|
55
|
+
paths_cfarray = CarbonCore.CFArrayCreate nil, paths_ptr, 1, nil
|
56
|
+
# puts "paths_cfarray = #{paths_cfarray}"
|
57
|
+
|
58
|
+
fsevent_stream = CarbonCore.FSEventStreamCreate nil, handler, nil, paths_cfarray, CarbonCore::KFSEventStreamEventIdSinceNow, 0.0, 0
|
59
|
+
# puts "fsevent_stream = #{fsevent_stream}"
|
60
|
+
|
61
|
+
# puts "CarbonCore.kCFRunLoopDefaultMode = #{CarbonCore.kCFRunLoopDefaultMode}"
|
62
|
+
# puts "len = #{CarbonCore.CFStringGetLength(CarbonCore.kCFRunLoopDefaultMode)}"
|
63
|
+
|
64
|
+
CarbonCore.FSEventStreamScheduleWithRunLoop fsevent_stream, CarbonCore.CFRunLoopGetCurrent, CarbonCore.kCFRunLoopDefaultMode
|
65
|
+
|
66
|
+
CarbonCore.FSEventStreamStart fsevent_stream
|
67
|
+
end
|
68
|
+
|
69
|
+
def run_loop
|
70
|
+
CarbonCore.CFRunLoopRun
|
71
|
+
end
|
72
|
+
|
73
|
+
def stop
|
74
|
+
CarbonCore.FSEventStreamStop(@stream)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
require 'osx/foundation'
|
3
|
+
OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
|
4
|
+
|
5
|
+
module EMDirWatcher
|
6
|
+
module Platform
|
7
|
+
module Mac
|
8
|
+
|
9
|
+
class FSEventStream
|
10
|
+
|
11
|
+
KFSEventStreamEventFlagMustScanSubDirs = 0x1
|
12
|
+
|
13
|
+
class StreamError < StandardError;
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(paths, &block)
|
17
|
+
raise ArgumentError, 'No callback block was specified.' unless block_given?
|
18
|
+
paths.each { |path| raise ArgumentError, "The specified path (#{path}) does not exist." unless File.exist?(path) }
|
19
|
+
|
20
|
+
callback = Proc.new do |stream, client_callback_info, number_of_events, paths_pointer, event_flags, event_ids|
|
21
|
+
paths_pointer.regard_as('*')
|
22
|
+
# event_flags.regard_as('*')
|
23
|
+
events = []
|
24
|
+
number_of_events.times {|i|
|
25
|
+
flags = event_flags[i]
|
26
|
+
code = if (flags & KFSEventStreamEventFlagMustScanSubDirs) == KFSEventStreamEventFlagMustScanSubDirs then '>' else '-' end
|
27
|
+
events << code + paths_pointer[i].to_s
|
28
|
+
}
|
29
|
+
block.call(events)
|
30
|
+
end
|
31
|
+
latency = 0.0
|
32
|
+
flags = 0
|
33
|
+
@stream = OSX.FSEventStreamCreate(OSX::KCFAllocatorDefault, callback, nil, paths, OSX::KFSEventStreamEventIdSinceNow, latency, flags)
|
34
|
+
raise(StreamError, 'Unable to create FSEvents stream.') unless @stream
|
35
|
+
OSX.FSEventStreamScheduleWithRunLoop(@stream, OSX.CFRunLoopGetCurrent, OSX::KCFRunLoopDefaultMode)
|
36
|
+
ok = OSX.FSEventStreamStart(@stream)
|
37
|
+
raise(StreamError, 'Unable to start FSEvents stream.') unless ok
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_loop
|
41
|
+
OSX.CFRunLoopRun
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
OSX.FSEventStreamStop(@stream)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
require "em-dir-watcher/invokers/subprocess_invoker"
|
3
|
+
|
4
|
+
module EMDirWatcher
|
5
|
+
module Platform
|
6
|
+
module Mac
|
7
|
+
|
8
|
+
class Watcher
|
9
|
+
|
10
|
+
STARTUP_DELAY = 0.5
|
11
|
+
|
12
|
+
attr_accessor :handler, :active
|
13
|
+
|
14
|
+
def initialize path, inclusions, exclusions
|
15
|
+
subprocess = lambda do |ready, output|
|
16
|
+
require "em-dir-watcher/platform/mac/rubycocoa_watcher"
|
17
|
+
# require "em-dir-watcher/platform/mac/ffi_fsevents_watcher"
|
18
|
+
stream = FSEventStream.new [path] do |changed_paths|
|
19
|
+
changed_paths.each { |path| output.call path }
|
20
|
+
end
|
21
|
+
ready.call()
|
22
|
+
stream.run_loop
|
23
|
+
end
|
24
|
+
|
25
|
+
@invoker = EMDirWatcher::Invokers::SubprocessInvoker.new subprocess do |path|
|
26
|
+
code, path = path[0], path[1..-1]
|
27
|
+
if code == ?> || code == ?-
|
28
|
+
refresh_subtree = (code == ?>)
|
29
|
+
yield path, refresh_subtree
|
30
|
+
end
|
31
|
+
end
|
32
|
+
# Mac OS X seems to require this delay till it really starts listening for file system changes.
|
33
|
+
# See README for explaination of the effect.
|
34
|
+
@invoker.additional_delay = STARTUP_DELAY
|
35
|
+
end
|
36
|
+
|
37
|
+
def when_ready_to_use &ready_to_use_handler
|
38
|
+
@invoker.when_ready_to_use &ready_to_use_handler
|
39
|
+
end
|
40
|
+
|
41
|
+
def ready_to_use?; true; end
|
42
|
+
|
43
|
+
def stop
|
44
|
+
@invoker.stop
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
$stdout.sync = true
|
2
|
-
|
3
2
|
require 'rubygems'
|
4
3
|
require 'socket'
|
5
4
|
require 'win32/changenotify'
|
@@ -14,7 +13,6 @@ socket = TCPSocket.open('127.0.0.1', port)
|
|
14
13
|
begin
|
15
14
|
cn.wait do |events|
|
16
15
|
events.each do |event|
|
17
|
-
# puts event.file_name
|
18
16
|
socket.puts event.file_name
|
19
17
|
end
|
20
18
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
require File.join(File.dirname(__FILE__), 'windows', 'path_to_ruby_exe')
|
3
|
+
require 'socket'
|
3
4
|
|
4
5
|
module EMDirWatcher
|
5
6
|
module Platform
|
@@ -21,23 +22,44 @@ module TcpHandler
|
|
21
22
|
end
|
22
23
|
|
23
24
|
class Watcher
|
24
|
-
|
25
|
-
def initialize path,
|
25
|
+
|
26
|
+
def initialize path, inclusions, exclusions, &handler
|
26
27
|
@path = path
|
27
|
-
@globs = globs
|
28
28
|
@handler = handler
|
29
29
|
@active = true
|
30
|
+
@ready_to_use = false
|
31
|
+
@ready_to_use_handlers = []
|
30
32
|
|
31
33
|
start_server
|
32
34
|
setup_listener
|
33
35
|
end
|
34
36
|
|
37
|
+
def when_ready_to_use &ready_to_use_handler
|
38
|
+
if @ready_to_use_handlers.nil?
|
39
|
+
ready_to_use_handler.call()
|
40
|
+
else
|
41
|
+
@ready_to_use_handlers << ready_to_use_handler
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def ready_to_use!
|
46
|
+
return if @ready_to_use
|
47
|
+
@ready_to_use = true
|
48
|
+
# give the child process additional 100ms to start watching loop
|
49
|
+
EM.add_timer 0.1 do
|
50
|
+
@ready_to_use_handlers.each { |handler| handler.call() }
|
51
|
+
@ready_to_use_handlers = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def ready_to_use?; @ready_to_use; end
|
56
|
+
|
35
57
|
def start_server
|
36
58
|
@server = EM.start_server '127.0.0.1', 0, TcpHandler do |server|
|
37
59
|
server.watcher = self
|
60
|
+
ready_to_use!
|
38
61
|
end
|
39
|
-
@server_port, _ = Socket.unpack_sockaddr_in(EM::get_sockname
|
40
|
-
puts "Server running on port #{@server_port}"
|
62
|
+
@server_port, _ = Socket.unpack_sockaddr_in(EM::get_sockname(@server))
|
41
63
|
end
|
42
64
|
|
43
65
|
def stop_server
|
@@ -54,7 +76,7 @@ class Watcher
|
|
54
76
|
|
55
77
|
def kill
|
56
78
|
if @io
|
57
|
-
Process.kill
|
79
|
+
Process.kill 9, @io.pid
|
58
80
|
Process.waitpid @io.pid
|
59
81
|
@io = nil
|
60
82
|
end
|
@@ -67,7 +89,7 @@ class Watcher
|
|
67
89
|
end
|
68
90
|
|
69
91
|
def path_changed path
|
70
|
-
@handler.call path
|
92
|
+
@handler.call path, true
|
71
93
|
end
|
72
94
|
|
73
95
|
def listener_died
|