aspera-cli 4.13.0 → 4.15.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 (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +81 -7
  4. data/CONTRIBUTING.md +22 -6
  5. data/README.md +2038 -1080
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/dascli +1 -1
  9. data/examples/proxy.pac +1 -1
  10. data/examples/rubyc +24 -0
  11. data/lib/aspera/aoc.rb +219 -159
  12. data/lib/aspera/ascmd.rb +25 -14
  13. data/lib/aspera/cli/basic_auth_plugin.rb +12 -9
  14. data/lib/aspera/cli/error.rb +17 -0
  15. data/lib/aspera/cli/extended_value.rb +47 -12
  16. data/lib/aspera/cli/formatter.rb +260 -179
  17. data/lib/aspera/cli/hints.rb +80 -0
  18. data/lib/aspera/cli/main.rb +104 -156
  19. data/lib/aspera/cli/manager.rb +259 -209
  20. data/lib/aspera/cli/plugin.rb +123 -63
  21. data/lib/aspera/cli/plugins/alee.rb +2 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +341 -261
  23. data/lib/aspera/cli/plugins/ats.rb +22 -21
  24. data/lib/aspera/cli/plugins/bss.rb +5 -5
  25. data/lib/aspera/cli/plugins/config.rb +578 -627
  26. data/lib/aspera/cli/plugins/console.rb +44 -6
  27. data/lib/aspera/cli/plugins/cos.rb +15 -17
  28. data/lib/aspera/cli/plugins/faspex.rb +114 -100
  29. data/lib/aspera/cli/plugins/faspex5.rb +411 -264
  30. data/lib/aspera/cli/plugins/node.rb +354 -259
  31. data/lib/aspera/cli/plugins/orchestrator.rb +61 -29
  32. data/lib/aspera/cli/plugins/preview.rb +82 -90
  33. data/lib/aspera/cli/plugins/server.rb +79 -32
  34. data/lib/aspera/cli/plugins/shares.rb +55 -42
  35. data/lib/aspera/cli/sync_actions.rb +68 -0
  36. data/lib/aspera/cli/transfer_agent.rb +66 -73
  37. data/lib/aspera/cli/transfer_progress.rb +74 -0
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/colors.rb +12 -8
  40. data/lib/aspera/command_line_builder.rb +14 -11
  41. data/lib/aspera/cos_node.rb +3 -2
  42. data/lib/aspera/data/6 +0 -0
  43. data/lib/aspera/environment.rb +24 -9
  44. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  45. data/lib/aspera/fasp/agent_base.rb +31 -77
  46. data/lib/aspera/fasp/agent_connect.rb +25 -21
  47. data/lib/aspera/fasp/agent_direct.rb +89 -103
  48. data/lib/aspera/fasp/agent_httpgw.rb +231 -149
  49. data/lib/aspera/fasp/agent_node.rb +41 -34
  50. data/lib/aspera/fasp/agent_trsdk.rb +75 -32
  51. data/lib/aspera/fasp/error_info.rb +4 -2
  52. data/lib/aspera/fasp/faux_file.rb +52 -0
  53. data/lib/aspera/fasp/installation.rb +53 -195
  54. data/lib/aspera/fasp/management.rb +244 -0
  55. data/lib/aspera/fasp/parameters.rb +71 -37
  56. data/lib/aspera/fasp/parameters.yaml +76 -8
  57. data/lib/aspera/fasp/products.rb +162 -0
  58. data/lib/aspera/fasp/resume_policy.rb +3 -3
  59. data/lib/aspera/fasp/transfer_spec.rb +7 -6
  60. data/lib/aspera/fasp/uri.rb +26 -24
  61. data/lib/aspera/faspex_gw.rb +2 -2
  62. data/lib/aspera/faspex_postproc.rb +2 -2
  63. data/lib/aspera/hash_ext.rb +14 -4
  64. data/lib/aspera/json_rpc.rb +49 -0
  65. data/lib/aspera/keychain/macos_security.rb +13 -13
  66. data/lib/aspera/line_logger.rb +23 -0
  67. data/lib/aspera/log.rb +58 -16
  68. data/lib/aspera/node.rb +157 -92
  69. data/lib/aspera/oauth.rb +37 -19
  70. data/lib/aspera/open_application.rb +4 -4
  71. data/lib/aspera/persistency_action_once.rb +1 -1
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -2
  74. data/lib/aspera/preview/generator.rb +22 -35
  75. data/lib/aspera/preview/options.rb +2 -0
  76. data/lib/aspera/preview/terminal.rb +73 -16
  77. data/lib/aspera/preview/utils.rb +21 -28
  78. data/lib/aspera/proxy_auto_config.js +2 -2
  79. data/lib/aspera/rest.rb +136 -68
  80. data/lib/aspera/rest_call_error.rb +1 -1
  81. data/lib/aspera/rest_error_analyzer.rb +15 -14
  82. data/lib/aspera/rest_errors_aspera.rb +37 -34
  83. data/lib/aspera/secret_hider.rb +18 -15
  84. data/lib/aspera/ssh.rb +5 -2
  85. data/lib/aspera/sync.rb +127 -119
  86. data/lib/aspera/temp_file_manager.rb +10 -3
  87. data/lib/aspera/web_auth.rb +10 -7
  88. data/lib/aspera/web_server_simple.rb +9 -4
  89. data.tar.gz.sig +0 -0
  90. metadata +34 -17
  91. metadata.gz.sig +0 -0
  92. data/docs/test_env.conf +0 -186
  93. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  94. data/lib/aspera/cli/listener/logger.rb +0 -22
  95. data/lib/aspera/cli/listener/progress.rb +0 -50
  96. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  97. data/lib/aspera/cli/plugins/sync.rb +0 -44
  98. data/lib/aspera/data/7 +0 -0
  99. data/lib/aspera/fasp/listener.rb +0 -13
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/fasp/agent_base'
4
+ require 'aspera/rest'
5
+ require 'aspera/json_rpc'
6
+ require 'aspera/open_application'
7
+ require 'securerandom'
8
+
9
+ module Aspera
10
+ module Fasp
11
+ class AgentAspera < Aspera::Fasp::AgentBase
12
+ # try twice the main init url in sequence
13
+ START_URIS = ['aspera://']
14
+ # delay between each try to start connect
15
+ SLEEP_SEC_BETWEEN_RETRY = 3
16
+ private_constant :START_URIS, :SLEEP_SEC_BETWEEN_RETRY
17
+ def initialize(options)
18
+ @application_id = SecureRandom.uuid
19
+ super(options)
20
+ raise 'Using client requires a graphical environment' if !OpenApplication.default_gui_mode.eql?(:graphical)
21
+ method_index = 0
22
+ begin
23
+ @client_app_api = Aspera::JsonRpcClient.new(Aspera::Rest.new(base_url: aspera_client_api_url))
24
+ client_info = @client_app_api.get_info
25
+ Log.log.debug{Log.dump(:client_version, client_info)}
26
+ # my_transfer_id = '0513fe85-65cf-465b-ad5f-18fd40d8c69f'
27
+ # @client_app_api.get_all_transfers({app_id: @application_id})
28
+ # @client_app_api.get_transfer(app_id: @application_id, transfer_id: my_transfer_id)
29
+ # @client_app_api.start_transfer(app_id: @application_id,transfer_spec: {})
30
+ # @client_app_api.remove_transfer
31
+ # @client_app_api.stop_transfer
32
+ # @client_app_api.modify_transfer
33
+ # @client_app_api.show_directory({app_id: @application_id, transfer_id: my_transfer_id})
34
+ # @client_app_api.get_files_list({app_id: @application_id, transfer_id: my_transfer_id})
35
+ Log.log.info('Client was reached') if method_index > 0
36
+ rescue StandardError => e # Errno::ECONNREFUSED
37
+ start_url = START_URIS[method_index]
38
+ method_index += 1
39
+ raise StandardError, "Unable to start connect #{method_index} times" if start_url.nil?
40
+ Log.log.warn{"Aspera Connect is not started (#{e}). Trying to start it ##{method_index}..."}
41
+ if !OpenApplication.uri_graphical(start_url)
42
+ OpenApplication.uri_graphical('https://downloads.asperasoft.com/connect2/')
43
+ raise StandardError, 'Connect is not installed'
44
+ end
45
+ sleep(SLEEP_SEC_BETWEEN_RETRY)
46
+ retry
47
+ end
48
+ end
49
+
50
+ def aspera_client_api_url
51
+ log_file = File.join(Dir.home, 'Library', 'Logs', 'IBM Aspera', 'ibm-aspera-desktop.log')
52
+ url = nil
53
+ File.open(log_file, 'r') do |file|
54
+ file.each_line do |line|
55
+ line = line.chomp
56
+ if (m = line.match(/JSON-RPC server listening on (.*)/))
57
+ url = "http://#{m[1]}"
58
+ end
59
+ end
60
+ end
61
+ return url
62
+ end
63
+
64
+ def start_transfer(transfer_spec, token_regenerator: nil)
65
+ @request_id = SecureRandom.uuid
66
+ # if there is a token, we ask connect client to use well known ssh private keys
67
+ # instead of asking password
68
+ transfer_spec['authentication'] = 'token' if transfer_spec.key?('token')
69
+ @client_app_api.start_transfer(app_id: @application_id,transfer_spec: transfer_spec)
70
+ # @xfer_id = res['transfer_specs'].first['transfer_spec']['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_id']
71
+ end
72
+
73
+ def wait_for_transfers_completion
74
+ client_activity_args = {'aspera_client_settings' => @client_settings}
75
+ started = false
76
+ pre_calc = false
77
+ session_id = @xfer_id
78
+ begin
79
+ loop do
80
+ tr_info = @client_api.create("transfers/info/#{@xfer_id}", client_activity_args)[:data]
81
+ Log.log.trace1{Log.dump(:tr_info, tr_info)}
82
+ if tr_info['transfer_info'].is_a?(Hash)
83
+ transfer = tr_info['transfer_info']
84
+ if transfer.nil?
85
+ Log.log.warn('no session in Connect')
86
+ break
87
+ end
88
+ # TODO: get session id
89
+ case transfer['status']
90
+ when 'initiating', 'queued'
91
+ notify_progress(session_id: nil, type: :pre_start, info: transfer['status'])
92
+ when 'running'
93
+ if !started
94
+ notify_progress(session_id: session_id, type: :session_start)
95
+ started = true
96
+ end
97
+ if !pre_calc && (transfer['bytes_expected'] != 0)
98
+ notify_progress(type: :session_size, session_id: session_id, info: transfer['bytes_expected'])
99
+ pre_calc = true
100
+ else
101
+ notify_progress(type: :transfer, session_id: session_id, info: transfer['bytes_written'])
102
+ end
103
+ when 'completed'
104
+ notify_progress(type: :end, session_id: session_id)
105
+ break
106
+ when 'failed'
107
+ notify_progress(type: :end, session_id: session_id)
108
+ raise Fasp::Error, transfer['error_desc']
109
+ when 'cancelled'
110
+ notify_progress(type: :end, session_id: session_id)
111
+ raise Fasp::Error, 'Transfer cancelled by user'
112
+ else
113
+ notify_progress(type: :end, session_id: session_id)
114
+ raise Fasp::Error, "unknown status: #{transfer['status']}: #{transfer['error_desc']}"
115
+ end
116
+ end
117
+ sleep(1)
118
+ end
119
+ rescue StandardError => e
120
+ return [e]
121
+ end
122
+ return [:success]
123
+ end # wait
124
+ end # AgentAspera
125
+ end
126
+ end
@@ -2,93 +2,47 @@
2
2
 
3
3
  module Aspera
4
4
  module Fasp
5
- # Base class for FASP transfer agents
6
- # sub classes shall implement start_transfer and shutdown
5
+ # Base class for transfer agents
7
6
  class AgentBase
8
- # fields description for JSON generation
9
- # spellchecker: disable
10
- INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
11
- DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
12
- ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec ArgScansAttempted
13
- ArgScansCompleted PathScansAttempted FileScansCompleted TransfersAttempted TransfersPassed Delay].freeze
14
- BOOLEAN_FIELDS = %w[Encryption Remote RateLock MinRateLock PolicyLock FilesEncrypt FilesDecrypt VLinkLocalEnabled VLinkRemoteEnabled
15
- MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect].freeze
16
- EXPECTED_METHODS = %i[text struct enhanced].freeze
17
- private_constant :INTEGER_FIELDS, :BOOLEAN_FIELDS, :EXPECTED_METHODS
18
- # spellchecker: enable
19
-
20
7
  class << self
