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.
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ = *SVN*
2
+
3
+ * Refactored everything
4
+ * Now acts as Stomp client
5
+ * Embedded Stomp Server
6
+ * Now works in JRuby (partially, using threads) and mswin32 (using stompserver)
7
+ * Added URLForwarder that adds a bookmarklet that sends the URL of the current page to another browser
8
+
1
9
  = 0.0.2 2007-05-08
2
10
 
3
11
  * Fixed potential file descriptor leak
data/Manifest.txt CHANGED
@@ -6,11 +6,19 @@ README.txt
6
6
  Rakefile
7
7
  bin/jscmd
8
8
  lib/jscmd.rb
9
- lib/jscmd/version.rb
10
- lib/jscmd/jscommander.rb
11
9
  lib/jscmd/asynchttpproxy.rb
10
+ lib/jscmd/inprocessbroker.rb
11
+ lib/jscmd/message.rb
12
+ lib/jscmd/pipebroker.rb
13
+ lib/jscmd/proxyserver.rb
14
+ lib/jscmd/shell.rb
15
+ lib/jscmd/stompproxy.rb
16
+ lib/jscmd/urlforwarder.rb
17
+ lib/jscmd/version.rb
12
18
  lib/jscmd/agent.js
13
19
  scripts/txt2html
14
20
  setup.rb
15
21
  test/test_helper.rb
16
- test/test_jscmd.rb
22
+ test/test_proxyserver.rb
23
+ test/test_shell.rb
24
+ examples/get_title.pl
data/Rakefile CHANGED
@@ -49,7 +49,7 @@ hoe = Hoe.new(GEM_NAME, VERS) do |p|
49
49
 
50
50
  # == Optional
51
51
  p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
52
- #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
52
+ p.extra_deps = [['stomp', '>= 1.0.5'], ['stompserver', '>= 0.9.7']] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
53
53
  #p.spec_extras = {} # A hash of extra values to set in the gemspec.
54
54
  end
55
55
 
data/bin/jscmd CHANGED
@@ -1,4 +1,4 @@
1
- #! /opt/local/bin/ruby
1
+ #!/usr/bin/env ruby
2
2
 
3
3
  begin
4
4
  require 'rubygems'
@@ -13,32 +13,337 @@ end
13
13
  require 'jscmd'
14
14
  require 'optparse'
15
15
 
16
- OPTIONS = {
17
- :port => 9000
18
- }
19
- MANDATORY_OPTIONS = %w()
20
-
21
- parser = OptionParser.new do |opts|
22
- opts.banner = <<BANNER
16
+ module JSCommander
17
+ class CommandLine
18
+ OPTIONS = {
19
+ :port => 9000,
20
+ :bind => "0.0.0.0",
21
+ :uport => 9001,
22
+ :ubind => "localhost",
23
+ :sport => 61613,
24
+ :sbind => "localhost",
25
+ :sjournal => ".stompserver",
26
+ :connect_to => "stomp://localhost:61613/",
27
+ }
28
+ MANDATORY_OPTIONS = %w()
29
+
30
+ def initialize
31
+
32
+ parser = OptionParser.new do |opts|
33
+ opts.banner = <<BANNER
23
34
  JS Commander: Remote JavaScript Console
24
35
 
25
36
  Usage: #{File.basename($0)} [options]
26
37
 
38
+ If no options were specified, jscmd starts up Shell and Proxy Server with internal broker.
27
39
  Options are:
28
40
  BANNER
