Class: ReactOnRails::Dev::PortSelector
- Inherits:
-
Object
- Object
- ReactOnRails::Dev::PortSelector
- Defined in:
- lib/react_on_rails/dev/port_selector.rb
Defined Under Namespace
Classes: NoPortAvailable
Constant Summary collapse
- DEFAULT_RAILS_PORT =
3000- DEFAULT_WEBPACK_PORT =
3035- MAX_ATTEMPTS =
100- BASE_PORT_RAILS_OFFSET =
Offsets from the base port when REACT_ON_RAILS_BASE_PORT (or a recognized tool-specific equivalent like CONDUCTOR_PORT) is set. The base port block is typically 10 consecutive ports allocated per workspace.
0- BASE_PORT_WEBPACK_OFFSET =
1- BASE_PORT_RENDERER_OFFSET =
2- TCP_PORT_MAX =
65_535- MAX_BASE_PORT =
TCP_PORT_MAX - BASE_PORT_RENDERER_OFFSET
- PRIVILEGED_PORT_MAX =
Ports 1..1023 are privileged on Linux/macOS and require root to bind.
1023- BASE_PORT_ENV_VARS =
Env vars checked (in order) for a base port value.
CONDUCTOR_PORT is an empirical interpretation based on Conductor.build (conductor.build) allocating a block of consecutive ports per workspace and exposing the block base via this env var. This contract is not in a public Conductor API, so treat CONDUCTOR_PORT support as best-effort until Conductor documents it. If a future release changes the meaning (e.g. CONDUCTOR_PORT becomes the Rails port itself rather than a block base), the derived offsets below will land on the wrong ports — users would see port-conflict failures at runtime rather than a clear misconfiguration error. A future “validate derived ports are reachable on startup” path could surface this earlier.
Escape hatch: REACT_ON_RAILS_BASE_PORT takes precedence, so users can override the CONDUCTOR_PORT interpretation without code changes.
%w[REACT_ON_RAILS_BASE_PORT CONDUCTOR_PORT].freeze
Class Method Summary collapse
-
.base_port_hash ⇒ Object
Pure derivation: returns the same port hash as #base_port_ports but without the “Base port X detected” log line or the derived-port collision warnings.
-
.base_port_ports(pro_renderer: true) ⇒ Object
Returns the base-port-derived port hash when a base port env var is set (with the same shape as #select_ports!), otherwise nil.
- .find_available_port(start_port, exclude: nil) ⇒ Object
-
.port_available?(port) ⇒ Boolean
Public so it can be stubbed in tests.
-
.select_ports(**kwargs) ⇒ Object
Deprecated alias for the pre-bang name.
-
.select_ports!(pro_renderer: true) ⇒ Object
Returns { rails: Integer, webpack: Integer, renderer: Integer|nil, base_port_mode: Boolean }.
-
.valid_port_string?(value) ⇒ Boolean
Strict port-string predicate shared with ServerManager so the two layers can’t silently diverge.
Class Method Details
.base_port_hash ⇒ Object
Pure derivation: returns the same port hash as #base_port_ports but without the “Base port X detected” log line or the derived-port collision warnings. Safe to call from any context where logging is undesirable (e.g. kill flows). Still delegates to #base_port_with_source, which surfaces invalid-value warnings — those describe the env input, not the port output, and are desirable even in silent callers.
152 153 154 155 156 157 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 152 def base_port_hash bp, _source = base_port_with_source return nil unless bp derive_ports_from_base(bp) end |
.base_port_ports(pro_renderer: true) ⇒ Object
Returns the base-port-derived port hash when a base port env var is set (with the same shape as #select_ports!), otherwise nil. Does not fall back to per-service env vars or auto-detect, so callers can branch on “is base-port mode active?” without triggering probing. Used by ServerManager so all bin/dev modes (development, static, production-like) honor the base-port contract consistently.
Logs the detected base port and warns on derived-port collisions. Callers that need the derived ports without user-facing output (e.g. ServerManager#kill_processes, which shouldn’t print a banner while killing) should use #base_port_hash instead.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 128 def base_port_ports(pro_renderer: true) bp, source = base_port_with_source return nil unless bp ports = derive_ports_from_base(bp) source_note = if source == "CONDUCTOR_PORT" " (unofficial contract; set REACT_ON_RAILS_BASE_PORT to override)" else "" end renderer_segment = pro_renderer ? ", renderer :#{ports[:renderer]}" : "" puts "Base port #{bp} detected via #{source}#{source_note}. Using Rails :#{ports[:rails]}, " \ "webpack :#{ports[:webpack]}#{renderer_segment}" warn_if_derived_ports_in_use(bp, ports, source: source, pro_renderer: pro_renderer) ports end |
.find_available_port(start_port, exclude: nil) ⇒ Object
159 160 161 162 163 164 165 166 167 168 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 159 def find_available_port(start_port, exclude: nil) MAX_ATTEMPTS.times do |i| port = start_port + i next if port == exclude return port if port_available?(port) end raise NoPortAvailable, "No available port found starting at #{start_port}." end |
.port_available?(port) ⇒ Boolean
Public so it can be stubbed in tests. NOTE: Inherent TOCTOU race — another process can claim the port between server.close and the caller binding to it. This is unavoidable with the probe-then-use pattern and acceptable for the worktree port-selection use case.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 102 def port_available?(port) # Check both IPv4 and IPv6 loopback. Node 22+ resolves "localhost" # to ::1 first, so webpack-dev-server often binds only to IPv6. # A pure-IPv4 probe would miss that listener. %w[127.0.0.1 ::1].all? do |host| server = TCPServer.new(host, port) server.close true rescue Errno::EADDRINUSE, Errno::EACCES false rescue Errno::EADDRNOTAVAIL, SocketError true # address family unavailable on this system end end |
.select_ports(**kwargs) ⇒ Object
Deprecated alias for the pre-bang name. Kept as a safety net for any external caller (generator extension, host-app rake task) that wired to ‘select_ports` before the rename. The bang form is preferred — it surfaces the ENV-mutation side effect at the call site, which was the whole point of the rename. Remove in a future major release.
94 95 96 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 94 def select_ports(**kwargs) select_ports!(**kwargs) end |
.select_ports!(pro_renderer: true) ⇒ Object
Returns { rails: Integer, webpack: Integer, renderer: Integer|nil,
base_port_mode: Boolean }.
Priority:
1. Base port (REACT_ON_RAILS_BASE_PORT or CONDUCTOR_PORT) — all ports
derived deterministically from the base; no probing.
2. Explicit per-service env vars (PORT, SHAKAPACKER_DEV_SERVER_PORT).
3. Auto-detect free ports starting from defaults.
The :renderer key is populated only when a base port is set (it is a Pro-only service and does not participate in auto-detection). :base_port_mode is true only in case 1.
NOTE: This method mutates ENV.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 68 def select_ports!(pro_renderer: true) base = base_port_ports(pro_renderer: pro_renderer) return base if base rails_port = explicit_rails_port webpack_port = explicit_webpack_port rails_auto = rails_port.nil? webpack_auto = webpack_port.nil? rails_port ||= find_available_port(DEFAULT_RAILS_PORT, exclude: webpack_port) webpack_port ||= find_available_port(DEFAULT_WEBPACK_PORT, exclude: rails_port) if (rails_auto && rails_port != DEFAULT_RAILS_PORT) || (webpack_auto && webpack_port != DEFAULT_WEBPACK_PORT) puts "Default ports in use. Using Rails :#{rails_port}, webpack :#{webpack_port}" end { rails: rails_port, webpack: webpack_port, renderer: nil, base_port_mode: false } end |
.valid_port_string?(value) ⇒ Boolean
Strict port-string predicate shared with ServerManager so the two layers can’t silently diverge. ‘String#to_i` would otherwise truncate `“3000abc”` to 3000 and slip it through here while ServerManager’s overwrite path rejected it.
174 175 176 177 178 179 180 181 182 |
# File 'lib/react_on_rails/dev/port_selector.rb', line 174 def valid_port_string?(value) return false if value.nil? stripped = value.to_s.strip return false if stripped.empty? return false unless stripped.match?(/\A\d+\z/) stripped.to_i.between?(1, TCP_PORT_MAX) end |