Skip to content

Commit b1cc0bb

Browse files
justin808claude
andcommitted
fix: round 22 review polish — 16 fixes from claude/cursor/codex bots
Two genuine bugs and 14 polish items from the latest bot reviews. Bugs: - run_production_like rescues PortSelector::NoPortAvailable so an exhausted port range exits cleanly instead of producing an unhandled Ruby backtrace, mirroring configure_ports. - warn_if_derived_ports_in_use accepts a pro_renderer keyword so OSS apps without the Pro renderer don't see a confusing "port base+2 (renderer) is already in use" warning when an unrelated service binds the renderer port. Polish: - Strip-and-write-back whitespace-padded but-otherwise-valid PORT / SHAKAPACKER_DEV_SERVER_PORT to match RENDERER_PORT normalization. - Prefix the RENDERER_PORT-without-URL derivation message with WARNING: so grep WARNING filtering catches it. - valid_port_string? uses the existing TCP_PORT_MAX constant. - sync_renderer_port_and_url calls warn_url_without_port without pretending its return value is meaningful. - file_manager.rb: SOCKET_PROBE_TIMEOUT_SECS hoisted to class body and reduced from 1.0s to 0.15s; dead File.readlink nil/empty guard removed. - cleanup_socket_files (kill flow) now uses the same overmind*.sock glob as FileManager#cleanup_overmind_sockets so renamed/copied variants are removed. - local_renderer_url_port_for_kill caches the parsed URI so localhost_renderer_url? doesn't re-parse the same string. Helper factored out as localhost_hostname?. - Procfile.static delegates to `pnpm run node-renderer` so future package.json flag additions propagate without divergence. - Docs: PortSelector#select_ports ENV mutation note strengthened with YARD-style @side_effect; killable_ports comments why pro_renderer_active? rarely fires during kill; warn_if_legacy message notes RENDERER_URL still activates the Pro path; CONDUCTOR_PORT caveat extended with the future-semantics-change failure mode. Specs: - New: PortSelector skips renderer port-in-use warning when pro_renderer is false. - New: run_production_like exits cleanly on NoPortAvailable. - New: kill flow cleans up renamed/copied overmind sockets via the shared glob. - Existing cleanup_socket_files spec updated to stub the new glob. All bots' "addressed" items from PR review summaries are covered or verified already-addressed in earlier commits (4dce031..a7d748a). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent a7d748a commit b1cc0bb

6 files changed

Lines changed: 152 additions & 48 deletions

File tree

react_on_rails/lib/react_on_rails/dev/file_manager.rb

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
module ReactOnRails
77
module Dev
88
class FileManager
9+
# Bounded probe so a stuck server with a full accept queue (rare for a
10+
# local overmind socket but theoretically possible) cannot stall
11+
# bin/dev startup indefinitely. UNIX domain socket connect() to a dead
12+
# listener typically fails in microseconds, so 150 ms is conservative
13+
# while keeping worst-case overhead low across many stale sockets.
14+
SOCKET_PROBE_TIMEOUT_SECS = 0.15
15+
private_constant :SOCKET_PROBE_TIMEOUT_SECS
16+
917
class << self
1018
def cleanup_stale_files
1119
socket_cleanup = cleanup_overmind_sockets
@@ -73,14 +81,6 @@ def process_running?(pid)
7381
true
7482
end
7583

76-
# Bounded probe so a stuck server with a full accept queue (rare for a
77-
# local overmind socket but theoretically possible) cannot stall
78-
# bin/dev startup indefinitely. UNIXSocket.open uses a blocking
79-
# connect(2) with no timeout; switching to connect_nonblock + IO.select
80-
# gives us a deadline.
81-
SOCKET_PROBE_TIMEOUT_SECS = 1.0
82-
private_constant :SOCKET_PROBE_TIMEOUT_SECS
83-
8484
def socket_active?(socket_path)
8585
return false unless File.exist?(socket_path)
8686

@@ -131,10 +131,8 @@ def working_directory_for_pid(pid)
131131
end
132132

