larynx 0.1.0
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/MIT-LICENSE +20 -0
- data/README.rdoc +191 -0
- data/Rakefile +55 -0
- data/bin/larynx +7 -0
- data/examples/guess.rb +39 -0
- data/examples/guess_form.rb +34 -0
- data/examples/multiple_apps.rb +63 -0
- data/lib/larynx/application.rb +24 -0
- data/lib/larynx/call_handler.rb +174 -0
- data/lib/larynx/callbacks.rb +32 -0
- data/lib/larynx/command.rb +73 -0
- data/lib/larynx/commands.rb +88 -0
- data/lib/larynx/fields.rb +143 -0
- data/lib/larynx/form.rb +19 -0
- data/lib/larynx/logger.rb +8 -0
- data/lib/larynx/observable.rb +35 -0
- data/lib/larynx/prompt.rb +88 -0
- data/lib/larynx/response.rb +57 -0
- data/lib/larynx/restartable_timer.rb +26 -0
- data/lib/larynx/session.rb +20 -0
- data/lib/larynx/version.rb +3 -0
- data/lib/larynx.rb +109 -0
- data/spec/fixtures/answer.rb +125 -0
- data/spec/fixtures/channel_data.rb +147 -0
- data/spec/fixtures/dtmf.rb +52 -0
- data/spec/fixtures/execute.rb +52 -0
- data/spec/fixtures/execute_complete.rb +133 -0
- data/spec/fixtures/reply_ok.rb +6 -0
- data/spec/larynx/call_handler_spec.rb +290 -0
- data/spec/larynx/command_spec.rb +76 -0
- data/spec/larynx/eventmachince_spec.rb +14 -0
- data/spec/larynx/fields_spec.rb +194 -0
- data/spec/larynx/prompt_spec.rb +222 -0
- data/spec/larynx_spec.rb +4 -0
- data/spec/spec_helper.rb +47 -0
- metadata +96 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
module Larynx
|
2
|
+
class Command
|
3
|
+
include Callbacks
|
4
|
+
attr_reader :command
|
5
|
+
|
6
|
+
define_callback :before, :after
|
7
|
+
|
8
|
+
def initialize(command, params=nil, &block)
|
9
|
+
@command, @params, @callbacks = command, params, {}
|
10
|
+
after(&block) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
@command
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@command
|
19
|
+
end
|
20
|
+
|
21
|
+
def interruptable?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class CallCommand < Command
|
27
|
+
def name
|
28
|
+
"#{@command}#{" #{@params}" if @params}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
cmd = "#{@command}"
|
33
|
+
cmd << " #{@params}" if @params
|
34
|
+
cmd << "\n\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ApiCommand < Command
|
39
|
+
def name
|
40
|
+
"#{@command}#{" #{@params}" if @params}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
cmd = "api #{@command}"
|
45
|
+
cmd << " #{@params}" if @params
|
46
|
+
cmd << "\n\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class AppCommand < Command
|
51
|
+
def initialize(command, params=nil, options={}, &block)
|
52
|
+
super command, params, &block
|
53
|
+
@options = options.reverse_merge(:bargein => true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def name
|
57
|
+
"#{@command}#{" '#{@params}'" if @params}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
cmd = "sendmsg\n"
|
62
|
+
cmd << "call-command: execute\n"
|
63
|
+
cmd << "execute-app-name: #{@command}\n"
|
64
|
+
cmd << "execute-app-arg: #{@params}\n" if @params
|
65
|
+
cmd << "event-lock: #{@options[:lock]}\n" if @options[:lock]
|
66
|
+
cmd << "\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
def interruptable?
|
70
|
+
@options[:bargein]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Larynx
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
def connect(&block)
|
5
|
+
execute CallCommand.new('connect', &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def myevents(&block)
|
9
|
+
execute CallCommand.new('myevents', &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter(type, &block)
|
13
|
+
execute CallCommand.new('filter', type, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def linger(&block)
|
17
|
+
execute CallCommand.new('linger', &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def answer(&block)
|
21
|
+
execute AppCommand.new('answer', &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def hangup(&block)
|
25
|
+
execute AppCommand.new('hangup', &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def playback(data, options={}, &block)
|
29
|
+
execute AppCommand.new('playback', data, options, &block)
|
30
|
+
end
|
31
|
+
alias_method :play, :playback
|
32
|
+
|
33
|
+
def speak(data, options={}, &block)
|
34
|
+
execute AppCommand.new('speak', data, options, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def phrase(data, options={}, &block)
|
38
|
+
execute AppCommand.new('phrase', data, options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Executes read command with some default values.
|
43
|
+
# Allows length option which expands into minimum and maximum length values. Length can be a range.
|
44
|
+
# Passes user input into callback block.
|
45
|
+
#
|
46
|
+
# Defaults:
|
47
|
+
# timeout: 5000 or 5 seconds
|
48
|
+
# termchar: #
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
#
|
52
|
+
# read(:minimum => 1, :maximum => 2, :sound_file => 'en/us/callie/conference/8000/conf-pin.wav') {|input|
|
53
|
+
# speak "You entered #{input}"
|
54
|
+
# }
|
55
|
+
#
|
56
|
+
# Or
|
57
|
+
#
|
58
|
+
# read(:length => 1..2, :sound_file => 'en/us/callie/conference/8000/conf-pin.wav') {|input|
|
59
|
+
# speak "You entered #{input}"
|
60
|
+
# }
|
61
|
+
#
|
62
|
+
def read(options={}, &block)
|
63
|
+
options.reverse_merge!(:timeout => 5000, :var_name => 'read_result', :termchar => '#')
|
64
|
+
options[:bargein] = false
|
65
|
+
|
66
|
+
if length = options.delete(:length)
|
67
|
+
values = length.is_a?(Range) ? [length.first, length.last] : [length, length]
|
68
|
+
options.merge!(:minimum => values[0], :maximum => values[1])
|
69
|
+
end
|
70
|
+
|
71
|
+
order = [:minimum, :maximum, :sound_file, :var_name, :timeout, :termchar]
|
72
|
+
data = order.inject('') {|data, key| data += " #{options[key]}"; data }.strip
|
73
|
+
|
74
|
+
execute AppCommand.new('read', data, options).after {
|
75
|
+
block.call(response.body[:variable_read_result])
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def prompt(options={}, &block)
|
80
|
+
execute Prompt.new(self, options, &block).command
|
81
|
+
end
|
82
|
+
|
83
|
+
def break!
|
84
|
+
execute AppCommand.new('break'), true
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Larynx
|
2
|
+
class NoPromptDefined < StandardError; end
|
3
|
+
|
4
|
+
module Fields
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
attr_accessor :fields
|
13
|
+
|
14
|
+
def field(name, options={}, &block)
|
15
|
+
@fields ||= []
|
16
|
+
@fields << Field.new(name, options, &block)
|
17
|
+
attr_accessor name
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
|
24
|
+
def next_field(field_name=nil)
|
25
|
+
@current_field ||= 0
|
26
|
+
@current_field = index_of_field(field_name) if field_name
|
27
|
+
if field = self.class.fields[@current_field]
|
28
|
+
field.run(self)
|
29
|
+
@current_field += 1
|
30
|
+
field
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def index_of_field(name)
|
35
|
+
field = self.class.fields.find {|f| f.name == name }
|
36
|
+
self.class.fields.index(field)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class Field
|
42
|
+
include Callbacks
|
43
|
+
|
44
|
+
attr_reader :name
|
45
|
+
define_callback :setup, :validate, :invalid, :success, :failure
|
46
|
+
|
47
|
+
def initialize(name, options, &block)
|
48
|
+
@name, @callbacks = name, {}
|
49
|
+
@options = options.reverse_merge(:attempts => 3)
|
50
|
+
@prompt_queue = []
|
51
|
+
|
52
|
+
instance_eval(&block)
|
53
|
+
raise(Larynx::NoPromptDefined, 'A field requires a prompt to be defined') if @prompt_queue.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def prompt(options)
|
57
|
+
add_prompt(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def reprompt(options)
|
61
|
+
raise 'A reprompt can only be used after a prompt' if @prompt_queue.empty?
|
62
|
+
add_prompt(options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_prompt(options)
|
66
|
+
repeats = options.delete(:repeats) || 1
|
67
|
+
options.merge!(@options.slice(:length, :min_length, :max_length))
|
68
|
+
@prompt_queue += ([options] * repeats)
|
69
|
+
end
|
70
|
+
|
71
|
+
def current_prompt
|
72
|
+
options = (@prompt_queue[@attempt-1] || @prompt_queue.last).dup
|
73
|
+
method = ([:play, :speak, :phrase] & options.keys).first
|
74
|
+
message = options[method].is_a?(Symbol) ? @app.send(options[method]) : options[method]
|
75
|
+
options[method] = message
|
76
|
+
|
77
|
+
Prompt.new(call, options) {|input|
|
78
|
+
set_instance_variables(input)
|
79
|
+
evaluate_input
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def execute_prompt
|
84
|
+
call.execute current_prompt.command
|
85
|
+
end
|
86
|
+
|
87
|
+
def increment_attempts
|
88
|
+
@attempt += 1
|
89
|
+
end
|
90
|
+
|
91
|
+
def fire_callback(callback)
|
92
|
+
if block = @callbacks[callback]
|
93
|
+
@app.instance_eval(&block)
|
94
|
+
else
|
95
|
+
true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def valid?
|
100
|
+
@value.size >= minimum_length && fire_callback(:validate)
|
101
|
+
end
|
102
|
+
|
103
|
+
def evaluate_input
|
104
|
+
if valid?
|
105
|
+
fire_callback(:success)
|
106
|
+
else
|
107
|
+
fire_callback(:invalid)
|
108
|
+
if @attempt < @options[:attempts]
|
109
|
+
increment_attempts
|
110
|
+
execute_prompt
|
111
|
+
else
|
112
|
+
fire_callback(:failure)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def set_instance_variables(input)
|
118
|
+
@value = input
|
119
|
+
@app.send("#{@name}=", input)
|
120
|
+
end
|
121
|
+
|
122
|
+
def maximum_length
|
123
|
+
@options[:max_length] || @options[:length]
|
124
|
+
end
|
125
|
+
|
126
|
+
def minimum_length
|
127
|
+
@options[:min_length] || @options[:length] || 1
|
128
|
+
end
|
129
|
+
|
130
|
+
def run(app)
|
131
|
+
@app = app
|
132
|
+
@attempt = 1
|
133
|
+
fire_callback(:setup)
|
134
|
+
execute_prompt
|
135
|
+
end
|
136
|
+
|
137
|
+
def call
|
138
|
+
@app.call
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
data/lib/larynx/form.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Larynx
|
2
|
+
class Form < Application
|
3
|
+
include Fields
|
4
|
+
|
5
|
+
def self.setup(&block)
|
6
|
+
@@setup = block
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
instance_eval &@@setup if @@setup
|
11
|
+
next_field
|
12
|
+
end
|
13
|
+
|
14
|
+
def restart_form
|
15
|
+
@current_field = 0
|
16
|
+
run
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Larynx
|
2
|
+
module Observable
|
3
|
+
|
4
|
+
def add_observer(object)
|
5
|
+
@observers ||= []
|
6
|
+
@observers << object
|
7
|
+
end
|
8
|
+
|
9
|
+
def remove_observer(object)
|
10
|
+
@observers && @observers.delete(object)
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear_observers!
|
14
|
+
@observers = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Like an observer stack which only notifies top observer
|
18
|
+
def notify_current_observer(event, data=nil)
|
19
|
+
return unless @observers
|
20
|
+
obs = @observers.last
|
21
|
+
if obs.respond_to?(event)
|
22
|
+
data ? obs.send(event, data) : obs.send(event)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def notify_observers(event, data=nil)
|
27
|
+
return unless @observers
|
28
|
+
@observers.each do |obs|
|
29
|
+
next unless obs.respond_to?(event)
|
30
|
+
data ? obs.send(event, data) : obs.send(event)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Larynx
|
2
|
+
class NoPromptCommandValue < StandardError; end
|
3
|
+
|
4
|
+
# The prompt class neatly wraps up a convention where you prompt for input of
|
5
|
+
# certain length. The prompt waits until the required input length is reached,
|
6
|
+
# the user presses the terminator button or the time runs out. Think of the
|
7
|
+
# play_and_get_digits command except it works for speak as well. It also
|
8
|
+
# provides a bargein option to allow or prevent the user from interrupting
|
9
|
+
# the speech or playback.
|
10
|
+
#
|
11
|
+
# Pass a block to the method as a callback which receives input as an argument.
|
12
|
+
#
|
13
|
+
class Prompt
|
14
|
+
attr_reader :call
|
15
|
+
|
16
|
+
def initialize(call, options, &block)
|
17
|
+
@call, @options, @block = call, options, block
|
18
|
+
@options.reverse_merge!(:bargein => true, :timeout => 10, :interdigit_timeout => 3, :termchar => '#')
|
19
|
+
raise NoPromptCommandValue, "No output command value supplied. Use one of playback, speak or phrase keys." if command_name.blank?
|
20
|
+
end
|
21
|
+
|
22
|
+
def command
|
23
|
+
@command ||= AppCommand.new(command_name, message, :bargein => @options[:bargein]).
|
24
|
+
before { call.clear_input }.
|
25
|
+
after {
|
26
|
+
if prompt_finished?
|
27
|
+
finalise
|
28
|
+
else
|
29
|
+
call.add_observer self
|
30
|
+
add_digit_timer
|
31
|
+
add_input_timer
|
32
|
+
end
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def input
|
37
|
+
(call.input.last == termchar ? call.input[0..-2] : call.input).join
|
38
|
+
end
|
39
|
+
|
40
|
+
def prompt_finished?
|
41
|
+
call.input.last == termchar || call.input.size == maximum_length
|
42
|
+
end
|
43
|
+
|
44
|
+
def termchar
|
45
|
+
@options[:termchar]
|
46
|
+
end
|
47
|
+
|
48
|
+
def maximum_length
|
49
|
+
@options[:max_length] || @options[:length]
|
50
|
+
end
|
51
|
+
|
52
|
+
def command_name
|
53
|
+
([:play, :speak, :phrase] & @options.keys).first.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def message
|
57
|
+
@options[command_name.to_sym]
|
58
|
+
end
|
59
|
+
|
60
|
+
def finalise
|
61
|
+
call.remove_observer self
|
62
|
+
@block.call(input)
|
63
|
+
end
|
64
|
+
|
65
|
+
def dtmf_received(digit)
|
66
|
+
if prompt_finished?
|
67
|
+
call.stop_timer(:input)
|
68
|
+
call.cancel_timer(:digit)
|
69
|
+
else
|
70
|
+
call.restart_timer(:digit)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_digit_timer
|
75
|
+
call.timer(:digit, @options[:interdigit_timeout]) {
|
76
|
+
call.cancel_timer :input
|
77
|
+
finalise
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_input_timer
|
82
|
+
call.timer(:input, @options[:timeout]) {
|
83
|
+
call.cancel_timer :digit
|
84
|
+
finalise
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Larynx
|
2
|
+
class Response
|
3
|
+
attr_reader :header, :body
|
4
|
+
|
5
|
+
def initialize(header, body)
|
6
|
+
@header = CallHandler.headers_2_hash(header)
|
7
|
+
if body
|
8
|
+
@body = body.match(/:/) ? CallHandler.headers_2_hash(body) : body
|
9
|
+
@body.each {|k,v| v.chomp! if v.is_a?(String)}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def reply?
|
14
|
+
@header[:content_type] == 'command/reply'
|
15
|
+
end
|
16
|
+
|
17
|
+
def event?
|
18
|
+
@header[:content_type] == 'text/event-plain'
|
19
|
+
end
|
20
|
+
|
21
|
+
def ok?
|
22
|
+
@header[:reply_text] =~ /\+OK/
|
23
|
+
end
|
24
|
+
|
25
|
+
def error?
|
26
|
+
@header[:reply_text] =~ /ERR/
|
27
|
+
end
|
28
|
+
|
29
|
+
def executing?
|
30
|
+
event_name == 'CHANNEL_EXECUTE'
|
31
|
+
end
|
32
|
+
|
33
|
+
def executed?
|
34
|
+
event_name == 'CHANNEL_EXECUTE_COMPLETE'
|
35
|
+
end
|
36
|
+
|
37
|
+
def command_name
|
38
|
+
@body[:application] if @body
|
39
|
+
end
|
40
|
+
|
41
|
+
def event_name
|
42
|
+
@body[:event_name] if @body
|
43
|
+
end
|
44
|
+
|
45
|
+
def dtmf?
|
46
|
+
@body[:event_name] == 'DTMF' if @body
|
47
|
+
end
|
48
|
+
|
49
|
+
def speech?
|
50
|
+
@body[:event_name] == 'DETECTED_SPEECH' if @body
|
51
|
+
end
|
52
|
+
|
53
|
+
def disconnect?
|
54
|
+
@header[:content_type] == 'text/disconnect-notice'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Larynx
|
2
|
+
# Adds restart to EM timer class. Implementation influenced by EM::PeriodicTimer class
|
3
|
+
# so hopefully it should not cause any issues.
|
4
|
+
class RestartableTimer < EM::Timer
|
5
|
+
def initialize(interval, callback=nil, &block)
|
6
|
+
@interval = interval
|
7
|
+
@code = callback || block
|
8
|
+
schedule
|
9
|
+
end
|
10
|
+
|
11
|
+
# Restart the timer
|
12
|
+
def restart
|
13
|
+
cancel
|
14
|
+
schedule
|
15
|
+
end
|
16
|
+
|
17
|
+
def schedule
|
18
|
+
@signature = EM::add_timer(@interval, method(:fire))
|
19
|
+
end
|
20
|
+
|
21
|
+
def fire
|
22
|
+
@code.call
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Larynx
|
2
|
+
class Session
|
3
|
+
attr_reader :variables
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
@variables = data
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(method, *args, &block)
|
10
|
+
if @variables.has_key?(method.to_sym)
|
11
|
+
@variables[method.to_sym]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@variables[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/larynx.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'active_support'
|
4
|
+
require 'logger'
|
5
|
+
require 'daemons/daemonize'
|
6
|
+
|
7
|
+
require 'larynx/version'
|
8
|
+
require 'larynx/logger'
|
9
|
+
require 'larynx/observable'
|
10
|
+
require 'larynx/callbacks'
|
11
|
+
require 'larynx/session'
|
12
|
+
require 'larynx/response'
|
13
|
+
require 'larynx/command'
|
14
|
+
require 'larynx/commands'
|
15
|
+
require 'larynx/prompt'
|
16
|
+
require 'larynx/application'
|
17
|
+
require 'larynx/fields'
|
18
|
+
require 'larynx/form'
|
19
|
+
require 'larynx/restartable_timer'
|
20
|
+
require 'larynx/call_handler'
|
21
|
+
|
22
|
+
module Larynx
|
23
|
+
class << self
|
24
|
+
include Callbacks
|
25
|
+
|
26
|
+
define_callback :connect, :answer, :hungup
|
27
|
+
|
28
|
+
def parse_options(args=ARGV)
|
29
|
+
@options = {
|
30
|
+
:ip => "0.0.0.0",
|
31
|
+
:port => 8084,
|
32
|
+
:pid_file => './larynx.pid',
|
33
|
+
:log_file => './larynx.log'
|
34
|
+
}
|
35
|
+
opts = OptionParser.new
|
36
|
+
opts.banner = "Usage: larynx [options]"
|
37
|
+
opts.separator ''
|
38
|
+
opts.separator "Larynx is a tool to develop FreeSWITCH IVR applications in Ruby."
|
39
|
+
opts.on('-i', '--ip IP', 'Listen for connections on this IP') {|ip| @options[:ip] = ip }
|
40
|
+
opts.on('-p', '--port PORT', 'Listen on this port', Integer) {|port| @options[:port] = port }
|
41
|
+
opts.on('-d', '--daemonize', 'Run as daemon') { @options[:daemonize] = true }
|
42
|
+
opts.on('-l', '--log-file FILE', 'Defaults to /app/root/larynx.log') {|log| @options[:log_file] = log }
|
43
|
+
opts.on( '--pid-file FILE', 'Defaults to /app/root/larynx.pid') {|pid| @options[:pid_file] = pid }
|
44
|
+
opts.on('-h', '--help', 'This is it') { $stderr.puts opts; exit 0 }
|
45
|
+
opts.on('-v', '--version') { $stderr.puts "Larynx version #{Larynx::VERSION}"; exit 0 }
|
46
|
+
opts.parse!(args)
|
47
|
+
end
|
48
|
+
|
49
|
+
def setup_logger
|
50
|
+
logger = Larynx::Logger.new(@options[:log_file])
|
51
|
+
logger.level = Logger::INFO
|
52
|
+
Object.const_set "LARYNX_LOGGER", logger
|
53
|
+
end
|
54
|
+
|
55
|
+
def graceful_exit
|
56
|
+
LARYNX_LOGGER.info "Shutting down Larynx"
|
57
|
+
EM.stop_server @em_signature
|
58
|
+
@em_signature = nil
|
59
|
+
remove_pid_file if @options[:daemonize]
|
60
|
+
exit 130
|
61
|
+
end
|
62
|
+
|
63
|
+
def daemonize
|
64
|
+
Daemonize.daemonize
|
65
|
+
Dir.chdir LARYNX_ROOT
|
66
|
+
File.open(@options[:pid_file], 'w+') {|f| f.write("#{Process.pid}\n") }
|
67
|
+
end
|
68
|
+
|
69
|
+
def remove_pid_file
|
70
|
+
File.delete @options[:pid_file]
|
71
|
+
end
|
72
|
+
|
73
|
+
def trap_signals
|
74
|
+
trap('TERM') { graceful_exit }
|
75
|
+
trap('INT') { graceful_exit }
|
76
|
+
end
|
77
|
+
|
78
|
+
def setup_app
|
79
|
+
if ARGV[0].nil?
|
80
|
+
$stderr.puts "You must specify an application file"
|
81
|
+
exit -1
|
82
|
+
end
|
83
|
+
Object.const_set "LARYNX_ROOT", File.expand_path(File.dirname(ARGV[0]))
|
84
|
+
require File.expand_path(ARGV[0])
|
85
|
+
end
|
86
|
+
|
87
|
+
def start_server
|
88
|
+
LARYNX_LOGGER.info "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
|
89
|
+
EM::run {
|
90
|
+
@em_signature = EM::start_server @options[:ip], @options[:port], Larynx::CallHandler
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def run
|
95
|
+
parse_options(ARGV)
|
96
|
+
setup_app
|
97
|
+
daemonize if @options[:daemonize]
|
98
|
+
setup_logger
|
99
|
+
trap_signals
|
100
|
+
start_server
|
101
|
+
end
|
102
|
+
|
103
|
+
def running?
|
104
|
+
!@em_signature.nil?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
Larynx.run unless defined?(TEST)
|