Class: ReactOnRails::Dev::PortSelector

Inherits:
Object
  • Object
show all
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

Class Method Details

.base_port_hashObject

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

Raises:



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.

Returns:

  • (Boolean)


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.

Parameters:

  • pro_renderer (Boolean) (defaults to: true)

    when false, suppresses the renderer port-in-use warning so OSS apps without a node renderer don’t see “port X (renderer)” noise on a coincidentally-bound base+2.



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.

Returns:

  • (Boolean)


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