cuprite 0.2.0
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.
- checksums.yaml +7 -0
- data/lib/capybara/cuprite.rb +28 -0
- data/lib/capybara/cuprite/browser.rb +286 -0
- data/lib/capybara/cuprite/browser/client.rb +70 -0
- data/lib/capybara/cuprite/browser/dom.rb +50 -0
- data/lib/capybara/cuprite/browser/frame.rb +109 -0
- data/lib/capybara/cuprite/browser/input.rb +123 -0
- data/lib/capybara/cuprite/browser/javascripts/index.js +407 -0
- data/lib/capybara/cuprite/browser/page.rb +278 -0
- data/lib/capybara/cuprite/browser/process.rb +167 -0
- data/lib/capybara/cuprite/browser/runtime.rb +194 -0
- data/lib/capybara/cuprite/browser/targets.rb +109 -0
- data/lib/capybara/cuprite/browser/web_socket.rb +60 -0
- data/lib/capybara/cuprite/cookie.rb +47 -0
- data/lib/capybara/cuprite/driver.rb +396 -0
- data/lib/capybara/cuprite/errors.rb +131 -0
- data/lib/capybara/cuprite/network/error.rb +25 -0
- data/lib/capybara/cuprite/network/request.rb +33 -0
- data/lib/capybara/cuprite/network/response.rb +42 -0
- data/lib/capybara/cuprite/node.rb +216 -0
- data/lib/capybara/cuprite/version.rb +7 -0
- metadata +231 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0b3d43410a4b7875e111108b5ef1b53bf07589f833925ea3a9e336d725d99b95
|
4
|
+
data.tar.gz: 6cea0c2abdef1b5d8f8b6468f2924dac1cbbe5b51f1a4b6ce74bb980a86278ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cd77da4241673d610343e552245f5e0421ee6fe8613cc437bd53d0e8bd3d63be8b422a4abf36fcfedf35d79bbed0b7db5fe26250e5b86f737ac4479c6e02281e
|
7
|
+
data.tar.gz: cd86413a1b47478e4b0545c28c5ba0ea54a592d889ef0420547d2192bfdac7bece55e9557d88d48cca0720a8d86562dbe281b0988e1234fc55a25d7fc44484c1
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "capybara"
|
4
|
+
|
5
|
+
Thread.abort_on_exception = true
|
6
|
+
Thread.report_on_exception = true
|
7
|
+
|
8
|
+
module Capybara::Cuprite
|
9
|
+
require "cuprite/driver"
|
10
|
+
require "cuprite/browser"
|
11
|
+
require "cuprite/node"
|
12
|
+
require "cuprite/errors"
|
13
|
+
require "cuprite/cookie"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def windows?
|
17
|
+
RbConfig::CONFIG["host_os"] =~ /mingw|mswin|cygwin/
|
18
|
+
end
|
19
|
+
|
20
|
+
def mri?
|
21
|
+
defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Capybara.register_driver(:cuprite) do |app|
|
27
|
+
Capybara::Cuprite::Driver.new(app)
|
28
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "forwardable"
|
5
|
+
require "cuprite/browser/targets"
|
6
|
+
require "cuprite/browser/process"
|
7
|
+
require "cuprite/browser/client"
|
8
|
+
require "cuprite/browser/page"
|
9
|
+
|
10
|
+
module Capybara::Cuprite
|
11
|
+
class Browser
|
12
|
+
TIMEOUT = 5
|
13
|
+
EXTENSIONS = [
|
14
|
+
File.expand_path("browser/javascripts/index.js", __dir__)
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
extend Forwardable
|
18
|
+
|
19
|
+
attr_reader :headers
|
20
|
+
|
21
|
+
def self.start(*args)
|
22
|
+
new(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
delegate subscribe: :@client
|
26
|
+
delegate %i(window_handle window_handles switch_to_window open_new_window
|
27
|
+
close_window within_window page) => :targets
|
28
|
+
delegate %i(visit status_code body all_text property attributes attribute
|
29
|
+
value visible? disabled? resize path network_traffic
|
30
|
+
clear_network_traffic response_headers refresh click right_click
|
31
|
+
double_click hover set click_coordinates drag drag_by select
|
32
|
+
trigger scroll_to send_keys evaluate evaluate_on evaluate_async
|
33
|
+
execute frame_url frame_title within_frame switch_to_frame
|
34
|
+
current_url title go_back go_forward) => :page
|
35
|
+
|
36
|
+
attr_reader :process, :logger
|
37
|
+
attr_writer :timeout
|
38
|
+
|
39
|
+
def initialize(options = nil)
|
40
|
+
@options = Hash(options)
|
41
|
+
@logger, @timeout = @options.values_at(:logger, :timeout)
|
42
|
+
|
43
|
+
if ENV["CUPRITE_DEBUG"]
|
44
|
+
STDOUT.sync = true
|
45
|
+
@logger = STDOUT
|
46
|
+
end
|
47
|
+
|
48
|
+
start
|
49
|
+
end
|
50
|
+
|
51
|
+
def extensions
|
52
|
+
@extensions ||= begin
|
53
|
+
exts = @options.fetch(:extensions, [])
|
54
|
+
(EXTENSIONS + exts).map { |p| File.read(p) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def timeout
|
59
|
+
@timeout || TIMEOUT
|
60
|
+
end
|
61
|
+
|
62
|
+
def source
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
|
66
|
+
def parents(node)
|
67
|
+
evaluate_on(node: node, expr: "_cuprite.parents(this)", by_value: false)
|
68
|
+
end
|
69
|
+
|
70
|
+
def find(method, selector)
|
71
|
+
find_all(method, selector)
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_within(node, method, selector)
|
75
|
+
resolved = page.command("DOM.resolveNode", nodeId: node["nodeId"])
|
76
|
+
object_id = resolved.dig("object", "objectId")
|
77
|
+
find_all(method, selector, { "objectId" => object_id })
|
78
|
+
end
|
79
|
+
|
80
|
+
def visible_text(node)
|
81
|
+
begin
|
82
|
+
evaluate_on(node: node, expr: "_cuprite.visibleText(this)")
|
83
|
+
rescue BrowserError => e
|
84
|
+
# FIXME: ObsoleteNode first arg is node, so it should be in node class
|
85
|
+
if e.message == "No node with given id found"
|
86
|
+
raise ObsoleteNode.new(self, e.response)
|
87
|
+
end
|
88
|
+
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete_text(node)
|
94
|
+
raise NotImplementedError
|
95
|
+
end
|
96
|
+
|
97
|
+
def select_file(node, value)
|
98
|
+
raise NotImplementedError
|
99
|
+
end
|
100
|
+
|
101
|
+
def render(path, _options = {})
|
102
|
+
# check_render_options!(options)
|
103
|
+
# options[:full] = !!options[:full]
|
104
|
+
data = Base64.decode64(render_base64)
|
105
|
+
File.open(path.to_s, "wb") { |f| f.write(data) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def render_base64(format = "png", _options = {})
|
109
|
+
# check_render_options!(options)
|
110
|
+
# options[:full] = !!options[:full]
|
111
|
+
page.command("Page.captureScreenshot", format: format)["data"]
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_zoom_factor(zoom_factor)
|
115
|
+
raise NotImplementedError
|
116
|
+
end
|
117
|
+
|
118
|
+
def set_paper_size(size)
|
119
|
+
raise NotImplementedError
|
120
|
+
end
|
121
|
+
|
122
|
+
def set_proxy(ip, port, type, user, password)
|
123
|
+
raise NotImplementedError
|
124
|
+
end
|
125
|
+
|
126
|
+
def headers=(headers)
|
127
|
+
@headers = {}
|
128
|
+
add_headers(headers)
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_headers(headers, permanent: true)
|
132
|
+
if headers["Referer"]
|
133
|
+
page.referrer = headers["Referer"]
|
134
|
+
headers.delete("Referer") unless permanent
|
135
|
+
end
|
136
|
+
|
137
|
+
@headers.merge!(headers)
|
138
|
+
user_agent = @headers["User-Agent"]
|
139
|
+
accept_language = @headers["Accept-Language"]
|
140
|
+
|
141
|
+
set_overrides(user_agent: user_agent, accept_language: accept_language)
|
142
|
+
page.command("Network.setExtraHTTPHeaders", headers: @headers)
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_header(header, permanent: true)
|
146
|
+
add_headers(header, permanent: permanent)
|
147
|
+
end
|
148
|
+
|
149
|
+
def set_overrides(user_agent: nil, accept_language: nil, platform: nil)
|
150
|
+
options = Hash.new
|
151
|
+
options[:userAgent] = user_agent if user_agent
|
152
|
+
options[:acceptLanguage] = accept_language if accept_language
|
153
|
+
options[:platform] if platform
|
154
|
+
|
155
|
+
page.command("Network.setUserAgentOverride", **options) if !options.empty?
|
156
|
+
end
|
157
|
+
|
158
|
+
def cookies
|
159
|
+
cookies = page.command("Network.getAllCookies")["cookies"]
|
160
|
+
cookies.map { |c| [c["name"], Cookie.new(c)] }.to_h
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_cookie(cookie)
|
164
|
+
page.command("Network.setCookie", **cookie)
|
165
|
+
end
|
166
|
+
|
167
|
+
def remove_cookie(options)
|
168
|
+
page.command("Network.deleteCookies", **options)
|
169
|
+
end
|
170
|
+
|
171
|
+
def clear_cookies
|
172
|
+
page.command("Network.clearBrowserCookies")
|
173
|
+
end
|
174
|
+
|
175
|
+
def set_http_auth(user, password)
|
176
|
+
raise NotImplementedError
|
177
|
+
end
|
178
|
+
|
179
|
+
def page_settings=(settings)
|
180
|
+
raise NotImplementedError
|
181
|
+
end
|
182
|
+
|
183
|
+
def url_whitelist=(whitelist)
|
184
|
+
@url_whitelist = Array(whitelist).map { |p| { urlPattern: p } }
|
185
|
+
page.command("Network.setRequestInterception", patterns: @url_whitelist)
|
186
|
+
end
|
187
|
+
|
188
|
+
def url_blacklist=(blacklist)
|
189
|
+
# FIXME: We have to change the format and make it compatible with Chrome not PhantomJS
|
190
|
+
@url_blacklist = Array(blacklist).map { |p| { urlPattern: p.include?("*") ? p : "*#{p}*" } }
|
191
|
+
page.command("Network.setRequestInterception", patterns: @url_blacklist)
|
192
|
+
end
|
193
|
+
|
194
|
+
def clear_memory_cache
|
195
|
+
page.command("Network.clearBrowserCache")
|
196
|
+
end
|
197
|
+
|
198
|
+
def accept_confirm
|
199
|
+
raise NotImplementedError
|
200
|
+
end
|
201
|
+
|
202
|
+
def dismiss_confirm
|
203
|
+
raise NotImplementedError
|
204
|
+
end
|
205
|
+
|
206
|
+
def accept_prompt(response)
|
207
|
+
raise NotImplementedError
|
208
|
+
end
|
209
|
+
|
210
|
+
def dismiss_prompt
|
211
|
+
raise NotImplementedError
|
212
|
+
end
|
213
|
+
|
214
|
+
def modal_message
|
215
|
+
raise NotImplementedError
|
216
|
+
end
|
217
|
+
|
218
|
+
def reset
|
219
|
+
@headers = {}
|
220
|
+
targets.reset
|
221
|
+
end
|
222
|
+
|
223
|
+
def restart
|
224
|
+
quit
|
225
|
+
start
|
226
|
+
end
|
227
|
+
|
228
|
+
def quit
|
229
|
+
@client.close
|
230
|
+
@process.stop
|
231
|
+
@client = @process = @targets = nil
|
232
|
+
end
|
233
|
+
|
234
|
+
def crash
|
235
|
+
command("Browser.crash")
|
236
|
+
end
|
237
|
+
|
238
|
+
def command(*args)
|
239
|
+
id = @client.command(*args)
|
240
|
+
@client.wait(id: id)
|
241
|
+
rescue DeadBrowser
|
242
|
+
restart
|
243
|
+
raise
|
244
|
+
end
|
245
|
+
|
246
|
+
def targets
|
247
|
+
@targets ||= Targets.new(self)
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
def start
|
253
|
+
@headers = {}
|
254
|
+
@process = Process.start(@options)
|
255
|
+
@client = Client.new(self, @process.ws_url)
|
256
|
+
end
|
257
|
+
|
258
|
+
def check_render_options!(options)
|
259
|
+
return if !options[:full] || !options.key?(:selector)
|
260
|
+
warn "Ignoring :selector in #render since :full => true was given at #{caller(1..1).first}"
|
261
|
+
options.delete(:selector)
|
262
|
+
end
|
263
|
+
|
264
|
+
def find_all(method, selector, within = nil)
|
265
|
+
begin
|
266
|
+
elements = if within
|
267
|
+
evaluate("_cuprite.find(arguments[0], arguments[1], arguments[2])", method, selector, within)
|
268
|
+
else
|
269
|
+
evaluate("_cuprite.find(arguments[0], arguments[1])", method, selector)
|
270
|
+
end
|
271
|
+
|
272
|
+
elements.map do |element|
|
273
|
+
# nodeType: 3, nodeName: "#text" e.g.
|
274
|
+
target_id, node = element.values_at("target_id", "node")
|
275
|
+
next if node["nodeType"] != 1
|
276
|
+
within ? node : [target_id, node]
|
277
|
+
end.compact
|
278
|
+
rescue JavaScriptError => e
|
279
|
+
if e.class_name == "InvalidSelector"
|
280
|
+
raise InvalidSelector.new(e.response, method, selector)
|
281
|
+
end
|
282
|
+
raise
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "cuprite/browser/web_socket"
|
5
|
+
|
6
|
+
module Capybara::Cuprite
|
7
|
+
class Browser
|
8
|
+
class Client
|
9
|
+
class IdError < RuntimeError; end
|
10
|
+
|
11
|
+
def initialize(browser, ws_url)
|
12
|
+
@command_id = 0
|
13
|
+
@subscribed = Hash.new { |h, k| h[k] = [] }
|
14
|
+
@browser = browser
|
15
|
+
@commands = Queue.new
|
16
|
+
@ws = WebSocket.new(ws_url, @browser.logger)
|
17
|
+
|
18
|
+
@thread = Thread.new do
|
19
|
+
while message = @ws.messages.pop
|
20
|
+
method, params = message.values_at("method", "params")
|
21
|
+
if method
|
22
|
+
@subscribed[method].each { |b| b.call(params) }
|
23
|
+
else
|
24
|
+
@commands.push(message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@commands.close
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def command(method, params = {})
|
33
|
+
message = build_message(method, params)
|
34
|
+
@ws.send_message(message)
|
35
|
+
message[:id]
|
36
|
+
end
|
37
|
+
|
38
|
+
def wait(id:)
|
39
|
+
message = Timeout.timeout(@browser.timeout, TimeoutError) { @commands.pop }
|
40
|
+
raise DeadBrowser unless message
|
41
|
+
raise IdError if message["id"] != id
|
42
|
+
error, response = message.values_at("error", "result")
|
43
|
+
raise BrowserError.new(error) if error
|
44
|
+
response
|
45
|
+
rescue IdError
|
46
|
+
retry
|
47
|
+
end
|
48
|
+
|
49
|
+
def subscribe(event, &block)
|
50
|
+
@subscribed[event] << block
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def close
|
55
|
+
@ws.close
|
56
|
+
@thread.kill
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def build_message(method, params)
|
62
|
+
{ method: method, params: params }.merge(id: next_command_id)
|
63
|
+
end
|
64
|
+
|
65
|
+
def next_command_id
|
66
|
+
@command_id += 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Capybara::Cuprite
|
2
|
+
class Browser
|
3
|
+
module DOM
|
4
|
+
def current_url
|
5
|
+
evaluate_in(@execution_context_id, "location.href")
|
6
|
+
end
|
7
|
+
|
8
|
+
def title
|
9
|
+
evaluate_in(@execution_context_id, "document.title")
|
10
|
+
end
|
11
|
+
|
12
|
+
def body
|
13
|
+
evaluate("document.documentElement.outerHTML")
|
14
|
+
end
|
15
|
+
|
16
|
+
def all_text(node)
|
17
|
+
evaluate_on(node: node, expr: "this.textContent")
|
18
|
+
end
|
19
|
+
|
20
|
+
def property(node, name)
|
21
|
+
evaluate_on(node: node, expr: %Q(this["#{name}"]))
|
22
|
+
end
|
23
|
+
|
24
|
+
def attributes(node)
|
25
|
+
value = evaluate_on(node: node, expr: "_cuprite.getAttributes(this)")
|
26
|
+
JSON.parse(value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def attribute(node, name)
|
30
|
+
evaluate_on(node: node, expr: %Q(_cuprite.getAttribute(this, "#{name}")))
|
31
|
+
end
|
32
|
+
|
33
|
+
def value(node)
|
34
|
+
evaluate_on(node: node, expr: "_cuprite.value(this)")
|
35
|
+
end
|
36
|
+
|
37
|
+
def visible?(node)
|
38
|
+
evaluate_on(node: node, expr: "_cuprite.isVisible(this)")
|
39
|
+
end
|
40
|
+
|
41
|
+
def disabled?(node)
|
42
|
+
evaluate_on(node: node, expr: "_cuprite.isDisabled(this)")
|
43
|
+
end
|
44
|
+
|
45
|
+
def path(node)
|
46
|
+
evaluate_on(node: node, expr: "_cuprite.path(this)")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Capybara::Cuprite
|
2
|
+
class Browser
|
3
|
+
module Frame
|
4
|
+
def execution_context_id
|
5
|
+
@mutex.synchronize do
|
6
|
+
if !@frame_stack.empty?
|
7
|
+
@frames[@frame_stack.last]["execution_context_id"]
|
8
|
+
else
|
9
|
+
@execution_context_id
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def frame_url
|
15
|
+
evaluate("window.location.href")
|
16
|
+
end
|
17
|
+
|
18
|
+
def frame_title
|
19
|
+
evaluate("document.title")
|
20
|
+
end
|
21
|
+
|
22
|
+
def switch_to_frame(handle)
|
23
|
+
case handle
|
24
|
+
when Capybara::Node::Base
|
25
|
+
@frame_stack << handle.native.node["frameId"]
|
26
|
+
inject_extensions
|
27
|
+
when :parent
|
28
|
+
@frame_stack.pop
|
29
|
+
when :top
|
30
|
+
@frame_stack = []
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def subscribe_events
|
37
|
+
@client.subscribe("Page.frameAttached") do |params|
|
38
|
+
@frames[params["frameId"]] = { "parent_id" => params["parentFrameId"] }
|
39
|
+
end
|
40
|
+
|
41
|
+
@client.subscribe("Page.frameStartedLoading") do |params|
|
42
|
+
@waiting_frames << params["frameId"]
|
43
|
+
@mutex.try_lock
|
44
|
+
end
|
45
|
+
|
46
|
+
@client.subscribe("Page.frameNavigated") do |params|
|
47
|
+
id = params["frame"]["id"]
|
48
|
+
if frame = @frames[id]
|
49
|
+
frame.merge!(params["frame"].slice("name", "url"))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@client.subscribe("Page.frameScheduledNavigation") do |params|
|
54
|
+
# Trying to lock mutex if frame is the main frame
|
55
|
+
@waiting_frames << params["frameId"]
|
56
|
+
@mutex.try_lock
|
57
|
+
end
|
58
|
+
|
59
|
+
@client.subscribe("Page.frameStoppedLoading") do |params|
|
60
|
+
# `DOM.performSearch` doesn't work without getting #document node first.
|
61
|
+
# It returns node with nodeId 1 and nodeType 9 from which descend the
|
62
|
+
# tree and we save it in a variable because if we call that again root
|
63
|
+
# node will change the id and all subsequent nodes have to change id too.
|
64
|
+
# `command` is not allowed in the block as it will deadlock the process.
|
65
|
+
if params["frameId"] == @frame_id
|
66
|
+
signal if @waiting_frames.empty?
|
67
|
+
@client.command("DOM.getDocument", depth: 0)
|
68
|
+
end
|
69
|
+
|
70
|
+
if @waiting_frames.include?(params["frameId"])
|
71
|
+
@waiting_frames.delete(params["frameId"])
|
72
|
+
signal if @waiting_frames.empty?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
@client.subscribe("Runtime.executionContextCreated") do |params|
|
77
|
+
frame_id = params.dig("context", "auxData", "frameId")
|
78
|
+
execution_context_id = params.dig("context", "id")
|
79
|
+
|
80
|
+
# Remember the very first frame since it's the main one
|
81
|
+
@frame_id ||= frame_id
|
82
|
+
@execution_context_id ||= execution_context_id
|
83
|
+
|
84
|
+
if @frames[frame_id]
|
85
|
+
@frames[frame_id].merge!("execution_context_id" => execution_context_id)
|
86
|
+
else
|
87
|
+
@frames[frame_id] = { "execution_context_id" => execution_context_id }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
@client.subscribe("Runtime.executionContextDestroyed") do |params|
|
92
|
+
execution_context_id = params["executionContextId"]
|
93
|
+
id, frame = @frames.find { |_, p| p["execution_context_id"] == execution_context_id }
|
94
|
+
frame["execution_context_id"] = nil if frame
|
95
|
+
|
96
|
+
if @execution_context_id == execution_context_id
|
97
|
+
@execution_context_id = nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
@client.subscribe("Runtime.executionContextsCleared") do
|
102
|
+
# If we didn't have time to set context id at the beginning we have
|
103
|
+
# to set lock and release it when we set something.
|
104
|
+
@execution_context_id = nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|