21
- # This checks the validity of the value returned by wait_for_transfers_completion
22
- # it must be a list of :success or exception
23
- def validate_status_list(statuses)
24
- raise "internal error: bad statuses type: #{statuses.class}" unless statuses.is_a?(Array)
25
- raise "internal error: bad statuses content: #{statuses}" unless statuses.select{|i|!i.eql?(:success) && !i.is_a?(StandardError)}.empty?
26
- end
27
- end
28
-
29
- private
30
-
31
- # translates legacy event into enhanced (JSON) event
32
- def enhanced_event_format(event)
33
- return event.keys.each_with_object({}) do |e, h|
34
- # capital_to_snake_case
35
- new_name = e
36
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
37
- .gsub(/([a-z\d])(usec)$/, '\1_\2')
38
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
39
- .downcase
40
- value = event[e]
41
- value = value.to_i if INTEGER_FIELDS.include?(e)
42
- value = value.eql?('Yes') if BOOLEAN_FIELDS.include?(e)
43
- h[new_name] = value
44
- end
45
- end
46
-
47
- def initialize
48
- @listeners = []
49
- end
50
-
51
- def notify_listeners(current_event_text, current_event_data)
52
- Log.log.debug('send event to listeners')
53
- enhanced_event = nil
54
- @listeners.each do |listener|
55
- listener.event_text(current_event_text) if listener.respond_to?(:event_text)
56
- listener.event_struct(current_event_data) if listener.respond_to?(:event_struct)
57
- if listener.respond_to?(:event_enhanced)
58
- enhanced_event = enhanced_event_format(current_event_data) if enhanced_event.nil?
59
- listener.event_enhanced(enhanced_event)
8
+ # compute options from user provided and default options
9
+ def options(default:, options:)
10
+ result = options.symbolize_keys
11
+ available = default.map{|k, v|"#{k}(#{v})"}.join(', ')
12
+ result.each do |k, _v|
13
+ raise "Unknown transfer agent parameter: #{k}, expect one of #{available}" unless default.key?(k)
14
+ end
15
+ default.each do |k, v|
16
+ raise "Missing required agent parameter: #{k}. Parameters: #{available}" if v.eql?(:required) && !result.key?(k)
17
+ result[k] = v unless result.key?(k)
60
18
  end
