@@ -24,7 +24,10 @@ module WebDriver
2424 class WebSocketConnection
2525 CONNECTION_ERRORS = [
2626 Errno ::ECONNRESET , # connection is aborted (browser process was killed)
27- Errno ::EPIPE # broken pipe (browser process was killed)
27+ Errno ::EPIPE , # broken pipe (browser process was killed)
28+ Errno ::EBADF , # file descriptor already closed (double-close or GC)
29+ IOError , # Ruby socket read/write after close
30+ EOFError # socket reached EOF after remote closed cleanly
2831 ] . freeze
2932
3033 RESPONSE_WAIT_TIMEOUT = 30
@@ -34,7 +37,8 @@ class WebSocketConnection
3437
3538 def initialize ( url :)
3639 @callback_threads = ThreadGroup . new
37-
40+ @mtx = Mutex . new
41+ @closing = false
3842 @session_id = nil
3943 @url = url
4044
@@ -43,72 +47,99 @@ def initialize(url:)
4347 end
4448
4549 def close
46- @callback_threads . list . each ( &:exit )
47- @socket_thread . exit
48- socket . close
50+ @mtx . synchronize do
51+ return if @closing
52+
53+ @closing = true
54+ end
55+
56+ begin
57+ socket . close
58+ rescue *CONNECTION_ERRORS => e
59+ WebDriver . logger . debug "WebSocket listener closed: #{ e . class } : #{ e . message } " , id : :ws
60+ # already closed
61+ end
62+
63+ # Let threads unwind instead of calling exit
64+ @socket_thread &.join ( 0.5 )
65+ @callback_threads . list . each do |thread |
66+ thread . join ( 0.5 )
67+ rescue StandardError
68+ nil
69+ end
4970 end
5071
5172 def callbacks
5273 @callbacks ||= Hash . new { |callbacks , event | callbacks [ event ] = [ ] }
5374 end
5475
5576 def add_callback ( event , &block )
56- callbacks [ event ] << block
57- block . object_id
77+ @mtx . synchronize do
78+ callbacks [ event ] << block
79+ block . object_id
80+ end
5881 end
5982
6083 def remove_callback ( event , id )
61- return if callbacks [ event ] . reject! { |callback | callback . object_id == id }
84+ removed = @mtx . synchronize { callbacks [ event ] . reject! { |cb | cb . object_id == id } }
85+ return if removed || @closing
6286
63- ids = callbacks [ event ] &.map ( &:object_id )
87+ ids = @mtx . synchronize { callbacks [ event ] &.map ( &:object_id ) }
6488 raise Error ::WebDriverError , "Callback with ID #{ id } does not exist for event #{ event } : #{ ids } "
6589 end
6690
6791 def send_cmd ( **payload )
6892 id = next_id
6993 data = payload . merge ( id : id )
70- WebDriver . logger . debug "WebSocket -> #{ data } " [ ...MAX_LOG_MESSAGE_SIZE ] , id : :bidi
94+ WebDriver . logger . debug "WebSocket -> #{ data } " [ ...MAX_LOG_MESSAGE_SIZE ] , id : :ws
7195 data = JSON . generate ( data )
7296 out_frame = WebSocket ::Frame ::Outgoing ::Client . new ( version : ws . version , data : data , type : 'text' )
73- socket . write ( out_frame . to_s )
7497
75- wait . until { messages . delete ( id ) }
98+ begin
99+ socket . write ( out_frame . to_s )
100+ rescue *CONNECTION_ERRORS => e
101+ raise Error ::WebDriverError , "WebSocket is closed (#{ e . class } : #{ e . message } )"
102+ end
103+
104+ wait . until do
105+ @mtx . synchronize { messages . delete ( id ) }
106+ end
76107 end
77108
78109 private
79110
80- # We should be thread-safe to use the hash without synchronization
81- # because its keys are WebSocket message identifiers and they should be
82- # unique within a devtools session.
83111 def messages
84112 @messages ||= { }
85113 end
86114
87115 def process_handshake
88116 socket . print ( ws . to_s )
89- ws << socket . readpartial ( 1024 )
117+ ws << socket . readpartial ( 1024 ) until ws . finished?
90118 end
91119
92120 def attach_socket_listener
93121 Thread . new do
94- Thread . current . abort_on_exception = true
95122 Thread . current . report_on_exception = false
96123
97- until socket . eof?
124+ loop do
125+ break if @closing
126+
98127 incoming_frame << socket . readpartial ( 1024 )
99128
100129 while ( frame = incoming_frame . next )
130+ break if @closing
131+
101132 message = process_frame ( frame )
102133 next unless message [ 'method' ]
103134
104135 params = message [ 'params' ]
105- callbacks [ message [ 'method' ] ] . each do |callback |
136+ @mtx . synchronize { callbacks [ message [ 'method' ] ] . dup } . each do |callback |
106137 @callback_threads . add ( callback_thread ( params , &callback ) )
107138 end
108139 end
109140 end
110- rescue *CONNECTION_ERRORS
111- Thread . stop
141+ rescue *CONNECTION_ERRORS , WebSocket :: Error => e
142+ WebDriver . logger . debug "WebSocket listener closed: #{ e . class } : #{ e . message } " , id : :ws
112143 end
113144 end
114145
@@ -122,27 +153,26 @@ def process_frame(frame)
122153 # Firefox will periodically fail on unparsable empty frame
123154 return { } if message . empty?
124155
125- message = JSON . parse ( message )
126- messages [ message [ 'id' ] ] = message
127- WebDriver . logger . debug "WebSocket <- #{ message } " [ ...MAX_LOG_MESSAGE_SIZE ] , id : :bidi
156+ msg = JSON . parse ( message )
157+ @mtx . synchronize { messages [ msg [ 'id' ] ] = msg if msg . key? ( 'id' ) }
128158
129- message
159+ WebDriver . logger . debug "WebSocket <- #{ msg } " [ ...MAX_LOG_MESSAGE_SIZE ] , id : :ws
160+ msg
130161 end
131162
132163 def callback_thread ( params )
133164 Thread . new do
134- Thread . current . abort_on_exception = true
135-
136- # We might end up blocked forever when we have an error in event.
137- # For example, if network interception event raises error,
138- # the browser will keep waiting for the request to be proceeded
139- # before returning back to the original thread. In this case,
140- # we should at least print the error.
141- Thread . current . report_on_exception = true
165+ Thread . current . abort_on_exception = false
166+ Thread . current . report_on_exception = false
167+ return if @closing
142168
143169 yield params
144- rescue Error ::WebDriverError , *CONNECTION_ERRORS
145- Thread . stop
170+ rescue Error ::WebDriverError , *CONNECTION_ERRORS => e
171+ WebDriver . logger . debug "Callback aborted: #{ e . class } : #{ e . message } " , id : :ws
172+ rescue StandardError => e
173+ # Unexpected handler failure; log with a short backtrace.
174+ bt = Array ( e . backtrace ) . first ( 5 ) . join ( "\n " )
175+ WebDriver . logger . error "Callback error: #{ e . class } : #{ e . message } \n #{ bt } " , id : :ws
146176 end
147177 end
148178
0 commit comments