Class: JsshSocket

Inherits:
Object
  • Object
show all
Defined in:
lib/vapir-firefox/jssh_socket.rb

Overview

A JsshSocket represents a connection to Firefox over a socket opened to the JSSH extension. It does the work of interacting with the socket and translating ruby values to javascript and back.

Constant Summary collapse

PROMPT =

end

"\n> "
PrototypeFile =
File.join(File.dirname(__FILE__), "prototype.functional.js")
DEFAULT_IP =

default IP Address of the machine where the script is to be executed. Default to localhost.

"127.0.0.1"
DEFAULT_PORT =

default port to connect to.

9997
DEFAULT_SOCKET_TIMEOUT =

maximum time JsshSocket waits for a value to be sent before giving up

64
SHORT_SOCKET_TIMEOUT =

maximum time JsshSocket will wait for additional reads on a socket that is actively sending

(2**-2).to_f
READ_SIZE =

the number of bytes to read from the socket at a time

4096

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ JsshSocket

Connects a new socket to jssh

Takes options:

  • :jssh_ip => the ip to connect to, default 127.0.0.1

  • :jssh_port => the port to connect to, default 9997

  • :send_prototype => true|false, whether to load and send the Prototype library (the functional programming part of it anyway, and JSON bits)



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/vapir-firefox/jssh_socket.rb', line 104

def initialize(options={})
  @ip=options[:jssh_ip] || DEFAULT_IP
  @port=options[:jssh_port] || DEFAULT_PORT
  @prototype=options.key?(:send_prototype) ? options[:send_prototype] : true
  begin
    @socket = TCPSocket::new(@ip, @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
    welcome="Welcome to the Mozilla JavaScript Shell!\n"
    read=read_value
    if !read
      @expecting_extra_maybe=true
      raise JsshUnableToStart, "Something went wrong initializing - no response" 
    elsif read != welcome
      @expecting_extra_maybe=true
      raise JsshUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}" 
    end
  rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
    err=JsshUnableToStart.new("Could not connect to JSSH sever #{@ip}:#{@port}. Ensure that Firefox is running and has JSSH configured, or try restarting firefox.\nMessage from TCPSocket:\n#{$!.message}")
    err.set_backtrace($!.backtrace)
    raise err
  end
  if @prototype
    ret=send_and_read(File.read(PrototypeFile))
    if ret != "done!"
      @expecting_extra_maybe=true
      raise JsshError, "Something went wrong loading Prototype - message #{ret.inspect}"
    end
  end
  ret=send_and_read("(function()
  { nativeJSON=Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON);
    nativeJSON_encode_length=function(object)
    { var encoded=nativeJSON.encode(object);
      return encoded.length.toString()+\"\\n\"+encoded;
    }
    return 'done!';
  })()")
  if ret != "done!"
    @expecting_extra_maybe=true
    raise JsshError, "Something went wrong initializing native JSON - message #{ret.inspect}"
  end
  root.JsshTemp={}
end

Instance Attribute Details

#ipObject (readonly)

the IP to which this socket is connected



92
93
94
# File 'lib/vapir-firefox/jssh_socket.rb', line 92

def ip
  @ip
end

#portObject (readonly)

the port on which this socket is connected



94
95
96
# File 'lib/vapir-firefox/jssh_socket.rb', line 94

def port
  @port
end

#prototypeObject (readonly)

whether the prototye javascript library is loaded



96
97
98
# File 'lib/vapir-firefox/jssh_socket.rb', line 96

def prototype
  @prototype
end

Class Method Details

.to_javascript(object) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/vapir-firefox/jssh_socket.rb', line 335

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?(JsshObject)
    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



723
724
725
726
727
728
729
730
731
732
733
734
735
736
# File 'lib/vapir-firefox/jssh_socket.rb', line 723

def assert_socket
  begin
    actual, expected=if prototype
      [value_json('["foo"]'), ["foo"]]
    else
      [value('"foo"'), "foo"]
    end
  rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
    raise(JsshConnectionError, "Encountered a socket error while checking the socket.\n#{$!.class}\n#{$!.message}", $!.backtrace)
  end
  unless expected==actual
    raise JsshError, "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 JSSH, which will be a string, the expression’s toString. Uses #value; see its documentation.



