vapir-firefox 1.7.0.rc1

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.
@@ -0,0 +1,1066 @@
1
+ require 'json'
2
+ require 'active_support'
3
+ require 'socket'
4
+ require 'timeout'
5
+ #require 'logger'
6
+
7
+ #class LoggerWithCallstack < Logger
8
+ # class TimeElapsedFormatter < Formatter
9
+ # def initialize
10
+ # super
11
+ # @time_started=Time.now
12
+ # end
13
+ # def format_datetime(time)
14
+ # "%10.3f"%(time.to_f-@time_started.to_f)
15
+ # end
16
+ #
17
+ # end
18
+ # def add(severity, message = nil, progname = nil, &block)
19
+ # severity ||= UNKNOWN
20
+ # if @logdev.nil? or severity < @level
21
+ # return true
22
+ # end
23
+ # progname ||= @progname
24
+ # if message.nil?
25
+ # if block_given?
26
+ # message = yield
27
+ # else
28
+ # message = progname
29
+ # progname = @progname
30
+ # end
31
+ # end
32
+ # message=message.to_s+" FROM: "+caller.map{|c|"\t\t#{c}\n"}.join("")
33
+ # @logdev.write(
34
+ # format_message(format_severity(severity), Time.now, progname, message))
35
+ # true
36
+ # end
37
+ #end
38
+
39
+ class Object
40
+ # this is like #to_json, but without the conflicting names between ActiveSupport and JSON gem,
41
+ # and also for JsshObject (which is a reference; not real json; see the overload in that class)
42
+ def to_jssh
43
+ ActiveSupport::JSON.encode(self)
44
+ end
45
+ end
46
+
47
+
48
+ class JsshError < StandardError
49
+ attr_accessor :source, :lineNumber, :stack, :fileName
50
+ end
51
+ # this exception covers all connection errors either on startup or during usage
52
+ class JsshConnectionError < JsshError;end
53
+ # This exception is thrown if we are unable to connect to JSSh.
54
+ class JsshUnableToStart < JsshConnectionError;end
55
+ class JsshJavascriptError < JsshError;end
56
+ class JsshSyntaxError < JsshJavascriptError;end
57
+ class JsshUndefinedValueError < JsshJavascriptError;end
58
+
59
+ class JsshSocket
60
+ # def self.logger
61
+ # @@logger||=begin
62
+ # logfile=File.open('c:/tmp/jssh_log.txt', File::WRONLY|File::TRUNC|File::CREAT)
63
+ # logfile.sync=true
64
+ # logger=Logger.new(logfile)
65
+ # logger.level = -1#Logger::DEBUG#Logger::INFO
66
+ # #logger.formatter=LoggerWithCallstack::TimeElapsedFormatter.new
67
+ # logger
68
+ # end
69
+ # end
70
+ # def logger
71
+ # self.class.logger
72
+ # end
73
+
74
+ PROMPT="\n> "
75
+
76
+ # IP Address of the machine where the script is to be executed. Default to localhost.
77
+ @@default_jssh_ip = "127.0.0.1"
78
+ @@default_jssh_port = 9997
79
+ PrototypeFile=File.join(File.dirname(__FILE__), "prototype.functional.js")
80
+
81
+ DEFAULT_SOCKET_TIMEOUT=64
82
+ SHORT_SOCKET_TIMEOUT=(2**-2).to_f
83
+
84
+ attr_reader :ip, :port, :prototype
85
+
86
+ # Connects a new socket to jssh
87
+ # Takes options:
88
+ # * :jssh_ip => the ip to connect to, default 127.0.0.1
89
+ # * :jssh_port => the port to connect to, default 9997
90
+ # * :send_prototype => true|false, whether to load and send the Prototype library (the functional programming part of it anyway, and JSON bits)
91
+ def initialize(options={})
92
+ @ip=options[:jssh_ip] || @@default_jssh_ip
93
+ @port=options[:jssh_port] || @@default_jssh_port
94
+ @prototype=options.key?(:send_prototype) ? options[:send_prototype] : true
95
+ begin
96
+ @socket = TCPSocket::new(@ip, @port)
97
+ @socket.sync = true
98
+ @expecting_prompt=false # initially, the welcome message comes before the prompt, so this so this is false to start with
99
+ @expecting_extra_maybe=false
100
+ welcome="Welcome to the Mozilla JavaScript Shell!\n"
101
+ read=read_value
102
+ if !read
103
+ @expecting_extra_maybe=true
104
+ raise JsshUnableToStart, "Something went wrong initializing - no response"
105
+ elsif read != welcome
106
+ @expecting_extra_maybe=true
107
+ raise JsshUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}"
108
+ end
109
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
110
+ 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}")
111
+ err.set_backtrace($!.backtrace)
112
+ raise err
113
+ end
114
+ if @prototype
115
+ ret=send_and_read(File.read(PrototypeFile))
116
+ if ret != "done!"
117
+ @expecting_extra_maybe=true
118
+ raise JsshError, "Something went wrong loading Prototype - message #{ret.inspect}"
119
+ end
120
+ end
121
+ ret=send_and_read("(function()
122
+ { nativeJSON=Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON);
123
+ nativeJSON_encode_length=function(object)
124
+ { var encoded=nativeJSON.encode(object);
125
+ return encoded.length.toString()+\"\\n\"+encoded;
126
+ }
127
+ return 'done!';
128
+ })()")
129
+ if ret != "done!"
130
+ @expecting_extra_maybe=true
131
+ raise JsshError, "Something went wrong initializing native JSON - message #{ret.inspect}"
132
+ end
133
+ temp_object.assign({})
134
+ end
135
+
136
+ private
137
+ # reads from the socket and returns what seems to be the value that should be returned, by stripping prompts
138
+ # from the beginning and end where appropriate.
139
+ #
140
+ # does not deal with prompts in between values, because attempting to parse those out is impossible, it being
141
+ # perfectly possible that a string the same as the prompt is part of actual data. (even stripping it from the
142
+ # ends of the string is not entirely certain; data could have it at the ends too, but that's the best that can
143
+ # be done.) so, read_value should be called after every line, or you can end up with stuff like:
144
+ # >> @socket.send "3\n4\n5\n", 0
145
+ # => 6
146
+ # >> read_value
147
+ # => "3\n> 4\n> 5"
148
+ #
149
+ # by default, read_value reads until the socket is done being ready. "done being ready" is defined as Kernel.select
150
+ # saying that the socket isn't ready after waiting for SHORT_SOCKET_TIMEOUT. usually this will be true after a
151
+ # single read, as most things only take one #recv call to get the whole value. this waiting for SHORT_SOCKET_TIMEOUT
152
+ # can add up to being slow if you're doing a lot of socket activity.
153
+ # to solve this, performance can be improved significantly using the :length_before_value option. with this, you have
154
+ # to write your javascript to return the length of the value to be sent, followed by a newline, followed by the actual
155
+ # value (which must be of the length it says it is, or this method will error).
156
+ # if this option is set, this doesn't do any SHORT_SOCKET_TIMEOUT waiting once it gets the full value, it returns
157
+ # immediately.
158
+ def read_value(options={})
159
+ options={:timeout => DEFAULT_SOCKET_TIMEOUT, :length_before_value => false, :read_size => 65536}.merge(options)
160
+ received_data = []
161
+ value_string = ""
162
+ size_to_read=options[:read_size]
163
+ timeout=options[:timeout]
164
+ already_read_length=false
165
+ expected_size=nil
166
+ # logger.add(-1) { "RECV_SOCKET is starting. timeout=#{timeout}" }
167
+ while size_to_read > 0 && Kernel.select([@socket] , nil , nil, timeout)
168
+ data = @socket.recv(size_to_read)
169
+ received_data << data
170
+ value_string << data
171
+ if @expecting_prompt && utf8_length_safe(value_string) > PROMPT.length
172
+ if value_string =~ /\A#{Regexp.escape(PROMPT)}/
173
+ value_string.sub!(/\A#{Regexp.escape(PROMPT)}/, '')
174
+ @expecting_prompt=false
175
+ else
176
+ value_string << clear_error
177
+ raise JsshError, "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}"
178
+ end
179
+ end
180
+ if !@expecting_prompt
181
+ if options[:length_before_value] && !already_read_length && value_string.length > 0
182
+ if value_string =~ /\A(\d+)\n/
183
+ expected_size=$1.to_i
184
+ already_read_length=true
185
+ value_string.sub!(/\A\d+\n/, '')
186
+ elsif value_string =~ /\A\d+\z/
187
+ # rather unlikely, but maybe we just received part of the number so far - ignore
188
+ else
189
+ @expecting_extra_maybe=true
190
+ raise JsshError, "Expected length! unexpected data with no preceding length received: #{value_string.inspect}"
191
+ end
192
+ end
193
+ if expected_size
194
+ size_to_read = expected_size - utf8_length_safe(value_string)
195
+ end
196
+ 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.
197
+ timeout=SHORT_SOCKET_TIMEOUT
198
+ end
199
+ end
200
+
201
+ # Kernel.select seems to indicate that a dead socket is ready to read, and returns endless blank strings to recv. rather irritating.
202
+ if received_data.length >= 3 && received_data[-3..-1].all?{|rd| rd==''}
203
+ raise JsshConnectionError, "Socket seems to no longer be connected"
204
+ end
205
+ # logger.add(-1) { "RECV_SOCKET is continuing. timeout=#{timeout}; data=#{data.inspect}" }
206
+ end
207
+ # logger.debug { "RECV_SOCKET is done. received_data=#{received_data.inspect}; value_string=#{value_string.inspect}" }
208
+ if @expecting_extra_maybe
209
+ if Kernel.select([@socket] , nil , nil, SHORT_SOCKET_TIMEOUT)
210
+ cleared_error=clear_error
211
+ if cleared_error==PROMPT
212
+ # 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
213
+ value_string << cleared_error
214
+ else
215
+ raise JsshError, "We finished receiving but the socket was still ready to send! extra data received was: #{cleared_error}"
216
+ end
217
+ end
218
+ @expecting_extra_maybe=false
219
+ end
220
+
221
+ if expected_size
222
+ value_string_length=value_string.unpack("U*").length # JSSH returns a utf-8 string, so unpack each character to get the right length
223
+
224
+ if value_string_length == expected_size
225
+ @expecting_prompt=true
226
+ elsif value_string_length == expected_size + PROMPT.length && value_string =~ /#{Regexp.escape(PROMPT)}\z/
227
+ value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
228
+ @expecting_prompt=false
229
+ else
230
+ @expecting_extra_maybe=true if value_string_length < expected_size
231
+ raise JsshError, "Expected a value of size #{expected_size}; received data of size #{value_string_length}: #{value_string.inspect}"
232
+ end
233
+ else
234
+ if value_string =~ /#{Regexp.escape(PROMPT)}\z/ # what if the value happens to end with the same string as the prompt?
235
+ value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
236
+ @expecting_prompt=false
237
+ else
238
+ @expecting_prompt=true
239
+ end
240
+ end
241
+ return value_string
242
+ end
243
+
244
+ private
245
+ def utf8_length_safe(string)
246
+ string=string.dup
247
+ begin
248
+ string.unpack("U*").length
249
+ rescue ArgumentError # this happens when the socket receive gets split across a utf8 character. we drop the incomplete character from the end.
250
+ if $!.message =~ /malformed UTF-8 character \(expected \d+ bytes, given (\d+) bytes\)/
251
+ given=$1.to_i
252
+ string[0...(-given)].unpack("U*").length
253
+ else # otherwise, this is some other issue we weren't expecting; we will not rescue it.
254
+ raise
255
+ end
256
+ end
257
+ end
258
+ # this should be called when an error occurs and we want to clear the socket of any value remaining on it.
259
+ # this will continue trying for DEFAULT_SOCKET_TIMEOUT until
260
+ def clear_error
261
+ data=""
262
+ while Kernel.select([@socket], nil, nil, SHORT_SOCKET_TIMEOUT)
263
+ # clear any other crap left on the socket
264
+ data << @socket.recv(65536)
265
+ end
266
+ if data =~ /#{Regexp.escape(PROMPT)}\z/
267
+ @expecting_prompt=false
268
+ end
269
+ data
270
+ end
271
+
272
+ public
273
+ def send_and_read(js_expr, options={})
274
+ # logger.add(-1) { "SEND_AND_READ is starting. options=#{options.inspect}" }
275
+ @last_expression=js_expr
276
+ js_expr=js_expr+"\n" unless js_expr =~ /\n\z/
277
+ # logger.debug { "SEND_AND_READ sending #{js_expr.inspect}" }
278
+ @socket.send(js_expr, 0)
279
+ return read_value(options)
280
+ end
281
+
282
+ def js_error(errclassname, message, source, stuff={})
283
+ errclass=if errclassname
284
+ unless JsshError.const_defined?(errclassname)
285
+ JsshError.const_set(errclassname, Class.new(JsshJavascriptError))
286
+ end
287
+ JsshError.const_get(errclassname)
288
+ else
289
+ JsshJavascriptError
290
+ end
291
+ err=errclass.new("#{message}\nEvaluating:\n#{source}\n\nOther stuff:\n#{stuff.inspect}")
292
+ err.source=source
293
+ ["lineNumber", "stack", "fileName"].each do |attr|
294
+ if stuff.key?(attr)
295
+ err.send(:"#{attr}=", stuff[attr])
296
+ end
297
+ end
298
+ raise err
299
+ end
300
+ private :js_error
301
+
302
+ # returns the value of the given javascript expression, as reported by JSSH.
303
+ # This will be a string, the given expression's toString.
304
+ def value(js)
305
+ # this is wrapped in a function so that ...
306
+ # dang, now I can't remember. I'm sure I had a good reason at the time.
307
+ send_and_read("(function(){return #{js}})()")
308
+ end
309
+
310
+ # assigns to the javascript reference on the left the javascript expression on the right.
311
+ # returns the value of the expression as reported by JSSH, which
312
+ # will be a string, the expression's toString. Uses #value; see its documentation.
313
+ def assign(js_left, js_right)
314
+ value("#{js_left}= #{js_right}")
315
+ end
316
+
317
+ # calls to the given function (javascript reference to a function) passing it the
318
+ # given arguments (javascript expressions). returns the return value of the function,
319
+ # a string, the toString of the javascript value. Uses #value; see its documentation.
320
+ def call(js_function, *js_args)
321
+ value("#{js_function}(#{js_args.join(', ')})")
322
+ end
323
+
324
+ # if the given javascript expression ends with an = symbol, #handle calls to #assign
325
+ # assuming it is given one argument; if the expression refers to a function, calls
326
+ # that function with the given arguments using #call; if the expression is some other
327
+ # value, returns that value (its javascript toString), calling #value, assuming
328
+ # given no arguments. Uses #value; see its documentation.
329
+ def handle(js_expr, *args)
330
+ if js_expr=~/=\z/ # doing assignment
331
+ js_left=$`
332
+ if args.size != 1
333
+ raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
334
+ end
335
+ assign(js_left, *args)
336
+ else
337
+ type=typeof(js_expr)
338
+ case type
339
+ when "function"
340
+ call(js_expr, *args)
341
+ when "undefined"
342
+ raise JsshUndefinedValueError, "undefined expression #{js_expr.inspect}"
343
+ else
344
+ if !args.empty?
345
+ raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
346
+ end
347
+ value(js_expr)
348
+ end
349
+ end
350
+ end
351
+
352
+ # returns the value of the given javascript expression. Assuming that it can
353
+ # be converted to JSON, will return the equivalent ruby data type to the javascript
354
+ # value. Will raise an error if the javascript errors.
355
+ def value_json(js, options={})
356
+ options={:error_on_undefined => true}.merge(options)
357
+ raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
358
+ ensure_prototype
359
+ ref_error=options[:error_on_undefined] ? "typeof(result)=='undefined' ? {errored: true, value: {'name': 'ReferenceError', 'message': 'undefined expression in: '+result_f.toString()}} : " : ""
360
+ wrapped_js=
361
+ "try
362
+ { var result_f=(function(){return #{js}});
363
+ var result=result_f();
364
+ nativeJSON_encode_length(#{ref_error} {errored: false, value: result});
365
+ }catch(e)
366
+ { nativeJSON_encode_length({errored: true, value: e});
367
+ }"
368
+ val=send_and_read(wrapped_js, options.merge(:length_before_value => true))
369
+ error_or_val_json(val, js)
370
+ end
371
+ def error_or_val_json(val, js)
372
+ if !val || val==''
373
+ @expecting_extra_maybe=true
374
+ raise JsshError, "received no value! may have timed out waiting for a value that was not coming."
375
+ end
376
+ if val=="SyntaxError: syntax error"
377
+ raise JsshSyntaxError, val
378
+ end
379
+ errord_and_val=parse_json(val)
380
+ unless errord_and_val.is_a?(Hash) && errord_and_val.keys.sort == ['errored', 'value'].sort
381
+ raise RuntimeError, "unexpected result: \n\t#{errord_and_val.inspect} \nencountered parsing value: \n\t#{val.inspect} \nreturned from expression: \n\t#{js.inspect}"
382
+ end
383
+ errord=errord_and_val['errored']
384
+ val= errord_and_val['value']
385
+ if errord
386
+ case val
387
+ when Hash
388
+ js_error(val['name'],val['message'],js,val)
389
+ when String
390
+ js_error(nil, val, js)
391
+ else
392
+ js_error(nil, val.inspect, js)
393
+ end
394
+ else
395
+ val
396
+ end
397
+ end
398
+
399
+ # assigns to the javascript reference on the left the object on the right.
400
+ # Assuming the right object can be converted to JSON, the javascript value will
401
+ # be the equivalent javascript data type to the ruby object. Will return
402
+ # the assigned value, converted from its javascript value back to ruby. So, the return
403
+ # value won't be exactly equivalent if you use symbols for example.
404
+ #
405
+ # >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
406
+ # => {"foo"=>["baz", "qux"]}
407
+ #
408
+ # Uses #value_json; see its documentation.
409
+ def assign_json(js_left, rb_right)
410
+ ensure_prototype
411
+ js_right=rb_right.to_jssh
412
+ value_json("#{js_left}=#{js_right}")
413
+ end
414
+
415
+ # calls to the given function (javascript reference to a function) passing it the
416
+ # given arguments, each argument being converted from a ruby object to a javascript object
417
+ # via JSON. returns the return value of the function, of equivalent type to the javascript
418
+ # return value, converted from javascript to ruby via JSON.
419
+ # Uses #value_json; see its documentation.
420
+ def call_json(js_function, *rb_args)
421
+ ensure_prototype
422
+ js_args=rb_args.map{|arg| arg.to_jssh}
423
+ value_json("#{js_function}(#{js_args.join(', ')})")
424
+ end
425
+
426
+ # does the same thing as #handle, but with json, calling #assign_json, #value_json,
427
+ # or #call_json.
428
+ # if the given javascript expression ends with an = symbol, #handle_json calls to
429
+ # #assign_json assuming it is given one argument; if the expression refers to a function,
430
+ # calls that function with the given arguments using #call_json; if the expression is
431
+ # some other value, returns that value, converted to ruby via JSON, assuming given no
432
+ # arguments. Uses #value_json; see its documentation.
433
+ def handle_json(js_expr, *args)
434
+ ensure_prototype
435
+ if js_expr=~/=\z/ # doing assignment
436
+ js_left=$`
437
+ if args.size != 1
438
+ raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
439
+ end
440
+ assign_json(js_left, *args)
441
+ else
442
+ type=typeof(js_expr)
443
+ case type
444
+ when "function"
445
+ call_json(js_expr, *args)
446
+ when "undefined"
447
+ raise JsshUndefinedValueError, "undefined expression #{js_expr}"
448
+ else
449
+ if !args.empty?
450
+ raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
451
+ end
452
+ value_json(js_expr)
453
+ end
454
+ end
455
+ end
456
+
457
+ # raises error if the prototype library (needed for JSON stuff in javascript) has not been loaded
458
+ def ensure_prototype
459
+ unless prototype
460
+ raise JsshError, "Cannot invoke JSON on a Jssh session that does not have the Prototype library"
461
+ end
462
+ end
463
+
464
+ # returns the type of the given expression using javascript typeof operator, with the exception that
465
+ # if the expression is null, returns 'null' - whereas typeof(null) in javascript returns 'object'
466
+ def typeof(expression)
467
+ ensure_prototype
468
+ js="try
469
+ { nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
470
+ } catch(e)
471
+ { if(e.name=='ReferenceError')
472
+ { nativeJSON_encode_length({errored: false, value: 'undefined'});
473
+ }
474
+ else
475
+ { nativeJSON_encode_length({errored: true, value: e});
476
+ }
477
+ }"
478
+ error_or_val_json(send_and_read(js, :length_before_value => true),js)
479
+ end
480
+
481
+ def instanceof(js_expression, js_interface)
482
+ value_json "(#{js_expression}) instanceof (#{js_interface})"
483
+ end
484
+
485
+ # parses the given JSON string using ActiveSupport::JSON.decode
486
+ # Raises ActiveSupport::JSON::ParseError if given a blank string, something that is not a string, or
487
+ # a string that contains invalid JSON
488
+ def parse_json(json)
489
+ err_class=JSON::ParserError
490
+ decoder=JSON.method(:parse)
491
+ # err_class=ActiveSupport::JSON::ParseError
492
+ # decoder=ActiveSupport::JSON.method(:decode)
493
+ raise err_class, "Not a string! got: #{json.inspect}" unless json.is_a?(String)
494
+ raise err_class, "Blank string!" if json==''
495
+ begin
496
+ return decoder.call(json)
497
+ rescue err_class
498
+ err=$!.class.new($!.message+"\nParsing: #{json.inspect}")
499
+ err.set_backtrace($!.backtrace)
500
+ raise err
501
+ end
502
+ end
503
+
504
+ def object(ref)
505
+ JsshObject.new(ref, self, :debug_name => ref)
506
+ end
507
+
508
+ def temp_object
509
+ @temp_object ||= object('JsshTemp')
510
+ end
511
+ def Components
512
+ @components ||= object('Components')
513
+ end
514
+ def getWindows
515
+ @getwindows ||= object('getWindows()')
516
+ end
517
+
518
+ # raises an informative error if the socket is down for some reason
519
+ def assert_socket
520
+ begin
521
+ actual, expected=if prototype
522
+ [value_json('["foo"]'), ["foo"]]
523
+ else
524
+ [value('"foo"'), "foo"]
525
+ end
526
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
527
+ raise(JsshConnectionError, "Encountered a socket error while checking the socket.\n#{$!.class}\n#{$!.message}", $!.backtrace)
528
+ end
529
+ unless expected==actual
530
+ raise JsshError, "The socket seems to have a problem: sent #{expected.inspect} but got back #{actual.inspect}"
531
+ end
532
+ end
533
+
534
+ def inspect
535
+ "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:ip, :port, :prototype].map{|attr| aa="@#{attr}";aa+'='+instance_variable_get(aa).inspect}.join(', ')}>"
536
+ end
537
+ end
538
+
539
+ class JsshObject
540
+ attr_reader :ref, :jssh_socket
541
+ attr_reader :type, :function_result, :debug_name
542
+ # def logger
543
+ # jssh_socket.logger
544
+ # end
545
+
546
+ public
547
+ # initializes a JsshObject with a string of javascript containing a reference to
548
+ # the object, and a JsshSocket that the object is defined on.
549
+ def initialize(ref, jssh_socket, other={})
550
+ other={:debug_name => ref, :function_result => false}.merge(other)
551
+ raise ArgumentError, "Empty object reference!" if !ref || ref==''
552
+ raise ArgumentError, "Reference must be a string - got #{ref.inspect} (#{ref.class.name})" unless ref.is_a?(String)
553
+ raise ArgumentError, "Not given a JsshSocket, instead given #{jssh_socket.inspect} (#{jssh_socket.class.name})" unless jssh_socket.is_a?(JsshSocket)
554
+ @ref=ref
555
+ @jssh_socket=jssh_socket
556
+ @debug_name=other[:debug_name]
557
+ @function_result=other[:function_result]
558
+ # logger.info { "#{self.class} initialized: #{debug_name} (type #{type})" }
559
+ end
560
+
561
+ # returns the value, via JsshSocket#value_json
562
+ def val
563
+ jssh_socket.value_json(ref, :error_on_undefined => !function_result)
564
+ end
565
+
566
+ # returns the value just as a string with no attempt to deal with type using json. via JsshSocket#value
567
+ #
568
+ # note that this can be slow if it evaluates to a blank string. for example, if ref is just ""
569
+ # then JsshSocket#value will wait DEFAULT_SOCKET_TIMEOUT seconds for data that is not to come.
570
+ # this also happens with functions that return undefined. if ref="function(){do_some_stuff;}"
571
+ # (with no return), it will also wait DEFAULT_SOCKET_TIMEOUT.
572
+ def val_str
573
+ jssh_socket.value(ref)
574
+ end
575
+
576
+ # returns javascript typeof this object
577
+ def type
578
+ if function_result # don't get type for function results, causes function evaluations when you probably didn't want that.
579
+ nil
580
+ else
581
+ # logger.add(-1) { "retrieving type for #{debug_name}" }
582
+ @type||= jssh_socket.typeof(ref)
583
+ end
584
+ end
585
+
586
+ # returns javascript instanceof operator on this and the given interface (expected to be a JsshObject)
587
+ # note that this is javascript, not to be confused with ruby's #instance_of? method.
588
+ #
589
+ # example:
590
+ # window.instanceof(window.jssh_socket.Components.interfaces.nsIDOMChromeWindow)
591
+ # => true
592
+ def instanceof(interface)
593
+ jssh_socket.instanceof(self.ref, interface.ref)
594
+ end
595
+ def implemented_interfaces
596
+ jssh_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
597
+ list << interface if instanceof(interface)
598
+ list
599
+ end
600
+ end
601
+
602
+ # returns the type of object that is reported by the javascript toString() method, which
603
+ # returns such as "[object Object]" or "[object XPCNativeWrapper [object HTMLDocument]]"
604
+ # This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
605
+ # Raises an error if this JsshObject points to something other than a javascript 'object'
606
+ # type ('function' or 'number' or whatever)
607
+ def object_type
608
+ @object_type ||= begin
609
+ case type
610
+ when 'object'
611
+ self.toString! =~ /\A\[object\s+(.*)\]\Z/
612
+ $1
613
+ else
614
+ raise JsshError, "Type is #{type}, not object"
615
+ end
616
+ end
617
+ end
618
+
619
+ def val_or_object(options={})
620
+ options={:error_on_undefined=>true}.merge(options)
621
+ if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
622
+ store_rand_object_key(jssh_socket.temp_object).val_or_object(:error_on_undefined => false)
623
+ else
624
+ case self.type
625
+ when 'undefined'
626
+ if function_result
627
+ nil
628
+ elsif !options[:error_on_undefined]
629
+ self
630
+ else
631
+ raise JsshUndefinedValueError, "undefined expression #{ref}"
632
+ end
633
+ when 'boolean','number','string','null'
634
+ val
635
+ when 'function','object'
636
+ self
637
+ else
638
+ # here we perhaps could (but won't for now) raise JsshError, "Unknown type: #{type}"
639
+ self
640
+ end
641
+ end
642
+ end
643
+
644
+ # returns a JsshObject representing the given attribute. Checks the type, and if
645
+ # it is a function, references the _return value_ of the function (with the given
646
+ # arguments, if any, which are in ruby, converted to_jssh). If the type of the
647
+ # expression is undefined, raises an error (if you want an attribute even if it's
648
+ # undefined, use #attr).
649
+ def invoke(attribute, *args)
650
+ attr_obj=attr(attribute)
651
+ type=attr_obj.type
652
+ case type
653
+ when 'function'
654
+ attr_obj.call(*args)
655
+ else
656
+ if args.empty?
657
+ attr_obj.val_or_object
658
+ else
659
+ raise ArgumentError, "Cannot pass arguments to expression #{attr_obj.ref} of type #{type}"
660
+ end
661
+ end
662
+ end
663
+
664
+ # returns a JsshObject referencing the given attribute of this object
665
+ def attr(attribute)
666
+ unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
667
+ raise JsshSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
668
+ end
669
+ JsshObject.new("#{ref}.#{attribute}", jssh_socket, :debug_name => "#{debug_name}.#{attribute}")
670
+ end
671
+
672
+ # assigns (via JsshSocket#assign) the given ruby value (converted to_jssh) to the reference
673
+ # for this object. returns self.
674
+ def assign(val)
675
+ @debug_name="(#{debug_name}=#{val.is_a?(JsshObject) ? val.debug_name : val.to_jssh})"
676
+ result=assign_expr val.to_jssh
677
+ # logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
678
+ result
679
+ end
680
+ # assigns the given javascript expression (string) to the reference for this object
681
+ def assign_expr(val)
682
+ jssh_socket.value_json("(function(val){#{ref}=val; return null;}(#{val}))")
683
+ @type=nil # uncache this
684
+ # don't want to use JsshSocket#assign_json because converting the result of the assignment (that is, the expression assigned) to json is error-prone and we don't really care about the result.
685
+ # don't want to use JsshSocket#assign because the result can be blank and cause send_and_read to wait for data that's not coming - also
686
+ # using a json function is better because it catches errors much more elegantly.
687
+ # so, wrap it in a function that returns nil.
688
+ self
689
+ end
690
+
691
+ # returns a JsshObject for this object - assumes that this object is a function - passing
692
+ # this function the specified arguments, which are converted to_jssh
693
+ def pass(*args)
694
+ JsshObject.new("#{ref}(#{args.map{|arg| arg.to_jssh}.join(', ')})", jssh_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JsshObject) ? arg.debug_name : arg.to_jssh}.join(', ')})")
695
+ end
696
+
697
+ # returns the value (via JsshSocket#value_json) or a JsshObject (see #val_or_object) of the return
698
+ # value of this function (assumes this object is a function) passing it the given arguments (which
699
+ # are converted to_jssh).
700
+ # simply, it just calls self.pass(*args).val_or_object
701
+ def call(*args)
702
+ pass(*args).val_or_object
703
+ end
704
+
705
+ # sets the given javascript variable to this object, and returns a JsshObject referring
706
+ # to the variable.
707
+ #
708
+ # >> foo=document.getElementById('guser').store('foo')
709
+ # => #<JsshObject:0x2dff870 @ref="foo" ...>
710
+ # >> foo.tagName
711
+ # => "DIV"
712
+ def store(js_variable, somewhere_meaningful=true)
713
+ stored=JsshObject.new(js_variable, jssh_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
714
+ stored.assign_expr(self.ref)
715
+ stored
716
+ end
717
+
718
+ def store_rand_named(&name_proc)
719
+ base=36
720
+ length=6
721
+ begin
722
+ name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
723
+ end while JsshObject.new(name,jssh_socket).type!='undefined'
724
+ # okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
725
+ store(name, false)
726
+ end
727
+
728
+ def store_rand_prefix(prefix)
729
+ store_rand_named do |r|
730
+ prefix+"_"+r
731
+ end
732
+ end
733
+
734
+ def store_rand_object_key(object)
735
+ raise ArgumentError("Object is not a JsshObject: got #{object.inspect}") unless object.is_a?(JsshObject)
736
+ store_rand_named do |r|
737
+ object.sub(r).ref
738
+ end
739
+ end
740
+
741
+ def store_rand_temp
742
+ store_rand_object_key(jssh_socket.temp_object)
743
+ end
744
+
745
+ # returns a JsshObject referring to a subscript of this object, specified as a _javascript_ expression
746
+ # (doesn't use to_jssh)
747
+ # def sub_expr(key_expr)
748
+ # JsshObject.new("#{ref}[#{key_expr}]", jssh_socket, :debug_name => "#{debug_name}[#{}]")
749
+ # end
750
+
751
+ # returns a JsshObject referring to a subscript of this object, specified as a ruby object converted to
752
+ # javascript via to_jssh
753
+ def sub(key)
754
+ JsshObject.new("#{ref}[#{key.to_jssh}]", jssh_socket, :debug_name => "#{debug_name}[#{key.is_a?(JsshObject) ? key.debug_name : key.to_jssh}]")
755
+ end
756
+
757
+ # returns a JsshObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
758
+ # subscript is specified as ruby (converted to_jssh).
759
+ def [](key)
760
+ sub(key).val_or_object(:error_on_undefined => false)
761
+ end
762
+ # assigns the given ruby value (passed through json via JsshSocket#assign_json) to the given subscript
763
+ # (key is converted to_jssh).
764
+ def []=(key, value)
765
+ self.sub(key).assign(value)
766
+ end
767
+
768
+ # calls a binary operator with self and another operand
769
+ def binary_operator(operator, operand)
770
+ JsshObject.new("(#{ref}#{operator}#{operand.to_jssh})", jssh_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JsshObject) ? operand.debug_name : operand.to_jssh})").val_or_object
771
+ end
772
+ def +(operand)
773
+ binary_operator('+', operand)
774
+ end
775
+ def -(operand)
776
+ binary_operator('-', operand)
777
+ end
778
+ def /(operand)
779
+ binary_operator('/', operand)
780
+ end
781
+ def *(operand)
782
+ binary_operator('*', operand)
783
+ end
784
+ def %(operand)
785
+ binary_operator('%', operand)
786
+ end
787
+ def ==(operand)
788
+ binary_operator('==', operand)
789
+ end
790
+ def >(operand)
791
+ binary_operator('>', operand)
792
+ end
793
+ def <(operand)
794
+ binary_operator('<', operand)
795
+ end
796
+ def >=(operand)
797
+ binary_operator('>=', operand)
798
+ end
799
+ def <=(operand)
800
+ binary_operator('<=', operand)
801
+ end
802
+
803
+ # method_missing handles unknown method calls in a way that makes it possible to write
804
+ # javascript-like syntax in ruby, to some extent.
805
+ #
806
+ # method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
807
+ # special characters, only alphanumeric/underscores, starting with alpha or underscore - with
808
+ # the exception of three special behaviors:
809
+ #
810
+ # If the method ends with an equals sign (=), it does assignment - it calls JsshSocket#assign_json
811
+ # to do the assignment and returns the assigned value.
812
+ #
813
+ # If the method ends with a bang (!), then it will attempt to get the value (using json) of the
814
+ # reference, using JsonObject#val. For simple types (null, string, boolean, number), this is what
815
+ # happens by default anyway, but if you have an object or an array that you know you can json-ize,
816
+ # you can use ! to force that. See #invoke documentation for more information.
817
+ #
818
+ # If the method ends with a question mark (?), then it will attempt to get a string representing the
819
+ # value, using JsonObject#val_str. This is safer than ! because the javascript conversion to json
820
+ # can error. This also catches the JsshUndefinedValueError that can occur, and just returns nil
821
+ # for undefined stuff.
822
+ #
823
+ # otherwise, method_missing calls to #invoke, and returns a JsshObject, a string, a boolean, a number, or
824
+ # null - see documentation for #invoke.
825
+ #
826
+ # Since #invoke returns a JsshObject for javascript objects, this means that you can string together
827
+ # method_missings and the result looks rather like javascript.
828
+ #
829
+ # this lets you do things like:
830
+ # >> jssh_socket.object('getWindows()').length
831
+ # => 2
832
+ # >> jssh_socket.object('getWindows()')[1].getBrowser.contentDocument?
833
+ # => "[object XPCNativeWrapper [object HTMLDocument]]"
834
+ # >> document=jssh_socket.object('getWindows()')[1].getBrowser.contentDocument
835
+ # => #<JsshObject:0x34f01fc @ref="getWindows()[1].getBrowser().contentDocument" ...>
836
+ # >> document.title
837
+ # => "ruby - Google Search"
838
+ # >> document.forms[0].q.value
839
+ # => "ruby"
840
+ # >> document.forms[0].q.value='foobar'
841
+ # => "foobar"
842
+ # >> document.forms[0].q.value
843
+ # => "foobar"
844
+ #
845
+ # $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
846
+ # methods to arrays and hashes - see http://www.prototypejs.org/
847
+ # You can use these methods with method_missing just like any other:
848
+ #
849
+ # >> js_hash=jssh_socket.object('$H')
850
+ # => #<JsshObject:0x2beb598 @ref="$H" ...>
851
+ # >> js_arr=jssh_socket.object('$A')
852
+ # => #<JsshObject:0x2be40e0 @ref="$A" ...>
853
+ #
854
+ # >> js_arr.pass(document.body.childNodes).pluck! :tagName
855
+ # => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
856
+ # >> js_arr.pass(document.body.childNodes).pluck! :id
857
+ # => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
858
+ # >> js_hash.pass(document.getElementById('tbd')).keys!
859
+ # => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", "style", "innerHTML", "nextSibling", "tagName", "id", "nodeName", "nodeValue", "nodeType", "childNodes", "firstChild", "lastChild", "previousSibling", "attributes", "ownerDocument", "insertBefore", "replaceChild", "removeChild", "hasChildNodes", "cloneNode", "normalize", "isSupported", "namespaceURI", "prefix", "localName", "hasAttributes", "getAttribute", "setAttribute", "removeAttribute", "getAttributeNode", "setAttributeNode", "removeAttributeNode", "getAttributeNS", "setAttributeNS", "removeAttributeNS", "getAttributeNodeNS", "setAttributeNodeNS", "getElementsByTagNameNS", "hasAttribute", "hasAttributeNS", "ELEMENT_NODE", "ATTRIBUTE_NODE", "TEXT_NODE", "CDATA_SECTION_NODE", "ENTITY_REFERENCE_NODE", "ENTITY_NODE", "PROCESSING_INSTRUCTION_NODE", "COMMENT_NODE", "DOCUMENT_NODE", "DOCUMENT_TYPE_NODE", "DOCUMENT_FRAGMENT_NODE", "NOTATION_NODE", "lang", "dir", "align", "offsetTop", "offsetLeft", "offsetWidth", "offsetHeight", "offsetParent", "scrollTop", "scrollLeft", "scrollHeight", "scrollWidth", "clientTop", "clientLeft", "clientHeight", "clientWidth", "tabIndex", "contentEditable", "blur", "focus", "spellcheck", "removeEventListener", "dispatchEvent", "baseURI", "compareDocumentPosition", "textContent", "isSameNode", "lookupPrefix", "isDefaultNamespace", "lookupNamespaceURI", "isEqualNode", "getFeature", "setUserData", "getUserData", "DOCUMENT_POSITION_DISCONNECTED", "DOCUMENT_POSITION_PRECEDING", "DOCUMENT_POSITION_FOLLOWING", "DOCUMENT_POSITION_CONTAINS", "DOCUMENT_POSITION_CONTAINED_BY", "DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC", "getElementsByClassName", "getClientRects", "getBoundingClientRect"]
860
+ #
861
+ def method_missing(method, *args)
862
+ method=method.to_s
863
+ if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
864
+ method = $1
865
+ special = $2
866
+ else # don't deal with any special character crap
867
+ #Object.instance_method(:method_missing).bind(self).call(method, *args) # let Object#method_missing raise its usual error
868
+ return super
869
+ end
870
+ case special
871
+ when nil
872
+ invoke(method, *args)
873
+ when '!'
874
+ got=invoke(method, *args)
875
+ got.is_a?(JsshObject) ? got.val : got
876
+ when '?'
877
+ begin
878
+ got=invoke(method, *args)
879
+ got.is_a?(JsshObject) ? got.val_str : got
880
+ rescue JsshUndefinedValueError
881
+ nil
882
+ end
883
+ when '='
884
+ attr(method).assign(*args)
885
+ else
886
+ Object.instance_method(:method_missing).bind(self).call(method, *args) # this shouldn't happen
887
+ end
888
+ end
889
+ def define_methods!
890
+ metaclass=(class << self; self; end)
891
+ self.to_hash.keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
892
+ metaclass.send(:define_method, key) do |*args|
893
+ invoke(key, *args)
894
+ end
895
+ end
896
+ end
897
+
898
+ def respond_to?(method)
899
+ super || object_respond_to?(method)
900
+ end
901
+ def object_respond_to?(method)
902
+ method=method.to_s
903
+ if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
904
+ method = $1
905
+ special = $2
906
+ else # don't deal with any special character crap
907
+ return false
908
+ end
909
+
910
+ if self.type=='undefined'
911
+ return false
912
+ elsif special=='='
913
+ if self.type=='object'
914
+ return true # yeah, you can generally assign attributes to objects
915
+ else
916
+ return false # no, you can't generally assign attributes to (boolean, number, string, null)
917
+ end
918
+ else
919
+ attr=attr(method)
920
+ return attr.type!='undefined'
921
+ end
922
+ end
923
+
924
+ def id(*args)
925
+ invoke :id, *args
926
+ end
927
+
928
+ # gives a reference for this object. this is the only class for which to_jssh doesn't
929
+ # convert the object to json.
930
+ def to_jssh
931
+ ref
932
+ end
933
+ # this still needs to be defined because when ActiveSupport::JSON.encode is called by to_jssh
934
+ # on an array or hash containing a JsshObject, it calls to_json. which apparently just freezes.
935
+ # I guess that's because JsshSocket circularly references itself with its instance variables.
936
+ def to_json(options={})
937
+ ref
938
+ end
939
+
940
+ def to_js_array
941
+ jssh_socket.object('$A').call(self)
942
+ end
943
+ def to_js_hash
944
+ jssh_socket.object('$H').call(self)
945
+ end
946
+ def to_js_hash_safe
947
+ jssh_socket.object('$_H').call(self)
948
+ end
949
+ def to_array
950
+ JsshArray.new(self.ref, self.jssh_socket, :debug_name => debug_name)
951
+ end
952
+ def to_hash
953
+ JsshHash.new(self.ref, self.jssh_socket, :debug_name => debug_name)
954
+ end
955
+ def to_dom
956
+ JsshDOMNode.new(self.ref, self.jssh_socket, :debug_name => debug_name)
957
+ end
958
+ def to_ruby_hash(options={})
959
+ options={:recurse => 1}.merge(options)
960
+ return self if !options[:recurse] || options[:recurse]==0
961
+ return self if self.type!='object'
962
+ next_options=options.merge(:recurse => options[:recurse]-1)
963
+ begin
964
+ keys=self.to_hash.keys
965
+ rescue JsshError
966
+ return self
967
+ end
968
+ keys.inject({}) do |hash, key|
969
+ val=begin
970
+ self[key]
971
+ rescue JsshError
972
+ $!
973
+ end
974
+ hash[key]=if val.is_a?(JsshObject)
975
+ val.to_ruby_hash(next_options)
976
+ else
977
+ val
978
+ end
979
+ hash
980
+ end
981
+ end
982
+
983
+ def inspect
984
+ "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
985
+ end
986
+ def pretty_print(pp)
987
+ pp.object_address_group(self) do
988
+ pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
989
+ pp.breakable ' '
990
+ pp.group(0) do
991
+ pp.text attr.to_s
992
+ pp.text ': '
993
+ #pp.breakable
994
+ pp.text send(attr)
995
+ end
996
+ end
997
+ end
998
+ end
999
+ end
1000
+
1001
+ class JsshDOMNode < JsshObject
1002
+ def inspect
1003
+ # "\#<#{self.class.name} #{[:nodeName, :nodeType, :tagName, :textContent, :id, :name, :value, :type].map{|attrn| attr=attr(attrn);attrn.to_s+'='+(attr.type=='undefined' ? 'undefined' : attr.val_or_object(:error_on_undefined => false).inspect)}.join(', ')}>"
1004
+ "\#<#{self.class.name} #{[:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map{|attrn|attr=attr(attrn);(['undefined','null'].include?(attr.type) ? nil : attrn.to_s+'='+attr.val_or_object(:error_on_undefined => false).inspect)}.select{|a|a}.join(', ')}>"
1005
+ end
1006
+ def dump(options={})
1007
+ options={:recurse => nil, :level => 0}.merge(options)
1008
+ next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
1009
+ result=(" "*options[:level]*2)+self.inspect+"\n"
1010
+ if options[:recurse]==0
1011
+ result+=(" "*next_options[:level]*2)+"...\n"
1012
+ else
1013
+ self.childNodes.to_array.each do |child|
1014
+ result+=child.to_dom.dump(next_options)
1015
+ end
1016
+ end
1017
+ result
1018
+ end
1019
+ end
1020
+
1021
+ class JsshArray < JsshObject
1022
+ def each
1023
+ length=self.length
1024
+ raise JsshError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
1025
+ for i in 0...length
1026
+ element=self[i]
1027
+ if element.is_a?(JsshObject)
1028
+ # yield a more permanent reference than the array subscript
1029
+ element=element.store_rand_temp
1030
+ end
1031
+ yield element
1032
+ end
1033
+ end
1034
+ include Enumerable
1035
+ def to_json(options={}) # Enumerable clobbers this; redefine
1036
+ ref
1037
+ end
1038
+ end
1039
+
1040
+ class JsshHash < JsshObject
1041
+ def keys
1042
+ keyfunc="function(obj)
1043
+ { var keys=[];
1044
+ for(var key in obj)
1045
+ { keys.push(key);
1046
+ }
1047
+ return keys;
1048
+ }"
1049
+ @keys=jssh_socket.object(keyfunc).pass(self).val
1050
+ end
1051
+ def each
1052
+ keys.each do |key|
1053
+ yield [key, self[key]]
1054
+ end
1055
+ end
1056
+ def each_pair
1057
+ each do |(k,v)|
1058
+ yield k,v
1059
+ end
1060
+ end
1061
+
1062
+ include Enumerable
1063
+ def to_json(options={}) # Enumerable clobbers this; redefine
1064
+ ref
1065
+ end
1066
+ end