lib/roby/interface.rb in roby-0.7.3 vs lib/roby/interface.rb in roby-0.8.0
- old
+ new
@@ -1,63 +1,7 @@
-require 'thread'
-require 'roby'
-require 'roby/planning'
-require 'facets/basicobject'
require 'utilrb/column_formatter'
-require 'stringio'
-require 'roby/robot'
-module Robot
- def self.prepare_action(name, arguments)
- control = Roby.control
-
- # Check if +name+ is a planner method, and in that case
- # add a planning method for it and plan it
- planner_model = control.planners.find do |planner_model|
- planner_model.has_method?(name)
- end
- if !planner_model
- raise ArgumentError, "no such planning method #{name}"
- end
-
- m = planner_model.model_of(name, arguments)
-
- # HACK: m.returns should not be nil, but it sometimes happen
- returns_model = (m.returns if m && m.returns) || Task
-
- if returns_model.kind_of?(Roby::TaskModelTag)
- task = Roby::Task.new
- task.extend returns_model
- else
- # Create an abstract task which will be planned
- task = returns_model.new
- end
-
- planner = Roby::PlanningTask.new(:planner_model => planner_model, :method_name => name, :method_options => arguments)
- task.planned_by planner
- return task, planner
- end
-
- def self.method_missing(name, *args)
- if name.to_s =~ /!$/
- name = $`.to_sym
- else
- super
- end
-
- if args.size > 1
- raise ArgumentError, "wrong number of arguments (#{args.size} for 1) in #{name}!"
- end
-
- options = args.first || {}
- task, planner = Robot.prepare_action(name, options)
- Roby.control.plan.insert(task)
-
- return task, planner
- end
-end
-
module Roby
# An augmented DRbObject which allow to properly interface with remotely
# running plan objects.
class RemoteObjectProxy < DRbObject
attr_accessor :remote_interface
@@ -86,12 +30,37 @@
# Create a RemoteInterface object for the remote object represented by
# +interface+, where +interface+ is a DRbObject for a remote Interface
# object.
def initialize(interface)
@interface = interface
- end
+ reconnect
+ end
+ def reconnect
+ remote_models = @interface.task_models
+ remote_models.map do |klass|
+ klass = klass.proxy(nil)
+
+ if klass.respond_to?(:remote_name)
+ # This is a local proxy for a remote model. Add it in our
+ # namespace as well.
+ path = klass.remote_name.split '::'
+ klass_name = path.pop
+ mod = Object
+ while !path.empty?
+ name = path.shift
+ mod = begin
+ mod.const_get(name)
+ rescue NameError
+ mod.const_set(name, Module.new)
+ end
+ end
+ mod.const_set(klass_name, klass)
+ end
+ end
+ end
+
# Returns a Query object which can be used to interactively query the
# running plan
def find_tasks(model = nil, args = nil)
q = Query.new(self)
if model
@@ -126,10 +95,171 @@
def instance_methods(include_super = false) # :nodoc:
Interface.instance_methods(false).
actions.map { |name| "#{name}!" }
end
+
+ def actions_summary(with_advanced = false)
+ methods = @interface.actions
+ if !with_advanced
+ methods = methods.delete_if { |m| m.description.advanced? }
+ end
+
+ if !methods.empty?
+ puts
+ desc = methods.map do |p|
+ doc = p.description.doc || ["(no description set)"]
+ Hash['Name' => "#{p.name}!", 'Description' => doc.join("\n")]
+ end
+
+ ColumnFormatter.from_hashes(desc, STDOUT,
+ :header_delimiter => true,
+ :column_delimiter => "|",
+ :order => %w{Name Description})
+ puts
+ end
+
+ nil
+ end
+
+ def actions(with_advanced = false)
+ @interface.actions.each do |m|
+ next if m.description.advanced? if !with_advanced
+ display_action_description(m)
+ puts
+ end
+ nil
+ end
+
+ # Standard way to display a set of tasks
+ def task_set_to_s(task_set) # :nodoc:
+ if task_set.empty?
+ return "no tasks"
+ end
+
+ task = task_set.map do |task|
+ state_name = %w{pending starting running finishing finished}.find do |state_name|
+ task.send("#{state_name}?")
+ end
+
+ since = task.start_time
+ lifetime = task.lifetime
+ Hash['Task' => task.to_s,
+ 'State' => state_name,
+ 'Since' => (since.asctime if since),
+ 'Lifetime' => (Time.at(lifetime).to_hms if lifetime)
+ ]
+ end
+
+ io = StringIO.new
+ ColumnFormatter.from_hashes(task, STDOUT,
+ :header_delimiter => true,
+ :column_delimiter => "|",
+ :order => %w{Task State Lifetime Since})
+ end
+
+ # Displays information about the plan's missions
+ def missions
+ missions = find_tasks.mission.to_a
+ task_set_to_s(missions)
+ nil
+ end
+
+ # Displays information about the running tasks
+ def running_tasks
+ tasks = find_tasks.running.to_a
+ task_set_to_s(tasks)
+ nil
+ end
+
+ # Displays details about the actions matching 'regex'
+ def describe(name, with_advanced = false)
+ name = Regexp.new(name)
+ m = @interface.actions.find_all { |p| name === p.name }
+
+ if !with_advanced
+ filtered = m.find_all { |m| !m.description.advanced? }
+ m = filtered if !filtered.empty?
+ end
+
+ if m.empty?
+ puts "no such method"
+ else
+ m.each do |desc|
+ puts
+ display_action_description(desc)
+ puts
+ end
+ end
+ nil
+ end
+
+ # Displays a help message
+ def help
+ puts
+ puts "Available Actions"
+ puts "================="
+ actions_summary
+ puts ""
+
+
+ puts <<-EOHELP
+each action is started with action_name!(:arg1 => value1, :arg2 => value2, ...)
+and returns the corresponding task object. A message is displayed in the shell
+when the task finishes."
+
+Shell Commands
+==============
+Command | Help
+---------------------------------------------------------------------------------------------
+actions_summary(advanced = false) | displays the list of actions with a short documentation |
+actions(advanced = false) | displays details for each available actions |
+describe(regex) | displays details about the actions matching 'regex' |
+missions | displays the set of running missions with their status |
+running_tasks | displays the set of running tasks with their status |
+ | |
+help | this help message |
+
+ EOHELP
+ end
+
+ # Standard display of an action description. +m+ is a PlanningMethod
+ # object.
+ def display_action_description(m) # :nodoc:
+ args = m.description.arguments.
+ sort_by { |arg_desc| arg_desc.name }
+
+ first = true
+ args_summary = args.map do |arg_desc|
+ name = arg_desc.name
+ is_required = arg_desc.required
+ format = if is_required then "%s"
+ else "[%s]"
+ end
+ text = format % ["#{", " if !first}:#{name} => #{name}"]
+ first = false
+ text
+ end
+
+ args_table = args.
+ map do |arg_desc|
+ Hash['Argument' => arg_desc.name,
+ 'Description' => (arg_desc.doc || "(no description set)")]
+ end
+
+ method_doc = m.description.doc || [""]
+ puts "#{m.name}! #{args_summary.join("")}\n#{method_doc.join("\n")}"
+ if m.description.arguments.empty?
+ puts "No arguments"
+ else
+ ColumnFormatter.from_hashes(args_table, STDOUT,
+ :left_padding => " ",
+ :header_delimiter => true,
+ :column_delimiter => "|",
+ :order => %w{Argument Description})
+ end
+ end
def method_missing(m, *args) # :nodoc:
result = @interface.send(m, *args)
if result.kind_of?(RemoteObjectProxy)
@@ -143,25 +273,27 @@
end
# This class is used to interface with the Roby event loop and plan. It is the
# main front object when accessing a Roby core remotely
class Interface
+ # This module defines the hooks needed to plug Interface objects onto
+ # ExecutionEngine
module GatherExceptions
# The set of Interface objects that have been registered to us
attribute(:interfaces) { Array.new }
# Register a new Interface object so that it gets feedback information
# from the running controller.
def register_interface(iface)
- Roby::Control.synchronize do
+ Roby.synchronize do
interfaces << iface
end
end
# Pushes a exception message to all the already registered remote interfaces.
def push_exception_message(name, error, tasks)
- Roby::Control.synchronize do
+ Roby.synchronize do
msg = Roby.format_exception(error.exception).join("\n")
msg << "\nThe following tasks have been killed:\n"
tasks.each do |t|
msg << " "
if error.exception.involved_plan_object?(t)
@@ -188,50 +320,51 @@
super if defined? super
push_exception_message("fatal exception", error, tasks)
end
end
- # The Roby::Control object this interface is working on
- attr_reader :control
+ # The engine this interface is tied to
+ attr_reader :engine
# The set of pending messages that are to be displayed on the remote interface
attr_reader :pending_messages
# Creates a local server for a remote interface, acting on +control+
- def initialize(control)
- @control = control
+ def initialize(engine)
@pending_messages = Queue.new
+ @engine = engine
- Roby::Control.extend GatherExceptions
- Roby::Control.register_interface self
+ engine.extend GatherExceptions
+ engine.register_interface self
end
# Clear the current plan: remove all running and permanent tasks.
def clear
- Roby.execute do
+ engine.execute do
plan.missions.dup.each { |t| plan.discard(t) }
- plan.keepalive.dup.each { |t| plan.auto(t) }
+ plan.permanent_tasks.dup.each { |t| plan.auto(t) }
+ plan.permanent_events.dup.each { |t| plan.auto(t) }
end
end
# Make the Roby event loop quit
- def stop; control.quit; nil end
+ def stop; engine.quit; nil end
# The Roby plan
- def plan; Roby.plan end
+ def plan; engine.plan end
# Synchronously call +m+ on +tasks+ with the given arguments. This,
# along with the implementation of RemoteInterface#method_missing,
# ensures that no interactive operations are performed outside the
# control thread.
def call(task, m, *args)
- Roby.execute do
+ engine.execute do
if m.to_s =~ /!$/
event_name = $`
# Check if the called event is terminal. If it is the case,
# discard the task before calling it, and make sure the user
# will get a message
#
if task.event(event_name).terminal?
- plan.discard(task)
+ plan.unmark_mission(task)
task.on(:stop) { |ev| pending_messages << "task #{ev.task} stopped by user request" }
else
task.on(event_name) { |ev| pending_messages << "done emitting #{ev.generator}" }
end
end
@@ -268,82 +401,33 @@
def reload
Roby.app.reload
nil
end
- # Displays the set of models as well as their superclasses
- def models
+ # Returns the set of task models as DRobyTaskModel objects. The standard
+ # Roby task models are excluded.
+ def task_models
task_models = []
- Roby.execute do
+ engine.execute do
ObjectSpace.each_object(Class) do |obj|
- task_models << obj if obj <= Roby::Task && obj.name !~ /^Roby::/
+ if obj <= Roby::Task && obj.name !~ /^Roby::/
+ task_models << obj
+ end
end
end
-
- task_models.map do |model|
- "#{model} #{model.superclass}"
- end
+ task_models.map { |t| t.droby_dump(nil) }
end
- # Displays the set of actions which are available through the planners
- # registered on #control. See Control#planners
+ # Returns the set of PlanningMethod objects that describe the methods
+ # exported in the application's planners.
def actions
- control.planners.
- map { |p| p.planning_methods_names.to_a }.
- flatten.
- sort
+ Roby.app.planners.
+ map do |p|
+ p.planning_methods
+ end.flatten.sort_by { |p| p.name }
end
- # Pretty-prints a set of tasks
- def task_set_to_s(task_set) # :nodoc:
- if task_set.empty?
- return "no tasks"
- end
-
- task = task_set.map do |task|
- state_name = %w{pending starting running finishing finished}.find do |state_name|
- task.send("#{state_name}?")
- end
-
- start_event = task.history.find { |ev| ev.symbol == :start }
- since = if start_event then start_event.time
- else 'N/A'
- end
- { 'Task' => task.to_s, 'Since' => since, 'State' => state_name }
- end
-
- io = StringIO.new
- ColumnFormatter.from_hashes(task, io) { %w{Task Since State} }
- "\n#{io.string}"
- end
-
- # Returns a string representing the set of running tasks
- def running_tasks
- Roby.execute do
- task_set_to_s(Roby.plan.find_tasks.running.to_a)
- end
- end
-
- # Returns a string representing the set of missions
- def missions
- Roby.execute do
- task_set_to_s(control.plan.missions)
- end
- end
-
- # Returns a string representing the set of tasks present in the plan
- def tasks
- Roby.execute do
- task_set_to_s(Roby.plan.known_tasks)
- end
- end
-
- def methods
- result = super
- result + actions.map { |n| "#{n}!" }
- end
-
# Called every once in a while by RemoteInterface to read and clear the
# set of pending messages.
def poll_messages
result = []
while !pending_messages.empty?
@@ -367,18 +451,18 @@
end
options = args.first || {}
task, planner = Robot.prepare_action(name, options)
begin
- Roby.wait_until(planner.event(:success)) do
- control.plan.insert(task)
+ engine.wait_until(planner.event(:success)) do
+ plan.add_mission(task)
yield(task, planner) if block_given?
end
rescue Roby::UnreachableEvent
raise RuntimeError, "cannot start #{name}: #{planner.terminal_event.context.first}"
end
- Roby.execute do
+ engine.execute do
result = planner.result
result.on(:failed) { |ev| pending_messages << "task #{ev.task} failed" }
result.on(:success) { |ev| pending_messages << "task #{ev.task} finished successfully" }
RemoteObjectProxy.new(result)
end