qcmd 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,7 +9,7 @@ def row label, record
9
9
  puts "%-12s%s" % [label, record.send(label)]
10
10
  end
11
11
 
12
- browser = DNSSD.browse '_qlab._udp' do |b|
12
+ def do_browse_on_service(b)
13
13
  DNSSD.resolve b.name, b.type, b.domain do |r|
14
14
  puts '*' * 40
15
15
  puts "FOUND QLAB:"
@@ -30,7 +30,24 @@ browser = DNSSD.browse '_qlab._udp' do |b|
30
30
  end
31
31
  end
32
32
 
33
- trap 'INT' do browser.stop; exit end
34
- trap 'TERM' do browser.stop; exit end
33
+ browsers = []
34
+
35
+ #browsers.push(DNSSD.browse('_qlab._udp.') do |b|
36
+ # do_browse_on_service(b)
37
+ #end)
38
+
39
+ browsers.push(DNSSD.browse('_qlab._tcp.') do |b|
40
+ do_browse_on_service(b)
41
+ end)
42
+
43
+ trap 'INT' do
44
+ browsers.map(&:stop);
45
+ exit
46
+ end
47
+
48
+ trap 'TERM' do
49
+ browsers.map(&:stop);
50
+ exit
51
+ end
35
52
 
36
53
  sleep
@@ -6,14 +6,12 @@ require 'readline'
6
6
  require 'rubygems'
7
7
 
8
8
  # use Qcmd's parser for type conversions and double quote recognizing
9
- require 'qcmd/parser'
9
+ require 'qcmd'
10
10
 
11
11
  # other gems
12
12
  require 'osc-ruby'
13
13
  require 'json'
14
-
15
- # handle Ctrl-C quitting
16
- trap("INT") { exit }
14
+ require 'trollop'
17
15
 
18
16
  # if there are args, there must be two:
19
17
  #
@@ -23,11 +21,27 @@ trap("INT") { exit }
23
21
  #
24
22
  # receive_port
25
23
 
26
- # default qlab send port 53000
24
+ VERSION_STRING = "qcmd simple console #{ Qcmd::VERSION } (c) 2012 Figure 53, Baltimore, MD."
25
+
26
+ opts = Trollop::options do
27
+ version VERSION_STRING
28
+ opt :debug, "Show full debug output", :default => false
29
+ end
30
+
31
+ if opts[:debug]
32
+ Qcmd.log_level = :debug
33
+ Qcmd.debug_mode = true
34
+ end
35
+
36
+ Qcmd.print VERSION_STRING
37
+ Qcmd.print
38
+
39
+ # default qlab send to localhost:53000
27
40
  send_address = 'localhost'
28
- send_port = 53000
41
+ send_port = 53000
29
42
 
30
- # default qlab receive port 53001
43
+ # default qlab receive port 53001. This is how QLab will send response
44
+ # messages.
31
45
  receive_port = 53001
32
46
 
33
47
  if ARGV.size > 0
@@ -39,38 +53,122 @@ if ARGV.size > 0
39
53
  elsif recv_matcher =~ ARGV[0]
40
54
  receive_port = $1
41
55
  else
42
- puts 'send address must be an address in the form SERVER_ADDRESS:PORT'
56
+ Qcmd.print 'Send address must be an address in the form SERVER_ADDRESS:PORT'
57
+ Qcmd.print
43
58
  end
44
59
 
45
60
  if ARGV[1]
46
61
  if recv_matcher =~ ARGV[1]
47
62
  receive_port = $1
48
63
  else
49
- puts 'send address must be a port number'
64
+ Qcmd.print 'Send address must be a port number'
50
65
  end
51
66
  end
52
67
  end
53
68
 
