Class: Roby::Interface::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/roby/interface/client.rb

Overview

The client-side object that allows to access an interface (e.g. a Roby app) from another process than the Roby controller

Defined Under Namespace

Classes: BatchContext, Job, NoSuchAction

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io, id) ⇒ Client

Create a client endpoint to a Roby interface [Server]

Parameters:

  • io (DRobyChannel)

    a channel to the server

  • id (String)

    a unique identifier for this client (e.g. host:port of the local endpoint when using TCP). It is passed to the server through Server#handshake

See Also:



45
46
47
48
49
50
51
52
53
54
55
# File 'lib/roby/interface/client.rb', line 45

def initialize(io, id)
    @pending_async_calls = Array.new
    @io = io
    @message_id = 0
    @notification_queue = Array.new
    @job_progress_queue = Array.new
    @exception_queue = Array.new
    @ui_event_queue = Array.new

    @actions, @commands = call([], :handshake, id)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(m, *args) ⇒ Object



543
544
545
546
547
548
549
# File 'lib/roby/interface/client.rb', line 543

def method_missing(m, *args)
    if sub = find_subcommand_by_name(m.to_s)
        SubcommandClient.new(self, m.to_s, sub.description, sub.commands)
    else
        call([], m, *args)
    end
end

Instance Attribute Details

#actionsArray<Roby::Actions::Model::Action> (readonly)

Returns set of known actions.

Returns:

  • (Array<Roby::Actions::Model::Action>)

    set of known actions



9
10
11
# File 'lib/roby/interface/client.rb', line 9

def actions
  @actions
end

#commandsHash (readonly)

Returns the set of available commands.

Returns:

  • (Hash)

    the set of available commands



11
12
13
# File 'lib/roby/interface/client.rb', line 11

def commands
  @commands
end

#cycle_indexInteger (readonly)

Returns index of the last processed cycle.

Returns:

  • (Integer)

    index of the last processed cycle



31
32
33
# File 'lib/roby/interface/client.rb', line 31

def cycle_index
  @cycle_index
end

#cycle_start_timeTime (readonly)

Returns time of the last processed cycle.

Returns:

  • (Time)

    time of the last processed cycle



33
34
35
# File 'lib/roby/interface/client.rb', line 33

def cycle_start_time
  @cycle_start_time
end

#exception_queueArray<Integer,Array> (readonly)

Returns list of existing exceptions. The integer is an ID that can be used to refer to the exception. It is always growing and will never collide with a notification ID.

Returns:

  • (Array<Integer,Array>)

    list of existing exceptions. The integer is an ID that can be used to refer to the exception. It is always growing and will never collide with a notification ID



24
25
26
# File 'lib/roby/interface/client.rb', line 24

def exception_queue
  @exception_queue
end

#ioDRobyChannel (readonly)

Returns the IO to the server.

Returns:



7
8
9
# File 'lib/roby/interface/client.rb', line 7

def io
  @io
end

#job_progress_queueArray<Integer,Array> (readonly)

Returns list of existing job progress information. The integer is an ID that can be used to refer to the job progress information. It is always growing and will never collide with a job progress and exception ID.

Returns:

  • (Array<Integer,Array>)

    list of existing job progress information. The integer is an ID that can be used to refer to the job progress information. It is always growing and will never collide with a job progress and exception ID



16
17
18
# File 'lib/roby/interface/client.rb', line 16

def job_progress_queue
  @job_progress_queue
end

#notification_queueArray<Integer,Array> (readonly)

Returns list of existing notifications. The integer is an ID that can be used to refer to the notification. It is always growing and will never collide with an exception ID.

Returns:

  • (Array<Integer,Array>)

    list of existing notifications. The integer is an ID that can be used to refer to the notification. It is always growing and will never collide with an exception ID



20
21
22
# File 'lib/roby/interface/client.rb', line 20

def notification_queue
  @notification_queue
end

#pending_async_callsArray<Hash> (readonly)

Returns list of the pending async calls.

Returns:

  • (Array<Hash>)

    list of the pending async calls



35
36
37
# File 'lib/roby/interface/client.rb', line 35

def pending_async_calls
  @pending_async_calls
end

#ui_event_queueArray<Integer,Array> (readonly)

