qcmd 0.1.6 → 0.1.7

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 CHANGED
@@ -4,6 +4,9 @@
4
4
  QLab 3's new OSC interface. `qcmd` should be useable from any machine on
5
5
  the same local network as the QLab workspace you intend to work with.
6
6
 
7
+ This project is OS X only and has been tested against Ruby 1.8.7-p358 (OS X
8
+ 10.8 default), Ruby 1.8.7 REE 2012.02, and Ruby 1.9.3-p327.
9
+
7
10
  **This project should be considered experimental. DO NOT RUN SHOWS WITH
8
11
  IT.**
9
12
 
@@ -16,6 +19,7 @@ Install this gem to your machine by running the following command:
16
19
 
17
20
  That should do ya.
18
21
 
22
+
19
23
  ## Starting the `qcmd` console.
20
24
 
21
25
  Run the following command in a terminal window:
@@ -122,6 +126,7 @@ An example session might look like this:
122
126
  > exit
123
127
  exiting...
124
128
 
129
+
125
130
  ## Contributing
126
131
 
127
132
  1. Fork it
data/lib/qcmd.rb CHANGED
@@ -1,11 +1,17 @@
1
+ require 'socket'
2
+ require 'osc-ruby'
3
+
1
4
  require 'qcmd/version'
5
+
6
+ require 'qcmd/plaintext'
7
+ require 'qcmd/commands'
2
8
  require 'qcmd/input_completer'
3
9
 
4
10
  require 'qcmd/core_ext/array'
5
11
  require 'qcmd/core_ext/osc/message'
12
+ require 'qcmd/core_ext/osc/stopping_server'
6
13
 
7
14
  module Qcmd
8
- # Your code goes here...
9
15
  autoload :Handler, 'qcmd/handler'
10
16
  autoload :Server, 'qcmd/server'
11
17
  autoload :Context, 'qcmd/context'
@@ -14,8 +20,6 @@ module Qcmd
14
20
  autoload :Machine, 'qcmd/machine'
15
21
  autoload :Network, 'qcmd/network'
16
22
  autoload :QLab, 'qcmd/qlab'
17
- autoload :Plaintext, 'qcmd/plaintext'
18
- autoload :Commands, 'qcmd/commands'
19
23
  autoload :VERSION, 'qcmd/version'
20
24
 
21
25
  class << self
@@ -33,13 +37,21 @@ module Qcmd
33
37
  self.log_level = :warning
34
38
  end
35
39
 
40
+ def silent!
41
+ self.log_level = :none
42
+ end
43
+
44
+ def silent?
45
+ self.log_level == :none
46
+ end
47
+
36
48
  def quiet?
37
49
  self.log_level == :warning
38
50
  end
39
51
 
40
52
  def while_quiet
41
53
  previous_level = self.log_level
42
- self.log_level = :warning unless previous_level == :debug
54
+ self.log_level = :warning
43
55
  yield
44
56
  self.log_level = previous_level
45
57
  end
data/lib/qcmd/cli.rb CHANGED
@@ -3,7 +3,6 @@ require 'qcmd/server'
3
3
  require 'readline'
4
4
 
5
5
  require 'osc-ruby'
6
- require 'osc-ruby/em_server'
7
6
 
8
7
  module Qcmd
9
8
  class CLI
@@ -36,7 +35,7 @@ module Qcmd
36
35
 
37
36
  if options[:command_given]
38
37
  handle_message options[:command]
39
- puts %[sent command "#{ options[:command] }"]
38
+ print %[sent command "#{ options[:command] }"]
40
39
  exit 0
41
40
  end
42
41
  end
@@ -146,7 +145,22 @@ module Qcmd
146
145
 
147
146
  Qcmd.debug "(using workspace: #{ workspace_name.inspect })"
148
147
 
149
- connect_to_workspace_by_name workspace_name, passcode
148
+ if workspace_name
149
+ connect_to_workspace_by_name workspace_name, passcode
150
+ else
151
+ print "No workspace name given. The following workspaces are available:"
152
+ Qcmd.context.print_workspace_list
153
+ end
154
+
155
+ when 'help'
156
+ help_command = args.shift
157
+
158
+ if help_command.nil?
159
+ Qcmd::Commands::Help.print_all_commands
160
+ # print help according to current context
161
+ else
162
+ # print command specific help
163
+ end
150
164
 
151
165
  when 'cues'
152
166
  if !Qcmd.context.workspace_connected?
@@ -179,13 +193,21 @@ module Qcmd
179
193
  print
180
194
  print " > cue NUMBER COMMAND ARGUMENTS"
181
195
  print
