aspera-cli 4.14.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
3
  require 'aspera/fasp/agent_base'
5
4
  require 'aspera/fasp/error'
6
5
  require 'aspera/fasp/parameters'
7
6
  require 'aspera/fasp/installation'
8
7
  require 'aspera/fasp/resume_policy'
9
8
  require 'aspera/fasp/transfer_spec'
9
+ require 'aspera/fasp/management'
10
10
  require 'aspera/log'
11
+ require 'aspera/assert'
11
12
  require 'socket'
12
- require 'timeout'
13
13
  require 'securerandom'
14
14
  require 'shellwords'
15
+ require 'English'
15
16
 
16
17
  module Aspera
17
18
  module Fasp
@@ -19,24 +20,30 @@ module Aspera
19
20
  class AgentDirect < Aspera::Fasp::AgentBase
20
21
  # options for initialize (same as values in option transfer_info)
21
22
  DEFAULT_OPTIONS = {
22
- spawn_timeout_sec: 3,
23
- spawn_delay_sec: 2,
23
+ spawn_timeout_sec: 2,
24
+ spawn_delay_sec: 2, # optional delay to start between sessions
24
25
  wss: true, # true: if both SSH and wss in ts: prefer wss
25
26
  multi_incr_udp: true,
26
27
  resume: {},
27
28
  ascp_args: [],
28
- quiet: true # by default no native ascp progress bar
29
+ check_ignore: nil, # callback with host,port
30
+ quiet: true, # by default no native ascp progress bar
31
+ trusted_certs: [] # list of files with trusted certificates (stores)
29
32
  }.freeze
30
- private_constant :DEFAULT_OPTIONS
33
+ LISTEN_ADDRESS = '127.0.0.1'
34
+ LISTEN_AVAILABLE_PORT = 0 # 0 means any available port
35
+ # spellchecker: enable
36
+ private_constant :DEFAULT_OPTIONS, :LISTEN_ADDRESS
31
37
 
32
- # start ascp transfer (non blocking), single or multi-session
33
- # job information added to @jobs
38
+ # method of Aspera::Fasp::AgentBase
39
+ # start ascp transfer(s) (non blocking), single or multi-session
40
+ # session information added to @sessions
34
41
  # @param transfer_spec [Hash] aspera transfer specification
42
+ # @param token_regenerator [Object] object with method refreshed_transfer_token
35
43
  def start_transfer(transfer_spec, token_regenerator: nil)
36
- the_job_id = SecureRandom.uuid
37
44
  # clone transfer spec because we modify it (first level keys)
38
45
  transfer_spec = transfer_spec.clone
39
- # if there is aspera tags
46
+ # if there are aspera tags
40
47
  if transfer_spec['tags'].is_a?(Hash) && transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED].is_a?(Hash)
41
48
  # TODO: what is this for ? only on local ascp ?
42
49
  # NOTE: important: transfer id must be unique: generate random id
@@ -44,19 +51,10 @@ module Aspera
44
51
  # all sessions in a multi-session transfer must have the same xfer_id (see admin manual)
45
52
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_id'] ||= SecureRandom.uuid
46
53
  Log.log.debug{"xfer id=#{transfer_spec['xfer_id']}"}
47
- # TODO: useful ? node only ?
54
+ # TODO: useful ? node only ? seems to be a timeout for retry in node
48
55
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_retry'] ||= 3600
49
56
  end
50
- Log.dump('ts', transfer_spec)
51
-
52
- # add bypass keys when authentication is token and no auth is provided
53
- if transfer_spec.key?('token') &&
54
- !transfer_spec.key?('remote_password') &&
55
- !transfer_spec.key?('EX_ssh_key_paths')
56
- # transfer_spec['remote_password'] = Installation.instance.bypass_pass # not used: no passphrase
57
- transfer_spec['EX_ssh_key_paths'] = Installation.instance.bypass_keys
58
- end
59
-
57
+ Log.log.debug{Log.dump('ts', transfer_spec)}
60
58
  # Compute this before using transfer spec because it potentially modifies the transfer spec
61
59
  # (even if the var is not used in single session)
62
60
  multi_session_info = nil
@@ -81,38 +79,24 @@ module Aspera
81
79
  end
82
80
  end
83
81
 
