sevenscale-adhearsion 0.7.1003 → 0.8.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.
Files changed (149) hide show
  1. data/CHANGELOG +8 -2
  2. data/EVENTS +11 -0
  3. data/Rakefile +92 -26
  4. data/adhearsion.gemspec +131 -23
  5. data/app_generators/ahn/ahn_generator.rb +21 -7
  6. data/app_generators/ahn/templates/.ahnrc +10 -4
  7. data/app_generators/ahn/templates/Rakefile +7 -2
  8. data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
  9. data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
  10. data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
  11. data/app_generators/ahn/templates/components/disabled/stomp_gateway/config.yml +12 -0
  12. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
  13. data/app_generators/ahn/templates/components/simon_game/{lib/simon_game.rb → simon_game.rb} +14 -19
  14. data/app_generators/ahn/templates/config/startup.rb +3 -6
  15. data/app_generators/ahn/templates/dialplan.rb +2 -3
  16. data/app_generators/ahn/templates/events.rb +32 -6
  17. data/bin/jahn +10 -0
  18. data/examples/asterisk_manager_interface/standalone.rb +51 -0
  19. data/lib/adhearsion/cli.rb +140 -23
  20. data/lib/adhearsion/component_manager/component_tester.rb +55 -0
  21. data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
  22. data/lib/adhearsion/component_manager.rb +169 -238
  23. data/lib/adhearsion/events_support.rb +59 -237
  24. data/lib/adhearsion/{core_extensions → foundation}/all.rb +0 -0
  25. data/lib/adhearsion/{blank_slate.rb → foundation/blank_slate.rb} +0 -0
  26. data/lib/adhearsion/{core_extensions → foundation}/custom_daemonizer.rb +0 -0
  27. data/lib/adhearsion/foundation/event_socket.rb +203 -0
  28. data/lib/adhearsion/foundation/future_resource.rb +36 -0
  29. data/lib/adhearsion/{core_extensions → foundation}/global.rb +0 -0
  30. data/lib/adhearsion/{core_extensions → foundation}/metaprogramming.rb +0 -0
  31. data/lib/adhearsion/foundation/numeric.rb +13 -0
  32. data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
  33. data/lib/adhearsion/{core_extensions → foundation}/relationship_properties.rb +2 -0
  34. data/lib/adhearsion/foundation/string.rb +26 -0
  35. data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
  36. data/lib/adhearsion/{core_extensions → foundation}/thread_safety.rb +0 -0
  37. data/lib/adhearsion/host_definitions.rb +5 -1
  38. data/lib/adhearsion/initializer/asterisk.rb +33 -11
  39. data/lib/adhearsion/initializer/configuration.rb +58 -6
  40. data/lib/adhearsion/initializer/database.rb +3 -46
  41. data/lib/adhearsion/initializer/drb.rb +9 -3
  42. data/lib/adhearsion/initializer/freeswitch.rb +3 -3
  43. data/lib/adhearsion/initializer/rails.rb +1 -1
  44. data/lib/adhearsion/initializer.rb +213 -87
  45. data/lib/adhearsion/tasks/deprecations.rb +59 -0
  46. data/lib/adhearsion/tasks.rb +2 -1
  47. data/lib/adhearsion/version.rb +3 -3
  48. data/lib/adhearsion/voip/asterisk/agi_server.rb +6 -6
  49. data/lib/adhearsion/voip/asterisk/commands.rb +100 -2
  50. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1754 -0
  51. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
  52. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
  53. data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
  54. data/lib/adhearsion/voip/asterisk/manager_interface.rb +562 -0
  55. data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
  56. data/lib/adhearsion/voip/asterisk.rb +1 -8
  57. data/lib/adhearsion/voip/call.rb +5 -1
  58. data/lib/adhearsion/voip/dial_plan.rb +74 -61
  59. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +1 -1
  60. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +2 -6
  61. data/lib/adhearsion/voip/dsl/numerical_string.rb +2 -2
  62. data/lib/adhearsion/voip/freeswitch/oes_server.rb +2 -2
  63. data/lib/adhearsion.rb +16 -11
  64. data/lib/theatre/README.markdown +64 -0
  65. data/lib/theatre/callback_definition_loader.rb +84 -0
  66. data/lib/theatre/guid.rb +23 -0
  67. data/lib/theatre/invocation.rb +121 -0
  68. data/lib/theatre/namespace_manager.rb +153 -0
  69. data/lib/theatre/version.rb +2 -0
  70. data/lib/theatre.rb +151 -0
  71. metadata +60 -147
  72. data/Manifest.txt +0 -151
  73. data/README.txt +0 -5
  74. data/ahn_generators/component/USAGE +0 -5
  75. data/ahn_generators/component/component_generator.rb +0 -57
  76. data/ahn_generators/component/templates/configuration.rb +0 -0
  77. data/ahn_generators/component/templates/lib/lib.rb.erb +0 -3
  78. data/ahn_generators/component/templates/test/test.rb.erb +0 -12
  79. data/ahn_generators/component/templates/test/test_helper.rb +0 -14
  80. data/app_generators/ahn/templates/components/simon_game/configuration.rb +0 -0
  81. data/app_generators/ahn/templates/components/simon_game/test/test_helper.rb +0 -14
  82. data/app_generators/ahn/templates/components/simon_game/test/test_simon_game.rb +0 -31
  83. data/lib/adhearsion/core_extensions/array.rb +0 -0
  84. data/lib/adhearsion/core_extensions/guid.rb +0 -5
  85. data/lib/adhearsion/core_extensions/hash.rb +0 -0
  86. data/lib/adhearsion/core_extensions/numeric.rb +0 -4
  87. data/lib/adhearsion/core_extensions/proc.rb +0 -0
  88. data/lib/adhearsion/core_extensions/pseudo_uuid.rb +0 -11
  89. data/lib/adhearsion/core_extensions/publishable.rb +0 -73
  90. data/lib/adhearsion/core_extensions/string.rb +0 -26
  91. data/lib/adhearsion/core_extensions/thread.rb +0 -13
  92. data/lib/adhearsion/core_extensions/time.rb +0 -0
  93. data/lib/adhearsion/distributed/gateways/dbus_gateway.rb +0 -0
  94. data/lib/adhearsion/distributed/gateways/osa_gateway.rb +0 -0
  95. data/lib/adhearsion/distributed/gateways/rest_gateway.rb +0 -9
  96. data/lib/adhearsion/distributed/gateways/soap_gateway.rb +0 -9
  97. data/lib/adhearsion/distributed/gateways/xmlrpc_gateway.rb +0 -9
  98. data/lib/adhearsion/distributed/peer_finder.rb +0 -0
  99. data/lib/adhearsion/distributed/remote_cli.rb +0 -0
  100. data/lib/adhearsion/hooks.rb +0 -57
  101. data/lib/adhearsion/initializer/paths.rb +0 -55
  102. data/lib/adhearsion/voip/asterisk/ami/actions.rb +0 -238
  103. data/lib/adhearsion/voip/asterisk/ami/machine.rb +0 -871
  104. data/lib/adhearsion/voip/asterisk/ami/machine.rl +0 -109
  105. data/lib/adhearsion/voip/asterisk/ami/parser.rb +0 -262
  106. data/lib/adhearsion/voip/asterisk/ami.rb +0 -147
  107. data/spec/fixtures/dialplan.rb +0 -3
  108. data/spec/initializer/test_configuration.rb +0 -267
  109. data/spec/initializer/test_loading.rb +0 -162
  110. data/spec/initializer/test_paths.rb +0 -43
  111. data/spec/sample.rb +0 -9
  112. data/spec/silence.rb +0 -10
  113. data/spec/test_ahn_command.rb +0 -149
  114. data/spec/test_code_quality.rb +0 -87
  115. data/spec/test_component_manager.rb +0 -97
  116. data/spec/test_constants.rb +0 -8
  117. data/spec/test_drb.rb +0 -104
  118. data/spec/test_events.rb +0 -136
  119. data/spec/test_helper.rb +0 -106
  120. data/spec/test_hooks.rb +0 -15
  121. data/spec/test_host_definitions.rb +0 -79
  122. data/spec/test_initialization.rb +0 -124
  123. data/spec/test_logging.rb +0 -80
  124. data/spec/test_relationship_properties.rb +0 -54
  125. data/spec/voip/asterisk/ami_response_definitions.rb +0 -23
  126. data/spec/voip/asterisk/config_file_generators/test_agents.rb +0 -253
  127. data/spec/voip/asterisk/config_file_generators/test_queues.rb +0 -325
  128. data/spec/voip/asterisk/config_file_generators/test_voicemail.rb +0 -306
  129. data/spec/voip/asterisk/menu_command/test_calculated_match.rb +0 -111
  130. data/spec/voip/asterisk/menu_command/test_matchers.rb +0 -98
  131. data/spec/voip/asterisk/mock_ami_server.rb +0 -176
  132. data/spec/voip/asterisk/test_agi_server.rb +0 -453
  133. data/spec/voip/asterisk/test_ami.rb +0 -227
  134. data/spec/voip/asterisk/test_commands.rb +0 -2006
  135. data/spec/voip/asterisk/test_config_manager.rb +0 -129
  136. data/spec/voip/dsl/dispatcher_spec_helper.rb +0 -45
  137. data/spec/voip/dsl/test_dialing_dsl.rb +0 -268
  138. data/spec/voip/dsl/test_dispatcher.rb +0 -82
  139. data/spec/voip/dsl/test_parser.rb +0 -87
  140. data/spec/voip/freeswitch/test_basic_connection_manager.rb +0 -39
  141. data/spec/voip/freeswitch/test_inbound_connection_manager.rb +0 -39
  142. data/spec/voip/freeswitch/test_oes_server.rb +0 -9
  143. data/spec/voip/test_call_routing.rb +0 -127
  144. data/spec/voip/test_dialplan_manager.rb +0 -442
  145. data/spec/voip/test_numerical_string.rb +0 -48
  146. data/spec/voip/test_phone_number.rb +0 -36
  147. data/test/test_ahn_generator.rb +0 -59
  148. data/test/test_component_generator.rb +0 -52
  149. data/test/test_generator_helper.rb +0 -20
