webdevreloader 0.5
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/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
|
+
|