diff --git a/lib/react_on_rails/dev/server_manager.rb b/lib/react_on_rails/dev/server_manager.rb index 832a416ac6..08587dbb2c 100644 --- a/lib/react_on_rails/dev/server_manager.rb +++ b/lib/react_on_rails/dev/server_manager.rb @@ -27,7 +27,7 @@ def kill_processes puts "🔪 Killing all development processes..." puts "" - killed_any = kill_running_processes || cleanup_socket_files + killed_any = kill_running_processes || kill_port_processes([3000, 3001]) || cleanup_socket_files print_kill_summary(killed_any) end @@ -75,6 +75,29 @@ def terminate_processes(pids) end end + 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 + + 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 + def cleanup_socket_files files = [".overmind.sock", "tmp/sockets/overmind.sock", "tmp/pids/server.pid"] killed_any = false diff --git a/spec/react_on_rails/dev/server_manager_spec.rb b/spec/react_on_rails/dev/server_manager_spec.rb index 69ecfddfd4..54226f6424 100644 --- a/spec/react_on_rails/dev/server_manager_spec.rb +++ b/spec/react_on_rails/dev/server_manager_spec.rb @@ -109,6 +109,10 @@ def mock_system_calls allow(Open3).to receive(:capture2) .with("pgrep", "-f", "bin/shakapacker-dev-server", err: File::NULL).and_return(["", nil]) + # Mock lsof calls for port checking + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["", nil]) + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3001", err: File::NULL).and_return(["", nil]) + allow(Process).to receive(:pid).and_return(9999) # Current process PID expect(Process).to receive(:kill).with("TERM", 1234) expect(Process).to receive(:kill).with("TERM", 5678) @@ -117,6 +121,22 @@ def mock_system_calls described_class.kill_processes end + it "kills processes on ports 3000 and 3001" do + # No pattern-based processes + allow(Open3).to receive(:capture2).with("pgrep", any_args).and_return(["", nil]) + + # Mock port processes + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["3456", nil]) + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3001", err: File::NULL).and_return(["3457\n3458", nil]) + + allow(Process).to receive(:pid).and_return(9999) + expect(Process).to receive(:kill).with("TERM", 3456) + expect(Process).to receive(:kill).with("TERM", 3457) + expect(Process).to receive(:kill).with("TERM", 3458) + + described_class.kill_processes + end + it "cleans up socket files when they exist" do # Make sure no processes are found so cleanup_socket_files gets called allow(Open3).to receive(:capture2).and_return(["", nil]) @@ -130,6 +150,59 @@ def mock_system_calls end end + describe ".find_port_pids" do + it "finds PIDs listening on a specific port" do + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["1234\n5678", nil]) + allow(Process).to receive(:pid).and_return(9999) + + pids = described_class.find_port_pids(3000) + expect(pids).to eq([1234, 5678]) + end + + it "excludes current process PID" do + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["1234\n9999", nil]) + allow(Process).to receive(:pid).and_return(9999) + + pids = described_class.find_port_pids(3000) + expect(pids).to eq([1234]) + end + + it "returns empty array when lsof is not found" do + allow(Open3).to receive(:capture2).and_raise(Errno::ENOENT) + + pids = described_class.find_port_pids(3000) + expect(pids).to eq([]) + end + + it "returns empty array on permission denied" do + allow(Open3).to receive(:capture2).and_raise(Errno::EACCES) + + pids = described_class.find_port_pids(3000) + expect(pids).to eq([]) + end + end + + describe ".kill_port_processes" do + it "kills processes on specified ports" do + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["1234", nil]) + allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3001", err: File::NULL).and_return(["5678", nil]) + allow(Process).to receive(:pid).and_return(9999) + + expect(Process).to receive(:kill).with("TERM", 1234) + expect(Process).to receive(:kill).with("TERM", 5678) + + result = described_class.kill_port_processes([3000, 3001]) + expect(result).to be true + end + + it "returns false when no processes found on ports" do + allow(Open3).to receive(:capture2).and_return(["", nil]) + + result = described_class.kill_port_processes([3000, 3001]) + expect(result).to be false + end + end + describe ".show_help" do it "displays help information" do expect { described_class.show_help }.to output(%r{Usage: bin/dev \[command\]}).to_stdout_from_any_process