19
+ return result
61
20
  end
62
- end # notify_listeners
63
-
64
- def notify_begin(id, size)
65
- notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size})
66
21
  end
67
-
68
- def notify_progress(id, size)
69
- notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'STATS', 'Bytescont' => size})
22
+ def wait_for_completion
23
+ # list of: :success or "error message string"
24
+ statuses = wait_for_transfers_completion
25
+ @progress&.reset
26
+ raise "internal error: bad statuses type: #{statuses.class}" unless statuses.is_a?(Array)
27
+ raise "internal error: bad statuses content: #{statuses}" unless statuses.select{|i|!i.eql?(:success) && !i.is_a?(StandardError)}.empty?
28
+ return statuses
70
29
  end
71
30
 
72
- def notify_end(id)
73
- notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'DONE'})
74
- end
75
-
76
- public
77
-
78
- LISTENER_SESSION_ID_B = 'ListenerSessionId'
79
- LISTENER_SESSION_ID_S = 'listener_session_id'
31
+ private
80
32
 
81
- # listener receives events
82
- def add_listener(listener)
83
- raise "expect one of #{EXPECTED_METHODS}" if EXPECTED_METHODS.inject(0){|m, e|m + (listener.respond_to?("event_#{e}") ? 1 : 0)}.eql?(0)
84
- @listeners.push(listener)
85
- self
33
+ def initialize(options)
34
+ raise 'internal error' unless respond_to?(:start_transfer)
35
+ raise 'internal error' unless respond_to?(:wait_for_transfers_completion)
36
+ # method `shutdown` is optional
37
+ Log.log.debug{Log.dump(:agent_options, options)}
38
+ raise "transfer agent options expecting Hash, but have #{options.class}" unless options.is_a?(Hash)
39
+ @progress = options[:progress]
40
+ options.delete(:progress)
86
41
  end