133133
def working_directory_via_proc(pid)
134-
path = File.readlink("/proc/#{pid}/cwd")
135-
return nil if path.nil? || path.empty?
136-
137-
path
134+
# File.readlink either returns a non-empty String or raises.
135+
File.readlink("/proc/#{pid}/cwd")
138136
rescue Errno::ENOENT, Errno::EACCES, Errno::EPERM, NotImplementedError
139137
# /proc not present (macOS, BSD), no permission to read this PID's cwd,
140138
# or readlink unsupported on this platform. Fall through to lsof.

react_on_rails/lib/react_on_rails/dev/port_selector.rb

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ class PortSelector
3030
# best-effort until Conductor documents it. If a future release changes
3131
# the meaning (e.g. CONDUCTOR_PORT becomes the Rails port itself rather
3232
# than a block base), the derived offsets below will land on the wrong
33-
# ports.
33+
# ports — users would see port-conflict failures at runtime rather than
34+
# a clear misconfiguration error. A future "validate derived ports are
35+
# reachable on startup" path could surface this earlier.
3436
#
3537
# Escape hatch: REACT_ON_RAILS_BASE_PORT takes precedence, so users can
3638
# override the CONDUCTOR_PORT interpretation without code changes.
@@ -52,13 +54,19 @@ class << self
5254
# Pro-only service and does not participate in auto-detection).
5355
# :base_port_mode is true only in case 1.
5456
#
55-
# NOTE: This method mutates ENV. Invalid PORT / SHAKAPACKER_DEV_SERVER_PORT
56-
# values are deleted via `read_and_sanitize_port_env!` so ServerManager's
57-
# apply_explicit_port_env path doesn't re-warn on the same bad value.
58-
# Intended for `bin/dev` startup; do not call from read-only contexts
59-
# that expect ENV to survive the call.
60-
def select_ports
61-
base = base_port_ports
57+
# NOTE: This method mutates ENV.
58+
# @side_effect Deletes invalid PORT / SHAKAPACKER_DEV_SERVER_PORT
59+
# values via `read_and_sanitize_port_env!` so ServerManager's
60+
# apply_explicit_port_env path doesn't re-warn on the same bad
61+
# value. Intended for `bin/dev` startup; do not call from
62+
# read-only contexts that expect ENV to survive the call. See
63+
# `read_and_sanitize_port_env!` (which uses the `!` suffix to make
64+
# the mutation explicit at the inner call site).
65+
# @param pro_renderer [Boolean] when false, suppresses the renderer
66+
# port-in-use warning so OSS apps without a node renderer don't
67+
# see "port X (renderer)" noise on a coincidentally-bound base+2.
68+
def select_ports(pro_renderer: true)
69+
base = base_port_ports(pro_renderer: pro_renderer)
6270
return base if base
6371

6472
rails_port = explicit_rails_port
@@ -108,14 +116,14 @@ def port_available?(port)
108116
# Callers that need the derived ports without user-facing output
109117
# (e.g. ServerManager#kill_processes, which shouldn't print a banner
110118
# while killing) should use #base_port_hash instead.
111-
def base_port_ports
119+
def base_port_ports(pro_renderer: true)
112120
bp, source = base_port_with_source
113121
return nil unless bp
114122

115123
ports = derive_ports_from_base(bp)
116124
puts "Base port #{bp} detected via #{source}. Using Rails :#{ports[:rails]}, " \
117125
"webpack :#{ports[:webpack]}, renderer :#{ports[:renderer]}"
118-
warn_if_derived_ports_in_use(bp, ports)
126+
warn_if_derived_ports_in_use(bp, ports, pro_renderer: pro_renderer)
119127
ports
120128
end
121129

@@ -155,7 +163,7 @@ def valid_port_string?(value)
155163
return false if stripped.empty?
156164
return false unless stripped.match?(/\A\d+\z/)
157165

158-
stripped.to_i.between?(1, 65_535)
166+
stripped.to_i.between?(1, TCP_PORT_MAX)
159167
end
160168

