webdevreloader 0.5

Sign up to get free protection for your applications and to get access to all the features.
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
+