87
42
 
88
- # the following methods must be implemented by subclass:
89
- # start_transfer(transfer_spec, token_regenerator: nil) : start transfer
90
- # wait_for_transfers_completion : wait for termination of all transfers, @return list of : :success or error message
91
- # optional: shutdown
43
+ def notify_progress(**parameters)
44
+ @progress&.event(**parameters)
45
+ end
92
46
  end
93
47
  end
94
48
  end
@@ -4,28 +4,29 @@ require 'aspera/fasp/agent_base'
4
4
  require 'aspera/rest'
5
5
  require 'aspera/open_application'
6
6
  require 'securerandom'
7
- require 'tty-spinner'
8
7
 
9
8
  module Aspera
10
9
  module Fasp
11
10
  class AgentConnect < Aspera::Fasp::AgentBase
11
+ # try twice the main init url in sequence
12
12
  CONNECT_START_URIS = ['fasp://initialize', 'fasp://initialize', 'aspera-drive://initialize', 'https://test-connect.ibmaspera.com/']
13
+ # delay between each try to start connect
13
14
  SLEEP_SEC_BETWEEN_RETRY = 3
14
15
  private_constant :CONNECT_START_URIS, :SLEEP_SEC_BETWEEN_RETRY
15
- def initialize(_options)
16
- super()
16
+ def initialize(options)
17
+ super(options)
17
18
  @connect_settings = {
18
19
  'app_id' => SecureRandom.uuid
19
20
  }
