webdevreloader 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README +50 -0
- data/bin/reloader +536 -0
- metadata +62 -0
data/README
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
Smart web application reloader
|
2
|
+
------------------------------
|
3
|
+
|
4
|
+
This is simple http request forwarder that checks your application
|
5
|
+
sources for changes and transparently restarts it if there are any.
|
6
|
+
|
7
|
+
It works with any web technology and provides development convenience
|
8
|
+
similar (but superior) to Rails development mode reloading.
|
9
|
+
|
10
|
+
Make your source change and than simply hit reload button. Reloader
|
11
|
+
will notice change and will transparently restart your application.
|
12
|
+
|
13
|
+
Usage
|
14
|
+
-----
|
15
|
+
|
16
|
+
Upstream port (the one you should point your broswer to) is controlled
|
17
|
+
by -u option. Downstream port where your application server listens is
|
18
|
+
controlled by -d option. You can pass file patterns to check for
|
19
|
+
changes by -w options. Rest of command line arguments are interpreted
|
20
|
+
as a command to run your application.
|
21
|
+
|
22
|
+
By default reloader kills application even when there is active
|
23
|
+
request execution. You can pass '--kill-gently' to make it wait active
|
24
|
+
requests completion before killing application. This usually matters
|
25
|
+
only for applications that use push.
|
26
|
+
|
27
|
+
All options has defaults suitable for Rails applications. In this case
|
28
|
+
just use command 'reloader ./script/server'. But remember to point
|
29
|
+
your browser to port 8080, not 3000. You should also turn off Rails
|
30
|
+
development mode reloading by setting config.cache_classes to true.
|
31
|
+
|
32
|
+
Other cases need suitable options. For example I use the following
|
33
|
+
command line to run ns_server which is erlang application:
|
34
|
+
|
35
|
+
reloader -T 30 -s TERM -u 3000 -d 8080 -w '**/*.erl' -w '**/*.js' -- sh -c 'make -j3 fast-rebuild && ./start_shell.sh -noshell'
|
36
|
+
|
37
|
+
This command forwards port 3000 to port 8080 (erlang server binds to
|
38
|
+
8080). It also checks mtimes of all erlang (*.erl) and javascript
|
39
|
+
(*.js) sources.
|
40
|
+
|
41
|
+
As you can see you can easily invoke make to (re)build
|
42
|
+
application. Also note that your application is run as background
|
43
|
+
process group, so it should not try to read stdin. I pass -noshell to
|
44
|
+
erlang to avoid that.
|
45
|
+
|
46
|
+
|
47
|
+
License
|
48
|
+
-------
|
49
|
+
|
50
|
+
GPLv3. See COPYING.
|
data/bin/reloader
ADDED
@@ -0,0 +1,536 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- mode: ruby -*-
|
3
|
+
|
4
|
+
require 'socket'
|
5
|
+
require 'optparse'
|
6
|
+
require 'thread'
|
7
|
+
require 'fcntl'
|
8
|
+
|
9
|
+
require "rubygems"
|
10
|
+
require "xray/thread_dump_signal_handler" rescue nil
|
11
|
+
|
12
|
+
require 'io/wait'
|
13
|
+
|
14
|
+
module Kernel
|
15
|
+
def p_log(category, message, *extra)
|
16
|
+
return if category.to_s =~ /_v$/ # suppress verbose messages
|
17
|
+
return if category == :downstream && message =~ /^got something/
|
18
|
+
return if category == :http
|
19
|
+
STDOUT.print Thread.current.object_id, ": "
|
20
|
+
STDOUT.print category, ": "
|
21
|
+
STDOUT.print message
|
22
|
+
ex = extra.map {|e| e.inspect}.join(", ")
|
23
|
+
STDOUT.puts ex
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Exception
|
28
|
+
def diagnose
|
29
|
+
class_str = self.class.to_s
|
30
|
+
out = "" << self.backtrace[0] << ": " << class_str
|
31
|
+
unless self.message.empty? || self.message == class_str
|
32
|
+
out << ": " << message
|
33
|
+
end
|
34
|
+
out << "\n\t" << self.backtrace[1..-1].join("\n\t") << "\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Thread
|
39
|
+
def self.run_diag(except = nil, &block)
|
40
|
+
Thread.new do
|
41
|
+
begin
|
42
|
+
block.call
|
43
|
+
rescue Exception
|
44
|
+
unless except === $!
|
45
|
+
p_log :thread_diag, $!.diagnose
|
46
|
+
end
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# simple recursive (for readers) rwlock that prefers writers over
|
54
|
+
# readers
|
55
|
+
class RWLock
|
56
|
+
def initialize
|
57
|
+
@mutex = Mutex.new
|
58
|
+
@state = :free
|
59
|
+
@shared_q = ConditionVariable.new
|
60
|
+
@shared_counter = 0
|
61
|
+
@exclusive_q = ConditionVariable.new
|
62
|
+
|
63
|
+
@exclusive_requests = 0
|
64
|
+
end
|
65
|
+
|
66
|
+
def take_shared
|
67
|
+
@mutex.synchronize do
|
68
|
+
while true
|
69
|
+
if @exclusive_requests == 0
|
70
|
+
case @state
|
71
|
+
when :free, :shared
|
72
|
+
@shared_counter += 1
|
73
|
+
@state = :shared
|
74
|
+
return
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
p_log :lock, "sleeping for shared lock"
|
79
|
+
@shared_q.wait(@mutex)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def release_shared
|
85
|
+
@mutex.synchronize do
|
86
|
+
@shared_counter -= 1
|
87
|
+
@state = :free if @shared_counter == 0
|
88
|
+
@exclusive_q.signal
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def take_exclusive
|
93
|
+
@mutex.synchronize do
|
94
|
+
@exclusive_requests += 1
|
95
|
+
while true
|
96
|
+
if @state == :free
|
97
|
+
@state = :exclusive
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
p_log :lock, "sleeping for x-lock. #{self.inspect}"
|
102
|
+
@exclusive_q.wait(@mutex)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def release_exclusive
|
108
|
+
@mutex.synchronize do
|
109
|
+
@exclusive_requests -= 1
|
110
|
+
|
111
|
+
@state = :free
|
112
|
+
if @exclusive_requests > 0
|
113
|
+
@exclusive_q.signal
|
114
|
+
else
|
115
|
+
@shared_q.signal
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def cannot_raise
|
121
|
+
yield
|
122
|
+
rescue Exception
|
123
|
+
puts "BUG!"
|
124
|
+
raise
|
125
|
+
end
|
126
|
+
|
127
|
+
def synchronize_shared
|
128
|
+
take_shared
|
129
|
+
yield
|
130
|
+
ensure
|
131
|
+
cannot_raise do
|
132
|
+
release_shared
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def synchronize_exclusive
|
137
|
+
take_exclusive
|
138
|
+
yield
|
139
|
+
ensure
|
140
|
+
cannot_raise do
|
141
|
+
release_exclusive
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
$gentle_killer = false
|
147
|
+
|
148
|
+
$child_rd_pipe = nil
|
149
|
+
$child_pgrp = nil
|
150
|
+
|
151
|
+
$kill_signal = "KILL"
|
152
|
+
|
153
|
+
$child_port = 3000
|
154
|
+
$child_spawn_timeout = 30
|
155
|
+
|
156
|
+
module ChildController
|
157
|
+
module_function
|
158
|
+
|
159
|
+
def port_busyness(port)
|
160
|
+
TCPSocket.new('127.0.0.1', port).close
|
161
|
+
true
|
162
|
+
rescue Errno::ECONNREFUSED
|
163
|
+
false
|
164
|
+
end
|
165
|
+
|
166
|
+
def poll_for_condition(timeout=5)
|
167
|
+
target = Time.now.to_f + timeout
|
168
|
+
begin
|
169
|
+
return if yield
|
170
|
+
sleep 0.05
|
171
|
+
end while (Time.now.to_f <= target)
|
172
|
+
raise Errno::ETIMEDOUT
|
173
|
+
end
|
174
|
+
|
175
|
+
def kill_child!(wait = false)
|
176
|
+
p_log :child, "killing child!"
|
177
|
+
Process.kill($kill_signal, -$child_pgrp) rescue nil
|
178
|
+
Process.kill($kill_signal, $child_pgrp) rescue nil
|
179
|
+
$child_pgrp, pid = nil, $child_pgrp
|
180
|
+
$child_rd_pipe.close rescue nil
|
181
|
+
$child_rd_pipe = nil
|
182
|
+
if wait
|
183
|
+
poll_for_condition do
|
184
|
+
!port_busyness($child_port)
|
185
|
+
end
|
186
|
+
begin
|
187
|
+
loop do
|
188
|
+
Process::waitpid(pid, Process::WNOHANG)
|
189
|
+
end
|
190
|
+
rescue Errno::ECHILD
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def child_alife?
|
196
|
+
$child_rd_pipe.read_nonblock(1)
|
197
|
+
raise "Cannot happen"
|
198
|
+
rescue Errno::EAGAIN
|
199
|
+
true
|
200
|
+
rescue EOFError
|
201
|
+
false
|
202
|
+
end
|
203
|
+
|
204
|
+
def spawn_child!
|
205
|
+
p_log :child, "spawning child!"
|
206
|
+
$child_pgrp = nil
|
207
|
+
rd, wr = IO.pipe
|
208
|
+
|
209
|
+
sleeper_pid = nil
|
210
|
+
sleeper_pid = fork do
|
211
|
+
Process.setpgid(0, 0)
|
212
|
+
$0 = "sleeper: " + $0
|
213
|
+
Process.kill("STOP", 0)
|
214
|
+
end
|
215
|
+
Process.wait(sleeper_pid, Process::WUNTRACED)
|
216
|
+
|
217
|
+
$child_pgrp = fork do
|
218
|
+
Process.setpgid(Process.pid, sleeper_pid)
|
219
|
+
rd.close
|
220
|
+
STDIN.reopen("/dev/null")
|
221
|
+
begin
|
222
|
+
exec(*ARGV)
|
223
|
+
rescue Exception
|
224
|
+
puts "failed to exec: #{$!.inspect}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
wr.close
|
229
|
+
$child_rd_pipe = rd
|
230
|
+
begin
|
231
|
+
Process.setpgid($child_pgrp, sleeper_pid)
|
232
|
+
rescue Errno::EACCESS
|
233
|
+
end
|
234
|
+
$child_pgrp = sleeper_pid
|
235
|
+
poll_for_condition($child_spawn_timeout) do
|
236
|
+
raise "The child exited before creating server socket" unless child_alife?
|
237
|
+
port_busyness($child_port)
|
238
|
+
end
|
239
|
+
p_log :child, "child is ready"
|
240
|
+
rescue Exception
|
241
|
+
if $child_pgrp
|
242
|
+
kill_child!
|
243
|
+
end
|
244
|
+
raise $!
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
at_exit do
|
249
|
+
p_log :child_v, "atexit!"
|
250
|
+
if $child_pgrp
|
251
|
+
ChildController.kill_child!
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
$interesting_files_patterns = ["vendor/**/*", "lib/**/*", "app/**/*", "config/**/*"]
|
256
|
+
$mtimes = {}
|
257
|
+
|
258
|
+
module DirWatcher
|
259
|
+
module_function
|
260
|
+
|
261
|
+
def collect_interesting_files!
|
262
|
+
mtimes = $mtimes = {}
|
263
|
+
names = $interesting_files_patterns.map {|p| Dir[p]}.flatten.sort.uniq
|
264
|
+
# p_log :collect_interesting_files!, "names: ", names
|
265
|
+
names.each do |path|
|
266
|
+
st = begin
|
267
|
+
File.stat(path)
|
268
|
+
rescue Errno::ENOENT
|
269
|
+
next
|
270
|
+
end
|
271
|
+
next unless st.file?
|
272
|
+
mtimes[path] = st.mtime.to_i
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def anything_changed?
|
277
|
+
$mtimes.each do |path, mtime|
|
278
|
+
st = begin
|
279
|
+
File.stat(path)
|
280
|
+
rescue Errno::ENOENT
|
281
|
+
return true
|
282
|
+
end
|
283
|
+
if st.mtime.to_i > mtime
|
284
|
+
p_log :child, "change detected on: ", path
|
285
|
+
return true
|
286
|
+
end
|
287
|
+
end
|
288
|
+
false
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
$socket_factory = lambda {TCPSocket.new('127.0.0.1', $child_port)}
|
293
|
+
|
294
|
+
class Reloader
|
295
|
+
class << self
|
296
|
+
attr_accessor :socket_factory
|
297
|
+
end
|
298
|
+
self.socket_factory = $socket_factory
|
299
|
+
|
300
|
+
module Utils
|
301
|
+
if defined?(Fcntl::FD_CLOEXEC)
|
302
|
+
def set_cloexec(io)
|
303
|
+
io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
304
|
+
end
|
305
|
+
else
|
306
|
+
def set_cloexec(io)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
HTTP_END_REQUEST_RE = /\r?\n\r?\n/m
|
311
|
+
HTTP_HEADER_END_RE = /\r?\n/m
|
312
|
+
|
313
|
+
def read_http_something(initial_data, io)
|
314
|
+
out = initial_data.dup
|
315
|
+
begin
|
316
|
+
io.readpartial(16384, out)
|
317
|
+
p_log :http, "got out: ", out
|
318
|
+
p_log :http_request, "req: ", out.split("\n").first.strip
|
319
|
+
end while out !~ HTTP_END_REQUEST_RE
|
320
|
+
|
321
|
+
offset = $~.begin(0)
|
322
|
+
request_length = $~.end(0)
|
323
|
+
request = out[0, offset]
|
324
|
+
|
325
|
+
headers = request.split(HTTP_HEADER_END_RE)
|
326
|
+
first_line = headers.shift
|
327
|
+
headers = headers.inject({}) do |h, line|
|
328
|
+
name, value = line.split(':', 2)
|
329
|
+
h[name.downcase.strip] = value.strip
|
330
|
+
h
|
331
|
+
end
|
332
|
+
|
333
|
+
# TODO: inspect other non-postable methods
|
334
|
+
if first_line =~ /^(GET |HEAD )/
|
335
|
+
return [out[0, request_length], out[request_length..-1]]
|
336
|
+
end
|
337
|
+
|
338
|
+
# transfer_coding = headers['transfer-coding']
|
339
|
+
# if transfer_coding
|
340
|
+
# unless transfer_coding == 'chunked'
|
341
|
+
# raise "unsupported transfer-coding: #{transfer_coding}"
|
342
|
+
# end
|
343
|
+
|
344
|
+
# while out[request_length..-1] !~ /([0-9A-Fa-f]+).*?\n\r?/ && request_length + 4096 < out.length
|
345
|
+
# io.readpartial(16384, out)
|
346
|
+
# end
|
347
|
+
|
348
|
+
# raise "missing chunk-size" unless $~
|
349
|
+
# request_length += $~.end
|
350
|
+
# chunk_size = $1.to_i(16)
|
351
|
+
# # HERE
|
352
|
+
# end
|
353
|
+
|
354
|
+
if headers['content-length']
|
355
|
+
length = headers['content-length'].to_i
|
356
|
+
length += request_length
|
357
|
+
if out.size < length
|
358
|
+
# p_log :http, "reading: ", length - out.size
|
359
|
+
out << io.read(length - out.size)
|
360
|
+
end
|
361
|
+
raise if out.size < length
|
362
|
+
p_log :http, "out_diag: ", length, out.length
|
363
|
+
return [out[0,length].dup,
|
364
|
+
out[length..-1].dup]
|
365
|
+
end
|
366
|
+
|
367
|
+
while true
|
368
|
+
begin
|
369
|
+
io.readpartial(16384, out)
|
370
|
+
rescue EOFError
|
371
|
+
break
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
[out, ""]
|
376
|
+
rescue EOFError
|
377
|
+
return ["", out] if out.size == initial_data.size
|
378
|
+
raise "Partial request or response"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
include Utils
|
383
|
+
|
384
|
+
class UpstreamConnection
|
385
|
+
include Utils
|
386
|
+
|
387
|
+
def initialize(socket, downstream)
|
388
|
+
@socket = socket
|
389
|
+
@downstream = downstream
|
390
|
+
@buffer = ""
|
391
|
+
end
|
392
|
+
|
393
|
+
def loop_iteration
|
394
|
+
request, @buffer = read_http_something(@buffer, @socket)
|
395
|
+
p_log :upstream_v, "out of read_http_something", request, @buffer
|
396
|
+
return :eof if request.empty?
|
397
|
+
@downstream.proxy_request(request)
|
398
|
+
end
|
399
|
+
|
400
|
+
def run
|
401
|
+
loop_iteration
|
402
|
+
ensure
|
403
|
+
close
|
404
|
+
end
|
405
|
+
|
406
|
+
def close
|
407
|
+
p_log :upstream, "closing"
|
408
|
+
@socket.close rescue nil
|
409
|
+
@downstream.close
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
class DownstreamConnection
|
414
|
+
include Utils
|
415
|
+
|
416
|
+
def initialize(socket, upstream_socket)
|
417
|
+
@socket = socket
|
418
|
+
@upstream_socket = upstream_socket
|
419
|
+
end
|
420
|
+
|
421
|
+
def proxy_request(request)
|
422
|
+
@socket << request
|
423
|
+
p_log :downstream, "sent request to downstream"
|
424
|
+
|
425
|
+
response, extra = read_http_something("", @socket)
|
426
|
+
if response.empty?
|
427
|
+
p_log :downstream, "downstream closed on in-flight request"
|
428
|
+
return :eof
|
429
|
+
end
|
430
|
+
|
431
|
+
p_log :downstream, "got something: #{response.inspect}"
|
432
|
+
raise unless extra.empty?
|
433
|
+
|
434
|
+
@upstream_socket << response
|
435
|
+
end
|
436
|
+
|
437
|
+
def close
|
438
|
+
return if @socket.closed?
|
439
|
+
@socket.close
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def initialize(port=8080)
|
444
|
+
@server = TCPServer.new(port)
|
445
|
+
set_cloexec(@server)
|
446
|
+
@lock = RWLock.new
|
447
|
+
end
|
448
|
+
|
449
|
+
def check_dir!
|
450
|
+
p_log :child, "entering check_dir"
|
451
|
+
unless $child_pgrp
|
452
|
+
p_log :child, "no child running"
|
453
|
+
DirWatcher.collect_interesting_files!
|
454
|
+
ChildController.spawn_child!
|
455
|
+
end
|
456
|
+
|
457
|
+
p_log :child, "checking changes"
|
458
|
+
if DirWatcher.anything_changed?
|
459
|
+
p_log :child, "changed!"
|
460
|
+
|
461
|
+
if $gentle_killer
|
462
|
+
# We spawn shared lock takers, so we can release lock right away
|
463
|
+
# Taking lock ensures that every in-progress request is completed
|
464
|
+
# and that's all we need.
|
465
|
+
@lock.synchronize_exclusive do
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
system "notify-send -t 1000 'killing child'"
|
470
|
+
ChildController.kill_child!(true)
|
471
|
+
|
472
|
+
p_log :child, "old worker is dead"
|
473
|
+
check_dir!
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
def loop
|
478
|
+
check_dir!
|
479
|
+
lock = @lock
|
480
|
+
|
481
|
+
while true
|
482
|
+
socket = @server.accept
|
483
|
+
p_log :upstream, "got new connection: #{socket.fileno}"
|
484
|
+
set_cloexec(socket)
|
485
|
+
|
486
|
+
begin
|
487
|
+
check_dir!
|
488
|
+
rescue Exception
|
489
|
+
p_log :critical, "Dropping connection due to exception from check_dir!:\n#{$!.diagnose}"
|
490
|
+
socket.close rescue nil
|
491
|
+
next
|
492
|
+
end
|
493
|
+
|
494
|
+
Thread.run_diag do
|
495
|
+
p_log :upstream, "spawned new thread"
|
496
|
+
lock.synchronize_shared do
|
497
|
+
downstream_socket = Reloader.socket_factory.call
|
498
|
+
set_cloexec(downstream_socket)
|
499
|
+
downstream = DownstreamConnection.new(downstream_socket, socket)
|
500
|
+
UpstreamConnection.new(socket, downstream).run
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
$server_port = 8080
|
508
|
+
$custom_patterns = false
|
509
|
+
|
510
|
+
opts = OptionParser.new
|
511
|
+
opts.banner = "Usage: #{File.basename($0)} [options] command..."
|
512
|
+
opts.on("-u", "--upstream-port=VAL", Integer, "Port to listen on") {|x| $server_port = x}
|
513
|
+
opts.on("-d", "--downstream-port=VAL", Integer, "Port where app server listens") {|x| $child_port = x}
|
514
|
+
opts.on("-T", "--spawn-timeout=VAL", Float, "Timeout for app server port readiness") {|x| $child_spawn_timeout = x}
|
515
|
+
opts.on("-s", "--kill-signal=VAL", "Signal used to kill child") {|x| $kill_signal = x}
|
516
|
+
opts.on("-g", "--kill-gently", "Wait requests completion before killing/respawning") { $gentle_killer = true }
|
517
|
+
opts.on("-w", "--watch=VAL", "File pattern to watch (add as many as you want)") do |x|
|
518
|
+
unless $custom_patterns
|
519
|
+
$custom_patterns = true
|
520
|
+
$interesting_files_patterns = []
|
521
|
+
end
|
522
|
+
$interesting_files_patterns << x
|
523
|
+
end
|
524
|
+
new_argv = opts.parse(*ARGV)
|
525
|
+
ARGV.replace(new_argv)
|
526
|
+
|
527
|
+
if ARGV.empty?
|
528
|
+
puts "Need command to spawn child"
|
529
|
+
exit 1
|
530
|
+
end
|
531
|
+
|
532
|
+
p_log :opts, "child_port: ", $child_port
|
533
|
+
p_log :opts, "server_port: ", $server_port
|
534
|
+
p_log :opts, "interesting_files_patterns: ", $interesting_files_patterns
|
535
|
+
|
536
|
+
Reloader.new($server_port).loop
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: webdevreloader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
version: "0.5"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Aliaksey Kandratsenka
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-07-06 00:00:00 -07:00
|
17
|
+
default_executable: reloader
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description: reloader watches your application files and reloads your app when something changes
|
21
|
+
email: alk@tut.by
|
22
|
+
executables:
|
23
|
+
- reloader
|
24
|
+
extensions: []
|
25
|
+
|
26
|
+
extra_rdoc_files: []
|
27
|
+
|
28
|
+
files:
|
29
|
+
- README
|
30
|
+
- bin/reloader
|
31
|
+
has_rdoc: true
|
32
|
+
homepage:
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- bin
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.3.6
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Smart web application reloader
|
61
|
+
test_files: []
|
62
|
+
|