Returns list of queued UI events. The integer is an ID that can be used to refer to the exception. It is always growing and will never collide with a notification ID.

Returns:

  • (Array<Integer,Array>)

    list of queued UI events. The integer is an ID that can be used to refer to the exception. It is always growing and will never collide with a notification ID



28
29
30
# File 'lib/roby/interface/client.rb', line 28

def ui_event_queue
  @ui_event_queue
end

Instance Method Details

#allocate_message_idObject

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.

Allocation of unique IDs for notification messages



186
187
188
# File 'lib/roby/interface/client.rb', line 186

def allocate_message_id
    @message_id += 1
end

#async_call(path, m, *args, &block) ⇒ Object

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.

Asynchronously call a method on the interface or on one of the interface’s subcommands

Parameters:

  • path (Array<String>)

    path to the subcommand. Empty means on the interface object itself.

  • m (Symbol)

    command or action name. Actions are always formatted as action_name!

  • args (Object)

    the command or action arguments

Returns:

  • (Object)

    an Object associated with the call @see async_call_pending?

Raises:

  • (RuntimeError)


328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/roby/interface/client.rb', line 328

def async_call(path, m, *args, &block)
    raise RuntimeError, "no callback block given" unless block_given?
    if m.to_s =~ /(.*)!$/
        action_name = $1
        if find_action_by_name(action_name)
            path = []
            m = :start_job
            args = [action_name, *args]
        else raise NoSuchAction, "there is no action called #{action_name} on #{self}"
        end
    end
    io.write_packet([path, m, *args])
    pending_async_calls << { block: block, path: path, m: m, args: args }
    pending_async_calls.last.freeze
end

#async_call_pending?(a_call) ⇒ Boolean

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.

Whether the async call is still pending

Parameters:

  • call (Object)

    the Object associated with the call

Returns:

  • (Boolean)

    true if the async call is pending, false otherwise



350
351
352
# File 'lib/roby/interface/client.rb', line 350

def async_call_pending?(a_call)
    pending_async_calls.any? { |item| item.equal?(a_call) }
end

#call(path, m, *args) ⇒ Object

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.

Call a method on the interface or on one of the interface’s subcommands

Parameters:

  • path (Array<String>)

    path to the subcommand. Empty means on the interface object itself.

  • m (Symbol)

    command or action name. Actions are always formatted as action_name!

  • args (Object)

    the command or action arguments

Returns:

  • (Object)

    the command result, or – in the case of an action – the job ID for the newly created action



306
307
308
309
310
311
312
313
314
315
# File 'lib/roby/interface/client.rb', line 306

def call(path, m, *args)
    if m.to_s =~ /(.*)!$/
        action_name = $1
        start_job(action_name, *args)
    else
        io.write_packet([path, m, *args])
        result, _ = poll(1)
        result
    end
end

#closeObject

Close the communication channel



63
64
65
# File 'lib/roby/interface/client.rb', line 63

def close
    io.close
end

#closed?Boolean

Whether the communication channel to the server is closed

Returns:

  • (Boolean)


58
59
60
# File 'lib/roby/interface/client.rb', line 58

def closed?
    io.closed?
end

#create_batchBatchContext

Create a batch context

Messages sent to the returned object are validated as much as possible and gathered in a list. Call #process_batch to send all the gathered calls at once to the remote server

Returns:



520
521
522
# File 'lib/roby/interface/client.rb', line 520

def create_batch
    BatchContext.new(self)
end

#each_jobObject

Enumerate the current jobs



497
498
499
500
501
502
# File 'lib/roby/interface/client.rb', line 497

def each_job
    return enum_for(__method__) if !block_given?
    jobs.each do |job_id, (job_state, placeholder_task, job_task)|
        yield(Job.new(job_id, job_state, placeholder_task, job_task))
    end
end

#find_action_by_name(name) ⇒ Actions::Models::Action?

Find an action by its name

This is a local operation using the information gathered at connection time

Parameters:

  • name (String)

    the name of the action to look for

Returns:



84
85
86
# File 'lib/roby/interface/client.rb', line 84

def find_action_by_name(name)
    actions.find { |act| act.name == name }
end

#find_all_actions_matching(matcher) ⇒ Array<Actions::Models::Action>

