webkit_remote 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +114 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/lib/webkit_remote/browser.rb +150 -0
- data/lib/webkit_remote/client/page.rb +59 -0
- data/lib/webkit_remote/client/page_events.rb +45 -0
- data/lib/webkit_remote/client/runtime.rb +406 -0
- data/lib/webkit_remote/client.rb +114 -0
- data/lib/webkit_remote/event.rb +138 -0
- data/lib/webkit_remote/process.rb +160 -0
- data/lib/webkit_remote/rpc.rb +205 -0
- data/lib/webkit_remote/top_level.rb +31 -0
- data/lib/webkit_remote.rb +12 -0
- data/test/fixtures/config.ru +12 -0
- data/test/fixtures/html/load.html +7 -0
- data/test/fixtures/html/runtime.html +31 -0
- data/test/helper.rb +44 -0
- data/test/webkit_remote/browser_test.rb +80 -0
- data/test/webkit_remote/client/page_test.rb +31 -0
- data/test/webkit_remote/client/remote_object_group_test.rb +81 -0
- data/test/webkit_remote/client/remote_object_test.rb +133 -0
- data/test/webkit_remote/client/runtime_test.rb +109 -0
- data/test/webkit_remote/client_test.rb +99 -0
- data/test/webkit_remote/event_test.rb +83 -0
- data/test/webkit_remote/process_test.rb +52 -0
- data/test/webkit_remote/rpc_test.rb +55 -0
- data/test/webkit_remote_test.rb +63 -0
- metadata +274 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
module WebkitRemote
|
2
|
+
|
3
|
+
# Client for the Webkit remote debugging protocol
|
4
|
+
#
|
5
|
+
# A client manages a single tab.
|
6
|
+
class Client
|
7
|
+
# Connects to the remote debugging server in a Webkit tab.
|
8
|
+
#
|
9
|
+
# @param [Hash] opts info on the tab to connect to
|
10
|
+
# @option opts [WebkitRemote::Tab] tab reference to the tab whose debugger
|
11
|
+
# server this RPC client connects to
|
12
|
+
# @option opts [Boolean] close_browser if true, the session to the brower
|
13
|
+
# that the tab belongs to will be closed when this RPC client's connection
|
14
|
+
# is closed
|
15
|
+
def initialize(opts = {})
|
16
|
+
unless tab = opts[:tab]
|
17
|
+
raise ArgumentError, 'Target tab not specified'
|
18
|
+
end
|
19
|
+
@rpc = WebkitRemote::Rpc.new opts
|
20
|
+
@browser = tab.browser
|
21
|
+
@close_browser = opts.fetch :close_browser, false
|
22
|
+
@closed = false
|
23
|
+
initialize_modules
|
24
|
+
end
|
25
|
+
|
26
|
+
# Closes the remote debugging connection.
|
27
|
+
#
|
28
|
+
# Call this method to avoid leaking resources.
|
29
|
+
#
|
30
|
+
# @return [WebkitRemote::Rpc] self
|
31
|
+
def close
|
32
|
+
return if @closed
|
33
|
+
@closed = true
|
34
|
+
@rpc.close
|
35
|
+
@rpc = nil
|
36
|
+
@browser.close if @close_browser
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Boolean] if true, the connection to the remote debugging server
|
41
|
+
# has been closed, and this instance is mostly useless
|
42
|
+
attr_reader :closed
|
43
|
+
alias_method :closed?, :closed
|
44
|
+
|
45
|
+
# @return [Boolean] if true, the master debugging connection to the browser
|
46
|
+
# associated with the client's tab will be automatically closed when this
|
47
|
+
# RPC client's connection is closed; in turn, this might stop the browser
|
48
|
+
# process
|
49
|
+
attr_accessor :close_browser
|
50
|
+
alias_method :close_browser?, :close_browser
|
51
|
+
|
52
|
+
# Continuously reports events sent by the remote debugging server.
|
53
|
+
#
|
54
|
+
# @yield once for each RPC event received from the remote debugger; break to
|
55
|
+
# stop the event listening loop
|
56
|
+
# @yieldparam [WebkitRemote::Event] event an instance of an Event sub-class
|
57
|
+
# that best represents the received event
|
58
|
+
# @return [WebkitRemote::Client] self
|
59
|
+
def each_event
|
60
|
+
@rpc.each_event do |rpc_event|
|
61
|
+
yield WebkitRemote::Event.for(rpc_event)
|
62
|
+
end
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
# Waits for the remote debugging server to send a specific event.
|
67
|
+
#
|
68
|
+
# @param (see WebkitRemote::Event#matches?)
|
69
|
+
# @return [WebkitRemote::Event] the event that matches the class requirement
|
70
|
+
def wait_for(conditions)
|
71
|
+
unless WebkitRemote::Event.can_receive? self, conditions
|
72
|
+
raise ArgumentError, "Cannot receive event with #{conditions.inspect}"
|
73
|
+
end
|
74
|
+
|
75
|
+
returned_event = nil
|
76
|
+
each_event do |event|
|
77
|
+
if event.matches? conditions
|
78
|
+
returned_event = event
|
79
|
+
break
|
80
|
+
end
|
81
|
+
end
|
82
|
+
returned_event
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [WebkitRemote::Rpc] the WebSocket RPC client; useful for making raw
|
86
|
+
# RPC calls to unsupported methods
|
87
|
+
attr_reader :rpc
|
88
|
+
|
89
|
+
# @return [WebkitRemote::Browser] master session to the browser that owns the
|
90
|
+
# tab debugged by this client
|
91
|
+
attr_reader :browser
|
92
|
+
|
93
|
+
# Call by the constructor. Replaced by the module initializers.
|
94
|
+
#
|
95
|
+
# @private Hook for module initializers to do their own setups.
|
96
|
+
def initialize_modules
|
97
|
+
# NOTE: this gets called after all the module initializers complete
|
98
|
+
end
|
99
|
+
|
100
|
+
# Registers a module initializer.
|
101
|
+
def self.initializer(name)
|
102
|
+
before_name = :"initialize_modules_before_#{name}"
|
103
|
+
alias_method before_name, :initialize_modules
|
104
|
+
remove_method :initialize_modules
|
105
|
+
eval <<END_METHOD
|
106
|
+
def initialize_modules
|
107
|
+
#{name}
|
108
|
+
#{before_name.to_s}
|
109
|
+
end
|
110
|
+
END_METHOD
|
111
|
+
end
|
112
|
+
end # class WebkitRemote::Client
|
113
|
+
|
114
|
+
end # namespace WebkitRemote
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module WebkitRemote
|
2
|
+
|
3
|
+
# An event received via a RPC notification from a Webkit remote debugger.
|
4
|
+
#
|
5
|
+
# This is a generic super-class for events.
|
6
|
+
class Event
|
7
|
+
# @return [String] event's domain, e.g. "Page", "DOM".
|
8
|
+
attr_reader :domain
|
9
|
+
|
10
|
+
# @return [String] event's name, e.g. "Page.loadEventFired".
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# @return [Hash<String, Object>] the raw event information provided by the
|
14
|
+
# RPC client
|
15
|
+
attr_reader :raw_data
|
16
|
+
|
17
|
+
# Checks if the event meets a set of conditions.
|
18
|
+
#
|
19
|
+
# This is used in WebkitRemote::Client#wait_for.
|
20
|
+
#
|
21
|
+
# @param [Hash<Symbol, Object>] conditions the conditions that must be met
|
22
|
+
# by an event to get out of the waiting loop
|
23
|
+
# @option conditions [Class] class the class of events to wait for; this
|
24
|
+
# condition is met if the event's class is a sub-class of the given class
|
25
|
+
# @option conditions [Class] type synonym for class that can be used with the
|
26
|
+
# Ruby 1.9 hash syntax
|
27
|
+
# @option conditions [String] name the event's name, e.g.
|
28
|
+
# "Page.loadEventFired"
|
29
|
+
# @return [Boolean] true if this event matches all the given conditions
|
30
|
+
def matches?(conditions)
|
31
|
+
conditions.all? do |key, value|
|
32
|
+
case key
|
33
|
+
when :class, :type
|
34
|
+
kind_of? value
|
35
|
+
when :name
|
36
|
+
name == value
|
37
|
+
else
|
38
|
+
# Simple cop-out.
|
39
|
+
send(key) == value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if a client can possibly meet an event meeting the given conditions.
|
45
|
+
#
|
46
|
+
# @private This is used by Client#wait_for to prevent hard-to-find bugs.
|
47
|
+
#
|
48
|
+
# @param [WebkitRemote::Client] client the client to be checked
|
49
|
+
# @param (see WebkitRemote::Event#matches?)
|
50
|
+
# @return [Boolean] false if calling WebkitRemote::Client#wait_for with the
|
51
|
+
# given conditions would get the client stuck
|
52
|
+
def self.can_receive?(client, conditions)
|
53
|
+
conditions.all? do |key, value|
|
54
|
+
case key
|
55
|
+
when :class, :type
|
56
|
+
value.can_reach?(client)
|
57
|
+
when :name
|
58
|
+
class_for(value).can_reach?(client)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Wraps raw event data received via a RPC notification.
|
64
|
+
#
|
65
|
+
# @param [Hash<Symbol, Object>] rpc_event event information yielded by a call
|
66
|
+
# to WebkitRemote::Rpc.each_event
|
67
|
+
# @return [WebkitRemote::Event] an instance of an Event subclass that best
|
68
|
+
# represents the given event
|
69
|
+
def self.for(rpc_event)
|
70
|
+
klass = class_for rpc_event[:name]
|
71
|
+
klass.new rpc_event
|
72
|
+
end
|
73
|
+
|
74
|
+
# The WebkitRemote::Event subclass registered to handle an event.
|
75
|
+
#
|
76
|
+
# @private Use WebkitRemote::Event#for instead of calling this directly.
|
77
|
+
#
|
78
|
+
# @param [String] rpc_event_name the value of the 'name' property of an event
|
79
|
+
# notice received via the remote debugging RPC
|
80
|
+
# @return [Class] WebkitRemote::Event or one of its subclasses
|
81
|
+
def self.class_for(rpc_event_name)
|
82
|
+
@registry[rpc_event_name] || Event
|
83
|
+
end
|
84
|
+
|
85
|
+
# Wraps raw event data received via a RPC notification.
|
86
|
+
#
|
87
|
+
# @private API clients should use Event#for instead of calling the
|
88
|
+
# constructor directly.
|
89
|
+
#
|
90
|
+
# @param [Hash<Symbol, Object>] rpc_event event information yielded by a call
|
91
|
+
# to WebkitRemote::Rpc.each_event
|
92
|
+
def initialize(rpc_event)
|
93
|
+
@name = rpc_event[:name]
|
94
|
+
@domain = rpc_event[:name].split('.', 2).first
|
95
|
+
@raw_data = rpc_event[:data] || {}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Registers an Event sub-class for to be instantiated when parsing an event.
|
99
|
+
#
|
100
|
+
# @private Only Event sub-classes should use this API.
|
101
|
+
#
|
102
|
+
# @param [String] name fully qualified event name, e.g. "Page.loadEventFired"
|
103
|
+
# @return [Class] self
|
104
|
+
def self.register(name)
|
105
|
+
WebkitRemote::Event.register_class self, name
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# Registers an Event sub-class for to be instantiated when parsing an event.
|
110
|
+
#
|
111
|
+
# @private Event sub-classes should call #register on themselves instead of
|
112
|
+
# calling this directly.
|
113
|
+
#
|
114
|
+
# @param [String] klass the Event subclass to be registered
|
115
|
+
# @param [String] name fully qualified event name, e.g. "Page.loadEventFired"
|
116
|
+
# @return [Class] self
|
117
|
+
def self.register_class(klass, name)
|
118
|
+
if @registry.has_key? name
|
119
|
+
raise ArgumentError, "#{@registry[name].name} already registered #{name}"
|
120
|
+
end
|
121
|
+
@registry[name] = klass
|
122
|
+
self
|
123
|
+
end
|
124
|
+
@registry = {}
|
125
|
+
|
126
|
+
# Checks if a client is set up to receive an event of this class.
|
127
|
+
#
|
128
|
+
# @private Use Event# instead of calling this directly.
|
129
|
+
#
|
130
|
+
# This method is overridden in Event sub-classes. For example, events in the
|
131
|
+
# Page domain can only be received if WebkitRemote::Client::Page#page_events
|
132
|
+
# is true.
|
133
|
+
def self.can_reach?(client)
|
134
|
+
true
|
135
|
+
end
|
136
|
+
end # class WebkitRemote::Event
|
137
|
+
|
138
|
+
end # namespace WebkitRemote
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'net/http'
|
3
|
+
require 'posix/spawn'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
module WebkitRemote
|
7
|
+
|
8
|
+
# Tracks a Webkit process.
|
9
|
+
class Process
|
10
|
+
# Tracker for a yet-unlaunched process.
|
11
|
+
#
|
12
|
+
# @param [Hash] opts tweak the options below
|
13
|
+
# @option opts [Integer] port the port used by the remote debugging server;
|
14
|
+
# the default port is 9292
|
15
|
+
# @option opts [Number] timeout number of seconds to wait for the browser
|
16
|
+
# to start; the default timeout is 10 seconds
|
17
|
+
def initialize(opts = {})
|
18
|
+
@port = opts[:port] || 9292
|
19
|
+
@timeout = opts[:timeout] || 10
|
20
|
+
@running = false
|
21
|
+
@data_dir = Dir.mktmpdir 'webkit-remote'
|
22
|
+
@pid = nil
|
23
|
+
@cli = chrome_cli opts
|
24
|
+
end
|
25
|
+
|
26
|
+
# Starts the browser process.
|
27
|
+
#
|
28
|
+
# @return [WebkitRemote::Browser] self
|
29
|
+
def start
|
30
|
+
return self if running?
|
31
|
+
unless @pid = POSIX::Spawn.spawn(*@cli)
|
32
|
+
# The launch failed
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
|
36
|
+
(@timeout * 20).times do
|
37
|
+
# Check if the browser exited.
|
38
|
+
begin
|
39
|
+
break if status = ::Process.wait(@pid, ::Process::WNOHANG)
|
40
|
+
rescue SystemCallError # no children
|
41
|
+
break
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check if the browser finished starting up.
|
45
|
+
begin
|
46
|
+
browser = WebkitRemote::Browser.new process: self
|
47
|
+
@running = true
|
48
|
+
return browser
|
49
|
+
rescue SystemCallError # most likely ECONNREFUSED
|
50
|
+
Kernel.sleep 0.05
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# The browser failed, or was too slow to start.
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Boolean] true if the Webkit process is running
|
58
|
+
attr_reader :running
|
59
|
+
alias_method :running?, :running
|
60
|
+
|
61
|
+
# Stops the browser process.
|
62
|
+
#
|
63
|
+
# Only call this after you're done with the process.
|
64
|
+
#
|
65
|
+
# @return [WebkitRemote::Process] self
|
66
|
+
def stop
|
67
|
+
return self unless running?
|
68
|
+
begin
|
69
|
+
::Process.kill 'TERM', @pid
|
70
|
+
::Process.wait @pid
|
71
|
+
end
|
72
|
+
FileUtils.rm_rf @data_dir if File.exists?(@data_dir)
|
73
|
+
@running = false
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Integer] port that the process' remote debugging server listens to
|
78
|
+
attr_reader :port
|
79
|
+
|
80
|
+
# Remove temporary directory if it's still there at garbage collection time.
|
81
|
+
def finalize
|
82
|
+
PathUtils.rm_rf @data_dir if File.exists?(@data_dir)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Command-line that launches Google Chrome / Chromium
|
86
|
+
#
|
87
|
+
# @param [Hash] opts options passed to the WebkitRemote::Process constructor
|
88
|
+
# @return [Array<String>] command line for launching Chrome
|
89
|
+
def chrome_cli(opts)
|
90
|
+
# The Chromium wiki recommends this page for available flags:
|
91
|
+
# http://peter.sh/experiments/chromium-command-line-switches/
|
92
|
+
[
|
93
|
+
self.class.chrome_binary,
|
94
|
+
'--disable-default-apps', # no bundled apps
|
95
|
+
'--disable-desktop-shortcuts', # don't mess with the desktop
|
96
|
+
'--disable-extensions', # no extensions
|
97
|
+
'--disable-internal-flash', # no plugins
|
98
|
+
'--disable-java', # no plugins
|
99
|
+
'--disable-logging', # don't trash stdout / stderr
|
100
|
+
'--disable-plugins', # no native content
|
101
|
+
'--disable-prompt-on-repost', # no confirmation dialog on POST refresh
|
102
|
+
'--disable-sync', # no talking with the Google servers
|
103
|
+
'--incognito', # don't use old state, don't preserve state
|
104
|
+
'--homepage=about:blank', # don't go to Google in new tabs
|
105
|
+
'--keep-alive-for-test', # don't kill process if the last window dies
|
106
|
+
'--lang=en-US', # set a default language
|
107
|
+
'--log-level=3', # FATAL, because there's no setting for "none"
|
108
|
+
'--no-default-browser-check', # don't hang when Chrome isn't default
|
109
|
+
'--no-experiments', # not sure this is useful
|
110
|
+
'--no-first-run', # don't show the help UI
|
111
|
+
'--no-js-randomness', # consistent Date.now() and Math.random()
|
112
|
+
'--no-message-box', # don't let user scripts show dialogs
|
113
|
+
'--no-service-autorun', # don't mess with autorun settings
|
114
|
+
'--noerrdialogs', # don't hang on error dialogs
|
115
|
+
"--remote-debugging-port=#{@port}", # Webkit remote debugging
|
116
|
+
"--user-data-dir=#{@data_dir}", # really ensure a clean slate
|
117
|
+
'--window-position=0,0', # remove randomness source
|
118
|
+
'--window-size=128,128', # remove randomness source
|
119
|
+
'about:blank' # don't load the homepage
|
120
|
+
]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Path to a Google Chrome / Chromium binary.
|
124
|
+
#
|
125
|
+
# @return [String] full-qualified path to a binary that launches Chrome
|
126
|
+
def self.chrome_binary
|
127
|
+
return @chrome_binary unless @chrome_binary == false
|
128
|
+
|
129
|
+
case RUBY_PLATFORM
|
130
|
+
when /linux/
|
131
|
+
[
|
132
|
+
'google-chrome',
|
133
|
+
'google-chromium',
|
134
|
+
].each do |binary|
|
135
|
+
path = `which #{binary}`
|
136
|
+
unless path.empty?
|
137
|
+
@chrome_binary = path.strip
|
138
|
+
break
|
139
|
+
end
|
140
|
+
end
|
141
|
+
when /darwin/
|
142
|
+
[
|
143
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
144
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
145
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
146
|
+
].each do |path|
|
147
|
+
if File.exist? path
|
148
|
+
@chrome_binary = path
|
149
|
+
break
|
150
|
+
end
|
151
|
+
end
|
152
|
+
else
|
153
|
+
raise "Unsupported platform #{RUBY_PLATFORM}"
|
154
|
+
end
|
155
|
+
@chrome_binary ||= nil
|
156
|
+
end
|
157
|
+
@chrome_binary = false
|
158
|
+
end # class WebkitRemote::Browser
|
159
|
+
|
160
|
+
end # namespace WebkitRemote
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'faye/websocket'
|
3
|
+
require 'json'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
module WebkitRemote
|
7
|
+
|
8
|
+
# RPC client for the Webkit remote debugging protocol.
|
9
|
+
class Rpc
|
10
|
+
# Connects to the remote debugging server in a Webkit tab.
|
11
|
+
#
|
12
|
+
# @param [Hash] opts info on the tab to connect to
|
13
|
+
# @option opts [WebkitRemote::Tab] tab reference to the tab whose debugger
|
14
|
+
# server this RPC client connects to
|
15
|
+
def initialize(opts = {})
|
16
|
+
unless tab = opts[:tab]
|
17
|
+
raise ArgumentError, 'Target tab not specified'
|
18
|
+
end
|
19
|
+
@closed = false
|
20
|
+
@send_queue = EventMachine::Queue.new
|
21
|
+
@recv_queue = Queue.new
|
22
|
+
@next_id = 2
|
23
|
+
@events = []
|
24
|
+
|
25
|
+
self.class.em_start
|
26
|
+
@web_socket = Faye::WebSocket::Client.new tab.debug_url
|
27
|
+
setup_web_socket
|
28
|
+
end
|
29
|
+
|
30
|
+
# Remote debugging RPC call.
|
31
|
+
#
|
32
|
+
# See the following URL for implemented calls.
|
33
|
+
# https://developers.google.com/chrome-developer-tools/docs/protocol/1.0/index
|
34
|
+
#
|
35
|
+
# @param [String] method name of the RPC method to be invoked
|
36
|
+
# @param [Hash<String, Object>, nil] params parameters for the RPC method to
|
37
|
+
# be invoked
|
38
|
+
# @return [Hash<String, Object>] the return value of the RPC method
|
39
|
+
def call(method, params = nil)
|
40
|
+
request_id = @next_id
|
41
|
+
@next_id += 1
|
42
|
+
request = {
|
43
|
+
jsonrpc: '2.0',
|
44
|
+
id: request_id,
|
45
|
+
method: method,
|
46
|
+
}
|
47
|
+
request[:params] = params if params
|
48
|
+
request_json = JSON.dump request
|
49
|
+
@send_queue.push request_json
|
50
|
+
|
51
|
+
loop do
|
52
|
+
result = receive_message request_id
|
53
|
+
return result if result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Continuously reports events sent by the remote debugging server.
|
58
|
+
#
|
59
|
+
# @yield once for each RPC event received from the remote debugger; break to
|
60
|
+
# stop the event listening loop
|
61
|
+
# @yieldparam [Hash<Symbol, Object>] event the name and information hash of
|
62
|
+
# the event, under the keys :name and :data
|
63
|
+
# @return [WebkitRemote::Rpc] self
|
64
|
+
def each_event
|
65
|
+
loop do
|
66
|
+
if @events.empty?
|
67
|
+
receive_message nil
|
68
|
+
else
|
69
|
+
yield @events.shift
|
70
|
+
end
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Closes the connection to the remote debugging server.
|
76
|
+
#
|
77
|
+
# Call this method to avoid leaking resources.
|
78
|
+
#
|
79
|
+
# @return [WebkitRemote::Rpc] self
|
80
|
+
def close
|
81
|
+
return if @closed
|
82
|
+
@closed = true
|
83
|
+
@web_socket.close
|
84
|
+
@web_socket = nil
|
85
|
+
self.class.em_stop
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Boolean] if true, the connection to the remote debugging server
|
90
|
+
# has been closed, and this instance is mostly useless
|
91
|
+
attr_reader :closed
|
92
|
+
alias_method :closed?, :closed
|
93
|
+
|
94
|
+
# Hooks up the event handlers of the WebSocket client.
|
95
|
+
def setup_web_socket
|
96
|
+
@web_socket.onopen = lambda do |event|
|
97
|
+
send_request
|
98
|
+
@web_socket.onmessage = lambda do |event|
|
99
|
+
data = event.data
|
100
|
+
EventMachine.defer do
|
101
|
+
@recv_queue.push data
|
102
|
+
end
|
103
|
+
end
|
104
|
+
@web_socket.onclose = lambda do |event|
|
105
|
+
code = event.code
|
106
|
+
EventMachine.defer do
|
107
|
+
@recv_queue.push code
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
private :setup_web_socket
|
113
|
+
|
114
|
+
# One iteration of the request sending loop.
|
115
|
+
#
|
116
|
+
# RPC requests are JSON-serialized on the sending thread, then pushed into
|
117
|
+
# the send queue, which is an EventMachine queue. On the reactor thread, the
|
118
|
+
# serialized message is sent as a WebSocket frame.
|
119
|
+
def send_request
|
120
|
+
@send_queue.pop do |json|
|
121
|
+
@web_socket.send json
|
122
|
+
send_request
|
123
|
+
end
|
124
|
+
end
|
125
|
+
private :send_request
|
126
|
+
|
127
|
+
# Blocks until a WebKit message is received, then parses it.
|
128
|
+
#
|
129
|
+
# RPC notifications are added to the @events array.
|
130
|
+
#
|
131
|
+
# @param [Integer, nil] expected_id if a RPC response is expected, this
|
132
|
+
# argument has the response id; otherwise, the argument should be nil
|
133
|
+
# @return [Hash<String, Object>, nil] a Hash containing the RPC result if an
|
134
|
+
# expected RPC response was received; nil if an RPC notice was received
|
135
|
+
def receive_message(expected_id)
|
136
|
+
json = @recv_queue.pop
|
137
|
+
unless json.respond_to? :to_str
|
138
|
+
close
|
139
|
+
raise RuntimeError, 'The Webkit debugging server closed the WebSocket'
|
140
|
+
end
|
141
|
+
begin
|
142
|
+
data = JSON.parse json
|
143
|
+
rescue JSONError
|
144
|
+
close
|
145
|
+
raise RuntimeError, 'Invalid JSON received'
|
146
|
+
end
|
147
|
+
if data['id']
|
148
|
+
# RPC result.
|
149
|
+
if data['id'] != expected_id
|
150
|
+
close
|
151
|
+
raise RuntimeError, 'Out of sequence RPC response id'
|
152
|
+
end
|
153
|
+
if data['error']
|
154
|
+
raise RuntimeError, "Error #{data['error']['code']}"
|
155
|
+
end
|
156
|
+
return data['result']
|
157
|
+
elsif data['method']
|
158
|
+
# RPC notice.
|
159
|
+
event = { name: data['method'], data: data['params'] }
|
160
|
+
@events << event
|
161
|
+
return nil
|
162
|
+
else
|
163
|
+
close
|
164
|
+
raise RuntimeError, "Unexpected / invalid RPC message #{data.inspect}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
private :receive_message
|
168
|
+
|
169
|
+
# Sets up an EventMachine reactor if necessary.
|
170
|
+
def self.em_start
|
171
|
+
@em_start_lock.synchronize do
|
172
|
+
if @em_clients == 0 and @em_thread.nil?
|
173
|
+
em_ready = ConditionVariable.new
|
174
|
+
@em_thread = Thread.new do
|
175
|
+
EventMachine.run do
|
176
|
+
@em_start_lock.synchronize { em_ready.signal }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
em_ready.wait @em_start_lock
|
180
|
+
end
|
181
|
+
@em_clients += 1
|
182
|
+
end
|
183
|
+
end
|
184
|
+
@em_clients = 0
|
185
|
+
@em_start_lock = Mutex.new
|
186
|
+
@em_thread = nil
|
187
|
+
|
188
|
+
# Shuts down an EventMachine reactor if necessary.
|
189
|
+
def self.em_stop
|
190
|
+
@em_start_lock.synchronize do
|
191
|
+
@em_clients -= 1
|
192
|
+
if @em_clients == 0
|
193
|
+
if @em_thread
|
194
|
+
EventMachine.stop_event_loop
|
195
|
+
# HACK(pwnall): having these in slows down the code a lot
|
196
|
+
# EventMachine.reactor_thread.join
|
197
|
+
# @em_thread.join
|
198
|
+
end
|
199
|
+
@em_thread = nil
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end # class WebkitRemote::Rpc
|
204
|
+
|
205
|
+
end # namespace webkitRemote
|