class Roby::Interface::ShellClient

An interface client using TCP that provides reconnection capabilities as well as proper formatting of the information

Attributes

client[R]

@return [Client,nil] the socket used to communicate to the server,

or nil if we have not managed to connect yet
connection_method[R]

@return [#call] an object that can create a Client instance

remote_name[R]

@return [String] a string that describes the remote host

Public Class Methods

new(remote_name, &connection_method) click to toggle source
# File lib/roby/interface/shell_client.rb, line 16
def initialize(remote_name, &connection_method)
    @connection_method = connection_method
    @remote_name = remote_name
    @silent = false
    connect
end

Public Instance Methods

__jobs() click to toggle source
# File lib/roby/interface/shell_client.rb, line 109
def __jobs
    call Hash[retry: true], [], :jobs
end
actions(regex = nil, verbose = false) click to toggle source
# File lib/roby/interface/shell_client.rb, line 56
def actions(regex = nil, verbose = false)
    actions = client.actions.sort_by {|act| act.name }
    if regex
        regex = Regexp.new(regex)
    else
        regex = Regexp.new(".*")
    end
    actions.each do |action|
        if regex.match(action.name)
            if verbose
                puts "\e[1m#{action.name}!\e[0m"

                arguments = action.arguments.sort_by {|arg| arg.name }
                required_arguments = []
                optional_arguments = []
                arguments.each do |argument|
                    if argument.required
                        required_arguments << argument
                    else
                        optional_arguments << argument
                    end
                end
                if !required_arguments.empty?
                    puts "    required arguments"
                    required_arguments.each do |argument|
                        puts "        #{argument.name}: #{argument.doc} [default: #{argument.default}]"
                    end
                end
                if !optional_arguments.empty?
                    puts "    optional arguments:"
                    optional_arguments.each do |argument|
                        puts "        #{argument.name}: #{argument.doc} [default: #{argument.default}]"
                    end
                end
                puts "    doc: #{action.doc}" unless action.doc.empty?
            else
                puts "\e[1m#{action.name}!\e[0m(#{action.arguments.map(&:name).sort.join(", ")}): #{action.doc}"
            end
        end
    end
    nil
end
call(options, path, m, *args) click to toggle source
# File lib/roby/interface/shell_client.rb, line 150
def call(options, path, m, *args)
    options = Kernel.validate_options options, retry: false
    if options[:retry]
        options = options.merge(retry: false)
        retry_on_com_error do
            return call options, path, m, *args
        end
    else
        client.call(path, m, *args)
    end
rescue Exception => e
    msg = Roby.format_exception(e)
    if msg[0]
        msg[0] = Roby.color(msg[0], :red)
    end
    puts msg.join("\n")
    puts "  " + e.backtrace.join("\n  ")
    nil
end
cancel() click to toggle source
# File lib/roby/interface/shell_client.rb, line 304
def cancel
    @batch = client.create_batch
    review
    nil
end
close() click to toggle source
# File lib/roby/interface/shell_client.rb, line 50
def close
    client.close
    @job_manager = nil
    @client = nil
end
closed?() click to toggle source
# File lib/roby/interface/shell_client.rb, line 46
def closed?
    client.closed?
end
connect(retry_period = 0.5) click to toggle source
# File lib/roby/interface/shell_client.rb, line 25
def connect(retry_period = 0.5)
    retry_warning = false
    begin
        @client = connection_method.call
        @batch = client.create_batch
        @batch_job_info = Hash.new
    rescue ConnectionError, ComError => e
        if retry_period
            if e.kind_of?(ComError)
                Roby::Interface.warn "failed handshake with #{remote_name}, retrying ..."
            elsif !retry_warning
                Roby::Interface.warn "cannot connect to #{remote_name}, retrying every #{retry_period} seconds..."
                retry_warning = true
            end
            sleep retry_period
            retry
        else raise
        end
    end
end
describe(matcher) click to toggle source
# File lib/roby/interface/shell_client.rb, line 137
def describe(matcher)
    if matcher.kind_of?(Roby::Actions::Action)
        pp matcher.model
    elsif matcher.kind_of?(Roby::Actions::Model::Action)
        pp matcher
    else
        client.find_all_actions_matching(matcher).each do |act|
            pp act
        end
    end
    nil
end
drop_job(job_id) click to toggle source
Calls superclass method
# File lib/roby/interface/shell_client.rb, line 264
def drop_job(job_id)
    if safe?
        if @batch_job_info[job_id] = resolve_job_id(job_id)
            @batch.drop_job job_id
            review
        end
    else
        super
    end
    nil
end
format_arguments(hash) click to toggle source
# File lib/roby/interface/shell_client.rb, line 99
def format_arguments(hash)
    hash.keys.map do |k|
        v = hash[k]
        v = if !v || v.respond_to?(:to_str) then v.inspect
            else v
            end
        "#{k} => #{v}"
    end.join(", ")
end
format_exception(kind, error, *args) click to toggle source
# File lib/roby/interface/shell_client.rb, line 186
def format_exception(kind, error, *args)
    color = if kind == ExecutionEngine::EXCEPTION_FATAL then [:red]
            elsif kind == ExecutionEngine::EXCEPTION_NONFATAL then [:magenta]
            else []
            end
    if error
        msg = Roby.format_exception(error.exception)
        if msg[0]
            msg[0] = Roby.color(msg[0], *color)
        end
    else
        msg = ["<something wrong happened in transmission of exception information>"]
    end
    return msg
end
format_job_info(id, state, task, planning_task) click to toggle source
# File lib/roby/interface/shell_client.rb, line 121
def format_job_info(id, state, task, planning_task)
    if planning_task.respond_to?(:action_model) && planning_task.action_model
        name = "#{planning_task.action_model.to_s}(#{format_arguments(planning_task.action_arguments)})"
    else name = task.to_s
    end
    "[%4d] (%s) %s" % [id, state.to_s, name]
end
format_job_progress(kind, job_id, job_name, *args) click to toggle source
# File lib/roby/interface/shell_client.rb, line 178
def format_job_progress(kind, job_id, job_name, *args)
    ["[#{job_id}] #{job_name}: #{kind}"]
end
format_notification(source, level, message) click to toggle source
# File lib/roby/interface/shell_client.rb, line 170
def format_notification(source, level, message)
    ["[#{level}] #{source}: #{message}"]
end
help(subcommand = client) click to toggle source
# File lib/roby/interface/shell_client.rb, line 342
def help(subcommand = client)
    puts
    if safe?
        puts Roby.color("Currently in safe mode, use 'unsafe' to switch", :bold)
        puts "Job commands like drop_job, kill_job, ... are queued, only sent if on 'process'"
        puts "review           display the pending job commands"
        puts "process          apply the pending job commands"
        puts "cancel           clear the pending job commands"
    else
        puts Roby.color("Currently in unsafe mode, use 'safe' to switch", :bold, :red)
        puts "Job commands like drop_job, kill_job, ... are sent directly"
    end

    puts
    if subcommand.respond_to?(:description)
        puts Roby.color(subcommand.description.join("\n"), :bold)
        puts
    end

    commands = subcommand.commands[''].commands
    if !commands.empty?
        puts Roby.color("Commands", :bold)
        puts Roby.color("--------", :bold)
        commands.keys.sort.each do |command_name|
            cmd = commands[command_name]
            puts "#{command_name}(#{cmd.arguments.keys.map(&:to_s).join(", ")}): #{cmd.description.first}"
        end
    end
    if subcommand.commands.size > 1
        puts if !commands.empty?
        puts Roby.color("Subcommands (use help <subcommand name> for more details)", :bold)
        puts Roby.color("-----------", :bold)
        subcommand.commands.keys.sort.each do |subcommand_name|
            next if subcommand_name.empty?
            puts "#{subcommand_name}: #{subcommand.commands[subcommand_name].description.first}"
        end
    end
    nil
end
jobs() click to toggle source
# File lib/roby/interface/shell_client.rb, line 113
def jobs
    jobs = __jobs
    jobs.each do |id, job_info|
        puts format_job_info(id, *job_info)
    end
    nil
end
kill_job(job_id) click to toggle source
Calls superclass method
# File lib/roby/interface/shell_client.rb, line 252
def kill_job(job_id)
    if safe?
        if @batch_job_info[job_id] = resolve_job_id(job_id)
            @batch.kill_job job_id
            review
        end
    else
        super
    end
    nil
end
method_missing(m, *args) click to toggle source
# File lib/roby/interface/shell_client.rb, line 310
def method_missing(m, *args)
    if sub = client.find_subcommand_by_name(m.to_s)
        ShellSubcommand.new(self, m.to_s, sub.description, sub.commands)
    elsif act = client.find_action_by_name(m.to_s)
        Roby::Actions::Action.new(act, *args)
    elsif @batch && m.to_s =~ /(.*)!$/
        action_name = $1
        @batch.start_job(action_name, *args)
        review
        nil
    else
        begin
            call Hash[], [], m, *args
        rescue NoMethodError => e
            if e.message =~ /undefined method .#{m}./
                puts "invalid command name #{m}, call 'help' for more information"
            else raise
            end
        rescue ArgumentError => e
            if e.message =~ /wrong number of arguments/ && e.backtrace.first =~ /#{m.to_s}/
                puts e.message
            else raise
            end
        end
    end
rescue ComError
    Roby::Interface.warn "Lost communication with remote, will not retry the command after reconnection"
    connect
rescue Interrupt
    Roby::Interface.warn "Interrupted"
end
notification_loop(period = 0.1) { |has_valid_connection, messages| ... } click to toggle source

Polls for messages from the remote interface and yields them. It handles automatic reconnection, when applicable, as well

It is meant to be called in a separate thread

@yieldparam [String] msg messages for the user @param [Float] period the polling period in seconds

# File lib/roby/interface/shell_client.rb, line 419
def notification_loop(period = 0.1)
    already_summarized = Set.new
    was_connected = nil
    while true
        has_valid_connection =
            begin
                client.poll
                true
            rescue Exception
                begin
                    connect(nil)
                    client.io.reset_thread_guard
                    true
                rescue Exception
                end
            end

        already_summarized, messages = 
            summarize_pending_messages(already_summarized)
        yield(has_valid_connection, messages)
        if has_valid_connection
            was_connected = true
        end

        if has_valid_connection && !was_connected
            RbReadline.puts "reconnected"
        elsif !has_valid_connection && was_connected
            RbReadline.puts "lost connection, reconnecting ..."
        end
        was_connected = has_valid_connection

        sleep period
    end
end
path() click to toggle source
# File lib/roby/interface/shell_client.rb, line 23
def path; [] end
process() click to toggle source
# File lib/roby/interface/shell_client.rb, line 294
def process
    if safe?
        @batch.__process
    else
        STDERR.puts "Not in batch context"
    end
    @batch = client.create_batch
    nil
end
quit() click to toggle source

Make the remote app quit

This is defined explicitely because otherwise IRB “hooks” on quit to terminate the shell instead

# File lib/roby/interface/shell_client.rb, line 463
def quit
    call(Hash.new, [], :quit)
end
resolve_job_id(job_id) click to toggle source
# File lib/roby/interface/shell_client.rb, line 244
def resolve_job_id(job_id)
    if job_info = __jobs[job_id]
        job_info
    else
        STDERR.puts Roby.color("No job #{job_id}", :bold, :bright_red)
    end
end
retry_on_com_error() { || ... } click to toggle source
# File lib/roby/interface/shell_client.rb, line 129
def retry_on_com_error
    yield
rescue ComError
    Roby::Interface.warn "Lost communication with remote, retrying command after reconnection"
    connect
    retry
end
review() click to toggle source
# File lib/roby/interface/shell_client.rb, line 276
def review
    if safe?
        puts "#{@batch.__calls.size} actions queued in the current batch, use #process to send, #cancel to delete"
        @batch.__calls.each do |context, m, *args|
            if m == :drop_job || m == :kill_job
                job_id = args.first
                job_info = format_job_info(job_id, *@batch_job_info[job_id])
                puts "#{Roby.color(m.to_s, :bold, :bright_red)} #{job_info}"
            elsif m == :start_job
                puts "#{Roby.color("#{args[0]}!", :bright_blue)}(#{args[1]})"
            else
                puts "#{Roby.color("#{m}!", :bright_blue)}(#{args.first})"
            end
        end
    end
    nil
end
safe() click to toggle source
# File lib/roby/interface/shell_client.rb, line 235
def safe
    @batch ||= client.create_batch
    nil
end
safe?() click to toggle source
# File lib/roby/interface/shell_client.rb, line 231
def safe?
    !!@batch
end
silent(be_silent) click to toggle source

Whether the shell should stop displaying any notification

# File lib/roby/interface/shell_client.rb, line 455
def silent(be_silent)
    @silent = be_silent
end
summarize_exception(kind, error, *args) click to toggle source
# File lib/roby/interface/shell_client.rb, line 202
def summarize_exception(kind, error, *args)
    msg = "(#{kind}) #{format_exception(kind, error, *args).first}"
    return msg, false
end
summarize_job_progress(kind, job_id, job_name, *args) click to toggle source
# File lib/roby/interface/shell_client.rb, line 182
def summarize_job_progress(kind, job_id, job_name, *args)
    return format_job_progress(kind, job_id, job_name, *args).first, true
end
summarize_notification(source, level, message) click to toggle source
# File lib/roby/interface/shell_client.rb, line 174
def summarize_notification(source, level, message)
    return format_notification(source, level, message).first, true
end
summarize_pending_messages(already_summarized = Set.new) click to toggle source

Processes the exception and job_progress queues, and yields with a message that summarizes the new ones

@param [Set] already_summarized the set of IDs of messages that

have already been summarized. This should be the value returned by
the last call to {#summarize_pending_messages}

@yieldparam [String] msg the message that summarizes the new

exception/job progress

@return [Set] the set of notifications still in the queues that

have already been summarized. Pass to the next call to
{#summarize_exception}
# File lib/roby/interface/shell_client.rb, line 393
def summarize_pending_messages(already_summarized = Set.new)
    summarized = Set.new
    messages = []
    queues = {exception: client.exception_queue,
              job_progress: client.job_progress_queue,
              notification: client.notification_queue}
    queues.each do |type, q|
        q.delete_if do |id, args|
            summarized << id
            if !already_summarized.include?(id)
                msg, complete = send("summarize_#{type}", *args)
                messages << "##{id} #{msg}"
                complete
            end
        end
    end
    return summarized, messages
end
unsafe() click to toggle source
# File lib/roby/interface/shell_client.rb, line 240
def unsafe
    @batch = nil
end
wtf?() click to toggle source
# File lib/roby/interface/shell_client.rb, line 207
def wtf?
    msg = []
    client.notification_queue.each do |id, (source, level, message)|
        msg << Roby.color("-- ##{id} (notification) --", :bold)
        msg.concat format_notification(source, level, message)
        msg << "\n"
    end
    client.job_progress_queue.each do |id, (kind, job_id, job_name, *args)|
        msg << Roby.color("-- ##{id} (job progress) --", :bold)
        msg.concat format_job_progress(kind, job_id, job_name, *args)
        msg << "\n"
    end
    client.exception_queue.each do |id, (kind, exception, tasks)|
        msg << Roby.color("-- ##{id} (#{kind} exception) --", :bold)
        msg.concat format_exception(kind, exception, tasks)
        msg << "\n"
    end
    client.job_progress_queue.clear
    client.exception_queue.clear
    client.notification_queue.clear
    puts msg.join("\n")
    nil
end