qcmd 0.1.7 → 0.1.8

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.
@@ -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