Class: Roby::Interface::Interface

Inherits:
CommandLibrary show all
Defined in:
lib/roby/interface/interface.rb

Overview

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.

Defined Under Namespace

Classes: State

Instance Attribute Summary collapse

Attributes inherited from CommandLibrary

#app, #subcommands

Instance Method Summary collapse

Methods inherited from CommandLibrary

command, #commands, #each_subcommand, #execution_engine, #plan, subcommand, #subcommand

Constructor Details

#initialize(app) ⇒ Interface

Creates an interface from an existing Roby application

Parameters:



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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

Instance Attribute Details

#cycle_end_listeners#call (readonly)

Returns the blocks that listen to end-of-cycle notifications. They are added with #on_cycle_end and removed with #remove_cycle_end.

Returns:



98
99
100
# File 'lib/roby/interface/interface.rb', line 98

def cycle_end_listeners
  @cycle_end_listeners
end

#job_listeners#call (readonly)

Returns the blocks that listen to job notifications. They are added with #on_job_notification and removed with #remove_job_listener.

Returns:



93
94
95
# File 'lib/roby/interface/interface.rb', line 93

def job_listeners
  @job_listeners
end

#job_notificationsObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The set of pending job notifications for this cycle



108
109
110
# File 'lib/roby/interface/interface.rb', line 108

def job_notifications
  @job_notifications
end

#tracked_jobsSet<Integer> (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the set of tracked jobs.

Returns:

  • (Set<Integer>)

    the set of tracked jobs

See Also:

  • tracked_job?


104
105
106
# File 'lib/roby/interface/interface.rb', line 104

def tracked_jobs
  @tracked_jobs
end

Instance Method Details

#actionsArray<Roby::Actions::Models::Action>

The set of actions available on CommandLibrary#app

Returns:



150
151
152
153
154
155
156
157
158
# 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) ⇒ Boolean

Drop a job

It removes the job from the list of missions but does not explicitely kill it

Parameters:

  • job_id (Integer)

    the ID of the job that should be terminated

Returns:

  • (Boolean)

    true if the job was found and terminated, and false otherwise

See Also:



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# 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 {|the| ... } ⇒ Object

Enumerates the job listeners currently registered through #on_job_notification

