mtrudel-adhearsion 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG +26 -0
  2. data/EVENTS +11 -0
  3. data/LICENSE +456 -0
  4. data/Rakefile +127 -0
  5. data/adhearsion.gemspec +149 -0
  6. data/app_generators/ahn/USAGE +5 -0
  7. data/app_generators/ahn/ahn_generator.rb +91 -0
  8. data/app_generators/ahn/templates/.ahnrc +34 -0
  9. data/app_generators/ahn/templates/README +8 -0
  10. data/app_generators/ahn/templates/Rakefile +25 -0
  11. data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
  12. data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
  13. data/app_generators/ahn/templates/components/disabled/restful_rpc/README.markdown +11 -0
  14. data/app_generators/ahn/templates/components/disabled/restful_rpc/example-client.rb +48 -0
  15. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +87 -0
  16. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.yml +34 -0
  17. data/app_generators/ahn/templates/components/disabled/restful_rpc/spec/restful_rpc_spec.rb +263 -0
  18. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +104 -0
  19. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +2 -0
  20. data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
  21. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
  22. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.yml +12 -0
  23. data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
  24. data/app_generators/ahn/templates/config/startup.rb +50 -0
  25. data/app_generators/ahn/templates/dialplan.rb +3 -0
  26. data/app_generators/ahn/templates/events.rb +32 -0
  27. data/bin/ahn +28 -0
  28. data/bin/ahnctl +68 -0
  29. data/bin/jahn +42 -0
  30. data/examples/asterisk_manager_interface/standalone.rb +51 -0
  31. data/lib/adhearsion.rb +37 -0
  32. data/lib/adhearsion/cli.rb +223 -0
  33. data/lib/adhearsion/component_manager.rb +207 -0
  34. data/lib/adhearsion/component_manager/component_tester.rb +55 -0
  35. data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
  36. data/lib/adhearsion/events_support.rb +84 -0
  37. data/lib/adhearsion/foundation/all.rb +9 -0
  38. data/lib/adhearsion/foundation/blank_slate.rb +5 -0
  39. data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
  40. data/lib/adhearsion/foundation/event_socket.rb +203 -0
  41. data/lib/adhearsion/foundation/future_resource.rb +36 -0
  42. data/lib/adhearsion/foundation/global.rb +1 -0
  43. data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
  44. data/lib/adhearsion/foundation/numeric.rb +13 -0
  45. data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
  46. data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
  47. data/lib/adhearsion/foundation/string.rb +26 -0
  48. data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
  49. data/lib/adhearsion/foundation/thread_safety.rb +7 -0
  50. data/lib/adhearsion/host_definitions.rb +67 -0
  51. data/lib/adhearsion/initializer.rb +373 -0
  52. data/lib/adhearsion/initializer/asterisk.rb +81 -0
  53. data/lib/adhearsion/initializer/configuration.rb +254 -0
  54. data/lib/adhearsion/initializer/database.rb +50 -0
  55. data/lib/adhearsion/initializer/drb.rb +31 -0
  56. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  57. data/lib/adhearsion/initializer/rails.rb +41 -0
  58. data/lib/adhearsion/logging.rb +92 -0
  59. data/lib/adhearsion/tasks.rb +16 -0
  60. data/lib/adhearsion/tasks/database.rb +5 -0
  61. data/lib/adhearsion/tasks/deprecations.rb +59 -0
  62. data/lib/adhearsion/tasks/generating.rb +20 -0
  63. data/lib/adhearsion/tasks/lint.rb +4 -0
  64. data/lib/adhearsion/tasks/testing.rb +37 -0
  65. data/lib/adhearsion/version.rb +9 -0
  66. data/lib/adhearsion/voip/asterisk.rb +4 -0
  67. data/lib/adhearsion/voip/asterisk/agi_server.rb +84 -0
  68. data/lib/adhearsion/voip/asterisk/commands.rb +1314 -0
  69. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  70. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  71. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  72. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  73. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  74. data/lib/adhearsion/voip/asterisk/manager_interface.rb +597 -0
  75. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1589 -0
  76. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
  77. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
  78. data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
  79. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  80. data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
  81. data/lib/adhearsion/voip/call.rb +453 -0
  82. data/lib/adhearsion/voip/call_routing.rb +64 -0
  83. data/lib/adhearsion/voip/commands.rb +9 -0
  84. data/lib/adhearsion/voip/constants.rb +39 -0
  85. data/lib/adhearsion/voip/conveniences.rb +18 -0
  86. data/lib/adhearsion/voip/dial_plan.rb +218 -0
  87. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  88. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  89. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  90. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  91. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  92. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  93. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  94. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  95. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  96. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  97. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  98. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  99. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  100. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  101. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  102. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  103. data/lib/theatre.rb +151 -0
  104. data/lib/theatre/README.markdown +64 -0
  105. data/lib/theatre/callback_definition_loader.rb +84 -0
  106. data/lib/theatre/guid.rb +23 -0
  107. data/lib/theatre/invocation.rb +121 -0
  108. data/lib/theatre/namespace_manager.rb +153 -0
  109. data/lib/theatre/version.rb +2 -0
  110. metadata +182 -0
