adhearsion 2.0.0.alpha3 → 2.0.0.beta1

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.
Files changed (34) hide show
  1. data/CHANGELOG.md +13 -1
  2. data/README.markdown +1 -1
  3. data/features/cli_daemon.feature +2 -3
  4. data/features/step_definitions/cli_steps.rb +0 -1
  5. data/lib/adhearsion/call.rb +50 -32
  6. data/lib/adhearsion/call_controller.rb +53 -4
  7. data/lib/adhearsion/call_controller/dial.rb +26 -6
  8. data/lib/adhearsion/call_controller/input.rb +1 -1
  9. data/lib/adhearsion/call_controller/menu.rb +2 -2
  10. data/lib/adhearsion/call_controller/output.rb +11 -11
  11. data/lib/adhearsion/call_controller/utility.rb +3 -3
  12. data/lib/adhearsion/calls.rb +0 -4
  13. data/lib/adhearsion/cli_commands.rb +2 -3
  14. data/lib/adhearsion/console.rb +42 -14
  15. data/lib/adhearsion/foundation/thread_safety.rb +8 -2
  16. data/lib/adhearsion/generators/app/templates/Rakefile +1 -4
  17. data/lib/adhearsion/initializer.rb +12 -5
  18. data/lib/adhearsion/logging.rb +23 -4
  19. data/lib/adhearsion/process.rb +11 -2
  20. data/lib/adhearsion/punchblock_plugin.rb +8 -0
  21. data/lib/adhearsion/punchblock_plugin/initializer.rb +11 -4
  22. data/lib/adhearsion/version.rb +1 -1
  23. data/spec/adhearsion/call_controller/dial_spec.rb +123 -85
  24. data/spec/adhearsion/call_controller/output_spec.rb +10 -0
  25. data/spec/adhearsion/call_controller_spec.rb +59 -52
  26. data/spec/adhearsion/call_spec.rb +47 -3
  27. data/spec/adhearsion/console_spec.rb +10 -1
  28. data/spec/adhearsion/initializer_spec.rb +5 -5
  29. data/spec/adhearsion/logging_spec.rb +17 -1
  30. data/spec/adhearsion/process_spec.rb +18 -0
  31. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +20 -8
  32. data/spec/adhearsion/punchblock_plugin_spec.rb +42 -0
  33. data/spec/spec_helper.rb +5 -1
  34. metadata +64 -64
@@ -1,3 +1,14 @@
1
+ # 2.0.0.beta1 - 2012-03-07
2
+ * Bugfix: #speak now correctly casts the argument to string if it is not SSML
3
+ * Bugfix: The console pauses controllers on a call while taking control
4
+ * Feature: Reopen logfiles on SIGHUP
5
+ * Feature: Toggle :trace logging on SIGALRM (useful for debugging a live process)
6
+ * Feature: It is now possible to execute a global component (using `Adhearsion::PunchblockPlugin.execute_component`)
7
+ * Feature: Now set XMPP JID resource to a concatenation of hostname and process ID for ID/debugging purposes
8
+ * Feature: CallController#dial now returns a DialStatus object indicating the status of the dial command
9
+ * Feature: Punchblock plugin can now configure the active media engine (mostly for use on Asterisk)
10
+ * Bugfix: Fix forcing Adhearsion to stop with enough SIGTERM or CTRL+C
11
+
1
12
  # 2.0.0.alpha3 - 2012-02-21
2
13
  * Feature: Add `ahn generate` command to allow invocation of generators
3
14
  * Feature: Add simple generator for call controllers
@@ -10,11 +21,12 @@
10
21
  * Feature: The console can take control of a call
11
22
  * Bugfix: CallController#dial now blocks until all outbound calls complete
12
23
  * Bugfix: Call commands timing out now raise a timeout exception in the caller, but do not crash the actor
24
+ * Feature: It is now possible to pause/resume call controllers
13
25
  * Bugfix: CallController#dial now unblocks immediately if the original call ends
14
26
  * Bugfix: CallController#dial now unblocks when the connected outbound call unjoins, rather than ending, incase post-processing on the outbound call is required
15
27
  * Bugfix: CallController#dial now hangs up outbound legs when it unblocks
16
28
  * Feature: CallController#dial now defaults the outbound caller ID to that of the controller's call
17
- * Change: The command to take control of a call is now 'take' rather than 'use'. 'take' called without a call ID present a list of currently running calls
29
+ * Change: The command to take control of a call is now 'take' rather than 'use'. 'take' called without a call ID present a list of currently running calls
18
30
 
