Class: Aspera::Cli::Main

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

Overview

The main CLI class

Constant Summary collapse

STATUS_FIELD =

Plugins store transfer result using this key and use result_transfer_multiple()

'status'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv) ⇒ Main

Minimum initialization, no exception raised



141
142
143
144
145
146
147
148
# File 'lib/aspera/cli/main.rb', line 141

def initialize(argv)
  @argv = argv
  Log.dump(:argv, @argv, level: :trace2)
  @option_help = false
  @option_show_config = false
  @bash_completion = false
  @context = Context.new
end

Class Method Details

.result_auto(data) ⇒ Object

Determines type of result based on data



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/aspera/cli/main.rb', line 121

def result_auto(data)
  case data
  when NilClass
    return result_special(:null)
  when Hash
    return result_single_object(data)
  when Array
    all_types = data.map(&:class).uniq
    return result_object_list(data) if all_types.eql?([Hash])
    unsupported_types = all_types - SCALAR_TYPES
    return result_value_list(data, name: 'list') if unsupported_types.empty?
    Aspera.error_unexpected_value(unsupported_types){'list item types'}
  when *SCALAR_TYPES
    return result_text(data)
  else Aspera.error_unexpected_value(data.class.name){'result type'}
  end
end

.result_emptyObject

Expect some list, but nothing to display



59
# File 'lib/aspera/cli/main.rb', line 59

def result_empty; result_special(:empty); end

.result_image(url_or_blob) ⇒ Object

Display image for that URL or directly blob



99
100
101
# File 'lib/aspera/cli/main.rb', line 99

def result_image(url_or_blob)
  return {type: :image, data: url_or_blob}
end

.result_nothingObject

Nothing expected



62
# File 'lib/aspera/cli/main.rb', line 62

def result_nothing; result_special(:nothing); end

.result_object_list(data, fields: nil, total: nil) ⇒ Object

An Array of Hash



109
110
111
# File 'lib/aspera/cli/main.rb', line 109

def result_object_list(data, fields: nil, total: nil)
  return {type: :object_list, data: data, fields: fields, total: total}
end

.result_single_object(data, fields: nil) ⇒ Object

A single object, must be Hash



104
105
106
# File 'lib/aspera/cli/main.rb', line 104

def result_single_object(data, fields: nil)
  return {type: :single_object, data: data, fields: fields}
end

.result_special(how) ⇒ Object



56
# File 'lib/aspera/cli/main.rb', line 56

def result_special(how); {type: :special, data: how}; end

.result_status(status) ⇒ Object

Result is some status, such as “complete”, “deleted”…

Parameters:

  • status (String)

    The status



66
# File 'lib/aspera/cli/main.rb', line 66

def result_status(status); return {type: :status, data: status}; end

.result_successObject



71
# File 'lib/aspera/cli/main.rb', line 71

def result_success; return result_status('complete'); end

.result_text(data) ⇒ Object

Text result coming from command result



69
# File 'lib/aspera/cli/main.rb', line 69

def result_text(data); return {type: :text, data: data}; end

.result_transfer(statuses) ⇒ Object

Process statuses of finished transfer sessions else returns an empty status

Raises:

  • exception if there is one error



76
77
78
79
80
# File 'lib/aspera/cli/main.rb', line 76

def result_transfer(statuses)
  worst = TransferAgent.session_status(statuses)
  raise worst unless worst.eql?(:success)
  return Main.result_nothing
end

.result_transfer_multiple(status_table) ⇒ Object

Used when one command executes several transfer jobs (each job being possibly multi session) Each element has a key STATUS_FIELD which contains the result of possibly multiple sessions

Parameters:

  • status_table (Array)
    array],…,…

Returns:

  • a status object suitable as command result



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/aspera/cli/main.rb', line 86

def result_transfer_multiple(status_table)
  global_status = :success
  # Transform status array into string and find if there was problem
  status_table.each do |item|
    worst = TransferAgent.session_status(item[STATUS_FIELD])
    global_status = worst unless worst.eql?(:success)
    item[STATUS_FIELD] = item[STATUS_FIELD].map(&:to_s).join(',')
  end
  raise global_status unless global_status.eql?(:success)
  return result_object_list(status_table)
end

.result_value_list(data, name: 'id') ⇒ Object

A list of values



114
115
116
117
118
# File 'lib/aspera/cli/main.rb', line 114

def result_value_list(data, name: 'id')
  Aspera.assert_type(data, Array)
  Aspera.assert_type(name, String)
  return {type: :value_list, data: data, name: name}
end

Instance Method Details

#init_agents_options_pluginsObject



254
255
256
257
258
# File 'lib/aspera/cli/main.rb', line 254

def init_agents_options_plugins
  init_agents_and_options
  # Find plugins, shall be after parse! ?
  Plugins::Factory.instance.add_plugins_from_lookup_folders
end

#process_command_lineObject