84
- # compute known args
85
- env_args = Parameters.new(transfer_spec, @options).ascp_args
86
-
87
- # add fallback cert and key as arguments if needed
88
- if ['1', 1, true, 'force'].include?(transfer_spec['http_fallback'])
89
- env_args[:args].unshift('-Y', Installation.instance.path(:fallback_cert_privkey))
90
- env_args[:args].unshift('-I', Installation.instance.path(:fallback_certificate))
91
- end
92
-
93
- env_args[:args].unshift('-q') if @options[:quiet]
94
-
95
- # transfer job can be multi session
96
- xfer_job = {
97
- id: the_job_id,
98
- sessions: [] # all sessions as below
99
- }
100
-
101
82
  # generic session information
102
83
  session = {
84
+ id: nil, # SessionId from INIT message in mgt port
85
+ job_id: SecureRandom.uuid, # job id (regroup sessions)
86
+ ts: transfer_spec, # transfer spec
103
87
  thread: nil, # Thread object monitoring management port, not nil when pushed to :sessions
104
88
  error: nil, # exception if failed
105
89
  io: nil, # management port server socket
106
- id: nil, # SessionId from INIT message in mgt port
107
90
  token_regenerator: token_regenerator, # regenerate bearer token with oauth
108
- env_args: env_args # env vars and args to ascp (from transfer spec)
91
+ # env vars and args to ascp (from transfer spec)
92
+ env_args: Parameters.new(transfer_spec, @options).ascp_args
109
93
  }
110
94
 
111
95
  if multi_session_info.nil?
112
96
  Log.log.debug('Starting single session thread')
113
97
  # single session for transfer : simple
114
98
  session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
115
- xfer_job[:sessions].push(session)
99
+ @sessions.push(session)
116
100
  else
117
101
  Log.log.debug('Starting multi session threads')
118
102
  1.upto(multi_session_info[:count]) do |i|
@@ -120,22 +104,19 @@ module Aspera
120
104
  sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
121
105
  # do deep copy (each thread has its own copy because it is modified here below and in thread)
122
106
  this_session = session.clone
107
+ this_session[:ts] = this_session[:ts].clone
123
108
  this_session[:env_args] = this_session[:env_args].clone
124
109
  this_session[:env_args][:args] = this_session[:env_args][:args].clone
110
+ # set multi session part
125
111
  this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
126
112
  # option: increment (default as per ascp manual) or not (cluster on other side ?)
127
113
  this_session[:env_args][:args].unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @options[:multi_incr_udp]
114
+ # finally start the thread
128
115
  this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
129
- xfer_job[:sessions].push(this_session)
116
+ @sessions.push(this_session)
130
117
  end
131
118
  end
132
- Log.log.debug('started session thread(s)')
133
-
134
- # add job to list of jobs
135
- @jobs[the_job_id] = xfer_job
136
- Log.log.debug{"jobs: #{@jobs.keys.count}"}
137
-
138
- return the_job_id
119
+ return session[:job_id]
139
120
  end # start_transfer
140
121
 
141
122
  # wait for completion of all jobs started
@@ -144,16 +125,14 @@ module Aspera
144
125
  Log.log.debug('wait_for_transfers_completion')
145
126
  # set to non-nil to exit loop
146
127
  result = []
147
- @jobs.each do |_id, job|
148
- job[:sessions].each do |session|
149
- Log.log.debug{"join #{session[:thread]}"}
150
- session[:thread].join
151
- result.push(session[:error] || :success)
152
- end
128
+ @sessions.each do |session|
129
+ Log.log.debug{"join #{session[:thread]}"}
130
+ session[:thread].join
131
+ result.push(session[:error] || :success)
153
132
  end
154
133
  Log.log.debug('all transfers joined')
155
134
  # since all are finished and we return the result, clear statuses
156
- @jobs.clear
135
+ @sessions.clear
157
136
  return result
158
137
  end
159
138
 
@@ -162,34 +141,75 @@ module Aspera
162
141
  Log.log.debug('fasp local shutdown')
163
142
  end
164
143
 
