Skip to content

Commit d1b333a

Browse files
authored
fix: Ferrum::NoExecutionContextError can be raised when we call frame_by and lock on execution_id (#307)
1 parent a81e24f commit d1b333a

File tree

5 files changed

+43
-19
lines changed

5 files changed

+43
-19
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- `#create_page(proxy: { host: "x.x.x.x", port: "8800", user: "user", password: "pa$$" })`
99
proxy option, supports creating a page in a new context that uses proxy settings.
1010
- `Ferrum::Page#timeout = n` page supports its own timeout
11+
- `Ferrum::Frame#execution_id` returns execution context id and doesn't raises error
12+
- `Ferrum::Frame#execution_id!` returns execution context id and raises error when times out on borrowing
1113

1214
### Changed
1315

@@ -16,6 +18,10 @@
1618
- `Ferrum::Keyboard`
1719
- `#up, #down` accept only one key.
1820
- `Ferrum::Page#goto` fixed undefined method url for nil:NilClass when page times out and there are pending requests.
21+
- `Runtime.consoleAPICalled` didn't show log messages
22+
- `Ferrum::Page#subscribe_frame_detached` added to clean up old frames
23+
- `Ferrum::Proxy` was hanging at the exit due to issue in Webrick
24+
- `Ferrum::NoExecutionContextError` is raised sometimes when we block on `Ferrum::Page#frame_by`
1925

2026
### Removed
2127

lib/ferrum/frame.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,34 @@ def content=(html)
118118

119119
#
120120
# Execution context id which is used by JS, each frame has it's own
121-
# context in which JS evaluates.
121+
# context in which JS evaluates. Locks for a page timeout and raises
122+
# an error if an execution id hasn't been set yet, if id is set
123+
# returns immediately.
122124
#
123125
# @return [Integer]
124126
#
125127
# @raise [NoExecutionContextError]
126128
#
127-
def execution_id
129+
def execution_id!
128130
value = @execution_id.borrow(@page.timeout, &:itself)
129131
raise NoExecutionContextError if value.instance_of?(Object)
130132

131133
value
132134
end
133135

136+
#
137+
# Execution context id which is used by JS, each frame has it's own
138+
# context in which JS evaluates.
139+
#
140+
# @return [Integer, nil]
141+
#
142+
def execution_id
143+
value = @execution_id.value
144+
return if value.instance_of?(Object)
145+
146+
value
147+
end
148+
134149
def execution_id=(value)
135150
if value.nil?
136151
@execution_id.try_take!

lib/ferrum/frame/runtime.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options)
112112
end
113113

114114
if params[:executionContextId].nil? && params[:objectId].nil?
115-
params = params.merge(executionContextId: execution_id)
115+
params = params.merge(executionContextId: execution_id!)
116116
end
117117

118118
response = @page.command("Runtime.callFunctionOn",

lib/ferrum/page.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def reset
3535
extend Forwardable
3636
delegate %i[at_css at_xpath css xpath
3737
current_url current_title url title body doctype content=
38-
execution_id evaluate evaluate_on evaluate_async execute evaluate_func
38+
execution_id execution_id! evaluate evaluate_on evaluate_async execute evaluate_func
3939
add_script_tag add_style_tag] => :main_frame
4040

4141
include Animation
@@ -72,7 +72,7 @@ def reset
7272
attr_reader :cookies
7373

7474
def initialize(target_id, browser, proxy: nil)
75-
@frames = {}
75+
@frames = Concurrent::Map.new
7676
@main_frame = Frame.new(nil, self)
7777
@browser = browser
7878
@target_id = target_id
@@ -411,7 +411,7 @@ def inject_extensions
411411
# We also evaluate script just in case because
412412
# `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
413413
command("Runtime.evaluate", expression: extension,
414-
contextId: execution_id,
414+
executionContextId: execution_id!,
415415
returnByValue: true)
416416
end
417417
end

lib/ferrum/page/frames.rb

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def frames
4444
# @param [String] name
4545
# Frame's name if there's one.
4646
#
47+
# @param [String] execution_id
48+
# Frame's context execution id.
49+
#
4750
# @return [Frame, nil]
4851
# The matching frame.
4952
#
@@ -83,26 +86,26 @@ def frames_subscribe
8386
def subscribe_frame_attached
8487
on("Page.frameAttached") do |params|
8588
parent_frame_id, frame_id = params.values_at("parentFrameId", "frameId")
86-
@frames[frame_id] = Frame.new(frame_id, self, parent_frame_id)
89+
@frames.put_if_absent(frame_id, Frame.new(frame_id, self, parent_frame_id))
8790
end
8891
end
8992

9093
def subscribe_frame_detached
9194
on("Page.frameDetached") do |params|
9295
frame = @frames[params["frameId"]]
9396

94-
if frame.main?
97+
if frame&.main?
9598
frame.execution_id = nil
9699
else
97-
@frames.delete(frame.id)
100+
@frames.delete(params["frameId"])
98101
end
99102
end
100103
end
101104

102105
def subscribe_frame_started_loading
103106
on("Page.frameStartedLoading") do |params|
104107
frame = @frames[params["frameId"]]
105-
frame.state = :started_loading
108+
frame.state = :started_loading if frame
106109
@event.reset
107110
end
108111
end
@@ -111,8 +114,11 @@ def subscribe_frame_navigated
111114
on("Page.frameNavigated") do |params|
112115
frame_id, name = params["frame"]&.values_at("id", "name")
113116
frame = @frames[frame_id]
114-
frame.state = :navigated
115-
frame.name = name unless name.to_s.empty?
117+
118+
if frame
119+
frame.state = :navigated
120+
frame.name = name
121+
end
116122
end
117123
end
118124

@@ -159,14 +165,12 @@ def subscribe_execution_context_created
159165
root_frame = command("Page.getFrameTree").dig("frameTree", "frame", "id")
160166
if frame_id == root_frame
161167
@main_frame.id = frame_id
162-
@frames[frame_id] = @main_frame
168+
@frames.put_if_absent(frame_id, @main_frame)
163169
end
164170
end
165171

166-
frame = @frames[frame_id] || Frame.new(frame_id, self)
172+
frame = @frames.fetch_or_store(frame_id, Frame.new(frame_id, self))
167173
frame.execution_id = context_id
168-
169-
@frames[frame_id] ||= frame
170174
end
171175
end
172176

@@ -180,13 +184,12 @@ def subscribe_execution_context_destroyed
180184

181185
def subscribe_execution_contexts_cleared
182186
on("Runtime.executionContextsCleared") do
183-
@frames.delete_if { |_, f| !f.main? }
184-
@main_frame.execution_id = nil
187+
@frames.each_value { |f| f.execution_id = nil }
185188
end
186189
end
187190

188191
def idling?
189-
@frames.all? { |_, f| f.state == :stopped_loading }
192+
@frames.values.all? { |f| f.state == :stopped_loading }
190193
end
191194
end
192195
end

0 commit comments

Comments
 (0)