@@ -1,6 +1,4 @@
1
1
  # Hardcoding require for now since for some reason it's not being loaded
2
- require 'adhearsion/blank_slate'
3
- require 'adhearsion/component_manager'
4
2
  require 'adhearsion/voip/dsl/dialplan/control_passing_exception'
5
3
 
6
4
  module Adhearsion
@@ -8,25 +6,40 @@ module Adhearsion
8
6
  attr_accessor :loader, :entry_points
9
7
  def initialize(loader = Loader)
10
8
  @loader = loader
11
- @entry_points = @loader.load_dial_plan.contexts
9
+ @entry_points = @loader.load_dialplans.contexts
12
10
  end
13
11
 
14
12
  ##
15
13
  # Lookup and return an entry point by context name
14
+ #
16
15
  def lookup(context_name)
17
16
  entry_points[context_name]
18
17
  end
19
18
 
20
19
  ##
21
- # Executable environment for a dial plan in the scope of a call
20
+ # Executable environment for a dial plan in the scope of a call. This class has all the dialplan methods mixed into it.
21
+ #
22
22
  class ExecutionEnvironment
23
23
 
24
+ class << self
25
+ def create(*args)
26
+ returning(new(*args)) { |instance| instance.stage! }
27
+ end
28
+ end
29
+
24
30
  attr_reader :call