144
+ # cspell:disable
145
+ # begin 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size
146
+ # progress 'Type' => 'STATS', 'Bytescont' => size
147
+ # end 'Type' => 'DONE'
148
+ # cspell:enable
149
+
150
+ # @param event management port event
151
+ def process_progress(event)
152
+ session_id = event['SessionId']
153
+ case event['Type']
154
+ when 'INIT'
155
+ @pre_calc_sent = false
156
+ @pre_calc_last_size = nil
157
+ notify_progress(session_id: session_id, type: :session_start)
158
+ when 'NOTIFICATION' # sent from remote
159
+ if event.key?('PreTransferBytes')
160
+ @pre_calc_sent = true
161
+ notify_progress(session_id: session_id, type: :session_size, info: event['PreTransferBytes'])
162
+ end
163
+ when 'STATS' # during transfer
164
+ @pre_calc_last_size = event['TransferBytes'].to_i + event['StartByte'].to_i
165
+ notify_progress(session_id: session_id, type: :transfer, info: @pre_calc_last_size)
166
+ when 'DONE', 'ERROR' # end of session
167
+ total_size = event['TransferBytes'].to_i + event['StartByte'].to_i
168
+ if !@pre_calc_sent && !total_size.zero?
169
+ notify_progress(session_id: session_id, type: :session_size, info: total_size)
170
+ end
171
+ if @pre_calc_last_size != total_size
172
+ notify_progress(session_id: session_id, type: :transfer, info: total_size)
173
+ end
174
+ notify_progress(session_id: session_id, type: :end)
175
+ # cspell:disable
176
+ when 'SESSION'
177
+ when 'ARGSTOP'
178
+ when 'FILEERROR'
179
+ when 'STOP'
180
+ # cspell:enable
181
+ # stop event when one file is completed
182
+ else
183
+ Log.log.debug{"unknown event type #{event['Type']}"}
184
+ end
185
+ end
186
+
165
187
  # This is the low level method to start the "ascp" process
166
188
  # currently, relies on command line arguments
167
189
  # start ascp with management port.
168
190
  # raises FaspError on error
169
191
  # if there is a thread info: set and broadcast session id
192
+ # runs in separate thread
170
193
  # @param env_args a hash containing :args :env :ascp_version
171
194
  # @param session this session information
172
195
  # could be private method
173
196
  def start_transfer_with_args_env(env_args, session)
174
- raise 'env_args must be Hash' unless env_args.is_a?(Hash)
175
- raise 'session must be Hash' unless session.is_a?(Hash)
176
- # by default we assume an exception will be raised (for ensure block)
177
- exception_raised = true
197
+ assert_type(env_args, Hash)
198
+ assert_type(session, Hash)
199
+ Log.log.debug{"env_args=#{env_args.inspect}"}
200
+ notify_progress(session_id: nil, type: :pre_start, info: 'starting')
178
201
  begin
179
- Log.log.debug{"env_args=#{env_args.inspect}"}
180
- # get location of ascp executable
181
- ascp_path = @mutex.synchronize do
182
- Fasp::Installation.instance.path(env_args[:ascp_version])
183
- end
184
- # (optional) check it exists
185
- raise Fasp::Error, "no such file: #{ascp_path}" unless File.exist?(ascp_path)
202
+ # we use Socket directly, instead of TCPServer, s it gives access to lower level options
203
+ mgt_server_socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
186
204
  # open an available (0) local TCP port as ascp management
187
- mgt_sock = TCPServer.new('127.0.0.1', 0)
188
- # clone arguments as we eed to modify with mgt port
189
- ascp_arguments = env_args[:args].clone
190
- # add management port on the selected local port
191
- ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
192
- # start ascp in sub process
205
+ # Socket.pack_sockaddr_in(LISTEN_AVAILABLE_PORT, LISTEN_ADDRESS)
206
+ mgt_server_socket.bind(Addrinfo.tcp(LISTEN_ADDRESS, LISTEN_AVAILABLE_PORT))
207
+ # clone arguments and add mgt port
208
+ ascp_arguments = ['-M', mgt_server_socket.local_address.ip_port.to_s].concat(env_args[:args])
209
+ # mgt_server_socket.addr[1]
210
+ # get location of ascp executable
211
+ ascp_path = Fasp::Installation.instance.path(env_args[:ascp_version])
212
+ # display ascp command line
193
213
  Log.log.debug do
194
214
  [
195
215
  'execute:',
@@ -198,97 +218,63 @@ module Aspera
198
218
  ascp_arguments.map{|a|Shellwords.shellescape(a)}
199
219
  ].flatten.join(' ')
200
220
  end
