comboy-autumn 3.1

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