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



77
78
79
# File 'lib/aspera/ascmd.rb', line 77

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



159
160
161
162
163
# File 'lib/aspera/ascmd.rb', line 159

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.



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
218
219
220
221
222
223
224
225
# File 'lib/aspera/ascmd.rb', line 168

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
    # return a list of type_buffer
    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 # normal case
        result[field_info[:name]] = parse(typed_buffer[:buffer], field_info[:is_a], indent_level)
      when :return_true # nothing to parse, just return true
        result[field_info[:name]] = true
      when :list_multiple # field appears multiple times, and is an array of values (base type)
        result[field_info[:name]] ||= []
        result[field_info[:name]].push(parse(typed_buffer[:buffer], field_info[:is_a], indent_level))
      when :list_tlv_list # field is an array of values in a list of buffers
        result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
      when :list_tlv_restart # field is an array of values, but a new value is started on index 1
        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:



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/aspera/ascmd.rb', line 85

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
    # ascmd uses space as token separator, and optional quotes ('") or \ to escape
    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