11require 'socket'
22require 'timeout'
33require 'fileutils'
4+ require 'net/http'
45require 'cypress_on_rails/configuration'
56
67module CypressOnRails
@@ -9,13 +10,16 @@ class Server
910
1011 def initialize ( options = { } )
1112 config = CypressOnRails . configuration
12-
13+
1314 @framework = options [ :framework ] || :cypress
1415 @host = options [ :host ] || config . server_host
1516 @port = options [ :port ] || config . server_port || find_available_port
1617 @port = @port . to_i if @port
1718 @install_folder = options [ :install_folder ] || config . install_folder || detect_install_folder
1819 @transactional = options . fetch ( :transactional , config . transactional_server )
20+ # Process management: track PID and process group for proper cleanup
21+ @server_pid = nil
22+ @server_pgid = nil
1923 end
2024
2125 def open
@@ -105,34 +109,91 @@ def spawn_server
105109
106110 puts "Starting Rails server: #{ server_args . join ( ' ' ) } "
107111
108- spawn ( *server_args , out : $stdout, err : $stderr)
112+ @server_pid = spawn ( *server_args , out : $stdout, err : $stderr, pgroup : true )
113+ begin
114+ @server_pgid = Process . getpgid ( @server_pid )
115+ rescue Errno ::ESRCH => e
116+ # Edge case: process terminated before we could get pgid
117+ # This is OK - send_term_signal will fall back to single-process kill
118+ CypressOnRails . configuration . logger . warn ( "Process #{ @server_pid } terminated immediately after spawn: #{ e . message } " )
119+ @server_pgid = nil
120+ end
121+ @server_pid
109122 end
110123
111124 def wait_for_server ( timeout = 30 )
112125 Timeout . timeout ( timeout ) do
113126 loop do
114- begin
115- TCPSocket . new ( host , port ) . close
116- break
117- rescue Errno ::ECONNREFUSED , Errno ::EHOSTUNREACH
118- sleep 0.1
119- end
127+ break if server_responding?
128+ sleep 0.1
120129 end
121130 end
122131 rescue Timeout ::Error
123132 raise "Rails server failed to start on #{ host } :#{ port } after #{ timeout } seconds"
124133 end
125134
135+ def server_responding?
136+ config = CypressOnRails . configuration
137+ readiness_path = config . server_readiness_path || '/'
138+ timeout = config . server_readiness_timeout || 5
139+ uri = URI ( "http://#{ host } :#{ port } #{ readiness_path } " )
140+
141+ response = Net ::HTTP . start ( uri . host , uri . port , open_timeout : timeout , read_timeout : timeout ) do |http |
142+ http . get ( uri . path )
143+ end
144+
145+ # Accept 200-399 (success and redirects), reject 404 and 5xx
146+ # 3xx redirects are considered "ready" because the server is responding correctly
147+ ( 200 ..399 ) . cover? ( response . code . to_i )
148+ rescue Errno ::ECONNREFUSED , Errno ::EADDRNOTAVAIL , Errno ::ETIMEDOUT , SocketError ,
149+ Net ::OpenTimeout , Net ::ReadTimeout , Net ::HTTPBadResponse
150+ false
151+ end
152+
126153 def stop_server ( pid )
127- if pid
128- puts "Stopping Rails server (PID: #{ pid } )"
129- Process . kill ( 'TERM' , pid )
130- Process . wait ( pid )
154+ return unless pid
155+
156+ puts "Stopping Rails server (PID: #{ pid } )"
157+ send_term_signal ( pid )
158+
159+ begin
160+ Timeout . timeout ( 10 ) do
161+ Process . wait ( pid )
162+ end
163+ rescue Timeout ::Error
164+ CypressOnRails . configuration . logger . warn ( "Server did not terminate after TERM signal, sending KILL" )
165+ safe_kill_process ( 'KILL' , pid )
166+ Process . wait ( pid ) rescue Errno ::ESRCH
131167 end
132168 rescue Errno ::ESRCH
133169 # Process already terminated
134170 end
135171
172+ def send_term_signal ( pid )
173+ if @server_pgid && process_exists? ( pid )
174+ Process . kill ( 'TERM' , -@server_pgid )
175+ else
176+ safe_kill_process ( 'TERM' , pid )
177+ end
178+ rescue Errno ::ESRCH , Errno ::EPERM => e
179+ CypressOnRails . configuration . logger . warn ( "Failed to kill process group #{ @server_pgid } : #{ e . message } , trying single process" )
180+ safe_kill_process ( 'TERM' , pid )
181+ end
182+
183+ def process_exists? ( pid )
184+ return false unless pid
185+ Process . kill ( 0 , pid )
186+ true
187+ rescue Errno ::ESRCH , Errno ::EPERM
188+ false
189+ end
190+
191+ def safe_kill_process ( signal , pid )
192+ Process . kill ( signal , pid ) if pid
193+ rescue Errno ::ESRCH , Errno ::EPERM
194+ # Process already terminated or permission denied
195+ end
196+
136197 def base_url
137198 "http://#{ host } :#{ port } "
138199 end
0 commit comments