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.
- data/CHANGELOG +8 -2
- data/EVENTS +11 -0
- data/Rakefile +92 -26
- data/adhearsion.gemspec +131 -23
- data/app_generators/ahn/ahn_generator.rb +21 -7
- data/app_generators/ahn/templates/.ahnrc +10 -4
- data/app_generators/ahn/templates/Rakefile +7 -2
- data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
- data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/config.yml +12 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
- data/app_generators/ahn/templates/components/simon_game/{lib/simon_game.rb → simon_game.rb} +14 -19
- data/app_generators/ahn/templates/config/startup.rb +3 -6
- data/app_generators/ahn/templates/dialplan.rb +2 -3
- data/app_generators/ahn/templates/events.rb +32 -6
- data/bin/jahn +10 -0
- data/examples/asterisk_manager_interface/standalone.rb +51 -0
- data/lib/adhearsion/cli.rb +140 -23
- data/lib/adhearsion/component_manager/component_tester.rb +55 -0
- data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
- data/lib/adhearsion/component_manager.rb +169 -238
- data/lib/adhearsion/events_support.rb +59 -237
- data/lib/adhearsion/{core_extensions → foundation}/all.rb +0 -0
- data/lib/adhearsion/{blank_slate.rb → foundation/blank_slate.rb} +0 -0
- data/lib/adhearsion/{core_extensions → foundation}/custom_daemonizer.rb +0 -0
- data/lib/adhearsion/foundation/event_socket.rb +203 -0
- data/lib/adhearsion/foundation/future_resource.rb +36 -0
- data/lib/adhearsion/{core_extensions → foundation}/global.rb +0 -0
- data/lib/adhearsion/{core_extensions → foundation}/metaprogramming.rb +0 -0
- data/lib/adhearsion/foundation/numeric.rb +13 -0
- data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
- data/lib/adhearsion/{core_extensions → foundation}/relationship_properties.rb +2 -0
- data/lib/adhearsion/foundation/string.rb +26 -0
- data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
- data/lib/adhearsion/{core_extensions → foundation}/thread_safety.rb +0 -0
- data/lib/adhearsion/host_definitions.rb +5 -1
- data/lib/adhearsion/initializer/asterisk.rb +33 -11
- data/lib/adhearsion/initializer/configuration.rb +58 -6
- data/lib/adhearsion/initializer/database.rb +3 -46
- data/lib/adhearsion/initializer/drb.rb +9 -3
- data/lib/adhearsion/initializer/freeswitch.rb +3 -3
- data/lib/adhearsion/initializer/rails.rb +1 -1
- data/lib/adhearsion/initializer.rb +213 -87
- data/lib/adhearsion/tasks/deprecations.rb +59 -0
- data/lib/adhearsion/tasks.rb +2 -1
- data/lib/adhearsion/version.rb +3 -3
- data/lib/adhearsion/voip/asterisk/agi_server.rb +6 -6
- data/lib/adhearsion/voip/asterisk/commands.rb +100 -2
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1754 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +562 -0
- data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
- data/lib/adhearsion/voip/asterisk.rb +1 -8
- data/lib/adhearsion/voip/call.rb +5 -1
- data/lib/adhearsion/voip/dial_plan.rb +74 -61
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +1 -1
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +2 -6
- data/lib/adhearsion/voip/dsl/numerical_string.rb +2 -2
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +2 -2
- data/lib/adhearsion.rb +16 -11
- data/lib/theatre/README.markdown +64 -0
- data/lib/theatre/callback_definition_loader.rb +84 -0
- data/lib/theatre/guid.rb +23 -0
- data/lib/theatre/invocation.rb +121 -0
- data/lib/theatre/namespace_manager.rb +153 -0
- data/lib/theatre/version.rb +2 -0
- data/lib/theatre.rb +151 -0
- metadata +60 -147
- data/Manifest.txt +0 -151
- data/README.txt +0 -5
- data/ahn_generators/component/USAGE +0 -5
- data/ahn_generators/component/component_generator.rb +0 -57
- data/ahn_generators/component/templates/configuration.rb +0 -0
- data/ahn_generators/component/templates/lib/lib.rb.erb +0 -3
- data/ahn_generators/component/templates/test/test.rb.erb +0 -12
- data/ahn_generators/component/templates/test/test_helper.rb +0 -14
- data/app_generators/ahn/templates/components/simon_game/configuration.rb +0 -0
- data/app_generators/ahn/templates/components/simon_game/test/test_helper.rb +0 -14
- data/app_generators/ahn/templates/components/simon_game/test/test_simon_game.rb +0 -31
- data/lib/adhearsion/core_extensions/array.rb +0 -0
- data/lib/adhearsion/core_extensions/guid.rb +0 -5
- data/lib/adhearsion/core_extensions/hash.rb +0 -0
- data/lib/adhearsion/core_extensions/numeric.rb +0 -4
- data/lib/adhearsion/core_extensions/proc.rb +0 -0
- data/lib/adhearsion/core_extensions/pseudo_uuid.rb +0 -11
- data/lib/adhearsion/core_extensions/publishable.rb +0 -73
- data/lib/adhearsion/core_extensions/string.rb +0 -26
- data/lib/adhearsion/core_extensions/thread.rb +0 -13
- data/lib/adhearsion/core_extensions/time.rb +0 -0
- data/lib/adhearsion/distributed/gateways/dbus_gateway.rb +0 -0
- data/lib/adhearsion/distributed/gateways/osa_gateway.rb +0 -0
- data/lib/adhearsion/distributed/gateways/rest_gateway.rb +0 -9
- data/lib/adhearsion/distributed/gateways/soap_gateway.rb +0 -9
- data/lib/adhearsion/distributed/gateways/xmlrpc_gateway.rb +0 -9
- data/lib/adhearsion/distributed/peer_finder.rb +0 -0
- data/lib/adhearsion/distributed/remote_cli.rb +0 -0
- data/lib/adhearsion/hooks.rb +0 -57
- data/lib/adhearsion/initializer/paths.rb +0 -55
- data/lib/adhearsion/voip/asterisk/ami/actions.rb +0 -238
- data/lib/adhearsion/voip/asterisk/ami/machine.rb +0 -871
- data/lib/adhearsion/voip/asterisk/ami/machine.rl +0 -109
- data/lib/adhearsion/voip/asterisk/ami/parser.rb +0 -262
- data/lib/adhearsion/voip/asterisk/ami.rb +0 -147
- data/spec/fixtures/dialplan.rb +0 -3
- data/spec/initializer/test_configuration.rb +0 -267
- data/spec/initializer/test_loading.rb +0 -162
- data/spec/initializer/test_paths.rb +0 -43
- data/spec/sample.rb +0 -9
- data/spec/silence.rb +0 -10
- data/spec/test_ahn_command.rb +0 -149
- data/spec/test_code_quality.rb +0 -87
- data/spec/test_component_manager.rb +0 -97
- data/spec/test_constants.rb +0 -8
- data/spec/test_drb.rb +0 -104
- data/spec/test_events.rb +0 -136
- data/spec/test_helper.rb +0 -106
- data/spec/test_hooks.rb +0 -15
- data/spec/test_host_definitions.rb +0 -79
- data/spec/test_initialization.rb +0 -124
- data/spec/test_logging.rb +0 -80
- data/spec/test_relationship_properties.rb +0 -54
- data/spec/voip/asterisk/ami_response_definitions.rb +0 -23
- data/spec/voip/asterisk/config_file_generators/test_agents.rb +0 -253
- data/spec/voip/asterisk/config_file_generators/test_queues.rb +0 -325
- data/spec/voip/asterisk/config_file_generators/test_voicemail.rb +0 -306
- data/spec/voip/asterisk/menu_command/test_calculated_match.rb +0 -111
- data/spec/voip/asterisk/menu_command/test_matchers.rb +0 -98
- data/spec/voip/asterisk/mock_ami_server.rb +0 -176
- data/spec/voip/asterisk/test_agi_server.rb +0 -453
- data/spec/voip/asterisk/test_ami.rb +0 -227
- data/spec/voip/asterisk/test_commands.rb +0 -2006
- data/spec/voip/asterisk/test_config_manager.rb +0 -129
- data/spec/voip/dsl/dispatcher_spec_helper.rb +0 -45
- data/spec/voip/dsl/test_dialing_dsl.rb +0 -268
- data/spec/voip/dsl/test_dispatcher.rb +0 -82
- data/spec/voip/dsl/test_parser.rb +0 -87
- data/spec/voip/freeswitch/test_basic_connection_manager.rb +0 -39
- data/spec/voip/freeswitch/test_inbound_connection_manager.rb +0 -39
- data/spec/voip/freeswitch/test_oes_server.rb +0 -9
- data/spec/voip/test_call_routing.rb +0 -127
- data/spec/voip/test_dialplan_manager.rb +0 -442
- data/spec/voip/test_numerical_string.rb +0 -48
- data/spec/voip/test_phone_number.rb +0 -36
- data/test/test_ahn_generator.rb +0 -59
- data/test/test_component_generator.rb +0 -52
- 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.
|
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
|
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
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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.
|
85
|
-
call.extract_failed_reason_from
|
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
|
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
|
-
|
128
|
-
|
129
|
-
|
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
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
145
|
-
|
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
|
-
@
|
166
|
+
@context_collector = ContextNameCollector.new
|
155
167
|
end
|
156
168
|
|
157
|
-
def
|
158
|
-
contexts
|
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/
|
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 =
|
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
|
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
|
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
|
-
"#{
|
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
|
-
|
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/
|
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(¬ify_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
|
data/lib/theatre/guid.rb
ADDED
@@ -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
|