Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
the browser; a failed `IO.close` is raised to the caller.

### Fixed
- `create_page` raised `Failed to find browser context with id` against a browser that came up with its own startup
window (browserless). That window lives in the browser's implicit context, which Chrome below 145
won't address by id. Ferrum now detects it and exposes as `Ferrum::Context#implicit?`. It's never
disposed, so `#reset` won't clear such a browser. [#578] [#627]
- Targets of a type `Ferrum::Contexts` doesn't track were left attached and paused by auto-attach for the lifetime
of the browser; Chrome opens two `browser_ui` ones per browser context. They're now resumed and detached from
- `Ferrum::Browser::Process` registered two `ObjectSpace` finalizers on the same object, a directory remover and
Expand Down
21 changes: 17 additions & 4 deletions lib/ferrum/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,25 @@ class Context

attr_reader :id, :targets

def initialize(client, contexts, id)
def initialize(client, contexts, id, implicit: false)
@id = id
@implicit = implicit
@client = client
@contexts = contexts
@targets = Concurrent::Map.new
@pendings = Concurrent::Map.new
end

# Whether this is the browser's implicit context, the one it puts its
# startup window in. We didn't create it, so we can't dispose it, and
# Chrome doesn't let us address it by id either: targets are created in
# it by omitting `browserContextId` altogether.
#
# @return [Boolean]
def implicit?
@implicit
end

# The context's first known target, creating one via
# `Target.createTarget` if none has attached yet.
#
Expand Down Expand Up @@ -92,7 +103,8 @@ def create_page(**options)
#
# @raise [NoSuchTargetError]
def create_target
target_id = @client.command("Target.createTarget", browserContextId: @id, url: "about:blank")["targetId"]
options = { browserContextId: @id } unless implicit?
target_id = @client.command("Target.createTarget", url: "about:blank", **Hash(options))["targetId"]

new_pending = Concurrent::IVar.new
pending = @pendings.put_if_absent(target_id, new_pending) || new_pending
Expand Down Expand Up @@ -184,9 +196,10 @@ def close_targets_connection
end
end

# Disposes this browser context and all of its targets.
# Disposes this browser context and all of its targets. The browser's
# implicit context cannot be disposed, see {#implicit?}.
#
# @return [Boolean]
# @return [Boolean, nil]
def dispose
@contexts.dispose(@id)
end
Expand Down
37 changes: 31 additions & 6 deletions lib/ferrum/contexts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def initialize(client)
@client = client
@contexts = Concurrent::Map.new
@manually_attached = Concurrent::Map.new
@default_context = find_implicit_context
subscribe
auto_attach
discover
Expand All @@ -33,7 +34,9 @@ def manually_attached(target_id)
@manually_attached[target_id] = true
end

# The browser's first context, created lazily.
# The context we work in unless told otherwise: the browser's implicit
# context when it came up with a startup window (see {#find_implicit_context}),
# otherwise one of our own, created lazily.
#
# @return [Context]
def default_context
Expand Down Expand Up @@ -84,14 +87,16 @@ def create(**options)
context
end

# Disposes a browser context and all of its targets.
# Disposes a browser context and all of its targets. The browser's implicit
# context is not ours to dispose, see {Context#implicit?}.
#
# @param [String] context_id
#
# @return [Boolean]
# @return [Boolean, nil]
def dispose(context_id)
context = @contexts[context_id]
return unless context
return if context.implicit?

context.close_targets_connection
@client.command("Target.disposeBrowserContext", browserContextId: context.id)
Expand Down Expand Up @@ -251,9 +256,29 @@ def auto_attach
def add_context(context_id)
return if @contexts[context_id]

context = Context.new(@client, self, context_id)
@contexts[context_id] = context
@default_context ||= context # rubocop:disable Naming/MemoizedInstanceVariableName
@contexts[context_id] = Context.new(@client, self, context_id)
end