This is the main function called by initial script just after constructor



151
152
153
154
155
156
157
158
159
160
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/aspera/cli/main.rb', line 151

def process_command_line
  # Catch exception information , if any
  exception_info = nil
  # False if command shall not be executed (e.g. --show-config)
  execute_command = true
  # Catch exceptions
  begin
    init_agents_options_plugins
    # Help requested without command ? (plugins must be known here)
    show_usage if @option_help && @context.options.command_or_arg_empty?
    generate_bash_completion if @bash_completion
    @context.config.periodic_check_newer_gem_version
    command_sym =
      if @option_show_config && @context.options.command_or_arg_empty?
        COMMAND_CONFIG
      else
        @context.options.get_next_command(Plugins::Factory.instance.plugin_list.unshift(COMMAND_HELP))
      end
    # Command will not be executed, but we need manual
    @context.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
    # Main plugin is not dynamically instantiated
    case command_sym
    when COMMAND_HELP
      show_usage
    when COMMAND_CONFIG
      command_plugin = @context.config
    else
      # Get plugin, set options, etc
      command_plugin = get_plugin_instance_with_options(command_sym)
      # Parse plugin specific options
      @context.options.parse_options!
    end
    # Help requested for current plugin
    show_usage(all: false) if @option_help
    if @option_show_config
      @context.formatter.display_results(type: :single_object, data: @context.options.known_options(only_defined: true).stringify_keys)
      execute_command = false
    end
    # Locking for single execution (only after "per plugin" option, in case lock port is there)
    lock_port = @context.options.get_option(:lock_port)
    if !lock_port.nil?
      begin
        # No need to close later, will be freed on process exit. must save in member else it is garbage collected
        Log.log.debug{"Opening lock port #{lock_port}"}
        # Loopback address, could also be 'localhost'
        @tcp_server = TCPServer.new('127.0.0.1', lock_port)
      rescue StandardError => e
        execute_command = false
        Log.log.warn{"Another instance is already running (#{e.message})."}
      end
    end
    pid_file = @context.options.get_option(:pid_file)
    if !pid_file.nil?
      File.write(pid_file, Process.pid)
      Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
      at_exit{File.delete(pid_file)}
    end
    # Execute and display (if not exclusive execution)
    @context.formatter.display_results(**command_plugin.execute_action) if execute_command
    # Save config file if command modified it
    @context.config.save_config_file_if_needed
    # Finish
    @context.transfer.shutdown
  rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
  rescue OpenSSL::SSL::SSLError => e;         exception_info = {e: e, t: 'SSL'}
  rescue Cli::BadArgument => e;               exception_info = {e: e, t: 'Argument', usage: true}
  rescue Cli::BadIdentifier => e;             exception_info = {e: e, t: 'Identifier'}
  rescue Cli::Error => e;                     exception_info = {e: e, t: 'Tool', usage: true}
  rescue Transfer::Error => e;                exception_info = {e: e, t: 'Transfer'}
  rescue RestCallError => e;                  exception_info = {e: e, t: 'Rest'}
  rescue SocketError => e;                    exception_info = {e: e, t: 'Network'}
  rescue StandardError => e;                  exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
  rescue Interrupt => e;                      exception_info = {e: e, t: 'Interruption', debug: true}
  end
  # Cleanup file list files
  TempFileManager.instance.cleanup
  # 1- processing of error condition
  unless exception_info.nil?
    Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
    @context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
    @context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
    # Is that a known error condition with proposal for remediation ?
    Hints.hint_for(exception_info[:e], @context.formatter)
  end
  # 2- processing of command not processed (due to exception or bad command line)
  if execute_command || @option_show_config
    @context.options.final_errors.each do |msg|
      @context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
      # Add code as exception if there is not already an error
      exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
    end
  end
  # 3- in case of error, fail the process status
  unless exception_info.nil?
    # Show stack trace in debug mode
    raise exception_info[:e] if Log.log.debug?
    # Else give hint and exit
    @context.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
    Process.exit(1)
  end
  return
end

#show_usage(all: true, exit: true) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/aspera/cli/main.rb', line 260

def show_usage(all: true, exit: true)
  # Display main plugin options (+config)
  @context.formatter.display_message(:error, @context.options.parser)
  if all
    @context.only_manual
    # List plugins that have a "require" field, i.e. all but main plugin
    Plugins::Factory.instance.plugin_list.each do |plugin_name_sym|
      # Config was already included in the global options
      next if plugin_name_sym.eql?(COMMAND_CONFIG)
      # Override main option parser with a brand new, to avoid having global options
      @context.options = Manager.new(Info::CMD_NAME)
      @context.options.parser.banner = '' # Remove default banner
      get_plugin_instance_with_options(plugin_name_sym)
      # Display generated help for plugin options
      @context.formatter.display_message(:error, @context.options.parser.help)
    end
  end
  Process.exit(0) if exit
end