Class: ReactOnRails::Dev::ServerManager

Inherits:
Object
  • Object
show all
Extended by:
ShakapackerConfigHelpers
Defined in:
lib/react_on_rails/dev/server_manager.rb,
sig/react_on_rails/dev/server_manager.rbs

Constant Summary collapse

HELP_FLAGS =
["-h", "--help"].freeze
TEST_WATCH_MODES =
%w[auto full client-only].freeze
CLEAN_SHAKAPACKER_ENVIRONMENTS =
%w[development test production].freeze
OPEN_BROWSER_WAIT_TIMEOUT =
60
OPEN_BROWSER_POLL_INTERVAL =
0.5
DOCS_BASE_URL =
"https://reactonrails.com/docs"
DEV_SERVER_AND_TESTING_DOCS_URL =
"#{DOCS_BASE_URL}/building-features/dev-server-and-testing/".freeze
TESTING_CONFIGURATION_DOCS_URL =
"#{DOCS_BASE_URL}/building-features/testing-configuration/".freeze
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::SHAKAPACKER_ASSETS_BUNDLER_ENV, ShakapackerConfigHelpers::SUPPORTED_ASSETS_BUNDLERS

Class Method Summary collapse

Class Method Details

.clean_generated_assets_and_cachesObject



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/react_on_rails/dev/server_manager.rb', line 81

def clean_generated_assets_and_caches
  puts "🧹 Cleaning generated bundles and caches..."
  puts ""
  kill_processes
  puts ""
  print_shakapacker_config_status
  puts ""

  clean_finished_without_warnings = remove_clean_targets(clean_targets)

  puts ""
  if clean_finished_without_warnings
    puts "✅ Generated bundles and caches cleaned"
  else
    puts "⚠️  Cleanup completed with warnings"
  end
end

.cleanup_socket_filesBoolean

Returns:

  • (Boolean)


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

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



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/react_on_rails/dev/server_manager.rb', line 146

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



137
138
139
140
141
142
143
144
# File 'lib/react_on_rails/dev/server_manager.rb', line 137

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_processesHash[String, String]

Returns:

  • (Hash[String, String])


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

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) ⇒ Array[Integer]

Parameters:

  • (Integer)

Returns:



248
249
250
251
252
253
254
# File 'lib/react_on_rails/dev/server_manager.rb', line 248

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) ⇒ Array[Integer]

Parameters:

  • (String)

Returns:



212
213
214
215
216
217
218
# File 'lib/react_on_rails/dev/server_manager.rb', line 212

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) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


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

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_processesvoid

This method returns an undefined value.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/react_on_rails/dev/server_manager.rb', line 64

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_processesBoolean

Returns:

  • (Boolean)


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

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.*reactonrails) 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.



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

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



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

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

This method returns an undefined value.

Parameters:

  • (Boolean)


277
278
279
280
281
282
283
284
285
# File 'lib/react_on_rails/dev/server_manager.rb', line 277

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)


178
179
180
181
182
183
# File 'lib/react_on_rails/dev/server_manager.rb', line 178

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) ⇒ void

This method returns an undefined value.

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity

Parameters:



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

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/clean/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 clean 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 "clean"
    clean_generated_assets_and_caches
  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_helpvoid

This method returns an undefined value.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/react_on_rails/dev/server_manager.rb', line 287

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) ⇒ void

This method returns an undefined value.

Parameters:

  • (mode)
  • (String, nil)
  • verbose: (Boolean) (defaults to: false)
  • route: (String, nil) (defaults to: nil)
  • rails_env: (String, nil) (defaults to: nil)


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

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) ⇒ void

This method returns an undefined value.

Parameters:



220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/react_on_rails/dev/server_manager.rb', line 220

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