webkit_remote 0.1.0
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.
- 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
|