puppeteer-ruby 0.41.0 → 0.43.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,13 +7,15 @@ class Puppeteer::Browser
7
7
  include Puppeteer::IfPresent
8
8
  using Puppeteer::DefineAsyncMethod
9
9
 
10
+ # @param product [String|nil] 'chrome' or 'firefox'
10
11
  # @param {!Puppeteer.Connection} connection
11
12
  # @param {!Array<string>} contextIds
12
13
  # @param {boolean} ignoreHTTPSErrors
13
14
  # @param {?Puppeteer.Viewport} defaultViewport
14
15
  # @param process [Puppeteer::BrowserRunner::BrowserProcess|NilClass]
15
16
  # @param {function()=} closeCallback
16
- def self.create(connection:,
17
+ def self.create(product:,
18
+ connection:,
17
19
  context_ids:,
18
20
  ignore_https_errors:,
19
21
  default_viewport:,
@@ -22,6 +24,7 @@ class Puppeteer::Browser
22
24
  target_filter_callback:,
23
25
  is_page_target_callback:)
24
26
  browser = Puppeteer::Browser.new(
27
+ product: product,
25
28
  connection: connection,
26
29
  context_ids: context_ids,
27
30
  ignore_https_errors: ignore_https_errors,
@@ -31,17 +34,19 @@ class Puppeteer::Browser
31
34
  target_filter_callback: target_filter_callback,
32
35
  is_page_target_callback: is_page_target_callback,
33
36
  )
34
- connection.send_message('Target.setDiscoverTargets', discover: true)
37
+ browser.send(:attach)
35
38
  browser
36
39
  end
37
40
 
41
+ # @param product [String|nil] 'chrome' or 'firefox'
38
42
  # @param {!Puppeteer.Connection} connection
39
43
  # @param {!Array<string>} contextIds
40
44
  # @param {boolean} ignoreHTTPSErrors
41
45
  # @param {?Puppeteer.Viewport} defaultViewport
42
46
  # @param {?Puppeteer.ChildProcess} process
43
47
  # @param {(function():Promise)=} closeCallback
44
- def initialize(connection:,
48
+ def initialize(product:,
49
+ connection:,
45
50
  context_ids:,
46
51
  ignore_https_errors:,
47
52
  default_viewport:,
@@ -49,6 +54,7 @@ class Puppeteer::Browser
49
54
  close_callback:,
50
55
  target_filter_callback:,
51
56
  is_page_target_callback:)
57
+ @product = product || 'chrome'
52
58
  @ignore_https_errors = ignore_https_errors
53
59
  @default_viewport = default_viewport
54
60
  @process = process
@@ -56,20 +62,26 @@ class Puppeteer::Browser
56
62
  @close_callback = close_callback
57
63
  @target_filter_callback = target_filter_callback || method(:default_target_filter_callback)
58
64
  @is_page_target_callback = is_page_target_callback || method(:default_is_page_target_callback)
59
-
60
65
  @default_context = Puppeteer::BrowserContext.new(@connection, self, nil)
61
66
  @contexts = {}
67
+
62
68
  context_ids.each do |context_id|
63
69
  @contexts[context_id] = Puppeteer::BrowserContext.new(@connection, self, context_id)
64
70
  end
65
- @targets = {}
66
- @wait_for_creating_targets = {}
67
- @connection.on_event(ConnectionEmittedEvents::Disconnected) do
68
- emit_event(BrowserEmittedEvents::Disconnected)
71
+
72
+ if @product == 'firefox'
73
+ @target_manager = Puppeteer::FirefoxTargetManager.new(
74
+ connection: connection,
75
+ target_factory: method(:create_target),
76
+ target_filter_callback: @target_filter_callback,
77
+ )
78
+ else
79
+ @target_manager = Puppeteer::ChromeTargetManager.new(
80
+ connection: connection,
81
+ target_factory: method(:create_target),
82
+ target_filter_callback: @target_filter_callback,
83
+ )
69
84
  end
70
- @connection.on_event('Target.targetCreated', &method(:handle_target_created))
71
- @connection.on_event('Target.targetDestroyed', &method(:handle_target_destroyed))
72
- @connection.on_event('Target.targetInfoChanged', &method(:handle_target_info_changed))
73
85
  end
74
86
 
75
87
  private def default_target_filter_callback(target_info)
@@ -100,11 +112,45 @@ class Puppeteer::Browser
100
112
  super(event_name.to_s, &block)
101
113
  end
102
114
 
115
+ private def attach
116
+ @connection_event_listeners ||= []
117
+ @connection_event_listeners << @connection.add_event_listener(ConnectionEmittedEvents::Disconnected) do
118
+ emit_event(BrowserEmittedEvents::Disconnected)
119
+ end
120
+ @target_manager_event_listeners ||= []
121
+ @target_manager.add_event_listener(
122
+ TargetManagerEmittedEvents::TargetAvailable,
123
+ &method(:handle_attached_to_target)
124
+ )
125
+ @target_manager.add_event_listener(
126
+ TargetManagerEmittedEvents::TargetGone,
127
+ &method(:handle_detached_from_target)
128
+ )
129
+ @target_manager.add_event_listener(
130
+ TargetManagerEmittedEvents::TargetChanged,
131
+ &method(:handle_target_changed)
132
+ )
133
+ @target_manager.add_event_listener(
134
+ TargetManagerEmittedEvents::TargetDiscovered,
135
+ &method(:handle_target_discovered)
136
+ )
137
+ @target_manager.init
138
+ end
139
+
140
+ private def detach
141
+ @connection.remove_event_listener(*@connection_event_listeners)
142
+ @target_manager.remove_event_listener(*@target_manager_event_listeners)
143
+ end
144
+
103
145
  # @return [Puppeteer::BrowserRunner::BrowserProcess]
104
146
  def process
105
147
  @process
106
148
  end
107
149
 
150
+ private def target_manager
151
+ @target_manager
152
+ end
153
+
108
154
  # @return [Puppeteer::BrowserContext]
109
155
  def create_incognito_browser_context
110
156
  result = @connection.send_message('Target.createBrowserContext')
@@ -123,19 +169,16 @@ class Puppeteer::Browser
123
169
 
124
170
  # @param context_id [String]
125
171
  def dispose_context(context_id)
172
+ return unless context_id
126
173
  @connection.send_message('Target.disposeBrowserContext', browserContextId: context_id)
127
174
  @contexts.delete(context_id)
128
175
  end
129
176
 
130
- class TargetAlreadyExistError < StandardError
131
- def initialize
132
- super('Target should not exist before targetCreated')
133
- end
134
- end
177
+ class MissingBrowserContextError < StandardError ; end
135
178
 
136
- # @param {!Protocol.Target.targetCreatedPayload} event
137
- def handle_target_created(event)
138
- target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
179
+ # @param target_info [Puppeteer::Target::TargetInfo]
180
+ # @param session [CDPSession|nil]
181
+ def create_target(target_info, session)
139
182
  browser_context_id = target_info.browser_context_id
140
183
  context =
141
184
  if browser_context_id && @contexts.has_key?(browser_context_id)
@@ -144,56 +187,39 @@ class Puppeteer::Browser
144
187
  @default_context
145
188
  end
146
189
 
147
- if @targets[target_info.target_id]
148
- raise TargetAlreadyExistError.new
190
+ unless context
191
+ raise MissingBrowserContextError.new('Missing browser context')
149
192
  end
150
193
 
151
- return unless @target_filter_callback.call(target_info)
152
-
153
- target = Puppeteer::Target.new(
194
+ Puppeteer::Target.new(
154
195
  target_info: target_info,
196
+ session: session,
155
197
  browser_context: context,
198
+ target_manager: @target_manager,
156
199
  session_factory: -> { @connection.create_session(target_info) },
157
200
  ignore_https_errors: @ignore_https_errors,
158
201
  default_viewport: @default_viewport,
159
202
  is_page_target_callback: @is_page_target_callback,
160
203
  )
161
- @targets[target_info.target_id] = target
162
- if_present(@wait_for_creating_targets.delete(target_info.target_id)) do |promise|
163
- promise.fulfill(target)
164
- end
165
- if await target.initialized_promise
204
+ end
205
+
206
+ private def handle_attached_to_target(target)
207
+ if target.initialized_promise.value!
166
208
  emit_event(BrowserEmittedEvents::TargetCreated, target)
167
- context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
209
+ target.browser_context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
168
210
  end
169
211
  end
170
212
 
171
- # @param {{targetId: string}} event
172
- def handle_target_destroyed(event)
173
- target_id = event['targetId']
174
- target = @targets[target_id]
213
+ private def handle_detached_from_target(target)
175
214
  target.ignore_initialize_callback_promise
176
- @targets.delete(target_id)
177
- if_present(@wait_for_creating_targets.delete(target_id)) do |promise|
178
- promise.reject('target destroyed')
179
- end
180
215
  target.closed_callback
181
- if await target.initialized_promise
216
+ if target.initialized_promise.value!
182
217
  emit_event(BrowserEmittedEvents::TargetDestroyed, target)
183
218
  target.browser_context.emit_event(BrowserContextEmittedEvents::TargetDestroyed, target)
184
219
  end
185
220
  end
186
221
 
187
- class TargetNotExistError < StandardError
188
- def initialize
189
- super('target should exist before targetInfoChanged')
190
- end
191
- end
192
-
193
- # @param {!Protocol.Target.targetInfoChangedPayload} event
194
- def handle_target_info_changed(event)
195
- target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
196
- target = @targets[target_info.target_id] or raise TargetNotExistError.new
222
+ private def handle_target_changed(target, target_info)
197
223
  previous_url = target.url
198
224
  was_initialized = target.initialized?
199
225
  target.handle_target_info_changed(target_info)
@@ -203,6 +229,10 @@ class Puppeteer::Browser
203
229
  end
204
230
  end
205
231
 
232
+ private def handle_target_discovered(target_info)
233
+ emit_event('targetdiscovered', target_info)
234
+ end
235
+
206
236
  # @return [String]
207
237
  def ws_endpoint
208
238
  @connection.url
@@ -212,44 +242,47 @@ class Puppeteer::Browser
212
242
  @default_context.new_page
213
243
  end
214
244
 
245
+ class MissingTargetError < StandardError ; end
246
+ class CreatePageError < StandardError ; end
247
+
215
248
  # @param {?string} contextId
216
249
  # @return {!Promise<!Puppeteer.Page>}
217
250
  def create_page_in_context(context_id)
218
- create_target_params = { url: 'about:blank' }
219
- if context_id
220
- create_target_params[:browserContextId] = context_id
221
- end
251
+ create_target_params = {
252
+ url: 'about:blank',
253
+ browserContextId: context_id,
254
+ }.compact
222
255
  result = @connection.send_message('Target.createTarget', **create_target_params)
223
256
  target_id = result['targetId']
224
- target = @targets[target_id]
257
+ target = @target_manager.available_targets[target_id]
225
258
  unless target
226
- # Target.targetCreated is often notified before the response of Target.createdTarget.
227
- # https://github.com/YusukeIwaki/puppeteer-ruby/issues/91
228
- # D, [2021-04-07T03:00:10.125241 #187] DEBUG -- : SEND >> {"method":"Target.createTarget","params":{"url":"about:blank","browserContextId":"56A86FC3391B50180CF9A6450A0D8C21"},"id":3}
229
- # D, [2021-04-07T03:00:10.142396 #187] DEBUG -- : RECV << {"id"=>3, "result"=>{"targetId"=>"A518447C415A1A3E1A8979454A155632"}}
230
- # D, [2021-04-07T03:00:10.145360 #187] DEBUG -- : RECV << {"method"=>"Target.targetCreated", "params"=>{"targetInfo"=>{"targetId"=>"A518447C415A1A3E1A8979454A155632", "type"=>"page", "title"=>"", "url"=>"", "attached"=>false, "canAccessOpener"=>false, "browserContextId"=>"56A86FC3391B50180CF9A6450A0D8C21"}}}
231
- # This is just a workaround logic...
232
- @wait_for_creating_targets[target_id] = resolvable_future
233
- target = await @wait_for_creating_targets[target_id]
259
+ raise MissingTargetError.new("Missing target for page (id = #{target_id})")
260
+ end
261
+ unless target.initialized_promise.value!
262
+ raise CreatePageError.new("Failed to create target for page (id = #{target_id})")
263
+ end
264
+ page = target.page
265
+ unless page
266
+ raise CreatePageError.new("Failed to create a page for context (id = #{context_id})")
234
267
  end
235
- await target.initialized_promise
236
- await target.page
268
+ page
237
269
  end
238
270
 
239
- # @return {!Array<!Target>}
271
+ # All active targets inside the Browser. In case of multiple browser contexts, returns
272
+ # an array with all the targets in all browser contexts.
240
273
  def targets
241
- @targets.values.select { |target| target.initialized? }
274
+ @target_manager.available_targets.values.select { |target| target.initialized? }
242
275
  end
243
276
 
244
277
 
245
- # @return {!Target}
278
+ # The target associated with the browser.
246
279
  def target
247
- targets.find { |target| target.type == 'browser' }
280
+ targets.find { |target| target.type == 'browser' } or raise 'Browser target is not found'
248
281
  end
249
282
 
250
283
  # used only in Target#opener
251
284
  private def find_target_by_id(target_id)
252
- @targets[target_id]
285
+ @target_manager.available_targets[target_id]
253
286
  end
254
287
 
255
288
  # @param predicate [Proc(Puppeteer::Target -> Boolean)]
@@ -293,12 +326,12 @@ class Puppeteer::Browser
293
326
 
294
327
  # @return [String]
295
328
  def version
296
- get_version.product
329
+ Version.fetch(@connection).product
297
330
  end
298
331
 
299
332
  # @return [String]
300
333
  def user_agent
301
- get_version.user_agent
334
+ Version.fetch(@connection).user_agent
302
335
  end
303
336
 
304
337
  def close
@@ -307,6 +340,7 @@ class Puppeteer::Browser
307
340
  end
308
341
 
309
342
  def disconnect
343
+ @target_manager.dispose
310
344
  @connection.dispose
311
345
  end
312
346
 
@@ -315,6 +349,10 @@ class Puppeteer::Browser
315
349
  end
316
350
 
317
351
  class Version
352
+ def self.fetch(connection)
353
+ new(connection.send_message('Browser.getVersion'))
354
+ end
355
+
318
356
  def initialize(hash)
319
357
  @protocol_version = hash['protocolVersion']
320
358
  @product = hash['product']
@@ -325,8 +363,4 @@ class Puppeteer::Browser
325
363
 
326
364
  attr_reader :protocol_version, :product, :revision, :user_agent, :js_version
327
365
  end
328
-
329
- private def get_version
330
- Version.new(@connection.send_message('Browser.getVersion'))
331
- end
332
366
  end
@@ -0,0 +1,67 @@
1
+ require_relative './browser'
2
+ require_relative './launcher/browser_options'
3
+
4
+ class Puppeteer::BrowserConnector
5
+ def initialize(options)
6
+ @browser_options = Puppeteer::Launcher::BrowserOptions.new(options)
7
+ @browser_ws_endpoint = options[:browser_ws_endpoint]
8
+ @browser_url = options[:browser_url]
9
+ @transport = options[:transport]
10
+ end
11
+
12
+ # @return [Puppeteer::Browser]
13
+ def connect_to_browser
14
+ version = Puppeteer::Browser::Version.fetch(connection)
15
+ product = version.product.downcase.include?('firefox') ? 'firefox' : 'chrome'
16
+
17
+ result = connection.send_message('Target.getBrowserContexts')
18
+ browser_context_ids = result['browserContextIds']
19
+
20
+ Puppeteer::Browser.create(
21
+ product: product,
22
+ connection: connection,
23
+ context_ids: browser_context_ids,
24
+ ignore_https_errors: @browser_options.ignore_https_errors?,
25
+ default_viewport: @browser_options.default_viewport,
26
+ process: nil,
27
+ close_callback: -> { connection.send_message('Browser.close') },
28
+ target_filter_callback: @browser_options.target_filter,
29
+ is_page_target_callback: @browser_options.is_page_target,
30
+ )
31
+ end
32
+
33
+ private def connection
34
+ @connection ||=
35
+ if @browser_ws_endpoint && @browser_url.nil? && @transport.nil?
36
+ connect_with_browser_ws_endpoint(@browser_ws_endpoint)
37
+ elsif @browser_ws_endpoint.nil? && @browser_url && @transport.nil?
38
+ connect_with_browser_url(@browser_url)
39
+ elsif @browser_ws_endpoint.nil? && @browser_url.nil? && @transport
40
+ connect_with_transport(@transport)
41
+ else
42
+ raise ArgumentError.new("Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect")
43
+ end
44
+ end
45
+
46
+ # @return [Puppeteer::Connection]
47
+ private def connect_with_browser_ws_endpoint(browser_ws_endpoint)
48
+ transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
49
+ Puppeteer::Connection.new(browser_ws_endpoint, transport, @browser_options.slow_mo)
50
+ end
51
+
52
+ # @return [Puppeteer::Connection]
53
+ private def connect_with_browser_url(browser_url)
54
+ require 'net/http'
55
+ uri = URI(browser_url)
56
+ uri.path = '/json/version'
57
+ response_body = Net::HTTP.get(uri)
58
+ json = JSON.parse(response_body)
59
+ connection_url = json['webSocketDebuggerUrl']
60
+ connect_with_browser_ws_endpoint(connection_url)
61
+ end
62
+
63
+ # @return [Puppeteer::Connection]
64
+ private def connect_with_transport(transport)
65
+ Puppeteer::Connection.new('', transport, @browser_options.slow_mo)
66
+ end
67
+ end
@@ -0,0 +1,256 @@
1
+ class Puppeteer::ChromeTargetManager
2
+ include Puppeteer::EventCallbackable
3
+
4
+ def initialize(connection:, target_factory:, target_filter_callback:)
5
+ @discovered_targets_by_target_id = {}
6
+ @attached_targets_by_target_id = {}
7
+ @attached_targets_by_session_id = {}
8
+ @ignored_targets = Set.new
9
+ @target_ids_for_init = Set.new
10
+
11
+ @connection = connection
12
+ @target_filter_callback = target_filter_callback
13
+ @target_factory = target_factory
14
+ @target_interceptors = {}
15
+ @initialize_promise = resolvable_future
16
+
17
+ @connection_event_listeners = []
18
+ @connection_event_listeners << @connection.add_event_listener(
19
+ 'Target.targetCreated',
20
+ &method(:handle_target_created)
21
+ )
22
+ @connection_event_listeners << @connection.add_event_listener(
23
+ 'Target.targetDestroyed',
24
+ &method(:handle_target_destroyed)
25
+ )
26
+ @connection_event_listeners << @connection.add_event_listener(
27
+ 'Target.targetInfoChanged',
28
+ &method(:handle_target_info_changed)
29
+ )
30
+ @connection_event_listeners << @connection.add_event_listener(
31
+ 'sessiondetached',
32
+ &method(:handle_session_detached)
33
+ )
34
+
35
+ setup_attachment_listeners(@connection)
36
+ @connection.async_send_message('Target.setDiscoverTargets', discover: true)
37
+ end
38
+
39
+ def init
40
+ @discovered_targets_by_target_id.each do |target_id, target_info|
41
+ if @target_filter_callback.call(target_info)
42
+ @target_ids_for_init << target_id
43
+ end
44
+ end
45
+ @connection.send_message('Target.setAutoAttach', {
46
+ waitForDebuggerOnStart: true,
47
+ flatten: true,
48
+ autoAttach: true,
49
+ })
50
+ @initialize_promise.value!
51
+ end
52
+
53
+ def dispose
54
+ @connection.remove_event_listener(*@connection_event_listeners)
55
+ remove_attachment_listeners(@connection)
56
+ end
57
+
58
+ def available_targets
59
+ @attached_targets_by_target_id
60
+ end
61
+
62
+ def add_target_interceptor(client, interceptor)
63
+ interceptors = @target_interceptors[client] || []
64
+ interceptors << interceptor
65
+ @target_interceptors[client] = interceptors
66
+ end
67
+
68
+ def remove_target_interceptor(client, interceptor)
69
+ @target_interceptors[client]&.delete_if { |current| current == interceptor }
70
+ end
71
+
72
+ private def setup_attachment_listeners(session)
73
+ @attachment_listener_ids ||= {}
74
+ @attachment_listener_ids[session] ||= []
75
+
76
+ @attachment_listener_ids[session] << session.add_event_listener('Target.attachedToTarget') do |event|
77
+ handle_attached_to_target(session, event)
78
+ end
79
+
80
+ @attachment_listener_ids[session] << session.add_event_listener('Target.detachedFromTarget') do |event|
81
+ handle_detached_from_target(session, event)
82
+ end
83
+ end
84
+
85
+ private def remove_attachment_listeners(session)
86
+ return unless @attachment_listener_ids
87
+ listener_ids = @attachment_listener_ids.delete(session)
88
+ return if !listener_ids || listener_ids.empty?
89
+ session.remove_event_listener(*listener_ids)
90
+ end
91
+
92
+ private def handle_session_detached(session)
93
+ remove_attachment_listeners(session)
94
+ @target_interceptors.delete(session)
95
+ end
96
+
97
+ private def handle_target_created(event)
98
+ target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
99
+ @discovered_targets_by_target_id[target_info.target_id] = target_info
100
+
101
+ emit_event(TargetManagerEmittedEvents::TargetDiscovered, target_info)
102
+
103
+ # The connection is already attached to the browser target implicitly,
104
+ # therefore, no new CDPSession is created and we have special handling
105
+ # here.
106
+ if target_info.type == 'browser' && target_info.attached
107
+ return if @attached_targets_by_target_id[target_info.target_id]
108
+
109
+ target = @target_factory.call(target_info, nil)
110
+ @attached_targets_by_target_id[target_info.target_id] = target
111
+ end
112
+
113
+ if target_info.type == 'shared_worker'
114
+ # Special case (https://crbug.com/1338156): currently, shared_workers
115
+ # don't get auto-attached. This should be removed once the auto-attach
116
+ # works.
117
+ @connection.create_session(target_info)
118
+ end
119
+ end
120
+
121
+ private def handle_target_destroyed(event)
122
+ target_id = event['targetId']
123
+ target_info = @discovered_targets_by_target_id.delete(target_id)
124
+ finish_initialization_if_ready(target_id)
125
+
126
+ if target_info.type == 'service_worker' && @attached_targets_by_target_id.has_key?(target_id)
127
+ # Special case for service workers: report TargetGone event when
128
+ # the worker is destroyed.
129
+ target = @attached_targets_by_target_id.delete(target_id)
130
+ emit_event(TargetManagerEmittedEvents::TargetGone, target)
131
+ end
132
+ end
133
+
134
+ private def handle_target_info_changed(event)
135
+ target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
136
+ @discovered_targets_by_target_id[target_info.target_id] = target_info
137
+
138
+ if @ignored_targets.include?(target_info.target_id) || !@attached_targets_by_target_id.has_key?(target_info.target_id) || !target_info.attached
139
+ return
140
+ end
141
+ original_target = @attached_targets_by_target_id[target_info.target_id]
142
+ emit_event(TargetManagerEmittedEvents::TargetChanged, original_target, target_info)
143
+ end
144
+
145
+ class SessionNotCreatedError < StandardError ; end
146
+
147
+ private def handle_attached_to_target(parent_session, event)
148
+ target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
149
+ session_id = event['sessionId']
150
+ session = @connection.session(session_id)
151
+ unless session
152
+ raise SessionNotCreatedError.new("Session #{session_id} was not created.")
153
+ end
154
+
155
+ silent_detach = -> {
156
+ begin
157
+ session.send_message('Runtime.runIfWaitingForDebugger')
158
+ rescue => err
159
+ Logger.new($stderr).warn(err)
160
+ end
161
+
162
+ # We don't use `session.detach()` because that dispatches all commands on
163
+ # the connection instead of the parent session.
164
+ begin
165
+ parent_session.send_message('Target.detachFromTarget', {
166
+ sessionId: session.id,
167
+ })
168
+ rescue => err
169
+ Logger.new($stderr).warn(err)
170
+ end
171
+ }
172
+
173
+ # Special case for service workers: being attached to service workers will
174
+ # prevent them from ever being destroyed. Therefore, we silently detach
175
+ # from service workers unless the connection was manually created via
176
+ # `page.worker()`. To determine this, we use
177
+ # `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we
178
+ # should determine if a target is auto-attached or not with the help of
179
+ # CDP.
180
+ if target_info.type == 'service_worker' && @connection.auto_attached?(target_info.target_id)
181
+ finish_initialization_if_ready(target_info.target_id)
182
+ silent_detach.call
183
+ if parent_session.is_a?(Puppeteer::CDPSession)
184
+ target = @target_factory.call(target_info, parent_session)
185
+ @attached_targets_by_target_id[target_info.target_id] = target
186
+ emit_event(TargetManagerEmittedEvents::TargetAvailable, target)
187
+ end
188
+
189
+ return
190
+ end
191
+
192
+ unless @target_filter_callback.call(target_info)
193
+ @ignored_targets << target_info.target_id
194
+ finish_initialization_if_ready(target_info.target_id)
195
+ silent_detach.call
196
+
197
+ return
198
+ end
199
+
200
+ target = @attached_targets_by_target_id[target_info.target_id] || @target_factory.call(target_info, session)
201
+ setup_attachment_listeners(session)
202
+
203
+ @attached_targets_by_target_id[target_info.target_id] ||= target
204
+ @attached_targets_by_session_id[session.id] = target
205
+
206
+ @target_interceptors[parent_session]&.each do |interceptor|
207
+ if parent_session.is_a?(Puppeteer::Connection)
208
+ interceptor.call(target, nil)
209
+ else
210
+ # Sanity check: if parent session is not a connection, it should be
211
+ # present in #attachedTargetsBySessionId.
212
+ attached_target = @attached_targets_by_session_id[parent_session.id]
213
+ unless attached_target
214
+ raise "No target found for the parent session: #{parent_session.id}"
215
+ end
216
+ interceptor.call(target, attached_target)
217
+ end
218
+ end
219
+
220
+ @target_ids_for_init.delete(target.target_id)
221
+ future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
222
+
223
+ if @target_ids_for_init.empty?
224
+ @initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
225
+ end
226
+
227
+ future do
228
+ # TODO: the browser might be shutting down here. What do we do with the error?
229
+ await_all(
230
+ session.async_send_message('Target.setAutoAttach', {
231
+ waitForDebuggerOnStart: true,
232
+ flatten: true,
233
+ autoAttach: true,
234
+ }),
235
+ session.async_send_message('Runtime.runIfWaitingForDebugger'),
236
+ )
237
+ rescue => err
238
+ Logger.new($stderr).warn(err)
239
+ end
240
+ end
241
+
242
+ private def finish_initialization_if_ready(target_id)
243
+ @target_ids_for_init.delete(target_id)
244
+ if @target_ids_for_init.empty?
245
+ @initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
246
+ end
247
+ end
248
+
249
+ private def handle_detached_from_target(parent_session, event)
250
+ session_id = event['sessionId']
251
+ target = @attached_targets_by_session_id.delete(session_id)
252
+ return unless target
253
+ @attached_targets_by_target_id.delete(target.target_id)
254
+ emit_event(TargetManagerEmittedEvents::TargetGone, target)
255
+ end
256
+ end