comboy-autumn 3.1

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 (81) hide show
  1. data/README.textile +1192 -0
  2. data/autumn.gemspec +25 -0
  3. data/bin/autumn +27 -0
  4. data/lib/autumn.rb +2 -0
  5. data/lib/autumn/authentication.rb +290 -0
  6. data/lib/autumn/channel_leaf.rb +107 -0
  7. data/lib/autumn/coder.rb +166 -0
  8. data/lib/autumn/console_boot.rb +9 -0
  9. data/lib/autumn/ctcp.rb +250 -0
  10. data/lib/autumn/daemon.rb +207 -0
  11. data/lib/autumn/datamapper_hacks.rb +290 -0
  12. data/lib/autumn/foliater.rb +231 -0
  13. data/lib/autumn/formatting.rb +236 -0
  14. data/lib/autumn/generator.rb +231 -0
  15. data/lib/autumn/genesis.rb +191 -0
  16. data/lib/autumn/inheritable_attributes.rb +162 -0
  17. data/lib/autumn/leaf.rb +738 -0
  18. data/lib/autumn/log_facade.rb +49 -0
  19. data/lib/autumn/misc.rb +87 -0
  20. data/lib/autumn/script.rb +74 -0
  21. data/lib/autumn/speciator.rb +165 -0
  22. data/lib/autumn/stem.rb +919 -0
  23. data/lib/autumn/stem_facade.rb +176 -0
  24. data/resources/daemons/Anothernet.yml +3 -0
  25. data/resources/daemons/AustHex.yml +29 -0
  26. data/resources/daemons/Bahamut.yml +67 -0
  27. data/resources/daemons/Dancer.yml +3 -0
  28. data/resources/daemons/GameSurge.yml +3 -0
  29. data/resources/daemons/IRCnet.yml +3 -0
  30. data/resources/daemons/Ithildin.yml +7 -0
  31. data/resources/daemons/KineIRCd.yml +56 -0
  32. data/resources/daemons/PTlink.yml +6 -0
  33. data/resources/daemons/QuakeNet.yml +20 -0
  34. data/resources/daemons/RFC1459.yml +158 -0
  35. data/resources/daemons/RFC2811.yml +16 -0
  36. data/resources/daemons/RFC2812.yml +36 -0
  37. data/resources/daemons/RatBox.yml +25 -0
  38. data/resources/daemons/Ultimate.yml +24 -0
  39. data/resources/daemons/Undernet.yml +6 -0
  40. data/resources/daemons/Unreal.yml +110 -0
  41. data/resources/daemons/_Other.yml +7 -0
  42. data/resources/daemons/aircd.yml +33 -0
  43. data/resources/daemons/bdq-ircd.yml +3 -0
  44. data/resources/daemons/hybrid.yml +38 -0
  45. data/resources/daemons/ircu.yml +67 -0
  46. data/resources/daemons/tr-ircd.yml +8 -0
  47. data/skel/Rakefile +135 -0
  48. data/skel/config/global.yml +2 -0
  49. data/skel/config/seasons/testing/database.yml +7 -0
  50. data/skel/config/seasons/testing/leaves.yml +7 -0
  51. data/skel/config/seasons/testing/season.yml +2 -0
  52. data/skel/config/seasons/testing/stems.yml +9 -0
  53. data/skel/leaves/administrator/README +20 -0
  54. data/skel/leaves/administrator/controller.rb +67 -0
  55. data/skel/leaves/administrator/views/autumn.txt.erb +1 -0
  56. data/skel/leaves/administrator/views/reload.txt.erb +11 -0
  57. data/skel/leaves/insulter/README +17 -0
  58. data/skel/leaves/insulter/controller.rb +65 -0
  59. data/skel/leaves/insulter/views/about.txt.erb +1 -0
  60. data/skel/leaves/insulter/views/help.txt.erb +1 -0
  61. data/skel/leaves/insulter/views/insult.txt.erb +1 -0
  62. data/skel/leaves/scorekeeper/README +34 -0
  63. data/skel/leaves/scorekeeper/config.yml +2 -0
  64. data/skel/leaves/scorekeeper/controller.rb +104 -0
  65. data/skel/leaves/scorekeeper/helpers/general.rb +64 -0
  66. data/skel/leaves/scorekeeper/models/channel.rb +12 -0
  67. data/skel/leaves/scorekeeper/models/person.rb +14 -0
  68. data/skel/leaves/scorekeeper/models/pseudonym.rb +11 -0
  69. data/skel/leaves/scorekeeper/models/score.rb +14 -0
  70. data/skel/leaves/scorekeeper/tasks/stats.rake +17 -0
  71. data/skel/leaves/scorekeeper/views/about.txt.erb +1 -0
  72. data/skel/leaves/scorekeeper/views/change.txt.erb +5 -0
  73. data/skel/leaves/scorekeeper/views/history.txt.erb +11 -0
  74. data/skel/leaves/scorekeeper/views/points.txt.erb +5 -0
  75. data/skel/leaves/scorekeeper/views/usage.txt.erb +1 -0
  76. data/skel/script/console +34 -0
  77. data/skel/script/daemon +29 -0
  78. data/skel/script/destroy +48 -0
  79. data/skel/script/generate +48 -0
  80. data/skel/script/server +15 -0
  81. metadata +170 -0
