class Roby::Application
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.
Plugin Integration¶ ↑
Plugins are integrated by providing methods that get called during setup and teardown of the application. It is therefore important to understand the order in which methods get called, and where the plugins can 'plug-in' this process.
On setup, the following methods are called:
-
load base configuration files. app.yml and init.rb
-
load_base_config
hook -
set up directories (log dir, …) and loggers
-
set up singletons
-
base_setup
hook -
setup hook. The difference is that the setup hook is called only if
setup
is called.base_setup
is always called. -
load models in models/tasks
-
require_models
hook -
load models in models/planners and models/actions
-
require_planners
hook -
load additional model files
-
finalize_model_loading hook
-
load config file config/ROBOT.rb
-
require_config hook
-
setup main planner
-
setup testing if in testing mode
-
setup shell interface
Constants
- DEFAULT_OPTIONS
Attributes
@return [Array<#call>] list of blocks that should be executed once the
application is started
@return [Array<String>] list of paths to files not in models/ that
contain some models. This is mainly used by the command-line tools so that the user can load separate "model-based scripts" files.
Allows to override the application base directory. See app_dir
Additional metadata saved in log_dir/info.yml by the app
Do not modify directly, use {#add_app_metadata} instead
Allows to override the app name
The –set options passed on the command line
@!method auto_load_models
? @!method auto_load_models
=(flag)
Controls whether Roby
should load the available the model files automatically in {#require_models}
@return [Boolean]
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
@return [Array<#call>] list of objects called when the app cleans up
(it is the opposite of setup)
@return [Array<#call>] list of objects called when the app is doing
{#clear_models}
@return [Array<#call>] list of blocks that should be executed once the
application is started
The list of directories created by this app in the paths to {#created_log_dirs}
They are deleted on cleanup if {#public_logs?} is false. Unlike with {#created_log_dirs}, they are not deleted if they are not empty.
@return [Array<String>]
The list of log directories created by this app
They are deleted on cleanup if {#public_logs?} is false. Unlike with {#created_log_base_dirs}, they are deleted even if they are not empty.
@return [Array<String>]
Array of regular expressions used to filter out backtraces
@return [Array<#call>] list of objects called when the app gets
initialized (i.e. just after init.rb is loaded)
The PID of the server that gives access to the log file
Its port is allocated automatically, and must be discovered through the Roby
interface
@return [Integer,nil]
The port on which the log server is started
It is by default started on an ephemeral port, that needs to be discovered by clients through the Roby
interface's {Interface#log_server_port}
@return [Integer,nil]
Allows to override the app's module name
The default is to convert the app dir's basename to camelcase, but that fails in some cases (mostly, when there are acronyms in the name)
@return [#call] the blocks that listen to notifications. They are
added with {#on_notification} and removed with {#remove_notification_listener}
Applicatio configuration information is stored in a YAML file config/app.yml. The options are saved in a hash.
This attribute contains the raw hash as read from the file. It is overlaid
The main plan on which this application acts
@return [ExecutablePlan]
A set of planners declared in this application
@return [Array]
An [name, module] array of the loaded plugins
A set of exceptions that have been encountered by the application The associated string, if given, is a hint about in which context this exception got raised
@return [Array<(Exception
,String)>] @see register_exception
clear_exceptions
@return [Array<#call>] list of objects called when the app gets
to require its models (i.e. after {#require_models})
The host to which the REST interface server should bind
@return [String]
The port on which the REST interface server should be
@return [Integer]
A list of paths in which files should be looked for in {#find_dirs}, {#find_files} and {#find_files_in_dirs}
If uninitialized, [app_dir] is used
@return [Array<#call>] list of objects called when the app gets
initialized (i.e. in {#setup} after {#base_setup})
The TCP server that gives access to the {Interface}
The host to which the shell interface server should bind
@return [String]
The port on which the shell interface server should be
@return [Integer]
@return [#call] the blocks that listen to ui events. They are
added with {#on_ui_event} and removed with {#remove_ui_event}
Public Class Methods
Allows to attribute configuration keys to override configuration parameters stored in config/app.yml
For instance,
attr_config 'log'
creates a log_overrides attribute, which contains a hash. Any value stored in this hash will override those stored in the config file. The final value can be accessed by accessing the generated config_key method:
E.g.,
in config/app.yml:
log: dir: test
in config/init.rb:
Roby.app.log_overrides['dir'] = 'bla'
Then, Roby.app
.log will return { 'dir' => 'bla }
This override mechanism is not meant to be used directly by the user, but as a tool for the rest of the framework
# File lib/roby/app.rb, line 166 def self.attr_config(config_key) config_key = config_key.to_s # Ignore if already done return if method_defined?("#{config_key}_overrides") attribute("#{config_key}_overrides") { Hash.new } define_method(config_key) do plain = self.options[config_key] || Hash.new overrides = instance_variable_get "@#{config_key}_overrides" if overrides plain.recursive_merge(overrides) else plain end end end
Defines common configuration options valid for all Roby-oriented scripts
# File lib/roby/app.rb, line 519 def self.common_optparse_setup(parser) Roby.app.load_config_yaml parser.on("--set=KEY=VALUE", String, "set a value on the Conf object") do |value| Roby.app.argv_set << value key, value = value.split('=') path = key.split('.') base_conf = path[0..-2].inject(Conf) { |c, name| c.send(name) } base_conf.send("#{path[-1]}=", YAML.load(value)) end parser.on("--log=SPEC", String, "configuration specification for text loggers. SPEC is of the form path/to/a/module:LEVEL[:FILE][,path/to/another]") do |log_spec| log_spec.split(',').each do |spec| mod, level, file = spec.split(':') Roby.app.log_setup(mod, level, file) end end parser.on('-r NAME', '--robot=NAME[,TYPE]', String, 'the robot name and type') do |name| robot_name, robot_type = name.split(',') Roby.app.setup_robot_names_from_config_dir Roby.app.robot(robot_name, robot_type) end parser.on('--debug', 'run in debug mode') do Roby.app.public_logs = true Roby.app.filter_backtraces = false require 'roby/app/debug' end parser.on_tail('-h', '--help', 'this help message') do STDERR.puts parser exit end end
# File lib/roby/app.rb, line 2393 def self.find_data(*name) name = File.join(*name) Roby::Conf.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::Conf.datadirs.join(":")}" end
Guess the app directory based on the current directory
@return [String,nil] the base of the app, or nil if the current
directory is not within an app
# File lib/roby/app.rb, line 324 def self.guess_app_dir if test_dir = ENV['ROBY_APP_DIR'] if !Application.is_app_dir?(test_dir) raise InvalidRobyAppDirEnv, "the ROBY_APP_DIR envvar is set to #{test_dir}, but this is not a valid Roby application path" end return test_dir end path = Pathname.new(Dir.pwd).find_matching_parent do |test_dir| Application.is_app_dir?(test_dir.to_s) end if path path.to_s end end
Sets up provided option parser to add the –host and –vagrant option
When added, a :host entry will be added to the provided options hash
# File lib/roby/app.rb, line 553 def self.host_options(parser, options) options[:host] ||= Roby.app.shell_interface_host || 'localhost' options[:port] ||= Roby.app.shell_interface_port || Interface::DEFAULT_PORT parser.on('--host URL', String, "sets the host to connect to as hostname[:PORT]") do |url| if url =~ /(.*):(\d+)$/ options[:host] = $1 options[:port] = Integer($2) else options[:host] = url end end parser.on('--vagrant NAME[:PORT]', String, "connect to a vagrant VM") do |vagrant_name| require 'roby/app/vagrant' if vagrant_name =~ /(.*):(\d+)$/ vagrant_name, port = $1, Integer($2) end options[:host] = Roby::App::Vagrant.resolve_ip(vagrant_name) options[:port] = port end end
Tests if the given directory looks like the root of a Roby
app
@param [String] test_dir the path to test
# File lib/roby/app.rb, line 311 def self.is_app_dir?(test_dir) File.file?(File.join(test_dir, 'config', 'app.yml')) || File.directory?(File.join(test_dir, 'models')) || File.directory?(File.join(test_dir, 'scripts', 'controllers')) || File.directory?(File.join(test_dir, 'config', 'robots')) end
# File lib/roby/app.rb, line 660 def initialize @plan = ExecutablePlan.new @argv_set = Array.new @auto_load_all = false @default_auto_load = true @auto_load_models = nil @app_name = nil @module_name = nil @app_dir = nil @backward_compatible_naming = true @development_mode = true @search_path = nil @plugins = Array.new @plugins_enabled = true @available_plugins = Array.new @options = DEFAULT_OPTIONS.dup @public_logs = false @log_create_current = true @created_log_dirs = [] @created_log_base_dirs = [] @additional_model_files = [] @restarting = false @shell_interface = nil @shell_interface_host = nil @shell_interface_port = Interface::DEFAULT_PORT @shell_abort_on_exception = true @rest_interface = nil @rest_interface_host = nil @rest_interface_port = Interface::DEFAULT_REST_PORT @automatic_testing = true @registered_exceptions = [] @app_extra_metadata = Hash.new @filter_out_patterns = [Roby::RX_IN_FRAMEWORK, Roby::RX_IN_METARUBY, Roby::RX_IN_UTILRB, Roby::RX_REQUIRE] self.abort_on_application_exception = true @planners = [] @notification_listeners = Array.new @ui_event_listeners = Array.new @init_handlers = Array.new @setup_handlers = Array.new @require_handlers = Array.new @clear_models_handlers = Array.new @cleanup_handlers = Array.new @controllers = Array.new @action_handlers = Array.new end
Defines accessors for a configuration parameter stored in options
This method allows to define a getter and a setter for a parameter stored in options
that should be user-overridable. It builds upon attr_config.
For instance:
overridable_configuration 'log', 'filter_backtraces'
will create a filter_backtraces getter and a filter_backtraces=
setter which allow to respectively access the log/filter_backtraces configuration value, and override it from its value in config/app.yml
The :predicate option allows to make the setter look like a predicate:
overridable_configuration 'log', 'filter_backtraces', predicate: true
will define filter_backtraces?
instead of filter_backtraces
# File lib/roby/app.rb, line 203 def self.overridable_configuration(config_set, config_key, options = Hash.new) options = Kernel.validate_options options, predicate: false, attr_name: config_key attr_config(config_set) define_method("#{options[:attr_name]}#{"?" if options[:predicate]}") do send(config_set)[config_key] end define_method("#{options[:attr_name]}=") do |new_value| send("#{config_set}_overrides")[config_key] = new_value end end
@api private
Read and validate the 'current' dir by means of the 'current' symlink that Roby
maintains in its log base directory
@param [String] current_path the path to the 'current' symlink
# File lib/roby/app.rb, line 1386 def self.read_current_dir(current_path) if !File.symlink?(current_path) raise ArgumentError, "#{current_path} does not exist or is not a symbolic link" end resolved_path = File.readlink(current_path) if !File.exist?(resolved_path) raise ArgumentError, "#{current_path} points to #{resolved_path}, which does not exist" elsif !File.directory?(resolved_path) raise ArgumentError, "#{current_path} points to #{resolved_path}, which is not a directory" end resolved_path end
# File lib/roby/app.rb, line 2402 def self.register_plugin(name, mod, &init) caller(1)[0] =~ /^([^:]+):\d/ dir = File.expand_path(File.dirname($1)) Roby.app.available_plugins.delete_if { |n| n == name } Roby.app.available_plugins << [name, dir, mod, init] end
Returns a unique directory name as a subdirectory of base_dir
, based on path_spec
. The generated name is of the form
<base_dir>/a/b/c/YYYYMMDD-HHMM-basename
if path_spec = "a/b/c/basename"
. A .<number> suffix is appended if the path already exists.
# File lib/roby/app.rb, line 1409 def self.unique_dirname(base_dir, path_spec, date_tag = nil) if path_spec =~ /\/$/ basename = "" dirname = path_spec else basename = File.basename(path_spec) dirname = File.dirname(path_spec) end date_tag ||= Time.now.strftime('%Y%m%d-%H%M') if basename && !basename.empty? basename = date_tag + "-" + basename else basename = date_tag 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) final_path, i = full_path, 0 while File.exists?(final_path) i += 1 final_path = full_path + ".#{i}" end final_path end
Public Instance Methods
Find an action on the planning interface that can generate the given task model
@return [Actions::Models::Action] @raise [ActionResolutionError] if there either none or more than one matching
action
# File lib/roby/app.rb, line 2587 def action_from_model(model) candidates = [] planners.each do |planner_model| planner_model.find_all_actions_by_type(model).each do |action| candidates << [planner_model, action] end end candidates = candidates.uniq if candidates.empty? raise ActionResolutionError, "cannot find an action to produce #{model}" elsif candidates.size > 1 raise ActionResolutionError, "more than one actions available produce #{model}: #{candidates.map { |pl, m| "#{pl}.#{m.name}" }.sort.join(", ")}" else candidates.first end end
Finds the action matching the given name
Unlike {#find_action_from_name}, it raises if no matching action has been found
@return [Actions::Models::Action] @raise [ActionResolutionError] if either none or more than one action
interface provide an action with this name
# File lib/roby/app.rb, line 2634 def action_from_name(name) action = find_action_from_name(name) if !action available_actions = planners.map do |planner_model| planner_model.each_action.map(&:name) end.flatten if available_actions.empty? raise ActionResolutionError, "cannot find an action named #{name}, there are no actions defined" else raise ActionResolutionError, "cannot find an action named #{name}, available actions are: #{available_actions.sort.join(", ")}" end end action end
Declares that the following block should be used to setup the main action interface
# File lib/roby/app.rb, line 978 def actions(&block) action_handlers << block end
Add some metadata to {#app_metadata}, and save it to the log dir's info.yml if it is already created
# File lib/roby/app.rb, line 1281 def add_app_metadata(metadata) app_extra_metadata.merge!(metadata) if created_log_dir? log_save_metadata end end
# File lib/roby/app.rb, line 1133 def add_plugin(name, mod) 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 # Refresh the relation sets in #plan to include relations # possibly added by the plugin plan.refresh_relations mod end
Returns the application base directory
@return [String,nil]
# File lib/roby/app.rb, line 276 def app_dir if defined?(APP_DIR) APP_DIR elsif @app_dir @app_dir end end
Returns true if the given path points to a file in the Roby
app
@param [String] path
# File lib/roby/app.rb, line 2436 def app_file?(path) !!find_base_path_for(path) end
Metadata used to describe the app
It is saved in the app's log directory under info.yml
@see add_app_metadata
# File lib/roby/app.rb, line 1293 def app_metadata Hash['time' => time_tag, 'cmdline' => "#{$0} #{ARGV.join(" ")}", 'robot_name' => robot_name, 'robot_type' => robot_type, 'app_name' => app_name, 'app_dir' => app_dir].merge(app_extra_metadata) end
Returns this app's toplevel module
# File lib/roby/app.rb, line 261 def app_module constant("::#{module_name}") end
Returns the name of the application
# File lib/roby/app.rb, line 237 def app_name if @app_name @app_name elsif app_dir @app_name = File.basename(app_dir).gsub(/[^\w]/, '_') else 'default' end end
# File lib/roby/app.rb, line 284 def app_path @app_path ||= Pathname.new(app_dir) end
@api private
Sets relevant configuration values from a configuration hash
# File lib/roby/app.rb, line 1750 def apply_config(config) if host_port = config['interface'] apply_config_interface(host_port) elsif host_port = config.fetch('droby', Hash.new)['host'] Roby.warn_deprecated 'the droby.host configuration parameter in config/app.yml is deprecated, use "interface" at the toplevel instead' apply_config_interface(host_port) end end
@api private
Parses and applies the 'interface' value from a configuration hash
It is a helper for {#apply_config}
# File lib/roby/app.rb, line 1764 def apply_config_interface(host_port) if host_port !~ /:\d+$/ host_port += ":#{Interface::DEFAULT_PORT}" end match = /(.*):(\d+)$/.match(host_port) host = match[1] @shell_interface_host = if !host.empty? host end @shell_interface_port = Integer(match[2]) end
# File lib/roby/app.rb, line 2372 def auto_load_models? if @auto_load_models.nil? @default_auto_load else @auto_load_models end end
@return [Array<String>] the search path for the auto-load feature. It
depends on the value of {#auto_load_all?}
# File lib/roby/app.rb, line 2382 def auto_load_search_path if auto_load_all? then search_path elsif app_dir then [app_dir] else [] end end
# File lib/roby/app.rb, line 1609 def auto_require_models # Require all common task models and the task models specific to # this robot if auto_load_models? load_all_model_files_in('tasks') if backward_compatible_naming? search_path = self.auto_load_search_path all_files = find_files_in_dirs('tasks', 'ROBOT', path: search_path, all: true, order: :specific_last, pattern: /\.rb$/) all_files.each do |p| require(p) end end call_plugins(:auto_require_models, self) end end
# File lib/roby/app.rb, line 1705 def auto_require_planners search_path = self.auto_load_search_path prefixes = ['actions'] if backward_compatible_naming? prefixes << 'planners' end prefixes.each do |prefix| load_all_model_files_in(prefix) end if backward_compatible_naming? main_files = find_files('planners', 'ROBOT', 'main.rb', all: true, order: :specific_first) main_files.each do |path| require path end planner_files = find_files_in_dirs('planners', 'ROBOT', all: true, order: :specific_first, pattern: /\.rb$/) planner_files.each do |path| require path end end call_plugins(:require_planners, self) end
Hook for the plugins to filter out some paths that should not be auto-loaded by {#each_test_file_in_app}. It does not affect {#each_test_file_for_loaded_models}.
@return [Boolean]
# File lib/roby/app.rb, line 2789 def autodiscover_tests_in?(path) suffix = File.basename(path) if robots.has_robot?(suffix) && ![robot_name, robot_type].include?(suffix) false elsif defined? super super else true end end
The inverse of base_setup
# File lib/roby/app.rb, line 791 def base_cleanup if !public_logs? created_log_dirs.delete_if do |dir| FileUtils.rm_rf dir true end created_log_base_dirs.sort_by(&:length).reverse_each do |dir| # .rmdir will ignore nonempty / nonexistent directories FileUtils.rmdir(dir) created_log_base_dirs.delete(dir) end end end
# File lib/roby/app.rb, line 777 def base_setup STDOUT.sync = true load_base_config if !@log_dir find_and_create_log_dir end setup_loggers(redirections: true) # Set up the loaded plugins call_plugins(:base_setup, self) end
Call method
on each loaded extension module which define it, with arguments args
# File lib/roby/app.rb, line 1069 def call_plugins(method, *args, deprecated: nil) each_responding_plugin(method) do |config_extension| if deprecated Roby.warn "#{config_extension} uses the deprecated .#{method} hook during setup and teardown, #{deprecated}" end config_extension.send(method, *args) end end
The inverse of setup
. It gets called at the end of run
# File lib/roby/app.rb, line 846 def cleanup # Run the cleanup handlers first, we want the plugins to still be # active cleanup_handlers.each(&:call) call_plugins(:cleanup, self) # Deprecated version of #cleanup call_plugins(:reset, self, deprecated: "define 'cleanup' instead") planners.clear plan.execution_engine.gather_propagation do plan.clear end clear_models clear_config stop_shell_interface base_cleanup end
# File lib/roby/app.rb, line 2462 def clear_config Conf.clear call_plugins(:clear_config, self) # Deprecated name for clear_config call_plugins(:reload_config, self) end
# File lib/roby/app.rb, line 1506 def clear_exceptions registered_exceptions.clear end
Whether this model should be cleared in {#clear_models}
The default implementation returns true for the models that are not registered as constants (more precisely, for which MetaRuby's permanent_model? returns false) and for the models defined in this app.
# File lib/roby/app.rb, line 2529 def clear_model?(m) !m.permanent_model? || (!testing? && model_defined_in_app?(m)) end
Clear all models for which {#clear_model?} returns true
# File lib/roby/app.rb, line 2535 def clear_models root_models.each do |root_model| submodels = root_model.each_submodel.to_a.dup submodels.each do |m| if clear_model?(m) m.permanent_model = false m.clear_model end end end DRoby::V5::DRobyConstant.clear_cache clear_models_handlers.each { |b| b.call } call_plugins(:clear_models, self) end
Declares that the following block should be used as the robot controller
# File lib/roby/app.rb, line 972 def controller(&block) controllers << block end
Test
whether this app already created its log directory
# File lib/roby/app.rb, line 1300 def created_log_dir? @log_dir && File.directory?(@log_dir) end
# File lib/roby/app.rb, line 1679 def define_actions_module if !app_module.const_defined_here?(:Actions) app_module.const_set(:Actions, Module.new) end end
# File lib/roby/app.rb, line 1685 def define_main_planner_if_needed if !app_module::Actions.const_defined_here?(:Main) app_module::Actions.const_set(:Main, Class.new(Roby::Actions::Interface)) end if backward_compatible_naming? if !Object.const_defined_here?(:Main) Object.const_set(:Main, app_module::Actions::Main) end end end
True if name
is a plugin known to us
# File lib/roby/app.rb, line 1044 def defined_plugin?(name) available_plugins.any? { |plugname, *_| plugname == name } end
Returns the downmost app file that was involved in the given model's definition
# File lib/roby/app.rb, line 1644 def definition_file_for(model) return if !model.respond_to?(:definition_location) || !model.definition_location model.definition_location.each do |location| file = location.absolute_path next if !(base_path = find_base_path_for(file)) relative = Pathname.new(file).relative_path_from(base_path) split = relative.each_filename.to_a next if split[0] != 'models' return file end nil end
Discover which tests should be run, and require them
@param [Boolean] all if set, list all files in {#app_dir}/test.
Otherwise, list only the tests that are related to the loaded models.
@param [Boolean] only_self if set, list only test files from within
{#app_dir}. Otherwise, consider test files from all over {#search_path}
@return [Array<String>]
# File lib/roby/app.rb, line 2766 def discover_test_files(all: true, only_self: false) if all test_files = each_test_file_in_app.inject(Hash.new) do |h, k| h[k] = Array.new h end if !only_self test_files.merge!(Hash[each_test_file_for_loaded_models.to_a]) end else test_files = Hash[each_test_file_for_loaded_models.to_a] if only_self test_files = test_files.find_all { |f, _| self_file?(f) } end end test_files end
Enumerate all models registered in this app
It basically enumerate all submodels of all models in {#root_models}
@param [nil,#each_submodel] root_model if non-nil, limit the
enumeration to the submodels of this root
@yieldparam [#each_submodel]
# File lib/roby/app.rb, line 2509 def each_model(root_model = nil) return enum_for(__method__, root_model) if !block_given? if !root_model self.root_models.each { |m| each_model(m, &Proc.new) } return end yield(root_model) root_model.each_submodel do |m| yield(m) end end
Enumerates the listeners currently registered through on_notification
@yieldparam [#call] the job listener object
# File lib/roby/app.rb, line 2723 def each_notification_listener(&block) notification_listeners.each(&block) end
Enumerates all available plugins, yielding only the plugin module (i.e. the plugin object itself)
# File lib/roby/app.rb, line 1050 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 plugin object that respond to method
# File lib/roby/app.rb, line 1061 def each_responding_plugin(method, on_available = false) each_plugin do |mod| yield(mod) if mod.respond_to?(method) end end
Enumerate the test files that should be run to test the current app configuration
@yieldparam [String] path the file's path @yieldparam [Array<Class<Roby::Task>>] models the models that are
meant to be tested by 'path'. It can be empty for tests that involve lib/
# File lib/roby/app.rb, line 2828 def each_test_file_for_loaded_models(&block) models_per_file = Hash.new { |h, k| h[k] = Set.new } each_model do |m| next if m.respond_to?(:has_ancestor?) && m.has_ancestor?(Roby::Event) next if m.respond_to?(:private_specialization?) && m.private_specialization? next if !m.name test_files_for(m).each do |test_path| models_per_file[test_path] << m end end find_files('test', 'actions', 'ROBOT', 'test_main.rb', order: :specific_first, all: true).each do |path| models_per_file[path] = Set[main_action_interface] end find_dirs('test', 'lib', order: :specific_first, all: true).each do |path| Pathname.new(path).find do |p| if p.basename.to_s =~ /^test_.*.rb$/ || p.basename.to_s =~ /_test\.rb$/ models_per_file[p.to_s] = Set.new end end end models_per_file.each(&block) end
Enumerate all the test files in this app and for this robot configuration
# File lib/roby/app.rb, line 2802 def each_test_file_in_app return enum_for(__method__) if !block_given? dir = File.join(app_dir, 'test') return if !File.directory?(dir) Find.find(dir) do |path| # Skip the robot-specific bits that don't apply on the # selected robot if File.directory?(path) Find.prune if !autodiscover_tests_in?(path) end if File.file?(path) && path =~ /test_.*\.rb$/ yield(path) end end end
Enumerates the listeners currently registered through on_ui_event
@yieldparam [#call] the job listener object
# File lib/roby/app.rb, line 2680 def each_ui_event_listener(&block) ui_event_listeners.each(&block) end
The engine associated with {#plan}
@return [ExecutionEngine,nil]
# File lib/roby/app.rb, line 106 def execution_engine; plan.execution_engine if plan end
Override the value stored in configuration files for filter_backtraces?
# File lib/roby/app.rb, line 607 overridable_configuration 'log', 'filter_backtraces', predicate: true
True if we should remove the framework code from the error backtraces
# File lib/roby/app.rb, line 598
Find an action with the given name on the action interfaces registered on {#planners}
@return [(ActionInterface,Actions::Models::Action),nil] @raise [ActionResolutionError] if more than one action interface provide an
action with this name
# File lib/roby/app.rb, line 2611 def find_action_from_name(name) candidates = [] planners.each do |planner_model| if m = planner_model.find_action_by_name(name) candidates << [planner_model, m] end end candidates = candidates.uniq if candidates.size > 1 raise ActionResolutionError, "more than one action interface provide the #{name} action: #{candidates.map { |pl, m| "#{pl}" }.sort.join(", ")}" else candidates.first end end
Create a log directory for the given time tag, and make it this app's log directory
The time tag given to this method also becomes the app's time tag
@param [String] time_tag
@return [String] the path to the log directory
# File lib/roby/app.rb, line 1207 def find_and_create_log_dir(time_tag = self.time_tag) base_dir = log_base_dir @time_tag = time_tag while true log_dir = Roby::Application.unique_dirname(base_dir, '', time_tag) new_dirs = Array.new dir = log_dir while !File.directory?(dir) new_dirs << dir dir = File.dirname(dir) end # Create all paths necessary, but check for possible concurrency # issues with other Roby-based tools creating a log dir with the # same name failed = new_dirs.reverse.any? do |dir| begin FileUtils.mkdir(dir) false rescue Errno::EEXIST true end end if !failed new_dirs.delete(log_dir) created_log_dirs << log_dir created_log_base_dirs.concat(new_dirs) @log_dir = log_dir log_save_metadata return log_dir end end end
Returns the path in search_path
that contains the given file or path
@param [String] path @return [nil,String]
# File lib/roby/app.rb, line 2413 def find_base_path_for(path) if @find_base_path_rx_paths != search_path @find_base_path_rx = search_path.map { |app_dir| [Pathname.new(app_dir), app_dir, %r{(^|/)#{app_dir}(/|$)}] }. sort_by { |_, app_dir, _| app_dir.size }. reverse @find_base_path_rx_paths = search_path.dup end longest_prefix_path, _ = @find_base_path_rx.find do |app_path, app_dir, rx| (path =~ rx) || ((path[0] != ?/) && File.file?(File.join(app_dir, path))) end longest_prefix_path end
# File lib/roby/app.rb, line 2389 def find_data(*name) Application.find_data(*name) end
Returns the first match from {#find_dirs}, or nil if nothing matches
# File lib/roby/app.rb, line 2293 def find_dir(*args) if !args.last.kind_of?(Hash) args.push(Hash.new) end args.last.delete('all') args.last.merge!(all: true) find_dirs(*args).first end
@overload find_files_in_dirs
(*path, options)
Enumerates the subdirectories of paths in {#search_path} matching the given path. The subdirectories are resolved using File.join(*path) If one of the elements of the path is the string 'ROBOT', it gets replaced by the robot name and type.
@option options [Boolean] :all (true) if true, all matching
directories are returned. Otherwise, only the first one is (the meaning of 'first' is controlled by the order option below)
@option options [:specific_first,:specific_last] :order if
:specific_first, the first returned match is the one that is most specific. The sorting order is to first sort by ROBOT and then by the place in search_dir. From the most specific to the least specific, ROBOT is assigned the robot name, the robot type and finally an empty string.
@return [Array<String>]
Given a search dir of [app2, app1]
app1/models/tasks/goto.rb app1/models/tasks/v3/goto.rb app2/models/tasks/asguard/goto.rb
@example
find_dirs('tasks', 'ROBOT', all: true, order: :specific_first) # returns [app1/models/tasks/v3, # app2/models/tasks/asguard, # app1/models/tasks/]
@example
find_dirs('tasks', 'ROBOT', all: false, order: :specific_first) # returns [app1/models/tasks/v3/goto.rb]
# File lib/roby/app.rb, line 2075 def find_dirs(*dir_path) Application.debug { "find_dirs(#{dir_path.map(&:inspect).join(", ")})" } if dir_path.last.kind_of?(Hash) options = dir_path.pop end options = Kernel.validate_options(options || Hash.new, :all, :order, :path) if dir_path.empty? raise ArgumentError, "no path given" end search_path = options[:path] || self.search_path if !options.has_key?(:all) raise ArgumentError, "no :all argument given" elsif !options.has_key?(:order) raise ArgumentError, "no :order argument given" elsif ![:specific_first, :specific_last].include?(options[:order]) raise ArgumentError, "expected either :specific_first or :specific_last for the :order argument, but got #{options[:order]}" end relative_paths = [] base_dir_path = dir_path.dup base_dir_path.delete_if { |p| p =~ /ROBOT/ } relative_paths = [base_dir_path] if dir_path.any? { |p| p =~ /ROBOT/ } && robot_name && robot_type replacements = [robot_type] if robot_type != robot_name replacements << robot_name end replacements.each do |replacement| robot_dir_path = dir_path.map do |s| s.gsub('ROBOT', replacement) end relative_paths << robot_dir_path end end root_paths = search_path.dup if options[:order] == :specific_first relative_paths = relative_paths.reverse else root_paths = root_paths.reverse end result = [] Application.debug { " relative paths: #{relative_paths.inspect}" } relative_paths.each do |rel_path| root_paths.each do |root| abs_path = File.expand_path(File.join(*rel_path), root) Application.debug { " absolute path: #{abs_path}" } if File.directory?(abs_path) Application.debug { " selected" } result << abs_path end end end if result.empty? return result elsif !options[:all] return [result.first] else return result end end
Returns the first match from {#find_files}, or nil if nothing matches
# File lib/roby/app.rb, line 2283 def find_file(*args) if !args.last.kind_of?(Hash) args.push(Hash.new) end args.last.delete('all') args.last.merge!(all: true) find_files(*args).first end
@overload find_files
(*path, options)
Enumerates files based on their relative paths in {#search_path}. The paths are resolved using File.join(*path) If one of the elements of the path is the string 'ROBOT', it gets replaced by the robot name and type.
@option options [Boolean] :all (true) if true, all matching
directories are returned. Otherwise, only the first one is (the meaning of 'first' is controlled by the order option below)
@option options [:specific_first,:specific_last] :order if
:specific_first, the first returned match is the one that is most specific. The sorting order is to first sort by ROBOT and then by the place in search_dir. From the most specific to the least specific, ROBOT is assigned the robot name, the robot type and finally an empty string.
@return [Array<String>]
Given a search dir of [app2, app1], a robot name of v3 and a robot type of asguard,
app1/config/v3.rb app2/config/asguard.rb
@example
find_files('config', 'ROBOT.rb', all: true, order: :specific_first) # returns [app1/config/v3.rb, # app2/config/asguard.rb]
@example
find_dirs('tasks', 'ROBOT', all: false, order: :specific_first) # returns [app1/config/v3.rb]
# File lib/roby/app.rb, line 2232 def find_files(*file_path) if file_path.last.kind_of?(Hash) options = file_path.pop end options = Kernel.validate_options(options || Hash.new, :all, :order, :path) if file_path.empty? raise ArgumentError, "no path given" end # Remove the filename from the complete path filename = file_path.pop filename = filename.split('/') file_path.concat(filename[0..-2]) filename = filename[-1] if filename =~ /ROBOT/ && robot_name args = file_path + [options.merge(pattern: filename.gsub('ROBOT', robot_name))] robot_name_matches = find_files_in_dirs(*args) robot_type_matches = [] if robot_name != robot_type args = file_path + [options.merge(pattern: filename.gsub('ROBOT', robot_type))] robot_type_matches = find_files_in_dirs(*args) end if options[:order] == :specific_first result = robot_name_matches + robot_type_matches else result = robot_type_matches + robot_name_matches end else args = file_path.dup args << options.merge(pattern: filename) result = find_files_in_dirs(*args) end orig_path = Pathname.new(File.join(*file_path)) orig_path += filename if orig_path.absolute? && File.file?(orig_path.to_s) if options[:order] == :specific_first result.unshift orig_path.to_s else result.push orig_path.to_s end end return result end
@overload find_files_in_dirs
(*path, options)
Enumerates the files that are present in subdirectories of paths in {#search_path}. The subdirectories are resolved using File.join(*path) If one of the elements of the path is the string 'ROBOT', it gets replaced by the robot name and type.
@option (see find_dirs
) @option options [#===] :pattern a filter to apply on the matching
results
@option options [Symbol] :all (false) if true, all files from all
matching directories are returned. Otherwise, only the files from the first matching directory is searched
@return [Array<String>]
Given a search dir of [app2, app1]
app1/models/tasks/goto.rb app1/models/tasks/v3/goto.rb app2/models/tasks/asguard/goto.rb
@example
find_files_in_dirs('tasks', 'ROBOT', all: true, order: :specific_first) # returns [app1/models/tasks/v3/goto.rb, # app2/models/tasks/asguard/goto.rb, # app1/models/tasks/goto.rb]
@example
find_files_in_dirs('tasks', 'ROBOT', all: false, order: :specific_first) # returns [app1/models/tasks/v3/goto.rb,
# File lib/roby/app.rb, line 2172 def find_files_in_dirs(*dir_path) Application.debug { "find_files_in_dirs(#{dir_path.map(&:inspect).join(", ")})" } if dir_path.last.kind_of?(Hash) options = dir_path.pop end options = Kernel.validate_options(options || Hash.new, :all, :order, :path, pattern: Regexp.new("")) dir_search = dir_path.dup dir_search << { all: true, order: options[:order], path: options[:path] } search_path = find_dirs(*dir_search) result = [] search_path.each do |dirname| Application.debug { " dir: #{dirname}" } Dir.new(dirname).each do |file_name| file_path = File.join(dirname, file_name) Application.debug { " file: #{file_path}" } if File.file?(file_path) && options[:pattern] === file_name Application.debug " added" result << file_path end end break if !options[:all] end return result end
Tests whether a path is within a framework library
@param [String] path
# File lib/roby/app.rb, line 2443 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
Guess the app directory based on the current directory, and sets {#app_dir}. It will not do anything if the current directory is not in a Roby
app. Moreover, it does nothing if app_dir
is already set
@return [String] the selected app directory
# File lib/roby/app.rb, line 350 def guess_app_dir return if @app_dir if app_dir = self.class.guess_app_dir @app_dir = app_dir end end
Whether there is a supporting app directory
# File lib/roby/app.rb, line 341 def has_app? !!@app_dir end
# File lib/roby/app.rb, line 1510 def isolate_load_errors(message, logger = Application, level = :warn) yield rescue Interrupt raise rescue ::Exception => e register_exception(e, message) if ignore_all_load_errors? Robot.warn message Roby.log_exception_with_backtrace(e, logger, level) else raise end end
# File lib/roby/app.rb, line 1935 def join @thread.join rescue Exception => e if @thread.alive? && execution_engine.running? if execution_engine.forced_exit? raise else execution_engine.quit retry end else raise end end
# File lib/roby/app.rb, line 1574 def load_all_model_files_in(prefix_name, ignored_exceptions: Array.new) search_path = auto_load_search_path dirs = find_dirs( "models", prefix_name, path: search_path, all: true, order: :specific_last) dirs.each do |dir| all_files = Set.new Find.find(dir) do |path| # Skip the robot-specific bits that don't apply on the # selected robot if File.directory?(path) suffix = File.basename(File.dirname(path)) if robots.has_robot?(suffix) && ![robot_name, robot_type].include?(suffix) Find.prune end end if File.file?(path) && path =~ /\.rb$/ all_files << path end end all_files.each do |path| begin require(path) rescue *ignored_exceptions => e ::Robot.warn "ignored file #{path}: #{e.message}" end end end end
Loads the base configuration
This method loads the two most basic configuration files:
* config/app.yml * config/init.rb
It also calls the plugin's 'load' method
# File lib/roby/app.rb, line 725 def load_base_config load_config_yaml setup_loggers(ignore_missing: true, redirections: false) setup_robot_names_from_config_dir # Get the application-wide configuration if plugins_enabled? register_plugins end update_load_path if initfile = find_file('config', 'init.rb', order: :specific_first) Application.info "loading init file #{initfile}" require initfile end update_load_path # Deprecated hook call_plugins(:load, self, deprecated: "define 'load_base_config' instead") call_plugins(:load_base_config, self) update_load_path if defined? Roby::Conf Roby::Conf.datadirs = find_dirs('data', 'ROBOT', all: true, order: :specific_first) end if has_app? require_robot_file end init_handlers.each(&:call) update_load_path # Define the app module if there is none, and define a root logger # on it app_module = begin self.app_module rescue NameError Object.const_set(module_name, Module.new) end if !app_module.respond_to?(:logger) module_name = self.module_name app_module.class_eval do extend ::Logger::Root(module_name, Logger::INFO) end end end
# File lib/roby/app.rb, line 1729 def load_config_yaml file = find_file('config', 'app.yml', order: :specific_first) return if !file Application.info "loading config file #{file}" options = YAML.load(File.open(file)) || Hash.new if robot_name && (robot_config = options.delete('robots')) options = options.recursive_merge(robot_config[robot_name] || Hash.new) end options = options.map_value do |k, val| val || Hash.new end options = @options.recursive_merge(options) apply_config(options) @options = options end
Helper to the robot config files to load the root files in models/ (e.g. models/tasks.rb)
# File lib/roby/app.rb, line 1633 def load_default_models ['tasks.rb', 'actions.rb'].each do |root_type| if path = find_file('models', root_type, path: [app_dir], order: :specific_first) require path end end call_plugins(:load_default_models, self) end
Load the given Roby
plugin file. It is usually called app.rb, and should call register_plugin
with the relevant information
Note that the file should not do anything yet. The actions required to have a functional plugin should be taken only in the block given to register_plugin
or in the relevant plugin methods.
# File lib/roby/app.rb, line 1023 def load_plugin_file(appfile) begin require appfile rescue Roby.warn "cannot load plugin #{appfile}: #{$!.full_message}\n" end Roby.info "loaded plugin #{appfile}" end
Looks into subdirectories of dir
for files called app.rb and registers them as Roby
plugins
# File lib/roby/app.rb, line 1002 def load_plugins_from_prefix(dir) dir = File.expand_path(dir) $LOAD_PATH.unshift 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) load_plugin_file(appfile) end ensure $LOAD_PATH.shift end
Returns true if name
is a loaded plugin
# File lib/roby/app.rb, line 1033 def loaded_plugin?(name) plugins.any? { |plugname, _| plugname == name } end
The base directory in which logs should be saved
Logs are saved in log_base_dir/$time_tag by default, and a log_base_dir/current symlink gets updated to reflect the most current log directory.
The path is controlled by the log/dir configuration variable. If the provided value, it is interpreted relative to the application directory. It defaults to “data”.
# File lib/roby/app.rb, line 1181 def log_base_dir maybe_relative_dir = if @log_base_dir ||= log['dir'] @log_base_dir elsif global_base_dir = ENV['ROBY_BASE_LOG_DIR'] File.join(global_base_dir, app_name) else 'logs' end File.expand_path(maybe_relative_dir, app_dir || Dir.pwd) end
Sets the directory under which logs should be created
This cannot be called after log_dir
has been set
# File lib/roby/app.rb, line 1196 def log_base_dir=(dir) @log_base_dir = dir end
The path to the current log directory
If {#log_dir} is set, it is used. Otherwise, the current log directory is inferred by the directory pointed to the 'current' symlink
# File lib/roby/app.rb, line 1352 def log_current_dir if @log_dir @log_dir else current_path = File.join(log_base_dir, "current") self.class.read_current_dir(current_path) end end
The path to the current log file
# File lib/roby/app.rb, line 1364 def log_current_file log_current_dir = self.log_current_dir metadata = log_read_metadata if metadata.empty? raise NoCurrentLog, "#{log_current_dir} is not a valid Roby log dir, it does not have an info.yml metadata file" elsif !(robot_name = metadata.map { |h| h['robot_name'] }.compact.last) raise NoCurrentLog, "#{log_current_dir}'s metadata does not specify the robot name" end full_path = File.join(log_current_dir, "#{robot_name}-events.log") if !File.file?(full_path) raise NoCurrentLog, "inferred log file #{full_path} for #{log_current_dir}, but that file does not exist" end full_path end
The directory in which logs are to be saved Defaults to app_dir/data/$time_tag
# File lib/roby/app.rb, line 1245 def log_dir if !@log_dir raise LogDirNotInitialized, "the log directory has not been initialized yet" end @log_dir end
Explicitely set the log directory
It is usually automatically created under {#log_base_dir} during {#base_setup}
# File lib/roby/app.rb, line 1266 def log_dir=(dir) if !File.directory?(dir) raise ArgumentError, "log directory #{dir} does not exist" end @log_dir = dir end
Read the time tag from the current log directory
# File lib/roby/app.rb, line 1330 def log_read_metadata dir = begin log_current_dir rescue ArgumentError end if dir && File.exists?(File.join(dir, 'info.yml')) YAML.load(File.read(File.join(dir, 'info.yml'))) else Array.new end end
# File lib/roby/app.rb, line 1343 def log_read_time_tag metadata = log_read_metadata.last metadata && metadata['time_tag'] end
Save {#app_metadata} in the log directory
@param [Boolean] append if true (the default), the value returned by
{#app_metadata} is appended to the existing data. Otherwise, it replaces the last entry in the file
# File lib/roby/app.rb, line 1309 def log_save_metadata(append: true) path = File.join(log_dir, 'info.yml') info = Array.new current = if File.file?(path) YAML.load(File.read(path)) || Array.new else Array.new end if append || current.empty? current << app_metadata else current[-1] = app_metadata end File.open(path, 'w') do |io| YAML.dump(current, io) end end
Sets whether the log server should be started
# File lib/roby/app.rb, line 619 overridable_configuration 'log', 'server', predicate: true, attr_name: 'log_server'
True if the log server should be started
# File lib/roby/app.rb, line 610
Configures a text logger in the system. It has to be called before setup
to have an effect.
It overrides configuration from the app.yml file
For instance,
log_setup 'roby/execution_engine', 'DEBUG'
will be equivalent to having the following entry in config/app.yml:
log: levels: roby/execution_engine: DEBUG
# File lib/roby/app.rb, line 592 def log_setup(mod_path, level, file = nil) levels = (log_overrides['levels'] ||= Hash.new) levels[mod_path] = [level, file].compact.join(":") end
Returns this app's main action interface
This is usually set up in the robot configuration file by calling Robot.actions
# File lib/roby/app.rb, line 269 def main_action_interface app_module::Actions::Main end
Transforms path
into a path relative to an entry in search_path
(usually the application root directory)
# File lib/roby/app.rb, line 1492 def make_path_relative(path) if !File.exists?(path) path elsif root_path = find_base_path_for(path) return Pathname.new(path).relative_path_from(root_path).to_s else path end end
Tests whether a model class has been defined in this app's code
# File lib/roby/app.rb, line 2480 def model_defined_in_app?(model) model.definition_location.each do |location| return if location.label == 'require' return true if app_file?(location.absolute_path) end false end
# File lib/roby/app.rb, line 2351 def modelling_only; self.modelling_only = true end
Returns the name of this app's toplevel module
# File lib/roby/app.rb, line 256 def module_name @module_name || app_name.camelcase(:upper) end
Call to check whether the current directory is within {#app_dir}. If not, raises
This is called by tools for which being in another app than the currently selected would be really too confusing
# File lib/roby/app.rb, line 375 def needs_to_be_in_current_app(allowed_outside: true) guessed_dir = self.class.guess_app_dir if guessed_dir && (@app_dir != guessed_dir) raise NotInCurrentApp, "#{@app_dir} is currently selected, but the current directory is within #{guessed_dir}" elsif !guessed_dir && !allowed_outside raise NotInCurrentApp, "not currently within an app dir" end end
Sends a message to all notification listeners
# File lib/roby/app.rb, line 2728 def notify(source, level, message) each_notification_listener do |block| block.call(source, level, message) end end
Declares that the following block should be called when {#clear_models} is called
# File lib/roby/app.rb, line 993 def on_cleanup(&block) if !block raise ArgumentError, "missing expected block argument" end cleanup_handlers << block end
Declares that the following block should be called when {#clear_models} is called
# File lib/roby/app.rb, line 984 def on_clear_models(&block) if !block raise ArgumentError, "missing expected block argument" end clear_models_handlers << block end
@deprecated use {#on_setup} instead
# File lib/roby/app.rb, line 966 def on_config(&block) on_setup(&block) end
Declares a block that should be executed when the Roby
app gets initialized (i.e. just after init.rb gets loaded)
# File lib/roby/app.rb, line 940 def on_init(&block) if !block raise ArgumentError, "missing expected block argument" end init_handlers << block end
Registers a block to be called when a message needs to be dispatched from {#notify}
@yieldparam [String] source the source of the message @yieldparam [String] level the log level @yieldparam [String] message the message itself @return [Object] the listener ID that can be given to
{#remove_notification_listener}
# File lib/roby/app.rb, line 2742 def on_notification(&block) if !block raise ArgumentError, "missing expected block argument" end notification_listeners << block block end
Declares a block that should be executed when the Roby
app loads models (i.e. in {#require_models})
# File lib/roby/app.rb, line 958 def on_require(&block) if !block raise ArgumentError, "missing expected block argument" end require_handlers << block end
Declares a block that should be executed when the Roby
app is begin setup
# File lib/roby/app.rb, line 949 def on_setup(&block) if !block raise ArgumentError, "missing expected block argument" end setup_handlers << block end
Registers a block to be called when a message needs to be dispatched from {#ui_event}
@yieldparam [String] name the event name @yieldparam args the UI event listener arguments @return [Object] the listener ID that can be given to
{#remove_ui_event_listener}
# File lib/roby/app.rb, line 2698 def on_ui_event(&block) if !block raise ArgumentError, "missing expected block argument" end ui_event_listeners << block block end
Returns the [name, dir, file, module] array definition of the plugin name
, or nil if name
is not a known plugin
# File lib/roby/app.rb, line 1039 def plugin_definition(name) available_plugins.find { |plugname, *_| plugname == name } end
Prepares the environment to actually run
# File lib/roby/app.rb, line 882 def prepare if public_shell_interface? setup_shell_interface end if public_rest_interface? setup_rest_interface end if public_logs? && log_create_current? FileUtils.rm_f File.join(log_base_dir, "current") FileUtils.ln_s log_dir, File.join(log_base_dir, 'current') end if log['events'] && public_logs? logfile_path = prepare_event_log # Start a log server if needed, and poll the log directory for new # data sources if log_server_options = (log.has_key?('server') ? log['server'] : Hash.new) if !log_server_options.kind_of?(Hash) log_server_options = Hash.new end plan.event_logger.sync = true start_log_server(logfile_path, log_server_options) Roby.info "log server started" else plan.event_logger.sync = false Roby.warn "log server disabled" end end call_plugins(:prepare, self) end
Generate the plan pattern that will call the required action on the planning interface, with the given arguments.
This returns immediately, and the action is not yet deployed at that point.
@return task, planning_task
# File lib/roby/app.rb, line 2656 def prepare_action(name, mission: false, **arguments) if name.kind_of?(Class) planner_model, m = action_from_model(name) else planner_model, m = action_from_name(name) end if mission plan.add_mission_task(task = m.plan_pattern(arguments)) else plan.add(task = m.plan_pattern(arguments)) end return task, task.planning_task end
@api private
# File lib/roby/app.rb, line 867 def prepare_event_log require 'roby/droby/event_logger' require 'roby/droby/logfile/writer' logfile_path = File.join(log_dir, "#{robot_name}-events.log") event_io = File.open(logfile_path, 'w') logfile = DRoby::Logfile::Writer.new(event_io, plugins: plugins.map { |n, _| n }) plan.event_logger = DRoby::EventLogger.new(logfile) plan.execution_engine.event_logger = plan.event_logger Robot.info "logs are in #{log_dir}" logfile_path end
# File lib/roby/app.rb, line 1502 def register_exception(e, reason = nil) registered_exceptions << [e, reason] end
# File lib/roby/app.rb, line 1078 def register_plugins(force: false) if !plugins_enabled? && !force raise PluginsDisabled, "cannot call #register_plugins while the plugins are disabled" end # Load the plugins 'main' files if plugin_path = ENV['ROBY_PLUGIN_PATH'] plugin_path.split(':').each do |plugin| if File.directory?(plugin) load_plugins_from_prefix plugin elsif File.file?(plugin) load_plugin_file plugin end end end end
Register a server port that can be discovered later
# File lib/roby/app.rb, line 1487 def register_server(name, port) end
Reload action models defined in models/actions/
# File lib/roby/app.rb, line 2561 def reload_actions unload_features("actions", ".*\.rb$") unload_features("models", "actions", ".*\.rb$") planners.each do |planner_model| planner_model.clear_model end require_planners end
Reload files in config/
# File lib/roby/app.rb, line 2470 def reload_config clear_config unload_features("config", ".*\.rb$") if has_app? require_robot_file end call_plugins(:require_config, self) end
Reload model files in models/
# File lib/roby/app.rb, line 2551 def reload_models clear_models unload_features("models", ".*\.rb$") additional_model_files.each do |path| unload_features(path) end require_models end
# File lib/roby/app.rb, line 2570 def reload_planners unload_features("planners", ".*\.rb$") unload_features("models", "planners", ".*\.rb$") planners.each do |planner_model| planner_model.clear_model end require_planners end
Removes a notification listener added with {#on_notification}
@param [Object] listener the listener ID returned by
{#on_notification}
# File lib/roby/app.rb, line 2754 def remove_notification_listener(listener) notification_listeners.delete(listener) end
Removes a notification listener added with {#on_ui_event}
@param [Object] listener the listener ID returned by
{#on_ui_event}
# File lib/roby/app.rb, line 2710 def remove_ui_event_listener(listener) ui_event_listeners.delete(listener) end
# File lib/roby/app.rb, line 1523 def require(absolute_path) # Make the file relative to the search path file = make_path_relative(absolute_path) Roby::Application.info "loading #{file} (#{absolute_path})" isolate_load_errors("ignored file #{file}") do if file != absolute_path Kernel.require(file) else Kernel.require absolute_path end end end
Call to require this roby application to be in a Roby
application
It tries to guess the app directory. If none is found, it raises.
# File lib/roby/app.rb, line 360 def require_app_dir(needs_current: false, allowed_outside: true) guess_app_dir if !@app_dir raise ArgumentError, "your current directory does not seem to be a Roby application directory; did you forget to run 'roby init'?" end if needs_current needs_to_be_in_current_app(allowed_outside: allowed_outside) end end
Loads the models, based on the given robot name and robot type
# File lib/roby/app.rb, line 1537 def require_models # Set up the loaded plugins call_plugins(:require_config, self, deprecated: "define 'require_models' instead") call_plugins(:require_models, self) require_handlers.each do |handler| isolate_load_errors("while calling #{handler}") do handler.call end end define_actions_module if auto_load_models? auto_require_planners end define_main_planner_if_needed action_handlers.each do |act| isolate_load_errors("error in #{act}") do app_module::Actions::Main.class_eval(&act) end end additional_model_files.each do |path| require File.expand_path(path) end if auto_load_models? auto_require_models end # Set up the loaded plugins call_plugins(:finalize_model_loading, self) plan.refresh_relations end
Loads the planner models
This method is called at the end of {#require_models}, before the plugins' require_models
hook is called
# File lib/roby/app.rb, line 1700 def require_planners Roby.warn_deprecated "Application#require_planners is deprecated and has been renamed into #auto_require_planners" auto_require_planners end
# File lib/roby/app.rb, line 1810 def require_robot_file p = find_file('config', 'robots', "#{robot_name}.rb", order: :specific_first) || find_file('config', 'robots', "#{robot_type}.rb", order: :specific_first) if p @default_auto_load = false require p if !robot_type robot(robot_name, robot_name) end elsif !find_dir('config', 'robots', order: :specific_first) || (robot_name == robots.default_robot_name) || !robots.strict? Roby.warn "#{robot_name}:#{robot_type} is selected as the robot, but there is" if robot_name == robot_type Roby.warn "no file named config/robots/#{robot_name}.rb" else Roby.warn "neither config/robots/#{robot_name}.rb nor config/robots/#{robot_type}.rb" end Roby.warn "run roby gen robot #{robot_name} in your app to create one" Roby.warn "initialization will go on, but this behaviour is deprecated and will be removed in the future" else raise NoSuchRobot, "cannot find config file for robot #{robot_name} of type #{robot_type} in config/robots/" end end
Reset the current log dir so that {#setup} picks a new one
# File lib/roby/app.rb, line 1253 def reset_log_dir @log_dir = nil end
Reset the plan to a new Plan
object
# File lib/roby/app.rb, line 1258 def reset_plan(plan = ExecutablePlan.new) @plan = plan end
Quits this app and replaces with a new one after a proper cleanup
@param [String] cmdline the command line to exec after quitting. If
not given, will restart using the same command line as the one that started this process
# File lib/roby/app.rb, line 1966 def restart(*cmdline) @restarting = true @restart_cmdline = if cmdline.empty? if defined? ORIGINAL_ARGV [$0, *ORIGINAL_ARGV] else [$0, *ARGV] end else cmdline end plan.execution_engine.quit end
Whether {#run} should exec a new process on quit or not
# File lib/roby/app.rb, line 1957 def restarting? !!@restarting end
Sets up the name and type of the robot. This can be called only once in a given Roby
controller.
# File lib/roby/app.rb, line 1168 def robot(name, type = nil) @robot_name, @robot_type = robots.resolve(name, type) end
The robot name
@return [String,nil]
# File lib/roby/app.rb, line 1151 def robot_name if @robot_name then @robot_name else robots.default_robot_name end end
Test
if the given name is a valid robot name
# File lib/roby/app.rb, line 1627 def robot_name?(name) !robots.strict? || robots.has_robot?(name) end
The robot type
@return [String,nil]
# File lib/roby/app.rb, line 1160 def robot_type if @robot_type then @robot_type else robots.default_robot_type end end
The robot names configuration
@return [App::RobotNames]
# File lib/roby/app.rb, line 929 def robots if !@robots robots = App::RobotNames.new(options['robots'] || Hash.new) robots.strict = !!options['robots'] @robots = robots end @robots end
The list of model classes that allow to discover all models in this app
@return [Array<#each_submodel>]
# File lib/roby/app.rb, line 2492 def root_models models = [Task, TaskService, TaskEvent, Actions::Interface, Actions::Library, Coordination::ActionScript, Coordination::ActionStateMachine, Coordination::TaskScript] each_responding_plugin(:root_models) do |config_extension| models.concat(config_extension.root_models) end models end
# File lib/roby/app.rb, line 1912 def run(thread_priority: 0, &block) prepare engine_config = self.engine engine = self.plan.execution_engine plugins = self.plugins.map { |_, mod| mod if (mod.respond_to?(:start) || mod.respond_to?(:run)) }.compact engine.once do run_plugins(plugins, &block) end @thread = Thread.new do Thread.current.priority = thread_priority engine.run cycle: engine_config['cycle'] || 0.1 end join ensure shutdown @thread = nil if restarting? Kernel.exec *@restart_cmdline end end
Helper for Application#run
to call the plugin's run or start methods while guaranteeing the system's cleanup
This method recursively calls each plugin's run
method (if defined) in block forms. This guarantees that the plugins can use a begin/rescue/end mechanism to do their cleanup
If no run-related cleanup is required, the plugin can define a start(app) method instead.
Note that the cleanup we talk about here is related to running. Cleanup required after setup
must be done in cleanup
# File lib/roby/app.rb, line 1992 def run_plugins(mods, &block) if mods.empty? yield if block_given? else mod = mods.shift if mod.respond_to?(:start) mod.start(self) run_plugins(mods, &block) else mod.run(self) do run_plugins(mods, &block) end end end end
Whether we're inside {#run}
# File lib/roby/app.rb, line 1952 def running? !!@thread end
The list of paths in which the application should be looking for files
@return [Array<String>]
# File lib/roby/app.rb, line 393 def search_path if !@search_path if app_dir [app_dir] else [] end else @search_path end end
Returns true if the given path points to a file under {#app_dir}
# File lib/roby/app.rb, line 2429 def self_file?(path) find_base_path_for(path) == app_path end
Does basic setup of the Roby
environment. It loads configuration files and sets up singleton objects.
After a call to setup
, the Roby
services that do not require an execution loop to run should be available
Plugins that define a setup(app) method will see their method called at this point
The cleanup
method is the reverse of setup
# File lib/roby/app.rb, line 815 def setup base_setup # Set up the loaded plugins call_plugins(:setup, self) # And run the setup handlers setup_handlers.each(&:call) require_models # Main is always included in the planner list self.planners << app_module::Actions::Main # Attach the global fault tables to the plan self.planners.each do |planner| if planner.respond_to?(:each_fault_response_table) planner.each_fault_response_table do |table, arguments| plan.use_fault_response_table table, arguments end end end rescue Exception begin cleanup rescue Exception => e Roby.warn "failed to cleanup after #setup raised" Roby.log_exception_with_backtrace(e, Roby, :warn) end raise end
# File lib/roby/app.rb, line 2302 def setup_for_minimal_tooling self.public_logs = false self.auto_load_models = false self.single = true self.modelling_only = true setup 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.
# File lib/roby/app.rb, line 1444 def setup_loggers(ignore_missing: false, redirections: true) Robot.logger.progname = robot_name || 'Robot' return if !log['levels'] # Set up log levels log['levels'].each do |name, value| const_name = name.modulize mod = begin Kernel.constant(const_name) rescue NameError => e if ignore_missing next elsif name != const_name raise InvalidLoggerName, "cannot resolve logger #{name} (resolved as #{const_name}): #{e.message}" else raise InvalidLoggerName, "cannot resolve logger #{name}: #{e.message}" end end if value =~ /^(\w+):(.+)$/ value, file = $1, $2 file = file.gsub('ROBOT', robot_name) if robot_name end level = Logger.const_get(value) io = if redirections && file path = File.expand_path(file, log_dir) Robot.info "redirected logger for #{mod} to #{path} (level #{level})" io = File.open(path, 'w') io.sync = true log_files[path] ||= io else STDOUT end new_logger = Logger.new(io) new_logger.level = level new_logger.formatter = mod.logger.formatter new_logger.progname = [name, robot_name].compact.join(" ") mod.logger = new_logger end end
Publishes a REST API
The REST API will long-term replace the shell interface. It is however currently too limited for this purpose. Whether one should use one or the other is up to the application, but prefer the REST API if it suits your needs
# File lib/roby/app.rb, line 1877 def setup_rest_interface require 'roby/interface/rest' if @rest_interface raise RuntimeError, "there is already a REST interface started, call #stop_rest_interface first" end composite_api = Class.new(Grape::API) composite_api.mount Interface::REST::API call_plugins(:setup_rest_interface, self, composite_api) @rest_interface = Interface::REST::Server.new( self, host: rest_interface_host, port: rest_interface_port, api: composite_api) @rest_interface.start if rest_interface_port != Interface::DEFAULT_REST_PORT Robot.info "REST interface started on port #{@rest_interface.port(timeout: nil)}" else Robot.debug "REST interface started on port #{rest_interface_port}" end @rest_interface end
# File lib/roby/app.rb, line 1797 def setup_robot_names_from_config_dir robot_config_files = find_files_in_dirs 'config', 'robots', all: true, order: :specific_first, pattern: lambda { |p| File.extname(p) == ".rb" } robots.strict = !robot_config_files.empty? robot_config_files.each do |path| robot_name = File.basename(path, ".rb") robots.robots[robot_name] ||= robot_name end end
Publishes a shell interface
This method publishes a Roby::Interface
object using {Interface::TCPServer}. It is published on {Interface::DEFAULT_PORT} by default. This default can be overriden by setting {#shell_interface_port} either in config/init.rb, or in a {Robot.setup} block in the robot configuration file.
The shell interface is started in setup
and stopped in cleanup
@see stop_shell_interface
# File lib/roby/app.rb, line 1845 def setup_shell_interface require 'roby/interface' if @shell_interface raise RuntimeError, "there is already a shell interface started, call #stop_shell_interface first" end @shell_interface = Interface::TCPServer.new( self, host: shell_interface_host, port: shell_interface_port) shell_interface.abort_on_exception = shell_abort_on_exception? if shell_interface_port != Interface::DEFAULT_PORT Robot.info "shell interface started on port #{shell_interface_port}" else Robot.debug "shell interface started on port #{shell_interface_port}" end end
# File lib/roby/app.rb, line 2346 def shell; self.shell = true end
# File lib/roby/app.rb, line 2341 def simulation; self.simulation = true end
# File lib/roby/app.rb, line 2348 def single; @single = true end
# File lib/roby/app.rb, line 2010 def start_log_server(logfile, options = Hash.new) require 'roby/droby/logfile/server' # Allocate a TCP server to get an ephemeral port, and pass it to # roby-display sampling_period = DRoby::Logfile::Server::DEFAULT_SAMPLING_PERIOD sampling_period = Float(options['sampling_period'] || sampling_period) tcp_server = TCPServer.new(Integer(options['port'] || 0)) server_flags = ["--fd=#{tcp_server.fileno}", "--sampling=#{sampling_period}", logfile] redirect_flags = Hash[tcp_server => tcp_server] if options['debug'] server_flags << "--debug" elsif options['silent'] redirect_flags[:out] = redirect_flags[:err] = :close end @log_server_port = tcp_server.local_address.ip_port @log_server_pid = Kernel.spawn( Gem.ruby, File.join(Roby::BIN_DIR, "roby-display"), 'server', *server_flags, redirect_flags) ensure tcp_server.close if tcp_server end
# File lib/roby/app.rb, line 2008 def stop; call_plugins(:stop, self) end
# File lib/roby/app.rb, line 2035 def stop_log_server if @log_server_pid Process.kill('INT', @log_server_pid) @log_server_pid = nil end end
Stops a running REST interface
# File lib/roby/app.rb, line 1901 def stop_rest_interface(join: false) if @rest_interface # In case we're shutting down while starting up, # we must synchronize with the start to ensure that # EventMachine will be properly stopped @rest_interface.wait_start @rest_interface.stop @rest_interface.join if join end end
Stops a running shell interface
This is a no-op if no shell interface is currently running
# File lib/roby/app.rb, line 1864 def stop_shell_interface if @shell_interface @shell_interface.close @shell_interface = nil end end
Given a model class, returns the full path of an existing test file that is meant to verify this model
# File lib/roby/app.rb, line 1659 def test_files_for(model) return [] if !model.respond_to?(:definition_location) || !model.definition_location test_files = Array.new model.definition_location.each do |location| file = location.absolute_path next if !(base_path = find_base_path_for(file)) relative = Pathname.new(file).relative_path_from(base_path) split = relative.each_filename.to_a next if split[0] != 'models' split[0] = 'test' split[-1] = "test_#{split[-1]}" canonical_testpath = [base_path, *split].join(File::SEPARATOR) if File.exist?(canonical_testpath) test_files << canonical_testpath end end test_files end
# File lib/roby/app.rb, line 2344 def testing; self.testing = true end
The time tag. It is a time formatted as YYYYMMDD-HHMM used to mark log directories
# File lib/roby/app.rb, line 1275 def time_tag @time_tag ||= Time.now.strftime('%Y%m%d-%H%M') end
Sends a message to all UI event listeners
# File lib/roby/app.rb, line 2685 def ui_event(name, *args) each_ui_event_listener do |block| block.call(name, *args) end end
Ensure tha require'd files that match the given pattern can be re-required
# File lib/roby/app.rb, line 2456 def unload_features(*pattern) patterns = search_path.map { |p| Regexp.new(File.join(p, *pattern)) } patterns << Regexp.new("^#{File.join(*pattern)}") $LOADED_FEATURES.delete_if { |path| patterns.any? { |p| p =~ path } } end
# File lib/roby/app.rb, line 1778 def update_load_path search_path.reverse.each do |app_dir| $LOAD_PATH.delete(app_dir) $LOAD_PATH.unshift(app_dir) libdir = File.join(app_dir, 'lib') if File.directory?(libdir) $LOAD_PATH.delete(libdir) $LOAD_PATH.unshift(libdir) end end find_dirs('lib', 'ROBOT', all: true, order: :specific_last). each do |libdir| if !$LOAD_PATH.include?(libdir) $LOAD_PATH.unshift libdir end end end
Loads the plugins whose name are listed in names
# File lib/roby/app.rb, line 1096 def using(*names, force: false) if !plugins_enabled? && !force raise PluginsDisabled, "plugins are disabled, cannot load #{names.join(", ")}" end register_plugins(force: true) names.map do |name| name = name.to_s unless plugin = plugin_definition(name) raise ArgumentError, "#{name} is not a known plugin (available plugins: #{available_plugins.map { |n, *_| n }.join(", ")})" end name, dir, mod, init = *plugin if already_loaded = plugins.find { |n, m| n == name && m == mod } next(already_loaded[1]) end if dir filter_out_patterns.push(/#{Regexp.quote(dir)}/) 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 add_plugin(name, mod) end end