25
- def initialize(call, entry_point=nil)
31
+ def initialize(call, entry_point)
26
32
  @call, @entry_point = call, entry_point
33
+ end
34
+
35
+ ##
36
+ # Adds the methods to this ExecutionEnvironment which make it useful. e.g. dialplan-related methods, call variables,
37
+ # and component methods.
38
+ #
39
+ def stage!
27
40
  extend_with_voip_commands!
28
41
  extend_with_call_variables!
29
- extend_with_components_with_call_context!
42
+ extend_with_dialplan_component_methods!
30
43
  end
31
44
 
32
45
  def run
@@ -41,27 +54,22 @@ module Adhearsion
41
54
  end
42
55
  end
43
56
 
44
- private
45
- attr_reader :entry_point
46
-
47
- def extend_with_voip_commands!
48
- extend(Adhearsion::VoIP::Conveniences)
49
- extend(Adhearsion::VoIP::Commands.for(call.originating_voip_platform))
50
- end
51
-
52
- def extend_with_call_variables!
53
- call.define_variable_accessors self
54
- end
55
-
56
- def extend_with_components_with_call_context!
57
- ComponentManager.components_with_call_context.keys.each do |component_name|
58
- eval <<-COMPONENT_BUILDER
59
- def self.new_#{component_name.underscore}(*args, &block)
60
- ComponentManager.components_with_call_context['#{component_name}'].instantiate_with_call_context(self, *args, &block)
61
- end
62
- COMPONENT_BUILDER
63
- end
64
- end
57
+ protected
58
+
59
+ attr_reader :entry_point
60
+ def extend_with_voip_commands!
61
+ extend Adhearsion::VoIP::Conveniences
62
+ extend Adhearsion::VoIP::Commands.for(call.originating_voip_platform)
63
+ end
64
+
65
+ def extend_with_call_variables!
66
+ call.define_variable_accessors self
67
+ end
68
+
69
+ def extend_with_dialplan_component_methods!
70
+ Components.component_manager.extend_object_with(self, :dialplan) if Components.component_manager
71
+ end
72
+
65
73
  end
