vapir-firefox 1.8.1 → 1.9.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.
- data/lib/vapir-firefox/browser.rb +226 -115
- data/lib/vapir-firefox/clear_tracks.rb +6 -6
- data/lib/vapir-firefox/config.rb +5 -0
- data/lib/vapir-firefox/container.rb +74 -65
- data/lib/vapir-firefox/element.rb +13 -13
- data/lib/vapir-firefox/firefox_socket/base.rb +790 -0
- data/lib/vapir-firefox/firefox_socket/jssh.rb +49 -0
- data/lib/vapir-firefox/firefox_socket/mozrepl.rb +58 -0
- data/lib/vapir-firefox/{prototype.functional.js → firefox_socket/prototype.functional.js} +0 -0
- data/lib/vapir-firefox/javascript_object.rb +736 -0
- data/lib/vapir-firefox/modal_dialog.rb +4 -4
- data/lib/vapir-firefox/page_container.rb +4 -4
- data/lib/vapir-firefox/version.rb +1 -1
- metadata +16 -13
- data/lib/vapir-firefox/jssh_socket.rb +0 -1418
@@ -9,12 +9,12 @@ module Vapir
|
|
9
9
|
# ["cache", "cookies", "offlineApps", "history", "formdata", "downloads", "passwords", "sessions", "siteSettings"]
|
10
10
|
def sanitizer # :nodoc:
|
11
11
|
@@sanitizer ||= begin
|
12
|
-
sanitizer_class =
|
13
|
-
|
14
|
-
loader =
|
12
|
+
sanitizer_class = firefox_socket.root['Sanitizer']
|
13
|
+
unless sanitizer_class
|
14
|
+
loader = firefox_socket.Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(firefox_socket.Components.interfaces.mozIJSSubScriptLoader)
|
15
15
|
loader.loadSubScript("chrome://browser/content/sanitize.js")
|
16
|
-
sanitizer_class =
|
17
|
-
end
|
16
|
+
sanitizer_class = firefox_socket.root['Sanitizer']
|
17
|
+
end
|
18
18
|
sanitizer_class.new
|
19
19
|
end
|
20
20
|
end
|
@@ -24,7 +24,7 @@ module Vapir
|
|
24
24
|
end
|
25
25
|
def clear_cookies
|
26
26
|
sanitizer.items.cookies.clear()
|
27
|
-
#cookie_manager =
|
27
|
+
#cookie_manager = firefox_socket.Components.classes["@mozilla.org/cookiemanager;1"].getService(firefox_socket.Components.interfaces.nsICookieManager)
|
28
28
|
#cookie_manager.removeAll()
|
29
29
|
end
|
30
30
|
def clear_cache
|
data/lib/vapir-firefox/config.rb
CHANGED
@@ -8,6 +8,11 @@ module Vapir
|
|
8
8
|
# add firefox-specific stuff to base, and then bring them in from env and yaml
|
9
9
|
@base_configuration.create(:firefox_profile)
|
10
10
|
@base_configuration.create(:firefox_binary_path)
|
11
|
+
@base_configuration.create_update(:firefox_extension, 'jssh')
|
12
|
+
@base_configuration.create(:firefox_mozrepl_port)
|
13
|
+
@base_configuration.create(:firefox_mozrepl_host)
|
14
|
+
@base_configuration.create(:firefox_jssh_port)
|
15
|
+
@base_configuration.create(:firefox_jssh_host)
|
11
16
|
@base_configuration.create_update(:firefox_quit_sleep_time, 4, :validator => :numeric)
|
12
17
|
@configurations.update_from_source
|
13
18
|
class Firefox
|
@@ -44,7 +44,7 @@ module Vapir
|
|
44
44
|
include Vapir::Container
|
45
45
|
|
46
46
|
def extra_for_contained
|
47
|
-
base_extra_for_contained.merge(:
|
47
|
+
base_extra_for_contained.merge(:firefox_socket => firefox_socket)
|
48
48
|
end
|
49
49
|
|
50
50
|
public
|
@@ -52,7 +52,7 @@ module Vapir
|
|
52
52
|
# Refer: https://developer.mozilla.org/en/DOM/document.evaluate
|
53
53
|
def element_objects_by_xpath(xpath)
|
54
54
|
elements=[]
|
55
|
-
result=document_object.evaluate(xpath, containing_object, nil,
|
55
|
+
result=document_object.evaluate(xpath, containing_object, nil, firefox_socket.Components.interfaces.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, nil)
|
56
56
|
while element=result.iterateNext
|
57
57
|
elements << element
|
58
58
|
end
|
@@ -62,7 +62,7 @@ module Vapir
|
|
62
62
|
# Returns the first element object that matches the given XPath query.
|
63
63
|
# Refer: http://developer.mozilla.org/en/docs/DOM:document.evaluate
|
64
64
|
def element_object_by_xpath(xpath)
|
65
|
-
document_object.evaluate(xpath, containing_object, nil,
|
65
|
+
document_object.evaluate(xpath, containing_object, nil, firefox_socket.Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, nil).singleNodeValue
|
66
66
|
end
|
67
67
|
|
68
68
|
# Returns the first element that matches the given xpath expression or query.
|
@@ -80,70 +80,79 @@ module Vapir
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
|
-
# returns a
|
84
|
-
#
|
83
|
+
# returns a JavascriptObject representing an array of text nodes below this element in the DOM
|
84
|
+
# heirarchy which are visible - that is, their parent element is visible.
|
85
|
+
#
|
86
|
+
# same as the Vapir::Common #visible_text_nodes implementation, but much much faster.
|
85
87
|
def visible_text_nodes
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
{ return function(node, parent_visibility)
|
90
|
-
{ if(node.nodeType==1 || node.nodeType==9)
|
91
|
-
{ var style = node.nodeType==1 ? document_object.defaultView.getComputedStyle(node, null) : null;
|
92
|
-
var our_visibility = style && style.visibility;
|
93
|
-
if(!(our_visibility && $A(['hidden', 'collapse', 'visible']).include(our_visibility.toLowerCase())))
|
94
|
-
{ our_visibility = parent_visibility;
|
95
|
-
}
|
96
|
-
var display = style && style.display;
|
97
|
-
if(display && display.toLowerCase()=='none')
|
98
|
-
{ return [];
|
99
|
-
}
|
100
|
-
else
|
101
|
-
{ return $A(node.childNodes).inject([], function(result, child_node)
|
102
|
-
{ return result.concat(recurse(child_node, our_visibility));
|
103
|
-
});
|
104
|
-
}
|
105
|
-
}
|
106
|
-
else if(node.nodeType==3)
|
107
|
-
{ if(parent_visibility && $A(['hidden', 'collapse']).include(parent_visibility.toLowerCase()))
|
108
|
-
{ return [];
|
109
|
-
}
|
110
|
-
else
|
111
|
-
{ return [node.data];
|
112
|
-
}
|
113
|
-
}
|
114
|
-
else
|
115
|
-
{ return [];
|
116
|
-
}
|
117
|
-
};
|
118
|
-
});
|
119
|
-
var element_to_check = element_object;
|
120
|
-
var real_visibility = null;
|
121
|
-
while(element_to_check)
|
122
|
-
{ var style = element_to_check.nodeType==1 ? document_object.defaultView.getComputedStyle(element_object, null) : null;
|
123
|
-
if(style)
|
124
|
-
{ // only pay attention to the innermost definition that really defines visibility - one of 'hidden', 'collapse' (only for table elements),
|
125
|
-
// or 'visible'. ignore 'inherit'; keep looking upward.
|
126
|
-
// this makes it so that if we encounter an explicit 'visible', we don't pay attention to any 'hidden' further up.
|
127
|
-
// this style is inherited - may be pointless for firefox, but IE uses the 'inherited' value. not sure if/when ff does.
|
128
|
-
if(real_visibility==null && (visibility=style.visibility))
|
129
|
-
{ var visibility=visibility.toLowerCase();
|
130
|
-
if($A(['hidden', 'collapse', 'visible']).include(visibility))
|
131
|
-
{ real_visibility=visibility;
|
132
|
-
}
|
133
|
-
}
|
134
|
-
// check for display property. this is not inherited, and a parent with display of 'none' overrides an immediate visibility='visible'
|
135
|
-
var display=style.display;
|
136
|
-
if(display && (display=display.toLowerCase())=='none')
|
137
|
-
{ // if display is none, then this element is not visible, and thus has no visible text nodes underneath.
|
138
|
-
return [];
|
139
|
-
}
|
140
|
-
}
|
141
|
-
element_to_check=element_to_check.parentNode;
|
142
|
-
}
|
143
|
-
return recurse_text_nodes(element_object, real_visibility);
|
144
|
-
)
|
145
|
-
end.to_array
|
88
|
+
assert_exists do
|
89
|
+
visible_text_nodes_method.call(containing_object, document_object).to_array
|
90
|
+
end
|
146
91
|
end
|
147
92
|
|
93
|
+
# takes one argument, a proc or a JavascriptObject representing a function in javascript.
|
94
|
+
# this will be yielded successive dom nodes, and should return true if the node matches whatever
|
95
|
+
# criteria you care to match; false otherwise.
|
96
|
+
#
|
97
|
+
# returns an ElementCollection consisting of the deepest elements within the dom heirarchy
|
98
|
+
# which match the given match_proc_or_function.
|
99
|
+
def innermost_by_node(match_proc_or_function)
|
100
|
+
if match_proc_or_function.is_a?(JavascriptObject)
|
101
|
+
ElementCollection.new(self, base_element_class, extra_for_contained.merge(:candidates => proc do |container|
|
102
|
+
firefox_socket.call_function(:match_function => match_proc_or_function, :containing_object => container.containing_object) do
|
103
|
+
%Q(
|
104
|
+
return Vapir.Ycomb(function(innermost_matching_nodes)
|
105
|
+
{ return function(container_node)
|
106
|
+
{ var child_nodes = $A(container_node.childNodes);
|
107
|
+
var matched_child_elements = child_nodes.select(function(node){ return node.nodeType==1 && match_function(node); });
|
108
|
+
if(matched_child_elements.length==0)
|
109
|
+
{ return [container_node];
|
110
|
+
}
|
111
|
+
else
|
112
|
+
{ return matched_child_elements.map(function(matched_child_element)
|
113
|
+
{ return innermost_matching_nodes(matched_child_element);
|
114
|
+
}).inject([], function(a, b){ return a.concat(b); });
|
115
|
+
}
|
116
|
+
}
|
117
|
+
})(containing_object);
|
118
|
+
)
|
119
|
+
end.to_array
|
120
|
+
end))
|
121
|
+
else
|
122
|
+
base_innermost_by_node(match_proc_or_function)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# takes text or regexp, and returns an ElementCollection consisting of deepest (innermost) elements in the dom heirarchy whose visible text
|
127
|
+
# matches what's given (by substring for text; by regexp match for regexp)
|
128
|
+
def innermost_matching_visible_text(text_or_regexp)
|
129
|
+
innermost_by_node(firefox_socket.call_function(:document_object => document_object, :text_or_regexp => text_or_regexp) do
|
130
|
+
%Q(
|
131
|
+
return function(node)
|
132
|
+
{ return Vapir.visible_text_nodes(node, document_object).join('').match(text_or_regexp);
|
133
|
+
};
|
134
|
+
)
|
135
|
+
end.to_function)
|
136
|
+
end
|
137
|
+
private
|
138
|
+
# returns a javascript function that takes a node and a document object, and returns
|
139
|
+
# true if the element's display property will allow it to be displayed; false if not.
|
140
|
+
def element_displayed_method
|
141
|
+
@element_displayed_method ||= firefox_socket.root.Vapir['element_displayed']
|
142
|
+
end
|
143
|
+
# returns a javascript function that takes a node and a document object, and returns
|
144
|
+
# the visibility of that node, obtained by ascending the dom until an explicit
|
145
|
+
# definition for visibility is found.
|
146
|
+
def element_real_visibility_method
|
147
|
+
@element_real_visibility_method ||= firefox_socket.root.Vapir['element_real_visibility']
|
148
|
+
end
|
149
|
+
|
150
|
+
# returns a proc that takes a node and a document object, and returns
|
151
|
+
# an Array of strings, each of which is the data of a text node beneath the given node which
|
152
|
+
# is visible.
|
153
|
+
def visible_text_nodes_method
|
154
|
+
@visible_text_nodes_method ||= firefox_socket.root.Vapir['visible_text_nodes']
|
155
|
+
end
|
148
156
|
end
|
149
157
|
end # module
|
158
|
+
|
@@ -10,17 +10,17 @@ module Vapir
|
|
10
10
|
|
11
11
|
# Creates new instance of Firefox::Element.
|
12
12
|
def initialize(how, what, extra={})
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
unless @
|
18
|
-
raise RuntimeError, "No
|
13
|
+
@firefox_socket=extra[:firefox_socket]
|
14
|
+
@firefox_socket||= (extra[:browser].firefox_socket if extra[:browser])
|
15
|
+
@firefox_socket||= (extra[:container].firefox_socket if extra[:container])
|
16
|
+
@firefox_socket||= (what.firefox_socket if how==:element_object)
|
17
|
+
unless @firefox_socket
|
18
|
+
raise RuntimeError, "No firefox socket given! Firefox elements need this (specified in the :firefox_socket key of the extra hash)"
|
19
19
|
end
|
20
20
|
default_initialize(how, what, extra)
|
21
21
|
end
|
22
22
|
|
23
|
-
attr_reader :
|
23
|
+
attr_reader :firefox_socket
|
24
24
|
|
25
25
|
def outer_html
|
26
26
|
temp_parent_element=document_object.createElement('div')
|
@@ -65,7 +65,7 @@ module Vapir
|
|
65
65
|
event=create_event_object(event_type, options)
|
66
66
|
if !options[:wait]
|
67
67
|
raise "need a content window on which to setTimeout if we are not waiting" unless content_window_object
|
68
|
-
content_window_object.setTimeout(
|
68
|
+
content_window_object.setTimeout(firefox_socket.call_function(:element_object => element_object, :event => event){ "return function(){ element_object.dispatchEvent(event) };" }, 0)
|
69
69
|
nil
|
70
70
|
else
|
71
71
|
result=element_object.dispatchEvent(event)
|
@@ -84,7 +84,7 @@ module Vapir
|
|
84
84
|
:right => 2,
|
85
85
|
}
|
86
86
|
|
87
|
-
# returns an object representing an event (a
|
87
|
+
# returns an object representing an event (a javascript object)
|
88
88
|
def create_event_object(event_type, options)
|
89
89
|
event_type = event_type.to_s.downcase # in case event_type was given as a symbol
|
90
90
|
if event_type =~ /\Aon(.*)\z/i
|
@@ -180,7 +180,7 @@ module Vapir
|
|
180
180
|
mouse_down_event=create_event_object('mousedown', options)
|
181
181
|
mouse_up_event=create_event_object('mouseup', options)
|
182
182
|
click_event=create_event_object('click', options)
|
183
|
-
content_window_object.setTimeout(
|
183
|
+
content_window_object.setTimeout(firefox_socket.call_function(:element_object => element_object, :mouse_down_event => mouse_down_event, :mouse_up_event => mouse_up_event, :click_event => click_event) do
|
184
184
|
" return function()
|
185
185
|
{ element_object.dispatchEvent(mouse_down_event);
|
186
186
|
element_object.dispatchEvent(mouse_up_event);
|
@@ -209,7 +209,7 @@ module Vapir
|
|
209
209
|
# or a parent is hidden.
|
210
210
|
def visible?
|
211
211
|
assert_exists do
|
212
|
-
|
212
|
+
firefox_socket.call_function(:element_to_check => element_object, :document_object => document_object) do %Q(
|
213
213
|
var really_visible=null;
|
214
214
|
while(element_to_check) //&& !(element_to_check instanceof Components.interfaces.nsIDOMDocument)
|
215
215
|
{ var style = element_to_check.nodeType==1 ? document_object.defaultView.getComputedStyle(element_to_check, null) : null;
|
@@ -245,7 +245,7 @@ module Vapir
|
|
245
245
|
|
246
246
|
|
247
247
|
def self.element_object_style(element_object, document_object)
|
248
|
-
if element_object.nodeType==1 #element_object.instanceof(element_object.
|
248
|
+
if element_object.nodeType==1 #element_object.instanceof(element_object.firefox_socket.Components.interfaces.nsIDOMDocument)
|
249
249
|
document_object.defaultView.getComputedStyle(element_object, nil)
|
250
250
|
else
|
251
251
|
nil
|
@@ -258,7 +258,7 @@ module Vapir
|
|
258
258
|
private
|
259
259
|
def element_object_exists?
|
260
260
|
return false unless @element_object
|
261
|
-
return
|
261
|
+
return firefox_socket.call_function(:parent => @element_object, :document_object => container.document_object) do # use the container's document so that frames look at their parent document, not their own document
|
262
262
|
" while(true)
|
263
263
|
{ if(!parent)
|
264
264
|
{ return false; // if we encounter a parent such that parentNode is nil, we aren't on the document.
|
@@ -0,0 +1,790 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'socket'
|
3
|
+
require 'timeout'
|
4
|
+
require 'vapir-common/config'
|
5
|
+
require 'vapir-firefox/javascript_object'
|
6
|
+
require 'vapir-common/external/core_extensions'
|
7
|
+
#require 'logger'
|
8
|
+
|
9
|
+
# :stopdoc:
|
10
|
+
#class LoggerWithCallstack < Logger
|
11
|
+
# class TimeElapsedFormatter < Formatter
|
12
|
+
# def initialize
|
13
|
+
# super
|
14
|
+
# @time_started=Time.now
|
15
|
+
# end
|
16
|
+
# def format_datetime(time)
|
17
|
+
# "%10.3f"%(time.to_f-@time_started.to_f)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# end
|
21
|
+
# def add(severity, message = nil, progname = nil, &block)
|
22
|
+
# severity ||= UNKNOWN
|
23
|
+
# if @logdev.nil? or severity < @level
|
24
|
+
# return true
|
25
|
+
# end
|
26
|
+
# progname ||= @progname
|
27
|
+
# if message.nil?
|
28
|
+
# if block_given?
|
29
|
+
# message = yield
|
30
|
+
# else
|
31
|
+
# message = progname
|
32
|
+
# progname = @progname
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
# message=message.to_s+" FROM: "+caller.map{|c|"\t\t#{c}\n"}.join("")
|
36
|
+
# @logdev.write(
|
37
|
+
# format_message(format_severity(severity), Time.now, progname, message))
|
38
|
+
# true
|
39
|
+
# end
|
40
|
+
#end
|
41
|
+
|
42
|
+
# :startdoc:
|
43
|
+
|
44
|
+
class IPSocket
|
45
|
+
# sends the whole message
|
46
|
+
#
|
47
|
+
# returns the number of broken-up sends that occured.
|
48
|
+
def sendall(message, flags=0)
|
49
|
+
bytes_sent = 0
|
50
|
+
packets = 0
|
51
|
+
while bytes_sent < message.length
|
52
|
+
send_result = send(message[bytes_sent..-1], flags)
|
53
|
+
bytes_sent+= send_result
|
54
|
+
packets+= 1
|
55
|
+
end
|
56
|
+
packets
|
57
|
+
end
|
58
|
+
# takes a timeout, and returns true if Kernel.select indicates that the socket
|
59
|
+
# is ready to read within that timeout. if Kernel.select indicates that the socket
|
60
|
+
# is in an error condition, this method will raise
|
61
|
+
def ready_to_recv?(timeout)
|
62
|
+
select_result = Kernel.select([self], nil, [self], timeout)
|
63
|
+
read_result, write_result, err_result = *select_result
|
64
|
+
if select_result && err_result.include?(self)
|
65
|
+
# never actually seen this condition, so not sure what error class to put here
|
66
|
+
raise "the socket indicated an error condition when checking that it was ready for reading"
|
67
|
+
else
|
68
|
+
return select_result && read_result.include?(self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# base exception class for all exceptions raised from FirefoxSocket
|
74
|
+
class FirefoxSocketError < StandardError;end
|
75
|
+
# this exception covers all connection errors either on startup or during usage. often it represents an Errno error such as Errno::ECONNRESET.
|
76
|
+
class FirefoxSocketConnectionError < FirefoxSocketError;end
|
77
|
+
# This exception is thrown if th FirefoxSocket is unable to initially connect.
|
78
|
+
class FirefoxSocketUnableToStart < FirefoxSocketConnectionError;end
|
79
|
+
# Represents an error encountered on the javascript side, caught in a try/catch block.
|
80
|
+
class FirefoxSocketJavascriptError < FirefoxSocketError
|
81
|
+
attr_accessor :source, :name, :js_err, :lineNumber, :stack, :fileName
|
82
|
+
end
|
83
|
+
# represents a syntax error in javascript.
|
84
|
+
class FirefoxSocketSyntaxError < FirefoxSocketJavascriptError;end
|
85
|
+
# raised when a javascript value is expected to be defined but is undefined
|
86
|
+
class FirefoxSocketUndefinedValueError < FirefoxSocketJavascriptError;end
|
87
|
+
|
88
|
+
# Base class for connecting to a firefox extension over a TCP socket.
|
89
|
+
# does the work of interacting with the socket and translating ruby values to javascript and back.
|
90
|
+
class FirefoxSocket
|
91
|
+
# :stopdoc:
|
92
|
+
# def self.logger
|
93
|
+
# @@logger||=begin
|
94
|
+
# logfile=File.open('c:/tmp/jssh_log.txt', File::WRONLY|File::TRUNC|File::CREAT)
|
95
|
+
# logfile.sync=true
|
96
|
+
# logger=Logger.new(logfile)
|
97
|
+
# logger.level = -1#Logger::DEBUG#Logger::INFO
|
98
|
+
# #logger.formatter=LoggerWithCallstack::TimeElapsedFormatter.new
|
99
|
+
# logger
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
# def logger
|
103
|
+
# self.class.logger
|
104
|
+
# end
|
105
|
+
|
106
|
+
PrototypeFile=File.join(File.dirname(__FILE__), "prototype.functional.js")
|
107
|
+
# :startdoc:
|
108
|
+
|
109
|
+
@base_configuration=Vapir::Configuration.new(nil) do |config|
|
110
|
+
config.create_update :host, 'localhost'
|
111
|
+
config.create :port, :validator => :positive_integer
|
112
|
+
config.create_update :default_timeout, 64, :validator => :numeric
|
113
|
+
config.create_update :short_timeout, (2**-2).to_f, :validator => :numeric
|
114
|
+
config.create_update :read_size, 4096, :validator => :positive_integer
|
115
|
+
end
|
116
|
+
@configuration_parent=@base_configuration
|
117
|
+
extend Vapir::Configurable
|
118
|
+
|
119
|
+
include Vapir::Configurable
|
120
|
+
def configuration_parent
|
121
|
+
self.class.config
|
122
|
+
end
|
123
|
+
|
124
|
+
# the host to which this socket is connected
|
125
|
+
def host
|
126
|
+
config.host
|
127
|
+
end
|
128
|
+
# the port on which this socket is connected
|
129
|
+
def port
|
130
|
+
config.port
|
131
|
+
end
|
132
|
+
|
133
|
+
# Connects a new socket to firefox
|
134
|
+
#
|
135
|
+
# Takes options:
|
136
|
+
# * :host => the ip to connect to, default localhost
|
137
|
+
# * :port => the port to connect to
|
138
|
+
def initialize(options={})
|
139
|
+
config.update_hash options
|
140
|
+
require 'thread'
|
141
|
+
@mutex = Mutex.new
|
142
|
+
handling_connection_error(:exception => FirefoxSocketUnableToStart.new("Could not connect to Firefox on #{host}:#{port}. Ensure that Firefox is running and has the extension listening on that port, or try restarting firefox.")) do
|
143
|
+
@socket = TCPSocket::new(host, port)
|
144
|
+
@socket.sync = true
|
145
|
+
@expecting_prompt=false # initially, the welcome message comes before the prompt, so this so this is false to start with
|
146
|
+
@expecting_extra_maybe=false
|
147
|
+
eat_welcome_message
|
148
|
+
end
|
149
|
+
initialize_environment
|
150
|
+
@temp_object = object('VapirTemp')
|
151
|
+
ret=send_and_read(File.read(PrototypeFile))
|
152
|
+
if ret !~ /done!/
|
153
|
+
@expecting_extra_maybe=true
|
154
|
+
raise FirefoxSocketError, "Something went wrong loading Prototype - message #{ret.inspect}"
|
155
|
+
end
|
156
|
+
# Y combinator in javascript.
|
157
|
+
#
|
158
|
+
# example - recursive length function.
|
159
|
+
#
|
160
|
+
# >> length=firefox_socket.root.Vapir.Ycomb(firefox_socket.function(:len){ "return function(list){ return list.length==0 ? 0 : 1+len(list.slice(1)); }; " })
|
161
|
+
# => #<JavascriptObject:0x01206880 type=function, debug_name=Vapir.Ycomb(function(len){ return function(list){ return list.length==0 ? 0 : 1+len(list.slice(1)); }; })>
|
162
|
+
# >> length.call(['a', 'b', 'c'])
|
163
|
+
# => 3
|
164
|
+
root.Vapir.Ycomb=function(:gen){ "return function(f){ return f(f); }(function(f){ return gen(function(){ return f(f).apply(null, arguments); }); });" }
|
165
|
+
end
|
166
|
+
|
167
|
+
# takes a block, calls the block, and returns the result of the block - unless the connection
|
168
|
+
# fails over the course of the block. if that happens, this handles the multitude of errors
|
169
|
+
# that may be the cause of that, and deals with them in a customizable manner. by default,
|
170
|
+
# raises a FirefoxSocketConnectionError with all of the information of the original error
|
171
|
+
# attached.
|
172
|
+
#
|
173
|
+
# recognizes options:
|
174
|
+
# - :exception - an instance of an exception which will be raised, e.g.
|
175
|
+
# :exception => RuntimError.new('something went wrong!')
|
176
|
+
# - :handle may take the following values:
|
177
|
+
# - :raise (default) - raises options[:exception], by default a FirefoxSocketConnectionError
|
178
|
+
# - :ignore - discards the exception and returns nil
|
179
|
+
# - :return - returns the exception; does not raise it
|
180
|
+
# - a Proc or Method - calls the proc, giving the raised exception as an argument.
|
181
|
+
# this is useful to return from a function when the connection fails, e.g.
|
182
|
+
# connection.handling_connection_error(:handle => proc{ return false }) { [code which may cause the socket to close] }
|
183
|
+
def handling_connection_error(options={})
|
184
|
+
options=handle_options(options, :handle => :raise, :exception => FirefoxSocketConnectionError.new("Encountered an error on the socket."))
|
185
|
+
begin
|
186
|
+
yield
|
187
|
+
rescue FirefoxSocketConnectionError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, SystemCallError
|
188
|
+
@expecting_extra_maybe = true
|
189
|
+
error = options[:exception].class.new(options[:exception].message + "\n#{$!.class}\n#{$!.message}")
|
190
|
+
error.set_backtrace($!.backtrace)
|
191
|
+
case options[:handle]
|
192
|
+
when :raise
|
193
|
+
raise error
|
194
|
+
when :ignore
|
195
|
+
nil
|
196
|
+
when :return
|
197
|
+
error
|
198
|
+
when Proc, Method
|
199
|
+
options[:handle].call(error)
|
200
|
+
else
|
201
|
+
raise ArgumentError, "Don't know what to do when told to handle by :handle => #{options[:handle].inspect}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
# sets the error state if an exception is encountered while running the given block. the
|
208
|
+
# exception is not rescued.
|
209
|
+
def ensuring_extra_handled
|
210
|
+
begin
|
211
|
+
yield
|
212
|
+
rescue Exception
|
213
|
+
@expecting_extra_maybe = true
|
214
|
+
raise
|
215
|
+
end
|
216
|
+
end
|
217
|
+
# reads from the socket and returns what seems to be the value that should be returned, by stripping prompts
|
218
|
+
# from the beginning and end where appropriate.
|
219
|
+
#
|
220
|
+
# does not deal with prompts in between values, because attempting to parse those out is impossible, it being
|
221
|
+
# perfectly possible that a string the same as the prompt is part of actual data. (even stripping it from the
|
222
|
+
# ends of the string is not entirely certain; data could have it at the ends too, but that's the best that can
|
223
|
+
# be done.) so, read_value should be called after every line, or you can end up with stuff like:
|
224
|
+
#
|
225
|
+
# >> @socket.send "3\n4\n5\n", 0
|
226
|
+
# => 6
|
227
|
+
# >> read_value
|
228
|
+
# => "3\n> 4\n> 5"
|
229
|
+
#
|
230
|
+
# by default, read_value reads until the socket is done being ready. "done being ready" is defined as Kernel.select
|
231
|
+
# saying that the socket isn't ready after waiting for config.short_timeout. usually this will be true after a
|
232
|
+
# single read, as most things only take one #recv call to get the whole value. this waiting for config.short_timeout
|
233
|
+
# can add up to being slow if you're doing a lot of socket activity.
|
234
|
+
#
|
235
|
+
# to solve this, performance can be improved significantly using the :length_before_value option. with this, you have
|
236
|
+
# to write your javascript to return the length of the value to be sent, followed by a newline, followed by the actual
|
237
|
+
# value (which must be of the length it says it is, or this method will error).
|
238
|
+
#
|
239
|
+
# if this option is set, this doesn't do any config.short_timeout waiting once it gets the full value, it returns
|
240
|
+
# immediately.
|
241
|
+
def read_value(options={})
|
242
|
+
options=options_from_config(options, {:timeout => :default_timeout, :read_size => :read_size}, [:length_before_value])
|
243
|
+
received_data = []
|
244
|
+
value_string = ""
|
245
|
+
size_to_read=options[:read_size]
|
246
|
+
timeout=options[:timeout]
|
247
|
+
already_read_length=false
|
248
|
+
expected_size=nil
|
249
|
+
# logger.add(-1) { "RECV_SOCKET is starting. timeout=#{timeout}" }
|
250
|
+
while size_to_read > 0 && ensuring_extra_handled { @socket.ready_to_recv?(timeout) }
|
251
|
+
data = ensuring_extra_handled { @socket.recv(size_to_read) }
|
252
|
+
received_data << data
|
253
|
+
value_string << data
|
254
|
+
if @prompt && @expecting_prompt && utf8_length_safe(value_string) > @prompt.length
|
255
|
+
if value_string =~ /\A#{Regexp.escape(@prompt)}/
|
256
|
+
value_string.sub!(/\A#{Regexp.escape(@prompt)}/, '')
|
257
|
+
@expecting_prompt=false
|
258
|
+
else
|
259
|
+
value_string << clear_error
|
260
|
+
raise FirefoxSocketError, "Expected a prompt! received unexpected data #{value_string.inspect}. maybe left on the socket by last evaluated expression? last expression was:\n\n#{@last_expression}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
if !@prompt || !@expecting_prompt
|
264
|
+
if options[:length_before_value] && !already_read_length && value_string.length > 0
|
265
|
+
if value_string =~ /\A(\d+)\n/
|
266
|
+
expected_size=$1.to_i
|
267
|
+
already_read_length=true
|
268
|
+
value_string.sub!(/\A\d+\n/, '')
|
269
|
+
elsif value_string =~ /\A\d+\z/
|
270
|
+
# rather unlikely, but maybe we just received part of the number so far - ignore
|
271
|
+
else
|
272
|
+
@expecting_extra_maybe=true
|
273
|
+
raise FirefoxSocketError, "Expected length! unexpected data with no preceding length received: #{value_string.inspect}\n\nlast thing we sent was: #{@last_expression}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
if expected_size
|
277
|
+
size_to_read = expected_size - utf8_length_safe(value_string)
|
278
|
+
end
|
279
|
+
unless value_string.empty? # switch to short timeout - unless we got a prompt (leaving value_string blank). switching to short timeout when all we got was a prompt would probably accidentally leave the value on the socket.
|
280
|
+
timeout=config.short_timeout
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Kernel.select seems to indicate that a dead socket is ready to read, and returns endless blank strings to recv. rather irritating.
|
285
|
+
if received_data.length >= 3 && received_data[-3..-1].all?{|rd| rd==''}
|
286
|
+
raise FirefoxSocketConnectionError, "Socket seems to no longer be connected"
|
287
|
+
end
|
288
|
+
# logger.add(-1) { "RECV_SOCKET is continuing. timeout=#{timeout}; data=#{data.inspect}" }
|
289
|
+
end
|
290
|
+
# logger.debug { "RECV_SOCKET is done. received_data=#{received_data.inspect}; value_string=#{value_string.inspect}" }
|
291
|
+
if @expecting_extra_maybe
|
292
|
+
if @socket.ready_to_recv?(config.short_timeout)
|
293
|
+
cleared_error=clear_error
|
294
|
+
if @prompt && cleared_error==@prompt
|
295
|
+
# if all we got was the prompt, just stick it on the value here so that the code below will deal with setting @execting_prompt correctly
|
296
|
+
value_string << cleared_error
|
297
|
+
else
|
298
|
+
raise FirefoxSocketError, "We finished receiving but the socket was still ready to send! extra data received were: #{cleared_error}"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
@expecting_extra_maybe=false
|
302
|
+
end
|
303
|
+
|
304
|
+
if expected_size
|
305
|
+
value_string_length=value_string.unpack("U*").length # JSSH returns a utf-8 string, so unpack each character to get the right length
|
306
|
+
|
307
|
+
if value_string_length == expected_size
|
308
|
+
@expecting_prompt=true if @prompt
|
309
|
+
elsif @prompt && value_string_length == expected_size + @prompt.length && value_string =~ /#{Regexp.escape(@prompt)}\z/
|
310
|
+
value_string.sub!(/#{Regexp.escape(@prompt)}\z/, '')
|
311
|
+
@expecting_prompt=false
|
312
|
+
else
|
313
|
+
@expecting_extra_maybe=true if value_string_length < expected_size
|
314
|
+
raise FirefoxSocketError, "Expected a value of size #{expected_size}; received data of size #{value_string_length}: #{value_string.inspect}"
|
315
|
+
end
|
316
|
+
else
|
317
|
+
if @prompt && value_string =~ /#{Regexp.escape(@prompt)}\z/ # what if the value happens to end with the same string as the prompt?
|
318
|
+
value_string.sub!(/#{Regexp.escape(@prompt)}\z/, '')
|
319
|
+
@expecting_prompt=false
|
320
|
+
else
|
321
|
+
@expecting_prompt=true if @prompt
|
322
|
+
end
|
323
|
+
end
|
324
|
+
return value_string
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
# returns the number of complete utf-8 encoded characters in the string, without erroring on
|
329
|
+
# partial characters.
|
330
|
+
def utf8_length_safe(string)
|
331
|
+
string=string.dup
|
332
|
+
begin
|
333
|
+
string.unpack("U*").length
|
334
|
+
rescue ArgumentError # this happens when the socket receive gets split across a utf8 character. we drop the incomplete character from the end.
|
335
|
+
if $!.message =~ /malformed UTF-8 character \(expected \d+ bytes, given (\d+) bytes\)/
|
336
|
+
given=$1.to_i
|
337
|
+
string[0...(-given)].unpack("U*").length
|
338
|
+
else # otherwise, this is some other issue we weren't expecting; we will not rescue it.
|
339
|
+
raise($!.class, $!.message+"\n\ngetting utf8 length of string #{string.inspect}", $!.backtrace)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
# this should be called when an error occurs and we want to clear the socket of any value remaining on it.
|
344
|
+
# tries for config.short_timeout to see if a value will appear on the socket; if one does, returns it.
|
345
|
+
def clear_error
|
346
|
+
data=""
|
347
|
+
while @socket.ready_to_recv?(config.short_timeout)
|
348
|
+
# clear any other crap left on the socket
|
349
|
+
data << @socket.recv(config.read_size)
|
350
|
+
end
|
351
|
+
if @prompt && data =~ /#{Regexp.escape(@prompt)}\z/
|
352
|
+
@expecting_prompt=false
|
353
|
+
end
|
354
|
+
data
|
355
|
+
end
|
356
|
+
|
357
|
+
# sends the given javascript expression which is evaluated, reads the resulting value from the socket, and returns that value.
|
358
|
+
#
|
359
|
+
# options are passed to #read_value untouched; the only one that probably ought to be used here is :timeout.
|
360
|
+
def send_and_read(js_expr, options={})
|
361
|
+
# logger.add(-1) { "SEND_AND_READ is starting. options=#{options.inspect}" }
|
362
|
+
@last_expression=js_expr
|
363
|
+
js_expr=js_expr+"\n" unless js_expr =~ /\n\z/
|
364
|
+
js_expr+=@input_terminator if @input_terminator
|
365
|
+
# logger.debug { "SEND_AND_READ sending #{js_expr.inspect}" }
|
366
|
+
@mutex.synchronize do
|
367
|
+
@socket.sendall(js_expr)
|
368
|
+
return read_value(options)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
private
|
373
|
+
# creates a ruby exception from the given information and raises it.
|
374
|
+
def js_error(name, message, source, js_error_object={})
|
375
|
+
require 'stringio'
|
376
|
+
require 'pp'
|
377
|
+
pretty_js_error_object=""
|
378
|
+
PP.pp(js_error_object, StringIO.new(pretty_js_error_object))
|
379
|
+
err=FirefoxSocketJavascriptError.new("#{message}\nEvaluating:\n#{source}\n\nJavascript error object:\n#{pretty_js_error_object}")
|
380
|
+
err.name=name
|
381
|
+
err.source=source
|
382
|
+
err.js_err=js_error_object
|
383
|
+
["lineNumber", "stack", "fileName"].each do |attr|
|
384
|
+
if js_error_object.key?(attr)
|
385
|
+
err.send("#{attr}=", js_error_object[attr])
|
386
|
+
end
|
387
|
+
end
|
388
|
+
raise err
|
389
|
+
end
|
390
|
+
public
|
391
|
+
|
392
|
+
# returns a string of javascript representing the given object. if given an Array or Hash,
|
393
|
+
# operates recursively. this is like converting to JSON, but this supports more data types
|
394
|
+
# than can be represented in JSON. supported data types are:
|
395
|
+
# - Array, Set (converts to javascript Array)
|
396
|
+
# - Hash (converts to javascript Object)
|
397
|
+
# - JavascriptObject (just uses the reference the JavascriptObject represents)
|
398
|
+
# - Regexp (converts to javascript RegExp)
|
399
|
+
# - String, Symbol (converts to a javascript string)
|
400
|
+
# - Integer, Float
|
401
|
+
# - true, false, nil
|
402
|
+
def self.to_javascript(object)
|
403
|
+
if ['Array', 'Set'].any?{|klass_name| Object.const_defined?(klass_name) && object.is_a?(Object.const_get(klass_name)) }
|
404
|
+
"["+object.map{|element| to_javascript(element) }.join(", ")+"]"
|
405
|
+
elsif object.is_a?(Hash)
|
406
|
+
"{"+object.map{|(key, value)| to_javascript(key)+": "+to_javascript(value) }.join(", ")+"}"
|
407
|
+
elsif object.is_a?(JavascriptObject)
|
408
|
+
object.ref
|
409
|
+
elsif [true, false, nil].include?(object) || [Integer, Float, String, Symbol].any?{|klass| object.is_a?(klass) }
|
410
|
+
object.to_json
|
411
|
+
elsif object.is_a?(Regexp)
|
412
|
+
# get the flags javascript recognizes - not the same ones as ruby.
|
413
|
+
js_flags = {Regexp::MULTILINE => 'm', Regexp::IGNORECASE => 'i'}.inject("") do |flags, (bit, flag)|
|
414
|
+
flags + (object.options & bit > 0 ? flag : '')
|
415
|
+
end
|
416
|
+
# "new RegExp("+to_javascript(object.source)+", "+to_javascript(js_flags)+")"
|
417
|
+
js_source = object.source.empty? ? "/(?:)/" : object.inspect
|
418
|
+
js_source.sub!(/\w*\z/, '') # drop ruby flags
|
419
|
+
js_source + js_flags
|
420
|
+
else
|
421
|
+
raise "Unable to represent object as javascript: #{object.inspect} (#{object.class})"
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# returns the value of the given javascript expression, as reported by the the firefox extension.
|
426
|
+
#
|
427
|
+
# This will be a string, the given expression's toString.
|
428
|
+
def value(js)
|
429
|
+
# this is wrapped in a function so that ...
|
430
|
+
# dang, now I can't remember. I'm sure I had a good reason at the time.
|
431
|
+
send_and_read("(function(){return #{js}})()")
|
432
|
+
end
|
433
|
+
|
434
|
+
# assigns to the javascript reference on the left the javascript expression on the right.
|
435
|
+
# returns the value of the expression as reported by the firefox extension, which
|
436
|
+
# will be a string, the expression's toString. Uses #value; see its documentation.
|
437
|
+
def assign(js_left, js_right)
|
438
|
+
value("#{js_left}= #{js_right}")
|
439
|
+
end
|
440
|
+
|
441
|
+
# calls to the given function (javascript reference to a function) passing it the
|
442
|
+
# given arguments (javascript expressions). returns the return value of the function,
|
443
|
+
# a string, the toString of the javascript value. Uses #value; see its documentation.
|
444
|
+
def call(js_function, *js_args)
|
445
|
+
value("#{js_function}(#{js_args.join(', ')})")
|
446
|
+
end
|
447
|
+
|
448
|
+
# if the given javascript expression ends with an = symbol, #handle calls to #assign
|
449
|
+
# assuming it is given one argument; if the expression refers to a function, calls
|
450
|
+
# that function with the given arguments using #call; if the expression is some other
|
451
|
+
# value, returns that value (its javascript toString), calling #value, assuming
|
452
|
+
# given no arguments. Uses #value; see its documentation.
|
453
|
+
def handle(js_expr, *args)
|
454
|
+
if js_expr=~/=\z/ # doing assignment
|
455
|
+
js_left=$`
|
456
|
+
if args.size != 1
|
457
|
+
raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
|
458
|
+
end
|
459
|
+
assign(js_left, *args)
|
460
|
+
else
|
461
|
+
type=typeof(js_expr)
|
462
|
+
case type
|
463
|
+
when "function"
|
464
|
+
call(js_expr, *args)
|
465
|
+
when "undefined"
|
466
|
+
raise FirefoxSocketUndefinedValueError, "undefined expression #{js_expr.inspect}"
|
467
|
+
else
|
468
|
+
if !args.empty?
|
469
|
+
raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
|
470
|
+
end
|
471
|
+
value(js_expr)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# returns the value of the given javascript expression. Assuming that it can
|
477
|
+
# be converted to JSON, will return the equivalent ruby data type to the javascript
|
478
|
+
# value. Will raise an error if the javascript errors.
|
479
|
+
def value_json(js, options={})
|
480
|
+
send_and_read_passthrough_options=[:timeout]
|
481
|
+
options=handle_options(options, {:error_on_undefined => true}, send_and_read_passthrough_options)
|
482
|
+
raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
|
483
|
+
ref_error=options[:error_on_undefined] ? "typeof(result)=='undefined' ? {errored: true, value: {'name': 'ReferenceError', 'message': 'undefined expression in: '+result_f.toString()}} : " : ""
|
484
|
+
wrapped_js=
|
485
|
+
"try
|
486
|
+
{ var result_f=(function(){return #{js}});
|
487
|
+
var result=result_f();
|
488
|
+
nativeJSON_encode_length(#{ref_error} {errored: false, value: result});
|
489
|
+
}catch(e)
|
490
|
+
{ nativeJSON_encode_length({errored: true, value: Object.extend({}, e)});
|
491
|
+
}"
|
492
|
+
val=send_and_read(wrapped_js, options.select_keys(*send_and_read_passthrough_options).merge(:length_before_value => true))
|
493
|
+
error_or_val_json(val, js)
|
494
|
+
end
|
495
|
+
private
|
496
|
+
# takes a json value (a string) of the form {errored: boolean, value: anything},
|
497
|
+
# checks if an error is indicated, and creates and raises an appropriate exception
|
498
|
+
# if so.
|
499
|
+
def error_or_val_json(val, js)
|
500
|
+
if !val || val==''
|
501
|
+
@expecting_extra_maybe=true
|
502
|
+
raise FirefoxSocketError, "received no value! may have timed out waiting for a value that was not coming."
|
503
|
+
end
|
504
|
+
if val=~ /\ASyntaxError: /
|
505
|
+
raise FirefoxSocketSyntaxError, val
|
506
|
+
end
|
507
|
+
errord_and_val=parse_json(val)
|
508
|
+
unless errord_and_val.is_a?(Hash) && errord_and_val.keys.sort == ['errored', 'value'].sort
|
509
|
+
raise RuntimeError, "unexpected result: \n\t#{errord_and_val.inspect} \nencountered parsing value: \n\t#{val.inspect} \nreturned from expression: \n\t#{js.inspect}"
|
510
|
+
end
|
511
|
+
errord=errord_and_val['errored']
|
512
|
+
val= errord_and_val['value']
|
513
|
+
if errord
|
514
|
+
case val
|
515
|
+
when Hash
|
516
|
+
js_error(val['name'],val['message'],js,val)
|
517
|
+
when String
|
518
|
+
js_error(nil, val, js)
|
519
|
+
else
|
520
|
+
js_error(nil, val.inspect, js)
|
521
|
+
end
|
522
|
+
else
|
523
|
+
val
|
524
|
+
end
|
525
|
+
end
|
526
|
+
public
|
527
|
+
|
528
|
+
# assigns to the javascript reference on the left the object on the right.
|
529
|
+
# Assuming the right object can be converted to JSON, the javascript value will
|
530
|
+
# be the equivalent javascript data type to the ruby object. Will return
|
531
|
+
# the assigned value, converted from its javascript value back to ruby. So, the return
|
532
|
+
# value won't be exactly equivalent if you use symbols for example.
|
533
|
+
#
|
534
|
+
# >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
|
535
|
+
# => {"foo"=>["baz", "qux"]}
|
536
|
+
#
|
537
|
+
# Uses #value_json; see its documentation.
|
538
|
+
def assign_json(js_left, rb_right)
|
539
|
+
js_right=FirefoxSocket.to_javascript(rb_right)
|
540
|
+
value_json("#{js_left}=#{js_right}")
|
541
|
+
end
|
542
|
+
|
543
|
+
# calls to the given function (javascript reference to a function) passing it the
|
544
|
+
# given arguments, each argument being converted from a ruby object to a javascript object
|
545
|
+
# via JSON. returns the return value of the function, of equivalent type to the javascript
|
546
|
+
# return value, converted from javascript to ruby via JSON.
|
547
|
+
# Uses #value_json; see its documentation.
|
548
|
+
def call_json(js_function, *rb_args)
|
549
|
+
js_args=rb_args.map{|arg| FirefoxSocket.to_javascript(arg) }
|
550
|
+
value_json("#{js_function}(#{js_args.join(', ')})")
|
551
|
+
end
|
552
|
+
|
553
|
+
# does the same thing as #handle, but with json, calling #assign_json, #value_json,
|
554
|
+
# or #call_json.
|
555
|
+
#
|
556
|
+
# if the given javascript expression ends with an = symbol, #handle_json calls to
|
557
|
+
# #assign_json assuming it is given one argument; if the expression refers to a function,
|
558
|
+
# calls that function with the given arguments using #call_json; if the expression is
|
559
|
+
# some other value, returns that value, converted to ruby via JSON, assuming given no
|
560
|
+
# arguments. Uses #value_json; see its documentation.
|
561
|
+
def handle_json(js_expr, *args)
|
562
|
+
if js_expr=~/=\z/ # doing assignment
|
563
|
+
js_left=$`
|
564
|
+
if args.size != 1
|
565
|
+
raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
|
566
|
+
end
|
567
|
+
assign_json(js_left, *args)
|
568
|
+
else
|
569
|
+
type=typeof(js_expr)
|
570
|
+
case type
|
571
|
+
when "function"
|
572
|
+
call_json(js_expr, *args)
|
573
|
+
when "undefined"
|
574
|
+
raise FirefoxSocketUndefinedValueError, "undefined expression #{js_expr}"
|
575
|
+
else
|
576
|
+
if !args.empty?
|
577
|
+
raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
|
578
|
+
end
|
579
|
+
value_json(js_expr)
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
# returns the type of the given expression using javascript typeof operator, with the exception that
|
585
|
+
# if the expression is null, returns 'null' - whereas typeof(null) in javascript returns 'object'
|
586
|
+
def typeof(expression)
|
587
|
+
js="try
|
588
|
+
{ nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
|
589
|
+
} catch(e)
|
590
|
+
{ nativeJSON_encode_length(e.name=='ReferenceError' ? {errored: false, value: 'undefined'} : {errored: true, value: Object.extend({}, e)});
|
591
|
+
}"
|
592
|
+
error_or_val_json(send_and_read(js, :length_before_value => true),js)
|
593
|
+
end
|
594
|
+
|
595
|
+
# uses the javascript 'instanceof' operator, passing it the given
|
596
|
+
# expression and interface. this should return true or false.
|
597
|
+
def instanceof(js_expression, js_interface)
|
598
|
+
value_json "(#{js_expression}) instanceof (#{js_interface})"
|
599
|
+
end
|
600
|
+
|
601
|
+
# parses the given JSON string using JSON.parse
|
602
|
+
# Raises JSON::ParserError if given a blank string, something that is not a string, or
|
603
|
+
# a string that contains invalid JSON
|
604
|
+
def parse_json(json)
|
605
|
+
err_class=JSON::ParserError
|
606
|
+
decoder=JSON.method(:parse)
|
607
|
+
# err_class=ActiveSupport::JSON::ParseError
|
608
|
+
# decoder=ActiveSupport::JSON.method(:decode)
|
609
|
+
raise err_class, "Not a string! got: #{json.inspect}" unless json.is_a?(String)
|
610
|
+
raise err_class, "Blank string!" if json==''
|
611
|
+
begin
|
612
|
+
return decoder.call(json)
|
613
|
+
rescue err_class
|
614
|
+
err=$!.class.new($!.message+"\nParsing: #{json.inspect}")
|
615
|
+
err.set_backtrace($!.backtrace)
|
616
|
+
raise err
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
# takes a reference and returns a new JavascriptObject representing that reference on this socket.
|
621
|
+
# ref should be a string representing a reference in javascript.
|
622
|
+
def object(ref, other={})
|
623
|
+
JavascriptObject.new(ref, self, {:debug_name => ref}.merge(other))
|
624
|
+
end
|
625
|
+
# takes a reference and returns a new JavascriptObject representing that reference on this socket,
|
626
|
+
# stored on this socket's temporary object.
|
627
|
+
def object_in_temp(ref, other={})
|
628
|
+
object(ref, other).store_rand_temp
|
629
|
+
end
|
630
|
+
|
631
|
+
# represents the root of the space seen by the FirefoxSocket, and implements #method_missing to
|
632
|
+
# return objects at the root level in a similar manner to JavascriptObject's #method_missing.
|
633
|
+
#
|
634
|
+
# for example, jssh_socket.root.Components will return the top-level Components object;
|
635
|
+
# jssh_socket.root.ctypes will return the ctypes top-level object if that is defined, or error
|
636
|
+
# if not.
|
637
|
+
#
|
638
|
+
# if the object is a function, then it will be called with any given arguments:
|
639
|
+
# >> jssh_socket.root.getWindows
|
640
|
+
# => #<JavascriptObject:0x0254d150 type=object, debug_name=getWindows()>
|
641
|
+
# >> jssh_socket.root.eval("3+2")
|
642
|
+
# => 5
|
643
|
+
#
|
644
|
+
# If any arguments are given to an object that is not a function, you will get an error:
|
645
|
+
# >> jssh_socket.root.Components('wat')
|
646
|
+
# ArgumentError: Cannot pass arguments to Javascript object #<JavascriptObject:0x02545978 type=object, debug_name=Components>
|
647
|
+
#
|
648
|
+
# special behaviors exist for the suffixes !, ?, and =.
|
649
|
+
#
|
650
|
+
# - '?' suffix returns nil if the object does not exist, rather than raising an exception. for
|
651
|
+
# example:
|
652
|
+
# >> jssh_socket.root.foo
|
653
|
+
# FirefoxSocketUndefinedValueError: undefined expression represented by #<JavascriptObject:0x024c3ae0 type=undefined, debug_name=foo> (javascript reference is foo)
|
654
|
+
# >> jssh_socket.root.foo?
|
655
|
+
# => nil
|
656
|
+
# - '=' suffix sets the named object to what is given, for example:
|
657
|
+
# >> jssh_socket.root.foo?
|
658
|
+
# => nil
|
659
|
+
# >> jssh_socket.root.foo={:x => ['y', 'z']}
|
660
|
+
# => {:x=>["y", "z"]}
|
661
|
+
# >> jssh_socket.root.foo
|
662
|
+
# => #<JavascriptObject:0x024a3510 type=object, debug_name=foo>
|
663
|
+
# - '!' suffix tries to convert the value to json in javascrit and back from json to ruby, even
|
664
|
+
# when it might be unsafe (causing infinite rucursion or other errors). for example:
|
665
|
+
# >> jssh_socket.root.foo!
|
666
|
+
# => {"x"=>["y", "z"]}
|
667
|
+
# it can be used with function results that would normally result in a JavascriptObject:
|
668
|
+
# >> jssh_socket.root.eval!("[1, 2, 3]")
|
669
|
+
# => [1, 2, 3]
|
670
|
+
# and of course it can error if you try to do something you shouldn't:
|
671
|
+
# >> jssh_socket.root.getWindows!
|
672
|
+
# FirefoxSocketError::NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIJSON.encode]
|
673
|
+
def root
|
674
|
+
firefox_socket=self
|
675
|
+
@root ||= begin
|
676
|
+
root = Object.new
|
677
|
+
root_metaclass = (class << root; self; end)
|
678
|
+
root_metaclass.send(:define_method, :method_missing) do |method, *args|
|
679
|
+
method=method.to_s
|
680
|
+
if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
|
681
|
+
method = $1
|
682
|
+
suffix = $2
|
683
|
+
firefox_socket.object(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
684
|
+
else
|
685
|
+
# don't deal with any special character crap
|
686
|
+
super
|
687
|
+
end
|
688
|
+
end
|
689
|
+
root_metaclass.send(:define_method, :[]) do |attribute|
|
690
|
+
firefox_socket.object(attribute).val_or_object(:error_on_undefined => false)
|
691
|
+
end
|
692
|
+
root_metaclass.send(:define_method, :[]=) do |attribute, value|
|
693
|
+
firefox_socket.object(attribute).assign(value).val_or_object(:error_on_undefined => false)
|
694
|
+
end
|
695
|
+
root
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# Creates and returns a JavascriptObject representing a function.
|
700
|
+
#
|
701
|
+
# Takes any number of arguments, which should be strings or symbols, which are arguments to the
|
702
|
+
# javascript function.
|
703
|
+
#
|
704
|
+
# The javascript function is specified as the result of a block which must be given to
|
705
|
+
# #function.
|
706
|
+
#
|
707
|
+
# An example:
|
708
|
+
# jssh_socket.function(:a, :b) do
|
709
|
+
# "return a+b;"
|
710
|
+
# end
|
711
|
+
# => #<JavascriptObject:0x0248e78c type=function, debug_name=function(a, b){ return a+b; }>
|
712
|
+
#
|
713
|
+
# This is exactly the same as doing
|
714
|
+
# jssh_socket.object("function(a, b){ return a+b; }")
|
715
|
+
# but it is a bit more concise and reads a bit more ruby-like.
|
716
|
+
#
|
717
|
+
# a longer example to return the text of a thing (rather contrived, but, it works):
|
718
|
+
#
|
719
|
+
# jssh_socket.function(:node) do %q[
|
720
|
+
# if(node.nodeType==3)
|
721
|
+
# { return node.data;
|
722
|
+
# }
|
723
|
+
# else if(node.nodeType==1)
|
724
|
+
# { return node.textContent;
|
725
|
+
# }
|
726
|
+
# else
|
727
|
+
# { return "what?";
|
728
|
+
# }
|
729
|
+
# ]
|
730
|
+
# end.call(some_node)
|
731
|
+
def function(*arg_names)
|
732
|
+
unless arg_names.all?{|arg| (arg.is_a?(String) || arg.is_a?(Symbol)) && arg.to_s =~ /\A[a-z_][a-z0-9_]*\z/i }
|
733
|
+
raise ArgumentError, "Arguments to \#function should be strings or symbols representing the names of arguments to the function. got #{arg_names.inspect}"
|
734
|
+
end
|
735
|
+
unless block_given?
|
736
|
+
raise ArgumentError, "\#function should be given a block which results in a string representing the body of a javascript function. no block was given!"
|
737
|
+
end
|
738
|
+
function_body = yield
|
739
|
+
unless function_body.is_a?(String)
|
740
|
+
raise ArgumentError, "The block given to \#function must return a string representing the body of a javascript function! instead got #{function_body.inspect}"
|
741
|
+
end
|
742
|
+
nl = function_body.include?("\n") ? "\n" : ""
|
743
|
+
description = function_body.include?("\n") ? "..." : function_body
|
744
|
+
JavascriptFunction.new("function(#{arg_names.join(", ")})#{nl}{ #{function_body} #{nl}}", self, {:debug_name => "function(#{arg_names.join(", ")}){ #{description} }"})
|
745
|
+
end
|
746
|
+
|
747
|
+
# takes a hash of arguments with keys that are strings or symbols that will be variables in the
|
748
|
+
# scope of the function in javascript, and a block which results in a string which should be the
|
749
|
+
# body of a javascript function. calls the given function with the given arguments.
|
750
|
+
#
|
751
|
+
# an example:
|
752
|
+
# jssh_socket.call_function(:x => 3, :y => {:z => 'foobar'}) do
|
753
|
+
# "return x + y['z'].length;"
|
754
|
+
# end
|
755
|
+
#
|
756
|
+
# will return 9.
|
757
|
+
def call_function(arguments_hash={}, &block)
|
758
|
+
argument_names, argument_vals = *arguments_hash.inject([[],[]]) do |(names, vals),(name, val)|
|
759
|
+
[names + [name], vals + [val]]
|
760
|
+
end
|
761
|
+
function(*argument_names, &block).call(*argument_vals)
|
762
|
+
end
|
763
|
+
|
764
|
+
# returns a JavascriptObject representing a designated top-level object for temporary storage of stuff
|
765
|
+
# on this socket.
|
766
|
+
#
|
767
|
+
# really, temporary values could be stored anywhere. this just gives one nice consistent designated place to stick them.
|
768
|
+
attr_reader :temp_object
|
769
|
+
# returns a JavascriptObject representing the Components top-level javascript object.
|
770
|
+
#
|
771
|
+
# https://developer.mozilla.org/en/Components_object
|
772
|
+
def Components
|
773
|
+
@components ||= root.Components
|
774
|
+
end
|
775
|
+
# raises an informative error if the socket is down for some reason
|
776
|
+
def assert_socket
|
777
|
+
actual, expected=handling_connection_error(:exception => FirefoxSocketConnectionError.new("Encountered a socket error while checking the socket.")) do
|
778
|
+
[value_json('["foo"]'), ["foo"]]
|
779
|
+
end
|
780
|
+
unless expected==actual
|
781
|
+
raise FirefoxSocketError, "The socket seems to have a problem: sent #{expected.inspect} but got back #{actual.inspect}"
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
# returns a string of basic information about this socket.
|
786
|
+
def inspect
|
787
|
+
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:host, :port].map{|attr| attr.to_s+'='+send(attr).inspect}.join(', ')}>"
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|