From 8c803afa21c463249052f46752ed0553d1bb244a Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sat, 6 Jan 2024 13:54:01 +0300 Subject: [PATCH 1/2] feat: Flatten mode --- CHANGELOG.md | 6 ++-- lib/ferrum/browser.rb | 3 ++ lib/ferrum/browser/options.rb | 3 +- lib/ferrum/client.rb | 54 ++++++++++++++++++++++++++++++++- lib/ferrum/client/subscriber.rb | 12 ++++++-- lib/ferrum/context.rb | 6 ++-- lib/ferrum/contexts.rb | 20 +++++++++++- lib/ferrum/target.rb | 11 +++++-- 8 files changed, 100 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc210a71..0d38cbd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `#wait` wait for file download to be completed - `#set_behavior` where and whether to store file - `Browser::Client#command` accepts :async parameter [#433] +- `Ferrum::Browser` introduce `:flatten` mode with one connection and sessions ### Changed - `Ferrum::Page#screeshot` accepts :area option [#410] @@ -533,9 +534,8 @@ to `Ferrum::Browser#default_context` ### Fixed ### Removed -- `Ferrum::EmptyTargetsError` -- the `hack` to handle `new window` which doesn't have events at all by `Ferrum::Page#session_id` with -`Target.attachToTarget` and `Target.detachFromTarget` usage +- `Ferrum::EmptyTargetsError` the hack to handle `new window` which doesn't have events at all by +`Ferrum::Page#session_id` with `Target.attachToTarget` and `Target.detachFromTarget` usage - `Ferrum::Page#close_connection` - the logic is moved to `Ferrum::Page#close` directly - the third argument (`new_window = false`) for `Ferrum::Page` initializer - `Ferrum::Targets` class with the delegations to `Ferrum::Targets` instance in `Ferrum::Browser` instance: diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index e1186472..17fb6057 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -47,6 +47,9 @@ class Browser # @option options [Boolean] :xvfb (false) # Run browser in a virtual framebuffer. # + # @option options [Boolean] :flatten (true) + # Use one websocket connection to the browser and all the pages in flatten mode. + # # @option options [(Integer, Integer)] :window_size ([1024, 768]) # The dimensions of the browser window in which to test, expressed as a # 2-element array, e.g. `[1024, 768]`. diff --git a/lib/ferrum/browser/options.rb b/lib/ferrum/browser/options.rb index 6f1c9acd..06a7cc17 100644 --- a/lib/ferrum/browser/options.rb +++ b/lib/ferrum/browser/options.rb @@ -15,7 +15,7 @@ class Options :js_errors, :base_url, :slowmo, :pending_connection_errors, :url, :env, :process_timeout, :browser_name, :browser_path, :save_path, :proxy, :port, :host, :headless, :browser_options, - :ignore_default_browser_options, :xvfb + :ignore_default_browser_options, :xvfb, :flatten attr_accessor :timeout, :ws_url, :default_user_agent def initialize(options = nil) @@ -27,6 +27,7 @@ def initialize(options = nil) @window_size = @options.fetch(:window_size, WINDOW_SIZE) @js_errors = @options.fetch(:js_errors, false) @headless = @options.fetch(:headless, true) + @flatten = @options.fetch(:flatten, true) @pending_connection_errors = @options.fetch(:pending_connection_errors, true) @process_timeout = @options.fetch(:process_timeout, PROCESS_TIMEOUT) @slowmo = @options[:slowmo].to_f diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index c909973b..80856e9a 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -5,11 +5,59 @@ require "ferrum/client/web_socket" module Ferrum + class SessionClient + attr_reader :client, :session_id + + def self.event_name(event, session_id) + [event, session_id].compact.join("_") + end + + def initialize(client, session_id) + @client = client + @session_id = session_id + end + + def command(method, async: false, **params) + message = build_message(method, params) + @client.send_message(message, async: async) + end + + def on(event, &block) + @client.on(event_name(event), &block) + end + + def subscribed?(event) + @client.subscribed?(event_name(event)) + end + + def respond_to_missing?(name, include_private) + @client.respond_to?(name, include_private) + end + + def method_missing(name, ...) + @client.send(name, ...) + end + + def close + @client.subscriber.clear(session_id: session_id) + end + + private + + def build_message(method, params) + @client.build_message(method, params).merge(sessionId: session_id) + end + + def event_name(event) + self.class.event_name(event, session_id) + end + end + class Client extend Forwardable delegate %i[timeout timeout=] => :options - attr_reader :options + attr_reader :options, :subscriber def initialize(ws_url, options) @command_id = 0 @@ -54,6 +102,10 @@ def subscribed?(event) @subscriber.subscribed?(event) end + def session(session_id) + SessionClient.new(self, session_id) + end + def close @ws.close # Give a thread some time to handle a tail of messages diff --git a/lib/ferrum/client/subscriber.rb b/lib/ferrum/client/subscriber.rb index 7fc52a64..74b8887c 100644 --- a/lib/ferrum/client/subscriber.rb +++ b/lib/ferrum/client/subscriber.rb @@ -35,6 +35,10 @@ def close @priority_thread&.kill end + def clear(session_id:) + @on.delete_if { |k, _| k.match?(session_id) } + end + private def start @@ -58,9 +62,11 @@ def start end def call(message) - method, params = message.values_at("method", "params") - total = @on[method].size - @on[method].each_with_index do |block, index| + method, session_id, params = message.values_at("method", "sessionId", "params") + event = SessionClient.event_name(method, session_id) + + total = @on[event].size + @on[event].each_with_index do |block, index| # In case of multiple callbacks we provide current index and total block.call(params, index, total) end diff --git a/lib/ferrum/context.rb b/lib/ferrum/context.rb index e0628017..af71f8d2 100644 --- a/lib/ferrum/context.rb +++ b/lib/ferrum/context.rb @@ -53,8 +53,8 @@ def create_target target end - def add_target(params) - new_target = Target.new(@client, params) + def add_target(params:, session_id: nil) + new_target = Target.new(@client, session_id, params) target = @targets.put_if_absent(new_target.id, new_target) target ||= new_target # `put_if_absent` returns nil if added a new value or existing if there was one already @pendings.put(target, @client.timeout) if @pendings.empty? @@ -71,7 +71,7 @@ def delete_target(target_id) def close_targets_connection @targets.each_value do |target| - next unless target.attached? + next unless target.connected? target.page.close_connection end diff --git a/lib/ferrum/contexts.rb b/lib/ferrum/contexts.rb index 9753c0e7..1a0f1ddf 100644 --- a/lib/ferrum/contexts.rb +++ b/lib/ferrum/contexts.rb @@ -12,6 +12,7 @@ def initialize(client) @contexts = Concurrent::Map.new @client = client subscribe + auto_attach discover end @@ -67,12 +68,23 @@ def size private def subscribe + @client.on("Target.attachedToTarget") do |params| + info, session_id = params.values_at("targetInfo", "sessionId") + next unless info["type"] == "page" + + context_id = info["browserContextId"] + @contexts[context_id]&.add_target(session_id: session_id, params: info) + if params["waitingForDebugger"] + @client.session(session_id).command("Runtime.runIfWaitingForDebugger", async: true) + end + end + @client.on("Target.targetCreated") do |params| info = params["targetInfo"] next unless info["type"] == "page" context_id = info["browserContextId"] - @contexts[context_id]&.add_target(info) + @contexts[context_id]&.add_target(params: info) end @client.on("Target.targetInfoChanged") do |params| @@ -97,5 +109,11 @@ def subscribe def discover @client.command("Target.setDiscoverTargets", discover: true) end + + def auto_attach + return unless @client.options.flatten + + @client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true) + end end end diff --git a/lib/ferrum/target.rb b/lib/ferrum/target.rb index 8dfc8af2..099851c9 100644 --- a/lib/ferrum/target.rb +++ b/lib/ferrum/target.rb @@ -8,17 +8,20 @@ class Target # where we enhance page class and build page ourselves. attr_writer :page - def initialize(client, params = nil) + attr_reader :session_id + + def initialize(client, session_id = nil, params = nil) @page = nil @client = client + @session_id = session_id @params = params end def update(params) - @params = params + @params.merge!(params) end - def attached? + def connected? !!@page end @@ -68,6 +71,8 @@ def maybe_sleep_if_new_window def build_client options = @client.options + return @client.session(session_id) if options.flatten + ws_url = options.ws_url.merge(path: "/devtools/page/#{id}").to_s Client.new(ws_url, options) end From bc9eb6817ac88f1a2c2891c8b06e58e49a3c7c17 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sat, 6 Jan 2024 14:00:30 +0300 Subject: [PATCH 2/2] chore: Add README entry --- CHANGELOG.md | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d38cbd3..96a9206a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - `#wait` wait for file download to be completed - `#set_behavior` where and whether to store file - `Browser::Client#command` accepts :async parameter [#433] -- `Ferrum::Browser` introduce `:flatten` mode with one connection and sessions +- `Ferrum::Browser` introduce `:flatten` mode with one connection and sessions [#434] ### Changed - `Ferrum::Page#screeshot` accepts :area option [#410] diff --git a/README.md b/README.md index 22315150..cc4689c3 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Ferrum::Browser.new(options) * `:headless` (String | Boolean) - Set browser as headless or not, `true` by default. You can set `"new"` to support [new headless mode](https://developer.chrome.com/articles/new-headless/). * `:xvfb` (Boolean) - Run browser in a virtual framebuffer, `false` by default. + * `:flatten` (Boolean) - Use one websocket connection to the browser and all the pages in flatten mode. * `:window_size` (Array) - The dimensions of the browser window in which to test, expressed as a 2-element array, e.g. [1024, 768]. Default: [1024, 768] * `:extensions` (Array[String | Hash]) - An array of paths to files or JS