20
21
  raise 'Using connect requires a graphical environment' if !OpenApplication.default_gui_mode.eql?(:graphical)
21
22
  method_index = 0
22
23
  begin
23
- connect_url = Installation.instance.connect_uri
24
+ connect_url = Products.connect_uri
24
25
  Log.log.debug{"found: #{connect_url}"}
25
26
  @connect_api = Rest.new({base_url: "#{connect_url}/v5/connect", headers: {'Origin' => Rest.user_agent}}) # could use v6 also now
26
27
  connect_info = @connect_api.read('info/version')[:data]
27
28
  Log.log.info('Connect was reached') if method_index > 0
28
- Log.dump(:connect_version, connect_info)
29
+ Log.log.debug{Log.dump(:connect_version, connect_info)}
29
30
  rescue StandardError => e # Errno::ECONNREFUSED
30
31
  start_url = CONNECT_START_URIS[method_index]
31
32
  method_index += 1
@@ -72,10 +73,12 @@ module Aspera
72
73
  def wait_for_transfers_completion
73
74
  connect_activity_args = {'aspera_connect_settings' => @connect_settings}
74
75
  started = false
75
- spinner = nil
76
+ pre_calc = false
77
+ session_id = @xfer_id
76
78
  begin
77
79
  loop do
78
80
  tr_info = @connect_api.create("transfers/info/#{@xfer_id}", connect_activity_args)[:data]
