webkit_remote 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ module WebkitRemote
2
+
3
+ class Event
4
+
5
+ # Emitted when a page's load event is triggerred.
6
+ class PageLoaded < WebkitRemote::Event
7
+ register 'Page.loadEventFired'
8
+
9
+ # @return [Number] the event timestamp
10
+ attr_reader :timestamp
11
+
12
+ # @private Use Event#for instead of calling this constructor directly.
13
+ def initialize(rpc_event)
14
+ super
15
+ @timestamp = raw_data['timestamp']
16
+ end
17
+
18
+ # @private Use Event#can_receive instead of calling this directly.
19
+ def self.can_reach?(client)
20
+ !!client.page_events
21
+ end
22
+ end # class WebkitRemote::Event::PageLoaded
23
+
24
+ # Emitted when a page's DOMcontent event is triggerred.
25
+ class PageDomReady < WebkitRemote::Event
26
+ register 'Page.domContentEventFired'
27
+
28
+ # @return [Number] the event timestamp
29
+ attr_reader :timestamp
30
+
31
+ # @private Use Event#for instead of calling this constructor directly.
32
+ def initialize(rpc_event)
33
+ super
34
+ @timestamp = raw_data['timestamp']
35
+ end
36
+
37
+ # @private Use Event#can_receive instead of calling this directly.
38
+ def self.can_reach?(client)
39
+ !!client.page_events
40
+ end
41
+ end # class WebkitRemote::Event::PageDomReady
42
+
43
+ end # namespace WebkitRemote::Event
44
+
45
+ end # namepspace WebkitRemote
@@ -0,0 +1,406 @@
1
+ module WebkitRemote
2
+
3
+ class Client
4
+
5
+ # API for the Runtime domain.
6
+ module Runtime
7
+ # Evals a JavaScript expression.
8
+ #
9
+ # @param [String] expression the JavaScript expression to be evaluated
10
+ # @param [Hash] opts tweaks
11
+ # @option opts [String, Symbol] group the name of an object group (think
12
+ # memory pools); the objects in a group can be released together by one
13
+ # call to WebkitRemote::Client::RemoteObjectGroup#release
14
+ # @return [WebkitRemote::Client::RemoteObject, Boolean, Number, String] the
15
+ # result of evaluating the expression
16
+ def remote_eval(expression, opts = {})
17
+ group_name = opts[:group] || '_'
18
+ # NOTE: returnByValue is always set to false to avoid some extra complexity
19
+ result = @rpc.call 'Runtime.evaluate', expression: expression,
20
+ objectGroup: group_name
21
+ object = WebkitRemote::Client::RemoteObject.for result['result'], self,
22
+ group_name
23
+ if result['wasThrown']
24
+ # TODO(pwnall): some wrapper for exceptions?
25
+ object
26
+ else
27
+ object
28
+ end
29
+ end
30
+
31
+ # Retrieves a group of remote objects by its name.
32
+ #
33
+ # @param [String, Symbol] group_name name given to remote_eval when the
34
+ # object was created
35
+ # @param [Boolean] create if true, fetching a group that does not exist will
36
+ # create the group; this parameter should only be used internally
37
+ # @return [WebkitRemote::Client::RemoteObject, Boolean, Number, String, nil]
38
+ # a Ruby wrapper for the evaluation result; primitives get wrapped by
39
+ # standard Ruby classes, and objects get wrapped by RemoteObject
40
+ # instances
41
+ def object_group(group_name, create = false)
42
+ group_name = group_name.to_s
43
+ group = @runtime_groups[group_name]
44
+ return group if group
45
+ if create
46
+ @runtime_groups[group_name] =
47
+ WebkitRemote::Client::RemoteObjectGroup.new(group_name, self)
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ # Removes a group from the list of tracked groups.
54
+ #
55
+ # @private Use WebkitRemote::Client::RemoteObjectGroup#release instead of
56
+ # calling this directly.
57
+ # @return [WebkitRemote::Client] self
58
+ def object_group_remove(group)
59
+ @runtime_groups.delete group.name
60
+ self
61
+ end
62
+
63
+ # @private Called by the WebkitRemote::Client constructor.
64
+ def initialize_runtime()
65
+ @runtime_groups = {}
66
+ end
67
+ end # module WebkitRemote::Client::Runtime
68
+
69
+ initializer :initialize_runtime
70
+ include Runtime
71
+
72
+ # Mirrors a RemoteObject, defined in the Runtime domain.
73
+ class RemoteObject
74
+ # @return [String] the class name computed by WebKit for this object
75
+ attr_reader :js_class_name
76
+
77
+ # @return [String] the return value of the JavaScript typeof operator
78
+ attr_reader :js_type
79
+
80
+ # @return [Symbol] an additional type hint for this object; documented values
81
+ # are :array, :date, :node, :null, :regexp
82
+ attr_reader :js_subtype
83
+
84
+ # @return [String] string that would be displayed in the Webkit console to
85
+ # represent this object
86
+ attr_reader :description
87
+
88
+ # @return [Object] primitive value for this object, if available
89
+ attr_reader :value
90
+
91
+ # @return [Hash<String, Object>] the raw info provided by the remote debugger
92
+ # RPC call; might be useful for accessing extended metadata that is not
93
+ # (yet) recognized by WebkitRemote
94
+ attr_reader :raw_data
95
+
96
+ # @return [Boolean] true if the objects in this group were already released
97
+ attr_reader :released
98
+ alias_method :released?, :released
99
+
100
+ # @return [WebkitRemote::Client] remote debugging client for the browser tab
101
+ # that owns the objects in this group
102
+ attr_reader :client
103
+
104
+ # @return [WebkitRemote::Client::RemoteObjectGroup] the group that contains
105
+ # this object; the object can be released by calling release_all on the
106
+ # group
107
+ attr_reader :group
108
+
109
+ # @return [String] identifies this object in the remote debugger
110
+ # @private Use the RemoteObject methods instead of calling this directly.
111
+ attr_reader :remote_id
112
+
113
+ # Releases this remote object on the browser side.
114
+ #
115
+ # @return [Webkit::Client::RemoteObject] self
116
+ def release
117
+ return if @released
118
+ @rpc.call 'Runtime.releaseObject', objectId: @remote_id
119
+ @group.remove self
120
+ released!
121
+ end
122
+
123
+ # This object's properties.
124
+ #
125
+ # If the object's properties have not been retrieved, this method retrieves
126
+ # them via a RPC call.
127
+ #
128
+ # @return [Hash<Symbol, Webkit::Client::RemoteProperty>] frozen Hash containg
129
+ # the object's properties
130
+ def properties
131
+ @properties || properties!
132
+ end
133
+
134
+ # This object's properties, guaranteed to be fresh.
135
+ #
136
+ # This method always reloads the object's properties via a RPC call.
137
+ #
138
+ # @return [Hash<Symbol, Webkit::Client::RemoteProperty>] frozen Hash containg
139
+ # the object's properties
140
+ def properties!
141
+ result = @rpc.call 'Runtime.getProperties', objectId: @remote_id
142
+ @properties = Hash[
143
+ result['result'].map do |raw_property|
144
+ property = WebkitRemote::Client::RemoteProperty.new raw_property, self
145
+ [property.name, property]
146
+ end
147
+ ].freeze
148
+ end
149
+
150
+ # Calls a function with "this" bound to this object.
151
+ #
152
+ # @param [String] function_expression a JavaScript expression that should
153
+ # evaluate to a function
154
+ # @param [Array<WebkitRemote::Client::Object, String, Number, Boolean, nil>]
155
+ # args the arguments passed to the function
156
+ # @return [WebkitRemote::Client::RemoteObject, Boolean, Number, String, nil]
157
+ # a Ruby wrapper for the given raw object; primitives get wrapped by
158
+ # standard Ruby classes, and objects get wrapped by RemoteObject
159
+ # instances
160
+ def bound_call(function_expression, *args)
161
+ call_args = args.map do |arg|
162
+ if arg.kind_of? WebkitRemote::Client::RemoteObject
163
+ { objectId: arg.remote_id }
164
+ else
165
+ { value: arg }
166
+ end
167
+ end
168
+ result = @rpc.call 'Runtime.callFunctionOn', objectId: @remote_id,
169
+ functionDeclaration: function_expression, arguments: call_args,
170
+ returnByValue: false
171
+ object = WebkitRemote::Client::RemoteObject.for result['result'], @client,
172
+ @group.name
173
+ if result['wasThrown']
174
+ # TODO(pwnall): some wrapper for exceptions?
175
+ object
176
+ else
177
+ object
178
+ end
179
+ end
180
+
181
+ # Wraps a raw object returned by the Webkit remote debugger RPC protocol.
182
+ #
183
+ # @private Use WebkitRemote::Client::Runtime#remote_eval instead of calling
184
+ # this directly.
185
+ #
186
+ # @param [Hash<String, Object>] raw_object a RemoteObject instance, according
187
+ # to the Webkit remote debugging protocol; this is the return value of a
188
+ # 'Runtime.evaluate' RPC call
189
+ # @param [WebkitRemote::Client::Runtime] client remote debugging client for
190
+ # the browser tab that owns this object
191
+ # @param [String] group_name name of the object group that will hold this
192
+ # object; object groups work like memory pools
193
+ # @return [WebkitRemote::Client::RemoteObject, Boolean, Number, String] a
194
+ # Ruby wrapper for the given raw object; primitives get wrapped by
195
+ # standard Ruby classes, and objects get wrapped by RemoteObject
196
+ # instances
197
+ def self.for(raw_object, client, group_name)
198
+ if remote_id = raw_object['objectId']
199
+ group = client.object_group group_name, true
200
+ return group.get(remote_id) ||
201
+ WebkitRemote::Client::RemoteObject.new(raw_object, group)
202
+ else
203
+ # primitive types
204
+ case raw_object['type'] ? raw_object['type'].to_sym : nil
205
+ when :boolean, :number, :string
206
+ return raw_object['value']
207
+ when :undefined
208
+ # TODO(pwnall): Not sure what to do here.
209
+ when :function
210
+ # TODO(pwnall): Not sure what to do here.
211
+ when :object
212
+ case raw_object['subtype'] ? raw_object['subtype'].to_sym : nil
213
+ when :null
214
+ return nil
215
+ end
216
+ # TODO(pwnall): Figure this out.
217
+ end
218
+ end
219
+ raise RuntimeError, "Unable to parse #{raw_object.inspect}"
220
+ end
221
+
222
+ # Wraps a remote JavaScript object
223
+ #
224
+ # @private RemoteObject#for should be used instead of this, as it handles
225
+ # some edge cases
226
+ def initialize(raw_object, group)
227
+ @group = group
228
+ @client = group.client
229
+ @rpc = client.rpc
230
+ @released = false
231
+
232
+ @raw_data = raw_object
233
+ @remote_id = raw_object['objectId']
234
+ @js_class_name = raw_object['className']
235
+ @description = raw_object['description']
236
+ @js_type = raw_object['type'].to_sym
237
+ if raw_object['subtype']
238
+ @js_subtype = raw_object['subtype'].to_sym
239
+ else
240
+ @js_subtype = nil
241
+ end
242
+ @value = raw_object['value']
243
+
244
+ group.add self
245
+ end
246
+
247
+ # Informs this object that it was released as part of a group release.
248
+ #
249
+ # @private Called by RemoteObjectGroup#release_all.
250
+ def released!
251
+ @released = true
252
+ @group = nil
253
+ end
254
+ end # class WebkitRemote::Client::RemoteObject
255
+
256
+ # Tracks the remote objects in a group (think memory pool).
257
+ class RemoteObjectGroup
258
+ # @return [String] the name of the group of remote objects
259
+ attr_reader :name
260
+
261
+ # @return [WebkitRemote::Client] remote debugging client for the browser tab
262
+ # that owns the objects in this group
263
+ attr_reader :client
264
+
265
+ # @return [Boolean] true if the objects in this group were already released
266
+ attr_reader :released
267
+ alias_method :released?, :released
268
+
269
+ # Releases all the remote objects in this group.
270
+ #
271
+ # @return [Webkit::Client::RemoteObjectGroup] self
272
+ def release_all
273
+ return if @objects.empty?
274
+ @rpc.call 'Runtime.releaseObjectGroup', objectGroup: name
275
+ @released = true
276
+ @objects.each_value { |object| object.released! }
277
+ @objects.clear
278
+ @client.object_group_remove self
279
+ self
280
+ end
281
+
282
+ # Checks if a remote object was allocated in this group.
283
+ #
284
+ # @param [WebkitRemote::Client] object
285
+ # @return [Boolean] true if the object belongs to this group, so releasing
286
+ # the group would get the object released
287
+ def include?(object)
288
+ @objects[object.remote_id] == object
289
+ end
290
+
291
+ # Creates a wrapper for a group of remote objects.
292
+ #
293
+ # @private Use WebkitRemote::Client::Runtime#remote_eval instead of calling
294
+ # this directly.
295
+ #
296
+ # @param [String] name name of this group of remote objects
297
+ # @param [WebkitRemote::Client] client remote debugging client for the
298
+ # browser tab that owns the objects in this group
299
+ def initialize(name, client)
300
+ @name = name
301
+ @client = client
302
+ @rpc = client.rpc
303
+ # TODO(pwnall): turn @objects into a set once equality is working
304
+ @objects = {}
305
+ @released = false
306
+ end
307
+
308
+ # Registers a remote object that belongs to this group.
309
+ #
310
+ # @private Use WebkitRemote::Client::Runtime#remote_eval instead of calling
311
+ # this directly.
312
+ #
313
+ # @param [WebkitRemote::Client::RemoteObject] object the object to be added
314
+ # to this group
315
+ # @return [WebkitRemote::Client::RemoteObjectGroup] self
316
+ def add(object)
317
+ if @released
318
+ raise RuntimeError, 'Remote object group already released'
319
+ end
320
+ @objects[object.remote_id] = object
321
+ self
322
+ end
323
+
324
+ # Removes a remote object that was individually released.
325
+ #
326
+ # @private Use WebkitRemote::Client::RemoteObject#release instead of calling
327
+ # this directly
328
+ #
329
+ # @param [WebkitRemote::Client::RemoteObject] object the object that will be
330
+ # removed from the group
331
+ # @return [WebkitRemote::Client::RemoteObjectGroup] self
332
+ def remove(object)
333
+ @objects.delete object.remote_id
334
+ if @objects.empty?
335
+ @released = true
336
+ @client.object_group_remove self
337
+ end
338
+ self
339
+ end
340
+
341
+ # Returns the object in this group with a given id.
342
+ #
343
+ # This helps avoid creating multiple wrappers for the same object.
344
+ #
345
+ # @param [String] remote_id the id to look for
346
+ # @return [WebkitRemote::Client::RemoteObject, nil] nil if there is no object
347
+ # whose remote_id matches the method's parameter
348
+ def get(remote_id)
349
+ @objects.fetch remote_id, nil
350
+ end
351
+ end # class WebkitRemote::Client::RemoteObjectGroup
352
+
353
+ # A property of a remote JavaScript object.
354
+ class RemoteProperty
355
+ # @return [Symbol] the
356
+ attr_reader :name
357
+
358
+ # @return [WebkitRemote::Client::RemoteObject, Boolean, Number, String, nil]
359
+ # a Ruby wrapper for the property's value; primitives get wrapped by
360
+ # standard Ruby classes, and objects get wrapped by RemoteObject
361
+ # instances
362
+ attr_reader :value
363
+
364
+ # @return [Boolean] true if JavaScript code can remove this property
365
+ attr_reader :configurable
366
+ alias_method :configurable?, :configurable
367
+
368
+ # @return [Boolean] true if JavaScript code can enumerate this property
369
+ attr_reader :enumerable
370
+ alias_method :enumerable?, :enumerable
371
+
372
+ # @return [Boolean] true if JavaScript code can change this property's value
373
+ attr_reader :writable
374
+ alias_method :writable?, :writable
375
+
376
+ # @return [WebkitRemote::RemoteObject] the object that this property belongs
377
+ # to
378
+ attr_reader :owner
379
+
380
+ # @param [Hash<String, Object>] raw_property a PropertyDescriptor instance,
381
+ # according to the Webkit remote debugging protocol; this is an item in
382
+ # the array returned by the 'Runtime.getProperties' RPC call
383
+ # @param [WebkitRemote::Client::RemoteObject] owner the object that this
384
+ # property belongs to
385
+ def initialize(raw_property, owner)
386
+ # NOTE: these are only used at construction time
387
+ client = owner.client
388
+ group_name = owner.group.name
389
+
390
+ @owner = owner
391
+ @name = raw_property['name'].to_sym
392
+ @configurable = !!raw_property['configurable']
393
+ @enumerable = !!raw_property['enumerable']
394
+ @writable = !!raw_property['writable']
395
+ @js_getter = raw_property['get'] && WebkitRemote::Client::RemoteObject.for(
396
+ raw_property['get'], client, group_name)
397
+ @js_setter = raw_property['set'] && WebkitRemote::Client::RemoteObject.for(
398
+ raw_property['set'], client, group_name)
399
+ @value = raw_property['value'] && WebkitRemote::Client::RemoteObject.for(
400
+ raw_property['value'], client, group_name)
401
+ end
402
+ end # class WebkitRemote::Client::RemoteProperty
403
+
404
+ end # namespace WebkitRemote::Client
405
+
406
+ end # namespace WebkitRemote