require 'roby'
require 'roby/distributed'
require 'roby/planning'
require 'roby/log'
require 'roby/log/event_stream'
require 'roby/robot'
require 'yaml'
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
# first and most important mode is the runtime mode
# (scripts/run). Other modes are the testing mode (#testing?
# returns true, entered through scripts/test) and the shell mode
# (#shell? returns true, entered through scripts/shell). Usually,
# user code does not have to take the modes into account, but it is
# sometime useful.
#
# Finally, in both testing and runtime mode, the code can be started in
# simulation or live setups (see #simulation?). Specific plugins can for
# instance start and set up a simulation system in simulation mode, and as
# well set up some simulation-specific configuration for the functional
# layer of the architecture.
#
# == Configuration files
#
# In all modes, a specific set of configuration files are loaded. The
# files that are actually loaded are defined by the robot name and type, as
# specified to #robot. The loaded files are, in order, the following:
# [config/app.yml]
# the application configuration as a YAML file. See the comments in that
# file for more details.
# [config/init.rb]
# Ruby code for the common configuration of all robots
# [config/ROBOT_NAME.rb or config/ROBOT_TYPE.rb]
# Ruby code for the configuration of either all robots of the same type,
# or a specific robot. It is one or the other. If a given robot needs to
# inherit the configuration of its type, explicitely require the
# ROBOT_TYPE.rb file in config/ROBOT_NAME.rb.
#
# == Runtime mode (scripts/run)
# Then, in runtime mode the robot controller
# controller/ROBOT_NAME.rb or controller/ROBOT_TYPE.rb is
# loaded. The same rules than for the configuration file
# config/ROBOT_NAME.rb apply.
#
# == Testing mode (scripts/test)
# 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
# The plain option hash saved in config/app.yml
attr_reader :options
# Logging options.
# events:: save a log of all events in the system. This log can be read using scripts/replay
# If this value is 'stats', only the data necessary for timing statistics is saved.
# levels:: a component => level hash of the minimum level of the messages that
# should be displayed on the console. The levels are DEBUG, INFO, WARN and FATAL.
# 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
# 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
# plug-in
attr_reader :available_plugins
# An [name, module] array of the loaded plugins
attr_reader :plugins
# The discovery options in multi-robot mode
attr_reader :discovery
# The robot's dRoby options
# 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
# An array of directories in which to search for plugins
attr_reader :plugin_dirs
# True if user interaction is disabled during tests
attr_predicate :automatic_testing?, true
# True if all logs should be kept after testing
attr_predicate :testing_keep_logs?, true
# True if all logs should be kept after testing
attr_predicate :testing_overwrites_logs?, true
# True if we should remove the framework code from the error backtraces
def filter_backtraces?; log['filter_backtraces'] end
def filter_backtraces=(value); log['filter_backtraces'] = value end
def initialize
@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 ]
@automatic_testing = true
@testing_keep_logs = false
@plugin_dirs = []
end
# Adds +dir+ in the list of directories searched for plugins
def plugin_dir(dir)
dir = File.expand_path(dir)
@plugin_dirs << dir
$LOAD_PATH.unshift File.expand_path(dir)
Dir.new(dir).each do |subdir|
subdir = File.join(dir, subdir)
next unless File.directory?(subdir)
appfile = File.join(subdir, "app.rb")
next unless File.file?(appfile)
begin
require appfile
rescue
Roby.warn "cannot load plugin in #{subdir}: #{$!.full_message}\n"
end
Roby.info "loaded plugin in #{subdir}"
end
end
# Returns true if +name+ is a loaded plugin
def loaded_plugin?(name)
plugins.any? { |plugname, _| plugname == name }
end
# Returns the [name, dir, file, module] array definition of the plugin
# +name+, or nil if +name+ is not a known plugin
def plugin_definition(name)
available_plugins.find { |plugname, *_| plugname == name }
end
# True if +name+ is a plugin known to us
def defined_plugin?(name)
available_plugins.any? { |plugname, *_| plugname == name }
end
def each_plugin(on_available = false)
plugins = self.plugins
if on_available
plugins = available_plugins.map { |name, _, mod, _| [name, mod] }
end
plugins.each do |_, mod|
yield(mod)
end
end
# Yields each extension modules that respond to +method+
def each_responding_plugin(method, on_available = false)
each_plugin do |mod|
yield(mod) if mod.respond_to?(method)
end
end
# Call +method+ on each loaded extension module which define it, with
# arguments +args+
def call_plugins(method, *args)
each_responding_plugin(method) do |config_extension|
config_extension.send(method, *args)
end
end
# Load configuration from the given option hash
def load_yaml(options)
options = options.dup
if robot_name && (robot_config = options['robots'])
if robot_config = robot_config[robot_name]
robot_config.each do |section, values|
if options[section]
options[section].merge! values
else
options[section] = values
end
end
options.delete('robots')
end
end
@options = options
load_option_hashes(options, %w{log control discovery droby})
call_plugins(:load, self, options)
end
def load_option_hashes(options, names)
names.each do |optname|
if options[optname]
send(optname).merge! options[optname]
end
end
end
# Loads the plugins whose name are listed in +names+
def using(*names)
names.each do |name|
name = name.to_s
unless plugin = plugin_definition(name)
raise ArgumentError, "#{name} is not a known plugin (#{available_plugins.map { |n, *_| n }.join(", ")})"
end
name, dir, mod, init = *plugin
if plugins.find { |n, m| n == name && m == mod }
next
end
if init
begin
$LOAD_PATH.unshift dir
init.call
mod.reset(self) if mod.respond_to?(:reset)
rescue Exception => e
Roby.fatal "cannot load plugin #{name}: #{e.full_message}"
exit(1)
ensure
$LOAD_PATH.shift
end
end
plugins << [name, mod]
extend mod
# If +load+ has already been called, call it on the module
if mod.respond_to?(:load) && options
mod.load(self, options)
end
end
end
def reset
if defined? State
State.clear
else
Roby.const_set(:State, StateSpace.new)
end
call_plugins(:reset, self)
end
# The robot name
attr_reader :robot_name
# The robot type
attr_reader :robot_type
# Sets up the name and type of the robot. This can be called only once
# in a given Roby controller.
def robot(name, type = name)
if @robot_name
if name != @robot_name && type != @robot_type
raise ArgumentError, "the robot is already set to #{name}, of type #{type}"
end
return
end
@robot_name = name
@robot_type = type
end
# The directory in which logs are to be saved
# Defaults to APP_DIR/log
def log_dir
File.expand_path(log['dir'] || 'log', APP_DIR)
end
# A path => File hash, to re-use the same file object for different
# logs
attribute(:log_files) { Hash.new }
# The directory in which results should be saved
# Defaults to APP_DIR/results
def results_dir
File.expand_path(log['results'] || 'results', APP_DIR)
end
# Returns a unique directory name as a subdirectory of
# +base_dir+, based on +path_spec+. The generated name
# is of the form
# /a/b/c/YYYYMMDD-basename
# if path_spec = "a/b/c/basename". A . suffix
# is appended if the path already exists.
def self.unique_dirname(base_dir, path_spec)
if path_spec =~ /\/$/
basename = ""
dirname = path_spec
else
basename = File.basename(path_spec)
dirname = File.dirname(path_spec)
end
date = Date.today
date = "%i%02i%02i" % [date.year, date.month, date.mday]
if basename && !basename.empty?
basename = date + "-" + basename
else
basename = date
end
# Check if +basename+ already exists, and if it is the case add a
# .x suffix to it
full_path = File.expand_path(File.join(dirname, basename), base_dir)
base_dir = File.dirname(full_path)
unless File.exists?(base_dir)
FileUtils.mkdir_p(base_dir)
end
final_path, i = full_path, 0
while File.exists?(final_path)
i += 1
final_path = full_path + ".#{i}"
end
final_path
end
# Sets up all the default loggers. It creates the logger for the Robot
# module (accessible through Robot.logger), and sets up log levels as
# specified in the config/app.yml file.
def setup_loggers
# Create the robot namespace
STDOUT.sync = true
Robot.logger = Logger.new(STDOUT)
Robot.logger.level = Logger::INFO
Robot.logger.formatter = Roby.logger.formatter
Robot.logger.progname = robot_name
# Set up log levels
log['levels'].each do |name, value|
name = name.camelize
if value =~ /^(\w+):(.+)$/
level, file = $1, $2
level = Logger.const_get(level)
file = file.gsub('ROBOT', robot_name) if robot_name
else
level = Logger.const_get(value)
end
new_logger = if file
path = File.expand_path(file, log_dir)
io = (log_files[path] ||= File.open(path, 'w'))
Logger.new(io)
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
end
end
def setup_dirs
Dir.mkdir(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
Roby::State.datadirs = []
datadir = File.join(APP_DIR, "data")
if File.directory?(datadir)
Roby::State.datadirs << datadir
end
end
# 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'))
# Load robot-specific configuration
planner_dir = File.join(APP_DIR, 'planners')
models_search = [planner_dir]
if robot_name
load_robotfile(File.join(APP_DIR, 'config', "ROBOT.rb"))
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
end
def setup
reset
$LOAD_PATH.unshift(APP_DIR) unless $LOAD_PATH.include?(APP_DIR)
# Get the application-wide configuration
file = File.join(APP_DIR, 'config', 'app.yml')
file = YAML.load(File.open(file))
load_yaml(file)
if File.exists?(initfile = File.join(APP_DIR, 'config', 'init.rb'))
load initfile
end
setup_dirs
setup_loggers
# Import some constants directly at toplevel before loading the
# user-defined models
unless Object.const_defined?(:Application)
Object.const_set(:Application, Roby::Application)
Object.const_set(:State, Roby::State)
end
require_models
# MainPlanner is always included in the planner list
Roby.control.planners << MainPlanner
# 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)
Roby::Test::TestCase.include mod.const_get(:Test)
end
end
end
end
def run(&block)
# 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)
else
DRb.start_service "druby://#{host}", Interface.new(Roby.control)
droby_config = { :ring_discovery => !!discovery['ring'],
:name => robot_name,
:plan => Roby.plan,
:period => discovery['period'] || 0.5 }
if discovery['tuplespace']
droby_config[:discovery_tuplespace] = DRbObject.new_with_uri("druby://#{discovery['tuplespace']}")
end
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::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 }
# 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.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
plugins = self.plugins.map { |_, mod| mod if mod.respond_to?(:run) }.compact
run_plugins(plugins, &block)
rescue Exception => e
if e.respond_to?(:pretty_print)
pp e
else
pp e.full_message
end
end
def run_plugins(mods, &block)
control = Roby.control
if mods.empty?
yield
control.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
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
# supposed to be started only once for a whole system
#
# If you have external servers to start for every robot, plug it into
# #start_server
def start_distributed
Thread.abort_on_exception = true
if !File.exists?(log_dir)
Dir.mkdir(log_dir)
end
unless single? || !discovery['tuplespace']
ts = Rinda::TupleSpace.new
discovery['tuplespace'] =~ /(:\d+)$/
DRb.start_service "druby://#{$1}", ts
new_db = ts.notify('write', DISCOVERY_TEMPLATE)
take_db = ts.notify('take', DISCOVERY_TEMPLATE)
Thread.start do
new_db.each { |_, t| STDERR.puts "new host #{t[1]}" }
end
Thread.start do
take_db.each { |_, t| STDERR.puts "host #{t[1]} has disconnected" }
end
Roby.warn "Started service discovery on #{discovery['tuplespace']}"
end
call_plugins(:start_distributed, self)
end
# Stop services needed for distributed operations. See #start_distributed
def stop_distributed
DRb.stop_service
call_plugins(:stop_distributed, self)
rescue Interrupt
end
attr_reader :log_server
attr_reader :log_sources
# Start services that should exist for every robot in the system. Services that
# are needed only once for all robots should be started in #start_distributed
def start_server
Thread.abort_on_exception = true
# Start a log server if needed, and poll the log directory for new
# data sources
if log_server = (log.has_key?('server') ? log['server'] : true)
require 'roby/log/server'
port = if log_server.kind_of?(Hash) && log_server['port']
Integer(log_server['port'])
end
@log_server = Log::Server.new(port ||= Log::Server::RING_PORT)
Roby::Log::Server.info "log server published on port #{port}"
@log_streams = []
@log_streams_poll = Thread.new do
begin
loop do
Thread.exclusive do
known_streams = @log_server.streams
streams = data_streams
(streams - known_streams).each do |s|
Roby::Log::Server.info "new stream found #{s.name} [#{s.type}]"
s.open
@log_server.added_stream(s)
end
(known_streams - streams).each do |s|
Roby::Log::Server.info "end of stream #{s.name} [#{s.type}]"
s.close
@log_server.removed_stream(s)
end
end
sleep(5)
end
rescue Interrupt
rescue
Roby::Log::Server.fatal $!.full_message
end
end
end
call_plugins(:start_server, self)
end
# Stop server. See #start_server
def stop_server
if @log_server
@log_streams_poll.raise Interrupt, "quitting"
@log_streams_poll.join
@log_server.quit
@log_streams.clear
end
call_plugins(:stop_server, self)
end
# Require all files in +dirname+
def require_dir(dirname)
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)
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)
return unless robot_name && robot_type
[robot_name, robot_type].each do |name|
dirname = pattern.gsub(/ROBOT/, name)
require_dir(dirname) 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)
return unless robot_name && robot_type
robot_config = pattern.gsub(/ROBOT/, robot_name)
if File.file?(robot_config)
Kernel.send(method, robot_config)
true
else
robot_config = pattern.gsub(/ROBOT/, robot_type)
if File.file?(robot_config)
Kernel.send(method, robot_config)
true
else
false
end
end
end
attr_predicate :simulation?, true
def simulation; self.simulation = true end
attr_predicate :testing?, true
def testing; self.testing = true end
attr_predicate :shell?, true
def shell; self.shell = true end
def single?; @single || discovery.empty? end
def single; @single = true 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
path = if path =~ /-(events|timings)\.log$/
$`
elsif File.exists?("#{path}-events.log")
path
end
if path
return [Roby::Log::EventStream.new(path)]
end
end
each_responding_plugin(:data_streams_of, true) do |config|
if streams = config.data_streams_of(filenames)
return streams
end
end
nil
end
# Returns the list of data streams suitable for data display known
# to the application
def data_streams(log_dir = nil)
log_dir ||= self.log_dir
streams = []
Dir.glob(File.join(log_dir, '*-events.log*')).each do |file|
next unless file =~ /-events\.log$/
streams << Roby::Log::EventStream.new($`)
end
each_responding_plugin(:data_streams, true) do |config|
if s = config.data_streams(log_dir)
streams += s
end
end
streams
end
def self.find_data(name)
Roby::State.datadirs.each do |dir|
path = File.join(dir, name)
return path if File.exists?(path)
end
raise Errno::ENOENT, "no file #{name} found in #{Roby::State.datadirs.join(":")}"
end
def self.register_plugin(name, mod, &init)
caller(1)[0] =~ /^([^:]+):\d/
dir = File.expand_path(File.dirname($1))
Roby.app.available_plugins << [name, dir, mod, init]
end
@@reload_model_filter = []
# Add a filter to model reloading. A task or planner model is
# reinitialized only if all filter blocks return true for it
def self.filter_reloaded_models(&block)
@@reload_model_filter << block
end
def model?(model)
(model <= Roby::Task) || (model.kind_of?(Roby::TaskModelTag)) ||
(model <= Planning::Planner) || (model <= Planning::Library)
end
def reload_model?(model)
@@reload_model_filter.all? { |filter| filter[model] }
end
def app_file?(path)
(path =~ %r{(^|/)#{APP_DIR}(/|$)}) ||
((path[0] != ?/) && File.file?(File.join(APP_DIR, path)))
end
def framework_file?(path)
if path =~ /roby\/.*\.rb$/
true
else
Roby.app.plugins.any? do |name, _|
_, dir, _, _ = Roby.app.plugin_definition(name)
path =~ %r{(^|/)#{dir}(/|$)}
end
end
end
def reload
# Always reload this file first. This ensure that one can use #reload
# to fix the reload code itself
load __FILE__
# Clear all event definitions in task models that are filtered out by
# Application.filter_reloaded_models
ObjectSpace.each_object(Class) do |model|
next unless model?(model)
next unless reload_model?(model)
model.clear_model
end
# Remove what we want to reload from LOADED_FEATURES and use
# require. Do not use 'load' as the reload order should be the
# require order.
needs_reload = []
$LOADED_FEATURES.delete_if do |feature|
if framework_file?(feature) || app_file?(feature)
needs_reload << feature
end
end
needs_reload.each do |feature|
begin
require feature.gsub(/\.rb$/, '')
rescue Exception => e
STDERR.puts e.full_message
end
end
end
end
# Load the plugins 'main' files
Roby.app.plugin_dir File.join(ROBY_ROOT_DIR, 'plugins')
if plugin_path = ENV['ROBY_PLUGIN_PATH']
plugin_path.split(':').each do |dir|
if File.directory?(dir)
Roby.app.plugin_dir File.expand_path(dir)
end
end
end
end