/
chrome_target_manager.rb
262 lines (223 loc) · 9.04 KB
/
chrome_target_manager.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
class Puppeteer::ChromeTargetManager
include Puppeteer::EventCallbackable
def initialize(connection:, target_factory:, target_filter_callback:)
@discovered_targets_by_target_id = {}
@attached_targets_by_target_id = {}
@attached_targets_by_session_id = {}
@ignored_targets = Set.new
@target_ids_for_init = Set.new
@connection = connection
@target_filter_callback = target_filter_callback
@target_factory = target_factory
@target_interceptors = {}
@initialize_promise = resolvable_future
@connection_event_listeners = []
@connection_event_listeners << @connection.add_event_listener(
'Target.targetCreated',
&method(:handle_target_created)
)
@connection_event_listeners << @connection.add_event_listener(
'Target.targetDestroyed',
&method(:handle_target_destroyed)
)
@connection_event_listeners << @connection.add_event_listener(
'Target.targetInfoChanged',
&method(:handle_target_info_changed)
)
@connection_event_listeners << @connection.add_event_listener(
'sessiondetached',
&method(:handle_session_detached)
)
setup_attachment_listeners(@connection)
@connection.async_send_message('Target.setDiscoverTargets', {
discover: true,
filter: [
{ type: 'tab', exclude: true },
{},
],
})
end
def init
@discovered_targets_by_target_id.each do |target_id, target_info|
if @target_filter_callback.call(target_info)
@target_ids_for_init << target_id
end
end
@connection.send_message('Target.setAutoAttach', {
waitForDebuggerOnStart: true,
flatten: true,
autoAttach: true,
})
@initialize_promise.value!
end
def dispose
@connection.remove_event_listener(*@connection_event_listeners)
remove_attachment_listeners(@connection)
end
def available_targets
@attached_targets_by_target_id
end
def add_target_interceptor(client, interceptor)
interceptors = @target_interceptors[client] || []
interceptors << interceptor
@target_interceptors[client] = interceptors
end
def remove_target_interceptor(client, interceptor)
@target_interceptors[client]&.delete_if { |current| current == interceptor }
end
private def setup_attachment_listeners(session)
@attachment_listener_ids ||= {}
@attachment_listener_ids[session] ||= []
@attachment_listener_ids[session] << session.add_event_listener('Target.attachedToTarget') do |event|
handle_attached_to_target(session, event)
end
@attachment_listener_ids[session] << session.add_event_listener('Target.detachedFromTarget') do |event|
handle_detached_from_target(session, event)
end
end
private def remove_attachment_listeners(session)
return unless @attachment_listener_ids
listener_ids = @attachment_listener_ids.delete(session)
return if !listener_ids || listener_ids.empty?
session.remove_event_listener(*listener_ids)
end
private def handle_session_detached(session)
remove_attachment_listeners(session)
@target_interceptors.delete(session)
end
private def handle_target_created(event)
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
@discovered_targets_by_target_id[target_info.target_id] = target_info
emit_event(TargetManagerEmittedEvents::TargetDiscovered, target_info)
# The connection is already attached to the browser target implicitly,
# therefore, no new CDPSession is created and we have special handling
# here.
if target_info.type == 'browser' && target_info.attached
return if @attached_targets_by_target_id[target_info.target_id]
target = @target_factory.call(target_info, nil)
@attached_targets_by_target_id[target_info.target_id] = target
end
if target_info.type == 'shared_worker'
# Special case (https://crbug.com/1338156): currently, shared_workers
# don't get auto-attached. This should be removed once the auto-attach
# works.
@connection.create_session(target_info)
end
end
private def handle_target_destroyed(event)
target_id = event['targetId']
target_info = @discovered_targets_by_target_id.delete(target_id)
finish_initialization_if_ready(target_id)
if target_info.type == 'service_worker' && @attached_targets_by_target_id.has_key?(target_id)
# Special case for service workers: report TargetGone event when
# the worker is destroyed.
target = @attached_targets_by_target_id.delete(target_id)
emit_event(TargetManagerEmittedEvents::TargetGone, target)
end
end
private def handle_target_info_changed(event)
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
@discovered_targets_by_target_id[target_info.target_id] = target_info
if @ignored_targets.include?(target_info.target_id) || !@attached_targets_by_target_id.has_key?(target_info.target_id) || !target_info.attached
return
end
original_target = @attached_targets_by_target_id[target_info.target_id]
emit_event(TargetManagerEmittedEvents::TargetChanged, original_target, target_info)
end
class SessionNotCreatedError < StandardError ; end
private def handle_attached_to_target(parent_session, event)
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
session_id = event['sessionId']
session = @connection.session(session_id)
unless session
raise SessionNotCreatedError.new("Session #{session_id} was not created.")
end
silent_detach = -> {
begin
session.send_message('Runtime.runIfWaitingForDebugger')
rescue => err
Logger.new($stderr).warn(err)
end
# We don't use `session.detach()` because that dispatches all commands on
# the connection instead of the parent session.
begin
parent_session.send_message('Target.detachFromTarget', {
sessionId: session.id,
})
rescue => err
Logger.new($stderr).warn(err)
end
}
# Special case for service workers: being attached to service workers will
# prevent them from ever being destroyed. Therefore, we silently detach
# from service workers unless the connection was manually created via
# `page.worker()`. To determine this, we use
# `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we
# should determine if a target is auto-attached or not with the help of
# CDP.
if target_info.type == 'service_worker' && @connection.auto_attached?(target_info.target_id)
finish_initialization_if_ready(target_info.target_id)
silent_detach.call
if parent_session.is_a?(Puppeteer::CDPSession)
target = @target_factory.call(target_info, parent_session)
@attached_targets_by_target_id[target_info.target_id] = target
emit_event(TargetManagerEmittedEvents::TargetAvailable, target)
end
return
end
unless @target_filter_callback.call(target_info)
@ignored_targets << target_info.target_id
finish_initialization_if_ready(target_info.target_id)
silent_detach.call
return
end
target = @attached_targets_by_target_id[target_info.target_id] || @target_factory.call(target_info, session)
setup_attachment_listeners(session)
@attached_targets_by_target_id[target_info.target_id] ||= target
@attached_targets_by_session_id[session.id] = target
@target_interceptors[parent_session]&.each do |interceptor|
if parent_session.is_a?(Puppeteer::Connection)
interceptor.call(target, nil)
else
# Sanity check: if parent session is not a connection, it should be
# present in #attachedTargetsBySessionId.
attached_target = @attached_targets_by_session_id[parent_session.id]
unless attached_target
raise "No target found for the parent session: #{parent_session.id}"
end
interceptor.call(target, attached_target)
end
end
@target_ids_for_init.delete(target.target_id)
future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
if @target_ids_for_init.empty?
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
end
future do
# TODO: the browser might be shutting down here. What do we do with the error?
await_all(
session.async_send_message('Target.setAutoAttach', {
waitForDebuggerOnStart: true,
flatten: true,
autoAttach: true,
}),
session.async_send_message('Runtime.runIfWaitingForDebugger'),
)
rescue => err
Logger.new($stderr).warn(err)
end
end
private def finish_initialization_if_ready(target_id)
@target_ids_for_init.delete(target_id)
if @target_ids_for_init.empty?
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
end
end
private def handle_detached_from_target(parent_session, event)
session_id = event['sessionId']
target = @attached_targets_by_session_id.delete(session_id)
return unless target
@attached_targets_by_target_id.delete(target.target_id)
emit_event(TargetManagerEmittedEvents::TargetGone, target)
end
end