puppeteer-ruby 0.0.10

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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +71 -0
  3. data/.github/stale.yml +16 -0
  4. data/.gitignore +19 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +302 -0
  7. data/.travis.yml +7 -0
  8. data/Dockerfile +6 -0
  9. data/Gemfile +6 -0
  10. data/README.md +54 -0
  11. data/Rakefile +1 -0
  12. data/bin/console +11 -0
  13. data/bin/setup +8 -0
  14. data/docker-compose.yml +15 -0
  15. data/docs/Puppeteer.html +2020 -0
  16. data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
  17. data/docs/Puppeteer/Browser.html +2148 -0
  18. data/docs/Puppeteer/BrowserContext.html +809 -0
  19. data/docs/Puppeteer/BrowserFetcher.html +214 -0
  20. data/docs/Puppeteer/BrowserRunner.html +914 -0
  21. data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
  22. data/docs/Puppeteer/CDPSession.html +813 -0
  23. data/docs/Puppeteer/CDPSession/Error.html +124 -0
  24. data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
  25. data/docs/Puppeteer/Connection.html +960 -0
  26. data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
  27. data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
  28. data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
  29. data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
  30. data/docs/Puppeteer/ConsoleMessage.html +565 -0
  31. data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
  32. data/docs/Puppeteer/DOMWorld.html +2219 -0
  33. data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
  34. data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
  35. data/docs/Puppeteer/DebugPrint.html +233 -0
  36. data/docs/Puppeteer/Device.html +470 -0
  37. data/docs/Puppeteer/Devices.html +139 -0
  38. data/docs/Puppeteer/ElementHandle.html +2542 -0
  39. data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
  40. data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
  41. data/docs/Puppeteer/ElementHandle/Point.html +492 -0
  42. data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
  43. data/docs/Puppeteer/EmulationManager.html +454 -0
  44. data/docs/Puppeteer/EventCallbackable.html +433 -0
  45. data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
  46. data/docs/Puppeteer/ExecutionContext.html +998 -0
  47. data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
  48. data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
  49. data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
  50. data/docs/Puppeteer/FileChooser.html +455 -0
  51. data/docs/Puppeteer/Frame.html +3677 -0
  52. data/docs/Puppeteer/FrameManager.html +2410 -0
  53. data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
  54. data/docs/Puppeteer/IfPresent.html +222 -0
  55. data/docs/Puppeteer/JSHandle.html +1352 -0
  56. data/docs/Puppeteer/Keyboard.html +1557 -0
  57. data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
  58. data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
  59. data/docs/Puppeteer/Launcher.html +237 -0
  60. data/docs/Puppeteer/Launcher/Base.html +385 -0
  61. data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
  62. data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
  63. data/docs/Puppeteer/Launcher/Chrome.html +669 -0
  64. data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
  65. data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
  66. data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
  67. data/docs/Puppeteer/LifecycleWatcher.html +834 -0
  68. data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
  69. data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
  70. data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
  71. data/docs/Puppeteer/Mouse.html +1105 -0
  72. data/docs/Puppeteer/Mouse/Button.html +136 -0
  73. data/docs/Puppeteer/NetworkManager.html +901 -0
  74. data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
  75. data/docs/Puppeteer/Page.html +5970 -0
  76. data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
  77. data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
  78. data/docs/Puppeteer/Page/ScriptTag.html +555 -0
  79. data/docs/Puppeteer/Page/StyleTag.html +448 -0
  80. data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
  81. data/docs/Puppeteer/RemoteObject.html +1087 -0
  82. data/docs/Puppeteer/Target.html +1336 -0
  83. data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
  84. data/docs/Puppeteer/Target/TargetInfo.html +729 -0
  85. data/docs/Puppeteer/TimeoutError.html +135 -0
  86. data/docs/Puppeteer/TimeoutSettings.html +496 -0
  87. data/docs/Puppeteer/TouchScreen.html +464 -0
  88. data/docs/Puppeteer/Viewport.html +837 -0
  89. data/docs/Puppeteer/WaitTask.html +637 -0
  90. data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
  91. data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
  92. data/docs/Puppeteer/WebSocket.html +673 -0
  93. data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
  94. data/docs/Puppeteer/WebSocketTransport.html +600 -0
  95. data/docs/Puppeteer/WebSocktTransportError.html +124 -0
  96. data/docs/_index.html +823 -0
  97. data/docs/class_list.html +51 -0
  98. data/docs/css/common.css +1 -0
  99. data/docs/css/full_list.css +58 -0
  100. data/docs/css/style.css +496 -0
  101. data/docs/file.README.html +123 -0
  102. data/docs/file_list.html +56 -0
  103. data/docs/frames.html +17 -0
  104. data/docs/index.html +123 -0
  105. data/docs/js/app.js +314 -0
  106. data/docs/js/full_list.js +216 -0
  107. data/docs/js/jquery.js +4 -0
  108. data/docs/method_list.html +4075 -0
  109. data/docs/top-level-namespace.html +126 -0
  110. data/lib/puppeteer.rb +200 -0
  111. data/lib/puppeteer/async_await_behavior.rb +38 -0
  112. data/lib/puppeteer/browser.rb +259 -0
  113. data/lib/puppeteer/browser_context.rb +90 -0
  114. data/lib/puppeteer/browser_fetcher.rb +6 -0
  115. data/lib/puppeteer/browser_runner.rb +161 -0
  116. data/lib/puppeteer/cdp_session.rb +100 -0
  117. data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
  118. data/lib/puppeteer/connection.rb +254 -0
  119. data/lib/puppeteer/console_message.rb +24 -0
  120. data/lib/puppeteer/debug_print.rb +20 -0
  121. data/lib/puppeteer/device.rb +12 -0
  122. data/lib/puppeteer/devices.rb +885 -0
  123. data/lib/puppeteer/dom_world.rb +484 -0
  124. data/lib/puppeteer/element_handle.rb +433 -0
  125. data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
  126. data/lib/puppeteer/element_handle/box_model.rb +19 -0
  127. data/lib/puppeteer/element_handle/point.rb +26 -0
  128. data/lib/puppeteer/emulation_manager.rb +46 -0
  129. data/lib/puppeteer/errors.rb +2 -0
  130. data/lib/puppeteer/event_callbackable.rb +88 -0
  131. data/lib/puppeteer/execution_context.rb +254 -0
  132. data/lib/puppeteer/file_chooser.rb +29 -0
  133. data/lib/puppeteer/frame.rb +286 -0
  134. data/lib/puppeteer/frame_manager.rb +378 -0
  135. data/lib/puppeteer/if_present.rb +18 -0
  136. data/lib/puppeteer/js_handle.rb +142 -0
  137. data/lib/puppeteer/keyboard.rb +183 -0
  138. data/lib/puppeteer/keyboard/key_description.rb +19 -0
  139. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
  140. data/lib/puppeteer/launcher.rb +25 -0
  141. data/lib/puppeteer/launcher/base.rb +48 -0
  142. data/lib/puppeteer/launcher/browser_options.rb +41 -0
  143. data/lib/puppeteer/launcher/chrome.rb +211 -0
  144. data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
  145. data/lib/puppeteer/launcher/launch_options.rb +68 -0
  146. data/lib/puppeteer/lifecycle_watcher.rb +171 -0
  147. data/lib/puppeteer/mouse.rb +123 -0
  148. data/lib/puppeteer/network_manager.rb +122 -0
  149. data/lib/puppeteer/page.rb +1065 -0
  150. data/lib/puppeteer/page/screenshot_options.rb +78 -0
  151. data/lib/puppeteer/remote_object.rb +143 -0
  152. data/lib/puppeteer/target.rb +150 -0
  153. data/lib/puppeteer/timeout_settings.rb +15 -0
  154. data/lib/puppeteer/touch_screen.rb +43 -0
  155. data/lib/puppeteer/version.rb +3 -0
  156. data/lib/puppeteer/viewport.rb +54 -0
  157. data/lib/puppeteer/wait_task.rb +188 -0
  158. data/lib/puppeteer/web_socket.rb +122 -0
  159. data/lib/puppeteer/web_socket_transport.rb +49 -0
  160. data/puppeteer-ruby.gemspec +32 -0
  161. metadata +355 -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,6 @@
