Class: ReactOnRails::Dev::ServerManager

Inherits:
Object
  • Object
show all
Extended by:
ShakapackerConfigHelpers
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

Constants included from ShakapackerConfigHelpers

ShakapackerConfigHelpers::DEFAULT_SHAKAPACKER_CONFIG_PATH, ShakapackerConfigHelpers::SUPPORTED_ASSETS_BUNDLERS

Class Method Summary collapse

Class Method Details

.cleanup_socket_filesObject



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/react_on_rails/dev/server_manager.rb', line 234

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



124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/react_on_rails/dev/server_manager.rb', line 124

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



115
116
117
118
119
120
121
122
# File 'lib/react_on_rails/dev/server_manager.rb', line 115

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



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/react_on_rails/dev/server_manager.rb', line 163

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



226
227
228
229
230
231
232
# File 'lib/react_on_rails/dev/server_manager.rb', line 226

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



190
191
192
193
194
195
196
# File 'lib/react_on_rails/dev/server_manager.rb', line 190

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



211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/react_on_rails/dev/server_manager.rb', line 211

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



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/react_on_rails/dev/server_manager.rb', line 60

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



175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/react_on_rails/dev/server_manager.rb', line 175

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.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/react_on_rails/dev/server_manager.rb', line 97

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



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/react_on_rails/dev/server_manager.rb', line 139

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


255
256
257
258
259
260
261
262
263
# File 'lib/react_on_rails/dev/server_manager.rb', line 255

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)


156
157
158
159
160
161
# File 'lib/react_on_rails/dev/server_manager.rb', line 156

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



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

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/react_on_rails/dev/server_manager.rb', line 265

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



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/react_on_rails/dev/server_manager.rb', line 35

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:, rails_env:,
                        skip_database_check:,
                        open_browser:,
                        open_browser_once:)
  when :static
    procfile ||= "Procfile.dev-static-assets"
    run_static_development(procfile, verbose:, route:,
                                     skip_database_check:,
                                     open_browser:,
                                     open_browser_once:)
  when :development, :hmr
    procfile ||= "Procfile.dev"
    run_development(procfile, verbose:, route:,
                              skip_database_check:,
                              open_browser:,
                              open_browser_once:)
  else
    raise ArgumentError, "Unknown mode: #{mode}"
  end
end

.terminate_processes(pids) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/react_on_rails/dev/server_manager.rb', line 198

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