Finds all actions whose name matches a pattern

Parameters:

  • matcher (#===)

    the matching object (usually a Regexp or String)

Returns:



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

def find_all_actions_matching(matcher)
    actions.find_all { |act| matcher === act.name }
end

#find_all_jobs_by_action_name(action_name) ⇒ Array<Job>

Find all the jobs that match the given action name

Returns:



507
508
509
510
511
# File 'lib/roby/interface/client.rb', line 507

def find_all_jobs_by_action_name(action_name)
    each_job.find_all do |j|
        j.action_model.name == action_name
    end
end

#find_subcommand_by_name(name) ⇒ Object



539
540
541
# File 'lib/roby/interface/client.rb', line 539

def find_subcommand_by_name(name)
    commands[name]
end

#has_action?(name) ⇒ Boolean

Tests whether the interface has an action with that name

Returns:

  • (Boolean)


73
74
75
# File 'lib/roby/interface/client.rb', line 73

def has_action?(name)
    !!find_action_by_name(name)
end

#has_exceptions?Boolean

Whether some exception notifications have been queued

Returns:

  • (Boolean)


265
266
267
# File 'lib/roby/interface/client.rb', line 265

def has_exceptions?
    !exception_queue.empty?
end

#has_job_progress?Boolean

Whether some job progress information is currently queued

Returns:

  • (Boolean)


201
202
203
# File 'lib/roby/interface/client.rb', line 201

def has_job_progress?
    !job_progress_queue.empty?
end

#has_notifications?Boolean

Whether some generic notifications have been queued

Returns:

  • (Boolean)


222
223
224
# File 'lib/roby/interface/client.rb', line 222

def has_notifications?
    !notification_queue.empty?
end

#has_ui_event?Boolean

Whether some UI events have been queued

Returns:

  • (Boolean)


243
244
245
# File 'lib/roby/interface/client.rb', line 243

def has_ui_event?
    !ui_event_queue.empty?
end

#poll(expected_count = 0) ⇒ Object

Polls for new data on the IO channel

Returns:

Raises:

  • (ComError)

    if the link seem to be broken

  • (ProtocolError)

    if some errors happened when validating the protocol



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/roby/interface/client.rb', line 159

def poll(expected_count = 0)
    result = nil
    timeout = if expected_count > 0 then nil
              else 0
              end

    has_cycle_end = false
    while packet = io.read_packet(timeout)
        has_cycle_end = process_packet(*packet) do |reply_value|
            if result
                raise ProtocolError, "got more than one sync reply in a single poll call"
            end
            result = reply_value
            expected_count -= 1
        end

        if expected_count <= 0
            break if has_cycle_end
            timeout = 0
        end
    end
    return result, has_cycle_end
end

#pop_exception(Integer,Array)

Remove and return the oldest exception notification

Returns:

  • ((Integer,Array))

    a unique and monotonically-increasing message ID and the generic notification information as specified by (Interface#on_exception)



274
275
276
# File 'lib/roby/interface/client.rb', line 274

def pop_exception
    exception_queue.shift
end

#pop_job_progress(Integer,Array)

Remove and return the oldest job information message

Returns:

  • ((Integer,Array))

    a unique and monotonically-increasing message ID and the arguments to job progress as specified on Interface#on_job_notification.



210
211
212
# File 'lib/roby/interface/client.rb', line 210

def pop_job_progress
    job_progress_queue.shift
end

#pop_notification(Integer,Array)

Remove and return the oldest generic notification message

Returns:

  • ((Integer,Array))

    a unique and monotonically-increasing message ID and the generic notification information as specified by (Application#notify)



231
232
233
# File 'lib/roby/interface/client.rb', line 231

def pop_notification
    notification_queue.shift
end

#pop_ui_eventObject

Remove the oldest UI event and return it



248
249
250
# File 'lib/roby/interface/client.rb', line 248

def pop_ui_event
    ui_event_queue.shift
end

#process_batch(batch) ⇒ Array

Send all commands gathered in a batch for processing on the remote server

Parameters:

Returns:

  • (Array)

    the return values of each of the calls gathered in the batch



530
531
532
533
# File 'lib/roby/interface/client.rb', line 530

def process_batch(batch)
    ret = call([], :process_batch, batch.__calls)
    BatchContext::Return.from_calls_and_return(batch.__calls, ret)
end

#process_packet(m, *args) ⇒ Boolean

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.

Process a message as received on #io

Returns:

  • (Boolean)

    whether the message was a cycle_end message



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/roby/interface/client.rb', line 102

def process_packet(m, *args)
    if m == :cycle_end
        @cycle_index, @cycle_start_time = *args
        return true
    end

    if m == :bad_call
        if !pending_async_calls.empty?
            process_pending_async_call(args.first, nil)
        else
            e = args.first
            raise e, e.message, (e.backtrace + caller)
        end
    elsif m == :reply
        if !pending_async_calls.empty?
            process_pending_async_call(nil, args.first)
        else
            yield args.first
        end
    elsif m == :job_progress
        queue_job_progress(*args)
    elsif m == :notification
        queue_notification(*args)
    elsif m == :ui_event
        queue_ui_event(*args)
    elsif m == :exception
        queue_exception(*args)
    else
        raise ProtocolError, "unexpected reply from #{io}: #{m} (#{args.map(&:to_s).join(",")})"
    end
    false
end

#process_pending_async_call(error, result) ⇒ Object

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.

Remove and call the block of a pending async call



148
149
150
151
# File 'lib/roby/interface/client.rb', line 148

def process_pending_async_call(error, result)
    current_call = pending_async_calls.shift
    current_call[:block].call(error, result)
end

#queue_exception(kind, error, tasks, job_ids) ⇒ Object

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.

Push an exception notification to #exception_queue

It can be retrieved with #pop_exception

See the yield parameters of Interface#on_exception for the overall argument format.



260
261
262
# File 'lib/roby/interface/client.rb', line 260

def queue_exception(kind, error, tasks, job_ids)
    exception_queue.push [allocate_message_id, [kind, error, tasks, job_ids]]
end

#queue_job_progress(kind, job_id, job_name, *args) ⇒ Object

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.

Push a job notification to #job_progress_queue

See the yield parameters of Interface#on_job_notification for the overall argument format.



196
197
198
# File 'lib/roby/interface/client.rb', line 196

def queue_job_progress(kind, job_id, job_name, *args)
    job_progress_queue.push [allocate_message_id, [kind, job_id, job_name, *args]]
end

#queue_notification(source, level, message) ⇒ Object

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.

Push a generic notification to #notification_queue



217
218
219
# File 'lib/roby/interface/client.rb', line 217

def queue_notification(source, level, message)
    notification_queue.push [allocate_message_id, [source, level, message]]
end

#queue_ui_event(event_name, *args) ⇒ Object

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.

Push a UI event to #ui_event_queue



238
239
240
# File 'lib/roby/interface/client.rb', line 238

def queue_ui_event(event_name, *args)
    ui_event_queue.push [allocate_message_id, [event_name, *args]]
end

#reload_actionsObject



535
536
537
# File 'lib/roby/interface/client.rb', line 535

def reload_actions
    @actions = call([], :reload_actions)
end

#start_job(action_name, **arguments) ⇒ Object

Start the given job within the batch

Parameters:

  • action_name (Symbol)

    the action name

  • arguments (Hash<Symbol,Object>)

    the action arguments

Raises:



287
288
289
290
291
292
# File 'lib/roby/interface/client.rb', line 287

def start_job(action_name, **arguments)
    if find_action_by_name(action_name)
        call([], :start_job, action_name, arguments)
    else raise NoSuchAction, "there is no action called #{action_name} on #{self}"
    end
end

#to_ioObject

The underlying IO object



68
69
70
# File 'lib/roby/interface/client.rb', line 68

def to_io
    io.to_io
end

#wait(timeout: nil) ⇒ Boolean

Wait until there is data to process on the IO channel

Parameters:

  • timeout (Numeric, nil) (defaults to: nil)

    a timeout after which the method will return. Use nil for no timeout

Returns:

  • (Boolean)

    falsy if the timeout was reached, true otherwise



141
142
143
# File 'lib/roby/interface/client.rb', line 141

def wait(timeout: nil)
    io.read_wait(timeout: timeout)
end