ruflet_cli 0.0.6 → 0.0.7
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 +4 -4
- data/lib/ruflet/cli/run_command.rb +201 -22
- data/lib/ruflet/cli/templates.rb +2 -2
- data/lib/ruflet/cli.rb +1 -1
- data/lib/ruflet/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd07971461f7a7a4f8f61df41b51b4a6537de8b0dfe152fd71c52148a344c15e
|
|
4
|
+
data.tar.gz: 65b0952b26820338f5a4f4115440aac955c0ab09bac94fb3fcc94d19e302eff1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28b45040c5f0f3b0901d48bedfdd995e644983e3d4e3748696442f21288175722d3b13688f646f4b1cbea662405213b0e6dedf970aa4614b57d9f2000a5e65a8
|
|
7
|
+
data.tar.gz: 891b2e7de50d432d0922f7f8aa0e2d28e764d4c8c02def6a7de5c58fbc26c86b9421687516427c84efd0a431e79d546c698962d8d421a54ddf97cb2f246b99ed
|
|
@@ -9,16 +9,19 @@ require "fileutils"
|
|
|
9
9
|
require "json"
|
|
10
10
|
require "net/http"
|
|
11
11
|
require "uri"
|
|
12
|
+
require "thread"
|
|
13
|
+
require "io/console"
|
|
12
14
|
|
|
13
15
|
module Ruflet
|
|
14
16
|
module CLI
|
|
15
17
|
module RunCommand
|
|
16
18
|
def command_run(args)
|
|
17
|
-
options = { target: "mobile" }
|
|
19
|
+
options = { target: "mobile", hot_reload: true }
|
|
18
20
|
parser = OptionParser.new do |o|
|
|
19
21
|
o.on("--web") { options[:target] = "web" }
|
|
20
|
-
o.on("--mobile") { options[:target] = "mobile" }
|
|
21
22
|
o.on("--desktop") { options[:target] = "desktop" }
|
|
23
|
+
o.on("--hot-reload", "--hot") { options[:hot_reload] = true }
|
|
24
|
+
o.on("--no-hot-reload") { options[:hot_reload] = false }
|
|
22
25
|
end
|
|
23
26
|
parser.parse!(args)
|
|
24
27
|
|
|
@@ -44,16 +47,8 @@ module Ruflet
|
|
|
44
47
|
print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
|
|
45
48
|
|
|
46
49
|
gemfile_path = find_nearest_gemfile(Dir.pwd)
|
|
47
|
-
cmd =
|
|
48
|
-
|
|
49
|
-
env["BUNDLE_GEMFILE"] = gemfile_path
|
|
50
|
-
bundle_ready = system(env, "bundle", "check", out: File::NULL, err: File::NULL)
|
|
51
|
-
return 1 unless bundle_ready || system(env, "bundle", "install")
|
|
52
|
-
|
|
53
|
-
["bundle", "exec", RbConfig.ruby, script_path]
|
|
54
|
-
else
|
|
55
|
-
[RbConfig.ruby, script_path]
|
|
56
|
-
end
|
|
50
|
+
cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env, hot_reload: options[:hot_reload])
|
|
51
|
+
return 1 unless cmd
|
|
57
52
|
|
|
58
53
|
child_pid = Process.spawn(env, *cmd, pgroup: true)
|
|
59
54
|
launched_client_pids = launch_target_client(options[:target], selected_port)
|
|
@@ -68,8 +63,22 @@ module Ruflet
|
|
|
68
63
|
previous_int = Signal.trap("INT") { forward_signal.call("INT") }
|
|
69
64
|
previous_term = Signal.trap("TERM") { forward_signal.call("TERM") }
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
if options[:hot_reload]
|
|
67
|
+
puts "Hot reload enabled. Press 'r' to reload backend or 'R' for hot restart."
|
|
68
|
+
run_hot_reload_loop(
|
|
69
|
+
child_pid: child_pid,
|
|
70
|
+
env: env,
|
|
71
|
+
cmd: cmd,
|
|
72
|
+
script_path: script_path,
|
|
73
|
+
gemfile_path: gemfile_path,
|
|
74
|
+
target: options[:target],
|
|
75
|
+
port: selected_port,
|
|
76
|
+
launched_client_pids: launched_client_pids
|
|
77
|
+
)
|
|
78
|
+
else
|
|
79
|
+
_pid, status = Process.wait2(child_pid)
|
|
80
|
+
status.success? ? 0 : (status.exitstatus || 1)
|
|
81
|
+
end
|
|
73
82
|
ensure
|
|
74
83
|
Signal.trap("INT", previous_int) if defined?(previous_int) && previous_int
|
|
75
84
|
Signal.trap("TERM", previous_term) if defined?(previous_term) && previous_term
|
|
@@ -93,10 +102,186 @@ module Ruflet
|
|
|
93
102
|
end
|
|
94
103
|
end
|
|
95
104
|
end
|
|
105
|
+
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
private
|
|
99
109
|
|
|
110
|
+
def run_hot_reload_loop(child_pid:, env:, cmd:, script_path:, gemfile_path:, target:, port:, launched_client_pids:)
|
|
111
|
+
hot_queue = Queue.new
|
|
112
|
+
hot_listener = start_hotkey_listener(hot_queue)
|
|
113
|
+
watch_roots = watch_roots_for_script(script_path, gemfile_path)
|
|
114
|
+
watch_snapshot = build_watch_snapshot(watch_roots)
|
|
115
|
+
puts "Watching #{watch_snapshot.size} Ruby files for hot reload."
|
|
116
|
+
|
|
117
|
+
loop do
|
|
118
|
+
exited = Process.waitpid(child_pid, Process::WNOHANG)
|
|
119
|
+
if exited
|
|
120
|
+
status = $?
|
|
121
|
+
return status&.success? ? 0 : (status&.exitstatus || 1)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
latest_snapshot = build_watch_snapshot(watch_roots)
|
|
125
|
+
if latest_snapshot != watch_snapshot
|
|
126
|
+
changed = changed_paths(watch_snapshot, latest_snapshot)
|
|
127
|
+
sample = changed.first(3).map { |p| File.basename(p) }.join(", ")
|
|
128
|
+
suffix = changed.size > 3 ? ", ..." : ""
|
|
129
|
+
puts "Detected change in #{changed.size} file(s): #{sample}#{suffix}"
|
|
130
|
+
hot_queue << :reload
|
|
131
|
+
watch_snapshot = latest_snapshot
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
process_hot_reload_actions(
|
|
135
|
+
queue: hot_queue,
|
|
136
|
+
child_pid: child_pid,
|
|
137
|
+
launched_client_pids: launched_client_pids,
|
|
138
|
+
env: env,
|
|
139
|
+
cmd: cmd,
|
|
140
|
+
target: target,
|
|
141
|
+
port: port
|
|
142
|
+
) do |new_child_pid, new_client_pids|
|
|
143
|
+
child_pid = new_child_pid
|
|
144
|
+
launched_client_pids = new_client_pids
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sleep 0.25
|
|
148
|
+
end
|
|
149
|
+
ensure
|
|
150
|
+
hot_listener&.kill
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def process_hot_reload_actions(queue:, child_pid:, launched_client_pids:, env:, cmd:, target:, port:)
|
|
154
|
+
until queue.empty?
|
|
155
|
+
action = queue.pop(true)
|
|
156
|
+
case action
|
|
157
|
+
when :interrupt
|
|
158
|
+
Process.kill("INT", Process.pid)
|
|
159
|
+
when :reload
|
|
160
|
+
puts "Reloading backend..."
|
|
161
|
+
child_pid = restart_backend(child_pid, env, cmd)
|
|
162
|
+
when :restart
|
|
163
|
+
puts "Hot restarting backend and client..."
|
|
164
|
+
child_pid = restart_backend(child_pid, env, cmd)
|
|
165
|
+
launched_client_pids = restart_clients(launched_client_pids, target, port)
|
|
166
|
+
end
|
|
167
|
+
yield child_pid, launched_client_pids if block_given?
|
|
168
|
+
end
|
|
169
|
+
rescue ThreadError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def start_hotkey_listener(queue)
|
|
174
|
+
return nil unless $stdin.tty?
|
|
175
|
+
|
|
176
|
+
Thread.new do
|
|
177
|
+
$stdin.raw do |stdin|
|
|
178
|
+
loop do
|
|
179
|
+
key = stdin.getc
|
|
180
|
+
break if key.nil?
|
|
181
|
+
|
|
182
|
+
action = hot_action_for_key(key)
|
|
183
|
+
queue << action if action
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
rescue StandardError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def hot_action_for_key(key)
|
|
192
|
+
return :interrupt if key == "\u0003" # Ctrl+C in raw mode
|
|
193
|
+
return :reload if key == "r"
|
|
194
|
+
return :restart if key == "R"
|
|
195
|
+
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def restart_backend(child_pid, env, cmd)
|
|
200
|
+
terminate_process(child_pid)
|
|
201
|
+
new_pid = Process.spawn(env, *cmd, pgroup: true)
|
|
202
|
+
puts "Hot reload applied."
|
|
203
|
+
new_pid
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def build_runtime_command(script_path, gemfile_path:, env:, hot_reload:)
|
|
207
|
+
if gemfile_path
|
|
208
|
+
env["BUNDLE_GEMFILE"] = gemfile_path
|
|
209
|
+
bundle_ready = system(env, RbConfig.ruby, "-S", "bundle", "check", out: File::NULL, err: File::NULL)
|
|
210
|
+
return nil unless bundle_ready || system(env, RbConfig.ruby, "-S", "bundle", "install")
|
|
211
|
+
|
|
212
|
+
return [RbConfig.ruby, "-rbundler/setup", script_path]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
[RbConfig.ruby, script_path]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def restart_clients(launched_client_pids, target, port)
|
|
219
|
+
Array(launched_client_pids).compact.each { |pid| terminate_process(pid) }
|
|
220
|
+
launch_target_client(target, port)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def terminate_process(pid)
|
|
224
|
+
return unless pid
|
|
225
|
+
|
|
226
|
+
begin
|
|
227
|
+
Process.kill("TERM", -pid)
|
|
228
|
+
rescue Errno::ESRCH
|
|
229
|
+
begin
|
|
230
|
+
Process.kill("TERM", pid)
|
|
231
|
+
rescue Errno::ESRCH
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
begin
|
|
237
|
+
Timeout.timeout(2) { Process.wait(pid) }
|
|
238
|
+
rescue StandardError
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def watch_roots_for_script(script_path, gemfile_path)
|
|
244
|
+
roots = [Dir.pwd, File.dirname(script_path)]
|
|
245
|
+
if gemfile_path
|
|
246
|
+
roots << File.dirname(gemfile_path)
|
|
247
|
+
roots.concat(extra_watch_roots_from_gemfile(gemfile_path))
|
|
248
|
+
end
|
|
249
|
+
roots.uniq.select { |root| File.directory?(root) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def extra_watch_roots_from_gemfile(gemfile_path)
|
|
253
|
+
base_dir = File.dirname(gemfile_path)
|
|
254
|
+
content = File.read(gemfile_path)
|
|
255
|
+
content.scan(/path:\s*["']([^"']+)["']/).flatten.map do |path|
|
|
256
|
+
File.expand_path(path, base_dir)
|
|
257
|
+
end.select { |path| File.directory?(path) }
|
|
258
|
+
rescue StandardError
|
|
259
|
+
[]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def build_watch_snapshot(roots)
|
|
263
|
+
snapshot = {}
|
|
264
|
+
roots.each do |root|
|
|
265
|
+
Dir.glob(File.join(root, "**", "*.rb")).each do |path|
|
|
266
|
+
next if path.include?("/.git/")
|
|
267
|
+
next if path.include?("/tmp/")
|
|
268
|
+
next if path.include?("/log/")
|
|
269
|
+
next unless File.file?(path)
|
|
270
|
+
|
|
271
|
+
snapshot[path] = File.mtime(path).to_f
|
|
272
|
+
rescue Errno::ENOENT
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
snapshot
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def changed_paths(old_snapshot, new_snapshot)
|
|
280
|
+
(old_snapshot.keys | new_snapshot.keys).select do |path|
|
|
281
|
+
old_snapshot[path] != new_snapshot[path]
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
100
285
|
def resolve_script(token)
|
|
101
286
|
path = File.expand_path(token, Dir.pwd)
|
|
102
287
|
return path if File.file?(path)
|
|
@@ -547,7 +732,7 @@ module Ruflet
|
|
|
547
732
|
end
|
|
548
733
|
probe.close
|
|
549
734
|
return port
|
|
550
|
-
rescue Errno::EADDRINUSE
|
|
735
|
+
rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
|
|
551
736
|
port += 1
|
|
552
737
|
end
|
|
553
738
|
end
|
|
@@ -556,13 +741,7 @@ module Ruflet
|
|
|
556
741
|
end
|
|
557
742
|
|
|
558
743
|
def resolve_backend_port(target)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return 8550 if port_available?(8550)
|
|
562
|
-
|
|
563
|
-
warn "Port 8550 is required for `ruflet run --#{target}`."
|
|
564
|
-
warn "Stop the process using 8550 and run again."
|
|
565
|
-
nil
|
|
744
|
+
find_available_port(8550)
|
|
566
745
|
end
|
|
567
746
|
|
|
568
747
|
def port_available?(port)
|
data/lib/ruflet/cli/templates.rb
CHANGED
data/lib/ruflet/cli.rb
CHANGED
|
@@ -54,7 +54,7 @@ module Ruflet
|
|
|
54
54
|
Commands:
|
|
55
55
|
ruflet create <appname>
|
|
56
56
|
ruflet new <appname>
|
|
57
|
-
ruflet run [scriptname|path] [--web|--
|
|
57
|
+
ruflet run [scriptname|path] [--web|--desktop] [--no-hot-reload]
|
|
58
58
|
ruflet debug [scriptname|path]
|
|
59
59
|
ruflet build <apk|ios|aab|web|macos|windows|linux>
|
|
60
60
|
ruflet devices
|
data/lib/ruflet/version.rb
CHANGED