19
31
  # 2.0.0.alpha2 - 2012-01-30
20
32
  * Change: Plugins no longer load dialplan/event/rpc/console methods using corresponding class methods
@@ -20,7 +20,7 @@ Features
20
20
  Requirements
21
21
  ------------
22
22
 
23
- * Ruby 1.9.2+ or JRuby 1.6.5+
23
+ * Ruby 1.9.2+ or JRuby 1.6.7+
24
24
  * A VoIP platform:
25
25
  * Asterisk 1.8+
26
26
  * Prism 11+ with rayo-server
@@ -9,8 +9,7 @@ Feature: Adhearsion Ahn CLI (daemon)
9
9
  When I run `ahn daemon path/somewhere`
10
10
  And I cd to "path/somewhere"
11
11
  And I terminate the process using the pid file "adhearsion.pid"
12
- Then the output should contain "Daemonizing now"
13
- And the exit status should be 0
12
+ Then the exit status should be 0
14
13
 
15
14
  Scenario: Command daemon with pid option
16
15
  Given JRuby skip test
@@ -18,4 +17,4 @@ Feature: Adhearsion Ahn CLI (daemon)
18
17
  When I run `ahn daemon path/somewhere --pid-file=ahn.pid`
19
18
  And I cd to "path/somewhere"
20
19
  And I terminate the process using the pid file "ahn.pid"
21
- Then the output should contain "Daemonizing now"
20
+ Then the exit status should be 0
@@ -69,6 +69,5 @@ When /^I terminate the process using the pid file "([^"]*)"$/ do |pidfile|
69
69
  pid = File.read(pidfile).to_i
70
70
  Process.kill("TERM", pid)
71
71
  sleep 1
72
- Process.kill("KILL", pid)
73
72
  end
74
73
  end
@@ -1,33 +1,27 @@
1
1
  require 'thread'
2
2
 
3
- module Celluloid
4
- module ClassMethods
5
- def ===(other)
6
- other.kind_of? self
7
- end
8
- end
9
-
10
- class ActorProxy
11
- def is_a?(klass)
12
- Actor.call @mailbox, :is_a?, klass
13
- end
14
-
15
- def kind_of?(klass)
16
- Actor.call @mailbox, :kind_of?, klass
17
- end
18
- end
19
- end
20
-
21
3
  module Adhearsion
22
4
  ##
23
5
  # Encapsulates call-related data and behavior.
24
6
  #
25
7
  class Call
26
8
 
9
+ ExpiredError = Class.new Celluloid::DeadActorError
10
+
27
11
  include Celluloid
28
12
  include HasGuardedHandlers
29
13
 
30
- attr_accessor :offer, :client, :end_reason, :commands, :variables
14
+ def self.new(*args, &block)
15
+ super.tap do |proxy|
16
+ def proxy.method_missing(*args)
17
+ super
18
+ rescue Celluloid::DeadActorError => e
19
+ raise ExpiredError, "This call is expired and is no longer accessible"
20
+ end
21
+ end
22
+ end
23
+
24
+ attr_accessor :offer, :client, :end_reason, :commands, :variables, :controllers
31
25
 
32
26
  delegate :[], :[]=, :to => :variables
33
27
  delegate :to, :from, :to => :offer, :allow_nil => true
@@ -35,9 +29,10 @@ module Adhearsion
35
29
  def initialize(offer = nil)
36
30
  register_initial_handlers
37
31
 
38
- @tags = []
39
- @commands = CommandRegistry.new
40
- @variables = {}
32
+ @tags = []
33
+ @commands = CommandRegistry.new
34
+ @variables = {}
35
+ @controllers = []
41
36
 
42
37
  self << offer if offer
43
38
  end
@@ -77,7 +72,7 @@ module Adhearsion
77
72
 
78
73
  alias << deliver_message
79
74
 
80
- def register_initial_handlers
75
+ def register_initial_handlers # :nodoc:
81
76
  register_event_handler Punchblock::Event::Offer do |offer|
82
77
  @offer = offer
83
78
  @client = offer.client
@@ -93,10 +88,14 @@ module Adhearsion
93
88
  clear_from_active_calls
94
89
  @end_reason = event.reason
95
90
  commands.terminate
96
- after(5) { current_actor.terminate! }
91
+ after(after_end_hold_time) { current_actor.terminate! }
97
92
  end
98
93
  end
99
94
 
95
+ def after_end_hold_time # :nodoc:
96
+ 30
97
+ end
98
+
100
99
  def on_end(&block)
101
100
  register_event_handler Punchblock::Event::End do |event|
102
101
  block.call event
