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.
@@ -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 = jssh_socket.object('Sanitizer')
13
- if sanitizer_class.type=='undefined'
14
- loader = jssh_socket.Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(jssh_socket.Components.interfaces.mozIJSSubScriptLoader)
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 = jssh_socket.object('Sanitizer')
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 = jssh_socket.Components.classes["@mozilla.org/cookiemanager;1"].getService(jssh_socket.Components.interfaces.nsICookieManager)
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
@@ -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(:jssh_socket => jssh_socket)
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, jssh_socket.Components.interfaces.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, 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, jssh_socket.Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, nil).singleNodeValue
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 JsshObject representing an array (in javascript) of the visible text nodes of this container. same as
84
- # the Vapir::Common #visible_text_nodes implementation, but much much faster.
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
- text_nodes = jssh_socket.call_function(:element_object => containing_object, :document_object => document_object) do %Q(
87
- var Ycomb = function(gen){ return function(f){ return f(f); }(function(f){ return gen(function(){ return f(f).apply(null, arguments); }); }); }; // TODO: move this somewhere better - jssh_socket?
88
- var recurse_text_nodes = Ycomb(function(recurse)
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
- @jssh_socket=extra[:jssh_socket]
14
- @jssh_socket||= (extra[:browser].jssh_socket if extra[:browser])
15
- @jssh_socket||= (extra[:container].jssh_socket if extra[:container])
16
- @jssh_socket||= (what.jssh_socket if how==:element_object)
17
- unless @jssh_socket
18
- raise RuntimeError, "No JSSH socket given! Firefox elements need this (specified in the :jssh_socket key of the extra hash)"
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 :jssh_socket
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(jssh_socket.call_function(:element_object => element_object, :event => event){ "return function(){ element_object.dispatchEvent(event) };" }, 0)
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 jssh object)
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(jssh_socket.call_function(:element_object => element_object, :mouse_down_event => mouse_down_event, :mouse_up_event => mouse_up_event, :click_event => click_event) do
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
- jssh_socket.call_function(:element_to_check => element_object, :document_object => document_object) do %Q(
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.jssh_socket.Components.interfaces.nsIDOMDocument)
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 jssh_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
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
+