vapir-firefox 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+