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

cycle_end_listeners[R]

@return [#call] the blocks that listen to end-of-cycle

notifications. They are added with {#on_cycle_end} and
removed with {#remove_cycle_end}
job_listeners[R]

@return [#call] the blocks that listen to job notifications. They are

added with {#on_job_notification} and removed with
{#remove_job_listener}
job_notifications[R]

@api private

The set of pending job notifications for this cycle

tracked_jobs[R]

@api private

@return [Set<Integer>] the set of tracked jobs @see tracked_job?

Public Class Methods

new(app) click to toggle source

Creates an interface from an existing Roby application

@param [Roby::Application] app the application

Calls superclass method 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

actions() click to toggle source

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_job(job_id) click to toggle source

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
each_job_listener(&block) click to toggle source

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_backtrace_filtering(enable: true) click to toggle source

Enable or disable backtrace filtering

# File lib/roby/interface/interface.rb, line 638
def enable_backtrace_filtering(enable: true)
    app.filter_backtraces = enable
end
find_job_by_id(id) click to toggle source

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
find_job_info_by_id(id) click to toggle source
# 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
find_job_placeholder_by_id(id) click to toggle source

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
job_id_of_task(task) click to toggle source

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
job_ids_of_task(task) click to toggle source

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
job_notify(kind, job_id, job_name, *args) click to toggle source

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
job_state(task) click to toggle source
# 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
jobs() click to toggle source

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_job(job_id) click to toggle source

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
log_dir() click to toggle source

Returns the app's log directory

# File lib/roby/interface/interface.rb, line 646
def log_dir
    app.log_dir
end
log_server_port() click to toggle source

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_job(planning_task, task, new_task: false) click to toggle source

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
notify_cycle_end() click to toggle source

@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
on_cycle_end(&block) click to toggle source

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
on_exception(&block) click to toggle source

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
on_job_notification(&block) click to toggle source

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
on_notification(&block) click to toggle source

(see Application#on_notification)

# File lib/roby/interface/interface.rb, line 281
def on_notification(&block)
    app.on_notification(&block)
end
on_ui_event(&block) click to toggle source

(see Application#on_ui_event)

# File lib/roby/interface/interface.rb, line 271
def on_ui_event(&block)
    app.on_ui_event(&block)
end
push_pending_job_notifications() click to toggle source

@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
quit() click to toggle source

Requests for the Roby application to quit

# File lib/roby/interface/interface.rb, line 618
def quit
    execution_engine.quit
end
reload_actions() click to toggle source

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_models() click to toggle source

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
reload_planners() click to toggle source

@deprecated use {#reload_actions} instead

# File lib/roby/interface/interface.rb, line 545
def reload_planners
    reload_actions
end
remove_cycle_end(listener) click to toggle source

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
remove_exception_listener(listener) click to toggle source

@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_job_listener(listener) click to toggle source

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
remove_notification_listener(listener) click to toggle source

(see Application#remove_notification_listener)

# File lib/roby/interface/interface.rb, line 286
def remove_notification_listener(listener)
    app.remove_notification_listener(listener)
end
remove_ui_event_listener(block) click to toggle source

(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
restart() click to toggle source

Requests for the Roby application to quit

# File lib/roby/interface/interface.rb, line 624
def restart
    app.restart
end
start_job(m, arguments = Hash.new) click to toggle source

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

track_planning_state(job_id, job_name, service, planning_task) click to toggle source
# 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