1
+ # Download latest chromium.
2
+ class Puppeteer::BrowserFetcher
3
+ def initialize(project_root, options = {})
4
+ # 未実装
5
+ end
6
+ end
@@ -0,0 +1,161 @@
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
+ @pid = @thread.pid
27
+ end
28
+
29
+ def kill
30
+ Process.kill(:KILL, @pid)
31
+ rescue Errno::ESRCH
32
+ # already killed
33
+ end
34
+
35
+ def dispose
36
+ [@stdout, @stderr].each { |io| io.close unless io.closed? }
37
+ @thread.join
38
+ end
39
+
40
+ attr_reader :stdout, :stderr
41
+ end
42
+
43
+ # @param {!(Launcher.LaunchOptions)=} options
44
+ def start(
45
+ executable_path: nil,
46
+ ignore_default_args: nil,
47
+ handle_SIGINT: nil,
48
+ handle_SIGTERM: nil,
49
+ handle_SIGHUP: nil,
50
+ timeout: nil,
51
+ dumpio: nil,
52
+ env: nil,
53
+ pipe: nil
54
+ )
55
+ @launch_options = Puppeteer::Launcher::LaunchOptions.new({
56
+ executable_path: executable_path,
57
+ ignore_default_args: ignore_default_args,
58
+ handle_SIGINT: handle_SIGINT,
59
+ handle_SIGTERM: handle_SIGTERM,
60
+ handle_SIGHUP: handle_SIGHUP,
61
+ timeout: timeout,
62
+ dumpio: dumpio,
63
+ env: env,
64
+ pipe: pipe,
65
+ }.compact)
66
+ @proc = BrowserProcess.new(
67
+ @launch_options.env,
68
+ @executable_path,
69
+ @process_arguments,
70
+ )
71
+ # if (dumpio) {
72
+ # this.proc.stderr.pipe(process.stderr);
73
+ # this.proc.stdout.pipe(process.stdout);
74
+ # }
75
+ @closed = false
76
+ @process_closing = -> {
77
+ @proc.dispose
78
+ @closed = true
79
+ if @temp_directory
80
+ FileUtils.rm_rf(@temp_directory)
81
+ end
82
+ }
83
+ trap(:EXIT) do
84
+ kill
85
+ end
86
+
87
+ if @launch_options.handle_SIGINT?
88
+ trap(:INT) do
89
+ kill
90
+ exit 130
91
+ end
92
+ end
93
+
94
+ if @launch_options.handle_SIGTERM?
95
+ trap(:TERM) do
96
+ close
97
+ end
98
+ end
99
+
100
+ if @launch_options.handle_SIGHUP?
101
+ trap(:HUP) do
102
+ close
103
+ end
104
+ end
105
+ end
106
+
107
+ # @return {Promise}
108
+ def close
109
+ return if @closed
110
+
111
+ if @temp_directory
112
+ kill
113
+ elsif @connection
114
+ begin
115
+ @connection.send_message('Browser.close')
116
+ rescue
117
+ kill
118
+ end
119
+ end
120
+
121
+ @process_closing.call
122
+ end
123
+
124
+ # @return {Promise}
125
+ def kill
126
+ unless @closed
127
+ @proc.kill
128
+ end
129
+ if @temp_directory
130
+ FileUtils.rm_rf(@temp_directory)
131
+ end
132
+ end
133
+
134
+
135
+ # @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
136
+ # @return {!Promise<!Connection>}
137
+ def setup_connection(use_pipe:, timeout:, slow_mo:, preferred_revision:)
138
+ if !use_pipe
139
+ browser_ws_endpoint = wait_for_ws_endpoint(@proc, timeout, preferred_revision)
140
+ transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
141
+ @connection = Puppeteer::Connection.new(browser_ws_endpoint, transport, slow_mo)
142
+ else
143
+ raise NotImplementedError.new('PipeTransport is not yet implemented')
144
+ end
145
+
146
+ @connection
147
+ end
148
+
149
+ private def wait_for_ws_endpoint(browser_process, timeout, preferred_revision)
150
+ Timeout.timeout(timeout / 1000.0) do
151
+ loop do
152
+ line = browser_process.stderr.readline
153
+ /^DevTools listening on (ws:\/\/.*)$/.match(line) do |m|
154
+ return m[1]
155
+ end
156
+ end
157
+ end
158
+ rescue Timeout::Error
159
+ 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.")
160
+ end
161
+ end
@@ -0,0 +1,100 @@
1
+ class Puppeteer::CDPSession
2
+ include Puppeteer::DebugPrint
3
+ include Puppeteer::EventCallbackable
4
+ using Puppeteer::AsyncAwaitBehavior
5
+
6
+ class Error < StandardError; end
7
+
8
+ # @param {!Connection} connection
9
+ # @param {string} targetType
10
+ # @param {string} sessionId
11
+ def initialize(connection, target_type, session_id)
12
+ @callbacks = {}
13
+ @connection = connection
14
+ @target_type = target_type
15
+ @session_id = session_id
16
+ @pending_messages = {}
17
+ end
18
+
19
+ attr_reader :connection
20
+
21
+ # @param method [String]
22
+ # @param params [Hash]
23
+ # @returns [Hash]
24
+ def send_message(method, params = {})
25
+ await async_send_message(method, params)
26
+ end
27
+
28
+ # @param method [String]
29
+ # @param params [Hash]
30
+ # @returns [Future<Hash>]
31
+ def async_send_message(method, params = {})
32
+ if !@connection
33
+ raise Error.new("Protocol error (#{method}): Session closed. Most likely the #{@target_type} has been closed.")
34
+ end
35
+ id = @connection.raw_send(message: { sessionId: @session_id, method: method, params: params })
36
+ promise = resolvable_future
37
+ callback = Puppeteer::Connection::MessageCallback.new(method: method, promise: promise)
38
+ if pending_message = @pending_messages.delete(id)
39
+ debug_puts "Pending message (id: #{id}) is handled"
40
+ callback_with_message(callback, pending_message)
41
+ else
42
+ @callbacks[id] = callback
43
+ end
44
+ promise
45
+ end
46
+
47
+ # @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
48
+ def handle_message(message)
49
+ if message['id']
50
+ if callback = @callbacks.delete(message['id'])
51
+ callback_with_message(callback, message)
52
+ else
53
+ debug_puts "unknown id: #{id}. Store it into pending message"
54
+
55
+ # RECV is often notified before SEND.
56
+ # Wait about 10 frames before throwing an error.
57
+ message_id = message['id']
58
+ @pending_messages[message_id] = message
59
+ Concurrent::Promises.schedule(0.16, message_id) do |id|
60
+ if @pending_messages.delete(id)
61
+ raise Error.new("unknown id: #{id}")
62
+ end
63
+ end
64
+ end
65
+ else
66
+ emit_event message['method'], message['params']
67
+ end
68
+ end
69
+
70
+ private def callback_with_message(callback, message)
71
+ if message['error']
72
+ callback.reject(
73
+ Puppeteer::Connection::ProtocolError.new(
74
+ method: callback.method,
75
+ error_message: message['error']['message'],
76
+ error_data: message['error']['data']))
77
+ else
78
+ callback.resolve(message['result'])
79
+ end
80
+ end
81
+
82
+ def detach
83
+ if !@connection
84
+ raise Error.new("Session already detarched. Most likely the #{@target_type} has been closed.")
85
+ end
86
+ @connection.send_message('Target.detachFromTarget', sessionId: @session_id)
87
+ end
88
+
89
+ def handle_closed
90
+ @callbacks.each_value do |callback|
91
+ callback.reject(
92
+ Puppeteer::Connection::ProtocolError.new(
93
+ method: callback.method,
94
+ error_message: 'Target Closed.'))
95
+ end
96
+ @callbacks.clear
97
+ @connection = nil
98
+ emit_event 'Events.CDPSession.Disconnected'
99
+ end
100
+ 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.handle_closed
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.each_value do |callback|
211
+ callback.reject(
212
+ ProtocolError.new(
213
+ method: callback.method,
214
+ error_message: 'Target Closed.'))
215
+ end
216
+ @callbacks.clear
217
+ @sessions.each_value 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