Yield Parameters:

  • the (#call)

    job listener object



225
226
227
# File 'lib/roby/interface/interface.rb', line 225

def each_job_listener(&block)
    job_listeners.each(&block)
end

#enable_backtrace_filtering(enable: true) ⇒ Object

Enable or disable backtrace filtering



638
639
640
# File 'lib/roby/interface/interface.rb', line 638

def enable_backtrace_filtering(enable: true)
    app.filter_backtraces = enable
end

#find_job_by_id(id) ⇒ Roby::Task?

Finds a job task by its ID

Parameters:

  • id (Integer)

Returns:



518
519
520
521
522
# 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) ⇒ Object



505
506
507
508
509
510
511
512
# 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) ⇒ Object

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



528
529
530
531
532
# 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) ⇒ Integer?

Returns the job ID of a task, where the task can either be a placeholder for the job or the job task itself

Returns:

  • (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



358
359
360
# 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) ⇒ Array<Integer>

Returns all the job IDs of this task

Parameters:

  • task (Roby::Task)

    the job task itself, or its placeholder task

Returns:

  • (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



341
342
343
344
345
346
347
348
349
350
351
# 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) ⇒ Object

Dispatch the given job-related notification to all listeners

Listeners are registered with #on_job_notification



232
233
234
# 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) ⇒ Object



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# 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

#jobsHash<Integer,(Symbol,Roby::Task,Roby::Task)>

The jobs currently running on CommandLibrary#app‘s plan

Returns:

  • (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



490
491
492
493
494
495
496
497
498
499
500
501
502
# 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) ⇒ Boolean

Kill a job

It removes the job from the list of missions and kills the job’s main task

Parameters:

  • job_id (Integer)

    the ID of the job that should be terminated

Returns:

  • (Boolean)

    true if the job was found and terminated, and false otherwise

See Also:



181
182
183
184
185
186
187
188
# 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_dirObject

Returns the app’s log directory



646
647
648
# File 'lib/roby/interface/interface.rb', line 646

def log_dir
    app.log_dir
end

#log_server_portInteger?

Returns the port of the log server

Returns:

  • (Integer, nil)

    the port, or nil if there is no log server



141
142
143
# 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) ⇒ Object

Monitor the given task as a job

It must be called within the Roby execution thread



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# 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_endObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Notify the end-of-cycle to the listeners registered with #on_cycle_end



606
607
608
609
610
# 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) {|the| ... } ⇒ Object

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 CommandLibrary#execution_engine, because the listener is guaranteed to be ordered properly w.r.t. #push_pending_job_notifications

Parameters:

  • block (#call)

    the listener

Yield Parameters:

Returns:



595
596
597
598
599
600
# 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 {|kind, error, tasks, job_ids| ... } ⇒ Object

Notification about plan exceptions

Yield Parameters:

See Also:



567
568
569
570
571
572
573
574
575
576
# 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 {|kind, job_id, job_name| ... } ⇒ Object #on_job_notification {|JOB_MONITORED, job_id, job_name, task, job_task| ... } ⇒ Object #on_job_notification {|JOB_REPLACED, job_id, job_name, task| ... } ⇒ Object

Registers a block to be called when a job changes state

All callbacks will be called with at minimum

Overloads:

  • #on_job_notification {|kind, job_id, job_name| ... } ⇒ Object

    Generic interface. Some of the notifications, detailed below, have additional parameters (after the job_name argument)

    Yield Parameters:

    • kind

      one of the JOB_* constants

    • job_id (Integer)

      the job ID (unique)

    • job_name (String)

      the job name (non-unique)

  • #on_job_notification {|JOB_MONITORED, job_id, job_name, task, job_task| ... } ⇒ Object

    Interface for JOB_MONITORED notifications, called when the job task is initially detected

    Yield Parameters:

    • JOB_MONITORED
    • job_id (Integer)

      the job ID (unique)

    • job_name (String)

      the job name (non-unique)

    • task (Task)

      the job’s placeholder task

    • job_task (Task)

      the job task

  • #on_job_notification {|JOB_REPLACED, job_id, job_name, task| ... } ⇒ Object

    Interface for JOB_REPLACED and JOB_LOST notifications

    Yield Parameters:

    • JOB_REPLACED

      or JOB_LOST

    • job_id (Integer)

      the job ID (unique)

    • job_name (String)

      the job name (non-unique)

    • task (Task)

      the new task this job is now tracking

Returns:



322
323
324
325
# File 'lib/roby/interface/interface.rb', line 322

def on_job_notification(&block)
    job_listeners << block
    block
end

#on_notification {|source, level, message| ... } ⇒ Object

Registers a block to be called when a message needs to be dispatched from #notify

Yield Parameters:

  • source (String)

    the source of the message

  • level (String)

    the log level

  • message (String)

    the message itself

Returns:



281
282
283
# File 'lib/roby/interface/interface.rb', line 281

def on_notification(&block)
    app.on_notification(&block)
end

#on_ui_event {|name, args| ... } ⇒ Object

Registers a block to be called when a message needs to be dispatched from #ui_event

Yield Parameters:

  • name (String)

    the event name

  • args

    the UI event listener arguments

Returns:



271
272
273
# File 'lib/roby/interface/interface.rb', line 271

def on_ui_event(&block)
    app.on_ui_event(&block)
end

#push_pending_job_notificationsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Called in at_cycle_end to push job notifications



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# 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

#quitObject

Requests for the Roby application to quit



618
619
620
# File 'lib/roby/interface/interface.rb', line 618

def quit
    execution_engine.quit
end

#reload_actionsObject

Reload the actions defined under the actions/ subfolder



550
551
552
553
554
555
# File 'lib/roby/interface/interface.rb', line 550

def reload_actions
    execution_engine.execute do
        app.reload_actions
    end
    actions
end

#reload_modelsObject

Reload all models from this Roby application

Do NOT do this while the robot does critical things



537
538
539
540
541
542
# File 'lib/roby/interface/interface.rb', line 537

def reload_models
    execution_engine.execute do
        app.reload_models
    end
    nil
end

#reload_plannersObject

Deprecated.

use #reload_actions instead



545
546
547
# File 'lib/roby/interface/interface.rb', line 545

def reload_planners
    reload_actions
end

#remove_cycle_end(listener) ⇒ Object

Remove a handler that has been added to #on_cycle_end



613
614
615
# File 'lib/roby/interface/interface.rb', line 613

def remove_cycle_end(listener)
    cycle_end_listeners.delete(listener)
end

#remove_exception_listener(listener) ⇒ Object



579
580
581
582
583
# 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) ⇒ Object

Remove a job listener added with #on_job_notification

Parameters:



331
332
333
# File 'lib/roby/interface/interface.rb', line 331

def remove_job_listener(listener)
    job_listeners.delete(listener)
end

#remove_notification_listener(listener) ⇒ Object

Removes a notification listener added with #on_notification

Parameters:



286
287
288
# File 'lib/roby/interface/interface.rb', line 286

def remove_notification_listener(listener)
    app.remove_notification_listener(listener)
end

#remove_ui_event_listener(block) ⇒ Object

Removes a notification listener added with #on_ui_event

Parameters:



276
277
278
# File 'lib/roby/interface/interface.rb', line 276

def remove_ui_event_listener(block)
    app.remove_ui_event_listener(block)
end

#restartObject

Requests for the Roby application to quit



624
625
626
# File 'lib/roby/interface/interface.rb', line 624

def restart
    app.restart
end

#start_job(m, arguments = Hash.new) ⇒ Integer

Starts a job

Returns:

  • (Integer)

    the job ID



164
165
166
167
168
169
# 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