Class: FirefoxSocket

Inherits:
Object
  • Object
show all
Extended by:
Vapir::Configurable
Includes:
Vapir::Configurable
Defined in:
lib/vapir-firefox/firefox_socket/base.rb

Overview

Base class for connecting to a firefox extension over a TCP socket. does the work of interacting with the socket and translating ruby values to javascript and back.

Direct Known Subclasses

JsshSocket, MozreplSocket

Constant Summary collapse

PrototypeFile =

end

File.join(File.dirname(__FILE__), "prototype.functional.js")

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ FirefoxSocket

Connects a new socket to firefox

Takes options:

  • :host => the ip to connect to, default localhost

  • :port => the port to connect to



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 138

def initialize(options={})
  config.update_hash options
  require 'thread'
  @mutex = Mutex.new
  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
    @socket = TCPSocket::new(host, port)
    @socket.sync = true
    @expecting_prompt=false # initially, the welcome message comes before the prompt, so this so this is false to start with 
    @expecting_extra_maybe=false
    eat_welcome_message
  end
  initialize_environment
  @temp_object = object('VapirTemp')
  ret=send_and_read(File.read(PrototypeFile))
  if ret !~ /done!/
    @expecting_extra_maybe=true
    raise FirefoxSocketError, "Something went wrong loading Prototype - message #{ret.inspect}"
  end
  # Y combinator in javascript. 
  #
  #  example - recursive length function.
  #
  #  >> length=firefox_socket.root.Vapir.Ycomb(firefox_socket.function(:len){ "return function(list){ return list.length==0 ? 0 : 1+len(list.slice(1)); }; " })
  #  => #<JavascriptObject:0x01206880 type=function, debug_name=Vapir.Ycomb(function(len){ return function(list){ return list.length==0 ? 0 : 1+len(list.slice(1)); };  })>
  #  >> length.call(['a', 'b', 'c'])
  #  => 3
  root.Vapir.Ycomb=function(:gen){ "return function(f){ return f(f); }(function(f){ return gen(function(){ return f(f).apply(null, arguments); }); });" }
end

Instance Attribute Details

#temp_objectObject (readonly)

returns a JavascriptObject representing a designated top-level object for temporary storage of stuff on this socket.

really, temporary values could be stored anywhere. this just gives one nice consistent designated place to stick them.



769
770
771
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 769

def temp_object
  @temp_object
end

Class Method Details

.to_javascript(object) ⇒ Object

returns a string of javascript representing the given object. if given an Array or Hash, operates recursively. this is like converting to JSON, but this supports more data types than can be represented in JSON. supported data types are:

  • Array, Set (converts to javascript Array)

  • Hash (converts to javascript Object)

  • JavascriptObject (just uses the reference the JavascriptObject represents)

  • Regexp (converts to javascript RegExp)

  • String, Symbol (converts to a javascript string)

  • Integer, Float

  • true, false, nil



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 403

def self.to_javascript(object)
  if ['Array', 'Set'].any?{|klass_name| Object.const_defined?(klass_name) && object.is_a?(Object.const_get(klass_name)) }
    "["+object.map{|element| to_javascript(element) }.join(", ")+"]"
  elsif object.is_a?(Hash)
    "{"+object.map{|(key, value)| to_javascript(key)+": "+to_javascript(value) }.join(", ")+"}"
  elsif object.is_a?(JavascriptObject)
    object.ref
  elsif [true, false, nil].include?(object) || [Integer, Float, String, Symbol].any?{|klass| object.is_a?(klass) }
    object.to_json
  elsif object.is_a?(Regexp)
    # get the flags javascript recognizes - not the same ones as ruby. 
    js_flags = {Regexp::MULTILINE => 'm', Regexp::IGNORECASE => 'i'}.inject("") do |flags, (bit, flag)|
      flags + (object.options & bit > 0 ? flag : '')
    end
    # "new RegExp("+to_javascript(object.source)+", "+to_javascript(js_flags)+")"
    js_source = object.source.empty? ? "/(?:)/" : object.inspect
    js_source.sub!(/\w*\z/, '') # drop ruby flags 
    js_source + js_flags
  else
    raise "Unable to represent object as javascript: #{object.inspect} (#{object.class})"
  end