66
74
 
67
75
  class Manager
@@ -81,20 +89,19 @@ module Adhearsion
81
89
 
82
90
  def handle(call)
83
91
  if call.failed_call?
84
- environment = ExecutionEnvironment.new(call)
85
- call.extract_failed_reason_from(environment)
92
+ environment = ExecutionEnvironment.create(call, nil)
93
+ call.extract_failed_reason_from environment
86
94
  raise FailedExtensionCallException.new(environment)
87
95
  end
88
96
 
89
97
  if call.hungup_call?
90
- raise HungupExtensionCallException.new(ExecutionEnvironment.new(call))
98
+ raise HungupExtensionCallException.new(ExecutionEnvironment.new(call, nil))
91
99
  end
92
100
 
93
101
  starting_entry_point = entry_point_for call
94
102
  raise NoContextError, "No dialplan entry point for call context '#{call.context}' -- Ignoring call!" unless starting_entry_point
95
-
96
- @context = ExecutionEnvironment.new(call, starting_entry_point)
97
- inject_context_names_into_environment(@context)
103
+ @context = ExecutionEnvironment.create(call, starting_entry_point)
104
+ inject_context_names_into_environment @context
98
105
  @context.run
99
106
  end
100
107
 
@@ -103,7 +110,7 @@ module Adhearsion
103
110
  def entry_point_for(call)
104
111
  if entry_point = dial_plan.lookup(call.context.to_sym)
105
112
  entry_point
106
- elsif m = call.request.path.match(%r{/([^/]+)})
113
+ elsif call.respond_to?(:request) && m = call.request.path.match(%r{/([^/]+)})
107
114
  dial_plan.lookup(m[1].to_sym)
108
115
  end
109
116
  end
@@ -124,50 +131,55 @@ module Adhearsion
124
131
  attr_accessor :default_dial_plan_file_name
125
132
 
126
133
  def load(dial_plan_as_string)
127
- returning new do |loader|
128
- inject_dial_plan_component_classes_into dial_plan_as_string
129
- loader.load(dial_plan_as_string)
134
+ string_io = StringIO.new dial_plan_as_string
135
+ def string_io.path
136
+ "(eval)"
130
137
  end
138
+ load_dialplans string_io
131
139
  end
132
140
 
133
- def load_dial_plan(file_name = default_dial_plan_file_name)
134
- load read_dialplan_file(AHN_ROOT.dial_plan_named(file_name))
135
- end
136
-
137
- private
138
- def inject_dial_plan_component_classes_into(dial_plan_as_string)
139
- dial_plan_as_string[0, 0] = ComponentManager.components_with_call_context.keys.map do |component|
140
- "#{component} = ::Adhearsion::ComponentManager.components_with_call_context['#{component}'] unless defined? #{component}\n"
141
- end.join
141
+ def load_dialplans(*files)
142
+ files = Adhearsion::AHN_CONFIG.files_from_setting("paths", "dialplan") if files.empty?
143
+ files = Array files
144
+ files.map! do |file|
145
+ case file
146
+ when File, StringIO
147
+ file
148
+ when String
149
+ File.new file
150
+ else
151
+ raise ArgumentError, "Unrecognized type of file #{file.inspect}"
152
+ end
142
153
  end
143
-
144
- def read_dialplan_file(filename)
145
- File.read filename
154
+ returning new do |loader|
155
+ files.each do |file|
156
+ loader.load file
157
+ end
146
158
  end
147
-
159
+ end
160
+
148
161
  end
149
162
 
150
163
  self.default_dial_plan_file_name ||= 'dialplan.rb'
151
164
 
152
- attr_reader :contexts
153
165
  def initialize
154
- @contexts = {}
166
+ @context_collector = ContextNameCollector.new
155
167
  end
156
168
 
157
- def load(dial_plan_as_string)
158
- contexts.update(ContextNameCollector.build(dial_plan_as_string))
169
+ def contexts
170
+ @context_collector.contexts
171
+ end
172
+
173
+ def load(dialplan_file)
174
+ dialplan_code = dialplan_file.read
175
+ @context_collector.instance_eval(dialplan_code, dialplan_file.path)
176
+ nil
159
177
  end
160
178
 