161169
private
@@ -172,8 +180,14 @@ def derive_ports_from_base(base)
172180
# Advisory: surface early conflicts when a base port's derived ports are
173181
# already bound (e.g. two worktrees share a base). Does not fail — the
174182
# actual bind at server start gives the definitive error.
175-
def warn_if_derived_ports_in_use(base, ports)
176-
%i[rails webpack renderer].each do |role|
183+
#
184+
# Skips the renderer port when `pro_renderer` is false: OSS apps don't
185+
# run a node renderer, so "port base+2 (renderer) is already in use"
186+
# would be confusing noise on a coincidental collision with an
187+
# unrelated local service.
188+
def warn_if_derived_ports_in_use(base, ports, pro_renderer: true)
189+
roles = pro_renderer ? %i[rails webpack renderer] : %i[rails webpack]
190+
roles.each do |role|
177191
port_num = ports[role]
178192
warn "WARNING: port #{port_num} (#{role}, derived from base #{base}) is already in use." \
179193
unless port_available?(port_num)

react_on_rails/lib/react_on_rails/dev/server_manager.rb

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ def kill_processes
6666
# [3000, 3001] when no base port is configured, plus the renderer port
6767
# when Pro renderer support is active. Uses PortSelector's pure
6868
# #base_port_hash so no "Base port detected" banner prints during a kill.
69+
#
70+
# `pro_renderer_active?` here is evaluated against the kill-time
71+
# environment, where RENDERER_PORT / REACT_RENDERER_URL set by
72+
# `configure_ports` in the *previous* `bin/dev` session are not
73+
# exported into the new shell. So in OSS apps the renderer port is
74+
# included only when the Pro gem is actually loaded — this is the
75+
# intended behavior, not a bug.
6976
def killable_ports
7077
base = PortSelector.base_port_hash
7178
return default_killable_ports unless base
@@ -98,10 +105,13 @@ def configured_renderer_port_for_kill
98105
def local_renderer_url_port_for_kill
99106
%w[REACT_RENDERER_URL RENDERER_URL].each do |var|
100107
url = ENV.fetch(var, nil)
101-
next if url.nil? || url.strip.empty? || !localhost_renderer_url?(url)
108+
next if url.nil? || url.strip.empty?
109+
110+
parsed = URI.parse(url)
111+
next unless localhost_hostname?(parsed.hostname)
102112
next unless url.match?(URL_WITH_EXPLICIT_PORT_RE)
103113

104-
return URI.parse(url).port
114+
return parsed.port
105115
rescue URI::InvalidURIError
106116
next
107117
end
@@ -188,7 +198,11 @@ def find_port_pids(port)
188198
end
189199

190200
def cleanup_socket_files
191-
files = [".overmind.sock", "tmp/sockets/overmind.sock", "tmp/pids/server.pid"]
201+
# Mirrors FileManager#cleanup_overmind_sockets so renamed/copied
202+
# variants like overmind-4100.sock are removed during `bin/dev kill`,
203+
# not just at startup.
204+
overmind_sockets = Dir.glob("tmp/sockets/overmind*.sock")
205+
files = [".overmind.sock", *overmind_sockets, "tmp/pids/server.pid"].uniq
192206
killed_any = false
193207

