rb-fsevent-legacy 0.1.0
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.
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +51 -0
- data/Rakefile +43 -0
- data/ext/rb-fsevent-legacy/extconf.rb +12 -0
- data/ext/rb-fsevent-legacy/fs_native_stream.c +442 -0
- data/lib/rb-fsevent-legacy/event.rb +17 -0
- data/lib/rb-fsevent-legacy/stream.rb +108 -0
- data/lib/rb-fsevent-legacy/version.rb +3 -0
- data/lib/rb-fsevent-legacy.rb +7 -0
- data/rb-fsevent-legacy.gemspec +26 -0
- data/spec/fixtures/folder1/file1.txt +0 -0
- data/spec/fixtures/folder1/file3.txt +0 -0
- data/spec/fixtures/folder1/folder2/file2.txt +0 -0
- data/spec/rb-fsevent/fsevent_spec.rb +148 -0
- data/spec/spec_helper.rb +16 -0
- metadata +141 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Thibaud Guillaume-Gentil
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
= rb-fsevent
|
2
|
+
|
3
|
+
Very simple & usable Mac OSX FSEvents API
|
4
|
+
|
5
|
+
- RubyCocoa not required!
|
6
|
+
- Signals are working (really) -- I don't know if they are or not.
|
7
|
+
- Tested on MRI 1.8.7 & 1.9.2, JRuby 1.6.3
|
8
|
+
- Tested on 10.5.8
|
9
|
+
- Works on PowerPC
|
10
|
+
|
11
|
+
== Install
|
12
|
+
|
13
|
+
gem install rb-fsevent-legacy
|
14
|
+
|
15
|
+
== Usage
|
16
|
+
|
17
|
+
=== Singular path
|
18
|
+
|
19
|
+
require 'rb-fsevent-legacy'
|
20
|
+
|
21
|
+
fsevent = FSEvent.new(Dir.pwd)
|
22
|
+
|
23
|
+
#some changes happen here, other program code, etcj
|
24
|
+
|
25
|
+
fsevent.events do |e|
|
26
|
+
puts "Detected change inside: #{e.event_path}"
|
27
|
+
end
|
28
|
+
|
29
|
+
=== Multiple paths
|
30
|
+
|
31
|
+
require 'rb-fsevent'
|
32
|
+
|
33
|
+
paths = ['/tmp/path/one', '/tmp/path/two', Dir.pwd]
|
34
|
+
|
35
|
+
fsevent = FSEvent.new
|
36
|
+
fsevent.add_paths(paths)
|
37
|
+
fsevent.watch do |e|
|
38
|
+
puts "Detected change inside: #{e.event_path}"
|
39
|
+
end
|
40
|
+
|
41
|
+
#some things happen
|
42
|
+
|
43
|
+
fsevent.events.collect { |e| e.event_path }
|
44
|
+
|
45
|
+
=== Threads
|
46
|
+
|
47
|
+
The native stream runs in it's own (isolated) CoreFoundation thread, which is started as soon as the gem
|
48
|
+
is loaded and it terminated when the program stops. You may check for changes as often as you like in
|
49
|
+
ruby loop, or from an additional Ruby thread.
|
50
|
+
|
51
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
|
5
|
+
require 'rspec'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
file "lib/rb-fsevent-legacy/fs_native_stream.bundle" => Dir.glob("ext/rb-fsevent-legacy/*{.rb,.c}") do
|
11
|
+
Dir.chdir("ext/rb-fsevent-legacy") do
|
12
|
+
ruby "extconf.rb"
|
13
|
+
sh "make"
|
14
|
+
end
|
15
|
+
cp "ext/rb-fsevent-legacy/fs_native_stream.bundle", "lib/rb-fsevent-legacy/fs_native_stream.bundle"
|
16
|
+
end
|
17
|
+
|
18
|
+
task :spec => "lib/rb-fsevent-legacy/fs_native_stream.bundle"
|
19
|
+
|
20
|
+
CLEAN.include('ext/**/*{.o,.log,.bundle}')
|
21
|
+
CLEAN.include('ext/**/Makefile')
|
22
|
+
CLOBBER.include('lib/**/*.bundle')
|
23
|
+
|
24
|
+
namespace(:spec) do
|
25
|
+
desc "Run all specs on multiple ruby versions (requires rbenv)"
|
26
|
+
task(:portability) do
|
27
|
+
%w[1.8.6 1.8.7 1.9.2 jruby-head].each do |version|
|
28
|
+
system <<-BASH
|
29
|
+
bash -c 'source ~/.rvm/scripts/rvm;
|
30
|
+
rvm #{version};
|
31
|
+
echo "--------- version #{version} ----------\n";
|
32
|
+
bundle install;
|
33
|
+
rake spec'
|
34
|
+
BASH
|
35
|
+
end
|
36
|
+
end
|
37
|
+
desc "run all specs with rcov"
|
38
|
+
RSpec::Core::RakeTask.new(:coverage) do |t|
|
39
|
+
t.rcov = true
|
40
|
+
t.rcov_opts = %q[--exclude "spec"]
|
41
|
+
t.verbose = true
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
|
3
|
+
if RUBY_PLATFORM.downcase.include?("darwin")
|
4
|
+
with_ldflags($LDFLAGS + ' -framework Foundation') { true }
|
5
|
+
with_ldflags($LDFLAGS + ' -framework CoreServices') { true }
|
6
|
+
else
|
7
|
+
raise "Compiling this for something other than OSX doesn't make sense."
|
8
|
+
end
|
9
|
+
|
10
|
+
$CFLAGS="-g -O0 -pipe -fno-common"
|
11
|
+
|
12
|
+
create_makefile("rb-fsevent-legacy/fs_native_stream")
|
@@ -0,0 +1,442 @@
|
|
1
|
+
#include <CoreFoundation/CoreFoundation.h>
|
2
|
+
#include <CoreServices/CoreServices.h>
|
3
|
+
|
4
|
+
#include <ruby.h>
|
5
|
+
#include <pthread.h>
|
6
|
+
#include <stdlib.h>
|
7
|
+
|
8
|
+
#define COMPILED_AT __DATE__ " " __TIME__
|
9
|
+
|
10
|
+
static VALUE cFSEventSystemStream;
|
11
|
+
|
12
|
+
typedef struct cFSEventEventType {
|
13
|
+
char * path;
|
14
|
+
long eventFlags;
|
15
|
+
unsigned int long eventId;
|
16
|
+
} cFSEventEvent;
|
17
|
+
|
18
|
+
typedef struct cFSEventSystemStreamType {
|
19
|
+
cFSEventEvent *events; // pointer to an arry of events
|
20
|
+
int numEvents;
|
21
|
+
double latency;
|
22
|
+
VALUE path;
|
23
|
+
VALUE running;
|
24
|
+
FSEventStreamRef native_stream;
|
25
|
+
} cFSEventSystemStreamType;
|
26
|
+
|
27
|
+
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
28
|
+
pthread_t FSEventThread;
|
29
|
+
int semaphore = 0;
|
30
|
+
|
31
|
+
CFRunLoopRef FSEventRunLoop;
|
32
|
+
int timer_passes;
|
33
|
+
VALUE last_events;
|
34
|
+
VALUE rbFSEventModule;
|
35
|
+
|
36
|
+
void *fs_rb_cf_allocate(CFIndex size, CFOptionFlags hint, void *info);
|
37
|
+
void *fs_rb_cf_reallocate(void *ptr, CFIndex newsize, CFOptionFlags hint, void *info);
|
38
|
+
void fs_rb_cf_deallocate(void *ptr, void *info);
|
39
|
+
void fs_callback( const FSEventStreamRef streamRef, void *callbackInfo, size_t numEvents, void *eventPaths,
|
40
|
+
const FSEventStreamEventFlags eventFlags[],
|
41
|
+
const FSEventStreamEventId eventIds[]);
|
42
|
+
static VALUE fs_init(VALUE self, VALUE directory, VALUE latency);
|
43
|
+
void *fs_runloop();
|
44
|
+
|
45
|
+
// We need to create a custom allocator so that we place nice with
|
46
|
+
// ruby's GC and allocation practices
|
47
|
+
static CFAllocatorRef rb_CFAllocator(void) {
|
48
|
+
static CFAllocatorRef allocator = NULL;
|
49
|
+
if(!allocator) {
|
50
|
+
CFAllocatorContext context = {
|
51
|
+
0, // CFIndex Version - always 0
|
52
|
+
NULL, // void * to context info
|
53
|
+
NULL, // retain callback insn't necessary
|
54
|
+
NULL, // release callback isn't necessary
|
55
|
+
NULL, // a callback which returns a description based on info, so NULL here too.
|
56
|
+
fs_rb_cf_allocate, // core foundation allocation functions which play nice
|
57
|
+
fs_rb_cf_reallocate, // with the ruby gc
|
58
|
+
fs_rb_cf_deallocate,
|
59
|
+
NULL // the preferred size allocater
|
60
|
+
};
|
61
|
+
allocator = CFAllocatorCreate(NULL, &context);
|
62
|
+
}
|
63
|
+
return allocator;
|
64
|
+
}
|
65
|
+
|
66
|
+
void * fs_rb_cf_allocate(CFIndex size, CFOptionFlags hint, void *info){
|
67
|
+
// hint and info are always going to bu null in our case.
|
68
|
+
// okay, so the size here is an int of BYTES. ruby's ALLOC_N macro
|
69
|
+
// requires that we allocate by pointer type. I think this
|
70
|
+
// is fairly cross platform - but I'm not sure.
|
71
|
+
ALLOC_N(char,size);
|
72
|
+
}
|
73
|
+
void * fs_rb_cf_reallocate(void *ptr, CFIndex newsize, CFOptionFlags hint, void *info){
|
74
|
+
// hint and info are always going to bu null in our case.
|
75
|
+
REALLOC_N(ptr, char, newsize);
|
76
|
+
}
|
77
|
+
|
78
|
+
void fs_rb_cf_deallocate(void *ptr, void *info){
|
79
|
+
xfree(ptr);
|
80
|
+
}
|
81
|
+
|
82
|
+
//function prototypes
|
83
|
+
//
|
84
|
+
|
85
|
+
// AND GO.
|
86
|
+
//
|
87
|
+
void fs_timer(){
|
88
|
+
#ifdef DEBUG
|
89
|
+
printf("Timer Fire\n");
|
90
|
+
#endif
|
91
|
+
pthread_mutex_lock(&mutex);
|
92
|
+
timer_passes++;
|
93
|
+
pthread_mutex_unlock(&mutex);
|
94
|
+
}
|
95
|
+
|
96
|
+
// so OSX doesn't actually have posix semaphores, and i can't make
|
97
|
+
// named sempahore work, so implement our own, rather stupid
|
98
|
+
// version of them.
|
99
|
+
void fs_wait_for_unlock(){
|
100
|
+
int deadlock = 0;
|
101
|
+
// really, it should never wait that long
|
102
|
+
while(semaphore < 0 && deadlock < 32767) { deadlock++; }
|
103
|
+
pthread_mutex_lock(&mutex);
|
104
|
+
semaphore = -1;
|
105
|
+
pthread_mutex_unlock(&mutex);
|
106
|
+
}
|
107
|
+
|
108
|
+
void fs_unlock(){
|
109
|
+
pthread_mutex_lock(&mutex);
|
110
|
+
semaphore = 0;
|
111
|
+
pthread_mutex_unlock(&mutex);
|
112
|
+
}
|
113
|
+
|
114
|
+
void fs_callback(
|
115
|
+
__attribute__((unused)) const FSEventStreamRef streamRef,
|
116
|
+
void *callbackInfo,
|
117
|
+
size_t numEvents,
|
118
|
+
void *eventPaths,
|
119
|
+
const FSEventStreamEventFlags eventFlags[],
|
120
|
+
const FSEventStreamEventId eventIds[]){
|
121
|
+
|
122
|
+
// this is actually running on our pthread.
|
123
|
+
// ruby is not NOT pthread aware or safe, we we
|
124
|
+
// lockout everything that could happen from
|
125
|
+
// calling ruby in this thread.
|
126
|
+
|
127
|
+
// this is only going to do anything
|
128
|
+
// if there's data corruption, in which case we probably
|
129
|
+
// want to bail out anyway.
|
130
|
+
|
131
|
+
// the situation is shutting down, so we don't really care
|
132
|
+
if(FSEventRunLoop == false){
|
133
|
+
#ifdef DEBUG
|
134
|
+
printf("CoreFoundation callback stopped\n");
|
135
|
+
#endif
|
136
|
+
return;
|
137
|
+
}
|
138
|
+
|
139
|
+
pthread_mutex_lock(&mutex);
|
140
|
+
|
141
|
+
#ifdef DEBUG
|
142
|
+
printf("CoreFoundation callback enter for stream <#FSEvent::NativeStream 0x%x>\n", (VALUE)callbackInfo * 2);fflush(stdout);
|
143
|
+
#endif
|
144
|
+
// we do object allocation here in another thread
|
145
|
+
// so mutex it incase the GC is running
|
146
|
+
rb_gc_disable();
|
147
|
+
|
148
|
+
int start_dex;
|
149
|
+
char **paths = eventPaths;
|
150
|
+
// #define REALLOC_N(var,type,n) (var)=(type*)xrealloc((char*)(var),sizeof(type)*(n))
|
151
|
+
// make a new FSEvent ruby object
|
152
|
+
cFSEventSystemStreamType *obj;
|
153
|
+
Check_Type((VALUE)callbackInfo, T_DATA);
|
154
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR((VALUE)callbackInfo);
|
155
|
+
|
156
|
+
if(obj->numEvents > 0){
|
157
|
+
#ifdef DEBUG
|
158
|
+
printf("Adding %d to %d existing events\n", numEvents, obj->numEvents);
|
159
|
+
#endif
|
160
|
+
start_dex = obj->numEvents;
|
161
|
+
obj->numEvents = obj->numEvents + numEvents;
|
162
|
+
} else {
|
163
|
+
start_dex = 0;
|
164
|
+
obj->numEvents = numEvents;
|
165
|
+
}
|
166
|
+
|
167
|
+
// just always do realloc - since it will act as an malloc
|
168
|
+
// if we've already nulled the pointer
|
169
|
+
#ifdef DEBUG
|
170
|
+
printf("Allocating for %d events\n", obj->numEvents);
|
171
|
+
#endif
|
172
|
+
cFSEventEvent *old = obj->events; // yes, i want to store the address.
|
173
|
+
obj->events = (cFSEventEvent *)realloc(obj->events, sizeof(cFSEventEvent) * (int)obj->numEvents);
|
174
|
+
|
175
|
+
|
176
|
+
if(obj->events == NULL){
|
177
|
+
rb_bug("Couldn't malloc events from secondary watch thread");
|
178
|
+
}
|
179
|
+
|
180
|
+
int i,j;
|
181
|
+
|
182
|
+
for(i = start_dex, j = 0; j < numEvents; i++, j++){
|
183
|
+
obj->events[i].path = malloc(strlen(paths[j])+1);
|
184
|
+
if(obj->events[i].path == NULL){
|
185
|
+
rb_bug("Couldn't malloc path from secondary watch thread");
|
186
|
+
}
|
187
|
+
strcpy(obj->events[i].path,paths[j]);
|
188
|
+
obj->events[i].eventFlags = eventFlags[i];
|
189
|
+
obj->events[i].eventId = eventIds[i];
|
190
|
+
}
|
191
|
+
rb_gc_enable();
|
192
|
+
|
193
|
+
#ifdef DEBUG
|
194
|
+
printf("CoreFoundation callback exited\n");
|
195
|
+
#endif
|
196
|
+
|
197
|
+
pthread_mutex_unlock(&mutex);
|
198
|
+
}
|
199
|
+
|
200
|
+
static VALUE fs_timer_events(VALUE self){
|
201
|
+
#ifdef DEBUG
|
202
|
+
printf("Timer value retrieved\n"); fflush(stdout);
|
203
|
+
#endif
|
204
|
+
|
205
|
+
VALUE timer;
|
206
|
+
pthread_mutex_lock(&mutex);
|
207
|
+
timer = INT2FIX(timer_passes);
|
208
|
+
pthread_mutex_unlock(&mutex);
|
209
|
+
return timer;
|
210
|
+
}
|
211
|
+
|
212
|
+
static VALUE fs_events(VALUE self) {
|
213
|
+
// this should be running on the main ruby thread
|
214
|
+
pthread_mutex_lock(&mutex);
|
215
|
+
#ifdef DEBUG
|
216
|
+
printf("Ruby thread event enter for <#FSEvent::NativeStream 0x%x>\n",(VALUE)self * 2); fflush(stdout);
|
217
|
+
#endif
|
218
|
+
|
219
|
+
cFSEventSystemStreamType *obj;
|
220
|
+
// DataGetStruct(self,cFSEventSystemStreamType,obj);
|
221
|
+
Check_Type(self, T_DATA);
|
222
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR(self);
|
223
|
+
|
224
|
+
if(obj->numEvents == 0){
|
225
|
+
#ifdef DEBUG
|
226
|
+
printf("Ruby thread event break\n"); fflush(stdout);
|
227
|
+
#endif
|
228
|
+
pthread_mutex_unlock(&mutex);
|
229
|
+
return Qnil;
|
230
|
+
}
|
231
|
+
|
232
|
+
volatile VALUE FSEvent = rb_const_get(rbFSEventModule,rb_intern("Event"));
|
233
|
+
|
234
|
+
if(rb_block_given_p()) {
|
235
|
+
int len = obj->numEvents;
|
236
|
+
int i;
|
237
|
+
#ifdef DEBUG
|
238
|
+
printf("Yielding %d values\n",obj->numEvents);
|
239
|
+
#endif
|
240
|
+
for(i=0;i<len;i++){
|
241
|
+
volatile VALUE fs_event = rb_funcall(FSEvent,rb_intern("new"),0);
|
242
|
+
rb_funcall(fs_event,rb_intern("event_id="),1,ULL2NUM(obj->events[i].eventId));
|
243
|
+
rb_funcall(fs_event,rb_intern("event_path="),1,rb_str_new2(obj->events[i].path));
|
244
|
+
rb_funcall(fs_event,rb_intern("event_flags="),1,LONG2NUM(obj->events[i].eventFlags));
|
245
|
+
|
246
|
+
#ifdef DEBUG
|
247
|
+
printf("Ruby thread event yield\n"); fflush(stdout);
|
248
|
+
#endif
|
249
|
+
|
250
|
+
rb_yield(fs_event);
|
251
|
+
}
|
252
|
+
|
253
|
+
// delete all of the events
|
254
|
+
obj->numEvents = 0;
|
255
|
+
free(obj->events);
|
256
|
+
obj->events = NULL;
|
257
|
+
}
|
258
|
+
#ifdef DEBUG
|
259
|
+
printf("Ruby thread event exit\n"); fflush(stdout);
|
260
|
+
#endif
|
261
|
+
|
262
|
+
pthread_mutex_unlock(&mutex);
|
263
|
+
return Qnil;
|
264
|
+
}
|
265
|
+
|
266
|
+
static VALUE fs_running(VALUE self){
|
267
|
+
cFSEventSystemStreamType *obj;
|
268
|
+
Check_Type(self, T_DATA);
|
269
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR(self);
|
270
|
+
return (VALUE)obj->running;
|
271
|
+
}
|
272
|
+
|
273
|
+
static VALUE fs_path(VALUE self){
|
274
|
+
cFSEventSystemStreamType *obj;
|
275
|
+
Check_Type(self, T_DATA);
|
276
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR(self);
|
277
|
+
return (VALUE)obj->path;
|
278
|
+
}
|
279
|
+
|
280
|
+
// create a new FS Event Stream
|
281
|
+
FSEventStreamRef fs_new_stream(char *dir, VALUE *self) {
|
282
|
+
// always runs on the ruby thread.
|
283
|
+
//
|
284
|
+
cFSEventSystemStreamType *obj;
|
285
|
+
Check_Type(self, T_DATA);
|
286
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR(self);
|
287
|
+
|
288
|
+
CFStringRef mypath = CFStringCreateWithCString(NULL,dir,CFStringGetSystemEncoding());
|
289
|
+
CFArrayRef paths = CFArrayCreate(NULL, (const void **)&mypath, 1, NULL);
|
290
|
+
FSEventStreamContext context = {0,self,NULL,NULL,NULL};
|
291
|
+
FSEventStreamRef stream;
|
292
|
+
CFAbsoluteTime latency = obj->latency;
|
293
|
+
|
294
|
+
stream = FSEventStreamCreate(
|
295
|
+
rb_CFAllocator(), // use the customer corefoundation allocator...
|
296
|
+
(FSEventStreamCallback)&fs_callback,
|
297
|
+
&context,
|
298
|
+
paths,
|
299
|
+
kFSEventStreamEventIdSinceNow, //allow to replace with a meaningufl "since"
|
300
|
+
latency,
|
301
|
+
kFSEventStreamCreateFlagNone //look this up
|
302
|
+
);
|
303
|
+
return stream;
|
304
|
+
}
|
305
|
+
|
306
|
+
static void fs_free(cFSEventSystemStreamType *me){
|
307
|
+
//unconditionally free unprocessed events
|
308
|
+
#ifdef DEBUG
|
309
|
+
printf("GC Free for native stream\n"); fflush(stdout);
|
310
|
+
#endif
|
311
|
+
|
312
|
+
pthread_mutex_lock(&mutex);
|
313
|
+
if(me->running == Qtrue){
|
314
|
+
|
315
|
+
#ifdef DEBUG
|
316
|
+
printf("Discarding %d events.\n",me->numEvents); fflush(stdout);
|
317
|
+
#endif
|
318
|
+
// we only get called from ruby's GC - but it's possible
|
319
|
+
// for this object to be GCd after the runloop itself has been
|
320
|
+
// so check for that situation
|
321
|
+
FSEventStreamFlushSync(me->native_stream);
|
322
|
+
FSEventStreamStop(me->native_stream);
|
323
|
+
FSEventStreamInvalidate(me->native_stream);
|
324
|
+
}
|
325
|
+
free(me->events);
|
326
|
+
FSEventStreamRelease(me->native_stream);
|
327
|
+
xfree(me);
|
328
|
+
pthread_mutex_unlock(&mutex);
|
329
|
+
}
|
330
|
+
|
331
|
+
static void fs_mark(cFSEventSystemStreamType *me){
|
332
|
+
#ifdef DEBUG
|
333
|
+
printf("GC Mark for native stream\n"); fflush(stdout);
|
334
|
+
#endif
|
335
|
+
|
336
|
+
rb_gc_mark(me->path);
|
337
|
+
rb_gc_mark(me->running); // i think that's extraneous - but mark it just to be safe.
|
338
|
+
}
|
339
|
+
|
340
|
+
static VALUE fs_stop(VALUE self){
|
341
|
+
#ifdef DEBUG
|
342
|
+
printf("Native stream stopping #<FSEvent::NativeStream 0x%x>\n",self * 2); fflush(stdout);
|
343
|
+
#endif
|
344
|
+
|
345
|
+
cFSEventSystemStreamType *obj;
|
346
|
+
Check_Type(self, T_DATA);
|
347
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR(self);
|
348
|
+
|
349
|
+
if(obj->running == Qfalse){
|
350
|
+
VALUE excp = rb_const_get(rb_cObject,rb_intern("RuntimeError"));
|
351
|
+
rb_raise(excp, "native stream watcher was stopped twice");
|
352
|
+
}
|
353
|
+
|
354
|
+
FSEventStreamFlushSync(obj->native_stream);
|
355
|
+
FSEventStreamStop(obj->native_stream);
|
356
|
+
FSEventStreamInvalidate(obj->native_stream);
|
357
|
+
obj->running = Qfalse;
|
358
|
+
|
359
|
+
return obj->running;
|
360
|
+
}
|
361
|
+
|
362
|
+
static VALUE fs_alloc(VALUE klass){
|
363
|
+
// create and assign an empty eventstream.
|
364
|
+
#ifdef DEBUG
|
365
|
+
printf("GC Alloc for native stream\n"); fflush(stdout);
|
366
|
+
#endif
|
367
|
+
cFSEventSystemStreamType *obj;
|
368
|
+
return Data_Make_Struct(klass,cFSEventSystemStreamType,fs_mark,fs_free,obj);
|
369
|
+
}
|
370
|
+
|
371
|
+
static VALUE fs_init(VALUE self, VALUE directory, VALUE latency) {
|
372
|
+
char *dir;
|
373
|
+
cFSEventSystemStreamType *obj;
|
374
|
+
/* obj, type, sval */
|
375
|
+
//DataGetStruct(self,cFSEventSystemStreamType,obj);
|
376
|
+
Check_Type(self, T_DATA);
|
377
|
+
obj = (cFSEventSystemStreamType *)DATA_PTR(self);
|
378
|
+
|
379
|
+
dir = RSTRING_PTR(StringValue(directory));
|
380
|
+
obj->latency = NUM2DBL(latency);
|
381
|
+
obj->native_stream = fs_new_stream(dir,(VALUE *)self);
|
382
|
+
obj->events = NULL;
|
383
|
+
obj->path = directory;
|
384
|
+
obj->numEvents = 0;
|
385
|
+
|
386
|
+
|
387
|
+
FSEventStreamScheduleWithRunLoop(obj->native_stream, FSEventRunLoop, kCFRunLoopDefaultMode);
|
388
|
+
FSEventStreamStart(obj->native_stream);
|
389
|
+
|
390
|
+
obj->running = Qtrue;
|
391
|
+
|
392
|
+
return self;
|
393
|
+
}
|
394
|
+
|
395
|
+
void *fs_runloop() {
|
396
|
+
FSEventRunLoop = CFRunLoopGetCurrent();
|
397
|
+
// create a one second timer to keep the event loop running even
|
398
|
+
// if we haven't actually added an FSEventStream yet.
|
399
|
+
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(
|
400
|
+
NULL,
|
401
|
+
(CFAbsoluteTime)0,
|
402
|
+
(CFTimeInterval)1.0, //why not.
|
403
|
+
0,
|
404
|
+
0,
|
405
|
+
&fs_timer,
|
406
|
+
NULL
|
407
|
+
);
|
408
|
+
CFRunLoopAddTimer(FSEventRunLoop,timer,kCFRunLoopCommonModes);
|
409
|
+
#ifdef DEBUG
|
410
|
+
printf("Starting CFRunLoop\n"); fflush(stdout);
|
411
|
+
#endif
|
412
|
+
// this will not get set until the runloop terminates
|
413
|
+
// when it will show the reason for termination
|
414
|
+
CFRunLoopRun();
|
415
|
+
FSEventRunLoop = false;
|
416
|
+
|
417
|
+
// do we need to do any cleanup from our thread?
|
418
|
+
}
|
419
|
+
|
420
|
+
static void fs_stoploop(VALUE dummy) {
|
421
|
+
#ifdef DEBUG
|
422
|
+
printf("Stopping CFRunLoop\n"); fflush(stdout);
|
423
|
+
#endif
|
424
|
+
CFRunLoopStop(FSEventRunLoop);
|
425
|
+
}
|
426
|
+
|
427
|
+
void Init_fs_native_stream(){
|
428
|
+
rbFSEventModule = rb_const_get(rb_cObject,rb_intern("FSEvent"));
|
429
|
+
cFSEventSystemStream = rb_define_class_under(rbFSEventModule, "NativeStream", rb_cObject);
|
430
|
+
rb_define_alloc_func(cFSEventSystemStream,fs_alloc);
|
431
|
+
rb_define_method(cFSEventSystemStream,"initialize",fs_init,2);
|
432
|
+
rb_define_method(cFSEventSystemStream,"events",fs_events,0);
|
433
|
+
rb_define_method(cFSEventSystemStream,"path",fs_path,0);
|
434
|
+
rb_define_method(cFSEventSystemStream,"running?",fs_running,0);
|
435
|
+
rb_define_method(cFSEventSystemStream,"stop",fs_stop,0);
|
436
|
+
|
437
|
+
rb_define_singleton_method(cFSEventSystemStream,"timer_events",fs_timer_events,0);
|
438
|
+
|
439
|
+
// we create a new pthread that we manage
|
440
|
+
pthread_create(&FSEventThread,NULL,&fs_runloop,NULL);
|
441
|
+
rb_set_end_proc(fs_stoploop,0);
|
442
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module FSEvent
|
2
|
+
class Event
|
3
|
+
#an ACTUAL filesystem event
|
4
|
+
attr_accessor :event_path, :event_flags, :event_id
|
5
|
+
|
6
|
+
def to_s
|
7
|
+
"#<%s:0x%x event_path='%s' event_flags='%s' event_id='%s'>" \
|
8
|
+
% [self.class, self.object_id.abs * 2,self.event_path, self.event_flags, self.event_id]
|
9
|
+
end
|
10
|
+
|
11
|
+
def event_flag
|
12
|
+
# honestly, since this is the legacy system i'm not sure this is
|
13
|
+
# ever anything but zero. so lazily do nothing until i know otherwise
|
14
|
+
# break out the event flags into the something meaningful
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module FSEvent
|
2
|
+
class Stream
|
3
|
+
class << self
|
4
|
+
def running?
|
5
|
+
if NativeStream.timer_events > 0 then true end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
#the callback recieves FSEvent objects which describe the events
|
10
|
+
def initialize(path = nil, latency = 1.0)
|
11
|
+
unless path.nil?
|
12
|
+
@streams = [NativeStream.new(path,Float(latency))]
|
13
|
+
else
|
14
|
+
@streams = []
|
15
|
+
end
|
16
|
+
@run = false;
|
17
|
+
end
|
18
|
+
|
19
|
+
def latency
|
20
|
+
@latency || 1.0
|
21
|
+
end
|
22
|
+
|
23
|
+
def latency= val
|
24
|
+
@latency = Float(val)
|
25
|
+
end
|
26
|
+
|
27
|
+
def watch path = nil, &block
|
28
|
+
@streams.push(NativeStream.new(path,self.latency)) unless path.nil?
|
29
|
+
@call_block = block
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty
|
34
|
+
@streams.each { |s| s.stop }.delete_if { |s| not s.running? }
|
35
|
+
end
|
36
|
+
|
37
|
+
def paths
|
38
|
+
@streams.collect { |s| s.path }
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_paths paths
|
42
|
+
paths.each do |p|
|
43
|
+
self.add_path p
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_path path
|
48
|
+
@streams.push(NativeStream.new(path,self.latency))
|
49
|
+
end
|
50
|
+
|
51
|
+
def status
|
52
|
+
status = @streams.collect do |s|
|
53
|
+
s.running?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def running?
|
58
|
+
status = @streams.collect { |s| s.running? }
|
59
|
+
if not status.include? true
|
60
|
+
false
|
61
|
+
elsif not status.include? false
|
62
|
+
true
|
63
|
+
else
|
64
|
+
raise StandardError, "native streams are in mixed states"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def killall
|
69
|
+
@streams.each do |s|
|
70
|
+
s.stop
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def streams
|
75
|
+
@streams
|
76
|
+
end
|
77
|
+
|
78
|
+
def events
|
79
|
+
events = []
|
80
|
+
@streams.each do |s|
|
81
|
+
s.events do |e|
|
82
|
+
if block_given?
|
83
|
+
events << (yield e)
|
84
|
+
else
|
85
|
+
events << e
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
events.compact
|
90
|
+
end
|
91
|
+
|
92
|
+
def run
|
93
|
+
@run = true
|
94
|
+
while @run do
|
95
|
+
self.events do |e|
|
96
|
+
if @call_block
|
97
|
+
@call_block.call(e)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
sleep 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def stop
|
105
|
+
@run = false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rb-fsevent-legacy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rb-fsevent-legacy"
|
7
|
+
s.version = FSEvent::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Stephen Prater']
|
10
|
+
s.email = ['me@stephenprater.com']
|
11
|
+
s.homepage = "http://github.com/stephenprater/rb-fsevents-legacy"
|
12
|
+
s.summary = "Very simple & usable FSEvents API - it works on PowerPCs and 10.5.8"
|
13
|
+
s.description = "A legacy compatible version of an FSEvents API for Darwin."
|
14
|
+
|
15
|
+
s.rubyforge_project = "rb-fsevent-legacy"
|
16
|
+
|
17
|
+
s.add_development_dependency 'bundler', '~> 1.0.10'
|
18
|
+
s.add_development_dependency 'rspec', '~> 2.5.0'
|
19
|
+
s.add_development_dependency 'rcov'
|
20
|
+
s.add_development_dependency 'pry'
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.extensions = ['ext/rb-fsevent-legacy/extconf.rb']
|
24
|
+
s.require_paths = ['lib','ext']
|
25
|
+
end
|
26
|
+
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FSEvent do
|
4
|
+
|
5
|
+
it "should automatically start the FSEvent native thread" do
|
6
|
+
fsevent = FSEvent::Stream.new(@fixture_path, 0.1)
|
7
|
+
fsevent.running?.should be_true
|
8
|
+
fsevent.streams.length.should == 1
|
9
|
+
fsevent.killall
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can start and stop native FSEvent watchers" do
|
13
|
+
fsevent = FSEvent::Stream.new(@fixture_path, 0.1)
|
14
|
+
fsevent.running?.should be_true
|
15
|
+
fsevent.streams.length.should == 1
|
16
|
+
fsevent.killall
|
17
|
+
fsevent.running?.should be_false
|
18
|
+
|
19
|
+
file = @fixture_path.join("newfile#{rand(10).to_s}.rb")
|
20
|
+
FileUtils.touch file
|
21
|
+
sleep 0.2
|
22
|
+
|
23
|
+
# that event should NOT show up because watcher is stopped
|
24
|
+
fsevent.events do |e|
|
25
|
+
raise "event block called even though events should return an empty array"
|
26
|
+
end.should == []
|
27
|
+
|
28
|
+
event = FSEvent::Stream.new(@fixture_path,0.1)
|
29
|
+
event.running?.should be_true
|
30
|
+
|
31
|
+
event.events.should == []
|
32
|
+
|
33
|
+
event.killall
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can only stop it once" do
|
37
|
+
fsevent = FSEvent::Stream.new(@fixture_path,0.1)
|
38
|
+
fsevent.killall
|
39
|
+
lambda { fsevent.killall }.should raise_error RuntimeError
|
40
|
+
end
|
41
|
+
|
42
|
+
it "has a long running thread timer" do
|
43
|
+
FSEvent::Stream.running?.should be_true
|
44
|
+
sleep 1
|
45
|
+
FSEvent::NativeStream.timer_events.should > 1
|
46
|
+
end
|
47
|
+
|
48
|
+
it "shouldn't throw an error when it gets GC'd" do
|
49
|
+
#basically, if this doesn't throw a bus error it probably worked.
|
50
|
+
#you can ifdef the debugging output in the c-extension if you want
|
51
|
+
#to futz about with it.
|
52
|
+
local_event = FSEvent::Stream.new(@fixture_path,0.1)
|
53
|
+
local_event.running?.should be_true
|
54
|
+
local_event = nil
|
55
|
+
GC.start
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should work with path with an apostrophe" do
|
59
|
+
fsevent = FSEvent::Stream.new(@fixture_path, 0.1)
|
60
|
+
custom_path = @fixture_path.join("custom 'path")
|
61
|
+
file = custom_path.join("newfile#{rand(10)}.rb").to_s
|
62
|
+
|
63
|
+
fsevent.add_path custom_path
|
64
|
+
fsevent.paths.should include("#{custom_path}")
|
65
|
+
FileUtils.touch file
|
66
|
+
|
67
|
+
sleep 0.2
|
68
|
+
|
69
|
+
fsevent.events.collect do |e|
|
70
|
+
e.event_path
|
71
|
+
end.should include(custom_path.to_s + '/')
|
72
|
+
|
73
|
+
File.delete file
|
74
|
+
fsevent.killall
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should catch new file" do
|
78
|
+
fsevent = FSEvent::Stream.new(@fixture_path, 0.1)
|
79
|
+
file = @fixture_path.join("newfile#{rand(10).to_s}.rb")
|
80
|
+
|
81
|
+
FileUtils.touch file
|
82
|
+
sleep 0.2
|
83
|
+
File.delete file
|
84
|
+
|
85
|
+
fsevent.events.collect do |e|
|
86
|
+
e.event_path
|
87
|
+
end.should include(@fixture_path.to_s + '/')
|
88
|
+
fsevent.killall
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should catch file update" do
|
92
|
+
fsevent = FSEvent::Stream.new(@fixture_path, 0.1)
|
93
|
+
file = @fixture_path.join("folder1/file1.txt")
|
94
|
+
File.exists?(file).should be_true
|
95
|
+
FileUtils.touch file
|
96
|
+
sleep 0.2
|
97
|
+
|
98
|
+
fsevent.events.collect do |e|
|
99
|
+
e.event_path
|
100
|
+
end.should include(@fixture_path.join("folder1/").to_s)
|
101
|
+
fsevent.killall
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should store events until called" do
|
105
|
+
fsevent = FSEvent::Stream.new(@fixture_path, 0.1)
|
106
|
+
file1 = @fixture_path.join("folder1/file3.txt")
|
107
|
+
file2 = @fixture_path.join("folder1/folder2/file2.txt")
|
108
|
+
File.exists?(file1).should be_true
|
109
|
+
File.exists?(file2).should be_true
|
110
|
+
FileUtils.touch file1
|
111
|
+
sleep 0.5
|
112
|
+
FileUtils.touch file2
|
113
|
+
sleep 0.2
|
114
|
+
|
115
|
+
fsevent.events.collect do |e|
|
116
|
+
e.event_path
|
117
|
+
end.should == [@fixture_path.join("folder1/").to_s, @fixture_path.join("folder1/folder2/").to_s]
|
118
|
+
fsevent.killall
|
119
|
+
end
|
120
|
+
|
121
|
+
it "will run in a separate thread" do
|
122
|
+
fsevent = nil
|
123
|
+
t = Thread.new do
|
124
|
+
fsevent = FSEvent::Stream.new(@fixture_path,0.1)
|
125
|
+
fsevent.run
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
file1 = @fixture_path.join("folder1/file1.txt")
|
130
|
+
file2 = @fixture_path.join("folder1/folder2/file2.txt")
|
131
|
+
|
132
|
+
FileUtils.touch file1
|
133
|
+
|
134
|
+
sleep 0.2
|
135
|
+
|
136
|
+
fsevent.events.collect do |e|
|
137
|
+
e.event_path
|
138
|
+
end.should include(@fixture_path.join("folder1/").to_s)
|
139
|
+
|
140
|
+
FileUtils.touch file2
|
141
|
+
|
142
|
+
sleep 0.2
|
143
|
+
|
144
|
+
fsevent.events.collect do |e|
|
145
|
+
e.event_path
|
146
|
+
end.should include(@fixture_path.join("folder1/folder2/").to_s)
|
147
|
+
end
|
148
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'rb-fsevent-legacy'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.color_enabled = true
|
6
|
+
|
7
|
+
config.before(:each) do
|
8
|
+
@fixture_path = Pathname.new(File.expand_path('../fixtures/', __FILE__))
|
9
|
+
end
|
10
|
+
|
11
|
+
config.after(:all) do
|
12
|
+
gem_root = Pathname.new(File.expand_path('../../', __FILE__))
|
13
|
+
system "rm -rf #{gem_root.join('bin')}"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rb-fsevent-legacy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Stephen Prater
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-02-27 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: bundler
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 0
|
32
|
+
- 10
|
33
|
+
version: 1.0.10
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 27
|
45
|
+
segments:
|
46
|
+
- 2
|
47
|
+
- 5
|
48
|
+
- 0
|
49
|
+
version: 2.5.0
|
50
|
+
type: :development
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rcov
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
type: :development
|
65
|
+
version_requirements: *id003
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: pry
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
type: :development
|
79
|
+
version_requirements: *id004
|
80
|
+
description: A legacy compatible version of an FSEvents API for Darwin.
|
81
|
+
email:
|
82
|
+
- me@stephenprater.com
|
83
|
+
executables: []
|
84
|
+
|
85
|
+
extensions:
|
86
|
+
- ext/rb-fsevent-legacy/extconf.rb
|
87
|
+
extra_rdoc_files: []
|
88
|
+
|
89
|
+
files:
|
90
|
+
- Gemfile
|
91
|
+
- LICENSE
|
92
|
+
- README.rdoc
|
93
|
+
- Rakefile
|
94
|
+
- ext/rb-fsevent-legacy/extconf.rb
|
95
|
+
- ext/rb-fsevent-legacy/fs_native_stream.c
|
96
|
+
- lib/rb-fsevent-legacy.rb
|
97
|
+
- lib/rb-fsevent-legacy/event.rb
|
98
|
+
- lib/rb-fsevent-legacy/stream.rb
|
99
|
+
- lib/rb-fsevent-legacy/version.rb
|
100
|
+
- rb-fsevent-legacy.gemspec
|
101
|
+
- spec/fixtures/folder1/file1.txt
|
102
|
+
- spec/fixtures/folder1/file3.txt
|
103
|
+
- spec/fixtures/folder1/folder2/file2.txt
|
104
|
+
- spec/rb-fsevent/fsevent_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
homepage: http://github.com/stephenprater/rb-fsevents-legacy
|
107
|
+
licenses: []
|
108
|
+
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
- ext
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
hash: 3
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
hash: 3
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
requirements: []
|
134
|
+
|
135
|
+
rubyforge_project: rb-fsevent-legacy
|
136
|
+
rubygems_version: 1.8.16
|
137
|
+
signing_key:
|
138
|
+
specification_version: 3
|
139
|
+
summary: Very simple & usable FSEvents API - it works on PowerPCs and 10.5.8
|
140
|
+
test_files: []
|
141
|
+
|