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.
- data/README.md +7 -2
- data/TODO.md +31 -0
- data/bin/qcmd +2 -1
- data/lib/qcmd.rb +15 -3
- data/lib/qcmd/action.rb +160 -0
- data/lib/qcmd/aliases.rb +25 -0
- data/lib/qcmd/cli.rb +503 -108
- data/lib/qcmd/commands.rb +198 -142
- data/lib/qcmd/configuration.rb +83 -0
- data/lib/qcmd/context.rb +19 -13
- data/lib/qcmd/core_ext/osc/tcp_client.rb +155 -0
- data/lib/qcmd/handler.rb +49 -67
- data/lib/qcmd/history.rb +26 -0
- data/lib/qcmd/input_completer.rb +12 -2
- data/lib/qcmd/network.rb +9 -3
- data/lib/qcmd/parser.rb +14 -83
- data/lib/qcmd/plaintext.rb +0 -4
- data/lib/qcmd/qlab.rb +1 -0
- data/lib/qcmd/qlab/cue.rb +20 -0
- data/lib/qcmd/qlab/cue_list.rb +83 -0
- data/lib/qcmd/qlab/reply.rb +18 -4
- data/lib/qcmd/qlab/workspace.rb +23 -1
- data/lib/qcmd/version.rb +1 -1
- data/lib/vendor/sexpistol/LICENSE +20 -0
- data/lib/vendor/sexpistol/sexpistol.rb +2 -0
- data/lib/vendor/sexpistol/sexpistol/sexpistol.rb +76 -0
- data/lib/vendor/sexpistol/sexpistol/sexpistol_parser.rb +94 -0
- data/sample/dnssd.rb +20 -3
- data/sample/simple_console.rb +186 -43
- data/sample/tcp_qlab_connection.rb +67 -0
- data/spec/unit/action_spec.rb +84 -0
- data/spec/unit/commands_spec.rb +135 -14
- data/spec/unit/parser_spec.rb +36 -5
- metadata +124 -122
- data/lib/qcmd/core_ext/osc/stopping_server.rb +0 -84
- data/lib/qcmd/server.rb +0 -175
- data/spec/unit/osc_server_spec.rb +0 -78
data/sample/dnssd.rb
CHANGED
@@ -9,7 +9,7 @@ def row label, record
|
|
9
9
|
puts "%-12s%s" % [label, record.send(label)]
|
10
10
|
end
|
11
11
|
|
12
|
-
|
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
|
-
|
34
|
-
|
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
|
data/sample/simple_console.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
|
64
|
+
Qcmd.print 'Send address must be a port number'
|
50
65
|
end
|
51
66
|
end
|
52
67
|
end
|
53
68
|
|
54
|
-
|
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
|
-
#
|
61
|
-
|
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
|
-
|
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
|
-
|
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)
|
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[^/]
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
263
|
+
if !responded
|
264
|
+
data = Marshal::dump({:message => JSON.pretty_generate(data)})
|
265
|
+
end
|
134
266
|
rescue JSON::GeneratorError
|
135
|
-
|
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
|
-
|
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
|
-
|
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
|