81
+ Log.log.trace1{Log.dump(:tr_info, tr_info)}
79
82
  if tr_info['transfer_info'].is_a?(Hash)
80
83
  transfer = tr_info['transfer_info']
81
84
  if transfer.nil?
@@ -84,29 +87,30 @@ module Aspera
84
87
  end
85
88
  # TODO: get session id
86
89
  case transfer['status']
87
- when 'completed'
88
- notify_end(@connect_settings['app_id'])
89
- break
90
90
  when 'initiating', 'queued'
91
- if spinner.nil?
92
- spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
93
- spinner.start
94
- end
95
- spinner.update(title: transfer['status'])
96
- spinner.spin
91
+ notify_progress(session_id: nil, type: :pre_start, info: transfer['status'])
97
92
  when 'running'
98
- # puts "running: sessions:#{transfer['sessions'].length}, #{transfer['sessions'].map{|i| i['bytes_transferred']}.join(',')}"
99
- if !started && (transfer['bytes_expected'] != 0)
100
- spinner&.success
101
- notify_begin(@connect_settings['app_id'], transfer['bytes_expected'])
93
+ if !started
94
+ notify_progress(session_id: session_id, type: :session_start)
102
95
  started = true
96
+ end
97
+ if !pre_calc && (transfer['bytes_expected'] != 0)
98
+ notify_progress(type: :session_size, session_id: session_id, info: transfer['bytes_expected'])
99
+ pre_calc = true
103
100
  else
104
- notify_progress(@connect_settings['app_id'], transfer['bytes_written'])
101
+ notify_progress(type: :transfer, session_id: session_id, info: transfer['bytes_written'])
105
102
  end
103
+ when 'completed'
104
+ notify_progress(type: :end, session_id: session_id)
105
+ break
106
106
  when 'failed'
107
- spinner&.error
107
+ notify_progress(type: :end, session_id: session_id)
108
108
  raise Fasp::Error, transfer['error_desc']
109
+ when 'cancelled'
110
+ notify_progress(type: :end, session_id: session_id)
111
+ raise Fasp::Error, 'Transfer cancelled by user'
109
112
  else
113
+ notify_progress(type: :end, session_id: session_id)
110
114
  raise Fasp::Error, "unknown status: #{transfer['status']}: #{transfer['error_desc']}"
111
115
  end
112
116
  end
@@ -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
11
  require 'socket'
12
12
  require 'timeout'
13
13
  require 'securerandom'
14
14
  require 'shellwords'
15
+ require 'English'
15
16
 
16
17
  module Aspera
17
18
  module Fasp
@@ -25,8 +26,11 @@ module Aspera
25
26
  multi_incr_udp: true,
26
27
  resume: {},
27
28
  ascp_args: [],
28
- quiet: true # by default no interactive 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
33
+ # spellchecker: enable
30
34
  private_constant :DEFAULT_OPTIONS
31
35
 
32
36
  # start ascp transfer (non blocking), single or multi-session
@@ -47,15 +51,7 @@ module Aspera
47
51
  # TODO: useful ? node only ?
48
52
  transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_retry'] ||= 3600
49
53
  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
54
+ Log.log.debug{Log.dump('ts', transfer_spec)}
59
55
 
60
56
  # Compute this before using transfer spec because it potentially modifies the transfer spec
61
57
  # (even if the var is not used in single session)
@@ -81,16 +77,8 @@ module Aspera
81
77
  end
82
78
  end
83
79
 
