Class: ReactOnRails::Dev::ServerManager
- Inherits:
-
Object
- Object
- ReactOnRails::Dev::ServerManager
- 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
- .cleanup_socket_files ⇒ Object
- .configured_renderer_port_for_kill ⇒ Object
- .default_killable_ports ⇒ Object
- .development_processes ⇒ Object
- .find_port_pids(port) ⇒ Object
- .find_process_pids(pattern) ⇒ Object
- .kill_port_processes(ports) ⇒ Object
- .kill_processes ⇒ Object
- .kill_running_processes ⇒ Object
-
.killable_ports ⇒ Object
Fallback port list for the port-scan kill path.
- .local_renderer_url_port_for_kill ⇒ Object
- .print_kill_summary(killed_any) ⇒ Object
- .remote_renderer_url_configured? ⇒ Boolean
-
.run_from_command_line(args = ARGV) ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity.
- .show_help ⇒ Object
- .start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil, skip_database_check: false, open_browser: false, open_browser_once: false) ⇒ Object
- .terminate_processes(pids) ⇒ Object
Class Method Details
.cleanup_socket_files ⇒ Object
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_kill ⇒ Object
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_ports ⇒ Object
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_processes ⇒ Object
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_processes ⇒ Object
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_processes ⇒ Object
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_ports ⇒ Object
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_kill ⇒ Object
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 |
.print_kill_summary(killed_any) ⇒ Object
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
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) } = (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: [: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" start(:static, "Procfile.dev-static-assets", verbose: [:verbose], route: [:route], skip_database_check: [:skip_database_check], open_browser: [:open_browser], open_browser_once: [:open_browser_once]) when "kill" kill_processes when "help" show_help when "test-watch" run_test_watch(test_watch_mode: [:test_watch_mode]) when "hmr", nil start(:development, "Procfile.dev", verbose: [:verbose], route: [:route], skip_database_check: [:skip_database_check], open_browser: [:open_browser], open_browser_once: [:open_browser_once]) else puts "Unknown argument: #{command}" puts "Run 'dev help' for usage information" exit 1 end end |
.show_help ⇒ Object
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 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 |