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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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.



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

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



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

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


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

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)


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

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



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

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



258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/react_on_rails/dev/server_manager.rb', line 258

def show_help
  puts help_usage
  puts ""
  puts help_commands
  puts ""
  puts help_options
  puts ""
  puts help_customization
  puts ""
  puts help_mode_details
  puts ""
  puts help_troubleshooting
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



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

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



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

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