194208
files.each do |file|
@@ -685,7 +699,14 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil, skip_databa
685699
# Clear the bad value first so procfile_port falls back to its default
686700
# (3001) instead of `"abc".to_i == 0`, which would scan from port 0.
687701
ENV.delete("PORT")
688-
ENV["PORT"] = PortSelector.find_available_port(procfile_port(procfile)).to_s
702+
# Match configure_ports' clean-exit behavior on exhaustion so
703+
# `bin/dev prod` surfaces a one-line error instead of a backtrace.
704+
begin
705+
ENV["PORT"] = PortSelector.find_available_port(procfile_port(procfile)).to_s
706+
rescue PortSelector::NoPortAvailable => e
707+
warn e.message
708+
exit 1
709+
end
689710
end
690711
sync_renderer_port_and_url
691712
end
@@ -928,7 +949,9 @@ def configure_ports
928949
# Single call: select_ports internally consults base_port_ports and
929950
# returns the same hash when base-port mode is active, so we branch
930951
# on :base_port_mode instead of calling base_port_ports twice.
931-
selected = PortSelector.select_ports
952+
# Pass pro_renderer so OSS apps don't get a "port base+2 (renderer)
953+
# is already in use" warning for a port they don't actually use.
954+
selected = PortSelector.select_ports(pro_renderer: pro_renderer_active?)
932955
if selected[:base_port_mode]
933956
apply_base_port_env(selected)
934957
else
@@ -947,7 +970,7 @@ def configure_ports
947970
# configure_ports) do that so it fires in every mode regardless of
948971
# whether base-port mode is active.
949972
def apply_base_port_if_active
950-
selected = PortSelector.base_port_ports
973+
selected = PortSelector.base_port_ports(pro_renderer: pro_renderer_active?)
951974
return false unless selected
952975

953976
apply_base_port_env(selected)
@@ -973,7 +996,10 @@ def warn_if_legacy_renderer_url_env_used
973996
if current.nil? || current.strip.empty?
974997
warn "WARNING: RENDERER_URL is set but REACT_RENDERER_URL is not. " \
975998
"RENDERER_URL was renamed to REACT_RENDERER_URL; update your " \
976-
"env var to avoid silent fallback to the default renderer URL."
999+
"env var to avoid silent fallback to the default renderer URL. " \
1000+
"Note: RENDERER_URL still activates the Pro renderer path here, " \
1001+
"so base-port mode will derive RENDERER_PORT/REACT_RENDERER_URL " \
1002+
"from it until the variable is renamed."
9771003
return
9781004
end
9791005

@@ -1027,6 +1053,12 @@ def apply_base_port_env(selected)
10271053
# of the renderer env vars (so they're configuring a renderer manually
10281054
# without the Pro gem). Keeps OSS environments clean while not
10291055
# silently dropping renderer env for any caller who actually wants it.
1056+
#
1057+
# The legacy `RENDERER_URL` (renamed to `REACT_RENDERER_URL`) is
1058+
# intentionally included so users mid-migration who still export
1059+
# `RENDERER_URL` keep base-port renderer-derivation behavior. The
1060+
# rename reminder lives in `warn_if_legacy_renderer_url_env_used`,
1061+
# which calls out that the legacy var still triggers this path.
10301062
def pro_renderer_active?
10311063
return true if Gem.loaded_specs.key?("react_on_rails_pro")
10321064

@@ -1076,7 +1108,16 @@ def apply_explicit_port_env(selected)
10761108
# warn_if_port_will_be_overridden's symmetry for base-port mode.
10771109
def overwrite_invalid_port_env(var_name, derived_port)
10781110
existing = ENV.fetch(var_name, nil)
1079-
return if valid_port_string?(existing)
1111+
if valid_port_string?(existing)
1112+
# Strip and write back so a whitespace-padded `" 3000 "` does not
1113+
# leak into the Procfile's `${PORT:-3000}` expansion (which would
1114+
# forward the spaces verbatim to `rails s -p`). Matches the
1115+
# normalization already done for RENDERER_PORT in
1116+
# sync_renderer_port_and_url.
1117+
stripped = existing.strip
1118+
ENV[var_name] = stripped if stripped != existing
1119+
return
1120+
end
10801121

10811122
unless existing.nil? || existing.strip.empty?
10821123
warn "WARNING: #{var_name}=#{existing.inspect} is not a valid port; " \
@@ -1092,7 +1133,10 @@ def valid_port_string?(value)
10921133
def sync_renderer_port_and_url
10931134
raw_port = ENV.fetch("RENDERER_PORT", nil)
10941135
url = ENV.fetch("REACT_RENDERER_URL", nil)
1095-
return warn_url_without_port(url) if raw_port.nil? || raw_port.strip.empty?
1136+
if raw_port.nil? || raw_port.strip.empty?
1137+
warn_url_without_port(url)
1138+
return
1139+
end
10961140