182
- print_wrapped("available cue commands are: #{Qcmd::InputCompleter::ReservedCueWords.join(', ')}")
196
+ print_wrapped("available cue commands are: #{Qcmd::Commands::ALL_CUE_COMMANDS.join(', ')}")
183
197
  elsif cue_action.nil?
184
198
  server.send_workspace_command("cue/#{ cue_number }")
185
199
  else
186
200
  server.send_cue_command(cue_number, cue_action, *args)
187
201
  end
188
202
 
203
+ when 'workspaces'
204
+ if !Qcmd.context.machine_connected?
205
+ print 'cannot load workspaces until you are connected to a machine'
206
+ return
207
+ end
208
+
209
+ server.load_workspaces
210
+
189
211
  when 'workspace'
190
212
  workspace_command = args.shift
191
213
 
data/lib/qcmd/commands.rb CHANGED
@@ -1,10 +1,34 @@
1
+ require 'qcmd/plaintext'
2
+
1
3
  module Qcmd
2
4
  module Commands
3
- # Commands that expect reponses
5
+ # All Commands
6
+ #
7
+ # *_RESPONSE lists are commands that expect responses
8
+ #
9
+
10
+ NO_MACHINE_RESPONSE = %w(connect)
11
+
4
12
  MACHINE_RESPONSE = %w(workspaces)
5
13
 
6
14
  WORKSPACE_RESPONSE = %w(
7
- cueLists selectedCues runningCues runningOrPausedCues connect thump
15
+ cueLists selectedCues runningCues runningOrPausedCues thump
16
+ )
17
+
18
+ WORKSPACE_NO_RESPONSE = %w(
19
+ go stop pause resume reset panic
20
+ )
21
+
22
+ ALL_WORKSPACE_COMMANDS = [WORKSPACE_RESPONSE + WORKSPACE_NO_RESPONSE]
23
+
24
+ # commands that take no args and do not respond
25
+ CUE_NO_RESPONSE = %w(
26
+ start stop pause resume load preview reset panic
27
+ )
28
+
29
+ # commands that take args but do not respond
30
+ CUE_ARG_NO_RESPONSE = %w(
31
+ loadAt
8
32
  )
9
33
 
10
34
  # commands that always expect a response
@@ -16,14 +40,29 @@ module Qcmd
16
40
  basics children
17
41
  )
18
42
 
19
- # commands that expect a response if given without args
43
+ # commands that take args but expect a response if given without args
20
44
  NO_ARG_CUE_RESPONSE = %w(
21
45
  number name notes cueTargetNumber cueTargetId preWait duration
22
46
  postWait continueMode flagged armed colorName
23
47
  sliderLevel
24
48
  )
25
49
 
50
+ # all cue commands that take arguments
51
+ CUE_ARG = CUE_ARG_NO_RESPONSE + NO_ARG_CUE_RESPONSE
52
+
53
+ ALL_CUE_COMMANDS = CUE_NO_RESPONSE +
54
+ CUE_ARG_NO_RESPONSE +
55
+ CUE_RESPONSE +
56
+ NO_ARG_CUE_RESPONSE
57
+
26
58
  class << self
