adhearsion 1.0.3 → 1.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/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ 1.1.0
2
+ - Added interactive call control console: ahn start console <path>
3
+ - Added centralized exception handler through eventing system
4
+ - Support for using ahn_hoptoad to send Adhearsion exceptions to Hoptoad
5
+ - Adhearsion.active_calls can now use hash syntax to find calls by ID
6
+ - Added Adhearsion::Calls#to_h
7
+ - Add a Monitor to synchronize access to an AGI connection
8
+
1
9
  1.0.3
2
10
  - Fix the play() command regression when passing an array of strings. This was breaking the SimonGame
3
11
  - Deprecate ManagerInterface#send_action_asynchronously
@@ -21,7 +21,6 @@ Gem::Specification.new do |s|
21
21
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
22
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
23
  s.require_paths = ["lib"]
24
- s.has_rdoc = true
25
24
 
26
25
  if s.respond_to? :specification_version then
27
26
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -38,6 +37,7 @@ Gem::Specification.new do |s|
38
37
  s.add_runtime_dependency("i18n")
39
38
  s.add_runtime_dependency("rubigen", [">= 1.5.6"])
40
39
  s.add_runtime_dependency("rake")
40
+ s.add_runtime_dependency("pry")
41
41
 
42
42
  # Development dependencies
43
43
  s.add_development_dependency('rubigen', [">= 1.5.6"])
@@ -20,6 +20,7 @@
20
20
  ##
21
21
  # Here is a list of the events included by default:
22
22
  #
23
+ # - events.exception
23
24
  # - events.asterisk.manager_interface
24
25
  # - events.after_initialized
25
26
  # - events.shutdown
@@ -30,3 +31,9 @@
30
31
  #
31
32
  # Note: events are mostly for components to register and expose to you.
32
33
  ##
34
+
35
+ events.exception.each do |e|
36
+ ahn_log.error "#{e.class}: #{e.message}"
37
+ ahn_log.error e.backtrace.join("\n\t")
38
+ end
39
+
@@ -6,7 +6,7 @@ module Adhearsion
6
6
  USAGE = <<USAGE
7
7
  Usage:
8
8
  ahn create /path/to/directory
9
- ahn start [daemon] [/path/to/directory]
9
+ ahn start [console|daemon] [/path/to/directory]
10
10
  ahn version|-v|--v|-version|--version
11
11
  ahn help|-h|--h|--help|-help
12
12
 
@@ -39,23 +39,27 @@ USAGE
39
39
  when 'start'
40
40
  pid_file_regexp = /^--pid-file=(.+)$/
41
41
  if args.size > 3
42
- fail_and_print_usage "Too many arguments supplied!" if args.size > 3
42
+ raise CommandHandler::CLIException, "Too many arguments supplied!" if args.size > 3
43
43
  elsif args.size == 3
44
- fail_and_print_usage "Unrecognized final argument #{args.last}" unless args.last =~ pid_file_regexp
44
+ raise CommandHandler::CLIException, "Unrecognized final argument #{args.last}" unless args.last =~ pid_file_regexp
45
45
  pid_file = args.pop[pid_file_regexp, 1]
46
46
  else
47
47
  pid_file = nil
48
48
  end
49
49
 
50
- if args.first == 'daemon' && args.size == 2
50
+ if args.size == 2
51
51
  path = args.last
52
- daemon = true
52
+ if args.first =~ /daemon|console/
53
+ mode = args.first.to_sym
54
+ else
55
+ raise CommandHandler::CLIException, "Invalid start mode requested: #{args.first}"
56
+ end
53
57
  elsif args.size == 1
54
- path, daemon = args.first, false
58
+ path, mode = args.first, :foreground
55
59
  else
56
- fail_and_print_usage "Invalid format for the start CLI command!"
60
+ raise CommandHandler::CLIException, "Invalid format for the start CLI command!"
57
61
  end
58
- [:start, path, daemon, pid_file]
62
+ [:start, path, mode, pid_file]
59
63
  when '-'
60
64
  [:start, Dir.pwd]
61
65
  when "enable", "disable"