@@ -126,7 +125,7 @@ module Adhearsion
126
125
  write_and_await_response Punchblock::Command::Hangup.new(:headers => headers)
127
126
  end
128
127
 
129
- def clear_from_active_calls
128
+ def clear_from_active_calls # :nodoc:
130
129
  Adhearsion.active_calls.remove_inactive_call current_actor
131
130
  end
132
131
 
@@ -180,17 +179,17 @@ module Adhearsion
180
179
  end
181
180
 
182
181
  def write_command(command)
183
- abort Hangup.new unless active? || command.is_a?(Punchblock::Command::Hangup)
182
+ abort Hangup.new(@end_reason) unless active? || command.is_a?(Punchblock::Command::Hangup)
184
183
  variables.merge! command.headers_hash if command.respond_to? :headers_hash
185
- logger.trace "Executing command #{command.inspect}"
184
+ logger.debug "Executing command #{command.inspect}"
186
185
  client.execute_command command, :call_id => id
187
186
  end
188
187
 
189
- def logger_id
188
+ def logger_id # :nodoc:
190
189
  "#{self.class}: #{id}"
191
190
  end
192
191
 
193
- def logger
192
+ def logger # :nodoc:
194
193
  super
195
194
  end
196
195
 
@@ -198,8 +197,15 @@ module Adhearsion
198
197
  [current_actor]
199
198
  end
200
199
 
200
+ def inspect
201
+ attrs = [:offer, :end_reason, :commands, :variables, :controllers, :to, :from].map do |attr|
202
+ "#{attr}=#{send(attr).inspect}"
203
+ end
204
+ "#<#{self.class}:#{id} #{attrs.join ', '}>"
205
+ end
206
+
201
207
  def execute_controller(controller, latch = nil)
202
- Adhearsion::Process.important_threads << Thread.new do
208
+ Thread.new do
203
209
  catching_standard_errors do
204
210
  begin
205
211
  CallController.exec controller
@@ -208,10 +214,22 @@ module Adhearsion
208
214
  end
209
215
  latch.countdown! if latch
210
216
  end
211
- end
217
+ end.tap { |t| Adhearsion::Process.important_threads << t }
218
+ end
219
+
220
+ def register_controller(controller)
221
+ @controllers << controller
222
+ end
223
+
224
+ def pause_controllers
225
+ controllers.each &:pause!
226
+ end
227
+
228
+ def resume_controllers
229
+ controllers.each &:resume!
212
230
  end
213
231
 
214
- class CommandRegistry < ThreadSafeArray
232
+ class CommandRegistry < ThreadSafeArray # :nodoc:
215
233
  def terminate
216
234
  hangup = Hangup.new
217
235
  each { |command| command.response = hangup if command.requested? }
@@ -54,13 +54,13 @@ module Adhearsion
54
54
 
55
55
  delegate :[], :[]=, :to => :@metadata
56
56
  delegate :variables, :logger, :to => :call
57
- delegate :write_and_await_response, :answer, :reject, :mute, :unmute, :join, :to => :call
58
57
 
59
58
  def initialize(call, metadata = nil, &block)
60
59
  @call, @metadata, @block = call, metadata || {}, block
61
60
  end
62
61
 
63
- def execute!(*options)
62
+ def execute!(*options) # :nodoc:
63
+ call.register_controller! self
64
64
  execute_callbacks :before_call
65
65
  run
66
66
  rescue Hangup
@@ -84,7 +84,7 @@ module Adhearsion
84
84
  throw :pass_controller, controller_class.new(call, metadata)
85
85
  end
86
86
 
87
- def execute_callbacks(type)
87
+ def execute_callbacks(type) # :nodoc:
88
88
  self.class.callbacks[type].each do |callback|
89
89
  catching_standard_errors do
90
90
  instance_exec &callback
@@ -92,15 +92,21 @@ module Adhearsion
92
92
  end
93
93
  end
94
94
 
95
- def after_call
95
+ def after_call # :nodoc:
96
96
  @after_call ||= execute_callbacks :after_call
97
97
  end
98
98
 
99
99
  def hangup(headers = nil)
100
+ block_until_resumed
100
101
  hangup_response = call.hangup headers
101
102
  after_call unless hangup_response == false
102
103
  end
103
104
 
105
+ def write_and_await_response(command)
106
+ block_until_resumed
107
+ call.write_and_await_response command
108
+ end
109
+
104
110
  def execute_component_and_await_completion(component)
105
111
  write_and_await_response component
106
112
 