29
- opts.separator ""
30
- opts.on("-p", "--port=PORT", Integer,
31
- "Start proxy server on the specified port.",
32
- "Default: 9000") { |OPTIONS[:port]| }
33
- opts.on("-h", "--help",
34
- "Show this help message.") { puts opts; exit }
35
- opts.parse!(ARGV)
36
-
37
- if MANDATORY_OPTIONS && MANDATORY_OPTIONS.find { |option| OPTIONS[option.to_sym].nil? }
38
- puts opts; exit
41
+ opts.separator ""
42
+ opts.on("-c", "--console",
43
+ "Startup console") { |OPTIONS[:console]| }
44
+ opts.on("-x", "--proxy",
45
+ "Startup proxy server") { |OPTIONS[:proxy]| }
46
+ opts.on("-p", "--port=PORT", Integer,
47
+ "Bind proxy server on specified TCP port. (default: 9000)") { |OPTIONS[:port]| }
48
+ opts.on("-b", "--bind=ADDRESS", String,
49
+ "Bind proxy server on specified IP address. (default: 0.0.0.0)") { |OPTIONS[:bind]| }
50
+ opts.on("-u", "--url-forwarder",
51
+ "Startup URL forwarder") { |OPTIONS[:url_forwarder]| }
52
+ opts.on("--uport=PORT", Integer,
53
+ "Bind URL forwarder on specified TCP port. (default: 9001)") { |OPTIONS[:uport]| }
54
+ opts.on("--ubind=ADDRESS", String,
55
+ "Bind URL forwarder on specified IP address. (default: localhost)") { |OPTIONS[:ubind]| }
56
+ opts.on("-S", "--server",
57
+ "Startup embedded Stomp server") { |v| OPTIONS[:server] = v || true }
58
+ opts.on("--sport=PORT", Integer,
59
+ "Bind Stomp server on specified TCP port. (default: 61613)") { |OPTIONS[:sport]| }
60
+ opts.on("--sbind=ADDRESS", String,
61
+ "Bind Stomp server on specified IP address. (default: localhost)") { |OPTIONS[:sbind]| }
62
+ opts.on("--sjournal=DIR", String,
63
+ "Specify journal directory of Stomp server. (default: ./.stompserver)") { |OPTIONS[:sjournal]| }
64
+ opts.on("-C", "--client", String,
65
+ "Acts as Stomp client.") {|OPTIONS[:client]|}
66
+ opts.on("--connect-to=URL", String,
67
+ "Specify URL of Stomp server in stomp://[user:pass@]host:port/ format.",
68
+ "(default: stomp://localhost:61613/)") { |OPTIONS[:connect_to]| }
69
+ opts.on("--debug-proxy",
70
+ "Enable debugging in proxy server.") { |OPTIONS[:debug_proxy]| }
71
+ opts.on("--debug-msg=[LOGFILE]",
72
+ "Print messages that are being sent.") { |OPTIONS[:debug_msg]| }
73
+ opts.on("--never-fork",
74
+ "Never fork - use threads only (experimental).") { |OPTIONS[:never_fork]| }
75
+ opts.on("-h", "--help",
76
+ "Show this help message.") { puts opts; exit }
77
+ opts.parse!(ARGV)
78
+
79
+ if MANDATORY_OPTIONS && MANDATORY_OPTIONS.find { |option| OPTIONS[option.to_sym].nil? }
80
+ puts opts; exit
81
+ end
82
+ end
83
+
84
+ unless [:console, :proxy, :server, :url_forwarder].any?{|o| OPTIONS[o]}
85
+ # default is -xc if no processes were specified
86
+ OPTIONS[:console] = true
87
+ OPTIONS[:proxy] = true
88
+ if windows?
89
+ OPTIONS[:server] = true
90
+ end
91
+ end
92
+ # puts "options: #{encode_options.join(' ')}"
93
+
94
+ @child_pid = nil
95
+ @threads = []
96
+
97
+ use_stomp = OPTIONS[:server] || OPTIONS[:client]
98
+ unless never_fork?
99
+ use_stomp ||= !OPTIONS[:console] || !OPTIONS[:proxy] || OPTIONS[:url_forwarder]
100
+ end
101
+
102
+ if use_stomp
103
+ # use stomp broker
104
+ startup_processes { StompProxy.new(OPTIONS[:connect_to]) }
105
+ else
106
+ # use internal broker
107
+ if !never_fork?
108
+ r1, w1 = IO.pipe
109
+ r2, w2 = IO.pipe
110
+ pid = $$
111
+ startup_processes do
112
+ if pid == $$
113
+ PipeBroker.new(r2, w1)
114
+ else
115
+ PipeBroker.new(r1, w2)
116
+ end
117
+ end
118
+ else
119
+ $stderr.puts "Starting up in multi-threaded mode"
120
+ # JRuby
121
+ broker = InProcessBroker.new
122
+ startup_processes { broker }
123
+ end
124
+ end
125
+ end
126
+
127
+ def startup_non_console_processes(&broker_factory)
128
+ if OPTIONS[:server]
129
+ t = StompServerLauncher.new
130
+ t.start
131
+ @threads << t
132
+ end
133
+ if OPTIONS[:proxy]
134
+ t = ProxyLauncher.new(broker_factory.call)
135
+ t.start
136
+ @threads << t
137
+ end
138
+ if OPTIONS[:url_forwarder]
139
+ t = URLForwarderLauncher.new(broker_factory.call)
140
+ t.start
141
+ @threads << t
142
+ end
143
+ end
144
+
145
+ def stop_threads
146
+ @threads.reverse.each do |t|
147
+ t.shutdown
148
+ end
149
+ end
150
+
151
+ def join_threads
152
+ @threads.reverse.each do |t|
153
+ t.thread.join
154
+ end
155
+ end
156
+
157
+ def encode_options
158
+ options = []
159
+ OPTIONS.each do |key, value|
160
+ option = "--#{key.to_s.gsub(/_/, "-")}"
161
+ case value
162
+ when true
163
+ options << option
164
+ when String, Integer
165
+ options << "#{option}=#{value}"
166
+ end
167
+ end
168
+ options
169
+ end
170
+
171
+ def startup_processes(&broker_factory)
172
+ if OPTIONS[:debug_msg]
173
+ case OPTIONS[:debug_msg]
174
+ when String
175
+ $debug_msg_file = open(OPTIONS[:debug_msg], "a")
176
+ else
177
+ $debug_msg_file = $stderr
178
+ end
179
+ original_factory = broker_factory
180
+ broker_factory = proc do
181
+ broker = original_factory.call
182
+ def broker.send(name, msg)
183
+ $debug_msg_file.puts "send #{name}, #{msg.inspect}"
184
+ $debug_msg_file.flush
185
+ super
186
+ end
187
+ broker
188
+ end
189
+ end
190
+ if never_fork?
191
+ startup_non_console_processes(&broker_factory)
192
+ run_shell(broker_factory.call)
193
+ stop_threads
194
+ join_threads
195
+ else
196
+ if OPTIONS[:console]
197
+ if OPTIONS[:proxy] || OPTIONS[:server] || OPTIONS[:url_forwarder]
198
+ if windows?
199
+ require "win32/process"
200
+ options = encode_options.delete_if{|o|o=="--console"}.map{|o|%("#{o}")}.join(" ")
201
+ $stderr.puts "Lauching another process with options: #{options}"
202
+ @win32_process = Process.create(:app_name => "ruby \"#{$0}\" #{options}")
203
+ else
204
+ @child_pid = fork do
205
+ trap("TERM"){stop_threads}
206
+ trap("INT"){}
207
+ startup_non_console_processes(&broker_factory)
208
+ join_threads
209
+ end
210
+ end
211
+ end
212
+ wait_for_port(OPTIONS[:port]) if OPTIONS[:proxy]
213
+ wait_for_port(OPTIONS[:sport]) if OPTIONS[:server]
214
+ run_shell(broker_factory.call)
215
+ else
216
+ trap("TERM"){stop_threads}
217
+ trap("INT"){stop_threads}
218
+ startup_non_console_processes(&broker_factory)
219
+ join_threads
220
+ end
221
+ end
222
+ end
223
+
224
+ def wait_for_port(port)
225
+ 30.times do
226
+ begin
227
+ s = TCPSocket.open("localhost", port)
228
+ s.close
229
+ break
230
+ rescue
231
+ end
232
+ sleep 0.3
233
+ end
234
+ end
235
+
236
+ def windows?
237
+ RUBY_PLATFORM =~ /-mswin32$/
238
+ end
239
+
240
+ def never_fork?
241
+ "java" == RUBY_PLATFORM || OPTIONS[:never_fork]
242
+ end
243
+
244
+ def run_shell(broker)
245
+ shell = Shell.new(broker)
246
+ shell.run
247
+ if @child_pid
248
+ Process.kill "TERM", @child_pid
249
+ Process.waitall
250
+ elsif @win32_process
251
+ Process.kill 4, @win32_process.process_id
252
+ Process.waitpid(@win32_process.process_id)
253
+ end
254
+ end
255
+
256
+ class ThreadLauncher
257
+ attr_reader :thread
258
+
259
+ def initialize(broker = nil)
260
+ @broker = broker
261
+ end
262
+
263
+ def start
264
+ @thread = Thread.start do
265
+ begin
266
+ run
267
+ rescue Exception
268
+ $stderr.puts $!
269
+ $stderr.puts $!.backtrace
270
+ end
271
+ end
272
+ wait if respond_to?(:wait)
273
+ @thread
274
+ end
275
+
276
+ def wait_for_port(port)
277
+ 30.times do
278
+ break unless @thread.alive?
279
+ begin
280
+ s = TCPSocket.open("localhost", port)
281
+ s.close
282
+ break
283
+ rescue
284
+ end
285
+ sleep 0.3
286
+ end
287
+ end
288
+ end
289
+
290
+ class ProxyLauncher < ThreadLauncher
291
+ def run
292
+ proxy_options = {:Port => OPTIONS[:port], :BindAddress => OPTIONS[:bind], :AccessLog => [], :ProxyVia => nil}
293
+ if OPTIONS[:debug_proxy]
294
+ proxy_options[:Logger] = WEBrick::Log.new(nil, WEBrick::BasicLog::DEBUG)
295
+ end
296
+ proxy = ProxyServer.new(@broker, proxy_options)
297
+ @proxy = proxy
298
+ proxy.start
299
+ end
300
+
301
+ def wait
302
+ wait_for_port(OPTIONS[:port])
303
+ end
304
+
305
+ def shutdown
306
+ @proxy.shutdown
307
+ end
308
+ end
309
+
310
+ class StompServerLauncher < ThreadLauncher
311
+ def start
312
+ require "stomp_server"
313
+ require "frame_journal"
314
+ super
315
+ end
316
+
317
+ def run
318
+ StompServer.setup(FrameJournal.new(OPTIONS[:sjournal]))
319
+ EventMachine.run do
320
+ puts "Stomp Server starting on port #{OPTIONS[:sport]}"
321
+ EventMachine.start_server OPTIONS[:sbind], OPTIONS[:sport], StompServer
322
+ end
323
+ end
324
+
325
+ def wait
326
+ wait_for_port(OPTIONS[:sport])
327
+ end
328
+
329
+ def shutdown
330
+ EventMachine.stop
331
+ end
332
+ end
333
+
334
+ class URLForwarderLauncher < ThreadLauncher
335
+ def run
336
+ @server = URLForwarder.new(@broker,
337
+ :Port => OPTIONS[:uport],
338
+ :BindAddress => OPTIONS[:ubind])
339
+ @server.start
340
+ end
341
+
342
+ def shutdown
343
+ @server.shutdown
344
+ end
345
+ end
39
346
  end
