livereload_rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/CODE_OF_CONDUCT.md +13 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +40 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +7 -0
  10. data/bin/setup +5 -0
  11. data/example/Gemfile +8 -0
  12. data/example/Gemfile.lock +119 -0
  13. data/example/Rakefile +6 -0
  14. data/example/app/assets/images/.keep +0 -0
  15. data/example/app/assets/javascripts/application.js +1 -0
  16. data/example/app/assets/stylesheets/application.css +3 -0
  17. data/example/app/controllers/application_controller.rb +8 -0
  18. data/example/app/views/application/home.html.erb +10 -0
  19. data/example/app/views/layouts/application.html.erb +14 -0
  20. data/example/bin/bundle +3 -0
  21. data/example/bin/rails +4 -0
  22. data/example/bin/rake +4 -0
  23. data/example/bin/setup +29 -0
  24. data/example/config.ru +4 -0
  25. data/example/config/application.rb +26 -0
  26. data/example/config/boot.rb +3 -0
  27. data/example/config/environment.rb +5 -0
  28. data/example/config/environments/development.rb +35 -0
  29. data/example/config/initializers/livereload_rails.rb +5 -0
  30. data/example/config/routes.rb +3 -0
  31. data/example/config/secrets.yml +14 -0
  32. data/example/log/.gitignore +2 -0
  33. data/example/public/favicon.ico +0 -0
  34. data/example/tmp/.gitignore +2 -0
  35. data/lib/livereload_rails.rb +48 -0
  36. data/lib/livereload_rails/client.rb +67 -0
  37. data/lib/livereload_rails/middleware.rb +56 -0
  38. data/lib/livereload_rails/railtie.rb +10 -0
  39. data/lib/livereload_rails/stream.rb +145 -0
  40. data/lib/livereload_rails/version.rb +3 -0
  41. data/lib/livereload_rails/watcher.rb +40 -0
  42. data/lib/livereload_rails/web_socket.rb +150 -0
  43. data/livereload_rails.gemspec +32 -0
  44. data/vendor/assets/javascripts/livereload.js +1183 -0
  45. metadata +229 -0