@@ -110,5 +116,48 @@ module Adhearsion
110
116
  raise StandardError, complete_event.reason.details if complete_event.reason.is_a? Punchblock::Event::Complete::Error
111
117
  component
112
118
  end
119
+
120
+ def answer(*args)
121
+ block_until_resumed
122
+ call.answer *args
123
+ end
124
+
125
+ def reject(*args)
126
+ block_until_resumed
127
+ call.reject *args
128
+ end
129
+
130
+ def mute(*args)
131
+ block_until_resumed
132
+ call.mute *args
133
+ end
134
+
135
+ def unmute(*args)
136
+ block_until_resumed
137
+ call.unmute *args
138
+ end
139
+
140
+ def join(*args)
141
+ block_until_resumed
142
+ call.join *args
143
+ end
144
+
145
+ def block_until_resumed # :nodoc:
146
+ @pause_latch && @pause_latch.wait
147
+ end
148
+
149
+ def pause! # :nodoc:
150
+ @pause_latch = CountDownLatch.new 1
151
+ end
152
+
153
+ def resume! # :nodoc:
154
+ return unless @pause_latch
155
+ @pause_latch.countdown!
156
+ @pause_latch = nil
157
+ end
158
+
159
+ def inspect
160
+ "#<#{self.class} call=#{call.id}, metadata=#{metadata.inspect}>"
161
+ end
113
162
  end#class
114
163
  end
@@ -32,6 +32,7 @@ module Adhearsion
32
32
  #
33
33
  def dial(to, options = {}, latch = nil)
34
34
  targets = Array(to)
35
+ status = DialStatus.new
35
36
 
36
37
  latch ||= CountDownLatch.new targets.size
37
38
 
@@ -43,7 +44,7 @@ module Adhearsion
43
44
  options[:from] ||= call.from
44
45
 
45
46
  calls = targets.map do |target|
46
- new_call = OutboundCall.new options
47
+ new_call = OutboundCall.new
47
48
 
48
49
  new_call.on_answer do |event|
49
50
  calls.each do |call_to_hangup, target|
@@ -57,17 +58,18 @@ module Adhearsion
57
58
  end
58
59
 
59
60
  new_call.register_event_handler Punchblock::Event::Unjoined, :other_call_id => call.id do |event|
60
- new_call[:"dial_countdown_#{call.id}"] = true
61
+ new_call["dial_countdown_#{call.id}"] = true
61
62
  latch.countdown!
62
63
  throw :pass
63
64
  end
64
65
 
65
66
  logger.debug "Joining call #{new_call.id} to #{call.id} due to a #dial"
66
67
  new_call.join call
68
+ status.answer!
67
69
  end
68
70
 
69
71
  new_call.on_end do |event|
70
- latch.countdown! unless new_call[:"dial_countdown_#{call.id}"]
72
+ latch.countdown! unless new_call["dial_countdown_#{call.id}"]
71
73
  end
72
74
 
73
75
  [new_call, target]
@@ -78,7 +80,10 @@ module Adhearsion
78
80
  call
79
81
  end
80
82
 
81
- timeout = latch.wait options[:timeout]
83
+ status.calls = calls
84
+
85
+ no_timeout = latch.wait options[:timeout]
86
+ status.timeout! unless no_timeout
82
87
 
83
88
  logger.debug "#dial finished. Hanging up #{calls.size} outbound calls #{calls.inspect}."
84
89
  calls.each do |outbound_call|
@@ -90,9 +95,24 @@ module Adhearsion
90
95
  end
91
96
  end
92
97
 
93
- return timeout unless timeout
98
+ status
99
+ end
100
+
101
+ class DialStatus
102
+ attr_accessor :calls
103
+ attr_reader :result
104
+
105
+ def initialize
106
+ @result = :no_answer
107
+ end
108
+
109
+ def answer!
110
+ @result = :answer
111
+ end
94
112
 
95
- calls.size == 1 ? calls.first : calls
113
+ def timeout!
114
+ @result = :timeout
115
+ end
96
116
  end
97
117
 
98
118
  end#module Dial
@@ -7,7 +7,7 @@ module Adhearsion
7
7
  # @param [Integer] the timeout to wait before returning, in seconds. nil or -1 mean no timeout.
8
8
  # @return [String|nil] the pressed key, or nil if timeout was reached.
9
9
  #
10
- def wait_for_digit(timeout = 1)
10
+ def wait_for_digit(timeout = 1) # :nodoc:
11
11
  timeout = nil if timeout == -1
12
12
  timeout *= 1_000 if timeout
13
13
  input_component = execute_component_and_await_completion ::Punchblock::Component::Input.new :mode => :dtmf,