370
371
372
# File 'lib/vapir-firefox/jssh_socket.rb', line 370

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.



471
472
473
474
475
# File 'lib/vapir-firefox/jssh_socket.rb', line 471

def assign_json(js_left, rb_right)
  ensure_prototype
  js_right=JsshSocket.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.



377
378
379
# File 'lib/vapir-firefox/jssh_socket.rb', line 377

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.



698
699
700
701
702
703
# File 'lib/vapir-firefox/jssh_socket.rb', line 698

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.



482
483
484
485
486
# File 'lib/vapir-firefox/jssh_socket.rb', line 482

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

#ComponentsObject

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

developer.mozilla.org/en/Components_object



715
716
717
# File 'lib/vapir-firefox/jssh_socket.rb', line 715

def Components
  @components ||= root.Components
end

#ensure_prototypeObject

raises error if the prototype library (needed for JSON stuff in javascript) has not been loaded



521
522
523
524
525
# File 'lib/vapir-firefox/jssh_socket.rb', line 521

def ensure_prototype
  unless prototype
    raise JsshError, "This functionality requires the prototype library; cannot be called on a Jssh session that has not loaded the Prototype library"
  end
end

#function(*arg_names) ⇒ Object

Creates and returns a JsshObject 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
=> #<JsshObject: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)


673
674
675
676
677
678
679
680
681
682
683
684
685
686
# File 'lib/vapir-firefox/jssh_socket.rb', line 673

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" : ""
  object("function(#{arg_names.join(", ")})#{nl}{ #{function_body} #{nl}}")
end

#getWindowsObject

returns a JsshObject representing the return value of JSSH’s builtin getWindows() function.



719
720
721
# File 'lib/vapir-firefox/jssh_socket.rb', line 719

def getWindows
  root.getWindows
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.



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/vapir-firefox/jssh_socket.rb', line 386

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 JsshUndefinedValueError, "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.



496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/vapir-firefox/jssh_socket.rb', line 496

def handle_json(js_expr, *args)
  ensure_prototype
  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 JsshUndefinedValueError, "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

#inspectObject

returns a string of basic information about this socket.



739
740
741
# File 'lib/vapir-firefox/jssh_socket.rb', line 739

def inspect
  "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:ip, :port, :prototype].map{|attr| aa="@#{attr}";aa+'='+instance_variable_get(aa).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.



546
547
548
# File 'lib/vapir-firefox/jssh_socket.rb', line 546

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 JsshObject representing that reference on this socket. ref should be a string representing a reference in javascript.



571
572
573
# File 'lib/vapir-firefox/jssh_socket.rb', line 571

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

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

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



576
577
578
# File 'lib/vapir-firefox/jssh_socket.rb', line 576

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)


553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/vapir-firefox/jssh_socket.rb', line 553

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

#rootObject

represents the root of the space seen by the JsshSocket, and implements #method_missing to return objects at the root level in a similar manner to JsshObject’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
=> #<JsshObject: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 #<JsshObject: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
    JsshUndefinedValueError: undefined expression represented by #<JsshObject: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
    => #<JsshObject: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 JsshObject:

    >> 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!
    JsshError::NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIJSON.encode]
    


622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/vapir-firefox/jssh_socket.rb', line 622

def root
  jssh_socket=self
#    @root ||= begin
    root = Object.new
    (class << root; self; end).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
        jssh_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
#    end
end

#temp_objectObject

returns a JsshObject 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.



709
710
711
# File 'lib/vapir-firefox/jssh_socket.rb', line 709

def temp_object
  @temp_object ||= root.JsshTemp
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’



529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/vapir-firefox/jssh_socket.rb', line 529

def typeof(expression)
  ensure_prototype
  js="try
{ nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
} catch(e)
{ if(e.name=='ReferenceError')
{ nativeJSON_encode_length({errored: false, value: 'undefined'});
}
else
{ nativeJSON_encode_length({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 JSSH.

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



361
362
363
364
365
# File 'lib/vapir-firefox/jssh_socket.rb', line 361

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)


412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/vapir-firefox/jssh_socket.rb', line 412

def value_json(js, options={})
  options={:error_on_undefined => true}.merge(options)
  raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
  ensure_prototype
  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.merge(:length_before_value => true))
  error_or_val_json(val, js)
end