10971141
# Reuse the canonical port-string predicate so whitespace handling and
10981142
# range checks match PortSelector exactly (`" 3800 "` is accepted
@@ -1121,7 +1165,8 @@ def sync_renderer_port_and_url
11211165
# log together with every other "I changed your env" warning in this
11221166
# file — stdout would leak through the same silencing attempt.
11231167
derived = "http://localhost:#{port}"
1124-
warn "RENDERER_PORT=#{port} set without REACT_RENDERER_URL; deriving REACT_RENDERER_URL=#{derived}."
1168+
warn "WARNING: RENDERER_PORT=#{port} set without REACT_RENDERER_URL; " \
1169+
"deriving REACT_RENDERER_URL=#{derived}."
11251170
ENV["REACT_RENDERER_URL"] = derived
11261171
elsif url_port_mismatch?(url, port)
11271172
# Both set but inconsistent — SSR will silently break otherwise.
@@ -1196,17 +1241,21 @@ def url_port_mismatch?(url, port)
11961241
end
11971242

11981243
def localhost_renderer_url?(url)
1199-
# Use `.hostname` not `.host`: for IPv6 URLs like `http://[::1]:3800`,
1200-
# `.host` returns `"[::1]"` (with brackets) while `.hostname` returns
1201-
# `"::1"` (bracket-stripped), matching the comparison list below.
1202-
# Downcase: URI preserves host case, so `http://LOCALHOST:3900` would
1203-
# otherwise be treated as non-local and skip the invalid-port URL
1204-
# remediation path, leaving Rails targeting a stale port.
1205-
%w[localhost 127.0.0.1 ::1].include?(URI.parse(url).hostname&.downcase)
1244+
localhost_hostname?(URI.parse(url).hostname)
12061245
rescue URI::InvalidURIError
12071246
false
12081247
end
12091248

1249+
# Use `.hostname` not `.host`: for IPv6 URLs like `http://[::1]:3800`,
1250+
# `.host` returns `"[::1]"` (with brackets) while `.hostname` returns
1251+
# `"::1"` (bracket-stripped), matching the comparison list below.
1252+
# Downcase: URI preserves host case, so `http://LOCALHOST:3900` would
1253+
# otherwise be treated as non-local and skip the invalid-port URL
1254+
# remediation path, leaving Rails targeting a stale port.
1255+
def localhost_hostname?(hostname)
1256+
%w[localhost 127.0.0.1 ::1].include?(hostname&.downcase)
1257+
end
1258+
12101259
# Callers are expected to have normalized ENV["PORT"] beforehand:
12111260
# run_production_like clears non-integer / out-of-range values before
12121261
# calling here, and the development/static paths route through

react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@
6262
expect(result).to include(rails: 5000, webpack: 5001, renderer: 5002)
6363
end
6464