@@ -192,9 +196,9 @@ component_name.upcase = ComponentTester.new("#{component_name}", File.dirname(__
192
196
  end
193
197
  end
194
198
 
195
- def start(path, daemon=false, pid_file=nil)
199
+ def start(path, mode=:foreground, pid_file=nil)
196
200
  raise PathInvalid, path unless File.exists? path + "/.ahnrc"
197
- Adhearsion::Initializer.start path, :daemon => daemon, :pid_file => pid_file
201
+ Adhearsion::Initializer.start path, :mode => mode, :pid_file => pid_file
198
202
  end
199
203
 
200
204
  def version
@@ -134,6 +134,13 @@ module Adhearsion
134
134
  end
135
135
  end
136
136
  container
137
+ rescue StandardError => e
138
+ # Non-fatal errors
139
+ Events.trigger(['exception'], e)
140
+ rescue Exception => e
141
+ # Fatal errors. Log them and keep passing them upward
142
+ Events.trigger(['exception'], e)
143
+ raise e
137
144
  end
138
145
 
139
146
  class ComponentDefinitionContainer < Module
@@ -0,0 +1,49 @@
1
+ require 'pry'
2
+
3
+ module Adhearsion
4
+ module Console
5
+ include Adhearsion
6
+
7
+ class << self
8
+ ##
9
+ # Start the Adhearsion console
10
+ #
11
+ def run
12
+ Pry.prompt = [ proc {|obj, nest_level| "AHN#{' ' * nest_level}> " },
13
+ proc {|obj, nest_level| "AHN#{' ' * nest_level}? " } ]
14
+ pry
15
+ end
16
+
17
+ def logger
18
+ Adhearsion::Logging
19
+ end
20
+
21
+ def calls
22
+ Adhearsion.active_calls
23
+ end
24
+
25
+ def use(call)
26
+ unless call.is_a? Adhearsion::Call
27
+ raise ArgumentError unless Adhearsion.active_calls[call]
28
+ call = Adhearsion.active_calls[call]
29
+ end
30
+ Pry.prompt = [ proc { "AHN<#{call.channel}> "},
31
+ proc { "AHN<#{call.channel}? "} ]
32
+
33
+ # Pause execution of the thread currently controlling the call
34
+ call.with_command_lock do
35
+ CallWrapper.new(call).pry
36
+ end
37
+ end
38
+ end
39
+
40
+ class CallWrapper
41
+ attr_accessor :call
42
+
43
+ def initialize(call)
44
+ @call = call
45
+ extend Adhearsion::VoIP::Commands.for('asterisk')
46
+ end
47
+ end
48
+ end
49
+ end
@@ -26,6 +26,7 @@ module Adhearsion
26
26
  DEFAULT_FRAMEWORK_EVENT_NAMESPACES = %w[
27
27
  /after_initialized
28
28
  /shutdown
29
+ /exception
29
30
  /asterisk/manager_interface
30
31
  /asterisk/before_call
31
32
  /asterisk/after_call
@@ -125,7 +125,7 @@ module Adhearsion
125
125
  def initialize(path=nil, options={})
126
126
  @@started = true
127
127
  @path = path
128
- @daemon = options[:daemon] || ENV['DAEMON']
128
+ @mode = options[:mode]
129
129
  @pid_file = options[:pid_file].nil? ? ENV['PID_FILE'] : options[:pid_file]
130
130
  @loaded_init_files = options[:loaded_init_files]
131
131
  self.class.ahn_root = path
@@ -137,6 +137,7 @@ module Adhearsion
137
137
  resolve_pid_file_path
138
138
  resolve_log_file_path
139
139
  daemonize! if should_daemonize?
140
+ launch_console if need_console?
140
141
  switch_to_root_directory
141
142
  catch_termination_signal
142
143
  create_pid_file if pid_file
@@ -168,7 +169,7 @@ module Adhearsion
168
169
  elsif pid_file then pid_file
169
170
  elsif pid_file.equal?(false) then nil
170
171
  # FIXME @pid_file = @daemon? Assignment or equality? I'm assuming equality.
171
- else @pid_file = @daemon ? default_pid_path : nil
172
+ else @pid_file = (@mode == :daemon) ? default_pid_path : nil
172
173
  end
173
174
  end
174
175
 
@@ -315,7 +316,11 @@ Adhearsion will abort until you fix this. Sorry for the incovenience.
315
316
  end
316
317
 
317
318
  def should_daemonize?
318
- @daemon
319
+ @mode == :daemon
320
+ end
321
+
322
+ def need_console?
323
+ @mode == :console
319
324
  end
320
325
 
321
326
  def daemonize!
@@ -324,6 +329,20 @@ Adhearsion will abort until you fix this. Sorry for the incovenience.
324
329
  daemonize log_file
325
330
  end
326
331
 
332
+ def launch_console
333
+ require 'adhearsion/console'
334
+ Thread.new do
335
+ begin
336
+ puts "Starting console"
337
+ Adhearsion::Console.run
338
+ Adhearsion.shutdown!
339
+ rescue Exception => e
340
+ puts e.message
341
+ puts e.backtrace.join("\n")
342
+ end
343
+ end
344
+ end
345
+
327
346
  def initialize_log_file
328
347
  Dir.mkdir(ahn_app_log_directory) unless File.directory? ahn_app_log_directory
329
348
  file_logger = Log4r::FileOutputter.new("Main Adhearsion log file", :filename => log_file, :trunc => false)
@@ -382,7 +401,7 @@ Adhearsion will abort until you fix this. Sorry for the incovenience.
382
401
  begin
383
402
  IMPORTANT_THREADS[index].join
384
403
  rescue => e
385
- ahn_log.error "Error after join()ing Thread #{thread.inspect}. #{e.message}"
404
+ ahn_log.error "Error after join()ing Thread #{Thread.inspect}. #{e.message}"
386
405
  ensure
387
406
  index = index + 1
388
407
  end
@@ -24,12 +24,15 @@ module Adhearsion
24
24
  end
25
25
  end
26
26
  end
27
+ alias :level= :logging_level=
27
28
 
28
- def logging_level
29
+ def logging_level(level = nil)
30
+ return self.logging_level= level unless level.nil?
29
31
  @@logging_level_lock.synchronize do
30
32
  return @@logging_level ||= Log4r::INFO
31
33
  end
32
34
  end
35
+ alias :level :logging_level
33
36
  end
34
37
 
35
38
  class AdhearsionLogger < Log4r::Logger
@@ -1,8 +1,8 @@
1
1
  module Adhearsion #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1 unless defined? MAJOR
4
- MINOR = 0 unless defined? MINOR
5
- TINY = 3 unless defined? TINY
4
+ MINOR = 1 unless defined? MINOR
5
+ TINY = 0 unless defined? TINY
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.') unless defined? STRING
8
8
  end
@@ -13,13 +13,16 @@ module Adhearsion #:nodoc:
13
13
  attr_reader :major, :minor, :revision
14
14
 
15
15
  def initialize(version="")
16
- @major, @minor, @revision = version.split(".").map(&:to_i)
16
+ version = "" if version.nil?
17
+ @major, @minor, @revision, @patchlevel = version.split(".", 4).map(&:to_i)
18
+ @major = 0 unless @major
17
19
  end
18
20
 
19
21
  def <=>(other)
20
22
  return @major <=> other.major if ((@major <=> other.major) != 0)
21
23
  return @minor <=> other.minor if ((@minor <=> other.minor) != 0)
22
24
  return @revision <=> other.revision if ((@revision <=> other.revision) != 0)
25
+ return 0
23
26
  end
24
27
 
25
28
  def self.sort
@@ -59,9 +59,8 @@ module Adhearsion
59
59
  ahn_log.agi "Ignoring meta-AGI request"
60
60
  call.hangup!
61
61
  # TBD: (may have more hooks than what Jay has defined in hooks.rb)
62
- rescue => e
63
- ahn_log.agi.error "#{e.class}: #{e.message}"
64
- ahn_log.agi.error e.backtrace.join("\n\t")
62
+ rescue SyntaxError, StandardError => e
63
+ Events.trigger(['exception'], e)
65
64
  ensure
66
65
  Adhearsion.remove_inactive_call call rescue nil
67
66
  end
@@ -94,10 +94,12 @@ module Adhearsion
94
94
  #
95
95
  # @see http://www.voip-info.org/wiki/view/Asterisk+FastAGI More information about FAGI
96
96
  def raw_response(message = nil)
97
- raise ArgumentError.new("illegal NUL in message #{message.inspect}") if message =~ /\0/
98
- ahn_log.agi.debug ">>> #{message}"
99
- write message if message
100
- read
97
+ @call.with_command_lock do
98
+ raise ArgumentError.new("illegal NUL in message #{message.inspect}") if message =~ /\0/
99
+ ahn_log.agi.debug ">>> #{message}"
100
+ write message if message
101
+ read
102
+ end
101
103
  end
102
104
 
103
105
  def response(command, *arguments)
@@ -39,8 +39,10 @@ module Adhearsion
39
39
  private
40
40
 
41
41
  def read_configuration
42
- normalized_file = self.class.normalize_configuration execute(read_command)
43
- normalized_file.split(/^\[([-_\w]+)\]$/)[1..-1].each_slice(2).map do |(name,properties)|
42
+ normalized_file = self.class.normalize_configuration File.open(@filename, 'r'){|f| f.read}
43
+ sections = normalized_file.split(/^\[([-_\w]+)\]$/)[1..-1]
44
+ return [] if sections.nil?
45
+ sections.each_slice(2).map do |(name,properties)|
44
46
  [name, hash_from_properties(properties)]
45
47
  end
46
48
  end
@@ -53,15 +55,6 @@ module Adhearsion
53
55
  property_hash
54
56
  end
55
57
  end
56
-
57
- def execute(command)
58
- %x[command]
59
- end
60
-
61
- def read_command
62
- "cat #{filename}"
63
- end
64
-
65
58
  end
66
59
  end
67
60
  end
@@ -458,7 +458,13 @@ WARN
458
458
  end
459
459
 
460
460
  def start_actions_writer_loop
461
- @actions_writer_thread = Thread.new(&method(:actions_writer_loop))
461
+ @actions_writer_thread = Thread.new do
462
+ begin
463
+ actions_writer_loop
464
+ rescue => e
465
+ Events.trigger(['exception'], e)
466
+ end
467
+ end
462
468
  end
463
469
 
464
470
  def stop_actions_writer_loop
@@ -24,6 +24,8 @@ module Adhearsion
24
24
  ##
25
25
  # This manages the list of calls the Adhearsion service receives
26
26
  class Calls
27
+ attr_reader :semaphore, :calls
28
+
27
29
  def initialize
28
30
  @semaphore = Monitor.new
29
31
  @calls = {}
@@ -60,6 +62,7 @@ module Adhearsion
60
62
  return calls[id]
61
63
  end
62
64
  end
65
+ alias :[] :find
63
66
 
64
67
  def clear!
65
68
  atomically do
@@ -75,16 +78,23 @@ module Adhearsion
75
78
  end
76
79
  end
77
80
 
81
+ def each
82
+ calls.each_pair{|id, call| yield id, call }
83
+ end
84
+
78
85
  def to_a
79
86
  calls.values
80
87
  end
81
88
 
89
+ def to_h
90
+ calls
91
+ end
92
+
82
93
  private
83
- attr_reader :semaphore, :calls
84
94
 
85
- def atomically(&block)
86
- semaphore.synchronize(&block)
87
- end
95
+ def atomically(&block)
96
+ semaphore.synchronize(&block)
97
+ end
88
98
 
89
99
  end
90
100
 
@@ -259,6 +269,15 @@ module Adhearsion
259
269
  @hungup_call
260
270
  end
261
271
 
272
+ # Lock the socket for a command. Can be used to allow the console to take
273
+ # control of the thread in between AGI commands coming from the dialplan.
274
+ def with_command_lock
275
+ @command_monitor ||= Monitor.new
276
+ @command_monitor.synchronize do
277
+ yield
278
+ end
279
+ end
280
+
262
281
  # Adhearsion indexes calls by this identifier so they may later be found and manipulated. For calls from Asterisk, this
263
282
  # method uses the following properties for uniqueness, falling back to the next if one is for some reason unavailable:
264
283
  #
@@ -130,19 +130,14 @@ module Theatre
130
130
 
131
131
  protected
132
132
 
133
- # This will use the Adhearsion logger eventually.
134
- def warn(exception)
135
- # STDERR.puts exception.message, *exception.backtrace
136
- end
137
-
138
133
  def thread_loop
139
134
  loop do
140
135
  begin
141
136
  next_invocation = @master_queue.pop
142
137
  return :stopped if next_invocation.equal? :THEATRE_SHUTDOWN!
143
138
  next_invocation.start
144
- rescue => error
145
- warn error
139
+ rescue Exception => error
140
+ Adhearsion::Events.trigger(['exception'], error)
146
141
  end
147
142
  end
148
143
  end