161
179
  class ContextNameCollector# < ::BlankSlate
162
180
 
163
181
  class << self
164
-
165
- def build(dial_plan_as_string)
166
- builder = new
167
- builder.instance_eval(dial_plan_as_string)
168
- builder.contexts
169
- end
170
-
182
+
171
183
  def const_missing(name)
172
184
  super
173
185
  rescue ArgumentError
@@ -185,6 +197,7 @@ module Adhearsion
185
197
  super if !block_given? || args.any?
186
198
  contexts[name] = DialplanContextProc.new(name, &block)
187
199
  end
200
+
188
201
  end
189
202
  end
190
203
  class DialplanContextProc < Proc
@@ -15,7 +15,7 @@
15
15
  # License along with this library; if not, write to the Free Software
16
16
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
- require 'adhearsion/core_extensions/metaprogramming'
18
+ require 'adhearsion/foundation/metaprogramming'
19
19
  require 'adhearsion/voip/conveniences'
20
20
  require 'adhearsion/voip/constants'
21
21
  require 'adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches'
@@ -14,14 +14,10 @@ module Adhearsion
14
14
 
15
15
  #TODO: separate into smaller pieces
16
16
  def self.get_contexts
17
- unless Adhearsion::Paths.manager_for?("dialplans")
18
- raise "No dialplan files found in .ahnrc!"
19
- end
20
-
21
17
  envelope = ContextsEnvelope.new
22
18
 
23
- dialplans = all_dialplans
24
- warn "No dialplan files were found!" if dialplans.empty?
19
+ dialplans = AHN_CONFIG.files_from_setting "paths", "dialplan"
20
+ ahn_log.dialplan.warn "No dialplan files were found!" if dialplans.empty?
25
21
 
26
22
  returning({}) do |contexts|
27
23
  dialplans.each do |file|
@@ -52,10 +52,10 @@ module Adhearsion
52
52
 
53
53
  # Checks against a pattern identifying US local numbers (i.e numbers
54
54
  # without an area code seven digits long)
55
- def local_number?() to_s =~ Adhearsion::VoIP::Constants::US_LOCAL_NUMBER end
55
+ def us_local_number?() to_s =~ Adhearsion::VoIP::Constants::US_LOCAL_NUMBER end
56
56
 
57
57
  # Checks against a pattern identifying US domestic numbers.
58
- def national_number?() to_s =~ Adhearsion::VoIP::Constants::US_NATIONAL_NUMBER end
58
+ def us_national_number?() to_s =~ Adhearsion::VoIP::Constants::US_NATIONAL_NUMBER end
59
59
 
60
60
  # Checks against a pattern identifying an ISN number. See http://freenum.org
61
61
  # for more info.
@@ -52,7 +52,7 @@ module Adhearsion
52
52
  # If the target context does not exist, warn and don't handle the call
53
53
  unless first_context
54
54
  log "No context '#{first_context_name}' found in " +
55
- "#{all_dialplans.to_sentence(:connector => "or")}. Ignoring request!"
55
+ "#{AHN_CONFIG.files_from_setting("paths", "").to_sentence(:connector => "or")}. Ignoring request!"
56
56
  return
57
57
  end
58
58
 
@@ -91,7 +91,7 @@ module Adhearsion
91
91
  # time.
92
92
  def should_reload_contexts?
93
93
  !@abstract_contexts || !@abstract_dispatcher ||
94
- all_dialplans.map { |x| File.mtime(x) }.max < Time.now
94
+ AHN_CONFIG.files_from_setting("paths", "dialplan").map { |x| File.mtime(x) }.max < Time.now
95
95
  end
96
96
 
97
97
  def rubyize_keys_for(hash)
