puppeteer-ruby 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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