jscmd 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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