Class: Aspera::AsCmd

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/ascmd.rb

Overview

Run ascmd commands using specified executor (usually, remotely on transfer node) Equivalent of SDK “command client” execute: “ascmd -h” to get syntax Note: “ls” can take filters: as_ls -f *.txt -f *.bin /

Defined Under Namespace

Classes: Error

Constant Summary collapse

OPERATIONS =

list of supported actions

OPS_ARGS.keys.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command_executor) ⇒ AsCmd

@param command_executor [Object] provides the “execute” method, taking a command to execute, and stdin to feed to it, typically: ssh or local



30
31
32
# File 'lib/aspera/ascmd.rb', line 30

def initialize(command_executor)
  @command_executor = command_executor
end

Class Method Details

.field_description(struct_name, typed_buffer) ⇒ Object

get description of structure’s field, @param struct_name, @param typed_buffer provides field name



152
153
154
155
156
# File 'lib/aspera/ascmd.rb', line 152

def field_description(struct_name, typed_buffer)
  result = TYPES_DESCR[struct_name][:fields][typed_buffer[:btype] - ENUM_START]
  raise "Unrecognized field for #{struct_name}: #{typed_buffer[:btype]}\n#{typed_buffer[:buffer]}" if result.nil?
  return result
end

.parse(buffer, type_name, indent_level = nil) ⇒ Object

decodes the provided buffer as provided type name :base : value, :buffer_list : an array of btype,buffer, :field_list : a hash, or array

Returns:

  • a decoded type.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/aspera/ascmd.rb', line 161

def parse(buffer, type_name, indent_level=nil)
  indent_level = (indent_level || -1) + 1
  type_descr = TYPES_DESCR[type_name]
  raise "Unexpected type #{type_name}" if type_descr.nil?
  Log.log.trace1{"#{'   .' * indent_level}parse:#{type_name}:#{type_descr[:decode]}:#{buffer[0, 16]}...".red}
  result = nil
  case type_descr[:decode]
  when :base
    num_bytes = type_name.eql?(:zstr) ? buffer.length : type_descr[:size]
    raise 'ERROR:not enough bytes' if buffer.length < num_bytes
    byte_array = buffer.shift(num_bytes)
    byte_array = [byte_array] unless byte_array.is_a?(Array)
    result = byte_array.pack('C*').unpack1(type_descr[:unpack])
    result.force_encoding('UTF-8') if type_name.eql?(:zstr)
    Log.log.trace1{"#{'   .' * indent_level}-> base:#{byte_array} -> #{result}"}
    result = Time.at(result) if type_name.eql?(:epoch)
  when :buffer_list
    result = []
    until buffer.empty?
      btype = parse(buffer, :int8, indent_level)
      length = parse(buffer, :int32, indent_level)
      raise 'ERROR:not enough bytes' if buffer.length < length
      value = buffer.shift(length)
      result.push({btype: btype, buffer: value})
      Log.log.trace1{"#{'   .' * indent_level}:buffer_list[#{result.length - 1}] #{result.last}"}
    end
  when :field_list
    # by default the result is one struct
    result = {}
    # get individual binary fields
    parse(buffer, :blist, indent_level).each do |typed_buffer|
      # what type of field is it ?
      field_info = field_description(type_name, typed_buffer)
      Log.log.trace1{"#{'   .' * indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green}
      case field_info[:special]
      when nil
        result[field_info[:name]] = parse(typed_buffer[:buffer], field_info[:is_a], indent_level)
      when :return_true
        result[field_info[:name]] = true
      when :sub_struct
        result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
      when :multiple
        result[field_info[:name]] ||= []
        result[field_info[:name]].push(parse(typed_buffer[:buffer], field_info[:is_a], indent_level))
      when :restart_on_first
        fl = result[field_info[:name]] = []
        parse(typed_buffer[:buffer], :blist, indent_level).map do |tb|
          fl.push({}) if tb[:btype].eql?(ENUM_START)
          fi = field_description(field_info[:is_a], tb)
          fl.last[fi[:name]] = parse(tb[:buffer], fi[:is_a], indent_level)
        end
      end
    end
  else Aspera.error_unexpected_value(type_descr[:decode])
  end
  return result
end

Instance Method Details

#execute_single(action_sym, arguments) ⇒ Object

execute an “as” command on a remote server

Parameters:

  • one (Symbol)

    of OPERATIONS

  • parameters (Array)

    for “as” command

Returns:

  • result of command, type depends on command (bool, array, hash)

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/aspera/ascmd.rb', line 38

def execute_single(action_sym, arguments)
  arguments = [] if arguments.nil?
  Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
  Aspera.assert_type(action_sym, Symbol)
  Aspera.assert_type(arguments, Array)
  Aspera.assert(arguments.all?(String), 'arguments must be strings')
  # lines of commands (String's)
  command_lines = []
  # add "as_" command
  main_command = "as_#{action_sym}"
  arg_batches =
    if OPS_ARGS[action_sym].nil? || OPS_ARGS[action_sym].zero?
      [arguments]
    else
      # split arguments into batches
      arguments.each_slice(OPS_ARGS[action_sym]).to_a
    end
  arg_batches.each do |args|
    command = [main_command]
    # enclose arguments in double quotes, protect backslash and double quotes
    args.each do |v|
      command.push(%Q{"#{v.gsub(/["\\]/){|s|"\\#{s}"}}"})
    end
    command_lines.push(command.join(' '))
  end
  command_lines.push('as_exit')
  command_lines.push('')
  # execute the main command and then exit
  stdin_input = command_lines.join("\n")
  Log.log.trace1{"execute_single:#{stdin_input}"}
  # execute, get binary output
  byte_buffer = @command_executor.execute('ascmd', stdin_input).unpack('C*')
  raise 'ERROR: empty answer from server' if byte_buffer.empty?
  # get hash or table result
  result = self.class.parse(byte_buffer, :result)
  raise 'ERROR: unparsed bytes remaining' unless byte_buffer.empty?
  # get and delete info,always present in results
  system_info = result[:info]
  result.delete(:info)
  # make single file result like a folder
  result[:dir] = [result.delete(:file)] if result.key?(:file)
  # add type field for stats
  if result.key?(:dir)
    result[:dir].each do |file|
      if file.key?(:smode)
        # Converts the first character of the file mode (see 'man ls') into a type.
        file[:type] = case file[:smode][0, 1]; when 'd' then:directory; when '-' then:file; when 'l' then:link; else; :other; end # rubocop:disable Style/Semicolon
      end
    end
  end
  # for info, second overrides first, so restore it
  case result.keys.length
  when 0 then result = system_info
  when 1 then result = result[result.keys.first]
  else Aspera.error_unexpected_value(result.keys.length)
  end
  # raise error as exception
  raise Error.new(result[:errno], result[:errstr], action_sym, arguments) if
    result.is_a?(Hash) && (result.keys.sort == TYPES_DESCR[:error][:fields].map{|i|i[:name]}.sort)
  return result
end