# The browser's implicit context, the context Chrome uses for its startup window.
# We register it so events for that window are routed to it.
# `Target.getBrowserContexts` reports every context created with
# `Target.createBrowserContext`, ours and those of anyone else driving the same
# browser, so the context holding targets that it doesn't report is the implicit
# one. Chrome also reports its id as `defaultBrowserContextId`, but only since
# 145 and only as an experimental field, so we don't rely on it.
#
# Returns nil if the browser has no startup window, which happens when it is
# launched with `--no-startup-window` (used for `incognito: true`). In that case,
# we create our own context instead.
#
# @return [Context, nil]
def find_implicit_context
created_ids = @client.command("Target.getBrowserContexts")["browserContextIds"]
target_infos = @client.command("Target.getTargets")["targetInfos"]
implicit_id = (target_infos.filter_map { |t| t["browserContextId"] } - created_ids).first
return unless implicit_id

@contexts[implicit_id] = Context.new(@client, self, implicit_id, implicit: true)
end
end
end
7 changes: 5 additions & 2 deletions sig/ferrum/context.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ module Ferrum
attr_reader targets: ::Concurrent::Map[String, Target]

@id: String
@implicit: bool
@client: Client
@contexts: Contexts
@targets: ::Concurrent::Map[String, Target]
@pendings: ::Concurrent::Map[String, ::Concurrent::IVar]
@default_target: Target?

def initialize: (Client, Contexts, String) -> void
def initialize: (Client, Contexts, String, ?implicit: bool) -> void

def implicit?: () -> bool

def default_target: () -> Target

Expand Down Expand Up @@ -42,7 +45,7 @@ module Ferrum

def close_targets_connection: () -> void

def dispose: () -> bool
def dispose: () -> bool?

def target?: (String target_id) -> bool

Expand Down
4 changes: 3 additions & 1 deletion sig/ferrum/contexts.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module Ferrum

def create: (**untyped options) -> Context

def dispose: (String context_id) -> bool
def dispose: (String context_id) -> bool?

def close_connections: () -> void

Expand Down Expand Up @@ -62,5 +62,7 @@ module Ferrum
def auto_attach: () -> void

def add_context: (String? context_id) -> Context?

def find_implicit_context: () -> Context?
end
end
45 changes: 45 additions & 0 deletions spec/contexts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,49 @@
browser.client.off("Target.attachedToTarget", attached_id)
browser.client.off("Target.detachedFromTarget", detached_id)
end

describe "#default_context" do
it "works in the browser's startup window when it came up with one" do
with_external_browser(incognito: false) do |url|
remote = Ferrum::Browser.new(url: url)

expect(remote.contexts.default_context).to be_implicit
expect { remote.create_page }.not_to raise_error
ensure
remote&.quit
end
end

it "creates a context of its own when the browser has no startup window" do
with_external_browser do |url|
remote = Ferrum::Browser.new(url: url)
remote.create_page

expect(remote.contexts.default_context).not_to be_implicit

remote.reset

expect(remote.contexts.size).to be_zero
ensure
remote&.quit
end
end

it "ignores contexts another client created in the same browser" do
with_external_browser do |url|
first = Ferrum::Browser.new(url: url)
context = first.contexts.create
context.create_page

second = Ferrum::Browser.new(url: url)

expect(second.contexts.default_context).not_to be_implicit
expect(second.contexts.default_context.id).not_to eq(context.id)
expect { second.create_page }.not_to raise_error
ensure
second&.quit
first&.quit
end
end
end
end
5 changes: 3 additions & 2 deletions spec/support/global_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ def with_timeout(new_timeout)
browser.timeout = old_timeout
end

def with_external_browser(host: "127.0.0.1", port: 32_001)
options = Ferrum::Browser::Options.new(host: host, port: port, window_size: [1400, 1400], headless: true)
def with_external_browser(host: "127.0.0.1", port: 32_001, incognito: true)
options = Ferrum::Browser::Options.new(host: host, port: port, window_size: [1400, 1400],
headless: true, incognito: incognito)
process = Ferrum::Browser::Process.new(options)

begin
Expand Down
Loading