puppeteer-ruby 0.45.6 → 0.50.0.alpha5
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -3
- data/AGENTS.md +169 -0
- data/CLAUDE/README.md +41 -0
- data/CLAUDE/architecture.md +253 -0
- data/CLAUDE/cdp_protocol.md +230 -0
- data/CLAUDE/concurrency.md +216 -0
- data/CLAUDE/porting_puppeteer.md +575 -0
- data/CLAUDE/rbs_type_checking.md +101 -0
- data/CLAUDE/spec_migration_plans.md +1041 -0
- data/CLAUDE/testing.md +278 -0
- data/CLAUDE.md +242 -0
- data/README.md +8 -0
- data/Rakefile +7 -0
- data/Steepfile +28 -0
- data/docs/api_coverage.md +105 -56
- data/lib/puppeteer/aria_query_handler.rb +3 -2
- data/lib/puppeteer/async_utils.rb +214 -0
- data/lib/puppeteer/browser.rb +98 -56
- data/lib/puppeteer/browser_connector.rb +18 -3
- data/lib/puppeteer/browser_context.rb +196 -3
- data/lib/puppeteer/browser_runner.rb +18 -10
- data/lib/puppeteer/cdp_session.rb +67 -23
- data/lib/puppeteer/chrome_target_manager.rb +65 -40
- data/lib/puppeteer/connection.rb +55 -36
- data/lib/puppeteer/console_message.rb +9 -1
- data/lib/puppeteer/console_patch.rb +47 -0
- data/lib/puppeteer/css_coverage.rb +5 -3
- data/lib/puppeteer/custom_query_handler.rb +80 -33
- data/lib/puppeteer/define_async_method.rb +31 -37
- data/lib/puppeteer/dialog.rb +47 -14
- data/lib/puppeteer/element_handle.rb +231 -62
- data/lib/puppeteer/emulation_manager.rb +1 -1
- data/lib/puppeteer/env.rb +1 -1
- data/lib/puppeteer/errors.rb +25 -2
- data/lib/puppeteer/event_callbackable.rb +15 -0
- data/lib/puppeteer/events.rb +4 -0
- data/lib/puppeteer/execution_context.rb +148 -3
- data/lib/puppeteer/file_chooser.rb +6 -0
- data/lib/puppeteer/frame.rb +162 -91
- data/lib/puppeteer/frame_manager.rb +69 -48
- data/lib/puppeteer/http_request.rb +114 -38
- data/lib/puppeteer/http_response.rb +24 -7
- data/lib/puppeteer/isolated_world.rb +64 -41
- data/lib/puppeteer/js_coverage.rb +5 -3
- data/lib/puppeteer/js_handle.rb +58 -16
- data/lib/puppeteer/keyboard.rb +30 -17
- data/lib/puppeteer/launcher/browser_options.rb +3 -1
- data/lib/puppeteer/launcher/chrome.rb +8 -5
- data/lib/puppeteer/launcher/launch_options.rb +7 -2
- data/lib/puppeteer/launcher.rb +4 -8
- data/lib/puppeteer/lifecycle_watcher.rb +38 -22
- data/lib/puppeteer/mouse.rb +273 -64
- data/lib/puppeteer/network_event_manager.rb +7 -0
- data/lib/puppeteer/network_manager.rb +393 -112
- data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
- data/lib/puppeteer/page.rb +568 -226
- data/lib/puppeteer/puppeteer.rb +171 -64
- data/lib/puppeteer/query_handler_manager.rb +112 -16
- data/lib/puppeteer/reactor_runner.rb +247 -0
- data/lib/puppeteer/remote_object.rb +127 -47
- data/lib/puppeteer/target.rb +74 -27
- data/lib/puppeteer/task_manager.rb +3 -1
- data/lib/puppeteer/timeout_helper.rb +6 -10
- data/lib/puppeteer/touch_handle.rb +39 -0
- data/lib/puppeteer/touch_screen.rb +72 -22
- data/lib/puppeteer/tracing.rb +3 -3
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +264 -101
- data/lib/puppeteer/web_socket.rb +2 -2
- data/lib/puppeteer/web_socket_transport.rb +91 -27
- data/lib/puppeteer/web_worker.rb +175 -0
- data/lib/puppeteer.rb +20 -4
- data/puppeteer-ruby.gemspec +15 -11
- data/sig/_external.rbs +8 -0
- data/sig/_supplementary.rbs +314 -0
- data/sig/puppeteer/browser.rbs +166 -0
- data/sig/puppeteer/cdp_session.rbs +64 -0
- data/sig/puppeteer/dialog.rbs +41 -0
- data/sig/puppeteer/element_handle.rbs +305 -0
- data/sig/puppeteer/execution_context.rbs +87 -0
- data/sig/puppeteer/frame.rbs +226 -0
- data/sig/puppeteer/http_request.rbs +214 -0
- data/sig/puppeteer/http_response.rbs +89 -0
- data/sig/puppeteer/js_handle.rbs +64 -0
- data/sig/puppeteer/keyboard.rbs +40 -0
- data/sig/puppeteer/mouse.rbs +113 -0
- data/sig/puppeteer/page.rbs +515 -0
- data/sig/puppeteer/puppeteer.rbs +98 -0
- data/sig/puppeteer/remote_object.rbs +78 -0
- data/sig/puppeteer/touch_handle.rbs +21 -0
- data/sig/puppeteer/touch_screen.rbs +35 -0
- data/sig/puppeteer/web_worker.rbs +83 -0
- metadata +116 -45
- data/CHANGELOG.md +0 -397
- data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
- data/lib/puppeteer/firefox_target_manager.rb +0 -157
- data/lib/puppeteer/launcher/firefox.rb +0 -453
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
2
|
require 'open3'
|
|
3
|
-
require 'timeout'
|
|
4
|
-
|
|
5
3
|
# https://github.com/puppeteer/puppeteer/blob/master/lib/Launcher.js
|
|
6
4
|
class Puppeteer::BrowserRunner
|
|
7
5
|
include Puppeteer::DebugPrint
|
|
@@ -9,8 +7,7 @@ class Puppeteer::BrowserRunner
|
|
|
9
7
|
# @param {string} executablePath
|
|
10
8
|
# @param {!Array<string>} processArguments
|
|
11
9
|
# @param {string=} tempDirectory
|
|
12
|
-
def initialize(
|
|
13
|
-
@for_firefox = for_firefox
|
|
10
|
+
def initialize(executable_path, process_arguments, user_data_dir, using_temp_user_data_dir)
|
|
14
11
|
@executable_path = executable_path
|
|
15
12
|
@process_arguments = process_arguments
|
|
16
13
|
@user_data_dir = user_data_dir
|
|
@@ -54,7 +51,7 @@ class Puppeteer::BrowserRunner
|
|
|
54
51
|
attr_reader :stdout, :stderr, :spawnargs
|
|
55
52
|
end
|
|
56
53
|
|
|
57
|
-
class LaunchError <
|
|
54
|
+
class LaunchError < Puppeteer::Error
|
|
58
55
|
def initialize(reason)
|
|
59
56
|
super("Failed to launch browser! #{reason}")
|
|
60
57
|
end
|
|
@@ -159,13 +156,18 @@ class Puppeteer::BrowserRunner
|
|
|
159
156
|
end
|
|
160
157
|
|
|
161
158
|
|
|
162
|
-
# @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
|
|
159
|
+
# @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string, protocolTimeout: number?})} options
|
|
163
160
|
# @return {!Promise<!Connection>}
|
|
164
|
-
def setup_connection(use_pipe:, timeout:, slow_mo:, preferred_revision:)
|
|
161
|
+
def setup_connection(use_pipe:, timeout:, slow_mo:, preferred_revision:, protocol_timeout: nil)
|
|
165
162
|
if !use_pipe
|
|
166
163
|
browser_ws_endpoint = wait_for_ws_endpoint(@proc, timeout, preferred_revision)
|
|
167
164
|
transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
|
|
168
|
-
@connection = Puppeteer::Connection.new(
|
|
165
|
+
@connection = Puppeteer::Connection.new(
|
|
166
|
+
browser_ws_endpoint,
|
|
167
|
+
transport,
|
|
168
|
+
slow_mo,
|
|
169
|
+
protocol_timeout: protocol_timeout,
|
|
170
|
+
)
|
|
169
171
|
else
|
|
170
172
|
raise NotImplementedError.new('PipeTransport is not yet implemented')
|
|
171
173
|
end
|
|
@@ -175,7 +177,7 @@ class Puppeteer::BrowserRunner
|
|
|
175
177
|
|
|
176
178
|
private def wait_for_ws_endpoint(browser_process, timeout, preferred_revision)
|
|
177
179
|
lines = []
|
|
178
|
-
|
|
180
|
+
wait_for_endpoint = lambda do
|
|
179
181
|
loop do
|
|
180
182
|
line = browser_process.stderr.readline
|
|
181
183
|
/^WebDriver BiDi listening on (ws:\/\/.*)$/.match(line) do |m|
|
|
@@ -188,9 +190,15 @@ class Puppeteer::BrowserRunner
|
|
|
188
190
|
lines << line
|
|
189
191
|
end
|
|
190
192
|
end
|
|
193
|
+
|
|
194
|
+
if timeout && timeout > 0
|
|
195
|
+
Puppeteer::AsyncUtils.async_timeout(timeout, wait_for_endpoint).wait
|
|
196
|
+
else
|
|
197
|
+
wait_for_endpoint.call
|
|
198
|
+
end
|
|
191
199
|
rescue EOFError
|
|
192
200
|
raise LaunchError.new("\n#{lines.join("\n")}\nTROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md")
|
|
193
|
-
rescue
|
|
201
|
+
rescue Async::TimeoutError
|
|
194
202
|
raise Puppeteer::TimeoutError.new("Timed out after #{timeout} ms while trying to connect to the browser! Only Chrome at revision r#{preferred_revision} is guaranteed to work.")
|
|
195
203
|
end
|
|
196
204
|
end
|
|
@@ -1,56 +1,91 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
1
3
|
class Puppeteer::CDPSession
|
|
2
4
|
include Puppeteer::DebugPrint
|
|
3
5
|
include Puppeteer::EventCallbackable
|
|
4
6
|
using Puppeteer::DefineAsyncMethod
|
|
5
7
|
|
|
6
|
-
class Error <
|
|
8
|
+
class Error < Puppeteer::Error; end
|
|
7
9
|
|
|
8
|
-
# @
|
|
9
|
-
# @
|
|
10
|
-
# @
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# @rbs connection: Puppeteer::Connection -- CDP connection
|
|
11
|
+
# @rbs target_type: String -- Target type
|
|
12
|
+
# @rbs session_id: String -- CDP session id
|
|
13
|
+
# @rbs parent_session: Puppeteer::CDPSession? -- Parent CDP session
|
|
14
|
+
# @rbs return: void -- No return value
|
|
15
|
+
def initialize(connection, target_type, session_id, parent_session: nil)
|
|
16
|
+
@callbacks = {}
|
|
17
|
+
@callbacks_mutex = Mutex.new
|
|
13
18
|
@connection = connection
|
|
14
19
|
@target_type = target_type
|
|
15
20
|
@session_id = session_id
|
|
21
|
+
@parent_session = parent_session
|
|
22
|
+
@ready_promise = Async::Promise.new
|
|
23
|
+
@target = nil
|
|
16
24
|
end
|
|
17
25
|
|
|
18
|
-
# @
|
|
26
|
+
# @rbs return: String -- CDP session id
|
|
19
27
|
def id
|
|
20
28
|
@session_id
|
|
21
29
|
end
|
|
22
30
|
|
|
23
|
-
attr_reader :connection
|
|
31
|
+
attr_reader :connection #: Puppeteer::Connection?
|
|
32
|
+
attr_reader :parent_session #: Puppeteer::CDPSession?
|
|
33
|
+
attr_accessor :target #: Puppeteer::Target?
|
|
34
|
+
|
|
35
|
+
# @rbs return: void -- Resolve session readiness
|
|
36
|
+
def mark_ready
|
|
37
|
+
@ready_promise.resolve(true) unless @ready_promise.resolved?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @rbs return: bool -- True when session is ready
|
|
41
|
+
def wait_for_ready
|
|
42
|
+
@ready_promise.wait
|
|
43
|
+
end
|
|
24
44
|
|
|
25
|
-
# @
|
|
26
|
-
# @
|
|
27
|
-
# @
|
|
45
|
+
# @rbs method: String -- CDP method name
|
|
46
|
+
# @rbs params: Hash[String, untyped] -- CDP parameters
|
|
47
|
+
# @rbs return: Hash[String, untyped] -- CDP response
|
|
28
48
|
def send_message(method, params = {})
|
|
29
|
-
|
|
49
|
+
protocol_timeout = @connection&.protocol_timeout
|
|
50
|
+
if protocol_timeout && protocol_timeout > 0
|
|
51
|
+
begin
|
|
52
|
+
Puppeteer::AsyncUtils.async_timeout(protocol_timeout, async_send_message(method, params)).wait
|
|
53
|
+
rescue Async::TimeoutError
|
|
54
|
+
raise Puppeteer::Connection::ProtocolError.new(
|
|
55
|
+
method: method,
|
|
56
|
+
error_message: "Timeout #{protocol_timeout}ms exceeded",
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
async_send_message(method, params).wait
|
|
61
|
+
end
|
|
30
62
|
end
|
|
31
63
|
|
|
32
|
-
# @
|
|
33
|
-
# @
|
|
34
|
-
# @
|
|
64
|
+
# @rbs method: String -- CDP method name
|
|
65
|
+
# @rbs params: Hash[String, untyped] -- CDP parameters
|
|
66
|
+
# @rbs return: Async::Promise[Hash[String, untyped]] -- Async CDP response
|
|
35
67
|
def async_send_message(method, params = {})
|
|
36
68
|
if !@connection
|
|
37
69
|
raise Error.new("Protocol error (#{method}): Session closed. Most likely the #{@target_type} has been closed.")
|
|
38
70
|
end
|
|
39
71
|
|
|
40
|
-
promise =
|
|
72
|
+
promise = Async::Promise.new
|
|
41
73
|
|
|
42
74
|
@connection.generate_id do |id|
|
|
43
|
-
@
|
|
75
|
+
@callbacks_mutex.synchronize do
|
|
76
|
+
@callbacks[id] = Puppeteer::Connection::MessageCallback.new(method: method, promise: promise)
|
|
77
|
+
end
|
|
44
78
|
@connection.raw_send(id: id, message: { sessionId: @session_id, method: method, params: params })
|
|
45
79
|
end
|
|
46
80
|
|
|
47
81
|
promise
|
|
48
82
|
end
|
|
49
83
|
|
|
50
|
-
# @
|
|
84
|
+
# @rbs message: Hash[String, untyped] -- Raw CDP message
|
|
85
|
+
# @rbs return: void -- No return value
|
|
51
86
|
def handle_message(message)
|
|
52
87
|
if message['id']
|
|
53
|
-
if callback = @callbacks.delete(message['id'])
|
|
88
|
+
if callback = @callbacks_mutex.synchronize { @callbacks.delete(message['id']) }
|
|
54
89
|
callback_with_message(callback, message)
|
|
55
90
|
else
|
|
56
91
|
raise Error.new("unknown id: #{message['id']}")
|
|
@@ -72,6 +107,7 @@ class Puppeteer::CDPSession
|
|
|
72
107
|
end
|
|
73
108
|
end
|
|
74
109
|
|
|
110
|
+
# @rbs return: void -- Detach the session
|
|
75
111
|
def detach
|
|
76
112
|
if !@connection
|
|
77
113
|
raise Error.new("Session already detarched. Most likely the #{@target_type} has been closed.")
|
|
@@ -79,24 +115,32 @@ class Puppeteer::CDPSession
|
|
|
79
115
|
@connection.send_message('Target.detachFromTarget', sessionId: @session_id)
|
|
80
116
|
end
|
|
81
117
|
|
|
118
|
+
# @rbs return: void -- Close the session and reject pending callbacks
|
|
82
119
|
def handle_closed
|
|
83
|
-
@
|
|
120
|
+
callbacks = @callbacks_mutex.synchronize do
|
|
121
|
+
@callbacks.values.tap { @callbacks.clear }
|
|
122
|
+
end
|
|
123
|
+
callbacks.each do |callback|
|
|
84
124
|
callback.reject(
|
|
85
125
|
Puppeteer::Connection::ProtocolError.new(
|
|
86
126
|
method: callback.method,
|
|
87
127
|
error_message: 'Target Closed.'))
|
|
88
128
|
end
|
|
89
|
-
@
|
|
129
|
+
@ready_promise.reject(Error.new("Session closed")) unless @ready_promise.resolved?
|
|
90
130
|
@connection = nil
|
|
91
131
|
emit_event(CDPSessionEmittedEvents::Disconnected)
|
|
92
132
|
end
|
|
93
133
|
|
|
94
|
-
# @
|
|
134
|
+
# @rbs event_name: String -- CDP event name
|
|
135
|
+
# @rbs &block: ^(untyped) -> void -- Event handler
|
|
136
|
+
# @rbs return: String -- Listener id
|
|
95
137
|
def on(event_name, &block)
|
|
96
138
|
add_event_listener(event_name, &block)
|
|
97
139
|
end
|
|
98
140
|
|
|
99
|
-
# @
|
|
141
|
+
# @rbs event_name: String -- CDP event name
|
|
142
|
+
# @rbs &block: ^(untyped) -> void -- Event handler
|
|
143
|
+
# @rbs return: String -- Listener id
|
|
100
144
|
def once(event_name, &block)
|
|
101
145
|
observe_first(event_name, &block)
|
|
102
146
|
end
|
|
@@ -8,12 +8,13 @@ class Puppeteer::ChromeTargetManager
|
|
|
8
8
|
@attached_targets_by_session_id = {}
|
|
9
9
|
@ignored_targets = Set.new
|
|
10
10
|
@target_ids_for_init = Set.new
|
|
11
|
+
@service_worker_detach_promises = {}
|
|
11
12
|
|
|
12
13
|
@connection = connection
|
|
13
14
|
@target_filter_callback = target_filter_callback
|
|
14
15
|
@target_factory = target_factory
|
|
15
16
|
@target_interceptors = {}
|
|
16
|
-
@initialize_promise =
|
|
17
|
+
@initialize_promise = Async::Promise.new
|
|
17
18
|
|
|
18
19
|
@connection_event_listeners = []
|
|
19
20
|
@connection_event_listeners << @connection.add_event_listener(
|
|
@@ -34,15 +35,17 @@ class Puppeteer::ChromeTargetManager
|
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
setup_attachment_listeners(@connection)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
|
|
39
|
+
Async do
|
|
40
|
+
@connection.async_send_message('Target.setDiscoverTargets', {
|
|
41
|
+
discover: true,
|
|
42
|
+
filter: [
|
|
43
|
+
{ type: 'tab', exclude: true },
|
|
44
|
+
{},
|
|
45
|
+
],
|
|
46
|
+
}).wait
|
|
44
47
|
store_existing_targets_for_init
|
|
45
|
-
|
|
48
|
+
rescue => err
|
|
46
49
|
debug_puts(err)
|
|
47
50
|
end
|
|
48
51
|
end
|
|
@@ -62,7 +65,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
62
65
|
autoAttach: true,
|
|
63
66
|
})
|
|
64
67
|
finish_initialization_if_ready
|
|
65
|
-
@initialize_promise.
|
|
68
|
+
@initialize_promise.wait
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
def dispose
|
|
@@ -74,6 +77,14 @@ class Puppeteer::ChromeTargetManager
|
|
|
74
77
|
@attached_targets_by_target_id
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
def wait_for_service_worker_detach(target_id)
|
|
81
|
+
promise = @service_worker_detach_promises[target_id]
|
|
82
|
+
return unless promise
|
|
83
|
+
promise.wait
|
|
84
|
+
ensure
|
|
85
|
+
@service_worker_detach_promises.delete(target_id)
|
|
86
|
+
end
|
|
87
|
+
|
|
77
88
|
def add_target_interceptor(client, interceptor)
|
|
78
89
|
interceptors = @target_interceptors[client] || []
|
|
79
90
|
interceptors << interceptor
|
|
@@ -157,7 +168,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
157
168
|
emit_event(TargetManagerEmittedEvents::TargetChanged, original_target, target_info)
|
|
158
169
|
end
|
|
159
170
|
|
|
160
|
-
class SessionNotCreatedError <
|
|
171
|
+
class SessionNotCreatedError < Puppeteer::Error ; end
|
|
161
172
|
|
|
162
173
|
private def handle_attached_to_target(parent_session, event)
|
|
163
174
|
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
|
|
@@ -167,17 +178,27 @@ class Puppeteer::ChromeTargetManager
|
|
|
167
178
|
raise SessionNotCreatedError.new("Session #{session_id} was not created.")
|
|
168
179
|
end
|
|
169
180
|
|
|
170
|
-
silent_detach = -> {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
silent_detach = ->(detached_promise = nil) {
|
|
182
|
+
Async do
|
|
183
|
+
begin
|
|
184
|
+
Puppeteer::AsyncUtils.await(session.async_send_message('Runtime.runIfWaitingForDebugger'))
|
|
185
|
+
rescue => err
|
|
186
|
+
Logger.new($stderr).warn(err)
|
|
187
|
+
end
|
|
174
188
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
# We don't use `session.detach()` because that dispatches all commands on
|
|
190
|
+
# the connection instead of the parent session.
|
|
191
|
+
begin
|
|
192
|
+
Puppeteer::AsyncUtils.await(parent_session.async_send_message('Target.detachFromTarget', {
|
|
193
|
+
sessionId: session.id,
|
|
194
|
+
}))
|
|
195
|
+
rescue => err
|
|
196
|
+
Logger.new($stderr).warn(err)
|
|
197
|
+
ensure
|
|
198
|
+
if detached_promise && !detached_promise.resolved?
|
|
199
|
+
detached_promise.resolve(true)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
181
202
|
end
|
|
182
203
|
}
|
|
183
204
|
|
|
@@ -192,12 +213,13 @@ class Puppeteer::ChromeTargetManager
|
|
|
192
213
|
# CDP.
|
|
193
214
|
if target_info.type == 'service_worker' && @connection.auto_attached?(target_info.target_id)
|
|
194
215
|
finish_initialization_if_ready(target_info.target_id)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
@service_worker_detach_promises[target_info.target_id] ||= Async::Promise.new
|
|
217
|
+
silent_detach.call(@service_worker_detach_promises[target_info.target_id])
|
|
218
|
+
return if @attached_targets_by_target_id.has_key?(target_info.target_id)
|
|
219
|
+
|
|
220
|
+
target = @target_factory.call(target_info, nil)
|
|
221
|
+
@attached_targets_by_target_id[target_info.target_id] = target
|
|
222
|
+
emit_event(TargetManagerEmittedEvents::TargetAvailable, target)
|
|
201
223
|
|
|
202
224
|
return
|
|
203
225
|
end
|
|
@@ -214,6 +236,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
214
236
|
|
|
215
237
|
target = @attached_targets_by_target_id[target_info.target_id] || @target_factory.call(target_info, session)
|
|
216
238
|
setup_attachment_listeners(session)
|
|
239
|
+
session.target = target
|
|
217
240
|
|
|
218
241
|
@attached_targets_by_target_id[target_info.target_id] ||= target
|
|
219
242
|
@attached_targets_by_session_id[session.id] = target
|
|
@@ -234,29 +257,31 @@ class Puppeteer::ChromeTargetManager
|
|
|
234
257
|
|
|
235
258
|
@target_ids_for_init.delete(target.target_id)
|
|
236
259
|
unless is_existing_target
|
|
237
|
-
|
|
260
|
+
Async do
|
|
261
|
+
Puppeteer::AsyncUtils.future_with_logging { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }.call
|
|
262
|
+
end
|
|
238
263
|
end
|
|
239
264
|
finish_initialization_if_ready
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
session.async_send_message('Runtime.runIfWaitingForDebugger'),
|
|
250
|
-
)
|
|
265
|
+
parent_session.emit_event(CDPSessionEmittedEvents::Ready, session)
|
|
266
|
+
|
|
267
|
+
Async do
|
|
268
|
+
Puppeteer::AsyncUtils.await(session.async_send_message('Target.setAutoAttach', {
|
|
269
|
+
waitForDebuggerOnStart: true,
|
|
270
|
+
flatten: true,
|
|
271
|
+
autoAttach: true,
|
|
272
|
+
}))
|
|
273
|
+
Puppeteer::AsyncUtils.await(session.async_send_message('Runtime.runIfWaitingForDebugger'))
|
|
251
274
|
rescue => err
|
|
252
275
|
Logger.new($stderr).warn(err)
|
|
276
|
+
ensure
|
|
277
|
+
session.mark_ready
|
|
253
278
|
end
|
|
254
279
|
end
|
|
255
280
|
|
|
256
281
|
private def finish_initialization_if_ready(target_id = nil)
|
|
257
282
|
@target_ids_for_init.delete(target_id) if target_id
|
|
258
283
|
if @target_ids_for_init.empty?
|
|
259
|
-
@initialize_promise.
|
|
284
|
+
@initialize_promise.resolve(nil) unless @initialize_promise.resolved?
|
|
260
285
|
end
|
|
261
286
|
end
|
|
262
287
|
|
data/lib/puppeteer/connection.rb
CHANGED
|
@@ -5,7 +5,7 @@ class Puppeteer::Connection
|
|
|
5
5
|
include Puppeteer::EventCallbackable
|
|
6
6
|
using Puppeteer::DefineAsyncMethod
|
|
7
7
|
|
|
8
|
-
class ProtocolError <
|
|
8
|
+
class ProtocolError < Puppeteer::Error
|
|
9
9
|
def initialize(method:, error_message:, error_data: nil)
|
|
10
10
|
msg = "Protocol error (#{method}): #{error_message}"
|
|
11
11
|
if error_data
|
|
@@ -19,14 +19,14 @@ class Puppeteer::Connection
|
|
|
19
19
|
# callback object stored in @callbacks.
|
|
20
20
|
class MessageCallback
|
|
21
21
|
# @param method [String]
|
|
22
|
-
# @param promise [
|
|
22
|
+
# @param promise [Async::Promise]
|
|
23
23
|
def initialize(method:, promise:)
|
|
24
24
|
@method = method
|
|
25
25
|
@promise = promise
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def resolve(result)
|
|
29
|
-
@promise.
|
|
29
|
+
@promise.resolve(result)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def reject(error)
|
|
@@ -36,11 +36,13 @@ class Puppeteer::Connection
|
|
|
36
36
|
attr_reader :method
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def initialize(url, transport, delay = 0)
|
|
39
|
+
def initialize(url, transport, delay = 0, protocol_timeout: nil)
|
|
40
40
|
@url = url
|
|
41
41
|
@last_id = 0
|
|
42
|
-
@callbacks =
|
|
42
|
+
@callbacks = {}
|
|
43
|
+
@callbacks_mutex = Mutex.new
|
|
43
44
|
@delay = delay
|
|
45
|
+
@protocol_timeout = protocol_timeout
|
|
44
46
|
|
|
45
47
|
@transport = transport
|
|
46
48
|
@transport.on_message do |data|
|
|
@@ -56,24 +58,26 @@ class Puppeteer::Connection
|
|
|
56
58
|
handle_close
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
@sessions =
|
|
61
|
+
@sessions = {}
|
|
62
|
+
@sessions_mutex = Mutex.new
|
|
60
63
|
@closed = false
|
|
61
64
|
@manually_attached = Set.new
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
attr_reader :protocol_timeout
|
|
68
|
+
|
|
64
69
|
# used only in Browser#connected?
|
|
65
70
|
def closed?
|
|
66
71
|
@closed
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
private def sleep_before_handling_message(message)
|
|
70
|
-
#
|
|
71
|
-
# So we don't have to sleep.
|
|
75
|
+
# Keep network events ordered without extra delay.
|
|
72
76
|
return if message['method']&.start_with?('Network.')
|
|
73
77
|
|
|
74
78
|
# For some reasons, sleeping a bit reduces trivial errors...
|
|
75
79
|
# 4ms is an interval of internal shared timer of WebKit.
|
|
76
|
-
|
|
80
|
+
Puppeteer::AsyncUtils.sleep_seconds(0.004)
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
private def should_handle_synchronously?(message)
|
|
@@ -83,9 +87,7 @@ class Puppeteer::Connection
|
|
|
83
87
|
when nil
|
|
84
88
|
false
|
|
85
89
|
when /^Network\./
|
|
86
|
-
|
|
87
|
-
# So we don't care their handling order.
|
|
88
|
-
false
|
|
90
|
+
true
|
|
89
91
|
when /^Page\.frame/
|
|
90
92
|
# Page.frameAttached
|
|
91
93
|
# Page.frameNavigated
|
|
@@ -117,7 +119,7 @@ class Puppeteer::Connection
|
|
|
117
119
|
# @param {string} sessionId
|
|
118
120
|
# @return {?CDPSession}
|
|
119
121
|
def session(session_id)
|
|
120
|
-
@sessions[session_id]
|
|
122
|
+
@sessions_mutex.synchronize { @sessions[session_id] }
|
|
121
123
|
end
|
|
122
124
|
|
|
123
125
|
def url
|
|
@@ -127,14 +129,24 @@ class Puppeteer::Connection
|
|
|
127
129
|
# @param {string} method
|
|
128
130
|
# @param {!Object=} params
|
|
129
131
|
def send_message(method, params = {})
|
|
130
|
-
|
|
132
|
+
if @protocol_timeout && @protocol_timeout > 0
|
|
133
|
+
begin
|
|
134
|
+
Puppeteer::AsyncUtils.async_timeout(@protocol_timeout, async_send_message(method, params)).wait
|
|
135
|
+
rescue Async::TimeoutError
|
|
136
|
+
raise ProtocolError.new(method: method, error_message: "Timeout #{@protocol_timeout}ms exceeded")
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
async_send_message(method, params).wait
|
|
140
|
+
end
|
|
131
141
|
end
|
|
132
142
|
|
|
133
143
|
def async_send_message(method, params = {})
|
|
134
|
-
promise =
|
|
144
|
+
promise = Async::Promise.new
|
|
135
145
|
|
|
136
146
|
generate_id do |id|
|
|
137
|
-
@
|
|
147
|
+
@callbacks_mutex.synchronize do
|
|
148
|
+
@callbacks[id] = MessageCallback.new(method: method, promise: promise)
|
|
149
|
+
end
|
|
138
150
|
raw_send(id: id, message: { method: method, params: params })
|
|
139
151
|
end
|
|
140
152
|
|
|
@@ -163,7 +175,7 @@ class Puppeteer::Connection
|
|
|
163
175
|
#
|
|
164
176
|
# So we have to know the message id in advance before send_text.
|
|
165
177
|
#
|
|
166
|
-
payload = JSON.
|
|
178
|
+
payload = JSON.generate(message.compact.merge(id: id))
|
|
167
179
|
@transport.send_text(payload)
|
|
168
180
|
request_debug_printer.handle_payload(payload)
|
|
169
181
|
end
|
|
@@ -194,7 +206,6 @@ class Puppeteer::Connection
|
|
|
194
206
|
'Network.responseReceived',
|
|
195
207
|
'Network.responseReceivedExtraInfo',
|
|
196
208
|
'Page.lifecycleEvent',
|
|
197
|
-
'Target.receivedMessageFromTarget', # only Firefox
|
|
198
209
|
]
|
|
199
210
|
|
|
200
211
|
def handle_message(message)
|
|
@@ -236,7 +247,7 @@ class Puppeteer::Connection
|
|
|
236
247
|
|
|
237
248
|
private def handle_message(message)
|
|
238
249
|
if @delay > 0
|
|
239
|
-
|
|
250
|
+
Puppeteer::AsyncUtils.sleep_seconds(@delay / 1000.0)
|
|
240
251
|
end
|
|
241
252
|
|
|
242
253
|
response_debug_printer.handle_message(message)
|
|
@@ -244,22 +255,28 @@ class Puppeteer::Connection
|
|
|
244
255
|
case message['method']
|
|
245
256
|
when 'Target.attachedToTarget'
|
|
246
257
|
session_id = message['params']['sessionId']
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
parent_session =
|
|
259
|
+
if message['sessionId']
|
|
260
|
+
@sessions_mutex.synchronize { @sessions[message['sessionId']] }
|
|
261
|
+
end
|
|
262
|
+
session = Puppeteer::CDPSession.new(
|
|
263
|
+
self,
|
|
264
|
+
message['params']['targetInfo']['type'],
|
|
265
|
+
session_id,
|
|
266
|
+
parent_session: parent_session,
|
|
267
|
+
)
|
|
268
|
+
@sessions_mutex.synchronize { @sessions[session_id] = session }
|
|
249
269
|
emit_event('sessionattached', session)
|
|
250
|
-
|
|
251
|
-
parent_session = @sessions[message['sessionId']]
|
|
252
|
-
parent_session&.emit_event('sessionattached', session)
|
|
253
|
-
end
|
|
270
|
+
parent_session&.emit_event('sessionattached', session)
|
|
254
271
|
when 'Target.detachedFromTarget'
|
|
255
272
|
session_id = message['params']['sessionId']
|
|
256
|
-
session = @sessions[session_id]
|
|
273
|
+
session = @sessions_mutex.synchronize { @sessions[session_id] }
|
|
257
274
|
if session
|
|
258
275
|
session.handle_closed
|
|
259
|
-
@sessions.delete(session_id)
|
|
276
|
+
@sessions_mutex.synchronize { @sessions.delete(session_id) }
|
|
260
277
|
emit_event('sessiondetached', session)
|
|
261
278
|
if message['sessionId']
|
|
262
|
-
parent_session = @sessions[message['sessionId']]
|
|
279
|
+
parent_session = @sessions_mutex.synchronize { @sessions[message['sessionId']] }
|
|
263
280
|
parent_session&.emit_event('sessiondetached', session)
|
|
264
281
|
end
|
|
265
282
|
end
|
|
@@ -267,10 +284,10 @@ class Puppeteer::Connection
|
|
|
267
284
|
|
|
268
285
|
if message['sessionId']
|
|
269
286
|
session_id = message['sessionId']
|
|
270
|
-
@sessions[session_id]&.handle_message(message)
|
|
287
|
+
@sessions_mutex.synchronize { @sessions[session_id] }&.handle_message(message)
|
|
271
288
|
elsif message['id']
|
|
272
289
|
# Callbacks could be all rejected if someone has called `.dispose()`.
|
|
273
|
-
if callback = @callbacks.delete(message['id'])
|
|
290
|
+
if callback = @callbacks_mutex.synchronize { @callbacks.delete(message['id']) }
|
|
274
291
|
if message['error']
|
|
275
292
|
callback.reject(
|
|
276
293
|
ProtocolError.new(
|
|
@@ -293,17 +310,19 @@ class Puppeteer::Connection
|
|
|
293
310
|
@closed = true
|
|
294
311
|
@transport.on_message
|
|
295
312
|
@transport.on_close
|
|
296
|
-
@
|
|
313
|
+
callbacks = @callbacks_mutex.synchronize do
|
|
314
|
+
@callbacks.values.tap { @callbacks.clear }
|
|
315
|
+
end
|
|
316
|
+
callbacks.each do |callback|
|
|
297
317
|
callback.reject(
|
|
298
318
|
ProtocolError.new(
|
|
299
319
|
method: callback.method,
|
|
300
320
|
error_message: 'Target Closed.'))
|
|
301
321
|
end
|
|
302
|
-
@
|
|
303
|
-
|
|
304
|
-
session.handle_closed
|
|
322
|
+
sessions = @sessions_mutex.synchronize do
|
|
323
|
+
@sessions.values.tap { @sessions.clear }
|
|
305
324
|
end
|
|
306
|
-
|
|
325
|
+
sessions.each(&:handle_closed)
|
|
307
326
|
emit_event(ConnectionEmittedEvents::Disconnected)
|
|
308
327
|
end
|
|
309
328
|
|
|
@@ -333,6 +352,6 @@ class Puppeteer::Connection
|
|
|
333
352
|
result = send_message('Target.attachToTarget', targetId: target_info.target_id, flatten: true)
|
|
334
353
|
session_id = result['sessionId']
|
|
335
354
|
@manually_attached.delete(target_info.target_id)
|
|
336
|
-
@sessions[session_id]
|
|
355
|
+
@sessions_mutex.synchronize { @sessions[session_id] }.tap { |session| session&.mark_ready }
|
|
337
356
|
end
|
|
338
357
|
end
|
|
@@ -17,7 +17,15 @@ class Puppeteer::ConsoleMessage
|
|
|
17
17
|
@log_type = log_type
|
|
18
18
|
@text = text
|
|
19
19
|
@args = args
|
|
20
|
-
@stack_trace_locations =
|
|
20
|
+
@stack_trace_locations =
|
|
21
|
+
case stack_trace_locations
|
|
22
|
+
when nil
|
|
23
|
+
[]
|
|
24
|
+
when Array
|
|
25
|
+
stack_trace_locations
|
|
26
|
+
else
|
|
27
|
+
[stack_trace_locations]
|
|
28
|
+
end
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
attr_reader :log_type, :text, :args
|