module Adhearsion
module Components
class Manager
attr_reader :active_components, :host_information, :started_components, :components_with_call_context
def initialize
@active_components = {}
@started_components = []
@components_with_call_context = {}
end
def [](component_name)
active_components[component_name].component_class.instance
end
def component(component_name)
active_components[component_name]
end
def has_component?(component_name)
active_components.has_key?(component_name)
end
def component_gems
@repository ||= RubygemsRepository.new
@repository.adhearsion_gems
end
def load
return unless File.exist?(AHN_ROOT.component_path)
component_directories = Dir.glob(File.join(AHN_ROOT.component_path, "*"))
component_directories.each do |component_directory|
component_name = File.basename(component_directory).to_sym
@active_components[component_name] = Component.new(self, component_name, component_directory)
end
end
def start
@active_components.keys.each do |name|
@active_components[name].start
end
end
def stop
@started_components.reverse.each do |name|
@active_components[name].stop
end
end
class RubygemsRepository
def initialize
require 'rubygems'
Gem.manage_gems
end
def adhearsion_gems
gems = {}
Gem.source_index.each {|name, spec| gems[spec.name] = spec.full_gem_path if spec.requirements.include?("adhearsion")}
gems
end
end
end
ClassToGetCallContext = Struct.new(:component_class, :instance_variable)
class ClassToGetCallContext
def instantiate_with_call_context(call_context, *args, &block)
component = component_class.allocate
component.instance_variable_set(("@"+instance_variable.to_s).to_sym, call_context)
component.send(:initialize, *args, &block)
component
end
end
#component behavior is shared across components
module Behavior
def self.included(component_class)
component_class.extend(ClassMethods)
end
def component_name
Component.name
end
def component_description
Configuration.description || Component.name
end
module ClassMethods
def add_call_context(params = {:as => :call_context})
attr_reader params[:as]
ComponentManager.components_with_call_context[name] = ClassToGetCallContext.new(self, params[:as])
end
end
end
class Component
class << self
def prepare_component_class(component_module, name)
component_class_name = name.to_s.camelize
component_module.module_eval(<<-EVAL, __FILE__, __LINE__)
class #{component_class_name}
def self.name
'#{component_class_name}'
end
include Adhearsion::Components::Behavior
end
EVAL
end
end
attr_reader :manager, :name, :path, :component_module, :component_class
def initialize(manager, name, path)
@manager = manager
@name = name
@path = path
unless File.exist?(main_file_name)
gem_path = @manager.component_gems[@name.to_s]
raise "The component '#{@name}' does not have the main file: #{main_file_name}" unless gem_path
@path = gem_path
end
@started = false
end
def start
return if @started
manager.started_components << @name
@started = true
@component_module = ComponentModule.new(self) do |component_module|
Component.prepare_component_class(component_module, @name)
component_module.load_configuration_file
end
@component_module.require(File.join("lib", @name.to_s))
end
def configuration
@component_module.const_get(:Configuration)
end
def stop
#@component_class.unload if @component_class && @component_class.respond_to?(:unload)
end
def configuration_file
File.join(path, configuration_file_name)
end
private
def main_file_name
File.join(@path, "lib", @name.to_s+".rb")
end
def configuration_file_name
"configuration.rb"
end
end
class ComponentModule < Module
# The file with which the Script was instantiated.
attr_reader :main_file
# The directory in which main_file is located, and relative to which
# #load searches for files before falling back to Kernel#load.
attr_reader :dir
# A hash that maps filename=>true for each file that has been
# required locally by the script. This has the same semantics as $",
# alias $LOADED_FEATURES, except that it is local to this script.
attr_reader :loaded_features
class << self
alias load new
end
# Creates new Script, and loads _main_file_ in the scope of the Script. If a
# block is given, the script is passed to it before loading from the file, and
# constants can be defined as inputs to the script.
attr_reader :component
def initialize(component) # :yields: self
extend ComponentModuleMethods
@component = component
@loaded_features = {}
const_set :Component, component
const_set :Configuration, OpenStruct.new(:description => nil)
yield self if block_given?
end
def load_configuration_file
load_in_module(component.configuration_file)
end
# Loads _file_ into this Script. Searches relative to the local dir, that is,
# the dir of the file given in the original call to
# Script.load(file), loads the file, if found, into this Script's
# scope, and returns true. If the file is not found, falls back to
# Kernel.load, which searches on $LOAD_PATH, loads the file,
# if found, into global scope, and returns true. Otherwise, raises
# LoadError.
#
# The _wrap_ argument is passed to Kernel.load in the fallback case,
# when the file is not found locally.
#
# Typically called from within the main file to load additional sub files, or
# from those sub files.
def load(file, wrap = false)
load_in_module(File.join(component.path, file))
true
rescue MissingFile
super
end
# Analogous to Kernel#require. First tries the local dir, then falls
# back to Kernel#require. Will load a given _feature_ only once.
#
# Note that extensions (*.so, *.dll) can be required in the global scope, as
# usual, but not in the local scope. (This is not much of a limitation in
# practice--you wouldn't want to load an extension more than once.) This
# implementation falls back to Kernel#require when the argument is an
# extension or is not found locally.
def require(feature)
unless @loaded_features[feature]
@loaded_features[feature] = true
file = feature
file += ".rb" unless /\.rb$/ =~ file
load_in_module(File.join(component.path, file))
end
rescue MissingFile
@loaded_features[feature] = false
super
end
# Raised by #load_in_module, caught by #load and #require.
class MissingFile < LoadError; end
# Loads _file_ in this module's context. Note that \_\_FILE\_\_ and
# \_\_LINE\_\_ work correctly in _file_.
# Called by #load and #require; not normally called directly.
def load_in_module(file)
module_eval(File.read(file), File.expand_path(file))
rescue Errno::ENOENT => e
if /#{file}$/ =~ e.message
raise MissingFile, e.message
else
raise
end
end
def to_s
"#<#{self.class}:#{File.basename(component.path)}>"
end
module ComponentModuleMethods
# This is so that def meth... behaves like in Ruby's top-level
# context. The implementation simply calls
# Module#module_function(name).
def method_added(name) # :nodoc:
module_function(name)
end
def start_component_after(*others)
others.each do |component_name|
component.manager.active_components[component_name].start
end
end
end
end
end
ComponentManager = Components::Manager.new unless defined? ComponentManager
end