Skip to content

Commit cc0b116

Browse files
justin808claude
andcommitted
Enhance bin/dev kill to terminate processes on ports 3000 and 3001
Previously, `bin/dev kill` only terminated processes by pattern matching (e.g., "rails", "puma", "webpack-dev-server"). This could miss processes that were holding ports 3000 or 3001 but didn't match the patterns. Now `bin/dev kill` also finds and terminates any process listening on ports 3000 and 3001 using `lsof -ti:PORT`, ensuring a clean restart even when unexpected processes are blocking the development ports. Changes: - Add kill_port_processes(ports) method to find and kill processes on specific ports - Add find_port_pids(port) helper that uses lsof to find PIDs listening on a port - Update kill_processes to call kill_port_processes([3000, 3001]) - Add comprehensive tests for port-killing functionality - Handle edge cases (lsof not found, permission denied) gracefully The port-killing is integrated as default behavior (no --force flag needed) since ports 3000/3001 are explicitly development ports and this matches the existing UX where kill doesn't ask for confirmation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 62d92e1 commit cc0b116

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

lib/react_on_rails/dev/server_manager.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def kill_processes
2727
puts "🔪 Killing all development processes..."
2828
puts ""
2929

30-
killed_any = kill_running_processes || cleanup_socket_files
30+
killed_any = kill_running_processes || kill_port_processes([3000, 3001]) || cleanup_socket_files
3131

3232
print_kill_summary(killed_any)
3333
end
@@ -75,6 +75,29 @@ def terminate_processes(pids)
7575
end
7676
end
7777

78+
def kill_port_processes(ports)
79+
killed_any = false
80+
81+
ports.each do |port|
82+
pids = find_port_pids(port)
83+
next unless pids.any?
84+
85+
puts " ☠️ Killing process on port #{port} (PIDs: #{pids.join(', ')})"
86+
terminate_processes(pids)
87+
killed_any = true
88+
end
89+
90+
killed_any
91+
end
92+
93+
def find_port_pids(port)
94+
stdout, _status = Open3.capture2("lsof", "-ti", ":#{port}", err: File::NULL)
95+
stdout.split("\n").map(&:to_i).reject { |pid| pid == Process.pid }
96+
rescue StandardError
97+
# lsof command not found or other error (permission denied, etc.)
98+
[]
99+
end
100+
78101
def cleanup_socket_files
79102
files = [".overmind.sock", "tmp/sockets/overmind.sock", "tmp/pids/server.pid"]
80103
killed_any = false

spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ def mock_system_calls
109109
allow(Open3).to receive(:capture2)
110110
.with("pgrep", "-f", "bin/shakapacker-dev-server", err: File::NULL).and_return(["", nil])
111111

112+
# Mock lsof calls for port checking
113+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["", nil])
114+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3001", err: File::NULL).and_return(["", nil])
115+
112116
allow(Process).to receive(:pid).and_return(9999) # Current process PID
113117
expect(Process).to receive(:kill).with("TERM", 1234)
114118
expect(Process).to receive(:kill).with("TERM", 5678)
@@ -117,6 +121,22 @@ def mock_system_calls
117121
described_class.kill_processes
118122
end
119123

124+
it "kills processes on ports 3000 and 3001" do
125+
# No pattern-based processes
126+
allow(Open3).to receive(:capture2).with("pgrep", any_args).and_return(["", nil])
127+
128+
# Mock port processes
129+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["3456", nil])
130+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3001", err: File::NULL).and_return(["3457\n3458", nil])
131+
132+
allow(Process).to receive(:pid).and_return(9999)
133+
expect(Process).to receive(:kill).with("TERM", 3456)
134+
expect(Process).to receive(:kill).with("TERM", 3457)
135+
expect(Process).to receive(:kill).with("TERM", 3458)
136+
137+
described_class.kill_processes
138+
end
139+
120140
it "cleans up socket files when they exist" do
121141
# Make sure no processes are found so cleanup_socket_files gets called
122142
allow(Open3).to receive(:capture2).and_return(["", nil])
@@ -130,6 +150,59 @@ def mock_system_calls
130150
end
131151
end
132152

153+
describe ".find_port_pids" do
154+
it "finds PIDs listening on a specific port" do
155+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["1234\n5678", nil])
156+
allow(Process).to receive(:pid).and_return(9999)
157+
158+
pids = described_class.find_port_pids(3000)
159+
expect(pids).to eq([1234, 5678])
160+
end
161+
162+
it "excludes current process PID" do
163+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["1234\n9999", nil])
164+
allow(Process).to receive(:pid).and_return(9999)
165+
166+
pids = described_class.find_port_pids(3000)
167+
expect(pids).to eq([1234])
168+
end
169+
170+
it "returns empty array when lsof is not found" do
171+
allow(Open3).to receive(:capture2).and_raise(Errno::ENOENT)
172+
173+
pids = described_class.find_port_pids(3000)
174+
expect(pids).to eq([])
175+
end
176+
177+
it "returns empty array on permission denied" do
178+
allow(Open3).to receive(:capture2).and_raise(Errno::EACCES)
179+
180+
pids = described_class.find_port_pids(3000)
181+
expect(pids).to eq([])
182+
end
183+
end
184+
185+
describe ".kill_port_processes" do
186+
it "kills processes on specified ports" do
187+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3000", err: File::NULL).and_return(["1234", nil])
188+
allow(Open3).to receive(:capture2).with("lsof", "-ti", ":3001", err: File::NULL).and_return(["5678", nil])
189+
allow(Process).to receive(:pid).and_return(9999)
190+
191+
expect(Process).to receive(:kill).with("TERM", 1234)
192+
expect(Process).to receive(:kill).with("TERM", 5678)
193+
194+
result = described_class.kill_port_processes([3000, 3001])
195+
expect(result).to be true
196+
end
197+
198+
it "returns false when no processes found on ports" do
199+
allow(Open3).to receive(:capture2).and_return(["", nil])
200+
201+
result = described_class.kill_port_processes([3000, 3001])
202+
expect(result).to be false
203+
end
204+
end
205+
133206
describe ".show_help" do
134207
it "displays help information" do
135208
expect { described_class.show_help }.to output(%r{Usage: bin/dev \[command\]}).to_stdout_from_any_process

0 commit comments

Comments
 (0)