84
- # compute known args
85
- env_args = Parameters.ts_to_env_args(transfer_spec, wss: @options[:wss], ascp_args: @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_key))
90
- env_args[:args].unshift('-I', Installation.instance.path(:fallback_cert))
91
- end
92
-
93
- env_args[:args].unshift('-q') if @options[:quiet]
80
+ # compute known arguments and environment variables
81
+ env_args = Parameters.new(transfer_spec, @options).ascp_args
94
82
 
95
83
  # transfer job can be multi session
96
84
  xfer_job = {
@@ -162,6 +150,49 @@ module Aspera
162
150
  Log.log.debug('fasp local shutdown')
163
151
  end
164
152
 
153
+ # cspell:disable
154
+ # begin 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size
155
+ # progress 'Type' => 'STATS', 'Bytescont' => size
156
+ # end 'Type' => 'DONE'
157
+ # cspell:enable
158
+
159
+ # @param event management port event
160
+ def process_progress(event)
161
+ session_id = event['SessionId']
162
+ case event['Type']
163
+ when 'INIT'
164
+ @pre_calc_sent = false
165
+ @pre_calc_last_size = nil
166
+ notify_progress(session_id: session_id, type: :session_start)
167
+ when 'NOTIFICATION' # sent from remote
168
+ if event.key?('PreTransferBytes')
169
+ @pre_calc_sent = true
170
+ notify_progress(session_id: session_id, type: :session_size, info: event['PreTransferBytes'])
171
+ end
172
+ when 'STATS' # during transfer
173
+ @pre_calc_last_size = event['TransferBytes'].to_i + event['StartByte'].to_i
174
+ notify_progress(session_id: session_id, type: :transfer, info: @pre_calc_last_size)
175
+ when 'DONE', 'ERROR' # end of session
176
+ total_size = event['TransferBytes'].to_i + event['StartByte'].to_i
177
+ if !@pre_calc_sent && !total_size.zero?
178
+ notify_progress(session_id: session_id, type: :session_size, info: total_size)
179
+ end
180
+ if @pre_calc_last_size != total_size
181
+ notify_progress(session_id: session_id, type: :transfer, info: total_size)
182
+ end
183
+ notify_progress(session_id: session_id, type: :end)
184
+ # cspell:disable
185
+ when 'SESSION'
186
+ when 'ARGSTOP'
187
+ when 'FILEERROR'
188
+ when 'STOP'
189
+ # cspell:enable
190
+ # stop event when one file is completed
191
+ else
192
+ Log.log.debug{"unknown event type #{event['Type']}"}
193
+ end
194
+ end
195
+
165
196
  # This is the low level method to start the "ascp" process
166
197
  # currently, relies on command line arguments
167
198
  # start ascp with management port.
@@ -173,8 +204,6 @@ module Aspera
173
204
  def start_transfer_with_args_env(env_args, session)
174
205
  raise 'env_args must be Hash' unless env_args.is_a?(Hash)
175
206
  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
178
207
  begin
179
208
  Log.log.debug{"env_args=#{env_args.inspect}"}
180
209
  # get location of ascp executable
@@ -183,13 +212,14 @@ module Aspera
183
212
  end
184
213
  # (optional) check it exists
185
214
  raise Fasp::Error, "no such file: #{ascp_path}" unless File.exist?(ascp_path)
215
+ notify_progress(session_id: nil, type: :pre_start, info: 'starting ascp')
186
216
  # open an available (0) local TCP port as ascp management
187
217
  mgt_sock = TCPServer.new('127.0.0.1', 0)
188
218
  # clone arguments as we eed to modify with mgt port
189
219
  ascp_arguments = env_args[:args].clone
190
220
  # add management port on the selected local port
191
221
  ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
192
- # start ascp in sub process
222
+ # display ascp command line
193
223
  Log.log.debug do
194
224
  [
195
225
  'execute:',
@@ -198,12 +228,13 @@ module Aspera
198
228
  ascp_arguments.map{|a|Shellwords.shellescape(a)}
199
229
  ].flatten.join(' ')
200
230
  end
201
- # start process
231
+ # start ascp in separate process
202
232
  ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments)