201
- # start process
202
- ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments)
203
- # in parent, wait for connection to socket max 3 seconds
204
- Log.log.debug{"before accept for pid (#{ascp_pid})"}
205
- # init management socket
206
- ascp_mgt_io = nil
207
- Timeout.timeout(@options[:spawn_timeout_sec]) do
208
- ascp_mgt_io = mgt_sock.accept
209
- # management messages include file names which may be utf8
210
- # by default socket is US-ASCII
211
- # TODO: use same value as Encoding.default_external
212
- ascp_mgt_io.set_encoding(Encoding::UTF_8)
213
- end
214
- Log.log.debug{"after accept (#{ascp_mgt_io})"}
221
+ # start ascp in separate process
222
+ ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments, close_others: true)
223
+ Log.log.debug{"spawned pid #{ascp_pid}"}
224
+ notify_progress(session_id: nil, type: :pre_start, info: 'waiting for ascp')
225
+ mgt_server_socket.listen(1)
226
+ # TODO: timeout does not work when Process.spawn is used... until process exits, then it works
227
+ Log.log.debug{"before select, timeout: #{@options[:spawn_timeout_sec]}"}
228
+ readable, _, _ = IO.select([mgt_server_socket], nil, nil, @options[:spawn_timeout_sec])
229
+ Log.log.debug('after select, before accept')
230
+ assert(readable, exception_class: Fasp::Error){'timeout waiting mgt port connect (select not readable)'}
231
+ # There is a connection to accept
232
+ client_socket, _client_addrinfo = mgt_server_socket.accept
233
+ Log.log.debug('after accept')
234
+ ascp_mgt_io = client_socket.to_io
235
+ # management messages include file names which may be utf8
236
+ # by default socket is US-ASCII
237
+ # TODO: use same value as Encoding.default_external
238
+ ascp_mgt_io.set_encoding(Encoding::UTF_8)
215
239
  session[:io] = ascp_mgt_io
216
- # exact text for event, with \n
217
- current_event_text = ''
218
- # parsed event (hash)
219
- current_event_data = nil
220
- # this is the last full status
221
- last_status_event = nil
222
- # read management port
223
- loop do
224
- # TODO: timeout here ?
225
- line = ascp_mgt_io.gets
226
- # nil when ascp process exits
227
- break if line.nil?
228
- current_event_text += line
229
- line.chomp!
230
- Log.log.debug{"line=[#{line}]"}
231
- case line
232
- when 'FASPMGR 2'
233
- # begin event
234
- current_event_data = {}
235
- current_event_text = ''
236
- when /^([^:]+): (.*)$/
237
- # event field
238
- current_event_data[Regexp.last_match(1)] = Regexp.last_match(2)
239
- when ''
240
- # empty line is separator to end event information
241
- raise 'unexpected empty line' if current_event_data.nil?
242
- current_event_data[AgentBase::LISTENER_SESSION_ID_B] = ascp_pid
243
- notify_listeners(current_event_text, current_event_data)
244
- case current_event_data['Type']
245
- when 'INIT'
246
- session[:id] = current_event_data['SessionId']
247
- Log.log.debug{"session id: #{session[:id]}"}
248
- when 'DONE', 'ERROR'
249
- # TODO: check if this is always the last event
250
- last_status_event = current_event_data
251
- end # event type
252
- else
253
- raise "unexpected line:[#{line}]"
254
- end # case
255
- end # loop (process mgt port lines)
240
+ processor = Management.new
241
+ # read management port, until socket is closed (gets returns nil)
242
+ while (line = ascp_mgt_io.gets)
243
+ event = processor.process_line(line.chomp)
244
+ next unless event
245
+ # event is ready
246
+ Log.log.trace1{Log.dump(:management_port, event)}
247
+ # Log.log.trace1{"event: #{JSON.generate(Management.enhanced_event_format(event))}"}
248
+ process_progress(event)
249
+ Log.log.error((event['Description']).to_s) if event['Type'].eql?('FILEERROR') # cspell:disable-line
250
+ end
251
+ Log.log.debug('management io closed')
252
+ last_event = processor.last_event
256
253
  # check that last status was received before process exit
257
- if last_status_event.is_a?(Hash)
258
- case last_status_event['Type']
259
- when 'DONE'
260
- # all went well
261
- exception_raised = false
254
+ if last_event.is_a?(Hash)
255
+ case last_event['Type']
262
256
  when 'ERROR'
263
- Log.log.error{"code: #{last_status_event['Code']}"}
264
- if /bearer token/i.match?(last_status_event['Description'])
265
- Log.log.error('need to regenerate token'.red)
266
- if session[:token_regenerator].respond_to?(:refreshed_transfer_token)
267
- # regenerate token here, expired, or error on it
268
- # Note: in multi-session, each session will have a different one.
269
- env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
270
- end
257
+ if /bearer token/i.match?(last_event['Description']) &&
258
+ session[:token_regenerator].respond_to?(:refreshed_transfer_token)
259
+ # regenerate token here, expired, or error on it
260
+ # Note: in multi-session, each session will have a different one.
261
+ Log.log.warn('Regenerating token for transfer')
262
+ env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
271
263
  end