end

Instance Method Details

#assert_socketObject

raises an informative error if the socket is down for some reason



777
778
779
780
781
782
783
784
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 777

def assert_socket
  actual, expected=handling_connection_error(:exception => FirefoxSocketConnectionError.new("Encountered a socket error while checking the socket.")) do
    [value_json('["foo"]'), ["foo"]]
  end
  unless expected==actual
    raise FirefoxSocketError, "The socket seems to have a problem: sent #{expected.inspect} but got back #{actual.inspect}"
  end
end

#assign(js_left, js_right) ⇒ Object

assigns to the javascript reference on the left the javascript expression on the right. returns the value of the expression as reported by the firefox extension, which will be a string, the expression’s toString. Uses #value; see its documentation.



438
439
440
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 438

def assign(js_left, js_right)
  value("#{js_left}= #{js_right}")
end

#assign_json(js_left, rb_right) ⇒ Object

assigns to the javascript reference on the left the object on the right. Assuming the right object can be converted to JSON, the javascript value will be the equivalent javascript data type to the ruby object. Will return the assigned value, converted from its javascript value back to ruby. So, the return value won’t be exactly equivalent if you use symbols for example.

>> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
=> {"foo"=>["baz", "qux"]}

Uses #value_json; see its documentation.



539
540
541
542
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 539

def assign_json(js_left, rb_right)
  js_right=FirefoxSocket.to_javascript(rb_right)
  value_json("#{js_left}=#{js_right}")
end

#call(js_function, *js_args) ⇒ Object

calls to the given function (javascript reference to a function) passing it the given arguments (javascript expressions). returns the return value of the function, a string, the toString of the javascript value. Uses #value; see its documentation.



445
446
447
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 445

def call(js_function, *js_args)
  value("#{js_function}(#{js_args.join(', ')})")
end

#call_function(arguments_hash = {}, &block) ⇒ Object

takes a hash of arguments with keys that are strings or symbols that will be variables in the scope of the function in javascript, and a block which results in a string which should be the body of a javascript function. calls the given function with the given arguments.

an example:

jssh_socket.call_function(:x => 3, :y => {:z => 'foobar'}) do
  "return x + y['z'].length;"
end

will return 9.



758
759
760
761
762
763
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 758

def call_function(arguments_hash={}, &block)
  argument_names, argument_vals = *arguments_hash.inject([[],[]]) do |(names, vals),(name, val)|
    [names + [name], vals + [val]]
  end
  function(*argument_names, &block).call(*argument_vals)
end

#call_json(js_function, *rb_args) ⇒ Object

calls to the given function (javascript reference to a function) passing it the given arguments, each argument being converted from a ruby object to a javascript object via JSON. returns the return value of the function, of equivalent type to the javascript return value, converted from javascript to ruby via JSON. Uses #value_json; see its documentation.



549
550
551
552
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 549

def call_json(js_function, *rb_args)
  js_args=rb_args.map{|arg| FirefoxSocket.to_javascript(arg) }
  value_json("#{js_function}(#{js_args.join(', ')})")
end

#ComponentsObject

returns a JavascriptObject representing the Components top-level javascript object.

developer.mozilla.org/en/Components_object



773
774
775
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 773

def Components
  @components ||= root.Components
end

#configuration_parentObject



120
121
122
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 120

def configuration_parent
  self.class.config
end

#function(*arg_names) ⇒ Object

Creates and returns a JavascriptObject representing a function.

Takes any number of arguments, which should be strings or symbols, which are arguments to the javascript function.

The javascript function is specified as the result of a block which must be given to #function.

An example:

jssh_socket.function(:a, :b) do
  "return a+b;"
