puppeteer-ruby 0.0.2
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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +7 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +15 -0
- data/example.rb +7 -0
- data/lib/puppeteer.rb +192 -0
- data/lib/puppeteer/async_await_behavior.rb +34 -0
- data/lib/puppeteer/browser.rb +240 -0
- data/lib/puppeteer/browser_context.rb +90 -0
- data/lib/puppeteer/browser_fetcher.rb +6 -0
- data/lib/puppeteer/browser_runner.rb +142 -0
- data/lib/puppeteer/cdp_session.rb +78 -0
- data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
- data/lib/puppeteer/connection.rb +254 -0
- data/lib/puppeteer/console_message.rb +24 -0
- data/lib/puppeteer/debug_print.rb +20 -0
- data/lib/puppeteer/device.rb +12 -0
- data/lib/puppeteer/devices.rb +885 -0
- data/lib/puppeteer/dom_world.rb +447 -0
- data/lib/puppeteer/element_handle.rb +433 -0
- data/lib/puppeteer/emulation_manager.rb +46 -0
- data/lib/puppeteer/errors.rb +4 -0
- data/lib/puppeteer/event_callbackable.rb +88 -0
- data/lib/puppeteer/execution_context.rb +230 -0
- data/lib/puppeteer/frame.rb +278 -0
- data/lib/puppeteer/frame_manager.rb +380 -0
- data/lib/puppeteer/if_present.rb +18 -0
- data/lib/puppeteer/js_handle.rb +142 -0
- data/lib/puppeteer/keyboard.rb +183 -0
- data/lib/puppeteer/keyboard/key_description.rb +19 -0
- data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
- data/lib/puppeteer/launcher.rb +26 -0
- data/lib/puppeteer/launcher/base.rb +48 -0
- data/lib/puppeteer/launcher/browser_options.rb +41 -0
- data/lib/puppeteer/launcher/chrome.rb +165 -0
- data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
- data/lib/puppeteer/launcher/launch_options.rb +68 -0
- data/lib/puppeteer/lifecycle_watcher.rb +168 -0
- data/lib/puppeteer/mouse.rb +120 -0
- data/lib/puppeteer/network_manager.rb +122 -0
- data/lib/puppeteer/page.rb +1001 -0
- data/lib/puppeteer/page/screenshot_options.rb +78 -0
- data/lib/puppeteer/remote_object.rb +124 -0
- data/lib/puppeteer/target.rb +150 -0
- data/lib/puppeteer/timeout_settings.rb +15 -0
- data/lib/puppeteer/touch_screen.rb +43 -0
- data/lib/puppeteer/version.rb +3 -0
- data/lib/puppeteer/viewport.rb +36 -0
- data/lib/puppeteer/wait_task.rb +6 -0
- data/lib/puppeteer/web_socket.rb +117 -0
- data/lib/puppeteer/web_socket_transport.rb +49 -0
- data/puppeteer-ruby.gemspec +29 -0
- metadata +213 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
class Puppeteer::BrowserContext
|
2
|
+
include Puppeteer::EventCallbackable
|
3
|
+
|
4
|
+
# @param {!Puppeteer.Connection} connection
|
5
|
+
# @param {!Browser} browser
|
6
|
+
# @param {?string} contextId
|
7
|
+
def initialize(connection, browser, context_id)
|
8
|
+
@connection = connection
|
9
|
+
@browser = browser
|
10
|
+
@id = context_id
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return {!Array<!Target>} target
|
14
|
+
def targets
|
15
|
+
@browser.targets.select{ |target| target.browser_context == self }
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param {function(!Target):boolean} predicate
|
19
|
+
# @param {{timeout?: number}=} options
|
20
|
+
# @return {!Promise<!Target>}
|
21
|
+
def wait_for_target(predicate:, timeout: nil)
|
22
|
+
@browser.wait_for_target(
|
23
|
+
predicate: ->(target) { target.browser_context == self && predicate.call(target) },
|
24
|
+
timeout: timeout,
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return {!Promise<!Array<!Puppeteer.Page>>}
|
29
|
+
def pages
|
30
|
+
targets.select{ |target| target.type == 'page' }.map(&:page).reject{ |page| !page }
|
31
|
+
end
|
32
|
+
|
33
|
+
def incognito?
|
34
|
+
!@id
|
35
|
+
end
|
36
|
+
|
37
|
+
# /**
|
38
|
+
# * @param {string} origin
|
39
|
+
# * @param {!Array<string>} permissions
|
40
|
+
# */
|
41
|
+
# async overridePermissions(origin, permissions) {
|
42
|
+
# const webPermissionToProtocol = new Map([
|
43
|
+
# ['geolocation', 'geolocation'],
|
44
|
+
# ['midi', 'midi'],
|
45
|
+
# ['notifications', 'notifications'],
|
46
|
+
# ['push', 'push'],
|
47
|
+
# ['camera', 'videoCapture'],
|
48
|
+
# ['microphone', 'audioCapture'],
|
49
|
+
# ['background-sync', 'backgroundSync'],
|
50
|
+
# ['ambient-light-sensor', 'sensors'],
|
51
|
+
# ['accelerometer', 'sensors'],
|
52
|
+
# ['gyroscope', 'sensors'],
|
53
|
+
# ['magnetometer', 'sensors'],
|
54
|
+
# ['accessibility-events', 'accessibilityEvents'],
|
55
|
+
# ['clipboard-read', 'clipboardRead'],
|
56
|
+
# ['clipboard-write', 'clipboardWrite'],
|
57
|
+
# ['payment-handler', 'paymentHandler'],
|
58
|
+
# // chrome-specific permissions we have.
|
59
|
+
# ['midi-sysex', 'midiSysex'],
|
60
|
+
# ]);
|
61
|
+
# permissions = permissions.map(permission => {
|
62
|
+
# const protocolPermission = webPermissionToProtocol.get(permission);
|
63
|
+
# if (!protocolPermission)
|
64
|
+
# throw new Error('Unknown permission: ' + permission);
|
65
|
+
# return protocolPermission;
|
66
|
+
# });
|
67
|
+
# await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._id || undefined, permissions});
|
68
|
+
# }
|
69
|
+
|
70
|
+
# async clearPermissionOverrides() {
|
71
|
+
# await this._connection.send('Browser.resetPermissions', {browserContextId: this._id || undefined});
|
72
|
+
# }
|
73
|
+
|
74
|
+
# @return [Future<Puppeteer::Page>]
|
75
|
+
def new_page
|
76
|
+
@browser.create_page_in_context(@id)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Browser]
|
80
|
+
def browser
|
81
|
+
@browser
|
82
|
+
end
|
83
|
+
|
84
|
+
def close
|
85
|
+
if !@id
|
86
|
+
raise 'Non-incognito profiles cannot be closed!'
|
87
|
+
end
|
88
|
+
@browser.dispose_context(@id)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'open3'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
# https://github.com/puppeteer/puppeteer/blob/master/lib/Launcher.js
|
6
|
+
class Puppeteer::BrowserRunner
|
7
|
+
# @param {string} executablePath
|
8
|
+
# @param {!Array<string>} processArguments
|
9
|
+
# @param {string=} tempDirectory
|
10
|
+
def initialize(executable_path, process_arguments, temp_directory)
|
11
|
+
@executable_path = executable_path
|
12
|
+
@process_arguments = process_arguments
|
13
|
+
@temp_directory = temp_directory
|
14
|
+
@proc = nil
|
15
|
+
@connection = nil
|
16
|
+
@closed = true
|
17
|
+
@listeners = []
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :proc, :connection
|
21
|
+
|
22
|
+
class BrowserProcess
|
23
|
+
def initialize(env, executable_path, args)
|
24
|
+
stdin, @stdout, @stderr, @thread = Open3.popen3(env, executable_path, *args)
|
25
|
+
stdin.close
|
26
|
+
end
|
27
|
+
|
28
|
+
def dispose
|
29
|
+
[@stdout, @stderr].each{ |io| io.close unless io.closed? }
|
30
|
+
@thread.join
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :stdout, :stderr
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param {!(Launcher.LaunchOptions)=} options
|
37
|
+
def start(
|
38
|
+
executable_path: nil,
|
39
|
+
ignore_default_args: nil,
|
40
|
+
handle_SIGINT: nil,
|
41
|
+
handle_SIGTERM: nil,
|
42
|
+
handle_SIGHUP: nil,
|
43
|
+
timeout: nil,
|
44
|
+
dumpio: nil,
|
45
|
+
env: nil,
|
46
|
+
pipe: nil
|
47
|
+
)
|
48
|
+
@launch_options = Puppeteer::Launcher::LaunchOptions.new({
|
49
|
+
executable_path: executable_path,
|
50
|
+
ignore_default_args: ignore_default_args,
|
51
|
+
handle_SIGINT: handle_SIGINT,
|
52
|
+
handle_SIGTERM: handle_SIGTERM,
|
53
|
+
handle_SIGHUP: handle_SIGHUP,
|
54
|
+
timeout: timeout,
|
55
|
+
dumpio: dumpio,
|
56
|
+
env: env,
|
57
|
+
pipe: pipe,
|
58
|
+
}.compact)
|
59
|
+
@proc = BrowserProcess.new(
|
60
|
+
@launch_options.env,
|
61
|
+
@executable_path,
|
62
|
+
@process_arguments,
|
63
|
+
)
|
64
|
+
# if (dumpio) {
|
65
|
+
# this.proc.stderr.pipe(process.stderr);
|
66
|
+
# this.proc.stdout.pipe(process.stdout);
|
67
|
+
# }
|
68
|
+
@closed = false
|
69
|
+
@process_closing = -> {
|
70
|
+
@proc.dispose
|
71
|
+
@closed = true
|
72
|
+
if @temp_directory
|
73
|
+
FileUtils.rm_rf(@temp_directory)
|
74
|
+
end
|
75
|
+
}
|
76
|
+
trap(:EXIT) do
|
77
|
+
kill
|
78
|
+
end
|
79
|
+
|
80
|
+
if @launch_options.handle_SIGINT?
|
81
|
+
trap(:INT) do
|
82
|
+
kill
|
83
|
+
exit 130
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if @launch_options.handle_SIGTERM?
|
88
|
+
trap(:TERM) do
|
89
|
+
close
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if @launch_options.handle_SIGHUP?
|
94
|
+
trap(:HUP) do
|
95
|
+
close
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return {Promise}
|
101
|
+
def close
|
102
|
+
return if @closed
|
103
|
+
|
104
|
+
if @temp_directory
|
105
|
+
kill
|
106
|
+
elsif @connection
|
107
|
+
@connection.sendCommand("Browser.close")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return {Promise}
|
112
|
+
def kill
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
|
117
|
+
# @return {!Promise<!Connection>}
|
118
|
+
def setup_connection(use_pipe:, timeout:, slow_mo:, preferred_revision:)
|
119
|
+
if !use_pipe
|
120
|
+
browser_ws_endpoint = wait_for_ws_endpoint(@proc, timeout, preferred_revision)
|
121
|
+
transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
|
122
|
+
@connection = Puppeteer::Connection.new(browser_ws_endpoint, transport, slow_mo)
|
123
|
+
else
|
124
|
+
raise NotImplementedError.new("PipeTransport is not yet implemented")
|
125
|
+
end
|
126
|
+
|
127
|
+
@connection
|
128
|
+
end
|
129
|
+
|
130
|
+
private def wait_for_ws_endpoint(browser_process, timeout, preferred_revision)
|
131
|
+
Timeout.timeout(timeout / 1000) do
|
132
|
+
loop do
|
133
|
+
line = browser_process.stderr.readline
|
134
|
+
/^DevTools listening on (ws:\/\/.*)$/.match(line) do |m|
|
135
|
+
return m[1]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
rescue Timeout::Error
|
140
|
+
raise Puppeteer::TimeoutError.new("Timed out after #{timeout} ms while trying to connect to the browser! Only Chrome at revision r#{preferredRevision} is guaranteed to work.")
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Puppeteer::CDPSession
|
2
|
+
include Puppeteer::EventCallbackable
|
3
|
+
using Puppeteer::AsyncAwaitBehavior
|
4
|
+
|
5
|
+
class Error < StandardError ; end
|
6
|
+
|
7
|
+
# @param {!Connection} connection
|
8
|
+
# @param {string} targetType
|
9
|
+
# @param {string} sessionId
|
10
|
+
def initialize(connection, target_type, session_id)
|
11
|
+
@callbacks = {}
|
12
|
+
@connection = connection
|
13
|
+
@target_type = target_type
|
14
|
+
@session_id = session_id
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :connection
|
18
|
+
|
19
|
+
# @param method [String]
|
20
|
+
# @param params [Hash]
|
21
|
+
# @returns [Hash]
|
22
|
+
def send_message(method, params = {})
|
23
|
+
await async_send_message(method, params)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param method [String]
|
27
|
+
# @param params [Hash]
|
28
|
+
# @returns [Future<Hash>]
|
29
|
+
def async_send_message(method, params = {})
|
30
|
+
if !@connection
|
31
|
+
raise Error.new("Protocol error (#{method}): Session closed. Most likely the #{@target_type} has been closed.")
|
32
|
+
end
|
33
|
+
id = @connection.raw_send(message: { sessionId: @session_id, method: method, params: params })
|
34
|
+
promise = resolvable_future
|
35
|
+
@callbacks[id] = Puppeteer::Connection::MessageCallback.new(method: method, promise: promise)
|
36
|
+
promise
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
|
40
|
+
def handle_message(message)
|
41
|
+
if message['id']
|
42
|
+
if callback = @callbacks.delete(message['id'])
|
43
|
+
if message['error']
|
44
|
+
callback.reject(
|
45
|
+
Puppeteer::Connection::ProtocolError.new(
|
46
|
+
method: callback.method,
|
47
|
+
error_message: response['error']['message'],
|
48
|
+
error_data: response['error']['data']))
|
49
|
+
else
|
50
|
+
callback.resolve(message['result'])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
raise Error.new("unknown id: #{message['id']}")
|
54
|
+
end
|
55
|
+
else
|
56
|
+
emit_event message['method'], message['params']
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def detach
|
61
|
+
if !@connection
|
62
|
+
raise Error.new("Session already detarched. Most likely the #{@target_type} has been closed.")
|
63
|
+
end
|
64
|
+
@connection.send_message('Target.detachFromTarget', sessionId: @session_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_closed
|
68
|
+
@callbacks.values.each do |callback|
|
69
|
+
callback.reject(
|
70
|
+
Puppeteer::Connection::ProtocolError.new(
|
71
|
+
method: callback.method,
|
72
|
+
error_message: 'Target Closed.'))
|
73
|
+
end
|
74
|
+
@callbacks.clear
|
75
|
+
@connection = nil
|
76
|
+
emit_event 'Events.CDPSession.Disconnected'
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# utility methods for Concurrent::Promises.
|
2
|
+
module Puppeteer::ConcurrentRubyUtils
|
3
|
+
def await_all(*args)
|
4
|
+
if args.length == 1 && args[0].is_a?(Enumerable)
|
5
|
+
Concurrent::Promises.zip(*(args[0])).value!
|
6
|
+
else
|
7
|
+
Concurrent::Promises.zip(*args).value!
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def await_any(*args)
|
12
|
+
if args.length == 1 && args[0].is_a?(Enumerable)
|
13
|
+
Concurrent::Promises.any(*(args[0])).value!
|
14
|
+
else
|
15
|
+
Concurrent::Promises.any(*args).value!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# blocking get value of Future.
|
20
|
+
def await(future_or_value)
|
21
|
+
if future_or_value.is_a?(Concurrent::Promises::Future)
|
22
|
+
future_or_value.value!
|
23
|
+
else
|
24
|
+
future_or_value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def future(&block)
|
29
|
+
Concurrent::Promises.future(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def resolvable_future
|
33
|
+
Concurrent::Promises.resolvable_future
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
include Puppeteer::ConcurrentRubyUtils
|
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Puppeteer::Connection
|
4
|
+
include Puppeteer::DebugPrint
|
5
|
+
include Puppeteer::EventCallbackable
|
6
|
+
using Puppeteer::AsyncAwaitBehavior
|
7
|
+
|
8
|
+
class ProtocolError < StandardError
|
9
|
+
def initialize(method:, error_message:, error_data: nil)
|
10
|
+
msg = "Protocol error (#{method}): #{error_message}"
|
11
|
+
if error_data
|
12
|
+
super("#{msg} #{error_data}")
|
13
|
+
else
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# callback object stored in @callbacks.
|
20
|
+
class MessageCallback
|
21
|
+
# @param method [String]
|
22
|
+
# @param promise [Concurrent::Promises::ResolvableFuture]
|
23
|
+
def initialize(method:, promise:)
|
24
|
+
@method = method
|
25
|
+
@promise = promise
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve(result)
|
29
|
+
@promise.fulfill(result)
|
30
|
+
end
|
31
|
+
|
32
|
+
def reject(error)
|
33
|
+
@promise.reject(error)
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :method
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(url, transport, delay = 0)
|
40
|
+
@url = url
|
41
|
+
@last_id = 0
|
42
|
+
@callbacks = {}
|
43
|
+
@delay = delay
|
44
|
+
|
45
|
+
@transport = transport
|
46
|
+
@transport.on_message do |data|
|
47
|
+
async_handle_message(JSON.parse(data))
|
48
|
+
end
|
49
|
+
@transport.on_close do |reason, code|
|
50
|
+
handle_close(reason, code)
|
51
|
+
end
|
52
|
+
|
53
|
+
@sessions = {}
|
54
|
+
@closed = false
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.from_session(session)
|
58
|
+
session.connection
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param {string} sessionId
|
62
|
+
# @return {?CDPSession}
|
63
|
+
def session(session_id)
|
64
|
+
@sessions[session_id]
|
65
|
+
end
|
66
|
+
|
67
|
+
def url
|
68
|
+
@url
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param {string} method
|
72
|
+
# @param {!Object=} params
|
73
|
+
def send_message(method, params = {})
|
74
|
+
await async_send_message(method, params)
|
75
|
+
end
|
76
|
+
|
77
|
+
def async_send_message(method, params = {})
|
78
|
+
id = raw_send(message: { method: method, params: params })
|
79
|
+
promise = resolvable_future
|
80
|
+
@callbacks[id] = MessageCallback.new(method: method, promise: promise)
|
81
|
+
promise
|
82
|
+
end
|
83
|
+
|
84
|
+
private def generate_id
|
85
|
+
@last_id += 1
|
86
|
+
end
|
87
|
+
|
88
|
+
def raw_send(message:)
|
89
|
+
id = generate_id
|
90
|
+
payload = JSON.fast_generate(message.compact.merge(id: id))
|
91
|
+
@transport.send_text(payload)
|
92
|
+
request_debug_printer.handle_payload(payload)
|
93
|
+
id
|
94
|
+
end
|
95
|
+
|
96
|
+
# Just for effective debugging :)
|
97
|
+
class RequestDebugPrinter
|
98
|
+
include Puppeteer::DebugPrint
|
99
|
+
|
100
|
+
def handle_payload(payload)
|
101
|
+
debug_puts "SEND >> #{decorate(payload)}"
|
102
|
+
end
|
103
|
+
|
104
|
+
private def decorate(payload)
|
105
|
+
payload.gsub(/"method":"([^"]+)"/, "\"method\":\"\u001b[32m\\1\u001b[0m\"")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class ResponseDebugPrinter
|
110
|
+
include Puppeteer::DebugPrint
|
111
|
+
|
112
|
+
NON_DEBUG_PRINT_METHODS = [
|
113
|
+
'Network.dataReceived',
|
114
|
+
'Network.loadingFinished',
|
115
|
+
'Network.requestWillBeSent',
|
116
|
+
'Network.requestWillBeSentExtraInfo',
|
117
|
+
'Network.responseReceived',
|
118
|
+
'Network.responseReceivedExtraInfo',
|
119
|
+
'Page.lifecycleEvent',
|
120
|
+
]
|
121
|
+
|
122
|
+
def handle_message(message)
|
123
|
+
if skip_debug_print?(message['method'])
|
124
|
+
debug_print "."
|
125
|
+
@prev_log_skipped = true
|
126
|
+
else
|
127
|
+
debug_print "\n" if @prev_log_skipped
|
128
|
+
@prev_log_skipped = nil
|
129
|
+
debug_puts "RECV << #{decorate(message)}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private def skip_debug_print?(method)
|
134
|
+
method && NON_DEBUG_PRINT_METHODS.include?(method)
|
135
|
+
end
|
136
|
+
|
137
|
+
private def decorate(message)
|
138
|
+
# decorate RED for error.
|
139
|
+
if message['error']
|
140
|
+
return "\u001b[31m#{message}\u001b[0m"
|
141
|
+
end
|
142
|
+
|
143
|
+
# ignore method call response, or with no method.
|
144
|
+
return message if message['id'] || !message['method']
|
145
|
+
|
146
|
+
# decorate cyan for method name.
|
147
|
+
message.to_s.gsub(message['method'], "\u001b[36m#{message['method']}\u001b[0m")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
private def request_debug_printer
|
152
|
+
@request_debug_printer ||= RequestDebugPrinter.new
|
153
|
+
end
|
154
|
+
|
155
|
+
private def response_debug_printer
|
156
|
+
@response_debug_printer ||= ResponseDebugPrinter.new
|
157
|
+
end
|
158
|
+
|
159
|
+
private def handle_message(message)
|
160
|
+
if @delay > 0
|
161
|
+
sleep(@delay / 1000.0)
|
162
|
+
end
|
163
|
+
|
164
|
+
response_debug_printer.handle_message(message)
|
165
|
+
|
166
|
+
case message['method']
|
167
|
+
when 'Target.attachedToTarget'
|
168
|
+
session_id = message['params']['sessionId']
|
169
|
+
session = Puppeteer::CDPSession.new(self, message['params']['targetInfo']['type'], session_id)
|
170
|
+
@sessions[session_id] = session
|
171
|
+
when 'Target.detachedFromTarget'
|
172
|
+
session_id = message['params']['sessionId']
|
173
|
+
session = @sessions[session_id]
|
174
|
+
if session
|
175
|
+
session._onClosed
|
176
|
+
@sessions.delete(session_id)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
if message['sessionId']
|
181
|
+
session_id = message['sessionId']
|
182
|
+
@sessions[session_id]&.handle_message(message)
|
183
|
+
elsif message['id']
|
184
|
+
# Callbacks could be all rejected if someone has called `.dispose()`.
|
185
|
+
if callback = @callbacks.delete(message['id'])
|
186
|
+
if message['error']
|
187
|
+
callback.reject(
|
188
|
+
ProtocolError.new(
|
189
|
+
method: callback.method,
|
190
|
+
error_message: response['error']['message'],
|
191
|
+
error_data: response['error']['data']))
|
192
|
+
else
|
193
|
+
callback.resolve(message["result"])
|
194
|
+
end
|
195
|
+
end
|
196
|
+
else
|
197
|
+
emit_event message['method'], message['params']
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
private async def async_handle_message(message)
|
202
|
+
handle_message(message)
|
203
|
+
end
|
204
|
+
|
205
|
+
private def handle_close
|
206
|
+
return if @closed
|
207
|
+
@closed = true
|
208
|
+
@transport.on_message
|
209
|
+
@transport.on_close
|
210
|
+
@callbacks.values.each do |callback|
|
211
|
+
callback.reject(
|
212
|
+
ProtocolError.new(
|
213
|
+
method: callback.method,
|
214
|
+
error_message: 'Target Closed.'))
|
215
|
+
end
|
216
|
+
@callbacks.clear
|
217
|
+
@sessions.values.each do |session|
|
218
|
+
session.handle_closed
|
219
|
+
end
|
220
|
+
@sessions.clear
|
221
|
+
emit_event 'Events.Connection.Disconnected'
|
222
|
+
end
|
223
|
+
|
224
|
+
def on_close(&block)
|
225
|
+
@on_close = block
|
226
|
+
end
|
227
|
+
|
228
|
+
def on_message(&block)
|
229
|
+
@on_message = block
|
230
|
+
end
|
231
|
+
|
232
|
+
def dispose
|
233
|
+
handle_close
|
234
|
+
@transport.close
|
235
|
+
end
|
236
|
+
|
237
|
+
# @param {Protocol.Target.TargetInfo} targetInfo
|
238
|
+
# @return [CDPSession]
|
239
|
+
def create_session(target_info)
|
240
|
+
result = send_message('Target.attachToTarget', targetId: target_info.target_id, flatten: true)
|
241
|
+
session_id = result['sessionId']
|
242
|
+
|
243
|
+
# Target.attachedToTarget is often notified after the result of Target.attachToTarget.
|
244
|
+
# D, [2020-04-04T23:04:30.736311 #91875] DEBUG -- : RECV << {"id"=>2, "result"=>{"sessionId"=>"DA002F8A95B04710502CB40D8430B95A"}}
|
245
|
+
# D, [2020-04-04T23:04:30.736649 #91875] DEBUG -- : RECV << {"method"=>"Target.attachedToTarget", "params"=>{"sessionId"=>"DA002F8A95B04710502CB40D8430B95A", "targetInfo"=>{"targetId"=>"EBAB949A7DE63F12CB94268AD3A9976B", "type"=>"page", "title"=>"about:blank", "url"=>"about:blank", "attached"=>true, "browserContextId"=>"46D23767E9B79DD9E589101121F6DADD"}, "waitingForDebugger"=>false}}
|
246
|
+
# So we have to wait for "Target.attachedToTarget" a bit.
|
247
|
+
20.times do
|
248
|
+
if @sessions[session_id]
|
249
|
+
return @sessions[session_id]
|
250
|
+
end
|
251
|
+
sleep 0.1
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|