203
233
  # in parent, wait for connection to socket max 3 seconds
204
234
  Log.log.debug{"before accept for pid (#{ascp_pid})"}
205
235
  # init management socket
206
236
  ascp_mgt_io = nil
237
+ notify_progress(session_id: nil, type: :pre_start, info: 'waiting for ascp')
207
238
  Timeout.timeout(@options[:spawn_timeout_sec]) do
208
239
  ascp_mgt_io = mgt_sock.accept
209
240
  # management messages include file names which may be utf8
@@ -213,73 +244,35 @@ module Aspera
213
244
  end
214
245
  Log.log.debug{"after accept (#{ascp_mgt_io})"}
215
246
  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)
247
+ processor = Management.new
248
+ # read management port, until socket is closed (gets returns nil)
249
+ while (line = ascp_mgt_io.gets)
250
+ event = processor.process_line(line.chomp)
251
+ next unless event
252
+ # event is ready
253
+ Log.log.debug{Log.dump(:management_port, event)}
254
+ # Log.log.trace1{"event: #{JSON.generate(Management.enhanced_event_format(event))}"}
255
+ process_progress(event)
256
+ Log.log.error((event['Description']).to_s) if event['Type'].eql?('FILEERROR') # cspell:disable-line
257
+ end
258
+ last_event = processor.last_event
256
259
  # 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
260
+ if last_event.is_a?(Hash)
261
+ case last_event['Type']
262
262
  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
263
+ if /bearer token/i.match?(last_event['Description']) &&
264
+ session[:token_regenerator].respond_to?(:refreshed_transfer_token)
265
+ # regenerate token here, expired, or error on it
266
+ # Note: in multi-session, each session will have a different one.
267
+ Log.log.warn('Regenerating bearer token')
268
+ env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
271
269
  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')
270
+ raise Fasp::Error.new(last_event['Description'], last_event['Code'].to_i)
271
+ when 'DONE'
272
+ nil
273
+ else
274
+ raise "unexpected last event type: #{last_event['Type']}"
275
+ end # case
283
276
  end
284
277
  rescue SystemCallError => e
285
278
  # Process.spawn
@@ -299,7 +292,7 @@ module Aspera
299
292
  if !status.success?
300
293
  message = "ascp failed with code #{status.exitstatus}"
301
294
  # raise error only if there was not already an exception
302
- raise Fasp::Error, message unless exception_raised
295
+ raise Fasp::Error, message unless $ERROR_INFO
303
296
  # else just debug, as main exception is already here
304
297
  Log.log.debug(message)
305
298
  end
@@ -323,7 +316,7 @@ module Aspera
323
316
  command = data
324
317
  .keys
325
318
  .map{|k|"#{k.capitalize}: #{data[k]}"}
326
- .unshift('FASPMGR 2')
319
+ .unshift(MGT_HEADER)
327
320
  .push('', '')
328
321
  .join("\n")
329
322
  session[:io].puts(command)
@@ -332,23 +325,16 @@ module Aspera
332
325
  private
333
326
 
334
327
  # @param options : keys(symbol): see DEFAULT_OPTIONS
335
- def initialize(options=nil)
336
- super()
337
- # all transfer jobs, key = SecureRandom.uuid, protected by mutex, condvar on change
328
+ def initialize(options={})
329
+ super(options)
330
+ # all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
338
331
  @jobs = {}
339
332
  # mutex protects global data accessed by threads
340
333
  @mutex = Mutex.new
341
334
  # 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}"}
335
+ @options = AgentBase.options(default: DEFAULT_OPTIONS, options: options)
351
336
  @resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
337
+ Log.log.debug{Log.dump(:agent_options, @options)}
352
338
  end
353
339
 
354
340
  # transfer thread entry