lib/roby/app.rb in roby-0.7.3 vs lib/roby/app.rb in roby-0.8.0

- old
+ new

@@ -1,18 +1,7 @@ -require 'roby' -require 'roby/distributed' -require 'roby/planning' -require 'roby/log' -require 'roby/log/event_stream' - -require 'roby/robot' -require 'yaml' - +require 'singleton' module Roby - # Returns the only one Application object - def self.app; Application.instance end - # = Roby Applications # # There is one and only one Application object, which holds mainly the # system-wide configuration and takes care of file loading and system-wide # setup (#setup). A Roby application can be started in multiple modes. The @@ -54,10 +43,13 @@ # == Testing mode (<tt>scripts/test</tt>) # This mode is used to run test suites in the +test+ directory. See # Roby::Test::TestCase for a description of Roby-specific tests. class Application include Singleton + + # A set of planners declared in this application + attr_reader :planners # The plain option hash saved in config/app.yml attr_reader :options # Logging options. @@ -68,10 +60,13 @@ # Roby: FATAL # Roby::Distributed: INFO # dir:: the log directory. Uses APP_DIR/log if not set # filter_backtraces:: true if the framework code should be removed from the error backtraces attr_reader :log + + # ExecutionEngine setup + attr_reader :engine # A [name, dir, file, module] array of available plugins, where 'name' # is the plugin name, 'dir' the directory in which it is installed, # 'file' the file which should be required to load the plugin and # 'module' the Application-compatible module for configuration of the @@ -86,16 +81,14 @@ # period:: the period of neighbour discovery # max_errors:: disconnect from a peer if there is more than +max_errors+ consecutive errors # detected attr_reader :droby - # Configuration of the control loop - # abort_on_exception:: if the control loop should abort if an uncaught task or event exception is received. Defaults - # to false - # abort_on_application_exception:: if the control should abort if an uncaught application exception (not originating - # from a task or event) is caught. Defaults to true. - attr_reader :control + # If true, abort if an unhandled exception is found + attr_predicate :abort_on_exception, true + # If true, abort if an application exception is found + attr_predicate :abort_on_application_exception, true # An array of directories in which to search for plugins attr_reader :plugin_dirs # True if user interaction is disabled during tests @@ -115,17 +108,17 @@ @plugins = Array.new @available_plugins = Array.new @log = Hash['events' => 'stats', 'levels' => Hash.new, 'filter_backtraces' => true] @discovery = Hash.new @droby = Hash['period' => 0.5, 'max_errors' => 1] - @control = Hash[ 'abort_on_exception' => false, - 'abort_on_application_exception' => true ] + @engine = Hash.new @automatic_testing = true @testing_keep_logs = false @plugin_dirs = [] + @planners = [] end # Adds +dir+ in the list of directories searched for plugins def plugin_dir(dir) dir = File.expand_path(dir) @@ -205,11 +198,11 @@ end end @options = options - load_option_hashes(options, %w{log control discovery droby}) + load_option_hashes(options, %w{log engine discovery droby}) call_plugins(:load, self, options) end def load_option_hashes(options, names) names.each do |optname| @@ -347,11 +340,11 @@ Robot.logger.formatter = Roby.logger.formatter Robot.logger.progname = robot_name # Set up log levels log['levels'].each do |name, value| - name = name.camelize + name = name.camelcase(true) if value =~ /^(\w+):(.+)$/ level, file = $1, $2 level = Logger.const_get(level) file = file.gsub('ROBOT', robot_name) if robot_name else @@ -365,23 +358,22 @@ else Logger.new(STDOUT) end new_logger.level = level new_logger.formatter = Roby.logger.formatter - if (mod = name.constantize rescue nil) - if robot_name - new_logger.progname = "#{name} #{robot_name}" - else - new_logger.progname = name - end - mod.logger = new_logger - end + mod = Kernel.constant(name) + if robot_name + new_logger.progname = "#{name} #{robot_name}" + else + new_logger.progname = name + end + mod.logger = new_logger end end def setup_dirs - Dir.mkdir(log_dir) unless File.exists?(log_dir) + FileUtils.mkdir_p(log_dir) unless File.exists?(log_dir) if File.directory?(libdir = File.join(APP_DIR, 'lib')) if !$LOAD_PATH.include?(libdir) $LOAD_PATH.unshift File.join(APP_DIR, 'lib') end end @@ -395,40 +387,44 @@ # Loads the models, based on the given robot name and robot type def require_models # Require all common task models and the task models specific to # this robot - require_dir(File.join(APP_DIR, 'tasks')) - require_robotdir(File.join(APP_DIR, 'tasks', 'ROBOT')) + list_dir('tasks') { |p| require(p) } + list_robotdir('tasks', 'ROBOT') { |p| require(p) } # Load robot-specific configuration - planner_dir = File.join(APP_DIR, 'planners') - models_search = [planner_dir] + models_search = ['planners'] if robot_name - load_robotfile(File.join(APP_DIR, 'config', "ROBOT.rb")) + models_search << File.join('planners', robot_name) << File.join('planners', robot_type) + file = robotfile('planners', 'ROBOT', 'main.rb') + end + file ||= File.join("planners", "main") + require file if File.file?(file) - models_search << File.join(planner_dir, robot_name) << File.join(planner_dir, robot_type) - if !require_robotfile(File.join(APP_DIR, 'planners', 'ROBOT', 'main.rb')) - require File.join(APP_DIR, "planners", "main") - end - else - require File.join(APP_DIR, "planners", "main") - end - # Load the other planners models_search.each do |base_dir| next unless File.directory?(base_dir) Dir.new(base_dir).each do |file| if File.file?(file) && file =~ /\.rb$/ && file !~ 'main\.rb$' require file end end end + + # Set up the loaded plugins + call_plugins(:require_models, self) end def setup + if !Roby.plan + Roby.instance_variable_set :@plan, Plan.new + end + reset + require 'roby/planning' + require 'roby/interface' $LOAD_PATH.unshift(APP_DIR) unless $LOAD_PATH.include?(APP_DIR) # Get the application-wide configuration file = File.join(APP_DIR, 'config', 'app.yml') @@ -446,18 +442,25 @@ unless Object.const_defined?(:Application) Object.const_set(:Application, Roby::Application) Object.const_set(:State, Roby::State) end + # Set up the loaded plugins + call_plugins(:setup, self) + require_models + if file = robotfile(APP_DIR, 'config', "ROBOT.rb") + load file + end + + # MainPlanner is always included in the planner list - Roby.control.planners << MainPlanner + if defined? MainPlanner + self.planners << MainPlanner + end - # Set up the loaded plugins - call_plugins(:setup, self) - # If we are in test mode, import the test extensions from plugins if testing? require 'roby/test/testcase' each_plugin do |mod| if mod.const_defined?(:Test) @@ -466,21 +469,23 @@ end end end def run(&block) + setup_global_singletons + # Set up dRoby, setting an Interface object as front server, for shell access host = droby['host'] || "" if host !~ /:\d+$/ host << ":#{Distributed::DEFAULT_DROBY_PORT}" end if single? || !robot_name host =~ /:(\d+)$/ - DRb.start_service "druby://:#{$1 || '0'}", Interface.new(Roby.control) + DRb.start_service "druby://:#{$1 || '0'}", Interface.new(Roby.engine) else - DRb.start_service "druby://#{host}", Interface.new(Roby.control) + DRb.start_service "druby://#{host}", Interface.new(Roby.engine) droby_config = { :ring_discovery => !!discovery['ring'], :name => robot_name, :plan => Roby.plan, :period => discovery['period'] || 0.5 } @@ -490,39 +495,30 @@ Roby::Distributed.state = Roby::Distributed::ConnectionSpace.new(droby_config) if discovery['ring'] Roby::Distributed.publish discovery['ring'] end - Roby::Control.every(discovery['period'] || 0.5) do + Roby.every(discovery['period'] || 0.5) do Roby::Distributed.state.start_neighbour_discovery end end @robot_name ||= 'common' @robot_type ||= 'common' - control_config = self.control - control = Roby.control - options = { :detach => true, :cycle => control_config['cycle'] || 0.1 } + engine_config = self.engine + engine = Roby.engine + options = { :cycle => engine_config['cycle'] || 0.1 } - # Add an executive if one is defined - if control_config['executive'] - self.executive = control_config['executive'] - end - if log['events'] require 'roby/log/file' logfile = File.join(log_dir, robot_name) - logger = Roby::Log::FileLogger.new(logfile) + logger = Roby::Log::FileLogger.new(logfile, :plugins => plugins.map { |n, _| n }) logger.stats_mode = log['events'] == 'stats' Roby::Log.add_logger logger end - control.abort_on_exception = - control_config['abort_on_exception'] - control.abort_on_application_exception = - control_config['abort_on_application_exception'] - control.run options + engine.run options plugins = self.plugins.map { |_, mod| mod if mod.respond_to?(:run) }.compact run_plugins(plugins, &block) rescue Exception => e @@ -531,47 +527,32 @@ else pp e.full_message end end def run_plugins(mods, &block) - control = Roby.control + engine = Roby.engine if mods.empty? yield - control.join + engine.join else mod = mods.shift mod.run(self) do run_plugins(mods, &block) end end rescue Exception => e - if Roby.control.running? - control.quit - control.join + if Roby.engine.running? + engine.quit + engine.join raise e, e.message, e.backtrace else raise end end - attr_reader :executive - - def executive=(name) - if executive - Control.event_processing.delete(executive.method(:initial_events)) - @executive = nil - end - return unless name - - full_name = "roby/executives/#{name}" - require full_name - @executive = full_name.camelize.constantize.new - Control.event_processing << executive.method(:initial_events) - end - def stop; call_plugins(:stop, self) end DISCOVERY_TEMPLATE = [:droby, nil, nil] # Starts services needed for distributed operations. These services are @@ -581,11 +562,11 @@ # #start_server def start_distributed Thread.abort_on_exception = true if !File.exists?(log_dir) - Dir.mkdir(log_dir) + FileUtils.mkdir_p(log_dir) end unless single? || !discovery['tuplespace'] ts = Rinda::TupleSpace.new @@ -676,56 +657,53 @@ end call_plugins(:stop_server, self) end - # Require all files in +dirname+ - def require_dir(dirname) + def list_dir(*path) + if !block_given? + return enum_for(:list_dir, *path) + end + + dirname = File.join(*path) Dir.new(dirname).each do |file| file = File.join(dirname, file) - file = file.gsub(/^#{Regexp.quote(APP_DIR)}\//, '') - require file if file =~ /\.rb$/ && File.file?(file) + if file =~ /\.rb$/ && File.file?(file) + file = file.gsub(/^#{Regexp.quote(APP_DIR)}\//, '') + yield(file) + end end - end + end # Require all files in the directories matching +pattern+. If +pattern+ # contains the word ROBOT, it is replaced by -- in order -- the robot # name and then the robot type - def require_robotdir(pattern) + def list_robotdir(*path, &block) + if !block_given? + return enum_for(:list_robotdir, *path) + end + return unless robot_name && robot_type - [robot_name, robot_type].each do |name| + pattern = File.expand_path(File.join(*path), APP_DIR) + [robot_name, robot_type].uniq.each do |name| dirname = pattern.gsub(/ROBOT/, name) - require_dir(dirname) if File.directory?(dirname) + list_dir(dirname, &block) if File.directory?(dirname) end end - # Loads the first file found matching +pattern+ - # - # See #require_robotfile - def load_robotfile(pattern) - require_robotfile(pattern, :load) - end - - # Requires or loads (according to the value of +method+) the first file - # found matching +pattern+. +pattern+ can contain the word ROBOT, in - # which case the file is first checked against the robot name and then - # against the robot type - def require_robotfile(pattern, method = :require) + def robotfile(*path) # :nodoc return unless robot_name && robot_type + pattern = File.join(*path) robot_config = pattern.gsub(/ROBOT/, robot_name) if File.file?(robot_config) - Kernel.send(method, robot_config) - true + robot_config else robot_config = pattern.gsub(/ROBOT/, robot_type) if File.file?(robot_config) - Kernel.send(method, robot_config) - true - else - false + robot_config end end end attr_predicate :simulation?, true @@ -736,10 +714,38 @@ attr_predicate :shell?, true def shell; self.shell = true end def single?; @single || discovery.empty? end def single; @single = true end + def setup_global_singletons + if !Roby.plan + Roby.instance_variable_set :@plan, Plan.new + end + + if !Roby.engine && Roby.plan.engine + # This checks coherence with Roby.control, and sets it + # accordingly + Roby.engine = Roby.plan.engine + elsif !Roby.control + Roby.control = DecisionControl.new + end + + if !Roby.engine + Roby.engine = ExecutionEngine.new(Roby.plan, Roby.control) + end + + if Roby.control != Roby.engine.control + raise "inconsistency between Roby.control and Roby.engine.control" + elsif Roby.engine != Roby.plan.engine + raise "inconsistency between Roby.engine and Roby.plan.engine" + end + + if !Roby.engine.scheduler && Roby.scheduler + Roby.engine.scheduler = Roby.scheduler + end + end + # Guesses the type of +filename+ if it is a source suitable for # data display in this application def data_streams_of(filenames) if filenames.size == 1 path = filenames.first @@ -853,9 +859,23 @@ rescue Exception => e STDERR.puts e.full_message end end end + end + + @app = Application.instance + class << self + # The one and only Application object + attr_reader :app + + # The scheduler object to be used during execution. See + # ExecutionEngine#scheduler. + # + # This is only used during the configuration of the application, and + # not afterwards. It is also possible to set per-engine through + # ExecutionEngine#scheduler= + attr_accessor :scheduler end # Load the plugins 'main' files Roby.app.plugin_dir File.join(ROBY_ROOT_DIR, 'plugins') if plugin_path = ENV['ROBY_PLUGIN_PATH']