272
- # cannot resolve address
273
- # if last_status_event['Code'].to_i.eql?(14)
274
- # Log.log.warn{"host: #{}"}
275
- # end
276
- raise Fasp::Error.new(last_status_event['Description'], last_status_event['Code'].to_i)
277
- else # case
278
- raise "unexpected last event type: #{last_status_event['Type']}"
279
- end
280
- else
281
- exception_raised = false
282
- Log.log.debug('no status read from ascp mgt port')
264
+ raise Fasp::Error.new(last_event['Description'], last_event['Code'].to_i)
265
+ when 'DONE'
266
+ nil
267
+ else
268
+ raise "unexpected last event type: #{last_event['Type']}"
269
+ end # case
283
270
  end
284
271
  rescue SystemCallError => e
285
272
  # Process.spawn
286
273
  raise Fasp::Error, e.message
287
- rescue Timeout::Error
288
- raise Fasp::Error, 'timeout waiting mgt port connect'
289
274
  rescue Interrupt
290
275
  raise Fasp::Error, 'transfer interrupted by user'
291
276
  ensure
277
+ mgt_server_socket.close
292
278
  # if ascp was successfully started
293
279
  unless ascp_pid.nil?
294
280
  # "wait" for process to avoid zombie
@@ -297,58 +283,62 @@ module Aspera
297
283
  ascp_pid = nil
298
284
  session.delete(:io)
299
285
  if !status.success?
300
- message = "ascp failed with code #{status.exitstatus}"
301
- # raise error only if there was not already an exception
302
- raise Fasp::Error, message unless exception_raised
303
- # else just debug, as main exception is already here
304
- Log.log.debug(message)
286
+ message = "ascp failed: #{status}"
287
+ # raise error only if there was not already an exception (ERROR_INFO)
288
+ raise Fasp::Error, message unless $ERROR_INFO
289
+ # else display this message also, as main exception is already here
290
+ Log.log.error(message)
305
291
  end
306
292
  end
307
293
  end # begin-ensure
308
294
  end # start_transfer_with_args_env
309
295
 
310
- # send command of management port to ascp session
296
+ # @return [Array] list of sessions for a job
297
+ def sessions_by_job(job_id)
298
+ @sessions.select{|s| s[:job_id].eql?(job_id)}
299
+ end
300
+
301
+ # @return [Hash] session information
302
+ def session_by_id(id)
303
+ matches = @sessions.select{|s| s[:id].eql?(id)}
304
+ raise 'no such session' if matches.empty?
305
+ raise 'more than one session' if matches.length > 1
306
+ return matches.first
307
+ end
308
+
309
+ # send command of management port to ascp session (used in `asession)
311
310
  # @param job_id identified transfer process
312
311
  # @param session_index index of session (for multi session)
313
312
  # @param data command on mgt port, examples:
314
313
  # {'type'=>'START','source'=>_path_,'destination'=>_path_}
315
314
  # {'type'=>'DONE'}
316
- def send_command(job_id, session_index, data)
317
- job = @jobs[job_id]
318
- raise 'no such job' if job.nil?
319
- session = job[:sessions][session_index]
320
- raise 'no such session' if session.nil?
315
+ def send_command(job_id, data)
316
+ session = session_by_id(job_id)
321
317
  Log.log.debug{"command: #{data}"}
322
318
  # build command
323
319
  command = data
324
320
  .keys
325
321
  .map{|k|"#{k.capitalize}: #{data[k]}"}
326
- .unshift('FASPMGR 2')
322
+ .unshift(MGT_HEADER)
327
323
  .push('', '')
328
324
  .join("\n")
329
325
  session[:io].puts(command)
330
326
  end
327
+ attr_reader :sessions
331
328
 
332
329
  private
333
330
 
334
331
  # @param options : keys(symbol): see DEFAULT_OPTIONS
335
- def initialize(options=nil)
336
- super()
332
+ def initialize(options={})
333
+ super(options)
334
+ # set default options and override if specified
335
+ @options = AgentBase.options(default: DEFAULT_OPTIONS, options: options)
336
+ Log.log.debug{Log.dump(:agent_options, @options)}
337
+ @resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
337
338
  # all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
338
- @jobs = {}
339
+ @sessions = []
339
340
  # mutex protects global data accessed by threads
340
341
  @mutex = Mutex.new
341
- # set default options and override if specified
342
- @options = DEFAULT_OPTIONS.dup
343
- if !options.nil?
344
- raise "expecting Hash (or nil), but have #{options.class}" unless options.is_a?(Hash)
345
- options.each do |k, v|
346
- raise "Unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.key?(k)
347
- @options[k] = v
348
- end
349
- end
350
- Log.log.debug{"local options= #{options}"}
351
- @resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
352
342
  end
353
343
 
354
344
  # transfer thread entry