require 'roby/plan-object' require 'roby/exceptions' require 'roby/event' require 'utilrb/module/attr_predicate' module Roby class TaskModelTag < Module module ClassExtension # Returns the list of static arguments required by this task model def arguments(*new_arguments) new_arguments.each do |arg_name| argument_set << arg_name.to_sym unless method_defined?(arg_name) define_method(arg_name) { arguments[arg_name] } end end @argument_enumerator ||= enum_for(:each_argument_set) end # Declares a set of arguments required by this task model def argument(*args); arguments(*args) end end include TaskModelTag::ClassExtension def initialize(&block) super do inherited_enumerable("argument_set", "argument_set") { ValueSet.new } unless const_defined? :ClassExtension const_set(:ClassExtension, Module.new) end self::ClassExtension.include TaskModelTag::ClassExtension end class_eval(&block) if block_given? end def clear_model @argument_set.clear if @argument_set end end # Base class for task events # When events are emitted, then the created object is # an instance of a class derived from this one class TaskEvent < Event # The task which fired this event attr_reader :task def initialize(task, generator, propagation_id, context, time = Time.now) @task = task @terminal_flag = generator.terminal_flag super(generator, propagation_id, context, time) end # Returns the set of events from the task that are the cause of this # event def task_sources result = ValueSet.new event_sources = sources for ev in event_sources gen = ev.generator if gen.respond_to?(:task) && gen.task == task result.merge ev.task_sources end end if result.empty? result << self end result end def to_s "#{generator.to_s}@#{propagation_id} [#{time.to_hms}]: #{context}" end def pretty_print(pp) pp.text "at [#{time.to_hms}/#{propagation_id}] in the " generator.pretty_print(pp) pp.breakable pp.group(2) do pp.seplist(context || []) { |v| v.pretty_print pp } end end # If the event model defines a controlable event # By default, an event is controlable if the model # responds to #call def self.controlable?; respond_to?(:call) end # If the event is controlable def controlable?; self.class.controlable? end class << self # Called by Task.update_terminal_flag to update the flag attr_writer :terminal end # If the event model defines a terminal event def self.terminal?; @terminal end # If this event is terminal def success?; @terminal_flag == :success end # If this event is terminal def failure?; @terminal_flag == :failure end # If this event is terminal def terminal?; @terminal_flag end # The event symbol def self.symbol; @symbol end # The event symbol def symbol; self.class.symbol end end # A task event model bound to a particular task instance # The Task/TaskEvent/TaskEventGenerator relationship is # comparable to the Class/UnboundMethod/Method one: # * a Task object is a model for a task, a Class in a model for an object # * a TaskEvent object is a model for an event instance (the instance being unspecified), # an UnboundMethod is a model for an instance method # * a TaskEventGenerator object represents a particular event model # *bound* to a particular task instance, a Method object represents a particular method # bound to a particular object class TaskEventGenerator < EventGenerator # The task we are part of attr_reader :task # The event symbol (its name as a Symbol object) attr_reader :symbol # The event class attr_reader :event_model def initialize(task, model) @task, @event_model = task, model @symbol = model.symbol super(model.respond_to?(:call)) end def default_command(context) event_model.call(task, context) end # See PlanObject::child_plan_object. child_plan_object :task # The event plan. It is the same as task.plan and is actually updated # by task.plan=. It is redefined here for performance reasons. attr_accessor :plan # Fire the event def fire(event) task.fire_event(event) super end # See EventGenerator#calling # # In TaskEventGenerator, this hook checks that the task is running def calling(context) super if defined? super if task.finished? && !terminal? raise CommandFailed.new(nil, self), "#{symbol}!(#{context})) called by #{Propagation.sources} but the task has finished. Task has been terminated by #{task.event(:stop).history.first.sources}." elsif task.pending? && symbol != :start raise CommandFailed.new(nil, self), "#{symbol}!(#{context})) called by #{Propagation.sources} but the task is not running" elsif task.running? && symbol == :start raise CommandFailed.new(nil, self), "#{symbol}!(#{context})) called by #{Propagation.sources} but the task is already running. Task has been started by #{task.event(:start).history.first.sources}." end end # See EventGenerator#fired # # In TaskEventGenerator, this hook calls the unreachable handlers added # by EventGenerator#if_unreachable when the task has finished, not # before def fired(event) super if defined? super if symbol == :stop task.each_event { |ev| ev.unreachable!(task.terminal_event) } end end def related_tasks(result = nil) tasks = super tasks.delete(task) tasks end def each_handler super if self_owned? task.each_handler(event_model.symbol) { |o| yield(o) } end end def each_precondition super task.each_precondition(event_model.symbol) { |o| yield(o) } end def controlable?; event_model.controlable? end attr_accessor :terminal_flag def terminal?; !!@terminal_flag end def success?; @terminal_flag == :success end def failure?; @terminal_flag == :failure end def added_child_object(child, relations, info) super if defined? super if relations.include?(EventStructure::CausalLink) && child.respond_to?(:task) && child.task == task && child.terminal_flag != terminal_flag task.update_terminal_flag end end def removed_child_object(child, relations) super if defined? super if relations.include?(EventStructure::CausalLink) && child.respond_to?(:task) && child.task == task && terminal_flag task.update_terminal_flag end end def new(context); event_model.new(task, self, Propagation.propagation_id, context) end def to_s "#{task}/#{symbol}" end def inspect "#{task.inspect}/#{symbol}: #{history.to_s}" end def pretty_print(pp) pp.text "#{symbol} event of #{task.class}:0x#{task.address.to_s(16)}" end def achieve_with(obj) child_task, child_event = case obj when Roby::Task: [obj, obj.event(:success)] when Roby::TaskEventGenerator: [obj.task, obj] end if child_task unless task.realized_by?(child_task) task.realized_by child_task, :success => [child_event.symbol], :remove_when_done => true end super(child_event) else super(obj) end end end class TaskArguments < Hash private :delete, :delete_if attr_reader :task def initialize(task) @task = task super() end def writable?(key) !(has_key?(key) && task.model.arguments.include?(key)) end def dup; self.to_hash end def to_hash inject({}) { |h, (k, v)| h[k] = v ; h } end alias :update! :[]= def []=(key, value) if writable?(key) if !task.read_write? raise OwnershipError, "cannot change the argument set of a task which is not owned #{task} is owned by #{task.owners} and #{task.plan} by #{task.plan.owners}" end updating super updated else raise ArgumentError, "cannot override task arguments" end end def updating; super if defined? super end def updated; super if defined? super end alias :do_merge! :merge! def merge!(hash) super do |key, old, new| if old == new then old elsif writable?(key) then new else raise ArgumentError, "cannot override task argument #{key}: trying to replace #{old} by #{new}" end end end end # In a plan, Task objects represent the activities of the robot. # # === Task models # # A task model is mainly described by: # # a set of named arguments, which are required to parametrize the # task instance. The argument list is described using Task.argument and # arguments are either set at object creation by passing an argument hash # to Task.new, or by calling Task#argument explicitely. # # a set of events, which are situations describing the task # progression. The base Roby::Task model defines the # +start+,+success+,+failed+ and +stop+ events. Events can be defined on # the models by using Task.event: # # class MyTask < Roby::Task # event :intermediate_event # end # # defines a non-controllable event, i.e. an event which can be emitted, but # cannot be triggered explicitely by the system. Controllable events are defined # by associating a block of code with the event, this block being responsible for # making the event emitted either in the future or just now. For instance, # # class MyTask < Roby::Task # event :intermediate_event do |context| # emit :intermediate_event # end # # event :other_event do |context| # Roby.once { emit :other_event } # end # end # # define two controllable event. In the first case, the event is # immediately emitted, and in the second case it will be emitted at the # beginning of the next execution cycle. # # === Executability # # By default, a task is not executable, which means that no event command # can be called and no event can be emitted. A task becomes executable # either because Task#executable= has explicitely been called or because it # has been inserted in a Plan object. Note that forcing executability with # #executable= is only useful for testing. When the Roby controller manages # a real systems, the executability property enforces the constraint that a # task cannot be executed outside of the plan supervision. # # Finally, it is possible to describe _abstract_ task models: tasks which # do represent an action, but for which the _means_ to perform that action # are still unknown. This is done by calling Task.abstract in the task definition: # # class AbstTask < Roby::Task # abstract # end # # An instance of an abstract model cannot be executed, even if it is included # in a plan. # # === Inheritance rules # # On task models, a submodel can inherit from a parent model if the actions # described by the parent model are also performed by the child model. For # instance, a Goto(x, y) model could be subclassed into a # Goto::ByFoot(x, y) model. # # The following constraints apply when subclassing a task model: # * a task subclass has at least the same events than the parent class # * changes to event attributes are limited. The rules are: # - a controlable event must remain controlable. Nonetheless, a # non-controlable event can become a controlable one # - a terminal event (i.e. a terminal event which ends the task # execution) cannot become non-terminal. Nonetheless, a non-terminal # event can become terminal. # class Task < PlanObject unless defined? RootTaskTag RootTaskTag = TaskModelTag.new include RootTaskTag end # Clears all definitions saved in this model. This is to be used by the # reloading code def self.clear_model class_eval do # Remove event models events.each_key do |ev_symbol| remove_const ev_symbol.to_s.camelize end [@events, @signal_sets, @forwarding_sets, @causal_link_sets, @argument_set, @handler_sets, @precondition_sets].each do |set| set.clear if set end end end # Declares an attribute set which follows the task models inheritance # hierarchy. Define the corresponding enumeration methods as well. # # For instance, # model_attribute_list 'signal' # # defines the model-level signals, which can be accessed through # .each_signal(model) # .signals(model) # #each_signal(model) # def self.model_attribute_list(name) # :nodoc: inherited_enumerable("#{name}_set", "#{name}_sets", :map => true) { Hash.new { |h, k| h[k] = ValueSet.new } } class_eval <<-EOD def self.each_#{name}(model) for obj in #{name}s(model) yield(obj) end self end def self.#{name}s(model) result = ValueSet.new each_#{name}_set(model, false) do |set| result.merge set end result end def each_#{name}(model); self.model.each_#{name}(model) { |o| yield(o) } end EOD end model_attribute_list('signal') model_attribute_list('forwarding') model_attribute_list('causal_link') model_attribute_list('handler') model_attribute_list('precondition') # The task arguments as symbol => value associative container attr_reader :arguments # The part of +arguments+ that is meaningful for this task model def meaningful_arguments(task_model = self.model) arguments.slice(*task_model.arguments) end # The task name def name @name ||= "#{model.name || self.class.name}#{arguments.to_s}:0x#{address.to_s(16)}" end # This predicate is true if this task is a mission for its owners. If # you want to know if it a mission for the local pDB, use Plan#mission? attr_predicate :mission?, true def inspect state = if pending? then 'pending' elsif starting? then 'starting' elsif running? then 'running' elsif finishing? then 'finishing' else 'finished' end "#<#{to_s} executable=#{executable?} state=#{state} plan=#{plan.to_s}>" end # Builds a task object using this task model # # The task object can be configured by a given block. After the # block is called, two things are checked: # * the task shall have a +start+ event # * the task shall have at least one terminal event. If no +stop+ event # is defined, then all terminal events are aliased to +stop+ def initialize(arguments = nil) #:yields: task_object super() if defined? super @arguments = TaskArguments.new(self) @arguments.merge!(arguments) if arguments @model = self.class yield(self) if block_given? initialize_events end # Helper methods which creates all the necessary TaskEventGenerator # objects and stores them in the #bound_events map def initialize_events # :nodoc: @instantiated_model_events = false # Create all event generators bound_events = Hash.new model.each_event do |ev_symbol, ev_model| bound_events[ev_symbol.to_sym] = TaskEventGenerator.new(self, ev_model) end @bound_events = bound_events end def model; self.class end def initialize_copy(old) # :nodoc: super @name = nil @history = old.history.dup @arguments = TaskArguments.new(self) arguments.do_merge! old.arguments arguments.instance_variable_set(:@task, self) initialize_events plan.discover(self) end def instantiate_model_event_relations return if @instantiated_model_events # Add the model-level signals to this instance @instantiated_model_events = true left_border = bound_events.values.to_value_set right_border = bound_events.values.to_value_set model.each_signal_set do |generator, signalled_events| next if signalled_events.empty? generator = bound_events[generator] right_border.delete(generator) for signalled in signalled_events signalled = bound_events[signalled] generator.signal signalled left_border.delete(signalled) end end model.each_forwarding_set do |generator, signalled_events| next if signalled_events.empty? generator = bound_events[generator] right_border.delete(generator) for signalled in signalled_events signalled = bound_events[signalled] generator.forward signalled left_border.delete(signalled) end end model.each_causal_link_set do |generator, signalled_events| next if signalled_events.empty? generator = bound_events[generator] right_border.delete(generator) for signalled in signalled_events signalled = bound_events[signalled] generator.add_causal_link signalled left_border.delete(signalled) end end update_terminal_flag # WARNING: this works only because: # * there is always at least updated_data as an intermediate event # * there is always one terminal event which is not stop start_event = bound_events[:start] stop_event = bound_events[:stop] left_border.delete(start_event) right_border.delete(start_event) left_border.delete(stop_event) right_border.delete(stop_event) for generator in left_border start_event.add_precedence(generator) unless generator.terminal? end # WARN: the start event CAN be terminal: it can be a signal from # :start to a terminal event # # Create the precedence relations between 'normal' events and the terminal events for terminal in left_border next unless terminal.terminal? for generator in right_border unless generator.terminal? generator.add_precedence(terminal) end end end end def plan=(new_plan) # :nodoc: if plan != new_plan if plan && plan.include?(self) raise ModelViolation.new, "still included in #{plan}, cannot change the plan" elsif self_owned? && running? raise ModelViolation.new, "cannot change the plan of a running task" end end super for _, ev in bound_events ev.plan = plan end end class << self # If this task is an abstract task # Abstract tasks are not executable. This attribute is # not inherited in the task hierarchy attr_reader :abstract alias :abstract? :abstract # Mark this task model as an abstract model def abstract @abstract = true end # Declare that nothing special is required to stop this task. # This makes +failed+ and +stop+ controlable events, and # makes the interruption sequence be stop! => calls failed! => # emits +failed+ => emits +stop+. def terminates event :failed, :command => true, :terminal => true interruptible end # Sets up a command for +stop+ in the case where +failed+ is also # controllable, if the command of +failed+ should be used to stop # the task. def interruptible if !has_event?(:failed) || !event_model(:failed).controlable? raise ArgumentError, "failed is not controlable" end event(:stop) do |context| if starting? on :start, self, :stop return end failed!(context) end end def setup_poll_method(block) # :nodoc: define_method(:poll) do return unless self_owned? begin poll_handler rescue Exception => e emit :failed, e end end define_method(:poll_handler, &block) end # Defines a block which will be called at each execution cycle for # each running task of this model. The block is called in the # instance context of the target task (i.e. using instance_eval) def poll(&block) if !block_given? raise "no block given" end setup_poll_method(block) on(:start) { Control.event_processing << method(:poll) } on(:stop) { Control.event_processing.delete(method(:poll)) } end end # Roby::Task is an abstract model. See Task::abstract abstract # Returns true if this task is from an abstract model. If it is the # case, the task is not executable. def abstract?; self.class.abstract? end # Check if this task is executable def executable?; !abstract? && !partially_instanciated? && super end # Returns true if this task's stop event is controlable def interruptible?; event(:stop).controlable? end # Set the executable flag. executable cannot be set to +false+ is the # task is running, and cannot be set to true on a finished task. def executable=(flag) return if flag == @executable return unless self_owned? if flag && !pending? raise ModelViolation, "cannot set the executable flag on a task which is not pending" elsif !flag && running? raise ModelViolation, "cannot unset the executable flag on a task which is running" end super end # True if all arguments defined by Task.argument on the task model are set. def fully_instanciated? @fully_instanciated ||= model.arguments.all? { |name| arguments.has_key?(name) } end # True if at least one argument required by the task model is not set. # See Task.argument. def partially_instanciated?; !fully_instanciated? end # True if this task has an event of the required model. The event model # can either be a event class or an event name. def has_event?(event_model) bound_events.has_key?(event_model) || self.class.has_event?(event_model) end # True if this task is starting, i.e. if its start event is pending # (has been called, but is not emitted yet) def starting?; event(:start).pending? end # True if this task has never been started def pending?; !starting? && !started? end # True if this task is currently running (i.e. is has already started, # and is not finished) def running?; started? && !finished? end # True if the task is finishing, i.e. if a terminal event is pending. def finishing? if running? each_event { |ev| return true if ev.terminal? && ev.pending? } end false end attr_predicate :started?, true attr_predicate :finished?, true attr_predicate :success?, true # True if the +failed+ event of this task has been fired def failed?; finished? && @success == false end # Remove all relations in which +self+ or its event are involved def clear_relations each_event { |ev| ev.clear_relations } super end # Update the terminal flag for the event models that are defined in # this task model. The event is terminal if model-level signals (set up # by Task::on) lead to the emission of the +stop+ event def self.update_terminal_flag # :nodoc: events = enum_events.map { |name, _| name } terminal_events = [:stop] events.delete(:stop) loop do old_size = terminal_events.size events.delete_if do |ev| if signals(ev).any? { |sig_ev| terminal_events.include?(sig_ev) } || forwardings(ev).any? { |sig_ev| terminal_events.include?(sig_ev) } terminal_events << ev true end end break if old_size == terminal_events.size end terminal_events.each do |sym| if ev = self.events[sym] ev.terminal = true else ev = superclass.event_model(sym) unless ev.terminal? event sym, :model => ev, :terminal => true, :command => (ev.method(:call) rescue nil) end end end end # Updates the terminal flag for all events in the task. An event is # terminal if the +stop+ event of the task will be called because this # event is. def update_terminal_flag # :nodoc: return unless @instantiated_model_events for _, ev in bound_events ev.terminal_flag = nil end success_events, failure_events, terminal_events = [event(:success)].to_value_set, [event(:failed)].to_value_set, [event(:stop), event(:success), event(:failed)].to_value_set loop do old_size = terminal_events.size for _, ev in bound_events for relation in [EventStructure::Signal, EventStructure::Forwarding] for target in ev.child_objects(relation) next if !target.respond_to?(:task) || target.task != self next if ev[target, relation] if success_events.include?(target) success_events << ev terminal_events << ev break elsif failure_events.include?(target) failure_events << ev terminal_events << ev break elsif terminal_events.include?(target) terminal_events << ev end end end success_events.include?(ev) || failure_events.include?(ev) || terminal_events.include?(ev) end break if old_size == terminal_events.size end for ev in success_events ev.terminal_flag = :success if ev.respond_to?(:task) && ev.task == self end for ev in failure_events ev.terminal_flag = :failure if ev.respond_to?(:task) && ev.task == self end for ev in (terminal_events - success_events - failure_events) ev.terminal_flag = true if ev.respond_to?(:task) && ev.task == self end end # Returns a sorted list of Event objects, for all events that have been # fired by this task def history history = [] each_event do |event| history += event.history end history.sort_by { |ev| ev.time } end # Returns the set of tasks directly related to this task, either # because of task relations or because of task events that are related # to other task events def related_tasks(result = nil) result = related_objects(nil, result) each_event do |ev| ev.related_tasks(result) end result end # Returns the set of events directly related to this task def related_events(result = nil) each_event do |ev| result = ev.related_events(result) end result.reject { |ev| ev.respond_to?(:task) && ev.task == self }. to_value_set end # This method is called by TaskEventGenerator#fire just before the event handlers # and commands are called def fire_event(event) # :nodoc: if !executable? raise TaskNotExecutable.new(self), "trying to fire #{event.generator.symbol} on #{self} but #{self} is not executable" end if finished? && !event.terminal? raise EmissionFailed.new(nil, self), "emit(#{event.symbol}: #{event.model}[#{event.context}]) called @#{event.propagation_id} by #{Propagation.sources} but the task has finished. Task has been terminated by #{event(:stop).history.first.sources}." elsif pending? && event.symbol != :start raise EmissionFailed.new(nil, self), "emit(#{event.symbol}: #{event.model}[#{event.context}]) called @#{event.propagation_id} by #{Propagation.sources} but the task is not running" elsif running? && event.symbol == :start raise EmissionFailed.new(nil, self), "emit(#{event.symbol}: #{event.model}[#{event.context}]) called @#{event.propagation_id} by #{Propagation.sources} but the task is already running. Task has been started by #{event(:start).history.first.sources}." end update_task_status(event) super if defined? super end # The event which has finished the task (if there is one) attr_reader :terminal_event # Call to update the task status because of +event+ def update_task_status(event) # :nodoc: if event.success? plan.task_index.set_state(self, :success?) self.success = true self.finished = true @terminal_event ||= event elsif event.failure? plan.task_index.set_state(self, :failed?) self.success = false self.finished = true @terminal_event ||= event elsif event.terminal? && !finished? plan.task_index.set_state(self, :finished?) self.finished = true @terminal_event ||= event end if event.symbol == :start plan.task_index.set_state(self, :running?) self.started = true end end # List of EventGenerator objects bound to this task attr_reader :bound_events # call-seq: # emit(event_model, *context) => self # # Emits +event_model+ in the given +context+. Event handlers are fired. # This is equivalent to # event(event_model).emit(*context) # def emit(event_model, *context) event(event_model).emit(*context) self end # Returns the TaskEventGenerator which describes the required event # model. +event_model+ can either be an event name or an Event class. def event(event_model) unless event = bound_events[event_model] event_model = self.event_model(event_model) unless event = bound_events[event_model.symbol] raise "cannot find #{event_model.symbol.inspect} in the set of bound events in #{self}. Known events are #{bound_events}." end end event end # call-seq: # on(event, task[, event1, event2, ...]) # on(event) { |event| ... } # on(event[, task, event1, event2, ...]) { |event| ... } # on(event, task[, event1, event2, ...], delay) # # Adds a signal from the given event to the specified targets, and/or # defines an event handler. Note that on(event, task) is # equivalent to on(event, task, event) # # +delay+, if given, specifies that the signal must be postponed for as # much time as specified. See EventGenerator#signal for valid values. def on(event_model, to = nil, *to_task_events, &user_handler) unless to || user_handler raise ArgumentError, "you must provide either a task or an event handler (got nil for both)" end generator = event(event_model) if Hash === to_task_events.last delay = to_task_events.pop end to_events = case to when Task if to_task_events.empty? [to.event(generator.symbol)] else to_task_events.map { |ev_model| to.event(ev_model) } end when EventGenerator: [to] else [] end to_events.push delay if delay generator.on(*to_events, &user_handler) self end # call-seq: # forward source_event, dest_task, ev1, ev2, ev3, ... # forward source_event, dest_task, ev1, ev2, ev3, delay_options # # Fowards +name+ to the events in +to_task_events+ on task +to+. The # target events will be emitted as soon as the +name+ event is emitted # on the receiving task, without calling any command. # # To call an event whenever other events are emitted, use the Signal # relation. See Task#on, Task.on and EventGenerator#on. As for Task#on, # forward(:start, task) is a shortcut to forward(:start, # task, :start). # # If a +delay_options+ hash is provided, the forwarding is not performed # immediately, but with a given delay. See EventGenerator#forward for # the delay specification. def forward(name, to, *to_task_events) generator = event(name) if Hash === to_task_events.last delay = to_task_events.pop end to_events = if to.respond_to?(:event) if to_task_events.empty? [to.event(generator.symbol)] else to_task_events.map { |ev| to.event(ev) } end elsif to.kind_of?(EventGenerator) [to] else raise ArgumentError, "expected Task or EventGenerator, got #{to}(#{to.class}: #{to.class.ancestors})" end to_events.each do |ev| generator.forward ev, delay end end attr_accessor :calling_event def method_missing(name, *args, &block) # :nodoc: if calling_event && calling_event.respond_to?(name) calling_event.send(name, *args, &block) else super end rescue raise $!, $!.message, $!.backtrace[1..-1] end @@event_command_id = 0 def self.allocate_event_command_id # :nodoc: @@event_command_id += 1 end # call-seq: # self.event(name, options = nil) { ... } -> event class or nil # # Define a new event in this task. # # ==== Available options # # command:: # either true, false or an event command for the new event. In that # latter case, the command is an object which must respond to # #to_proc. If it is true, a default handler is defined which simply # emits the event. If a block is given, it is used as the event # command. # # terminal:: # set to true if this event is a terminal event, i.e. if its emission # means that the task has finished. # # model:: # base class for the event model (see "Event models" below). The default is the # TaskEvent class # # ==== Event models # # When a task event (for instance +start+) is emitted, a Roby::Event # object is created to describe the information related to this # emission (time, sources, context information, ...). Task.event # defines a specific event model MyTask::MyEvent for each task event # with name :my_event. This specific model is by default a subclass of # Roby::TaskEvent, but it is possible to override that by using the +model+ # option. def self.event(ev, options = Hash.new, &block) options = validate_options(options, :command => nil, :terminal => nil, :model => TaskEvent) ev_s = ev.to_s ev = ev.to_sym if !options.has_key?(:command) if block define_method("event_command_#{ev_s}", &block) method = instance_method("event_command_#{ev_s}") end if method check_arity(method, 1) options[:command] = lambda do |dst_task, *event_context| begin dst_task.calling_event = dst_task.event(ev) method.bind(dst_task).call(*event_context) ensure dst_task.calling_event = nil end end end end validate_event_definition_request(ev, options) command_handler = options[:command] if options[:command].respond_to?(:call) # Define the event class task_klass = self new_event = Class.new(options[:model]) do @terminal = options[:terminal] @symbol = ev @command_handler = command_handler define_method(:name) { "#{task.name}::#{ev_s.camelize}" } singleton_class.class_eval do attr_reader :command_handler define_method(:name) { "#{task_klass.name}::#{ev_s.camelize}" } def to_s; name end end end setup_terminal_handler = false old_model = find_event_model(ev) if new_event.symbol != :stop && options[:terminal] && (!old_model || !old_model.terminal?) setup_terminal_handler = true end events[new_event.symbol] = new_event if setup_terminal_handler forward(new_event => :stop) end const_set(ev_s.camelize, new_event) if options[:command] # check that the supplied command handler can take two arguments check_arity(command_handler, 2) if command_handler # define #call on the event model new_event.singleton_class.class_eval do if command_handler define_method(:call, &command_handler) else def call(task, context) # :nodoc: task.emit(symbol, *context) end end end # define an instance method which calls the event command define_method("#{ev_s}!") do |*context| begin generator = event(ev) generator.call(*context) rescue EventNotExecutable => e if partially_instanciated? raise EventNotExecutable.new(generator), "#{ev_s}! called on #{generator.task} which is partially instanciated" elsif !plan raise EventNotExecutable.new(generator), "#{ev_s}! called on #{generator.task} but the task is in no plan" elsif !plan.executable? raise EventNotExecutable.new(generator), "#{ev_s}! called on #{generator.task} but the plan is not executable" elsif abstract? raise EventNotExecutable.new(generator), "#{ev_s}! called on #{generator.task} but the task is abstract" else raise EventNotExecutable.new(generator), "#{ev_s}! called on #{generator.task} which is not executable: #{e.message}" end end end end new_event end def self.validate_event_definition_request(ev, options) #:nodoc: if options[:command] && options[:command] != true && !options[:command].respond_to?(:call) raise ArgumentError, "Allowed values for :command option: true, false, nil and an object responding to #call. Got #{options[:command]}" end if ev.to_sym == :stop if options.has_key?(:terminal) && !options[:terminal] raise ArgumentError, "the 'stop' event cannot be non-terminal" end options[:terminal] = true end # Check for inheritance rules if events.include?(ev) raise ArgumentError, "event #{ev} already defined" elsif old_event = find_event_model(ev) if old_event.terminal? && !options[:terminal] raise ArgumentError, "trying to override a terminal event into a non-terminal one", caller(2) elsif old_event.controlable? && !options[:command] raise ArgumentError, "trying to override a controlable event into a non-controlable one", caller(2) end end end # Events defined by the task model inherited_enumerable(:event, :events, :map => true) { Hash.new } def self.enum_events @__enum_events__ ||= enum_for(:each_event) end # Iterates on all the events defined for this task def each_event # :yield:bound_event for _, ev in bound_events yield(ev) end end alias :each_plan_child :each_event # Returns the set of terminal events this task has. A terminal event is # an event whose emission announces the end of the task. In most case, # it is an event which is forwarded directly on indirectly to +stop+. def terminal_events bound_events.values.find_all { |ev| ev.terminal? } end # Get the list of terminal events for this task model def self.terminal_events enum_events.find_all { |_, e| e.terminal? }. map { |_, e| e } end # Get the event model for +event+ def event_model(model); self.model.event_model(model) end # Find the event class for +event+, or nil if +event+ is not an event name for this model def self.find_event_model(name) name = name.to_sym each_event { |sym, e| return e if sym == name } nil end # Checks that all events in +events+ are valid events for this task. # The requested events can be either an event name (symbol or string) # or an event class # # Returns the corresponding array of event classes def self.event_model(model_def) #:nodoc: if model_def.respond_to?(:to_sym) ev_model = find_event_model(model_def.to_sym) unless ev_model all_events = enum_events.map { |name, _| name } raise ArgumentError, "#{model_def} is not an event of #{name}: #{all_events}" unless ev_model end elsif model_def.respond_to?(:has_ancestor?) && model_def.has_ancestor?(TaskEvent) # Check that model_def is an event class for us ev_model = find_event_model(model_def.symbol) if !ev_model raise ArgumentError, "no #{model_def.symbol} event in #{name}" elsif ev_model != model_def raise ArgumentError, "the event model #{model_def} is not a model for #{name} (found #{ev_model} with the same name)" end else raise ArgumentError, "wanted either a symbol or an event class, got #{model_def}" end ev_model end class << self # Checks if _name_ is a name for an event of this task alias :has_event? :find_event_model private :validate_event_definition_request end # call-seq: # on(event_model) { |event| ... } # on(event_model => ev1, ev2 => [ ev3, ev4 ]) { |event| ... } # # Adds an event handler for the given event model. When the event is fired, # all events given in argument will be called. If they are controlable, # then the command is called. If not, they are just fired def self.on(mappings, &user_handler) if user_handler check_arity(user_handler, 1) end mappings = [*mappings].zip([]) unless Hash === mappings mappings.each do |from, to| from = event_model(from).symbol to = if to Array[*to].map do |ev| model = event_model(ev) raise ArgumentError, "trying to signal #{ev} which is not controlable" unless model.controlable? model.symbol end else; [] end signal_sets[from].merge to.to_value_set update_terminal_flag if user_handler method_name = "event_handler_#{from}_#{Object.address_from_id(user_handler.object_id).to_s(16)}" define_method(method_name, &user_handler) handler_sets[from] << lambda { |event| event.task.send(method_name, event) } end end end # call-seq: # causal_link(:from => :to) # # Declares a causal link between two events in the task. See # EventStructure::CausalLink for a description of the causal link # relation. def self.causal_link(mappings) mappings.each do |from, to| from = event_model(from).symbol causal_link_sets[from].merge Array[*to].map { |ev| event_model(ev).symbol }.to_value_set end update_terminal_flag end # call-seq: # forward :from => :to # # Defines a forwarding relation between two events of the same task # instance. See EventStructure::Forward for a description of the # forwarding relation. # # See also Task#forward and EventGenerator#forward. def self.forward(mappings) mappings.each do |from, to| from = event_model(from).symbol forwarding_sets[from].merge Array[*to].map { |ev| event_model(ev).symbol }.to_value_set end update_terminal_flag end def self.precondition(event, reason, &block) event = event_model(event) precondition_sets[event.symbol] << [reason, block] end def to_s # :nodoc: s = name.dup id = owners.map do |owner| next if owner == Roby::Distributed sibling = remote_siblings[owner] "#{sibling ? Object.address_from_id(sibling.ref).to_s(16) : 'nil'}@#{owner.remote_name}" end unless id.empty? s << "[" << id.join(",") << "]" end s end def pretty_print(pp) # :nodoc: pp.text "#{self.class.name}:0x#{self.address.to_s(16)}" pp.breakable pp.nest(2) do pp.text " owners: " pp.seplist(owners) { |r| pp.text r.to_s } pp.breakable pp.text "arguments: " arguments.pretty_print(pp) end end # True if this task is a null task. See NullTask. def null?; false end # Converts this object into a task object def to_task; self end event :start, :command => true # Define :stop before any other terminal event event :stop event :success, :terminal => true event :failed, :terminal => true event :aborted forward :aborted => :failed # The internal data for this task attr_reader :data # Sets the internal data value for this task. This calls the # #updated_data hook, and emits +updated_data+ if the task is running. def data=(value) @data = value updated_data emit :updated_data if running? end # This hook is called whenever the internal data of this task is # updated. See #data, #data= and the +updated_data+ event def updated_data super if defined? super end event :updated_data, :command => false # Checks if +task+ is in the same execution state than +self+ # Returns true if they are either both running or both pending def compatible_state?(task) finished? || !(running? ^ task.running?) end # Returns the lists of tags this model fullfills. def self.tags ancestors.find_all { |m| m.instance_of?(TaskModelTag) } end # The fullfills? predicate checks if this task can be used # to fullfill the need of the given +model+ and +arguments+ # The default is to check if # * the needed task model is an ancestor of this task # * the task # * +args+ is included in the task arguments def fullfills?(models, args = {}) if models.kind_of?(Task) klass, args = models.class, models.meaningful_arguments models = [klass] else models = [*models] end self_model = self.model self_args = self.arguments args = args.dup # Check the arguments that are required by the model for tag in models unless self_model.has_ancestor?(tag) return false end unless args.empty? for arg_name in tag.arguments if user_arg = args.delete(arg_name) return false unless user_arg == self_args[arg_name] end break if args.empty? end end end if !args.empty? raise ArgumentError, "the arguments '#{args.keys.join(", ")}' are unknown to the tags #{models.join(", ")}" end true end include ExceptionHandlingObject inherited_enumerable('exception_handler', 'exception_handlers') { Array.new } # Lists all exception handlers attached to this task def each_exception_handler(&iterator); model.each_exception_handler(&iterator) end @@exception_handler_id = 0 # call-seq: # on_exception(TaskModelViolation, ...) { |task, exception_object| ... } # # Defines an exception handler. matcher === exception_object is used to # determine if the handler should be called when +exception_object+ has # been fired. The first matching handler is called. Call #pass_exception to pass # the exception to previous handlers # # on_exception(TaskModelViolation, ...) do |task, exception_object| # if cannot_handle # task.pass_exception # send to the next handler # end # do_handle # end def self.on_exception(*matchers, &handler) check_arity(handler, 1) id = (@@exception_handler_id += 1) define_method("exception_handler_#{id}", &handler) exception_handlers.unshift [matchers, instance_method("exception_handler_#{id}")] end # We can't add relations on objects we don't own def add_child_object(child, type, info) unless read_write? && child.read_write? raise OwnershipError, "cannot add a relation between tasks we don't own. #{self} by #{owners.to_a} and #{child} is owned by #{child.owners.to_a}" end super end # This method is called during the commit process to def commit_transaction super if defined? super arguments.dup.each do |key, value| if value.kind_of?(Roby::Transactions::Proxy) arguments.update!(key, value.__getobj__) end end end # Create a new task of the same model and with the same arguments # than this one. Insert this task in the plan and make it replace # the fresh one. # # See Plan#respawn def respawn plan.respawn(self) end # Replaces, in the plan, the subplan generated by this plan object by # the one generated by +object+. In practice, it means that we transfer # all parent edges whose target is +self+ from the receiver to # +object+. It calls the various add/remove hooks defined in # DirectedRelationSupport. def replace_subplan_by(object) super # Compute the set of tasks that are in our subtree and not in # object's *after* the replacement own_subtree = ValueSet.new TaskStructure.each_root_relation do |rel| own_subtree.merge generated_subgraph(rel) own_subtree -= object.generated_subgraph(rel) end changes = [] each_event do |event| next unless object.has_event?(event.symbol) changes.clear event.each_relation do |rel| parents = [] event.each_parent_object(rel) do |parent| if !parent.respond_to?(:task) || !own_subtree.include?(parent.task) parents << parent << parent[event, rel] end end children = [] event.each_child_object(rel) do |child| if !child.respond_to?(:task) || !own_subtree.include?(child.task) children << child << event[child, rel] end end changes << rel << parents << children end event.apply_relation_changes(object.event(event.symbol), changes) end end # Replaces +self+ by +object+ in all relations +self+ is part of, and # do the same for the task's event generators. def replace_by(object) each_event do |event| event_name = event.symbol if object.has_event?(event_name) event.replace_by object.event(event_name) end end super end # Define a polling block for this task. The polling block is called at # each execution cycle while the task is running (i.e. in-between the # emission of +start+ and the emission of +stop+. # # Raises ArgumentError if the task is already running. # # See also Task::poll def poll(&block) if !pending? raise ArgumentError, "cannot set a polling block when the task is not pending" end singleton_class.class_eval do setup_poll_method(block) end on(:start) { Control.event_processing << method(:poll) } on(:stop) { Control.event_processing.delete(method(:poll)) } end end # A special task model which does nothing and emits +success+ # as soon as it is started. class NullTask < Task event :start, :command => true event :stop forward :start => :success # Always true. See Task#null? def null?; true end end # A virtual task is a task representation for a combination of two events. # This allows to combine two unrelated events, one being the +start+ event # of the virtual task and the other its success event. # # The task fails if the success event becomes unreachable. # # See VirtualTask.create class VirtualTask < Task # The start event attr_reader :start_event # The success event attr_accessor :success_event # Set the start event def start_event=(ev) if !ev.controlable? raise ArgumentError, "the start event of a virtual task must be controlable" end @start_event = ev end event :start do event(:start).achieve_with(start_event) start_event.call end on :start do success_event.forward_once event(:success) success_event.if_unreachable(true) do emit :failed if executable? end end terminates # Creates a new VirtualTask with the given start and success events def self.create(start, success) task = VirtualTask.new task.start_event = start task.success_event = success if start.respond_to?(:task) task.realized_by start.task end if success.respond_to?(:task) task.realized_by success.task end task end end unless defined? TaskStructure TaskStructure = RelationSpace(Task) end end require 'roby/task-operations'