vapir-firefox 1.8.1 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|