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.
Files changed (3) hide show
  1. data/README +50 -0
  2. data/bin/reloader +536 -0
  3. 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
+