data/lib/adhearsion.rb CHANGED
@@ -3,30 +3,35 @@ STDERR.puts "WARNING: You are running Adhearsion in an unsupported
3
3
  version of Ruby (Ruby #{RUBY_VERSION} #{RUBY_RELEASE_DATE})!
4
4
  Please upgrade to at least Ruby v1.8.5." if RUBY_VERSION < "1.8.5"
5
5
 
6
- module Adhearsion
7
- # Sets up the Gem require path.
8
- AHN_INSTALL_DIR = File.expand_path(File.dirname(__FILE__) + "/..")
9
- CONFIG = {}
10
- end
11
-
12
6
  $: << File.expand_path(File.dirname(__FILE__))
13
7
 
14
8
  require 'rubygems'
9
+
15
10
  require 'adhearsion/version'
16
11
  require 'adhearsion/voip/call'
17
12
  require 'adhearsion/voip/dial_plan'
18
13
  require 'adhearsion/voip/asterisk/special_dial_plan_managers'
19
- require 'adhearsion/core_extensions/all'
20
- require 'adhearsion/blank_slate'
21
- require 'adhearsion/hooks'
14
+ require 'adhearsion/foundation/all'
22
15
  require 'adhearsion/events_support'
23
16
  require 'adhearsion/logging'
17
+ require 'adhearsion/component_manager'
24
18
  require 'adhearsion/initializer/configuration'
25
19
  require 'adhearsion/initializer'
26
- require 'adhearsion/initializer/paths'
27
20
  require 'adhearsion/voip/dsl/numerical_string'
28
21
  require 'adhearsion/voip/dsl/dialplan/parser'
29
22
  require 'adhearsion/voip/commands'
30
23
  require 'adhearsion/voip/asterisk/commands'
31
24
  require 'adhearsion/voip/dsl/dialing_dsl'
32
- require 'adhearsion/voip/call_routing'
25
+ require 'adhearsion/voip/call_routing'
26
+
27
+ module Adhearsion
28
+ # Sets up the Gem require path.
29
+ AHN_INSTALL_DIR = File.expand_path(File.dirname(__FILE__) + "/..")
30
+ AHN_CONFIG = Configuration.new
31
+
32
+ ##
33
+ # This Array holds all the Threads whose life matters. Adhearsion will not exit until all of these have died.
34
+ #
35
+ IMPORTANT_THREADS = []
36
+
37
+ end
@@ -0,0 +1,64 @@
1
+ Theatre
2
+ =======
3
+
4
+ Present status: stable
5
+
6
+ A library for choreographing a dynamic pool of hierarchically organized actors. This was originally extracted from the [Adhearsion](http://adhearsion.com) framework by Jay Phillips.
7
+
8
+ In the Adhearsion framework, it was necessary to develop an internal message-passing system that could work either synchronously or asynchronously. This is used by the framework itself and for framework extensions (called _components_) to talk with each other. The source of the events is completely undefined -- events could originate from within the framework out outside the framework. For example, a Message Queue such as [Stomp](http://stomp.codehaus.org) can wire incoming events into Theatre and catch events going to a particular destination so it can proxy them out to the server.
9
+
10
+ Motivations and Design Decisions
11
+ --------------------------------
12
+
13
+ * Must maintain Ruby 1.8 and JRuby compatibility
14
+ * Must be Thread-safe
15
+ * Must provide some level of transparency into the events going through it
16
+ * Must be dynamic enough to reallocate the number of triggerrs based on load
17
+ * Must help facilitate test-driven development of Actor functionality
18
+ * Must allow external persistence in case of a crash
19
+
20
+ Example
21
+ -------
22
+
23
+ Below is an example taken from Adhearsion for executing framework-level callbacks. Note: the framework treats this callback synchronously.
24
+
25
+ events.framework.asterisk.before_call.each do |event|
26
+ # Pull headers from event and respond to it here.
27
+ end
28
+
29
+ Below is an example of integration with [Stomp](http://stomp.codehaus.org/), a simple yet robust open-protocol message queue.
30
+
31
+ events.stomp.new_call.each do |event|
32
+ # Handle all events from the Stomp MQ server whose name is "new_call" (the String)
33
+ end
34
+
35
+ This will filter all events whose name is "new_call" and yield the Stomp::Message to the block.
36
+
37
+ Framework terminology
38
+ --------------------
39
+
40
+ Below are definitions of terms I use in Theatre. See the respective links for more information.
41
+
42
+ * **callback**: This is the block given to the `each` method which triggers events coming in.
43
+ * **payload**: This is the "message" sent to the Theatre and is what will ultimately be yielded to the callback
44
+ * **[Actor](http://en.wikipedia.org/wiki/Actor_model)**: This refers to concurrent responders to events in a concurrent system.
45
+
46
+ Synchronous vs. Asynchronous
47
+ ----------------------------
48
+
49
+ With Theatre, all events are asynchronous with the optional ability to synchronously block until the event is scheduled, triggered, and has returned. If you wish to synchronously trigger the event, simple call `wait` on an `Invocation` object returned from `trigger` and then check the `Invocation#current_state` property for `:success` or `:error`. Optionally the `Invocation#success?` and `Invocation#error?` methods also provide more intuitive access to the finished state. If the event finished with `:success`, you may retrieve the returned value of the event Proc by calling `Invocation#returned_value`. If the event finished with `:error`, you may get the Exception with `Invocation#error`.
50
+
51
+ Because many callbacks can be registered for a particular namespace, each needing its own Invocation object, the `Theatre#trigger` method returns an Array of Invocation objects.
52
+
53
+ # We'll assume only one callback is registered and call #first on the Array of Invocations returned by #trigger
54
+ invocation = my_theatre.trigger("your/namespace/here", YourSpecialClass.new("this can be anything")).first
55
+ invocation.wait
56
+ raise invocation.error if invocation.error?
57
+ log "Actor finished with return value #{invocation.returned_value}"
58
+
59
+ Ruby 1.8 vs. Ruby 1.9 vs. JRuby
60
+ -------------------------------
61
+
62
+ Theatre was created for Ruby 1.8 because no good Actor system existed on Ruby 1.8 that met Adhearsion's needs (e.g. hierarchal with synchronous and asynchronous modes. If you wish to achieve real processor-level concurrency, use JRuby.
63
+
64
+ Presently Ruby 1.9 compatibility is not a priority but patches for compatibility will be accepted, as long as they preserve compatibility with both MRI and JRuby.
@@ -0,0 +1,84 @@
1
+ module Theatre
2
+
3
+ ##
4
+ # This class provides the a wrapper aroung which an events.rb file can be instance_eval'd.
5
+ #
6
+ class CallbackDefinitionLoader
7
+
8
+ attr_reader :theatre, :root_name
9
+ def initialize(theatre, root_name=:events)
10
+ @theatre = theatre
11
+ @root_name = root_name
12
+
13
+ create_recorder_method root_name
14
+ end
15
+
16
+ def anonymous_recorder
17
+ BlankSlateMessageRecorder.new(&method(:callback_registered))
18
+ end
19
+
20
+ ##
21
+ # Parses the given Ruby source code file and returns this object.
22
+ #
23
+ # @param [String, File] file The filename or File object for the Ruby source code file to parse.
24
+ #
25
+ def load_events_file(file)
26
+ file = File.open(file) if file.kind_of? String
27
+ instance_eval file.read, file.path
28
+ self
29
+ end
30
+
31
+ ##
32
+ # Parses the given Ruby source code and returns this object.
33
+ #
34
+ # NOTE: Only use this if you're generating the code yourself! If you're loading a file from the filesystem, you should
35
+ # use load_events_file() since load_events_file() will properly attribute errors in the code to the file from which the
36
+ # code was loaded.
37
+ #
38
+ # @param [String] code The Ruby source code to parse
39
+ #
40
+ def load_events_code(code)
41
+ instance_eval code
42
+ self
43
+ end
44
+
45
+ protected
46
+
47
+ ##
48
+ # Immediately register the namespace and callback with the Theatre instance given to the constructor. This method is only
49
+ # called when a new BlankSlateMessageRecorder is instantiated and receives #each().
50
+ #
51
+ def callback_registered(namespaces, callback)
52
+ # Get rid of all arguments passed to the namespaces. Will support arguments in the future.
53
+ namespaces = namespaces.map { |namespace| namespace.first }
54
+
55
+ theatre.namespace_manager.register_callback_at_namespace namespaces, callback
56
+ end
57
+
58
+ def create_recorder_method(record_method_name)
59
+ (class << self; self; end).send(:alias_method, record_method_name, :anonymous_recorder)
60
+ end
61
+
62
+ class BlankSlateMessageRecorder
63
+
64
+ (instance_methods - %w[__send__ __id__]).each { |m| undef_method m }
65
+
66
+ def initialize(&notify_on_completion)
67
+ @notify_on_completion = notify_on_completion
68
+ @namespaces = []
69
+ end
70
+
71
+ def method_missing(*method_name_and_args)
72
+ raise ArgumentError, "Supplying a block is not supported" if block_given?
73
+ @namespaces << method_name_and_args
74
+ self
75
+ end
76
+
77
+ def each(&callback)
78
+ @notify_on_completion.call(@namespaces, callback)
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # Right now Adhearsion also defines this method. The eventual solution will be to extract the Adhearsion features on which
2
+ # Theatre depends and make that a dependent library.
3
+
4
+ unless respond_to? :new_guid
5
+
6
+ def random_character
7
+ case random_digit = rand(62)
8
+ when 0...10 : random_digit.to_s
9
+ when 10...36 : (random_digit + 55).chr
10
+ when 36...62 : (random_digit + 61).chr
11
+ end
12
+ end
13
+
14
+ def random_string(length_of_string=8)
15
+ Array.new(length_of_string) { random_character }.join
16
+ end
17
+
18
+ # This GUID implementation doesn't adhere to the RFC which wants to make certain segments based on the MAC address of a
19
+ # network interface card and other wackiness. It's sufficiently random for our needs.
20
+ def new_guid
21
+ [8,4,4,4,12].map { |segment_length| random_string(segment_length) }.join('-')
22
+ end
23
+ end
@@ -0,0 +1,121 @@
1
+ require 'theatre/guid'
2
+ require 'thread'
3
+ require 'monitor'
4
+
5
+ module Theatre
6
+
7
+ ##
8
+ # An Invocation is an object which Theatre generates and returns from Theatre#trigger.
9
+ #
10
+ class Invocation
11
+
12
+ attr_reader :queued_time, :started_time, :finished_time, :unique_id, :callback, :namespace, :error, :returned_value
13
+
14
+ class InvalidStateError < Exception; end
15
+
16
+ ##
17
+ # Create a new Invocation.
18
+ #
19
+ # @param [String] namespace The "/foo/bar/qaz" path to the namespace to which this Invocation belongs.
20
+ # @param [Proc] callback The block which should be executed by an Actor scheduler.
21
+ # @param [Object] payload The message that will be sent to the callback for processing.
22
+ #
23
+ def initialize(namespace, callback, payload=:theatre_no_payload)
24
+ raise ArgumentError, "Callback must be a Proc" unless callback.kind_of? Proc
25
+ @payload = payload
26
+ @unique_id = new_guid.freeze
27
+ @callback = callback
28
+ @current_state = :new
29
+ @state_lock = Mutex.new
30
+
31
+ # Used just to protect access to the @returned_value instance variable
32
+ @returned_value_lock = Monitor.new
33
+
34
+ # Used when wait() is called to notify all waiting threads by using a ConditionVariable
35
+ @returned_value_blocker = Monitor::ConditionVariable.new @returned_value_lock
36
+ end
37
+
38
+ def queued
39
+ with_state_lock do
40
+ raise InvalidStateError unless @current_state == :new
41
+ @current_state = :queued
42
+ @queued_time = Time.now.freeze
43
+ end
44
+ true
45
+ end
46
+
47
+ def current_state
48
+ with_state_lock { @current_state }
49
+ end
50
+
51
+ def start
52
+ with_state_lock do
53
+ raise InvalidStateError unless @current_state == :queued
54
+ @current_state = :running
55
+ end
56
+ @started_time = Time.now.freeze
57
+
58
+ begin
59
+ self.returned_value = if @payload.equal? :theatre_no_payload
60
+ @callback.call
61
+ else
62
+ @callback.call @payload
63
+ end
64
+ with_state_lock { @current_state = :success }
65
+ rescue => @error
66
+ with_state_lock { @current_state = :error }
67
+ ensure
68
+ @finished_time = Time.now.freeze
69
+ end
70
+ end
71
+
72
+ def execution_duration
73
+ return nil unless @finished_time
74
+ @finished_time - @started_time
75
+ end
76
+
77
+ def error?
78
+ current_state.equal? :error
79
+ end
80
+
81
+ def success?
82
+ current_state.equal? :success
83
+ end
84
+
85
+ ##
86
+ # When this Invocation has been queued, started, and entered either the :success or :error state, this method will
87
+ # finally return. Until then, it blocks the Thread.
88
+ #
89
+ # @return [Object] The result of invoking this Invocation's callback
90
+ #
91
+ def wait
92
+ with_returned_value_lock { return @returned_value if defined? @returned_value }
93
+ @returned_value_blocker.wait
94
+ # Return the returned_value
95
+ with_returned_value_lock { @returned_value }
96
+ end
97
+
98
+ protected
99
+
100
+ ##
101
+ # Protected setter which does some other housework when the returned value is found (such as notifying wait()ers)
102
+ #
103
+ # @param [returned_value] The value to set this returned value to.
104
+ #
105
+ def returned_value=(returned_value)
106
+ with_returned_value_lock do
107
+ @returned_value = returned_value
108
+ @returned_value_blocker.broadcast
109
+ end
110
+ end
111
+
112
+ def with_returned_value_lock(&block)
113
+ @returned_value_lock.synchronize(&block)
114
+ end
115
+
116
+ def with_state_lock(&block)
117
+ @state_lock.synchronize(&block)
118
+ end
119
+
120
+ end
121
+ end