end
=> #<JavascriptObject:0x0248e78c type=function, debug_name=function(a, b){ return a+b; }>

This is exactly the same as doing

jssh_socket.object("function(a, b){ return a+b; }")

but it is a bit more concise and reads a bit more ruby-like.

a longer example to return the text of a thing (rather contrived, but, it works):

jssh_socket.function(:node) do %q[
  if(node.nodeType==3)
  { return node.data;
  }
  else if(node.nodeType==1)
  { return node.textContent;
  }
  else
  { return "what?";
  }
]
end.call(some_node)


732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 732

def function(*arg_names)
  unless arg_names.all?{|arg| (arg.is_a?(String) || arg.is_a?(Symbol)) && arg.to_s =~ /\A[a-z_][a-z0-9_]*\z/i }
    raise ArgumentError, "Arguments to \#function should be strings or symbols representing the names of arguments to the function. got #{arg_names.inspect}"
  end
  unless block_given?
    raise ArgumentError, "\#function should be given a block which results in a string representing the body of a javascript function. no block was given!"
  end
  function_body = yield
  unless function_body.is_a?(String)
    raise ArgumentError, "The block given to \#function must return a string representing the body of a javascript function! instead got #{function_body.inspect}"
  end
  nl = function_body.include?("\n") ? "\n" : ""
  description = function_body.include?("\n") ? "..." : function_body
  JavascriptFunction.new("function(#{arg_names.join(", ")})#{nl}{ #{function_body} #{nl}}", self, {:debug_name => "function(#{arg_names.join(", ")}){ #{description} }"})
end

#handle(js_expr, *args) ⇒ Object

if the given javascript expression ends with an = symbol, #handle calls to #assign assuming it is given one argument; if the expression refers to a function, calls that function with the given arguments using #call; if the expression is some other value, returns that value (its javascript toString), calling #value, assuming given no arguments. Uses #value; see its documentation.



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 454

def handle(js_expr, *args)
  if js_expr=~/=\z/ # doing assignment
    js_left=$`
    if args.size != 1
      raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
    end
    assign(js_left, *args)
  else
    type=typeof(js_expr)
    case type
    when "function"
      call(js_expr, *args)
    when "undefined"
      raise FirefoxSocketUndefinedValueError, "undefined expression #{js_expr.inspect}"
    else
      if !args.empty?
        raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
      end
      value(js_expr)
    end
  end
end

#handle_json(js_expr, *args) ⇒ Object

does the same thing as #handle, but with json, calling #assign_json, #value_json, or #call_json.

if the given javascript expression ends with an = symbol, #handle_json calls to #assign_json assuming it is given one argument; if the expression refers to a function, calls that function with the given arguments using #call_json; if the expression is some other value, returns that value, converted to ruby via JSON, assuming given no arguments. Uses #value_json; see its documentation.



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 562

def handle_json(js_expr, *args)
  if js_expr=~/=\z/ # doing assignment
    js_left=$`
    if args.size != 1
      raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
    end
    assign_json(js_left, *args)
  else
    type=typeof(js_expr)
    case type
    when "function"
      call_json(js_expr, *args)
    when "undefined"
      raise FirefoxSocketUndefinedValueError, "undefined expression #{js_expr}"
    else
      if !args.empty?
        raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
      end
      value_json(js_expr)
    end
  end
end

#handling_connection_error(options = {}) ⇒ Object

takes a block, calls the block, and returns the result of the block - unless the connection fails over the course of the block. if that happens, this handles the multitude of errors that may be the cause of that, and deals with them in a customizable manner. by default, raises a FirefoxSocketConnectionError with all of the information of the original error attached.

recognizes options:

  • :exception - an instance of an exception which will be raised, e.g. :exception => RuntimError.new(‘something went wrong!’)

  • :handle may take the following values:

    • :raise (default) - raises options, by default a FirefoxSocketConnectionError

    • :ignore - discards the exception and returns nil

    • :return - returns the exception; does not raise it

    • a Proc or Method - calls the proc, giving the raised exception as an argument. this is useful to return from a function when the connection fails, e.g. connection.handling_connection_error(:handle => proc{ return false }) { [code which may cause the socket to close] }



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 183