@@ -0,0 +1,290 @@
1
+ # A set of hacks to make DataMapper play more nicely with classes within
2
+ # modules.
3
+
4
+ module DataMapper # :nodoc:
5
+
6
+ #HACK Add module names to auto-generated class names in relationships
7
+ #
8
+ # When a class name is automatically inferred from a relationship name (e.g.,
9
+ # guessing that has_many :widgets refers to a Widget class), it is necessary
10
+ # to enclose these class names in the same modules as the calling class. For
11
+ # example, if MyLeaf::Factory has_many :widgets, this hack will ensure the
12
+ # inferred class name is MyLeaf::Widget, instead of just ::Widget.
13
+ #
14
+ # This hack is performed for each of the association types in DataMapper. An
15
+ # :old_behavior option is given to revert to the unhacked method.
16
+
17
+ module Associations # :nodoc:
18
+ module OneToMany # :nodoc:
19
+ class << self
20
+ alias_method :old_setup, :setup
21
+ def setup(name, model, options={})
22
+ class_name = options.fetch(:class_name, Extlib::Inflection.classify(name))
23
+ if not options[:old_behavior] and not class_name.include?('::') then
24
+ modules = model.to_s.split('::')
25
+ modules.pop
26
+ modules << class_name
27
+ options[:class_name] = modules.join('::')
28
+ end
29
+ old_setup(name, model, options)
30
+ end
31
+ end
32
+ end
33
+
34
+ module OneToOne # :nodoc:
35
+ class << self
36
+ alias_method :old_setup, :setup
37
+ def setup(name, model, options={})
38
+ class_name = options.fetch(:class_name, Extlib::Inflection.classify(name))
39
+ if not options[:old_behavior] and not class_name.include?('::') then
40
+ modules = model.to_s.split('::')
41
+ modules.pop
42
+ modules << class_name
43
+ options[:class_name] = modules.join('::')
44
+ end
45
+ old_setup(name, model, options)
46
+ end
47
+ end
48
+ end
49
+
50
+ module ManyToOne # :nodoc:
51
+ class << self
52
+ alias_method :old_setup, :setup
53
+ def setup(name, model, options={})
54
+ class_name = options.fetch(:class_name, Extlib::Inflection.classify(name))
55
+ if not options[:old_behavior] and not class_name.include?('::') then
56
+ modules = model.to_s.split('::')
57
+ modules.pop
58
+ modules << class_name
59
+ options[:class_name] = modules.join('::')
60
+ end
61
+ old_setup(name, model, options)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ #HACK Strip module names when auto-generating table names for has-many-through
69
+ # relationships.
70
+ #
71
+ # By default, DataMapper will not strip module names when creating the join
72
+ # tables for has-many-through relationships. So, if MyLeaf::Post has and belongs
73
+ # to many MyLeaf::Category, the join table will be called
74
+ # "my_leaf/categories_my_leaf/posts", which is clearly an invalid table name.
75
+ # This hack strips module components from a class name before generating the
76
+ # join table's name.
77
+ #
78
+ # A side effect of this hack is that no two DataMapper models for the same
79
+ # repository can share the same name, even if they are in separate modules.
80
+ #
81
+ # This also fixes a bug that can occur when script/console is launched. The
82
+ # double assignment of the relationship variable seems to mess up IRb, so it has
83
+ # been split into two assignments.
84
+
85
+ DataMapper::Associations::ManyToMany.module_eval do # :nodoc:
86
+ def self.setup(name, model, options={})
87
+ class_name = options.fetch(:class_name, Extlib::Inflection.classify(name))
88
+ if not options[:old_behavior] and not class_name.include?('::') then
89
+ modules = model.to_s.split('::')
90
+ modules.pop
91
+ modules << class_name
92
+ options[:class_name] = modules.join('::')
93
+ end
94
+
95
+ assert_kind_of 'name', name, Symbol
96
+ assert_kind_of 'model', model, DataMapper::Model
97
+ assert_kind_of 'options', options, Hash
98
+
99
+ repository_name = model.repository.name
100
+
101
+ model.class_eval <<-EOS, __FILE__, __LINE__
102
+ def #{name}(query = {})
103
+ #{name}_association.all(query)
104
+ end
105
+
106
+ def #{name}=(children)
107
+ #{name}_association.replace(children)
108
+ end
109
+
110
+ private
111
+
112
+ def #{name}_association
113
+ @#{name}_association ||= begin
114
+ unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
115
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
116
+ end
117
+ association = Proxy.new(relationship, self)
118
+ parent_associations << association
119
+ association
120
+ end
121
+ end
122
+ EOS
123
+
124
+ opts = options.dup
125
+ opts.delete(:through)
126
+ opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
127
+ opts[:parent_model] = model
128
+ opts[:repository_name] = repository_name
129
+ opts[:remote_relationship_name] ||= opts.delete(:remote_name) || Extlib::Inflection.tableize(opts[:child_model])
130
+ opts[:parent_key] = opts[:parent_key]
131
+ opts[:child_key] = opts[:child_key]
132
+ opts[:mutable] = true
133
+
134
+ names = [ opts[:child_model].demodulize, opts[:parent_model].name.demodulize ].sort
135
+ model_name = names.join.gsub("::", "")
136
+ storage_name = Extlib::Inflection.tableize(Extlib::Inflection.pluralize(names[0]) + names[1])
137
+ model_module = model.to_s.split('::')
138
+ model_module.pop
139
+ model_module = model_module.join('::')
140
+ model_module = model_module.empty? ? Object : eval("::#{model_module}")
141
+
142
+ opts[:near_relationship_name] = Extlib::Inflection.tableize(model_name).to_sym
143
+
144
+ model.has(model.n, opts[:near_relationship_name], :old_behavior => true)
145
+
146
+ relationship = DataMapper::Associations::RelationshipChain.new(opts)
147
+ model.relationships(repository_name)[name] = relationship
148
+
149
+ unless model_module.const_defined?(model_name)
150
+ model = DataMapper::Model.new(storage_name)
151
+
152
+ model.class_eval <<-EOS, __FILE__, __LINE__
153
+ def self.name; #{model_name.inspect} end
154
+ def self.default_repository_name; #{repository_name.inspect} end
155
+ def self.many_to_many; true end
156
+ EOS
157
+
158
+ names.each do |n|
159
+ model.belongs_to(Extlib::Inflection.underscore(n).gsub("/", "_").to_sym, :class_name => n)
160
+ end
161
+
162
+ model_module.const_set(model_name, model)
163
+ end
164
+
165
+ relationship
166
+ end
167
+ end
168
+
169
+ #HACK Update methods in RelationshipChain to use the scoped repository.
170
+ #
171
+ # This hack will update methods to use the currently-scoped repository, instead
172
+ # of always using the default repository.
173
+
174
+ module DataMapper # :nodoc:
175
+ module Associations # :nodoc:
176
+ class RelationshipChain # :nodoc:
177
+ def near_relationship
178
+ parent_model.relationships(repository.name)[@near_relationship_name]
179
+ end
180
+
181
+ def remote_relationship
182
+ return nil unless near_relationship
183
+ near_relationship.child_model.relationships(repository.name)[@remote_relationship_name] ||
184
+ near_relationship.child_model.relationships(repository.name)[@remote_relationship_name.to_s.singularize.to_sym]
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ DataMapper::Model.class_eval do
191
+
192
+ #HACK Determine the child key from the given repository, not the default one.
193
+ #
194
+ # Updates this method to use the hacked child_key method.
195
+
196
+ def properties_with_subclasses(repository_name = default_repository_name)
197
+ properties = DataMapper::PropertySet.new
198
+ ([ self ].to_set + (respond_to?(:descendants) ? descendants : [])).each do |model|
199
+ model.relationships(repository_name).each_value { |relationship| relationship.child_key(repository_name) }
200
+ model.many_to_one_relationships.each do |relationship| relationship.child_key(repository_name) end
201
+ model.properties(repository_name).each do |property|
202
+ properties << property unless properties.has_property?(property.name)
203
+ end
204
+ end
205
+ properties
206
+ end
207
+ end
208
+
209
+ DataMapper::Associations::Relationship.class_eval do
210
+
211
+ #HACK Determine the child key from the given repository, not the default one.
212
+ #
213
+ # Updates this method to take a repository name. The child key will be
214
+ # determined from the properties scoped to the given repository.
215
+ #
216
+ # The @child_key class variable is changed to a hash that maps repository
217
+ # names to the appropriate key.
218
+
219
+ def child_key(repository_name=nil)
220
+ repository_name ||= repository.name
221
+ @child_key ||= Hash.new
222
+ @child_key[repository_name] ||= begin
223
+ child_key = nil
224
+ repository(repository_name).scope do |r|
225
+ model_properties = child_model.properties(repository_name)
226
+
227
+ child_key = parent_key(repository_name).zip(@child_properties || []).map do |parent_property,property_name|
228
+ # TODO: use something similar to DM::NamingConventions to determine the property name
229
+ parent_name = Extlib::Inflection.underscore(Extlib::Inflection.demodulize(parent_model.base_model.name))
230
+ property_name ||= "#{parent_name}_#{parent_property.name}".to_sym
231
+
232
+ if model_properties.has_property?(property_name)
233
+ model_properties[property_name]
234
+ else
235
+ options = {}
236
+
237
+ [ :length, :precision, :scale ].each do |option|
238
+ options[option] = parent_property.send(option)
239
+ end
240
+
241
+ # NOTE: hack to make each many to many child_key a true key,
242
+ # until I can figure out a better place for this check
243
+ if child_model.respond_to?(:many_to_many)
244
+ options[:key] = true
245
+ end
246
+
247
+ child_model.property(property_name, parent_property.primitive, options)
248
+ end
249
+ end
250
+ end
251
+ DataMapper::PropertySet.new(child_key)
252
+ end
253
+ return @child_key[repository_name]
254
+ end
255
+
256
+ #HACK Determine the parent key from the given repository, not the default one.
257
+ #
258
+ # Updates this method to take a repository name. The parent key will be
259
+ # determined from the properties scoped to the given repository.
260
+ #
261
+ # The @parent_key class variable is changed to a hash that maps repository
262
+ # names to the appropriate key.
263
+
264
+ def parent_key(repository_name=nil)
265
+ repository_name ||= repository.name
266
+ @parent_key ||= Hash.new
267
+ @parent_key[repository_name] ||= begin
268
+ parent_key = nil
269
+ repository(repository_name).scope do |r|
270
+ parent_key = if @parent_properties
271
+ parent_model.properties(repository_name).slice(*@parent_properties)
272
+ else
273
+ parent_model.key(repository_name)
274
+ end
275
+ end
276
+ DataMapper::PropertySet.new(parent_key)
277
+ end
278
+ return @parent_key[repository_name]
279
+ end
280
+ end
281
+
282
+ # Add a method to return all models defined for a repository.
283
+
284
+ DataMapper::Repository.class_eval do
285
+ def models
286
+ DataMapper::Resource.descendants.select { |cl| not cl.properties(name).empty? or not cl.relationships(name).empty? }
287
+ #HACK we are assuming that if a model has properties or relationships
288
+ # defined for a repository, then it must be contextual to that repo
289
+ end
290
+ end
@@ -0,0 +1,231 @@
1
+ # Defines the Autumn::Foliater class, which instantiates stems and leaves and
2
+ # keeps watch over their threads.
3
+
4
+ module Autumn
5
+
6
+ # Loads Stems and Leaves and executes them in their own threads. Manages the
7
+ # threads and oversees all leaves. This is a singleton class.
8
+
9
+ class Foliater
10
+ include Singleton
11
+
12
+ # The Speciator singleton.
13
+ attr_reader :config
14
+ # A hash of all Stem instances by their config names.
15
+ attr_reader :stems
16
+ # A hash of all Leaf instances by their config names.
17
+ attr_reader :leaves
18
+
19
+ def initialize # :nodoc:
20
+ @config = Speciator.instance
21
+ @stems = Hash.new
22
+ @leaves = Hash.new
23
+ @ctcp = Autumn::CTCP.new
24
+ end
25
+
26
+ # Loads the config files and their classes, initializes all stems and leaves
27
+ # and begins the stems' execution processes in their own threads. You must
28
+ # pass the stem and leaf config hashes (from the stems.yml and leaves.yml
29
+ # files).
30
+ #
31
+ # If +invoke+ is set to false, start_stems will not be called.
32
+
33
+ def load(stem_config, leaf_config, invoke=true)
34
+ load_configs stem_config, leaf_config
35
+ load_leaf_classes
36
+ load_leaves
37
+ load_all_leaf_models
38
+ load_stems
39
+ start_stems if invoke
40
+ end
41
+
42
+ # Reloads a leaf while it is running. Re-opens class definition files and
43
+ # runs them to redefine the classes. Does not work exactly as it should,
44
+ # but well enough for a rough hot-reload capability.
45
+
46
+ def hot_reload(leaf)
47
+ type = leaf.class.to_s.split('::').first
48
+ load_leaf_controller type
49
+ load_leaf_helpers type
50
+ load_leaf_models leaf
51
+ load_leaf_views type
52
+ end
53
+
54
+ # Returns true if there is at least one stem still running.
55
+
56
+ def alive?
57
+ @stem_threads and @stem_threads.any? { |name, thread| thread.alive? }
58
+ end
59
+
60
+ # This method yields each Stem that was loaded, allowing you to iterate over
61
+ # each stem. For instance, to take attendance:
62
+ #
63
+ # Foliater.instance.each_stem { |stem| stem.message "Here!" }
64
+
65
+ def each_stem
66
+ @leaves.each { |leaf| yield leaf }
67
+ end
68
+
69
+ # This method yields each Leaf subclass that was loaded, allowing you to
70
+ # iterate over each leaf. For instance, to take attendance:
71
+ #
72
+ # Foliater.instance.each_leaf { |leaf| leaf.stems.message "Here!" }
73
+
74
+ def each_leaf
75
+ @leaves.each { |leaf| yield leaf }
76
+ end
77
+
78
+ private
79
+
80
+ def load_configs(stem_config, leaf_config)
81
+ leaf_config.each do |name, options|
82
+ global_config_file = "#{APP_ROOT}/leaves/#{options['class'].snakecase}/config.yml"
83
+ if File.exist? global_config_file then
84
+ config.leaf name, YAML.load(File.open(global_config_file))
85
+ end
86
+ config.leaf name, options
87
+ config.leaf name, :logger => LogFacade.new(config.global(:logfile), 'Leaf', name)
88
+ end
89
+ stem_config.each do |name, options|
90
+ config.stem name, options
91
+ config.stem name, :logger => LogFacade.new(config.global(:logfile), 'Stem', name)
92
+ end
93
+ end
94
+
95
+ def load_leaf_classes
96
+ config.all_leaf_classes.each do |type|
97
+ Object.class_eval "module #{type}; end"
98
+
99
+ config.leaf type, :module => Object.const_get(type)
100
+
101
+ load_leaf_controller(type)
102
+ load_leaf_helpers(type)
103
+ load_leaf_views(type)
104
+ end
105
+ end
106
+
107
+ def load_leaf_controller(type)
108
+ controller_file = "#{APP_ROOT}/leaves/#{type.snakecase}/controller.rb"
109
+ raise "controller.rb file for leaf #{type} not found" unless File.exist? controller_file
110
+ controller_code = nil
111
+ begin
112
+ File.open("#{APP_ROOT}/leaves/#{type.snakecase}/controller.rb", 'r') { |f| controller_code = f.read }
113
+ rescue Errno::ENOENT
114
+ raise "controller.rb file for leaf #{type} not found"
115
+ end
116
+ config.leaf(type, :module).module_eval controller_code
117
+ end
118
+
119
+ def load_leaf_helpers(type)
120
+ mod = config.leaf(type, :module)
121
+ helper_code = nil
122
+ Dir.glob("#{APP_ROOT}/leaves/#{type.snakecase}/helpers/*.rb").each do |helper_file|
123
+ File.open(helper_file, 'r') { |f| helper_code = f.read }
124
+ mod.module_eval helper_code
125
+ end
126
+
127
+ leaf_class = nil
128
+ begin
129
+ leaf_class = mod.const_get('Controller')
130
+ rescue NameError
131
+ raise NameError, "Couldn't find Controller class for leaf #{type}"
132
+ end
133
+
134
+ config.leaf type, :helpers => Set.new
135
+ mod.constants.select { |const_name| const_name =~ /Helper$/ }.map { |helper_name| mod.const_get helper_name }.each do |helper|
136
+ config.leaf(type, :helpers) << helper
137
+ end
138
+ end
139
+
140
+ def load_leaf_views(type)
141
+ views = Hash.new
142
+ view_text = nil
143
+ Dir.glob("#{APP_ROOT}/leaves/#{type.snakecase}/views/*.txt.erb").each do |view_file|
144
+ view_name = File.basename(view_file).match(/^(.+)\.txt\.erb$/)[1]
145
+ File.open(view_file, 'r') { |f| view_text = f.read }
146
+ views[view_name] = view_text
147
+ end
148
+ config.leaf type, :views => views
149
+ end
150
+
151
+ def load_leaves
152
+ config.each_leaf do |name, options|
153
+ options = config.options_for_leaf(name)
154
+ options[:root] = "#{config.global :root}/leaves/#{options[:class].snakecase}"
155
+ begin
156
+ leaf_class = options[:module].const_get('Controller')
157
+ rescue NameError
158
+ raise NameError, "Couldn't find Controller class for leaf #{name}"
159
+ end
160
+ @leaves[name] = leaf_class.new(options)
161
+ formatter = Autumn::Formatting.const_get options[:formatter].to_sym if options[:formatter] and (Autumn::Formatting.constants.include? options[:formatter] or Autumn::Formatting.constants.include? options[:formatter].to_sym)
162
+ formatter ||= Autumn::Formatting::DEFAULT
163
+ @leaves[name].extend formatter
164
+ options[:helpers].each { |helper| @leaves[name].extend helper }
165
+ # extend the formatter first so helper methods override its methods if necessary
166
+ end
167
+ end
168
+
169
+ def load_all_leaf_models
170
+ @leaves.each { |name, instance| load_leaf_models instance }
171
+ end
172
+
173
+ def load_leaf_models(leaf)
174
+ model_code = nil
175
+ mod = config.leaf(leaf.options[:class], :module)
176
+ leaf.database do
177
+ Dir.glob("#{APP_ROOT}/leaves/#{leaf.options[:class].snakecase}/models/*.rb").each do |model_file|
178
+ File.open(model_file, 'r') { |f| model_code = f.read }
179
+ mod.module_eval model_code
180
+ end
181
+ # Need to manually set the table names of the models because we loaded
182
+ # them inside a module
183
+ unless $NO_DATABASE
184
+ mod.constants.map { |const_name| mod.const_get(const_name) }.select { |const| const.ancestors.include? DataMapper::Resource }.each do |model|
185
+ model.storage_names[leaf.database_name] = model.to_s.demodulize.snakecase.pluralize
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def load_stems
192
+ config.each_stem do |name, options|
193
+ options = config.options_for_stem(name)
194
+ server = options[:server]
195
+ nick = options[:nick]
196
+
197
+ @stems[name] = Stem.new(server, nick, options)
198
+ leaves = options[:leaves]
199
+ leaves ||= [ options[:leaf] ]
200
+ leaves.each do |leaf|
201
+ raise "Unknown leaf #{leaf} in configuration for stem #{name}" unless @leaves[leaf]
202
+ @stems[name].add_listener @leaves[leaf]
203
+ @stems[name].add_listener @ctcp
204
+ #TODO a configurable way of specifying listeners to add by default
205
+ @leaves[leaf].stems << @stems[name]
206
+ end
207
+ end
208
+ end
209
+
210
+ def start_stems
211
+ @leaves.each { |name, leaf| leaf.preconfigure }
212
+ @leaves.each { |name, leaf| leaf.will_start_up }
213
+ @stem_threads = Hash.new
214
+ config.each_stem do |name, options|
215
+ @stem_threads[name] = Thread.new(@stems[name], Thread.current) do |stem, parent_thread|
216
+ # The thread will run the stem until it exits, then inform the main
217
+ # thread that it has exited. When the main thread wakes, it checks if
218
+ # all stems have terminated; if so, it terminates itself.
219
+ begin
220
+ stem.start
221
+ rescue
222
+ options[:logger].fatal $!
223
+ ensure
224
+ parent_thread.wakeup # Schedule the parent thread to wake up after this thread finishes
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ end
231
+ end