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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +40 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +5 -0
- data/example/Gemfile +8 -0
- data/example/Gemfile.lock +119 -0
- data/example/Rakefile +6 -0
- data/example/app/assets/images/.keep +0 -0
- data/example/app/assets/javascripts/application.js +1 -0
- data/example/app/assets/stylesheets/application.css +3 -0
- data/example/app/controllers/application_controller.rb +8 -0
- data/example/app/views/application/home.html.erb +10 -0
- data/example/app/views/layouts/application.html.erb +14 -0
- data/example/bin/bundle +3 -0
- data/example/bin/rails +4 -0
- data/example/bin/rake +4 -0
- data/example/bin/setup +29 -0
- data/example/config.ru +4 -0
- data/example/config/application.rb +26 -0
- data/example/config/boot.rb +3 -0
- data/example/config/environment.rb +5 -0
- data/example/config/environments/development.rb +35 -0
- data/example/config/initializers/livereload_rails.rb +5 -0
- data/example/config/routes.rb +3 -0
- data/example/config/secrets.yml +14 -0
- data/example/log/.gitignore +2 -0
- data/example/public/favicon.ico +0 -0
- data/example/tmp/.gitignore +2 -0
- data/lib/livereload_rails.rb +48 -0
- data/lib/livereload_rails/client.rb +67 -0
- data/lib/livereload_rails/middleware.rb +56 -0
- data/lib/livereload_rails/railtie.rb +10 -0
- data/lib/livereload_rails/stream.rb +145 -0
- data/lib/livereload_rails/version.rb +3 -0
- data/lib/livereload_rails/watcher.rb +40 -0
- data/lib/livereload_rails/web_socket.rb +150 -0
- data/livereload_rails.gemspec +32 -0
- data/vendor/assets/javascripts/livereload.js +1183 -0
- metadata +229 -0
@@ -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
|
File without changes
|
@@ -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,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
|