Class: ReactOnRails::Dev::ServerManager

Inherits:
Object
  • Object
show all
Defined in:
lib/react_on_rails/dev/server_manager.rb

Constant Summary collapse

HELP_FLAGS =
["-h", "--help"].freeze
TEST_WATCH_MODES =
%w[auto full client-only].freeze
OPEN_BROWSER_WAIT_TIMEOUT =
60
OPEN_BROWSER_POLL_INTERVAL =
0.5
FLAGS_WITH_VALUES =

Flags that take a value as the next argument (not using = syntax)

%w[--route --rails-env --test-watch-mode].freeze

Class Method Summary collapse

Class Method Details

.cleanup_socket_filesObject



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/react_on_rails/dev/server_manager.rb', line 229

def cleanup_socket_files
  # Mirrors FileManager#cleanup_overmind_sockets so renamed/copied
  # variants like overmind-4100.sock are removed during `bin/dev kill`,
  # not just at startup.
  overmind_sockets = Dir.glob("tmp/sockets/overmind*.sock")
  files = [".overmind.sock", *overmind_sockets, "tmp/pids/server.pid"].uniq
  killed_any = false

  files.each do |file|
    next unless File.exist?(file)

    puts "   🧹 Removing #{file}"
    File.delete(file)
    killed_any = true
  rescue StandardError
    nil
  end

  killed_any
end

.configured_renderer_port_for_killObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/react_on_rails/dev/server_manager.rb', line 119

def configured_renderer_port_for_kill
  raw_port = ENV.fetch("RENDERER_PORT", nil)
  return raw_port.strip.to_i if valid_port_string?(raw_port)

  local_url_port = local_renderer_url_port_for_kill
  return local_url_port if local_url_port
  return nil if remote_renderer_url_configured?

  # Only fall back to the default renderer port when the user has set
  # at least one renderer env var. Without that signal (Pro gem loaded
  # but no renderer ever started), `bin/dev kill` would otherwise
  # target an unrelated process bound to 3800 in OSS+Pro-gem apps.
  renderer_env_signal? ? 3800 : nil
end

.default_killable_portsObject



110
111
112
113
114
115
116
117
# File 'lib/react_on_rails/dev/server_manager.rb', line 110

def default_killable_ports
  ports = [3000, 3001]
  if pro_renderer_active?
    renderer_port = configured_renderer_port_for_kill
    ports << renderer_port if renderer_port
  end
  ports
end

.development_processesObject



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/react_on_rails/dev/server_manager.rb', line 158

def development_processes
  {
    "rails" => "Rails server",
    "node.*react[-_]on[-_]rails" => "React on Rails Node processes",
    "overmind" => "Overmind process manager",
    "foreman" => "Foreman process manager",
    "ruby.*puma" => "Puma server",
    "webpack-dev-server" => "Webpack dev server",
    "bin/shakapacker-dev-server" => "Shakapacker dev server"
  }
end

.find_port_pids(port) ⇒ Object



221
222
223
224
225
226
227
# File 'lib/react_on_rails/dev/server_manager.rb', line 221

def find_port_pids(port)
  stdout, _status = Open3.capture2("lsof", "-ti", ":#{port}", err: File::NULL)
  stdout.split("\n").map(&:to_i).reject { |pid| pid == Process.pid }
rescue StandardError
  # lsof command not found or other error (permission denied, etc.)
  []
end

.find_process_pids(pattern) ⇒ Object



185
186
187
188
189
190
191
# File 'lib/react_on_rails/dev/server_manager.rb', line 185

def find_process_pids(pattern)
  stdout, _status = Open3.capture2("pgrep", "-f", pattern, err: File::NULL)
  stdout.split("\n").map(&:to_i).reject { |pid| pid == Process.pid }
rescue Errno::ENOENT
  # pgrep command not found
  []
end

.kill_port_processes(ports) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/react_on_rails/dev/server_manager.rb', line 206

def kill_port_processes(ports)
  killed_any = false

  ports.each do |port|
    pids = find_port_pids(port)
    next unless pids.any?

    puts "   ☠️  Killing process on port #{port} (PIDs: #{pids.join(', ')})"
    terminate_processes(pids)
    killed_any = true
  end

  killed_any
