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']