class Roby::Interface::Interface
The server-side implementation of the command-based interface
This exports all the services and/or APIs that are available through e.g. the Roby
shell. It does not do any marshalling/demarshalling
Most methods can be accessed outside of the Roby
execution thread. Methods that cannot will be noted in their documentation
About job management¶ ↑
One of the tasks of this class is to do job management. Jobs are the unit that is used to interact with a running Roby
instance at a high level, as e.g. through a shell or a GUI
. In Roby
, jobs are represented by tasks that provide the {Job} task service and have a non-nil job ID. Up to two tasks can be associated with the job. The first is obviously the job task itself, i.e. the task that provides {Job}. Quite often, the job task will be a planning task (actually, one can see that {Actions::Task} provides {Job}). In this case, the planned task will be also associated with the job as its placeholder: while the job task represents the job's deployment status, the placeholder task will represent the job's execution status.
Constants
- State
Attributes
@return [#call] the blocks that listen to end-of-cycle
notifications. They are added with {#on_cycle_end} and removed with {#remove_cycle_end}
@return [#call] the blocks that listen to job notifications. They are
added with {#on_job_notification} and removed with {#remove_job_listener}
@api private
The set of pending job notifications for this cycle
@api private
@return [Set<Integer>] the set of tracked jobs @see tracked_job?
Public Class Methods
Creates an interface from an existing Roby
application
@param [Roby::Application] app the application
Roby::Interface::CommandLibrary::new
# File lib/roby/interface/interface.rb, line 113 def initialize(app) super(app) app.plan.add_trigger Roby::Interface::Job do |task| if task.job_id && (planned_task = task.planned_task) monitor_job(task, planned_task, new_task: true) end end execution_engine.at_cycle_end do push_pending_job_notifications notify_cycle_end end @tracked_jobs = Set.new @job_notifications = Array.new @job_listeners = Array.new @job_monitoring_state = Hash.new @cycle_end_listeners = Array.new end
Public Instance Methods
The set of actions available on {#app}
@return [Array<Roby::Actions::Models::Action>]
# File lib/roby/interface/interface.rb, line 150 def actions result = [] app.planners.each do |planner_model| planner_model.each_registered_action do |_, act| result << act end end result end
Drop a job
It removes the job from the list of missions but does not explicitely kill it
@param [Integer] job_id the ID of the job that should be
terminated
@return [Boolean] true if the job was found and terminated, and
false otherwise
@see kill_job
# File lib/roby/interface/interface.rb, line 202 def drop_job(job_id) return if !(task = find_job_by_id(job_id)) placeholder_task = task.planned_task if !placeholder_task plan.unmark_mission_task(task) return true end placeholder_task.remove_planning_task(task) if job_ids_of_task(placeholder_task).empty? plan.unmark_mission_task(placeholder_task) true else false end end
Enumerates the job listeners currently registered through {#on_job_notification}
@yieldparam [#call] the job listener object
# File lib/roby/interface/interface.rb, line 225 def each_job_listener(&block) job_listeners.each(&block) end
Enable or disable backtrace filtering
# File lib/roby/interface/interface.rb, line 638 def enable_backtrace_filtering(enable: true) app.filter_backtraces = enable end
Finds a job task by its ID
@param [Integer] id @return [Roby::Task,nil]
# File lib/roby/interface/interface.rb, line 518 def find_job_by_id(id) execution_engine.execute do return plan.find_tasks(Job).with_arguments(job_id: id).to_a.first end end
# File lib/roby/interface/interface.rb, line 505 def find_job_info_by_id(id) execution_engine.execute do if planning_task = plan.find_tasks(Job).with_arguments(job_id: id).to_a.first task = planning_task.planned_task || planning_task return job_state(task), task, planning_task end end end
Finds the task that represents the given job ID
It can be different than the job task when e.g. the job task is a planning task
# File lib/roby/interface/interface.rb, line 528 def find_job_placeholder_by_id(id) if task = find_job_by_id(id) return task.planned_task || task end end
Returns the job ID of a task, where the task can either be a placeholder for the job or the job task itself
@return [Integer,nil] the task's job ID or nil if (1) the task is
not a job task or (2) its job ID is not set
# File lib/roby/interface/interface.rb, line 358 def job_id_of_task(task) job_ids_of_task(task).first end
Returns all the job IDs of this task
@param [Roby::Task] task the job task itself, or its placeholder
task
@return [Array<Integer>] the task's job IDs. May be empty if
the task is not a job task, or if its job ID is not set
# File lib/roby/interface/interface.rb, line 341 def job_ids_of_task(task) if task.fullfills?(Job) [task.job_id] else task.each_planning_task.map do |planning_task| if planning_task.fullfills?(Job) planning_task.job_id end end.compact end end
Dispatch the given job-related notification to all listeners
Listeners are registered with {#on_job_notification}
# File lib/roby/interface/interface.rb, line 232 def job_notify(kind, job_id, job_name, *args) job_notifications << [kind, job_id, job_name, args] end
# File lib/roby/interface/interface.rb, line 456 def job_state(task) if !task.plan return JOB_FINALIZED elsif !plan.mission_task?(task) return JOB_DROPPED elsif task.success_event.emitted? return JOB_SUCCESS elsif task.failed_event.emitted? return JOB_FAILED elsif task.stop_event.emitted? return JOB_FINISHED elsif task.running? return JOB_STARTED elsif task.pending? if planner = task.planning_task if planner.success? return JOB_READY elsif planner.stop? return JOB_PLANNING_FAILED elsif planner.running? return JOB_PLANNING else return JOB_PLANNING_READY end else return JOB_READY end end end
The jobs currently running on {#app}'s plan
@return [Hash<Integer,(Symbol,Roby::Task,Roby::Task)>] the mapping
from job ID to the job's state (as returned by {#job_state}), the placeholder job task and the job task itself
# File lib/roby/interface/interface.rb, line 490 def jobs result = Hash.new execution_engine.execute do planning_tasks = plan.find_tasks(Job).to_a planning_tasks.each do |job_task| job_id = job_task.job_id next if !job_id placeholder_job_task = job_task.planned_task || job_task result[job_id] = [job_state(placeholder_job_task), placeholder_job_task, job_task] end end result end
Kill a job
It removes the job from the list of missions and kills the job's main task
@param [Integer] job_id the ID of the job that should be
terminated
@return [Boolean] true if the job was found and terminated, and
false otherwise
@see drop_job
# File lib/roby/interface/interface.rb, line 181 def kill_job(job_id) if task = find_job_placeholder_by_id(job_id) plan.unmark_mission_task(task) task.stop! if task.running? true else false end end
Returns the app's log directory
# File lib/roby/interface/interface.rb, line 646 def log_dir app.log_dir end
Returns the port of the log server
@return [Integer,nil] the port, or nil if there is no log server
# File lib/roby/interface/interface.rb, line 141 def log_server_port app.log_server_port end
Monitor the given task as a job
It must be called within the Roby
execution thread
# File lib/roby/interface/interface.rb, line 365 def monitor_job(planning_task, task, new_task: false) # NOTE: this method MUST queue job notifications # UNCONDITIONALLY. Job tracking is done on a per-cycle basis (in # at_cycle_end) by {#push_pending_job_notifications} job_id = planning_task.job_id job_name = planning_task.job_name # This happens when a placeholder/planning pair is replaced by # another, but the job ID is inherited. We do this when e.g. # running an action that returns another planning pair if (state = @job_monitoring_state[job_id]) track_planning_state( state.job_id, state.job_name, state.service, planning_task) return end service = PlanService.new(task) @job_monitoring_state[job_id] = State.new(service, false, job_id, job_name) service.when_finalized do @job_monitoring_state.delete(job_id) end service.on_plan_status_change(initial: true) do |status| state = @job_monitoring_state[job_id] if !state.monitored? && (status == :mission) job_notify(JOB_MONITORED, job_id, job_name, service.task, service.task.planning_task) job_notify(job_state(service.task), job_id, job_name) state.monitored = true elsif state.monitored? && (status != :mission) job_notify(JOB_DROPPED, job_id, job_name) state.monitored = false end end track_planning_state(job_id, job_name, service, planning_task) service.on_replacement do |_current, new| if plan.mission_task?(new) && job_ids_of_task(new).include?(job_id) job_notify(JOB_REPLACED, job_id, job_name, new) job_notify(job_state(new), job_id, job_name) else job_notify(JOB_LOST, job_id, job_name, new) end end service.on(:start) do |ev| job_notify(JOB_STARTED, job_id, job_name) end service.on(:success) do |ev| job_notify(JOB_SUCCESS, job_id, job_name) end service.on(:failed) do |ev| job_notify(JOB_FAILED, job_id, job_name) end service.when_finalized do job_notify(JOB_FINALIZED, job_id, job_name) end end
@api private
Notify the end-of-cycle to the listeners registered with {#on_cycle_end}
# File lib/roby/interface/interface.rb, line 606 def notify_cycle_end cycle_end_listeners.each do |listener| listener.call end end
Add a handler called at each end of cycle
Interface-related objects that need to be notified must use this method instead of using {ExecutionEngine#at_cycle_end} on {#execution_engine}, because the listener is guaranteed to be ordered properly w.r.t. {#push_pending_job_notifications}
@param [#call] block the listener @yieldparam [ExecutionEngine] the underlying execution execution_engine @return [Object] and ID that can be passed to {#remove_cycle_end}
# File lib/roby/interface/interface.rb, line 595 def on_cycle_end(&block) execution_engine.execute do cycle_end_listeners << block block end end
Notification about plan exceptions
@yieldparam [Symbol] kind one of {ExecutionEngine::EXCEPTION_NONFATAL},
{ExecutionEngine::EXCEPTION_FATAL} or {ExecutionEngine::EXCEPTION_HANDLED}
@yieldparam [Roby::ExecutionException] error the exception @yieldparam [Array<Roby::Task>] tasks the tasks that are involved in this exception @yieldparam [Set<Integer>] job_ids the job ID of the involved jobs
@see ExecutionEngine#on_exception
# File lib/roby/interface/interface.rb, line 567 def on_exception(&block) execution_engine.execute do execution_engine.on_exception(on_error: :raise) do |kind, exception, tasks| involved_job_ids = tasks.flat_map do |t| job_ids_of_task(t) if t.plan end.compact.to_set block.call(kind, exception, tasks, involved_job_ids) end end end
Registers a block to be called when a job changes state
All callbacks will be called with at minimum
@overload on_job_notification
@yieldparam kind one of the JOB_* constants @yieldparam [Integer] job_id the job ID (unique) @yieldparam [String] job_name the job name (non-unique) Generic interface. Some of the notifications, detailed below, have additional parameters (after the job_name argument)
@overload on_job_notification
@yieldparam JOB_MONITORED @yieldparam [Integer] job_id the job ID (unique) @yieldparam [String] job_name the job name (non-unique) @yieldparam [Task] task the job's placeholder task @yieldparam [Task] job_task the job task Interface for JOB_MONITORED notifications, called when the job task is initially detected
@overload on_job_notification
@yieldparam JOB_REPLACED or JOB_LOST @yieldparam [Integer] job_id the job ID (unique) @yieldparam [String] job_name the job name (non-unique) @yieldparam [Task] task the new task this job is now tracking Interface for JOB_REPLACED and JOB_LOST notifications
@return [Object] the listener ID that can be given to
{#remove_job_listener}
# File lib/roby/interface/interface.rb, line 322 def on_job_notification(&block) job_listeners << block block end
(see Application#on_notification
)
# File lib/roby/interface/interface.rb, line 281 def on_notification(&block) app.on_notification(&block) end
(see Application#on_ui_event
)
# File lib/roby/interface/interface.rb, line 271 def on_ui_event(&block) app.on_ui_event(&block) end
@api private
Called in at_cycle_end to push job notifications
# File lib/roby/interface/interface.rb, line 239 def push_pending_job_notifications final_tracked_jobs = tracked_jobs.dup # Re-track jobs for which we have a recapture event job_notifications.each do |event, job_id, *| if event == JOB_MONITORED tracked_jobs << job_id final_tracked_jobs << job_id elsif event == JOB_DROPPED || event == JOB_LOST || event == JOB_FINALIZED final_tracked_jobs.delete(job_id) end end job_notifications = self.job_notifications.find_all do |event, job_id, *| if event == JOB_DROPPED !final_tracked_jobs.include?(job_id) else tracked_jobs.include?(job_id) end end self.job_notifications.clear each_job_listener do |listener| job_notifications.each do |kind, job_id, job_name, args| listener.call(kind, job_id, job_name, *args) end end @tracked_jobs = final_tracked_jobs end
Requests for the Roby
application to quit
# File lib/roby/interface/interface.rb, line 618 def quit execution_engine.quit end
Reload the actions defined under the actions/ subfolder
# File lib/roby/interface/interface.rb, line 550 def reload_actions execution_engine.execute do app.reload_actions end actions end
Reload all models from this Roby
application
Do NOT do this while the robot does critical things
# File lib/roby/interface/interface.rb, line 537 def reload_models execution_engine.execute do app.reload_models end nil end
@deprecated use {#reload_actions} instead
# File lib/roby/interface/interface.rb, line 545 def reload_planners reload_actions end
Remove a handler that has been added to {#on_cycle_end}
# File lib/roby/interface/interface.rb, line 613 def remove_cycle_end(listener) cycle_end_listeners.delete(listener) end
@see ExecutionEngine#remove_exception_listener
# File lib/roby/interface/interface.rb, line 579 def remove_exception_listener(listener) execution_engine.execute do execution_engine.remove_exception_listener(listener) end end
Remove a job listener added with {#on_job_notification}
@param [Object] listener the listener ID returned by
{#on_job_notification}
# File lib/roby/interface/interface.rb, line 331 def remove_job_listener(listener) job_listeners.delete(listener) end
(see Application#remove_notification_listener
)
# File lib/roby/interface/interface.rb, line 286 def remove_notification_listener(listener) app.remove_notification_listener(listener) end
(see Application#remove_ui_event_listener
)
# File lib/roby/interface/interface.rb, line 276 def remove_ui_event_listener(block) app.remove_ui_event_listener(block) end
Requests for the Roby
application to quit
# File lib/roby/interface/interface.rb, line 624 def restart app.restart end
Starts a job
@return [Integer] the job ID
# File lib/roby/interface/interface.rb, line 164 def start_job(m, arguments = Hash.new) execution_engine.execute do task, planning_task = app.prepare_action(m, mission: true, job_id: Job.allocate_job_id, **arguments) planning_task.job_id end end
Private Instance Methods
# File lib/roby/interface/interface.rb, line 426 def track_planning_state(job_id, job_name, service, planning_task) planning_task.start_event.on do |ev| job_task = planning_task.planned_task if job_task == service.task job_notify(JOB_PLANNING, job_id, job_name) end end planning_task.success_event.on do |ev| job_task = planning_task.planned_task if job_task == service.task if job_task.pending? || job_task.starting? job_notify(JOB_READY, job_id, job_name) end end end planning_task.stop_event.on do |ev| job_task = planning_task.planned_task if job_task == service.task && !ev.task.success? job_notify(JOB_PLANNING_FAILED, job_id, job_name) end end PlanService.new(planning_task).when_finalized do job_task = planning_task.planned_task if job_task == service.task job_notify(JOB_FINALIZED, job_id, job_name) end end end