@@ -0,0 +1,5 @@
1
+ if defined?(LivereloadRails)
2
+ LivereloadRails.configure do |config|
3
+ config.logger.level = Logger::DEBUG
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ root to: "application#home"
3
+ end
@@ -0,0 +1,14 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Your secret key is used for verifying the integrity of signed cookies.
4
+ # If you change this key, all old signed cookies will become invalid!
5
+
6
+ # Make sure the secret is at least 30 characters and all random,
7
+ # no regular words or you'll be exposed to dictionary attacks.
8
+ # You can use `rake secret` to generate a secure secret key.
9
+
10
+ # Make sure the secrets in this file are kept private
11
+ # if you're sharing your code publicly.
12
+
13
+ development:
14
+ secret_key_base: f0c7e03d29eb39c19a35c0d7d985db54d133053cd6c99db410fc0f8d6b061fd0e39d7306985fc6c0cd6b4bf2db58e018f2643658a009e3c3c2ff5d9f333da9ae
@@ -0,0 +1,2 @@
1
+ *
2
+ !.gitignore
File without changes
@@ -0,0 +1,2 @@
1
+ *
2
+ !.gitignore
@@ -0,0 +1,48 @@
1
+ require "logger"
2
+ require "livereload_rails/version"
3
+ require "livereload_rails/watcher"
4
+ require "livereload_rails/stream"
5
+ require "livereload_rails/web_socket"
6
+ require "livereload_rails/client"
7
+ require "livereload_rails/middleware"
8
+ require "livereload_rails/railtie" if defined?(Rails)
9
+
10
+ module LivereloadRails
11
+ class Error < StandardError; end
12
+ class HijackingNotSupported < Error; end
13
+
14
+ @matchers = {}
15
+ @logger = Logger.new(File::NULL)
16
+ @paths = lambda { |paths| paths }
17
+
18
+ class << self
19
+ attr_accessor :matchers
20
+ attr_accessor :logger
21
+ attr_accessor :paths
22
+
23
+ def configure
24
+ yield self
25
+ end
26
+ end
27
+ end
28
+
29
+ LivereloadRails.configure do |config|
30
+ config.matchers[:stylesheets] = lambda do |file|
31
+ "everything.css" if file["assets/stylesheets/"]
32
+ end
33
+
34
+ config.matchers[:assets] = lambda do |file|
35
+ "everything#{File.extname(file)}" if file["assets/"]
36
+ end
37
+
38
+ config.matchers[:views] = lambda do |file|
39
+ "everything.html" if file["views/"]
40
+ end
41
+
42
+ config.paths = lambda do |paths|
43
+ [File.join(Dir.pwd, "app/views")].concat(paths)
44
+ end
45
+
46
+ config.logger = Logger.new($stderr)
47
+ config.logger.level = Logger::INFO
48
+ end
@@ -0,0 +1,67 @@
1
+ module LivereloadRails
2
+ class Client
3
+ FSM = {
4
+ initial: {},
5
+ opened: { "hello" => :on_hello },
6
+ idle: { "info" => nil },
7
+ closed: {},
8
+ }
9
+
10
+ def initialize(ws)
11
+ @state = :initial
12
+
13
+ @connection = ws
14
+ @connection.on(:open) { @state = :opened }
15
+ @connection.on(:close) { close }
16
+ @connection.on(:message) do |frame|
17
+ data = JSON.parse(frame.data)
18
+ command = data["command"]
19
+
20
+ if FSM[@state].has_key?(command)
21
+ if handler = FSM[@state][command]
22
+ public_send(handler, data)
23
+ end
24
+ else
25
+ raise "Unexpected #{data["command"].inspect} in #{@state}."
26
+ end
27
+ end
28
+ end
29
+
30
+ def on_hello(frame)
31
+ send_data({
32
+ command: "hello",
33
+ protocols: [
34
+ "http://livereload.com/protocols/official-7"
35
+ ],
36
+ serverName: "Elabs' LivereloadRails",
37
+ })
38
+
39
+ @state = :idle
40
+ end
41
+
42
+ def reload(path, live: true)
43
+ send_data(command: "reload", path: path, liveCSS: live)
44
+ end
45
+
46
+ def alert(message)
47
+ send_data(command: "alert", message: message)
48
+ end
49
+
50
+ def close(reason = nil)
51
+ if @state != :closed
52
+ @state = :closed
53
+ alert(reason) if reason
54
+ @connection.close
55
+ true
56
+ else
57
+ false
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def send_data(object)
64
+ @connection.write JSON.generate(object)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,56 @@
1
+ require "monitor"
2
+ require "set"
3
+ require "filewatcher"
4
+
5
+ module LivereloadRails
6
+ class Middleware
7
+ ASYNC_RESPONSE = [-1, {}, []]
8
+
9
+ def initialize(app, assets:, matchers: LivereloadRails.matchers)
10
+ @app = app
11
+ @clients = Set.new
12
+ @clients.extend(MonitorMixin)
13
+
14
+ assets.configure do |environment|
15
+ @watcher = Watcher.new(LivereloadRails.paths.call(environment.paths), matchers: matchers) do |file|
16
+ client_path = "#{assets.prefix}/#{file}"
17
+ clients = @clients.synchronize { @clients.dup }
18
+
19
+ LivereloadRails.logger.debug "Reloading #{clients.size} clients with #{client_path}."
20
+ clients.each { |client| client.reload(client_path) }
21
+ end
22
+
23
+ @watcher_thread = Thread.new do
24
+ Thread.current.abort_on_exception = true
25
+ @watcher.run
26
+ end
27
+ end
28
+ end
29
+
30
+ def call(env)
31
+ if env["PATH_INFO"] == "/livereload"
32
+ websocket = LivereloadRails::WebSocket.from_rack(env) do |ws|
33
+ client = LivereloadRails::Client.new(ws)
34
+
35
+ ws.on(:open) do
36
+ @clients.synchronize { @clients.add(client) }
37
+ LivereloadRails.logger.debug "#{client} joined: #{@clients.size}."
38
+ end
39
+
40
+ ws.on(:close) do
41
+ @clients.synchronize { @clients.delete(client) }
42
+ LivereloadRails.logger.debug "#{client} left: #{@clients.size}."
43
+ end
44
+ end
45
+
46
+ if websocket
47
+ ASYNC_RESPONSE
48
+ else
49
+ @app.call(env)
50
+ end
51
+ else
52
+ @app.call(env)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ require "livereload_rails"
2
+ require "rails/engine"
3
+ require "rack-livereload"
4
+
5
+ module LivereloadRails
6
+ class Railtie < ::Rails::Engine
7
+ config.app_middleware.use Rack::LiveReload, live_reload_port: Rails::Server.new.options[:Port]
8
+ config.app_middleware.use LivereloadRails::Middleware, assets: config.assets
9
+ end
10
+ end
@@ -0,0 +1,145 @@
1
+ require "thread"
2
+ require "nio"
3
+
4
+ module LivereloadRails
5
+ # A non-blocking connection.
6
+ class Stream
7
+ READ_CHUNK = 1024 * 10
8
+ EMPTY = "".freeze
9
+ SWALLOW_ERRORS = [EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::EPROTOTYPE]
10
+
11
+ # @example
12
+ # stream = Stream.new(io) do |input|
13
+ # # handle input
14
+ # end
15
+ # stream.loop
16
+ #
17
+ # @param [#read_nonblock, #write_nonblock] io
18
+ #
19
+ # @yield [input] whenever there is input to be consumed.
20
+ # @yieldparam input [String] streaming input data.
21
+ def initialize(io)
22
+ @io = io
23
+ @io_writer = @io.dup
24
+
25
+ @read_block = Proc.new
26
+
27
+ @input_buffer = "".b
28
+ @output_buffer = "".b
29
+ @output_queue = []
30
+ @mutex = Mutex.new
31
+
32
+ @wakeup, @waker = IO.pipe
33
+ end
34
+
35
+ # Queue a message to be sent later on the stream.
36
+ #
37
+ # There is no guarantee that the message will arrive. If you want a receipt
38
+ # of any kind you will need to wait for a reply.
39
+ #
40
+ # @param [String] message
41
+ def write(message)
42
+ @mutex.synchronize do
43
+ @output_queue.push(message)
44
+ @waker.write("\0")
45
+ end
46
+ end
47
+
48
+ # Close the connection immediately.
49
+ #
50
+ # TODO: SO_LINGER, close before or after sending outgoing data?
51
+ def close
52
+ @io.close unless @io.closed?
53
+ @io_writer.close unless @io_writer.closed?
54
+ @waker.close unless @waker.closed?
55
+ @wakeup.close unless @wakeup.closed?
56
+ end
57
+
58
+ # @return [Boolean] true if stream is closed.
59
+ def closed?
60
+ @io.closed?
61
+ end
62
+
63
+ # @param [NIO::Selector] selector
64
+ def loop(selector = NIO::Selector.new)
65
+ @looping = ! closed?
66
+ return unless @looping
67
+
68
+ wakeup_monitor = selector.register(@wakeup, :r)
69
+ wakeup_monitor.value = handler_for(:wakeup_handler)
70
+
71
+ read_monitor = selector.register(@io, :r)
72
+ read_monitor.value = handler_for(:read_handler)
73
+
74
+ register_writer(selector)
75
+
76
+ while @looping
77
+ selector.select { |monitor| monitor.value.call(monitor) }
78
+ end
79
+ ensure
80
+ selector.deregister(@io)
81
+ selector.deregister(@io_writer)
82
+ selector.deregister(@wakeup)
83
+ end
84
+
85
+ private
86
+
87
+ # @note The returned string is always the same object.
88
+ # @return [String] the current output buffer, possibly refilled from the output queue.
89
+ def output_buffer
90
+ if @output_buffer.empty? and @output_queue.length > 0
91
+ message = @mutex.synchronize { @output_queue.pop }
92
+ @output_buffer.replace(message)
93
+ end
94
+
95
+ @output_buffer
96
+ end
97
+
98
+ def register_writer(selector)
99
+ return if selector.registered?(@io_writer)
100
+ return if output_buffer.empty?
101
+
102
+ write_monitor = selector.register(@io_writer, :w)
103
+ write_monitor.value = handler_for(:write_handler)
104
+ end
105
+
106
+ def handler_for(method_name)
107
+ handler_method = method(method_name)
108
+
109
+ lambda do |monitor|
110
+ begin
111
+ handler_method.call(monitor)
112
+ rescue IO::WaitReadable, IO::WaitWritable
113
+ # No op. Let monitor continue be selected.
114
+ rescue *SWALLOW_ERRORS
115
+ @looping = false
116
+ ensure
117
+ @looping = false if $!
118
+ end
119
+ end
120
+ end
121
+
122
+ def wakeup_handler(monitor)
123
+ @wakeup.read(@wakeup.stat.size)
124
+ register_writer(monitor.selector)
125
+ end
126
+
127
+ def write_handler(monitor)
128
+ until output_buffer.empty?
129
+ bytes_written = @io.write_nonblock(output_buffer)
130
+ output_buffer[0, bytes_written] = EMPTY
131
+ end
132
+
133
+ monitor.close # write_nonblock did not raise, so we have no more output.
134
+ end
135
+
136
+ def read_handler(monitor)
137
+ Kernel.loop do
138
+ @io.read_nonblock(READ_CHUNK, @input_buffer)
139
+ @read_block[@input_buffer]
140
+ end
141
+ ensure
142
+ @input_buffer.clear
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,3 @@
1
+ module LivereloadRails
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,40 @@
1
+ module LivereloadRails
2
+ class Watcher
3
+ def initialize(paths, matchers:, &update)
4
+ LivereloadRails.logger.debug "Watching #{paths} for changes."
5
+ @watcher = FileWatcher.new(paths)
6
+ @matchers = matchers
7
+ @update = update
8
+ end
9
+
10
+ def run(timeout = 0.2)
11
+ @watcher.watch(timeout) do |path, event|
12
+ unless FileTest.file?(path)
13
+ LivereloadRails.logger.debug "#{path} -> not a file."
14
+ next
15
+ end
16
+
17
+ unless filename = translate(path)
18
+ LivereloadRails.logger.debug "#{path} -> no match."
19
+ next
20
+ end
21
+
22
+ if filename.empty?
23
+ LivereloadRails.logger.debug "#{path} -> ignored."
24
+ next
25
+ end
26
+
27
+ LivereloadRails.logger.debug "#{path} -> #{filename}."
28
+ @update[filename]
29
+ end
30
+ end
31
+
32
+ def translate(path)
33
+ @matchers.find do |name, matcher|
34
+ if value = matcher.call(path)
35
+ return value
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,150 @@
1
+ require "websocket"
2
+ require "thread"
3
+ require "nio"
4
+
5
+ # TODO: Global flag could interfere with other code.
6
+ ::WebSocket.should_raise = true
7
+
8
+ module LivereloadRails
9
+ # Embodies a WebSocket connection as a separate thread.
10
+ class WebSocket
11
+ PING_TIMEOUT = 1
12
+
13
+ class << self
14
+ # Same as #initialize, but first checks if the request is a websocket upgrade.
15
+ #
16
+ # @example
17
+ # WebSocket.from_rack(env) do |ws|
18
+ # …
19
+ # ws.on(:open) { … }
20
+ # ws.on(:message) { … }
21
+ # ws.on(:close) { … }
22
+ # end
23
+ #
24
+ # @return [WebSocket, nil] a websocket instance, or nil if request was not a websocket.
25
+ def from_rack(env, &block)
26
+ new(env, &block) if env["HTTP_UPGRADE"] == "websocket"
27
+ end
28
+ end
29
+
30
+ # @param env a rack environment hash
31
+ def initialize(env)
32
+ raise ArgumentError, "no block given" unless block_given?
33
+
34
+ @env = env
35
+ @handlers = { open: Set.new, close: Set.new, message: Set.new }
36
+ @handshake = ::WebSocket::Handshake::Server.new(secure: false)
37
+
38
+ queue = Queue.new
39
+
40
+ @thread = Thread.new do
41
+ begin
42
+ finish_initialize = proc do |event|
43
+ finish_initialize = nil
44
+ queue << event
45
+ end
46
+
47
+ hijack do
48
+ yield self
49
+ finish_initialize[:connected]
50
+ end
51
+ ensure
52
+ finish_initialize[$!] if finish_initialize
53
+ end
54
+ end
55
+
56
+ message = queue.pop
57
+ raise message if message.is_a?(Exception)
58
+ end
59
+
60
+ attr_reader :thread
61
+
62
+ # Register an event handler.
63
+ #
64
+ # @example
65
+ # handler = websocket.on(:open) { … }
66
+ #
67
+ # @note If an event handler raises an error, handlers after it will not run.
68
+ # @param [Symbol] event (one of :open, :close, :message)
69
+ def on(event, &handler)
70
+ raise ArgumentError, "no event named #{event.inspect}" unless @handlers.has_key?(event)
71
+
72
+ @handlers[event].add(handler)
73
+ handler
74
+ end
75
+
76
+ # Queues data for writing. It is not guaranteed that client will receive message.
77
+ #
78
+ # @param [#to_s] data
79
+ # @param [Symbol] type
80
+ def write(data, type: :text)
81
+ frame = ::WebSocket::Frame::Outgoing::Server.new(data: data, type: type, version: @handshake.version)
82
+ @stream.write(frame.to_s)
83
+ end
84
+
85
+ # Close the connection.
86
+ #
87
+ # Can safely be called multiple times.
88
+ def close
89
+ @stream.close if @stream
90
+ end
91
+
92
+ private
93
+
94
+ # Trigger all handlers for the given event with the given arguments.
95
+ #
96
+ # @param [Symbol] event
97
+ def trigger(event, *args)
98
+ @handlers[event].each { |handler| handler.call(*args) }
99
+ end
100
+
101
+ # Main loop of the WebSocket thread.
102
+ def hijack
103
+ unless @env["rack.hijack?"]
104
+ raise HijackingNotSupported, "server does not support hijacking"
105
+ end
106
+
107
+ # See http://www.rubydoc.info/github/rack/rack/file/SPEC
108
+ @env["rack.hijack"].call
109
+ @io = @env["rack.hijack_io"]
110
+
111
+ yield
112
+
113
+ @handshake.from_rack(@env)
114
+ raise @handshake.error unless @handshake.valid?
115
+
116
+ frame_parser = ::WebSocket::Frame::Incoming::Server.new(version: @handshake.version)
117
+ @stream = LivereloadRails::Stream.new(@io) do |input|
118
+ handle_frames(frame_parser, input)
119
+ end
120
+ @stream.write(@handshake.to_s)
121
+
122
+ trigger :open
123
+ handle_frames(frame_parser, @handshake.leftovers)
124
+
125
+ @stream.loop
126
+ ensure
127
+ close
128
+ trigger :close, *$!
129
+ end
130
+
131
+ def handle_frames(frame_parser, data)
132
+ frame_parser << data
133
+
134
+ while frame = frame_parser.next
135
+ case frame.type
136
+ when :text, :binary
137
+ trigger :message, frame
138
+ when :ping
139
+ write(nil, :pong)
140
+ when :pong
141
+ # TODO: reset timeout timer.
142
+ when :close
143
+ close
144
+ else
145
+ raise "unknown frame type #{frame.type}"
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end