puppeteer-ruby 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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