@@ -0,0 +1,123 @@
1
+ require File.join(File.dirname(__FILE__), 'calculated_match')
2
+
3
+ module Adhearsion
4
+ module VoIP
5
+ class MatchCalculator
6
+
7
+ class << self
8
+
9
+ def build_with_pattern(pattern, match_payload, &block)
10
+ class_for_pattern_type(pattern.class.name).new(pattern, match_payload, &block)
11
+ end
12
+
13
+ def inherited(klass)
14
+ subclasses << klass
15
+ end
16
+
17
+ private
18
+
19
+ def class_for_pattern_type(pattern_type)
20
+ sought_class_name = "Adhearsion::VoIP::#{pattern_type.camelize}MatchCalculator"
21
+ subclasses.find { |klass| klass.name == sought_class_name }
22
+ end
23
+
24
+ def subclasses
25
+ @@subclasses ||= []
26
+ end
27
+
28
+ end
29
+
30
+ attr_reader :pattern, :match_payload
31
+ def initialize(pattern, match_payload)
32
+ @pattern, @match_payload = pattern, match_payload
33
+ end
34
+
35
+ protected
36
+
37
+ def new_calculated_match(options)
38
+ CalculatedMatch.new({:pattern => pattern, :match_payload => match_payload}.merge(options))
39
+ end
40
+
41
+ def coerce_to_numeric(victim)
42
+ victim.kind_of?(Numeric) ? victim : (victim.to_s =~ /^\d+$/ ? victim.to_s.to_i : nil )
43
+ end
44
+ end
45
+
46
+ class RangeMatchCalculator < MatchCalculator
47
+
48
+ def initialize(pattern, match_payload)
49
+ raise unless pattern.first.kind_of?(Numeric) && pattern.last.kind_of?(Numeric)
50
+ super
51
+ end
52
+
53
+ def match(query)
54
+ numerical_query = coerce_to_numeric query
55
+ if numerical_query
56
+ exact_match = pattern.include?(numerical_query) ? query : nil
57
+ potential_matches = numbers_in_range_like numerical_query
58
+ potential_matches.reject! { |m| m.to_s == exact_match.to_s } if exact_match
59
+
60
+ new_calculated_match :query => query, :exact_matches => exact_match,
61
+ :potential_matches => potential_matches
62
+ else
63
+ CalculatedMatch.failed_match!(pattern, query, match_payload)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Returns all numbers in the range (@pattern) that +begin with+ the number given
70
+ # as the first argument.
71
+ #
72
+ # NOTE! If you're having trouble reading what this method is actually doing, it's
73
+ # effectively a much more efficient version of this:
74
+ #
75
+ # pattern.to_a.select { |x| x.to_s.starts_with? num.to_s }.flatten
76
+ #
77
+ # Huge thanks to Dave Troy (http://davetroy.blogspot.com) for this awesomely
78
+ # efficient code!
79
+ def numbers_in_range_like(num)
80
+ return (pattern === 0 ? [0] : nil) if num == 0
81
+ raise ArgumentError unless num.kind_of?(Numeric)
82
+ returning Array.new do |matches|
83
+ first, last = pattern.first, pattern.last
84
+ power = 0
85
+ while num < last
86
+ ones_count = 10**power - 1
87
+ matches.concat((([num, first].max)..[(num+ones_count), last].min).to_a)
88
+ num *= 10
89
+ power += 1
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ class FixnumMatchCalculator < MatchCalculator
96
+ def match(query)
97
+ numeric_query = coerce_to_numeric query
98
+ exact_match, potential_match = nil
99
+ if pattern == numeric_query
100
+ exact_match = pattern
101
+ elsif pattern.to_s.starts_with? query.to_s
102
+ potential_match = pattern
103
+ end
104
+ new_calculated_match :query => query, :exact_matches => exact_match, :potential_matches => potential_match
105
+ end
106
+ end
107
+
108
+ class StringMatchCalculator < MatchCalculator
109
+ def match(query)
110
+ args = { :query => query, :exact_matches => nil,
111
+ :potential_matches => nil }
112
+
113
+ if pattern == query.to_s
114
+ args[:exact_matches] = [pattern]
115
+ elsif pattern.starts_with? query.to_s
116
+ args[:potential_matches] = [pattern]
117
+ end
118
+
119
+ new_calculated_match args
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), 'matchers.rb')
2
+
3
+ module Adhearsion
4
+ module VoIP
5
+ class MenuBuilder
6
+
7
+ def initialize
8
+ @patterns = []
9
+ @menu_callbacks = {}
10
+ end
11
+
12
+ def method_missing(match_payload, *patterns, &block)
13
+ name_string = match_payload.to_s
14
+ if patterns.any?
15
+ patterns.each do |pattern|
16
+ @patterns << MatchCalculator.build_with_pattern(pattern, match_payload)
17
+ end
18
+ else
19
+ raise ArgumentError, "You cannot call this method without patterns!"
20
+ end
21
+ nil
22
+ end
23
+
24
+ def weighted_match_calculators
25
+ @patterns
26
+ end
27
+
28
+ def execute_hook_for(symbol, input)
29
+ callback = @menu_callbacks[symbol]
30
+ callback.call input if callback
31
+ end
32
+
33
+ def on_invalid(&block)
34
+ raise LocalJumpError, "Must supply a block!" unless block_given?
35
+ @menu_callbacks[:invalid] = block
36
+ end
37
+
38
+ def on_premature_timeout(&block)
39
+ raise LocalJumpError, "Must supply a block!" unless block_given?
40
+ @menu_callbacks[:premature_timeout] = block
41
+ end
42
+
43
+ def on_failure(&block)
44
+ raise LocalJumpError, "Must supply a block!" unless block_given?
45
+ @menu_callbacks[:failure] = block
46
+ end
47
+
48
+ def calculate_matches_for(result)
49
+ returning CalculatedMatchCollection.new do |collection|
50
+ weighted_match_calculators.each do |pattern|
51
+ collection << pattern.match(result)
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,149 @@
1
+ require 'adhearsion/voip/menu_state_machine/menu_builder'
2
+ module Adhearsion
3
+ module VoIP
4
+ class Menu
5
+
6
+ DEFAULT_MAX_NUMBER_OF_TRIES = 1
7
+ DEFAULT_TIMEOUT = 5 # seconds
8
+
9
+ relationships :menu_builder => MenuBuilder
10
+
11
+ attr_reader :builder, :timeout, :tries_count, :max_number_of_tries
12
+ def initialize(options={}, &block)
13
+ @tries_count = 0 # Counts the number of tries the menu's been executed
14
+
15
+ @timeout = options[:timeout] || DEFAULT_TIMEOUT
16
+ @max_number_of_tries = options[:tries] || DEFAULT_MAX_NUMBER_OF_TRIES
17
+
18
+ @builder = menu_builder.new
19
+ yield @builder
20
+
21
+ initialize_digit_buffer
22
+ end
23
+
24
+ def <<(other)
25
+ digit_buffer << other
26
+ end
27
+
28
+ def digit_buffer
29
+ @digit_buffer
30
+ end
31
+
32
+ def digit_buffer_string
33
+ digit_buffer.to_s
34
+ end
35
+
36
+ def digit_buffer_empty?
37
+ digit_buffer.empty?
38
+ end
39
+
40
+ def continue
41
+ raise MenuGetAnotherDigitOrTimeout if digit_buffer_empty?
42
+
43
+ calculated_matches = builder.calculate_matches_for digit_buffer_string
44
+
45
+ if calculated_matches.exact_match_count >= 1
46
+ first_exact_match = calculated_matches.exact_matches.first
47
+ if calculated_matches.potential_match_count.zero?
48
+ # Match found with no extenuating ambiguities! Go with the first exact match
49
+ menu_result_found! first_exact_match.match_payload, digit_buffer_string
50
+ else
51
+ get_another_digit_or_finish!(first_exact_match.match_payload, first_exact_match.query)
52
+ end
53
+ elsif calculated_matches.potential_match_count >= 1
54
+ get_another_digit_or_timeout!
55
+ else
56
+ invalid!
57
+ end
58
+ end
59
+
60
+ def should_continue?
61
+ tries_count < max_number_of_tries
62
+ end
63
+
64
+ def restart!
65
+ @tries_count += 1
66
+ digit_buffer.clear!
67
+ end
68
+
69
+ def execute_invalid_hook
70
+ builder.execute_hook_for(:invalid, digit_buffer_string)
71
+ end
72
+
73
+ def execute_timeout_hook
74
+ builder.execute_hook_for(:premature_timeout, digit_buffer_string)
75
+ end
76
+
77
+ def execute_failure_hook
78
+ builder.execute_hook_for(:failure, digit_buffer_string)
79
+ end
80
+
81
+ protected
82
+
83
+ # If you're using a more complex class in subclasses, you may want to override this method in addition to the
84
+ # digit_buffer, digit_buffer_empty, and digit_buffer_string methods
85
+ def initialize_digit_buffer
86
+ @digit_buffer = ClearableStringBuffer.new
87
+ end
88
+
89
+ def invalid!
90
+ raise MenuResultInvalid
91
+ end
92
+
93
+ def menu_result_found!(match_payload, new_extension)
94
+ raise MenuResultFound.new(match_payload, new_extension)
95
+ end
96
+
97
+ def get_another_digit_or_finish!(match_payload, new_extension)
98
+ raise MenuGetAnotherDigitOrFinish.new(match_payload, new_extension)
99
+ end
100
+
101
+ def get_another_digit_or_timeout!
102
+ raise MenuGetAnotherDigitOrTimeout
103
+ end
104
+
105
+ # The superclass from which all message-like exceptions decend. It should never
106
+ # be instantiated directly.
107
+ class MenuResult < Exception; end
108
+
109
+ # Raised when the user's input matches
110
+ class MenuResultFound < MenuResult
111
+ attr_reader :match_payload, :new_extension
112
+ def initialize(match_payload, new_extension)
113
+ super()
114
+ @match_payload = match_payload
115
+ @new_extension = new_extension
116
+ end
117
+ end
118
+
119
+ module MenuGetAnotherDigit; end
120
+
121
+ class MenuGetAnotherDigitOrFinish < MenuResultFound
122
+ include MenuGetAnotherDigit
123
+ end
124
+
125
+ class MenuGetAnotherDigitOrTimeout < MenuResult
126
+ include MenuGetAnotherDigit
127
+ end
128
+
129
+ class MenuResultFound < MenuResult; end
130
+
131
+ # Raised when the user's input matches no patterns
132
+ class MenuResultInvalid < MenuResult; end
133
+
134
+ # For our default purposes, we need the digit_buffer to behave much like a normal String except that it should
135
+ # handle its own resetting (clearing).
136
+ class ClearableStringBuffer < String
137
+ def clear!
138
+ replace ""
139
+ end
140
+
141
+ def <<(other)
142
+ super other.to_s
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,151 @@
1
+ require 'thread'
2
+ require 'rubygems'
3
+
4
+ $: << File.expand_path(File.dirname(__FILE__))
5
+
6
+ require 'theatre/version'
7
+ require 'theatre/namespace_manager'
8
+ require 'theatre/invocation'
9
+ require 'theatre/callback_definition_loader'
10
+
11
+ module Theatre
12
+
13
+ class Theatre
14
+
15
+ attr_reader :namespace_manager
16
+
17
+ ##
18
+ # Creates a new stopped Theatre. You must call start!() after you instantiate this for it to begin processing events.
19
+ #
20
+ # @param [Fixnum] thread_count Number of Threads to spawn when started.
21
+ #
22
+ def initialize(thread_count=6)
23
+ @thread_count = thread_count
24
+ @started = false
25
+ @namespace_manager = ActorNamespaceManager.new
26
+ @thread_group = ThreadGroup.new
27
+ @master_queue = Queue.new
28
+ @loader_mixins = []
29
+ end
30
+
31
+ ##
32
+ # Send a message to this Theatre for asynchronous processing.
33
+ #
34
+ # @param [String] namespace The namespace to which the payload should be sent
35
+ # @param [Object] payload The actual content to be sent to the callback. Optional.
36
+ # @return [Array<Theatre::Invocation>] An Array of Invocation objects
37
+ # @raise Theatre::NamespaceNotFound Raised when told to enqueue an unrecognized namespace
38
+ #
39
+ def trigger(namespace, payload=:argument_undefined)
40
+ @namespace_manager.callbacks_for_namespaces(namespace).map do |callback|
41
+ invocation = if payload.equal?(:argument_undefined)
42
+ Invocation.new(namespace, callback)
43
+ else
44
+ Invocation.new(namespace, callback, payload)
45
+ end
46
+ invocation.queued
47
+ @master_queue << invocation
48
+ invocation
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Send a message to this Theatre for synchronous processing. The execution of this will not go through this Theatre's
54
+ # Thread pool. If an error occurred in any of callbacks, the Exception object will be placed in the returned Array
55
+ # instead for you to act upon.
56
+ #
57
+ # @param [String] namespace The namespace to which the payload should be sent
58
+ # @param [Object] payload The actual content to be sent to the callback. Optional.
59
+ # @return [Array] An Array containing each callback's return value (or Exception raised, if any) when given the payload
60
+ # @raise Theatre::NamespaceNotFound Raised when told to enqueue an unrecognized namespace
61
+ #
62
+ def trigger_immediately(namespace, payload=:argument_undefined)
63
+ @namespace_manager.callbacks_for_namespaces(namespace).map do |callback|
64
+ begin
65
+ invocation = if payload.equal?(:argument_undefined)
66
+ callback.call
67
+ else
68
+ callback.call payload
69
+ end
70
+ rescue => captured_error_to_be_returned
71
+ captured_error_to_be_returned
72
+ end
73
+ end
74
+ end
75
+
76
+ def load_events_code(code, *args)
77
+ loader = CallbackDefinitionLoader.new(self, *args)
78
+ loader.load_events_code code
79
+ end
80
+
81
+ def load_events_file(file, *args)
82
+ loader = CallbackDefinitionLoader.new(self, *args)
83
+ loader.load_events_file file
84
+ end
85
+
86
+ def register_namespace_name(*args)
87
+ @namespace_manager.register_namespace_name(*args)
88
+ end
89
+
90
+ def register_callback_at_namespace(*args)
91
+ @namespace_manager.register_callback_at_namespace(*args)
92
+ end
93
+
94
+ def register_loader_mixin(mod)
95
+ @loader_mixins << mod
96
+ end
97
+
98
+ def join
99
+ @thread_group.list.each do |thread|
100
+ begin
101
+ thread.join
102
+ rescue
103
+ # Ignore any exceptions
104
+ end
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Starts this Theatre.
110
+ #
111
+ # When this method is called, the Threads are spawned and begin pulling messages off this Theatre's master queue.
112
+ #
113
+ def start!
114
+ return false if @thread_group.list.any? # Already started
115
+ @started_time = Time.now
116
+ @thread_count.times do
117
+ @thread_group.add Thread.new(&method(:thread_loop))
118
+ end
119
+ end
120
+
121
+ ##
122
+ # Notifies all Threads for this Theatre to stop by sending them special messages. Any messages which were queued and
123
+ # untriggered when this method is received will still be processed. Note: you may start this Theatre again later once it
124
+ # has been stopped.
125
+ #
126
+ def graceful_stop!
127
+ @thread_count.times { @master_queue << :THEATRE_SHUTDOWN! }
128
+ @started_time = nil
129
+ end
130
+
131
+ protected
132
+
133
+ # This will use the Adhearsion logger eventually.
134
+ def warn(exception)
135
+ # STDERR.puts exception.message, *exception.backtrace
136
+ end
137
+
138
+ def thread_loop
139
+ loop do
140
+ begin
141
+ next_invocation = @master_queue.pop
142
+ return :stopped if next_invocation.equal? :THEATRE_SHUTDOWN!
143
+ next_invocation.start
144
+ rescue => error
145
+ warn error
146
+ end
147
+ end
148
+ end
149
+
150
+ end
151
+ end