54
- puts %[connecting to server #{send_address}:#{send_port} with receiver at port #{receive_port}]
69
+ Qcmd.print %[connecting to server #{send_address}:#{send_port} with receiver at port #{receive_port}]
55
70
 
56
71
  # how long to wait for responses from QLab. If you notice responses coming in
57
72
  # out of order, you may need to increase this value.
58
73
  REPLY_TIMEOUT = 1
59
74
 
60
- # open IO pipes to communicate between client / server process
61
- response_receiver, writer = IO.pipe
75
+ # IO pipes to communicate between client / server process. In this case,
76
+ # the process talking to QLab is the client, which receives QLab's responses
77
+ # via the response_receiver.
78
+ response_receiver, response_writer = IO.pipe
79
+
80
+ class ClientReceiver
81
+ attr_accessor :state, :channel
82
+
83
+ def initialize channel, state
84
+ @channel = channel
85
+ @state = state
86
+ end
87
+
88
+ def wait
89
+ # wait for response until TIMEOUT seconds
90
+ select = IO.select([channel], [], [], REPLY_TIMEOUT)
91
+ if !select.nil?
92
+ rs = select[0]
93
+
94
+ # get readable channel
95
+ if in_channel = rs[0]
96
+ data = []
97
+
98
+ # read everything until end of stream
99
+ while line = in_channel.gets
100
+ if line.strip != '<<EOS>>'
101
+ data << line
102
+ else
103
+ break
104
+ end
105
+ end
106
+
107
+ new_state = data.join
108
+ Qcmd.debug "[response_receiver] new_state #{ new_state.inspect }"
109
+
110
+ new_state_obj = Marshal::load(new_state)
111
+ Qcmd.debug "[response_receiver] got state: #{ new_state_obj.inspect }"
112
+
113
+ if new_state_obj[:state]
114
+ self.state.merge! new_state_obj[:state]
115
+ end
116
+
117
+ if new_state_obj[:message]
118
+ Qcmd.print new_state_obj[:message]
119
+ end
120
+ end
121
+ else
122
+ Qcmd.debug '[response_receiver] timed out'
123
+
124
+ # select timed out, probably not going to get a response,
125
+ # go back to command line mode
126
+ end
127
+ end
128
+ end
62
129
 
63
130
  # fork readline process to allow server to communicate because if we use
64
131
  # Thread.new, readline locks the WHOLE Ruby VM and the server can't start
65
132
  pid = fork do
133
+ # handle Ctrl-C quitting
134
+ trap("INT") { exit }
135
+
66
136
  # close the IO channel that server process will be using
67
- writer.close
137
+ response_writer.close
68
138
 
69
139
  # native OSC connection, outbound
70
140
  client = OSC::Client.new 'localhost', send_port
71
141
 
142
+ command_state = {}
143
+ receiver = ClientReceiver.new response_receiver, command_state
144
+
145
+ # load list of workspaces
146
+ client.send OSC::Message.new('/workspaces')
147
+ receiver.wait
148
+
149
+ # connect to frontmost workspace
150
+ client.send OSC::Message.new('/connect')
151
+ receiver.wait
152
+
72
153
  loop do
73
- command_string = Readline.readline('> ', true)
154
+ # command prompt
155
+ pre_prompt = nil
156
+ prompt = "> "
157
+ if command_state[:workspace_id]
158
+ if command_state[:workspaces]
159
+ name = command_state[:workspaces].fetch(command_state[:workspace_id], {}).fetch('displayName', nil)
160
+ else
161
+ name = command_state[:workspace_id]
162
+ end
163
+
164
+ if !name.nil?
165
+ pre_prompt = "[#{ name }]"
166
+ end
167
+ end
168
+
169
+ Qcmd.print(pre_prompt) if !pre_prompt.nil?
170
+ command_string = Readline.readline(prompt, true)
171
+
74
172
  next if command_string.nil? || command_string.strip.size == 0
75
173
 
76
174
  # break command string up and properly typecast all given values
@@ -78,10 +176,16 @@ pid = fork do
78
176
  address = args.shift
79
177
 
80
178
  # quit, q, and exit all quit
81
- exit if /^(q(uit)?|exit)/i =~ address
179
+ exit if /^(q(uit)?|exit) ?$/i =~ address
180
+
181
+ case address
182
+ when 'state'
183
+ Qcmd.print JSON.pretty_generate(command_state)
184
+ next
185
+ end
82
186
 
83
187
  # "sanitize" the given address
84
- if %r[^/] != address
188
+ if %r[^/] !~ address
85
189
  if address == '>'
86
190
  # pasted previous command line entry
87
191
  address = args.shift
@@ -93,31 +197,12 @@ pid = fork do
93
197
 
94
198
  message = OSC::Message.new(address, *args)
95
199
  client.send message
200
+ receiver.wait
96
201
 
97
- # wait for response until TIMEOUT seconds
98
- select = IO.select([response_receiver], [], [], REPLY_TIMEOUT)
99
- if !select.nil?
100
- rs = select[0]
101
-
102
- # get readable channel
103
- if in_channel = rs[0]
104
- # read everything until end of stream
105
- while line = in_channel.gets
106
- if line.strip != '<<EOS>>'
107
- puts line
108
- else
109
- break
110
- end
111
- end
112
- end
113
- else
114
- # select timed out, probably not going to get a response,
115
- # go back to command line mode
116
- end
117
202
  end
118
203
  end
119
204
 
120
- puts "launched console with process id #{ pid }, use Ctrl-c or 'exit' to quit"
205
+ Qcmd.print "launched console with process id #{ pid }, use Ctrl-c or 'exit' to quit"
121
206
 
122
207
  # close unused pipe
123
208
  response_receiver.close
@@ -125,18 +210,71 @@ response_receiver.close
125
210
  # native OSC connection, inbound
126
211
  server = OSC::Server.new receive_port
127
212
 
128
- # server listens and forwards responses to the forked process
213
+ response_handlers = [
214
+ [
215
+ %r{^/workspaces$},
216
+ Proc.new { |data, message, response|
217
+ workspaces = {}
218
+
219
+ data.each {|ws|
220
+ workspaces[ws['uniqueID']] = ws
221
+ }
222
+
223
+ data_out = Marshal::dump({
224
+ :message => "#{workspaces.size} workspaces available: #{workspaces.values.map {|ws| ws['displayName']}.join(', ')}",
225
+ :state => {:workspaces => workspaces}
226
+ })
227
+
228
+ Qcmd.debug "[/connect responder] sending marshalled object: #{ data_out.inspect }"
229
+
230
+ response.puts data_out
231
+ }
232
+ ],
233
+ [
234
+ %r{^/workspace/.+/connect$},
235
+ Proc.new { |data, message, response|
236
+ if data == 'ok'
237
+ data = Marshal::dump({:message => 'ok', :state => {:workspace_id => message['workspace_id']}})
238
+ else
239
+ data = Marshal::dump({:message => 'connection failed', :state => {:workspace_id => nil}})
240
+ end
241
+
242
+ Qcmd.debug "[/connect responder] sending marshalled object: #{ data.inspect }"
243
+ response.puts data
244
+ }
245
+ ]
246
+ ]
247
+
248
+ # server listens and forwards responses to the console process
129
249
  server.add_method %r[/reply] do |osc_message|
130
- data = JSON.parse(osc_message.to_a.first)['data']
250
+ response = JSON.parse(osc_message.to_a.first)
251
+ address = response['address']
252
+ data = response['data']
253
+
254
+ responded = false
255
+ response_handlers.each do |(match, action)|
256
+ if match =~ address
257
+ action.call data, response, response_writer
258
+ responded = true
259
+ end
260
+ end
131
261
 
132
262
  begin
133
- writer.puts JSON.pretty_generate(data)
263
+ if !responded
264
+ data = Marshal::dump({:message => JSON.pretty_generate(data)})
265
+ end
134
266
  rescue JSON::GeneratorError
135
- writer.puts data.to_s
267
+ data = Marshal::dump({:message => data.to_s})
268
+ end
269
+
270
+ if !responded
271
+ Qcmd.debug "[server] sending marshalled object: #{ data.inspect }"
272
+ response_writer.puts data
136
273
  end
137
274
 
138
275
  # end of signal
139
- writer.puts '<<EOS>>'
276
+ Qcmd.debug "[server] sending <<EOS>>"
277
+ response_writer.puts '<<EOS>>'
140
278
  end
141
279
 
142
280
  # start blocking server
@@ -145,4 +283,9 @@ Thread.new do
145
283
  end
146
284
 
147
285
  # chill until the command line process quits
148
- Process.wait pid
286
+ begin
287
+ Process.wait pid
288
+ rescue Interrupt
289
+ # ignore
290
+ exit
291
+ end
@@ -0,0 +1,67 @@
1
+ require 'qcmd'
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ # try it out
7
+
8
+ qlab = OSC::TCPClient.new 'localhost', 53000
9
+
10
+ def receive(rcv)
11
+ if rcv
12
+ rcv.each do |osc_message|
13
+ begin
14
+ response = JSON.parse(osc_message.to_a.first)
15
+
16
+ address = response['address']
17
+ data = response['data']
18
+ status = response['status']
19
+
20
+ puts address
21
+
22
+ if status
23
+ puts "status -> #{ status }"
24
+ end
25
+
26
+ if data
27
+ puts JSON.pretty_generate(data)
28
+ end
29
+ rescue => ex
30
+ puts "parsing response failed: #{ ex.message }"
31
+ end
32
+ end
33
+ else
34
+ puts 'no response...'
35
+ end
36
+ end
37
+
38
+ msg = OSC::Message.new '/workspaces'
39
+ qlab.send(msg) do |response|
40
+ receive(response)
41
+ end
42
+
43
+ # don't always expect a reply
44
+ msg = OSC::Message.new '/alwaysReply', 0
45
+ qlab.send(msg) do |response|
46
+ receive(response)
47
+ end
48
+
49
+ # non-responsive command
50
+ msg = OSC::Message.new '/workspace/65E9D86D-87DD-4CB1-A659-6584BAE57AB2/go'
51
+ qlab.send(msg) do |response|
52
+ receive(response)
53
+ end
54
+
55
+ # always expect a reply
56
+ msg = OSC::Message.new '/alwaysReply', 1
57
+ qlab.send(msg) do |response|
58
+ receive(response)
59
+ end
60
+
61
+ # non-responsive command is now a responsive command
62
+ msg = OSC::Message.new '/workspace/65E9D86D-87DD-4CB1-A659-6584BAE57AB2/go'
63
+ qlab.send(msg) do |response|
64
+ receive(response)
65
+ end
66
+
67
+
@@ -0,0 +1,84 @@
1
+ require 'qcmd'
2
+
3
+ def test_log msg=''
4
+ puts msg
5
+ end
6
+
7
+ class DeadHandler
8
+ def self.handle response
9
+ # do nothing :P
10
+ end
11
+ end
12
+
13
+ describe Qcmd::Action do
14
+ before do
15
+ Qcmd.context = Qcmd::Context.new
16
+ Qcmd.context.machine = Qcmd::Machine.new('test machine', 'localhost', 53000)
17
+ Qcmd.context.connect_to_qlab DeadHandler
18
+
19
+ # always reply ON
20
+ Qcmd.context.qlab.send(OSC::Message.new('/alwaysReply', 1))
21
+ end
22
+
23
+ it "should call `parse` when initialized" do
24
+ Qcmd::Action.any_instance.stub(:parse) { true }
25
+ Qcmd::Action.any_instance.should_receive(:parse)
26
+ Qcmd::Action.new [:cue, 10, :name]
27
+ end
28
+
29
+ it 'should send a command when evaluated' do
30
+ action = Qcmd::Action.new 'workspaces'
31
+
32
+ action.stub(:send_message) { true }
33
+ action.should_receive :send_message
34
+
35
+ action.evaluate
36
+ end
37
+
38
+ it 'should return the data resulting from an OSC message' do
39
+ action = Qcmd::Action.new 'cueLists'
40
+
41
+ result = nil
42
+
43
+ expect {
44
+ result = action.evaluate
45
+ }.to_not raise_error
46
+
47
+ result.should_not be_nil
48
+ result.should be_an_instance_of(Array)
49
+ end
50
+
51
+ describe 'workspace specific action' do
52
+ before do
53
+ osc_message = OSC::Message.new '/workspaces'
54
+ Qcmd.context.qlab.send(osc_message) do |response|
55
+ reply = Qcmd::QLab::Reply.new(response)
56
+ Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
57
+ end
58
+ end
59
+
60
+ it 'should be able to connect' do
61
+ workspace = Qcmd.context.machine.workspaces.first
62
+
63
+ ws_action_string = "workspace/#{workspace.id}/connect"
64
+
65
+ reply = Qcmd::Action.evaluate(ws_action_string)
66
+
67
+ reply.should eql('ok')
68
+ end
69
+ end
70
+
71
+ describe 'cue specific action' do
72
+ it 'should send a cue OSC message' do
73
+ action = Qcmd::CueAction.new 'cue 1 isRunning'
74
+ action.send(:osc_address).should eql('/cue/1/isRunning')
75
+ action.send(:osc_arguments).should eql([])
76
+ end
77
+
78
+ it 'should nest actions' do
79
+ action = Qcmd::CueAction.new 'cue 1 name (cue 2 name)'
80
+ action.code[3].should be_an_instance_of(Qcmd::CueAction)
81
+ end
82
+ end
83
+
84
+ end