def handling_connection_error(options={})
  options=handle_options(options, :handle => :raise, :exception => FirefoxSocketConnectionError.new("Encountered an error on the socket."))
  begin
    yield
  rescue FirefoxSocketConnectionError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, SystemCallError
    @expecting_extra_maybe = true
    error = options[:exception].class.new(options[:exception].message + "\n#{$!.class}\n#{$!.message}")
    error.set_backtrace($!.backtrace)
    case options[:handle]
    when :raise
      raise error
    when :ignore
      nil
    when :return
      error
    when Proc, Method
      options[:handle].call(error)
    else
      raise ArgumentError, "Don't know what to do when told to handle by :handle => #{options[:handle].inspect}"
    end
  end
end

#hostObject

the host to which this socket is connected



125
126
127
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 125

def host
  config.host
end

#inspectObject

returns a string of basic information about this socket.



787
788
789
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 787

def inspect
  "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:host, :port].map{|attr| attr.to_s+'='+send(attr).inspect}.join(', ')}>"
end

#instanceof(js_expression, js_interface) ⇒ Object

uses the javascript ‘instanceof’ operator, passing it the given expression and interface. this should return true or false.



598
599
600
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 598

def instanceof(js_expression, js_interface)
  value_json "(#{js_expression}) instanceof (#{js_interface})"
end

#object(ref, other = {}) ⇒ Object

takes a reference and returns a new JavascriptObject representing that reference on this socket. ref should be a string representing a reference in javascript.



623
624
625
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 623

def object(ref, other={})
  JavascriptObject.new(ref, self, {:debug_name => ref}.merge(other))
end

#object_in_temp(ref, other = {}) ⇒ Object

takes a reference and returns a new JavascriptObject representing that reference on this socket, stored on this socket’s temporary object.



628
629
630
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 628

def object_in_temp(ref, other={})
  object(ref, other).store_rand_temp
end

#parse_json(json) ⇒ Object

parses the given JSON string using JSON.parse Raises JSON::ParserError if given a blank string, something that is not a string, or a string that contains invalid JSON

Raises:

  • (err_class)


605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 605

def parse_json(json)
  err_class=JSON::ParserError
  decoder=JSON.method(:parse)
  # err_class=ActiveSupport::JSON::ParseError
  # decoder=ActiveSupport::JSON.method(:decode)
  raise err_class, "Not a string! got: #{json.inspect}" unless json.is_a?(String)
  raise err_class, "Blank string!" if json==''
  begin
    return decoder.call(json)
  rescue err_class
    err=$!.class.new($!.message+"\nParsing: #{json.inspect}")
    err.set_backtrace($!.backtrace)
    raise err
  end
end

#portObject

the port on which this socket is connected



129
130
131
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 129

def port
  config.port
end

#rootObject

represents the root of the space seen by the FirefoxSocket, and implements #method_missing to return objects at the root level in a similar manner to JavascriptObject’s #method_missing.

for example, jssh_socket.root.Components will return the top-level Components object; jssh_socket.root.ctypes will return the ctypes top-level object if that is defined, or error if not.

if the object is a function, then it will be called with any given arguments:

>> jssh_socket.root.getWindows
=> #<JavascriptObject:0x0254d150 type=object, debug_name=getWindows()>
>> jssh_socket.root.eval("3+2")
=> 5

If any arguments are given to an object that is not a function, you will get an error:

>> jssh_socket.root.Components('wat')
ArgumentError: Cannot pass arguments to Javascript object #<JavascriptObject:0x02545978 type=object, debug_name=Components>