40
347
  end
41
348
 
42
- # do stuff
43
- server = JSCommander::Broker.new(:Port => OPTIONS[:port], :AccessLog => [], :ProxyVia => nil)
44
- server.start
349
+ JSCommander::CommandLine.new
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env perl
2
+ # An example script that prints title of the current page
3
+ use strict;
4
+ use warnings;
5
+ use Net::Stomp;
6
+
7
+ my $stomp = Net::Stomp->new({hostname => 'localhost', port => '61613'});
8
+ $stomp->connect({login => '', passcode => ''});
9
+ $stomp->subscribe({destination => "/topic/events"});
10
+ my $id = rand();
11
+ $stomp->send({destination => "/topic/commands",
12
+ "jscmd.type" => "eval",
13
+ "jscmd.id" => $id,
14
+ body => "document.title"});
15
+ for (;;) {
16
+ my $frame = $stomp->receive_frame;
17
+ if ($id eq $frame->headers->{"jscmd.in-reply-to"}) {
18
+ print $frame->body . "\n";
19
+ last;
20
+ }
21
+ }
22
+ $stomp->disconnect;
data/lib/jscmd/agent.js CHANGED
@@ -54,36 +54,45 @@
54
54
  },
55
55
 
56
56
  poll: function() {
57
- var content = this.queue.length > 0 ? this.queue.shift() : "";
57
+ var content = this.queue.length > 0 ? this.queue.shift() : {type: "connect", body: ""};
58
58
  var xhr = this.createXHR();
59
59
  xhr.open("POST", "/_remote_js_proxy/poll", true);
60
60
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
61
+ xhr.setRequestHeader("X-JSCmd-Agent-Id", this.agentId);
62
+ if (content.inReplyTo) {
63
+ xhr.setRequestHeader("X-JSCmd-In-Reply-To", content.inReplyTo);
64
+ }
61
65
  var self = this;
62
66
  xhr.onreadystatechange = function() {
63
67
  if (xhr.readyState == 4) {
64
68
  delete self.currentXHR;
65
69
  try {
66
70
  if (xhr.status == 200) {
67
- var text = xhr.responseText;
68
- if (text) {
71
+ var commandId = xhr.getResponseHeader("X-JSCmd-Command-Id");
72
+ var requestType = xhr.getResponseHeader("X-JSCmd-Type");
73
+ var script = xhr.responseText;
74
+ if (requestType) {
69
75
  var value = null;
76
+ var type = null;
70
77
  try {
71
- var option = text.substring(0, 1);
72
- var script = text.substring(1, text.length);
73
- if (option == 'P') {
78
+ if (requestType == 'properties') {
74
79
  // get properties for code completion
75
80
  var obj = self.evaluate.apply(window, [script, false]);
76
- value = "V" + self.getAllProperties(obj).join(",");
77
- } else if (option == 'E') {
81
+ value = self.getAllProperties(obj).join(",");
82
+ type = "value";
83
+ } else if (requestType == 'eval') {
78
84
  // just evaluate
79
- value = "V" + self.evaluate.apply(window, [script, true]);
85
+ value = self.evaluate.apply(window, [script, true]);
86
+ type = "value";
80
87
  } else {
81
- value = "Ebad request: " + text;
88
+ value = "bad request: " + script;
89
+ type = "error";
82
90
  }
83
91
  } catch (e) {
84
- value = "E" + e.toString();
92
+ value = e.toString();
93
+ type = "error";
85
94
  }
86
- self.queue.push(value);
95
+ self.queue.push({inReplyTo: commandId, type: type, body: value});
87
96
  }
88
97
  }
89
98
  } catch (e) {
@@ -102,14 +111,25 @@
102
111
  this.currentXHR = xhr;
103
112
  if (this.queue.length > 0) {
104
113
  // mark that there are more messages pending
105
- content = "+" + content;
114
+ xhr.setRequestHeader("X-JSCmd-More", "true");
115
+ }
116
+ xhr.setRequestHeader("X-JSCmd-Type", content.type);
117
+ xhr.send(encodeURI(content.body));
118
+ },
119
+
120
+ generateAgentId: function() {
121
+ var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
122
+ var str = "";
123
+ for (var i = 0; i < 16; i++) {
124
+ str += chars.charAt(Math.floor(Math.random() * chars.length));
106
125
  }
107
- xhr.send(encodeURI(content));
126
+ this.agentId = str;
108
127
  },
109
128
 
110
129
  startPoll: function() {
111
130
  this.startPoll = function() {}
112
131
  if (window.top.parent != window) return;
132
+ this.generateAgentId();
113
133
  this.poll();
114
134
  },
115
135
 
@@ -225,10 +245,11 @@
225
245
  this.attachEvent("pageshow", function() { self.startPoll() });
226
246
 
227
247
  window.onerror = function(message, file, line) {
228
- self.pushValue("E" +
229
- "Message: " + message + "\n" +
230
- "File: " + file + "\n" +
231
- "Line: " + line + "\n");
248
+ self.pushValue({type: "error",
249
+ body:
250
+ "Message: " + message + "\n" +
251
+ "File: " + file + "\n" +
252
+ "Line: " + line + "\n"});
232
253
  }
233
254
  setTimeout(function() { self.startPoll() }, 3000);
234
255
 
@@ -243,7 +264,8 @@
243
264
  for (var i = 0; i < arguments.length; i++) {
244
265
  message.push(arguments[i]);
245
266
  }
246
- self.pushValue("V[" + level + "] " + message.join(", "));
267
+ self.pushValue({type: "value",
268
+ body:"[" + level + "] " + message.join(", ")});
247
269
  }
248
270
  })();
249
271
  }
@@ -51,7 +51,8 @@ module WEBrick
51
51
 
52
52
  response = nil
53
53
  q = Queue.new
54
- p_reader, p_writer = IO.pipe
54
+ data_queue = Queue.new
55
+ # p_reader, p_writer = IO.pipe
55
56
  thread = Thread.start do
56
57
  begin
57
58
  @logger.debug "downloading #{uri}"
@@ -87,10 +88,12 @@ module WEBrick
87
88
  if last_size / 500000 != size / 500000
88
89
  @logger.debug "downloading #{uri}: size=#{size}"
89
90
  end
90
- p_writer.write str
91
+ # p_writer.write str
92
+ data_queue.push str
91
93
  end
92
94
  @logger.debug "finished downloading #{uri}: size=#{size}"
93
- p_writer.close
95
+ # p_writer.close
96
+ data_queue.push nil
94
97
  end
95
98
  }
