@@ -139,16 +139,11 @@ def authenticate(timeout: 300, auto_open_browser: true)
139139 server = start_callback_server ( result , mutex , condition )
140140
141141 begin
142- # 4. Open browser to authorization URL
143- if auto_open_browser
144- @opener . open_browser ( auth_url )
145- @synchronized_logger . info ( "\n Opening browser for authorization..." )
146- @synchronized_logger . info ( "If browser doesn't open automatically, visit this URL:" )
147- else
148- @synchronized_logger . info ( "\n Please visit this URL to authorize:" )
149- end
150- @synchronized_logger . info ( auth_url )
151- @synchronized_logger . info ( "\n Waiting for authorization..." )
142+ announce_authorization_flow ( auth_url , auto_open_browser )
143+
144+ # Allow callback worker to begin processing only after setup logging/browser open
145+ # to reduce cross-thread test-double races under JRuby.
146+ server . start
152147
153148 # 5. Wait for callback with timeout
154149 mutex . synchronize do
@@ -267,29 +262,13 @@ def start_callback_server(result, mutex, condition)
267262 server = @http_server . start_server
268263 @synchronized_logger . debug ( "Started callback server on http://127.0.0.1:#{ @callback_port } #{ @callback_path } " )
269264
270- running = true
271-
272- # Start server in background thread
273- thread = Thread . new do
274- while running
275- begin
276- # Use wait_readable with timeout to allow checking running flag
277- next unless server . wait_readable ( 0.5 )
278-
279- client = server . accept
280- handle_http_request ( client , result , mutex , condition )
281- rescue IOError , Errno ::EBADF
282- # Server was closed, exit loop
283- break
284- rescue StandardError => e
285- mark_callback_failure ( result , mutex , condition , e )
286- break
287- end
288- end
289- end
265+ control = build_callback_thread_control
266+ thread = build_callback_worker_thread ( server , result , mutex , condition , control )
267+ stop_proc = -> { stop_callback_worker ( control ) }
268+ start_proc = -> { start_callback_worker ( control ) }
290269
291270 # Return wrapper with shutdown method
292- Browser ::CallbackServer . new ( server , thread , -> { running = false } )
271+ Browser ::CallbackServer . new ( server , thread , stop_proc , start_proc )
293272 end
294273
295274 # Handle incoming HTTP request on callback server
@@ -298,6 +277,8 @@ def start_callback_server(result, mutex, condition)
298277 # @param mutex [Mutex] synchronization mutex
299278 # @param condition [ConditionVariable] wait condition
300279 def handle_http_request ( client , result , mutex , condition )
280+ callback_result = nil
281+
301282 @http_server . configure_client_socket ( client )
302283
303284 request_line = @http_server . read_request_line ( client )
@@ -317,18 +298,17 @@ def handle_http_request(client, result, mutex, condition)
317298 # Parse and extract OAuth parameters
318299 params = @callback_handler . parse_callback_params ( path , @http_server )
319300 oauth_params = @callback_handler . extract_oauth_params ( params )
320-
321- # Update result with OAuth parameters
322- @callback_handler . update_result_with_oauth_params ( oauth_params , result , mutex , condition )
301+ callback_result = build_callback_result ( oauth_params )
323302
324303 # Send response
325- if result [ :error ]
326- @http_server . send_http_response ( client , 400 , "text/html" , @pages . error_page ( result [ :error ] ) )
304+ if callback_result [ :error ]
305+ @http_server . send_http_response ( client , 400 , "text/html" , @pages . error_page ( callback_result [ :error ] ) )
327306 else
328307 @http_server . send_http_response ( client , 200 , "text/html" , @pages . success_page )
329308 end
330309 ensure
331- client &.close
310+ apply_callback_result ( callback_result , result , mutex , condition ) if callback_result
311+ close_callback_client ( client )
332312 end
333313
334314 # Wake the waiting authentication flow with a deterministic error when callback
@@ -344,6 +324,103 @@ def mark_callback_failure(result, mutex, condition, error)
344324
345325 @synchronized_logger . warn ( "OAuth callback worker failed: #{ error . class } : #{ error . message } " )
346326 end
327+
328+ def build_callback_result ( oauth_params )
329+ if oauth_params [ :error ]
330+ { code : nil , state : nil , error : oauth_params [ :error_description ] || oauth_params [ :error ] }
331+ elsif oauth_params [ :code ] && oauth_params [ :state ]
332+ { code : oauth_params [ :code ] , state : oauth_params [ :state ] , error : nil }
333+ else
334+ { code : nil , state : nil , error : "Invalid callback: missing code or state parameter" }
335+ end
336+ end
337+
338+ def apply_callback_result ( callback_result , result , mutex , condition )
339+ mutex . synchronize do
340+ return if result [ :completed ]
341+
342+ result [ :code ] = callback_result [ :code ]
343+ result [ :state ] = callback_result [ :state ]
344+ result [ :error ] = callback_result [ :error ]
345+ result [ :completed ] = true
346+ condition . signal
347+ end
348+ end
349+
350+ def close_callback_client ( client )
351+ client &.close
352+ rescue IOError , SystemCallError => e
353+ @synchronized_logger . debug ( "Error closing OAuth callback client socket: #{ e . class } : #{ e . message } " )
354+ end
355+
356+ def announce_authorization_flow ( auth_url , auto_open_browser )
357+ if auto_open_browser
358+ @opener . open_browser ( auth_url )
359+ @synchronized_logger . info ( "\n Opening browser for authorization..." )
360+ @synchronized_logger . info ( "If browser doesn't open automatically, visit this URL:" )
361+ else
362+ @synchronized_logger . info ( "\n Please visit this URL to authorize:" )
363+ end
364+ @synchronized_logger . info ( auth_url )
365+ @synchronized_logger . info ( "\n Waiting for authorization..." )
366+ end
367+
368+ def build_callback_thread_control
369+ {
370+ mutex : Mutex . new ,
371+ condition : ConditionVariable . new ,
372+ running : true ,
373+ accepting : false
374+ }
375+ end
376+
377+ def build_callback_worker_thread ( server , result , result_mutex , condition , control )
378+ Thread . new do
379+ wait_for_callback_worker_start ( control )
380+
381+ while callback_worker_running? ( control )
382+ begin
383+ # Use wait_readable with timeout to allow checking stop signal
384+ next unless server . wait_readable ( 0.5 )
385+
386+ client = server . accept
387+ handle_http_request ( client , result , result_mutex , condition )
388+ break if result_mutex . synchronize { result [ :completed ] }
389+ rescue IOError , Errno ::EBADF
390+ # Server was closed, exit loop
391+ break
392+ rescue StandardError => e
393+ mark_callback_failure ( result , result_mutex , condition , e )
394+ break
395+ end
396+ end
397+ end
398+ end
399+
400+ def wait_for_callback_worker_start ( control )
401+ control [ :mutex ] . synchronize do
402+ control [ :condition ] . wait ( control [ :mutex ] ) until control [ :accepting ] || !control [ :running ]
403+ end
404+ end
405+
406+ def callback_worker_running? ( control )
407+ control [ :mutex ] . synchronize { control [ :running ] }
408+ end
409+
410+ def start_callback_worker ( control )
411+ control [ :mutex ] . synchronize do
412+ control [ :accepting ] = true
413+ control [ :condition ] . signal
414+ end
415+ end
416+
417+ def stop_callback_worker ( control )
418+ control [ :mutex ] . synchronize do
419+ control [ :running ] = false
420+ control [ :accepting ] = true
421+ control [ :condition ] . broadcast
422+ end
423+ end
347424 end
348425 end
349426 end
0 commit comments