special behaviors exist for the suffixes !, ?, and =.

  • ‘?’ suffix returns nil if the object does not exist, rather than raising an exception. for example:

    >> jssh_socket.root.foo
    FirefoxSocketUndefinedValueError: undefined expression represented by #<JavascriptObject:0x024c3ae0 type=undefined, debug_name=foo> (javascript reference is foo)
    >> jssh_socket.root.foo?
    => nil
    
  • ‘=’ suffix sets the named object to what is given, for example:

    >> jssh_socket.root.foo?
    => nil
    >> jssh_socket.root.foo={:x => ['y', 'z']}
    => {:x=>["y", "z"]}
    >> jssh_socket.root.foo
    => #<JavascriptObject:0x024a3510 type=object, debug_name=foo>
    
  • ‘!’ suffix tries to convert the value to json in javascrit and back from json to ruby, even when it might be unsafe (causing infinite rucursion or other errors). for example:

    >> jssh_socket.root.foo!
    => {"x"=>["y", "z"]}
    

    it can be used with function results that would normally result in a JavascriptObject:

    >> jssh_socket.root.eval!("[1, 2, 3]")
    => [1, 2, 3]
    

    and of course it can error if you try to do something you shouldn’t:

    >> jssh_socket.root.getWindows!
    FirefoxSocketError::NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIJSON.encode]
    


674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 674

def root
  firefox_socket=self
  @root ||= begin
    root = Object.new
    root_metaclass = (class << root; self; end)
    root_metaclass.send(:define_method, :method_missing) do |method, *args|
      method=method.to_s
      if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
        method = $1
        suffix = $2
        firefox_socket.object(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
      else
        # don't deal with any special character crap 
        super
      end
    end
    root_metaclass.send(:define_method, :[]) do |attribute|
      firefox_socket.object(attribute).val_or_object(:error_on_undefined => false)
    end
    root_metaclass.send(:define_method, :[]=) do |attribute, value|
      firefox_socket.object(attribute).assign(value).val_or_object(:error_on_undefined => false)
    end
    root
  end
end

#typeof(expression) ⇒ Object

returns the type of the given expression using javascript typeof operator, with the exception that if the expression is null, returns ‘null’ - whereas typeof(null) in javascript returns ‘object’



587
588
589
590
591
592
593
594
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 587

def typeof(expression)
  js="try
{ nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
} catch(e)
{ nativeJSON_encode_length(e.name=='ReferenceError' ? {errored: false, value: 'undefined'} : {errored: true, value: Object.extend({}, e)});
}"
  error_or_val_json(send_and_read(js, :length_before_value => true),js)
end

#value(js) ⇒ Object

returns the value of the given javascript expression, as reported by the the firefox extension.

This will be a string, the given expression’s toString.



429
430
431
432
433
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 429

def value(js)
  # this is wrapped in a function so that ...
  # dang, now I can't remember. I'm sure I had a good reason at the time. 
  send_and_read("(function(){return #{js}})()")
end

#value_json(js, options = {}) ⇒ Object

returns the value of the given javascript expression. Assuming that it can be converted to JSON, will return the equivalent ruby data type to the javascript value. Will raise an error if the javascript errors.

Raises:

  • (ArgumentError)


480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/vapir-firefox/firefox_socket/base.rb', line 480

def value_json(js, options={})
  send_and_read_passthrough_options=[:timeout]
  options=handle_options(options, {:error_on_undefined => true}, send_and_read_passthrough_options)
  raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
  ref_error=options[:error_on_undefined] ? "typeof(result)=='undefined' ? {errored: true, value: {'name': 'ReferenceError', 'message': 'undefined expression in: '+result_f.toString()}} : " : ""
  wrapped_js=
    "try
     { var result_f=(function(){return #{js}});
       var result=result_f();
       nativeJSON_encode_length(#{ref_error} {errored: false, value: result});
     }catch(e)
     { nativeJSON_encode_length({errored: true, value: Object.extend({}, e)});
     }"
  val=send_and_read(wrapped_js, options.select_keys(*send_and_read_passthrough_options).merge(:length_before_value => true))
  error_or_val_json(val, js)
end