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