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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. 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(for_firefox, executable_path, process_arguments, user_data_dir, using_temp_user_data_dir)
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 < StandardError
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(browser_ws_endpoint, transport, slow_mo)
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
- Timeout.timeout(timeout / 1000.0) do
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 Timeout::Error
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 < StandardError; end
8
+ class Error < Puppeteer::Error; end
7
9
 
8
- # @param {!Connection} connection
9
- # @param {string} targetType
10
- # @param {string} sessionId
11
- def initialize(connection, target_type, session_id)
12
- @callbacks = Concurrent::Hash.new
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
- # @internal
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
- # @param method [String]
26
- # @param params [Hash]
27
- # @returns [Hash]
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
- await async_send_message(method, params)
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
- # @param method [String]
33
- # @param params [Hash]
34
- # @returns [Future<Hash>]
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 = resolvable_future
72
+ promise = Async::Promise.new
41
73
 
42
74
  @connection.generate_id do |id|
43
- @callbacks[id] = Puppeteer::Connection::MessageCallback.new(method: method, promise: promise)
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
- # @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
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
- @callbacks.each_value do |callback|
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
- @callbacks.clear
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
- # @param event_name [String]
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
- # @param event_name [String]
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 = resolvable_future
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
- @connection.async_send_message('Target.setDiscoverTargets', {
38
- discover: true,
39
- filter: [
40
- { type: 'tab', exclude: true },
41
- {},
42
- ],
43
- }).then do
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
- end.rescue do |err|
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.value!
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 < StandardError ; end
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
- session.async_send_message('Runtime.runIfWaitingForDebugger').rescue do |err|
172
- Logger.new($stderr).warn(err)
173
- end
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
- # We don't use `session.detach()` because that dispatches all commands on
176
- # the connection instead of the parent session.
177
- parent_session.async_send_message('Target.detachFromTarget', {
178
- sessionId: session.id,
179
- }).rescue do |err|
180
- Logger.new($stderr).warn(err)
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
- silent_detach.call
196
- if parent_session.is_a?(Puppeteer::CDPSession)
197
- target = @target_factory.call(target_info, parent_session)
198
- @attached_targets_by_target_id[target_info.target_id] = target
199
- emit_event(TargetManagerEmittedEvents::TargetAvailable, target)
200
- end
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
- future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
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
- future do
242
- # TODO: the browser might be shutting down here. What do we do with the error?
243
- await_all(
244
- session.async_send_message('Target.setAutoAttach', {
245
- waitForDebuggerOnStart: true,
246
- flatten: true,
247
- autoAttach: true,
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.fulfill(nil) unless @initialize_promise.resolved?
284
+ @initialize_promise.resolve(nil) unless @initialize_promise.resolved?
260
285
  end
261
286
  end
262
287
 
@@ -5,7 +5,7 @@ class Puppeteer::Connection
5
5
  include Puppeteer::EventCallbackable
6
6
  using Puppeteer::DefineAsyncMethod
7
7
 
8
- class ProtocolError < StandardError
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 [Concurrent::Promises::ResolvableFuture]
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.fulfill(result)
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 = Concurrent::Hash.new
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 = Concurrent::Hash.new
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
- # Puppeteer doesn't handle any Network monitoring responses.
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
- sleep 0.004
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
- # Puppeteer doesn't handle any Network monitoring responses.
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
- await async_send_message(method, params)
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 = resolvable_future
144
+ promise = Async::Promise.new
135
145
 
136
146
  generate_id do |id|
137
- @callbacks[id] = MessageCallback.new(method: method, promise: promise)
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.fast_generate(message.compact.merge(id: id))
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
- sleep(@delay / 1000.0)
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
- session = Puppeteer::CDPSession.new(self, message['params']['targetInfo']['type'], session_id)
248
- @sessions[session_id] = session
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
- if message['sessionId']
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
- @callbacks.each_value do |callback|
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
- @callbacks.clear
303
- @sessions.each_value do |session|
304
- session.handle_closed
322
+ sessions = @sessions_mutex.synchronize do
323
+ @sessions.values.tap { @sessions.clear }
305
324
  end
306
- @sessions.clear
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 = 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