-
-
Notifications
You must be signed in to change notification settings - Fork 159
/
Copy pathwatch.cr
368 lines (309 loc) · 9.77 KB
/
watch.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
require "lucky_task"
require "colorize"
require "yaml"
require "http"
require "../src/lucky/server_settings"
require "../src/lucky/page_helpers/time_helpers"
# Based on the sentry shard with some modifications to output and build process.
module LuckySentry
FILE_TIMESTAMPS = {} of String => String # {file => timestamp}
# Base Watcher class
abstract class Watcher
abstract def start : Nil
abstract def reload : Nil
abstract def running? : Bool
abstract def running_at : String?
end
# Watcher using WebSockets to reload browser
class WebSocketWatcher < Watcher
@captured_sockets = [] of HTTP::WebSocket
@server : HTTP::Server
def initialize
handler = HTTP::WebSocketHandler.new do |socket|
@captured_sockets << socket
socket.on_close do
@captured_sockets.delete(socket)
end
end
@server = HTTP::Server.new([handler])
end
def start : Nil
@server.bind_tcp(Lucky::ServerSettings.host, Lucky::ServerSettings.reload_port)
spawn { @server.listen }
end
def reload : Nil
@captured_sockets.each do |socket|
socket.send("data: update")
socket.close
end
end
def running? : Bool
@server.listening?
end
def running_at : Nil
end
end
# Watcher using ServerSentEvents (SSE) to reload browser
class ServerSentEventWatcher < Watcher
@captured_contexts = [] of HTTP::Server::Context
@server : HTTP::Server
def initialize
@server = HTTP::Server.new do |context|
context.response.headers.merge!({
"Access-Control-Allow-Origin" => "*",
"Content-Type" => "text/event-stream",
"Connection" => "keep-alive",
"Cache-Control" => "no-cache",
})
context.response.status_code = 200
@captured_contexts << context
# SSE start
loop do
break if context.response.closed?
sleep 0.1
end
# SSE stop
end
end
def start : Nil
@server.bind_tcp(Lucky::ServerSettings.host, Lucky::ServerSettings.reload_port)
spawn { @server.listen }
end
def reload : Nil
while context = @captured_contexts.shift?
context.response.print "data: update\n\n"
context.response.flush
context.response.close
end
end
def running? : Bool
@server.listening?
end
def running_at : Nil
end
end
# Watcher using browsersync to reload browser
class BrowsersyncWatcher < Watcher
@options : String
@is_running : Bool = false
def initialize
host_url = "http://#{Lucky::ServerSettings.host}:#{Lucky::ServerSettings.port}"
@options = ["-c", "bs-config.js", "--port", Lucky::ServerSettings.reload_port, "-p", host_url].join(" ")
end
def start : Nil
spawn do
Process.run \
"RUNNING_IN_BROWSERSYNC=true yarn run browser-sync start #{@options}",
output: STDOUT,
error: STDERR,
shell: true
end
@is_running = true
end
def reload : Nil
if running?
Process.run \
"yarn run browser-sync reload --port #{Lucky::ServerSettings.reload_port}",
output: STDOUT,
error: STDERR,
shell: true
end
end
def running? : Bool
@is_running
end
def running_at : String
"http://#{Lucky::ServerSettings.host}:#{Lucky::ServerSettings.reload_port}"
end
end
class ProcessRunner
include LuckyTask::TextHelpers
include Lucky::TimeHelpers
getter build_processes = [] of Process
getter app_processes = [] of Process
getter! watcher : Watcher
property successful_compilations
property app_built
property? reload_browser
@app_built : Bool = false
@successful_compilations : Int32 = 0
@build_started : Time::Span
def initialize(@build_commands : Array(String), @run_commands : Array(String), @files : Array(String), @reload_browser : Bool, @watcher : Watcher?)
@build_started = Time.monotonic
end
private def build_app_processes_and_start
@build_processes.clear
@build_commands.each do |command|
@build_processes << Process.new(command, shell: true, output: STDOUT, error: STDERR)
end
build_processes_copy = @build_processes.dup
spawn do
build_statuses = build_processes_copy.map(&.wait)
success = build_statuses.all?(&.success?)
if build_processes == build_processes_copy # if this build was not aborted in #stop_all_processes
start_all_processes(success)
end
end
end
private def create_app_processes
@app_processes.clear
@run_commands.each do |command|
@app_processes << Process.new(command, shell: false, output: STDOUT, error: STDERR)
end
@successful_compilations += 1
if reload_browser?
reload_or_start_watcher
end
if @successful_compilations == 1
spawn do
sleep(0.3)
print_running_at
end
end
end
private def reload_or_start_watcher
if @successful_compilations == 1
start_watcher
else
reload_watcher
end
end
private def start_watcher
watcher.start unless watcher.running?
end
private def print_running_at
STDOUT.puts ""
STDOUT.puts running_at_background
STDOUT.puts running_at_message.colorize.on_cyan.black
STDOUT.puts running_at_background
STDOUT.puts ""
end
private def running_at_background
extra_space_for_emoji = 1
(" " * (running_at_message.size + extra_space_for_emoji)).colorize.on_cyan
end
private def running_at_message
" 🎉 App running at #{running_at} "
end
private def running_at
if reload_browser?
watcher.running_at || original_url
else
original_url
end
end
private def original_url
"http://#{Lucky::ServerSettings.host}:#{Lucky::ServerSettings.port}"
end
private def reload_watcher
watcher.reload
end
private def get_timestamp(file : String)
File.info(file).modification_time.to_s("%Y%m%d%H%M%S")
end
def restart_app
build_in_progress = @build_processes.any?(&.exists?)
stop_all_processes
puts build_in_progress ? "Recompiling..." : "\nCompiling..."
@build_started = Time.monotonic
build_app_processes_and_start
end
private def stop_all_processes
@build_processes.each do |process|
unless process.terminated?
# kill child process, because we started build process with shell option
Process.run("pkill -P #{process.pid}", shell: true)
process.terminate
end
end
@app_processes.each do |process|
process.terminate unless process.terminated?
end
end
private def start_all_processes(build_success : Bool)
if build_success
self.app_built = true
create_app_processes
elapsed_time = Time.monotonic - @build_started
message = String.build do |io|
io << " DONE ".colorize.on_cyan.black
io << " Compiled successfully in #{distance_of_time_in_words(elapsed_time)}".colorize.cyan
end
puts message
elsif !app_built
print_error_message
end
end
private def print_error_message
if successful_compilations.zero?
puts <<-ERROR
#{"---".colorize.dim}
Feeling stuck? Try this...
▸ Run setup: #{"script/setup".colorize.bold}
▸ Reinstall shards: #{"rm -rf lib bin && shards install".colorize.bold}
▸ Ask for help: #{"https://luckyframework.org/chat".colorize.bold}
ERROR
end
end
def scan_files
file_changed = false
app_processes = @app_processes
files = @files
Dir.glob(files) do |file|
timestamp = get_timestamp(file)
if FILE_TIMESTAMPS[file]? && FILE_TIMESTAMPS[file] != timestamp
FILE_TIMESTAMPS[file] = timestamp
file_changed = true
elsif FILE_TIMESTAMPS[file]?.nil?
FILE_TIMESTAMPS[file] = timestamp
file_changed = true if app_processes.none?(&.terminated?)
end
end
restart_app if file_changed # (file_changed || app_processes.empty?)
end
end
end
class Watch < LuckyTask::Task
summary "Start and recompile project when files change"
switch :error_trace, "Show full error trace"
switch :reload_browser, "Reloads browser on changes",
shortcut: "-r"
arg :watcher, "Watcher type for reloading browser",
shortcut: "-w",
optional: true,
format: /(sse|browsersync)/
def call
build_commands = %w(crystal build ./src/start_server.cr -o bin/start_server)
files = ["./src/**/*.cr", "./src/**/*.ecr", "./config/**/*.cr", "./shard.lock"]
watcher_class = nil
if reload_browser?
case watcher
when "sse"
build_commands << "-Dlivereloadsse"
watcher_class = LuckySentry::ServerSentEventWatcher.new
files.concat(Lucky::ServerSettings.reload_watch_paths)
when "browsersync"
watcher_class = LuckySentry::BrowsersyncWatcher.new
else
build_commands << "-Dlivereloadws"
watcher_class = LuckySentry::WebSocketWatcher.new
files.concat(Lucky::ServerSettings.reload_watch_paths)
end
end
build_commands << "--error-trace" if error_trace?
build_commands = [build_commands.join(" ")]
run_commands = %w(./bin/start_server)
process_runner = LuckySentry::ProcessRunner.new(
files: files,
build_commands: build_commands,
run_commands: run_commands,
reload_browser: reload_browser?,
watcher: watcher_class
)
puts "Beginning to watch your project"
loop do
process_runner.scan_files
sleep 0.1
end
end
end