jscmd 0.0.2 → 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.
@@ -0,0 +1,62 @@
1
+ module JSCommander
2
+ class PipeBroker
3
+ class PipeQueue
4
+ attr_reader :reader, :writer
5
+
6
+ def initialize(r, w)
7
+ @reader, @writer = [r, w]
8
+ end
9
+
10
+ def push(obj)
11
+ data = Marshal.dump(obj)
12
+ @writer.write([data.size].pack("N") + data)
13
+ end
14
+
15
+ def pop
16
+ size = @reader.read(4).unpack("N")[0]
17
+ Marshal.load(@reader.read(size))
18
+ end
19
+
20
+ def close
21
+ @reader.close
22
+ @writer.close
23
+ end
24
+ end
25
+
26
+ PipeMessage = Struct.new(:name, :message)
27
+
28
+ def initialize(reader, writer)
29
+ @subscribers = {}
30
+ @queue = PipeQueue.new(reader, writer)
31
+ Thread.start do
32
+ begin
33
+ loop do
34
+ pm = @queue.pop
35
+ if @subscribers[pm.name]
36
+ @subscribers[pm.name].each do |proc|
37
+ proc.call pm.message
38
+ end
39
+ end
40
+ end
41
+ rescue Exception
42
+ $stderr.puts $!
43
+ end
44
+ end
45
+ end
46
+
47
+ def subscribe(name, &proc)
48
+ @subscribers[name] ||= []
49
+ @subscribers[name] << proc
50
+ proc
51
+ end
52
+
53
+ def unsubscribe(name, subscriber)
54
+ sub = @subscribers[name].delete(subscriber)
55
+ end
56
+
57
+ def send(name, msg)
58
+ # puts "send #{name}, #{msg.inspect}"
59
+ @queue.push(PipeMessage.new(name, msg))
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,224 @@
1
+ #
2
+ # jscommander.rb: Remote JavaScript Console
3
+ #
4
+ # Copyright 2007 Shinya Kasatani
5
+ #
6
+
7
+ require 'uri'
8
+ require 'thread'
9
+ require 'webrick'
10
+ require 'zlib'
11
+
12
+ require "jscmd/asynchttpproxy"
13
+
14
+ module WEBrick
15
+ class HTTPResponse
16
+ alias_method :original_send_body, :send_body
17
+ def send_body(socket)
18
+ begin
19
+ original_send_body(socket)
20
+ rescue Errno::ECONNABORTED
21
+ raise Errno::ECONNRESET
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ module JSCommander
28
+ class ProxyServer < WEBrick::AsyncHTTPProxyServer
29
+ SCRIPT_DIR = "/_remote_js_proxy/"
30
+
31
+ class CommandDispatcher
32
+ class CLIENT_ABORTED < StandardError; end
33
+ class SHUTDOWN < StandardError; end
34
+
35
+ def initialize(server, command_queue)
36
+ @client_mutex = Mutex.new
37
+ @client_ready = ConditionVariable.new
38
+ @clients = []
39
+ @thread = Thread.start do
40
+ while server.status != :Shutdown
41
+ command = command_queue.pop
42
+ @client_mutex.synchronize do
43
+ while @clients.empty?
44
+ @client_ready.wait(@client_mutex)
45
+ end
46
+ @clients.first.push_command(command)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def shutdown
53
+ @clients.each do |client|
54
+ client.push_command SHUTDOWN
55
+ end
56
+ end
57
+
58
+ def format_clients
59
+ @clients.map{|c|"[#{c.hostname}]"}.join(",")
60
+ end
61
+
62
+ def new_client(req, &block)
63
+ client = Client.new(req)
64
+ @client_mutex.synchronize do
65
+ @clients << client
66
+ @client_ready.signal
67
+ end
68
+ begin
69
+ yield client
70
+ ensure
71
+ @client_mutex.synchronize do
72
+ @clients.delete(client)
73
+ end
74
+ end
75
+ end
76
+
77
+ class Client
78
+ def initialize(req)
79
+ @request = req
80
+ @queue = Queue.new
81
+ req_socket = req.instance_eval{@socket}
82
+ @thread = Thread.start do
83
+ sleep 1 until req_socket.eof?
84
+ @queue.push CLIENT_ABORTED
85
+ end
86
+ end
87
+
88
+ def hostname
89
+ uri = @request.header["referer"].to_s
90
+ if uri =~ %r{^\w+://([^/]+)}
91
+ $1
92
+ else
93
+ uri
94
+ end
95
+ end
96
+
97
+ def push_command(command)
98
+ @queue.push(command)
99
+ end
100
+
101
+ def pop_command
102
+ r = @queue.pop
103
+ # thread is automatically stopped if r == CLIENT_ABORTED
104
+ if r != CLIENT_ABORTED
105
+ @thread.kill
106
+ end
107
+ r
108
+ end
109
+ end
110
+ end
111
+
112
+ def initialize(broker, args = {})
113
+ @broker = broker
114
+ @commands = Queue.new
115
+ @command_dispatcher = CommandDispatcher.new(self, @commands)
116
+ @broker.subscribe("commands") do |msg|
117
+ if msg.attributes["type"] == "ping"
118
+ @broker.send("events", Message.new(nil,
119
+ :type => "pong",
120
+ :clients => @command_dispatcher.format_clients,
121
+ "in-reply-to" => msg.attributes["id"]))
122
+ else
123
+ @commands.push(msg)
124
+ end
125
+ end
126
+ @clients = []
127
+ super({:ProxyContentHandler => method(:handle_content).to_proc}.merge(args))
128
+ end
129
+
130
+ def service(req, res)
131
+ if req.path == "#{SCRIPT_DIR}agent.js"
132
+ res.content_type = "application/x-javascript"
133
+ res.body = File.read(File.join(File.dirname(__FILE__), "agent.js"))
134
+ elsif req.path == "#{SCRIPT_DIR}poll"
135
+ serve_script(req, res)
136
+ else
137
+ if req.header["accept-encoding"] && !req.header["accept-encoding"].empty?
138
+ if req.header["accept-encoding"].first.split(/,/).include?("gzip")
139
+ req.header["accept-encoding"] = ["gzip"]
140
+ else
141
+ req.header.delete "accept-encoding"
142
+ end
143
+ end
144
+ super
145
+ end
146
+ end
147
+
148
+ def shutdown
149
+ @command_dispatcher.shutdown
150
+ super
151
+ end
152
+
153
+ def serve_script(req, res)
154
+ # puts "serve:#{req.body}"
155
+ begin
156
+ @command_dispatcher.new_client(req) do |client|
157
+ type = req.header["x-jscmd-type"].first
158
+ if type && type != "connect"
159
+ @broker.send("events", Message.new(URI.decode(req.body),
160
+ :type => type,
161
+ :clients => @command_dispatcher.format_clients,
162
+ "in-reply-to" => req.header["x-jscmd-in-reply-to"].first))
163
+ if "true" == req.header["x-jscmd-more"].first
164
+ # immediately send empty response to wait for another event
165
+ res.content_type = "text/plain"
166
+ res.body = ''
167
+ return
168
+ end
169
+ else
170
+ # new connection
171
+ @broker.send("events", Message.new(nil, :type => "connect",
172
+ :clients => @command_dispatcher.format_clients))
173
+ end
174
+
175
+ command = client.pop_command
176
+ if command.is_a?(Class) # exception class
177
+ raise command
178
+ end
179
+ res.content_type = "text/plain"
180
+ res.header["X-JSCmd-Command-Id"] = command.attributes["id"]
181
+ res.header["X-JSCmd-Type"] = command.attributes["type"]
182
+ res.body = command.body
183
+ end
184
+ rescue CommandDispatcher::CLIENT_ABORTED
185
+ @broker.send("events", Message.new(nil, :type => "abort",
186
+ :clients => @command_dispatcher.format_clients))
187
+ raise WEBrick::HTTPStatus::EOFError
188
+ rescue CommandDispatcher::SHUTDOWN
189
+ raise WEBrick::HTTPStatus::EOFError
190
+ end
191
+ end
192
+
193
+ def handle_content(req, res)
194
+ # $stderr.puts "handle_content:type=#{res.content_type}, status=#{res.status}, encoding=#{res.header["content-encoding"]}"
195
+ if res.content_type =~ %r{^text/html} && res.status == 200
196
+ res.flush_body
197
+ # we cannot always trust content_type, so check if the content looks like html
198
+ body = res.body
199
+ if res.header["content-encoding"] == "gzip"
200
+ body = Zlib::GzipReader.new(StringIO.new(body)).read
201
+ end
202
+ if body =~ /^\s*</
203
+ body = body.dup
204
+ # puts "injecting javascript"
205
+ script_tag = '<script type="text/javascript" src="/_remote_js_proxy/agent.js"></script>'
206
+ unless body.sub!(%r{<head( .*?)?>}i){|s|s+script_tag}
207
+ body = script_tag + body
208
+ end
209
+ if res.header["content-encoding"] == "gzip"
210
+ io = StringIO.new
211
+ writer = Zlib::GzipWriter.new(io)
212
+ writer.write(body)
213
+ writer.close
214
+ res.body = io.string
215
+ else
216
+ res.body = body
217
+ end
218
+ res.content_length = res.body.size if res.content_length
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+
@@ -0,0 +1,201 @@
1
+ begin
2
+ require "readline"
3
+ rescue LoadError
4
+ end
5
+
6
+ module JSCommander
7
+ class Shell
8
+ class SimpleConsole
9
+ def initialize(input = $stdin)
10
+ @input = input
11
+ end
12
+
13
+ def show_banner
14
+ end
15
+
16
+ def readline
17
+ begin
18
+ line = @input.readline
19
+ line.chomp! if line
20
+ line
21
+ rescue EOFError
22
+ nil
23
+ end
24
+ end
25
+
26
+ def close
27
+ end
28
+ end
29
+
30
+ class ReadlineConsole
31
+ HISTORY_FILE = ".jscmd_history"
32
+ MAX_HISTORY = 200
33
+
34
+ def history_path
35
+ File.join(ENV['HOME'] || ENV['USERPROFILE'], HISTORY_FILE)
36
+ end
37
+
38
+ def initialize(shell)
39
+ @shell = shell
40
+ if File.exist?(history_path)
41
+ hist = File.readlines(history_path).map{|line| line.chomp}
42
+ Readline::HISTORY.push(*hist)
43
+ end
44
+ if Readline.methods.include?("basic_word_break_characters=")
45
+ Readline.basic_word_break_characters = " \t\n\\`@><=;|&{([+-*/%"
46
+ end
47
+ Readline.completion_append_character = nil
48
+ Readline.completion_proc = @shell.method(:complete_property).to_proc
49
+ end
50
+
51
+ def show_banner
52
+ $stderr.puts "Press Ctrl+D to exit."
53
+ end
54
+
55
+ def close
56
+ open(history_path, "wb") do |f|
57
+ history = Readline::HISTORY.to_a
58
+ if history.size > MAX_HISTORY
59
+ history = history[history.size - MAX_HISTORY, MAX_HISTORY]
60
+ end
61
+ history.each{|line| f.puts(line)}
62
+ end
63
+ end
64
+
65
+ def readline
66
+ line = Readline.readline("#{@shell.clients}> ", true)
67
+ Readline::HISTORY.pop if /^\s*$/ =~ line
68
+ line
69
+ end
70
+ end
71
+
72
+ def console
73
+ @console ||= $stdin.tty? ? ReadlineConsole.new(self) : SimpleConsole.new
74
+ # @console ||= SimpleConsole.new
75
+ end
76
+
77
+ attr_reader :clients
78
+
79
+ def initialize(broker)
80
+ @broker = broker
81
+ @clients = nil
82
+ @msg_lock = Mutex.new
83
+ @wait_for_event = {}
84
+ end
85
+
86
+ def generate_id
87
+ Array.new(16){rand(62)}.pack("C*").tr("\x00-\x3d", "A-Za-z0-9")
88
+ end
89
+
90
+ def send_script(line, type, &handler)
91
+ queue = Queue.new
92
+ @msg_lock.synchronize do
93
+ id = generate_id
94
+ @broker.send "commands", Message.new(line,
95
+ :type => type,
96
+ :id => id)
97
+ # wait until it gets response
98
+ @wait_for_event[id] = [queue, proc{|msg| yield msg}]
99
+ end
100
+ queue.pop
101
+ end
102
+
103
+ def object_props(object)
104
+ send_script(object, "properties") do |msg|
105
+ if msg.attributes["type"] == "value" && msg.body
106
+ msg.body.split(/,/)
107
+ else
108
+ []
109
+ end
110
+ end
111
+ end
112
+
113
+ def complete_property(word)
114
+ if word =~ /\.$/
115
+ object_props($`).map{|name| word + name}
116
+ elsif word =~ /\.([^.]+)$/
117
+ prefix = $1
118
+ parent = $`
119
+ props = object_props(parent)
120
+ props.select{|name|name[0...(prefix.size)] == prefix}.map{|name| "#{parent}.#{name}"}
121
+ else
122
+ props = object_props("this")
123
+ prefix = word
124
+ props.select{|name|name[0...(prefix.size)] == prefix}
125
+ end
126
+ end
127
+
128
+ def run
129
+ console.show_banner
130
+
131
+ main_thread = Thread.current
132
+
133
+ @broker.subscribe("events") do |msg|
134
+ id = msg.attributes["in-reply-to"]
135
+ type = msg.attributes["type"]
136
+ @clients = msg.attributes["clients"]
137
+ if id
138
+ @msg_lock.synchronize do
139
+ handler = @wait_for_event.delete(id)
140
+ if handler
141
+ queue, proc = handler
142
+ queue.push(proc.call(msg))
143
+ end
144
+ end
145
+ msg = nil
146
+ elsif "abort" == type
147
+ @msg_lock.synchronize do
148
+ handler = @wait_for_event.values.first
149
+ if handler
150
+ queue, proc = handler
151
+ queue.push(nil)
152
+ end
153
+ end
154
+ end
155
+ unless msg.nil?
156
+ case type
157
+ when "value", "error"
158
+ puts msg.body
159
+ when "abort"
160
+ puts "aborted"
161
+ when "connect"
162
+ puts
163
+ end
164
+ if Process.methods.include?("kill")
165
+ Process.kill "INT", $$ # interrupt readline
166
+ else
167
+ # JRuby
168
+ main_thread.raise Interrupt
169
+ end
170
+ end
171
+ end
172
+
173
+ begin
174
+ loop do
175
+ break unless line = console.readline
176
+ if line.chomp == ''
177
+ send_script(nil, "ping") {}
178
+ else
179
+ send_script(line, "eval") do |msg|
180
+ puts msg.body
181
+ end
182
+ end
183
+ end
184
+ $stderr.puts "Exiting shell..."
185
+ rescue Interrupt
186
+ # $stderr.puts "int"
187
+ retry
188
+ rescue SystemExit
189
+ return
190
+ rescue Exception => e
191
+ $stderr.puts "#{e.inspect} at:\n#{e.backtrace.join("\n")}"
192
+ ensure
193
+ begin
194
+ console.close
195
+ rescue Exception => e
196
+ $stderr.puts "failed to close console: #{e.inspect}"
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,36 @@
1
+ module JSCommander
2
+ # This broker proxy should be instantiated in each process.
3
+ class StompProxy
4
+ def initialize(url = nil)
5
+ require "stomp"
6
+
7
+ url = "stomp://localhost:61613/" if url.nil?
8
+ @stomp = Stomp::Client.new(url)
9
+ end
10
+
11
+ def subscribe(name, &proc)
12
+ @stomp.subscribe("/topic/#{name}") do |msg|
13
+ attributes = {}
14
+ msg.headers.each do |key, value|
15
+ if key =~ /^jscmd\.(.*)/
16
+ attributes[$1] = value
17
+ end
18
+ end
19
+ proc.call(Message.new(msg.body, attributes))
20
+ end
21
+ proc
22
+ end
23
+
24
+ def unsubscribe(name, subscriber)
25
+ @stomp.unsubscribe("/topic/#{name}")
26
+ end
27
+
28
+ def send(name, msg)
29
+ headers = {}
30
+ msg.attributes.each do |key, value|
31
+ headers["jscmd." + key] = value
32
+ end
33
+ @stomp.send("/topic/#{name}", msg.body || "", headers)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ require "cgi"
2
+
3
+ module JSCommander
4
+ class URLForwarder < WEBrick::HTTPServer
5
+ def initialize(broker, args)
6
+ @broker = broker
7
+ super(args)
8
+ mount_proc("/send/", method(:forward_url).to_proc)
9
+ mount_proc("/", method(:show_bookmarklet).to_proc)
10
+ end
11
+
12
+ def show_bookmarklet(req, res)
13
+ res.content_type = "text/html"
14
+ res.body = <<EOS
15
+ <html>
16
+ <head>
17
+ <title>Bookmarklet for forwarding URL</title>
18
+ <script type="text/javascript">
19
+ function setBrowserName(name) {
20
+ var link = document.getElementById("link");
21
+ link.removeChild(link.firstChild);
22
+ link.appendChild(document.createTextNode("Open with " + name));
23
+ }
24
+ </script>
25
+ </head>
26
+ <body>
27
+ <form onsubmit="return false">Enter the name of another browser: <input type="text" value="Wii" oninput="setBrowserName(this.value)"/></form>
28
+ <p>Drag the following link to your bookmarks toolbar.</p>
29
+ <p><a id="link" href="javascript:void(window.open('http://localhost:#{@config[:Port]}/send/'+escape(location.href)))">Open with Wii</a></p>
30
+ </body>
31
+ EOS
32
+ end
33
+
34
+ def forward_url(req, res)
35
+ uri = CGI.unescape(req.unparsed_uri.sub(%r{^/send/}, ""))
36
+ puts "opening " + uri
37
+ line = "(function() { var c = location.href; var l = '#{uri.gsub(/\\/, "\\\\").gsub("'", "\\'")}'; location.href = l; if (l.replace(/#.*$/,'') == c.replace(/#.*$/,'')) location.reload(); })()"
38
+ @broker.send("commands", Message.new(line, :type => "eval"))
39
+ res.content_type = "text/html"
40
+ res.body = '<html><head><script type="text/javascript">window.close()</script></head></html>'
41
+ end
42
+ end
43
+ end
data/lib/jscmd/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  module Jscmd #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 0
5
- TINY = 2
4
+ MINOR = 1
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/jscmd.rb CHANGED
@@ -1 +1,14 @@
1
- Dir[File.join(File.dirname(__FILE__), 'jscmd/**/*.rb')].sort.each { |lib| require lib }
1
+ # Dir[File.join(File.dirname(__FILE__), 'jscmd/**/*.rb')].sort.each { |lib| require lib }
2
+
3
+ # message
4
+ require "jscmd/message"
5
+
6
+ # brokers
7
+ require "jscmd/inprocessbroker"
8
+ require "jscmd/pipebroker"
9
+ require "jscmd/stompproxy"
10
+
11
+ # clients
12
+ require "jscmd/proxyserver"
13
+ require "jscmd/shell"
14
+ require "jscmd/urlforwarder"
data/test/test_helper.rb CHANGED
@@ -1,2 +1,4 @@
1
- # require 'test/unit'
2
- require File.dirname(__FILE__) + '/../lib/jscmd'
1
+ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
2
+ require "rubygems"
3
+ require "test/unit"
4
+ require "jscmd"