livereload_rails 1.0.0

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 (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