96
99
  rescue Exception => err
@@ -115,7 +118,7 @@ module WEBrick
115
118
  choose_header(response, res)
116
119
  set_cookie(response, res)
117
120
  set_via(res)
118
- res.body = p_reader
121
+ res.body = QueueInput.new(data_queue) # p_reader
119
122
  def res.flush_body
120
123
  if @body.is_a?(IO)
121
124
  begin
@@ -135,6 +138,39 @@ module WEBrick
135
138
  handler.call(req, res)
136
139
  end
137
140
  end
141
+
142
+ class QueueInput < IO
143
+ def initialize(queue)
144
+ @queue = queue
145
+ @eof = false
146
+ end
147
+
148
+ def close; end
149
+
150
+ def eof?
151
+ @eof
152
+ end
153
+
154
+ def pop
155
+ buf = @queue.pop
156
+ @eof = true if buf.nil?
157
+ buf
158
+ end
159
+
160
+ def read(len = nil)
161
+ return nil if eof?
162
+ if len
163
+ pop
164
+ else
165
+ data = ""
166
+ while buf = pop
167
+ data << buf
168
+ end
169
+ data
170
+ end
171
+ end
172
+ end
138
173
  end
174
+
139
175
  end
140
176
 
@@ -0,0 +1,31 @@
1
+ module JSCommander
2
+ class InProcessBroker
3
+ def initialize
4
+ @subscribers = {}
5
+ end
6
+
7
+ def subscribe(name, &block)
8
+ @subscribers[name] ||= []
9
+ @subscribers[name] << block
10
+ block
11
+ end
12
+
13
+ def unsubscribe(name, subscriber)
14
+ @subscribers[name].delete(subscriber)
15
+ end
16
+
17
+ def send(name, msg)
18
+ if @subscribers[name]
19
+ @subscribers[name].each do |sub|
20
+ Thread.start do
21
+ begin
22
+ sub.call(msg)
23
+ rescue Exception
24
+ $stderr.puts $!
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ =begin
2
+ Messages sent between clients and broker
3
+
4
+ * Topic "events": proxy -> broker -> shell
5
+
6
+ Common attributes:
7
+ type - Type of the message. (e.g. "connect")
8
+ clients - List of clients that are connected. (e.g. "[www.google.com],[www.yahoo.com]")
9
+ in-reply-to - id of the command that caused this message (optional; only used with "value", "error" and "pong" types)
10
+
11
+ Types:
12
+ connect - Notify that the new client has connected. The body must be empty.
13
+ abort - Notify that the client aborted. The body must be empty.
14
+ pong - Reply to "ping" command. body must be empty.
15
+ value - Send value with body.
16
+ error - Send error message with body.
17
+
18
+ * Topic "commands": shell -> broker -> proxy
19
+
20
+ Common attributes:
21
+ type - Type of the message. (e.g. "eval")
22
+ id - unique value that identifies this command. (optional; only used with "eval" and "properties" types)
23
+
24
+ Types:
25
+ ping - Request "update_clients". The body must be empty.
26
+ eval - Evaluate body.
27
+ properties - Evaluate body and get list of properties.
28
+
29
+ =end
30
+ module JSCommander
31
+ class Message
32
+ attr_accessor :body, :attributes
33
+
34
+ def initialize(body = nil, attributes = {})
35
+ @body = body
36
+ @attributes = {}
37
+ attributes.each do |key, value|
38
+ @attributes[key.to_s] = value
39
+ end
40
+ end
41
+ end
42
+ end