@@ -103,7 +103,7 @@ module Adhearsion
103
103
  return :done
104
104
  end
105
105
 
106
- def play_sound_files_for_menu(menu_instance, sound_files)
106
+ def play_sound_files_for_menu(menu_instance, sound_files) # :nodoc:
107
107
  digit = nil
108
108
  if sound_files.any? && menu_instance.digit_buffer_empty?
109
109
  digit = interruptible_play *sound_files
@@ -111,7 +111,7 @@ module Adhearsion
111
111
  digit || wait_for_digit(menu_instance.timeout)
112
112
  end
113
113
 
114
- def jump_to(match_object, overrides = nil)
114
+ def jump_to(match_object, overrides = nil) # :nodoc:
115
115
  if match_object.block
116
116
  instance_exec overrides[:extension], &match_object.block
117
117
  else
@@ -2,7 +2,7 @@ module Adhearsion
2
2
  class CallController
3
3
  module Output
4
4
  def speak(text, options = {})
5
- play_ssml(text, options) || output(:text, text, options)
5
+ play_ssml(text, options) || output(:text, text.to_s, options)
6
6
  end
7
7
 
8
8
  #
@@ -110,18 +110,18 @@ module Adhearsion
110
110
  play_ssml ssml_for_audio(argument, options)
111
111
  end
112
112
 
113
- def play_ssml(ssml, options = {})
113
+ def play_ssml(ssml, options = {}) # :nodoc:
114
114
  if [RubySpeech::SSML::Speak, Nokogiri::XML::Document].include? ssml.class
115
115
  output :ssml, ssml.to_s, options
116
116
  end
117
117
  end
118
118
 
119
- def output(type, content, options = {})
119
+ def output(type, content, options = {}) # :nodoc:
120
120
  options.merge! type => content
121
121
  execute_component_and_await_completion ::Punchblock::Component::Output.new(options)
122
122
  end
123
123
 
124
- def output!(type, content, options = {})
124
+ def output!(type, content, options = {}) # :nodoc:
125
125
  options.merge! type => content
126
126
  execute_component_and_await_completion ::Punchblock::Component::Output.new(options)
127
127
  end
@@ -170,7 +170,7 @@ module Adhearsion
170
170
  result
171
171
  end
172
172
 
173
- def detect_type(output)
173
+ def detect_type(output) # :nodoc:
174
174
  result = nil
175
175
  result = :time if [Date, Time, DateTime].include? output.class
176
176
  result = :numeric if output.kind_of?(Numeric) || output =~ /^\d+$/
@@ -178,7 +178,7 @@ module Adhearsion
178
178
  result ||= :text
179
179
  end
180
180
 
181
- def play_ssml_for(*args)
181
+ def play_ssml_for(*args) # :nodoc:
182
182
  play_ssml ssml_for(args)
183
183
  end
184
184
 
@@ -189,7 +189,7 @@ module Adhearsion
189
189
  # @param [String|Hash|RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
190
190
  # @return [RubySpeech::SSML::Speak] an SSML document
191
191
  #
192
- def ssml_for(*args)
192
+ def ssml_for(*args) # :nodoc:
193
193
  return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
194
194
  argument, options = args.flatten
195
195
  options ||= {}
@@ -197,11 +197,11 @@ module Adhearsion
197
197
  send "ssml_for_#{type}", argument, options
198
198
  end
199
199
 
200
- def ssml_for_text(argument, options = {})
200
+ def ssml_for_text(argument, options = {}) # :nodoc:
201
201
  RubySpeech::SSML.draw { argument }
202
202
  end
203
203
 
204
- def ssml_for_time(argument, options = {})
204
+ def ssml_for_time(argument, options = {}) # :nodoc:
205
205
  interpretation = case argument
206
206
  when Date then 'date'
207
207
  when Time then 'time'
@@ -217,13 +217,13 @@ module Adhearsion
217
217
  end
218
218
  end
219
219
 
220
- def ssml_for_numeric(argument, options = {})
220
+ def ssml_for_numeric(argument, options = {}) # :nodoc:
221
221
  RubySpeech::SSML.draw do
222
222
  say_as(:interpret_as => 'cardinal') { argument.to_s }
223
223
  end
224
224
  end
225
225
 
226
- def ssml_for_audio(argument, options = {})
226
+ def ssml_for_audio(argument, options = {}) # :nodoc:
227
227
  fallback = (options || {}).delete :fallback
228
228
  RubySpeech::SSML.draw do
229
229
  audio(:src => argument) { fallback }