end

.kill_processesObject



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/react_on_rails/dev/server_manager.rb', line 55

def kill_processes
  puts "🔪 Killing all development processes..."
  puts ""

  # Run every cleanup step unconditionally so a successful first step
  # (e.g. pattern-based kill) doesn't leave stale port-bound processes
  # or socket/pid files behind. `.any?` still gives us the
  # "anything actually got killed?" signal for the summary message.
  killed_any = [
    kill_running_processes,
    kill_port_processes(killable_ports),
    cleanup_socket_files
  ].any?

  print_kill_summary(killed_any)
end

.kill_running_processesObject



170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/react_on_rails/dev/server_manager.rb', line 170

def kill_running_processes
  killed_any = false

  development_processes.each do |pattern, description|
    pids = find_process_pids(pattern)
    next unless pids.any?

    puts "   ☠️  Killing #{description} (PIDs: #{pids.join(', ')})"
    terminate_processes(pids)
    killed_any = true
  end

  killed_any
end

.killable_portsObject

Fallback port list for the port-scan kill path. Uses the base-port derived ports when REACT_ON_RAILS_BASE_PORT / CONDUCTOR_PORT is set, so ‘bin/dev kill` in a worktree on ports 5000/5001/5002 targets the right ports instead of the 3000/3001 default. Falls back to

3000, 3001

when no base port is configured, plus the renderer port

when Pro renderer support is active. Uses PortSelector’s pure #base_port_hash so no “Base port detected” banner prints during a kill.

In base-port mode we include base whenever the Pro gem is loaded, even if the current shell has no renderer env vars set. The user has explicitly claimed this port range, and ‘bin/dev kill` is usually invoked from a fresh shell where RENDERER_PORT / *_URL aren’t carried over from the dev session — so requiring env-var presence would let a stale renderer survive. Pattern-based killing (development_processes / node.*react[-]on[-]rails) does NOT catch the Pro renderer because it runs as ‘node renderer/node-renderer.js` with no “react_on_rails” substring in the command line. Port-based killing is the only reliable path. The default-port branch keeps the tighter renderer_env_signal? guard via configured_renderer_port_for_kill because 3800 is a shared default that could belong to an unrelated process.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/react_on_rails/dev/server_manager.rb', line 92

def killable_ports
  base = PortSelector.base_port_hash
  return default_killable_ports unless base

  ports = [base[:rails], base[:webpack]]
  if pro_renderer_active?
    # When the Pro gem is loaded but no renderer env var is set, the
    # user may not realize base+2 is being scanned. Surface it so an
    # unrelated process killed on that port isn't a silent surprise.
    unless renderer_env_signal?
      puts "   ℹ️  Including renderer port #{base[:renderer]} (base+2): " \
           "react_on_rails_pro is loaded but no renderer env var is set."
    end
    ports << base[:renderer]
  end
  ports
end

.local_renderer_url_port_for_killObject



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/react_on_rails/dev/server_manager.rb', line 134

def local_renderer_url_port_for_kill
  %w[REACT_RENDERER_URL RENDERER_URL].each do |var|
    url = ENV.fetch(var, nil)
    next if url.nil? || url.strip.empty?

    parsed = URI.parse(url)
    next unless localhost_hostname?(parsed.hostname)
    next unless url.match?(URL_WITH_EXPLICIT_PORT_RE)

    return parsed.port
  rescue URI::InvalidURIError
    next
  end

  nil
end


250
251
252
253
254
255
256
257
258
# File 'lib/react_on_rails/dev/server_manager.rb', line 250

def print_kill_summary(killed_any)
  if killed_any
    puts ""
    puts "✅ All processes terminated and sockets cleaned"
    puts "💡 You can now run 'bin/dev' for a clean start"
  else
    puts "   ℹ️  No development processes found running"
  end
end

.remote_renderer_url_configured?Boolean

Returns:

  • (Boolean)


151
152
153
154
155
156
# File 'lib/react_on_rails/dev/server_manager.rb', line 151

def remote_renderer_url_configured?
  %w[REACT_RENDERER_URL RENDERER_URL].any? do |var|
    url = ENV.fetch(var, nil)
    !url.nil? && !url.strip.empty? && !localhost_renderer_url?(url)
  end
end

.run_from_command_line(args = ARGV) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity



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
# File 'lib/react_on_rails/dev/server_manager.rb', line 280

def run_from_command_line(args = ARGV)
  # Get the command early to check for help/kill before running hooks
  # We need to do this before OptionParser processes flags like -h/--help
  # Skip arguments that are values for flags (e.g., "hello_world" after "--route")
  command = extract_command_from_args(args)

  # Check if help flags are present in args (before OptionParser processes them)
  help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

  options = parse_cli_options(args)

  # Run precompile hook once before starting any mode (except kill/help)
  # Then set environment variable to prevent duplicate execution in spawned processes.
  # Note: We always set SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true (even when no hook is configured)
  # to provide a consistent signal that bin/dev is managing the precompile lifecycle.
  # This allows custom scripts to detect bin/dev's presence and adjust behavior accordingly.
  unless %w[kill help].include?(command) || help_requested
    run_precompile_hook_if_present
    ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
  end

  # Main execution
  case command
  when "production-assets", "prod"
    start(:production_like, nil, verbose: options[:verbose], route: options[:route],
                                 rails_env: options[:rails_env],
                                 skip_database_check: options[:skip_database_check],
                                 open_browser: options[:open_browser],
                                 open_browser_once: options[:open_browser_once])
  when "static"
    start(:static, "Procfile.dev-static-assets", verbose: options[:verbose], route: options[:route],
                                                 skip_database_check: options[:skip_database_check],
                                                 open_browser: options[:open_browser],
                                                 open_browser_once: options[:open_browser_once])
  when "kill"
    kill_processes
  when "help"
    show_help
  when "test-watch"
    run_test_watch(test_watch_mode: options[:test_watch_mode])
  when "hmr", nil
    start(:development, "Procfile.dev", verbose: options[:verbose], route: options[:route],
                                        skip_database_check: options[:skip_database_check],
                                        open_browser: options[:open_browser],
                                        open_browser_once: options[:open_browser_once])
  else
    puts "Unknown argument: #{command}"
    puts "Run 'dev help' for usage information"
    exit 1
  end
end

.show_helpObject



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/react_on_rails/dev/server_manager.rb', line 260

def show_help
  default_mode = default_dev_server_mode

  puts help_usage
  puts ""
  puts help_commands(default_mode)
  puts ""
  puts help_options
  puts ""
  puts help_customization(default_mode)
  puts ""
  puts help_mode_details(default_mode)
  puts ""
  puts help_troubleshooting(default_mode)
end

.start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil, skip_database_check: false, open_browser: false, open_browser_once: false) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/react_on_rails/dev/server_manager.rb', line 30

def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil,
          skip_database_check: false, open_browser: false, open_browser_once: false)
  case mode
  when :production_like
    run_production_like(_verbose: verbose, route: route, rails_env: rails_env,
                        skip_database_check: skip_database_check,
                        open_browser: open_browser,
                        open_browser_once: open_browser_once)
  when :static
    procfile ||= "Procfile.dev-static-assets"
    run_static_development(procfile, verbose: verbose, route: route,
                                     skip_database_check: skip_database_check,
                                     open_browser: open_browser,
                                     open_browser_once: open_browser_once)
  when :development, :hmr
    procfile ||= "Procfile.dev"
    run_development(procfile, verbose: verbose, route: route,
                              skip_database_check: skip_database_check,
                              open_browser: open_browser,
                              open_browser_once: open_browser_once)
  else
    raise ArgumentError, "Unknown mode: #{mode}"
  end
end

.terminate_processes(pids) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/react_on_rails/dev/server_manager.rb', line 193

def terminate_processes(pids)
  pids.each do |pid|
    Process.kill("TERM", pid)
  rescue Errno::ESRCH, ArgumentError, RangeError
    # Process already stopped, or invalid signal/PID - silently skip
    nil
  rescue Errno::EPERM
    # Permission denied - warn the user
    puts "   ⚠️  Process #{pid} - permission denied (process owned by another user)"
    nil
  end
end