adhearsion 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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