59
+ def no_machine_response_matcher
60
+ @no_machine_response_matcher ||= %r[(#{NO_MACHINE_RESPONSE.join('|')})]
61
+ end
62
+ def no_machine_response_match command
63
+ !!(no_machine_response_matcher =~ command)
64
+ end
65
+
27
66
  def machine_response_matcher
28
67
  @machine_response_matcher ||= %r[(#{MACHINE_RESPONSE.join('|')})]
29
68
  end
@@ -63,7 +102,7 @@ module Qcmd
63
102
  when :none
64
103
  # shouldn't be dealing with OSC messages when unconnected to
65
104
  # machine or workspace
66
- response = false
105
+ response = no_machine_response_match(address)
67
106
  when :machine
68
107
  # could be workspace or machine command
69
108
  response = machine_response_match(address) ||
@@ -100,5 +139,85 @@ module Qcmd
100
139
  /workspace/ =~ address && !(%r[cue/] =~ address || %r[cue_id/] =~ address)
101
140
  end
102
141
  end
142
+
143
+ module Help
144
+ class << self
145
+
146
+ def print_all_commands
147
+ Qcmd.print %[
148
+ #{Qcmd.centered_text(' Available Commands ', '-')}
149
+
150
+ exit
151
+
152
+ close qcmd
153
+
154
+
155
+ connect MACHINE_NAME
156
+
157
+ connect to the machine with name MACHINE_NAME
158
+
159
+
160
+ disconnect
161
+
162
+ disconnect from the current machine and workspace
163
+
164
+
165
+ use WORKSPACE_NAME [PASSCODE]
166
+
167
+ connect to the workspace with name WORKSPACE_NAME using passcode PASSCODE. A
168
+ passcode is only required if the workspace has one enabled.
169
+
170
+
171
+ workspaces
172
+
173
+ show a list of the available workspaces for the currently connected machine.
174
+
175
+
176
+ workspace COMMAND [VALUE]
177
+
178
+ pass the given COMMAND to the connected workspace. The following commands will
179
+ act on a workspace but will not return a value:
180
+
181
+ #{Qcmd.wrapped_text(Qcmd::Commands::WORKSPACE_NO_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
182
+
183
+ And these commands will not act on a workspace, but will return a value
184
+ (usually a list of cues):
185
+
186
+ #{Qcmd.wrapped_text((Qcmd::Commands::WORKSPACE_RESPONSE - ['connect']).sort.join(', '), :indent => ' ').join("\n")}
187
+
188
+ * Pro Tip: once you are connected to a workspace, you can just use COMMAND
189
+ and leave off "workspace" to quickly send the given COMMAND to the connected
190
+ workspace.
191
+
192
+
193
+ cue NUMBER COMMAND [VALUE [ANOTHER_VALUE ...]]
194
+
195
+ send a command to the cue with the given NUMBER.
196
+
197
+ NUMBER can be a string or a number, depending on the command.
198
+
199
+ COMMAND can be one of:
200
+
201
+ #{Qcmd.wrapped_text(Qcmd::Commands::ALL_CUE_COMMANDS.sort.join(', '), :indent => ' ').join("\n")}
202
+
203
+ Of those commands, only some accept a VALUE. The following commands, if given
204
+ a value, will update the cue:
205
+
206
+ #{Qcmd.wrapped_text(Qcmd::Commands::CUE_ARG.sort.join(', '), :indent => ' ').join("\n")}
207
+
208
+ Some cues are Read-Only and will return information about a cue:
209
+
210
+ #{Qcmd.wrapped_text(Qcmd::Commands::CUE_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
211
+
212
+ Finally, some commands act on a cue, but don't take a VALUE and don't
213
+ respond:
214
+
215
+ #{Qcmd.wrapped_text(Qcmd::Commands::CUE_NO_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
216
+
217
+ ]
218
+ end
219
+ end
220
+
221
+ end
103
222
  end
104
223
  end
data/lib/qcmd/context.rb CHANGED
@@ -33,5 +33,18 @@ module Qcmd
33
33
  :workspace
34
34
  end
35
35
  end
36
+
37
+ def print_workspace_list
38
+ Qcmd.print Qcmd.centered_text(" Workspaces ", '-')
39
+ Qcmd.print
40
+
41
+ machine.workspaces.each_with_index do |ws, n|
42
+ Qcmd.print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
43
+ end
44
+
45
+ Qcmd.print
46
+ Qcmd.print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is optional.')
47
+ Qcmd.print
48
+ end
36
49
  end
37
50
  end
@@ -0,0 +1,84 @@
1
+ module OSC
2
+ class StoppingServer < Server
3
+ def initialize *args
4
+ @state = :initialized
5
+ @port = args.first
6
+ super(*args)
7
+ end
8
+
9
+ def run
10
+ @state = :starting
11
+ super
12
+ end
13
+
14
+ def stop
15
+ @state = :stopping
16
+ stop_detector
17
+ stop_dispatcher
18
+ end
19
+
20
+ def state
21
+ @state
22
+ end
23
+
24
+ private
25
+
26
+ def stop_detector
27
+ # send listening port a "CLOSE" signal on the open UDP port
28
+ _closer = UDPSocket.new
29
+ _closer.connect('', @port)
30
+ _closer.puts "CLOSE-#{@port}"
31
+ _closer.close unless _closer.closed? || !_closer.respond_to?(:close)
32
+ end
33
+
34
+ def stop_dispatcher
35
+ @queue << :stop
36
+ end
37
+
38
+ def dispatcher
39
+ loop do
40
+ mesg = @queue.pop
41
+ dispatch_message( mesg )
42
+ end
43
+ rescue StopException
44
+ @state = :stopped
45
+ end
46
+
47
+ def dispatch_message message
48
+ if message.is_a?(Symbol) && message.to_s == 'stop'
49
+ raise StopException.new
50
+ end
51
+
52
+ super(message)
53
+ end
54
+
55
+ def detector
56
+ @state = :listening
57
+
58
+ loop do
59
+ osc_data, network = @socket.recvfrom( 16384 )
60
+
61
+ # quit if socket receives the close signal
62
+ if osc_data == "CLOSE-#{@port}"
63
+ @socket.close if !@socket.closed? && @socket.respond_to?(:close)
64
+ break
65
+ end
66
+
67
+ unpack_socket_receipt osc_data, network
68
+ end
69
+ end
70
+
71
+ def unpack_socket_receipt osc_data, network
72
+ ip_info = Array.new
73
+ ip_info << network[1]
74
+ ip_info.concat(network[2].split('.'))
75
+ OSC::OSCPacket.messages_from_network( osc_data, ip_info ).each do |message|
76
+ @queue.push(message)
77
+ end
78
+ rescue EOFError
79
+ # pass
80
+ end
81
+ end
82
+
83
+ class StopException < Exception; end
84
+ end
data/lib/qcmd/handler.rb CHANGED
@@ -10,15 +10,7 @@ module Qcmd
10
10
  Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
11
11
 
12
12
  unless Qcmd.quiet?
13
- print centered_text(" Workspaces ", '-')
14
- print
15
- Qcmd.context.machine.workspaces.each_with_index do |ws, n|
16
- print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
17
- end
18
-
19
- print
20
- print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is optional.')
21
- print
13
+ Qcmd.context.print_workspace_list
22
14
  end
23
15
 
24
16
  when %r[/workspace/[^/]+/connect]
@@ -69,8 +61,24 @@ module Qcmd
69
61
  if result.is_a?(String) || result.is_a?(Numeric)
70
62
  print result
71
63
  else
72
- print result.inspect
64
+ case reply.address
65
+ when %r[/basics]
66
+ keys = result.keys.sort
67
+ table(['Field Name', 'Value'], keys.map {|key|
68
+ [key, result[key]]
69
+ })
70
+ else
71
+ begin
72
+ print JSON.pretty_generate(result)
73
+ rescue JSON::GeneratorError
74
+ print result.to_s
75
+ end
76
+ end
73
77
  end
78
+
79
+ when %r[/thump]
80
+ print reply.data
81
+
74
82
  else
75
83
  Qcmd.debug "(unrecognized message from QLab, cannot handle #{ reply.address })"
76
84
  end
@@ -4,23 +4,12 @@ module Qcmd
4
4
  module InputCompleter
5
5
  # the commands listed here should represent every possible legal command
6
6
  ReservedWords = %w[
7
- connect exit quit workspace workspaces disconnect
7
+ connect exit quit workspace workspaces disconnect help
8
8
  ]
9
9
 
10
- ReservedWorkspaceWords = %w[
11
- cueLists selectedCues runningCues runningOrPausedCues thump
12
- go stop pause resume reset panic disconnect
13
- ]
10
+ ReservedWorkspaceWords = Qcmd::Commands::ALL_WORKSPACE_COMMANDS
14
11
 
15
- ReservedCueWords = %w[
16
- cue stop pause resume load preview reset panic loadAt uniqueID
17
- hasFileTargets hasCueTargets allowsEditingDuration isLoaded isRunning
18
- isPaused isBroken preWaitElapsed actionElapsed postWaitElapsed
19
- percentPreWaitElapsed percentActionElapsed percentPostWaitElapsed
20
- type number name notes cueTargetNumber cueTargetId preWait duration
21
- postWait continueMode flagged armed colorName basics children
22
- sliderLevel sliderLevels
23
- ]
12
+ ReservedCueWords = Qcmd::Commands::ALL_CUE_COMMANDS
24
13
 
25
14
  CompletionProc = Proc.new {|input|
26
15
  # puts "input: #{ input }"
data/lib/qcmd/network.rb CHANGED
@@ -2,7 +2,7 @@ require 'dnssd'
2
2
 
3
3
  module Qcmd
4
4
  class Network
5
- BROWSE_TIMEOUT = 8
5
+ BROWSE_TIMEOUT = 2
6
6
 
7
7
  class << self
8
8
  attr_accessor :machines, :browse_thread
@@ -18,19 +18,7 @@ module Qcmd
18
18
  end
19
19
  end
20
20
 
21
- naps = 0
22
- changed = false
23
- previous = 0
24
-
25
- # sleep for BROWSE_TIMEOUT loops
26
- while naps < BROWSE_TIMEOUT
27
- sleep 0.2
28
- naps += 1
29
-
30
- if machines.size != previous
31
- previous = machines.size
32
- end
33
- end
21
+ sleep BROWSE_TIMEOUT
34
22
 
35
23
  Thread.kill(browse_thread) if browse_thread.alive?
36
24
  end
data/lib/qcmd/osc.rb ADDED
@@ -0,0 +1 @@
1
+ require 'qcmd/osc/server'
@@ -8,17 +8,21 @@ module Qcmd
8
8
  end
9
9
  end
10
10
 
11
- # always output
11
+ # display output unless absolutely silent
12
12
  def print message=nil
13
- log(message)
13
+ log(message) unless Qcmd.silent?
14
+ end
15
+
16
+ def set_columns value
17
+ @columns = value
14
18
  end
15
19
 
16
20
  def columns
17
- begin
18
- `stty size`.split.last.to_i
19
- rescue
20
- 80
21
- end
21
+ @columns || (begin
22
+ @columns = `stty size`.split.last.to_i
23
+ rescue
24
+ @columns = 80
25
+ end)
22
26
  end
23
27
 
24
28
  def pluralize n, word
@@ -27,12 +31,35 @@ module Qcmd
27
31
 
28
32
  def word_wrap(text, options={})
29
33
  options = {
30
- :line_width => columns
34
+ :line_width => columns,
35
+ :preserve_whitespace => false
31
36
  }.merge options
32
37
 
33
- text.split("\n").collect do |line|
34
- line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
35
- end * "\n"
38
+ unless options[:preserve_whitespace]
39
+ text = text.gsub(/\s+/, ' ') # collapse whitespace
40
+ end
41
+
42
+ prefix = options[:indent] ? options[:indent] : ''
43
+
44
+ line_width = options[:line_width]
45
+ lines = ['']
46
+
47
+ space = ' '
48
+ space_size = 2
49
+ space_left = line_width
50
+ text.split.each do |word|
51
+ if (word.size + space.size) >= space_left
52
+ word = "%s%s" % [prefix, word]
53
+ space_left = line_width - (word.size + space_size)
54
+ lines << ""
55
+ else
56
+ space_left = space_left - (word.size + space_size)
57
+ end
58
+
59
+ lines.last << "%s%s" % [word, space]
60
+ end
61
+
62
+ lines
36
63
  end
37
64
 
38
65
  def ascii_qlab
@@ -53,9 +80,14 @@ module Qcmd
53
80
  end
54
81
 
55
82
  # turn line into lines of text of columns length
56
- def wrapped_text line
57
- line = line.gsub(/\s+/, ' ') # collapse whitespace
58
- word_wrap(line, :line_width => columns).split("\n")
83
+ def wrapped_text *args
84
+ options = {
85
+ :line_width => columns
86
+ }.merge args.extract_options!
87
+
88
+ line = args.shift
89
+
90
+ word_wrap(line, options)
59
91
  end
60
92
 
61
93
  def print_wrapped line
@@ -103,6 +135,14 @@ module Qcmd
103
135
  def table headers, rows
104
136
  print
105
137
  columns = headers.map(&:size)
138
+
139
+ # coerce row values to strings
140
+ rows.each do |row|
141
+ columns.each_with_index do |col, n|
142
+ row[n] = row[n].to_s
143
+ end
144
+ end
145
+
106
146
  rows.each do |row|
107
147
  columns.each_with_index do |col, n|
108
148
  columns[n] = [col, row[n].size].max + 1
data/lib/qcmd/server.rb CHANGED
@@ -1,9 +1,4 @@
1
1
  require 'osc-ruby'
2
- require 'osc-ruby/em_server'
3
- begin
4
- require 'ruby-debug'
5
- rescue LoadError
6
- end
7
2
 
8
3
  require 'json'
9
4
 
@@ -49,11 +44,10 @@ module Qcmd
49
44
  # initialize
50
45
  def listen
51
46
  if receive_channel
52
- Qcmd.debug "(stopping existing server)"
53
47
  stop
54
48
  end
55
49
 
56
- self.receive_channel = OSC::EMServer.new(self.receive_port)
50
+ self.receive_channel = OSC::StoppingServer.new(self.receive_port)
57
51
 
58
52
  Qcmd.debug "(opening receiving channel: #{ self.receive_channel.inspect })"
59
53
 
@@ -133,7 +127,7 @@ module Qcmd
133
127
  end
134
128
 
135
129
  def stop
136
- Thread.kill(receive_thread) if receive_thread.alive?
130
+ receive_channel.stop if receive_channel && receive_channel.state == :listening
137
131
  end
138
132
 
139
133
  def run
data/lib/qcmd/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Qcmd
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
data/qcmd.gemspec CHANGED
@@ -18,13 +18,11 @@ Gem::Specification.new do |gem|
18
18
  gem.require_paths = ["lib"]
19
19
 
20
20
  gem.add_runtime_dependency 'dnssd'
21
- gem.add_runtime_dependency 'eventmachine'
22
21
  gem.add_runtime_dependency 'json'
23
22
  gem.add_runtime_dependency 'osc-ruby'
24
23
  gem.add_runtime_dependency 'trollop'
25
24
 
26
- gem.add_development_dependency "rspec"
25
+ gem.add_development_dependency "rspec", '~> 2.10.0'
27
26
  gem.add_development_dependency "cucumber"
28
27
  gem.add_development_dependency "aruba"
29
- gem.add_development_dependency 'ruby-debug'
30
28
  end
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # ruby builtin
4
+ require 'readline'
5
+
6
+ require 'rubygems'
7
+
8
+ # use Qcmd's parser for type conversions and double quote recognizing
9
+ require 'qcmd/parser'
10
+
11
+ # other gems
12
+ require 'osc-ruby'
13
+ require 'json'
14
+
15
+ # handle Ctrl-C quitting
16
+ trap("INT") { exit }
17
+
18
+ # if there are args, there must be two:
19
+ #
20
+ # send_address:send_port
21
+ #
22
+ # and
23
+ #
24
+ # receive_port
25
+
26
+ # default qlab send port 53000
27
+ send_address = 'localhost'
28
+ send_port = 53000
29
+
30
+ # default qlab receive port 53001
31
+ receive_port = 53001
32
+
33
+ if ARGV.size > 0
34
+ send_matcher = /([^:]+):(\d+)/
35
+ recv_matcher = /(\d+)/
36
+
37
+ if send_matcher =~ ARGV[0]
38
+ send_address, send_port = $1, $2
39
+ elsif recv_matcher =~ ARGV[0]
40
+ receive_port = $1
41
+ else
42
+ puts 'send address must be an address in the form SERVER_ADDRESS:PORT'
43
+ end
44
+
45
+ if ARGV[1]
46
+ if recv_matcher =~ ARGV[1]
47
+ receive_port = $1
48
+ else
49
+ puts 'send address must be a port number'
50
+ end
51
+ end
52
+ end
53
+
54
+ puts %[connecting to server #{send_address}:#{send_port} with receiver at port #{receive_port}]
55
+
56
+ # how long to wait for responses from QLab. If you notice responses coming in
57
+ # out of order, you may need to increase this value.
58
+ REPLY_TIMEOUT = 1
59
+
60
+ # open IO pipes to communicate between client / server process
61
+ response_receiver, writer = IO.pipe
62
+
63
+ # fork readline process to allow server to communicate because if we use
64
+ # Thread.new, readline locks the WHOLE Ruby VM and the server can't start
65
+ pid = fork do
66
+ # close the IO channel that server process will be using
67
+ writer.close
68
+
69
+ # native OSC connection, outbound
70
+ client = OSC::Client.new 'localhost', send_port
71
+
72
+ loop do
73
+ command_string = Readline.readline('> ', true)
74
+ next if command_string.nil? || command_string.strip.size == 0
75
+
76
+ # break command string up and properly typecast all given values
77
+ args = Qcmd::Parser.parse(command_string)
78
+ address = args.shift
79
+
80
+ # quit, q, and exit all quit
81
+ exit if /^(q(uit)?|exit)/i =~ address
82
+
83
+ # "sanitize" the given address
84
+ if %r[^/] != address
85
+ if address == '>'
86
+ # pasted previous command line entry
87
+ address = args.shift
88
+ else
89
+ # add lazy slash
90
+ address = "/#{ address }"
91
+ end
92
+ end
93
+
94
+ message = OSC::Message.new(address, *args)
95
+ client.send message
96
+
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
+ end
118
+ end
119
+
120
+ puts "launched console with process id #{ pid }, use Ctrl-c or 'exit' to quit"
121
+
122
+ # close unused pipe
123
+ response_receiver.close
124
+
125
+ # native OSC connection, inbound
126
+ server = OSC::Server.new receive_port
127
+
128
+ # server listens and forwards responses to the forked process
129
+ server.add_method %r[/reply] do |osc_message|
130
+ data = JSON.parse(osc_message.to_a.first)['data']
131
+
132
+ begin
133
+ writer.puts JSON.pretty_generate(data)
134
+ rescue JSON::GeneratorError
135
+ writer.puts data.to_s
136
+ end
137
+
138
+ # end of signal
139
+ writer.puts '<<EOS>>'
140
+ end
141
+
142
+ # start blocking server
143
+ Thread.new do
144
+ server.run
145
+ end
146
+
147
+ # chill until the command line process quits
148
+ Process.wait pid
@@ -0,0 +1,2 @@
1
+ require 'osc-ruby'
2
+ require 'rspec'
File without changes
File without changes
@@ -0,0 +1,78 @@
1
+ require File.join( File.dirname(__FILE__) , '..', 'spec_helper' )
2
+ require 'qcmd'
3
+ require 'socket'
4
+
5
+ class PortFactory
6
+ @@counter = 12345
7
+
8
+ def self.new_port
9
+ @@counter += 1
10
+ @@counter
11
+ end
12
+ end
13
+
14
+ describe OSC::StoppingServer do
15
+ before :each do
16
+ @port = PortFactory.new_port
17
+ end
18
+
19
+ it "should bind to a socket when initialized" do
20
+ UDPSocket.any_instance.should_receive(:bind).with('', @port)
21
+ server = OSC::StoppingServer.new @port
22
+ end
23
+
24
+ it 'should start a listening thread when started' do
25
+ server = OSC::StoppingServer.new @port
26
+
27
+ test_thread = Thread.new do
28
+ Thread.should_receive :fork
29
+ server.run
30
+ end
31
+
32
+ server.stop
33
+ end
34
+
35
+ it 'should kill the listening thread and close socket when stopped' do
36
+ server = OSC::StoppingServer.new @port
37
+
38
+ test_thread = Thread.new do
39
+ server.run
40
+ end
41
+
42
+ sleep 0.1
43
+ server.stop
44
+ sleep 0.1
45
+
46
+ # server has stopped blocking
47
+ test_thread.alive?.should == false
48
+
49
+ # server claims it is closed
50
+ server.state.should == :stopped
51
+ end
52
+
53
+ it 'should create messages for legitimate OSC commands' do
54
+ server = OSC::StoppingServer.new @port
55
+
56
+ received = nil
57
+
58
+ server.add_method '/test' do |message|
59
+ received = message
60
+ end
61
+
62
+ test_thread = Thread.new do
63
+ server.run
64
+ end
65
+
66
+ received.should == nil
67
+
68
+ client = OSC::Client.new 'localhost', @port
69
+ client.send OSC::Message.new('/test', 'ansible')
70
+
71
+ sleep 0.1
72
+ server.stop
73
+
74
+ received.is_a?(OSC::Message).should == true
75
+ received.to_a.first.should == 'ansible'
76
+ end
77
+ end
78
+
File without changes
@@ -3,8 +3,7 @@ require 'qcmd'
3
3
  describe Qcmd do
4
4
  # tests go here
5
5
  it "should log debug messages when in verbose mode" do
6
- Qcmd.should_receive(:log, 'hello')
7
-
6
+ Qcmd.should_receive(:log).with('hello')
8
7
  Qcmd.verbose!
9
8
  Qcmd.log_level.should eql(:debug)
10
9
  Qcmd.debug 'hello'
@@ -16,4 +15,17 @@ describe Qcmd do
16
15
  Qcmd.log_level.should eql(:warning)
17
16
  Qcmd.debug 'hello'
18
17
  end
18
+
19
+ it 'should not log debug messages when in quiet block' do
20
+ Qcmd.verbose!
21
+ Qcmd.log_level.should eql(:debug)
22
+
23
+ Qcmd.while_quiet do
24
+ Qcmd.should_not_receive(:log)
25
+ Qcmd.log_level.should eql(:warning)
26
+ Qcmd.debug 'hello'
27
+ end
28
+
29
+ Qcmd.log_level.should eql(:debug)
30
+ end
19
31
  end
File without changes
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qcmd
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 21
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 6
10
- version: 0.1.6
9
+ - 7
10
+ version: 0.1.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - Adam Bachman
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-11-21 00:00:00 -05:00
18
+ date: 2012-11-24 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -33,7 +33,7 @@ dependencies:
33
33
  type: :runtime
34
34
  version_requirements: *id001
35
35
  - !ruby/object:Gem::Dependency
36
- name: eventmachine
36
+ name: json
37
37
  prerelease: false
38
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
39
  none: false
@@ -47,7 +47,7 @@ dependencies:
47
47
  type: :runtime
48
48
  version_requirements: *id002
49
49
  - !ruby/object:Gem::Dependency
50
- name: json
50
+ name: osc-ruby
51
51
  prerelease: false
52
52
  requirement: &id003 !ruby/object:Gem::Requirement
53
53
  none: false
@@ -61,7 +61,7 @@ dependencies:
61
61
  type: :runtime
62
62
  version_requirements: *id003
63
63
  - !ruby/object:Gem::Dependency
64
- name: osc-ruby
64
+ name: trollop
65
65
  prerelease: false
66
66
  requirement: &id004 !ruby/object:Gem::Requirement
67
67
  none: false
@@ -75,21 +75,23 @@ dependencies:
75
75
  type: :runtime
76
76
  version_requirements: *id004
77
77
  - !ruby/object:Gem::Dependency
78
- name: trollop
78
+ name: rspec
79
79
  prerelease: false
80
80
  requirement: &id005 !ruby/object:Gem::Requirement
81
81
  none: false
82
82
  requirements:
83
- - - ">="
83
+ - - ~>
84
84
  - !ruby/object:Gem::Version
85
- hash: 3
85
+ hash: 39
86
86
  segments:
87
+ - 2
88
+ - 10
87
89
  - 0
88
- version: "0"
89
- type: :runtime
90
+ version: 2.10.0
91
+ type: :development
90
92
  version_requirements: *id005
91
93
  - !ruby/object:Gem::Dependency
92
- name: rspec
94
+ name: cucumber
93
95
  prerelease: false
94
96
  requirement: &id006 !ruby/object:Gem::Requirement
95
97
  none: false
@@ -103,7 +105,7 @@ dependencies:
103
105
  type: :development
104
106
  version_requirements: *id006
105
107
  - !ruby/object:Gem::Dependency
106
- name: cucumber
108
+ name: aruba
107
109
  prerelease: false
108
110
  requirement: &id007 !ruby/object:Gem::Requirement
109
111
  none: false
@@ -116,34 +118,6 @@ dependencies:
116
118
  version: "0"
117
119
  type: :development
118
120
  version_requirements: *id007
119
- - !ruby/object:Gem::Dependency
120
- name: aruba
121
- prerelease: false
122
- requirement: &id008 !ruby/object:Gem::Requirement
123
- none: false
124
- requirements:
125
- - - ">="
126
- - !ruby/object:Gem::Version
127
- hash: 3
128
- segments:
129
- - 0
130
- version: "0"
131
- type: :development
132
- version_requirements: *id008
133
- - !ruby/object:Gem::Dependency
134
- name: ruby-debug
135
- prerelease: false
136
- requirement: &id009 !ruby/object:Gem::Requirement
137
- none: false
138
- requirements:
139
- - - ">="
140
- - !ruby/object:Gem::Version
141
- hash: 3
142
- segments:
143
- - 0
144
- version: "0"
145
- type: :development
146
- version_requirements: *id009
147
121
  description: A simple interactive QLab 3 command line controller
148
122
  email:
149
123
  - adam.bachman@gmail.com
@@ -168,10 +142,12 @@ files:
168
142
  - lib/qcmd/context.rb
169
143
  - lib/qcmd/core_ext/array.rb
170
144
  - lib/qcmd/core_ext/osc/message.rb
145
+ - lib/qcmd/core_ext/osc/stopping_server.rb
171
146
  - lib/qcmd/handler.rb
172
147
  - lib/qcmd/input_completer.rb
173
148
  - lib/qcmd/machine.rb
174
149
  - lib/qcmd/network.rb
150
+ - lib/qcmd/osc.rb
175
151
  - lib/qcmd/parser.rb
176
152
  - lib/qcmd/plaintext.rb
177
153
  - lib/qcmd/qlab.rb
@@ -182,11 +158,14 @@ files:
182
158
  - lib/qcmd/version.rb
183
159
  - qcmd.gemspec
184
160
  - sample/dnssd.rb
185
- - spec/cli_spec.rb
186
- - spec/commands_spec.rb
187
- - spec/parser_spec.rb
188
- - spec/qcmd_spec.rb
189
- - spec/qlab_spec.rb
161
+ - sample/simple_console.rb
162
+ - spec/spec_helper.rb
163
+ - spec/unit/cli_spec.rb
164
+ - spec/unit/commands_spec.rb
165
+ - spec/unit/osc_server_spec.rb
166
+ - spec/unit/parser_spec.rb
167
+ - spec/unit/qcmd_spec.rb
168
+ - spec/unit/qlab_spec.rb
190
169
  has_rdoc: true
191
170
  homepage: https://github.com/abachman/qcmd
192
171
  licenses: []
@@ -223,8 +202,10 @@ specification_version: 3
223
202
  summary: QLab 3 console
224
203
  test_files:
225
204
  - features/support/setup.rb
226
- - spec/cli_spec.rb
227
- - spec/commands_spec.rb
228
- - spec/parser_spec.rb
229
- - spec/qcmd_spec.rb
230
- - spec/qlab_spec.rb
205
+ - spec/spec_helper.rb
206
+ - spec/unit/cli_spec.rb
207
+ - spec/unit/commands_spec.rb
208
+ - spec/unit/osc_server_spec.rb
209
+ - spec/unit/parser_spec.rb
210
+ - spec/unit/qcmd_spec.rb
211
+ - spec/unit/qlab_spec.rb