65+
it "skips the renderer port-in-use warning when pro_renderer is false (OSS)" do
66+
allow(described_class).to receive(:port_available?).and_return(false)
67+
expect { described_class.select_ports(pro_renderer: false) }
68+
.to output(/port 5000 \(rails.+already in use.+port 5001 \(webpack.+already in use/m).to_stderr
69+
expect { described_class.select_ports(pro_renderer: false) }
70+
.not_to output(/port 5002 \(renderer/).to_stderr
71+
end
72+
73+
it "still warns about the renderer port when pro_renderer is true (default)" do
74+
allow(described_class).to receive(:port_available?).and_return(false)
75+
expect { described_class.select_ports }
76+
.to output(/port 5002 \(renderer, derived from base 5000\) is already in use/).to_stderr
77+
end
78+
6579
it "prints a base port message" do
6680
expect { described_class.select_ports }.to output(/Base port 5000 detected/).to_stdout
6781
end

react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,17 @@ def mock_port_selector_defaults
346346
described_class.start(:production_like)
347347
expect(ENV.fetch("PORT", nil)).to eq("4242")
348348
end
349+
350+
it "exits cleanly when find_available_port raises NoPortAvailable" do
351+
# Mirrors the existing rescue in `configure_ports`. Without the rescue
352+
# in `run_production_like`, an exhausted port range produced an
353+
# unhandled Ruby backtrace instead of a one-line warning.
354+
allow(ReactOnRails::Dev::PortSelector).to receive(:find_available_port)
355+
.and_raise(ReactOnRails::Dev::PortSelector::NoPortAvailable, "No port found")
356+
ENV["PORT"] = "abc"
357+
expect_any_instance_of(Kernel).to receive(:exit).with(1)
358+
expect { described_class.start(:production_like) }.to output(/No port found/).to_stderr
359+
end
349360
end
350361

351362
context "when configuring ports" do
@@ -1138,14 +1149,30 @@ def mock_port_selector_defaults
11381149
# Make sure no processes are found so cleanup_socket_files gets called
11391150
allow(Open3).to receive(:capture2).and_return(["", nil])
11401151

1152+
allow(Dir).to receive(:glob).with("tmp/sockets/overmind*.sock").and_return([])
11411153
allow(File).to receive(:exist?).with(".overmind.sock").and_return(true)
1142-
allow(File).to receive(:exist?).with("tmp/sockets/overmind.sock").and_return(false)
11431154
allow(File).to receive(:exist?).with("tmp/pids/server.pid").and_return(false)
11441155
expect(File).to receive(:delete).with(".overmind.sock")
11451156

11461157
described_class.kill_processes
11471158
end
11481159

1160+
it "cleans up renamed/copied overmind sockets via the same glob FileManager uses" do
1161+
# Mirrors FileManager#cleanup_overmind_sockets: variants like
1162+
# overmind-4100.sock from copied app dirs must also be removed during
1163+
# `bin/dev kill`, not just at startup.
1164+
allow(Open3).to receive(:capture2).and_return(["", nil])
1165+
1166+
copied = "tmp/sockets/overmind-4100.sock"
1167+
allow(Dir).to receive(:glob).with("tmp/sockets/overmind*.sock").and_return([copied])
1168+
allow(File).to receive(:exist?).with(".overmind.sock").and_return(false)
1169+
allow(File).to receive(:exist?).with(copied).and_return(true)
1170+
allow(File).to receive(:exist?).with("tmp/pids/server.pid").and_return(false)
1171+
expect(File).to receive(:delete).with(copied)
1172+
1173+
described_class.kill_processes
1174+
end
1175+
11491176
it "targets base-port-derived ports when REACT_ON_RAILS_BASE_PORT is active" do
11501177
# Without base-port awareness, `bin/dev kill` in a worktree running on
11511178
# 5000/5001/5002 would fall back to killing stale processes on 3000/3001

react_on_rails_pro/spec/dummy/Procfile.static

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ rails: bin/rails s -p ${PORT:-3000}
55
rails-client-assets: pnpm run build:dev:watch
66

77
# Start Node server for server rendering.
8-
# Mirrors `pnpm run node-renderer` (see package.json): both use the same
9-
# `RENDERER_PORT=${RENDERER_PORT:-3800}` shell default so REACT_ON_RAILS_BASE_PORT
10-
# assignments reach the renderer identically whether it is launched via foreman
11-
# or directly from pnpm. Keep the two commands in sync.
12-
node-renderer: RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
8+
# Delegates to `pnpm run node-renderer` (see package.json) so any future flag
9+
# additions (e.g. --max-old-space-size, --experimental-vm-modules) propagate
10+
# without diverging from `pnpm run node-renderer` invocations elsewhere. The
11+
# `RENDERER_PORT=${RENDERER_PORT:-3800}` shell default lives inside that npm
12+
# script and still picks up REACT_ON_RAILS_BASE_PORT assignments via foreman's
13+
# environment.
14+
node-renderer: pnpm run node-renderer

0 commit comments

Comments
 (0)