-
-
Notifications
You must be signed in to change notification settings - Fork 161
/
Copy pathwatch.cr
245 lines (208 loc) · 6.88 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
require "lucky_task"
require "option_parser"
require "colorize"
require "yaml"
require "../src/lucky/server_settings"
# Based on the sentry shard with some modifications to output and build process.
module LuckySentry
FILE_TIMESTAMPS = {} of String => String # {file => timestamp}
BROWSERSYNC_PORT = 3001
class ProcessRunner
include LuckyTask::TextHelpers
getter build_processes = [] of Process
getter app_processes = [] of Process
property successful_compilations
property app_built
property? reload_browser
@app_built : Bool = false
@successful_compilations : Int32 = 0
def initialize(build_commands : Array(String), run_commands : Array(String), files : Array(String), @reload_browser : Bool)
@build_commands = build_commands
@run_commands = run_commands
@files = files
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
self.successful_compilations += 1
if reload_browser?
reload_or_start_browser_sync
end
if successful_compilations == 1
spawn do
sleep(0.3)
print_running_at
end
end
end
private def reload_or_start_browser_sync
if successful_compilations == 1
if browsersync_port_is_available?
start_browsersync
else
print_browsersync_port_taken_error
end
else
reload_browsersync
end
end
private def browsersync_port_is_available? : Bool
if File.executable?(`which lsof`.chomp)
io = IO::Memory.new
Process.run("lsof -i :#{BROWSERSYNC_PORT}", output: io, error: STDERR, shell: true)
io.to_s.empty?
else
true
end
end
private def print_browsersync_port_taken_error
io = IO::Memory.new
Process.run("ps -p `lsof -ti :#{BROWSERSYNC_PORT}` -o command", output: io, error: STDERR, shell: true)
puts "There was a problem starting browsersync. Port #{BROWSERSYNC_PORT} is in use.".colorize(:red)
puts <<-ERROR
Try closing these programs...
#{io}
ERROR
end
private def start_browsersync
spawn do
Process.run \
"RUNNING_IN_BROWSERSYNC=true yarn run browser-sync start #{browsersync_options}",
output: STDOUT,
error: STDERR,
shell: true
end
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?
browsersync_url
else
original_url
end
end
private def browsersync_options
"-c bs-config.js --port #{BROWSERSYNC_PORT} -p #{original_url}"
end
private def browsersync_url
"http://#{Lucky::ServerSettings.host}:#{BROWSERSYNC_PORT}"
end
private def original_url
"http://#{Lucky::ServerSettings.host}:#{Lucky::ServerSettings.port}"
end
private def reload_browsersync
Process.run "yarn run browser-sync reload --port #{BROWSERSYNC_PORT}",
output: STDOUT,
error: STDERR,
shell: true
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_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()
puts "#{" Done ".colorize.on_cyan.black} compiling"
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 :reload_browser, "Reloads browser on changes using browser-sync", shortcut: "-r"
switch :error_trace, "Show full error trace"
def call
build_commands = ["crystal build ./src/start_server.cr -o bin/start_server"]
build_commands[0] += " --error-trace" if error_trace?
run_commands = ["./bin/start_server"]
files = ["./src/**/*.cr", "./src/**/*.ecr", "./config/**/*.cr", "./shard.lock"]
process_runner = LuckySentry::ProcessRunner.new(
files: files,
build_commands: build_commands,
run_commands: run_commands,
reload_browser: reload_browser?
)
puts "Beginning to watch your project"
loop do
process_runner.scan_files
sleep 0.1
end
end
end